hotpocket/services/backend/hotpocket_backend/apps/ui/views/associations.py
Tomek Wójcik b4338e2769
Some checks failed
CI / Checks (push) Failing after 13m2s
Release v1.0.0
2025-08-20 21:00:50 +02:00

396 lines
11 KiB
Python

# -*- 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,
},
)