Skip to content

Commit

Permalink
Better academic year selector, filtering and sorting (#292)
Browse files Browse the repository at this point in the history
* feat: academic year selector, better pagination and filtering composables

* chore: added course pagination
  • Loading branch information
EwoutV authored Apr 11, 2024
1 parent be64b5a commit 0a6e5e5
Show file tree
Hide file tree
Showing 15 changed files with 171 additions and 139 deletions.
34 changes: 17 additions & 17 deletions backend/api/management/commands/seed_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,26 +491,26 @@ def seed_data(self, amount, provider_function, update_function):
def handle(self, *args, **options):
start_time = time.time()
# TODO maybey take as option
# amount_of_students = 50_000
# amount_of_assistants = 300
# amount_of_teachers = 500
# amount_of_courses = 1_000
# amount_of_projects = 3_000
# amount_of_groups = 9_000
# amount_of_submissions = 50_000
# amount_of_file_extensions = 20
# amount_of_structure_checks = 12_000

amount_of_students = 10
amount_of_assistants = 0
amount_of_teachers = 0
amount_of_courses = 0
amount_of_projects = 0
amount_of_groups = 0
amount_of_submissions = 0
amount_of_students = 50_000
amount_of_assistants = 5_000
amount_of_teachers = 1_500
amount_of_courses = 1_500
amount_of_projects = 3_000
amount_of_groups = 3_000
amount_of_submissions = 3_000
amount_of_file_extensions = 0
amount_of_structure_checks = 0

# amount_of_students = 10
# amount_of_assistants = 0
# amount_of_teachers = 0
# amount_of_courses = 0
# amount_of_projects = 0
# amount_of_groups = 0
# amount_of_submissions = 0
# amount_of_file_extensions = 0
# amount_of_structure_checks = 0

self.seed_data(amount_of_students, fake.provide_student, update_Student_providers)
self.stdout.write(self.style.SUCCESS('Successfully seeded students!'))
self.seed_data(amount_of_assistants, fake.provide_assistant, update_Assistant_providers)
Expand Down
8 changes: 5 additions & 3 deletions backend/api/views/course_view.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from django.db.models import Min, Max

from api.models.course import Course
from api.permissions.course_permissions import (CourseAssistantPermission,
CoursePermission,
Expand Down Expand Up @@ -26,6 +28,8 @@
from rest_framework.request import Request
from rest_framework.response import Response

from api.views.pagination.course_pagination import CoursePagination


class CourseViewSet(viewsets.ModelViewSet):
"""Actions for general course logic"""
Expand All @@ -47,10 +51,8 @@ def create(self, request: Request, *_):

return Response(serializer.data, status=status.HTTP_201_CREATED)

@action(detail=False)
@action(detail=False, pagination_class=CoursePagination)
def search(self, request: Request) -> Response:
self.pagination_class = BasicPagination

# Extract filter params
search = request.query_params.get("search", "")
years = request.query_params.getlist("years[]")
Expand Down
21 changes: 21 additions & 0 deletions backend/api/views/pagination/course_pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.db.models import Min, Max
from rest_framework.response import Response
from api.models.course import Course
from api.views.pagination.basic_pagination import BasicPagination


class CoursePagination(BasicPagination):
def get_paginated_response(self, schema):
# Get min and max years
years = Course.objects.all().aggregate(
Min('academic_startyear'), Max('academic_startyear')
)

return Response({
'count': self.page.paginator.count,
'next': self.get_next_link(),
'previous': self.get_previous_link(),
'min_year': years['academic_startyear__min'],
'max_year': years['academic_startyear__max'],
'results': schema,
})
16 changes: 12 additions & 4 deletions frontend/src/components/projects/ProjectList.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
<script setup lang="ts">
import { type Course } from '@/types/Course.ts';
import { computed, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { useProject } from '@/composables/services/project.service.ts';
import ProjectCard from '@/components/projects/ProjectCard.vue';
import { useI18n } from 'vue-i18n';
import moment from 'moment';
import Skeleton from 'primevue/skeleton';
import InputSwitch from 'primevue/inputswitch';
/* Props */
const props = withDefaults(
defineProps<{
courses: Course[] | null;
showPast?: boolean;
cols?: number;
}>(),
{
courses: () => [],
showPast: false,
cols: 4,
},
);
Expand All @@ -26,6 +25,7 @@ const { t } = useI18n();
const { projects, getProjectsByCourse } = useProject();
/* State */
const showPast = ref(false);
// The merged projects from all courses
const allProjects = computed(() => props.courses?.flatMap((course) => course.projects) ?? null);
Expand All @@ -35,7 +35,7 @@ const allProjects = computed(() => props.courses?.flatMap((course) => course.pro
*/
const sortedProjects = computed(() => {
const projects =
allProjects.value?.filter((project) => (!props.showPast ? moment(project.deadline).isAfter() : true)) ?? null;
allProjects.value?.filter((project) => (!showPast.value ? moment(project.deadline).isAfter() : true)) ?? null;
if (projects === null) {
return projects;
Expand Down Expand Up @@ -70,6 +70,14 @@ watch(
</script>

<template>
<!-- Show past projects switch -->
<div class="flex gap-3 align-items-center mb-5">
<InputSwitch input-id="show-past" v-model="showPast" />
<label for="show-past">
{{ t('views.dashboard.showPastProjects') }}
</label>
</div>
<!-- Project list -->
<div class="grid align-items-stretch">
<template v-if="sortedProjects !== null">
<template v-if="sortedProjects.length > 0">
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/composables/filters/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useRoute, useRouter } from 'vue-router';

export interface FilterState {
filter: Ref<Filter>;
onFilter: (callback: () => Promise<void>) => void;
onFilter: (callback: () => Promise<void>, debounce?: number, immediate?: boolean) => void;
}

export function useFilter(initial: Filter): FilterState {
Expand All @@ -18,8 +18,10 @@ export function useFilter(initial: Filter): FilterState {
* On filter callback
*
* @param callback
* @param debounce
* @param immediate
*/
function onFilter(callback: () => Promise<void>): void {
function onFilter(callback: () => Promise<void>, debounce: number = 500, immediate: boolean = true): void {
watchDebounced(
filter,
async () => {
Expand All @@ -32,7 +34,7 @@ export function useFilter(initial: Filter): FilterState {

await callback();
},
{ debounce: 500, immediate: true, deep: true },
{ debounce, immediate, deep: true },
);
}

Expand Down
51 changes: 22 additions & 29 deletions frontend/src/composables/filters/paginator.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { computed, type Ref, ref, watch } from 'vue';
import { type LocationQuery, useRoute, useRouter } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { type PaginatorResponse } from '@/types/filter/Paginator.ts';

export interface PaginatorState {
page: Ref<number>;
pageSize: Ref<number>;
first: Ref<number>;
paginate: (newFirst: number) => Promise<void>;
onPaginate: (callback: () => Promise<void>) => void;
resetPagination: (pagination: Ref<PaginatorResponse<any> | null>) => Promise<void>;
}

export function usePaginator(initialPage: number = 1, initialPageSize: number = 20): PaginatorState {
Expand All @@ -15,23 +16,9 @@ export function usePaginator(initialPage: number = 1, initialPageSize: number =
const { push } = useRouter();

/* State */
const page = ref<number>(initialPage);
const pageSize = ref<number>(initialPageSize);
const page = ref<number>(parseInt(query.page?.toString() ?? initialPage.toString()));

/* Watchers */
watch(
() => query,
(query: LocationQuery) => {
if (query.page !== undefined) {
page.value = parseInt(query.page as string);
}

if (query.pageSize !== undefined) {
pageSize.value = parseInt(query.pageSize as string);
}
},
{ immediate: true },
);
const pageSize = ref<number>(parseInt(query.pageSize?.toString() ?? initialPageSize.toString()));

/* Computed */
const first = computed({
Expand All @@ -40,19 +27,18 @@ export function usePaginator(initialPage: number = 1, initialPageSize: number =
});

/**
* Paginate using a new first item index.
* Reset the pagination to the first page.
*
* @param newFirst
* @returns void
*/
async function paginate(newFirst: number): Promise<void> {
first.value = newFirst;
async function resetPagination(pagination: Ref<PaginatorResponse<any> | null>): Promise<void> {
// Paginate to the first page upon filter change
first.value = 0;

await push({
query: {
page: page.value,
pageSize: pageSize.value,
},
});
if (pagination.value !== null) {
// Reset the results
pagination.value.results = null;
}
}

/**
Expand All @@ -62,6 +48,13 @@ export function usePaginator(initialPage: number = 1, initialPageSize: number =
*/
function onPaginate(callback: () => Promise<void>): void {
watch(page, async () => {
await push({
query: {
page: page.value,
pageSize: pageSize.value,
},
});

await callback();
});
}
Expand All @@ -70,7 +63,7 @@ export function usePaginator(initialPage: number = 1, initialPageSize: number =
page,
pageSize,
first,
paginate,
onPaginate,
resetPagination,
};
}
6 changes: 3 additions & 3 deletions frontend/src/composables/services/courses.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { Course } from '@/types/Course.ts';
import { type Ref, ref } from 'vue';
import { endpoints } from '@/config/endpoints.ts';
import { get, getList, create, deleteId, getPaginatedList } from '@/composables/services/helpers.ts';
import { type PaginatorResponse } from '@/types/filter/Paginator.ts';
import { type CoursePaginatorResponse } from '@/types/filter/Paginator.ts';
import { type Filter } from '@/types/filter/Filter.ts';

interface CoursesState {
pagination: Ref<PaginatorResponse<Course> | null>;
pagination: Ref<CoursePaginatorResponse | null>;
courses: Ref<Course[] | null>;
course: Ref<Course | null>;
getCourseByID: (id: string) => Promise<void>;
Expand All @@ -21,7 +21,7 @@ interface CoursesState {
}

export function useCourses(): CoursesState {
const pagination = ref<PaginatorResponse<Course> | null>(null);
const pagination = ref<CoursePaginatorResponse | null>(null);
const courses = ref<Course[] | null>(null);
const course = ref<Course | null>(null);

Expand Down
29 changes: 29 additions & 0 deletions frontend/src/types/Course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,32 @@ export class Course {
);
}
}

/**
* Get the academic year of a date.
*
* @param date
* @returns number
*/
export function getAcademicYear(date: Date = new Date()): number {
const year = date.getFullYear();

if (date.getMonth() >= 9) {
return year;
}

return year - 1;
}

/**
* Get the academic years between a minimum and maximum.
*
* @param years
* @returns number[]
*/
export function getAcademicYears(...years: number[]): number[] {
const min = Math.min(...years);
const max = Math.max(...years);

return Array.from({ length: max - min + 1 }, (_, i) => max - i);
}
9 changes: 8 additions & 1 deletion frontend/src/types/filter/Paginator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { type Course } from '@/types/Course.ts';

export interface PaginatorResponse<T> {
count: number;
next: string | null;
previous: string | null;
results: T[];
results: T[] | null;
}

export interface CoursePaginatorResponse extends PaginatorResponse<Course> {
min_year: number;
max_year: number;
}
26 changes: 0 additions & 26 deletions frontend/src/types/users/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,6 @@ export class User {
public last_login: Date | null,
) {}

/**
* Get the academic years of the user.
*/
get academic_years(): number[] {
const startYear = User.getAcademicYear(this.create_time);
const endYear = User.getAcademicYear(new Date());

return Array.from({ length: endYear - startYear + 1 }, (_, i) => startYear + i);
}

/**
* Get the full name of the user.
*
Expand All @@ -36,22 +26,6 @@ export class User {
return `${this.first_name} ${this.last_name}`;
}

/**
* Get the academic year of a date.
*
* @param date
* @returns number
*/
public static getAcademicYear(date: Date = new Date()): number {
const year = date.getFullYear();

if (date.getMonth() >= 9) {
return year;
}

return year - 1;
}

/**
* Check if the user is a student.
*
Expand Down
Loading

0 comments on commit 0a6e5e5

Please sign in to comment.