From 48703ea9fc03905a99514fe01bd844c9bc3b1a4a Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 22 Sep 2023 01:37:57 +0200 Subject: [PATCH] init --- .env.example | 5 + .gitignore | 9 + README.md | 1 + api_v2/__init__.py | 0 api_v2/admin.py | 3 + api_v2/api.py | 8 + api_v2/apps.py | 6 + api_v2/migrations/__init__.py | 0 api_v2/models.py | 3 + api_v2/tests.py | 3 + api_v2/urls.py | 5 + api_v2/views.py | 3 + api_v2/views/__init__.py | 0 api_v2/views/users.py | 38 ++++ frontend/__init__.py | 0 frontend/admin.py | 3 + frontend/apps.py | 6 + frontend/migrations/__init__.py | 0 frontend/models.py | 3 + frontend/tests.py | 3 + frontend/views.py | 3 + manage.py | 22 +++ poetry.lock | 251 ++++++++++++++++++++++++++ pronounscc/__init__.py | 0 pronounscc/admin/__init__.py | 9 + pronounscc/admin/user.py | 71 ++++++++ pronounscc/asgi.py | 16 ++ pronounscc/migrations/0001_initial.py | 84 +++++++++ pronounscc/migrations/__init__.py | 0 pronounscc/models/__init__.py | 1 + pronounscc/models/user.py | 79 ++++++++ pronounscc/settings.py | 138 ++++++++++++++ pronounscc/snowflake.py | 105 +++++++++++ pronounscc/urls.py | 23 +++ pronounscc/wsgi.py | 16 ++ pyproject.toml | 19 ++ 36 files changed, 936 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 api_v2/__init__.py create mode 100644 api_v2/admin.py create mode 100644 api_v2/api.py create mode 100644 api_v2/apps.py create mode 100644 api_v2/migrations/__init__.py create mode 100644 api_v2/models.py create mode 100644 api_v2/tests.py create mode 100644 api_v2/urls.py create mode 100644 api_v2/views.py create mode 100644 api_v2/views/__init__.py create mode 100644 api_v2/views/users.py create mode 100644 frontend/__init__.py create mode 100644 frontend/admin.py create mode 100644 frontend/apps.py create mode 100644 frontend/migrations/__init__.py create mode 100644 frontend/models.py create mode 100644 frontend/tests.py create mode 100644 frontend/views.py create mode 100755 manage.py create mode 100644 poetry.lock create mode 100644 pronounscc/__init__.py create mode 100644 pronounscc/admin/__init__.py create mode 100644 pronounscc/admin/user.py create mode 100644 pronounscc/asgi.py create mode 100644 pronounscc/migrations/0001_initial.py create mode 100644 pronounscc/migrations/__init__.py create mode 100644 pronounscc/models/__init__.py create mode 100644 pronounscc/models/user.py create mode 100644 pronounscc/settings.py create mode 100644 pronounscc/snowflake.py create mode 100644 pronounscc/urls.py create mode 100644 pronounscc/wsgi.py create mode 100644 pyproject.toml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5fb3f9c --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +DATABASE_NAME=postgres +DATABASE_USER=postgres +DATABASE_PASSWORD=postgres +DATABASE_HOST=localhost + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ec93b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.py[cod] +*$py.class +local_settings.py +__pypackages__/ +celerybeat-schedule +celerybeat.pid +.env +venv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6e83d7 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# pronouns.cc diff --git a/api_v2/__init__.py b/api_v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api_v2/admin.py b/api_v2/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/api_v2/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/api_v2/api.py b/api_v2/api.py new file mode 100644 index 0000000..9261db8 --- /dev/null +++ b/api_v2/api.py @@ -0,0 +1,8 @@ +from ninja import NinjaAPI + +from .views.users import router as users_router + +api = NinjaAPI() + +api.add_router("/users/", users_router) + diff --git a/api_v2/apps.py b/api_v2/apps.py new file mode 100644 index 0000000..02d0a4a --- /dev/null +++ b/api_v2/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiV2Config(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "api_v2" diff --git a/api_v2/migrations/__init__.py b/api_v2/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api_v2/models.py b/api_v2/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/api_v2/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/api_v2/tests.py b/api_v2/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/api_v2/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/api_v2/urls.py b/api_v2/urls.py new file mode 100644 index 0000000..14dbc29 --- /dev/null +++ b/api_v2/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +from .api import api + +urlpatterns = [path("/", api.urls)] diff --git a/api_v2/views.py b/api_v2/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/api_v2/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/api_v2/views/__init__.py b/api_v2/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api_v2/views/users.py b/api_v2/views/users.py new file mode 100644 index 0000000..00ad636 --- /dev/null +++ b/api_v2/views/users.py @@ -0,0 +1,38 @@ +from ninja import Router, Field, Schema +from ninja.errors import HttpError + +from pronounscc.models import User + + +router = Router() + + +class UserOut(Schema): + id: str = Field(..., alias="uid") + username: str + display_name: str = Field(..., alias="userprofile.display_name") + bio: str = Field(..., alias="userprofile.bio") + + +@router.get("/@me") +def get_me(request): + return {"data": "me endpoint, eventually"} + + +@router.get("/{user_ref}", response=UserOut) +def get_user(request, user_ref: str): + try: + user_id = int(user_ref) + try: + user = User.objects.get(pk=user_id) + return user + except User.DoesNotExist: + pass + except ValueError: + pass + + try: + user = User.objects.get(username=user_ref) + return user + except User.DoesNotExist: + raise HttpError(404, "User not found") diff --git a/frontend/__init__.py b/frontend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/admin.py b/frontend/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/frontend/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/frontend/apps.py b/frontend/apps.py new file mode 100644 index 0000000..c626efa --- /dev/null +++ b/frontend/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FrontendConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "frontend" diff --git a/frontend/migrations/__init__.py b/frontend/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/models.py b/frontend/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/frontend/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/frontend/tests.py b/frontend/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/frontend/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/frontend/views.py b/frontend/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/frontend/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..d6fae0d --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/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", "pronounscc.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/poetry.lock b/poetry.lock new file mode 100644 index 0000000..01da2a7 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,251 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "asgiref" +version = "3.7.2" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, + {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "django" +version = "4.2.5" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Django-4.2.5-py3-none-any.whl", hash = "sha256:b6b2b5cae821077f137dc4dade696a1c2aa292f892eca28fa8d7bfdf2608ddd4"}, + {file = "Django-4.2.5.tar.gz", hash = "sha256:5e5c1c9548ffb7796b4a8a4782e9a2e5a3df3615259fc1bfd3ebc73b646146c1"}, +] + +[package.dependencies] +asgiref = ">=3.6.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-ninja" +version = "0.22.2" +description = "Django Ninja - Fast Django REST framework" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "django_ninja-0.22.2-py3-none-any.whl", hash = "sha256:c53b098a8190f373ea2605c276a6061d48b2626500a9c6b9916c503e4b4a20eb"}, + {file = "django_ninja-0.22.2.tar.gz", hash = "sha256:913ebde7571d6a6968c9ac0b9e8a24680c46444d44fdd552f8831dbeede1292c"}, +] + +[package.dependencies] +Django = ">=2.2" +pydantic = ">=1.6,<2.0.0" + +[package.extras] +dev = ["pre-commit"] +doc = ["markdown-include", "mkdocs", "mkdocs-material", "mkdocstrings"] +test = ["black", "django-stubs", "flake8", "isort", "mypy (==0.931)", "psycopg2-binary", "pytest", "pytest-asyncio", "pytest-cov", "pytest-django"] + +[[package]] +name = "environs" +version = "9.5.0" +description = "simplified environment variable parsing" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"}, + {file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"}, +] + +[package.dependencies] +marshmallow = ">=3.0.0" +python-dotenv = "*" + +[package.extras] +dev = ["dj-database-url", "dj-email-url", "django-cache-url", "flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)", "pytest", "tox"] +django = ["dj-database-url", "dj-email-url", "django-cache-url"] +lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"] +tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"] + +[[package]] +name = "marshmallow" +version = "3.20.1" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "marshmallow-3.20.1-py3-none-any.whl", hash = "sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c"}, + {file = "marshmallow-3.20.1.tar.gz", hash = "sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889"}, +] + +[package.dependencies] +packaging = ">=17.0" + +[package.extras] +dev = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"] +docs = ["alabaster (==0.7.13)", "autodocsumm (==0.2.11)", "sphinx (==7.0.1)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] +lint = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)"] +tests = ["pytest", "pytz", "simplejson"] + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "psycopg" +version = "3.1.10" +description = "PostgreSQL database adapter for Python" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "psycopg-3.1.10-py3-none-any.whl", hash = "sha256:8bbeddae5075c7890b2fa3e3553440376d3c5e28418335dee3c3656b06fa2b52"}, + {file = "psycopg-3.1.10.tar.gz", hash = "sha256:15b25741494344c24066dc2479b0f383dd1b82fa5e75612fa4fa5bb30726e9b6"}, +] + +[package.dependencies] +typing-extensions = ">=4.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (==3.1.10)"] +c = ["psycopg-c (==3.1.10)"] +dev = ["black (>=23.1.0)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.4.1)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] +pool = ["psycopg-pool"] +test = ["anyio (>=3.6.2)", "mypy (>=1.4.1)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + +[[package]] +name = "pydantic" +version = "1.10.12" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, + {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, + {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, + {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, + {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, + {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, + {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, + {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, + {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, + {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, + {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, + {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, + {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, + {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, + {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, + {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, + {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, + {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, + {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, + {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, + {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, + {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, + {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, +] + +[package.dependencies] +typing-extensions = ">=4.2.0" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "python-dotenv" +version = "1.0.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, + {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "sqlparse" +version = "0.4.4" +description = "A non-validating SQL parser." +category = "main" +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 = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "224eac820fafccb151cadbf018af6de7a5cfb05d43fb1b593e0a001c7aa497f4" diff --git a/pronounscc/__init__.py b/pronounscc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pronounscc/admin/__init__.py b/pronounscc/admin/__init__.py new file mode 100644 index 0000000..646642f --- /dev/null +++ b/pronounscc/admin/__init__.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from django.contrib.auth.models import Group + +from pronounscc.models import User +from .user import UserAdmin + + +admin.site.register(User, UserAdmin) +admin.site.unregister(Group) diff --git a/pronounscc/admin/user.py b/pronounscc/admin/user.py new file mode 100644 index 0000000..7b893b6 --- /dev/null +++ b/pronounscc/admin/user.py @@ -0,0 +1,71 @@ +from django import forms +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.forms import ReadOnlyPasswordHashField +from django.core.exceptions import ValidationError + +from pronounscc.models import User, UserProfile + + +class UserCreationForm(forms.ModelForm): + password1 = forms.CharField(label="Password", widget=forms.PasswordInput) + password2 = forms.CharField( + label="Password confirmation", widget=forms.PasswordInput + ) + + class Meta: + model = User + fields = ["username", "email"] + + def clean_password2(self): + # Check that the two password entries match + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + raise ValidationError("Passwords don't match") + return password2 + + def save(self, commit=True): + # Save the provided password in hashed format + user = super().save(commit=False) + user.set_password(self.cleaned_data["password1"]) + if commit: + user.save() + return user + + +class UserChangeForm(forms.ModelForm): + """A form for updating users. Includes all the fields on + the user, but replaces the password field with admin's + disabled password hash display field. + """ + + password = ReadOnlyPasswordHashField() + + class Meta: + model = User + fields = ["username", "email", "password", "is_moderator", "is_admin"] + + +class UserProfileInline(admin.StackedInline): + model = UserProfile + + +class UserAdmin(BaseUserAdmin): + form = UserChangeForm + add_form = UserCreationForm + + inlines = (UserProfileInline,) + + list_display = ["uid", "username", "is_moderator", "is_admin"] + list_filter = ["is_moderator", "is_admin"] + fieldsets = [ + (None, {"fields": ["uid", "username"]}), + ("Authentication", {"fields": ["email", "discord_username"]}), + ("Permissions", {"fields": ["is_moderator", "is_admin"]}), + ] + readonly_fields = ["uid", "discord_username"] + + search_fields = ["username", "email"] + ordering = ["uid", "username", "email"] + filter_horizontal = [] diff --git a/pronounscc/asgi.py b/pronounscc/asgi.py new file mode 100644 index 0000000..8ed8b87 --- /dev/null +++ b/pronounscc/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for pronounscc 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/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pronounscc.settings") + +application = get_asgi_application() diff --git a/pronounscc/migrations/0001_initial.py b/pronounscc/migrations/0001_initial.py new file mode 100644 index 0000000..28e7bf4 --- /dev/null +++ b/pronounscc/migrations/0001_initial.py @@ -0,0 +1,84 @@ +# Generated by Django 4.2.5 on 2023-09-21 13:59 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "uid", + models.BigIntegerField( + primary_key=True, serialize=False, verbose_name="user id" + ), + ), + ("username", models.CharField(unique=True, verbose_name="username")), + ( + "email", + models.CharField( + blank=True, null=True, unique=True, verbose_name="email" + ), + ), + ( + "discord", + models.BigIntegerField( + null=True, unique=True, verbose_name="discord id" + ), + ), + ( + "discord_username", + models.TextField(blank=True, verbose_name="discord username"), + ), + ( + "is_moderator", + models.BooleanField(default=False, verbose_name="is moderator"), + ), + ( + "is_admin", + models.BooleanField(default=False, verbose_name="is admin"), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="UserProfile", + fields=[ + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "display_name", + models.TextField( + blank=True, max_length=100, verbose_name="display name" + ), + ), + ( + "bio", + models.TextField(blank=True, max_length=1000, verbose_name="bio"), + ), + ], + ), + ] diff --git a/pronounscc/migrations/__init__.py b/pronounscc/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pronounscc/models/__init__.py b/pronounscc/models/__init__.py new file mode 100644 index 0000000..b1491b9 --- /dev/null +++ b/pronounscc/models/__init__.py @@ -0,0 +1 @@ +from .user import User, UserProfile diff --git a/pronounscc/models/user.py b/pronounscc/models/user.py new file mode 100644 index 0000000..0e3fe09 --- /dev/null +++ b/pronounscc/models/user.py @@ -0,0 +1,79 @@ +from django.contrib.auth.models import BaseUserManager, AbstractBaseUser +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from pronounscc.snowflake import Snowflake + + +class UserManager(BaseUserManager): + def create_user( + self, + username: str, + email: str | None, + discord: tuple[int, str] | None, + password=None, + ): + if (not email and not password) and not discord: + raise ValueError("Users must have an authentication method") + + user = self.model( + username=username, + email=self.normalize_email(email) if email else None, + discord=discord[0] if discord else None, + discord_username=discord[1] if discord else None, + ) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, username: str, email: str, password=None): + user = self.model( + username=username, + email=self.normalize_email(email), + ) + + user.set_password(password) + user.is_admin = True + user.save(using=self._db) + return user + + +class User(AbstractBaseUser): + """The basic user model that handles authentication. + + User profile data is in UserProfile instead.""" + + uid = models.BigIntegerField(_("user id"), primary_key=True, default=Snowflake.generate) + username = models.CharField(_("username"), unique=True) + email = models.CharField(_("email"), unique=True, blank=True, null=True) + + discord = models.BigIntegerField(_("discord id"), unique=True, null=True) + discord_username = models.CharField(_("discord username"), blank=True) + + is_moderator = models.BooleanField(_("is moderator"), default=False) + is_admin = models.BooleanField(_("is admin"), default=False) + + objects = UserManager() + + USERNAME_FIELD = "username" + EMAIL_FIELD = "email" + REQUIRED_FIELDS = ["email"] + + @property + def is_staff(self): + return self.is_moderator or self.is_admin + + def has_perm(self, perm, obj=None): + return self.is_staff + + def has_module_perms(self, app_label): + return self.is_staff + + + +class UserProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) + + display_name = models.CharField(_("display name"), max_length=100, blank=True) + bio = models.TextField(_("bio"), max_length=1000, blank=True) diff --git a/pronounscc/settings.py b/pronounscc/settings.py new file mode 100644 index 0000000..0194377 --- /dev/null +++ b/pronounscc/settings.py @@ -0,0 +1,138 @@ +""" +Django settings for pronounscc project. + +Generated by 'django-admin startproject' using Django 4.2.5. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from pathlib import Path +from environs import Env + +env = Env() +env.read_env() + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = env("SECRET_KEY") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = env.bool("DEBUG", False) + +ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", []) + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "pronounscc", + "api_v2", + "frontend", +] + +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", +] + +ROOT_URLCONF = "pronounscc.urls" + +TEMPLATES = [ + { + "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 = "pronounscc.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": env("DATABASE_NAME", "postgres"), + "USER": env("DATABASE_USER", "postgres"), + "PASSWORD": env("DATABASE_PASSWORD", "postgres"), + "HOST": env("DATABASE_HOST", "localhost"), + "PORT": env("DATABASE_PORT", "5432"), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/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", + }, +] + +# Authentication + +AUTH_USER_MODEL = "pronounscc.User" + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/pronounscc/snowflake.py b/pronounscc/snowflake.py new file mode 100644 index 0000000..3a2da29 --- /dev/null +++ b/pronounscc/snowflake.py @@ -0,0 +1,105 @@ +import os +import threading +from datetime import datetime, timezone +from random import randrange + +_local = threading.local() + +def _get_increment() -> int: + if not hasattr(_local, "increment"): + _local.increment = randrange(0, 4095) + + increment = _local.increment + _local.increment += 1 + return increment + + +class Snowflake: + """A Snowflake ID (https://en.wikipedia.org/wiki/Snowflake_ID). + This class wraps an integer and adds convenience functions.""" + + EPOCH = 1_640_995_200_000 # 2022-01-01 at 00:00:00 UTC + + _raw: int + + def __init__(self, src: int): + self._raw = src + + def __str__(self) -> str: + return str(self.id) + + def __repr__(self) -> str: + return f"Snowflake<{self.id}, {self.process}, {self.thread}, {self.increment}, {self.timestamp}>" + + def __int__(self) -> int: + return self._raw + + def __float__(self) -> float: + return float(self._raw) + + def __lt__(self, y: "Snowflake"): + return self.id < y.id + + def __le__(self, y: "Snowflake"): + return self.id <= y.id + + def __eq__(self, y: "Snowflake"): + return self.id == y.id + + def __ne__(self, y: "Snowflake"): + return self.id != y.id + + def __gt__(self, y: "Snowflake"): + return self.id > y.id + + def __ge__(self, y: "Snowflake"): + return self.id >= y.id + + @property + def id(self) -> int: + """The raw integer value of the snowflake.""" + return self._raw + + @property + def time(self) -> datetime: + """The time embedded into the snowflake.""" + return datetime.fromtimestamp(self.timestamp, tz=timezone.utc) + + @property + def timestamp(self) -> float: + """The unix timestamp embedded into the snowflake.""" + return ((self._raw >> 22) + self.EPOCH) / 1000 + + @property + def process(self) -> int: + """The process ID embedded into the snowflake.""" + return (self._raw & 0x3E0000) >> 17 + + @property + def thread(self) -> int: + """The thread ID embedded into the snowflake.""" + return (self._raw & 0x1F000) >> 12 + + @property + def increment(self) -> int: + """The increment embedded into the snowflake.""" + return self._raw & 0xFFF + + @classmethod + def generate(cls, time: datetime | None = None): + """Generates a new snowflake. + If `time` is set, use that time for the snowflake, otherwise, use the current time. + """ + + process_id = os.getpid() + thread_id = threading.get_native_id() + increment = _get_increment() + now = time if time else datetime.now(tz=timezone.utc) + timestamp = round(now.timestamp() * 1000) - cls.EPOCH + + return cls( + timestamp << 22 + | (process_id % 32) << 17 + | (thread_id % 32) << 12 + | (increment % 4096) + ) diff --git a/pronounscc/urls.py b/pronounscc/urls.py new file mode 100644 index 0000000..d602d67 --- /dev/null +++ b/pronounscc/urls.py @@ -0,0 +1,23 @@ +""" +URL configuration for pronounscc project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/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.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path("api/v2", include("api_v2.urls")), + path("admin/", admin.site.urls), +] diff --git a/pronounscc/wsgi.py b/pronounscc/wsgi.py new file mode 100644 index 0000000..cf29624 --- /dev/null +++ b/pronounscc/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for pronounscc 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/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pronounscc.settings") + +application = get_wsgi_application() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..88927a4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "pronounscc" +version = "0.1.0" +description = "Pronoun website and API" +authors = ["sam "] +license = "AGPL-3.0-only" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +django = "^4.2.5" +environs = "^9.5.0" +psycopg = "^3.1.10" +django-ninja = "^0.22.2" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api"