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 %}
- + Add new Event @@ -24,11 +28,11 @@

Events

{% endif %}
+
{% 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 %}
+ +

Past Events

-
- {% 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}