Compare commits
28 Commits
feat/add-m
...
feat/add-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
72fca4228c
|
|||
|
9f779407af
|
|||
|
f2f594afb6
|
|||
|
95ab896e5f
|
|||
|
4f58cb0320
|
|||
|
294ea9a28b
|
|||
|
e56aff1a5c
|
|||
|
71d4580a82
|
|||
|
0356374870
|
|||
|
ffc33d3be4
|
|||
|
730e041794
|
|||
|
e0f3f094ff
|
|||
|
157ee875e1
|
|||
|
12de01c2dc
|
|||
|
718aeb9cf5
|
|||
|
9761daf820
|
|||
|
1d0fd27e04
|
|||
|
ac31336acc
|
|||
|
82514a9418
|
|||
|
b599ac6fa3
|
|||
|
fd6209470b
|
|||
|
de48747884
|
|||
|
684871833a
|
|||
|
ef69b99068
|
|||
|
88c797e4b0
|
|||
|
a0819a6552
|
|||
|
e4add89ba8
|
|||
|
7cbec4fc9c
|
@@ -4,3 +4,6 @@ from django.apps import AppConfig
|
||||
class HomeConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "home"
|
||||
|
||||
def ready(self):
|
||||
import home.signals
|
||||
|
||||
26
home/migrations/0017_chatmessage.py
Normal file
26
home/migrations/0017_chatmessage.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
25
home/migrations/0018_courseindexpage.py
Normal file
25
home/migrations/0018_courseindexpage.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-19 14:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('home', '0017_chatmessage'),
|
||||
('wagtailcore', '0096_referenceindex_referenceindex_source_object_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CourseIndexPage',
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
),
|
||||
]
|
||||
18
home/migrations/0019_coursepage_description.py
Normal file
18
home/migrations/0019_coursepage_description.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-19 15:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('home', '0018_courseindexpage'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='coursepage',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
]
|
||||
24
home/models/__init__.py
Normal file
24
home/models/__init__.py
Normal 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",
|
||||
]
|
||||
26
home/models/chat_message.py
Normal file
26
home/models/chat_message.py
Normal 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)
|
||||
38
home/models/event_occurrence.py
Normal file
38
home/models/event_occurrence.py
Normal 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()
|
||||
@@ -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.db import models
|
||||
from django.forms import CheckboxSelectMultiple
|
||||
@@ -9,7 +8,7 @@ from modelcluster.contrib.taggit import ClusterTaggableManager
|
||||
from modelcluster.fields import ParentalKey
|
||||
from taggit.models import TaggedItemBase
|
||||
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.copying import ParentalManyToManyField
|
||||
from wagtail_color_panel.edit_handlers import NativeColorPanel
|
||||
@@ -26,8 +25,29 @@ class HomePage(Page):
|
||||
content_panels = Page.content_panels + ["body"]
|
||||
|
||||
|
||||
class CourseIndexPage(Page):
|
||||
subpage_types = ["home.CoursePage"]
|
||||
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
all_courses = self.get_children().live()
|
||||
purchased_courses = set()
|
||||
other_courses = set()
|
||||
|
||||
for course in all_courses:
|
||||
if course.specific._user_has_access(request.user):
|
||||
purchased_courses.add(course)
|
||||
else:
|
||||
other_courses.add(course)
|
||||
|
||||
context["purchased_courses"] = sorted(
|
||||
purchased_courses, key=lambda c: c.title.lower()
|
||||
)
|
||||
context["other_courses"] = sorted(other_courses, key=lambda c: c.title.lower())
|
||||
return context
|
||||
|
||||
|
||||
class CoursePage(Page):
|
||||
body = RichTextField(blank=True)
|
||||
course_image = models.ForeignKey(
|
||||
"wagtailimages.Image",
|
||||
null=True,
|
||||
@@ -35,6 +55,8 @@ class CoursePage(Page):
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="+",
|
||||
)
|
||||
description = models.CharField(max_length=255, blank=True)
|
||||
body = RichTextField(blank=True)
|
||||
allowed_groups = ParentalManyToManyField(
|
||||
Group,
|
||||
related_name="course_pages",
|
||||
@@ -54,9 +76,11 @@ class CoursePage(Page):
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("course_image"),
|
||||
FieldPanel("description"),
|
||||
FieldPanel("body"),
|
||||
FieldPanel("allowed_groups", widget=CheckboxSelectMultiple),
|
||||
]
|
||||
parent_page_types = ["home.CourseIndexPage"]
|
||||
subpage_types = ["home.CourseModulePage"]
|
||||
|
||||
|
||||
@@ -260,36 +284,3 @@ class EventPage(Page):
|
||||
FieldPanel("recurrence_repeat_until"),
|
||||
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
39
home/signals.py
Normal 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
|
||||
29
home/templates/chat/admin/admin_chat.html
Normal file
29
home/templates/chat/admin/admin_chat.html
Normal 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 %}
|
||||
|
||||
|
||||
23
home/templates/chat/admin/admin_chat_dashboard.html
Normal file
23
home/templates/chat/admin/admin_chat_dashboard.html
Normal 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 %}
|
||||
|
||||
26
home/templates/chat/user_chat.html
Normal file
26
home/templates/chat/user_chat.html
Normal 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 %}
|
||||
52
home/templates/home/course_index_page.html
Normal file
52
home/templates/home/course_index_page.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n wagtailcore_tags wagtailimages_tags %}
|
||||
|
||||
{% block title %}{% trans "Courses" %}{% endblock title %}
|
||||
|
||||
{% block body_class %}template-courseindex{% endblock body_class %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">{% trans "Courses" %}</h1>
|
||||
|
||||
<h2 class="text-2xl font-semibold mb-4">{% trans "Purchased Courses" %}</h2>
|
||||
<div class="flex flex-wrap -mx-4">
|
||||
{% for course in purchased_courses %}
|
||||
<div class="w-full md:w-1/2 lg:w-1/3 px-4 mb-8">
|
||||
<a href="{{ course.url }}" class="block bg-green-50 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
|
||||
{% image course.specific.course_image original %}
|
||||
<div class="p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h2 class="text-xl font-semibold">{{ course.specific.title }}</h2>
|
||||
<div class="relative w-8 h-8 rounded-full bg-green-500">
|
||||
<i class="fi fi-br-lock-open-alt leading-0 absolute left-0 top-1/2 translate-x-1/2 -translate-y-1/2 text-white" title="{% trans "Purchased" %}"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-600">{{ course.specific.description|truncatewords:20 }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h2 class="text-2xl font-semibold mb-4">{% trans "Available Courses" %}</h2>
|
||||
<div class="flex flex-wrap -mx-4">
|
||||
{% for course in other_courses %}
|
||||
<div class="w-full md:w-1/2 lg:w-1/3 px-4 mb-8">
|
||||
<a href="{{ course.url }}" class="block bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
|
||||
{% image course.specific.course_image original %}
|
||||
<div class="p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h2 class="text-xl font-semibold">{{ course.specific.title }}</h2>
|
||||
<div class="relative w-8 h-8 rounded-full bg-gray-200">
|
||||
<i class="fi fi-br-shopping-basket leading-0 absolute left-0 top-1/2 translate-x-1/2 -translate-y-1/2 text-gray-700" title="{% trans "Not Purchased" %}"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-600">{{ course.specific.description|truncatewords:20 }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
@@ -17,9 +17,13 @@
|
||||
</h1>
|
||||
|
||||
{% if page.course_image %}
|
||||
{% image page.course_image original alt=page.title class="w-full h-auto rounded-lg mb-6" %}
|
||||
{% image page.course_image original alt=page.title class="w-full h-auto rounded-lg mb-4" %}
|
||||
{% endif %}
|
||||
|
||||
<p class="not-prose text-gray-600 mb-6 text-lg">
|
||||
{{ page.description }}
|
||||
</p>
|
||||
|
||||
{{ page.body|richtext }}
|
||||
|
||||
{% if user_has_access %}
|
||||
|
||||
7
home/urls.py
Normal file
7
home/urls.py
Normal 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
44
home/views.py
Normal 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")
|
||||
@@ -1,7 +1,12 @@
|
||||
from wagtail.admin.menu import MenuItem
|
||||
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
|
||||
from wagtail.admin.rich_text.converters.html_to_contentstate import BlockElementHandler
|
||||
from wagtail import hooks
|
||||
|
||||
from django.urls import path, reverse
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
@hooks.register("register_rich_text_features")
|
||||
def register_code_block_feature(features):
|
||||
@@ -39,3 +44,20 @@ def register_code_block_feature(features):
|
||||
|
||||
# Optional: add to default features
|
||||
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
11
kursy/oauth_validators.py
Normal 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,
|
||||
}
|
||||
@@ -58,6 +58,7 @@ INSTALLED_APPS = [
|
||||
"allauth.account",
|
||||
"allauth.socialaccount",
|
||||
"allauth.socialaccount.providers.github",
|
||||
"oauth2_provider",
|
||||
"tailwind",
|
||||
"theme",
|
||||
"widget_tweaks",
|
||||
@@ -75,6 +76,7 @@ MIDDLEWARE = [
|
||||
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
|
||||
"allauth.account.middleware.AccountMiddleware",
|
||||
"django.middleware.locale.LocaleMiddleware",
|
||||
"oauth2_provider.middleware.OAuth2TokenMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "kursy.urls"
|
||||
@@ -131,6 +133,26 @@ SOCIALACCOUNT_PROVIDERS = {
|
||||
|
||||
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
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||
@@ -252,3 +274,6 @@ WAGTAILDOCS_EXTENSIONS = [
|
||||
]
|
||||
|
||||
TAILWIND_APP_NAME = "theme"
|
||||
|
||||
# Gitea API
|
||||
GITEA_URL = "http://localhost:3000/api/v1"
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
<header class="bg-blue-900 text-white shadow-md relative">
|
||||
<div class="container mx-auto flex items-center justify-between py-4 px-6">
|
||||
{% wagtail_site as current_site %}
|
||||
<a class="text-xl font-bold" href="/">{{ current_site.site_name }}</a>
|
||||
<div class="flex items-center gap-4">
|
||||
<a class="text-xl font-bold" href="/">{{ current_site.site_name }}</a>
|
||||
<a href="{% slugurl 'courses' %}" class="hover:underline">{% trans "Courses" %}</a>
|
||||
<a href="{% url 'calendar' %}" class="hover:underline">{% trans "Course Calendar" %}</a>
|
||||
</div>
|
||||
|
||||
<nav class="flex items-center gap-4">
|
||||
<a href="{% url 'calendar' %}" class="hover:underline">{% trans "Course Calendar" %}</a>
|
||||
{% if user.is_authenticated %}
|
||||
<a href="{% url 'account_logout' %}" class="hover:underline">{% trans "Logout" %}</a>
|
||||
{% else %}
|
||||
|
||||
@@ -20,7 +20,10 @@ urlpatterns = [
|
||||
path("accounts/profile/", views.profile, name="profile"),
|
||||
path("accounts/signup/", views.signup, name="signup"),
|
||||
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"),
|
||||
# TODO: move occurrence related urls to home app
|
||||
path(
|
||||
"occurrence/<int:occurrence_id>/",
|
||||
views.occurrence_detail,
|
||||
|
||||
@@ -8,6 +8,7 @@ dependencies = [
|
||||
"django-allauth-ui>=1.8.1",
|
||||
"django-allauth[socialaccount]>=65.15.0",
|
||||
"django-browser-reload>=1.21.0",
|
||||
"django-oauth-toolkit>=3.2.0",
|
||||
"django-tailwind>=4.4.2",
|
||||
"django-widget-tweaks>=1.5.1",
|
||||
"python-dotenv>=1.2.2",
|
||||
|
||||
11
theme/static_src/package-lock.json
generated
11
theme/static_src/package-lock.json
generated
@@ -12,7 +12,6 @@
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"cross-env": "^10.1.0",
|
||||
"daisyui": "^5.3.10",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-nested": "^7.0.2",
|
||||
@@ -596,16 +595,6 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz",
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"cross-env": "^10.1.0",
|
||||
"daisyui": "^5.3.10",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-nested": "^7.0.2",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@plugin "daisyui";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
30
uv.lock
generated
30
uv.lock
generated
@@ -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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "django-permissionedforms"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "kursy"
|
||||
version = "0.1.0"
|
||||
@@ -466,6 +494,7 @@ dependencies = [
|
||||
{ name = "django-allauth", extra = ["socialaccount"] },
|
||||
{ name = "django-allauth-ui" },
|
||||
{ name = "django-browser-reload" },
|
||||
{ name = "django-oauth-toolkit" },
|
||||
{ name = "django-tailwind" },
|
||||
{ name = "django-widget-tweaks" },
|
||||
{ name = "python-dotenv" },
|
||||
@@ -482,6 +511,7 @@ requires-dist = [
|
||||
{ name = "django-allauth", extras = ["socialaccount"], specifier = ">=65.15.0" },
|
||||
{ name = "django-allauth-ui", specifier = ">=1.8.1" },
|
||||
{ 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-widget-tweaks", specifier = ">=1.5.1" },
|
||||
{ name = "python-dotenv", specifier = ">=1.2.2" },
|
||||
|
||||
Reference in New Issue
Block a user