From c9f8c486642c10d1e835f1d46f5188ac59258c11 Mon Sep 17 00:00:00 2001 From: m5ka Date: Mon, 25 Mar 2024 10:36:10 +0000 Subject: [PATCH] initial commit --- .env.example | 24 + .gitignore | 78 +++ manage.py | 23 + moku/__init__.py | 1 + moku/config/__init__.py | 0 moku/config/apps.py | 7 + moku/config/asgi.py | 16 + moku/config/settings.py | 179 +++++++ moku/config/urls.py | 40 ++ moku/config/wsgi.py | 16 + moku/constants.py | 72 +++ moku/forms/__init__.py | 0 moku/forms/post.py | 16 + moku/forms/user.py | 51 ++ moku/migrations/0001_initial.py | 49 ++ moku/migrations/0002_post.py | 32 ++ moku/migrations/__init__.py | 0 moku/models/__init__.py | 8 + moku/models/post.py | 76 +++ moku/models/user.py | 69 +++ moku/static/css/moku.css | 298 +++++++++++ moku/static/css/reset.css | 48 ++ moku/templates/moku/base.jinja | 33 ++ moku/templates/moku/feed.jinja | 68 +++ moku/templates/moku/login.jinja | 19 + moku/templates/moku/profile/edit.jinja | 24 + moku/templates/moku/profile/show.jinja | 34 ++ moku/templates/moku/signup.jinja | 36 ++ .../templates/moku/snippets/form_errors.jinja | 9 + moku/templates/moku/snippets/post.jinja | 13 + moku/utils.py | 18 + moku/validators.py | 32 ++ moku/views/__init__.py | 0 moku/views/auth.py | 19 + moku/views/post.py | 38 ++ moku/views/user.py | 34 ++ poetry.lock | 477 ++++++++++++++++++ pyproject.toml | 60 +++ 38 files changed, 2017 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100755 manage.py create mode 100644 moku/__init__.py create mode 100644 moku/config/__init__.py create mode 100644 moku/config/apps.py create mode 100644 moku/config/asgi.py create mode 100644 moku/config/settings.py create mode 100644 moku/config/urls.py create mode 100644 moku/config/wsgi.py create mode 100644 moku/constants.py create mode 100644 moku/forms/__init__.py create mode 100644 moku/forms/post.py create mode 100644 moku/forms/user.py create mode 100644 moku/migrations/0001_initial.py create mode 100644 moku/migrations/0002_post.py create mode 100644 moku/migrations/__init__.py create mode 100644 moku/models/__init__.py create mode 100644 moku/models/post.py create mode 100644 moku/models/user.py create mode 100644 moku/static/css/moku.css create mode 100644 moku/static/css/reset.css create mode 100644 moku/templates/moku/base.jinja create mode 100644 moku/templates/moku/feed.jinja create mode 100644 moku/templates/moku/login.jinja create mode 100644 moku/templates/moku/profile/edit.jinja create mode 100644 moku/templates/moku/profile/show.jinja create mode 100644 moku/templates/moku/signup.jinja create mode 100644 moku/templates/moku/snippets/form_errors.jinja create mode 100644 moku/templates/moku/snippets/post.jinja create mode 100644 moku/utils.py create mode 100644 moku/validators.py create mode 100644 moku/views/__init__.py create mode 100644 moku/views/auth.py create mode 100644 moku/views/post.py create mode 100644 moku/views/user.py create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f518535 --- /dev/null +++ b/.env.example @@ -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" ' +# 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" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20509ff --- /dev/null +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..8ccccd9 --- /dev/null +++ b/manage.py @@ -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() diff --git a/moku/__init__.py b/moku/__init__.py new file mode 100644 index 0000000..b057dc5 --- /dev/null +++ b/moku/__init__.py @@ -0,0 +1 @@ +default_app_config = "moku.config.apps.MokuConfig" diff --git a/moku/config/__init__.py b/moku/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/moku/config/apps.py b/moku/config/apps.py new file mode 100644 index 0000000..4c21ca9 --- /dev/null +++ b/moku/config/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class MokuConfig(AppConfig): + name = "moku" + label = "moku" + verbose_name = "moku.blog" diff --git a/moku/config/asgi.py b/moku/config/asgi.py new file mode 100644 index 0000000..f60fb5a --- /dev/null +++ b/moku/config/asgi.py @@ -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() diff --git a/moku/config/settings.py b/moku/config/settings.py new file mode 100644 index 0000000..25c5402 --- /dev/null +++ b/moku/config/settings.py @@ -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" ') +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") diff --git a/moku/config/urls.py b/moku/config/urls.py new file mode 100644 index 0000000..b203ac6 --- /dev/null +++ b/moku/config/urls.py @@ -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/", 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) diff --git a/moku/config/wsgi.py b/moku/config/wsgi.py new file mode 100644 index 0000000..47fd858 --- /dev/null +++ b/moku/config/wsgi.py @@ -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() diff --git a/moku/constants.py b/moku/constants.py new file mode 100644 index 0000000..f081ed6 --- /dev/null +++ b/moku/constants.py @@ -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"), + ( + "๐Ÿฅ„", "๐Ÿด", "๐Ÿฝ๏ธ", "๐Ÿฅฃ", "๐Ÿฅก", "๐Ÿฅข", "๐Ÿง‚", "๐Ÿ”ช", "๐Ÿช“", + ) + ) +] diff --git a/moku/forms/__init__.py b/moku/forms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/moku/forms/post.py b/moku/forms/post.py new file mode 100644 index 0000000..9006a54 --- /dev/null +++ b/moku/forms/post.py @@ -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"), + } diff --git a/moku/forms/user.py b/moku/forms/user.py new file mode 100644 index 0000000..17f891d --- /dev/null +++ b/moku/forms/user.py @@ -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"), + } diff --git a/moku/migrations/0001_initial.py b/moku/migrations/0001_initial.py new file mode 100644 index 0000000..f8bb8de --- /dev/null +++ b/moku/migrations/0001_initial.py @@ -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()), + ], + ), + ] diff --git a/moku/migrations/0002_post.py b/moku/migrations/0002_post.py new file mode 100644 index 0000000..c938cea --- /dev/null +++ b/moku/migrations/0002_post.py @@ -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)), + ], + ), + ] diff --git a/moku/migrations/__init__.py b/moku/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/moku/models/__init__.py b/moku/models/__init__.py new file mode 100644 index 0000000..4fa1ead --- /dev/null +++ b/moku/models/__init__.py @@ -0,0 +1,8 @@ +from moku.models.post import Post +from moku.models.user import User + + +__all__ = [ + "Post", + "User", +] diff --git a/moku/models/post.py b/moku/models/post.py new file mode 100644 index 0000000..9c4b8af --- /dev/null +++ b/moku/models/post.py @@ -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"@{self.created_by.username}", + "food": escape(self.food), + } diff --git a/moku/models/user.py b/moku/models/user.py new file mode 100644 index 0000000..731dbe6 --- /dev/null +++ b/moku/models/user.py @@ -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 diff --git a/moku/static/css/moku.css b/moku/static/css/moku.css new file mode 100644 index 0000000..07a90c6 --- /dev/null +++ b/moku/static/css/moku.css @@ -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); +} \ No newline at end of file diff --git a/moku/static/css/reset.css b/moku/static/css/reset.css new file mode 100644 index 0000000..cf3d1dd --- /dev/null +++ b/moku/static/css/reset.css @@ -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; +} \ No newline at end of file diff --git a/moku/templates/moku/base.jinja b/moku/templates/moku/base.jinja new file mode 100644 index 0000000..6223065 --- /dev/null +++ b/moku/templates/moku/base.jinja @@ -0,0 +1,33 @@ + + + + + + moku.blog + + + + +
+

