Add filtering Classes and Courses by tag

This commit is contained in:
Tanner Collin 2022-01-28 07:47:25 +00:00
parent 14f3e46586
commit da510f2ab4
5 changed files with 134 additions and 28 deletions

View File

@ -117,6 +117,7 @@ class Course(models.Model):
name = models.TextField(blank=True, null=True) name = models.TextField(blank=True, null=True)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
is_old = models.BooleanField(default=False) is_old = models.BooleanField(default=False)
tags = models.CharField(max_length=128, blank=True)
history = HistoricalRecords() history = HistoricalRecords()

View File

@ -449,7 +449,7 @@ class StudentTrainingSerializer(TrainingSerializer):
class CourseSerializer(serializers.ModelSerializer): class CourseSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.Course model = models.Course
fields = ['id', 'name', 'is_old', 'description'] fields = ['id', 'name', 'is_old', 'description', 'tags']
class SessionSerializer(serializers.ModelSerializer): class SessionSerializer(serializers.ModelSerializer):
student_count = serializers.SerializerMethodField() student_count = serializers.SerializerMethodField()

View File

@ -1,12 +1,13 @@
import React, { useState, useEffect, useReducer } from 'react'; import React, { useState, useEffect, useReducer } from 'react';
import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom'; import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom';
import './light.css'; import './light.css';
import { Button, Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react'; import { Label, Button, Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { isAdmin, isInstructor, getInstructor, BasicTable, requester } from './utils.js'; import { isAdmin, isInstructor, getInstructor, BasicTable, requester } from './utils.js';
import { NotFound, PleaseLogin } from './Misc.js'; import { NotFound, PleaseLogin } from './Misc.js';
import { InstructorClassDetail, InstructorClassAttendance } from './InstructorClasses.js'; import { InstructorClassDetail, InstructorClassAttendance } from './InstructorClasses.js';
import { PayPalPayNow } from './PayPal.js'; import { PayPalPayNow } from './PayPal.js';
import { tags } from './Courses.js';
function ClassTable(props) { function ClassTable(props) {
const { classes } = props; const { classes } = props;
@ -92,6 +93,13 @@ function NewClassTable(props) {
</Link> </Link>
</Header> </Header>
{!!x.course.tags && x.course.tags.split(',').map(name =>
<Label color={tags[name]} tag>
{name}
</Label>
)}
<Table compact unstackable singleLine basic='very'> <Table compact unstackable singleLine basic='very'>
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
@ -140,6 +148,7 @@ function NewClassTable(props) {
let classesCache = false; let classesCache = false;
let sortCache = true; let sortCache = true;
let tagFilterCache = false;
export function ClassFeed(props) { export function ClassFeed(props) {
const [classes, setClasses] = useState(classesCache); const [classes, setClasses] = useState(classesCache);
@ -181,6 +190,7 @@ export function ClassFeed(props) {
export function Classes(props) { export function Classes(props) {
const [classes, setClasses] = useState(classesCache); const [classes, setClasses] = useState(classesCache);
const [sortByCourse, setSortByCourse] = useState(sortCache); const [sortByCourse, setSortByCourse] = useState(sortCache);
const [tagFilter, setTagFilter] = useState(tagFilterCache);
const { token, user } = props; const { token, user } = props;
useEffect(() => { useEffect(() => {
@ -194,8 +204,11 @@ export function Classes(props) {
}); });
}, []); }, []);
const isTeaching = (x) => x.instructor_id === user.member.id; const byTeaching = (x) => x.instructor_id === user.member.id;
const sortDate = (a, b) => a.datetime > b.datetime ? 1 : -1; const byDate = (a, b) => a.datetime > b.datetime ? 1 : -1;
const byTag = (x) => tagFilter ? x.course_data.tags.includes(tagFilter) : true;
console.log(tagFilter);
return ( return (
<Container> <Container>
@ -203,38 +216,70 @@ export function Classes(props) {
<p><Link to={'/courses'}>Click here to view the list of all courses.</Link></p> <p><Link to={'/courses'}>Click here to view the list of all courses.</Link></p>
{!!user && !!classes.length && !!classes.filter(isTeaching).length && {!!user && !!classes.length && !!classes.filter(byTeaching).length &&
<> <>
<Header size='medium'>Classes You're Teaching</Header> <Header size='medium'>Classes You're Teaching</Header>
<ClassTable classes={classes.slice().filter(isTeaching).sort(sortDate)} /> <ClassTable classes={classes.slice().filter(byTeaching).sort(byDate)} />
</> </>
} }
<Button <p>
onClick={() => { <Button
setSortByCourse(true); onClick={() => {
sortCache = true; setSortByCourse(true);
}} sortCache = true;
active={sortByCourse} }}
> active={sortByCourse}
Sort by course >
</Button> Sort by course
</Button>
<Button
onClick={() => {
setSortByCourse(false);
sortCache = false;
}}
active={!sortByCourse}
>
Sort by date
</Button>
</p>
<p>
Filter by tag:
<div className='coursetags'>
{Object.entries(tags).map(([name, color]) =>
<Label
onClick={() => {
setTagFilter(name);
tagFilterCache = name;
}}
as='a'
color={color}
tag
>
{name}
</Label>
)}
</div>
</p>
<p>
{tagFilter && <Button
onClick={() => {
setTagFilter(false);
tagFilterCache = false;
}}
>
Clear {tagFilter} filter
</Button>}
</p>
<Button
onClick={() => {
setSortByCourse(false);
sortCache = false;
}}
active={!sortByCourse}
>
Sort by date
</Button>
{classes.length ? {classes.length ?
sortByCourse ? sortByCourse ?
<NewClassTable classes={classes} /> <NewClassTable classes={classes.filter(byTag)} />
: :
<ClassTable classes={classes.slice().sort(sortDate)} /> <ClassTable classes={classes.slice().filter(byTag).sort(byDate)} />
: :
<p>Loading...</p> <p>Loading...</p>
} }

View File

@ -1,17 +1,35 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom'; import { BrowserRouter as Router, Switch, Route, Link, useParams } from 'react-router-dom';
import './light.css'; import './light.css';
import { Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react'; import { Button, Label, Container, Divider, Dropdown, Form, Grid, Header, Icon, Image, Menu, Message, Segment, Table } from 'semantic-ui-react';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { isInstructor, getInstructor, requester } from './utils.js'; import { isInstructor, getInstructor, requester } from './utils.js';
import { NotFound, PleaseLogin } from './Misc.js'; import { NotFound, PleaseLogin } from './Misc.js';
import { InstructorCourseList, InstructorCourseDetail } from './InstructorCourses.js'; import { InstructorCourseList, InstructorCourseDetail } from './InstructorCourses.js';
import { InstructorClassList } from './InstructorClasses.js'; import { InstructorClassList } from './InstructorClasses.js';
export const tags = {
Protospace: 'black',
Laser: 'red',
CNC: 'orange',
Niche: 'yellow',
//name: 'olive',
Electronics: 'green',
Computers: 'teal',
Metal: 'blue',
//name: 'violet',
Event: 'purple',
Outing: 'pink',
Wood: 'brown',
Misc: 'grey',
};
let courseCache = false; let courseCache = false;
let tagFilterCache = false;
export function Courses(props) { export function Courses(props) {
const [courses, setCourses] = useState(courseCache); const [courses, setCourses] = useState(courseCache);
const [tagFilter, setTagFilter] = useState(tagFilterCache);
const { token, user } = props; const { token, user } = props;
useEffect(() => { useEffect(() => {
@ -25,6 +43,8 @@ export function Courses(props) {
}); });
}, []); }, []);
const byTag = (x) => tagFilter ? x.tags.includes(tagFilter) : true;
return ( return (
<Container> <Container>
<Header size='large'>Courses</Header> <Header size='large'>Courses</Header>
@ -33,6 +53,35 @@ export function Courses(props) {
<InstructorCourseList courses={courses} setCourses={setCourses} {...props} /> <InstructorCourseList courses={courses} setCourses={setCourses} {...props} />
</Segment>} </Segment>}
<p>
Filter by tag:
<div className='coursetags'>
{Object.entries(tags).map(([name, color]) =>
<Label
onClick={() => {
setTagFilter(name);
tagFilterCache = name;
}}
as='a'
color={color}
tag
>
{name}
</Label>
)}
</div>
</p>
<p>
{tagFilter && <Button
onClick={() => {
setTagFilter(false);
tagFilterCache = false;
}}
>
Clear {tagFilter} filter
</Button>}
</p>
{courses ? {courses ?
<Table basic='very'> <Table basic='very'>
<Table.Header> <Table.Header>
@ -43,10 +92,17 @@ export function Courses(props) {
<Table.Body> <Table.Body>
{courses.length ? {courses.length ?
courses.map(x => courses.filter(byTag).map(x =>
<Table.Row key={x.id}> <Table.Row key={x.id}>
<Table.Cell> <Table.Cell>
<Link to={'/courses/'+x.id}>{x.name}</Link> <Link to={'/courses/'+x.id}>{x.name}</Link>
<span style={{ marginLeft: '2rem' }}>
{!!x.tags && x.tags.split(',').map(name =>
<Label color={tags[name]} tag>
{name}
</Label>
)}
</span>
</Table.Cell> </Table.Cell>
</Table.Row> </Table.Row>
) )

View File

@ -129,6 +129,10 @@ body {
margin: 0 1.5em 1em !important; margin: 0 1.5em 1em !important;
} }
.coursetags .ui.tag.label {
margin-top: 1rem;
}
.footer { .footer {
margin-top: -20rem; margin-top: -20rem;
background: black; background: black;