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