From a1a48f54361ffdf98027682440867ead2940eeab Mon Sep 17 00:00:00 2001 From: Yash Kumar Singh Date: Wed, 18 Mar 2026 19:33:29 +0530 Subject: [PATCH 1/3] added sig dashboard --- DASHBOARD_SUMMARY.md | 0 corpus/accounts/models.py | 4 + corpus/config/SIG_ADMIN_GUIDE.md | 0 corpus/config/admin.py | 144 +++++++++++++- corpus/pages/forms.py | 60 ++++++ corpus/pages/urls.py | 6 + corpus/pages/views.py | 139 ++++++++++++- corpus/templates/pages/sig.html | 5 + corpus/templates/pages/sig_dashboard.html | 184 ++++++++++++++++++ .../templates/pages/sig_dashboard_list.html | 28 +++ corpus/templates/pages/sig_event_edit.html | 60 ++++++ .../templates/pages/sig_events_dashboard.html | 101 ++++++++++ 12 files changed, 727 insertions(+), 4 deletions(-) create mode 100644 DASHBOARD_SUMMARY.md create mode 100644 corpus/config/SIG_ADMIN_GUIDE.md create mode 100644 corpus/pages/forms.py create mode 100644 corpus/templates/pages/sig_dashboard.html create mode 100644 corpus/templates/pages/sig_dashboard_list.html create mode 100644 corpus/templates/pages/sig_event_edit.html create mode 100644 corpus/templates/pages/sig_events_dashboard.html diff --git a/DASHBOARD_SUMMARY.md b/DASHBOARD_SUMMARY.md new file mode 100644 index 00000000..e69de29b diff --git a/corpus/accounts/models.py b/corpus/accounts/models.py index d955e3cb..6e6f5817 100644 --- a/corpus/accounts/models.py +++ b/corpus/accounts/models.py @@ -211,3 +211,7 @@ class Meta: ) term_start = models.DateField() term_end = models.DateField() + + def __str__(self): + full_name = f"{self.user.first_name} {self.user.last_name}".strip() + return full_name or self.user.email diff --git a/corpus/config/SIG_ADMIN_GUIDE.md b/corpus/config/SIG_ADMIN_GUIDE.md new file mode 100644 index 00000000..e69de29b diff --git a/corpus/config/admin.py b/corpus/config/admin.py index 720a64eb..5964d267 100644 --- a/corpus/config/admin.py +++ b/corpus/config/admin.py @@ -1,19 +1,157 @@ from django.contrib import admin +from django.utils.html import format_html +from django.urls import reverse from .models import ModuleConfiguration from .models import SIG from .models import Society -# Define the fields to be displayed in the admin panel class SocietyAdmin(admin.ModelAdmin): - list_display = ("id", "name", "url") + list_display = ("name", "has_image", "has_dark_image", "linked_sigs", "faculty_advisors", "url") list_display_links = ("name",) + search_fields = ("name", "description", "url") + list_filter = ("sigs",) + filter_horizontal = ("sigs",) + + fieldsets = ( + ("Basic Information", { + "fields": ("name", "url", "description"), + "description": "Core society information" + }), + ("Images", { + "fields": ("image", "dark_image"), + "description": "Upload society logos (light and dark mode versions)" + }), + ("SIG Association", { + "fields": ("sigs",), + "description": "Link this society to relevant SIGs" + }), + ) + + def has_image(self, obj): + if obj.image: + return format_html( + '', + obj.image.url + ) + return format_html('No image') + has_image.short_description = "Light Image" + + def has_dark_image(self, obj): + if obj.dark_image: + return format_html( + '', + obj.dark_image.url + ) + return format_html('No dark image') + has_dark_image.short_description = "Dark Image" + + def linked_sigs(self, obj): + sigs = obj.sigs.all() + if sigs: + badges = [] + for sig in sigs: + color = sig.color if sig.color else "#6b7280" + badges.append( + f'{sig.name}' + ) + return format_html("".join(badges)) + return format_html('No SIGs') + linked_sigs.short_description = "Linked SIGs" + + def faculty_advisors(self, obj): + from accounts.models import Faculty + count = Faculty.objects.filter(society=obj).count() + if count > 0: + return format_html( + '{} advisor{}', + count, + 's' if count != 1 else '' + ) + return format_html('No advisors') + faculty_advisors.short_description = "Faculty Advisors" class SIGAdmin(admin.ModelAdmin): - list_display = ("id", "name") + list_display = ("id", "name", "slug", "has_image", "event_count", "society_count", "view_events_link") list_display_links = ("name",) + search_fields = ("name", "slug", "about", "what_we_do") + list_filter = ("name",) + prepopulated_fields = {"slug": ("name",)} + + fieldsets = ( + ("Basic Information", { + "fields": ("name", "slug", "color"), + "description": "Core SIG identification and branding" + }), + ("Visual Content", { + "fields": ("sig_image",), + "description": "Upload or update the SIG's featured image" + }), + ("About Section", { + "fields": ("about",), + "description": "Edit the 'About Us' section displayed on the SIG page", + "classes": ("wide",) + }), + ("What We Do Section", { + "fields": ("what_we_do",), + "description": "Edit the 'What We Do' section describing SIG activities", + "classes": ("wide",) + }), + ) + + def has_image(self, obj): + if obj.sig_image: + return format_html( + '', + obj.sig_image.url + ) + return format_html('No image') + has_image.short_description = "SIG Image" + + def event_count(self, obj): + count = obj.events.count() + if count > 0: + return format_html( + '{} events', + count + ) + return format_html('No events') + event_count.short_description = "Events" + + def society_count(self, obj): + count = obj.societies.count() + if count > 0: + return format_html( + '{} societ{}', + count, + 'ies' if count != 1 else 'y' + ) + return format_html('No societies') + society_count.short_description = "Societies" + + def view_events_link(self, obj): + url = reverse("newsletter_manage_announcements") + return format_html( + '' + 'Manage Events', + url + ) + view_events_link.short_description = "Events Dashboard" + + def get_readonly_fields(self, request, obj=None): + if obj: + return self.readonly_fields + return self.readonly_fields + + class Media: + css = { + 'all': ('admin/css/widgets.css',) + } admin.site.register(Society, SocietyAdmin) diff --git a/corpus/pages/forms.py b/corpus/pages/forms.py new file mode 100644 index 00000000..7a452ed0 --- /dev/null +++ b/corpus/pages/forms.py @@ -0,0 +1,60 @@ +from accounts.models import Faculty +from config.models import SIG +from config.models import Society +from corpus.forms import CorpusModelForm +from django import forms +from newsletter.models import Event + + +class SIGContentForm(CorpusModelForm): + class Meta: + model = SIG + fields = ["about", "what_we_do", "sig_image"] + + +class SocietyDashboardForm(CorpusModelForm): + faculty_advisors = forms.ModelMultipleChoiceField( + queryset=Faculty.objects.none(), + required=False, + help_text="Assign faculty advisors from this SIG.", + widget=forms.CheckboxSelectMultiple( + attrs={"class": "checkbox checkbox-primary checkbox-sm"} + ), + ) + + class Meta: + model = Society + fields = ["name", "url", "description", "image", "dark_image"] + + def __init__(self, *args, **kwargs): + self.sig = kwargs.pop("sig", None) + super().__init__(*args, **kwargs) + self.fields["faculty_advisors"].widget.attrs["class"] = "checkbox checkbox-primary checkbox-sm" + + if self.sig is not None: + advisors_qs = Faculty.objects.filter(sig=self.sig).select_related("user") + self.fields["faculty_advisors"].queryset = advisors_qs + self.fields["faculty_advisors"].label_from_instance = ( + lambda obj: ( + f"{obj.user.first_name} {obj.user.last_name}".strip() + or obj.user.email + ) + ) + if self.instance and self.instance.pk: + self.fields["faculty_advisors"].initial = advisors_qs.filter( + society=self.instance + ) + + def save_faculty_advisors(self, society, sig): + selected_advisor_ids = list( + self.cleaned_data.get("faculty_advisors", []).values_list("id", flat=True) + ) + + Faculty.objects.filter(sig=sig, society=society).exclude( + id__in=selected_advisor_ids + ).update(society=None) + + if selected_advisor_ids: + Faculty.objects.filter(sig=sig, id__in=selected_advisor_ids).update( + society=society + ) diff --git a/corpus/pages/urls.py b/corpus/pages/urls.py index 9ca59247..c83eaf65 100644 --- a/corpus/pages/urls.py +++ b/corpus/pages/urls.py @@ -6,5 +6,11 @@ path("", views.index, name="index"), path("about_us/", views.about_us, name="about_us"), path("sig//", views.sig, name="sig"), + path("sig-dashboard/", views.sig_dashboard_list, name="sig_dashboard_list"), + path( + "sig-dashboard//", + views.sig_dashboard, + name="sig_dashboard", + ), path("team/", views.team, name="team"), ] diff --git a/corpus/pages/views.py b/corpus/pages/views.py index 975e66d0..2aa6eac3 100644 --- a/corpus/pages/views.py +++ b/corpus/pages/views.py @@ -1,21 +1,58 @@ from datetime import date,timedelta -from pyexpat.errors import messages from accounts.models import ExecutiveMember from accounts.models import Faculty from config.models import ModuleConfiguration from config.models import SIG from config.models import Society +from django.contrib import messages +from django.contrib.auth.decorators import login_required from django.db.models import Q from django.shortcuts import get_object_or_404 from django.shortcuts import render, redirect from newsletter.models import Event from corpus.decorators import module_enabled +from .forms import SIGContentForm +from .forms import SocietyDashboardForm from django.utils import timezone +def _get_sig_dashboard_permissions(user): + if not user.is_authenticated: + return {} + + if user.is_superuser: + sigs = SIG.objects.exclude(slug__isnull=True) + return {sig.slug: sig for sig in sigs} + + try: + exec_member = ( + ExecutiveMember.objects.select_related("sig", "core__post") + .get(user=user) + ) + except ExecutiveMember.DoesNotExist: + return {} + + if not hasattr(exec_member, "core"): + return {} + + post_name = (exec_member.core.post.name or "").lower() + if "chair" not in post_name: + return {} + + if not exec_member.sig.slug: + return {} + + return {exec_member.sig.slug: exec_member.sig} + + +def _get_authorized_sig_or_none(request, sig_slug): + authorized_sigs = _get_sig_dashboard_permissions(request.user) + return authorized_sigs.get(sig_slug) + + def get_active_members(): try: active_batches = ModuleConfiguration.objects.get(module_name="teampage").module_config @@ -117,6 +154,7 @@ def sig(request, sig_name): ).order_by("start_date") number_of_members = get_active_members().filter(sig=sig_data).count() number_of_events = events_past_year.count() + can_manage_sig = sig_data.slug in _get_sig_dashboard_permissions(request.user) return render( request, @@ -128,6 +166,7 @@ def sig(request, sig_name): "alumni_logos": alumnilogos_linked_to_sig, "no_of_members": number_of_members, "no_of_events": number_of_events, + "can_manage_sig": can_manage_sig, }, ) @@ -220,3 +259,101 @@ def team(request): def farewell(request): return render(request, "pages/farewell.html") + + +@login_required +def sig_dashboard_list(request): + authorized_sigs = _get_sig_dashboard_permissions(request.user) + return render( + request, + "pages/sig_dashboard_list.html", + {"authorized_sigs": authorized_sigs.values()}, + ) + + +@login_required +def sig_dashboard(request, sig_slug): + sig_data = _get_authorized_sig_or_none(request, sig_slug) + if sig_data is None: + messages.error( + request, + "Permission denied. You can only access dashboards for SIGs you chair.", + ) + return redirect("sig_dashboard_list") + + sig_form = SIGContentForm(instance=sig_data) + society_form = SocietyDashboardForm(sig=sig_data) + + invalid_society_form = None + invalid_society_id = None + + if request.method == "POST": + action = request.POST.get("action") + + if action == "update_sig": + sig_form = SIGContentForm(request.POST, request.FILES, instance=sig_data) + if sig_form.is_valid(): + sig_form.save() + messages.success(request, f"{sig_data.name} page details updated.") + return redirect("sig_dashboard", sig_slug=sig_slug) + + elif action == "create_society": + society_form = SocietyDashboardForm(request.POST, request.FILES, sig=sig_data) + if society_form.is_valid(): + society = society_form.save(commit=False) + society.save() + society.sigs.add(sig_data) + society_form.save_faculty_advisors(society=society, sig=sig_data) + messages.success(request, "Society added to SIG successfully.") + return redirect("sig_dashboard", sig_slug=sig_slug) + + elif action == "update_society": + society_id = request.POST.get("society_id") + society_instance = get_object_or_404(Society, id=society_id, sigs=sig_data) + edit_form = SocietyDashboardForm( + request.POST, + request.FILES, + instance=society_instance, + sig=sig_data, + ) + if edit_form.is_valid(): + society = edit_form.save() + edit_form.save_faculty_advisors(society=society, sig=sig_data) + messages.success(request, "Society details updated.") + return redirect("sig_dashboard", sig_slug=sig_slug) + invalid_society_form = edit_form + invalid_society_id = society_instance.id + + elif action == "remove_society": + society_id = request.POST.get("society_id") + society = get_object_or_404(Society, id=society_id, sigs=sig_data) + society.sigs.remove(sig_data) + Faculty.objects.filter(sig=sig_data, society=society).update(society=None) + messages.success(request, "Society removed from this SIG.") + return redirect("sig_dashboard", sig_slug=sig_slug) + + elif action == "clear_society_advisors": + society_id = request.POST.get("society_id") + society = get_object_or_404(Society, id=society_id, sigs=sig_data) + Faculty.objects.filter(sig=sig_data, society=society).update(society=None) + messages.success(request, "All faculty advisors removed from this society.") + return redirect("sig_dashboard", sig_slug=sig_slug) + + linked_societies = sig_data.societies.all().order_by("name") + society_edit_forms = [] + for society in linked_societies: + if invalid_society_id == society.id and invalid_society_form is not None: + society_edit_forms.append((society, invalid_society_form)) + else: + society_edit_forms.append((society, SocietyDashboardForm(instance=society, sig=sig_data))) + + return render( + request, + "pages/sig_dashboard.html", + { + "sig": sig_data, + "sig_form": sig_form, + "society_form": society_form, + "society_edit_forms": society_edit_forms, + }, + ) diff --git a/corpus/templates/pages/sig.html b/corpus/templates/pages/sig.html index ab5421de..a424f670 100644 --- a/corpus/templates/pages/sig.html +++ b/corpus/templates/pages/sig.html @@ -20,6 +20,11 @@ class="relative text-4xl sm:text-6xl md:text-7xl lg:text-9xl font-extrabold mb-6 dark:text-white drop-shadow-[0_0_35px_rgba(255,255,255,0.5)]"> {{ sig.name }} + {% if can_manage_sig %} + + Manage {{ sig.name }} + + {% endif %} diff --git a/corpus/templates/pages/sig_dashboard.html b/corpus/templates/pages/sig_dashboard.html new file mode 100644 index 00000000..57ae3f53 --- /dev/null +++ b/corpus/templates/pages/sig_dashboard.html @@ -0,0 +1,184 @@ +{% extends 'base.html' %} + +{% block title %} +{{ sig.name }} Dashboard +{% endblock %} + +{% block content %} + +
+
+
+
+

