diff --git a/Pipfile b/Pipfile index 2fd61531..d4c4e4af 100644 --- a/Pipfile +++ b/Pipfile @@ -24,6 +24,7 @@ pyyaml = "*" uritemplate = "*" whitenoise = "*" django-taggit = "*" +django-qr-code = "*" [dev-packages] coveralls = "*" diff --git a/api/serializers.py b/api/serializers.py index e78e80c1..02f04504 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): @@ -258,13 +259,39 @@ class TummyTimeSerializer(CoreModelWithDurationSerializer, TaggableSerializer): ) -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = ("id", "username") - - class WeightSerializer(CoreModelSerializer, TaggableSerializer): class Meta: model = models.Weight 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") + + def get_api_key(self, value): + return self.instance.api_key().key + + class Meta: + model = babybuddy_models.Settings + fields = ( + "user", + "language", + "timezone", + "api_key", + ) + extra_kwargs = {k: {"read_only": True} for k in fields} diff --git a/api/tests.py b/api/tests.py index c8ee0e4b..4dc2a1a4 100644 --- a/api/tests.py +++ b/api/tests.py @@ -874,3 +874,37 @@ 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( + { + "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) + self.assertTrue(isinstance(response.data["api_key"], str)) + self.assertGreater(len(response.data["api_key"]), 30) diff --git a/api/urls.py b/api/urls.py index 10382c71..12d38df0 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,11 +1,51 @@ # -*- coding: utf-8 -*- +from collections import OrderedDict + +from typing import NamedTuple, List, Any + from django.urls import include, path from rest_framework import routers 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) @@ -21,18 +61,21 @@ router.register(r"timers", views.TimerViewSet) router.register(r"tummy-times", views.TummyTimeViewSet) router.register(r"weight", views.WeightViewSet) +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)), path("api/auth/", include("rest_framework.urls", namespace="rest_framework")), - path( - "api/schema", - get_schema_view( - title="Baby Buddy API", - version=1, - description="API documentation for the Baby Buddy application", - ), - name="openapi-schema", - ), ] diff --git a/api/views.py b/api/views.py index 3a7964cf..bd15f7c8 100644 --- a/api/views.py +++ b/api/views.py @@ -1,9 +1,13 @@ # -*- 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 rest_framework.schemas.openapi import AutoSchema from core import models +from babybuddy import models as babybuddy_models from . import serializers, filters @@ -113,3 +117,20 @@ class WeightViewSet(viewsets.ModelViewSet): queryset = models.Weight.objects.all() serializer_class = serializers.WeightSerializer filterset_fields = ("child", "date") + + +class ProfileView(views.APIView): + schema = AutoSchema(operation_id_base="CurrentProfile") + + action = "get" + 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) diff --git a/babybuddy/settings/base.py b/babybuddy/settings/base.py index d15609ae..5b026e50 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/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 %}