21 Commits

Author SHA1 Message Date
71d4580a82 feat(settings/base.py): add GITEA_URL config 2026-03-19 15:26:58 +01:00
0356374870 feat(settings/base.py): add OAUTH2_PROVIDER config 2026-03-19 15:26:42 +01:00
ffc33d3be4 feat(oauth_validators.py): add CustomOAuth2Validator to supply OIDC scopes 2026-03-19 15:23:07 +01:00
730e041794 feat(kursy/urls.py): add oauth2/ url 2026-03-19 15:22:11 +01:00
e0f3f094ff feat(home/apps.py): register signals 2026-03-19 15:21:26 +01:00
157ee875e1 feat(signals.py): add signals 2026-03-19 15:20:36 +01:00
12de01c2dc chore(settings/base.py): add oauth to INSTALLED_APPS and MIDDLEWARE 2026-03-18 11:47:58 +01:00
718aeb9cf5 build(uv.lock): add depedency on django-oauth-toolkit 2026-03-18 11:47:13 +01:00
9761daf820 build(pyproject.toml): add depedency on django-oauth-toolkit 2026-03-18 11:47:04 +01:00
1d0fd27e04 chore(migrations/0017): add ChatMessage 2026-03-18 10:24:41 +01:00
ac31336acc feat(kusry/urls.py): include home urls 2026-03-18 10:24:14 +01:00
82514a9418 feat(home/urls.py): add home urls 2026-03-18 10:23:27 +01:00
b599ac6fa3 feat(home/views.py): add chat views 2026-03-18 10:22:58 +01:00
fd6209470b feat(wagtail_hooks.py): add admin chat dashboard to wagtail admin menu 2026-03-18 10:04:49 +01:00
de48747884 feat(templates/chat): add chat-related templates 2026-03-18 10:04:07 +01:00
684871833a refactor(models/__init__.py): correct ChatMessage import 2026-03-18 09:48:07 +01:00
ef69b99068 refactor(chat_message.py): rename Message -> ChatMessage 2026-03-18 09:46:10 +01:00
88c797e4b0 chore(models): import Message model 2026-03-17 13:48:20 +01:00
a0819a6552 feat(models/message.py): add Message model 2026-03-17 13:47:55 +01:00
e4add89ba8 chore(tailwind): remove daisyui 2026-03-17 13:33:20 +01:00
7cbec4fc9c refactor(models): split models.py into submodules 2026-03-17 13:27:23 +01:00
21 changed files with 379 additions and 50 deletions

View File

@@ -4,3 +4,6 @@ from django.apps import AppConfig
class HomeConfig(AppConfig): class HomeConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "home" name = "home"
def ready(self):
import home.signals

View File

@@ -0,0 +1,26 @@
# Generated by Django 6.0.3 on 2026-03-17 14:28
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0016_modulelessonpage'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ChatMessage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(limit_choices_to={'is_staff': False}, on_delete=django.db.models.deletion.CASCADE, related_name='support_chats', to=settings.AUTH_USER_MODEL)),
],
),
]

24
home/models/__init__.py Normal file
View File

@@ -0,0 +1,24 @@
from .pages import (
EmptyPage,
HomePage,
CoursePage,
CourseModulePage,
ModuleLessonPage,
EventPage,
)
from .event_occurrence import EventOccurrence
from .chat_message import ChatMessage
__all__ = [
"HomePage",
"EmptyPage",
"CoursePage",
"CourseModulePage",
"ModuleLessonPage",
"EventPage",
"EventOccurrence",
"ChatMessage",
]

View File

@@ -0,0 +1,26 @@
from django.contrib.auth.models import User
from django.db import models
class ChatMessage(models.Model):
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="support_chats",
limit_choices_to={"is_staff": False},
) # The requester (non-admin)
sender = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="sent_messages"
) # The sender (user or admin)
content = models.TextField()
timestamp = models.DateTimeField(auto_now_add=True)
@classmethod
def get_support_chat(cls, user):
return cls.objects.filter(user=user).order_by("timestamp")
@classmethod
def get_all_user_senders(cls):
user_ids = cls.objects.values_list("user", flat=True).distinct()
return User.objects.filter(id__in=user_ids, is_staff=False)

View File

