Merge branch 'nikolaik/up' into 'master'

chore: Modernize all the bits

See merge request edb/kassa!1
This commit is contained in:
nikolark committed 2025-05-31 20:35:55 +00:00
commit cb3dfffbb8
41 files changed
+8153 -10763

No files matched your search

+1 -1
View File
@@ -7,4 +7,4 @@ venv
node_modules
bower_components
staticfiles
static/dist/
static/dist/
+43
View File
@@ -0,0 +1,43 @@
variables:
PIP_CACHE_DIR: "${CI_PROJECT_DIR}/.cache/pip"
default:
image: nikolaik/python-nodejs:python3.12-nodejs20
cache:
paths:
- .cache/pip
- .venv
django-checks:
stage: build
script:
- uv run manage.py migrate
- uv run manage.py check
tests:
stage: test
script:
- uv run bin/test
lint:
stage: test
script:
- uv run bin/pre-commit
deploy:
stage: deploy
before_script:
# Install ssh if not already
- 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'
- eval $(ssh-agent -s)
# Make SSH key available
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
# Add known host keys
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- ssh gitdeploy@dreamcast.neuf.no "$(< bin/deploy)"
only:
- main
+14
View File
@@ -0,0 +1,14 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.11.7'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/adamchainz/django-upgrade
rev: "1.24.0"
hooks:
- id: django-upgrade
args: [--target-version, "4.2"]
+1
View File
@@ -0,0 +1 @@
3.12
+16 -8
View File
@@ -1,6 +1,9 @@
# kassa
Kassa helps you register purchased membership cards and memberships. It's a tool for volunteers and bar employees at The Norwegian Student Society.
## TODO
* replace nunjucks with something that is maintained
* autoprefixer
* bootstrap 4.x
@@ -8,21 +11,23 @@ Kassa helps you register purchased membership cards and memberships. It's a tool
* Galtinn/LDAP auth
## Installation
```shell script
apt install libldap2-dev libsasl2-dev # pyldap deps
python3 -m venv venv
. venv/bin/activate
pip install -U pip wheel
pip install -r requirements.txt
python manage.py migrate
uv run manage.py migrate
uv run manage.py runserver
```
### Development tasks
```shell script
npm run build:templates # regenerate nunjucks templates
```
## Configuration
Create a user in Galtinn with the following permissions:
* galtinn | User | Can view User
* galtinn | Member card | Can change Member card
* galtinn | Member card | Can view Member card
@@ -31,17 +36,20 @@ Create a user in Galtinn with the following permissions:
* galtinn | Order | Can view Order
Fetch GALTINN_API_KEY:
```shell script
curl -H "Content-Type: application/json" -X POST -d '{"username":"apiuser","password":"test"}' GALTINN_API_URL/auth/obtain-token/
```shell
curl -H "Content-Type: application/json" -X POST -d '{"username":"apiuser","password":"test"}' GALTINN_API_URL/auth/obtain-token/
```
If developing you can disable calls to Tekstmelding in local_settings.py:
```python
TEKSTMELDING_ENABLED = False
```
## LDAP
```shell script
```shell
sudo docker run -e LDAP_DOMAIN=neuf.no -e LDAP_ORGANISATION="Neuf" -e LDAP_ADMIN_PASSWORD="toor" -p 389:389 -d nikolaik/openldap
ldapadd -D "cn=admin,dc=neuf,dc=no" -w "toor" -f test/testdata.ldif # Testdata
# Verify import
+15 -8
View File
@@ -1,12 +1,19 @@
from apps.kassa.models import KassaEvent
from django.contrib import admin
from apps.kassa.models import KassaEvent
@admin.register(KassaEvent)
class KassaEventAdmin(admin.ModelAdmin):
list_display = ['id', 'event', 'card_number', 'user_phone_number', 'user_galtinn_id',
'user_inside_id', 'created', 'transaction_id']
list_filter = ['event']
search_fields = ['card_number', 'user_phone_number', 'user_galtinn_id',
'user_inside_id', 'transaction_id']
admin.site.register(KassaEvent, KassaEventAdmin)
list_display = [
"id",
"event",
"card_number",
"user_phone_number",
"user_galtinn_id",
"user_inside_id",
"created",
"transaction_id",
]
list_filter = ["event"]
search_fields = ["card_number", "user_phone_number", "user_galtinn_id", "user_inside_id", "transaction_id"]
+2 -2
View File
@@ -1,5 +1,5 @@
from django.conf import settings
def sentry(request):
return {'SENTRY_DSN': settings.SENTRY_DSN or '', 'SENTRY_ENVIRONMENT': settings.SENTRY_ENVIRONMENT}
def sentry(_):
return {"SENTRY_DSN": settings.SENTRY_DSN or "", "SENTRY_ENVIRONMENT": settings.SENTRY_ENVIRONMENT}
+4 -5
View File
@@ -1,17 +1,16 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from phonenumber_field.formfields import PhoneNumberField
from django.utils.translation import ugettext_lazy as _
class SearchUserForm(forms.Form):
query = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('Firstname, lastname, card number or email')})
required=False, widget=forms.TextInput(attrs={"placeholder": _("Firstname, lastname, card number or email")})
)
class AddCardForm(forms.Form):
card_number = forms.IntegerField(widget=forms.NumberInput(attrs={'autofocus': True}))
phone_number = PhoneNumberField(widget=forms.TextInput(attrs={'type': 'tel'}))
card_number = forms.IntegerField(widget=forms.NumberInput(attrs={"autofocus": True}))
phone_number = PhoneNumberField(widget=forms.TextInput(attrs={"type": "tel"}))
user_id = forms.IntegerField(widget=forms.HiddenInput(), required=False)
order_uuid = forms.IntegerField(widget=forms.HiddenInput(), required=False)
+20 -15
View File
@@ -1,25 +1,30 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
name='KassaEvent',
name="KassaEvent",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('event', models.TextField(max_length=255, choices=[('new_card_membership', 'Added new card membership (no user yet)'), ('update_card', 'Updated user card number'), ('add_or_renew', 'Added or renewed membership user membership')])),
('card_number', models.IntegerField(null=True, blank=True)),
('user_inside_id', models.IntegerField(null=True, blank=True)),
('user_phone_number', models.TextField(max_length=100, null=True, blank=True)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)),
(
"event",
models.TextField(
max_length=255,
choices=[
("new_card_membership", "Added new card membership (no user yet)"),
("update_card", "Updated user card number"),
("add_or_renew", "Added or renewed membership user membership"),
],
),
),
("card_number", models.IntegerField(null=True, blank=True)),
("user_inside_id", models.IntegerField(null=True, blank=True)),
("user_phone_number", models.TextField(max_length=100, null=True, blank=True)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
],
),
]
@@ -1,24 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kassa', '0001_initial'),
("kassa", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name='kassaevent',
name='event',
field=models.CharField(max_length=255, choices=[('new_card_membership', 'Added new card membership (no user yet)'), ('update_card', 'Updated user card number'), ('add_or_renew', 'Added or renewed membership user membership')]),
model_name="kassaevent",
name="event",
field=models.CharField(
max_length=255,
choices=[
("new_card_membership", "Added new card membership (no user yet)"),
("update_card", "Updated user card number"),
("add_or_renew", "Added or renewed membership user membership"),
],
),
),
migrations.AlterField(
model_name='kassaevent',
name='user_phone_number',
model_name="kassaevent",
name="user_phone_number",
field=models.CharField(max_length=100, null=True, blank=True),
),
]
@@ -1,19 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kassa', '0002_auto_20150810_1327'),
("kassa", "0002_auto_20150810_1327"),
]
operations = [
migrations.AlterField(
model_name='kassaevent',
name='event',
field=models.CharField(max_length=255, choices=[('new_card_membership', 'Added new card membership (no user yet)'), ('sms_card_notify', 'Notified phone number about activation'), ('update_card', 'Updated user card number'), ('add_or_renew', 'Added or renewed membership user membership')]),
model_name="kassaevent",
name="event",
field=models.CharField(
max_length=255,
choices=[
("new_card_membership", "Added new card membership (no user yet)"),
("sms_card_notify", "Notified phone number about activation"),
("update_card", "Updated user card number"),
("add_or_renew", "Added or renewed membership user membership"),
],
),
),
]
@@ -1,19 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kassa', '0003_auto_20150812_1530'),
("kassa", "0003_auto_20150812_1530"),
]
operations = [
migrations.AlterField(
model_name='kassaevent',
name='event',
field=models.CharField(max_length=255, choices=[('new_card_membership', 'Added new card membership (no user yet)'), ('sms_card_notify', 'Notified phone number about activation'), ('update_card', 'Updated user card number'), ('add_or_renew', 'Added or renewed membership user membership'), ('refund', 'Refunded membership')]),
model_name="kassaevent",
name="event",
field=models.CharField(
max_length=255,
choices=[
("new_card_membership", "Added new card membership (no user yet)"),
("sms_card_notify", "Notified phone number about activation"),
("update_card", "Updated user card number"),
("add_or_renew", "Added or renewed membership user membership"),
("refund", "Refunded membership"),
],
),
),
]
@@ -1,19 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kassa', '0004_auto_20150825_1130'),
("kassa", "0004_auto_20150825_1130"),
]
operations = [
migrations.AlterField(
model_name='kassaevent',
name='event',
field=models.CharField(max_length=255, choices=[('new_card_membership', 'Added new card membership (no user yet)'), ('sms_card_notify', 'Notified phone number about activation'), ('update_card', 'Updated user card number'), ('add_or_renew', 'Added or renewed user membership'), ('renew_only', 'Renewed user membership'), ('refund', 'Refunded membership')]),
model_name="kassaevent",
name="event",
field=models.CharField(
max_length=255,
choices=[
("new_card_membership", "Added new card membership (no user yet)"),
("sms_card_notify", "Notified phone number about activation"),
("update_card", "Updated user card number"),
("add_or_renew", "Added or renewed user membership"),
("renew_only", "Renewed user membership"),
("refund", "Refunded membership"),
],
),
),
]
@@ -1,19 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kassa', '0005_auto_20150829_1525'),
("kassa", "0005_auto_20150829_1525"),
]
operations = [
migrations.AlterField(
model_name='kassaevent',
name='event',
field=models.CharField(max_length=255, choices=[('add_or_renew', 'New or renewed user membership'), ('new_card_membership', 'New card membership (no user yet)'), ('refund', 'Refunded membership'), ('renew_only', 'Renewed user membership'), ('sms_card_notify', 'Sent activation link to phone number'), ('update_card', 'Updated card'), ('membership_trial', 'New membership trial')]),
model_name="kassaevent",
name="event",
field=models.CharField(
max_length=255,
choices=[
("add_or_renew", "New or renewed user membership"),
("new_card_membership", "New card membership (no user yet)"),
("refund", "Refunded membership"),
("renew_only", "Renewed user membership"),
("sms_card_notify", "Sent activation link to phone number"),
("update_card", "Updated card"),
("membership_trial", "New membership trial"),
],
),
),
]
@@ -1,20 +1,17 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-22 12:40
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kassa', '0006_auto_20150829_1811'),
("kassa", "0006_auto_20150829_1811"),
]
operations = [
migrations.AddField(
model_name='kassaevent',
name='transaction_id',
model_name="kassaevent",
name="transaction_id",
field=models.UUIDField(blank=True, null=True),
),
]
@@ -1,20 +1,17 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-22 12:43
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kassa', '0007_kassaevent_transaction_id'),
("kassa", "0007_kassaevent_transaction_id"),
]
operations = [
migrations.AddField(
model_name='kassaevent',
name='user_galtinn_id',
model_name="kassaevent",
name="user_galtinn_id",
field=models.IntegerField(blank=True, null=True),
),
]
+17 -17
View File
@@ -1,32 +1,32 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
class KassaEvent(models.Model):
ADD_OR_RENEW = 'add_or_renew'
NEW_CARD_MEMBERSHIP = 'new_card_membership'
REFUND = 'refund'
RENEW_ONLY = 'renew_only'
SMS_CARD_NOTIFY = 'sms_card_notify'
UPDATE_CARD = 'update_card'
MEMBERSHIP_TRIAL = 'membership_trial'
ADD_OR_RENEW = "add_or_renew"
NEW_CARD_MEMBERSHIP = "new_card_membership"
REFUND = "refund"
RENEW_ONLY = "renew_only"
SMS_CARD_NOTIFY = "sms_card_notify"
UPDATE_CARD = "update_card"
MEMBERSHIP_TRIAL = "membership_trial"
EVENT_CHOICES = (
(ADD_OR_RENEW, _('New or renewed user membership')),
(NEW_CARD_MEMBERSHIP, _('New card membership (no user yet)')),
(REFUND, _('Refunded membership')),
(RENEW_ONLY, _('Renewed user membership')),
(SMS_CARD_NOTIFY, _('Sent activation link to phone number')),
(UPDATE_CARD, _('Updated card')),
(MEMBERSHIP_TRIAL, _('New membership trial'))
(ADD_OR_RENEW, _("New or renewed user membership")),
(NEW_CARD_MEMBERSHIP, _("New card membership (no user yet)")),
(REFUND, _("Refunded membership")),
(RENEW_ONLY, _("Renewed user membership")),
(SMS_CARD_NOTIFY, _("Sent activation link to phone number")),
(UPDATE_CARD, _("Updated card")),
(MEMBERSHIP_TRIAL, _("New membership trial")),
)
event = models.CharField(max_length=255, choices=EVENT_CHOICES)
card_number = models.IntegerField(null=True, blank=True)
user_galtinn_id = models.IntegerField(null=True, blank=True)
user_inside_id = models.IntegerField(null=True, blank=True)
user_phone_number = models.CharField(max_length=100, null=True, blank=True)
user_phone_number = models.CharField(max_length=100, null=True, blank=True) # noqa: DJ001
transaction_id = models.UUIDField(null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return 'KassaEvent: <{}>'.format(self.pk)
return f"KassaEvent: <{self.pk}>"
-1
View File
@@ -1 +0,0 @@
from django.test import TestCase
+15 -9
View File
@@ -1,13 +1,19 @@
from django.urls import path, re_path
from django.urls import path
from apps.kassa.views import (register, user_search, check_card, check_phone_number, register_card_and_membership,
renew_membership)
from apps.kassa.views import (
check_card,
check_phone_number,
register,
register_card_and_membership,
renew_membership,
user_search,
)
urlpatterns = [
re_path(r'^$', register, name='register'),
path('user-search/', user_search, name='user-search'),
path('check-phone-number/', check_phone_number, name='check-phone-number'),
path('check-card/', check_card, name='check-card'),
path('register-card-membership/', register_card_and_membership, name='register-card-and-membership'),
path('renew-membership/', renew_membership, name='renew-membership'),
path("", register, name="register"),
path("user-search/", user_search, name="user-search"),
path("check-phone-number/", check_phone_number, name="check-phone-number"),
path("check-card/", check_card, name="check-card"),
path("register-card-membership/", register_card_and_membership, name="register-card-and-membership"),
path("renew-membership/", renew_membership, name="renew-membership"),
]
+53 -57
View File
@@ -1,102 +1,98 @@
# coding: utf-8
from django.conf import settings
import datetime
import logging
import phonenumbers
import requests
import logging
from django.conf import settings
logger = logging.getLogger(__name__)
galtinn_auth = {
'Authorization': 'Token {}'.format(settings.GALTINN_API_KEY)
}
galtinn_auth = {"Authorization": f"Token {settings.GALTINN_API_KEY}"}
tekstmelding_auth = {
'Authorization': 'Token {}'.format(settings.TEKSTMELDING_API_KEY)
}
tekstmelding_auth = {"Authorization": f"Token {settings.TEKSTMELDING_API_KEY}"}
def send_sms_activation_link(phone_number, transaction_id):
to = phone_number.replace('+', '')
to = phone_number.replace("+", "")
code = transaction_id[:8]
activation_url = settings.GALTINN_ACTIVATION_URL.format(to=to, code=code)
message = settings.TEKSTMELDING_ACTIVATION_SMS_TEMPLATE.format(url=activation_url)
payload = {
'to': to,
'message': message
}
payload = {"to": to, "message": message}
if not settings.TEKSTMELDING_ENABLED:
logger.info('Tekstmelding disabled. Payload would be: {}'.format(payload))
return
url = '{}send'.format(settings.TEKSTMELDING_API_URL)
response = requests.post(url, json=payload, headers=tekstmelding_auth)
logger.info(f"Tekstmelding disabled. Payload would be: {payload}")
return None
url = f"{settings.TEKSTMELDING_API_URL}send"
response = requests.post(url, json=payload, headers=tekstmelding_auth, timeout=10)
return response.json()
def get_card(card_number):
url = '{}cards/{}/'.format(settings.GALTINN_API_URL, card_number)
return requests.get(url, headers=galtinn_auth)
url = f"{settings.GALTINN_API_URL}cards/{card_number}/"
return requests.get(url, headers=galtinn_auth, timeout=10)
def get_order(order_uuid):
url = '{}orders/{}/'.format(settings.GALTINN_API_URL, order_uuid)
return requests.get(url, headers=galtinn_auth)
url = f"{settings.GALTINN_API_URL}orders/{order_uuid}/"
return requests.get(url, headers=galtinn_auth, timeout=10)
def get_latest_order_by_card(card_number):
url = '{}orders/'.format(settings.GALTINN_API_URL)
payload = {'card_number': card_number}
orders = requests.get(url, params=payload, headers=galtinn_auth).json()
return orders.get('results')[0]
url = f"{settings.GALTINN_API_URL}orders/"
payload = {"card_number": card_number}
orders = requests.get(url, params=payload, headers=galtinn_auth, timeout=10).json()
return orders.get("results")[0]
def get_latest_order_by_phone(phone_number):
url = '{}orders/'.format(settings.GALTINN_API_URL)
payload = {'phone_number': phone_number}
orders = requests.get(url, params=payload, headers=galtinn_auth).json()
if orders.get('count'):
return orders.get('results')[0]
url = f"{settings.GALTINN_API_URL}orders/"
payload = {"phone_number": phone_number}
orders = requests.get(url, params=payload, headers=galtinn_auth, timeout=10).json()
if orders.get("count"):
return orders.get("results")[0]
return None
def get_user(user_id):
url = '{}users/{}/'.format(settings.GALTINN_API_URL, user_id)
return requests.get(url, headers=galtinn_auth)
url = f"{settings.GALTINN_API_URL}users/{user_id}/"
return requests.get(url, headers=galtinn_auth, timeout=10)
def get_user_by_phone(phone_number):
url = '{}users/'.format(settings.GALTINN_API_URL)
payload = {'phone_number': phone_number}
users = requests.get(url, params=payload, headers=galtinn_auth).json()
if users.get('count') == 1:
return users.get('results')[0]
url = f"{settings.GALTINN_API_URL}users/"
payload = {"phone_number": phone_number}
users = requests.get(url, params=payload, headers=galtinn_auth, timeout=10).json()
if users.get("count") == 1:
return users.get("results")[0]
return None
def update_card(card_number, user_id=None, order_uuid=None, transaction_id=None):
assert not (user_id and order_uuid)
url = '{}kassa/card/'.format(settings.GALTINN_API_URL)
if user_id and order_uuid:
raise ValueError("Cannot provide both user_id and order_uuid. Provide one or the other.")
url = f"{settings.GALTINN_API_URL}kassa/card/"
payload = {
'member_card': card_number,
'user': user_id,
'order': order_uuid,
'transaction_id': str(transaction_id) if transaction_id else None,
"member_card": card_number,
"user": user_id,
"order": order_uuid,
"transaction_id": str(transaction_id) if transaction_id else None,
}
return requests.patch(url, json=payload, headers=galtinn_auth)
return requests.patch(url, json=payload, headers=galtinn_auth, timeout=10)
def update_membership(user=None, phone_number=None, card_number=None, membership_type=None,
transaction_id=None):
assert membership_type in ('standard', 'trial')
url = '{}kassa/membership/'.format(settings.GALTINN_API_URL)
def update_membership(user=None, phone_number=None, card_number=None, membership_type=None, transaction_id=None):
if membership_type not in ("standard", "trial"):
raise ValueError("Invalid membership type provided. Must be 'standard' or 'trial'.")
url = f"{settings.GALTINN_API_URL}kassa/membership/"
payload = {
'user': user,
'phone_number': phone_number,
'member_card': card_number,
'membership_type': membership_type,
'transaction_id': transaction_id,
"user": user,
"phone_number": phone_number,
"member_card": card_number,
"membership_type": membership_type,
"transaction_id": transaction_id,
}
return requests.post(url, json=payload, headers=galtinn_auth)
return requests.post(url, json=payload, headers=galtinn_auth, timeout=10)
def format_phone_number(number):
@@ -104,7 +100,7 @@ def format_phone_number(number):
return number
try:
p = phonenumbers.parse(number, region='NO')
p = phonenumbers.parse(number, region="NO")
except phonenumbers.NumberParseException:
return number
@@ -115,7 +111,7 @@ def format_phone_number(number):
def is_autumn():
today = datetime.date.today()
today = datetime.datetime.now(tz=datetime.UTC).date()
_min = datetime.date(year=today.year, month=8, day=1)
_max = datetime.date(year=today.year + 1, month=1, day=1)
return _min <= today < _max
+95 -91
View File
@@ -1,21 +1,30 @@
# coding: utf-8
import json
import logging
import uuid
from http import HTTPStatus
import phonenumbers
import requests
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import render
from django.utils import timezone
from itertools import groupby
import json
import uuid
import logging
import phonenumbers
import requests
from apps.kassa.models import KassaEvent
from apps.kassa.forms import AddCardForm, SearchUserForm
from apps.kassa.utils import send_sms_activation_link, update_card, get_card, \
get_order, get_latest_order_by_card, get_latest_order_by_phone, get_user, get_user_by_phone, \
galtinn_auth, update_membership, format_phone_number, is_autumn
from apps.kassa.models import KassaEvent
from apps.kassa.utils import (
format_phone_number,
galtinn_auth,
get_card,
get_latest_order_by_card,
get_latest_order_by_phone,
get_user,
get_user_by_phone,
is_autumn,
send_sms_activation_link,
update_card,
update_membership,
)
logger = logging.getLogger(__name__)
@@ -23,142 +32,136 @@ logger = logging.getLogger(__name__)
@login_required
def register(request):
context = {
'add_card_form': AddCardForm(),
'search_user_form': SearchUserForm(),
'show_trial_membership': is_autumn()
"add_card_form": AddCardForm(),
"search_user_form": SearchUserForm(),
"show_trial_membership": is_autumn(),
}
return render(request, 'kassa/register.html', context)
return render(request, "kassa/register.html", context)
@login_required
def user_search(request):
payload = {
'search': request.GET.get('search', '')
}
url = '{}users/'.format(settings.GALTINN_API_URL)
data = requests.get(url, params=payload, headers=galtinn_auth).json()
payload = {"search": request.GET.get("search", "")}
url = f"{settings.GALTINN_API_URL}users/"
data = requests.get(url, params=payload, headers=galtinn_auth, timeout=10).json()
return JsonResponse(data)
@login_required
def check_phone_number(request):
number = request.GET.get('phone_number', '').strip()
number = request.GET.get("phone_number", "").strip()
if len(number) < 2:
return JsonResponse({'error': 'Phone number is too short'})
min_length = 2
if len(number) < min_length:
return JsonResponse({"error": "Phone number is too short"})
# Is phone number valid?
try:
p = phonenumbers.parse(number, region='NO')
p = phonenumbers.parse(number, region="NO")
if not phonenumbers.is_valid_number(p):
return JsonResponse({'error': "Phone number '{}' is invalid.".format(number)})
return JsonResponse({"error": f"Phone number '{number}' is invalid."})
except phonenumbers.NumberParseException as e:
return JsonResponse({'error': str(e).replace('(1) ', '')})
return JsonResponse({"error": str(e).replace("(1) ", "")})
number = phonenumbers.format_number(p, phonenumbers.PhoneNumberFormat.E164)
user = get_user_by_phone(number)
order = get_latest_order_by_phone(number)
return JsonResponse({
'user': user,
'order': order
})
return JsonResponse({"user": user, "order": order})
@login_required
def check_card(request):
if request.method != 'GET':
return JsonResponse({'error': 'Only method GET supported'})
if request.method != "GET":
return JsonResponse({"error": "Only method GET supported"})
card_number = request.GET.get('card_number')
card_number = request.GET.get("card_number")
response = get_card(card_number)
if response.status_code != 200:
if response.status_code != HTTPStatus.OK:
return JsonResponse(response.json(), status=response.status_code)
card = response.json()
if card.get('user'):
card['user'] = get_user(card['user']).json()
card['order'] = None
if card.get('orders'):
card['order'] = get_latest_order_by_card(card_number)
card.pop('orders')
if card.get("user"):
card["user"] = get_user(card["user"]).json()
card["order"] = None
if card.get("orders"):
card["order"] = get_latest_order_by_card(card_number)
card.pop("orders")
return JsonResponse(card, status=response.status_code)
@login_required
def register_card_and_membership(request):
if request.method != 'POST':
return JsonResponse({'error': 'Only method POST supported'})
if request.method != "POST":
return JsonResponse({"error": "Only method POST supported"})
post_data = json.loads(request.body.decode('utf-8'))
post_data = json.loads(request.body.decode("utf-8"))
# new_card_membership, update_card, add_or_renew, sms_card_notify
# TODO: multiple actions (to allow renewal only)
action = post_data.get('action')
user_id = post_data.get('user_id')
order_uuid = post_data.get('order_uuid')
card_number = post_data.get('card_number')
phone_number_raw = post_data.get('phone_number')
membership_trial = post_data.get('membership_trial')
membership_type = 'trial' if membership_trial else 'standard'
action = post_data.get("action")
user_id = post_data.get("user_id")
order_uuid = post_data.get("order_uuid")
card_number = post_data.get("card_number")
phone_number_raw = post_data.get("phone_number")
membership_trial = post_data.get("membership_trial")
membership_type = "trial" if membership_trial else "standard"
transaction_id = uuid.uuid4()
logger.debug('register_card_and_membership post_data %r', post_data)
logger.debug("register_card_and_membership post_data %r", post_data)
phone_number = format_phone_number(phone_number_raw)
# Update card number on user or order
if action in ('update_card', 'sms_card_notify'):
if action == 'update_card' and user_id:
response = update_card(card_number,
user_id=user_id)
elif action == 'sms_card_notify' and order_uuid:
response = update_card(card_number,
order_uuid=order_uuid,
transaction_id=transaction_id)
else:
assert user_id or order_uuid
if response.status_code != 200:
if action in ("update_card", "sms_card_notify"):
if action == "update_card" and user_id:
response = update_card(card_number, user_id=user_id)
elif action == "sms_card_notify" and order_uuid:
response = update_card(card_number, order_uuid=order_uuid, transaction_id=transaction_id)
elif not user_id and not order_uuid:
return JsonResponse({"error": "Either user_id or order_uuid must be set"}, status=HTTPStatus.BAD_REQUEST)
if response.status_code != HTTPStatus.OK:
return JsonResponse(response.json(), status=response.status_code)
event_data = {
'event': KassaEvent.UPDATE_CARD,
'user_phone_number': phone_number,
'card_number': card_number,
'user_galtinn_id': user_id,
'transaction_id': transaction_id
"event": KassaEvent.UPDATE_CARD,
"user_phone_number": phone_number,
"card_number": card_number,
"user_galtinn_id": user_id,
"transaction_id": transaction_id,
}
logger.debug(event_data)
KassaEvent.objects.create(**event_data)
# Add initial or renew membership for existing user or order
if action in ('add_or_renew', 'new_card_membership'):
if action in ("add_or_renew", "new_card_membership"):
response = update_membership(
user=user_id,
phone_number=phone_number,
card_number=card_number,
membership_type=membership_type,
transaction_id=str(transaction_id))
transaction_id=str(transaction_id),
)
KassaEvent.objects.create(
event=KassaEvent.ADD_OR_RENEW if membership_type != 'trial' else KassaEvent.MEMBERSHIP_TRIAL,
event=KassaEvent.ADD_OR_RENEW if membership_type != "trial" else KassaEvent.MEMBERSHIP_TRIAL,
user_galtinn_id=user_id,
card_number=card_number,
user_phone_number=phone_number,
transaction_id=transaction_id
transaction_id=transaction_id,
)
# Send activation notification (link) to user by SMS
if action in ('new_card_membership', 'sms_card_notify'):
if action in ("new_card_membership", "sms_card_notify"):
event = KassaEvent.NEW_CARD_MEMBERSHIP
if action == 'sms_card_notify':
if action == "sms_card_notify":
event = KassaEvent.SMS_CARD_NOTIFY
# FIXME: could be async
send_sms_activation_link(phone_number=phone_number,
transaction_id=str(transaction_id))
send_sms_activation_link(phone_number=phone_number, transaction_id=str(transaction_id))
KassaEvent.objects.create(
event=event,
card_number=card_number,
user_phone_number=phone_number,
transaction_id=transaction_id
event=event, card_number=card_number, user_phone_number=phone_number, transaction_id=transaction_id
)
return JsonResponse(response.json(), status=response.status_code)
@@ -166,18 +169,18 @@ def register_card_and_membership(request):
@login_required
def renew_membership(request):
if request.method != 'POST':
return JsonResponse({'error': 'Only method POST supported'})
if request.method != "POST":
return JsonResponse({"error": "Only method POST supported"})
post_data = json.loads(request.body.decode('utf-8'))
user_id = post_data.get('user_id')
phone_number_raw = post_data.get('phone_number')
card_number = post_data.get('card_number')
membership_trial = post_data.get('membership_trial')
membership_type = 'trial' if membership_trial else 'standard'
post_data = json.loads(request.body.decode("utf-8"))
user_id = post_data.get("user_id")
phone_number_raw = post_data.get("phone_number")
card_number = post_data.get("card_number")
membership_trial = post_data.get("membership_trial")
membership_type = "trial" if membership_trial else "standard"
transaction_id = str(uuid.uuid4())
logger.debug('renew_membership post_data %r', post_data)
logger.debug("renew_membership post_data %r", post_data)
phone_number = format_phone_number(phone_number_raw)
response = update_membership(
@@ -185,11 +188,12 @@ def renew_membership(request):
phone_number=phone_number,
card_number=card_number,
membership_type=membership_type,
transaction_id=transaction_id)
transaction_id=transaction_id,
)
KassaEvent.objects.create(
event=KassaEvent.RENEW_ONLY if membership_trial is None else KassaEvent.MEMBERSHIP_TRIAL,
user_galtinn_id=user_id,
transaction_id=transaction_id
transaction_id=transaction_id,
)
return JsonResponse(response.json(), status=response.status_code)
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
set -eu
cd static
npm i
npm run build
Executable
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
set -eu
source "$HOME/.local/bin/env"
export DJANGO_SETTINGS_MODULE=kassasite.settings
PROJECT_PATH=/var/www/neuf.no/kassa
PROCESS_NAME="kassa.neuf.no"
echo "Deploying kassa to ${PROJECT_PATH}"
cd "$PROJECT_PATH" || exit 1
git pull
# Frontend
bin/build-frontend
# Backend
uv sync
umask 022; uv run manage.py collectstatic --noinput -i node_modules -c # Collect static (including frontend)
uv run manage.py migrate # Run DB migrations
# Reload backend
echo "Reloading supervisor processes ${PROCESS_NAME} ${PROCESS_NAME}-celery"
sudo /usr/bin/supervisorctl pid "$PROCESS_NAME" | xargs kill -HUP
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -eu
pre-commit run -a
Vendored
+15 -16
View File
@@ -7,27 +7,26 @@ from invoke import task
@task()
def deploy(c):
"""Make sure proxy_user is set to your neuf username."""
project_path = '/var/www/neuf.no/kassa'
proxy_user = os.getenv('DEPLOY_USER', os.getenv('USER'))
project_path = "/var/www/neuf.no/kassa"
proxy_user = os.getenv("DEPLOY_USER", os.getenv("USER"))
c = Connection(host='gitdeploy@dreamcast.neuf.no', gateway=Connection('login.neuf.no', user=proxy_user))
c = Connection(host="gitdeploy@dreamcast.neuf.no", gateway=Connection("login.neuf.no", user=proxy_user))
with c.cd(project_path), c.prefix('source {}/venv/bin/activate'.format(project_path)):
c.run('git pull') # Get source
c.run('pip install -U pip')
c.run('pip install -r requirements.txt') # install deps in virtualenv
with c.cd('static'): # install and compile frontend deps
c.run('npm i')
c.run('npm run build')
c.run('python manage.py collectstatic --noinput -i node_modules') # Collect static
c.run('python manage.py migrate') # Run DB migrations
with c.cd(project_path), c.prefix(f"source {project_path}/venv/bin/activate"):
c.run("git pull") # Get source
c.run("uv sync") # install deps in virtualenv
with c.cd("static"): # install and compile frontend deps
c.run("npm i")
c.run("npm run build")
c.run("uv run manage.py collectstatic --noinput -i node_modules") # Collect static
c.run("uv run manage.py migrate") # Run DB migrations
# Reload gunicorn
c.sudo('/usr/bin/supervisorctl pid kassa.neuf.no | xargs kill -HUP', shell=False)
c.sudo("/usr/bin/supervisorctl pid kassa.neuf.no | xargs kill -HUP", shell=False)
@task
def build(c):
with c.cd('static'):
c.run('npm i')
c.run('npm run build')
with c.cd("static"):
c.run("npm i")
c.run("npm run build")
+67 -81
View File
@@ -9,16 +9,17 @@ https://docs.djangoproject.com/en/1.8/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
import contextlib
from pathlib import Path
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = Path(__file__).parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '79l0@&(^ra9=s783rqeo2awhs6@c!j&wan6t-0wi8azyd@!x+7'
SECRET_KEY = "79l0@&(^ra9=s783rqeo2awhs6@c!j&wan6t-0wi8azyd@!x+7" # noqa: S105
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@@ -29,132 +30,117 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'bootstrapform',
'django_extensions',
'corsheaders',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"bootstrapform",
"django_extensions",
"corsheaders",
)
LOCAL_APPS = ('apps.kassa',)
LOCAL_APPS = ("apps.kassa",)
INSTALLED_APPS += LOCAL_APPS
MIDDLEWARE = (
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django.middleware.security.SecurityMiddleware",
)
ROOT_URLCONF = 'kassasite.urls'
ROOT_URLCONF = "kassasite.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'apps/kassa/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',
'apps.kassa.context_processors.sentry',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR.joinpath("apps/kassa/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",
"apps.kassa.context_processors.sentry",
],
},
},
]
WSGI_APPLICATION = 'kassasite.wsgi.application'
WSGI_APPLICATION = "kassasite.wsgi.application"
# Database
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR.joinpath("db.sqlite3"),
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Europe/Oslo'
LANGUAGE_CODE = "en-us"
TIME_ZONE = "Europe/Oslo"
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.8/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATIC_URL = "/static/"
STATICFILES_DIRS = (BASE_DIR.joinpath("static"),)
STATIC_ROOT = BASE_DIR.joinpath("staticfiles")
LOGIN_REDIRECT_URL = '/'
LOGIN_REDIRECT_URL = "/"
GALTINN_API_KEY = ''
GALTINN_API_URL = 'https://galtinn.neuf.no/api/'
GALTINN_ACTIVATION_URL = 'https://galtinn.neuf.no/activate/{to}/{code}'
GALTINN_API_KEY = ""
GALTINN_API_URL = "https://galtinn.neuf.no/api/"
GALTINN_ACTIVATION_URL = "https://galtinn.neuf.no/activate/{to}/{code}"
TEKSTMELDING_API_KEY = ''
TEKSTMELDING_API_URL = 'https://tekstmelding.neuf.no/'
TEKSTMELDING_API_KEY = ""
TEKSTMELDING_API_URL = "https://tekstmelding.neuf.no/"
TEKSTMELDING_ENABLED = True
TEKSTMELDING_ACTIVATION_SMS_TEMPLATE = 'Velkommen som medlem! Registrer deg her: {url}'
TEKSTMELDING_ACTIVATION_SMS_TEMPLATE = "Velkommen som medlem! Registrer deg her: {url}"
CORS_ORIGIN_ALLOW_ALL = True
LOGGING = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s '
'%(process)d %(thread)d %(message)s'
},
"version": 1,
"disable_existing_loggers": True,
"formatters": {
"verbose": {"format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'verbose'
}
},
'loggers': {
'root': {
'level': 'WARNING',
'handlers': ['console'],
"handlers": {"console": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose"}},
"loggers": {
"root": {
"level": "WARNING",
"handlers": ["console"],
},
'django.db.backends': {
'level': 'ERROR',
'handlers': ['console'],
'propagate': False,
"django.db.backends": {
"level": "ERROR",
"handlers": ["console"],
"propagate": False,
},
},
}
SENTRY_DSN = None
SENTRY_ENVIRONMENT = 'dev'
SENTRY_ENVIRONMENT = "dev"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Local settings
try:
from .local_settings import *
except ImportError:
pass
with contextlib.suppress(ImportError):
from .local_settings import * # noqa: F403
+2 -8
View File
@@ -1,13 +1,7 @@
from django.conf.urls import include, url
from django.contrib import admin
from django.urls import path
from django.contrib.auth import urls as auth_urls
from django.urls import include, path
from apps.kassa import urls as kassa_urls
urlpatterns = [
path('admin/', admin.site.urls),
url('^', include(kassa_urls)),
path('accounts/', include(auth_urls))
]
urlpatterns = [path("admin/", admin.site.urls), path("", include(kassa_urls)), path("accounts/", include(auth_urls))]
+47
View File
@@ -0,0 +1,47 @@
[project]
name = "kassa"
version = "0.1.0"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"django==4.2.*",
"django-auth-ldap==5.*",
"django-bootstrap-form==3.4",
"django-cors-headers==4.*",
"django-extensions==4.*",
"django-phonenumber-field==8.*",
"fabric==3.*",
"gunicorn==23.*",
"mysqlclient==2.*",
"phonenumbers==9.*",
"requests==2.*",
"sentry-sdk==1.*",
]
[tool.ruff]
line-length = 120
target-version = "py312"
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"D", # pydocstyle: Allow undocumented code
"ANN", # flake8-annotations: Allow untyped code
"TD", "FIX", # Allow TODOs and FIXMEs
"FBT002", # Allow boolean args
"G004", # Allow logging using f-string
"EM101", "EM102", # Allow string in exceptions (prioritice code readability vs exception time readability)
"TRY003", # Allow long exception messages
"E501", # Allow long lines
"COM812", "ISC001", # Avoid conflicting with ruff formatter
"RUF012" # FIXME: Fixy fix
]
[dependency-groups]
dev = [
"ruff>=0.11.12",
]
# [tool.ruff.lint.pyupgrade]
# keep-runtime-typing = true
-12
View File
@@ -1,12 +0,0 @@
django-auth-ldap==2.1.*
django-bootstrap-form==3.4
django-cors-headers==3.2.*
django-extensions==2.2.*
django-phonenumber-field==4.0.*
django==2.2.*
fabric==2.5.*
gunicorn==20.0.*
mysqlclient==1.4.6
phonenumbers==8.11.*
requests==2.22.*
sentry-sdk==0.13.*
-4
View File
@@ -1,4 +0,0 @@
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime"]
}
-29
View File
@@ -1,29 +0,0 @@
{
"env": {
"browser": true
},
"extends": [
"airbnb-base",
"plugin:prettier/recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"rules": {
"no-plusplus": "off",
"no-console": "off",
"no-underscore-dangle": "off",
"no-param-reassign": "off",
"no-restricted-syntax": "off",
},
"settings": {
"import/resolver": {
"node": {}, // https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-509384041
"webpack": {
"config": "webpack.config.js"
}
}
}
,
"plugins": ["prettier"],
"parser": "babel-eslint"
}
+30
View File
@@ -0,0 +1,30 @@
module.exports = {
env: {
browser: true,
},
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
extends: ['plugin:prettier/recommended', 'plugin:import/errors', 'plugin:import/warnings'],
rules: {
'no-console': 'off',
'prefer-destructuring': 'off',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: ['**/*.test.js', '**/*.spec.js', '**/__tests__/**'],
},
],
'import/prefer-default-export': 'off',
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']],
'newlines-between': 'always',
alphabetize: { order: 'asc' },
},
],
},
plugins: ['prettier'],
};
+1
View File
@@ -0,0 +1 @@
v20
+1 -3
View File
@@ -1,5 +1,3 @@
{
"singleQuote": true,
"printWidth": 120,
"arrowParens": "always"
"printWidth": 120
}
+165 -166
View File
@@ -1,15 +1,16 @@
import * as $ from 'jquery';
import moment from 'moment';
import nunjucks from 'nunjucks';
import debounce from 'lodash.debounce';
import sortBy from 'lodash.sortby';
import fromPairs from 'lodash.frompairs';
import $ from "jquery";
window.$ = window.jQuery = $; // Make sure jQuery is available globally
import debounce from "lodash.debounce";
import fromPairs from "lodash.frompairs";
import sortBy from "lodash.sortby";
import moment from "moment";
import nunjucks from "nunjucks";
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
import "bootstrap-sass/assets/javascripts/bootstrap/tooltip";
import "bootstrap-sass/assets/javascripts/bootstrap/popover";
import './templates';
import './index.scss';
import "./templates";
import "./index.scss";
/* Init and global vars */
let _dom;
@@ -20,29 +21,29 @@ let membershipOrder = null; /* selected order object */
const cardForm = {
fields: {
cardNumber: false,
phoneNumber: false
}
phoneNumber: false,
},
};
const STATE_ICON = {
success: 'ok',
warning: 'warning-sign',
error: 'remove'
success: "ok",
warning: "warning-sign",
error: "remove",
};
// TODO: replace with lodash template
const nunjucksEnv = new nunjucks.Environment();
const urls = {
userSearch: '/user-search/',
checkPhoneNumber: '/check-phone-number/',
checkCard: '/check-card/',
registerCardAndMembership: '/register-card-membership/',
renewMembership: '/renew-membership/'
userSearch: "/user-search/",
checkPhoneNumber: "/check-phone-number/",
checkCard: "/check-card/",
registerCardAndMembership: "/register-card-membership/",
renewMembership: "/renew-membership/",
};
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = $.trim(cookies[i]);
// Does this cookie string begin with the name we want?
@@ -56,12 +57,12 @@ function getCookie(name) {
}
function getParameterByName(name) {
return new URLSearchParams(window.location.search).get(name) || '';
return new URLSearchParams(window.location.search).get(name) || "";
}
function formatPhoneNumber(str) {
if (!str || str.length <= 1) {
return 'No phone number';
return "No phone number";
}
if (str.length === 11) {
return `${str.slice(0, 3)} ${str.slice(3, 6)} ${str.slice(6, 8)} ${str.slice(8)}`;
@@ -83,7 +84,7 @@ function sortSearchResults(results) {
}
function formatResults(results) {
return nunjucksEnv.render('search_results.html', { results });
return nunjucksEnv.render("search_results.html", { results });
}
function renderSelectedUser(userData) {
@@ -93,7 +94,7 @@ function renderSelectedUser(userData) {
} else {
context = { res: userData, checked: true, no_user_actions: true };
}
const selectedUserHtml = nunjucksEnv.render('search_result.html', context);
const selectedUserHtml = nunjucksEnv.render("search_result.html", context);
_dom.selectedEntityWrap.html(selectedUserHtml);
}
@@ -102,7 +103,7 @@ function renderSelectedOrder(order) {
return;
}
const context = { order, checked: true, no_user_actions: true };
const selectedOrderHtml = nunjucksEnv.render('selected_order.html', context);
const selectedOrderHtml = nunjucksEnv.render("selected_order.html", context);
_dom.selectedEntityWrap.html(selectedOrderHtml);
}
@@ -110,31 +111,29 @@ function setToast(message, messageType) {
const context = {
message,
message_type: messageType,
icon: STATE_ICON[messageType]
icon: STATE_ICON[messageType],
};
const html = nunjucksEnv.render('toast.html', context);
const html = nunjucksEnv.render("toast.html", context);
_dom.toastWrap.html(html);
_dom.toastWrap.addClass('visible');
_dom.toastWrap.addClass("visible");
setTimeout(() => {
_dom.toastWrap.removeClass('visible');
_dom.toastWrap.removeClass("visible");
}, 5000);
}
function updateMemberShipButton(user, order) {
const todayPlusThirtyDays = moment()
.add(30, 'days')
.format('YYYY-MM-DD');
// const todayPlusThirtyDays = moment().add(30, 'days').format('YYYY-MM-DD');
/* User with valid membership */
if (user && user.is_member) {
/* ...which is lifelong cannot renew */
if (user.last_membership.membership_type === 'lifelong') {
_dom.membershipSubmitButton.prop('disabled', true);
if (user.last_membership.membership_type === "lifelong") {
_dom.membershipSubmitButton.prop("disabled", true);
}
/* ...or expiring soon and the user has a card */
// else if (user.last_membership.end_date <= todayPlusThirtyDays && user.active_member_card) {
// FIXME: ponas workaround 2019-12-17, block below commented out
_dom.membershipSubmitButton.prop('disabled', false);
_dom.membershipSubmitButton.prop("disabled", false);
/* ...with a membership that expires soon can renew */
// else if (user.last_membership.end_date <= todayPlusThirtyDays) {
@@ -144,10 +143,10 @@ function updateMemberShipButton(user, order) {
/* User with expired membership can renew */
// else if (user && !user.is_member && user.active_member_card) {
else if (user && !user.is_member) {
_dom.membershipSubmitButton.prop('disabled', false);
_dom.membershipSubmitButton.prop("disabled", false);
} else if (order) {
// FIXME: ponas workaround 2019-12-17, block below commented out
_dom.membershipSubmitButton.prop('disabled', false);
_dom.membershipSubmitButton.prop("disabled", false);
/* Order */
/* If the membership has expired it can be renewed */
@@ -155,7 +154,7 @@ function updateMemberShipButton(user, order) {
// _dom.membershipSubmitButton.prop('disabled', false);
// }
} else {
_dom.membershipSubmitButton.prop('disabled', true);
_dom.membershipSubmitButton.prop("disabled", true);
}
}
@@ -163,14 +162,14 @@ function setSelectedUser(user, updateSearchResult) {
selectedUser = user;
updateMemberShipButton(user, null);
_dom.userIdField.val((user && user.id) || '').trigger('change');
_dom.userIdField.val((user && user.id) || "").trigger("change");
if (updateSearchResult) {
const staleSearchResult = _dom.results.find('.search-result').removeClass('selected');
staleSearchResult.find('input').prop('checked', false);
const staleSearchResult = _dom.results.find(".search-result").removeClass("selected");
staleSearchResult.find("input").prop("checked", false);
/* User is still in search result, mark selected */
if (user && users && users.find(({ id }) => id === user.id)) {
const searchResult = _dom.results.find(`[data-user-id="${user.id}"]`).toggleClass('selected');
searchResult.find('input').prop('checked', true);
const searchResult = _dom.results.find(`[data-user-id="${user.id}"]`).toggleClass("selected");
searchResult.find("input").prop("checked", true);
}
}
}
@@ -180,40 +179,40 @@ function setSelectedOrder(order) {
if (order) {
updateMemberShipButton(null, order);
}
_dom.orderUuidField.val((order && order.uuid) || '').trigger('change');
_dom.orderUuidField.val((order && order.uuid) || "").trigger("change");
}
function setFieldState(fieldElement, state, helpText) {
if (!helpText) {
helpText = '';
helpText = "";
}
const helpSpan = `<span class="help-block ">${helpText}</span>`;
const feedbackSpan = `<span class="glyphicon glyphicon-${STATE_ICON[state]} form-control-feedback" aria-hidden="true"></span>`;
const cssClass = `has-${state}`;
const formGroup = fieldElement.parent().parent();
formGroup.removeClass('has-success has-warning has-error has-feedback'); // reset
if (['success', 'warning', 'error'].indexOf(state) !== -1) {
formGroup.removeClass("has-success has-warning has-error has-feedback"); // reset
if (["success", "warning", "error"].indexOf(state) !== -1) {
formGroup.addClass(`${cssClass} has-feedback`);
/* Feedback icon */
const feedbackEl = formGroup.find('.form-control-feedback');
const feedbackEl = formGroup.find(".form-control-feedback");
if (!feedbackEl.length) {
// add
formGroup.append(feedbackSpan);
} else {
// Update
feedbackEl.removeClass('glyphicon-ok glypicon-warning-sign glyphicon-remove');
feedbackEl.removeClass("glyphicon-ok glypicon-warning-sign glyphicon-remove");
feedbackEl.addClass(`glyphicon-${STATE_ICON[state]}`);
}
/* Help text */
const helpTextEl = formGroup.find('.help-block');
const helpTextEl = formGroup.find(".help-block");
if (!helpTextEl.length) {
formGroup.append(helpSpan);
} else {
helpTextEl.text(helpText);
}
} else {
formGroup.find('.form-control-feedback').remove();
formGroup.find('.help-block').remove();
formGroup.find(".form-control-feedback").remove();
formGroup.find(".help-block").remove();
}
}
function resetCardForm(resetNative) {
@@ -221,18 +220,18 @@ function resetCardForm(resetNative) {
_dom.registerCardForm.get(0).reset();
}
/* Clear field states */
setFieldState(_dom.phoneNumberField, '');
setFieldState(_dom.phoneNumberField, "");
cardForm.fields.phoneNumber = false;
setFieldState(_dom.cardNumberField, '');
setFieldState(_dom.cardNumberField, "");
cardForm.fields.cardNumber = false;
selectedUser = null;
membershipOrder = null;
_dom.userIdField.val('').trigger('change');
_dom.membershipTrialCheckBox.prop('checked', false);
_dom.userIdField.val("").trigger("change");
_dom.membershipTrialCheckBox.prop("checked", false);
/* Disable submit buttons */
_dom.registerSubmitButton.prop('disabled', true);
_dom.membershipSubmitButton.prop('disabled', true);
_dom.registerSubmitButton.prop("disabled", true);
_dom.membershipSubmitButton.prop("disabled", true);
}
function resetSearchForm(resetNative) {
if (resetNative) {
@@ -240,7 +239,7 @@ function resetSearchForm(resetNative) {
}
/* Clear search results */
users = null;
_dom.results.html('');
_dom.results.html("");
}
function cardFormIsValid() {
@@ -249,41 +248,41 @@ function cardFormIsValid() {
function updateSubmitButton() {
if (cardFormIsValid()) {
_dom.registerSubmitButton.prop('disabled', false);
_dom.registerSubmitButton.prop("disabled", false);
return;
}
_dom.registerSubmitButton.prop('disabled', true);
_dom.registerSubmitButton.prop("disabled", true);
}
function validatePhoneNumber(val) {
if (val.length === 0) {
cardForm.fields.phoneNumber = false;
updateSubmitButton();
return 'Phone number should not be empty.';
return "Phone number should not be empty.";
}
cardForm.fields.phoneNumber = true;
updateSubmitButton();
return '';
return "";
}
function validateCardNumber(val) {
if (val.length === 0) {
cardForm.fields.cardNumber = false;
updateSubmitButton();
return 'Card number should not be empty.';
return "Card number should not be empty.";
}
if (val.length > 0 && val[0] !== '1') {
if (val.length > 0 && val[0] !== "1") {
cardForm.fields.cardNumber = false;
updateSubmitButton();
return 'Card number should start with 1.';
return "Card number should start with 1.";
}
if (val.length !== 9) {
cardForm.fields.cardNumber = false;
updateSubmitButton();
return 'Card number should be 9 digits long.';
return "Card number should be 9 digits long.";
}
cardForm.fields.cardNumber = true;
updateSubmitButton();
return '';
return "";
}
function getFormData(formElement) {
@@ -307,7 +306,7 @@ function checkPhoneNumber() {
if (data.error) {
setSelectedUser(null, true);
setSelectedOrder(null);
setFieldState(_dom.phoneNumberField, 'error', data.error);
setFieldState(_dom.phoneNumberField, "error", data.error);
cardForm.fields.phoneNumber = false;
updateSubmitButton();
return;
@@ -315,12 +314,12 @@ function checkPhoneNumber() {
const { user } = data;
const { order } = data;
let message = '';
let message = "";
/* Has valid card membership */
if (!user && order && order.member_card && order.product.is_valid) {
message = `Phone number is already tied to card ${order.member_card} and is valid until ${order.product.end_date}.`;
setFieldState(_dom.phoneNumberField, 'error', message);
setFieldState(_dom.phoneNumberField, "error", message);
cardForm.fields.phoneNumber = false;
setSelectedOrder(order);
updateSubmitButton();
@@ -329,8 +328,8 @@ function checkPhoneNumber() {
if (user) {
/* Existing user */
message = 'Phone number belongs to existing user.';
setFieldState(_dom.phoneNumberField, 'success', message);
message = "Phone number belongs to existing user.";
setFieldState(_dom.phoneNumberField, "success", message);
setSelectedUser(user, true);
setSelectedOrder(null);
cardForm.fields.phoneNumber = true;
@@ -339,9 +338,9 @@ function checkPhoneNumber() {
setSelectedOrder(order);
/* Valid membership, no card associated */
if (order.product.is_valid) {
message = 'Phone number has a valid membership.';
message = "Phone number has a valid membership.";
} else {
message = 'Phone number has an expired membership.';
message = "Phone number has an expired membership.";
}
} else {
/* Valid phone number associated user or order */
@@ -349,61 +348,61 @@ function checkPhoneNumber() {
setSelectedUser(null);
}
setFieldState(_dom.phoneNumberField, 'success', message);
setFieldState(_dom.phoneNumberField, "success", message);
cardForm.fields.phoneNumber = true;
updateSubmitButton();
});
}
/* Add filter |phoneNumber */
nunjucksEnv.addFilter('phoneNumber', formatPhoneNumber);
nunjucksEnv.addFilter("phoneNumber", formatPhoneNumber);
/* On document ready */
$(document).ready(function() {
$(function () {
_dom = {
results: $('.results-wrap'),
query: $('#id_query'),
phoneNumberLabel: $('[for=id_phone_number]'),
phoneNumberField: $('#id_phone_number'),
cardNumberField: $('#id_card_number'),
registerCardForm: $('.register-card-form'),
registerSubmitButton: $('#register-submit-btn'),
usernameField: $('#id_username'),
userIdField: $('#id_user_id'),
orderUuidField: $('#id_order_uuid'),
selectedEntityWrap: $('.register-card-form--selected-entity-wrap'),
toastWrap: $('.toast-wrap'),
registerResetButton: $('.register-reset-btn'),
searchResetButton: $('.search-reset-btn'),
membershipSubmitButton: $('#membership-submit-btn'),
membershipTrialCheckBox: $('#id_membership_trial')
results: $(".results-wrap"),
query: $("#id_query"),
phoneNumberLabel: $("[for=id_phone_number]"),
phoneNumberField: $("#id_phone_number"),
cardNumberField: $("#id_card_number"),
registerCardForm: $(".register-card-form"),
registerSubmitButton: $("#register-submit-btn"),
usernameField: $("#id_username"),
userIdField: $("#id_user_id"),
orderUuidField: $("#id_order_uuid"),
selectedEntityWrap: $(".register-card-form--selected-entity-wrap"),
toastWrap: $(".toast-wrap"),
registerResetButton: $(".register-reset-btn"),
searchResetButton: $(".search-reset-btn"),
membershipSubmitButton: $("#membership-submit-btn"),
membershipTrialCheckBox: $("#id_membership_trial"),
};
const lazyCheckPhoneNumber = debounce(checkPhoneNumber, 250);
_dom.phoneNumberField.on('input', (e) => {
_dom.phoneNumberField.on("input", (e) => {
const val = e.target.value.trim();
const validationMsg = validatePhoneNumber(val);
if (validationMsg !== '') {
if (validationMsg !== "") {
setSelectedUser(null, true);
setFieldState(_dom.phoneNumberField, 'error', validationMsg);
setFieldState(_dom.phoneNumberField, "error", validationMsg);
return;
}
lazyCheckPhoneNumber();
});
/* Card number as you type */
_dom.cardNumberField.on('input', (e) => {
_dom.cardNumberField.on("input", (e) => {
/* Card number should exist in the database and not tied to existing user */
const val = e.target.value.trim();
const validationMsg = validateCardNumber(val);
if (validationMsg !== '') {
setFieldState(_dom.cardNumberField, 'error', validationMsg);
if (validationMsg !== "") {
setFieldState(_dom.cardNumberField, "error", validationMsg);
return;
}
$.getJSON(urls.checkCard, { card_number: val }, (data) => {
if (data.error) {
updateSubmitButton();
cardForm.fields.cardNumber = false;
setFieldState(_dom.cardNumberField, 'error', data.error);
setFieldState(_dom.cardNumberField, "error", data.error);
return;
}
@@ -414,18 +413,18 @@ $(document).ready(function() {
if (!card) {
cardForm.fields.cardNumber = false;
setFieldState(_dom.cardNumberField, 'error', 'Cannot find card number in database.');
setFieldState(_dom.cardNumberField, "error", "Cannot find card number in database.");
} else if (card && card.registered !== null && user === null && order && !order.product.is_valid) {
/* An already registered card (with no user) can repurchase membership if expired */
setSelectedOrder(order);
cardForm.fields.cardNumber = true;
setFieldState(
_dom.cardNumberField,
'success',
"success",
`Card number is in use (not activated) and belongs to ${formatPhoneNumber(order.phone_number)}.`
);
} else if (card && card.registered !== null) {
let ownerString = '???';
let ownerString = "???";
if (user) {
ownerString = `existing user: ${user.first_name} ${user.last_name} (${formatPhoneNumber(user.phone_number)})`;
} else {
@@ -433,26 +432,26 @@ $(document).ready(function() {
ownerString = `phone number: ${formatPhoneNumber(order.phone_number)}`;
}
cardForm.fields.cardNumber = false;
setFieldState(_dom.cardNumberField, 'error', `Card number is in use and belongs to ${ownerString}.`);
setFieldState(_dom.cardNumberField, "error", `Card number is in use and belongs to ${ownerString}.`);
} else if (card && user === null) {
cardForm.fields.cardNumber = true;
setFieldState(_dom.cardNumberField, 'success');
setFieldState(_dom.cardNumberField, "success");
} else {
cardForm.fields.cardNumber = true;
setFieldState(_dom.cardNumberField, '');
setFieldState(_dom.cardNumberField, "");
}
updateSubmitButton();
}).fail(() => {
cardForm.fields.cardNumber = false;
setFieldState(_dom.cardNumberField, 'error', 'Cannot find card number in database.');
setFieldState(_dom.cardNumberField, "error", "Cannot find card number in database.");
});
});
/* User search as you type */
_dom.query.on('input', () => {
_dom.query.on("input", () => {
const val = _dom.query.val().trim();
if (val.length <= 2) {
_dom.results.html('');
_dom.results.html("");
return;
}
if (searching) {
@@ -465,38 +464,38 @@ $(document).ready(function() {
users = data.results;
} else if (data.error) {
_dom.results.html(data.error);
} else if (val !== '') {
} else if (val !== "") {
_dom.results.html(`Found no user matching: '${val}'`);
} else {
_dom.results.html('');
_dom.results.html("");
}
});
});
/* On search result click */
_dom.results.on('click', '.search-result input', () => {
_dom.results.on("click", ".search-result input", () => {
const label = $(this).parent();
let number = label.attr('data-phone-number');
const userId = label.attr('data-user-id');
let number = label.attr("data-phone-number");
const userId = label.attr("data-user-id");
/* Update selected state */
$('.search-result').removeClass('selected');
label.toggleClass('selected');
$(".search-result").removeClass("selected");
label.toggleClass("selected");
setSelectedUser(users.find(({ id }) => id === userId));
/* Update phone number field */
if (number === '-') {
number = '';
if (number === "-") {
number = "";
}
_dom.phoneNumberField.val(number).trigger('input');
_dom.phoneNumberField.val(number).trigger("input");
});
/* On submit button click */
_dom.registerSubmitButton.on('click', (e) => {
_dom.registerSubmitButton.on("click", (e) => {
e.preventDefault();
if (!cardFormIsValid()) {
setToast('Either phone number or card number is not valid', 'error');
setToast("Either phone number or card number is not valid", "error");
return;
}
const payload = getFormData(_dom.registerCardForm);
@@ -506,66 +505,66 @@ $(document).ready(function() {
// action: 'new_card_membership', 'update_card', 'add_or_renew', 'sms_card_notify'
if (selectedUser === null && membershipOrder === null) {
// Unknown phone / card, new membership
payload.action = 'new_card_membership';
payload.action = "new_card_membership";
} else if (selectedUser === null && membershipOrder !== null) {
if (membershipOrder.member_card === null && membershipOrder.product.is_valid) {
// Associate member card with a valid membership order
payload.action = 'sms_card_notify';
payload.action = "sms_card_notify";
payload.order_uuid = membershipOrder.uuid;
} else {
// New card for expired order -- treat as new card membership
payload.action = 'new_card_membership';
payload.action = "new_card_membership";
}
} else if (selectedUser !== null && selectedUser.is_member) {
// Add member card to a user
payload.action = 'update_card';
payload.action = "update_card";
} else {
// New order for user
payload.action = 'add_or_renew'; // Note: could also update card number
payload.action = "add_or_renew"; // Note: could also update card number
}
$.ajax(urls.registerCardAndMembership, {
data: JSON.stringify(payload),
contentType: 'application/json',
dataType: 'json',
type: 'post',
headers: { 'X-CSRFToken': getCookie('csrftoken') }
contentType: "application/json",
dataType: "json",
type: "post",
headers: { "X-CSRFToken": getCookie("csrftoken") },
})
.done(() => {
let successMessage = `New card registered ${payload.card_number}`;
if (payload.action === 'add_or_renew') {
if (payload.action === "add_or_renew") {
const fullName = `${selectedUser.first_name} ${selectedUser.last_name}`;
successMessage = `New membership and card number ${payload.card_number} registered to ${fullName} `;
} else if (payload.action === 'new_card_membership') {
} else if (payload.action === "new_card_membership") {
successMessage = `New membership and card registered to ${formatPhoneNumber(
payload.phone_number
)}. Activation SMS sent `;
} else if (payload.action === 'sms_card_notify') {
} else if (payload.action === "sms_card_notify") {
successMessage = `New card registered to ${formatPhoneNumber(
payload.phone_number
)}. Membership already paid ${membershipOrder.product.start_date}. Activation SMS sent `;
}
setToast(`${successMessage} :-)`, 'success');
setToast(`${successMessage} :-)`, "success");
resetCardForm(true);
})
.fail((data) => {
console.log('failed', data);
console.log("failed", data);
let errorText = data.responseText;
if (data.responseJSON) {
if (data.responseJSON.non_field_errors) {
errorText = data.responseJSON.non_field_errors.join('<br>');
errorText = data.responseJSON.non_field_errors.join("<br>");
} else {
errorText = 'Not sure why. :-(';
errorText = "Not sure why. :-(";
}
}
setToast(`Failed! ${errorText}`, 'error');
setToast(`Failed! ${errorText}`, "error");
});
});
/* Membership renewal button */
_dom.membershipSubmitButton.on('click', (e) => {
_dom.membershipSubmitButton.on("click", (e) => {
e.preventDefault();
if (!selectedUser && !membershipOrder) {
setToast('Computer says no.', 'error');
setToast("Computer says no.", "error");
return;
}
@@ -579,62 +578,62 @@ $(document).ready(function() {
$.ajax(urls.renewMembership, {
data: JSON.stringify(payload),
contentType: 'application/json',
dataType: 'json',
type: 'post',
headers: { 'X-CSRFToken': getCookie('csrftoken') }
contentType: "application/json",
dataType: "json",
type: "post",
headers: { "X-CSRFToken": getCookie("csrftoken") },
})
.done((data) => {
let name = 'unknown';
let name = "unknown";
if (data.user) {
name = `${selectedUser.first_name} ${selectedUser.last_name}`;
} else {
name = `phone number ${data.phone_number}`;
}
const successMessage = `Membership renewed for ${name} :-)`;
setToast(successMessage, 'success');
setToast(successMessage, "success");
resetCardForm(true);
})
.fail((data) => {
console.log('failed', data);
console.log("failed", data);
let errorText = data.responseText;
if (data.responseJSON) {
if (data.responseJSON.non_field_errors) {
errorText = data.responseJSON.non_field_errors.join('<br>');
errorText = data.responseJSON.non_field_errors.join("<br>");
} else {
errorText = 'Not sure why. :-(';
errorText = "Not sure why. :-(";
}
}
setToast(`Failed! ${errorText}`, 'error');
setToast(`Failed! ${errorText}`, "error");
});
});
/* On user id change */
_dom.userIdField.on('change', function() {
_dom.userIdField.on("change", function () {
/* Render selected user in search form */
renderSelectedUser(selectedUser);
});
/* On order uuid change */
_dom.orderUuidField.on('change', function() {
_dom.orderUuidField.on("change", function () {
/* Render selected order */
renderSelectedOrder(membershipOrder);
});
/* Reset buttons */
_dom.registerResetButton.on('click', function() {
_dom.registerResetButton.on("click", function () {
resetCardForm();
});
_dom.searchResetButton.on('click', function() {
_dom.searchResetButton.on("click", function () {
resetSearchForm();
});
/* Ninja add some icons to bootstrap form */
const iconMobile =
'<span class="input-group-addon"><span class="glyphicon glyphicon-phone glyphicon-phone-large"></span></span>';
_dom.phoneNumberField.parent().addClass('input-group');
_dom.phoneNumberField.parent().addClass("input-group");
_dom.phoneNumberField.before(iconMobile);
/* Render initial placeholder user */
const selectedUserHtml = nunjucksEnv.render('search_result.html', { placeholder: true });
const selectedUserHtml = nunjucksEnv.render("search_result.html", { placeholder: true });
_dom.selectedEntityWrap.html(selectedUserHtml);
/* Initial focus */
@@ -643,18 +642,18 @@ $(document).ready(function() {
}
/* Load query params */
if (getParameterByName('user_id')) {
_dom.userIdField.val(getParameterByName('user_id')).trigger('change');
if (getParameterByName("user_id")) {
_dom.userIdField.val(getParameterByName("user_id")).trigger("change");
}
if (getParameterByName('phone_number')) {
_dom.phoneNumberField.val(getParameterByName('phone_number')).trigger('input');
if (getParameterByName("phone_number")) {
_dom.phoneNumberField.val(getParameterByName("phone_number")).trigger("input");
}
if (getParameterByName('card_number')) {
_dom.cardNumberField.val(getParameterByName('card_number')).trigger('input');
if (getParameterByName("card_number")) {
_dom.cardNumberField.val(getParameterByName("card_number")).trigger("input");
}
$('[data-toggle="popover"]').popover({
html: true,
trigger: 'click'
trigger: "click",
});
});
-134
View File
@@ -1,134 +0,0 @@
/* global require */
'use strict';
var gulp = require('gulp');
// load plugins
var $ = require('gulp-load-plugins')();
var bowerFiles = require('main-bower-files');
var del = require('del');
var browserSync = require('browser-sync').create();
var reload = browserSync.reload;
gulp.task('styles', function () {
gulp.src('app/styles/*.scss')
.pipe($.sass({
includePaths: ['bower_components/foundation/scss'],
sourceComments: 'map' // TODO does this work?
}).on('error', $.sass.logError))
.pipe($.autoprefixer('last 1 version'))
.pipe(gulp.dest('dist/styles'))
.pipe(browserSync.stream())
.pipe($.size());
});
gulp.task('diststyles', function () {
return gulp.src('app/styles/*.scss')
.pipe($.sass({
includePaths: ['bower_components/foundation/scss']
}))
.pipe($.autoprefixer('last 1 version'))
.pipe($.csso())
.pipe(gulp.dest('dist/styles'))
.pipe(browserSync.stream())
.pipe($.size());
});
gulp.task('vendorscripts', function () {
return gulp.src(bowerFiles())
.pipe($.filter('**/*.js'))
//.pipe($.uglify())
.pipe($.concat('vendor.js'))
.pipe(gulp.dest('dist/scripts'))
.pipe($.size());
});
gulp.task('vendorstyles', function () {
// TODO: copy styles to scss-files instead?
return gulp.src(bowerFiles())
.pipe($.filter('**/*.css'))
.pipe($.concat('vendor.css'))
//.pipe($.csso())
.pipe(gulp.dest('dist/styles'))
.pipe($.size());
});
gulp.task('scripts', function () {
return gulp.src('app/scripts/**/*.js')
.pipe($.jshint())
.pipe($.jshint.reporter(require('jshint-stylish')))
.pipe(gulp.dest('dist/scripts'))
.pipe($.size());
});
gulp.task('distscripts', function () {
return gulp.src('app/scripts/**/*.js')
.pipe($.jshint())
.pipe($.jshint.reporter(require('jshint-stylish')))
//.pipe($.uglify())
.pipe(gulp.dest('dist/scripts'))
.pipe($.size());
});
gulp.task('images', function () {
return gulp.src('app/images/**/*')
.pipe($.imagemin({
optimizationLevel: 3,
progressive: true,
interlaced: true
}))
.pipe(gulp.dest('dist/images'))
.pipe($.size());
});
gulp.task('fonts', function () {
return gulp.src(bowerFiles().concat(['app/fonts/**/*.*']))
.pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}'))
.pipe($.flatten())
.pipe(gulp.dest('dist/fonts'))
.pipe($.size());
});
gulp.task('extras', function () {
return gulp.src(['app/*.*', '!app/*.php'], { dot: true })
.pipe(gulp.dest('dist'));
});
gulp.task('templates', function () {
return gulp.src('app/templates/*.html')
.pipe($.nunjucks())
.pipe($.concat('templates.js'))
.pipe(gulp.dest('dist/scripts'))
.pipe($.size());
});
gulp.task('clean', function (cb) {
del(['dist'], cb);
});
gulp.task('build', ['vendorscripts', 'vendorstyles', 'distscripts', 'diststyles', 'images', 'fonts', 'extras', 'templates']);
gulp.task('default', ['clean'], function () {
gulp.start('build');
});
gulp.task('serve', ['styles'], function () {
});
gulp.task('watch', ['serve'], function () {
browserSync.init({
proxy: 'localhost:8001'
});
// watch for changes
gulp.watch([
'../apps/kassa/templates/**/*.html',
'dist/scripts/**/*.js',
'dist/images/**/*'
]).on('change', reload);
gulp.watch('app/styles/**/*.scss', ['styles']);
gulp.watch('app/scripts/**/*.js', ['scripts']);
gulp.watch('app/templates/**/*.html', ['templates']);
gulp.watch('app/images/**/*', ['images']);
gulp.watch('bower.json', ['vendorscripts']);
});
+6733 -9908
View File
File diff suppressed because it is too large. Load diff
+19 -33
View File
@@ -2,51 +2,37 @@
"name": "kassa",
"private": true,
"scripts": {
"build": "webpack -p --env production --display-error-details --mode production",
"build": "rspack build --mode production",
"build:templates": "echo '/* eslint-disable */' > app/templates.js && nunjucks-precompile app/templates >> app/templates.js",
"start": "webpack-dev-server --hot --env development --mode development",
"start": "rspack serve --mode development",
"format": "eslint --fix \"app/**/*.js\"",
"lint": "eslint --quiet \"app/**/*.js\""
},
"dependencies": {
"bootstrap-sass": "^3.4.1",
"jquery": "^3.4.1",
"bootstrap-sass": "^3.4.3",
"jquery": "^3.7.1",
"list.js": "^1.5.0",
"lodash.debounce": "^4.0.8",
"lodash.frompairs": "^4.0.1",
"lodash.sortby": "^4.7.0",
"moment": "^2.24.0",
"nunjucks": "^3.2.0"
"moment": "^2.30.1",
"nunjucks": "^3.2.4"
},
"devDependencies": {
"@babel/core": "^7.7.7",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.7",
"@babel/preset-react": "^7.7.4",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.4.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.7.0",
"eslint-import-resolver-babel-module": "^5.1.0",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-prettier": "^3.1.2",
"file-loader": "^5.0.2",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.13.0",
"prettier": "^1.19.1",
"sass-loader": "^8.0.0",
"style-loader": "^1.1.2",
"url-loader": "^3.0.0",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.1"
"@rspack/cli": "^1.3.13",
"@rspack/core": "^1.3.13",
"css-loader": "^7.1.2",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0",
"file-loader": "^6.2.0",
"prettier": "^2.6.2",
"sass-embedded": "^1.89.1",
"sass-loader": "^16.0.5",
"style-loader": "^4.0.0"
},
"engines": {
"node": "12"
"node": "20"
}
}
+85
View File
@@ -0,0 +1,85 @@
const path = require('path');
const { defineConfig } = require('@rspack/cli');
const { rspack } = require('@rspack/core');
module.exports = defineConfig({
entry: './app/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
resolve: {
extensions: ['.jsx', '.js', '.json', '.scss', '.css'],
modules: ['node_modules'],
},
plugins: [
new rspack.CopyRspackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'app', 'images'),
to: path.resolve(__dirname, 'dist', 'images'),
},
],
}),
new rspack.CssExtractRspackPlugin(),
new rspack.ProvidePlugin({
jQuery: 'jquery',
}),
],
module: {
rules: [
{
test: /\.(png|jpg|gif|svg)$/,
use: [
{
loader: require.resolve('file-loader'),
options: {},
},
],
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
type: 'asset',
},
{
test: /\.css$/,
use: [rspack.CssExtractRspackPlugin.loader, require.resolve('css-loader')],
},
{
test: /\.scss$/,
use: [
rspack.CssExtractRspackPlugin.loader,
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
modules: false,
sourceMap: true,
},
},
{
loader: require.resolve('sass-loader'),
options: {
api: 'modern-compiler',
implementation: require.resolve('sass-embedded'),
sassOptions: {
silenceDeprecations: ['global-builtin', 'legacy-js-api', 'mixed-decls', 'color-functions', 'import'],
},
},
},
],
},
],
},
// FIXME(nikolark): This workflow does not work
devServer: {
port: 8001,
proxy: [
{
context: '/api',
target: 'http://localhost:8000',
},
],
},
});
-96
View File
@@ -1,96 +0,0 @@
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const baseStyleLoader = (env) => (env === 'production' ? MiniCssExtractPlugin.loader : require.resolve('style-loader'));
module.exports = (env, argv) => {
return {
entry: './app/index.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
resolve: {
extensions: ['.jsx', '.js', '.json', '.scss', '.css'],
modules: ['node_modules']
},
plugins: [
new MiniCssExtractPlugin(),
env === 'production' ? new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) : null,
env !== 'production' ? new HtmlWebpackPlugin({ template: './app/index.html' }) : null,
new webpack.ProvidePlugin({
jQuery: 'jquery'
})
].filter(Boolean),
module: {
rules: [
{
test: /\.(png|jpg|gif|svg)$/,
use: [
{
loader: require.resolve('file-loader'),
options: {}
}
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: require.resolve('file-loader')
},
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: require.resolve('babel-loader')
},
{
test: /\.css$/,
loaders: [baseStyleLoader(env), require.resolve('css-loader')]
},
{
test: /\.scss$/,
loaders: [
baseStyleLoader(env),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
modules: false,
sourceMap: true
}
},
{
loader: require.resolve('sass-loader'),
options: {
sassOptions: {
precision: 8
}
}
}
]
}
]
},
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
historyApiFallback: true,
overlay: {
errors: true,
warnings: false
},
port: 8001,
publicPath: '/',
proxy: {
'/api': 'http://localhost:8000'
// TODO: get urls
},
stats: {
assets: false,
modules: false,
chunks: false
}
},
devtool: env === 'production' ? 'source-map' : 'cheap-module-eval-source-map'
};
};
Generated
+580
View File
@@ -0,0 +1,580 @@
version = 1
revision = 2
requires-python = ">=3.12"
[[package]]
name = "asgiref"
version = "3.8.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186, upload-time = "2024-03-22T14:39:36.863Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" },
]
[[package]]
name = "bcrypt"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" },
{ url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" },
{ url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" },
{ url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" },
{ url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" },
{ url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" },
{ url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" },
{ url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" },
{ url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" },
{ url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" },
{ url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" },
{ url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" },
{ url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" },
{ url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" },
{ url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" },
{ url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" },
{ url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" },
{ url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" },
{ url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" },
{ url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" },
{ url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" },
{ url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" },
{ url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" },
{ url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" },
{ url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" },
{ url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" },
{ url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" },
{ url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" },
{ url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" },
{ url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" },
{ url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" },
{ url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" },
{ url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" },
{ url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" },
{ url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" },
{ url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" },
{ url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" },
{ url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" },
{ url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" },
{ url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" },
{ url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" },
{ url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" },
]
[[package]]
name = "certifi"
version = "2025.4.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
]
[[package]]
name = "cffi"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]]
name = "cryptography"
version = "45.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/13/1f/9fa001e74a1993a9cadd2333bb889e50c66327b8594ac538ab8a04f915b7/cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899", size = 744738, upload-time = "2025-05-25T14:17:24.777Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/b2/2345dc595998caa6f68adf84e8f8b50d18e9fc4638d32b22ea8daedd4b7a/cryptography-45.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71", size = 7056239, upload-time = "2025-05-25T14:16:12.22Z" },
{ url = "https://files.pythonhosted.org/packages/71/3d/ac361649a0bfffc105e2298b720d8b862330a767dab27c06adc2ddbef96a/cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b", size = 4205541, upload-time = "2025-05-25T14:16:14.333Z" },
{ url = "https://files.pythonhosted.org/packages/70/3e/c02a043750494d5c445f769e9c9f67e550d65060e0bfce52d91c1362693d/cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f", size = 4433275, upload-time = "2025-05-25T14:16:16.421Z" },
{ url = "https://files.pythonhosted.org/packages/40/7a/9af0bfd48784e80eef3eb6fd6fde96fe706b4fc156751ce1b2b965dada70/cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942", size = 4209173, upload-time = "2025-05-25T14:16:18.163Z" },
{ url = "https://files.pythonhosted.org/packages/31/5f/d6f8753c8708912df52e67969e80ef70b8e8897306cd9eb8b98201f8c184/cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9", size = 3898150, upload-time = "2025-05-25T14:16:20.34Z" },
{ url = "https://files.pythonhosted.org/packages/8b/50/f256ab79c671fb066e47336706dc398c3b1e125f952e07d54ce82cf4011a/cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56", size = 4466473, upload-time = "2025-05-25T14:16:22.605Z" },
{ url = "https://files.pythonhosted.org/packages/62/e7/312428336bb2df0848d0768ab5a062e11a32d18139447a76dfc19ada8eed/cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca", size = 4211890, upload-time = "2025-05-25T14:16:24.738Z" },
{ url = "https://files.pythonhosted.org/packages/e7/53/8a130e22c1e432b3c14896ec5eb7ac01fb53c6737e1d705df7e0efb647c6/cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1", size = 4466300, upload-time = "2025-05-25T14:16:26.768Z" },
{ url = "https://files.pythonhosted.org/packages/ba/75/6bb6579688ef805fd16a053005fce93944cdade465fc92ef32bbc5c40681/cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578", size = 4332483, upload-time = "2025-05-25T14:16:28.316Z" },
{ url = "https://files.pythonhosted.org/packages/2f/11/2538f4e1ce05c6c4f81f43c1ef2bd6de7ae5e24ee284460ff6c77e42ca77/cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497", size = 4573714, upload-time = "2025-05-25T14:16:30.474Z" },
{ url = "https://files.pythonhosted.org/packages/f5/bb/e86e9cf07f73a98d84a4084e8fd420b0e82330a901d9cac8149f994c3417/cryptography-45.0.3-cp311-abi3-win32.whl", hash = "sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710", size = 2934752, upload-time = "2025-05-25T14:16:32.204Z" },
{ url = "https://files.pythonhosted.org/packages/c7/75/063bc9ddc3d1c73e959054f1fc091b79572e716ef74d6caaa56e945b4af9/cryptography-45.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490", size = 3412465, upload-time = "2025-05-25T14:16:33.888Z" },
{ url = "https://files.pythonhosted.org/packages/71/9b/04ead6015229a9396890d7654ee35ef630860fb42dc9ff9ec27f72157952/cryptography-45.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06", size = 7031892, upload-time = "2025-05-25T14:16:36.214Z" },
{ url = "https://files.pythonhosted.org/packages/46/c7/c7d05d0e133a09fc677b8a87953815c522697bdf025e5cac13ba419e7240/cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57", size = 4196181, upload-time = "2025-05-25T14:16:37.934Z" },
{ url = "https://files.pythonhosted.org/packages/08/7a/6ad3aa796b18a683657cef930a986fac0045417e2dc428fd336cfc45ba52/cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716", size = 4423370, upload-time = "2025-05-25T14:16:39.502Z" },
{ url = "https://files.pythonhosted.org/packages/4f/58/ec1461bfcb393525f597ac6a10a63938d18775b7803324072974b41a926b/cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8", size = 4197839, upload-time = "2025-05-25T14:16:41.322Z" },
{ url = "https://files.pythonhosted.org/packages/d4/3d/5185b117c32ad4f40846f579369a80e710d6146c2baa8ce09d01612750db/cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc", size = 3886324, upload-time = "2025-05-25T14:16:43.041Z" },
{ url = "https://files.pythonhosted.org/packages/67/85/caba91a57d291a2ad46e74016d1f83ac294f08128b26e2a81e9b4f2d2555/cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342", size = 4450447, upload-time = "2025-05-25T14:16:44.759Z" },
{ url = "https://files.pythonhosted.org/packages/ae/d1/164e3c9d559133a38279215c712b8ba38e77735d3412f37711b9f8f6f7e0/cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b", size = 4200576, upload-time = "2025-05-25T14:16:46.438Z" },
{ url = "https://files.pythonhosted.org/packages/71/7a/e002d5ce624ed46dfc32abe1deff32190f3ac47ede911789ee936f5a4255/cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782", size = 4450308, upload-time = "2025-05-25T14:16:48.228Z" },
{ url = "https://files.pythonhosted.org/packages/87/ad/3fbff9c28cf09b0a71e98af57d74f3662dea4a174b12acc493de00ea3f28/cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65", size = 4325125, upload-time = "2025-05-25T14:16:49.844Z" },
{ url = "https://files.pythonhosted.org/packages/f5/b4/51417d0cc01802304c1984d76e9592f15e4801abd44ef7ba657060520bf0/cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b", size = 4560038, upload-time = "2025-05-25T14:16:51.398Z" },
{ url = "https://files.pythonhosted.org/packages/80/38/d572f6482d45789a7202fb87d052deb7a7b136bf17473ebff33536727a2c/cryptography-45.0.3-cp37-abi3-win32.whl", hash = "sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab", size = 2924070, upload-time = "2025-05-25T14:16:53.472Z" },
{ url = "https://files.pythonhosted.org/packages/91/5a/61f39c0ff4443651cc64e626fa97ad3099249152039952be8f344d6b0c86/cryptography-45.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2", size = 3395005, upload-time = "2025-05-25T14:16:55.134Z" },
]
[[package]]
name = "decorator"
version = "5.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
]
[[package]]
name = "deprecated"
version = "1.2.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" },
]
[[package]]
name = "django"
version = "4.2.21"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c1/bb/2fad5edc1af2945cb499a2e322ac28e4714fc310bd5201ed1f5a9f73a342/django-4.2.21.tar.gz", hash = "sha256:b54ac28d6aa964fc7c2f7335138a54d78980232011e0cd2231d04eed393dcb0d", size = 10424638, upload-time = "2025-05-07T14:07:07.992Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/4f/aeaa3098da18b625ed672f3da6d1cd94e188d1b2cc27c2c841b2f9666282/django-4.2.21-py3-none-any.whl", hash = "sha256:1d658c7bf5d31c7d0cac1cab58bc1f822df89255080fec81909256c30e6180b3", size = 7993839, upload-time = "2025-05-07T14:07:01.318Z" },
]
[[package]]
name = "django-auth-ldap"
version = "5.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "python-ldap" },
]
sdist = { url = "https://files.pythonhosted.org/packages/88/70/6f6a89474667376080f8362f7c17c744d1c52720f0eb085cf74182149efe/django_auth_ldap-5.2.0.tar.gz", hash = "sha256:08ba6efc0340d9874725a962311b14991e29a33593eb150a8fb640709dbfa80f", size = 55287, upload-time = "2025-05-07T12:15:56.774Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/65/0d26a8b5c19039305d7ae0e8e702613a9a1fe1ef3ebbd6206b9e104b7c43/django_auth_ldap-5.2.0-py3-none-any.whl", hash = "sha256:7dc6eb576ba36051850b580e4bdf4464e04bbe7367c3827a3121b4d7c51fb175", size = 20913, upload-time = "2025-05-07T12:15:54.962Z" },
]
[[package]]
name = "django-bootstrap-form"
version = "3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c7/3d/4ec51e3816d3bd5959058e210598e819fb86d492d16a19d195a025a64758/django-bootstrap-form-3.4.tar.gz", hash = "sha256:de3f7893e515352834d446c441c0cb861637f92cebbe59d8229469c6cd3dc640", size = 4422, upload-time = "2018-03-15T07:04:42.039Z" }
[[package]]
name = "django-cors-headers"
version = "4.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/93/6c/16f6cb6064c63074fd5b2bd494eb319afd846236d9c1a6c765946df2c289/django_cors_headers-4.7.0.tar.gz", hash = "sha256:6fdf31bf9c6d6448ba09ef57157db2268d515d94fc5c89a0a1028e1fc03ee52b", size = 21037, upload-time = "2025-02-06T22:15:28.924Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/a2/7bcfff86314bd9dd698180e31ba00604001606efb518a06cca6833a54285/django_cors_headers-4.7.0-py3-none-any.whl", hash = "sha256:f1c125dcd58479fe7a67fe2499c16ee38b81b397463cf025f0e2c42937421070", size = 12794, upload-time = "2025-02-06T22:15:24.341Z" },
]
[[package]]
name = "django-extensions"
version = "4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6d/b3/ed0f54ed706ec0b54fd251cc0364a249c6cd6c6ec97f04dc34be5e929eac/django_extensions-4.1.tar.gz", hash = "sha256:7b70a4d28e9b840f44694e3f7feb54f55d495f8b3fa6c5c0e5e12bcb2aa3cdeb", size = 283078, upload-time = "2025-04-11T01:15:39.617Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/96/d967ca440d6a8e3861120f51985d8e5aec79b9a8bdda16041206adfe7adc/django_extensions-4.1-py3-none-any.whl", hash = "sha256:0699a7af28f2523bf8db309a80278519362cd4b6e1fd0a8cd4bf063e1e023336", size = 232980, upload-time = "2025-04-11T01:15:37.701Z" },
]
[[package]]
name = "django-phonenumber-field"
version = "8.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/4b/edec2f3e4b8f8048540d7d1844bc576a0bdb711e1f6a4cde364c505c865d/django_phonenumber_field-8.1.0.tar.gz", hash = "sha256:f19450625574ad3a2de047c409527d0e57699496849e71e53b53e8679ff67b27", size = 43854, upload-time = "2025-04-10T16:09:29.363Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/98/0413e47d7d1fab0e5a81eda6caacdcba69d50f74f64b65febf943cbd1f1e/django_phonenumber_field-8.1.0-py3-none-any.whl", hash = "sha256:9ba66873bf47401b13fc57797fbf85e785864a6bfdc8b9daa3a9012886567045", size = 65208, upload-time = "2025-04-10T16:09:27.843Z" },
]
[[package]]
name = "fabric"
version = "3.2.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "decorator" },
{ name = "deprecated" },
{ name = "invoke" },
{ name = "paramiko" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0d/3f/337f278b70ba339c618a490f6b8033b7006c583bd197a897f12fbc468c51/fabric-3.2.2.tar.gz", hash = "sha256:8783ca42e3b0076f08b26901aac6b9d9b1f19c410074e7accfab902c184ff4a3", size = 183215, upload-time = "2023-08-31T01:42:05.55Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/1f/e99e23ee01847147fa194e8d41cfcf2535a2dbfcb51414c541cadb15c5d7/fabric-3.2.2-py3-none-any.whl", hash = "sha256:91c47c0be68b14936c88b34da8a1f55e5710fd28397dac5d4ff2e21558113a6f", size = 59417, upload-time = "2023-08-31T01:42:03.917Z" },
]
[[package]]
name = "gunicorn"
version = "23.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "invoke"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/42/127e6d792884ab860defc3f4d80a8f9812e48ace584ffc5a346de58cdc6c/invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5", size = 299835, upload-time = "2023-07-12T18:05:17.998Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/66/7f8c48009c72d73bc6bbe6eb87ac838d6a526146f7dab14af671121eb379/invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820", size = 160274, upload-time = "2023-07-12T18:05:16.294Z" },
]
[[package]]
name = "kassa"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "django" },
{ name = "django-auth-ldap" },
{ name = "django-bootstrap-form" },
{ name = "django-cors-headers" },
{ name = "django-extensions" },
{ name = "django-phonenumber-field" },
{ name = "fabric" },
{ name = "gunicorn" },
{ name = "mysqlclient" },
{ name = "phonenumbers" },
{ name = "requests" },
{ name = "sentry-sdk" },
]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "django", specifier = "==4.2.*" },
{ name = "django-auth-ldap", specifier = "==5.*" },
{ name = "django-bootstrap-form", specifier = "==3.4" },
{ name = "django-cors-headers", specifier = "==4.*" },
{ name = "django-extensions", specifier = "==4.*" },
{ name = "django-phonenumber-field", specifier = "==8.*" },
{ name = "fabric", specifier = "==3.*" },
{ name = "gunicorn", specifier = "==23.*" },
{ name = "mysqlclient", specifier = "==2.*" },
{ name = "phonenumbers", specifier = "==9.*" },
{ name = "requests", specifier = "==2.*" },
{ name = "sentry-sdk", specifier = "==1.*" },
]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.11.12" }]
[[package]]
name = "mysqlclient"
version = "2.2.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/68/810093cb579daae426794bbd9d88aa830fae296e85172d18cb0f0e5dd4bc/mysqlclient-2.2.7.tar.gz", hash = "sha256:24ae22b59416d5fcce7e99c9d37548350b4565baac82f95e149cac6ce4163845", size = 91383, upload-time = "2025-01-10T12:06:00.763Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/b5/2a8a4bcba3440550f358b839638fe8ec9146fa3c9194890b4998a530c926/mysqlclient-2.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:4b4c0200890837fc64014cc938ef2273252ab544c1b12a6c1d674c23943f3f2e", size = 208032, upload-time = "2025-01-10T11:56:29.879Z" },
{ url = "https://files.pythonhosted.org/packages/29/01/e80141f1cd0459e4c9a5dd309dee135bbae41d6c6c121252fdd853001a8a/mysqlclient-2.2.7-cp313-cp313-win_amd64.whl", hash = "sha256:201a6faa301011dd07bca6b651fe5aaa546d7c9a5426835a06c3172e1056a3c5", size = 208000, upload-time = "2025-01-10T11:56:32.293Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "paramiko"
version = "3.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bcrypt" },
{ name = "cryptography" },
{ name = "pynacl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/15/ad6ce226e8138315f2451c2aeea985bf35ee910afb477bae7477dc3a8f3b/paramiko-3.5.1.tar.gz", hash = "sha256:b2c665bc45b2b215bd7d7f039901b14b067da00f3a11e6640995fd58f2664822", size = 1566110, upload-time = "2025-02-04T02:37:59.783Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/f8/c7bd0ef12954a81a1d3cea60a13946bd9a49a0036a5927770c461eade7ae/paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61", size = 227298, upload-time = "2025-02-04T02:37:57.672Z" },
]
[[package]]
name = "phonenumbers"
version = "9.0.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/40/57/2923088eb7fe397a544dcf31f31705ddd3d2d6f965d68c8d7ce2afe70856/phonenumbers-9.0.6.tar.gz", hash = "sha256:bfb228cfc4a5644f7c1a32ded1bd2ee6f8e3749e422fbdabd57661e86ad8872d", size = 2297098, upload-time = "2025-05-28T05:45:48.251Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/65/b760522ce1b080e1d8af50d63e537beb5642ff043d789e4d7f5e3b4c3b30/phonenumbers-9.0.6-py2.py3-none-any.whl", hash = "sha256:b4a0f5a0dc30235b273d06acfe9afc2160cce4f1c8220067985ed81d72bb29a9", size = 2582823, upload-time = "2025-05-28T05:45:44.078Z" },
]
[[package]]
name = "pyasn1"
version = "0.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
]
[[package]]
name = "pyasn1-modules"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
]
[[package]]
name = "pycparser"
version = "2.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
]
[[package]]
name = "pynacl"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854, upload-time = "2022-01-07T22:05:41.134Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920, upload-time = "2022-01-07T22:05:49.156Z" },
{ url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722, upload-time = "2022-01-07T22:05:50.989Z" },
{ url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087, upload-time = "2022-01-07T22:05:52.539Z" },
{ url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678, upload-time = "2022-01-07T22:05:54.251Z" },
{ url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660, upload-time = "2022-01-07T22:05:56.056Z" },
{ url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824, upload-time = "2022-01-07T22:05:57.434Z" },
{ url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912, upload-time = "2022-01-07T22:05:58.665Z" },
{ url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624, upload-time = "2022-01-07T22:06:00.085Z" },
{ url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141, upload-time = "2022-01-07T22:06:01.861Z" },
]
[[package]]
name = "python-ldap"
version = "3.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
{ name = "pyasn1-modules" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/8b/1eeb4025dc1d3ac2f16678f38dec9ebdde6271c74955b72db5ce7a4dbfbd/python-ldap-3.4.4.tar.gz", hash = "sha256:7edb0accec4e037797705f3a05cbf36a9fde50d08c8f67f2aef99a2628fab828", size = 377889, upload-time = "2023-11-17T21:14:16.32Z" }
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
]
[[package]]
name = "ruff"
version = "0.11.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289, upload-time = "2025-05-29T13:31:40.037Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597, upload-time = "2025-05-29T13:30:57.539Z" },
{ url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154, upload-time = "2025-05-29T13:31:00.865Z" },
{ url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048, upload-time = "2025-05-29T13:31:03.413Z" },
{ url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062, upload-time = "2025-05-29T13:31:05.539Z" },
{ url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152, upload-time = "2025-05-29T13:31:07.986Z" },
{ url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067, upload-time = "2025-05-29T13:31:10.57Z" },
{ url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807, upload-time = "2025-05-29T13:31:12.88Z" },
{ url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261, upload-time = "2025-05-29T13:31:15.236Z" },
{ url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601, upload-time = "2025-05-29T13:31:18.68Z" },
{ url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186, upload-time = "2025-05-29T13:31:21.216Z" },
{ url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032, upload-time = "2025-05-29T13:31:23.417Z" },
{ url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370, upload-time = "2025-05-29T13:31:25.777Z" },
{ url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529, upload-time = "2025-05-29T13:31:28.396Z" },
{ url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642, upload-time = "2025-05-29T13:31:30.647Z" },
{ url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511, upload-time = "2025-05-29T13:31:32.917Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573, upload-time = "2025-05-29T13:31:35.782Z" },
{ url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" },
]
[[package]]
name = "sentry-sdk"
version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c8/28/02c0cd9184f9108e3c52519f9628b215077a3854240e0b17ae845e664855/sentry_sdk-1.45.1.tar.gz", hash = "sha256:a16c997c0f4e3df63c0fc5e4207ccb1ab37900433e0f72fef88315d317829a26", size = 244774, upload-time = "2024-07-26T13:48:32.375Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/9f/105366a122efa93f0cb1914f841747d160788e4d022d0488d2d44c2ba26c/sentry_sdk-1.45.1-py2.py3-none-any.whl", hash = "sha256:608887855ccfe39032bfd03936e3a1c4f4fc99b3a4ac49ced54a4220de61c9c1", size = 267163, upload-time = "2024-07-26T13:48:29.38Z" },
]
[[package]]
name = "sqlparse"
version = "0.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]
[[package]]
name = "urllib3"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
]
[[package]]
name = "wrapt"
version = "1.17.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" },
{ url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" },
{ url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" },
{ url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" },
{ url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" },
{ url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" },
{ url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" },
{ url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" },
{ url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" },
{ url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" },
{ url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" },
{ url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" },
{ url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" },
{ url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" },
{ url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload-time = "2025-01-14T10:34:29.167Z" },
{ url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload-time = "2025-01-14T10:34:31.702Z" },
{ url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" },
{ url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" },
{ url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" },
{ url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" },
{ url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" },
{ url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" },
{ url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" },
{ url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" },
{ url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" },
{ url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload-time = "2025-01-14T10:34:52.297Z" },
{ url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload-time = "2025-01-14T10:34:53.489Z" },
{ url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" },
{ url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" },
{ url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" },
{ url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" },
{ url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" },
{ url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" },
]