Skip to content

Gpa search #1114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d89a763
copied changes from min_gpa branch
brianlogic Apr 6, 2025
f33889a
moved changes from min_gpa to gpa-search due to conflicts
brianlogic Apr 13, 2025
5f8274c
Merge branch 'dev' into gpa-search
brianlogic Apr 13, 2025
01f4b6a
minor changes to fix styling
brianlogic Apr 13, 2025
f50d80a
fixed styling with availability and gpa search
brianlogic Apr 13, 2025
abe2422
fixed issue with gpa input javascript
brianlogic Apr 13, 2025
f7922c5
some minor fixes to styling and white space
brianlogic Apr 20, 2025
2e8fcd2
fixing mobile filter
YuDavidCao Apr 20, 2025
4a71008
Merge branch 'gpa-search' of https://github.com/thecourseforum/theCou…
YuDavidCao Apr 20, 2025
1e3a7f7
updated styling of slider and cleared out old js and styling for drop…
brianlogic Apr 20, 2025
cba72e3
Merge branch 'gpa-search' of https://github.com/thecourseforum/theCou…
brianlogic Apr 20, 2025
74e8c18
Merge branch 'gpa-search' of https://github.com/thecourseforum/theCou…
brianlogic Apr 20, 2025
b01d838
Merge branch 'dev' into gpa-search
brianlogic Apr 20, 2025
56e65c2
set better default for min gpa in context and implemented inline filt…
brianlogic Apr 25, 2025
299d08c
minor whitespace fixes and syncing of mobile / desktop for min gpa
brianlogic Apr 25, 2025
456067c
synced mobile and desktop view for min gpa and availability along wit…
brianlogic Apr 25, 2025
da79d82
white space fix for pylint
brianlogic Apr 25, 2025
9bae276
changed availability text
brianlogic Apr 27, 2025
2894db6
changed availability text on desktop view
brianlogic Apr 27, 2025
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
1 change: 1 addition & 0 deletions tcf_core/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,6 @@ def searchbar_context(request):
"from_time": saved_filters.get("from_time", ""),
"to_time": saved_filters.get("to_time", ""),
"open_sections": saved_filters.get("open_sections", False),
"min_gpa": saved_filters.get("min_gpa", 2.0)
}
return context
9 changes: 9 additions & 0 deletions tcf_website/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,15 @@ def filter_by_time(cls, days=None, start_time=None, end_time=None):
query = query.filter(section_conditions)
return query.distinct()

@classmethod
def filter_by_gpa(cls, min_gpa=None):
"""Filter courses by minimum GPA."""
return cls.objects.annotate(
avg_gpa=Avg("coursegrade__average")
).filter(
avg_gpa__gte=min_gpa if min_gpa is not None else float('-inf')
)

@classmethod
def filter_by_open_sections(cls):
"""Filter courses that have at least one open section."""
Expand Down
235 changes: 224 additions & 11 deletions tcf_website/templates/search/searchbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,24 +93,62 @@ <h6 class="filter-header">
</div>
</div>
</div>
<!-- Open Section selections -->
<!-- Hidden inputs for min_gpa -->
<input type="hidden" id="min_gpa" name="min_gpa" value="{% if min_gpa %}{{ min_gpa }}{% endif %}">
<!-- Open Section & Minimum GPA selections mobile -->
<div class="filter-section compact availability-section">
<h6 class="filter-header">
Availability
</h6>
<div class="form-check d-inline-flex align-items-center" style="background: white; border: 1px solid #cbd5e0; border-radius: 6px; padding: 0.55rem 0.67rem;">
<div class="form-check d-inline-flex align-items-center availability-check" style="background: white; border: 1px solid #cbd5e0; border-radius: 6px; padding: 0.55rem 0.67rem;">
<input
class="form-check-input me-2"
type="checkbox"
id="open-sections"
name="open_sections"
id="open-sections-compact"
{% if request.session.search_filters.open_sections %}checked{% endif %}
>
<label class="form-check-label" for="open-sections">
Only display courses with open sections
<label class="form-check-label" for="open-sections-compact">
Show courses with open sections
</label>
</div>
</div>
<div class="filter-section compact availability-section">
<h6 class="filter-header">
Minimum GPA
</h6>
<div class="min-gpa-div">
<input type="text" id="gpa-input-compact" class="min-gpa-input" placeholder="Select GPA" value="{% if min_gpa %}{{ min_gpa }}{% endif %}">
<input type="range" id="gpa-slider-compact" class="gpa-slider" min="1.0" max="4.0" step="0.1" value="{% if min_gpa %}{{ min_gpa }}{% endif %}">
</div>
</div>
<!-- Open Section & Minimum GPA selections desktop -->
<div class="filter-grid middle-filter-section">
<h6 class="filter-header">
Availability
</h6>
<h6 class="filter-header">
Minimum GPA
</h6>
<div class="availability-check">
<input
class="form-check-input me-2"
type="checkbox"
id="open-sections"
name="open_sections"
{% if request.session.search_filters.open_sections %}checked{% endif %}
>
<label class="form-check-label" for="open-sections">
Show courses with open sections
</label>
</div>
<!-- Min GPA selection -->
<div class="min-gpa-div">
<div class="gpa-input-container">
<input type="text" id="gpa-input" class="min-gpa-input" placeholder="Select GPA" value="{% if min_gpa %}{{ min_gpa }}{% endif %}">
<input type="range" id="gpa-slider" class="gpa-slider" min="1.0" max="4.0" step="0.1" value="{% if min_gpa %}{{ min_gpa }}{% endif %}">
</div>
</div>
</div>
<!-- List of subjects -->
<div class="filter-grid">
<div class="filter-section">
Expand Down Expand Up @@ -230,7 +268,60 @@ <h6 class="filter-header">
const filterButton = document.getElementById('filter-button');
const timeFrom = document.getElementById('from_time');
const timeTo = document.getElementById('to_time');
const openSections = document.getElementById('open-sections');
const desktopCheckbox = document.getElementById("open-sections");
const compactCheckbox = document.getElementById("open-sections-compact");

