Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions web/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
ForumCategory,
ForumReply,
ForumTopic,
Gathering,
GatheringAnnouncement,
GatheringRegistration,
Goods,
LearningStreak,
MembershipPlan,
Expand Down Expand Up @@ -889,3 +892,47 @@ class VideoRequestAdmin(admin.ModelAdmin):
list_display = ("title", "status", "category", "requester", "created_at")
list_filter = ("status", "category")
search_fields = ("title", "description", "requester__username")


@admin.register(Gathering)
class GatheringAdmin(admin.ModelAdmin):
list_display = ("title", "gathering_type", "organizer", "start_datetime", "status", "visibility", "attendee_count")
list_filter = ("gathering_type", "status", "visibility", "is_virtual")
search_fields = ("title", "description", "organizer__username", "organizer__email")
prepopulated_fields = {"slug": ("title",)}
raw_id_fields = ("organizer",)
readonly_fields = ("created_at", "updated_at")
date_hierarchy = "start_datetime"
fieldsets = (
(None, {"fields": ("title", "slug", "description", "gathering_type", "organizer", "image", "tags")}),
("Schedule", {"fields": ("start_datetime", "end_datetime")}),
("Location", {"fields": ("is_virtual", "meeting_link", "location", "latitude", "longitude")}),
(
"Registration",
{"fields": ("registration_required", "max_attendees", "price")},
),
("Visibility", {"fields": ("status", "visibility")}),
("Timestamps", {"fields": ("created_at", "updated_at"), "classes": ("collapse",)}),
)

def attendee_count(self, obj):
return obj.attendee_count

attendee_count.short_description = "Attendees"


@admin.register(GatheringRegistration)
class GatheringRegistrationAdmin(admin.ModelAdmin):
list_display = ("gathering", "attendee", "status", "registered_at")
list_filter = ("status",)
search_fields = ("gathering__title", "attendee__username", "attendee__email")
raw_id_fields = ("gathering", "attendee")
readonly_fields = ("registered_at", "updated_at")


@admin.register(GatheringAnnouncement)
class GatheringAnnouncementAdmin(admin.ModelAdmin):
list_display = ("gathering", "title", "author", "created_at")
search_fields = ("gathering__title", "title", "author__username")
raw_id_fields = ("gathering", "author")
readonly_fields = ("created_at", "updated_at")
99 changes: 99 additions & 0 deletions web/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
CourseMaterial,
EducationalVideo,
ForumCategory,
Gathering,
GatheringAnnouncement,
GatheringRegistration,
Goods,
GradeableLink,
LinkGrade,
Expand Down Expand Up @@ -112,6 +115,9 @@
"SurveyForm",
"VirtualClassroomForm",
"VirtualClassroomCustomizationForm",
"GatheringForm",
"GatheringRegistrationForm",
"GatheringAnnouncementForm",
]

fernet = Fernet(settings.SECURE_MESSAGE_KEY)
Expand Down Expand Up @@ -2084,3 +2090,96 @@ def clean_desks_per_row(self):
if value is None or value < 1 or value > 8:
raise forms.ValidationError("Desks per row must be between 1 and 8.")
return value


class GatheringForm(forms.ModelForm):
"""Form for creating and editing a Gathering (event, meetup, class, etc.)."""

class Meta:
model = Gathering
fields = [
"title",
"description",
"gathering_type",
"start_datetime",
"end_datetime",
"is_virtual",
"meeting_link",
"location",
"max_attendees",
"registration_required",
"price",
"visibility",
"status",
"image",
"tags",
]
widgets = {
"title": TailwindInput(),
"description": TailwindTextarea(attrs={"rows": 5}),
"gathering_type": TailwindSelect(),
"start_datetime": TailwindDateTimeInput(),
"end_datetime": TailwindDateTimeInput(),
"meeting_link": URLInput(
attrs={
"class": (
"w-full px-4 py-2 border border-gray-300 dark:border-gray-600 "
"rounded-lg focus:ring-2 focus:ring-teal-400 "
"bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
),
"placeholder": "https://meet.example.com/...",
}
),
"location": TailwindInput(attrs={"placeholder": "Address or venue name"}),
"max_attendees": TailwindNumberInput(attrs={"min": "1", "placeholder": "Leave blank for unlimited"}),
"price": TailwindNumberInput(attrs={"min": "0", "step": "0.01"}),
"visibility": TailwindSelect(),
"status": TailwindSelect(),
"tags": TailwindInput(attrs={"placeholder": "python, django, web (comma separated)"}),
"is_virtual": TailwindCheckboxInput(),
"registration_required": TailwindCheckboxInput(),
}