{{ sig.name }} Dashboard

+ Manage Events +
+
+ +
+

Edit SIG Page Content

+
+ {% csrf_token %} + + +
+ + {{ sig_form.about }} + {{ sig_form.about.errors }} +
+ +
+ + {{ sig_form.what_we_do }} + {{ sig_form.what_we_do.errors }} +
+ +
+ + {{ sig_form.sig_image }} + {{ sig_form.sig_image.errors }} +
+ + +
+
+ +
+

Linked Societies

+ + {% for society, edit_form in society_edit_forms %} +
+ + {{ society.get_name_display }} ({{ society.name }}) + +
+
+ {% csrf_token %} + + + + {{ edit_form.non_field_errors }} + +
+ + {{ edit_form.name }} + {{ edit_form.name.errors }} +
+
+ + {{ edit_form.url }} + {{ edit_form.url.errors }} +
+
+ + {{ edit_form.description }} + {{ edit_form.description.errors }} +
+
+ + {{ edit_form.image }} + {{ edit_form.image.errors }} +
+
+ + {{ edit_form.dark_image }} + {{ edit_form.dark_image.errors }} +
+
+ +
+ {% for checkbox in edit_form.faculty_advisors %} + + {% empty %} + No faculty advisors available for this SIG. + {% endfor %} +
+ {{ edit_form.faculty_advisors.errors }} +
+ + +
+ +
+ {% csrf_token %} + + + +
+ +
+ {% csrf_token %} + + + +
+
+
+ {% empty %} +
+ No societies linked to this SIG yet. +
+ {% endfor %} +
+ +
+