@@ -0,0 +1,38 @@
from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone
from modelcluster.fields import ParentalKey
from .pages import EventPage
class EventOccurrence(models.Model):
event = ParentalKey(EventPage, related_name="occurrences", on_delete=models.CASCADE)
start = models.DateTimeField()
end = models.DateTimeField()
signed_up_users = models.ManyToManyField(
User,
related_name="event_occurrences_signed_up",
blank=True,
help_text="Users who have signed up for this occurrence.",
)
class Meta:
ordering = ["start"]
def __str__(self):
return f"{self.event.title} ({self.start} - {self.end})"
@property
def attendees_count(self):
return self.signed_up_users.count()
@property
def is_past(self):
return self.end < timezone.now()
def user_signed_up(self, user):
if not user.is_authenticated:
return False
return self.signed_up_users.filter(id=user.id).exists()

View File

@@ -1,6 +1,5 @@
from datetime import date, datetime, timedelta from datetime import datetime, timedelta
from django import forms
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.db import models from django.db import models
from django.forms import CheckboxSelectMultiple from django.forms import CheckboxSelectMultiple
@@ -9,7 +8,7 @@ from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey from modelcluster.fields import ParentalKey
from taggit.models import TaggedItemBase from taggit.models import TaggedItemBase
from wagtail.admin.panels import FieldPanel from wagtail.admin.panels import FieldPanel
from wagtail.fields import RichTextField, StreamField from wagtail.fields import RichTextField
from wagtail.models import Page from wagtail.models import Page
from wagtail.models.copying import ParentalManyToManyField from wagtail.models.copying import ParentalManyToManyField
from wagtail_color_panel.edit_handlers import NativeColorPanel from wagtail_color_panel.edit_handlers import NativeColorPanel
@@ -260,36 +259,3 @@ class EventPage(Page):
FieldPanel("recurrence_repeat_until"), FieldPanel("recurrence_repeat_until"),
FieldPanel("recurrence_endless"), FieldPanel("recurrence_endless"),
] ]
class EventOccurrence(models.Model):
event = ParentalKey(EventPage, related_name="occurrences", on_delete=models.CASCADE)
start = models.DateTimeField()
end = models.DateTimeField()
signed_up_users = models.ManyToManyField(
User,
related_name="event_occurrences_signed_up",
blank=True,
help_text="Users who have signed up for this occurrence.",
)
class Meta:
ordering = ["start"]
def __str__(self):
return f"{self.event.title} ({self.start} - {self.end})"
@property
def attendees_count(self):
return self.signed_up_users.count()
@property
def is_past(self):
from django.utils import timezone
return self.end < timezone.now()
def user_signed_up(self, user):
if not user.is_authenticated:
return False
return self.signed_up_users.filter(id=user.id).exists()

39
home/signals.py Normal file
View File

@@ -0,0 +1,39 @@
import os
import requests
from django.conf import settings
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=User)
def notify_external_service_on_signup(sender, instance, created, **kwargs):
pass
# if created and not instance.is_staff:
# payload = {
# "user_id": instance.id,
# "username": f"KURSY-{instance.id}",
# "email": instance.email,
# "full_name": f"{instance.first_name} {instance.last_name}".strip(),
# # "must_change_password": True,
# # "password": instance.password,
# "visibility": "private",
# }
# api_url = getattr(settings, "GITEA_URL", None)
# if api_url:
# url = f"{api_url}/admin/users"
# try:
# response = requests.post(
# url,
# json=payload,
# timeout=5,
# headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
# )
# response.raise_for_status()
# print(f"Successfully created Gitea account for {instance.email}")
# except Exception as e:
# print(
# f"Failed to create Gitea account for user {instance.email}: {e}\n{response.text}"
# )
# raise e

View File

@@ -0,0 +1,29 @@
{% extends "wagtailadmin/base.html" %}
{% load static i18n %}
{% block titletag %}
{% trans "Chat with" %} {{ chat_user.email }}
{% endblock titletag %}
{% block content %}
{% include "wagtailadmin/shared/header.html" with title="Chat" icon="mail" %}
<h1>{% trans "Admin Chat View" %}</h1>
<p>{% trans "This is the admin view of the chat. Here you can manage conversations and monitor user interactions." %}</p>
<ul>
{% for message in chat_messages %}
<li>
<strong>{{ message.sender.email }}:</strong> {{ message.content }}
</li>
{% empty %}
<li>{% trans "No messages found." %}</li>
{% endfor %}
</ul>
<form action="/chat/send/{{ chat_user.id }}/" method="post">
{% csrf_token %}
<input type="text" name="content" placeholder="{% trans "Type your message here..." %}" required>
<button type="submit">{% trans "Send" %}</button>
</form>
{% endblock content %}

View File