moku.blog

+ +
+ {% block content %}{% endblock content %} + + \ No newline at end of file diff --git a/moku/templates/moku/feed.jinja b/moku/templates/moku/feed.jinja new file mode 100644 index 0000000..c41f39c --- /dev/null +++ b/moku/templates/moku/feed.jinja @@ -0,0 +1,68 @@ +{% extends "moku/base.jinja" %} + +{% block content %} +
+ +
+ {% if not posts %} +

{% trans %}no posts yet... ๐Ÿฅฑ{% endtrans %}

+ {% endif %} + {% for post in posts %} + {% include "moku/snippets/post.jinja" %} + {% endfor %} +
+
+{% endblock content %} \ No newline at end of file diff --git a/moku/templates/moku/login.jinja b/moku/templates/moku/login.jinja new file mode 100644 index 0000000..848e047 --- /dev/null +++ b/moku/templates/moku/login.jinja @@ -0,0 +1,19 @@ +{% extends "moku/base.jinja" %} + +{% block content %} +
+
+ {% include "moku/snippets/form_errors.jinja" %} + {% csrf_token %} +
+ + +
+
+ + +
+ +
+
+{% endblock content %} \ No newline at end of file diff --git a/moku/templates/moku/profile/edit.jinja b/moku/templates/moku/profile/edit.jinja new file mode 100644 index 0000000..206ed9c --- /dev/null +++ b/moku/templates/moku/profile/edit.jinja @@ -0,0 +1,24 @@ +{% extends "moku/base.jinja" %} + +{% block content %} +
+

