Skip to content
Open
8 changes: 7 additions & 1 deletion app/controllers/main/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from app.models.courseInstructor import CourseInstructor
from app.models.backgroundCheckType import BackgroundCheckType

from app.logic.events import getUpcomingEventsForUser, getParticipatedEventsForUser, getTrainingEvents, getEventRsvpCountsForTerm, getUpcomingVolunteerOpportunitiesCount, getVolunteerOpportunities, getBonnerEvents, getCeltsLabor, getEngagementEvents
from app.logic.events import getUpcomingEventsForUser, getParticipatedEventsForUser, getTrainingEvents, getEventRsvpCountsForTerm, getUpcomingVolunteerOpportunitiesCount, getVolunteerOpportunities, getBonnerEvents, getCeltsLabor, getEngagementEvents, getpastVolunteerOpportunitiesCount
from app.logic.transcript import *
from app.logic.loginManager import logout
from app.logic.searchUsers import searchUsers
Expand Down Expand Up @@ -92,6 +92,7 @@ def events(selectedTerm, activeTab, programID):
currentEventRsvpAmount = getEventRsvpCountsForTerm(term)
volunteerOpportunities = getVolunteerOpportunities(term)
countUpcomingVolunteerOpportunities = getUpcomingVolunteerOpportunitiesCount(term, currentTime)
countPastVolunteerOpportunities = getpastVolunteerOpportunitiesCount(term, currentTime)
trainingEvents = getTrainingEvents(term, g.current_user)
engagementEvents = getEngagementEvents(term)
bonnerEvents = getBonnerEvents(term)
Expand All @@ -109,6 +110,8 @@ def events(selectedTerm, activeTab, programID):

# Get the count of all term events for each category to display in the event list page.
volunteerOpportunitiesCount: int = len(studentEvents)
countUpcomingVolunteerOpportunitiesCount: int = len(countUpcomingVolunteerOpportunities)
countPastVolunteerOpportunitiesCount: int = len(countPastVolunteerOpportunities)
trainingEventsCount: int = len(trainingEvents)
engagementEventsCount: int = len(engagementEvents)
bonnerEventsCount: int = len(bonnerEvents)
Expand All @@ -133,6 +136,8 @@ def events(selectedTerm, activeTab, programID):
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
"volunteerOpportunitiesCount": volunteerOpportunitiesCount,
"countPastVolunteerOpportunitiesCount": countPastVolunteerOpportunitiesCount,
"countUpcomingVolunteerOpportunitiesCount": countUpcomingVolunteerOpportunitiesCount,
"trainingEventsCount": trainingEventsCount,
"engagementEventsCount": engagementEventsCount,
"bonnerEventsCount": bonnerEventsCount,
Expand All @@ -155,6 +160,7 @@ def events(selectedTerm, activeTab, programID):
programID = int(programID),
managersProgramDict = managersProgramDict,
countUpcomingVolunteerOpportunities = countUpcomingVolunteerOpportunities,
countPastVolunteerOpportunities = countPastVolunteerOpportunities,
toggleState = toggleState,
)

Expand Down
32 changes: 29 additions & 3 deletions app/logic/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ def getEngagementEvents(term):
.execute())
return engagementEvents

def getUpcomingVolunteerOpportunitiesCount(term, currentTime):
def getUpcomingVolunteerOpportunitiesCount(term, currentDate):
"""
Return a count of all upcoming events for each volunteer opportunitiesprogram.
"""
Expand All @@ -265,8 +265,8 @@ def getUpcomingVolunteerOpportunitiesCount(term, currentTime):
(Event.deletionDate.is_null(True)) &
(Event.isService == True) &
((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) &
((Event.startDate > currentTime) |
((Event.startDate == currentTime) & (Event.timeEnd >= currentTime))) &
((Event.startDate > currentDate) |
((Event.startDate == currentDate) & (Event.timeEnd >= currentDate))) &
(Event.isCanceled == False)
)
.group_by(Program.id)
Expand All @@ -277,6 +277,32 @@ def getUpcomingVolunteerOpportunitiesCount(term, currentTime):
programCountDict[programCount.id] = programCount.eventCount
return programCountDict

