From d5bbbd4ee45b607a3cd31d2ab2f273c29c05ae50 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Wed, 24 Aug 2022 22:20:08 +0200 Subject: [PATCH 01/25] Add login QR-code to settings-page --- Pipfile | 1 + babybuddy/models.py | 20 +++++++++++++++++++ .../babybuddy/user_settings_form.html | 8 +++++++- babybuddy/views.py | 14 ++++++++++++- requirements.txt | 1 + 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index 0afc37d2..3bfe5209 100644 --- a/Pipfile +++ b/Pipfile @@ -23,6 +23,7 @@ pyyaml = "*" uritemplate = "*" whitenoise = "*" django-taggit = "*" +qrcode = "*" [dev-packages] coveralls = "*" diff --git a/babybuddy/models.py b/babybuddy/models.py index be565050..7b4c42d1 100644 --- a/babybuddy/models.py +++ b/babybuddy/models.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- import pytz +import json +import io from django.conf import settings from django.contrib.auth.models import User @@ -12,6 +14,8 @@ from django.utils.translation import gettext_lazy as _ from rest_framework.authtoken.models import Token +import qrcode + class Settings(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) @@ -82,6 +86,22 @@ class Settings(models.Model): Token.objects.get(user=self.user).delete() return Token.objects.get_or_create(user=self.user)[0] + def generate_login_qr_code_png(self, page_root: str) -> bytes: + json_data = { + "url": page_root, + "api_key": str(self.api_key()), + } + qr_code_data = r"BABYBUDDY-LOGIN:" + json.dumps(json_data) + + qr = qrcode.QRCode(border=1, box_size=5) + qr.add_data(qr_code_data) + qr.make(fit=True) + image = qr.make_image() + + bytesio = io.BytesIO() + image.save(bytesio, format="png") + return bytesio.getbuffer() + @property def dashboard_refresh_rate_milliseconds(self): """ diff --git a/babybuddy/templates/babybuddy/user_settings_form.html b/babybuddy/templates/babybuddy/user_settings_form.html index b7b99bf9..17c6b175 100644 --- a/babybuddy/templates/babybuddy/user_settings_form.html +++ b/babybuddy/templates/babybuddy/user_settings_form.html @@ -78,12 +78,18 @@
{% trans "API" %}
- +
{{ user.settings.api_key }}
+
+ +
+ +
+
diff --git a/babybuddy/views.py b/babybuddy/views.py index a53cde16..0d34a957 100644 --- a/babybuddy/views.py +++ b/babybuddy/views.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +import base64 + +from urllib.parse import urljoin + from django.contrib import messages from django.contrib.auth import update_session_auth_hash from django.contrib.auth.forms import PasswordChangeForm @@ -158,14 +162,22 @@ class UserSettings(LoginRequiredMixin, View): template_name = "babybuddy/user_settings_form.html" def get(self, request): + settings = request.user.settings + + page_root = request.build_absolute_uri(reverse("babybuddy:root-router")) + base64_png = base64.b64encode( + settings.generate_login_qr_code_png(page_root) + ) + return render( request, self.template_name, { "form_user": self.form_user_class(instance=request.user), "form_settings": self.form_settings_class( - instance=request.user.settings + instance=settings ), + "login_qr_code_png": base64_png.decode(), }, ) diff --git a/requirements.txt b/requirements.txt index d0a010fe..4ebffb44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,6 +31,7 @@ python-dateutil==2.8.2 python-dotenv==0.20.0 pytz==2022.1 pyyaml==6.0 +qrcode==7.3.1 s3transfer==0.6.0 setuptools==63.1.0 six==1.16.0 From a6433732b657343310c70a846e3f9a9ebd598bea Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Thu, 25 Aug 2022 10:15:09 +0200 Subject: [PATCH 02/25] Experimental changes --- api/serializers.py | 7 +++++++ api/urls.py | 21 ++++++++++++++++----- api/views.py | 19 ++++++++++++++++++- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index e78e80c1..70a873ab 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -9,6 +9,7 @@ from django.utils import timezone from taggit.serializers import TagListSerializerField, TaggitSerializer from core import models +from babybuddy import models as babybuddy_models class CoreModelSerializer(serializers.HyperlinkedModelSerializer): @@ -268,3 +269,9 @@ class WeightSerializer(CoreModelSerializer, TaggableSerializer): class Meta: model = models.Weight fields = ("id", "child", "weight", "date", "notes", "tags") + + +class ProfileSerializer(serializers.ModelSerializer): + class Meta: + model = babybuddy_models.Settings + fields = ("user", "language") diff --git a/api/urls.py b/api/urls.py index 10382c71..b71110c1 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from typing import NamedTuple, Optional, Any + from django.urls import include, path from rest_framework import routers from rest_framework.schemas import get_schema_view @@ -21,13 +23,14 @@ router.register(r"timers", views.TimerViewSet) router.register(r"tummy-times", views.TummyTimeViewSet) router.register(r"weight", views.WeightViewSet) -app_name = "api" +class ExtraUrl(NamedTuple): + view: Any + name: Optional[str] = None -urlpatterns = [ - path("api/", include(router.urls)), - path("api/auth/", include("rest_framework.urls", namespace="rest_framework")), +extra_api_urls = [ + path("api/profile", views.ProfileView.as_view()), path( - "api/schema", + "api/schema", get_schema_view( title="Baby Buddy API", version=1, @@ -36,3 +39,11 @@ urlpatterns = [ name="openapi-schema", ), ] + +app_name = "api" + + +urlpatterns = [ + path("api/", include(router.urls + list(extra_api_urls))), + path("api/auth/", include("rest_framework.urls", namespace="rest_framework")), +] + extra_api_urls diff --git a/api/views.py b/api/views.py index 3a7964cf..44892353 100644 --- a/api/views.py +++ b/api/views.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- -from rest_framework import viewsets +from django.shortcuts import get_object_or_404 + +from rest_framework import viewsets, views from rest_framework.decorators import action from rest_framework.response import Response from core import models +from babybuddy import models as babybuddy_models from . import serializers, filters @@ -113,3 +116,17 @@ class WeightViewSet(viewsets.ModelViewSet): queryset = models.Weight.objects.all() serializer_class = serializers.WeightSerializer filterset_fields = ("child", "date") + + +class ProfileView(views.APIView): + basename = "profile" + queryset = babybuddy_models.Settings.objects.all() + serializer_class = serializers.ProfileSerializer + + def get(self, request): + settings = get_object_or_404( + babybuddy_models.Settings.objects, + user=request.user + ) + serializer = self.serializer_class(settings) + return Response(serializer.data) From 29cdc368fae9e3bd705870f058dddb44ffaf336c Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Thu, 25 Aug 2022 11:17:30 +0200 Subject: [PATCH 03/25] Add custom router that renders out the urls of custom views --- api/urls.py | 72 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/api/urls.py b/api/urls.py index b71110c1..12d38df0 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -from typing import NamedTuple, Optional, Any +from collections import OrderedDict + +from typing import NamedTuple, List, Any from django.urls import include, path from rest_framework import routers @@ -7,7 +9,43 @@ from rest_framework.schemas import get_schema_view from . import views -router = routers.DefaultRouter() + +class ExtraPath(NamedTuple): + path: str + reverese_name: str + route: Any + + +class CustomRouterWithExtraPaths(routers.DefaultRouter): + extra_api_urls: List[ExtraPath] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.extra_api_urls = [] + + def add_detail_path(self, url_path, reverese_name, *args, **kwargs): + self.extra_api_urls = self.extra_api_urls or [] + kwargs["name"] = reverese_name + self.extra_api_urls.append( + ExtraPath(url_path, reverese_name, path(url_path, *args, **kwargs)) + ) + + def get_api_root_view(self, api_urls=None): + api_root_dict = OrderedDict() + list_name = self.routes[0].name + for prefix, viewset, basename in self.registry: + api_root_dict[prefix] = list_name.format(basename=basename) + for extra_path in self.extra_api_urls: + api_root_dict[extra_path.path] = extra_path.reverese_name + + return self.APIRootView.as_view(api_root_dict=api_root_dict) + + @property + def urls(self): + return super().urls + [e.route for e in self.extra_api_urls] + + +router = CustomRouterWithExtraPaths() router.register(r"bmi", views.BMIViewSet) router.register(r"changes", views.DiaperChangeViewSet) router.register(r"children", views.ChildViewSet) @@ -23,27 +61,21 @@ router.register(r"timers", views.TimerViewSet) router.register(r"tummy-times", views.TummyTimeViewSet) router.register(r"weight", views.WeightViewSet) -class ExtraUrl(NamedTuple): - view: Any - name: Optional[str] = None - -extra_api_urls = [ - path("api/profile", views.ProfileView.as_view()), - path( - "api/schema", - get_schema_view( - title="Baby Buddy API", - version=1, - description="API documentation for the Baby Buddy application", - ), - name="openapi-schema", +router.add_detail_path("profile", "profile", views.ProfileView.as_view()) +router.add_detail_path( + "schema", + "openapi-schema", + get_schema_view( + title="Baby Buddy API", + version=1, + description="API documentation for the Baby Buddy application", ), -] +) + app_name = "api" - urlpatterns = [ - path("api/", include(router.urls + list(extra_api_urls))), + path("api/", include(router.urls)), path("api/auth/", include("rest_framework.urls", namespace="rest_framework")), -] + extra_api_urls +] From abcd832591adf22bb6fea5d8a6c092852907e4c2 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Thu, 25 Aug 2022 11:46:12 +0200 Subject: [PATCH 04/25] Serialize full profile page --- api/serializers.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/api/serializers.py b/api/serializers.py index 70a873ab..a6981af1 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -272,6 +272,37 @@ class WeightSerializer(CoreModelSerializer, TaggableSerializer): class ProfileSerializer(serializers.ModelSerializer): + api_key = serializers.SerializerMethodField("get_api_key") + first_name = serializers.SerializerMethodField("get_first_name") + last_name = serializers.SerializerMethodField("get_last_name") + email = serializers.SerializerMethodField("get_email") + staff = serializers.SerializerMethodField("is_staff") + + def get_api_key(self, value): + return self.instance.api_key().key + + def get_first_name(self, value): + return self.instance.user.first_name + + def get_last_name(self, value): + return self.instance.user.last_name + + def get_email(self, value): + return self.instance.user.email + + def is_staff(self, value): + return self.instance.user.is_staff + class Meta: model = babybuddy_models.Settings - fields = ("user", "language") + fields = ( + "user", + "first_name", + "last_name", + "email", + "staff", + "language", + "timezone", + "api_key", + ) + extra_kwargs = {k: {"read_only": True} for k in fields} From e238563b53f76315201a74f2fb9f6bbc927f6005 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Thu, 25 Aug 2022 23:56:18 +0200 Subject: [PATCH 05/25] Replace qrcode generator with template tag --- babybuddy/models.py | 16 --- .../babybuddy/user_settings_form.html | 9 +- babybuddy/templatetags/babybuddy_tags.py | 104 ++++++++++++++++++ babybuddy/views.py | 10 +- 4 files changed, 112 insertions(+), 27 deletions(-) diff --git a/babybuddy/models.py b/babybuddy/models.py index 7b4c42d1..2631ba03 100644 --- a/babybuddy/models.py +++ b/babybuddy/models.py @@ -86,22 +86,6 @@ class Settings(models.Model): Token.objects.get(user=self.user).delete() return Token.objects.get_or_create(user=self.user)[0] - def generate_login_qr_code_png(self, page_root: str) -> bytes: - json_data = { - "url": page_root, - "api_key": str(self.api_key()), - } - qr_code_data = r"BABYBUDDY-LOGIN:" + json.dumps(json_data) - - qr = qrcode.QRCode(border=1, box_size=5) - qr.add_data(qr_code_data) - qr.make(fit=True) - image = qr.make_image() - - bytesio = io.BytesIO() - image.save(bytesio, format="png") - return bytesio.getbuffer() - @property def dashboard_refresh_rate_milliseconds(self): """ diff --git a/babybuddy/templates/babybuddy/user_settings_form.html b/babybuddy/templates/babybuddy/user_settings_form.html index 17c6b175..7ac8901b 100644 --- a/babybuddy/templates/babybuddy/user_settings_form.html +++ b/babybuddy/templates/babybuddy/user_settings_form.html @@ -1,5 +1,5 @@ {% extends 'babybuddy/page.html' %} -{% load i18n widget_tweaks %} +{% load i18n widget_tweaks babybuddy_tags %} {% block title %}{% trans "User Settings" %}{% endblock %} @@ -87,7 +87,12 @@
- + {% url 'babybuddy:root-router' as relative_root_url %} + {% make_absolute_url relative_root_url as absolute_root_url %} + +
diff --git a/babybuddy/templatetags/babybuddy_tags.py b/babybuddy/templatetags/babybuddy_tags.py index dc5f6cab..e9f62b0d 100644 --- a/babybuddy/templatetags/babybuddy_tags.py +++ b/babybuddy/templatetags/babybuddy_tags.py @@ -1,8 +1,13 @@ # -*- coding: utf-8 -*- +import io +import base64 +from multiprocessing.sharedctypes import Value + from django import template from django.apps import apps from django.utils import timezone from django.utils.translation import to_locale, get_language +from django.template.defaultfilters import stringfilter from core.models import Child @@ -55,3 +60,102 @@ def get_child_count(): @register.simple_tag() def get_current_timezone(): return timezone.get_current_timezone_name() + + +@register.simple_tag() +def inline_png_qrcode(qr_code_data, border=1, box_size=5): + import qrcode + + qr = qrcode.QRCode(border=border, box_size=box_size) + qr.add_data(qr_code_data) + qr.make(fit=True) + image = qr.make_image() + + bytesio = io.BytesIO() + image.save(bytesio, format="png") + base64_data = base64.b64encode(bytesio.getbuffer()).decode() + return f"data:image/png;base64,{base64_data}" + + +@register.simple_tag(takes_context=True) +def make_absolute_url(context, url): + request = context["request"] + abs_url = request.build_absolute_uri(url) + return abs_url + + +class QrCodeNode(template.Node): + def __init__(self, nodelist, strip, border, box_size) -> None: + super().__init__() + self.__nodelist = nodelist + self.__strip = strip + self.__border = border + self.__box_size = box_size + + def render(self, context): + contents = "" + for node in self.__nodelist: + contents += node.render(context) + if self.__strip: + contents = contents.strip() + + import qrcode + + qr = qrcode.QRCode(border=self.__border, box_size=self.__box_size) + qr.add_data(contents) + qr.make(fit=True) + image = qr.make_image() + + bytesio = io.BytesIO() + image.save(bytesio, format="png") + base64_data = base64.b64encode(bytesio.getbuffer()).decode() + return f"data:image/png;base64,{base64_data}" + + +@register.tag_function +def qrcodepng(parser, token): + contents = token.split_contents() + params = contents[1:] + + def get_parameter(name: str, value_type=None): + search_for = name + if value_type is not None: + search_for += "=" + + for p in params: + if p.startswith(search_for): + if value_type is None: + if p != search_for: + continue + params.remove(p) + return True + else: + str_value = p[len(search_for) :] + print("AHAGAGAG", str_value) + try: + result = value_type(str_value) + except ValueError: + raise template.TemplateSyntaxError( + f"Invalid parameter '{p}' does " + f"not have type '{value_type}'" + ) + else: + params.remove(p) + return result + + if value_type is None: + return False + return None + + strip = get_parameter("stripwhitespace") + border = get_parameter("border", int) or 1 + box_size = get_parameter("box_size", int) or 5 + + if params: + raise template.TemplateSyntaxError( + f"Unkown arguments for qrcode template tag: {', '.join(params)}" + ) + + nodelist = parser.parse(("endqrcodepng",)) + parser.delete_first_token() + return QrCodeNode(nodelist, strip, border, box_size) diff --git a/babybuddy/views.py b/babybuddy/views.py index 0d34a957..c2ad4cda 100644 --- a/babybuddy/views.py +++ b/babybuddy/views.py @@ -164,20 +164,12 @@ class UserSettings(LoginRequiredMixin, View): def get(self, request): settings = request.user.settings - page_root = request.build_absolute_uri(reverse("babybuddy:root-router")) - base64_png = base64.b64encode( - settings.generate_login_qr_code_png(page_root) - ) - return render( request, self.template_name, { "form_user": self.form_user_class(instance=request.user), - "form_settings": self.form_settings_class( - instance=settings - ), - "login_qr_code_png": base64_png.decode(), + "form_settings": self.form_settings_class(instance=settings), }, ) From 500fb0645329a844e58d205bd977ec3f9f8b441c Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Fri, 26 Aug 2022 00:03:48 +0200 Subject: [PATCH 06/25] Remove debug print and add docstring --- babybuddy/templatetags/babybuddy_tags.py | 40 ++++++++++++++---------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/babybuddy/templatetags/babybuddy_tags.py b/babybuddy/templatetags/babybuddy_tags.py index e9f62b0d..5a840e75 100644 --- a/babybuddy/templatetags/babybuddy_tags.py +++ b/babybuddy/templatetags/babybuddy_tags.py @@ -62,21 +62,6 @@ def get_current_timezone(): return timezone.get_current_timezone_name() -@register.simple_tag() -def inline_png_qrcode(qr_code_data, border=1, box_size=5): - import qrcode - - qr = qrcode.QRCode(border=border, box_size=box_size) - qr.add_data(qr_code_data) - qr.make(fit=True) - image = qr.make_image() - - bytesio = io.BytesIO() - image.save(bytesio, format="png") - base64_data = base64.b64encode(bytesio.getbuffer()).decode() - return f"data:image/png;base64,{base64_data}" - - @register.simple_tag(takes_context=True) def make_absolute_url(context, url): request = context["request"] @@ -114,6 +99,30 @@ class QrCodeNode(template.Node): @register.tag_function def qrcodepng(parser, token): + """ + This template tag allows the generation of arbirary qr code pngs that + can be displayed, for example, in html tags. + + The template tag can be used as follows: + + + + This will produce a qrcode that encodes the + string "\n Hello World\n ". One can use the qrcode parameter + ``stripwhitespace`` to strip the extra whitespace at the start and end of + the string: + + {% qrcodepng stripwhitespace %} + + All supported arguments: + + - stripwhitespace: strip whitespace of the qrcode-contents + - border=[int]: Border of the qrcode in pixels (default: 1) + - box_size=[int]: Pixel size of the qr-code blocks (default: 5) + """ + contents = token.split_contents() params = contents[1:] @@ -131,7 +140,6 @@ def qrcodepng(parser, token): return True else: str_value = p[len(search_for) :] - print("AHAGAGAG", str_value) try: result = value_type(str_value) except ValueError: From 9723bccc40a6bc09260c4d239380fee4ef0f0e84 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Fri, 26 Aug 2022 15:42:30 +0200 Subject: [PATCH 07/25] Black --- api/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/api/views.py b/api/views.py index 44892353..04585fd1 100644 --- a/api/views.py +++ b/api/views.py @@ -125,8 +125,7 @@ class ProfileView(views.APIView): def get(self, request): settings = get_object_or_404( - babybuddy_models.Settings.objects, - user=request.user + babybuddy_models.Settings.objects, user=request.user ) serializer = self.serializer_class(settings) - return Response(serializer.data) + return Response(serializer.data) From be4f987d354e8e89393a1681c117ecb5f9240898 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Tue, 30 Aug 2022 21:26:45 +0200 Subject: [PATCH 08/25] Remove qrcode library and replace with django-qre-code Required some rework to make qr-codes work via a template machanism. I think that the new solution is working neatly as well. --- Pipfile | 2 +- babybuddy/settings/base.py | 1 + .../templates/babybuddy/login_qr_code.txt | 4 + .../babybuddy/user_settings_form.html | 11 +- babybuddy/templatetags/babybuddy_tags.py | 100 ------------------ babybuddy/views.py | 5 + requirements.txt | 2 +- 7 files changed, 16 insertions(+), 109 deletions(-) create mode 100644 babybuddy/templates/babybuddy/login_qr_code.txt diff --git a/Pipfile b/Pipfile index 3bfe5209..b724e67a 100644 --- a/Pipfile +++ b/Pipfile @@ -23,7 +23,7 @@ pyyaml = "*" uritemplate = "*" whitenoise = "*" django-taggit = "*" -qrcode = "*" +django-qr-code = "*" [dev-packages] coveralls = "*" diff --git a/babybuddy/settings/base.py b/babybuddy/settings/base.py index c51fb1ce..27d813e8 100644 --- a/babybuddy/settings/base.py +++ b/babybuddy/settings/base.py @@ -36,6 +36,7 @@ INSTALLED_APPS = [ "imagekit", "storages", "import_export", + "qr_code", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", diff --git a/babybuddy/templates/babybuddy/login_qr_code.txt b/babybuddy/templates/babybuddy/login_qr_code.txt new file mode 100644 index 00000000..9b4182c9 --- /dev/null +++ b/babybuddy/templates/babybuddy/login_qr_code.txt @@ -0,0 +1,4 @@ +{% load i18n widget_tweaks babybuddy_tags qr_code %} +{% url 'babybuddy:root-router' as relative_root_url %} +{% make_absolute_url relative_root_url as absolute_root_url %} +BABYBUDDY-LOGIN:{"url":"{{ absolute_root_url }}","api_key":"{{ user.settings.api_key }}"} \ No newline at end of file diff --git a/babybuddy/templates/babybuddy/user_settings_form.html b/babybuddy/templates/babybuddy/user_settings_form.html index 7ac8901b..d91091d0 100644 --- a/babybuddy/templates/babybuddy/user_settings_form.html +++ b/babybuddy/templates/babybuddy/user_settings_form.html @@ -1,5 +1,5 @@ {% extends 'babybuddy/page.html' %} -{% load i18n widget_tweaks babybuddy_tags %} +{% load i18n widget_tweaks babybuddy_tags qr_code %} {% block title %}{% trans "User Settings" %}{% endblock %} @@ -87,12 +87,9 @@
- {% url 'babybuddy:root-router' as relative_root_url %} - {% make_absolute_url relative_root_url as absolute_root_url %} - - +
+ {% qr_from_text qr_code_data size="s" %} +
diff --git a/babybuddy/templatetags/babybuddy_tags.py b/babybuddy/templatetags/babybuddy_tags.py index 5a840e75..4f8c8c3e 100644 --- a/babybuddy/templatetags/babybuddy_tags.py +++ b/babybuddy/templatetags/babybuddy_tags.py @@ -67,103 +67,3 @@ def make_absolute_url(context, url): request = context["request"] abs_url = request.build_absolute_uri(url) return abs_url - - -class QrCodeNode(template.Node): - def __init__(self, nodelist, strip, border, box_size) -> None: - super().__init__() - self.__nodelist = nodelist - self.__strip = strip - self.__border = border - self.__box_size = box_size - - def render(self, context): - contents = "" - for node in self.__nodelist: - contents += node.render(context) - if self.__strip: - contents = contents.strip() - - import qrcode - - qr = qrcode.QRCode(border=self.__border, box_size=self.__box_size) - qr.add_data(contents) - qr.make(fit=True) - image = qr.make_image() - - bytesio = io.BytesIO() - image.save(bytesio, format="png") - base64_data = base64.b64encode(bytesio.getbuffer()).decode() - return f"data:image/png;base64,{base64_data}" - - -@register.tag_function -def qrcodepng(parser, token): - """ - This template tag allows the generation of arbirary qr code pngs that - can be displayed, for example, in html tags. - - The template tag can be used as follows: - - - - This will produce a qrcode that encodes the - string "\n Hello World\n ". One can use the qrcode parameter - ``stripwhitespace`` to strip the extra whitespace at the start and end of - the string: - - {% qrcodepng stripwhitespace %} - - All supported arguments: - - - stripwhitespace: strip whitespace of the qrcode-contents - - border=[int]: Border of the qrcode in pixels (default: 1) - - box_size=[int]: Pixel size of the qr-code blocks (default: 5) - """ - - contents = token.split_contents() - params = contents[1:] - - def get_parameter(name: str, value_type=None): - search_for = name - if value_type is not None: - search_for += "=" - - for p in params: - if p.startswith(search_for): - if value_type is None: - if p != search_for: - continue - params.remove(p) - return True - else: - str_value = p[len(search_for) :] - try: - result = value_type(str_value) - except ValueError: - raise template.TemplateSyntaxError( - f"Invalid parameter '{p}' does " - f"not have type '{value_type}'" - ) - else: - params.remove(p) - return result - - if value_type is None: - return False - return None - - strip = get_parameter("stripwhitespace") - border = get_parameter("border", int) or 1 - box_size = get_parameter("box_size", int) or 5 - - if params: - raise template.TemplateSyntaxError( - f"Unkown arguments for qrcode template tag: {', '.join(params)}" - ) - - nodelist = parser.parse(("endqrcodepng",)) - parser.delete_first_token() - return QrCodeNode(nodelist, strip, border, box_size) diff --git a/babybuddy/views.py b/babybuddy/views.py index c2ad4cda..24acf494 100644 --- a/babybuddy/views.py +++ b/babybuddy/views.py @@ -160,16 +160,21 @@ class UserSettings(LoginRequiredMixin, View): form_user_class = forms.UserForm form_settings_class = forms.UserSettingsForm template_name = "babybuddy/user_settings_form.html" + qr_code_template = "babybuddy/login_qr_code.txt" def get(self, request): settings = request.user.settings + qr_code_response = render(request, self.qr_code_template) + qr_code_data = qr_code_response.content.decode().strip() + return render( request, self.template_name, { "form_user": self.form_user_class(instance=request.user), "form_settings": self.form_settings_class(instance=settings), + "qr_code_data": qr_code_data, }, ) diff --git a/requirements.txt b/requirements.txt index 4ebffb44..5e387283 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ django-filter==22.1 django-imagekit==4.1.0 django-import-export==2.8.0 django-ipware==4.0.2 +django-qr-code==3.1.1 django-storages==1.12.3 django-taggit==3.0.0 django-widget-tweaks==1.4.12 @@ -31,7 +32,6 @@ python-dateutil==2.8.2 python-dotenv==0.20.0 pytz==2022.1 pyyaml==6.0 -qrcode==7.3.1 s3transfer==0.6.0 setuptools==63.1.0 six==1.16.0 From da7597fd374106c03d7359159d98c6cc3e790a7c Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Tue, 30 Aug 2022 21:32:10 +0200 Subject: [PATCH 09/25] Revert unused/incorrect imports --- babybuddy/models.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/babybuddy/models.py b/babybuddy/models.py index 2631ba03..be565050 100644 --- a/babybuddy/models.py +++ b/babybuddy/models.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- import pytz -import json -import io from django.conf import settings from django.contrib.auth.models import User @@ -14,8 +12,6 @@ from django.utils.translation import gettext_lazy as _ from rest_framework.authtoken.models import Token -import qrcode - class Settings(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) From 099178081678fcea99b767a3553dc8c99eafda43 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Tue, 30 Aug 2022 21:33:27 +0200 Subject: [PATCH 10/25] Remove more unused imports --- babybuddy/templatetags/babybuddy_tags.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/babybuddy/templatetags/babybuddy_tags.py b/babybuddy/templatetags/babybuddy_tags.py index 4f8c8c3e..e9170883 100644 --- a/babybuddy/templatetags/babybuddy_tags.py +++ b/babybuddy/templatetags/babybuddy_tags.py @@ -1,13 +1,9 @@ # -*- coding: utf-8 -*- -import io -import base64 -from multiprocessing.sharedctypes import Value from django import template from django.apps import apps from django.utils import timezone from django.utils.translation import to_locale, get_language -from django.template.defaultfilters import stringfilter from core.models import Child From 7424155ba4c666f65927f21c77f90343b2c5137b Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Tue, 30 Aug 2022 21:34:04 +0200 Subject: [PATCH 11/25] Remove unused imports --- babybuddy/views.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/babybuddy/views.py b/babybuddy/views.py index 24acf494..209ef246 100644 --- a/babybuddy/views.py +++ b/babybuddy/views.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -import base64 - -from urllib.parse import urljoin - from django.contrib import messages from django.contrib.auth import update_session_auth_hash from django.contrib.auth.forms import PasswordChangeForm From 878ff64fade4c27d0a0d658899f2901acff497e8 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Tue, 30 Aug 2022 22:49:54 +0200 Subject: [PATCH 12/25] Add tests, add username to api/profile endpoint --- api/serializers.py | 5 ++++ api/tests.py | 29 +++++++++++++++++++ .../babybuddy/user_settings_form.html | 2 +- babybuddy/tests/tests_views.py | 5 ++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/api/serializers.py b/api/serializers.py index a6981af1..102f14d5 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -273,6 +273,7 @@ class WeightSerializer(CoreModelSerializer, TaggableSerializer): class ProfileSerializer(serializers.ModelSerializer): api_key = serializers.SerializerMethodField("get_api_key") + username = serializers.SerializerMethodField("get_username") first_name = serializers.SerializerMethodField("get_first_name") last_name = serializers.SerializerMethodField("get_last_name") email = serializers.SerializerMethodField("get_email") @@ -281,6 +282,9 @@ class ProfileSerializer(serializers.ModelSerializer): def get_api_key(self, value): return self.instance.api_key().key + def get_username(self, value): + return self.instance.user.username + def get_first_name(self, value): return self.instance.user.first_name @@ -297,6 +301,7 @@ class ProfileSerializer(serializers.ModelSerializer): model = babybuddy_models.Settings fields = ( "user", + "username", "first_name", "last_name", "email", diff --git a/api/tests.py b/api/tests.py index c8ee0e4b..65d60d1a 100644 --- a/api/tests.py +++ b/api/tests.py @@ -874,3 +874,32 @@ class WeightAPITestCase(TestBase.BabyBuddyAPITestCaseBase): ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, entry) + + +class TestProfileAPITestCase(APITestCase): + endpoint = reverse("api:profile") + + def setUp(self): + self.client.login(username="admin", password="admin") + + def test_get(self): + response = self.client.get(self.endpoint) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictContainsSubset( + { + "user": 1, + "username": "admin", + "first_name": "", + "last_name": "", + "email": "", + "staff": True, + "language": "en-US", + "timezone": "UTC", + }, + response.data, + ) + + # Test that api_key is in the mix and "some long string" + self.assertIn("api_key", response.data) + self.assertTrue(isinstance(response.data["api_key"], str)) + self.assertGreater(len(response.data["api_key"]), 30) diff --git a/babybuddy/templates/babybuddy/user_settings_form.html b/babybuddy/templates/babybuddy/user_settings_form.html index d91091d0..24e80c0c 100644 --- a/babybuddy/templates/babybuddy/user_settings_form.html +++ b/babybuddy/templates/babybuddy/user_settings_form.html @@ -87,7 +87,7 @@
-
+
{% qr_from_text qr_code_data size="s" %}
diff --git a/babybuddy/tests/tests_views.py b/babybuddy/tests/tests_views.py index 90c9cb4b..25ac7a7b 100644 --- a/babybuddy/tests/tests_views.py +++ b/babybuddy/tests/tests_views.py @@ -52,6 +52,11 @@ class ViewsTestCase(TestCase): page = self.c.get("/user/settings/") self.assertEqual(page.status_code, 200) + self.assertRegex( + page.content.decode(), + r""".*
]* data-qr-code-content="[^"]+"[^>]*>.*""", + ) + def test_user_views(self): # Staff setting is required to access user management. page = self.c.get("/users/") From 02ea6487d5c43d0a75034d8ee0f01093e0a421e4 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Mon, 5 Sep 2022 22:11:20 +0200 Subject: [PATCH 13/25] Remove UserSerializer --- api/serializers.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index 102f14d5..f3044409 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -259,12 +259,6 @@ class TummyTimeSerializer(CoreModelWithDurationSerializer, TaggableSerializer): ) -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = ("id", "username") - - class WeightSerializer(CoreModelSerializer, TaggableSerializer): class Meta: model = models.Weight From 5ea7d33ef6aac33e993a267fc10f3af6150f6634 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Mon, 5 Sep 2022 23:00:03 +0200 Subject: [PATCH 14/25] Add back the UserProfile and use DRF nested relationship for profile page - Also fix part of the openapi-specs. Item-model for profile page is still not rendered out however --- api/serializers.py | 40 +++++++++++++++------------------------- api/views.py | 5 +++++ 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index f3044409..02f04504 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -265,41 +265,31 @@ class WeightSerializer(CoreModelSerializer, TaggableSerializer): fields = ("id", "child", "weight", "date", "notes", "tags") +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ( + "id", + "username", + "first_name", + "last_name", + "email", + "is_staff", + ) + extra_kwargs = {k: {"read_only": True} for k in fields} + + class ProfileSerializer(serializers.ModelSerializer): + user = UserSerializer(many=False) api_key = serializers.SerializerMethodField("get_api_key") - username = serializers.SerializerMethodField("get_username") - first_name = serializers.SerializerMethodField("get_first_name") - last_name = serializers.SerializerMethodField("get_last_name") - email = serializers.SerializerMethodField("get_email") - staff = serializers.SerializerMethodField("is_staff") def get_api_key(self, value): return self.instance.api_key().key - def get_username(self, value): - return self.instance.user.username - - def get_first_name(self, value): - return self.instance.user.first_name - - def get_last_name(self, value): - return self.instance.user.last_name - - def get_email(self, value): - return self.instance.user.email - - def is_staff(self, value): - return self.instance.user.is_staff - class Meta: model = babybuddy_models.Settings fields = ( "user", - "username", - "first_name", - "last_name", - "email", - "staff", "language", "timezone", "api_key", diff --git a/api/views.py b/api/views.py index 04585fd1..bd15f7c8 100644 --- a/api/views.py +++ b/api/views.py @@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404 from rest_framework import viewsets, views from rest_framework.decorators import action from rest_framework.response import Response +from rest_framework.schemas.openapi import AutoSchema from core import models from babybuddy import models as babybuddy_models @@ -119,7 +120,11 @@ class WeightViewSet(viewsets.ModelViewSet): class ProfileView(views.APIView): + schema = AutoSchema(operation_id_base="CurrentProfile") + + action = "get" basename = "profile" + queryset = babybuddy_models.Settings.objects.all() serializer_class = serializers.ProfileSerializer From 5b13a38f989e9200f3a1130faa01128acd6c252b Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Mon, 5 Sep 2022 23:03:24 +0200 Subject: [PATCH 15/25] Update the openapi-schema --- openapi-schema.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openapi-schema.yml b/openapi-schema.yml index fdd25599..9961906e 100644 --- a/openapi-schema.yml +++ b/openapi-schema.yml @@ -4530,6 +4530,19 @@ paths: description: '' tags: - api + /api/profile: + get: + operationId: retrieveCurrentProfile + description: '' + parameters: [] + responses: + '200': + content: + application/json: + schema: {} + description: '' + tags: + - api /api/timers/{id}/restart/: patch: operationId: restartTimer @@ -4848,7 +4861,7 @@ components: slug: type: string readOnly: true - pattern: ^[-a-zA-Z0-9_]+$ + pattern: ^[-\w]+\z name: type: string maxLength: 100 From f8212bfe80188f614a1b9da217b27bd79188590b Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Mon, 5 Sep 2022 23:07:56 +0200 Subject: [PATCH 16/25] Fix tests --- api/tests.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/api/tests.py b/api/tests.py index 65d60d1a..4dc2a1a4 100644 --- a/api/tests.py +++ b/api/tests.py @@ -887,17 +887,22 @@ class TestProfileAPITestCase(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertDictContainsSubset( { - "user": 1, - "username": "admin", - "first_name": "", - "last_name": "", - "email": "", - "staff": True, "language": "en-US", "timezone": "UTC", }, response.data, ) + self.assertDictContainsSubset( + { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "", + "email": "", + "is_staff": True, + }, + response.data["user"], + ) # Test that api_key is in the mix and "some long string" self.assertIn("api_key", response.data) From 0f4c006c05f5eb1a1aed378fc3cf519354bf5711 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Mon, 17 Oct 2022 09:40:06 +0200 Subject: [PATCH 17/25] Add new "Add a device" view, remove QR-code from settings-view --- .../templates/babybuddy/nav-dropdown.html | 1 + .../templates/babybuddy/user_add_device.html | 44 +++++++++++++++++ .../babybuddy/user_settings_form.html | 8 --- babybuddy/urls.py | 1 + babybuddy/views.py | 49 +++++++++++++++---- 5 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 babybuddy/templates/babybuddy/user_add_device.html diff --git a/babybuddy/templates/babybuddy/nav-dropdown.html b/babybuddy/templates/babybuddy/nav-dropdown.html index 3d9fac7e..d577efae 100644 --- a/babybuddy/templates/babybuddy/nav-dropdown.html +++ b/babybuddy/templates/babybuddy/nav-dropdown.html @@ -323,6 +323,7 @@ {% trans "Settings" %} {% trans "Password" %} + {% trans "Add a device" %}
{% csrf_token %}
-
- -
-
- {% qr_from_text qr_code_data size="s" %} -
-
-
diff --git a/babybuddy/urls.py b/babybuddy/urls.py index b6ecb3ab..1fa99921 100644 --- a/babybuddy/urls.py +++ b/babybuddy/urls.py @@ -42,6 +42,7 @@ app_patterns = [ path("users//delete/", views.UserDelete.as_view(), name="user-delete"), path("user/password/", views.UserPassword.as_view(), name="user-password"), path("user/settings/", views.UserSettings.as_view(), name="user-settings"), + path("user/add-device/", views.UserAddDevice.as_view(), name="user-add-device"), ] urlpatterns = [ diff --git a/babybuddy/views.py b/babybuddy/views.py index 209ef246..438a1512 100644 --- a/babybuddy/views.py +++ b/babybuddy/views.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- +from xmlrpc.client import Boolean from django.contrib import messages from django.contrib.auth import update_session_auth_hash from django.contrib.auth.forms import PasswordChangeForm from django.contrib.auth.models import User from django.contrib.auth.views import LogoutView as LogoutViewBase from django.contrib.messages.views import SuccessMessageMixin +from django.core.exceptions import BadRequest from django.http import HttpResponseForbidden from django.middleware.csrf import REASON_BAD_ORIGIN from django.shortcuts import redirect, render @@ -147,7 +149,16 @@ class UserPassword(LoginRequiredMixin, View): return render(request, self.template_name, {"form": form}) -class UserSettings(LoginRequiredMixin, View): +class RegenerateApiKey: + def handle_api_key_post(self, request) -> Boolean: + if request.POST.get("api_key_regenerate"): + request.user.settings.api_key(reset=True) + messages.success(request, _("User API key regenerated.")) + return True + return False + + +class UserSettings(LoginRequiredMixin, RegenerateApiKey, View): """ Handles both the User and Settings models. Based on this SO answer: https://stackoverflow.com/a/45056835. @@ -156,30 +167,23 @@ class UserSettings(LoginRequiredMixin, View): form_user_class = forms.UserForm form_settings_class = forms.UserSettingsForm template_name = "babybuddy/user_settings_form.html" - qr_code_template = "babybuddy/login_qr_code.txt" def get(self, request): settings = request.user.settings - qr_code_response = render(request, self.qr_code_template) - qr_code_data = qr_code_response.content.decode().strip() - return render( request, self.template_name, { "form_user": self.form_user_class(instance=request.user), "form_settings": self.form_settings_class(instance=settings), - "qr_code_data": qr_code_data, }, ) def post(self, request): - if request.POST.get("api_key_regenerate"): - request.user.settings.api_key(reset=True) - messages.success(request, _("User API key regenerated.")) + if self.handle_api_key_post(request): return redirect("babybuddy:user-settings") - + form_user = self.form_user_class(instance=request.user, data=request.POST) form_settings = self.form_settings_class( instance=request.user.settings, data=request.POST @@ -200,6 +204,31 @@ class UserSettings(LoginRequiredMixin, View): ) +class UserAddDevice(LoginRequiredMixin, RegenerateApiKey, View): + form_user_class = forms.UserForm + template_name = "babybuddy/user_add_device.html" + qr_code_template = "babybuddy/login_qr_code.txt" + + def get(self, request): + qr_code_response = render(request, self.qr_code_template) + qr_code_data = qr_code_response.content.decode().strip() + + return render( + request, + self.template_name, + { + "form_user": self.form_user_class(instance=request.user), + "qr_code_data": qr_code_data, + }, + ) + + def post(self, request): + if self.handle_api_key_post(request): + return redirect("babybuddy:user-settings") + else: + raise BadRequest() + + class Welcome(LoginRequiredMixin, TemplateView): """ Basic introduction to Baby Buddy (meant to be shown when no data is in the From 1f835d181d966a6b42eada0e3574811a82a59843 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Mon, 17 Oct 2022 09:45:26 +0200 Subject: [PATCH 18/25] Fix test --- babybuddy/tests/tests_views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/babybuddy/tests/tests_views.py b/babybuddy/tests/tests_views.py index 25ac7a7b..7147516d 100644 --- a/babybuddy/tests/tests_views.py +++ b/babybuddy/tests/tests_views.py @@ -52,6 +52,8 @@ class ViewsTestCase(TestCase): page = self.c.get("/user/settings/") self.assertEqual(page.status_code, 200) + def test_add_device_page(self): + page = self.c.get("/user/add-device/") self.assertRegex( page.content.decode(), r""".*
]* data-qr-code-content="[^"]+"[^>]*>.*""", From 8b617b0bff67290ce3876bd77c535b5afa4aa0ec Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Mon, 17 Oct 2022 09:48:26 +0200 Subject: [PATCH 19/25] Black --- babybuddy/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/babybuddy/views.py b/babybuddy/views.py index 438a1512..de0664f2 100644 --- a/babybuddy/views.py +++ b/babybuddy/views.py @@ -183,7 +183,7 @@ class UserSettings(LoginRequiredMixin, RegenerateApiKey, View): def post(self, request): if self.handle_api_key_post(request): return redirect("babybuddy:user-settings") - + form_user = self.form_user_class(instance=request.user, data=request.POST) form_settings = self.form_settings_class( instance=request.user.settings, data=request.POST From d604f6f85a52ff5b7976b191527256a8b6b5bb97 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Mon, 17 Oct 2022 10:02:21 +0200 Subject: [PATCH 20/25] Better coverage --- babybuddy/tests/tests_forms.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/babybuddy/tests/tests_forms.py b/babybuddy/tests/tests_forms.py index 58f9b234..dca51ea1 100644 --- a/babybuddy/tests/tests_forms.py +++ b/babybuddy/tests/tests_forms.py @@ -117,9 +117,23 @@ class FormsTestCase(TestCase): page = self.c.post("/user/settings/", params, follow=True) self.assertEqual(page.status_code, 200) - self.assertNotEqual( - api_key_before, User.objects.get(pk=self.user.id).settings.api_key() - ) + new_api_key = User.objects.get(pk=self.user.id).settings.api_key() + self.assertNotEqual(api_key_before, new_api_key) + + # API key can also be regenerated on the add-device page + api_key_before = new_api_key + params = {"api_key_regenerate": "Regenerate"} + page = self.c.post("/user/add-device/", params, follow=True) + self.assertEqual(page.status_code, 200) + new_api_key = User.objects.get(pk=self.user.id).settings.api_key() + self.assertNotEqual(api_key_before, new_api_key) + + def test_invalid_post_to_add_device(self): + self.c.login(**self.credentials) + page = self.c.get("/user/add-device/") + self.assertEqual(page.status_code, 200) + page = self.c.post("/user/add-device/", params={"garbage": True}, follow=True) + self.assertEqual(page.status_code, 400) def test_user_settings_invalid(self): self.c.login(**self.credentials) From f559f553a0afa2846d5710b262f7f470556e526f Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Wed, 19 Oct 2022 21:30:09 +0200 Subject: [PATCH 21/25] Rework titles/texts on the add-device page --- .../templates/babybuddy/user_add_device.html | 43 ++++++++----------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/babybuddy/templates/babybuddy/user_add_device.html b/babybuddy/templates/babybuddy/user_add_device.html index 293b0da3..db0071d8 100644 --- a/babybuddy/templates/babybuddy/user_add_device.html +++ b/babybuddy/templates/babybuddy/user_add_device.html @@ -11,34 +11,27 @@ {% block content %}

{% trans "Add a device" %}

-

- {% blocktrans trimmed %} - This page allows you to gain access to babybuddy from a - third party app or device. - {% endblocktrans %} -

-
+ {% trans "Authentication Methods" %}
- {% trans "Device login options" %} -
- -
-
- {% csrf_token %} - {{ user.settings.api_key }} - -
-
-
-
- -
-
- {% qr_from_text qr_code_data size="s" %} +
+ +
+
+ {% csrf_token %} + {{ user.settings.api_key }} + +
-
-
+
+ +
+
+ {% qr_from_text qr_code_data size="s" %} +
+
+
+
{% endblock %} From 901bb99bd76044da5351be55af86d04c174c6a31 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Wed, 19 Oct 2022 21:30:21 +0200 Subject: [PATCH 22/25] Fix copy-pasta mistake redirecting to the wrong page --- babybuddy/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/babybuddy/views.py b/babybuddy/views.py index de0664f2..78197112 100644 --- a/babybuddy/views.py +++ b/babybuddy/views.py @@ -224,7 +224,7 @@ class UserAddDevice(LoginRequiredMixin, RegenerateApiKey, View): def post(self, request): if self.handle_api_key_post(request): - return redirect("babybuddy:user-settings") + return redirect("babybuddy:user-add-device") else: raise BadRequest() From 58b2c78ab39b23a00af25df3810b7217eda6c7ec Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Wed, 19 Oct 2022 21:36:19 +0200 Subject: [PATCH 23/25] Fix breadcrumbs --- babybuddy/templates/babybuddy/user_add_device.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/babybuddy/templates/babybuddy/user_add_device.html b/babybuddy/templates/babybuddy/user_add_device.html index db0071d8..b7cb5f30 100644 --- a/babybuddy/templates/babybuddy/user_add_device.html +++ b/babybuddy/templates/babybuddy/user_add_device.html @@ -5,7 +5,7 @@ {% block breadcrumbs %} - + {% endblock %} {% block content %} From f7a583f50cb0005184f66b52ca50ee58e1ff3d3d Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Wed, 19 Oct 2022 21:37:13 +0200 Subject: [PATCH 24/25] Remove unneeded dependecy in template --- babybuddy/templates/babybuddy/user_settings_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/babybuddy/templates/babybuddy/user_settings_form.html b/babybuddy/templates/babybuddy/user_settings_form.html index db1bd6e3..3f4db185 100644 --- a/babybuddy/templates/babybuddy/user_settings_form.html +++ b/babybuddy/templates/babybuddy/user_settings_form.html @@ -1,5 +1,5 @@ {% extends 'babybuddy/page.html' %} -{% load i18n widget_tweaks babybuddy_tags qr_code %} +{% load i18n widget_tweaks babybuddy_tags %} {% block title %}{% trans "User Settings" %}{% endblock %} From 608005863509ec958fa8b670fe3d7fb4b2a6bda2 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Thu, 20 Oct 2022 10:53:10 +0200 Subject: [PATCH 25/25] Refactor handle_api_key_post, fix type annotation --- babybuddy/views.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/babybuddy/views.py b/babybuddy/views.py index 78197112..d7324fba 100644 --- a/babybuddy/views.py +++ b/babybuddy/views.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from xmlrpc.client import Boolean from django.contrib import messages from django.contrib.auth import update_session_auth_hash from django.contrib.auth.forms import PasswordChangeForm @@ -149,16 +148,22 @@ class UserPassword(LoginRequiredMixin, View): return render(request, self.template_name, {"form": form}) -class RegenerateApiKey: - def handle_api_key_post(self, request) -> Boolean: - if request.POST.get("api_key_regenerate"): - request.user.settings.api_key(reset=True) - messages.success(request, _("User API key regenerated.")) - return True - return False +def handle_api_regenerate_request(request) -> bool: + """ + Checks if the current request contains a request to update the API key + and if it does, updeates the API key. + + Returns True, if the API-key regenerate request was detected and handled. + """ + + if request.POST.get("api_key_regenerate"): + request.user.settings.api_key(reset=True) + messages.success(request, _("User API key regenerated.")) + return True + return False -class UserSettings(LoginRequiredMixin, RegenerateApiKey, View): +class UserSettings(LoginRequiredMixin, View): """ Handles both the User and Settings models. Based on this SO answer: https://stackoverflow.com/a/45056835. @@ -181,7 +186,7 @@ class UserSettings(LoginRequiredMixin, RegenerateApiKey, View): ) def post(self, request): - if self.handle_api_key_post(request): + if handle_api_regenerate_request(request): return redirect("babybuddy:user-settings") form_user = self.form_user_class(instance=request.user, data=request.POST) @@ -204,7 +209,7 @@ class UserSettings(LoginRequiredMixin, RegenerateApiKey, View): ) -class UserAddDevice(LoginRequiredMixin, RegenerateApiKey, View): +class UserAddDevice(LoginRequiredMixin, View): form_user_class = forms.UserForm template_name = "babybuddy/user_add_device.html" qr_code_template = "babybuddy/login_qr_code.txt" @@ -223,7 +228,7 @@ class UserAddDevice(LoginRequiredMixin, RegenerateApiKey, View): ) def post(self, request): - if self.handle_api_key_post(request): + if handle_api_regenerate_request(request): return redirect("babybuddy:user-add-device") else: raise BadRequest()