12 Commits

15 changed files with 269 additions and 50 deletions

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.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
@@ -260,36 +259,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()

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
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")

View File

@@ -20,7 +20,9 @@ urlpatterns = [
path("accounts/profile/", views.profile, name="profile"),
path("accounts/signup/", views.signup, name="signup"),
path("i18n/", include("django.conf.urls.i18n")),
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,

View File

@@ -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",

View File

@@ -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",

View File

@@ -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.