Skip to content

Commit

Permalink
feat: contact form
Browse files Browse the repository at this point in the history
  • Loading branch information
japsu committed Sep 10, 2024
1 parent aa38f57 commit a6f55df
Show file tree
Hide file tree
Showing 20 changed files with 758 additions and 309 deletions.
5 changes: 2 additions & 3 deletions backend/edegal/models/album.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
import os
import re
from datetime import date
from typing import Any
from typing import Any, Self
from zipfile import ZipFile

from django.conf import settings
from django.db import models
from django.db.models import F
from django.shortcuts import get_object_or_404
from django_prose_editor.fields import ProseEditorField
from mptt.models import MPTTModel, TreeForeignKey

from ..utils import pick_attrs, slugify, strip_photographer_name_from_title
Expand Down Expand Up @@ -505,7 +504,7 @@ def _update_family(self, path_changed):
album.save(traverse=False)

@classmethod
def get_album_by_path(cls, path, or_404=False, **extra_criteria):
def get_album_by_path(cls, path, or_404=False, **extra_criteria) -> Series | Self:
# Is it a Series?
try:
return Series.objects.get(path=path)
Expand Down
5 changes: 5 additions & 0 deletions backend/edegal/models/photographer.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def make_credit(self, include_larppikuvat_profile=False, **extra_attrs):
"instagram_handle",
"facebook_handle",
"flickr_handle",
"has_email",
**extra_attrs,
)

Expand Down Expand Up @@ -159,5 +160,9 @@ def title(self):
def is_public(self):
return self.cover_picture_id is not None

@property
def has_email(self):
return bool(self.email)

class Meta:
ordering = ("display_name",)
15 changes: 15 additions & 0 deletions backend/edegal/templates/contact_email.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Someone has reached out to you through the contact form on {{ site_name }}.
If you reply to this email, your response will be sent to the sender.

Context (album or picture):
{{ context }}

Sender email:
{{ email }}

Subject:
{{ subject }}

Message:

{{ message }}
34 changes: 27 additions & 7 deletions backend/edegal/urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@

from .views import api_v3_view, status_view, photographers_api_v3_view, photographer_api_v3_view, random_picture_api_v3_view
from django.urls import re_path

from .views import (
api_v3_view,
contact_view,
photographer_api_v3_view,
photographers_api_v3_view,
random_picture_api_v3_view,
status_view,
)

urlpatterns = [
re_path(r'^api/v3/status/?$', status_view, name='status_view'),
re_path(r'^api/v3/photographers/?$', photographers_api_v3_view, name='photographers_api_v3_view'),
re_path(r'^api/v3/photographers/(?P<photographer_slug>[a-z0-9-]+?)/?$', photographer_api_v3_view, name='photographer_api_v3_view'),
re_path(r'^api/v3/random/?$', random_picture_api_v3_view, name='random_picture_api_v3_view'),
re_path(r'^api/v3(?P<path>[a-z0-9/-]+?)/?$', api_v3_view, name='api_v3_view'),
re_path(r"^api/v3/status/?$", status_view, name="status_view"),
re_path(r"^api/v3/contact/?$", contact_view, name="contact_view"),
re_path(
r"^api/v3/photographers/?$",
photographers_api_v3_view,
name="photographers_api_v3_view",
),
re_path(
r"^api/v3/photographers/(?P<photographer_slug>[a-z0-9-]+?)/?$",
photographer_api_v3_view,
name="photographer_api_v3_view",
),
re_path(
r"^api/v3/random/?$",
random_picture_api_v3_view,
name="random_picture_api_v3_view",
),
re_path(r"^api/v3(?P<path>[a-z0-9/-]+?)/?$", api_v3_view, name="api_v3_view"),
]
100 changes: 97 additions & 3 deletions backend/edegal/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
import logging
from typing import ClassVar, Literal

import pydantic
from django.conf import settings
from django.core.mail import EmailMessage
from django.http import HttpRequest, JsonResponse
from django.template.loader import render_to_string
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import View

from .models import Album, Photographer, Picture
from .models import Album, Photographer, Picture, Series
from .models.media_spec import FORMAT_CHOICES

SUPPORTED_FORMATS = {format for (format, disp) in FORMAT_CHOICES}
logger = logging.getLogger(__name__)


class StatusView(View):
Expand Down Expand Up @@ -126,8 +134,94 @@ def get(self, request):
return response


class ContactRequest(pydantic.BaseModel):
context: str = pydantic.Field(min_length=1)
email: pydantic.EmailStr
subject: Literal["permission", "takedown", "other"]
message: str = pydantic.Field(min_length=1)

subject_map: ClassVar[dict[str, str]] = dict(
permission="Usage permission inquiry",
takedown="Takedown request",
other="Contact request",
)

def send(self, request: HttpRequest | None = None):
album = Album.get_album_by_path(self.context)

if isinstance(album, Series):
# fmh
raise Album.DoesNotExist()

