Skip to content

Commit 2367622

Browse files
committed
Merge branch 'Labor_Attendance_report_1626' of github.com:BCStudentSoftwareDevTeam/celts into Labor_Attendance_report_1626
2 parents 78df4d5 + 368521c commit 2367622

6 files changed

Lines changed: 191 additions & 37 deletions

File tree

app/controllers/main/routes.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from app.models.courseInstructor import CourseInstructor
2727
from app.models.backgroundCheckType import BackgroundCheckType
2828

29-
from app.logic.events import getUpcomingEventsForUser, getParticipatedEventsForUser, getTrainingEvents, getEventRsvpCountsForTerm, getUpcomingVolunteerOpportunitiesCount, getVolunteerOpportunities, getBonnerEvents, getCeltsLabor, getEngagementEvents
29+
from app.logic.events import getUpcomingEventsForUser, getParticipatedEventsForUser, getTrainingEvents, getEventRsvpCountsForTerm, getUpcomingVolunteerOpportunitiesCount, getVolunteerOpportunities, getBonnerEvents, getCeltsLabor, getEngagementEvents, getPastVolunteerOpportunitiesCount
3030
from app.logic.transcript import *
3131
from app.logic.loginManager import logout
3232
from app.logic.searchUsers import searchUsers
@@ -92,6 +92,7 @@ def events(selectedTerm, activeTab, programID):
9292
currentEventRsvpAmount = getEventRsvpCountsForTerm(term)
9393
volunteerOpportunities = getVolunteerOpportunities(term)
9494
countUpcomingVolunteerOpportunities = getUpcomingVolunteerOpportunitiesCount(term, currentTime)
95+
countPastVolunteerOpportunities = getPastVolunteerOpportunitiesCount(term, currentTime)
9596
trainingEvents = getTrainingEvents(term, g.current_user)
9697
engagementEvents = getEngagementEvents(term)
9798
bonnerEvents = getBonnerEvents(term)
@@ -109,6 +110,8 @@ def events(selectedTerm, activeTab, programID):
109110

110111
# Get the count of all term events for each category to display in the event list page.
111112
volunteerOpportunitiesCount: int = len(studentEvents)
113+
countUpcomingVolunteerOpportunitiesCount: int = len(countUpcomingVolunteerOpportunities)
114+
countPastVolunteerOpportunitiesCount: int = len(countPastVolunteerOpportunities)
112115
trainingEventsCount: int = len(trainingEvents)
113116
engagementEventsCount: int = len(engagementEvents)
114117
bonnerEventsCount: int = len(bonnerEvents)
@@ -133,6 +136,8 @@ def events(selectedTerm, activeTab, programID):
133136
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
134137
return jsonify({
135138
"volunteerOpportunitiesCount": volunteerOpportunitiesCount,
139+
"countPastVolunteerOpportunitiesCount": countPastVolunteerOpportunitiesCount,
140+
"countUpcomingVolunteerOpportunitiesCount": countUpcomingVolunteerOpportunitiesCount,
136141
"trainingEventsCount": trainingEventsCount,
137142
"engagementEventsCount": engagementEventsCount,
138143
"bonnerEventsCount": bonnerEventsCount,
@@ -155,6 +160,7 @@ def events(selectedTerm, activeTab, programID):
155160
programID = int(programID),
156161
managersProgramDict = managersProgramDict,
157162
countUpcomingVolunteerOpportunities = countUpcomingVolunteerOpportunities,
163+
countPastVolunteerOpportunities = countPastVolunteerOpportunities,
158164
toggleState = toggleState,
159165
)
160166

app/logic/events.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ def getEngagementEvents(term):
253253
.execute())
254254
return engagementEvents
255255

