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
61 changes: 60 additions & 1 deletion coderdojochi/old_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
from django.db.models import IntegerField
from django.db.models import When
from django.http import HttpResponse
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import redirect
from django.shortcuts import render
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from django.views.decorators.cache import never_cache
Expand Down Expand Up @@ -236,9 +238,16 @@ def cdc_admin(request, template_name="admin.html"):
output_field=IntegerField(),
),
)
.order_by("-start_date")
.order_by("-start_date")[:5]
)

# Check if there are more sessions available (within the last 365 days)
one_year_ago = timezone.now() - timedelta(days=365)
total_recent_sessions_count = Session.objects.filter(
start_date__gte=one_year_ago
).count()
has_more_sessions = total_recent_sessions_count > 5

meetings = (
Meeting.objects.select_related()
.annotate(
Expand Down Expand Up @@ -298,6 +307,7 @@ def cdc_admin(request, template_name="admin.html"):
# 'past_sessions': past_sessions,
# 'past_sessions_count': past_sessions_count,
"sessions": sessions,
"has_more_sessions": has_more_sessions,
"total_checked_in_orders_count": total_checked_in_orders_count,
"total_past_orders_count": total_past_orders_count,
# 'upcoming_meetings': upcoming_meetings,
Expand All @@ -308,6 +318,55 @@ def cdc_admin(request, template_name="admin.html"):
)


@login_required
def load_more_sessions(request):
"""AJAX endpoint to load additional sessions for admin page"""
if not request.user.is_staff:
return JsonResponse({"error": "Permission denied"}, status=403)

# Get offset from request, default to 5 (after the initial 5 sessions)
offset = int(request.GET.get("offset", 5))
limit = int(request.GET.get("limit", 10)) # Load 10 more at a time

# Limit to sessions within the last 365 days
one_year_ago = timezone.now() - timedelta(days=365)

sessions = (
Session.objects.select_related()
.annotate(
num_orders=Count("order"),
num_attended=Count(
Case(When(order__check_in__isnull=False, then=1)),
),
is_future=Case(
When(start_date__gte=timezone.now(), then=1),
default=0,
output_field=IntegerField(),
),
)
.filter(start_date__gte=one_year_ago)
.order_by("-start_date")[offset:offset+limit]
)

# Render the sessions to HTML
html = render_to_string(
"dashboard/admin_sessions_rows.html",
{"sessions": sessions},
request=request
)

# Check if there are more sessions to load
has_more = Session.objects.filter(
start_date__gte=one_year_ago
).count() > offset + limit

return JsonResponse({
"html": html,
"has_more": has_more,
"next_offset": offset + limit
})


@login_required
@never_cache
def session_stats(request, pk, template_name="session_stats.html"):
Expand Down
73 changes: 62 additions & 11 deletions coderdojochi/templates/dashboard/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ <h2 class="title text-left">Classes <span class="badge">{{ sessions|length }}</s
<th class="text-center"><abbr title="Capacity">C</abbr></th>
</tr>
</thead>
<tbody>
<tbody id="sessions-tbody">
{% for session in sessions %}
<tr class="
{% if session.is_active == False %}
Expand All @@ -183,9 +183,6 @@ <h2 class="title text-left">Classes <span class="badge">{{ sessions|length }}</s
success
{% endif %}
{% endif %}
{% if forloop.counter > 5 %}
collapse session-collapse
{% endif %}
"
>
<td>{{ session.start_date|date:"M j, Y" }}</td>
Expand Down Expand Up @@ -258,13 +255,14 @@ <h2 class="title text-left">Classes <span class="badge">{{ sessions|length }}</s
</tr>
{% endfor %}
</tbody>
{% if sessions.count > 5 %}
<tfoot>
<tr>
<td colspan="100" class="text-center">
<button class="button" type="button" data-toggle="collapse" data-target=".session-collapse">Show/Hide</button>
</td>
</tr>
{% if has_more_sessions %}
<tfoot id="load-more-section">
<tr>
<td colspan="100" class="text-center">
<button id="load-more-btn" class="button" type="button">Load More Classes</button>
<div id="loading-indicator" style="display: none;">Loading...</div>
</td>
</tr>
</tfoot>
{% endif %}
</table>
Expand Down Expand Up @@ -372,6 +370,59 @@ <h2 class="title text-left">Meetings <span class="badge">{{ meetings|length }}</
<p>No upcoming meetings.</p>
{% endif %}
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
const loadMoreBtn = document.getElementById('load-more-btn');
const loadingIndicator = document.getElementById('loading-indicator');
const sessionTable = document.getElementById('sessions-tbody');
let currentOffset = 5; // Start after the initial 5 sessions

if (loadMoreBtn && sessionTable) {
loadMoreBtn.addEventListener('click', function() {
// Show loading indicator and hide button
loadMoreBtn.style.display = 'none';
loadingIndicator.style.display = 'block';

// Make AJAX request
fetch(`/admin/load-more-sessions/?offset=${currentOffset}&limit=10`)
.then(response => response.json())
.then(data => {
if (data.error) {
alert('Error loading more sessions: ' + data.error);
return;
}

// Append new session rows
sessionTable.insertAdjacentHTML('beforeend', data.html);

// Update offset for next request
currentOffset = data.next_offset;

// Hide loading indicator
loadingIndicator.style.display = 'none';

// Show button again if there are more sessions
if (data.has_more) {
loadMoreBtn.style.display = 'block';
} else {
// Replace button text to indicate no more sessions
const loadMoreSection = document.getElementById('load-more-section');
loadMoreSection.innerHTML = '<tr><td colspan="100" class="text-center text-muted">All sessions from the last 365 days have been loaded</td></tr>';
}
})
.catch(error => {
console.error('Error:', error);
alert('Error loading more sessions. Please try again.');
// Reset UI state
loadingIndicator.style.display = 'none';
loadMoreBtn.style.display = 'block';
});
});
}
});
</script>