Add New Society

+
+ {% csrf_token %} + + + {{ society_form.non_field_errors }} + +
+ + {{ society_form.name }} + {{ society_form.name.errors }} +
+
+ + {{ society_form.url }} + {{ society_form.url.errors }} +
+
+ + {{ society_form.description }} + {{ society_form.description.errors }} +
+
+ + {{ society_form.image }} + {{ society_form.image.errors }} +
+
+ + {{ society_form.dark_image }} + {{ society_form.dark_image.errors }} +
+
+ +
+ {% for checkbox in society_form.faculty_advisors %} + + {% empty %} + No faculty advisors available for this SIG. + {% endfor %} +
+ {{ society_form.faculty_advisors.errors }} +
+ + +
+
+
+
+{% endblock %} diff --git a/corpus/templates/pages/sig_dashboard_list.html b/corpus/templates/pages/sig_dashboard_list.html new file mode 100644 index 00000000..29c1ac6b --- /dev/null +++ b/corpus/templates/pages/sig_dashboard_list.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block title %} +SIG Dashboard +{% endblock %} + +{% block content %} +
+
+

SIG Dashboard

+

You can access only the SIGs where you are the Chair/Vice Chair.

+ + {% if authorized_sigs %} +
+ {% for sig in authorized_sigs %} + + Manage {{ sig.name }} + + {% endfor %} +
+ {% else %} +
+ No SIG dashboard access found for your account. +
+ {% endif %} +
+
+{% endblock %} diff --git a/corpus/templates/pages/sig_event_edit.html b/corpus/templates/pages/sig_event_edit.html new file mode 100644 index 00000000..073468f4 --- /dev/null +++ b/corpus/templates/pages/sig_event_edit.html @@ -0,0 +1,60 @@ +{% extends 'base.html' %} + +{% block title %} +Edit Event +{% endblock %} + +{% block content %} +
+
+

