Browse Source

initial

master
Lucas 12 months ago
commit
39cd44d022
  1. 11
      .gitignore
  2. 40
      Makefile
  3. 53
      README.md
  4. 120
      contrib/scripts/make_scripts.sh
  5. 0
      django_conc/__init__.py
  6. 0
      django_conc/apps/__init__.py
  7. 0
      django_conc/apps/accounts/__init__.py
  8. 64
      django_conc/apps/accounts/admin.py
  9. 7
      django_conc/apps/accounts/apps.py
  10. 13
      django_conc/apps/accounts/backends.py
  11. 30
      django_conc/apps/accounts/forms.py
  12. 6
      django_conc/apps/accounts/managers.py
  13. 51
      django_conc/apps/accounts/migrations/0001_initial.py
  14. 0
      django_conc/apps/accounts/migrations/__init__.py
  15. 41
      django_conc/apps/accounts/models.py
  16. 3
      django_conc/apps/accounts/templates/accounts/form.html
  17. 4
      django_conc/apps/accounts/templates/accounts/index.html
  18. 3
      django_conc/apps/accounts/tests.py
  19. 17
      django_conc/apps/accounts/urls.py
  20. 30
      django_conc/apps/accounts/views.py
  21. 1
      django_conc/apps/core/__init__.py
  22. 3
      django_conc/apps/core/admin.py
  23. 7
      django_conc/apps/core/apps.py
  24. 69
      django_conc/apps/core/helpers.py
  25. 35
      django_conc/apps/core/management/commands/elements.py
  26. 11
      django_conc/apps/core/managers.py
  27. 27
      django_conc/apps/core/middleware.py
  28. 0
      django_conc/apps/core/migrations/__init__.py
  29. 22
      django_conc/apps/core/mixins.py
  30. 131
      django_conc/apps/core/models.py
  31. 31
      django_conc/apps/core/static/core/_hs/main._hs
  32. 226
      django_conc/apps/core/static/core/css/main.css
  33. 1
      django_conc/apps/core/static/core/js/vendor/_hyperscript.min.js
  34. 1
      django_conc/apps/core/static/core/js/vendor/htmx.min.js
  35. 47
      django_conc/apps/core/templates/core/base.html
  36. 38
      django_conc/apps/core/templates/core/pages/confirm_delete.html
  37. 42
      django_conc/apps/core/templates/core/pages/confirm_exit.html
  38. 12
      django_conc/apps/core/templates/core/pages/form.html
  39. 7
      django_conc/apps/core/templates/core/pages/index.html
  40. 56
      django_conc/apps/core/templates/core/pages/list.html
  41. 0
      django_conc/apps/core/templates/core/pages/void.html
  42. 36
      django_conc/apps/core/templates/core/partials/modal/base.html
  43. 31
      django_conc/apps/core/templatetags/tag_helpers.py
  44. 3
      django_conc/apps/core/tests.py
  45. 17
      django_conc/apps/core/urls.py
  46. 134
      django_conc/apps/core/views.py
  47. 16
      django_conc/asgi.py
  48. 145
      django_conc/settings.py
  49. 33
      django_conc/urls.py
  50. 16
      django_conc/wsgi.py
  51. 22
      manage.py
  52. 345
      poetry.lock
  53. 75
      pyproject.toml
  54. 11
      templates/registration/login.html

11
.gitignore

@ -0,0 +1,11 @@
venv
.venv
.env
*.sqlite3
media
*__pycache__
.idea
*.code-workspace
.report.json
report.json
.vscode

40
Makefile

@ -0,0 +1,40 @@
.DEFAULT_GOAL = default
SCRIPT = contrib/scripts/make_scripts.sh
## @ env
.PHONY: env
env: ## creates a .env file
@./${SCRIPT} make_env_file
## @ task
.PHONY: check run shell_plus clear_migrations show_migrations migrations migrate elements
check: ## same as manage.py check
@./${SCRIPT} check
run: ## same as manage.py run server
@./${SCRIPT} run
shell_plus: ## same as .manage.py shell_plus
@./${SCRIPT} shell_plus
clear_migrations: ## same as manage.py showmigrations
@./${SCRIPT} clear_migrations
show_migrations: ## same as manage.py showmigrations
@./${SCRIPT} show_migrations
migrations: ## same as manage.py makemigrations
@./${SCRIPT} migrations
migrate: ## same as manage.py migrate
@./${SCRIPT} migrate
elements: ## create initial app elements
@./${SCRIPT} elements
## @ help
.PHONY: help
help: ## display all make commands
@./${SCRIPT} help $(MAKEFILE_LIST)
default: help

53
README.md