function syncCheckboxes(source, target) {
target.checked = source.checked;
}

compactCheckbox.addEventListener("change", () => syncCheckboxes(compactCheckbox, desktopCheckbox));
desktopCheckbox.addEventListener("change", () => syncCheckboxes(desktopCheckbox, compactCheckbox));

// Get the GPA input and slider elements for both desktop and mobile and hidden input
const gpaInputDesktop = document.getElementById('gpa-input');
const gpaSliderDesktop = document.getElementById('gpa-slider');
const gpaInputMobile = document.getElementById('gpa-input-compact');
const gpaSliderMobile = document.getElementById('gpa-slider-compact');
const hiddenGpaInput = document.getElementById('min_gpa');

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should not be this much repeated logic for the desktop and mobile versions. I think it makes more sense to just hide filters on mobile view.

// Function to sync all GPA values
function syncGpaValues(value) {
gpaInputDesktop.value = value;
gpaSliderDesktop.value = value;
gpaInputMobile.value = value;
gpaSliderMobile.value = value;
hiddenGpaInput.value = value;
}

// Initialize with any existing value
if (hiddenGpaInput.value) {
syncGpaValues(hiddenGpaInput.value);
}

// Add event listeners to all inputs
gpaInputDesktop.addEventListener('input', function() {
syncGpaValues(this.value);
});

gpaSliderDesktop.addEventListener('input', function() {
syncGpaValues(this.value);
});

gpaInputMobile.addEventListener('input', function() {
syncGpaValues(this.value);
});

gpaSliderMobile.addEventListener('input', function() {
syncGpaValues(this.value);
});

// Form submission - ensure the hidden input has the current value
document.querySelector('form').addEventListener('submit', function() {
const currentValue = window.innerWidth < 768 ?
gpaInputMobile.value : gpaInputDesktop.value;
hiddenGpaInput.value = currentValue;
});

// Check initial state (in case of page refresh with active filters)
updateButtonState();
Expand All @@ -246,7 +337,8 @@ <h6 class="filter-header">
timeFrom.addEventListener('input', updateButtonState);
timeTo.addEventListener('input', updateButtonState);

openSections.addEventListener('change', updateButtonState);
desktopCheckbox.addEventListener('change', updateButtonState);
hiddenGpaInput.addEventListener('input', updateButtonState);

