Add filtering Classes and Courses by tag
This commit is contained in:
parent
14f3e46586
commit
da510f2ab4
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,13 +216,14 @@ 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)} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSortByCourse(true);
|
setSortByCourse(true);
|
||||||
|
@ -229,12 +243,43 @@ export function Classes(props) {
|
||||||
>
|
>
|
||||||
Sort by date
|
Sort by date
|
||||||
</Button>
|
</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>
|
||||||
|
|
||||||
|
|
||||||
{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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user