@ -0,0 +1,53 @@
# Django Conc
É um exemplo de aplicação Django para testar concorrência no momento de atualizar um modelo.
Fortemente inspirado em [django-optimistic-lock](https://github.com/gavinwahl/django-optimistic-lock).
## Instalação
via `pip`
```bash
git clone https://git.lucasf.dev/public/django_conc.git
cd django_conc
python -m venv .venv
. ./.venv/bin/activate
pip install -r requirements.txt
```
via `poetry`
```bash
git clone https://git.lucasf.dev/public/django_conc.git
cd django_conc
poetry shell
poetry install
```
## Criando arquivo .env
```bash
make env
```
## Criando elementos para aplicação
primeiro informe no arquivo `.env` gerado os valores para as seguintes variáveis:
ADMIN_USERNAME
ADMIN_EMAIL
ADMIN_PASSWORD
```bash
make elements
```
## Iniciar a aplicação
```bash
make run
```

120
contrib/scripts/make_scripts.sh

@ -0,0 +1,120 @@
#!/usr/bin/env bash
HERE="$(cd "$(dirname "$0")" && pwd)"
BASEDIR="$(cd "$(dirname "$1")" && pwd)"
CHARS="abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
for ((i=0;i<${#CHARS};i++)); do ARRAY[$i]="${CHARS:i:1}"; done
MSG_SUCCESS="DONE!"
POETRY=0
PYTHON=0
key_gen() {
for ((c=1; c<=50; c++)); do
KEY="$KEY${ARRAY[$((RANDOM % 50))]}"
done
echo $KEY
}
make_env_file() {
if [[ ! -f ".env" ]]; then
ENV="SECRET_KEY='$(key_gen)'\n
ALLOWED_HOSTS=localhost, 10.0.2.2, 127.0.0.1\n
DEBUG=True\n\n
#DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5433/db\n\n
ADMIN_USERNAME=\n
ADMIN_EMAIL=\n
ADMIN_PASSWORD=\n\n
EMAIL_HOST=\n
EMAIL_PORT=\n
EMAIL_HOST_USER=\n
EMAIL_HOST_PASSWORD=\n
EMAIL_USE_TLS=True\n
DEFAULT_FROM_EMAIL=
"
$(echo -e $ENV | sed -e 's/^[ \t]*//' > .env)
echo "ENV FILE - $MSG_SUCCESS"
fi
}
verify_poetry() {
if command -v poetry &> /dev/null; then
POETRY=1
fi
}
verify_python() {
if command -v python3 &> /dev/null; then
PYTHON=1
fi
}
venv_name() {
if [[ -d "$BASEDIR/.venv" ]]; then
echo ".venv"
fi
if [[ -d "$BASEDIR/venv" ]]; then
echo "venv"
fi
}
python_name() {
if [[ PYTHON -eq 1 ]]; then
echo "python3"
else
echo "python"
fi
}
help() {
awk 'BEGIN {FS="## @ "; print "Usage: make";} /^## @ / { printf "\033[31m\n" substr($1, 5) "\n";} {FS=" ## ";} /^[a-zA-Z_-]+:.*? ##/ { print "\033[33m -", $1 "\033[37m", $2}' $ARG
}
run() {
$(venv_name)/bin/$(python_name) manage.py runserver 0.0.0.0:8000
}
shell_plus() {
$(venv_name)/bin/$(python_name) manage.py shell_plus
}
check() {
$(venv_name)/bin/$(python_name) manage.py check
IS_OK=$?
}
show_migrations() {
$(venv_name)/bin/$(python_name) manage.py showmigrations
}
migrations() {
$(venv_name)/bin/$(python_name) manage.py makemigrations
}
migrate() {
$(venv_name)/bin/$(python_name) manage.py migrate
}
check() {
$(venv_name)/bin/$(python_name) manage.py check
}
elements() {
$(venv_name)/bin/$(python_name) manage.py elements
}
clear_migrations() {
find $BASEDIR -path '*/migrations/*.py' -not -name '__init__.py' -not -path '*/.venv/*' -delete
find $BASEDIR -path '*/migrations/*.pyc' -not -name '__init__.py' -not -path '*/.venv/*' -delete
if [[ -f $BASEDIR/media/ ]]; then
rm $BASEDIR/media/*
fi
if [[ -f db.sqlite3 ]];then
rm db.sqlite3
fi
}
verify_python
verify_poetry
ARG=$2
$1

0
django_conc/__init__.py

0
django_conc/apps/__init__.py

0
django_conc/apps/accounts/__init__.py

64
django_conc/apps/accounts/admin.py

@ -0,0 +1,64 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_protect
from .models import User
csrf_protect_m = method_decorator(csrf_protect)
class BaseUserAdmin(UserAdmin):
fieldsets = (
(None, {"fields": ("username", "password")}),
(
_("Personal info"),
{
"fields": (
"first_name",
"last_name",
"email",
"avatar",
"use_dark_theme",
"version",
)
},
),
(
_("Permissions"),
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
),
},
),
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": ("username", "password1", "password2"),
},
),
)
list_display = (
"username",
"email",
"first_name",
"last_name",
"is_staff",
)
list_filter = (
"is_staff",
"is_superuser",
"is_active",
)
search_fields = ("username", "first_name", "last_name", "email")
admin.site.register(User, BaseUserAdmin)

7
django_conc/apps/accounts/apps.py

@ -0,0 +1,7 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "accounts"
default = False

13
django_conc/apps/accounts/backends.py

@ -0,0 +1,13 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend as BaseModelBackend
class ModelBackend(BaseModelBackend):
def authenticate(self, request, username=None, password=None):
if not username is None:
try:
user = get_user_model().objects.get(email=username)
if user.check_password(password):
return user
except get_user_model().DoesNotExist:
pass

30
django_conc/apps/accounts/forms.py

@ -0,0 +1,30 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.utils.translation import gettext_lazy as _
from .models import User
class UserAdminForm(forms.ModelForm):
class Meta:
model = User
fields = UserCreationForm.Meta.fields + (
"username",
"email",
"first_name",
"is_active",
"is_staff",
)
class ListUserForm(forms.ModelForm):
class Meta:
model = User
fields = ["username", "email", "is_active", "avatar", "version"]
actions = True
class NewUserForm(forms.ModelForm):
class Meta:
model = User
fields = ["username", "email", "is_active", "avatar", "version"]

6
django_conc/apps/accounts/managers.py

@ -0,0 +1,6 @@
from django.contrib.auth.models import UserManager
class CustomUserManager(UserManager):
def all(self):
return self.get_queryset().filter(is_superuser=False)

51
django_conc/apps/accounts/migrations/0001_initial.py

@ -0,0 +1,51 @@
# Generated by Django 5.0.2 on 2024-02-11 14:18
import django.contrib.auth.validators
import django.utils.timezone
import django_conc.apps.accounts.managers
import django_conc.apps.core.helpers
import django_conc.apps.core.models
import uuid
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=[
('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')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('version', django_conc.apps.core.models.VersionField(default=0, verbose_name='Version')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='Identifier')),
('avatar', models.ImageField(blank=True, null=True, upload_to=django_conc.apps.core.helpers.UploadToPathAndRename('user_avatar'), verbose_name='Photo')),
('is_staff', models.BooleanField(default=False, verbose_name='staff status')),
('is_active', models.BooleanField(default=False, verbose_name='active')),
('is_admin', models.BooleanField(default=False, verbose_name='is admin')),
('use_dark_theme', models.BooleanField(default=False, verbose_name='Uses dark theme')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('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',
'ordering': ['username', 'first_name'],
},
managers=[
('objects', django_conc.apps.accounts.managers.CustomUserManager()),
],
),
]

0
django_conc/apps/accounts/migrations/__init__.py

41
django_conc/apps/accounts/models.py

@ -0,0 +1,41 @@
import uuid
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from ..core.helpers import UploadToPathAndRename
from ..core.models import VersionedModel
from . import managers
class User(VersionedModel, AbstractUser):
id = models.UUIDField(
_("Identifier"), primary_key=True, default=uuid.uuid4, editable=False
)
avatar = models.ImageField(
_("Photo"),
upload_to=UploadToPathAndRename("user_avatar"),
null=True,
blank=True,
)
is_staff = models.BooleanField(_("staff status"), default=False)
is_active = models.BooleanField(_("active"), default=False)
is_admin = models.BooleanField(_("is admin"), default=False)
use_dark_theme = models.BooleanField(_("Uses dark theme"), default=False)
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
EMAIL_FIELD = "email"
USERNAME_FIELD = "username"
REQUIRED_FIELDS = ["email"]
objects = managers.CustomUserManager()
class Meta:
verbose_name = _("User")
verbose_name_plural = _("Users")
ordering = ["username", "first_name"]
def __str__(self):
return self.first_name or self.username

3
django_conc/apps/accounts/templates/accounts/form.html

@ -0,0 +1,3 @@
{% extends "core/pages/form.html" %}
{% load static %}

4
django_conc/apps/accounts/templates/accounts/index.html

@ -0,0 +1,4 @@
{% extends "core/pages/list.html" %}
{% load static %}

3
django_conc/apps/accounts/tests.py

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

17
django_conc/apps/accounts/urls.py

@ -0,0 +1,17 @@
from django.urls import path
from . import views
app_name = "accounts"
urlpatterns = [
path("", views.Index.as_view(), name="index"),
path("new/", views.New.as_view(), name="new"),
path("edit/<uuid:pk>/", views.Edit.as_view(), name="edit"),
path(
"confirm-delete/<uuid:pk>/",
views.ConfirmDelete.as_view(),
name="confirm_delete",
),
path("delete/<uuid:pk>/", views.Delete.as_view(), name="delete"),
]

30
django_conc/apps/accounts/views.py

@ -0,0 +1,30 @@
from ..core import views as cv
from . import forms
from .models import User
class Common(cv.Common):
app_name = "accounts"
model = User
class Crud(Common):
form_class = forms.NewUserForm
class Index(Common, cv.List):
template_name = "accounts/index.html"
paginate_by = 10
form_class = forms.ListUserForm
class New(Crud, cv.New): ...
class Edit(Crud, cv.Update): ...
class ConfirmDelete(Crud, cv.ConfirmDelete): ...
class Delete(Common, cv.Delete): ...

1
django_conc/apps/core/__init__.py

@ -0,0 +1 @@

3
django_conc/apps/core/admin.py

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

7
django_conc/apps/core/apps.py

@ -0,0 +1,7 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "core"
default = False

69
django_conc/apps/core/helpers.py

@ -0,0 +1,69 @@
import os
from enum import Enum
from io import BytesIO
from uuid import uuid4
from django.utils.deconstruct import deconstructible
from PIL import Image
@deconstructible
class UploadToPathAndRename(object):
def __init__(self, path):
self.sub_path = path
def __call__(self, instance, filename):
ext = filename.split(".")[-1]
filename = "{}.{}".format(uuid4().hex, ext)
return os.path.join(self.sub_path, filename)
class AvatarThumbnail:
def create_thumb(self, image, size=(200, 200)):
temp_file = BytesIO()
img = Image.open(image).convert("RGB")
if (
isinstance(size, tuple)
and len(size) == 2
and isinstance(size[0], int)
and isinstance(size[1], int)
):
try:
width, height = img.size
if width > size[0] and height > size[1]:
img.thumbnail((width, height))
if height < width:
left = (width - height) / 2
right = (width + height) / 2
top = 0
bottom = height
img = img.crop((left, top, right, bottom))
elif width < height:
left = 0
right = width
top = 0
bottom = width
img = img.crop((left, top, right, bottom))
if width > size[0] and height > size[1]:
img.thumbnail((size[0], size[1]))
img.save(temp_file, format="WEBP")
except Exception as e:
print(e)
return temp_file
raise ValueError(
"The size must be a tuple of integer with length and width values. Example (350,350)."
)
def is_mobile(request):
if "mobile" in request.META.get("HTTP_USER_AGENT", "").lower():
return True
return False

35
django_conc/apps/core/management/commands/elements.py

@ -0,0 +1,35 @@
from decouple import config
from django.core.management.base import BaseCommand
from django.utils.translation import gettext_lazy as _
from django_conc.apps.accounts.models import User
class AppException(Exception, BaseCommand):
def __init__(self, exception):
print(
BaseCommand().stdout.write(
BaseCommand().style.NOTICE(f"Error: {exception}")
)
)
class Command(BaseCommand):
help = _("Creates initial information for application")
su_created = False
def handle(self, *args, **options):
if not User.objects.filter(username=config("ADMIN_USERNAME")):
su = User.objects.create_superuser(
username=config("ADMIN_USERNAME"),
email=config("ADMIN_EMAIL"),
password=config("ADMIN_PASSWORD"),
is_active=True,
is_staff=True,
)
if su:
self.stdout.write(self.style.SUCCESS(_("Superuser created!")))
else:
self.stdout.write(
self.style.NOTICE(_("Superuser already exists!"))
)

11
django_conc/apps/core/managers.py

@ -0,0 +1,11 @@
from django.db import models
class BaseManager(models.Manager):
def find(self, pk):
queryset = self.get_queryset().filter(pk=pk)
if queryset:
return queryset.first()
return self.queryset.none()

27
django_conc/apps/core/middleware.py

@ -0,0 +1,27 @@
from django.contrib import messages
from django.core.signals import got_request_exception
from django.http import HttpResponseRedirect
from django.utils.translation import gettext as _
from .models import ConcurrentUpdateException
class ConcurrencyMiddleware:
"""Intercept :ref:`ConcurrentUpdateException` and redirect to the same view passing an error message."""
def __init__(self, get_response=None):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
return response
def process_exception(self, request, exception):
if isinstance(exception, ConcurrentUpdateException):
got_request_exception.send(sender=self, request=request)
messages.add_message(
request,
messages.ERROR,
_("The object has changed during the transaction."),
)
return HttpResponseRedirect(request.path)

0
django_conc/apps/core/migrations/__init__.py

22
django_conc/apps/core/mixins.py

@ -0,0 +1,22 @@
from django.conf import settings
from django.contrib.auth.mixins import AccessMixin
from django.http import HttpResponseRedirect
from django.shortcuts import render, resolve_url
class HtmxMixin(AccessMixin):
def dispatch(self, request, *args, **kwargs):
if not request.headers.get("Hx-request"):
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
def handle_no_permission(self):
if self.request.user.is_anonymous:
return self.redirect_to_login_redirect()
return HttpResponseRedirect(resolve_url(settings.LOGIN_REDIRECT_URL))
def redirect_to_login_redirect(self):
redire = render(self.request, "core/pages/void.html")
redire["HX-Redirect"] = ""
return redire

131
django_conc/apps/core/models.py

@ -0,0 +1,131 @@
from django import forms
from django.contrib.admin.widgets import AdminIntegerFieldWidget
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils.translation import gettext_lazy as _
class ConcurrentUpdateException(Exception):
"""
Raised when a model can not be saved due to a concurrent update.
"""
def __init__(self, *args, **kwargs):
self.target = kwargs.pop("target")
super().__init__(*args, **kwargs)
class ReadonlyInput(forms.TextInput):
"""
A HiddenInput would be perfect for version fields, but hidden
inputs leave ugly empty rows in the admin. The version must
be submitted, of course, to be checked, so we can't just use
ModelAdmin.readonly_fields.
Pending Django ticket #11277, this displays the version in an
uneditable input so there's no empty row in the admin table.
https://code.djangoproject.com/ticket/11277
"""
def __init__(self, *args, **kwargs):
super(ReadonlyInput, self).__init__(*args, **kwargs)
# just readonly, because disabled won't submit the value
self.attrs["readonly"] = "readonly"
class VersionField(models.PositiveIntegerField):
"""
An integer field to track versions. Every time the model is saved,
it is incremented by one.
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault("default", 0)
super(VersionField, self).__init__(*args, **kwargs)
def formfield(self, **kwargs):
widget = kwargs.get("widget")
if widget:
if issubclass(widget, AdminIntegerFieldWidget):
widget = ReadonlyInput()
else:
widget = forms.HiddenInput
kwargs["widget"] = widget
return super(VersionField, self).formfield(**kwargs)
class VersionedModel(models.Model):
# class VersionedBaseModel:
"""
Model mixin implementing version checking during saving.
When a concurrent update is detected, saving is aborted and
ConcurrentUpdate will be raised.
"""
version = VersionField(_("Version"))
def _do_update(
self, base_qs, using, pk_val, values, update_fields, forced_update
):
version_field = self.get_version_field()
# _do_update is called once for each model in the inheritance
# hierarchy. We only care about the model with the version field.
if version_field.model != base_qs.model:
return super()._do_update(
base_qs, using, pk_val, values, update_fields, forced_update
)
if version_field.attname in self.get_deferred_fields():
# With a deferred VersionField, it's not possible to do any
# sensible concurrency checking, so throw an error. The
# other option would be to treat deferring the VersionField
# the same as excluding it from `update_fields` -- a way to
# bypass checking altogether.
raise RuntimeError(
"It doesn't make sense to save a model with a deferred VersionField"
)
# pre_save may or may not have been called at this point, based on if
# version_field is in update_fields. Since we need to reliably know the
# old version, we can't increment there.
old_version = version_field.value_from_object(self)
# so increment it here instead. Now old_version is reliable.
for i, value_tuple in enumerate(values):
if isinstance(value_tuple[0], VersionField):
assert old_version == value_tuple[2]
values[i] = (
value_tuple[0],
value_tuple[1],
value_tuple[2] + 1,
)
setattr(self, version_field.attname, old_version + 1)
updated = super()._do_update(
base_qs=base_qs.filter(**{version_field.attname: old_version}),
using=using,
pk_val=pk_val,
values=values,
update_fields=(
update_fields if values else None
), # Make sure base_qs is always checked
forced_update=forced_update,
)
if not updated and base_qs.filter(pk=pk_val).exists():
raise ConcurrentUpdateException(target=base_qs)
return updated
def get_version_field(self):
for field in self._meta.fields:
if isinstance(field, VersionField):
return field
raise ImproperlyConfigured(
"VersionedMixin models must have a VersionField"
)
class Meta:
abstract = True

31
django_conc/apps/core/static/core/_hs/main._hs

@ -0,0 +1,31 @@
on every contextmenu
halt
end
behavior HideModal(id)
on click
if id
set :children to Array.from(id.children)
for x in :children
remove x
end
end
end
end
behavior PulseModal(id)
on click
if target == me
transition scale to 1.05 over 0.1s then transition scale to initial over 0.1s
end
end
end
behavior Toast()
on load
transition my *top to 50px
then wait 3s
then transition my *top to initial
then remove me
end
end

226
django_conc/apps/core/static/core/css/main.css

@ -0,0 +1,226 @@
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
display: grid;
text-rendering: optimizeSpeed;
line-height: 1.5rem;
place-content: center;
align-items: center;
height: 100vh;
}
a {
display: inline-block;
line-height: normal;
}
a:not([class]) {
text-decoration-skip-ink: auto;
}
table {
min-width: 600px;
}
tr {
text-align: center;
}
th,
td {
border: 1px solid black;
padding: 0.5rem;
}
input:not([type="checkbox"]) {
color: var(--color-text, #65666d);
display: block;
width: 100%;
height: var(--input-height, calc(1.5em + 0.75rem + 2px));
padding: 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
background-color: fff;
background-clip: padding-box;
border: 1px solid #d1d3e2;
border-radius: 0.35rem;
transition:
border-color 0.15s ease-in-out,
box-shadow 0.15s ease-in-out;
}
input:focus {
color: var(--color-text, #65666d);
background-color: var(--input-color-background, #fff);
border-color: var(--input-color-border-focus, #96c8da);
outline: 0;
box-shadow: 0 0 0 0.1rem var(--input-color-focus, rgba(78, 115, 223, 0.1));
}
.btn {
padding: 0.5rem 1rem;
border: 1px solid black;
background-color: #b9bbbd;
text-decoration: none;
color: white;
font-weight: bold;
cursor: pointer;
text-transform: uppercase;
}
.btn-small {
padding: 0.25rem 0.5rem;
background-color: #b9bbbd;
}
.btn:hover {
background-color: #a9c7ff;
}
.btn-primary {
background-color: #7297f0;
}
.btn-secondary {
background-color: #5b5c60;
}
.btn-danger {
background-color: #e66969;
}
a.btn {
text-decoration: none;
}
.helptext {
font-size: 0.8rem;
}
.errorlist {
background-color: #ff9a92;
color: white;
padding: 1rem;
font-weight: bold;
}
.flex {
display: flex;
}
.align-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.gap-1 {
gap: 1rem;
}
.modal-container {
position: fixed;
display: flex;
align-items: center;
justify-content: center;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
overflow: auto;
z-index: 205;
}
.modal-content {
display: grid;
position: relative;
place-items: center;
grid-template-rows: 50px 1fr 50px;
grid-template-areas: "header" "body" "footer";
margin: auto;
width: 600px;
max-width: calc(100vw - 2rem);
padding: 0.5rem 1rem 1rem 1rem;
background-color: white;
outline: 0;
border-radius: 0.3rem;
z-index: 250;
}
.modal-header {
grid-area: header;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
border-bottom: 1px solid #e5e5e5;
padding-bottom: 0.5rem;
}
.modal-body {
grid-area: body;
padding: 1rem;
width: 100%;
max-height: 100%;
}
.modal-footer {
grid-area: footer;
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
gap: 10px;
padding-top: 1rem;
border-top: 1px solid #e5e5e5;
}
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
backdrop-filter: blur(2px);
background-color: rgba(0, 0, 0, 0.6);
z-index: 200;
}
.alert {
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.alert-error {
background-color: #ff1161;
color: white;
}
.toast {
position: fixed;
width: 50%;
left: 50%;
top: -250px;
transform: translateX(-50%);
padding: 1.5rem;
z-index: 100;
}

1
django_conc/apps/core/static/core/js/vendor/_hyperscript.min.js

File diff suppressed because one or more lines are too long

1
django_conc/apps/core/static/core/js/vendor/htmx.min.js

File diff suppressed because one or more lines are too long

47
django_conc/apps/core/templates/core/base.html

@ -0,0 +1,47 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet"
href="{% static 'core/css/main.css' %}">
<title>
{% block title %}
Django Concurrency
{% endblock title %}
</title>
</head>
<body>
{% if messages %}
{% for message in messages %}
<div class="toast" _="install Toast()">
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
</div>
{% endfor %}
{% endif %}
{% block main %}
{% endblock main %}
<div id="Modal">
</div>
<script>
document.body.addEventListener("htmx:configRequest", (event) => {
event.detail.headers["X-CSRFToken"] = "{{csrf_token}}";
});
</script>
{% block script %}
{% endblock script %}
{% block hyperscript %}
{% endblock hyperscript %}
<script type="text/hyperscript"
src="{% static 'core/_hs/main._hs' %}"></script>
<script src="{% static 'core/js/vendor/_hyperscript.min.js' %}"></script>
<script src="{% static 'core/js/vendor/htmx.min.js' %}"></script>
</body>
</html>

38
django_conc/apps/core/templates/core/pages/confirm_delete.html

@ -0,0 +1,38 @@
{% extends "core/partials/modal/base.html" %}
{% load tag_helpers i18n %}
{% block modal_content %}
<form action="{% url extra_content.urls.delete object.pk %}"
method="post">
{% csrf_token %}
{{ block.super }}
</form>
{% endblock modal_content %}
{% block modal_header %}
<h2>{% translate "Confirm Delete" %}</h2>
{% endblock modal_header %}
{% block modal_body %}
{{ block.super }}
<div class="row mb-5">
<div class="md-12 text-center">
<h3>{% translate "Confirm delete the object" %} "{{ object }}",</h3>
<h4>{% translate "Are you sure ?" %}</h4>
</div>
</div>
{% endblock modal_body %}
{% block modal_footer %}
<button class="btn btn-primary btn-rounded"
hx-post="{% url extra_content.urls.delete object.pk %}"
hx-target="#Modal">
{% translate "Confirm" %}
</button>
<button class="btn btn-secondary btn-rounded"
type="button"
_="install HideModal(id:#Modal)">
{% translate "Cancel" %}
</button>
{% endblock modal_footer %}

42
django_conc/apps/core/templates/core/pages/confirm_exit.html

@ -0,0 +1,42 @@
{% extends "core/partials/modal/base.html" %}
{% load tag_helpers i18n %}
{% block modal_content %}
<form action="{% url 'logout' %}"
method="post">
{% csrf_token %}
{{ block.super }}
</form>
{% endblock modal_content %}
{% block modal_header %}
<h2>
{% translate "Confirm Exit" %}
</h2>
{% endblock modal_header %}
{% block modal_body %}
<div class="row mb-5">
<div class="md-12 text-center">
<h3>
{% translate "Confirm exit application" %},
</h3>
<h4>
{% translate "Are you sure ?" %}
</h4>
</div>
</div>
{% endblock modal_body %}
{% block modal_footer %}
<button class="btn btn-primary btn-rounded" type="submit">
{% translate "Confirm" %}
</button>
<button class="btn btn-secondary btn-rounded"
type="button"
href="javascript:void(0);"
_="install HideModal(id:#Modal)">
{% translate "Cancel" %}
</button>
{% endblock modal_footer %}

12
django_conc/apps/core/templates/core/pages/form.html

@ -0,0 +1,12 @@
{% extends "core/pages/list.html" %}
{% block main %}
<div>
<a class="btn btn-secondary" href="{% url extra_content.urls.index %}">BACK</a>
</div>
<form action="" method="post" autocomplete="off" enctype='multipart/form-data' style="margin-top: 2rem;display: grid;gap:1rem">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">SAVE</button>
</form>
{% endblock main %}

7
django_conc/apps/core/templates/core/pages/index.html

@ -0,0 +1,7 @@
{% extends "core/base.html" %}
{% load static %}
{% block main %}
<a class="btn btn-primary" href="{% url 'core:accounts:index' %}">Users</a>
{% endblock main %}

56
django_conc/apps/core/templates/core/pages/list.html

@ -0,0 +1,56 @@
{% extends "core/base.html" %}
{% load static tag_helpers %}
{% block main %}
<div class="flex align-center justify-between" style="margin-bottom:1rem;">
<span>User: {{ request.user }}</span>
<form action="{% url 'logout' %}" method="post">
{% csrf_token %}
<button
class="btn btn-small"
hx-get="{% url 'core:confirm_exit' %}"
hx-target="#Modal"
>Logout</button>
</form>
</div>
<div>
<a class="btn btn-primary" href="{% url extra_content.urls.new %}">New</a>
</div>
<table style="margin-top:2rem">
{% with form|get_meta:"actions" as actions %}
<tr>
{% if actions %}
<th>Actions</th>
{% endif %}
{% for field in form.fields %}
<th>{{ field|capfirst }}</th>
{% endfor %}
</tr>
{% for object in object_list %}
<tr>
{% if actions %}
<td>
<div class="flex align-center justify-center gap-1">
<a class="btn btn-small btn-secondary" href="{% url extra_content.urls.edit object.pk %}">Edit</a>
<button
type="button"
class="btn btn-small btn-danger"
hx-post="{% url extra_content.urls.confirm object.pk %}"
hx-target="#Modal"
>
Remove
</button>
</div>
</td>
{% endif %}
{% for field in form.fields %}
<td>{{ object|get_field:field }}</td>
{% endfor %}
</tr>
{% endfor %}
{% endwith %}
</table>
{% endblock main %}

0
django_conc/apps/core/templates/core/pages/void.html

36
django_conc/apps/core/templates/core/partials/modal/base.html

@ -0,0 +1,36 @@
{% load tag_helpers i18n %}
<div class="modal-container grid-center"
_="install PulseModal(id:#Modal)">
{% block modal_content %}
<div class="modal-content">
<div class="modal-header">
{% block modal_header %}
<h2>
Modal title
</h2>
{% endblock modal_header %}
</div>
<div class="modal-body o-scroll"
style="max-height:80vh">
{% block modal_body %}
{% endblock modal_body %}
</div>
<div class="modal-footer">
{% block modal_footer %}
<button class="btn btn-secondary"
type="button"
_="install HideModal(id:#Modal)">
{% translate "Cancel" %}
</button>
{% endblock modal_footer %}
</div>
</div>
{% endblock modal_content %}
</div>
<div class="modal-backdrop">
</div>

31
django_conc/apps/core/templatetags/tag_helpers.py

@ -0,0 +1,31 @@
from django import template
from django.db.models.fields.files import FieldFile, ImageFieldFile
from django.utils.safestring import mark_safe
register = template.Library()
@register.filter(name="times")
def times(start, end=None):
"""Return a range starting 1 by default until number informed."""
if end:
return range(int(start), int(end))
return range(1, int(start))
@register.filter(name="get_field")
def get_field(obj, field):
"""Return an object attribute"""
field = getattr(obj, field)
if isinstance(field, ImageFieldFile) or isinstance(field, FieldFile):
if field:
return mark_safe(
f'<a class="btn btn-small btn-secondary" target="_blank" href="{field.url}">attach</a>'
)
return field
@register.filter(name="get_meta")
def get_meta(form, attr):
"""Return an attribute from form meta class"""
return getattr(form.Meta, attr, None)

3
django_conc/apps/core/tests.py

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

17
django_conc/apps/core/urls.py

@ -0,0 +1,17 @@
from django.urls import include, path
from . import views
app_name = "core"
urlpatterns = [
path("", views.Index.as_view(), name="index"),
path("confirm-exit/", views.ConfirmExit.as_view(), name="confirm_exit"),
path(
"accounts/",
include(
"django_conc.apps.accounts.urls",
namespace="accounts",
),
),
]

134
django_conc/apps/core/views.py

@ -0,0 +1,134 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ValidationError
from django.db.models import ProtectedError
from django.shortcuts import render
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DeleteView, ListView, TemplateView, \
UpdateView
from . import mixins
class Common:
app_name = None
model = None
page_title = None
def __init__(self):
self.success_url = reverse_lazy(f"core:{self.app_name}:index")
self.extra_content = {
"urls": {
"index": f"core:{self.app_name}:index",
"new": f"core:{self.app_name}:new",
"edit": f"core:{self.app_name}:edit",
"search": f"core:{self.app_name}:search",
"confirm": f"core:{self.app_name}:confirm_delete",
"delete": f"core:{self.app_name}:delete",
"detail": f"core:{self.app_name}:detail",
},
"page_title": "Index",
}
def get_text_url(self, action="index"):
return f"core:{self.app_name}:{action}"
def get_url(self, action, kwargs=None):
return reverse_lazy(self.get_text_url(action), kwargs=kwargs)
def get_pk(self, name="pk"):
return self.kwargs.get(name)
def htmx_redirect_to(
self, request, url, context={}, template_name="core/pages/void.html"
):
redire = render(
request=request, template_name=template_name, context=context
)
redire["HX-Redirect"] = url
return redire
class Index(LoginRequiredMixin, TemplateView):
template_name = "core/pages/index.html"
class CrudViews(LoginRequiredMixin, Common):
def __init__(self):
super().__init__()
self.extra_content.update(page_title=self.page_title)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["extra_content"] = self.extra_content
return context
class Base(CrudViews, TemplateView):
template_name = "core/pages/void.html"
class List(CrudViews, ListView):
template_name = "core/pages/index.html"
paginate_by = 10
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["form"] = self.form_class
return context
class New(CrudViews, CreateView):
template_name = "core/pages/form.html"
page_title = "New"
class Update(CrudViews, UpdateView):
template_name = "core/pages/form.html"
page_title = "Update"
class ConfirmDelete(mixins.HtmxMixin, CrudViews, TemplateView):
template_name = "core/pages/confirm_delete.html"
action_url = None
def post(self, request, *args, **kwargs):
data = {
"object": self.model.objects.filter(pk=kwargs.get("pk")).first(),
"extra_content": self.extra_content,
}
return render(request, self.template_name, context=data)
class Delete(CrudViews, DeleteView):
template_name = "core/pages/confirm_delete.html"
def form_valid(self, form):
context = self.get_context_data()
try:
self.object.delete()
except ProtectedError as e:
form.add_error(
None,
ValidationError(
_(
"Cannot delete some instances because they are referenced through protected objects: %(value)s)"
),
code="protected",
params={"value": e.protected_objects},
),
)
context.update(form=form, extra_content=self.extra_content)
return self.render_to_response(context=context)
return self.htmx_redirect_to(
request=self.request, url=self.get_success_url(), context=context
)
class ConfirmExit(LoginRequiredMixin, mixins.HtmxMixin, TemplateView):
template_name = "core/pages/confirm_exit.html"
http_method_names = ["get"]

16
django_conc/asgi.py

@ -0,0 +1,16 @@
"""
ASGI config for django_conc 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", "django_conc.settings")
application = get_asgi_application()

145
django_conc/settings.py

@ -0,0 +1,145 @@
"""
Django settings for django_conc project.
Generated by 'django-admin startproject' using Django 5.0.2.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
import os
from pathlib import Path
from decouple import Csv, config
from dj_database_url import parse as dburl
# 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/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config("DEBUG", default=False, cast=bool)
ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=Csv())
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_extensions",
"django_conc.apps.core",
"django_conc.apps.accounts",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django_conc.apps.core.middleware.ConcurrencyMiddleware",
"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 = "django_conc.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": ["templates", BASE_DIR / "templates"],
"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 = "django_conc.wsgi.application"
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
default_dburl = "sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3")
DATABASES = {
"default": config("DATABASE_URL", default=default_dburl, cast=dburl),
}
# 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",
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/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/5.0/howto/static-files/
STATIC_URL = "static/"
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "staticfiles"),
]
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = "accounts.User"
LOGIN_URL = "login"
LOGIN_REDIRECT_URL = "core:index"
LOGOUT_REDIRECT_URL = "login"

33
django_conc/urls.py

@ -0,0 +1,33 @@
"""
URL configuration for django_conc 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.contrib.auth import views as auth_views
from django.urls import include, path
urlpatterns = [
path("", include("django_conc.apps.core.urls", namespace="core")),
path("auth/sign_in/", auth_views.LoginView.as_view(), name="login"),
path("auth/logout/", auth_views.LogoutView.as_view(), name="logout"),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
urlpatterns += [
path("admin/", admin.site.urls),
]

16
django_conc/wsgi.py

@ -0,0 +1,16 @@
"""
WSGI config for django_conc 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", "django_conc.settings")
application = get_wsgi_application()

22
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", "django_conc.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()

345
poetry.lock

@ -0,0 +1,345 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "asgiref"
version = "3.7.2"
description = "ASGI specs, helper code, and adapters"
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 = "dj-database-url"
version = "2.1.0"
description = "Use Database URLs in your Django Application."
optional = false
python-versions = "*"
files = [
{file = "dj-database-url-2.1.0.tar.gz", hash = "sha256:f2042cefe1086e539c9da39fad5ad7f61173bf79665e69bf7e4de55fa88b135f"},
{file = "dj_database_url-2.1.0-py3-none-any.whl", hash = "sha256:04bc34b248d4c21aaa13e4ab419ae6575ef5f10f3df735ce7da97722caa356e0"},
]
[package.dependencies]
Django = ">=3.2"
typing-extensions = ">=3.10.0.0"
[[package]]
name = "dj-static"
version = "0.0.6"
description = "Serve production static files with Django."
optional = false
python-versions = "*"
files = [
{file = "dj-static-0.0.6.tar.gz", hash = "sha256:032ec1c532617922e6e3e956d504a6fb1acce4fc1c7c94612d0fda21828ce8ef"},
]
[package.dependencies]
static3 = "*"
[[package]]
name = "django"
version = "5.0.2"
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.2-py3-none-any.whl", hash = "sha256:56ab63a105e8bb06ee67381d7b65fe6774f057e41a8bab06c8020c8882d8ecd4"},
{file = "Django-5.0.2.tar.gz", hash = "sha256:b5bb1d11b2518a5f91372a282f24662f58f66749666b0a286ab057029f728080"},
]
[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-cleanup"
version = "8.1.0"
description = "Deletes old files."
optional = false
python-versions = "*"
files = [
{file = "django-cleanup-8.1.0.tar.gz", hash = "sha256:70df905076a44e7a111b31198199af633dee08876e199e6dce36ca8dd6b8b10f"},
{file = "django_cleanup-8.1.0-py2.py3-none-any.whl", hash = "sha256:7903873ea73b3f7e61e055340d27dba49b70634f60c87a573ad748e172836458"},
]
[[package]]
name = "django-extensions"
version = "3.2.3"
description = "Extensions for Django"
optional = false
python-versions = ">=3.6"
files = [
{file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"},
{file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"},
]
[package.dependencies]
Django = ">=3.2"
[[package]]
name = "django-storages"
version = "1.14.2"
description = "Support for many storage backends in Django"
optional = false
python-versions = ">=3.7"
files = [
{file = "django-storages-1.14.2.tar.gz", hash = "sha256:51b36af28cc5813b98d5f3dfe7459af638d84428c8df4a03990c7d74d1bea4e5"},
{file = "django_storages-1.14.2-py3-none-any.whl", hash = "sha256:1db759346b52ada6c2efd9f23d8241ecf518813eb31db9e2589207174f58f6ad"},
]
[package.dependencies]
Django = ">=3.2"
[package.extras]
azure = ["azure-core (>=1.13)", "azure-storage-blob (>=12)"]
boto3 = ["boto3 (>=1.4.4)"]
dropbox = ["dropbox (>=7.2.1)"]
google = ["google-cloud-storage (>=1.27)"]
libcloud = ["apache-libcloud"]
s3 = ["boto3 (>=1.4.4)"]
sftp = ["paramiko (>=1.15)"]
[[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 = "psycopg2-binary"
version = "2.9.9"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
optional = false
python-versions = ">=3.7"
files = [
{file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"},
{file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"},
{file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"},
{file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"},
{file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"},
{file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"},
{file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"},
]
[[package]]
name = "python-decouple"
version = "3.8"
description = "Strict separation of settings from code."
optional = false
python-versions = "*"
files = [
{file = "python-decouple-3.8.tar.gz", hash = "sha256:ba6e2657d4f376ecc46f77a3a615e058d93ba5e465c01bbe57289bfb7cce680f"},
{file = "python_decouple-3.8-py3-none-any.whl", hash = "sha256:d0d45340815b25f4de59c974b855bb38d03151d81b037d9e3f463b0c9f8cbd66"},
]
[[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 = "static3"
version = "0.7.0"
description = "A really simple WSGI way to serve static (or mixed) content."
optional = false
python-versions = "*"
files = [
{file = "static3-0.7.0.tar.gz", hash = "sha256:674641c64bc75507af2eb20bef7e7e3593dca993dec6674be108fa15b42f47c8"},
]
[package.extras]
genshimagic = ["Genshi"]
kidmagic = ["kid"]
[[package]]
name = "typing-extensions"
version = "4.9.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
]
[[package]]
name = "tzdata"
version = "2023.4"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
files = [
{file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"},
{file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "0f777838b72c1d9452050ee4b309fb5d50f9407ea5fcad2138b188288ce1bbfa"

75
pyproject.toml

@ -0,0 +1,75 @@
[tool.poetry]
name = "django_conc"
version = "0.1.0"
description = ""
authors = ["Lucas F. <lucas@lucasf.dev>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
django = "^5.0.2"
dj-database-url = "^2.1.0"
django-storages = "^1.14.2"
pillow = "^10.2.0"
python-decouple = "^3.8"
django-cleanup = "^8.1.0"
psycopg2-binary = "^2.9.9"
django-extensions = "^3.2.3"
dj-static = "^0.0.6"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.isort]
profile = "black"
line_length = 89
multi_line_output = 2
include_trailing_comma = false
extend_skip = [
".git",
".hg",
".mypy_cache",
".tox",
".venv",
"_build",
"buck-out",
"build",
"dist",
"migrations",
]
[tool.black]
line-length = 79
include = '\.pyi?$'
force-exclude = '''
/(
\.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| migrations
)/
'''
[tool.djlint]
profile = "django"
max_attribute_length = 0
blank_line_after_tag = "load, extends, endblock, endwith"
line_break_after_multiline_tag = true
format_attribute_template_tags = true
[tool.pytest.ini_options]
minversion = "6.0"
DJANGO_SETTINGS_MODULE = "django_conc.settings"
python_files = [
"tests.py",
"test_*.py",
"*_tests.py",
]

11
templates/registration/login.html

@ -0,0 +1,11 @@
{% extends "core/pages/index.html" %}
{% load static %}
{% block main %}
<form action="" method="post" autocomplete="off" style="display: grid;gap: 1rem;">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-primary" type="submit" style="margin-top:1rem;">Login</button>
</form>
{% endblock main %}
Loading…
Cancel
Save