@@ -0,0 +1,23 @@
{% extends "wagtailadmin/base.html" %}
{% load static i18n %}
{% block titletag %}
{% trans "Chat" %}
{% endblock titletag %}
{% block content %}
{% include "wagtailadmin/shared/header.html" with title="Chat" icon="mail" %}
<h1>{% trans "Admin Chat Dashboard" %}</h1>
<ul>
{% for user in chats %}
<li>
<a href="{% url 'admin_chat' user.id %}">
{{ user.email }}
</a>
</li>
{% empty %}
<li>{% trans "No active chats found." %}</li>
{% endfor %}
</ul>
{% endblock content %}

View File

@@ -0,0 +1,26 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}
{% trans "Chat" %}
{% endblock title %}
{% block content %}
<h1>{% trans "Chat with Support" %}</h1>
<p>{% trans "This is the user chat interface. Here you can communicate with our support team for assistance." %}</p>
<ul>
{% for message in chat_messages %}
<li>
<strong>{{ message.sender.email }}:</strong> {{ message.content }}
</li>
{% empty %}
<li>{% trans "No messages found." %}</li>
{% endfor %}
</ul>
<form action="/chat/send/{{ user.id }}/" method="post">
{% csrf_token %}
<textarea name="content" placeholder="{% trans "Type your message here..." %}"></textarea>
<button type="submit">{% trans "Send" %}</button>
</form>
{% endblock content %}

7
home/urls.py Normal file
View File

@@ -0,0 +1,7 @@
from django.urls import path
from . import views
urlpatterns = [
path("chat/", views.user_chat, name="user_chat"),
path("chat/send/<int:user_id>/", views.user_chat_send, name="user_chat_send"),
]

44
home/views.py Normal file
View File

@@ -0,0 +1,44 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.shortcuts import redirect, render
from home.models import ChatMessage
@login_required
def admin_chat_dashboard(request):
chats = ChatMessage.get_all_user_senders()
return render(request, "chat/admin/admin_chat_dashboard.html", {"chats": chats})
@login_required
def admin_chat(request, user_id):
chat_user = User.objects.filter(id=user_id, is_staff=False).first()
print(chat_user)
chat_messages = ChatMessage.get_support_chat(chat_user)
return render(
request,
"chat/admin/admin_chat.html",
{"chat_user": chat_user, "chat_messages": chat_messages},
)
@login_required
def user_chat(request):
if request.user.is_staff:
return redirect("admin_chat_dashboard")
chat_messages = ChatMessage.get_support_chat(request.user)
return render(request, "chat/user_chat.html", {"chat_messages": chat_messages})
@login_required
def user_chat_send(request, user_id):
if request.method == "POST":
content = request.POST.get("content")
content = content.strip() if content else ""
if content:
user = User.objects.filter(id=user_id).first()
ChatMessage.objects.create(user=user, sender=request.user, content=content)
if request.user.is_staff:
return redirect("admin_chat", user_id=user_id)
return redirect("user_chat")

View File

@@ -1,7 +1,12 @@
from wagtail.admin.menu import MenuItem
import wagtail.admin.rich_text.editors.draftail.features as draftail_features import wagtail.admin.rich_text.editors.draftail.features as draftail_features
from wagtail.admin.rich_text.converters.html_to_contentstate import BlockElementHandler from wagtail.admin.rich_text.converters.html_to_contentstate import BlockElementHandler
from wagtail import hooks from wagtail import hooks
from django.urls import path, reverse
from . import views
@hooks.register("register_rich_text_features") @hooks.register("register_rich_text_features")
def register_code_block_feature(features): def register_code_block_feature(features):
@@ -39,3 +44,20 @@ def register_code_block_feature(features):
# Optional: add to default features # Optional: add to default features
features.default_features.append(feature_name) features.default_features.append(feature_name)
@hooks.register("register_admin_urls")
def register_admin_chat_dashboard_url():
return [
path("chat/", views.admin_chat_dashboard, name="admin_chat_dashboard"),
path(
"chat/user/<int:user_id>/",
views.admin_chat,
name="admin_chat",
),
]
@hooks.register("register_admin_menu_item")
def register_admin_chat_menu_item():
return MenuItem("Chat", reverse("admin_chat_dashboard"), icon_name="mail")

11
kursy/oauth_validators.py Normal file
View File

@@ -0,0 +1,11 @@
from oauth2_provider.oauth2_validators import OAuth2Validator
class CustomOAuth2Validator(OAuth2Validator):
def get_additional_claims(self, request):
print("get_additional_claims", request.user)
return {
"name": " ".join([request.user.first_name, request.user.last_name]),
"preferred_username": f"studio77-{request.user.username}",
"email": request.user.email,
}

View File

