Skip to content

Commit

Permalink
1766 Wprowadzenie stałego filtra na przedmioty
Browse files Browse the repository at this point in the history
Aby osiągnąć ten cel i w widokach przedmiotów (m.in. zakładki Przedmioty, Prototyp planu ale również Oferta)
były zapamiętywane ostatnio użyte filtry dokonałem następujących zmian:

1. Informacje o wybranych filtrach usunąłem z URL.searchParams i przeniosłem do sessionStorage z kluczem "last_searched_params".
Dzięki temu przy przechodzeniu między podstronami nie musimy pamiętać o doklejaniu końcówki poprzedniego URL-a do nowego
tylko możemy zostawić puste searchParams a ostatnio użyty filtr wygodnie wyciągnać z sessionStorage.
Ciastko to jest tworzone i aktualizowane, gdy użytkownik wybierze niepuste filtry, a usuwane, gdy użytkownik wyczyści filtry.
Jeśli nie ma ciastka (nie wybraliśmy jeszcze żadnych filtrów lub je wyczyściliśmy) to używane są domyślne filtry.
Takie rozwiązanie sprawia, że nawet niezalogowany użytkownik ma na czas danej sesji włączony ten feature.
Ciastka te są usuwane po zakończeniu sesji, więc informacje o filtrze niezalogowanych użytkowników nie są zapamiętywane między sesjami.

2. Na początku, przy pierwszym ładowaniu strony, jeśli użytkownik był od razu zalogowany ciastko to będzie uzupełnianie informacją z bazy danych.
Jeśli użytkownik nie był zalogowany to początkowo używamy domyślnych filtrów.

3. Jeśli użytkownik zaloguje się później to wszelkie preferencje, które nie miały stworzonego klucza w sesji (co oznacza że miały wartość domyślną)
zostają nadpisane przez preferencje z bazy danych. Jeśli użytkownik zmienił filtry przed zalogowaniem się to zostaną one zachowane po zalogowaniu kosztem
tych użytych w poprzedniej sesji.

4. Przy wylogowaniu się użytkownika jego preferencje są zapisywane do bazy danych o ile użytkownik jest studentem/wykładowcą (czyli nie działa dla kont typu "asm").

5. Przy wyłączaniu strony preferencje z sessionStorage są zapisywane do bazy danych o ile użytkownik jest zalogowanym studentem/wykładowcą.

Rozwiązania z bazą danych opisane w punktach 2, 3, 4 i 5 jeszcze nie działają, gdyż nie potrafiłem dokonać migracji bazy danych tak, aby
dodać do modeli Student i Employee pola "last_searched_params", w którym będziemy trzymać ostatni wyszukany filtr.
Prawdopodobnie nie jest to trudne i wystarczy wykonać 1 nieznane mi polecenie w terminalu, ale mi makemigrations nie działało, bo miałem problem
z połączeniem z bazą danych.
Aby dokonać migracji należy w pliku zapisy/apps/users/models.py odkomentować oba TODOsy i jeśli migracja się uda to potem odkomentować
middleware InitUserPreferences w zapisy/zapisy/settings.py co pozwoli na pobieranie preferencji użytkownika zalogowanego już podczas pierwszego załadowania strony.
Jeśli podczas migracji wystąpią jakieś błędy to niewykluczone, że ich rozwiązaniem jest odkomentowanie/zamiana pozostałych 3 TODOsów w tym pliku.
Jeśli migracja się uda, ale wystąpią jakieś błędy po niej, to proszę dać mi znać na Slacku w prywatnej wiadomości wraz ze zrzutem bazy danych po udanej migracji.
  • Loading branch information
Mateusz Mazur committed Jan 6, 2025
1 parent fa71671 commit 5ad9677
Show file tree
Hide file tree
Showing 14 changed files with 179 additions and 30 deletions.
2 changes: 1 addition & 1 deletion zapisy/apps/effects/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@


