diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..39651e84
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,8 @@
+{
+ "python-envs.defaultEnvManager": "ms-python.python:pipenv",
+ "python-envs.pythonProjects": [],
+ "python.linting.pylintEnabled": false,
+ "python.linting.flake8Enabled": true,
+ "python.linting.enabled": true,
+ "python.formatting.provider": "black"
+}
\ No newline at end of file
diff --git a/apps/events/static/cosmos_events/js/events.js b/apps/events/static/cosmos_events/js/events.js
index c345968c..7bcbded4 100644
--- a/apps/events/static/cosmos_events/js/events.js
+++ b/apps/events/static/cosmos_events/js/events.js
@@ -17,4 +17,56 @@
// flatpickr("#id_end_date_time", { enableTime: true, dateFormat: "Y-m-d H:i", time_24hr: true, });
// }
// });
-// }
\ No newline at end of file
+// }
+
+document.addEventListener("DOMContentLoaded", () => {
+ const container = document.getElementById("past-events-container");
+ const loader = document.getElementById("past-events-loader");
+
+ if (!container) return;
+
+ let page = parseInt(container.dataset.nextPage, 10);
+ let hasNext = container.dataset.hasNext === "true";
+ let loading = false;
+ let failed = false;
+
+ function loadMorePastEvents() {
+ if (!hasNext || loading || failed) return;
+
+ loading = true;
+ loader.style.display = "block";
+
+ fetch(`/events/past/?page=${page}`, {
+ headers: { "X-Requested-With": "XMLHttpRequest" }
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error("Server error");
+ }
+ return response.json();
+ })
+ .then(data => {
+ container.insertAdjacentHTML("beforeend", data.html);
+ hasNext = data.has_next;
+ page += 1;
+ })
+ .catch(err => {
+ console.error("Infinite scroll failed:", err);
+ failed = true; // 🔒 STOP retry loop
+ })
+ .finally(() => {
+ loading = false;
+ loader.style.display = "none";
+ });
+ }
+
+ window.addEventListener("scroll", () => {
+ const nearBottom =
+ window.innerHeight + window.scrollY >=
+ document.body.offsetHeight - 300;
+
+ if (nearBottom) {
+ loadMorePastEvents();
+ }
+ });
+});
\ No newline at end of file
diff --git a/apps/events/templates/events/_past_events_cards.html b/apps/events/templates/events/_past_events_cards.html
new file mode 100644
index 00000000..96128b7d
--- /dev/null
+++ b/apps/events/templates/events/_past_events_cards.html
@@ -0,0 +1,34 @@
+{% load thumbnail %}
+
+{% for event in events_list_past %}
+
+
+
+ {% thumbnail event.image "600" crop="center" as im %}
+

