This commit is contained in:
sam 2023-09-22 01:37:57 +02:00
commit 48703ea9fc
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
36 changed files with 936 additions and 0 deletions

5
.env.example Normal file
View file

@ -0,0 +1,5 @@
DATABASE_NAME=postgres
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
DATABASE_HOST=localhost

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
__pycache__/
*.py[cod]
*$py.class
local_settings.py
__pypackages__/
celerybeat-schedule
celerybeat.pid
.env
venv/

1
README.md Normal file
View file

@ -0,0 +1 @@
# pronouns.cc

0
api_v2/__init__.py Normal file
View file

3
api_v2/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

8
api_v2/api.py Normal file
View file

@ -0,0 +1,8 @@
from ninja import NinjaAPI
from .views.users import router as users_router
api = NinjaAPI()
api.add_router("/users/", users_router)

6
api_v2/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApiV2Config(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api_v2"

View file

3
api_v2/models.py Normal file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
api_v2/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

5
api_v2/urls.py Normal file
View file

@ -0,0 +1,5 @@
from django.urls import path
from .api import api
urlpatterns = [path("/", api.urls)]

3
api_v2/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

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

38
api_v2/views/users.py Normal file
View file

@ -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")

0
frontend/__init__.py Normal file
View file

3
frontend/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
frontend/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class FrontendConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "frontend"

View file

3
frontend/models.py Normal file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
frontend/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
frontend/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

22
manage.py Executable file
View file

@ -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()

251
poetry.lock generated Normal file
View file

@ -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"

0
pronounscc/__init__.py Normal file
View file

View file

@ -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)

71
pronounscc/admin/user.py Normal file
View file

@ -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 = []

16
pronounscc/asgi.py Normal file
View file

@ -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()

View file

@ -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"),
),
],
),
]

View file

View file

@ -0,0 +1 @@
from .user import User, UserProfile

79
pronounscc/models/user.py Normal file
View file

@ -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)

138
pronounscc/settings.py Normal file
View file

@ -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"

105
pronounscc/snowflake.py Normal file
View file

@ -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)
)

23
pronounscc/urls.py Normal file
View file

@ -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),
]

16
pronounscc/wsgi.py Normal file
View file

@ -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()

19
pyproject.toml Normal file
View file

@ -0,0 +1,19 @@
[tool.poetry]
name = "pronounscc"
version = "0.1.0"
description = "Pronoun website and API"
authors = ["sam <sam@sleepycat.moe>"]
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"