๐Ÿ‘ˆ {% trans %}back to my profile{% endtrans %}

+
+ {% include "moku/snippets/form_errors.jinja" %} + {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+ +
+
+{% endblock content %} \ No newline at end of file diff --git a/moku/templates/moku/profile/show.jinja b/moku/templates/moku/profile/show.jinja new file mode 100644 index 0000000..109e1bc --- /dev/null +++ b/moku/templates/moku/profile/show.jinja @@ -0,0 +1,34 @@ +{% extends "moku/base.jinja" %} + +{% block content %} +
+ +
+ {% if not posts %} +

{% trans %}no posts yet... ๐Ÿฅฑ{% endtrans %}

+ {% endif %} + {% for post in posts %} + {% include "moku/snippets/post.jinja" %} + {% endfor %} +
+
+{% endblock content %} \ No newline at end of file diff --git a/moku/templates/moku/signup.jinja b/moku/templates/moku/signup.jinja new file mode 100644 index 0000000..2ce7a01 --- /dev/null +++ b/moku/templates/moku/signup.jinja @@ -0,0 +1,36 @@ +{% extends "moku/base.jinja" %} + +{% block content %} +
+
+ {% include "moku/snippets/form_errors.jinja" %} + {% csrf_token %} +
+ + + {{ form.username.help_text }} +
+
+ + + {{ form.email.help_text }} +
+
+ + + {{ form.password1.help_text|safe }} +
+
+ + + {{ form.password2.help_text }} +
+
+ + {{ form.captcha }} + {% trans %}please let us know you're not a robot. i'm scared of robots!{% endtrans %} +
+ +
+
+{% endblock content %} \ No newline at end of file diff --git a/moku/templates/moku/snippets/form_errors.jinja b/moku/templates/moku/snippets/form_errors.jinja new file mode 100644 index 0000000..8888ae4 --- /dev/null +++ b/moku/templates/moku/snippets/form_errors.jinja @@ -0,0 +1,9 @@ +{% if form.errors %} +
    + {% for err_errors in form.errors.values() %} + {% for error in err_errors %} +
  • {{ error }}
  • + {% endfor %} + {% endfor %} +
+{% endif %} \ No newline at end of file diff --git a/moku/templates/moku/snippets/post.jinja b/moku/templates/moku/snippets/post.jinja new file mode 100644 index 0000000..dd80f9e --- /dev/null +++ b/moku/templates/moku/snippets/post.jinja @@ -0,0 +1,13 @@ +
+ {% if post.image %} + + + + {% endif %} +
{{ post.emoji }}
+
+

{{ post.text|safe }}

+ + +
+
\ No newline at end of file diff --git a/moku/utils.py b/moku/utils.py new file mode 100644 index 0000000..977c628 --- /dev/null +++ b/moku/utils.py @@ -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=("", "")) diff --git a/moku/validators.py b/moku/validators.py new file mode 100644 index 0000000..9aaa336 --- /dev/null +++ b/moku/validators.py @@ -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, + } + ) diff --git a/moku/views/__init__.py b/moku/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/moku/views/auth.py b/moku/views/auth.py new file mode 100644 index 0000000..2d6bf4c --- /dev/null +++ b/moku/views/auth.py @@ -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" diff --git a/moku/views/post.py b/moku/views/post.py new file mode 100644 index 0000000..d1afb7f --- /dev/null +++ b/moku/views/post.py @@ -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 + ) + } diff --git a/moku/views/user.py b/moku/views/user.py new file mode 100644 index 0000000..c534f1f --- /dev/null +++ b/moku/views/user.py @@ -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 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..1d211e8 --- /dev/null +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2a969b9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,60 @@ +[tool.poetry] +name = "moku" +version = "0.1.0" +description = "a lightweight food blogging site" +authors = ["m5ka "] +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"