initial
This commit is contained in:
commit
39cd44d022
54 changed files with 2144 additions and 0 deletions
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
venv
|
||||||
|
.venv
|
||||||
|
.env
|
||||||
|
*.sqlite3
|
||||||
|
media
|
||||||
|
*__pycache__
|
||||||
|
.idea
|
||||||
|
*.code-workspace
|
||||||
|
.report.json
|
||||||
|
report.json
|
||||||
|
.vscode
|
||||||
40
Makefile
Normal file
40
Makefile
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
.DEFAULT_GOAL = default
|
||||||
|
SCRIPT = contrib/scripts/make_scripts.sh
|
||||||
|
|
||||||
|
## @ env
|
||||||
|
.PHONY: env
|
||||||
|
env: ## creates a .env file
|
||||||
|
@./${SCRIPT} make_env_file
|
||||||
|
|
||||||
|
## @ task
|
||||||
|
.PHONY: check run shell_plus clear_migrations show_migrations migrations migrate elements
|
||||||
|
check: ## same as manage.py check
|
||||||
|
@./${SCRIPT} check
|
||||||
|
|
||||||
|
run: ## same as manage.py run server
|
||||||
|
@./${SCRIPT} run
|
||||||
|
|
||||||
|
shell_plus: ## same as .manage.py shell_plus
|
||||||
|
@./${SCRIPT} shell_plus
|
||||||
|
|
||||||
|
clear_migrations: ## same as manage.py showmigrations
|
||||||
|
@./${SCRIPT} clear_migrations
|
||||||
|
|
||||||
|
show_migrations: ## same as manage.py showmigrations
|
||||||
|
@./${SCRIPT} show_migrations
|
||||||
|
|
||||||
|
migrations: ## same as manage.py makemigrations
|
||||||
|
@./${SCRIPT} migrations
|
||||||
|
|
||||||
|
migrate: ## same as manage.py migrate
|
||||||
|
@./${SCRIPT} migrate
|
||||||
|
|
||||||
|
elements: ## create initial app elements
|
||||||
|
@./${SCRIPT} elements
|
||||||
|
|
||||||
|
## @ help
|
||||||
|
.PHONY: help
|
||||||
|
help: ## display all make commands
|
||||||
|
@./${SCRIPT} help $(MAKEFILE_LIST)
|
||||||
|
|
||||||
|
default: help
|
||||||
53
README.md
Normal file
53
README.md
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Django Conc
|
||||||
|
|
||||||
|
É um exemplo de aplicação Django para testar concorrência no momento de atualizar um modelo.
|
||||||
|
Fortemente inspirado em [django-optimistic-lock](https://github.com/gavinwahl/django-optimistic-lock).
|
||||||
|
|
||||||
|
|
||||||
|
## Instalação
|
||||||
|
|
||||||
|
via `pip`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.lucasf.dev/public/django_conc.git
|
||||||
|
cd django_conc
|
||||||
|
python -m venv .venv
|
||||||
|
. ./.venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
via `poetry`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.lucasf.dev/public/django_conc.git
|
||||||
|
cd django_conc
|
||||||
|
poetry shell
|
||||||
|
poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Criando arquivo .env
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make env
|
||||||
|
```
|
||||||
|
|
||||||
|
## Criando elementos para aplicação
|
||||||
|
|
||||||
|
primeiro informe no arquivo `.env` gerado os valores para as seguintes variáveis:
|
||||||
|
|
||||||
|
ADMIN_USERNAME
|
||||||
|
ADMIN_EMAIL
|
||||||
|
ADMIN_PASSWORD
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make elements
|
||||||
|
```
|
||||||
|
|
||||||
|
## Iniciar a aplicação
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make run
|
||||||
|
```
|
||||||
|
|
||||||
120
contrib/scripts/make_scripts.sh
Executable file
120
contrib/scripts/make_scripts.sh
Executable file
|
|
@ -0,0 +1,120 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
BASEDIR="$(cd "$(dirname "$1")" && pwd)"
|
||||||
|
CHARS="abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
|
||||||
|
for ((i=0;i<${#CHARS};i++)); do ARRAY[$i]="${CHARS:i:1}"; done
|
||||||
|
MSG_SUCCESS="DONE!"
|
||||||
|
POETRY=0
|
||||||
|
PYTHON=0
|
||||||
|
|
||||||
|
key_gen() {
|
||||||
|
for ((c=1; c<=50; c++)); do
|
||||||
|
KEY="$KEY${ARRAY[$((RANDOM % 50))]}"
|
||||||
|
done
|
||||||
|
echo $KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
make_env_file() {
|
||||||
|
if [[ ! -f ".env" ]]; then
|
||||||
|
ENV="SECRET_KEY='$(key_gen)'\n
|
||||||
|
ALLOWED_HOSTS=localhost, 10.0.2.2, 127.0.0.1\n
|
||||||
|
DEBUG=True\n\n
|
||||||
|
#DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5433/db\n\n
|
||||||
|
ADMIN_USERNAME=\n
|
||||||
|
ADMIN_EMAIL=\n
|
||||||
|
ADMIN_PASSWORD=\n\n
|
||||||
|
EMAIL_HOST=\n
|
||||||
|
EMAIL_PORT=\n
|
||||||
|
EMAIL_HOST_USER=\n
|
||||||
|
EMAIL_HOST_PASSWORD=\n
|
||||||
|
EMAIL_USE_TLS=True\n
|
||||||
|
DEFAULT_FROM_EMAIL=
|
||||||
|
"
|
||||||
|
|
||||||
|
$(echo -e $ENV | sed -e 's/^[ \t]*//' > .env)
|
||||||
|
echo "ENV FILE - $MSG_SUCCESS"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_poetry() {
|
||||||
|
if command -v poetry &> /dev/null; then
|
||||||
|
POETRY=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_python() {
|
||||||
|
if command -v python3 &> /dev/null; then
|
||||||
|
PYTHON=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
venv_name() {
|
||||||
|
if [[ -d "$BASEDIR/.venv" ]]; then
|
||||||
|
echo ".venv"
|
||||||
|
fi
|
||||||
|
if [[ -d "$BASEDIR/venv" ]]; then
|
||||||
|
echo "venv"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
python_name() {
|
||||||
|
if [[ PYTHON -eq 1 ]]; then
|
||||||
|
echo "python3"
|
||||||
|
else
|
||||||
|
echo "python"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
help() {
|
||||||
|
awk 'BEGIN {FS="## @ "; print "Usage: make";} /^## @ / { printf "\033[31m\n" substr($1, 5) "\n";} {FS=" ## ";} /^[a-zA-Z_-]+:.*? ##/ { print "\033[33m -", $1 "\033[37m", $2}' $ARG
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
$(venv_name)/bin/$(python_name) manage.py runserver 0.0.0.0:8000
|
||||||
|
}
|
||||||
|
|
||||||
|
shell_plus() {
|
||||||
|
$(venv_name)/bin/$(python_name) manage.py shell_plus
|
||||||
|
}
|
||||||
|
|
||||||
|
check() {
|
||||||
|
$(venv_name)/bin/$(python_name) manage.py check
|
||||||
|
IS_OK=$?
|
||||||
|
}
|
||||||
|
|
||||||
|
show_migrations() {
|
||||||
|
$(venv_name)/bin/$(python_name) manage.py showmigrations
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations() {
|
||||||
|
$(venv_name)/bin/$(python_name) manage.py makemigrations
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate() {
|
||||||
|
$(venv_name)/bin/$(python_name) manage.py migrate
|
||||||
|
}
|
||||||
|
|
||||||
|
check() {
|
||||||
|
$(venv_name)/bin/$(python_name) manage.py check
|
||||||
|
}
|
||||||
|
|
||||||
|
elements() {
|
||||||
|
$(venv_name)/bin/$(python_name) manage.py elements
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_migrations() {
|
||||||
|
find $BASEDIR -path '*/migrations/*.py' -not -name '__init__.py' -not -path '*/.venv/*' -delete
|
||||||
|
find $BASEDIR -path '*/migrations/*.pyc' -not -name '__init__.py' -not -path '*/.venv/*' -delete
|
||||||
|
if [[ -f $BASEDIR/media/ ]]; then
|
||||||
|
rm $BASEDIR/media/*
|
||||||
|
fi
|
||||||
|
if [[ -f db.sqlite3 ]];then
|
||||||
|
rm db.sqlite3
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_python
|
||||||
|
verify_poetry
|
||||||
|
ARG=$2
|
||||||
|
$1
|
||||||
0
django_conc/__init__.py
Normal file
0
django_conc/__init__.py
Normal file
0
django_conc/apps/__init__.py
Normal file
0
django_conc/apps/__init__.py
Normal file
0
django_conc/apps/accounts/__init__.py
Normal file
0
django_conc/apps/accounts/__init__.py
Normal file
64
django_conc/apps/accounts/admin.py
Normal file
64
django_conc/apps/accounts/admin.py
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.decorators.csrf import csrf_protect
|
||||||
|
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
csrf_protect_m = method_decorator(csrf_protect)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseUserAdmin(UserAdmin):
|
||||||
|
fieldsets = (
|
||||||
|
(None, {"fields": ("username", "password")}),
|
||||||
|
(
|
||||||
|
_("Personal info"),
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"email",
|
||||||
|
"avatar",
|
||||||
|
"use_dark_theme",
|
||||||
|
"version",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
_("Permissions"),
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"is_active",
|
||||||
|
"is_staff",
|
||||||
|
"is_superuser",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
||||||
|
)
|
||||||
|
add_fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"classes": ("wide",),
|
||||||
|
"fields": ("username", "password1", "password2"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
list_display = (
|
||||||
|
"username",
|
||||||
|
"email",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"is_staff",
|
||||||
|
)
|
||||||
|
list_filter = (
|
||||||
|
"is_staff",
|
||||||
|
"is_superuser",
|
||||||
|
"is_active",
|
||||||
|
)
|
||||||
|
search_fields = ("username", "first_name", "last_name", "email")
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(User, BaseUserAdmin)
|
||||||
7
django_conc/apps/accounts/apps.py
Normal file
7
django_conc/apps/accounts/apps.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "accounts"
|
||||||
|
default = False
|
||||||
13
django_conc/apps/accounts/backends.py
Normal file
13
django_conc/apps/accounts/backends.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.backends import ModelBackend as BaseModelBackend
|
||||||
|
|
||||||
|
|
||||||
|
class ModelBackend(BaseModelBackend):
|
||||||
|
def authenticate(self, request, username=None, password=None):
|
||||||
|
if not username is None:
|
||||||
|
try:
|
||||||
|
user = get_user_model().objects.get(email=username)
|
||||||
|
if user.check_password(password):
|
||||||
|
return user
|
||||||
|
except get_user_model().DoesNotExist:
|
||||||
|
pass
|
||||||
30
django_conc/apps/accounts/forms.py
Normal file
30
django_conc/apps/accounts/forms.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.forms import UserCreationForm
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
class UserAdminForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = UserCreationForm.Meta.fields + (
|
||||||
|
"username",
|
||||||
|
"email",
|
||||||
|
"first_name",
|
||||||
|
"is_active",
|
||||||
|
"is_staff",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ListUserForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ["username", "email", "is_active", "avatar", "version"]
|
||||||
|
actions = True
|
||||||
|
|
||||||
|
|
||||||
|
class NewUserForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ["username", "email", "is_active", "avatar", "version"]
|
||||||
6
django_conc/apps/accounts/managers.py
Normal file
6
django_conc/apps/accounts/managers.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.contrib.auth.models import UserManager
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserManager(UserManager):
|
||||||
|
def all(self):
|
||||||
|
return self.get_queryset().filter(is_superuser=False)
|
||||||
51
django_conc/apps/accounts/migrations/0001_initial.py
Normal file
51
django_conc/apps/accounts/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Generated by Django 5.0.2 on 2024-02-11 14:18
|
||||||
|
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
import django.utils.timezone
|
||||||
|
import django_conc.apps.accounts.managers
|
||||||
|
import django_conc.apps.core.helpers
|
||||||
|
import django_conc.apps.core.models
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
|
('version', django_conc.apps.core.models.VersionField(default=0, verbose_name='Version')),
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='Identifier')),
|
||||||
|
('avatar', models.ImageField(blank=True, null=True, upload_to=django_conc.apps.core.helpers.UploadToPathAndRename('user_avatar'), verbose_name='Photo')),
|
||||||
|
('is_staff', models.BooleanField(default=False, verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=False, verbose_name='active')),
|
||||||
|
('is_admin', models.BooleanField(default=False, verbose_name='is admin')),
|
||||||
|
('use_dark_theme', models.BooleanField(default=False, verbose_name='Uses dark theme')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'User',
|
||||||
|
'verbose_name_plural': 'Users',
|
||||||
|
'ordering': ['username', 'first_name'],
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django_conc.apps.accounts.managers.CustomUserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
django_conc/apps/accounts/migrations/__init__.py
Normal file
0
django_conc/apps/accounts/migrations/__init__.py
Normal file
41
django_conc/apps/accounts/models.py
Normal file
41
django_conc/apps/accounts/models.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from ..core.helpers import UploadToPathAndRename
|
||||||
|
from ..core.models import VersionedModel
|
||||||
|
from . import managers
|
||||||
|
|
||||||
|
|
||||||
|
class User(VersionedModel, AbstractUser):
|
||||||
|
id = models.UUIDField(
|
||||||
|
_("Identifier"), primary_key=True, default=uuid.uuid4, editable=False
|
||||||
|
)
|
||||||
|
avatar = models.ImageField(
|
||||||
|
_("Photo"),
|
||||||
|
upload_to=UploadToPathAndRename("user_avatar"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
is_staff = models.BooleanField(_("staff status"), default=False)
|
||||||
|
is_active = models.BooleanField(_("active"), default=False)
|
||||||
|
is_admin = models.BooleanField(_("is admin"), default=False)
|
||||||
|
use_dark_theme = models.BooleanField(_("Uses dark theme"), default=False)
|
||||||
|
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
|
||||||
|
|
||||||
|
EMAIL_FIELD = "email"
|
||||||
|
USERNAME_FIELD = "username"
|
||||||
|
REQUIRED_FIELDS = ["email"]
|
||||||
|
|
||||||
|
objects = managers.CustomUserManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("User")
|
||||||
|
verbose_name_plural = _("Users")
|
||||||
|
ordering = ["username", "first_name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.first_name or self.username
|
||||||
3
django_conc/apps/accounts/templates/accounts/form.html
Normal file
3
django_conc/apps/accounts/templates/accounts/form.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% extends "core/pages/form.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
4
django_conc/apps/accounts/templates/accounts/index.html
Normal file
4
django_conc/apps/accounts/templates/accounts/index.html
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{% extends "core/pages/list.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
3
django_conc/apps/accounts/tests.py
Normal file
3
django_conc/apps/accounts/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
17
django_conc/apps/accounts/urls.py
Normal file
17
django_conc/apps/accounts/urls.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "accounts"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", views.Index.as_view(), name="index"),
|
||||||
|
path("new/", views.New.as_view(), name="new"),
|
||||||
|
path("edit/<uuid:pk>/", views.Edit.as_view(), name="edit"),
|
||||||
|
path(
|
||||||
|
"confirm-delete/<uuid:pk>/",
|
||||||
|
views.ConfirmDelete.as_view(),
|
||||||
|
name="confirm_delete",
|
||||||
|
),
|
||||||
|
path("delete/<uuid:pk>/", views.Delete.as_view(), name="delete"),
|
||||||
|
]
|
||||||
30
django_conc/apps/accounts/views.py
Normal file
30
django_conc/apps/accounts/views.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
from ..core import views as cv
|
||||||
|
from . import forms
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
class Common(cv.Common):
|
||||||
|
app_name = "accounts"
|
||||||
|
model = User
|
||||||
|
|
||||||
|
|
||||||
|
class Crud(Common):
|
||||||
|
form_class = forms.NewUserForm
|
||||||
|
|
||||||
|
|
||||||
|
class Index(Common, cv.List):
|
||||||
|
template_name = "accounts/index.html"
|
||||||
|
paginate_by = 10
|
||||||
|
form_class = forms.ListUserForm
|
||||||
|
|
||||||
|
|
||||||
|
class New(Crud, cv.New): ...
|
||||||
|
|
||||||
|
|
||||||
|
class Edit(Crud, cv.Update): ...
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmDelete(Crud, cv.ConfirmDelete): ...
|
||||||
|
|
||||||
|
|
||||||
|
class Delete(Common, cv.Delete): ...
|
||||||
1
django_conc/apps/core/__init__.py
Normal file
1
django_conc/apps/core/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
3
django_conc/apps/core/admin.py
Normal file
3
django_conc/apps/core/admin.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
7
django_conc/apps/core/apps.py
Normal file
7
django_conc/apps/core/apps.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "core"
|
||||||
|
default = False
|
||||||
69
django_conc/apps/core/helpers.py
Normal file
69
django_conc/apps/core/helpers.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import os
|
||||||
|
from enum import Enum
|
||||||
|
from io import BytesIO
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.utils.deconstruct import deconstructible
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
@deconstructible
|
||||||
|
class UploadToPathAndRename(object):
|
||||||
|
def __init__(self, path):
|
||||||
|
self.sub_path = path
|
||||||
|
|
||||||
|
def __call__(self, instance, filename):
|
||||||
|
ext = filename.split(".")[-1]
|
||||||
|
filename = "{}.{}".format(uuid4().hex, ext)
|
||||||
|
return os.path.join(self.sub_path, filename)
|
||||||
|
|
||||||
|
|
||||||
|
class AvatarThumbnail:
|
||||||
|
def create_thumb(self, image, size=(200, 200)):
|
||||||
|
temp_file = BytesIO()
|
||||||
|
img = Image.open(image).convert("RGB")
|
||||||
|
|
||||||
|
if (
|
||||||
|
isinstance(size, tuple)
|
||||||
|
and len(size) == 2
|
||||||
|
and isinstance(size[0], int)
|
||||||
|
and isinstance(size[1], int)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
width, height = img.size
|
||||||
|
if width > size[0] and height > size[1]:
|
||||||
|
img.thumbnail((width, height))
|
||||||
|
|
||||||
|
if height < width:
|
||||||
|
left = (width - height) / 2
|
||||||
|
right = (width + height) / 2
|
||||||
|
top = 0
|
||||||
|
bottom = height
|
||||||
|
img = img.crop((left, top, right, bottom))
|
||||||
|
|
||||||
|
elif width < height:
|
||||||
|
left = 0
|
||||||
|
right = width
|
||||||
|
top = 0
|
||||||
|
bottom = width
|
||||||
|
img = img.crop((left, top, right, bottom))
|
||||||
|
|
||||||
|
if width > size[0] and height > size[1]:
|
||||||
|
img.thumbnail((size[0], size[1]))
|
||||||
|
|
||||||
|
img.save(temp_file, format="WEBP")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
return temp_file
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
"The size must be a tuple of integer with length and width values. Example (350,350)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_mobile(request):
|
||||||
|
if "mobile" in request.META.get("HTTP_USER_AGENT", "").lower():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
35
django_conc/apps/core/management/commands/elements.py
Normal file
35
django_conc/apps/core/management/commands/elements.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
from decouple import config
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from django_conc.apps.accounts.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class AppException(Exception, BaseCommand):
|
||||||
|
def __init__(self, exception):
|
||||||
|
print(
|
||||||
|
BaseCommand().stdout.write(
|
||||||
|
BaseCommand().style.NOTICE(f"Error: {exception}")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = _("Creates initial information for application")
|
||||||
|
su_created = False
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
if not User.objects.filter(username=config("ADMIN_USERNAME")):
|
||||||
|
su = User.objects.create_superuser(
|
||||||
|
username=config("ADMIN_USERNAME"),
|
||||||
|
email=config("ADMIN_EMAIL"),
|
||||||
|
password=config("ADMIN_PASSWORD"),
|
||||||
|
is_active=True,
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
if su:
|
||||||
|
self.stdout.write(self.style.SUCCESS(_("Superuser created!")))
|
||||||
|
else:
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.NOTICE(_("Superuser already exists!"))
|
||||||
|
)
|
||||||
11
django_conc/apps/core/managers.py
Normal file
11
django_conc/apps/core/managers.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class BaseManager(models.Manager):
|
||||||
|
def find(self, pk):
|
||||||
|
queryset = self.get_queryset().filter(pk=pk)
|
||||||
|
|
||||||
|
if queryset:
|
||||||
|
return queryset.first()
|
||||||
|
|
||||||
|
return self.queryset.none()
|
||||||
27
django_conc/apps/core/middleware.py
Normal file
27
django_conc/apps/core/middleware.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.core.signals import got_request_exception
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from .models import ConcurrentUpdateException
|
||||||
|
|
||||||
|
|
||||||
|
class ConcurrencyMiddleware:
|
||||||
|
"""Intercept :ref:`ConcurrentUpdateException` and redirect to the same view passing an error message."""
|
||||||
|
|
||||||
|
def __init__(self, get_response=None):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
response = self.get_response(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def process_exception(self, request, exception):
|
||||||
|
if isinstance(exception, ConcurrentUpdateException):
|
||||||
|
got_request_exception.send(sender=self, request=request)
|
||||||
|
messages.add_message(
|
||||||
|
request,
|
||||||
|
messages.ERROR,
|
||||||
|
_("The object has changed during the transaction."),
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(request.path)
|
||||||
0
django_conc/apps/core/migrations/__init__.py
Normal file
0
django_conc/apps/core/migrations/__init__.py
Normal file
22
django_conc/apps/core/mixins.py
Normal file
22
django_conc/apps/core/mixins.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.mixins import AccessMixin
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.shortcuts import render, resolve_url
|
||||||
|
|
||||||
|
|
||||||
|
class HtmxMixin(AccessMixin):
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if not request.headers.get("Hx-request"):
|
||||||
|
return self.handle_no_permission()
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def handle_no_permission(self):
|
||||||
|
if self.request.user.is_anonymous:
|
||||||
|
return self.redirect_to_login_redirect()
|
||||||
|
|
||||||
|
return HttpResponseRedirect(resolve_url(settings.LOGIN_REDIRECT_URL))
|
||||||
|
|
||||||
|
def redirect_to_login_redirect(self):
|
||||||
|
redire = render(self.request, "core/pages/void.html")
|
||||||
|
redire["HX-Redirect"] = ""
|
||||||
|
return redire
|
||||||
131
django_conc/apps/core/models.py
Normal file
131
django_conc/apps/core/models.py
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.admin.widgets import AdminIntegerFieldWidget
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class ConcurrentUpdateException(Exception):
|
||||||
|
"""
|
||||||
|
Raised when a model can not be saved due to a concurrent update.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.target = kwargs.pop("target")
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ReadonlyInput(forms.TextInput):
|
||||||
|
"""
|
||||||
|
A HiddenInput would be perfect for version fields, but hidden
|
||||||
|
inputs leave ugly empty rows in the admin. The version must
|
||||||
|
be submitted, of course, to be checked, so we can't just use
|
||||||
|
ModelAdmin.readonly_fields.
|
||||||
|
|
||||||
|
Pending Django ticket #11277, this displays the version in an
|
||||||
|
uneditable input so there's no empty row in the admin table.
|
||||||
|
|
||||||
|
https://code.djangoproject.com/ticket/11277
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ReadonlyInput, self).__init__(*args, **kwargs)
|
||||||
|
# just readonly, because disabled won't submit the value
|
||||||
|
self.attrs["readonly"] = "readonly"
|
||||||
|
|
||||||
|
|
||||||
|
class VersionField(models.PositiveIntegerField):
|
||||||
|
"""
|
||||||
|
An integer field to track versions. Every time the model is saved,
|
||||||
|
it is incremented by one.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs.setdefault("default", 0)
|
||||||
|
super(VersionField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def formfield(self, **kwargs):
|
||||||
|
widget = kwargs.get("widget")
|
||||||
|
if widget:
|
||||||
|
if issubclass(widget, AdminIntegerFieldWidget):
|
||||||
|
widget = ReadonlyInput()
|
||||||
|
else:
|
||||||
|
widget = forms.HiddenInput
|
||||||
|
kwargs["widget"] = widget
|
||||||
|
return super(VersionField, self).formfield(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class VersionedModel(models.Model):
|
||||||
|
# class VersionedBaseModel:
|
||||||
|
"""
|
||||||
|
Model mixin implementing version checking during saving.
|
||||||
|
When a concurrent update is detected, saving is aborted and
|
||||||
|
ConcurrentUpdate will be raised.
|
||||||
|
"""
|
||||||
|
|
||||||
|
version = VersionField(_("Version"))
|
||||||
|
|
||||||
|
def _do_update(
|
||||||
|
self, base_qs, using, pk_val, values, update_fields, forced_update
|
||||||
|
):
|
||||||
|
version_field = self.get_version_field()
|
||||||
|
|
||||||
|
# _do_update is called once for each model in the inheritance
|
||||||
|
# hierarchy. We only care about the model with the version field.
|
||||||
|
if version_field.model != base_qs.model:
|
||||||
|
return super()._do_update(
|
||||||
|
base_qs, using, pk_val, values, update_fields, forced_update
|
||||||
|
)
|
||||||
|
|
||||||
|
if version_field.attname in self.get_deferred_fields():
|
||||||
|
# With a deferred VersionField, it's not possible to do any
|
||||||
|
# sensible concurrency checking, so throw an error. The
|
||||||
|
# other option would be to treat deferring the VersionField
|
||||||
|
# the same as excluding it from `update_fields` -- a way to
|
||||||
|
# bypass checking altogether.
|
||||||
|
raise RuntimeError(
|
||||||
|
"It doesn't make sense to save a model with a deferred VersionField"
|
||||||
|
)
|
||||||
|
|
||||||
|
# pre_save may or may not have been called at this point, based on if
|
||||||
|
# version_field is in update_fields. Since we need to reliably know the
|
||||||
|
# old version, we can't increment there.
|
||||||
|
old_version = version_field.value_from_object(self)
|
||||||
|
|
||||||
|
# so increment it here instead. Now old_version is reliable.
|
||||||
|
for i, value_tuple in enumerate(values):
|
||||||
|
if isinstance(value_tuple[0], VersionField):
|
||||||
|
assert old_version == value_tuple[2]
|
||||||
|
values[i] = (
|
||||||
|
value_tuple[0],
|
||||||
|
value_tuple[1],
|
||||||
|
value_tuple[2] + 1,
|
||||||
|
)
|
||||||
|
setattr(self, version_field.attname, old_version + 1)
|
||||||
|
|
||||||
|
updated = super()._do_update(
|
||||||
|
base_qs=base_qs.filter(**{version_field.attname: old_version}),
|
||||||
|
using=using,
|
||||||
|
pk_val=pk_val,
|
||||||
|
values=values,
|
||||||
|
update_fields=(
|
||||||
|
update_fields if values else None
|
||||||
|
), # Make sure base_qs is always checked
|
||||||
|
forced_update=forced_update,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not updated and base_qs.filter(pk=pk_val).exists():
|
||||||
|
raise ConcurrentUpdateException(target=base_qs)
|
||||||
|
|
||||||
|
return updated
|
||||||
|
|
||||||
|
def get_version_field(self):
|
||||||
|
for field in self._meta.fields:
|
||||||
|
if isinstance(field, VersionField):
|
||||||
|
return field
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"VersionedMixin models must have a VersionField"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
31
django_conc/apps/core/static/core/_hs/main._hs
Normal file
31
django_conc/apps/core/static/core/_hs/main._hs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
on every contextmenu
|
||||||
|
halt
|
||||||
|
end
|
||||||
|
|
||||||
|
behavior HideModal(id)
|
||||||
|
on click
|
||||||
|
if id
|
||||||
|
set :children to Array.from(id.children)
|
||||||
|
for x in :children
|
||||||
|
remove x
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
behavior PulseModal(id)
|
||||||
|
on click
|
||||||
|
if target == me
|
||||||
|
transition scale to 1.05 over 0.1s then transition scale to initial over 0.1s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
behavior Toast()
|
||||||
|
on load
|
||||||
|
transition my *top to 50px
|
||||||
|
then wait 3s
|
||||||
|
then transition my *top to initial
|
||||||
|
then remove me
|
||||||
|
end
|
||||||
|
end
|
||||||
226
django_conc/apps/core/static/core/css/main.css
Normal file
226
django_conc/apps/core/static/core/css/main.css
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: grid;
|
||||||
|
text-rendering: optimizeSpeed;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
place-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([class]) {
|
||||||
|
text-decoration-skip-ink: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid black;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:not([type="checkbox"]) {
|
||||||
|
color: var(--color-text, #65666d);
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--input-height, calc(1.5em + 0.75rem + 2px));
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
background-color: fff;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 1px solid #d1d3e2;
|
||||||
|
border-radius: 0.35rem;
|
||||||
|
transition:
|
||||||
|
border-color 0.15s ease-in-out,
|
||||||
|
box-shadow 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
color: var(--color-text, #65666d);
|
||||||
|
background-color: var(--input-color-background, #fff);
|
||||||
|
border-color: var(--input-color-border-focus, #96c8da);
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 0.1rem var(--input-color-focus, rgba(78, 115, 223, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid black;
|
||||||
|
background-color: #b9bbbd;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background-color: #b9bbbd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #a9c7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #7297f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #5b5c60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #e66969;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.btn {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helptext {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorlist {
|
||||||
|
background-color: #ff9a92;
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-1 {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: auto;
|
||||||
|
z-index: 205;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
display: grid;
|
||||||
|
position: relative;
|
||||||
|
place-items: center;
|
||||||
|
grid-template-rows: 50px 1fr 50px;
|
||||||
|
grid-template-areas: "header" "body" "footer";
|
||||||
|
margin: auto;
|
||||||
|
width: 600px;
|
||||||
|
max-width: calc(100vw - 2rem);
|
||||||
|
padding: 0.5rem 1rem 1rem 1rem;
|
||||||
|
background-color: white;
|
||||||
|
outline: 0;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
z-index: 250;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
grid-area: header;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
grid-area: body;
|
||||||
|
padding: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
grid-area: footer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
gap: 10px;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background-color: #ff1161;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
width: 50%;
|
||||||
|
left: 50%;
|
||||||
|
top: -250px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 1.5rem;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
1
django_conc/apps/core/static/core/js/vendor/_hyperscript.min.js
vendored
Normal file
1
django_conc/apps/core/static/core/js/vendor/_hyperscript.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
django_conc/apps/core/static/core/js/vendor/htmx.min.js
vendored
Normal file
1
django_conc/apps/core/static/core/js/vendor/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
47
django_conc/apps/core/templates/core/base.html
Normal file
47
django_conc/apps/core/templates/core/base.html
Normal file
|
|
@ -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 %}
|
||||||
42
django_conc/apps/core/templates/core/pages/confirm_exit.html
Normal file
42
django_conc/apps/core/templates/core/pages/confirm_exit.html
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
{% extends "core/partials/modal/base.html" %}
|
||||||
|
|
||||||
|
{% load tag_helpers i18n %}
|
||||||
|
|
||||||
|
{% block modal_content %}
|
||||||
|
<form action="{% url 'logout' %}"
|
||||||
|
method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ block.super }}
|
||||||
|
</form>
|
||||||
|
{% endblock modal_content %}
|
||||||
|
|
||||||
|
{% block modal_header %}
|
||||||
|
<h2>
|
||||||
|
{% translate "Confirm Exit" %}
|
||||||
|
</h2>
|
||||||
|
{% endblock modal_header %}
|
||||||
|
|
||||||
|
{% block modal_body %}
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="md-12 text-center">
|
||||||
|
<h3>
|
||||||
|
{% translate "Confirm exit application" %},
|
||||||
|
</h3>
|
||||||
|
<h4>
|
||||||
|
{% translate "Are you sure ?" %}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock modal_body %}
|
||||||
|
|
||||||
|
{% block modal_footer %}
|
||||||
|
<button class="btn btn-primary btn-rounded" type="submit">
|
||||||
|
{% translate "Confirm" %}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary btn-rounded"
|
||||||
|
type="button"
|
||||||
|
href="javascript:void(0);"
|
||||||
|
_="install HideModal(id:#Modal)">
|
||||||
|
{% translate "Cancel" %}
|
||||||
|
</button>
|
||||||
|
{% endblock modal_footer %}
|
||||||
12
django_conc/apps/core/templates/core/pages/form.html
Normal file
12
django_conc/apps/core/templates/core/pages/form.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "core/pages/list.html" %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-secondary" href="{% url extra_content.urls.index %}">BACK</a>
|
||||||
|
</div>
|
||||||
|
<form action="" method="post" autocomplete="off" enctype='multipart/form-data' style="margin-top: 2rem;display: grid;gap:1rem">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit" class="btn btn-primary">SAVE</button>
|
||||||
|
</form>
|
||||||
|
{% endblock main %}
|
||||||
7
django_conc/apps/core/templates/core/pages/index.html
Normal file
7
django_conc/apps/core/templates/core/pages/index.html
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "core/base.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<a class="btn btn-primary" href="{% url 'core:accounts:index' %}">Users</a>
|
||||||
|
{% endblock main %}
|
||||||
56
django_conc/apps/core/templates/core/pages/list.html
Normal file
56
django_conc/apps/core/templates/core/pages/list.html
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
{% extends "core/base.html" %}
|
||||||
|
|
||||||
|
{% load static tag_helpers %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div class="flex align-center justify-between" style="margin-bottom:1rem;">
|
||||||
|
<span>User: {{ request.user }}</span>
|
||||||
|
<form action="{% url 'logout' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button
|
||||||
|
class="btn btn-small"
|
||||||
|
hx-get="{% url 'core:confirm_exit' %}"
|
||||||
|
hx-target="#Modal"
|
||||||
|
>Logout</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-primary" href="{% url extra_content.urls.new %}">New</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table style="margin-top:2rem">
|
||||||
|
{% with form|get_meta:"actions" as actions %}
|
||||||
|
<tr>
|
||||||
|
{% if actions %}
|
||||||
|
<th>Actions</th>
|
||||||
|
{% endif %}
|
||||||
|
{% for field in form.fields %}
|
||||||
|
<th>{{ field|capfirst }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% for object in object_list %}
|
||||||
|
<tr>
|
||||||
|
{% if actions %}
|
||||||
|
<td>
|
||||||
|
<div class="flex align-center justify-center gap-1">
|
||||||
|
<a class="btn btn-small btn-secondary" href="{% url extra_content.urls.edit object.pk %}">Edit</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-small btn-danger"
|
||||||
|
hx-post="{% url extra_content.urls.confirm object.pk %}"
|
||||||
|
hx-target="#Modal"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
{% for field in form.fields %}
|
||||||
|
<td>{{ object|get_field:field }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
</table>
|
||||||
|
{% endblock main %}
|
||||||
0
django_conc/apps/core/templates/core/pages/void.html
Normal file
0
django_conc/apps/core/templates/core/pages/void.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{% load tag_helpers i18n %}
|
||||||
|
|
||||||
|
<div class="modal-container grid-center"
|
||||||
|
_="install PulseModal(id:#Modal)">
|
||||||
|
{% block modal_content %}
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
{% block modal_header %}
|
||||||
|
<h2>
|
||||||
|
Modal title
|
||||||
|
</h2>
|
||||||
|
{% endblock modal_header %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-body o-scroll"
|
||||||
|
style="max-height:80vh">
|
||||||
|
{% block modal_body %}
|
||||||
|
{% endblock modal_body %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
{% block modal_footer %}
|
||||||
|
<button class="btn btn-secondary"
|
||||||
|
type="button"
|
||||||
|
_="install HideModal(id:#Modal)">
|
||||||
|
{% translate "Cancel" %}
|
||||||
|
</button>
|
||||||
|
{% endblock modal_footer %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock modal_content %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-backdrop">
|
||||||
|
</div>
|
||||||
31
django_conc/apps/core/templatetags/tag_helpers.py
Normal file
31
django_conc/apps/core/templatetags/tag_helpers.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
from django import template
|
||||||
|
from django.db.models.fields.files import FieldFile, ImageFieldFile
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="times")
|
||||||
|
def times(start, end=None):
|
||||||
|
"""Return a range starting 1 by default until number informed."""
|
||||||
|
if end:
|
||||||
|
return range(int(start), int(end))
|
||||||
|
return range(1, int(start))
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="get_field")
|
||||||
|
def get_field(obj, field):
|
||||||
|
"""Return an object attribute"""
|
||||||
|
field = getattr(obj, field)
|
||||||
|
if isinstance(field, ImageFieldFile) or isinstance(field, FieldFile):
|
||||||
|
if field:
|
||||||
|
return mark_safe(
|
||||||
|
f'<a class="btn btn-small btn-secondary" target="_blank" href="{field.url}">attach</a>'
|
||||||
|
)
|
||||||
|
return field
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="get_meta")
|
||||||
|
def get_meta(form, attr):
|
||||||
|
"""Return an attribute from form meta class"""
|
||||||
|
return getattr(form.Meta, attr, None)
|
||||||
3
django_conc/apps/core/tests.py
Normal file
3
django_conc/apps/core/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
17
django_conc/apps/core/urls.py
Normal file
17
django_conc/apps/core/urls.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "core"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", views.Index.as_view(), name="index"),
|
||||||
|
path("confirm-exit/", views.ConfirmExit.as_view(), name="confirm_exit"),
|
||||||
|
path(
|
||||||
|
"accounts/",
|
||||||
|
include(
|
||||||
|
"django_conc.apps.accounts.urls",
|
||||||
|
namespace="accounts",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
134
django_conc/apps/core/views.py
Normal file
134
django_conc/apps/core/views.py
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db.models import ProtectedError
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.generic import CreateView, DeleteView, ListView, TemplateView, \
|
||||||
|
UpdateView
|
||||||
|
|
||||||
|
from . import mixins
|
||||||
|
|
||||||
|
|
||||||
|
class Common:
|
||||||
|
app_name = None
|
||||||
|
model = None
|
||||||
|
page_title = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.success_url = reverse_lazy(f"core:{self.app_name}:index")
|
||||||
|
self.extra_content = {
|
||||||
|
"urls": {
|
||||||
|
"index": f"core:{self.app_name}:index",
|
||||||
|
"new": f"core:{self.app_name}:new",
|
||||||
|
"edit": f"core:{self.app_name}:edit",
|
||||||
|
"search": f"core:{self.app_name}:search",
|
||||||
|
"confirm": f"core:{self.app_name}:confirm_delete",
|
||||||
|
"delete": f"core:{self.app_name}:delete",
|
||||||
|
"detail": f"core:{self.app_name}:detail",
|
||||||
|
},
|
||||||
|
"page_title": "Index",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_text_url(self, action="index"):
|
||||||
|
return f"core:{self.app_name}:{action}"
|
||||||
|
|
||||||
|
def get_url(self, action, kwargs=None):
|
||||||
|
return reverse_lazy(self.get_text_url(action), kwargs=kwargs)
|
||||||
|
|
||||||
|
def get_pk(self, name="pk"):
|
||||||
|
return self.kwargs.get(name)
|
||||||
|
|
||||||
|
def htmx_redirect_to(
|
||||||
|
self, request, url, context={}, template_name="core/pages/void.html"
|
||||||
|
):
|
||||||
|
redire = render(
|
||||||
|
request=request, template_name=template_name, context=context
|
||||||
|
)
|
||||||
|
redire["HX-Redirect"] = url
|
||||||
|
return redire
|
||||||
|
|
||||||
|
|
||||||
|
class Index(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = "core/pages/index.html"
|
||||||
|
|
||||||
|
|
||||||
|
class CrudViews(LoginRequiredMixin, Common):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.extra_content.update(page_title=self.page_title)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["extra_content"] = self.extra_content
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class Base(CrudViews, TemplateView):
|
||||||
|
template_name = "core/pages/void.html"
|
||||||
|
|
||||||
|
|
||||||
|
class List(CrudViews, ListView):
|
||||||
|
template_name = "core/pages/index.html"
|
||||||
|
paginate_by = 10
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["form"] = self.form_class
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class New(CrudViews, CreateView):
|
||||||
|
template_name = "core/pages/form.html"
|
||||||
|
page_title = "New"
|
||||||
|
|
||||||
|
|
||||||
|
class Update(CrudViews, UpdateView):
|
||||||
|
template_name = "core/pages/form.html"
|
||||||
|
page_title = "Update"
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmDelete(mixins.HtmxMixin, CrudViews, TemplateView):
|
||||||
|
template_name = "core/pages/confirm_delete.html"
|
||||||
|
action_url = None
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
data = {
|
||||||
|
"object": self.model.objects.filter(pk=kwargs.get("pk")).first(),
|
||||||
|
"extra_content": self.extra_content,
|
||||||
|
}
|
||||||
|
return render(request, self.template_name, context=data)
|
||||||
|
|
||||||
|
|
||||||
|
class Delete(CrudViews, DeleteView):
|
||||||
|
template_name = "core/pages/confirm_delete.html"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
context = self.get_context_data()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.object.delete()
|
||||||
|
except ProtectedError as e:
|
||||||
|
form.add_error(
|
||||||
|
None,
|
||||||
|
ValidationError(
|
||||||
|
_(
|
||||||
|
"Cannot delete some instances because they are referenced through protected objects: %(value)s)"
|
||||||
|
),
|
||||||
|
code="protected",
|
||||||
|
params={"value": e.protected_objects},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
context.update(form=form, extra_content=self.extra_content)
|
||||||
|
return self.render_to_response(context=context)
|
||||||
|
|
||||||
|
return self.htmx_redirect_to(
|
||||||
|
request=self.request, url=self.get_success_url(), context=context
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmExit(LoginRequiredMixin, mixins.HtmxMixin, TemplateView):
|
||||||
|
template_name = "core/pages/confirm_exit.html"
|
||||||
|
http_method_names = ["get"]
|
||||||
16
django_conc/asgi.py
Normal file
16
django_conc/asgi.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for django_conc project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_conc.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
145
django_conc/settings.py
Normal file
145
django_conc/settings.py
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
"""
|
||||||
|
Django settings for django_conc project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.0.2.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from decouple import Csv, config
|
||||||
|
from dj_database_url import parse as dburl
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = config("SECRET_KEY")
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = config("DEBUG", default=False, cast=bool)
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=Csv())
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"django_extensions",
|
||||||
|
"django_conc.apps.core",
|
||||||
|
"django_conc.apps.accounts",
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"django_conc.apps.core.middleware.ConcurrencyMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "django_conc.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": ["templates", BASE_DIR / "templates"],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "django_conc.wsgi.application"
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
||||||
|
|
||||||
|
default_dburl = "sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3")
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": config("DATABASE_URL", default=default_dburl, cast=dburl),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
|
TIME_ZONE = "UTC"
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
os.path.join(BASE_DIR, "staticfiles"),
|
||||||
|
]
|
||||||
|
|
||||||
|
MEDIA_URL = "/media/"
|
||||||
|
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||||
|
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = "accounts.User"
|
||||||
|
|
||||||
|
LOGIN_URL = "login"
|
||||||
|
|
||||||
|
LOGIN_REDIRECT_URL = "core:index"
|
||||||
|
|
||||||
|
LOGOUT_REDIRECT_URL = "login"
|
||||||
33
django_conc/urls.py
Normal file
33
django_conc/urls.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
"""
|
||||||
|
URL configuration for django_conc project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/5.0/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth import views as auth_views
|
||||||
|
from django.urls import include, path
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", include("django_conc.apps.core.urls", namespace="core")),
|
||||||
|
path("auth/sign_in/", auth_views.LoginView.as_view(), name="login"),
|
||||||
|
path("auth/logout/", auth_views.LogoutView.as_view(), name="logout"),
|
||||||
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
]
|
||||||
16
django_conc/wsgi.py
Normal file
16
django_conc/wsgi.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for django_conc project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_conc.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
22
manage.py
Executable file
22
manage.py
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_conc.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
345
poetry.lock
generated
Normal file
345
poetry.lock
generated
Normal file
|
|
@ -0,0 +1,345 @@
|
||||||
|
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asgiref"
|
||||||
|
version = "3.7.2"
|
||||||
|
description = "ASGI specs, helper code, and adapters"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"},
|
||||||
|
{file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dj-database-url"
|
||||||
|
version = "2.1.0"
|
||||||
|
description = "Use Database URLs in your Django Application."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "dj-database-url-2.1.0.tar.gz", hash = "sha256:f2042cefe1086e539c9da39fad5ad7f61173bf79665e69bf7e4de55fa88b135f"},
|
||||||
|
{file = "dj_database_url-2.1.0-py3-none-any.whl", hash = "sha256:04bc34b248d4c21aaa13e4ab419ae6575ef5f10f3df735ce7da97722caa356e0"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Django = ">=3.2"
|
||||||
|
typing-extensions = ">=3.10.0.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dj-static"
|
||||||
|
version = "0.0.6"
|
||||||
|
description = "Serve production static files with Django."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "dj-static-0.0.6.tar.gz", hash = "sha256:032ec1c532617922e6e3e956d504a6fb1acce4fc1c7c94612d0fda21828ce8ef"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
static3 = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django"
|
||||||
|
version = "5.0.2"
|
||||||
|
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
files = [
|
||||||
|
{file = "Django-5.0.2-py3-none-any.whl", hash = "sha256:56ab63a105e8bb06ee67381d7b65fe6774f057e41a8bab06c8020c8882d8ecd4"},
|
||||||
|
{file = "Django-5.0.2.tar.gz", hash = "sha256:b5bb1d11b2518a5f91372a282f24662f58f66749666b0a286ab057029f728080"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
asgiref = ">=3.7.0,<4"
|
||||||
|
sqlparse = ">=0.3.1"
|
||||||
|
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
argon2 = ["argon2-cffi (>=19.1.0)"]
|
||||||
|
bcrypt = ["bcrypt"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-cleanup"
|
||||||
|
version = "8.1.0"
|
||||||
|
description = "Deletes old files."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "django-cleanup-8.1.0.tar.gz", hash = "sha256:70df905076a44e7a111b31198199af633dee08876e199e6dce36ca8dd6b8b10f"},
|
||||||
|
{file = "django_cleanup-8.1.0-py2.py3-none-any.whl", hash = "sha256:7903873ea73b3f7e61e055340d27dba49b70634f60c87a573ad748e172836458"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-extensions"
|
||||||
|
version = "3.2.3"
|
||||||
|
description = "Extensions for Django"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"},
|
||||||
|
{file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Django = ">=3.2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-storages"
|
||||||
|
version = "1.14.2"
|
||||||
|
description = "Support for many storage backends in Django"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "django-storages-1.14.2.tar.gz", hash = "sha256:51b36af28cc5813b98d5f3dfe7459af638d84428c8df4a03990c7d74d1bea4e5"},
|
||||||
|
{file = "django_storages-1.14.2-py3-none-any.whl", hash = "sha256:1db759346b52ada6c2efd9f23d8241ecf518813eb31db9e2589207174f58f6ad"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Django = ">=3.2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
azure = ["azure-core (>=1.13)", "azure-storage-blob (>=12)"]
|
||||||
|
boto3 = ["boto3 (>=1.4.4)"]
|
||||||
|
dropbox = ["dropbox (>=7.2.1)"]
|
||||||
|
google = ["google-cloud-storage (>=1.27)"]
|
||||||
|
libcloud = ["apache-libcloud"]
|
||||||
|
s3 = ["boto3 (>=1.4.4)"]
|
||||||
|
sftp = ["paramiko (>=1.15)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pillow"
|
||||||
|
version = "10.2.0"
|
||||||
|
description = "Python Imaging Library (Fork)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"},
|
||||||
|
{file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"},
|
||||||
|
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"},
|
||||||
|
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"},
|
||||||
|
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"},
|
||||||
|
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"},
|
||||||
|
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"},
|
||||||
|
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"},
|
||||||
|
{file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"},
|
||||||
|
{file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"},
|
||||||
|
{file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"},
|
||||||
|
{file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"},
|
||||||
|
{file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"},
|
||||||
|
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"},
|
||||||
|
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"},
|
||||||
|
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"},
|
||||||
|
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"},
|
||||||
|
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"},
|
||||||
|
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"},
|
||||||
|
{file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"},
|
||||||
|
{file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"},
|
||||||
|
{file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"},
|
||||||
|
{file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"},
|
||||||
|
{file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"},
|
||||||
|
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"},
|
||||||
|
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"},
|
||||||
|
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"},
|
||||||
|
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"},
|
||||||
|
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"},
|
||||||
|
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"},
|
||||||
|
{file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"},
|
||||||
|
{file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"},
|
||||||
|
{file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"},
|
||||||
|
{file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"},
|
||||||
|
{file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"},
|
||||||
|
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"},
|
||||||
|
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"},
|
||||||
|
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"},
|
||||||
|
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"},
|
||||||
|
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"},
|
||||||
|
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"},
|
||||||
|
{file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"},
|
||||||
|
{file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"},
|
||||||
|
{file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"},
|
||||||
|
{file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"},
|
||||||
|
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"},
|
||||||
|
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"},
|
||||||
|
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"},
|
||||||
|
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"},
|
||||||
|
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"},
|
||||||
|
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"},
|
||||||
|
{file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"},
|
||||||
|
{file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"},
|
||||||
|
{file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"},
|
||||||
|
{file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"},
|
||||||
|
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"},
|
||||||
|
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"},
|
||||||
|
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"},
|
||||||
|
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"},
|
||||||
|
{file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"},
|
||||||
|
{file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"},
|
||||||
|
{file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"},
|
||||||
|
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"},
|
||||||
|
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"},
|
||||||
|
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"},
|
||||||
|
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"},
|
||||||
|
{file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"},
|
||||||
|
{file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
|
||||||
|
fpx = ["olefile"]
|
||||||
|
mic = ["olefile"]
|
||||||
|
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
|
||||||
|
typing = ["typing-extensions"]
|
||||||
|
xmp = ["defusedxml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg2-binary"
|
||||||
|
version = "2.9.9"
|
||||||
|
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"},
|
||||||
|
{file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-decouple"
|
||||||
|
version = "3.8"
|
||||||
|
description = "Strict separation of settings from code."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "python-decouple-3.8.tar.gz", hash = "sha256:ba6e2657d4f376ecc46f77a3a615e058d93ba5e465c01bbe57289bfb7cce680f"},
|
||||||
|
{file = "python_decouple-3.8-py3-none-any.whl", hash = "sha256:d0d45340815b25f4de59c974b855bb38d03151d81b037d9e3f463b0c9f8cbd66"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlparse"
|
||||||
|
version = "0.4.4"
|
||||||
|
description = "A non-validating SQL parser."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
files = [
|
||||||
|
{file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"},
|
||||||
|
{file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["build", "flake8"]
|
||||||
|
doc = ["sphinx"]
|
||||||
|
test = ["pytest", "pytest-cov"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static3"
|
||||||
|
version = "0.7.0"
|
||||||
|
description = "A really simple WSGI way to serve static (or mixed) content."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "static3-0.7.0.tar.gz", hash = "sha256:674641c64bc75507af2eb20bef7e7e3593dca993dec6674be108fa15b42f47c8"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
genshimagic = ["Genshi"]
|
||||||
|
kidmagic = ["kid"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.9.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
|
||||||
|
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2023.4"
|
||||||
|
description = "Provider of IANA time zone data"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2"
|
||||||
|
files = [
|
||||||
|
{file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"},
|
||||||
|
{file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.0"
|
||||||
|
python-versions = "^3.11"
|
||||||
|
content-hash = "0f777838b72c1d9452050ee4b309fb5d50f9407ea5fcad2138b188288ce1bbfa"
|
||||||
75
pyproject.toml
Normal file
75
pyproject.toml
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "django_conc"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Lucas F. <lucas@lucasf.dev>"]
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.11"
|
||||||
|
django = "^5.0.2"
|
||||||
|
dj-database-url = "^2.1.0"
|
||||||
|
django-storages = "^1.14.2"
|
||||||
|
pillow = "^10.2.0"
|
||||||
|
python-decouple = "^3.8"
|
||||||
|
django-cleanup = "^8.1.0"
|
||||||
|
psycopg2-binary = "^2.9.9"
|
||||||
|
django-extensions = "^3.2.3"
|
||||||
|
dj-static = "^0.0.6"
|
||||||
|
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
line_length = 89
|
||||||
|
multi_line_output = 2
|
||||||
|
include_trailing_comma = false
|
||||||
|
extend_skip = [
|
||||||
|
".git",
|
||||||
|
".hg",
|
||||||
|
".mypy_cache",
|
||||||
|
".tox",
|
||||||
|
".venv",
|
||||||
|
"_build",
|
||||||
|
"buck-out",
|
||||||
|
"build",
|
||||||
|
"dist",
|
||||||
|
"migrations",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 79
|
||||||
|
include = '\.pyi?$'
|
||||||
|
force-exclude = '''
|
||||||
|
/(
|
||||||
|
\.git
|
||||||
|
| \.hg
|
||||||
|
| \.mypy_cache
|
||||||
|
| \.tox
|
||||||
|
| \.venv
|
||||||
|
| _build
|
||||||
|
| buck-out
|
||||||
|
| build
|
||||||
|
| dist
|
||||||
|
| migrations
|
||||||
|
)/
|
||||||
|
'''
|
||||||
|
|
||||||
|
[tool.djlint]
|
||||||
|
profile = "django"
|
||||||
|
max_attribute_length = 0
|
||||||
|
blank_line_after_tag = "load, extends, endblock, endwith"
|
||||||
|
line_break_after_multiline_tag = true
|
||||||
|
format_attribute_template_tags = true
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
minversion = "6.0"
|
||||||
|
DJANGO_SETTINGS_MODULE = "django_conc.settings"
|
||||||
|
python_files = [
|
||||||
|
"tests.py",
|
||||||
|
"test_*.py",
|
||||||
|
"*_tests.py",
|
||||||
|
]
|
||||||
11
templates/registration/login.html
Normal file
11
templates/registration/login.html
Normal file
|
|
@ -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…
Add table
Add a link
Reference in a new issue