def clean(self):
cleaned_data = super().clean()
start = cleaned_data.get("start_datetime")
end = cleaned_data.get("end_datetime")
is_virtual = cleaned_data.get("is_virtual")
meeting_link = cleaned_data.get("meeting_link")
location = cleaned_data.get("location")

if start and end and end <= start:
raise forms.ValidationError("End date/time must be after the start date/time.")

if is_virtual and not meeting_link:
self.add_error("meeting_link", "A meeting link is required for virtual gatherings.")

if not is_virtual and not location:
self.add_error("location", "A location is required for in-person gatherings.")

return cleaned_data


class GatheringRegistrationForm(forms.ModelForm):
"""Form for attendees to register for a Gathering."""

class Meta:
model = GatheringRegistration
fields = ["notes"]
widgets = {
"notes": TailwindTextarea(
attrs={"rows": 3, "placeholder": "Any notes or questions for the organizer (optional)"}
),
}


class GatheringAnnouncementForm(forms.ModelForm):
"""Form for organizers to post announcements to gathering attendees."""

class Meta:
model = GatheringAnnouncement
fields = ["title", "content"]
widgets = {
"title": TailwindInput(attrs={"placeholder": "Announcement title"}),
"content": TailwindTextarea(attrs={"rows": 4, "placeholder": "Write your announcement here..."}),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("web", "0063_virtualclassroom_virtualclassroomcustomization_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="Gathering",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("title", models.CharField(max_length=255)),
("slug", models.SlugField(blank=True, max_length=255, unique=True)),
("description", models.TextField()),
(
"gathering_type",
models.CharField(
choices=[
("meetup", "Meetup"),
("workshop", "Workshop"),
("class", "Class"),
("webinar", "Webinar"),
("conference", "Conference"),
("study_group", "Study Group"),
("networking", "Networking"),
("hackathon", "Hackathon"),
("club_meeting", "Club Meeting"),
("other", "Other"),
],
default="meetup",
max_length=50,
),
),
("start_datetime", models.DateTimeField()),
("end_datetime", models.DateTimeField()),
("is_virtual", models.BooleanField(default=False)),
("meeting_link", models.URLField(blank=True)),
("location", models.CharField(blank=True, max_length=255)),
(
"latitude",
models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
(
"longitude",
models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
),
(
"max_attendees",
models.PositiveIntegerField(blank=True, help_text="Leave blank for unlimited", null=True),
),
("registration_required", models.BooleanField(default=True)),
(
"price",
models.DecimalField(decimal_places=2, default=0.0, max_digits=10),
),
(
"visibility",
models.CharField(
choices=[
("public", "Public"),
("private", "Private"),
("invite_only", "Invite Only"),
],
default="public",
max_length=20,
),
),
(
"status",
models.CharField(
choices=[
("draft", "Draft"),
("published", "Published"),
("cancelled", "Cancelled"),
("completed", "Completed"),
],
default="draft",
max_length=20,
),
),
("image", models.ImageField(blank=True, upload_to="gatherings/")),
(
"tags",
models.CharField(blank=True, help_text="Comma-separated tags", max_length=255),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"organizer",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="organized_gatherings",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Gathering",
"verbose_name_plural": "Gatherings",
"ordering": ["start_datetime"],
},
),
migrations.CreateModel(
name="GatheringRegistration",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
(
"status",
models.CharField(
choices=[
("pending", "Pending"),
("confirmed", "Confirmed"),
("cancelled", "Cancelled"),
("waitlisted", "Waitlisted"),
("attended", "Attended"),
],
default="pending",
max_length=20,
),
),
(
"notes",
models.TextField(blank=True, help_text="Optional notes from the attendee"),
),
("registered_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"attendee",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="gathering_registrations",
to=settings.AUTH_USER_MODEL,
),
),
(
"gathering",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="registrations",
to="web.gathering",
),
),
],
options={
"verbose_name": "Gathering Registration",
"verbose_name_plural": "Gathering Registrations",
"ordering": ["registered_at"],
"unique_together": {("gathering", "attendee")},
},
),
migrations.CreateModel(
name="GatheringAnnouncement",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("title", models.CharField(max_length=255)),
("content", models.TextField()),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"author",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="gathering_announcements",
to=settings.AUTH_USER_MODEL,
),
),
(
"gathering",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="announcements",
to="web.gathering",
),
),
],
options={
"verbose_name": "Gathering Announcement",
"verbose_name_plural": "Gathering Announcements",
"ordering": ["-created_at"],
},
),
]
Loading