+ {% endthumbnail %}
+
+
+
{{ event.name }}
+
{{ event.lead }}
+
+
+
+
+
+{% endfor %}
\ No newline at end of file
diff --git a/apps/events/templates/events/events_list.html b/apps/events/templates/events/events_list.html
index 16b743ac..edd96c0d 100644
--- a/apps/events/templates/events/events_list.html
+++ b/apps/events/templates/events/events_list.html
@@ -2,13 +2,16 @@
{% load crispy_forms_tags %}
{% load thumbnail %}
+{% load static %}
-{% block title %}Events{% endblock title%}
+{% block title %}Events{% endblock title %}
{% block content %}
+
+
Events
@@ -16,7 +19,8 @@
Events
{% if perms.events.add_event %}
+
{% for event in events_list %}
- {# transform is added so that stretched link does not go into footer #}
{% thumbnail event.image "600" crop="center" as im %}

@@ -37,67 +41,55 @@
Events
{{ event.name }}
{{ event.lead }}
-
+
{% endfor %}
+
+
-
- {% for event in events_list_past %}
-
-
- {# transform is added so that stretched link does not go into footer #}
-
- {% thumbnail event.image "600" crop="center" as im %}
-

- {% endthumbnail %}
-
-
-
{{ event.name }}
-
{{ event.lead }}
-
-
-
-
-
- {% endfor %}
+
+
+
+ {% include "events/_past_events_cards.html" %}
+
+
+
-{% endblock content %}
\ No newline at end of file
+{% endblock content %}
+
+{% block extra_js %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/apps/events/urls.py b/apps/events/urls.py
index 7b0087e0..a8883b0f 100644
--- a/apps/events/urls.py
+++ b/apps/events/urls.py
@@ -13,4 +13,5 @@
path("
/update/", apps.events.views.EventUpdate.as_view(), name="events-update"),
path("/delete/", apps.events.views.EventDelete.as_view(), name="events-delete"),
path("carousel/", apps.events.views.event_carousel, name="event-carousel"),
+ path("past/", apps.events.views.past_events_page, name="events-past-page"),
]
diff --git a/apps/events/views.py b/apps/events/views.py
index 94e64713..760911b4 100644
--- a/apps/events/views.py
+++ b/apps/events/views.py
@@ -3,8 +3,12 @@
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.sites.models import Site
from django.core.exceptions import PermissionDenied
+from django.core.paginator import EmptyPage, Paginator
+from django.http import Http404, JsonResponse
from django.shortcuts import get_object_or_404, render
+from django.template.loader import render_to_string
from django.urls import reverse_lazy
+from django.utils import timezone
from django.views.generic import CreateView, DeleteView, UpdateView
from django_ical.views import ICalFeed
@@ -12,33 +16,67 @@
from apps.events.forms import EventForm
from apps.events.models import Event
+PAST_EVENTS_PAGE_SIZE = 20
+
def events_list(request):
+ now = timezone.now()
+
if request.user.is_authenticated:
- events_list = (
- Event.objects.order_by("start_date_time").filter(end_date_time__gte=datetime.datetime.today()).all()
- )
- events_list_past = (
- Event.objects.order_by("-start_date_time").filter(end_date_time__lte=datetime.datetime.today()).all()
- )
+ upcoming_events = Event.objects.order_by("start_date_time").filter(end_date_time__gte=now)
+ past_events = Event.objects.order_by("-start_date_time").filter(end_date_time__lte=now)
else:
- events_list = (
- Event.objects.filter(member_only=False)
- .order_by("start_date_time")
- .filter(end_date_time__gte=datetime.datetime.today())
+ upcoming_events = (
+ Event.objects.filter(member_only=False).order_by("start_date_time").filter(end_date_time__gte=now)
)
- events_list_past = (
- Event.objects.filter(member_only=False)
- .order_by("-start_date_time")
- .filter(end_date_time__lte=datetime.datetime.today())
+ past_events = (
+ Event.objects.filter(member_only=False).order_by("-start_date_time").filter(end_date_time__lte=now)
)
+
+ paginator = Paginator(past_events, PAST_EVENTS_PAGE_SIZE)
+ first_page = paginator.page(1)
+
context = {
- "events_list": events_list,
- "events_list_past": events_list_past,
+ "events_list": upcoming_events,
+ "events_list_past": first_page.object_list,
+ "past_has_next": first_page.has_next(),
}
return render(request, "events/events_list.html", context)
+def past_events_page(request):
+ if request.headers.get("x-requested-with") != "XMLHttpRequest":
+ raise Http404()
+
+ now = timezone.now()
+
+ past_events = Event.objects.filter(
+ end_date_time__lte=now,
+ member_only=False if not request.user.is_authenticated else None,
+ ).order_by("-start_date_time")
+
+ paginator = Paginator(past_events, PAST_EVENTS_PAGE_SIZE)
+ page_number = request.GET.get("page", 1)
+
+ try:
+ page_obj = paginator.page(page_number)
+ except EmptyPage:
+ return JsonResponse({"html": "", "has_next": False})
+
+ html = render_to_string(
+ "events/_past_events_cards.html",
+ {"events_list_past": page_obj.object_list},
+ request=request,
+ )
+
+ return JsonResponse(
+ {
+ "html": html,
+ "has_next": page_obj.has_next(),
+ }
+ )
+
+
def event_view(request, pk):
event = get_object_or_404(Event, pk=pk)
context = {"event": event}