class EffectsConfig(AppConfig):
name = 'effects'
name = 'apps.effects'
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import LabelsFilter from "./filters/LabelsFilter.vue";
import MultiSelectFilter from "./filters/MultiSelectFilter.vue";
import CheckFilter from "./filters/CheckFilter.vue";
import { FilterDataJSON, MultiselectFilterData } from "./../models";
import { getSearchParams } from "../store/filters";
export default Vue.extend({
components: {
Expand Down Expand Up @@ -56,7 +57,7 @@ export default Vue.extend({
.filter((ref: any) => ref.filterKey)
.map((filter: any) => filter.property);
// Expand the filters if there are any initially specified in the search params.
const searchParams = new URL(window.location.href).searchParams;
const searchParams = getSearchParams();
if (filterableProperties.some((p: string) => searchParams.has(p))) {
this.collapsed = false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { property } from "lodash";
import Vue from "vue";
import { mapMutations } from "vuex";
import { Filter } from "../../store/filters";
import { Filter, LAST_FILTER_KEY, getSearchParams } from "../../store/filters";
class BooleanFilter implements Filter {
constructor(public on: boolean, public propertyName: string) {}
Expand Down Expand Up @@ -33,7 +33,7 @@ export default Vue.extend({
};
},
created: function () {
const searchParams = new URL(window.location.href).searchParams;
const searchParams = getSearchParams();
if (searchParams.has(this.property)) {
if (searchParams.get(this.property) === "true") {
Expand All @@ -54,13 +54,14 @@ export default Vue.extend({
},
watch: {
on: function (newOn: boolean) {
const url = new URL(window.location.href);
if (newOn) {
url.searchParams.set(this.property, newOn.toString());
const searchParams = getSearchParams();
if (!newOn) {
searchParams.delete(this.property);
sessionStorage.removeItem(LAST_FILTER_KEY);
} else {
url.searchParams.delete(this.property);
searchParams.set(this.property, newOn.toString());
sessionStorage.setItem(LAST_FILTER_KEY, searchParams.toString());
}
window.history.replaceState(null, "", url.toString());
this.registerFilter({
k: this.filterKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { property, intersection, isEmpty, keys, fromPairs } from "lodash";
import Vue from "vue";
import { mapMutations } from "vuex";
import { Filter } from "../../store/filters";
import { Filter, getSearchParams, LAST_FILTER_KEY } from "../../store/filters";
import { KVDict } from "../../models";
class IntersectionFilter implements Filter {
Expand Down Expand Up @@ -51,13 +51,14 @@ export default Vue.extend({
_afterSelectionChanged() {
const selectedIds = this.allLabelIds.filter((id) => this.selected[id]);
const url = new URL(window.location.href);
if (selectedIds.length > 0) {
url.searchParams.set(this.property, selectedIds.join(","));
const searchParams = getSearchParams();
if (selectedIds.length == 0) {
searchParams.delete(this.property);
sessionStorage.removeItem(LAST_FILTER_KEY);
} else {
url.searchParams.delete(this.property);
searchParams.set(this.property, selectedIds.join(","));
sessionStorage.setItem(LAST_FILTER_KEY, searchParams.toString());
}
window.history.replaceState(null, "", url.toString());
this.registerFilter({
k: this.filterKey,
Expand All @@ -70,7 +71,7 @@ export default Vue.extend({
created: function () {
this.selected = fromPairs(this.allLabelIds.map((k) => [k, false]));
const searchParams = new URL(window.location.href).searchParams;
const searchParams = getSearchParams();
if (searchParams.has(this.property)) {
const selectedIds = searchParams
.get(this.property)!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { defineComponent } from "vue";
import { mapMutations } from "vuex";
import Multiselect from "vue-multiselect";
import { Filter } from "@/enrollment/timetable/assets/store/filters";
import {
Filter,
getSearchParams,
LAST_FILTER_KEY,
} from "@/enrollment/timetable/assets/store/filters";
import { MultiselectFilterDataItem } from "../../models";
class ExactFilter implements Filter {
Expand Down Expand Up @@ -81,7 +85,7 @@ export default defineComponent<Props, any, Data, Computed, Methods>({
};
},
created: function () {
const searchParams = new URL(window.location.href).searchParams;
const searchParams = getSearchParams();
if (searchParams.has(this.property)) {
const property = searchParams.get(this.property);
if (property && property.length) {
Expand Down Expand Up @@ -151,13 +155,14 @@ export default defineComponent<Props, any, Data, Computed, Methods>({
(selectedFilter: Option) => selectedFilter.value
);
const url = new URL(window.location.href);
const searchParams = getSearchParams();
if (isEmpty(selectedIds)) {
url.searchParams.delete(this.property);
searchParams.delete(this.property);
sessionStorage.removeItem(LAST_FILTER_KEY);
} else {
url.searchParams.set(this.property, selectedIds.join(","));
searchParams.set(this.property, selectedIds.join(","));
sessionStorage.setItem(LAST_FILTER_KEY, searchParams.toString());
}
window.history.replaceState(null, "", url.toString());
this.registerFilter({
k: this.filterKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Vue from "vue";
import { mapMutations } from "vuex";
import { CourseInfo } from "../../store/courses";
import { Filter } from "../../store/filters";
import { Filter, getSearchParams, LAST_FILTER_KEY } from "../../store/filters";
class TextFilter implements Filter {
constructor(public pattern: string = "", public propertyName: string) {}
Expand Down Expand Up @@ -33,7 +33,7 @@ export default Vue.extend({
};
},
created: function () {
const searchParams = new URL(window.location.href).searchParams;
const searchParams = getSearchParams();
if (searchParams.has(this.property)) {
// TypeScript doesn't infer that property is present, manual cast required.
Expand All @@ -53,13 +53,14 @@ export default Vue.extend({
},
watch: {
pattern: function (newPattern: string, _) {
const url = new URL(window.location.href);
const searchParams = getSearchParams();
if (newPattern.length == 0) {
url.searchParams.delete(this.property);
searchParams.delete(this.property);
sessionStorage.removeItem(LAST_FILTER_KEY);
} else {
url.searchParams.set(this.property, newPattern);
searchParams.set(this.property, newPattern);
sessionStorage.setItem(LAST_FILTER_KEY, searchParams.toString());
}
window.history.replaceState(null, "", url.toString());
this.registerFilter({
k: this.filterKey,
Expand Down
14 changes: 14 additions & 0 deletions zapisy/apps/enrollment/timetable/assets/store/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ const mutations = {

const actions = {};

// When loading site tries to get last used filters from session.
// If there is no given key in session uses default filters.
// During first page load filters should be loaded
// from database to session if user is logged. (TODO: doesnt work until migration is done)
// After user logs in session preferences will be overriden by
// database preferences only if session preferences are empty (default filters).
export const LAST_FILTER_KEY = "last_searched_params";
export function getSearchParams() {
const sessionSearchParams = sessionStorage.getItem(LAST_FILTER_KEY);
return sessionSearchParams
? new URLSearchParams(sessionSearchParams)
: new URL(window.location.href).searchParams;
}

export default {
namespaced: true,
state,
Expand Down
9 changes: 9 additions & 0 deletions zapisy/apps/news/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
from django.conf import settings
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Q
from django.http import HttpResponse
from django.shortcuts import redirect, render
from django.urls import reverse

from zapisy.middleware.user_preferences import save_user_preferences_to_database
from .models import News


Expand Down Expand Up @@ -49,3 +51,10 @@ def main_page(request):
all_news_except_hidden = News.objects.published().select_related('author')
recent_news = all_news_except_hidden[:2] if all_news_except_hidden else None
return render(request, 'common/index.html', {'recent_news': recent_news})


# when user closes website we save his preferences
# from sessionStorage to database if he was logged
def close_page(request):
save_user_preferences_to_database(request)
return HttpResponse("")
14 changes: 14 additions & 0 deletions zapisy/apps/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ class Employee(models.Model):
title = models.CharField(max_length=20, verbose_name="tytuł naukowy", null=True, blank=True)
usos_id = models.PositiveIntegerField(verbose_name="ID w USOSie", null=True, blank=True)

# TODO: uncomment before migration
# last used search filters
# last_searched_params = models.CharField(max_length=200,
# verbose_name="parametry wyszukiwania",
# blank=True,
# default="")

def __str__(self):
return self.user.get_full_name()

Expand Down Expand Up @@ -92,6 +99,13 @@ class Student(models.Model):
usos_id = models.PositiveIntegerField(
null=True, blank=True, unique=True, verbose_name='Kod studenta w systemie USOS')

# TODO: uncomment before migration
# last used search filters
# last_searched_params = models.CharField(max_length=200,
# verbose_name="parametry wyszukiwania",
# blank=True,
# default="")

def __str__(self):
return f"{self.user.get_full_name()} ({self.matricula})"

Expand Down
20 changes: 20 additions & 0 deletions zapisy/apps/users/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import json
import logging

from django_cas_ng import views as cas_views
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.serializers.json import DjangoJSONEncoder
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import Http404, redirect, render, reverse
from django.views.decorators.http import require_POST

Expand All @@ -15,12 +17,30 @@
from apps.notifications.views import create_form
from apps.users.decorators import employee_required, external_contractor_forbidden

from zapisy.middleware.user_preferences \
import load_user_preferences_from_database, save_user_preferences_to_database
from .forms import EmailChangeForm, EmployeeDataForm
from .models import Employee, PersonalDataConsent, Student


logger = logging.getLogger()


# Overriding login view to get user preferences from database after login
class CustomLoginView(cas_views.LoginView):
def successful_login(self, request: HttpRequest, next_page: str) -> HttpResponseRedirect:
load_user_preferences_from_database(request)
return HttpResponseRedirect(next_page)


# TODO: not sure if it will work as I couldn't test it because of failed migration
# Overriding logout view to save user preferences to database before logout
class CustomLogoutView(cas_views.LogoutView):
def dispatch(self, request: HttpRequest, *args, **kwargs):
save_user_preferences_to_database(request)
return super().dispatch(request, *args, **kwargs)


@login_required
@external_contractor_forbidden
def students_view(request, user_id: int = None):
Expand Down
9 changes: 9 additions & 0 deletions zapisy/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@

gtag('js', new Date());
gtag('config', 'UA-109984921-1');

// when user closes website we save his preferences
// from sessionStorage to database if he was logged
window.addEventListener("beforeunload", function(event) {
var xhr = new XMLHttpRequest();
xhr.open("POST", "close/", false);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.send(JSON.stringify({}));
});
</script>

{% render_bundle "common-main" %}
Expand Down
62 changes: 62 additions & 0 deletions zapisy/zapisy/middleware/user_preferences.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from django.utils.deprecation import MiddlewareMixin

# list of database keys which are user preferences
PREFERENCE_KEYS = ["last_searched_params"]

# Extra session key so that we dont attempt to load database
# preferences each time page is reloaded
ALREADY_LOADED_KEY = "preferences_loaded"


# returns database model or None if user is not Student/Employee
def get_user_db_object(user):
if user.is_authenticated:
if hasattr(user, "student"):
return user.student
elif hasattr(user, "employee"):
return user.employee
return None


# loads user preferences from database either when page loads
# for the first time with logged user or when user logs in later
def load_user_preferences_from_database(request):
user_object = get_user_db_object(request.user)
if user_object is None:
request.session[ALREADY_LOADED_KEY] = True
return

# Set non-default user preferences in the session
for key in PREFERENCE_KEYS:
if key not in request.session:
if hasattr(user_object, key):
request.session[key] = getattr(user_object, key)
request.session[ALREADY_LOADED_KEY] = True


# when user closes website we save his preferences
# from sessionStorage to database if he was logged
def save_user_preferences_to_database(request):
user_object = get_user_db_object(request.user)
if user_object is None:
return

# Save non-default user preferences to database
for key in PREFERENCE_KEYS:
if key in request.session:
if hasattr(user_object, key):
setattr(user_object, key, request.session[key])
user_object.save()


# TODO: this feature is now disabled in zapisy/zapisy/settings.py
# MIDDLEWARE variable because of migration problems

# once per session, when website is loaded for the first time,
# we should load user preferences from database to session storage
class InitUserPreferences(MiddlewareMixin):
def process_request(self, request):
# Save up time by not checking user preferences each time page is reloaded
if request.session.get(ALREADY_LOADED_KEY, False):
return
load_user_preferences_from_database(request)
Loading

0 comments on commit 5ad9677

Please sign in to comment.