initial commit

This commit is contained in:
m5ka 2024-03-25 10:36:10 +00:00
commit c9f8c48664
38 changed files with 2017 additions and 0 deletions

24
.env.example Normal file
View file

@ -0,0 +1,24 @@
DEBUG=true
DATABASE_URL="postgres://localhost:5432/moku"
SECRET_KEY="moku-keyboard-cat"
RECAPTCHA_PUBLIC_KEY=""
RECAPTCHA_PRIVATE_KEY=""
# The following are defaults.
# Uncomment and set a value to use a different value.
# DEBUG_TOOLBAR=true
# ALLOWED_HOSTS=host.example.com,host.example.org
# CSRF_TRUSTED_ORIGINS=
# STATIC_URL="static/"
# MEDIA_URL="media/"
# EMAIL_FROM='"Sender" <sender@example.com>'
# EMAIL_HOST="localhost"
# EMAIL_PORT=25
# EMAIL_HOST_USER=""
# EMAIL_HOST_PASSWORD=""
# EMAIL_USE_TLS=false
# EMAIL_USE_SSL=false
# EMAIL_TIMEOUT=3
# USERNAME_MIN_LENGTH=3
# USERNAME_MAX_LENGTH=36
# SITE_ROOT_URL="https://moku.blog"

78
.gitignore vendored Normal file
View file

@ -0,0 +1,78 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
*.manifest
*.spec
pip-log.txt
pip-delete-this-directory.txt
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
*.mo
*.pot
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
instance/
.webassets-cache
.scrapy
docs/_build/
.pybuilder/
target/
.ipynb_checkpoints
profile_default/
ipython_config.py
.python-version
.pdm.toml
__pypackages__/
celerybeat-schedule
celerybeat.pid
*.sage.py
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
.spyderproject
.spyproject
.ropeproject
/site
.mypy_cache/
.dmypy.json
dmypy.json
.pyre/
.pytype/
cython_debug/
.idea/

23
manage.py Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "moku.config.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

1
moku/__init__.py Normal file
View file

@ -0,0 +1 @@
default_app_config = "moku.config.apps.MokuConfig"

0
moku/config/__init__.py Normal file
View file

7
moku/config/apps.py Normal file
View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
class MokuConfig(AppConfig):
name = "moku"
label = "moku"
verbose_name = "moku.blog"

16
moku/config/asgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
ASGI config for moku project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "moku.settings")
application = get_asgi_application()

179
moku/config/settings.py Normal file
View file

@ -0,0 +1,179 @@
import sys
from pathlib import Path
import environ
# Initial environment
env = environ.Env()
BASE_DIR = Path(__file__).resolve().parent.parent.parent
env.read_env(BASE_DIR / ".env")
# Debug
DEBUG = env.bool("DEBUG", default=False)
# Secret key
SECRET_KEY = env.str("SECRET_KEY", default="insecure-keyboard-cat-abcd1234")
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_jinja",
"django_jinja.contrib._humanize",
"django_recaptcha",
"moku",
]
# Middleware
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
# Debug toolbar
# https://django-debug-toolbar.readthedocs.io/en/latest/
if DEBUG and env.bool("DEBUG_TOOLBAR", default=True):
try:
import debug_toolbar # noqa: F401
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE
DEBUG_TOOLBAR = True
except ImportError:
DEBUG_TOOLBAR = False
else:
DEBUG_TOOLBAR = False
ROOT_URLCONF = "moku.config.urls"
TEMPLATES = [
{
"BACKEND": "django_jinja.backend.Jinja2",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.debug",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages",
],
"filters": {
"unemoji": "moku.utils.unemoji",
},
"policies": {"ext.i18n.trimmed": True},
},
},
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]
},
},
]
WSGI_APPLICATION = "moku.config.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
"default": env.db(
"DATABASE_URL", engine="postgres", default="postgres://localhost:5432/moku"
)
}
# Authentication models
# https://docs.djangoproject.com/en/5.0/topics/auth/customizing/
AUTH_USER_MODEL = "moku.User"
LOGIN_URL = "login"
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": (
"django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
)
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internal IP addresses
# https://docs.djangoproject.com/en/5.0/ref/settings/
INTERNAL_IPS = ["localhost", "127.0.0.1"]
# Allowed hosts
# https://docs.djangoproject.com/en/5.0/ref/settings/#allowed-hosts
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", cast=str, default=[])
# CSRF trusted origins
# https://docs.djangoproject.com/en/5.0/ref/settings/#csrf-trusted-origins
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", cast=str, default=[])
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = "en-gb"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = env.str("STATIC_URL", default="static/")
STATIC_ROOT = BASE_DIR / "moku/static"
# User-uploaded content
# https://docs.djangoproject.com/en/5.0/ref/settings/#media-root
MEDIA_URL = env.str("MEDIA_URL", default="media/")
MEDIA_ROOT = BASE_DIR / "media"
# Email settings
# https://docs.djangoproject.com/en/5.0/topics/email/
EMAIL_FROM = env.str("EMAIL_FROM", default='"Sender" <sender@example.com>')
EMAIL_HOST = env.str("EMAIL_HOST", default="localhost")
EMAIL_PORT = env.int("EMAIL_PORT", default=25)
EMAIL_HOST_USER = env.str("EMAIL_HOST_USER", default="")
EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD", default="")
EMAIL_USE_TLS = env.bool("EMAIL_USE_TLS", default=False)
EMAIL_USE_SSL = env.bool("EMAIL_USE_SSL", default=False)
EMAIL_TIMEOUT = env.int("EMAIL_TIMEOUT", default=3)
# Username length
USERNAME_MIN_LENGTH = 3
USERNAME_MAX_LENGTH = 24
# URL configuration
SITE_ROOT_URL = env.str("SITE_ROOT_URL", default="https://moku.blog")
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Recaptcha settings
# https://pypi.org/project/django-recaptcha/#installation
if DEBUG:
SILENCED_SYSTEM_CHECKS = ["django_recaptcha.recaptcha_test_key_error"]
else:
RECAPTCHA_PUBLIC_KEY = env.str("RECAPTCHA_PUBLIC_KEY")
RECAPTCHA_PRIVATE_KEY = env.str("RECAPTCHA_PRIVATE_KEY")