def getpastVolunteerOpportunitiesCount(term, currentDate):
"""
Return a count of all past events for each volunteer opportunities program.
"""

pastCount = (
Program
.select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
.join(Event, on=(Program.id == Event.program_id))
.where(
(Event.term == term) &
(Event.deletionDate.is_null(True)) &
(Event.isService == True) &
((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) &
((Event.startDate < currentDate) |
((Event.startDate == currentDate) & (Event.timeStart <= currentDate))) &
(Event.isCanceled == False)
)
.group_by(Program.id)
)

programCountDict = {}
for programCount in pastCount:
programCountDict[programCount.id] = programCount.eventCount
return programCountDict

def getTrainingEvents(term, user):
"""
The allTrainingsEvent query is designed to select and count eventId's after grouping them
Expand Down
27 changes: 25 additions & 2 deletions app/static/js/eventList.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,39 @@ function updateIndicatorCounts(isChecked){
},
success: function(eventsCount) {
const volunteerOpportunitiesCount = Number(eventsCount.volunteerOpportunitiesCount);
const upcomingVolunteerCount = Number(eventsCount.countUpcomingVolunteerOpportunitiesCount);
const trainingEventsCount = Number(eventsCount.trainingEventsCount);
const engagementEventsCount = Number(eventsCount.engagementEventsCount);
const bonnerEventsCount = Number(eventsCount.bonnerEventsCount);
const celtsLaborCount = Number(eventsCount.celtsLaborCount);
const toggleStatus = eventsCount.toggleStatus;

$("#viewPastEventsToggle").prop(toggleStatus, true);

// Update tab labels with event counts:
// - When toggle is ON, show total volunteer opportunities (upcoming + past)
// - When toggle is OFF, show upcoming volunteer opportunities only
// - For all tabs, show counts only if greater than zero; otherwise show the label without a count

// use ternary operators to populate the tab with a number if there are events, and clear the count if there are none
volunteerOpportunitiesCount > 0 ? $("#volunteerOpportunities").html(`Volunteer Opportunities (${volunteerOpportunitiesCount})`) : $("#volunteerOpportunities").html(`Volunteer Opportunities`)
if (toggleStatus === "checked") {
// Toggle ON: show total (upcoming + past)
if (volunteerOpportunitiesCount > 0) {
$("#volunteerOpportunities").html(
`Volunteer Opportunities (${volunteerOpportunitiesCount})`
);
} else {
$("#volunteerOpportunities").html(`Volunteer Opportunities`);
}
} else {
// Toggle OFF: show upcoming only
if (upcomingVolunteerCount > 0) {
$("#volunteerOpportunities").html(
`Volunteer Opportunities (${upcomingVolunteerCount})`
);
} else {
$("#volunteerOpportunities").html(`Volunteer Opportunities`);
}
}
trainingEventsCount > 0 ? $("#trainingEvents").html(`Trainings (${trainingEventsCount})`) : $("#trainingEvents").html(`Trainings`)
engagementEventsCount > 0 ? $("#engagementEvents").html(`Education and Engagement (${engagementEventsCount})`) : $("#engagementEvents").html('Education and Engagement')
bonnerEventsCount > 0 ? $("#bonnerScholarsEvents").html(`Bonner Scholars (${bonnerEventsCount})`) : $("#bonnerScholarsEvents").html(`Bonner Scholars`)
Expand Down
62 changes: 38 additions & 24 deletions app/templates/events/eventList.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ <h1 class="text-center">Events List for {{selectedTerm.description}}</h1>
<ul class="nav nav-tabs nav-fill mx-2 mb-2" id="pills-tab" role="tablist">
<li class="col-md-2 col-12 nav-item" role="presentation">
<button class="nav-link {{'active' if activeTab == 'volunteerOpportunities' else ''}}" id="volunteerOpportunities"
data-bs-toggle="pill" data-bs-target="#pills-volunteer-opportunities" type="button" role="tab" aria-controls="pills-volunteer-opportunities" aria-selected="true">Volunteer Opportunities</button>
data-bs-toggle="pill" data-bs-target="#pills-volunteer-opportunities" type="button" role="tab" aria-controls="pills-volunteer-opportunities" aria-selected="true">Volunteer Opportunities </button>
</li>

{% if trainingEvents and trainingEvents|length %}
<li class="col-md-2 col-12 nav-item" role="presentation">
<button class="nav-link {{'active' if activeTab == 'trainingEvents' else ''}}" id="trainingEvents"
data-bs-toggle="pill" data-bs-target="#pills-training" type="button" role="tab" aria-controls="pills-training" aria-selected="false">Training</button>
data-bs-toggle="pill" data-bs-target="#pills-training" type="button" role="tab" aria-controls="pills-training" aria-selected="false">Trainings</button>
</li>
{% endif %}

Expand Down Expand Up @@ -175,29 +175,29 @@ <h1 class="text-center">Events List for {{selectedTerm.description}}</h1>
<p class="m-2 fs-4">There are no {{typemap[type]}} events for this term.</p>
{% endif %}
{% endmacro %}

<div class="tab-content" id="pills-tabContent">
<div class="tab-pane fade show {{'active' if activeTab == 'volunteerOpportunities' else ''}}" id="pills-volunteer-opportunities" role="tabpanel" aria-labelledby="pills-volunteer-opportunities-tab">
{% if volunteerOpportunities %}
<div class="accordion" id="categoryAccordion">
{% for program,events in volunteerOpportunities.items() %}
<div class="accordion-item">
<div class="accordion-header" id="accordion__header_{{program}}">
<button class="accordion-button {{'show' if programID == program.id else 'collapsed'}}"
type="button"
data-bs-toggle="collapse"
data-bs-target="#accordion__body_{{program}}_num_{{ loop.index }}"
aria-expanded="true"
aria-controls="accordion__body_{{program}}">
{{program.programName}}
{% if program.id not in countUpcomingVolunteerOpportunities%}
<span class="ms-auto fw-light fst-italic">0 upcoming events</span>
{% else %}
<span class="ms-auto fw-light fst-italic">{{countUpcomingVolunteerOpportunities[program.id]}} upcoming event{% if countUpcomingVolunteerOpportunities[program.id] > 1 %}s{% endif %}</span>
{% endif %}
<div class="tab-pane fade show {{ 'active' if activeTab == 'volunteerOpportunities' else '' }}"
id="pills-volunteer-opportunities"
role="tabpanel"
aria-labelledby="pills-volunteer-opportunities-tab">

</button>
</div>
{% if volunteerOpportunities %}
<div class="accordion" id="categoryAccordion">
{% for program, events in volunteerOpportunities.items() %}
<div class="accordion-item">
<div class="accordion-header" id="accordion__header_{{ program }}">
<button class="accordion-button {{ 'show' if programID == program.id else 'collapsed' }}"
type="button"
data-bs-toggle="collapse"
data-bs-target="#accordion__body_{{ program }}_num_{{ loop.index }}"
aria-expanded="true"
aria-controls="accordion__body_{{ program }}">
{{ program.programName }}
{% set upcoming = countUpcomingVolunteerOpportunities.get(program.id, 0) %}
{% set past = countPastVolunteerOpportunities.get(program.id, 0) %}
<span class="ms-auto fw-light fst-italic"> {{ upcoming }} upcoming event{% if upcoming != 1 %}s{% endif %} and {{ past }} past event{% if past != 1 %}s{% endif %} </span>
</button>
</div>
<div id="accordion__body_{{program}}_num_{{ loop.index }}"
class="accordion-collapse collapse {{'show' if programID == program.id else ''}}"
aria-labelledby="accordion__header_{{program}}"
Expand All @@ -208,7 +208,21 @@ <h1 class="text-center">Events List for {{selectedTerm.description}}</h1>
{% endfor %}
</div>
{% else %}
<p class="m-2 fs-4">There are no events that earn service for this term.</p>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th scope="col">Program</th>
<th scope="col">Event Name</th>
<th scope="col">Date</th>
<th scope="col">Time</th>
<th scope="col">Location</th>
<th scope="col"></th>
</tr>
</thead>
</table>
</div>
<td colspan="{{colspan_value}}" class="p-3 no-upcoming">There are no upcoming events for this program</td>
{% endif %}
</div>
<div class="tab-pane fade show" id="pills-training" role="tabpanel" aria-labelledby="pills-training-tab">
Expand Down
87 changes: 86 additions & 1 deletion tests/code/test_event_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from app.models.term import Term
from app.models.user import User
from app.models.eventViews import EventView
from app.logic.events import getVolunteerOpportunities, getEngagementEvents, getTrainingEvents, getBonnerEvents, getCeltsLabor, addEventView, getUpcomingVolunteerOpportunitiesCount
from app.logic.events import getVolunteerOpportunities, getEngagementEvents, getTrainingEvents, getBonnerEvents, getCeltsLabor, addEventView, getUpcomingVolunteerOpportunitiesCount, getpastVolunteerOpportunitiesCount

@pytest.mark.integration
@pytest.fixture
Expand Down Expand Up @@ -154,6 +154,91 @@ def test_getUpcomingVolunteerOpportunitiesCount():

transaction.rollback()

@pytest.mark.integration
def test_getPastVolunteerOpportunitiesCount():
with mainDB.atomic() as transaction:
testDate = datetime.strptime("2021-08-01 05:00", "%Y-%m-%d %H:%M")
currentTestTerm = Term.get_by_id(5)

# Move any existing term-5 events into the future
Event.update(startDate=date(2021, 8, 5)).where(Event.term_id == 5).execute()

# Past Volunteer Opportunity (AGP)
pastAgpEvent = Event.create(
name="Test past AGP event",
term=currentTestTerm,
description="Past volunteer opportunity (AGP).",
timeStart="03:00:00",
timeEnd="04:00:00",
location="Mars",
isTraining=False,
isService=True,
startDate="2021-07-31",
program=3
)

# Past Volunteer Opportunity (same day, before test time)
Event.create(
name="Test same-day past AGP event",
term=currentTestTerm,
description="Same day but earlier time.",
timeStart="04:00:00",
timeEnd="04:30:00",
location="Venus",
isTraining=False,
isService=True,
startDate="2021-08-01",
program=3
)

# Future Volunteer Opportunity (should NOT be counted)
Event.create(
name="Test future AGP event",
term=currentTestTerm,
description="Future volunteer opportunity.",
timeStart="06:00:00",
timeEnd="07:00:00",
location="Moon",
isTraining=False,
isService=True,
startDate="2021-08-02",
program=3
)

# Verify two past AGP events
pastVolunteerOpportunities = getpastVolunteerOpportunitiesCount(
currentTestTerm, testDate
)
assert pastVolunteerOpportunities == {3: 2}

# Cancel one past event → should reduce count
Event.update(isCanceled=True).where(Event.id == pastAgpEvent.id).execute()
pastVolunteerOpportunities = getpastVolunteerOpportunitiesCount(
currentTestTerm, testDate
)
assert pastVolunteerOpportunities == {3: 1}

# Create past event for another program (Buddies)
pastBuddiesEvent = Event.create(
name="Test past Buddies event",
term=currentTestTerm,
description="Past volunteer opportunity (Buddies).",
timeStart="02:00:00",
timeEnd="03:00:00",
location="Earth",
isTraining=False,
isService=True,
startDate="2021-07-30",
program=2
)

pastVolunteerOpportunities = getpastVolunteerOpportunitiesCount(
currentTestTerm, testDate
)
assert pastVolunteerOpportunities == {2: 1, 3: 1}

transaction.rollback()

@pytest.mark.integration
def test_getTrainingEvents(training_events):
with mainDB.atomic() as transaction:
Expand Down