From 381c85e8f7c9a43aba7538580c290b04b5cea11a Mon Sep 17 00:00:00 2001 From: Benno Date: Mon, 8 Jul 2024 22:13:42 +0200 Subject: [PATCH 1/2] Fix recent events widget avatars --- spybot/recorder/cron/cron.py | 4 ++-- spybot/templates/spybot/home/recent_events/fragment.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spybot/recorder/cron/cron.py b/spybot/recorder/cron/cron.py index 51399d0..d5e762b 100644 --- a/spybot/recorder/cron/cron.py +++ b/spybot/recorder/cron/cron.py @@ -101,9 +101,9 @@ def _generate_news_event_for_top_user_of_week(user: TSUser, idx: int, points: in end = f", {num_overall} award overall." user_name_unescaped = escape.unescape(user.name) - second_line = f"This is {previous_times_specifier} {user_name_unescaped} won {metal_type_specifier} award{end}" + second_line = f"This is {previous_times_specifier} {user_name_unescaped} won {metal_type_specifier} award{end}" - message = f"{user_name_unescaped} earned the {metal} award for being the{specifier} most active user of week {week_of_year} " \ + message = f"{user_name_unescaped} earned the {metal} award for being the{specifier} most active user of week {week_of_year} " \ f"in {year}. Congratulations! {second_line}" n = NewsEvent(text=message, website_link=link) diff --git a/spybot/templates/spybot/home/recent_events/fragment.html b/spybot/templates/spybot/home/recent_events/fragment.html index 05d48aa..4155eee 100644 --- a/spybot/templates/spybot/home/recent_events/fragment.html +++ b/spybot/templates/spybot/home/recent_events/fragment.html @@ -6,7 +6,7 @@
- {{ event.text|make_list|first }} + {{ event.text|striptags|make_list|first }}
{{ event.text|safe }} From 1a371715d4ed1c10b4dc881dcc091a39298a0264 Mon Sep 17 00:00:00 2001 From: Benno Date: Sat, 17 Aug 2024 18:03:22 +0200 Subject: [PATCH 2/2] Add modal mechanism powered by HTMX. Add ability to add own SteamID in profile. --- Spybot2/settings.py | 12 +++- frontend/main.js | 1 + frontend/modal.js | 20 ++++++ poetry.lock | 52 +++++++++++++- pyproject.toml | 1 + spybot/forms.py | 46 ++++++++++++ spybot/remote/steam_api.py | 24 +++++++ spybot/templates/spybot/base/base.html | 5 ++ spybot/templates/spybot/form_snippet.html | 35 +++++++++ spybot/templates/spybot/profile.html | 61 ---------------- .../spybot/profile/add_steamid_modal.html | 56 +++++++++++++++ spybot/templates/spybot/profile/profile.html | 71 +++++++++++++++++++ .../spybot/profile/steamids_fragment.html | 29 ++++++++ spybot/urls.py | 3 + spybot/views/fragments/profile_steamids.py | 15 ++++ spybot/views/views.py | 62 ++++++++++++---- 16 files changed, 415 insertions(+), 78 deletions(-) create mode 100644 frontend/modal.js create mode 100644 spybot/remote/steam_api.py create mode 100644 spybot/templates/spybot/form_snippet.html delete mode 100644 spybot/templates/spybot/profile.html create mode 100644 spybot/templates/spybot/profile/add_steamid_modal.html create mode 100644 spybot/templates/spybot/profile/profile.html create mode 100644 spybot/templates/spybot/profile/steamids_fragment.html create mode 100644 spybot/views/fragments/profile_steamids.py diff --git a/Spybot2/settings.py b/Spybot2/settings.py index a596d83..fa7aa0b 100644 --- a/Spybot2/settings.py +++ b/Spybot2/settings.py @@ -66,6 +66,8 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.forms', + 'bootstrap4', ] MIDDLEWARE = [ @@ -217,4 +219,12 @@ # 'root': { # 'handlers': ['console'], # } -# } \ No newline at end of file +# } + +FORM_RENDERER = 'spybot.forms.CustomFormRenderer' + +BOOTSTRAP4 = { + 'include_jquery': False, + 'javascript_in_head': False, + 'label_class': 'form-label', +} \ No newline at end of file diff --git a/frontend/main.js b/frontend/main.js index 5dbc186..0f6e57b 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -5,6 +5,7 @@ import * as passkeys from './passkeys'; import "@tabler/core/dist/css/tabler.min.css" import "@tabler/core/dist/css/tabler-vendors.min.css" +import './modal' window.passkeys = passkeys; diff --git a/frontend/modal.js b/frontend/modal.js new file mode 100644 index 0000000..e140aa4 --- /dev/null +++ b/frontend/modal.js @@ -0,0 +1,20 @@ + +const modal = new bootstrap.Modal(document.querySelector("#modal"), {}); + +htmx.on("htmx:afterSwap", (e) => { + if (e.detail.target.id === "dialog") { + modal.show() + } +}) + +htmx.on("htmx:beforeSwap", (e) => { + // Empty response targeting #dialog => hide the modal + if (e.detail.target.id === "dialog" && !e.detail.xhr.response) { + modal.hide() + e.detail.shouldSwap = false + } +}) + +htmx.on("hidden.bs.modal", () => { + document.getElementById("dialog").innerHTML = "" +}) diff --git a/poetry.lock b/poetry.lock index 030baaa..45f9ea1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -56,6 +56,28 @@ files = [ tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +category = "main" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "certifi" version = "2024.6.2" @@ -309,6 +331,22 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-bootstrap4" +version = "24.3" +description = "Django extensions by Zostera" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django_bootstrap4-24.3-py3-none-any.whl", hash = "sha256:b555d87740a571036f100ad6026b1f62aabcb913404fb7f08f521881019b14bc"}, + {file = "django_bootstrap4-24.3.tar.gz", hash = "sha256:819bc0ba7b25fcdeb12eb04353962436dbe95b228ba4cf4b49f5d3fee53692e1"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.8.0" +Django = ">=4.1" + [[package]] name = "django-crontab" version = "0.7.1" @@ -657,6 +695,18 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + [[package]] name = "sqlparse" version = "0.5.0" @@ -763,4 +813,4 @@ ua-parser = ">=0.10.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "71bb9fa9811555f3f1bf09f873013a6b9b24dcafa079f0d697cb6854e18befc5" +content-hash = "8b2af78082ec57aba410df52e27c789a820550182af7f8547f2b883866ae89c6" diff --git a/pyproject.toml b/pyproject.toml index 25349fa..dfd0d7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ num2words = "^0.5.12" requests = "^2.31.0" fido2 = "^1.1.3" user-agents = "^2.2.0" +django-bootstrap4 = "^24.3" [tool.poetry.group.test.dependencies] diff --git a/spybot/forms.py b/spybot/forms.py index f7631ac..77588d6 100644 --- a/spybot/forms.py +++ b/spybot/forms.py @@ -1,5 +1,12 @@ from django import forms +from django.core.exceptions import ValidationError +from django.forms.renderers import TemplatesSetting +from spybot.remote.steam_api import get_steam_user_playing_info, get_steam_account_info + + +class CustomFormRenderer(TemplatesSetting): + form_template_name = 'spybot/form_snippet.html' class TimeRangeForm(forms.Form): RANGES = ( @@ -25,3 +32,42 @@ def clean(self): cleaned_data[name] = field.initial return cleaned_data + + +class AddSteamIDForm(forms.Form): + steamid = forms.CharField(label="Account ID (what comes after https://steamcommunity.com/profiles/)", initial="123456", required=True) + name = forms.CharField(label="Account name") + + def clean_steamid(self): + cleaned_data = super().clean() + steamid = cleaned_data.get("steamid") + name = cleaned_data.get("name") + + steam_info = get_steam_account_info(steamid) + if steam_info is None: + raise ValidationError("Can't load steam information for this user") + + return cleaned_data + + + """ + +
+ + https://steamcommunity.com/profiles/ + + +
+
+
+ + + + + {% block content_body_end %} + {% endblock %} diff --git a/spybot/templates/spybot/form_snippet.html b/spybot/templates/spybot/form_snippet.html new file mode 100644 index 0000000..e9cd6c0 --- /dev/null +++ b/spybot/templates/spybot/form_snippet.html @@ -0,0 +1,35 @@ +{% for field in form %} +
+
+
+ {{ field.label_tag }} + {{ field }} +
+
+ {{ field.errors }} +
+{% endfor %} + +{##} +{##} +{#
#} +{#
#} +{#
#} +{# #} +{#
#} +{# #} +{# https://steamcommunity.com/profiles/#} +{# #} +{# #} +{#
#} +{#
#} +{#
#} +{#
#} +{#
#} +{#
#} +{#
#} +{# #} +{# #} +{#
#} +{#
#} +{#
#} \ No newline at end of file diff --git a/spybot/templates/spybot/profile.html b/spybot/templates/spybot/profile.html deleted file mode 100644 index 3587a0f..0000000 --- a/spybot/templates/spybot/profile.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends 'spybot/base/base.html' %} -{% load util %} -{% load tabler_icons %} -{% load ts_filters %} - -{% block content %} -
-
-
-

Logged in as {{ logged_in_user.name }}

-
-
-
-
Passkeys
-
- - - - - - - - - - - - {% for key in passkeys %} - - - - - - - - - {% endfor %} -
NamePlatformLast usedCreated
{{ key.name }}{{ key.platform }}{{ key.last_used }}{{ key.added_on }}Delete
-
-
- -
- -
-
- - - -{% endblock content %} - -{% block header %} - -{% endblock header %} \ No newline at end of file diff --git a/spybot/templates/spybot/profile/add_steamid_modal.html b/spybot/templates/spybot/profile/add_steamid_modal.html new file mode 100644 index 0000000..0eeec3f --- /dev/null +++ b/spybot/templates/spybot/profile/add_steamid_modal.html @@ -0,0 +1,56 @@ +{% load tabler_icons %} +{% load bootstrap4 %} + +
\ No newline at end of file diff --git a/spybot/templates/spybot/profile/profile.html b/spybot/templates/spybot/profile/profile.html new file mode 100644 index 0000000..c2d8f32 --- /dev/null +++ b/spybot/templates/spybot/profile/profile.html @@ -0,0 +1,71 @@ +{% extends 'spybot/base/base.html' %} +{% load util %} +{% load tabler_icons %} +{% load ts_filters %} + +{% block content %} +
+
+
+
+

Logged in as {{ logged_in_user.name }}

+
+
+
+
+
+
Passkeys
+
+ + + + + + + + + + + + {% for key in passkeys %} + + + + + + + + + {% endfor %} +
NamePlatformLast usedCreated
{{ key.name }}{{ key.platform }}{{ key.last_used }}{{ key.added_on }}Delete
+
+
+ +
+
+
+
+ {% include 'spybot/profile/steamids_fragment.html' %} +
+
+ +{% endblock content %} + +{% block content_body_end %} + +{% endblock content_body_end %} + +{% block header %} + +{% endblock header %} \ No newline at end of file diff --git a/spybot/templates/spybot/profile/steamids_fragment.html b/spybot/templates/spybot/profile/steamids_fragment.html new file mode 100644 index 0000000..b8e597d --- /dev/null +++ b/spybot/templates/spybot/profile/steamids_fragment.html @@ -0,0 +1,29 @@ +{% with data=profile_steamids %} +
+
Linked Steam Accounts
+
+ + + + + + + + + + {% for sid in data.steamids %} + + + + + + + {% endfor %} +
Account NameSteam ID
{{ sid.account_name }}{{ sid.steam_id }}Delete
+
+
+ +
+
+{% endwith %} \ No newline at end of file diff --git a/spybot/urls.py b/spybot/urls.py index 9737d58..b4011cb 100644 --- a/spybot/urls.py +++ b/spybot/urls.py @@ -17,6 +17,9 @@ path('recent_events_fragment', views.recent_events_fragment, name='recent_events_fragment'), path('profile', views.profile, name='profile'), path('profile/passkey/', views.profile_passkey, name='profile_passkey'), + path('profile/add_steamid', views.profile_add_steamid, name='profile_add_steamid'), + path('profile/steamid/', views.profile_delete_steamid, name='profile_delete_steamid'), + path('profile/steamids', views.profile_steamids_fragment, name='profile_steamids_fragment'), path('login', views.login, name='login'), path('login_teamspeak', views.login_teamspeak, name='login_teamspeak'), path('link_auth', auth.link_login, name='link_login'), diff --git a/spybot/views/fragments/profile_steamids.py b/spybot/views/fragments/profile_steamids.py new file mode 100644 index 0000000..9341a46 --- /dev/null +++ b/spybot/views/fragments/profile_steamids.py @@ -0,0 +1,15 @@ +from django.contrib.auth.decorators import login_required +from django.shortcuts import render + +from spybot.models import SteamID + + +@login_required +def profile_steamids_data(request): + steamids = SteamID.objects.filter(merged_user=request.user).all() + return { "profile_steamids" : { "steamids": steamids }} + + +@login_required +def fragment(request): + return render(request, 'spybot/', profile_steamids_data(request)) diff --git a/spybot/views/views.py b/spybot/views/views.py index cf626d9..caf056e 100644 --- a/spybot/views/views.py +++ b/spybot/views/views.py @@ -11,15 +11,17 @@ from spybot import visualization -from spybot.forms import TimeRangeForm +from spybot.forms import TimeRangeForm, AddSteamIDForm +from spybot.remote.steam_api import get_steam_user_playing_info from spybot.views.fragments.activity_chart import activity_chart_data -from spybot.models import TSChannel, TSUserActivity, NewsEvent, MergedUser, UserPasskey +from spybot.models import TSChannel, TSUserActivity, NewsEvent, MergedUser, UserPasskey, SteamID from spybot.templatetags import ts_filters from Spybot2 import settings import requests from spybot.views.common import get_user, get_context +from spybot.views.fragments.profile_steamids import profile_steamids_data def get_passkeys(user: MergedUser) -> List[UserPasskey]: @@ -268,18 +270,9 @@ def get_steam_game(mu: MergedUser): for sid in steam_ids: steam_id = sid.steam_id - #print(f"Trying steamID {steam_id} for user {mu.name}") - steam_api_key = settings.STEAM_API_KEY - req = "http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={key}&steamids={id}" \ - .format(key=steam_api_key, id=steam_id) - - steam_data = requests.get(req) - steam_info = steam_data.json().get('response').get('players')[0] - game_id = steam_info.get('gameid', 0) - game_name = steam_info.get('gameextrainfo', "") - if game_id != 0: + game_id, game_name = get_steam_user_playing_info(steam_id) + if game_id is not None: return game_id, game_name - return 0, "" @@ -295,8 +288,8 @@ def recent_events_fragment(request): def profile(request): user = get_user(request) passkeys = get_passkeys(user) - return render(request, 'spybot/profile.html', {**get_context(request), 'user': user, 'passkeys': passkeys}) - + steamids = profile_steamids_data(request) + return render(request, 'spybot/profile/profile.html', {**steamids, **get_context(request), 'user': user, 'passkeys': passkeys}) def login(request): user = get_user(request) @@ -319,3 +312,42 @@ def profile_passkey(request, id: str): passkey.delete() return HttpResponse('') return None + + +def create_steamid(user: MergedUser, steam_id: str, account_name:str): + sid = SteamID(steam_id=int(steam_id), account_name=account_name, merged_user=user) + sid.save() + + +@login_required +def profile_add_steamid(request): + user = get_user(request) + if request.method == "POST": + form = AddSteamIDForm(request.POST) + if form.is_valid(): + create_steamid(user, form.cleaned_data['steamid'], form.cleaned_data['name']) + return HttpResponse(status=204, headers={'HX-Trigger': 'steamids_changed'}) + else: + form = AddSteamIDForm() + return render(request, + 'spybot/profile/add_steamid_modal.html', + {**get_context(request), 'user': user, 'form': form} + ) + + +def profile_delete_steamid(request, id): + if request.method == "DELETE": + print(f"trying to delete steamid with id {id}") + steamid = get_object_or_404(SteamID, id=id) + if steamid.merged_user != request.user: + return HttpResponseForbidden() + + steamid.delete() + return HttpResponse(status=200, headers={'HX-Trigger': 'steamids_changed'}) + return None + + +def profile_steamids_fragment(request): + user = get_user(request) + steamids = profile_steamids_data(request) + return render(request, 'spybot/profile/steamids_fragment.html', {**steamids, **get_context(request), 'user': user})