Compare commits

..
Author SHA1 Message Date
imperosol 582e6af2c9 add a switch to filter closed UEs 2026-09-01 00:34:29 +02:00
imperosol bbdf6a5dd2 TS-ify guide-index.ts 2026-08-31 23:58:46 +02:00
6 changed files with 126 additions and 173 deletions
+8
View File
@@ -153,6 +153,7 @@ class UeFilterSchema(FilterSchema):
set[Literal["CS", "TM", "EC", "OM", "QC"]] | None,
FilterLookup("credit_type__in"),
] = None
is_open: bool | None = None
language: str = "FR"
department: Annotated[set[str] | None, FilterLookup("department__in")] = None
@@ -187,3 +188,10 @@ class UeFilterSchema(FilterSchema):
return Q()
value.add("AUTUMN_AND_SPRING")
return Q(semester__in=value)
def filter_is_open(self, value: bool | None) -> Q: # noqa: FBT001
if value is None:
return Q()
if not value:
return Q(semester="CLOSED")
return ~Q(semester="CLOSED")
@@ -1,118 +0,0 @@
import {
getCurrentUrlParams,
History,
updateQueryString,
} from "#core:utils/history.ts";
import { ueFetchUeList } from "#openapi";
const pageDefault = 1;
const pageSizeDefault = 100;
document.addEventListener("alpine:init", () => {
Alpine.data("ue_search", () => ({
ues: {
count: 0,
next: null,
previous: null,
results: [],
},
loading: false,
page: pageDefault,
// biome-ignore lint/style/useNamingConvention: api is in snake_case
page_size: pageSizeDefault,
search: "",
department: [],
// biome-ignore lint/style/useNamingConvention: api is in snake_case
credit_type: [],
semester: [],
// biome-ignore lint/style/useNamingConvention: api is in snake_case
to_change: [],
pushstate: History.Push,
update: undefined,
initializeArgs() {
const url = getCurrentUrlParams();
this.pushstate = History.Replace;
this.page = Number.parseInt(url.get("page"), 10) || pageDefault;
this.page_size = Number.parseInt(url.get("page_size"), 10) || pageSizeDefault;
this.search = url.get("search") || "";
this.department = url.getAll("department");
this.credit_type = url.getAll("credit_type");
/* The semester is easier to use on the backend as an enum (spring/autumn/both/none)
and easier to use on the frontend as an array ([spring, autumn]).
Thus there is some conversion involved when both communicate together */
this.semester = url.has("semester") ? url.get("semester").split("_AND_") : [];
this.update();
},
async init() {
this.update = Alpine.debounce(async () => {
/* Create the whole url before changing everything all at once */
const first = this.to_change.shift();
let url = updateQueryString(first.param, first.value, History.None);
for (const value of this.to_change) {
url = updateQueryString(value.param, value.value, History.None, url);
}
updateQueryString(first.param, first.value, this.pushstate, url);
await this.fetchData(); /* reload data on form change */
this.to_change = [];
this.pushstate = History.Push;
}, 50);
const searchParams = ["search", "department", "credit_type", "semester"];
const paginationParams = ["page", "page_size"];
for (const param of searchParams) {
this.$watch(param, () => {
if (this.pushstate !== History.Push) {
/* This means that we are doing a mass param edit */
return;
}
/* Reset pagination on search */
this.page = pageDefault;
this.page_size = pageSizeDefault;
});
}
for (const param of searchParams.concat(paginationParams)) {
this.$watch(param, (value) => {
this.to_change.push({ param: param, value: value });
this.update();
});
}
window.addEventListener("popstate", () => {
this.initializeArgs();
});
this.initializeArgs();
},
async fetchData() {
this.loading = true;
const args = {
// biome-ignore lint/style/useNamingConvention: api is in snake_case
page_size: this.page_size,
};
for (const [param, value] of new URL(
window.location.href,
).searchParams.entries()) {
// Deal with array type params
if (["credit_type", "department", "semester"].includes(param)) {
if (args[param] === undefined) {
args[param] = [];
}
args[param].push(value);
} else {
args[param] = value;
}
}
this.ues = (await ueFetchUeList({ query: args })).data;
this.loading = false;
},
maxPage() {
return Math.ceil(this.ues.count / this.page_size);
},
}));
});
@@ -0,0 +1,114 @@
import { getCurrentUrlParams, updateQueryString } from "#core:utils/history";
import { type SimpleUeSchema, ueFetchUeList } from "#openapi";
const pageDefault = 1;
const pageSizeDefault = 100;
document.addEventListener("alpine:init", () => {
Alpine.data("ue_search", () => ({
ues: {
count: 0,
next: null as string | null,
previous: null as string | null,
results: [] as SimpleUeSchema[],
},
loading: false,
page: pageDefault,
// biome-ignore lint/style/useNamingConvention: api is in snake_case
page_size: pageSizeDefault,
search: "",
hideClosedUes: true,
department: [] as string[],
// biome-ignore lint/style/useNamingConvention: api is in snake_case
credit_type: [] as string[],
semester: [] as string[],
// biome-ignore lint/style/useNamingConvention: api is in snake_case
to_change: [] as { param: string; value: string }[],
// dummy implementation to make TS happy.
// The real function is initialized in init
update: () => {
console.warn("Update not yet initialized");
},
initializeArgs() {
const url = getCurrentUrlParams();
this.page = Number.parseInt(url.get("page") || pageDefault.toString(), 10);
this.page_size = Number.parseInt(
url.get("page_size") || pageSizeDefault.toString(),
10,
);
this.search = url.get("search") || "";
this.hideClosedUes = url.get("hideClosed") || true;
this.department = url.getAll("department");
this.credit_type = url.getAll("credit_type");
/* The semester is easier to use on the backend as an enum (spring/autumn/both/none)
and easier to use on the frontend as an array ([spring, autumn]).
Thus there is some conversion involved when both communicate together */
this.semester = url.get("semester")?.split("_AND_") || [];
this.update();
},
async init() {
this.update = Alpine.debounce(async () => {
/* Create the whole url before changing everything all at once */
for (const val of this.to_change) {
updateQueryString(val.param, val.value);
}
await this.fetchData(); /* reload data on form change */
this.to_change = [];
}, 50);
const searchParams = [
"search",
"hideClosedUes",
"department",
"credit_type",
"semester",
];
const paginationParams = ["page", "page_size"];
for (const param of searchParams) {
this.$watch(param, () => {
/* Reset pagination on search */
this.page = pageDefault;
this.page_size = pageSizeDefault;
});
}
for (const param of searchParams.concat(paginationParams)) {
this.$watch(param, (value: string) => {
this.to_change.push({ param: param, value: value });
this.update();
});
}
this.initializeArgs();
},
async fetchData() {
this.loading = true;
const res = await ueFetchUeList({
query: {
// biome-ignore lint/style/useNamingConvention: api is in snake_case
page_size: this.page_size,
// biome-ignore lint/style/useNamingConvention: api is in snake_case
credit_type: this.credit_type.length > 0 ? this.credit_type : undefined,
semester: this.semester.length > 0 ? this.semester : undefined,
// biome-ignore lint/style/useNamingConvention: api is snake_case
is_open: this.hideClosedUes ? true : undefined,
department: this.department.length > 0 ? this.department : undefined,
search: this.search || undefined,
},
});
if (res.data !== undefined) {
this.ues = res.data;
}
this.loading = false;
},
maxPage() {
return Math.ceil(this.ues.count / this.page_size);
},
}));
});
+4
View File
@@ -44,6 +44,10 @@
x-model.debounce.500ms="search"
/>
</fieldset>
<fieldset>
<input type="checkbox" class="switch" x-model="hideClosedUes" id="hide-closed-ues" name="hide-closed-ues">
<label for="hide-closed-ues">{% trans %}Hide closed UEs{% endtrans %}</label>
</fieldset>
<div class="row gap-3x margin-bottom radio-guide">
<fieldset>
{% set departments = [
@@ -1,47 +0,0 @@
# Generated by Django 5.2.17 on 2026-09-01 08:44
from typing import TYPE_CHECKING
import django.utils.timezone
from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import F, Min, Value
from django.db.models.functions import Least
from django.utils.timezone import now
if TYPE_CHECKING:
import subscription.models
def make_default_creation_date(apps: StateApps, schema_editor):
Subscription = apps.get_model("subscription", "Subscription")
Subscription.objects.update(
created_at=Least(
F("subscription_start"), Value(now()), output_field=models.DateTimeField()
)
)
class Migration(migrations.Migration):
dependencies = [("subscription", "0016_alter_subscription_subscription_type")]
operations = [
migrations.AddField(
model_name="subscription",
name="created_at",
field=models.DateTimeField(
auto_now_add=True,
default=django.utils.timezone.now,
help_text=(
"When this subscription was created. "
"This date may differ from the start of the subscription."
),
verbose_name="created_at",
),
preserve_default=False,
),
migrations.RunPython(
make_default_creation_date,
reverse_code=migrations.RunPython.noop,
elidable=True,
),
]
-8
View File
@@ -64,14 +64,6 @@ class Subscription(models.Model):
max_length=20,
verbose_name=_("location"),
)
created_at = models.DateTimeField(
_("created_at"),
help_text=_(
"When this subscription was created. "
"This date may differ from the start of the subscription."
),
auto_now_add=True,
)
class Meta:
ordering = ["subscription_start"]