40
moku/config/urls.py Normal file
View file

@ -0,0 +1,40 @@
"""
URL configuration for moku project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from moku.views.auth import LoginView, LogoutView
from moku.views.post import FeedView
from moku.views.user import EditProfileView, ProfileView, SignupView
urlpatterns = [
path("admin/", admin.site.urls),
path("", FeedView.as_view(), name="feed"),
path("login", LoginView.as_view(), name="login"),
path("logout", LogoutView.as_view(), name="logout"),
path("signup", SignupView.as_view(), name="signup"),
path("profile", EditProfileView.as_view(), name="profile.edit"),
path("user/<str:username>", ProfileView.as_view(), name="profile"),
]
if settings.DEBUG_TOOLBAR:
urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

16
moku/config/wsgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
WSGI config for moku project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "moku.settings")
application = get_wsgi_application()

72
moku/constants.py Normal file
View file

@ -0,0 +1,72 @@
from django.utils.translation import gettext_lazy as _
class Verbs:
ATE = "ate"
MADE = "made"
COOKED = "cooked"
BAKED = "baked"
ORDERED = "ordered"
CHOICES = (
(ATE, _("%(user)s ate %(food)s")),
(MADE, _("%(user)s made %(food)s")),
(COOKED, _("%(user)s cooked %(food)s")),
(BAKED, _("%(user)s baked %(food)s")),
(ORDERED, _("%(user)s ordered %(food)s")),
)
EMOJI_CATEGORIES = [
(
_("fruit & veg"),
(
"🍏", "🍎", "🍐", "🍊", "🍋", "🍋‍🟩", "🍌", "🍉", "🍇", "🍓", "🫐", "🍈", "🍒", "🍑", "🥭",
"🍍", "🥥", "🥝", "🍅", "🍆", "🥑", "🫛", "🥦", "🥬", "🥒", "🌶️", "🫑", "🌽", "🥕", "🫒",
"🧄", "🧅", "🥔", "🍠", "🫚",
),
),
(
_("savoury dishes"),
(
"🥯", "🍞", "🥖", "🥨", "🧀", "🥚", "🍳", "🧈", "🥞", "🧇", "🥓", "🥩", "🍗", "🍖", "🦴",
"🌭", "🍔", "🍟", "🍕", "🫓", "🥪", "🥙", "🧆", "🌮", "🌯", "🫔", "🥗", "🥘", "🫕", "🥫",
"🫙", "🍝", "🍜", "🍲", "🍛", "🍣", "🍱", "🥟", "🦪", "🍤", "🍙", "🍚", "🍘", "🍥", "🌰",
"🥜", "🫘", "🧊",
),
),
(
_("sweet treats"),
(
"🥠", "🥮", "🍢", "🍡", "🍧", "🍨", "🍦", "🥧", "🧁", "🍰", "🎂", "🍮", "🍭", "🍬", "🍫",
"🥐", "🍿", "🍩", "🍪", "🍯",
),
),
(
_("drinks"),
(
"🥛", "🫗", "🍼", "🫖", "☕️", "🍵", "🧃", "🥤", "🧋", "🍶", "🍺", "🍻", "🥂", "🍷", "🥃",
"🍸", "🍹", "🧉", "🍾",
),
),
(
_("people"),
(
"😀", "😃", "😄", "😁", "😆", "🥹", "😅", "😂", "🤣", "🥲", "😊", "😇", "🙂", "🙃", "😉",
"😌", "😌", "😍", "🥰", "😘", "😗", "😙", "😚", "😋", "😛", "😝", "😜", "🤪", "🤨", "🧐",
),
),
(
_("animals"),
(
"🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐨", "🐯", "🦁", "🐮", "🐷", "🐽", "🐸",
"🐵", "🙈", "🙉", "🙊", "🐒", "🐔", "🐧", "🐦", "🐤", "🐣", "🐥", "🪿", "🦆", "🐦‍⬛", "🦅",
"🦉", "🦇", "🐺", "🐗", "🐴", "🦄", "🫎", "🐝", "🪱", "🐛", "🦋", "🐌", "🐞", "🐜", "🪰",
),
),
(
_("tools & things"),
(
"🥄", "🍴", "🍽️", "🥣", "🥡", "🥢", "🧂", "🔪", "🪓",
)
)
]

0
moku/forms/__init__.py Normal file
View file

16
moku/forms/post.py Normal file
View file

@ -0,0 +1,16 @@
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from moku.models import Post
class PostForm(ModelForm):
class Meta:
model = Post
fields = ("emoji", "verb", "food", "image")
labels = {
"emoji": _("emoji"),
"verb": _("verb"),
"food": _("food"),
"image": _("image"),
}

51
moku/forms/user.py Normal file
View file

@ -0,0 +1,51 @@
from django import forms
from django.contrib.auth import password_validation
from django.contrib.auth.forms import UserCreationForm
from django.utils.translation import gettext_lazy as _
from django_recaptcha.fields import ReCaptchaField
from moku.models.user import User
class UserForm(UserCreationForm):
password1 = forms.CharField(
label=_("password"),
strip=False,
widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
help_text=_("make a secure password that you've never used before!"),
)
password2 = forms.CharField(
label=_("password (again)"),
widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
strip=False,
help_text=_("just type the password again to confirm."),
)
captcha = ReCaptchaField(required=True)
class Meta(UserCreationForm.Meta):
model = User
fields = ("username", "email")
labels = {
"username": _("username"),
"email": _("email address"),
}
help_texts = {
"username": User._meta.get_field("username").help_text,
"email": User._meta.get_field("email").help_text,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["captcha"].error_messages = {"required": _("make sure you've ticked the captcha.")}
class ProfileForm(forms.ModelForm):
class Meta:
model = User
fields = ("pronouns", "location", "bio")
labels = {
"pronouns": _("pronouns"),
"location": _("location"),
"bio": _("about me"),
}

View file

@ -0,0 +1,49 @@
# Generated by Django 5.0.3 on 2024-03-24 17:24
import django.contrib.auth.models
import django.core.validators
import django.utils.timezone
import moku.validators
import re
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('username', models.CharField(db_index=True, help_text="this is the unique identifier you'll use to log in. it may only contain letters, numbers, hyphens, dashes and dots.", max_length=64, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[a-zA-Z0-9-_.]+\\Z'), 'Username may only contain letters, numbers, hyphens, underscores and dots.', 'invalid'), moku.validators.validate_username_length], verbose_name='username')),
('email', models.EmailField(help_text="this should be your email address. make sure it's valid and that you have access to it.", max_length=128, unique=True, verbose_name='email address')),
('email_confirmed_at', models.DateTimeField(blank=True, null=True)),
('pronouns', models.CharField(blank=True, help_text='what pronouns should people use when referring to you?', max_length=64, verbose_name='pronouns')),
('location', models.CharField(blank=True, help_text='where in the world are you?', max_length=64, verbose_name='location')),
('bio', models.TextField(blank=True, help_text='write something about yourself!', verbose_name='about me')),
('last_seen_at', models.DateTimeField(blank=True, help_text='the last time this user accessed the site.', null=True, verbose_name='last seen at')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 5.0.3 on 2024-03-25 10:04
import django.db.models.deletion
import moku.models.post
import moku.validators
import shortuuid.django_fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('moku', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Post',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', shortuuid.django_fields.ShortUUIDField(alphabet=None, help_text='the unique id that identifies this post.', length=22, max_length=22, prefix='', verbose_name='unique id')),
('emoji', models.CharField(help_text='an emoji to accompany your post!', max_length=8, validators=[moku.validators.validate_emoji], verbose_name='emoji')),
('verb', models.CharField(choices=[('ate', '%(user)s ate %(food)s'), ('made', '%(user)s made %(food)s'), ('cooked', '%(user)s cooked %(food)s'), ('baked', '%(user)s baked %(food)s'), ('ordered', '%(user)s ordered %(food)s')], help_text='how should we best phrase this entry?', max_length=32, verbose_name='verb')),
('food', models.CharField(help_text='what did you eat?', max_length=128, verbose_name='food')),
('image', models.ImageField(blank=True, help_text='here you can upload a picture of what you ate!', upload_to=moku.models.post.post_image_filename, verbose_name='image')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='when this post was created.')),
('updated_at', models.DateTimeField(auto_now=True, help_text='when this post was last updated.')),
('created_by', models.ForeignKey(db_column='created_by_user_id', on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL)),
],
),
]

View file

8
moku/models/__init__.py Normal file
View file

@ -0,0 +1,8 @@
from moku.models.post import Post
from moku.models.user import User
__all__ = [
"Post",
"User",
]

76
moku/models/post.py Normal file
View file

@ -0,0 +1,76 @@
from django.db import models
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _
from shortuuid.django_fields import ShortUUIDField
from moku.constants import Verbs
from moku.utils import process_image
from moku.validators import validate_emoji
def post_image_filename(instance, filename):
fn = filename.split(".")
ext = "png" if len(fn) < 2 else fn[-1]
return f"posts/{instance.created_by.username}__{instance.uuid}.{ext}"
class Post(models.Model):
uuid = ShortUUIDField(
verbose_name=_("unique id"),
max_length=22,
length=22,
help_text=_("the unique id that identifies this post."),
)
emoji = models.CharField(
verbose_name=_("emoji"),
max_length=8,
validators=[validate_emoji],
help_text=_("an emoji to accompany your post!"),
)
verb = models.CharField(
verbose_name=_("verb"),
max_length=32,
choices=Verbs.CHOICES,
help_text=_("how should we best phrase this entry?"),
)
food = models.CharField(
verbose_name=_("food"),
max_length=128,
help_text=_("what did you eat?"),
)
image = models.ImageField(
verbose_name=_("image"),
blank=True,
upload_to=post_image_filename,
help_text=_("here you can upload a picture of what you ate!"),
)
created_by = models.ForeignKey(
"User",
related_name="posts",
db_index=True,
db_column="created_by_user_id",
on_delete=models.CASCADE,
)
created_at = models.DateTimeField(
auto_now_add=True,
help_text=_("when this post was created."),
)
updated_at = models.DateTimeField(
auto_now=True,
help_text=_("when this post was last updated."),
)
def __str__(self):
return self.uuid
def save(self, *args, **kwargs):
if not self.id and self.image:
self.image = process_image(self.image)
super().save(*args, **kwargs)
@property
def text(self):
return self.get_verb_display() % {
"user": f"<a href=\"{self.created_by.get_absolute_url()}\">@{self.created_by.username}</a>",
"food": escape(self.food),
}

69
moku/models/user.py Normal file
View file

@ -0,0 +1,69 @@
from django.contrib.auth.models import AbstractUser, BaseUserManager
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from moku.validators import validate_username_regex, validate_username_length
class User(AbstractUser):
username = models.CharField(
verbose_name=_("username"),
max_length=64,
unique=True,
db_index=True,
validators=[validate_username_regex, validate_username_length],
help_text=_(
"this is the unique identifier you'll use to log in. it may only contain "
"letters, numbers, hyphens, dashes and dots."
)
)
email = models.EmailField(
verbose_name=_("email address"),
max_length=128,
unique=True,
help_text=_(
"this should be your email address. make sure it's valid and that you have "
"access to it."
)
)
email_confirmed_at = models.DateTimeField(
blank=True,
null=True,
)
pronouns = models.CharField(
verbose_name=_("pronouns"),
max_length=64,
blank=True,
help_text=_("what pronouns should people use when referring to you?"),
)
location = models.CharField(
verbose_name=_("location"),
max_length=64,
blank=True,
help_text=_("where in the world are you?"),
)
bio = models.TextField(
verbose_name=_("about me"),
blank=True,
help_text=_("write something about yourself!"),
)
last_seen_at = models.DateTimeField(
verbose_name=_("last seen at"),
blank=True,
null=True,
help_text=_("the last time this user accessed the site."),
)
first_name = None
last_name = None
def __str__(self):
return self.username
def get_absolute_url(self):
return reverse("profile", kwargs={"username": self.username})
@property
def email_confirmed(self):
return self.email_confirmed_at is not None

298
moku/static/css/moku.css Normal file
View file

@ -0,0 +1,298 @@
:root {
--tangerine: #F7A278;
--orange: #c94c10;
--champagne: #FDE4D8;
--dusty-champagne: #EFC6B8;
--charcoal: #626262;
--emoji-picker-rounding: 6px;
--image-max-size: 324px;
}
*, *::before, *::after {
box-sizing: border-box;
}
html {
font-size: 62.5%;
line-height: 1.5;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
body {
background: var(--champagne);
font-size: 1.6rem;
line-height: 1.4;
max-width: 768px;
margin-inline: auto;
padding: 4rem 1.2rem 2.4rem 1.2rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
.mt {
margin-block-start: 1.6rem;
}
.mb {
margin-block-end: 1.6rem;
}
button.logout {
border: none;
outline: none;
background: none;
display: inline;
font-size: 1.6rem;
cursor: pointer;
padding: 0;
}
a, button.logout {
color: var(--orange);
font-weight: bold;
text-decoration: none;
text-decoration-skip-ink: none;
text-underline-offset: 2px;
}
a:hover, button.logout:hover {
text-decoration: wavy underline;
}
.profile h2 {
font-size: 1.8rem;
font-weight: bold;
margin-block-end: 1.2rem;
color: var(--orange);
}
dl {
display: grid;
grid-template-columns: 1fr 1fr;
row-gap: 1.2rem;
column-gap: 1.8rem;
}
dl > div {
display: grid;
}
dl dt {
font-size: 1.4rem;
font-weight: bold;
color: var(--charcoal);
}
dl > .double {
grid-column: 1 / span 2;
}
form .errors {
font-size: 1.5rem;
display: grid;
margin-block-end: .8rem;
}
form .errors::before {
content: "fix these and we're good to go:";
font-size: 1.4rem;
color: var(--charcoal);
margin-block-end: .4rem;
}
form .errors li::before {
content: "❌ ";
}
form .errors li {
text-transform: lowercase;
}
form:not(.logout) {
display: grid;
row-gap: .8rem;
}
form.auth {
max-width: 378px;
}
form .field {
display: grid;
row-gap: .4rem;
}
form .field .help {
color: var(--charcoal);
font-size: 1.4rem;
}
form input,
form select {
width: 100%;
}
form textarea {
height: 6ch;
resize: vertical;
}
form input[type=text],
form input[type=password],
form input[type=email],
form textarea,
form select {
padding: .2rem;
font-size: 1.6rem;
border: 1px solid var(--charcoal);
border-radius: 3px;
background: var(--champagne);
}
form input[type=text]:focus,
form input[type=password]:focus,
form input[type=email]:focus,
form textarea:focus,
form select:focus {
background: white;
outline: 2px solid var(--tangerine);
}
form .emoji-picker {
display: grid;
row-gap: .2rem;
}
form .emoji-picker details:first-of-type summary {
border-top-right-radius: var(--emoji-picker-rounding);
border-top-left-radius: var(--emoji-picker-rounding);
}
form .emoji-picker details:last-of-type:not([open]) summary {
border-bottom-right-radius: var(--emoji-picker-rounding);
border-bottom-left-radius: var(--emoji-picker-rounding);
}
form .emoji-picker details summary {
background: var(--dusty-champagne);
font-size: 1.4rem;
padding: .4rem .6rem;
user-select: none;
cursor: default;
}
form .emoji-picker ul {
display: grid;
grid-template-columns: repeat(5, 1fr);
column-gap: .2rem;
row-gap: .2rem;
}
form .emoji-picker ul li label {
padding: .3rem;
}
form .emoji-picker ul li input[type=radio] {
display: none;
}
form .emoji-picker ul li input[type=radio]:checked + label {
outline: 3px solid var(--tangerine);
border-radius: 50%;
}
form button[type=submit]:not(.logout) {
padding: .4rem .6rem;
font-size: 1.5rem;
background: var(--tangerine);
border: 1px solid var(--orange);
outline: none;
border-radius: 4px;
}
header {
display: grid;
grid-template-columns: auto 1fr;
column-gap: 2.4rem;
align-items: center;
margin-block-end: 2.4rem;
}
header h1 {
font-weight: bold;
font-size: 2.4rem;
}
header nav ul {
display: flex;
align-items: center;
justify-content: flex-end;
column-gap: 1.2rem;
}
.grid-content {
display: grid;
grid-template-columns: 214px 1fr;
column-gap: 2.4rem;
align-items: flex-start;
}
.grid-content main {
display: grid;
row-gap: 1.8rem;
}
.grid-content main article {
padding: .8rem 1.8rem;
border-left: 4px solid var(--tangerine);
word-break: break-all;
display: grid;
grid-template-columns: auto 1fr;
column-gap: 1.2rem;
}
.grid-content main article .image {
grid-column: 1 / span 2;
margin-block-end: 1rem;
}
.grid-content main article .image img {
max-width: var(--image-max-size);
max-height: var(--image-max-size);
border-radius: 8px;
}
.grid-content main article .emoji {
font-size: 2rem;
padding-top: .8rem;
}
.grid-content main article .body {
display: grid;
row-gap: .2rem;
}
.grid-content main article .body .metadata {
font-size: 1.4rem;
color: var(--charcoal);
}
.grid-content main article .body .recipe {
margin-block-start: .4rem;
}
.grid-content main article .body .recipe summary {
font-size: 1.4rem;
cursor: default;
user-select: none;
}
.grid-content main article .body .recipe ol {
font-size: 1.5rem;
list-style: decimal;
margin-block-start: 1rem;
padding-inline-start: 3.4rem;
border-left: 2px solid var(--tangerine);
}

48
moku/static/css/reset.css Normal file
View file

@ -0,0 +1,48 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

View file

@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<title>moku.blog</title>
<link rel="stylesheet" href="{{ static('css/reset.css') }}" type="text/css">
<link rel="stylesheet" href="{{ static('css/moku.css') }}" type="text/css">
</head>
<body>
<header>
<h1>moku.blog</h1>
<nav>
<ul>
{% if request.user.is_authenticated %}
<li><a href="{{ url('feed') }}">{% trans %}feed{% endtrans %}</a></li>
<li><a href="{{ url('profile', username=request.user.username) }}">{% trans %}my profile{% endtrans %}</a></li>
<li>
<form action="{{ url('logout') }}" method="POST" class="logout">
{% csrf_token %}
<button class="logout" type="submit">{% trans %}log out{% endtrans %}</button>
</form>
</li>
{% else %}
<li><a href="{{ url('login') }}">{% trans %}log in{% endtrans %}</a></li>
<li><a href="{{ url('signup') }}">{% trans %}sign up{% endtrans %}</a></li>
{% endif %}
</ul>
</nav>
</header>
{% block content %}{% endblock content %}
</body>
</html>

View file

@ -0,0 +1,68 @@
{% extends "moku/base.jinja" %}
{% block content %}
<div class="grid-content">
<aside>
{% if request.user.is_authenticated %}
<form action="" method="POST" enctype="{% if form.is_multipart %}multipart/form-data{% else %}application/x-www-form-urlencoded{% endif %}">
{% include "moku/snippets/form_errors.jinja" %}
{% csrf_token %}
<div class="emoji-picker">
{% for emoji_category in emoji %}
{% set outer_loop = loop %}
<details{% if loop.index0 == 0 %} open{% endif %}>
<summary>{{ emoji_category[0] }}</summary>
<ul>
{% for emoji_choice in emoji_category[1] %}
{% set emoji_label = emoji_choice|unemoji %}
<li>
<input type="radio" value="{{ emoji_choice }}" name="emoji" id="id_emoji_{{ emoji_label }}" required{% if loop.index0 == 0 and outer_loop.index0 == 0 %} checked{% endif %}>
<label for="id_emoji_{{ emoji_label }}">{{ emoji_choice }}</label>
</li>
{% endfor %}
</ul>
</details>
{% endfor %}
</div>
<div class="field">
<label for="id_food">{{ form.food.label }}</label>
<input type="text" name="food" id="id_food" required aria-describedby="help_food">
<span class="help" id="help_food">{{ form.food.help_text }}</span>
</div>
<div class="field">
<label for="id_verb">{{ form.verb.label }}</label>
<select name="verb" id="id_verb">
{% for verb, verb_label in verbs %}
<option value="{{ verb }}">{{ verb_label }}</option>
{% endfor %}
</select>
<span class="help" id="help_verb">{{ form.verb.help_text }}</span>
</div>
<div class="field">
<label for="id_image">{{ form.image.label }}</label>
{{ form.image }}
<span class="help" id="help_image">{{ form.image.help_text }}</span>
</div>
<div class="field">
<button type="submit">{% trans %}post!{% endtrans %}</button>
</div>
</form>
{% else %}
<p>{% trans %}want to post?{% endtrans %}</p>
<p>
{% with login_url=url('login'), signup_url=url('signup') %}
{% trans %}<a href="{{ login_url }}">log in</a> or <a href="{{ signup_url }}">make an account</a>!{% endtrans %}
{% endwith %}
</p>
{% endif %}
</aside>
<main>
{% if not posts %}
<p>{% trans %}no posts yet... 🥱{% endtrans %}</p>
{% endif %}
{% for post in posts %}
{% include "moku/snippets/post.jinja" %}
{% endfor %}
</main>
</div>
{% endblock content %}

View file

@ -0,0 +1,19 @@
{% extends "moku/base.jinja" %}
{% block content %}
<div class="content">
<form action="" method="POST" class="auth">
{% include "moku/snippets/form_errors.jinja" %}
{% csrf_token %}
<div class="field">
<label for="id_username">{% trans %}username{% endtrans %}</label>
<input type="text" name="username" id="id_username">
</div>
<div class="field">
<label for="id_password">{% trans %}password{% endtrans %}</label>
<input type="password" name="password" id="id_password">
</div>
<button type="submit">{% trans %}log in{% endtrans %}</button>
</form>
</div>
{% endblock content %}

View file

@ -0,0 +1,24 @@
{% extends "moku/base.jinja" %}
{% block content %}
<div class="content">
<p class="mb"><a href="{{ url('profile', username=request.user.username) }}">👈 {% trans %}back to my profile{% endtrans %}</a></p>
<form action="" method="POST" class="auth">
{% include "moku/snippets/form_errors.jinja" %}
{% csrf_token %}
<div class="field">
<label for="id_pronouns">{{ form.pronouns.label }}</label>
<input type="text" name="pronouns" id="id_pronouns" value="{{ form.pronouns.value() or "" }}">
</div>
<div class="field">
<label for="id_location">{{ form.location.label }}</label>
<input type="text" name="location" id="id_location" value="{{ form.location.value() or "" }}">
</div>
<div class="field">
<label for="id_bio">{{ form.bio.label }}</label>
<textarea name="bio" id="id_bio">{{ form.bio.value() or "" }}</textarea>
</div>
<button type="submit">update!</button>
</form>
</div>
{% endblock content %}

View file

@ -0,0 +1,34 @@
{% extends "moku/base.jinja" %}
{% block content %}
<div class="grid-content">
<aside class="profile">
<h2>@{{ user.username }}</h2>
<dl>
<div>
<dt>{% trans %}pronouns{% endtrans %}</dt>
<dd>{{ user.pronouns or "not set" }}</dd>
</div>
<div>
<dt>{% trans %}location{% endtrans %}</dt>
<dd>{{ user.location or "not set" }}</dd>
</div>
<div class="double">
<dt>{% trans %}about me{% endtrans %}</dt>
<dd>{{ user.bio or "not set" }}</dd>
</div>
</dl>
{% if user.id == request.user.id %}
<p class="mt"><a href="{{ url('profile.edit') }}">{% trans %}edit{% endtrans %}</a></p>
{% endif %}
</aside>
<main>
{% if not posts %}
<p>{% trans %}no posts yet... 🥱{% endtrans %}</p>
{% endif %}
{% for post in posts %}
{% include "moku/snippets/post.jinja" %}
{% endfor %}
</main>
</div>
{% endblock content %}

View file

@ -0,0 +1,36 @@
{% extends "moku/base.jinja" %}
{% block content %}
<div class="content">
<form action="" method="POST" class="auth">
{% include "moku/snippets/form_errors.jinja" %}
{% csrf_token %}
<div class="field">
<label for="id_username">{{ form.username.label }}</label>
<input type="text" name="username" id="id_username" value="{{ form.username.value() or "" }}" required>
<span class="help">{{ form.username.help_text }}</span>
</div>
<div class="field">
<label for="id_email">{{ form.email.label }}</label>
<input type="email" name="email" id="id_email" value="{{ form.username.value() or "" }}" required>
<span class="help">{{ form.email.help_text }}</span>
</div>
<div class="field">
<label for="id_password1">{{ form.password1.label }}</label>
<input type="password" name="password1" id="id_password1" value="{{ form.username.value() or "" }}" required>
<span class="help">{{ form.password1.help_text|safe }}</span>
</div>
<div class="field">
<label for="id_password2">{{ form.password2.label }}</label>
<input type="password" name="password2" id="id_password2" value="{{ form.username.value() or "" }}" required>
<span class="help">{{ form.password2.help_text }}</span>
</div>
<div class="field">
<label for="id_captcha">captcha</label>
{{ form.captcha }}
<span class="help">{% trans %}please let us know you're not a robot. i'm scared of robots!{% endtrans %}</span>
</div>
<button type="submit">{% trans %}sign up!{% endtrans %}</button>
</form>
</div>
{% endblock content %}

View file

@ -0,0 +1,9 @@
{% if form.errors %}
<ul class="errors">
{% for err_errors in form.errors.values() %}
{% for error in err_errors %}
<li>{{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}

View file

@ -0,0 +1,13 @@
<article>
{% if post.image %}
<a href="{{ post.image.url }}" target="_blank" rel="noreferrer noopener" class="image">
<img src="{{ post.image.url }}">
</a>
{% endif %}
<div class="emoji">{{ post.emoji }}</div>
<div class="body">
<p class="food">{{ post.text|safe }}</p>
<p class="metadata">{{ post.created_at|naturaltime }}</p>
<!-- <details class="recipe"><summary>recipe</summary><ol>...</ol></details> -->
</div>
</article>

18
moku/utils.py Normal file
View file

@ -0,0 +1,18 @@
from io import BytesIO
from django.core.files import File
from emoji import demojize
from PIL import Image
def process_image(image_file):
image = Image.open(image_file)
image.convert("RGB")
image.thumbnail((486, 486))
thumb_io = BytesIO()
image.save(thumb_io, "WEBP")
return File(thumb_io, name=".".join(image_file.name.split(".")[:-1] + ["webp"]))
def unemoji(txt: str):
return demojize(txt, delimiters=("", ""))

32
moku/validators.py Normal file
View file

@ -0,0 +1,32 @@
import re
from django.conf import settings
from django.core.validators import RegexValidator, ValidationError
from django.utils.translation import gettext_lazy as _
from emoji import is_emoji
def validate_emoji(value):
if not is_emoji(value):
raise ValidationError(_("Must be an emoji."))
validate_username_regex = RegexValidator(
re.compile(r"^[a-zA-Z0-9-_.]+\Z"),
_("Username may only contain letters, numbers, hyphens, underscores and dots."),
"invalid",
)
def validate_username_length(value):
if (
len(value) < settings.USERNAME_MIN_LENGTH
or len(value) > settings.USERNAME_MAX_LENGTH
):
raise ValidationError(
_("Username must be between %(min_length)d and %(max_length)d characters.")
% {
"min_length": settings.USERNAME_MIN_LENGTH,
"max_length": settings.USERNAME_MAX_LENGTH,
}
)

0
moku/views/__init__.py Normal file
View file

19
moku/views/auth.py Normal file
View file

@ -0,0 +1,19 @@
from django.contrib.auth.views import LoginView as BaseLoginView, LogoutView as BaseLogoutView
from django.shortcuts import redirect
from django.urls import reverse_lazy
class LoginView(BaseLoginView):
template_name = "moku/login.jinja"
def get(self, request, *args, **kwargs):
if self.request.user.is_authenticated:
return redirect(self.get_success_url())
return super().get(request, *args, **kwargs)
def get_success_url(self):
return self.request.GET.get("next", reverse_lazy("feed"))
class LogoutView(BaseLogoutView):
next_page = "feed"

38
moku/views/post.py Normal file
View file

@ -0,0 +1,38 @@
from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect
from django.views.generic import FormView
from moku.constants import EMOJI_CATEGORIES, Verbs
from moku.models.post import Post
from moku.forms.post import PostForm
class FeedView(FormView):
template_name = "moku/feed.jinja"
form_class = PostForm
def form_valid(self, form):
if not self.request.user.is_authenticated:
raise PermissionDenied
form.instance.created_by = self.request.user
form.instance.save()
return redirect("feed")
def get_context_data(self, **kwargs):
context = {
**super().get_context_data(**kwargs),
"posts": Post.objects.order_by("-created_at").all()[:128]
}
if self.request.user.is_authenticated:
return self.get_authenticated_context_data(context)
return context
def get_authenticated_context_data(self, context):
return {
**context,
"emoji": EMOJI_CATEGORIES,
"verbs": (
(verb[0], verb[1] % {"user": f"@{self.request.user.username}", "food": "..."})
for verb in Verbs.CHOICES
)
}

34
moku/views/user.py Normal file
View file

@ -0,0 +1,34 @@
from django.shortcuts import redirect
from django.views.generic import FormView, TemplateView
from moku.forms.user import ProfileForm, UserForm
from moku.models.user import User
class EditProfileView(FormView):
template_name = "moku/profile/edit.jinja"
form_class = ProfileForm
def form_valid(self, form):
form.save()
return redirect("profile", username=form.instance.username)
def get_form(self):
return self.form_class(instance=self.request.user, **self.get_form_kwargs())
class ProfileView(TemplateView):
template_name = "moku/profile/show.jinja"
def get_context_data(self, **kwargs):
user = User.objects.get(username=self.kwargs.get("username"))
return {
**super().get_context_data(**kwargs),
"user": user,
"posts": user.posts.order_by("-created_at").all(),
}
class SignupView(FormView):
template_name = "moku/signup.jinja"
form_class = UserForm

477
poetry.lock generated Normal file
View file

@ -0,0 +1,477 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "asgiref"
version = "3.8.1"
description = "ASGI specs, helper code, and adapters"
optional = false
python-versions = ">=3.8"
files = [
{file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"},
{file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"},
]
[package.extras]
tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "django"
version = "5.0.3"
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
optional = false
python-versions = ">=3.10"
files = [
{file = "Django-5.0.3-py3-none-any.whl", hash = "sha256:5c7d748ad113a81b2d44750ccc41edc14e933f56581683db548c9257e078cc83"},
{file = "Django-5.0.3.tar.gz", hash = "sha256:5fb37580dcf4a262f9258c1f4373819aacca906431f505e4688e37f3a99195df"},
]
[package.dependencies]
asgiref = ">=3.7.0,<4"
sqlparse = ">=0.3.1"
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
[package.extras]
argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"]
[[package]]
name = "django-debug-toolbar"
version = "4.3.0"
description = "A configurable set of panels that display various debug information about the current request/response."
optional = false
python-versions = ">=3.8"
files = [
{file = "django_debug_toolbar-4.3.0-py3-none-any.whl", hash = "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6"},
{file = "django_debug_toolbar-4.3.0.tar.gz", hash = "sha256:0b0dddee5ea29b9cb678593bc0d7a6d76b21d7799cb68e091a2148341a80f3c4"},
]
[package.dependencies]
django = ">=3.2.4"
sqlparse = ">=0.2"
[[package]]
name = "django-environ"
version = "0.11.2"
description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application."
optional = false
python-versions = ">=3.6,<4"
files = [
{file = "django-environ-0.11.2.tar.gz", hash = "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be"},
{file = "django_environ-0.11.2-py2.py3-none-any.whl", hash = "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05"},
]
[package.extras]
develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"]
docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"]
testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"]
[[package]]
name = "django-jinja"
version = "2.11.0"
description = "Jinja2 templating language integrated in Django."
optional = false
python-versions = ">=3.8"
files = [
{file = "django-jinja-2.11.0.tar.gz", hash = "sha256:47c06d3271e6b2f27d3596278af517bfe2e19c1eb36ae1c0b1cc302d7f0259af"},
{file = "django_jinja-2.11.0-py3-none-any.whl", hash = "sha256:cc4c72246a6e346aa0574e0c56c3e534c1a20ef47b8476f05d7287781f69a0a9"},
]
[package.dependencies]
django = ">=3.2"
jinja2 = ">=3"
[[package]]
name = "django-recaptcha"
version = "4.0.0"
description = "Django recaptcha form field/widget app."
optional = false
python-versions = "*"
files = [
{file = "django-recaptcha-4.0.0.tar.gz", hash = "sha256:5316438f97700c431d65351470d1255047e3f2cd9af0f2f13592b637dad9213e"},
{file = "django_recaptcha-4.0.0-py3-none-any.whl", hash = "sha256:0d912d5c7c009df4e47accd25029133d47a74342dbd2a8edc2877b6bffa971a3"},
]
[package.dependencies]
django = "*"
[[package]]
name = "emoji"
version = "2.10.1"
description = "Emoji for Python"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
{file = "emoji-2.10.1-py2.py3-none-any.whl", hash = "sha256:11fb369ea79d20c14efa4362c732d67126df294a7959a2c98bfd7447c12a218e"},
{file = "emoji-2.10.1.tar.gz", hash = "sha256:16287283518fb7141bde00198f9ffff4e1c1cb570efb68b2f1ec50975c3a581d"},
]
[package.extras]
dev = ["coverage", "coveralls", "pytest"]
[[package]]
name = "gunicorn"
version = "21.2.0"
description = "WSGI HTTP Server for UNIX"
optional = false
python-versions = ">=3.5"
files = [
{file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"},
{file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"},
]
[package.dependencies]
packaging = "*"
[package.extras]
eventlet = ["eventlet (>=0.24.1)"]
gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"]
tornado = ["tornado (>=0.2)"]
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "jinja2"
version = "3.1.3"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
files = [
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "markupsafe"
version = "2.1.5"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.7"
files = [
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"},
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"},
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"},
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"},
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"},
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"},
{file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"},
{file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"},
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
{file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
{file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
{file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
{file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"},
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"},
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"},
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"},
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"},
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"},
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"},
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"},
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"},
{file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"},
{file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"},
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"},
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"},
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"},
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"},
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"},
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"},
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"},
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"},
{file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"},
{file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"},
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
]
[[package]]
name = "packaging"
version = "24.0"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.7"
files = [
{file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
{file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
]
[[package]]
name = "pillow"
version = "10.2.0"
description = "Python Imaging Library (Fork)"
optional = false
python-versions = ">=3.8"
files = [
{file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"},
{file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"},
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"},
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"},
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"},
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"},
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"},
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"},
{file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"},
{file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"},
{file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"},
{file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"},
{file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"},
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"},
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"},
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"},
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"},
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"},
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"},
{file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"},
{file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"},
{file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"},
{file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"},
{file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"},
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"},
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"},
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"},
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"},
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"},
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"},
{file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"},
{file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"},
{file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"},
{file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"},
{file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"},
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"},
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"},
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"},
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"},
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"},
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"},
{file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"},
{file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"},
{file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"},
{file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"},
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"},
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"},
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"},
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"},
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"},
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"},
{file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"},
{file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"},
{file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"},
{file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"},
{file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"},
{file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"},
{file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"},
]
[package.extras]
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
fpx = ["olefile"]
mic = ["olefile"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
typing = ["typing-extensions"]
xmp = ["defusedxml"]
[[package]]
name = "pluggy"
version = "1.4.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
{file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "psycopg2"
version = "2.9.9"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
optional = false
python-versions = ">=3.7"
files = [
{file = "psycopg2-2.9.9-cp310-cp310-win32.whl", hash = "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516"},
{file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"},
{file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"},
{file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"},
{file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"},
{file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"},
{file = "psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"},
{file = "psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"},
{file = "psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"},
{file = "psycopg2-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e"},
{file = "psycopg2-2.9.9-cp39-cp39-win32.whl", hash = "sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59"},
{file = "psycopg2-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913"},
{file = "psycopg2-2.9.9.tar.gz", hash = "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156"},
]
[[package]]
name = "pytest"
version = "8.1.1"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"},
{file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.4,<2.0"
[package.extras]
testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-django"
version = "4.8.0"
description = "A Django plugin for pytest."
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"},
{file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"},
]
[package.dependencies]
pytest = ">=7.0.0"
[package.extras]
docs = ["sphinx", "sphinx-rtd-theme"]
testing = ["Django", "django-configurations (>=2.0)"]
[[package]]
name = "ruff"
version = "0.3.4"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"},
{file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"},
{file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"},
{file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"},
{file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"},
{file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"},
{file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"},
{file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"},
{file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"},
{file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"},
{file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"},
]
[[package]]
name = "shortuuid"
version = "1.0.13"
description = "A generator library for concise, unambiguous and URL-safe UUIDs."
optional = false
python-versions = ">=3.6"
files = [
{file = "shortuuid-1.0.13-py3-none-any.whl", hash = "sha256:a482a497300b49b4953e15108a7913244e1bb0d41f9d332f5e9925dba33a3c5a"},
{file = "shortuuid-1.0.13.tar.gz", hash = "sha256:3bb9cf07f606260584b1df46399c0b87dd84773e7b25912b7e391e30797c5e72"},
]
[[package]]
name = "sqlparse"
version = "0.4.4"
description = "A non-validating SQL parser."
optional = false
python-versions = ">=3.5"
files = [
{file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"},
{file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"},
]
[package.extras]
dev = ["build", "flake8"]
doc = ["sphinx"]
test = ["pytest", "pytest-cov"]
[[package]]
name = "tzdata"
version = "2024.1"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
files = [
{file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
{file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "a2d30838ebf562b336e3c1b80401aadbfd231979f3619645d94a45484bc04f40"

60
pyproject.toml Normal file
View file

@ -0,0 +1,60 @@
[tool.poetry]
name = "moku"
version = "0.1.0"
description = "a lightweight food blogging site"
authors = ["m5ka <m5ka@posteo.de>"]
license = "BSD 2-Clause"
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
django = "^5.0.3"
django-environ = "^0.11.2"
django-jinja = "^2.11.0"
django-recaptcha = "^4.0.0"
emoji = "^2.10.1"
gunicorn = "^21.2.0"
pillow = "^10.2.0"
psycopg2 = "^2.9.9"
python = "^3.12"
shortuuid = "^1.0.13"
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
django-debug-toolbar = "^4.3.0"
ruff = "^0.3.4"
[tool.poetry.group.test]
optional = true
[tool.poetry.group.test.dependencies]
pytest = "^8.1.1"
pytest-django = "^4.8.0"
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "moku.config.settings"
[tool.ruff]
include = ["pyproject.toml", "moku/**/*.py", "manage.py"]
line-length = 88
indent-width = 4
target-version = "py312"
[tool.ruff.lint]
select = ["F", "E", "W", "I"]
[tool.ruff.lint.isort]
split-on-trailing-comma = false
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = true
docstring-code-format = true
docstring-code-line-length = "dynamic"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"