You've already forked hotpocket
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
from . import accounts # noqa: F401
|
||||
from . import associations # noqa: F401
|
||||
from . import errors # noqa: F401
|
||||
from . import imports # noqa: F401
|
||||
from . import index # noqa: F401
|
||||
from . import integrations # noqa: F401
|
||||
from . import meta # noqa: F401
|
||||
from . import saves # noqa: F401
|
||||
247
services/backend/hotpocket_backend/apps/ui/views/accounts.py
Normal file
247
services/backend/hotpocket_backend/apps/ui/views/accounts.py
Normal file
@@ -0,0 +1,247 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
from django.contrib.auth.views import LoginView as BaseLoginView
|
||||
from django.core.exceptions import PermissionDenied
|
||||
import django.db
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import FormView, RedirectView
|
||||
|
||||
from hotpocket_backend.apps.accounts.decorators import account_required
|
||||
from hotpocket_backend.apps.accounts.mixins import AccountRequiredMixin
|
||||
from hotpocket_backend.apps.core.conf import settings as django_settings
|
||||
from hotpocket_backend.apps.ui.forms.accounts import (
|
||||
FederatedPasswordForm,
|
||||
FederatedProfileForm,
|
||||
LoginForm,
|
||||
PasswordForm,
|
||||
ProfileForm,
|
||||
SettingsForm,
|
||||
)
|
||||
|
||||
|
||||
@account_required
|
||||
def index(request: HttpRequest) -> HttpResponse:
|
||||
return redirect(reverse('ui.accounts.settings'))
|
||||
|
||||
|
||||
@account_required
|
||||
def browse(request: HttpRequest) -> HttpResponse:
|
||||
raise Http404()
|
||||
|
||||
|
||||
class LoginView(BaseLoginView):
|
||||
template_name = 'ui/accounts/login.html'
|
||||
form_class = LoginForm
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
request.session['post_login_next_url'] = request.GET.get('next', None)
|
||||
request.session.save()
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('ui.accounts.post_login')
|
||||
|
||||
|
||||
class PostLoginView(RedirectView):
|
||||
def get_redirect_url(self, *args, **kwargs) -> str:
|
||||
next_url = self.request.session.pop('post_login_next_url', None)
|
||||
self.request.session.save()
|
||||
|
||||
allowed_hosts = None
|
||||
if len(django_settings.ALLOWED_HOSTS) > 0:
|
||||
allowed_hosts = set(filter(
|
||||
lambda value: value != '*',
|
||||
django_settings.ALLOWED_HOSTS,
|
||||
))
|
||||
|
||||
if next_url is not None:
|
||||
next_url_is_safe = url_has_allowed_host_and_scheme(
|
||||
url=next_url,
|
||||
allowed_hosts=allowed_hosts,
|
||||
require_https=self.request.is_secure(),
|
||||
)
|
||||
|
||||
if next_url_is_safe is False:
|
||||
next_url = None
|
||||
|
||||
return next_url or reverse('ui.index.index')
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
if request.user.is_anonymous is True:
|
||||
raise PermissionDenied('NOPE')
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
def logout(request: HttpRequest) -> HttpResponse:
|
||||
if request.user.is_authenticated is True:
|
||||
auth_logout(request)
|
||||
|
||||
return render(request, 'ui/accounts/logout.html')
|
||||
|
||||
|
||||
@account_required
|
||||
def settings(request: HttpRequest) -> HttpResponse:
|
||||
return redirect(reverse('ui.accounts.settings.profile'))
|
||||
|
||||
|
||||
class BaseSettingsView(AccountRequiredMixin, FormView):
|
||||
template_name = 'ui/accounts/settings.html'
|
||||
|
||||
@property
|
||||
def is_federated(self) -> bool:
|
||||
return all((
|
||||
self.request.user.is_anonymous is False,
|
||||
self.request.user.has_usable_password() is False,
|
||||
))
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
result = super().get_context_data(**kwargs)
|
||||
|
||||
result.update({
|
||||
'is_federated': self.is_federated,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ProfileView(BaseSettingsView):
|
||||
def get_form_class(self) -> type[ProfileForm]:
|
||||
if self.is_federated is True:
|
||||
return FederatedProfileForm
|
||||
|
||||
return ProfileForm
|
||||
|
||||
def get_initial(self) -> dict:
|
||||
result = super().get_initial()
|
||||
|
||||
result.update({
|
||||
'username': self.request.user.username,
|
||||
'first_name': self.request.user.first_name,
|
||||
'last_name': self.request.user.last_name,
|
||||
'email': self.request.user.email,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
result = super().get_context_data(**kwargs)
|
||||
|
||||
result.update({
|
||||
'title': _('Profile'),
|
||||
'active_tab': 'profile',
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def form_valid(self, form: ProfileForm) -> HttpResponse:
|
||||
assert self.is_federated is False, (
|
||||
'Refuse to save profile of a federated account: '
|
||||
f'account=`{self.request.user}`'
|
||||
)
|
||||
|
||||
with django.db.transaction.atomic():
|
||||
self.request.user.first_name = form.cleaned_data['first_name']
|
||||
self.request.user.last_name = form.cleaned_data['last_name']
|
||||
self.request.user.email = form.cleaned_data['email']
|
||||
self.request.user.save()
|
||||
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.SUCCESS,
|
||||
message=_('Your profile has been been updated!'),
|
||||
)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('ui.accounts.settings.profile')
|
||||
|
||||
|
||||
class PasswordView(BaseSettingsView):
|
||||
def get_form_class(self) -> type[PasswordForm]:
|
||||
if self.is_federated is True:
|
||||
return FederatedPasswordForm
|
||||
|
||||
return PasswordForm
|
||||
|
||||
def get_form(self,
|
||||
form_class: type[PasswordForm] | None = None,
|
||||
) -> PasswordForm:
|
||||
form_class = form_class or self.get_form_class()
|
||||
return form_class(self.request.user, **self.get_form_kwargs())
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
result = super().get_context_data(**kwargs)
|
||||
|
||||
result.update({
|
||||
'title': _('Password'),
|
||||
'active_tab': 'password',
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def form_valid(self, form: PasswordForm) -> HttpResponse:
|
||||
assert self.is_federated is False, (
|
||||
'Refuse to change password of a federated account: '
|
||||
f'account=`{self.request.user}`'
|
||||
)
|
||||
|
||||
with django.db.transaction.atomic():
|
||||
form.set_password_and_save(
|
||||
self.request.user,
|
||||
password_field_name='new_password1',
|
||||
)
|
||||
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.SUCCESS,
|
||||
message=_('Your password has been changed!'),
|
||||
)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('ui.accounts.settings.password')
|
||||
|
||||
|
||||
class SettingsView(BaseSettingsView):
|
||||
form_class = SettingsForm
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
result = super().get_context_data(**kwargs)
|
||||
|
||||
result.update({
|
||||
'title': _('Settings'),
|
||||
'active_tab': 'settings',
|
||||
'is_federated': False,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def get_initial(self) -> dict:
|
||||
return self.request.user.settings
|
||||
|
||||
def form_valid(self, form: PasswordForm) -> HttpResponse:
|
||||
with django.db.transaction.atomic():
|
||||
self.request.user.raw_settings = form.cleaned_data
|
||||
self.request.user.save()
|
||||
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.SUCCESS,
|
||||
message=_('Your settings have been updated!'),
|
||||
)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('ui.accounts.settings.settings')
|
||||
395
services/backend/hotpocket_backend/apps/ui/views/associations.py
Normal file
395
services/backend/hotpocket_backend/apps/ui/views/associations.py
Normal file
@@ -0,0 +1,395 @@
|
||||
# -*- 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,
|
||||
},
|
||||
)
|
||||
86
services/backend/hotpocket_backend/apps/ui/views/errors.py
Normal file
86
services/backend/hotpocket_backend/apps/ui/views/errors.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from django.http import (
|
||||
HttpRequest,
|
||||
HttpResponseBadRequest,
|
||||
HttpResponseForbidden,
|
||||
HttpResponseNotFound,
|
||||
HttpResponseServerError,
|
||||
)
|
||||
from django.template.loader import render_to_string
|
||||
from django.views.decorators.csrf import requires_csrf_token
|
||||
from django.views.defaults import (
|
||||
ERROR_400_TEMPLATE_NAME,
|
||||
ERROR_403_TEMPLATE_NAME,
|
||||
ERROR_404_TEMPLATE_NAME,
|
||||
ERROR_500_TEMPLATE_NAME,
|
||||
)
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@requires_csrf_token
|
||||
def page_not_found(request: HttpRequest,
|
||||
exception: Exception,
|
||||
template_name: str = ERROR_404_TEMPLATE_NAME,
|
||||
) -> HttpResponseNotFound:
|
||||
if exception:
|
||||
LOGGER.error('Exception: %s', exception, exc_info=exception)
|
||||
|
||||
return HttpResponseNotFound(render_to_string(
|
||||
'ui/errors/page_not_found.html',
|
||||
context={},
|
||||
request=request,
|
||||
))
|
||||
|
||||
|
||||
@requires_csrf_token
|
||||
def server_error(request: HttpRequest,
|
||||
template_name: str = ERROR_500_TEMPLATE_NAME,
|
||||
) -> HttpResponseServerError:
|
||||
return HttpResponseServerError(render_to_string(
|
||||
'ui/errors/internal_server_error.html',
|
||||
context={},
|
||||
request=request,
|
||||
))
|
||||
|
||||
|
||||
@requires_csrf_token
|
||||
def bad_request(request: HttpRequest,
|
||||
exception: Exception,
|
||||
template_name: str = ERROR_400_TEMPLATE_NAME,
|
||||
) -> HttpResponseBadRequest:
|
||||
if exception:
|
||||
LOGGER.error('Exception: %s', exception, exc_info=exception)
|
||||
|
||||
return HttpResponseBadRequest(render_to_string(
|
||||
'ui/errors/internal_server_error.html',
|
||||
context={},
|
||||
request=request,
|
||||
))
|
||||
|
||||
|
||||
@requires_csrf_token
|
||||
def permission_denied(request: HttpRequest,
|
||||
exception: Exception,
|
||||
template_name: str = ERROR_403_TEMPLATE_NAME,
|
||||
) -> HttpResponseForbidden:
|
||||
if exception:
|
||||
LOGGER.error('Exception: %s', exception, exc_info=exception)
|
||||
|
||||
return HttpResponseForbidden(render_to_string(
|
||||
'ui/errors/internal_server_error.html',
|
||||
context={},
|
||||
request=request,
|
||||
))
|
||||
|
||||
|
||||
def csrf_failure(request: HttpRequest, reason: str = ''):
|
||||
return HttpResponseBadRequest(render_to_string(
|
||||
'ui/errors/internal_server_error.html',
|
||||
context={},
|
||||
request=request,
|
||||
))
|
||||
52
services/backend/hotpocket_backend/apps/ui/views/imports.py
Normal file
52
services/backend/hotpocket_backend/apps/ui/views/imports.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import FormView
|
||||
|
||||
from hotpocket_backend.apps.accounts.mixins import AccountRequiredMixin
|
||||
from hotpocket_backend.apps.core.conf import settings
|
||||
from hotpocket_backend.apps.ui.forms.imports import PocketImportForm
|
||||
from hotpocket_backend.apps.ui.services import UIImportsService
|
||||
|
||||
|
||||
class PocketImportView(AccountRequiredMixin, FormView):
|
||||
template_name = 'ui/imports/pocket.html'
|
||||
form_class = PocketImportForm
|
||||
|
||||
def form_valid(self, form: PocketImportForm) -> HttpResponse:
|
||||
csv_filename = '{user_pk}-{filename}'.format(
|
||||
user_pk=self.request.user.pk,
|
||||
filename=form.cleaned_data['csv'].name,
|
||||
)
|
||||
|
||||
csv_path = settings.UPLOADS_PATH / csv_filename
|
||||
with open(csv_path, 'wb') as csv_file:
|
||||
for chunk in form.cleaned_data['csv']:
|
||||
csv_file.write(chunk)
|
||||
|
||||
result = UIImportsService().schedule_import_from_pocket(
|
||||
account_uuid=self.request.user.pk,
|
||||
csv_path=str(csv_path),
|
||||
)
|
||||
|
||||
if result.ready() is True:
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.SUCCESS,
|
||||
_('Import complete!'),
|
||||
)
|
||||
else:
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.INFO,
|
||||
_('The import has been scheduled.'),
|
||||
)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('ui.associations.index')
|
||||
12
services/backend/hotpocket_backend/apps/ui/views/index.py
Normal file
12
services/backend/hotpocket_backend/apps/ui/views/index.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request: HttpRequest) -> HttpResponse:
|
||||
return redirect(reverse('ui.associations.browse'))
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import android # noqa: F401
|
||||
from . import ios # noqa: F401
|
||||
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import http
|
||||
import logging
|
||||
|
||||
import django.db
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from hotpocket_backend.apps.accounts.decorators import account_required
|
||||
from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@account_required
|
||||
def share_sheet(request: HttpRequest) -> HttpResponse:
|
||||
LOGGER.debug('POST=`%s`', request.POST)
|
||||
|
||||
try:
|
||||
with django.db.transaction.atomic():
|
||||
assert request.user.is_anonymous is False, 'Login required'
|
||||
assert 'text' in request.POST, 'Bad request: Missing `text`'
|
||||
|
||||
url = request.POST['text'].split('\n')[0].strip()
|
||||
assert url != '', 'Bad request: Empty `text`'
|
||||
|
||||
return CreateSaveWorkflow().run(
|
||||
request=request,
|
||||
account=request.user,
|
||||
url=url,
|
||||
force_post_save=True,
|
||||
)
|
||||
except Exception as exception:
|
||||
LOGGER.error(
|
||||
'Unhandled exception: %s',
|
||||
exception,
|
||||
exc_info=exception,
|
||||
)
|
||||
|
||||
return render(
|
||||
request,
|
||||
'ui/errors/internal_server_error.html',
|
||||
# Returning 200 here to avoid browsers showing generic error pages.
|
||||
status=http.HTTPStatus.OK,
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import http
|
||||
import logging
|
||||
|
||||
import django.db
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
|
||||
from hotpocket_backend.apps.accounts.decorators import account_required
|
||||
from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@account_required
|
||||
def shortcut(request: HttpRequest) -> HttpResponse:
|
||||
LOGGER.debug('GET=`%s`', request.GET)
|
||||
|
||||
try:
|
||||
with django.db.transaction.atomic():
|
||||
assert request.user.is_anonymous is False, 'Login required'
|
||||
assert 'url' in request.GET, 'Bad request: Missing `url`'
|
||||
|
||||
url = request.GET['url'].split('\n')[0].strip()
|
||||
assert url != '', 'Bad request: Empty `url`'
|
||||
|
||||
return CreateSaveWorkflow().run(
|
||||
request=request,
|
||||
account=request.user,
|
||||
url=url,
|
||||
force_post_save=True,
|
||||
)
|
||||
except Exception as exception:
|
||||
LOGGER.error(
|
||||
'Unhandled exception: %s',
|
||||
exception,
|
||||
exc_info=exception,
|
||||
)
|
||||
|
||||
return render(
|
||||
request,
|
||||
'ui/errors/internal_server_error.html',
|
||||
# Returning 200 here to avoid browsers showing generic error pages.
|
||||
status=http.HTTPStatus.OK,
|
||||
)
|
||||
44
services/backend/hotpocket_backend/apps/ui/views/meta.py
Normal file
44
services/backend/hotpocket_backend/apps/ui/views/meta.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
from django.http import HttpRequest, JsonResponse
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
|
||||
from hotpocket_backend.apps.core.conf import settings
|
||||
|
||||
|
||||
def manifest_json(request: HttpRequest) -> JsonResponse:
|
||||
result = {
|
||||
'name': settings.SITE_TITLE,
|
||||
'short_name': settings.SITE_SHORT_TITLE,
|
||||
'start_url': reverse('ui.associations.browse'),
|
||||
'display': 'standalone',
|
||||
'background_color': '#212529',
|
||||
'theme_color': '#2b3035',
|
||||
'icons': [
|
||||
{
|
||||
'src': static('ui/img/icon-192.png'),
|
||||
'sizes': '192x192',
|
||||
'type': 'image/png',
|
||||
},
|
||||
{
|
||||
'src': static('ui/img/icon-512.png'),
|
||||
'sizes': '512x512',
|
||||
'type': 'image/png',
|
||||
},
|
||||
],
|
||||
'share_target': {
|
||||
'action': reverse('ui.integrations.android.share_sheet'),
|
||||
'method': 'POST',
|
||||
'enctype': 'multipart/form-data',
|
||||
'params': {
|
||||
'title': 'title',
|
||||
'text': 'text',
|
||||
'url': 'url',
|
||||
'files': [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return JsonResponse(result)
|
||||
78
services/backend/hotpocket_backend/apps/ui/views/saves.py
Normal file
78
services/backend/hotpocket_backend/apps/ui/views/saves.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import annotations
|
||||
|
||||
import http
|
||||
import logging
|
||||
|
||||
import django.db
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.views.generic import FormView
|
||||
from django_htmx.http import trigger_client_event
|
||||
|
||||
from hotpocket_backend.apps.accounts.mixins import AccountRequiredMixin
|
||||
from hotpocket_backend.apps.ui.forms.saves import CreateForm
|
||||
from hotpocket_backend.apps.ui.services import UISavesService
|
||||
from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CreateView(AccountRequiredMixin, FormView):
|
||||
template_name = 'ui/saves/create.html'
|
||||
form_class = CreateForm
|
||||
|
||||
def form_valid(self, form: CreateForm) -> HttpResponse:
|
||||
with django.db.transaction.atomic():
|
||||
return CreateSaveWorkflow().run(
|
||||
request=self.request,
|
||||
account=self.request.user,
|
||||
url=form.cleaned_data['url'],
|
||||
)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('ui.associations.browse')
|
||||
|
||||
|
||||
def embed(request: HttpRequest) -> HttpResponse:
|
||||
try:
|
||||
assert request.method == http.HTTPMethod.POST, (
|
||||
f'405 Method Not Allowed: `{request.method}`'
|
||||
)
|
||||
assert 'pk' in request.POST, (
|
||||
'400 Bad Request: `pk` missing'
|
||||
)
|
||||
assert request.htmx, (
|
||||
'400 Bad Reqquest: Not an HTMX request'
|
||||
)
|
||||
except AssertionError as exception:
|
||||
LOGGER.error(
|
||||
'Unable to render save embed: pk=`pk`: %s',
|
||||
exception,
|
||||
exc_info=exception,
|
||||
)
|
||||
return HttpResponse(
|
||||
b'NOPE',
|
||||
status=http.HTTPStatus.BAD_REQUEST,
|
||||
content_type='text/plain;charset=utf-8',
|
||||
)
|
||||
|
||||
save = UISavesService().get_or_404(pk=request.POST['pk'])
|
||||
|
||||
response = render(
|
||||
request,
|
||||
'ui/saves/partials/embed.html',
|
||||
{
|
||||
'save': save,
|
||||
},
|
||||
)
|
||||
|
||||
trigger_client_event(
|
||||
response,
|
||||
'HotPocket:ViewAssociationView:loadEmbed',
|
||||
{},
|
||||
after='swap',
|
||||
)
|
||||
|
||||
return response
|
||||
Reference in New Issue
Block a user