Source code for users.views

"""The views of the users app."""

from __future__ import annotations

from io import BytesIO
from json import dumps
from typing import TYPE_CHECKING

from django.contrib.auth import logout
from django.contrib.auth.decorators import login_required
from django.contrib.messages import error, info
from django.core.serializers.json import DjangoJSONEncoder
from django.db import IntegrityError
from django.db.models import Subquery
from django.http import FileResponse, Http404, HttpResponse
from django.shortcuts import redirect, render
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django.views.generic import FormView
from django.views.generic.base import TemplateView

from allauth.account.forms import UserTokenForm
from allauth.account.models import EmailAddress
from allauth.account.views import (
    LogoutView, PasswordResetFromKeyView, _ajax_response
)

from MangAdventure.jsonld import breadcrumbs

from reader.models import Chapter

from .forms import UserProfileForm
from .models import Bookmark, UserProfile

if TYPE_CHECKING:  # pragma: no cover
    from django.http import HttpRequest


[docs]@login_required @cache_control(private=True, max_age=3600) def profile(request: HttpRequest) -> HttpResponse: """ View that serves the profile page of a user. A :class:`UserProfile` will be created if it doesn't exist. It serves the logged in user's profile by default, but accepts an ``id`` query parameter to view an arbitrary user's profile. :param request: The original request. :return: A response with the rendered ``profile.html`` template. :raises Http404: If there is no active user with the specified ``id``. """ try: uid = int(request.GET.get('id', request.user.id)) # type: ignore prof = UserProfile.objects.select_related('user').only( 'avatar', 'bio', 'user__email', 'user__username', 'user__first_name', 'user__last_name', 'user__is_active', 'user__is_superuser' ).get_or_create(user_id=uid)[0] except (ValueError, IntegrityError) as e: raise Http404 from e if not prof.user.is_active: # pragma: no cover raise Http404('Inactive user') if uid != request.user.id and prof.user.is_superuser: raise Http404('Cannot view profile of superuser') uri = request.build_absolute_uri(request.path) crumbs = breadcrumbs([('User', uri)]) return render(request, 'profile.html', { 'profile': prof, 'breadcrumbs': crumbs })
[docs]@login_required def export(request: HttpRequest) -> FileResponse: """ View that exports a user's data. :param request: The original request. :return: A response with the JSON data. """ profile = UserProfile.objects.get_or_create(user_id=request.user.id)[0] data = dumps(profile.export(), cls=DjangoJSONEncoder).encode() return FileResponse( BytesIO(data), as_attachment=True, filename='data.json', content_type='application/json' )
[docs]@method_decorator(login_required, 'dispatch') @method_decorator(cache_control(private=True, no_store=True), 'dispatch') class EditUser(TemplateView): """View that serves the edit form for a user's profile.""" #: The template that this view will render. template_name = 'edit_user.html'
[docs] def setup(self, request: HttpRequest, *args, **kwargs): """ Initialize attributes shared by all view methods. A :class:`~users.models.UserProfile` will be created if the request user does not yet have one. :param request: The original request. """ super().setup(request) if request.user.is_authenticated: self.profile = UserProfile.objects.defer( 'token', 'user__last_login', 'user__is_staff', 'user__date_joined', 'user__is_active', 'user__is_superuser' ).select_related('user').get_or_create( user_id=request.user.id )[0] url = request.path p_url = url.rsplit('/', 2)[0] + '/' crumbs = breadcrumbs([ ('User', request.build_absolute_uri(p_url)), ('Edit', request.build_absolute_uri(url)) ]) self.extra_context = {'breadcrumbs': crumbs}
[docs] def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: """ Handle ``GET`` requests. :param request: The original request. :return: A response with the rendered :obj:`template <EditUser.template_name>`. """ form = UserProfileForm(instance=self.profile) return self.render_to_response(self.get_context_data(form=form))
[docs] def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: """ Handle ``POST`` requests. If the user has changed their e-mail, a confirmation mail will be sent. :param request: The original request. :return: A response with the rendered :obj:`template <EditUser.template_name>`. """ form = UserProfileForm( request.POST, request.FILES, instance=self.profile ) if form.is_valid(): form.save() email = form.cleaned_data['email'] if request.user.email != email: # type: ignore EmailAddress.objects.add_email( request, request.user, email, confirm=True ) info(request, 'Please confirm your new e-mail address.') else: error(request, 'Error: please check the fields and try again.') return self.render_to_response(self.get_context_data(form=form))
[docs]@method_decorator(login_required, 'dispatch') class Logout(LogoutView): """A :class:`LogoutView` that disallows ``GET`` requests.""" #: The allowed HTTP methods. http_method_names = ('post', 'head', 'options')
[docs]@method_decorator(cache_control(private=True, no_store=True), 'dispatch') class PasswordReset(PasswordResetFromKeyView): """ A :class:`PasswordResetFromKeyView` without the extra redirect. .. seealso:: `pennersr/django-allauth#2201`__ __ https://github.com/pennersr/django-allauth/issues/2201 """ # HACK: patch PasswordResetFromKeyView.dispatch to fix #27
[docs] def dispatch(self, request: HttpRequest, uidb36: str, key: str, **kwargs) -> HttpResponse: # pragma: no cover self.request, self.key = request, key token_form = UserTokenForm(data={'uidb36': uidb36, 'key': self.key}) if token_form.is_valid(): self.reset_user = token_form.reset_user return super(FormView, self).dispatch( request, uidb36, self.key, **kwargs ) self.reset_user = None response = self.render_to_response( self.get_context_data(token_fail=True) ) return _ajax_response( self.request, response, form=token_form )
[docs]@method_decorator(login_required, 'dispatch') @method_decorator(cache_control(private=True, max_age=600), 'dispatch') class Bookmarks(TemplateView): """View that serves a user's bookmarks page.""" #: The template that this view will render. template_name = 'bookmarks.html'
[docs] def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: """ Handle ``GET`` requests. :param request: The original request. :return: A response with the rendered :obj:`template <Bookmarks.template_name>`. """ url = request.path p_url = url.rsplit('/', 2)[0] + '/' crumbs = breadcrumbs([ ('User', request.build_absolute_uri(p_url)), ('Bookmarks', request.build_absolute_uri(url)) ]) chapters = Chapter.objects.filter(series_id__in=Subquery( Bookmark.objects.filter(user_id=request.user.id).values('series') )).select_related('series').order_by('-published').only( 'title', 'volume', 'number', 'published', 'final', 'series__cover', 'series__slug', 'series__title', 'series__format' ) token = UserProfile.objects.only('token') \ .get_or_create(user_id=request.user.id)[0].token return self.render_to_response(self.get_context_data( releases=list(chapters), breadcrumbs=crumbs, token=token ))
[docs] def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: """ Handle ``POST`` requests. If a bookmark exists, it will be deleted. If not, it will be created. :param request: The original request. :return: | An empty :status:`201` response when creating a bookmark. | An empty :status:`204` response when deleting a bookmark. """ bookmark, created = Bookmark.objects.only('id').get_or_create( user_id=request.user.id, series_id=request.POST.get('series', 0) ) if not created: bookmark.delete() return HttpResponse(status=201 if created else 204)
[docs]@method_decorator(login_required, 'dispatch') @method_decorator(cache_control(private=True, no_store=True), 'dispatch') class Delete(TemplateView): """View that allows users to delete their accounts.""" #: The template that this view will render. template_name = 'delete.html'
[docs] def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: """ Handle ``GET`` requests (confirm the deactivation). :param request: The original request. :return: A response with the rendered :obj:`template <Delete.template_name>`. """ return self.render_to_response({})
[docs] def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: """ Handle ``POST`` requests (delete the account). :param request: The original request. :return: A redirect to :func:`index`. """ uid = request.user.id logout(request) UserProfile.objects.get(user_id=uid).delete() return redirect('index')
__all__ = [ 'profile', 'export', 'EditUser', 'Bookmarks', 'Logout', 'PasswordReset', 'Delete' ]