if not album.photographer or not album.photographer.has_email:
raise Photographer.DoesNotExist()

site_name = Album.objects.get(path="/").title
subject_display = self.subject_map.get(self.subject, self.subject_map["other"])
context_display = f"{settings.EDEGAL_FRONTEND_URL}{self.context}"

vars = dict(
site_name=site_name,
context=context_display,
email=self.email,
subject=subject_display,
message=self.message,
)

body = render_to_string("contact_email.txt", vars, request)

if settings.DEBUG:
print(body)

# send email to album.photographer.email
EmailMessage(
subject=f"[{site_name}] {subject_display} ({self.context})",
body=body,
reply_to=[self.email],
to=[album.photographer.email],
).send()


class ContactView(View):
http_method_names = ["post"]

def post(self, request):
try:
contact_request = ContactRequest.model_validate_json(request.body)
except pydantic.ValidationError as e:
logger.error("Invalid contact request (failed to validate)", exc_info=e)
return JsonResponse(
{"status": 400, "message": "Invalid request"},
status=400,
)

try:
contact_request.send(request)
except Album.DoesNotExist as e:
logger.error(
"Invalid contact request (context refers to a nonexistent picture or album)",
exc_info=e,
)
return JsonResponse(
{"status": 400, "message": "Invalid request"},
status=400,
)
except Photographer.DoesNotExist as e:
logger.error(
"Invalid contact request (album has no photographer or photographer has no email)",
exc_info=e,
)
return JsonResponse(
{"status": 400, "message": "Invalid request"},
status=400,
)

return JsonResponse({"status": 200, "message": "OK"})


api_v3_view = ApiV3View.as_view()
photographers_api_v3_view = PhotographersApiV3View.as_view()
photographer_api_v3_view = PhotographerApiV3View.as_view()
random_picture_api_v3_view = RandomPictureAPIV3View.as_view()
status_view = StatusView.as_view()
contact_view = csrf_exempt(ContactView.as_view())
3 changes: 2 additions & 1 deletion backend/requirements.in
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
git+https://github.com/con2/django-admin-multiupload.git@django3#egg=admin-multiupload
beautifulsoup4
celery
Django
django-environ
django-mptt
django-prose-editor[sanitize]
django-redis
git+https://github.com/con2/django-admin-multiupload.git@django3#egg=admin-multiupload
gunicorn
Pillow
pillow-avif-plugin
pip-tools
psycopg2
pydantic[email]
python-memcached
redis
requests
Expand Down
36 changes: 27 additions & 9 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ admin-multiupload @ git+https://github.com/con2/django-admin-multiupload.git@b70
# via -r requirements.in
amqp==5.2.0
# via kombu
annotated-types==0.7.0
# via pydantic
asgiref==3.8.1
# via django
beautifulsoup4==4.12.3
# via -r requirements.in
billiard==4.2.0
# via celery
build==1.2.1
build==1.2.2
# via pip-tools
celery==5.4.0
# via -r requirements.in
certifi==2024.7.4
certifi==2024.8.30
# via requests
charset-normalizer==3.3.2
# via requests
Expand All @@ -31,7 +33,7 @@ click-plugins==1.1.1
# via celery
click-repl==0.3.0
# via celery
django==5.1
django==5.1.1
# via
# -r requirements.in
# django-js-asset
Expand All @@ -40,17 +42,25 @@ django==5.1
django-environ==0.11.2
# via -r requirements.in
django-js-asset==2.2.0
# via django-mptt
# via
# django-mptt
# django-prose-editor
django-mptt==0.16.0
# via -r requirements.in
django-prose-editor==0.7.1
django-prose-editor==0.8.0
# via -r requirements.in
django-redis==5.4.0
# via -r requirements.in
dnspython==2.6.1
# via email-validator
email-validator==2.2.0
# via pydantic
gunicorn==23.0.0
# via -r requirements.in
idna==3.7
# via requests
idna==3.8
# via
# email-validator
# requests
kombu==5.4.0
# via celery
nh3==0.2.18
Expand All @@ -73,6 +83,10 @@ prompt-toolkit==3.0.47
# via click-repl
psycopg2==2.9.9
# via -r requirements.in
pydantic==2.9.1
# via -r requirements.in
pydantic-core==2.23.3
# via pydantic
pyproject-hooks==1.1.0
# via
# build
Expand All @@ -91,14 +105,18 @@ requests==2.32.3
# requests-oauthlib
requests-oauthlib==2.0.0
# via -r requirements.in
setuptools==72.1.0
setuptools==74.1.2
# via pip-tools
six==1.16.0
# via python-dateutil
soupsieve==2.5
soupsieve==2.6
# via beautifulsoup4
sqlparse==0.5.1
# via django
typing-extensions==4.12.2
# via
# pydantic
# pydantic-core
tzdata==2024.1
# via celery
urllib3==2.2.2
Expand Down
Loading

0 comments on commit a6f55df

Please sign in to comment.