Skip to content

Commit

Permalink
Interview scheduler bug fixes and features update (#178)
Browse files Browse the repository at this point in the history
* basic frontend stuff complete :3. Finished the general ui

* first attempt, implementing inductee view of their schedule backend only

* Push some frontend changes

* update

* Finish all availabilities display page

* Added backend for getting all availabilities, not single yet

* Update styling

* some post hopefully

* Update all availabilities page

* Add utils file for interview-related pages

* Update sidebar navigation

* Start edit own availability from scratch

* Update response type to const

* Update app url pathing

* Map correct url to schedule overview page

* Move utils file

* inducteeviewset post delete

* Update app routing

* Update user views

* Implement interview get api backends

* Update overall interview schedule

* Skeleton for individual interview schedule

* function for editschedule

* addavailability function

* Complete edit schedule page

* Edit InductionClassViewSet

* Update interview related functions

* Complete initial interview schedule overview

* Finish up interview system

* Delete .venv

* Add comments on functions and files

* Fix clicking on timeslot does not update availability

* Add functionality to see individual inductee availabilities

* Remove TODO comment

---------

Co-authored-by: jyeh2 <yehjames526@gmail.com>
Co-authored-by: niannianwang <niw002@ucsd.edu>
  • Loading branch information
3 people authored Dec 21, 2024
1 parent b03fb22 commit 461aca2
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 15 deletions.
4 changes: 4 additions & 0 deletions frontend/src/Pages/EditSchedule.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,16 @@
* Changes 'available' attribute of timeslot to true if available, false if unavailable
*/
timeslot.addEventListener('click', (event) => {
let day = timeslot.id.split('-')[0];
let slotNum = timeslot.id.split('-')[1];
if (event.target.getAttribute('available') == 'false') {
event.target.setAttribute('available', true);
event.target.style.background = AVAILABLE_COLOR;
availability[day][slotNum] = 1;
} else if (event.target.getAttribute('available') == 'true') {
event.target.setAttribute('available', false);
event.target.style.background = UNAVAILABLE_COLOR;
availability[day][slotNum] = 0;
}
});
Expand Down
170 changes: 155 additions & 15 deletions frontend/src/Pages/InterviewSchedule.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,28 @@
import { onMount } from "svelte";
import { generateSchedule, UNAVAILABLE_COLOR, AVAILABLE_COLOR, SELECTED_COLOR, NUM_DAYS, NUM_SLOTS } from "./interviewscheduleutils.js"
let availabilities = null;
let availabilities;
let inductee_availabilities = {};
let inductees;
let selected_slot = null;
let loaded = false;
onMount(async () => {
// Retrieve availabilities of all inductees and officers from backend
await getAvailabilities();
await getInducteeAvailabilities();
// Generate table for schedule
generateSchedule();
loaded = generateSchedule();
// Populate the schedule according to availabilities retrieved
if (availabilities != null) {
populateSchedule();
}
document.getElementById('slot_availability').style.display = 'flex';
document.getElementById('schedule').style.display = 'flex';
/*
* Add event listener to document to manage clicks on timeslots
* If a timeslot is clicked, lock 'avaiilability' display to that timeslot
Expand Down Expand Up @@ -95,6 +102,30 @@
}
}
/**
* Make an api call to the backend to retrieve all inductee availabilities
* Format of inductee_availabilities: dictionary of user_id: availability
*/
async function getInducteeAvailabilities() {
const response = await fetch(`api/inductionclasses/inductee_availabilities/`);
let list;
if (response.ok) {
list = await response.json();
} else {
list = null;
}
if (list != null) {
inductees = [];
for (let user_id in list) {
inductees.push([user_id, list[user_id][0]]);
inductee_availabilities[user_id] = list[user_id][1];
}
} else {
inductee_availabilities = null;
}
}
/*
* Sets the availability display to show the inductees and officers available at the selected timeslot
*/
Expand All @@ -106,7 +137,7 @@
let inductees;
let officers;
try {
inductees = availabilities[day][slot]['inductees'];
inductees = availabilities[day][slot]['inductees'].filter(inductee => inductee == inductee_option[1] || inductee_option == "all");
officers = availabilities[day][slot]['officers'];
} catch {
return;
Expand Down Expand Up @@ -199,6 +230,98 @@
}
}
}
/*
* Populate the schedule with individual inductee's availabilities
* Attach mouseover and mouseout events on slots with availabilties
* Mouseover event displays inductees and officers available at that timeslot in the availability display
* Set on click event to only display the selected inductee
*/
function populateInducteeSchedule(inductee_availability) {
for (let day = 0; day < NUM_DAYS; day++) {
for (let slotNum = 0; slotNum < NUM_SLOTS; slotNum++) {
let timeslot = document.getElementById(`${day}-${slotNum}`);
// Make timeslot colored if an inductee has availability at that time
if (inductee_availability[day][slotNum] == 1) {
timeslot.style.background = AVAILABLE_COLOR;
timeslot.setAttribute('available', true);
}
// Add mouseover event listener to display inductees and officers at timeslot
timeslot.addEventListener('mouseover', function() {
const P_STYLE = "margin: 1px 0px 1px 0px;";
if (selected_slot != null) {
return;
}
clearAvailabilityDisplay();
// Populate availability display with inductees available at that time
let inductees = availabilities[day][slotNum]['inductees'].filter(inductee => inductee == inductee_option[1]);
let available_inductees = document.getElementById('available_inductees');
inductees.forEach(inductee => {
let name = document.createElement('p');
name.innerText = inductee;
name.style = P_STYLE;
available_inductees.appendChild(name);
});
// Populate availability display with officers available at that time
let officers = availabilities[day][slotNum]['officers'];
let available_officers = document.getElementById('available_officers');
officers.forEach(officer => {
let name = document.createElement('p');
name.innerText = officer;
name.style = P_STYLE;
available_officers.appendChild(name);
})
});
// Add mouseout event listener to clear availability display
timeslot.addEventListener('mouseleave', function() {
if (selected_slot != null) {
return;
}
clearAvailabilityDisplay();
});
}
}
}
/**
* Clear schedule
*/
function clear_schedule() {
for (let day = 0; day < NUM_DAYS; day++) {
for (let slotNum = 0; slotNum < NUM_SLOTS; slotNum++) {
let timeslot = document.getElementById(`${day}-${slotNum}`);
timeslot.style.background = UNAVAILABLE_COLOR;
timeslot.setAttribute('available', false);
}
}
}
let inductee_option;
/*
* Filter out the selected inductee's availabilities
*/
function filter() {
if (inductee_availabilities[inductee_option[0]] != null) {
clear_schedule();
populateInducteeSchedule(inductee_availabilities[inductee_option[0]]);
} else {
clear_schedule();
populateSchedule();
}
}
// Filter the data when schedule if loaded and any inductee is selected from dropdown
$: {
inductee_option;
if (loaded && inductee_availabilities) filter();
}
</script>

<svelte:head>
Expand All @@ -211,36 +334,53 @@
<div style="padding-left:50px">
<h1>Overall Schedule</h1>
</div>
<div style="display: flex; flex-direction: row;">
<div id="slot_availability">
<h3 style="margin: 2px 0px 2px 0px;">Available</h3>
<h4 style="margin: 2px 0px 2px 0px;">Inductees:</h4>
<div id="available_inductees"></div>
<h4 style="margin: 2px 0px 2px 0px;">Officers:</h4>
<div id="available_officers"></div>
<div style="display: flex; flex-direction: column;">
{#if inductees}
<div style="margin-left: 50px">
<form>
<select bind:value={inductee_option} name="inductees">
<option value="all">Filter by Inductee</option>
{#each inductees as inductee}
<option value={inductee}>{inductee[1]}</option>
{/each}
</select>
</form>
</div>
{:else}
<h1 style="margin-left: 50px">Loading</h1>
{/if}
<div style="display: flex; flex-direction: row;">
<div id="schedule"></div>
<div id="slot_availability">
<h3 style="margin: 2px 0px 2px 0px;">Available</h3>
<h4 style="margin: 2px 0px 2px 0px;">Inductees:</h4>
<div id="available_inductees"></div>
<h4 style="margin: 2px 0px 2px 0px;">Officers:</h4>
<div id="available_officers"></div>
</div>
</div>
<div id="schedule"></div>
</div>
</body>
</Layout>

<style>
#slot_availability {
display: flex;
display: none;
flex-direction: column;
padding-left: 5px;
margin-left: 50px;
margin-left: 10px;
width: 15%;
height: 100%;
border: 1px solid black;
border-radius: 5%;
}
#schedule {
display: flex;
display: none;
flex-direction: row;
padding-left: 10px;
padding-bottom: 3vh;
width: 80%;
max-width: 80%;
height: 100%;
margin-left: 50px;
}
</style>
1 change: 1 addition & 0 deletions frontend/src/Pages/interviewscheduleutils.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,5 @@ export function generateSchedule() {
dayCol.appendChild(timeslot);
}
}
return true;
}
23 changes: 23 additions & 0 deletions myapp/api/views/user_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,29 @@ def list_all_availabilities(self, request, pk=None):
elif (user_type == 'officer'):
overall_availability[i][j]['officers'].append(name)
return Response(overall_availability, status=status.HTTP_200_OK)

@action(detail=False, methods=['GET'], url_path='inductee_availabilities')
def get_inductee_availabilities(self, request, pk=None):
'''
Retrieve all inductees who filled out availabilities for a specific induction class.
'''
# Find current induction class
induction_classes = InductionClass.objects.all()
curr_induction_class = None
for induction_class in induction_classes:
if (datetime.now().date() > induction_class.start_date and datetime.now().date() < induction_class.end_date):
curr_induction_class = induction_class

if (curr_induction_class == None):
return Response(status=status.HTTP_400_BAD_REQUEST)

inductees = {}
for (user_id, availability) in curr_induction_class.availabilities.items():
user = CustomUser.objects.get(user_id=user_id)
if user.groups.filter(name='inductee').exists():
inductees[user_id] = [f'{user.preferred_name} {user.last_name}', availability]

return Response(inductees, status=status.HTTP_200_OK)

@action(detail=False, methods=['GET'], url_path='get_availability')
def individual_availability(self, request, pk=None):
Expand Down

0 comments on commit 461aca2

Please sign in to comment.