Skip to content
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

Restrictions filter #618

Closed
wants to merge 15 commits into from
Closed
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Grid, IconButton, Paper, Theme, useTheme } from '@material-ui/core';
import { IconButton, Theme } from '@material-ui/core';
import { withStyles } from '@material-ui/core/styles';
import { ClassNameMap, Styles } from '@material-ui/core/styles/withStyles';
import CloseIcon from '@material-ui/icons/Close';
Expand Down Expand Up @@ -214,6 +214,7 @@ class CourseRenderPane extends PureComponent<CourseRenderPaneProps, CourseRender
department: formData.deptValue,
term: formData.term,
ge: formData.ge,
restrictions: formData.restrictions,
courseNumber: formData.courseNumber,
sectionCodes: formData.sectionCode,
instructorName: formData.instructor,
Expand All @@ -232,11 +233,86 @@ class CourseRenderPane extends PureComponent<CourseRenderPaneProps, CourseRender
} else {
jsonResp = await queryWebsoc(params);
}
this.setState({
loading: false,
error: false,
courseData: flattenSOCObject(jsonResp),
});

// WHEN PETERPORTAL HAS AN ENDPOINT, GET RID OF THIS :)), AND MAYBE REDO THE RESTRICTION PARAMS TO MESH PROPERLY IF NEEDED ^
if (params.restrictions === 'ALL') {
// console.log(flattenSOCObject(jsonResp)) // (31 in GE-5B)
this.setState({
loading: false,
error: false,
courseData: flattenSOCObject(jsonResp),
});
} else {
// formData.restrictions is an array of strings BUT is of type string (?!)...
// so it must be converted to a string, split into an array (again), then can be filtered
// (ex: ["A", "B", "X", ''])
// if there's a cleaner way, please fix / LMK :)
const restrictionLetters = formData.restrictions.toString().includes(':')
? formData.restrictions
.toString()
.split(',')
.map((value) => value.split(':')[0].trim())
.filter((value) => /^[A-Z]$/.test(value))
.sort((a, b) => a.localeCompare(b))
: formData.restrictions
.toString()
.split('')
.filter((value) => /^[A-Z]$/.test(value))
.sort((a, b) => a.localeCompare(b));

const courseData = flattenSOCObject(jsonResp)
// IF the letter is checked, it CANNOT be in the returned courses

// Filters for courses that have NONE of its restriction values within the search restriction params
// ex: 'C and D' with ["A", "B", "L", ""] works
// ex: 'A and D' with ["A", "B", "L", ""] does not work because "A" IS in the search restriction params
.filter((course) => {
// sections doesn't exist on type School, so it has to be pre-checked
if ('sections' in course) {
return course.sections[0].restrictions
.split(' and ') // converts "A and L" into ["A", "L"]
.every((element) => !restrictionLetters.includes(element));
}
return true;
})
// The prior filter may result in "empty" schools and/or departments, so this second round filters out any School | Department object which:
.filter((currentObj, index, array) => {
const nextIndex = index + 1;

if ('deptName' in currentObj) {
// A. The next object is a School | Department object (thus it must not have a course associatied)
if (nextIndex < array.length && currentObj.deptName) {
return Object.prototype.hasOwnProperty.call(array[nextIndex], 'schoolName') ||
Object.prototype.hasOwnProperty.call(array[nextIndex], 'deptName')
? false
: true;
}

// B.The object is at the end of the array and is a School | Department object
if (
index == array.length - 1 &&
Object.prototype.hasOwnProperty.call(array[index], 'deptName')
) {
return false;
}
}
return true;
})
// The first filter may create an instance where there is an empty School and Department remaining.
// The second filter DOES removes the "empty" Department, but "forgets" to iterate back and check if School now has nothing following.
// This final filter checks for a final remaining School
.filter((obj, index, array) => {
return array.length == 1 ? false : true;
});

// console.log(courseData) // (16 in GE-5B; No A restrictions)
// Checks out because there are 15 total removed objects: courses (13), departments w/ only A courses (2: Stats & Math) 31-15=16!)
this.setState({
loading: false,
error: false,
courseData: courseData,
});
}
} catch (error) {
this.setState({
loading: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import CourseNumberSearchBar from './CourseNumberSearchBar';
import DeptSearchBar from './DeptSearchBar/DeptSearchBar';
import GESelector from './GESelector';
import SectionCodeSearchBar from './SectionCodeSearchBar';
import RestrictionsFilter from './RestrictionsFilter';

const styles: Styles<Theme, object> = {
container: {
Expand Down Expand Up @@ -57,6 +58,7 @@ function LegacySearch(props: { classes: ClassNameMap; onSubmit: () => void; onRe

<div className={classes.margin}>
<GESelector />
<RestrictionsFilter />
<SectionCodeSearchBar />
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { Checkbox, FormControl, InputLabel, ListItemText, MenuItem, Select } from '@material-ui/core';
import { withStyles } from '@material-ui/core/styles';
import { ClassNameMap } from '@material-ui/core/styles/withStyles';
import { ChangeEvent, PureComponent } from 'react';

import RightPaneStore from '../../RightPaneStore';

// clean up styling later
const styles = {
formControl: {
flexGrow: 1,
marginRight: 15,
width: 120,
},
};

interface RestrictionFilterProps {
classes: ClassNameMap;
}

interface RestrictionFilterState {
restrictions: string[];
}
const restrictionList: { value: string; label: string }[] = [
// "All: Include All Restrictions",
// "NONE: Filter all restrictions",
{ value: 'A', label: 'Prerequisite required' },
{ value: 'B', label: 'Authorization code required' },
{ value: 'C', label: 'Fee required' },
{ value: 'D', label: 'Pass/Not Pass option only' },
{ value: 'E', label: 'Freshmen only' },
{ value: 'F', label: 'Sophomores only' },
{ value: 'G', label: 'Lower-division only' },
{ value: 'H', label: 'Juniors only' },
{ value: 'I', label: 'Seniors only' },
{ value: 'J', label: 'Upper-division only' },
{ value: 'K', label: 'Graduate only' },
{ value: 'L', label: 'Major only' },
{ value: 'M', label: 'Non-major only' },
{ value: 'N', label: 'School major only' },
{ value: 'O', label: 'Non-school major only' },
{ value: 'R', label: 'Biomedical Pass/Fail course (School of Medicine only)' },
{ value: 'S', label: 'Satisfactory/Unsatisfactory only' },
{ value: 'X', label: 'Separate authorization codes required to add, drop, or change enrollment' },
];

class RestrictionsFilter extends PureComponent<RestrictionFilterProps, RestrictionFilterState> {
updateRestrictionsAndGetFormData() {
RightPaneStore.updateFormValue('restrictions', RightPaneStore.getUrlRestrictionsValue());
return RightPaneStore.getFormData().restrictions;
}
KevinWu098 marked this conversation as resolved.
Show resolved Hide resolved

getRestrictions() {
return RightPaneStore.getUrlRestrictionsValue().trim()
? this.updateRestrictionsAndGetFormData()
: RightPaneStore.getFormData().restrictions;
KevinWu098 marked this conversation as resolved.
Show resolved Hide resolved
}

state = {
restrictions:
this.getRestrictions() !== 'ALL' && typeof this.getRestrictions() === 'string' // guards for type errors
? this.getRestrictions()
.split('')
.filter((item: unknown): item is string => typeof item === 'string')
: // .map((restriction) => staticRestrictionList[restriction as string]) // Converts URL letters to the full code so the Select / Checkboxes can be checked/unchecked
[this.getRestrictions()],
};

handleChange = (event: ChangeEvent<{ restrictions?: string | undefined; value: unknown }>) => {
let value: unknown;

if ((event.target.value as string[])[0] == 'ALL' || Array.isArray((event.target.value as string[])[0])) {
value = (event.target.value as string[]).slice(1);
} else {
value = event.target.value as string[];
}

this.setState(
{
restrictions: value as string[],
},
() => {
RightPaneStore.updateFormValue('restrictions', value as unknown as string);
}
);

const stateObj = { url: 'url' };
const url = new URL(window.location.href);
const urlParam = new URLSearchParams(url.search);
urlParam.delete('restrictions');

if ((value as string[]) && (value as string[])[0] !== undefined) {
urlParam.append(
'restrictions',
(value as string[])
.map((value) => value.split(':')[0].trim()) // Prevents the URL from becoming too long
.sort((a, b) => a.localeCompare(b))
.join('')
);
}

const param = urlParam.toString();
const new_url = `${param.trim() ? '?' : ''}${param}`;
history.replaceState(stateObj, 'url', '/' + new_url);
KevinWu098 marked this conversation as resolved.
Show resolved Hide resolved
};

componentDidMount() {
RightPaneStore.on('formReset', this.resetField);
}

componentWillUnmount() {
RightPaneStore.removeListener('formReset', this.resetField);
}

resetField = () => {
this.setState({ restrictions: RightPaneStore.getFormData().restrictions.split(', ') });
};

render() {
const { classes } = this.props;

return (
<div>
<FormControl className={classes.formControl}>
<InputLabel>Restrictions</InputLabel>
<Select
multiple
value={
Array.isArray(this.state.restrictions)
? this.state.restrictions.filter(
(item: unknown): item is string => typeof item === 'string'
)
: this.state.restrictions
}
onChange={this.handleChange}
//some nonsense to keep renderValue clean
renderValue={(selected) =>
(selected as string[][0]) == 'ALL'
? "ALL: Don't filter for restrictions"
: (selected as string[])
.filter((item) => typeof item === 'string')
.map((value) => value.split(':')[0].trim())
.sort((a, b) => a.localeCompare(b))
.join(', ')
}
>
{restrictionList.map((restriction) => (
<MenuItem key={restriction.value} value={restriction.value}>
<Checkbox
checked={this.state.restrictions.indexOf(restriction.value) >= 0}
color="default"
/>
<ListItemText primary={`${restriction.value}: ${restriction.label}`} />
</MenuItem>
))}
</Select>
</FormControl>
</div>
);
}
}

export default withStyles(styles)(RestrictionsFilter);
4 changes: 4 additions & 0 deletions apps/antalmanac/src/components/RightPane/RightPaneStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const defaultFormValues: Record<string, string> = {
deptValue: 'ALL',
deptLabel: 'ALL: Include All Departments',
ge: 'ANY',
restrictions: 'ALL',
term: getDefaultTerm().shortName,
courseNumber: '',
sectionCode: '',
Expand All @@ -32,6 +33,7 @@ class RightPaneStore extends EventEmitter {
private urlCourseCodeValue: string;
private urlTermValue: string;
private urlGEValue: string;
private urlRestrictionsValue: string;
private urlCourseNumValue: string;
private urlDeptLabel: string;
private urlDeptValue: string;
Expand All @@ -47,6 +49,7 @@ class RightPaneStore extends EventEmitter {
this.urlCourseCodeValue = search.get('courseCode') || '';
this.urlTermValue = search.get('term') || '';
this.urlGEValue = search.get('GE') || '';
this.urlRestrictionsValue = search.get('restrictions') || '';
this.urlCourseNumValue = search.get('courseNumber') || '';
this.urlDeptLabel = search.get('deptLabel') || '';
this.urlDeptValue = search.get('deptValue') || '';
Expand All @@ -71,6 +74,7 @@ class RightPaneStore extends EventEmitter {
getUrlCourseCodeValue = () => this.urlCourseCodeValue;
getUrlTermValue = () => this.urlTermValue;
getUrlGEValue = () => this.urlGEValue;
getUrlRestrictionsValue = () => this.urlRestrictionsValue;
getUrlCourseNumValue = () => this.urlCourseNumValue;
getUrlDeptLabel = () => this.urlDeptLabel;
getUrlDeptValue = () => this.urlDeptValue;
Expand Down