"""The views of the users app."""
from __future__ import annotations
from typing import TYPE_CHECKING
from django.contrib.auth.decorators import login_required
from django.contrib.messages import error, info
from django.db import IntegrityError
from django.db.models import Subquery
from django.http import Http404, HttpResponse
from django.shortcuts import 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]@method_decorator(login_required, name='dispatch')
@method_decorator(cache_control(no_store=True), name='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, name='dispatch')
@method_decorator(cache_control(no_store=True), name='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(no_store=True), name='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 = request
self.key = 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, name='dispatch')
@method_decorator(cache_control(private=True, max_age=600), name='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 <EditUser.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)
__all__ = ['profile', 'EditUser', 'Bookmarks', 'Logout', 'PasswordReset']