mybuddy/babybuddy/views.py

296 lines
9.7 KiB
Python

# -*- coding: utf-8 -*-
import json
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.views import LogoutView as LogoutViewBase
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import BadRequest
from django.forms import Form
from django.http import HttpResponseForbidden
from django.middleware.csrf import REASON_BAD_ORIGIN
from django.shortcuts import redirect, render
from django.template import loader
from django.urls import reverse, reverse_lazy
from django.utils import translation
from django.utils.decorators import method_decorator
from django.utils.text import format_lazy
from django.utils.translation import gettext as _, gettext_lazy
from django.views import csrf
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.http import require_POST
from django.views.generic import View
from django.views.generic.base import TemplateView, RedirectView
from django.views.generic.detail import BaseDetailView
from django.views.generic.edit import (
CreateView,
DeleteView,
FormMixin,
SingleObjectTemplateResponseMixin,
UpdateView,
)
from django.views.i18n import set_language
from axes.utils import reset
from django_filters.views import FilterView
from babybuddy import forms
from babybuddy.mixins import LoginRequiredMixin, PermissionRequiredMixin, StaffOnlyMixin
def csrf_failure(request, reason=""):
"""
Overrides the 403 CSRF failure template for bad origins in order to provide more
userful information about how to resolve the issue.
"""
if (
"HTTP_ORIGIN" in request.META
and reason == REASON_BAD_ORIGIN % request.META["HTTP_ORIGIN"]
):
context = {
"title": _("Forbidden"),
"main": _("CSRF verification failed. Request aborted."),
"reason": reason,
"origin": request.META["HTTP_ORIGIN"],
}
template = loader.get_template("error/403_csrf_bad_origin.html")
return HttpResponseForbidden(template.render(context), content_type="text/html")
return csrf.csrf_failure(request, reason, "403_csrf.html")
class RootRouter(LoginRequiredMixin, RedirectView):
"""
Redirects to the site dashboard.
"""
def get_redirect_url(self, *args, **kwargs):
self.url = reverse("dashboard:dashboard")
return super(RootRouter, self).get_redirect_url(self, *args, **kwargs)
class BabyBuddyFilterView(FilterView):
"""
Disables "strictness" for django-filter. It is unclear from the
documentation exactly what this does...
"""
# TODO Figure out the correct way to use this.
strict = False
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
children = {o.child for o in context["object_list"] if hasattr(o, "child")}
if len(children) == 1:
context["unique_child"] = True
return context
@method_decorator(csrf_protect, name="dispatch")
@method_decorator(never_cache, name="dispatch")
@method_decorator(require_POST, name="dispatch")
class LogoutView(LogoutViewBase):
pass
class UserList(StaffOnlyMixin, BabyBuddyFilterView):
model = get_user_model()
template_name = "babybuddy/user_list.html"
ordering = "username"
paginate_by = 10
filterset_fields = ("username", "first_name", "last_name", "email")
class UserAdd(StaffOnlyMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView):
model = get_user_model()
template_name = "babybuddy/user_form.html"
permission_required = ("admin.add_user",)
form_class = forms.UserAddForm
success_url = reverse_lazy("babybuddy:user-list")
success_message = gettext_lazy("User %(username)s added!")
class UserUpdate(
StaffOnlyMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
):
model = get_user_model()
template_name = "babybuddy/user_form.html"
permission_required = ("admin.change_user",)
form_class = forms.UserUpdateForm
success_url = reverse_lazy("babybuddy:user-list")
success_message = gettext_lazy("User %(username)s updated.")
class UserUnlock(
StaffOnlyMixin,
PermissionRequiredMixin,
SuccessMessageMixin,
FormMixin,
SingleObjectTemplateResponseMixin,
BaseDetailView,
):
model = get_user_model()
template_name = "babybuddy/user_confirm_unlock.html"
permission_required = ("admin.change_user",)
form_class = Form
success_message = gettext_lazy("User unlocked.")
def post(self, request, *args, **kwargs):
user = self.get_object()
form = self.get_form()
if form.is_valid():
reset(username=user.username)
return self.form_valid(form)
else:
return self.form_invalid(form)
def get_success_url(self):
return reverse("babybuddy:user-update", kwargs={"pk": self.kwargs["pk"]})
class UserDelete(
StaffOnlyMixin, PermissionRequiredMixin, DeleteView, SuccessMessageMixin
):
model = get_user_model()
template_name = "babybuddy/user_confirm_delete.html"
permission_required = ("admin.delete_user",)
success_url = reverse_lazy("babybuddy:user-list")
def get_success_message(self, cleaned_data):
return format_lazy(gettext_lazy("User {user} deleted."), user=self.get_object())
class UserPassword(LoginRequiredMixin, View):
"""
Handles user password changes.
"""
form_class = forms.UserPasswordForm
template_name = "babybuddy/user_password_form.html"
def get(self, request):
return render(
request, self.template_name, {"form": self.form_class(request.user)}
)
def post(self, request):
form = PasswordChangeForm(request.user, request.POST)
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user)
messages.success(request, _("Password updated."))
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):
"""
Handles both the User and Settings models.
Based on this SO answer: https://stackoverflow.com/a/45056835.
"""
form_user_class = forms.UserForm
form_settings_class = forms.UserSettingsForm
template_name = "babybuddy/user_settings_form.html"
def get(self, request):
settings = request.user.settings
return render(
request,
self.template_name,
{
"form_user": self.form_user_class(instance=request.user),
"form_settings": self.form_settings_class(instance=settings),
},
)
def post(self, request):
if handle_api_regenerate_request(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
)
if form_user.is_valid() and form_settings.is_valid():
user = form_user.save(commit=False)
user_settings = form_settings.save(commit=False)
user.settings = user_settings
user.save()
translation.activate(user.settings.language)
messages.success(request, _("Settings saved!"))
translation.deactivate()
return set_language(request)
return render(
request,
self.template_name,
{"user_form": form_user, "settings_form": form_settings},
)
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):
# Assemble qr_code json-data. For Home Assistant ingress support, we
# also need to extract the ingress_session token to allow an external
# app to authenticate with home assistant so it can reach baby buddy
session_cookies = {}
if request.is_homeassistant_ingress_request:
session_cookies["ingress_session"] = request.COOKIES.get("ingress_session")
qr_code_response = render(
request,
self.qr_code_template,
{"session_cookies": json.dumps(session_cookies)},
)
qr_code_data = qr_code_response.content.decode().strip()
# Now that the qr_code json-data is assembled, we can pass the json
# structure as data to the user_add_device - template where it will
# be converted into a qr-code.
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):
"""
Basic introduction to Baby Buddy (meant to be shown when no data is in the
database).
"""
template_name = "babybuddy/welcome.html"