Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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"
}
54 changes: 53 additions & 1 deletion apps/events/static/cosmos_events/js/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,56 @@
// flatpickr("#id_end_date_time", { enableTime: true, dateFormat: "Y-m-d H:i", time_24hr: true, });
// }
// });
// }
// }

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();
}
});
});
34 changes: 34 additions & 0 deletions apps/events/templates/events/_past_events_cards.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{% load thumbnail %}

{% for event in events_list_past %}
<div class="col">
<div class="card h-100">
<div class="ratio ratio-16x9">
{% thumbnail event.image "600" crop="center" as im %}
<img class="card-img-top events-image" src="{{ im.url }}">
{% endthumbnail %}
</div>
<div class="card-body">
<h5 class="card-title">{{ event.name }}</h5>
<p class="event-truncate">{{ event.lead }}</p>
<a class="stretched-link"
href="{% url 'cosmos_events:events-view' event.id %}"></a>
</div>
<div class="card-footer">
{% if perms.events.change_event and perms.events.delete_event %}
<div class="float-end">
<a class="btn p-0 btn-over-stretched"
href="{% url 'cosmos_events:events-update' event.id %}">
<i class="bi bi-pencil-square"></i>
</a>
<a class="btn p-0 btn-over-stretched"
href="{% url 'cosmos_events:events-delete' event.id %}">
<i class="bi bi-trash"></i>
</a>
</div>
{% endif %}
<small class="text-muted">{{ event.start_date_time }}</small>
</div>
</div>
</div>
{% endfor %}
78 changes: 35 additions & 43 deletions apps/events/templates/events/events_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,37 @@

{% load crispy_forms_tags %}
{% load thumbnail %}
{% load static %}

{% block title %}Events{% endblock title%}
{% block title %}Events{% endblock title %}

{% block content %}

<div class="container">
<div class="w-75 mx-auto">

<!-- Upcoming events -->
<div class="row">
<div class="col">
<h2>Events</h2>
</div>
<div class="col-md-3 py-1">
{% if perms.events.add_event %}
<div class="float-end">
<a class="btn btn-cosmos-primary" href="{% url 'cosmos_events:events-create' %}">
<a class="btn btn-cosmos-primary"
href="{% url 'cosmos_events:events-create' %}">
<i class="bi bi-plus-lg"></i>
Add new Event
</a>
</div>
{% endif %}
</div>
</div>

<div class="row row-cols-1 row-cols-md-3 g-2 mt-2" id="EventGrid">
{% for event in events_list %}
<div class="col">
<div class="card h-100">
{# transform is added so that stretched link does not go into footer #}
<div class="ratio ratio-16x9">
{% thumbnail event.image "600" crop="center" as im %}
<img class="card-img-top events-image" src="{{ im.url }}">
Expand All @@ -37,67 +41,55 @@ <h2>Events</h2>
<div class="card-body">
<h5 class="card-title">{{ event.name }}</h5>
<p class="event-truncate">{{ event.lead }}</p>
<a class="stretched-link" href="{% url 'cosmos_events:events-view' event.id %}"></a>
<a class="stretched-link"
href="{% url 'cosmos_events:events-view' event.id %}"></a>
</div>
<div class="card-footer">
{% if perms.events.change_event and perms.events.delete_event %}
<div class="float-end">
<a class="btn p-0 btn-over-stretched" href="{% url 'cosmos_events:events-update' event.id %}">
<a class="btn p-0 btn-over-stretched"
href="{% url 'cosmos_events:events-update' event.id %}">
<i class="bi bi-pencil-square"></i>
</a>
<a class="btn p-0 btn-over-stretched" href="{% url 'cosmos_events:events-delete' event.id %}">
<a class="btn p-0 btn-over-stretched"
href="{% url 'cosmos_events:events-delete' event.id %}">
<i class="bi bi-trash"></i>
</a>
</div>
{% endif %}
<small class="text-muted">
{{ event.start_date_time }}
</small>
<small class="text-muted">{{ event.start_date_time }}</small>
</div>
</div>
</div>
{% endfor %}
</div>

<!-- Past events -->
<div class="row mt-5">
<div class="col">
<h2>Past Events</h2>
</div>
</div>
<div class="row row-cols-1 row-cols-md-3 g-2 mt-2" id="EventGrid">
{% for event in events_list_past %}
<div class="col">
<div class="card h-100">
{# transform is added so that stretched link does not go into footer #}
<div class="ratio ratio-16x9">
{% thumbnail event.image "600" crop="center" as im %}
<img class="card-img-top events-image" src="{{ im.url }}">
{% endthumbnail %}
</div>
<div class="card-body">
<h5 class="card-title">{{ event.name }}</h5>
<p class="event-truncate">{{ event.lead }}</p>
<a class="stretched-link" href="{% url 'cosmos_events:events-view' event.id %}"></a>
</div>
<div class="card-footer">
{% if perms.events.change_event and perms.events.delete_event %}
<div class="float-end">
<a class="btn p-0 btn-over-stretched" href="{% url 'cosmos_events:events-update' event.id %}">
<i class="bi bi-pencil-square"></i>
</a>
<a class="btn p-0 btn-over-stretched" href="{% url 'cosmos_events:events-delete' event.id %}">
<i class="bi bi-trash"></i>
</a>
</div>
{% endif %}
<small class="text-muted">
{{ event.start_date_time }}
</small>
</div>
</div>
</div>
{% endfor %}

<div class="row row-cols-1 row-cols-md-3 g-2 mt-2"
id="past-events-container"
data-next-page="2"
data-has-next="{{ past_has_next|yesno:'true,false' }}">

{% include "events/_past_events_cards.html" %}
</div>

<div class="text-center my-4"
id="past-events-loader"
style="display:none;">
<div class="spinner-border"></div>
</div>

</div>
</div>

{% endblock content %}
{% endblock content %}

{% block extra_js %}
<script src="{% static 'js/events.js' %}"></script>
{% endblock %}
1 change: 1 addition & 0 deletions apps/events/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@
path("<int:pk>/update/", apps.events.views.EventUpdate.as_view(), name="events-update"),
path("<int:pk>/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"),
]
70 changes: 54 additions & 16 deletions apps/events/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,80 @@
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

from apps.core.models import News
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}
Expand Down
Loading