mirror of https://github.com/snachodog/mybuddy.git
Merge pull request #524 from MrApplejuice/app-integration
Extra API endpoints and login qr-code for app integration
This commit is contained in:
commit
50ffd2775b
1
Pipfile
1
Pipfile
|
@ -24,6 +24,7 @@ pyyaml = "*"
|
||||||
uritemplate = "*"
|
uritemplate = "*"
|
||||||
whitenoise = "*"
|
whitenoise = "*"
|
||||||
django-taggit = "*"
|
django-taggit = "*"
|
||||||
|
django-qr-code = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
coveralls = "*"
|
coveralls = "*"
|
||||||
|
|
|
@ -9,6 +9,7 @@ from django.utils import timezone
|
||||||
from taggit.serializers import TagListSerializerField, TaggitSerializer
|
from taggit.serializers import TagListSerializerField, TaggitSerializer
|
||||||
|
|
||||||
from core import models
|
from core import models
|
||||||
|
from babybuddy import models as babybuddy_models
|
||||||
|
|
||||||
|
|
||||||
class CoreModelSerializer(serializers.HyperlinkedModelSerializer):
|
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 WeightSerializer(CoreModelSerializer, TaggableSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Weight
|
model = models.Weight
|
||||||
fields = ("id", "child", "weight", "date", "notes", "tags")
|
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}
|
||||||
|
|
34
api/tests.py
34
api/tests.py
|
@ -874,3 +874,37 @@ class WeightAPITestCase(TestBase.BabyBuddyAPITestCaseBase):
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data, entry)
|
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)
|
||||||
|
|
63
api/urls.py
63
api/urls.py
|
@ -1,11 +1,51 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from typing import NamedTuple, List, Any
|
||||||
|
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
from rest_framework.schemas import get_schema_view
|
from rest_framework.schemas import get_schema_view
|
||||||
|
|
||||||
from . import views
|
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"bmi", views.BMIViewSet)
|
||||||
router.register(r"changes", views.DiaperChangeViewSet)
|
router.register(r"changes", views.DiaperChangeViewSet)
|
||||||
router.register(r"children", views.ChildViewSet)
|
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"tummy-times", views.TummyTimeViewSet)
|
||||||
router.register(r"weight", views.WeightViewSet)
|
router.register(r"weight", views.WeightViewSet)
|
||||||
|
|
||||||
app_name = "api"
|
router.add_detail_path("profile", "profile", views.ProfileView.as_view())
|
||||||
|
router.add_detail_path(
|
||||||
urlpatterns = [
|
"schema",
|
||||||
path("api/", include(router.urls)),
|
"openapi-schema",
|
||||||
path("api/auth/", include("rest_framework.urls", namespace="rest_framework")),
|
|
||||||
path(
|
|
||||||
"api/schema",
|
|
||||||
get_schema_view(
|
get_schema_view(
|
||||||
title="Baby Buddy API",
|
title="Baby Buddy API",
|
||||||
version=1,
|
version=1,
|
||||||
description="API documentation for the Baby Buddy application",
|
description="API documentation for the Baby Buddy application",
|
||||||
),
|
),
|
||||||
name="openapi-schema",
|
)
|
||||||
),
|
|
||||||
|
|
||||||
|
app_name = "api"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("api/", include(router.urls)),
|
||||||
|
path("api/auth/", include("rest_framework.urls", namespace="rest_framework")),
|
||||||
]
|
]
|
||||||
|
|
23
api/views.py
23
api/views.py
|
@ -1,9 +1,13 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.schemas.openapi import AutoSchema
|
||||||
|
|
||||||
from core import models
|
from core import models
|
||||||
|
from babybuddy import models as babybuddy_models
|
||||||
|
|
||||||
from . import serializers, filters
|
from . import serializers, filters
|
||||||
|
|
||||||
|
@ -113,3 +117,20 @@ class WeightViewSet(viewsets.ModelViewSet):
|
||||||
queryset = models.Weight.objects.all()
|
queryset = models.Weight.objects.all()
|
||||||
serializer_class = serializers.WeightSerializer
|
serializer_class = serializers.WeightSerializer
|
||||||
filterset_fields = ("child", "date")
|
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)
|
||||||
|
|
|
@ -36,6 +36,7 @@ INSTALLED_APPS = [
|
||||||
"imagekit",
|
"imagekit",
|
||||||
"storages",
|
"storages",
|
||||||
"import_export",
|
"import_export",
|
||||||
|
"qr_code",
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
|
|
|
@ -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 }}"}
|
|
@ -323,6 +323,7 @@
|
||||||
<h6 class="dropdown-header">{% trans "User" %}</h6>
|
<h6 class="dropdown-header">{% trans "User" %}</h6>
|
||||||
<a href="{% url 'babybuddy:user-settings' %}" class="dropdown-item">{% trans "Settings" %}</a>
|
<a href="{% url 'babybuddy:user-settings' %}" class="dropdown-item">{% trans "Settings" %}</a>
|
||||||
<a href="{% url 'babybuddy:user-password' %}" class="dropdown-item">{% trans "Password" %}</a>
|
<a href="{% url 'babybuddy:user-password' %}" class="dropdown-item">{% trans "Password" %}</a>
|
||||||
|
<a href="{% url 'babybuddy:user-add-device' %}" class="dropdown-item">{% trans "Add a device" %}</a>
|
||||||
<form action="{% url 'babybuddy:logout' %}" role="form" method="post">
|
<form action="{% url 'babybuddy:logout' %}" role="form" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="dropdown-item">
|
<button class="dropdown-item">
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
{% extends 'babybuddy/page.html' %}
|
||||||
|
{% load i18n widget_tweaks babybuddy_tags qr_code %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Add a device" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<li class="breadcrumb-item">{% trans "User" %}</li>
|
||||||
|
<li class="breadcrumb-item active">{% trans "Add a device" %}</li>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "Add a device" %}</h1>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<legend>{% trans "Authentication Methods" %}</legend>
|
||||||
|
<fieldset>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-2 col-form-label">{% trans "Key" %}</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<samp>{{ user.settings.api_key }}</samp>
|
||||||
|
<input type="submit" name="api_key_regenerate" value="{% trans "Regenerate" %}" class="btn btn-danger btn-xs" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-2 col-form-label">{% trans "Login QR code" %}</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div style="display:inline-block;background-color:white;" data-qr-code-content="{{ qr_code_data }}">
|
||||||
|
{% qr_from_text qr_code_data size="s" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends 'babybuddy/page.html' %}
|
{% extends 'babybuddy/page.html' %}
|
||||||
{% load i18n widget_tweaks %}
|
{% load i18n widget_tweaks babybuddy_tags %}
|
||||||
|
|
||||||
{% block title %}{% trans "User Settings" %}{% endblock %}
|
{% block title %}{% trans "User Settings" %}{% endblock %}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "API" %}</legend>
|
<legend>{% trans "API" %}</legend>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="id_email" class="col-sm-2 col-form-label">{% trans "Key" %}</label>
|
<label class="col-sm-2 col-form-label">{% trans "Key" %}</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<samp>{{ user.settings.api_key }}</samp>
|
<samp>{{ user.settings.api_key }}</samp>
|
||||||
<input type="submit" name="api_key_regenerate" value="{% trans "Regenerate" %}" class="btn btn-danger btn-xs" />
|
<input type="submit" name="api_key_regenerate" value="{% trans "Regenerate" %}" class="btn btn-danger btn-xs" />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -55,3 +56,10 @@ def get_child_count():
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def get_current_timezone():
|
def get_current_timezone():
|
||||||
return timezone.get_current_timezone_name()
|
return timezone.get_current_timezone_name()
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
|
@ -117,9 +117,23 @@ class FormsTestCase(TestCase):
|
||||||
|
|
||||||
page = self.c.post("/user/settings/", params, follow=True)
|
page = self.c.post("/user/settings/", params, follow=True)
|
||||||
self.assertEqual(page.status_code, 200)
|
self.assertEqual(page.status_code, 200)
|
||||||
self.assertNotEqual(
|
new_api_key = User.objects.get(pk=self.user.id).settings.api_key()
|
||||||
api_key_before, 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):
|
def test_user_settings_invalid(self):
|
||||||
self.c.login(**self.credentials)
|
self.c.login(**self.credentials)
|
||||||
|
|
|
@ -52,6 +52,13 @@ class ViewsTestCase(TestCase):
|
||||||
page = self.c.get("/user/settings/")
|
page = self.c.get("/user/settings/")
|
||||||
self.assertEqual(page.status_code, 200)
|
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""".*<div [^>]* data-qr-code-content="[^"]+"[^>]*>.*""",
|
||||||
|
)
|
||||||
|
|
||||||
def test_user_views(self):
|
def test_user_views(self):
|
||||||
# Staff setting is required to access user management.
|
# Staff setting is required to access user management.
|
||||||
page = self.c.get("/users/")
|
page = self.c.get("/users/")
|
||||||
|
|
|
@ -42,6 +42,7 @@ app_patterns = [
|
||||||
path("users/<int:pk>/delete/", views.UserDelete.as_view(), name="user-delete"),
|
path("users/<int:pk>/delete/", views.UserDelete.as_view(), name="user-delete"),
|
||||||
path("user/password/", views.UserPassword.as_view(), name="user-password"),
|
path("user/password/", views.UserPassword.as_view(), name="user-password"),
|
||||||
path("user/settings/", views.UserSettings.as_view(), name="user-settings"),
|
path("user/settings/", views.UserSettings.as_view(), name="user-settings"),
|
||||||
|
path("user/add-device/", views.UserAddDevice.as_view(), name="user-add-device"),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.contrib.auth.forms import PasswordChangeForm
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.views import LogoutView as LogoutViewBase
|
from django.contrib.auth.views import LogoutView as LogoutViewBase
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
|
from django.core.exceptions import BadRequest
|
||||||
from django.http import HttpResponseForbidden
|
from django.http import HttpResponseForbidden
|
||||||
from django.middleware.csrf import REASON_BAD_ORIGIN
|
from django.middleware.csrf import REASON_BAD_ORIGIN
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
|
@ -147,6 +148,21 @@ class UserPassword(LoginRequiredMixin, View):
|
||||||
return render(request, self.template_name, {"form": form})
|
return render(request, self.template_name, {"form": form})
|
||||||
|
|
||||||
|
|
||||||
|
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, View):
|
class UserSettings(LoginRequiredMixin, View):
|
||||||
"""
|
"""
|
||||||
Handles both the User and Settings models.
|
Handles both the User and Settings models.
|
||||||
|
@ -158,21 +174,19 @@ class UserSettings(LoginRequiredMixin, View):
|
||||||
template_name = "babybuddy/user_settings_form.html"
|
template_name = "babybuddy/user_settings_form.html"
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
settings = request.user.settings
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
self.template_name,
|
self.template_name,
|
||||||
{
|
{
|
||||||
"form_user": self.form_user_class(instance=request.user),
|
"form_user": self.form_user_class(instance=request.user),
|
||||||
"form_settings": self.form_settings_class(
|
"form_settings": self.form_settings_class(instance=settings),
|
||||||
instance=request.user.settings
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
if request.POST.get("api_key_regenerate"):
|
if handle_api_regenerate_request(request):
|
||||||
request.user.settings.api_key(reset=True)
|
|
||||||
messages.success(request, _("User API key regenerated."))
|
|
||||||
return redirect("babybuddy:user-settings")
|
return redirect("babybuddy:user-settings")
|
||||||
|
|
||||||
form_user = self.form_user_class(instance=request.user, data=request.POST)
|
form_user = self.form_user_class(instance=request.user, data=request.POST)
|
||||||
|
@ -195,6 +209,31 @@ class UserSettings(LoginRequiredMixin, 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"
|
||||||
|
|
||||||
|
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 handle_api_regenerate_request(request):
|
||||||
|
return redirect("babybuddy:user-add-device")
|
||||||
|
else:
|
||||||
|
raise BadRequest()
|
||||||
|
|
||||||
|
|
||||||
class Welcome(LoginRequiredMixin, TemplateView):
|
class Welcome(LoginRequiredMixin, TemplateView):
|
||||||
"""
|
"""
|
||||||
Basic introduction to Baby Buddy (meant to be shown when no data is in the
|
Basic introduction to Baby Buddy (meant to be shown when no data is in the
|
||||||
|
|
|
@ -4530,6 +4530,19 @@ paths:
|
||||||
description: ''
|
description: ''
|
||||||
tags:
|
tags:
|
||||||
- api
|
- api
|
||||||
|
/api/profile:
|
||||||
|
get:
|
||||||
|
operationId: retrieveCurrentProfile
|
||||||
|
description: ''
|
||||||
|
parameters: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
description: ''
|
||||||
|
tags:
|
||||||
|
- api
|
||||||
/api/timers/{id}/restart/:
|
/api/timers/{id}/restart/:
|
||||||
patch:
|
patch:
|
||||||
operationId: restartTimer
|
operationId: restartTimer
|
||||||
|
@ -4848,7 +4861,7 @@ components:
|
||||||
slug:
|
slug:
|
||||||
type: string
|
type: string
|
||||||
readOnly: true
|
readOnly: true
|
||||||
pattern: ^[-a-zA-Z0-9_]+$
|
pattern: ^[-\w]+\z
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
maxLength: 100
|
maxLength: 100
|
||||||
|
|
|
@ -13,6 +13,7 @@ django-imagekit==4.1.0
|
||||||
django-import-export==2.9.0
|
django-import-export==2.9.0
|
||||||
django-ipware==4.0.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
django-ipware==4.0.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||||
django-storages==1.13.1
|
django-storages==1.13.1
|
||||||
|
django-qr-code==3.1.1
|
||||||
django-taggit==3.0.0
|
django-taggit==3.0.0
|
||||||
django-widget-tweaks==1.4.12
|
django-widget-tweaks==1.4.12
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.14.0
|
||||||
|
|
Loading…
Reference in New Issue