Edit Event: {{ event.name }}

+ +
+ {% csrf_token %} + {{ form.non_field_errors }} + +
+ + {{ form.name }} + {{ form.name.errors }} +
+
+ + {{ form.description }} + {{ form.description.errors }} +
+
+
+ + {{ form.start_date }} + {{ form.start_date.errors }} +
+
+ + {{ form.end_date }} + {{ form.end_date.errors }} +
+
+
+ + {{ form.page_link }} + {{ form.page_link.errors }} +
+
+ + {{ form.thumbnail }} + {{ form.thumbnail.errors }} +
+
+ + +
+ +
+ + Cancel +
+
+
+
+{% endblock %} diff --git a/corpus/templates/pages/sig_events_dashboard.html b/corpus/templates/pages/sig_events_dashboard.html new file mode 100644 index 00000000..85a784bf --- /dev/null +++ b/corpus/templates/pages/sig_events_dashboard.html @@ -0,0 +1,101 @@ +{% extends 'base.html' %} + +{% block title %} +{{ sig.name }} Events Dashboard +{% endblock %} + +{% block content %} +
+
+
+
+

{{ sig.name }} Events Dashboard

+ Back to SIG Dashboard +
+
+ +
+

Add Event

+
+ {% csrf_token %} + {{ form.non_field_errors }} + +
+ + {{ form.name }} + {{ form.name.errors }} +
+
+ + {{ form.description }} + {{ form.description.errors }} +
+
+
+ + {{ form.start_date }} + {{ form.start_date.errors }} +
+
+ + {{ form.end_date }} + {{ form.end_date.errors }} +
+
+
+ + {{ form.page_link }} + {{ form.page_link.errors }} +
+
+ + {{ form.thumbnail }} + {{ form.thumbnail.errors }} +
+
+ + +
+ + +
+
+ +
+