@@ -58,6 +58,7 @@ INSTALLED_APPS = [
"allauth.account", "allauth.account",
"allauth.socialaccount", "allauth.socialaccount",
"allauth.socialaccount.providers.github", "allauth.socialaccount.providers.github",
"oauth2_provider",
"tailwind", "tailwind",
"theme", "theme",
"widget_tweaks", "widget_tweaks",
@@ -75,6 +76,7 @@ MIDDLEWARE = [
"wagtail.contrib.redirects.middleware.RedirectMiddleware", "wagtail.contrib.redirects.middleware.RedirectMiddleware",
"allauth.account.middleware.AccountMiddleware", "allauth.account.middleware.AccountMiddleware",
"django.middleware.locale.LocaleMiddleware", "django.middleware.locale.LocaleMiddleware",
"oauth2_provider.middleware.OAuth2TokenMiddleware",
] ]
ROOT_URLCONF = "kursy.urls" ROOT_URLCONF = "kursy.urls"
@@ -131,6 +133,26 @@ SOCIALACCOUNT_PROVIDERS = {
WSGI_APPLICATION = "kursy.wsgi.application" WSGI_APPLICATION = "kursy.wsgi.application"
OAUTH2_PROVIDER = {
"OIDC_ENABLED": True,
"OIDC_RPID_ENDPOINT": "http://127.0.0.1:8000/oauth2",
"OIDC_ISS_ENDPOINT": "http://127.0.0.1:8000",
"PKCE_REQUIRED": False,
"OAUTH2_VALIDATOR_CLASS": "kursy.oauth_validators.CustomOAuth2Validator",
"SCOPES": {
"openid": "OpenID Connect scope",
"profile": "User profile scope",
"email": "User email scope",
"read": "Read scope",
"write": "Write scope",
},
"OIDC_CLAIM_MAPS": {
"nickname": "preferred_username",
"email": "email",
},
"DEFAULT_SCOPES": ["openid", "profile", "email"],
}
# Database # Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases # https://docs.djangoproject.com/en/6.0/ref/settings/#databases
@@ -252,3 +274,6 @@ WAGTAILDOCS_EXTENSIONS = [
] ]
TAILWIND_APP_NAME = "theme" TAILWIND_APP_NAME = "theme"
# Gitea API
GITEA_URL = "http://localhost:3000/api/v1"

View File

