# -*- coding: utf-8 -*- from __future__ import annotations import logging import uuid from django.contrib import messages import django.db from django.http import HttpRequest, HttpResponse, JsonResponse from django.shortcuts import redirect, render from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.generic import FormView, View from django_htmx.http import trigger_client_event from hotpocket_backend.apps.accounts.decorators import account_required from hotpocket_backend.apps.accounts.mixins import AccountRequiredMixin from hotpocket_backend.apps.htmx import messages as htmx_messages from hotpocket_backend.apps.ui.constants import StarUnstarAssociationViewMode from hotpocket_backend.apps.ui.dto.saves import BrowseParams from hotpocket_backend.apps.ui.forms.associations import ( ArchiveForm, DeleteForm, EditForm, RefreshForm, ) from hotpocket_backend.apps.ui.services import UIAssociationsService from hotpocket_common.constants import NULL_UUID, AssociationsSearchMode from hotpocket_soa.dto.associations import ( AssociationOut, AssociationsQuery, AssociationUpdateIn, AssociationWithTargetOut, ) from hotpocket_soa.services import AssociationsService, SaveProcessorService LOGGER = logging.getLogger(__name__) class AssociationMixin: ALLOW_ARCHIVED_ASSOCIATION = False def get_association(self, with_target: bool = False, ) -> AssociationOut | AssociationWithTargetOut: attribute = '_instance' if with_target is True: attribute = '_instance_with_target' if hasattr(self, attribute) is False: setattr( self, attribute, UIAssociationsService().get_or_404( account_uuid=self.request.user.pk, # type: ignore[attr-defined] pk=self.kwargs['pk'], # type: ignore[attr-defined] with_target=with_target, allow_archived=self.ALLOW_ARCHIVED_ASSOCIATION, ), ) return getattr(self, attribute) class DetailView(AssociationMixin, AccountRequiredMixin, FormView): def get_context_data(self, **kwargs) -> dict: result = super().get_context_data(**kwargs) result.update({ 'association': self.get_association(), }) return result class ConfirmationView(DetailView): def get_initial(self) -> dict: result = super().get_initial() association: AssociationWithTargetOut = self.get_association( # type: ignore[assignment] with_target=True, ) result.update({ 'canhazconfirm': 'hai', 'title': association.title, 'url': association.target.url, }) return result def get_success_url(self) -> str: return reverse('ui.associations.browse') @account_required def index(request: HttpRequest) -> HttpResponse: return redirect(reverse('ui.associations.browse')) @account_required def browse(request: HttpRequest) -> HttpResponse: params = BrowseParams.from_request(request=request) associations = AssociationsService().search( query=AssociationsQuery.model_validate(dict( account_uuid=request.user.pk, before=params.before, search=params.search or None, mode=params.mode, )), limit=params.limit, ) before: uuid.UUID | None = None if len(associations) == params.limit: before = associations[-1].pk next_url: str | None = None if before is not None: next_url = reverse('ui.associations.browse', query=[ ('before', before), ('search', params.search or ''), ('limit', params.limit), ('mode', params.mode.value), ]) context = { 'associations': associations, 'params': params, 'before': before, 'next_url': next_url, } if request.htmx: response = render( request, 'ui/associations/partials/associations.html', context, ) return trigger_client_event( response, 'HotPocket:BrowseSavesView:updateLoadMoreButton', {'next_url': next_url}, after='swap', ) title: str match params.mode: case AssociationsSearchMode.STARRED: title = _('Starred') case AssociationsSearchMode.ARCHIVED: title = _('Archived') case _: title = 'HotPocket' context['title'] = title return render( request, 'ui/associations/browse.html', context, ) def view(request: HttpRequest, pk: uuid.UUID) -> HttpResponse: account_uuid: uuid.UUID | None = None allow_archived = False is_share = request.GET.get('share', None) is not None if request.user.is_anonymous is True or request.user.is_active is False: if is_share is True: account_uuid = None else: account_uuid = NULL_UUID else: if is_share is False: account_uuid = request.user.pk allow_archived = True association = UIAssociationsService().get_or_404( account_uuid=account_uuid, pk=pk, with_target=True, allow_archived=allow_archived, ) show_controls = True if association.archived_at is not None: show_controls = show_controls and False if request.user.is_anonymous is True: show_controls = show_controls and False else: if is_share is True: show_controls = show_controls and False return render( request, 'ui/associations/view.html', { 'association': association, 'show_controls': show_controls, }, ) class EditView(DetailView): template_name = 'ui/associations/edit.html' form_class = EditForm def get_initial(self) -> dict: result = super().get_initial() association = self.get_association(with_target=True) result.update({ 'url': association.target.url, # type: ignore[attr-defined] 'target_title': association.get_title(), 'target_description': association.get_description(), }) return result def form_valid(self, form: EditForm) -> HttpResponse: with django.db.transaction.atomic(): updated_association = AssociationsService().update( # noqa: F841 association=self.get_association(), update=AssociationUpdateIn.model_validate(form.cleaned_data), ) messages.add_message( self.request, messages.SUCCESS, _('The save has been updated!'), ) return super().form_valid(form) def get_success_url(self) -> str: return reverse( 'ui.associations.view', args=(self.get_association(with_target=True).pk,), ) class StarUnstarView(AssociationMixin, AccountRequiredMixin, View): mode = StarUnstarAssociationViewMode.STAR def get(self, request: HttpRequest, pk: uuid.UUID) -> HttpResponse: with django.db.transaction.atomic(): association = self.get_association() match self.mode: case StarUnstarAssociationViewMode.STAR: _ = AssociationsService().star(association=association) case StarUnstarAssociationViewMode.UNSTAR: _ = AssociationsService().unstar(association=association) case _: raise ValueError(f'Unknown mode: {self.mode.value}') if request.htmx: return render( request, 'ui/associations/partials/association_card.html', { 'association': self.get_association(with_target=True), }, ) return redirect('ui.associations.browse') @account_required def post_save(request: HttpRequest, pk: uuid.UUID) -> HttpResponse: association = UIAssociationsService().get_or_404( account_uuid=request.user.pk, pk=pk, with_target=True, ) return render( request, 'ui/associations/post_save.html', { 'association': association, }, ) class RefreshView(ConfirmationView): template_name = 'ui/associations/refresh.html' form_class = RefreshForm def form_valid(self, form: RefreshForm) -> HttpResponse: result = SaveProcessorService().schedule_process_save( save=self.get_association(with_target=True).target, # type: ignore[attr-defined] ) if self.request.htmx: response = JsonResponse({ 'status': 'ok', 'result': result.id, }) htmx_messages.add_htmx_message( request=self.request, response=response, level=htmx_messages.SUCCESS, message=_('The save has been refreshed!'), ) return response else: messages.add_message( self.request, messages.SUCCESS, _('The save has been refreshed!'), ) return super().form_valid(form) class ArchiveView(ConfirmationView): template_name = 'ui/associations/archive.html' form_class = ArchiveForm def form_valid(self, form: ArchiveView) -> HttpResponse: with django.db.transaction.atomic(): result = AssociationsService().archive( association=self.get_association(), ) if self.request.htmx: return JsonResponse({ 'status': 'ok', 'result': result, }) messages.add_message( self.request, messages.SUCCESS, _('The save has been archived.'), ) return super().form_valid(form) class DeleteView(ConfirmationView): template_name = 'ui/associations/delete.html' form_class = DeleteForm ALLOW_ARCHIVED_ASSOCIATION = True def form_valid(self, form: DeleteForm) -> HttpResponse: with django.db.transaction.atomic(): result = AssociationsService().delete( association=self.get_association(), ) if self.request.htmx: response = JsonResponse({ 'status': 'ok', 'result': result, }) htmx_messages.add_htmx_message( request=self.request, response=response, level=htmx_messages.SUCCESS, message=_('The save has been deleted.'), ) return response messages.add_message( self.request, messages.SUCCESS, _('The save has been deleted.'), ) return super().form_valid(form) def get_success_url(self) -> str: return reverse( 'ui.associations.browse', query={ 'mode': AssociationsSearchMode.ARCHIVED.value, }, )