256-
def getUpcomingVolunteerOpportunitiesCount(term, currentTime):
256+
def getUpcomingVolunteerOpportunitiesCount(term, currentDate):
257257
"""
258258
Return a count of all upcoming events for each volunteer opportunitiesprogram.
259259
"""
@@ -267,8 +267,8 @@ def getUpcomingVolunteerOpportunitiesCount(term, currentTime):
267267
(Event.deletionDate.is_null(True)) &
268268
(Event.isService == True) &
269269
((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) &
270-
((Event.startDate > currentTime) |
271-
((Event.startDate == currentTime) & (Event.timeEnd >= currentTime))) &
270+
((Event.startDate > currentDate) |
271+
((Event.startDate == currentDate) & (Event.timeEnd >= currentDate.time()))) &
272272
(Event.isCanceled == False)
273273
)
274274
.group_by(Program.id)
@@ -279,6 +279,32 @@ def getUpcomingVolunteerOpportunitiesCount(term, currentTime):
279279
programCountDict[programCount.id] = programCount.eventCount
280280
return programCountDict
281281

282+
def getPastVolunteerOpportunitiesCount(term, currentDate):
283+
"""
284+
Return a count of all past events for each volunteer opportunities program.
285+
"""
286+
287+
pastCount = (
288+
Program
289+
.select(Program.id, fn.COUNT(Event.id).alias("eventCount"))
290+
.join(Event, on=(Program.id == Event.program_id))
291+
.where(
292+
(Event.term == term) &
293+
(Event.deletionDate.is_null(True)) &
294+
(Event.isService == True) &
295+
((Event.isLaborOnly == False) | Event.isLaborOnly.is_null(True)) &
296+
((Event.startDate < currentDate) |
297+
((Event.startDate == currentDate) & (Event.timeStart <= currentDate.time()))) &
298+
(Event.isCanceled == False)
299+
)
300+
.group_by(Program.id)
301+
)
302+
303+
programCountDict = {}
304+
for programCount in pastCount:
305+
programCountDict[programCount.id] = programCount.eventCount
306+
return programCountDict
307+
282308
def getTrainingEvents(term, user):
283309
"""
284310
The allTrainingsEvent query is designed to select and count eventId's after grouping them

app/logic/volunteerSpreadsheet.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,15 +227,15 @@ def calculateRetentionRate(fallDict, springDict):
227227
return retentionDict
228228

229229
def laborAttendanceByTerm(term):
230-
full_name = fn.CONCAT(User.firstName, ' ', User.lastName).alias('fullName')
230+
fullName = fn.CONCAT(User.firstName, ' ', User.lastName).alias('fullName')
231231
email = fn.CONCAT(User.username, '@berea.edu').alias('email')
232-
meetings_attended = fn.COUNT(fn.DISTINCT(Event.id)).alias('meetingsAttended')
232+
meetingsAttended = fn.COUNT(fn.DISTINCT(Event.id)).alias('meetingsAttended')
233233
validEvent = (EventParticipant.event == Event.id) & (Event.term == term) & (Event.isLaborOnly == True) & (Event.deletionDate.is_null()) & (Event.isCanceled == False)
234234

235235
laborQuery = ( #so that all Celts Labor students appear even if they didn't attend anything
236236
CeltsLabor
237237
.select(
238-
full_name,User.bnumber,email,meetings_attended)
238+
fullName,User.bnumber,email,meetingsAttended)
239239
.join(User)
240240
.switch(CeltsLabor)
241241
.join(
@@ -255,7 +255,7 @@ def laborAttendanceByTerm(term):
255255

256256
nonLaborQuery = ( #so that non-labor attendees who are not in CeltsLabor also appear
257257
EventParticipant
258-
.select(full_name,User.bnumber,email,meetings_attended)
258+
.select(fullName,User.bnumber,email,meetingsAttended)
259259
.join(User)
260260
.switch(EventParticipant)
261261
.join(

app/static/js/eventList.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,39 @@ function updateIndicatorCounts(isChecked){
8787
},
8888
success: function(eventsCount) {
8989
const volunteerOpportunitiesCount = Number(eventsCount.volunteerOpportunitiesCount);
90+
const upcomingVolunteerCount = Number(eventsCount.countUpcomingVolunteerOpportunitiesCount);
9091
const trainingEventsCount = Number(eventsCount.trainingEventsCount);
9192
const engagementEventsCount = Number(eventsCount.engagementEventsCount);
9293
const bonnerEventsCount = Number(eventsCount.bonnerEventsCount);
9394
const celtsLaborCount = Number(eventsCount.celtsLaborCount);
9495
const toggleStatus = eventsCount.toggleStatus;
9596

96-
$("#viewPastEventsToggle").prop(toggleStatus, true);
97+
$("#viewPastEventsToggle").prop("checked", toggleStatus === "checked");
98+
99+
// Update tab labels with event counts:
100+
// - When toggle is ON, show total volunteer opportunities (upcoming + past)
101+
// - When toggle is OFF, show upcoming volunteer opportunities only
102+
// - For all tabs, show counts only if greater than zero; otherwise show the label without a count
97103

98-
// use ternary operators to populate the tab with a number if there are events, and clear the count if there are none
99-
volunteerOpportunitiesCount > 0 ? $("#volunteerOpportunities").html(`Volunteer Opportunities (${volunteerOpportunitiesCount})`) : $("#volunteerOpportunities").html(`Volunteer Opportunities`)
104+
if (toggleStatus === "checked") {
105+
// Toggle ON: show total (upcoming + past)
106+
if (volunteerOpportunitiesCount > 0) {
107+
$("#volunteerOpportunities").html(
108+
`Volunteer Opportunities (${volunteerOpportunitiesCount})`
109+
);
110+
} else {
111+
$("#volunteerOpportunities").html(`Volunteer Opportunities`);
112+
}
113+
} else {
114+
// Toggle OFF: show upcoming only
115+
if (upcomingVolunteerCount > 0) {
116+
$("#volunteerOpportunities").html(
117+
`Volunteer Opportunities (${upcomingVolunteerCount})`
118+
);
119+
} else {
120+
$("#volunteerOpportunities").html(`Volunteer Opportunities`);
121+
}
122+
}
100123
trainingEventsCount > 0 ? $("#trainingEvents").html(`Trainings (${trainingEventsCount})`) : $("#trainingEvents").html(`Trainings`)
101124
engagementEventsCount > 0 ? $("#engagementEvents").html(`Education and Engagement (${engagementEventsCount})`) : $("#engagementEvents").html('Education and Engagement')
102125
bonnerEventsCount > 0 ? $("#bonnerScholarsEvents").html(`Bonner Scholars (${bonnerEventsCount})`) : $("#bonnerScholarsEvents").html(`Bonner Scholars`)

app/templates/events/eventList.html

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,13 @@ <h1 class="text-center">Events List for {{selectedTerm.description}}</h1>
5151
<ul class="nav nav-tabs nav-fill mx-2 mb-2" id="pills-tab" role="tablist">
5252
<li class="col-md-2 col-12 nav-item" role="presentation">
5353
<button class="nav-link {{'active' if activeTab == 'volunteerOpportunities' else ''}}" id="volunteerOpportunities"
54-
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>
54+
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>
5555
</li>
5656

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

@@ -175,29 +175,29 @@ <h1 class="text-center">Events List for {{selectedTerm.description}}</h1>
175175
<p class="m-2 fs-4">There are no {{typemap[type]}} events for this term.</p>
176176
{% endif %}
177177
{% endmacro %}
178-
179178
<div class="tab-content" id="pills-tabContent">
180-
<div class="tab-pane fade show {{'active' if activeTab == 'volunteerOpportunities' else ''}}" id="pills-volunteer-opportunities" role="tabpanel" aria-labelledby="pills-volunteer-opportunities-tab">
181-
{% if volunteerOpportunities %}
182-
<div class="accordion" id="categoryAccordion">
183-
{% for program,events in volunteerOpportunities.items() %}
184-
<div class="accordion-item">
185-
<div class="accordion-header" id="accordion__header_{{program}}">
186-
<button class="accordion-button {{'show' if programID == program.id else 'collapsed'}}"
187-
type="button"
188-
data-bs-toggle="collapse"
189-
data-bs-target="#accordion__body_{{program}}_num_{{ loop.index }}"
190-
aria-expanded="true"
191-
aria-controls="accordion__body_{{program}}">
192-
{{program.programName}}
193-
{% if program.id not in countUpcomingVolunteerOpportunities%}
194-
<span class="ms-auto fw-light fst-italic">0 upcoming events</span>
195-
{% else %}
196-
<span class="ms-auto fw-light fst-italic">{{countUpcomingVolunteerOpportunities[program.id]}} upcoming event{% if countUpcomingVolunteerOpportunities[program.id] > 1 %}s{% endif %}</span>
197-
{% endif %}
179+
<div class="tab-pane fade show {{ 'active' if activeTab == 'volunteerOpportunities' else '' }}"
180+
id="pills-volunteer-opportunities"
181+
role="tabpanel"
182+
aria-labelledby="pills-volunteer-opportunities-tab">
198183

199-
</button>
200-
</div>
184+
{% if volunteerOpportunities %}
185+
<div class="accordion" id="categoryAccordion">
186+
{% for program, events in volunteerOpportunities.items() %}
187+
<div class="accordion-item">
188+
<div class="accordion-header" id="accordion__header_{{ program }}">
189+
<button class="accordion-button {{ 'show' if programID == program.id else 'collapsed' }}"
190+
type="button"
191+
data-bs-toggle="collapse"
192+
data-bs-target="#accordion__body_{{ program }}_num_{{ loop.index }}"
193+
aria-expanded="true"
194+
aria-controls="accordion__body_{{ program }}">
195+
{{ program.programName }}
196+
{% set upcoming = countUpcomingVolunteerOpportunities.get(program.id, 0) %}
197+
{% set past = countPastVolunteerOpportunities.get(program.id, 0) %}
198+
<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>
199+
</button>
200+
</div>
201201
<div id="accordion__body_{{program}}_num_{{ loop.index }}"
202202
class="accordion-collapse collapse {{'show' if programID == program.id else ''}}"
203203
aria-labelledby="accordion__header_{{program}}"
@@ -208,11 +208,25 @@ <h1 class="text-center">Events List for {{selectedTerm.description}}</h1>
208208
{% endfor %}
209209
</div>
210210
{% else %}
211-
<p class="m-2 fs-4">There are no events that earn service for this term.</p>
211+
<div class="table-responsive">
212+
<table class="table">
213+
<thead>
214+
<tr>
215+
<th scope="col">Program</th>
216+
<th scope="col">Event Name</th>
217+
<th scope="col">Date</th>
218+
<th scope="col">Time</th>
219+
<th scope="col">Location</th>
220+
<th scope="col"></th>
221+
</tr>
222+
</thead>
223+
</table>
224+
</div>
225+
<td colspan="{{colspan_value}}" class="p-3 no-upcoming">There are no upcoming events for this program</td>
212226
{% endif %}
213227
</div>
214228
<div class="tab-pane fade show" id="pills-training" role="tabpanel" aria-labelledby="pills-training-tab">
215-
{{createTable(trainingEvents, "trainings")}}
229+
{{createTable(trainingEvents, "training")}}
216230
</div>
217231
<div class="tab-pane fade show" id="pills-engagement" role="tabpanel" aria-labelledby="pills-engagement-tab">
218232
{{createTable(engagementEvents, "education and engagement")}}

tests/code/test_event_list.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from app.models.term import Term
1010
from app.models.user import User
1111
from app.models.eventViews import EventView
12-
from app.logic.events import getVolunteerOpportunities, getEngagementEvents, getTrainingEvents, getBonnerEvents, getCeltsLabor, addEventView, getUpcomingVolunteerOpportunitiesCount
12+
from app.logic.events import getVolunteerOpportunities, getEngagementEvents, getTrainingEvents, getBonnerEvents, getCeltsLabor, addEventView, getUpcomingVolunteerOpportunitiesCount, getPastVolunteerOpportunitiesCount
1313

1414
@pytest.mark.integration
1515
@pytest.fixture
@@ -154,6 +154,91 @@ def test_getUpcomingVolunteerOpportunitiesCount():
154154

155155
transaction.rollback()
156156

157+
@pytest.mark.integration
158+
def test_getPastVolunteerOpportunitiesCount():
159+
with mainDB.atomic() as transaction:
160+
testDate = datetime.strptime("2021-08-01 05:00", "%Y-%m-%d %H:%M")
161+
currentTestTerm = Term.get_by_id(5)
162+
163+
# Move any existing term-5 events into the future
164+
Event.update(startDate=date(2021, 8, 5)).where(Event.term_id == 5).execute()
165+
166+
# Past Volunteer Opportunity (AGP)
167+
pastAgpEvent = Event.create(
168+
name="Test past AGP event",
169+
term=currentTestTerm,
170+
description="Past volunteer opportunity (AGP).",
171+
timeStart="03:00:00",
172+
timeEnd="04:00:00",
173+
location="Mars",
174+
isTraining=False,
175+
isService=True,
176+
startDate="2021-07-31",
177+
program=3
178+
)
179+
180+
# Past Volunteer Opportunity (same day, before test time)
181+
Event.create(
182+
name="Test same-day past AGP event",
183+
term=currentTestTerm,
184+
description="Same day but earlier time.",
185+
timeStart="04:00:00",
186+
timeEnd="04:30:00",
187+
location="Venus",
188+
isTraining=False,
189+
isService=True,
190+
startDate="2021-08-01",
191+
program=3
192+
)
193+
194+
# Future Volunteer Opportunity (should NOT be counted)
195+
Event.create(
196+
name="Test future AGP event",
197+
term=currentTestTerm,
198+
description="Future volunteer opportunity.",
199+
timeStart="06:00:00",
200+
timeEnd="07:00:00",
201+
location="Moon",
202+
isTraining=False,
203+
isService=True,
204+
startDate="2021-08-02",
205+
program=3
206+
)
207+
208+
# Verify two past AGP events
209+
pastVolunteerOpportunities = getPastVolunteerOpportunitiesCount(
210+
currentTestTerm, testDate
211+
)
212+
assert pastVolunteerOpportunities == {3: 2}
213+
214+
# Cancel one past event → should reduce count
215+
Event.update(isCanceled=True).where(Event.id == pastAgpEvent.id).execute()
216+
pastVolunteerOpportunities = getPastVolunteerOpportunitiesCount(
217+
currentTestTerm, testDate
218+
)
219+
assert pastVolunteerOpportunities == {3: 1}
220+
221+
# Create past event for another program (Buddies)
222+
pastBuddiesEvent = Event.create(
223+
name="Test past Buddies event",
224+
term=currentTestTerm,
225+
description="Past volunteer opportunity (Buddies).",
226+
timeStart="02:00:00",
227+
timeEnd="03:00:00",
228+
location="Earth",
229+
isTraining=False,
230+
isService=True,
231+
startDate="2021-07-30",
232+
program=2
233+
)
234+
235+
pastVolunteerOpportunities = getPastVolunteerOpportunitiesCount(
236+
currentTestTerm, testDate
237+
)
238+
assert pastVolunteerOpportunities == {2: 1, 3: 1}
239+
240+
transaction.rollback()
241+
157242
@pytest.mark.integration
158243
def test_getTrainingEvents(training_events):
159244
with mainDB.atomic() as transaction:

0 commit comments

Comments
 (0)