Lucas
12 months ago
commit
39cd44d022
54 changed files with 2144 additions and 0 deletions
@ -0,0 +1,11 @@ |
|||
venv |
|||
.venv |
|||
.env |
|||
*.sqlite3 |
|||
media |
|||
*__pycache__ |
|||
.idea |
|||
*.code-workspace |
|||
.report.json |
|||
report.json |
|||
.vscode |
@ -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 |
@ -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 |
|||
``` |
|||
|
@ -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,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) |
@ -0,0 +1,7 @@ |
|||
from django.apps import AppConfig |
|||
|
|||
|
|||
class AccountsConfig(AppConfig): |
|||
default_auto_field = "django.db.models.BigAutoField" |
|||
name = "accounts" |
|||
default = False |
@ -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 |
@ -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"] |
@ -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) |
@ -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,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 |
@ -0,0 +1,3 @@ |
|||
{% extends "core/pages/form.html" %} |
|||
|
|||
{% load static %} |
@ -0,0 +1,4 @@ |
|||
{% extends "core/pages/list.html" %} |
|||
|
|||
{% load static %} |
|||
|
@ -0,0 +1,3 @@ |
|||
from django.test import TestCase |
|||
|
|||
# Create your tests here. |
@ -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"), |
|||
] |
@ -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): ... |
@ -0,0 +1 @@ |
|||
|
@ -0,0 +1,3 @@ |
|||
from django.contrib import admin |
|||
|
|||
# Register your models here. |
@ -0,0 +1,7 @@ |
|||
from django.apps import AppConfig |
|||
|
|||
|
|||
class CoreConfig(AppConfig): |
|||
default_auto_field = "django.db.models.BigAutoField" |
|||
name = "core" |
|||
default = False |
@ -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 |
@ -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!")) |
|||
) |
@ -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() |
@ -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,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 |
@ -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 |
@ -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 |
@ -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; |
|||
} |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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> |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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,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> |
@ -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) |
@ -0,0 +1,3 @@ |
|||
from django.test import TestCase |
|||
|
|||
# Create your tests here. |
@ -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", |
|||
), |
|||
), |
|||
] |
@ -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"] |
@ -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() |
@ -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" |
@ -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), |
|||
] |
@ -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() |
@ -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() |
@ -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" |
@ -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", |
|||
] |
@ -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…
Reference in new issue