@@ -20,7 +20,10 @@ urlpatterns = [
path("accounts/profile/", views.profile, name="profile"), path("accounts/profile/", views.profile, name="profile"),
path("accounts/signup/", views.signup, name="signup"), path("accounts/signup/", views.signup, name="signup"),
path("i18n/", include("django.conf.urls.i18n")), path("i18n/", include("django.conf.urls.i18n")),
path("oauth2/", include("oauth2_provider.urls", namespace="oauth2_provider")),
path("", include("home.urls")),
path("calendar/", views.calendar, name="calendar"), path("calendar/", views.calendar, name="calendar"),
# TODO: move occurrence related urls to home app
path( path(
"occurrence/<int:occurrence_id>/", "occurrence/<int:occurrence_id>/",
views.occurrence_detail, views.occurrence_detail,

View File

@@ -8,6 +8,7 @@ dependencies = [
"django-allauth-ui>=1.8.1", "django-allauth-ui>=1.8.1",
"django-allauth[socialaccount]>=65.15.0", "django-allauth[socialaccount]>=65.15.0",
"django-browser-reload>=1.21.0", "django-browser-reload>=1.21.0",
"django-oauth-toolkit>=3.2.0",
"django-tailwind>=4.4.2", "django-tailwind>=4.4.2",
"django-widget-tweaks>=1.5.1", "django-widget-tweaks>=1.5.1",
"python-dotenv>=1.2.2", "python-dotenv>=1.2.2",

View File

@@ -12,7 +12,6 @@
"@tailwindcss/postcss": "^4.1.16", "@tailwindcss/postcss": "^4.1.16",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"daisyui": "^5.3.10",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"postcss-cli": "^11.0.1", "postcss-cli": "^11.0.1",
"postcss-nested": "^7.0.2", "postcss-nested": "^7.0.2",
@@ -596,16 +595,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/daisyui": {
"version": "5.5.19",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.19.tgz",
"integrity": "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
}
},
"node_modules/dependency-graph": { "node_modules/dependency-graph": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz",

View File

@@ -16,7 +16,6 @@
"@tailwindcss/postcss": "^4.1.16", "@tailwindcss/postcss": "^4.1.16",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"daisyui": "^5.3.10",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"postcss-cli": "^11.0.1", "postcss-cli": "^11.0.1",
"postcss-nested": "^7.0.2", "postcss-nested": "^7.0.2",

View File

@@ -1,8 +1,6 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "@tailwindcss/typography"; @plugin "@tailwindcss/typography";
@plugin "daisyui";
/** /**
* A catch-all path to Django template files, JavaScript, and Python files * A catch-all path to Django template files, JavaScript, and Python files
* that contain Tailwind CSS classes and will be scanned by Tailwind to generate the final CSS file. * that contain Tailwind CSS classes and will be scanned by Tailwind to generate the final CSS file.

30
uv.lock generated
View File

@@ -308,6 +308,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/e4/ec99d52aa04e204e938564b603f4591e2e82e236ed59af664fee35179e75/django_modelcluster-6.4.1-py2.py3-none-any.whl", hash = "sha256:ccc190cd9e22c24900ea2410bff64d444d48f43f0f4aedeed0f6cd94e2536698", size = 29315, upload-time = "2025-12-04T12:21:39.911Z" }, { url = "https://files.pythonhosted.org/packages/a5/e4/ec99d52aa04e204e938564b603f4591e2e82e236ed59af664fee35179e75/django_modelcluster-6.4.1-py2.py3-none-any.whl", hash = "sha256:ccc190cd9e22c24900ea2410bff64d444d48f43f0f4aedeed0f6cd94e2536698", size = 29315, upload-time = "2025-12-04T12:21:39.911Z" },
] ]
[[package]]
name = "django-oauth-toolkit"
version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "jwcrypto" },
{ name = "oauthlib" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/95/efd83b35c34b86eb2249d2b54c5eaf383c48f3f19034aa6f3807e37471b6/django_oauth_toolkit-3.2.0.tar.gz", hash = "sha256:c36761ae6810083d95a652e9c820046cde0d45a2e2a5574bbe7202656ec20bb6", size = 114211, upload-time = "2026-01-08T22:03:13.311Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/cc/f27a784c0ecd13335abd9ef85ebb80dbc04945f919da5f496f56e3562751/django_oauth_toolkit-3.2.0-py3-none-any.whl", hash = "sha256:bd2cd2719b010231a2f370f927dbcc740454fb1d0dd7e7f4138f36227363dc26", size = 87077, upload-time = "2026-01-08T22:03:12.123Z" },
]
[[package]] [[package]]
name = "django-permissionedforms" name = "django-permissionedforms"
version = "0.1" version = "0.1"
@@ -456,6 +471,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
] ]
[[package]]
name = "jwcrypto"
version = "1.5.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/db/870e5d5fb311b0bcf049630b5ba3abca2d339fd5e13ba175b4c13b456d08/jwcrypto-1.5.6.tar.gz", hash = "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039", size = 87168, upload-time = "2024-03-06T19:58:31.831Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/58/4a1880ea64032185e9ae9f63940c9327c6952d5584ea544a8f66972f2fda/jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789", size = 92520, upload-time = "2024-03-06T19:58:29.765Z" },
]
[[package]] [[package]]
name = "kursy" name = "kursy"
version = "0.1.0" version = "0.1.0"
@@ -466,6 +494,7 @@ dependencies = [
{ name = "django-allauth", extra = ["socialaccount"] }, { name = "django-allauth", extra = ["socialaccount"] },
{ name = "django-allauth-ui" }, { name = "django-allauth-ui" },
{ name = "django-browser-reload" }, { name = "django-browser-reload" },
{ name = "django-oauth-toolkit" },
{ name = "django-tailwind" }, { name = "django-tailwind" },
{ name = "django-widget-tweaks" }, { name = "django-widget-tweaks" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
@@ -482,6 +511,7 @@ requires-dist = [
{ name = "django-allauth", extras = ["socialaccount"], specifier = ">=65.15.0" }, { name = "django-allauth", extras = ["socialaccount"], specifier = ">=65.15.0" },
{ name = "django-allauth-ui", specifier = ">=1.8.1" }, { name = "django-allauth-ui", specifier = ">=1.8.1" },
{ name = "django-browser-reload", specifier = ">=1.21.0" }, { name = "django-browser-reload", specifier = ">=1.21.0" },
{ name = "django-oauth-toolkit", specifier = ">=3.2.0" },
{ name = "django-tailwind", specifier = ">=4.4.2" }, { name = "django-tailwind", specifier = ">=4.4.2" },
{ name = "django-widget-tweaks", specifier = ">=1.5.1" }, { name = "django-widget-tweaks", specifier = ">=1.5.1" },
{ name = "python-dotenv", specifier = ">=1.2.2" }, { name = "python-dotenv", specifier = ">=1.2.2" },