Existing Events

+
+ + + + + + + + + + + {% for event in events %} + + + + + + + {% empty %} + + + + {% endfor %} + +
NameDatesArchivedActions
{{ event.name }}{{ event.start_date }} - {{ event.end_date }}{{ event.archive_event|yesno:"Yes,No" }} + Edit +
+ {% csrf_token %} + +
+
No events linked to this SIG yet.
+
+
+
+
+{% endblock %} From 40997d7f18aafa88b5921862ed1e683a3795c004 Mon Sep 17 00:00:00 2001 From: Yash Kumar Singh <157038634+yashhash2@users.noreply.github.com> Date: Wed, 18 Mar 2026 19:48:27 +0530 Subject: [PATCH 2/3] Delete DASHBOARD_SUMMARY.md --- DASHBOARD_SUMMARY.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 DASHBOARD_SUMMARY.md diff --git a/DASHBOARD_SUMMARY.md b/DASHBOARD_SUMMARY.md deleted file mode 100644 index e69de29b..00000000 From 66b06a99e6fcff878d3cc1699362b7add15bb5c0 Mon Sep 17 00:00:00 2001 From: Yash Kumar Singh <157038634+yashhash2@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:02:28 +0530 Subject: [PATCH 3/3] Delete corpus/config/SIG_ADMIN_GUIDE.md --- corpus/config/SIG_ADMIN_GUIDE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 corpus/config/SIG_ADMIN_GUIDE.md diff --git a/corpus/config/SIG_ADMIN_GUIDE.md b/corpus/config/SIG_ADMIN_GUIDE.md deleted file mode 100644 index e69de29b..00000000