{% endblock %}

{% block footer_base %}{% endblock %}
80 changes: 80 additions & 0 deletions coderdojochi/templates/dashboard/admin_sessions_rows.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{% for session in sessions %}
<tr class="
{% if session.is_active == False %}
danger
{% else %}
{% if session.is_future == 1 %}
success
{% endif %}
{% endif %}
"
>
<td>{{ session.start_date|date:"M j, Y" }}</td>
<td class="text-right">{{ session.start_date|time:"H:i" }}</td>
<td class="text-right">{{ session.end_date|time:"H:i" }}</td>
<td class="hidden-xs hidden-sm"><a href="{{ session.get_absolute_url }}">{{ session.course.title }}</a></td>
<td>{{ session.instructor }}</td>
<td class="hidden-xs hidden-sm">{{ session.location.name }}</td>
<td class="text-center hidden-xs hidden-sm">
{% if session.announced_date_guardians|yesno:'yes,no' == 'yes' %}
<i class="fa fa-calendar-check-o" title="{{ session.announced_date_guardians }}" aria-hidden="true"></i>
{% endif %}
</td>
<td class="text-right">
{% if not session.external_enrollment_url %}
{{ session.num_attended|default:'-' }}
{% else %}
&times;
{% endif %}
</td>
<td class="text-right">
{% if not session.external_enrollment_url %}
{{ session.num_orders|default:'-' }}
{% else %}
&times;
{% endif %}
</td>
<td class="text-right">
{% if not session.external_enrollment_url %}
{{ session.capacity|default:'-' }}
{% else %}
&times;
{% endif %}
</td>
<td class="text-center">
{% if not session.external_enrollment_url %}
<a href="{% url 'student-check-in' session.id %}"><span class="glyphicon glyphicon-check"></span></a>
{% else %}
&times;
{% endif %}
</td>
<td class="text-center">
{% if not session.external_enrollment_url %}
{{ session.get_checked_in_mentor_orders|length|default:'-' }}
{% else %}
&times;
{% endif %}
</td>
<td class="text-center">
{% if not session.external_enrollment_url %}
{{ session.get_mentor_orders|length|default:'-' }}
{% else %}
&times;
{% endif %}
</td>
<td class="text-center">
{% if not session.external_enrollment_url %}
{{ session.mentor_capacity|default:'-' }}
{% else %}
&times;
{% endif %}
</td>
<td>
{% if not session.external_enrollment_url %}
<a href="{% url 'mentor-check-in' session.id %}"><span class="glyphicon glyphicon-check"></span></a>
{% else %}
&times;
{% endif %}
</td>
</tr>
{% endfor %}
72 changes: 72 additions & 0 deletions coderdojochi/tests/test_admin_load_more_sessions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from datetime import timedelta
import json

from django.contrib.auth import get_user_model
from django.test import TestCase, Client
from django.urls import reverse
from django.utils import timezone

from coderdojochi.models import Session


User = get_user_model()


class TestAdminLoadMoreSessions(TestCase):
def setUp(self):
self.client = Client()
self.staff_user = User.objects.create_user(
username='staff_user',
email='staff@example.com',
is_staff=True
)
self.regular_user = User.objects.create_user(
username='regular_user',
email='user@example.com',
is_staff=False
)

def test_load_more_sessions_requires_staff(self):
"""Test that non-staff users cannot access the endpoint"""
self.client.login(username='regular_user', password='password')
response = self.client.get(reverse('load-more-sessions'))
self.assertEqual(response.status_code, 403)

def test_load_more_sessions_requires_login(self):
"""Test that unauthenticated users cannot access the endpoint"""
response = self.client.get(reverse('load-more-sessions'))
self.assertEqual(response.status_code, 302) # Redirect to login

def test_load_more_sessions_staff_access(self):
"""Test that staff users can access the endpoint"""
self.client.force_login(self.staff_user)
response = self.client.get(reverse('load-more-sessions'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')

def test_load_more_sessions_pagination(self):
"""Test pagination parameters"""
self.client.force_login(self.staff_user)

# Test with custom offset and limit
response = self.client.get(reverse('load-more-sessions'), {
'offset': 10,
'limit': 5
})
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertIn('html', data)
self.assertIn('has_more', data)
self.assertIn('next_offset', data)
self.assertEqual(data['next_offset'], 15)

def test_load_more_sessions_365_day_limit(self):
"""Test that only sessions from last 365 days are included"""
self.client.force_login(self.staff_user)

# This test would require creating Session objects with specific dates
# For now, just verify the endpoint responds correctly
response = self.client.get(reverse('load-more-sessions'))
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertIsInstance(data['has_more'], bool)
2 changes: 2 additions & 0 deletions coderdojochi/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
# Admin
# /admin/
path("", old_views.cdc_admin, name="cdc-admin"),
# /admin/load-more-sessions/
path("load-more-sessions/", old_views.load_more_sessions, name="load-more-sessions"),
path(
"classes/",
include(
Expand Down
4 changes: 2 additions & 2 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.