// Checks for active filters or inactive day filters to determine button state
function updateButtonState() {
Expand All @@ -256,6 +348,7 @@ <h6 class="filter-header">
let timeFromChanged = false;
let timeToChanged = false;
let openSectionsChanged = false;
let gpaChanged = false;

if (timeFrom.value) {
timeFromChanged = timeFrom.value !== '';
Expand All @@ -264,11 +357,15 @@ <h6 class="filter-header">
timeToChanged = timeTo.value !== '';
}

if (openSections.checked) {
if (desktopCheckbox.checked) {
openSectionsChanged = true;
}

if (activeFilters.length > 0 || activeDayFilters.length > 0 || timeFromChanged || timeToChanged || openSectionsChanged) {
if (hiddenGpaInput.value) {
gpaChanged = hiddenGpaInput.value !== '';
}

if (activeFilters.length > 0 || activeDayFilters.length > 0 || timeFromChanged || timeToChanged || openSectionsChanged || gpaChanged) {
filterButton.classList.add('filter-active');
filterButton.textContent = 'Filters Active';
} else {
Expand Down Expand Up @@ -376,6 +473,12 @@ <h6 class="filter-header">
document.getElementById('from_time').value = '';
document.getElementById('to_time').value = '';

// Clear GPA dropdown
document.getElementById('gpa-input').value = '';
document.getElementById('gpa-slider').value = '';

syncGpaValues(''); // Sync all GPA values to empty

updateWeekdays();
updateButtonState();

Expand Down Expand Up @@ -414,6 +517,37 @@ <h6 class="filter-header">
});
});

// Stops invalid inputs from being entered into min GPA input (only numbers and decimal)
gpaInputDesktop.addEventListener('input', function() {
// Use a more efficient regex that handles all cases
this.value = this.value.replace(/[^0-9.]/g, '');
const parts = this.value.split('.');
if (parts.length > 2) {
this.value = parts[0] + '.' + parts.slice(1).join('');
}
});

gpaInputMobile.addEventListener('input', function() {
// Use a more efficient regex that handles all cases
this.value = this.value.replace(/[^0-9.]/g, '');
const parts = this.value.split('.');
if (parts.length > 2) {
this.value = parts[0] + '.' + parts.slice(1).join('');
}
});

// Input validation for GPA field desktop and mobile
gpaInputMobile.addEventListener('input', function() {
// Use a more efficient regex that handles all cases
this.value = this.value.replace(/[^0-9.]/g, '');

// More efficient way to handle multiple decimals
const parts = this.value.split('.');
if (parts.length > 2) {
this.value = parts[0] + '.' + parts.slice(1).join('');
}
});

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are there 3 of the same thing

// Initialize weekdays
updateWeekdays();

Expand Down Expand Up @@ -485,7 +619,7 @@ <h6 class="filter-header">
.filter-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
gap: 0.3rem 1.5rem;
align-items: start;
}

Expand Down Expand Up @@ -744,4 +878,83 @@ <h6 class="filter-header">
opacity: 0;
visibility: hidden;
}

.min-gpa-div {
display: inline-flex;
gap: 0;
}

.gpa-slider {
width: 80%;
background: #d75626;
border-radius: 5px;
outline: none;
height: 0.5rem;
margin: 5% 0;
}

.availability-check {
display: inline-flex;
justify-content: left;
border-radius: 4px;
padding: 0.375rem 0.75rem;
border: 1px solid #ccc;
height: 100%;
}

.availability-section {
display: none;
}

/* Show it only on mobile devices (screen width less than 768px) */
@media (max-width: 768px) {
.availability-section {
display: block;
}

.middle-filter-section {
display: none !important;
}

.availability-check {
display: flex;
align-items: center;
justify-content: flex-start;
border-radius: 4px;
padding: 0.375rem 0.75rem;
border: 1px solid #ccc;
height: fit-content;
width: fit-content;
gap: 0.5rem;
}

.availability-check .form-check-input {
margin: 0;
}

.availability-check .form-check-label {
margin-left: 1.2rem;
white-space: normal;
}
}

.min-gpa-input{
padding: 8px 12px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
width: 35%;
margin-right: 0.5rem;
}

.gpa-input-container {
position: relative;
display: flex;
width: 100%;
}

.button:focus {
outline: none;

}
</style>
6 changes: 5 additions & 1 deletion tcf_website/views/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def search(request):
"from_time": request.GET.get("from_time"),
"to_time": request.GET.get("to_time"),
"open_sections": request.GET.get("open_sections") == "on",
"min_gpa": request.GET.get("min_gpa")
}

# Save filters to session
Expand Down Expand Up @@ -181,7 +182,6 @@ def normalize_search_query(q: str) -> str:
}
for course in results
]

return courses


Expand Down Expand Up @@ -248,4 +248,8 @@ def apply_filters(results, filters):
)
results = results.filter(id__in=time_filtered.values_list("id", flat=True))

min_gpa = filters.get("min_gpa")
if filters.get("min_gpa"):
gpa_filtered = Course.filter_by_gpa(min_gpa=min_gpa)
results = results.filter(id__in=gpa_filtered.values_list("id", flat=True))
return results
Loading