6 Commits

Author SHA1 Message Date
5b57f75b4e custom django command for promo logos
added path vailidity verification and IOError handling

added option to overwrite existing logo and force flag

improved uppon suggestions

mistake correction

fixed string conversion bugs and logical error

corrected path conversion

f

better error handling and corrections

ajout d'une section de documentation pour la feature

copié coller

fixed documentation bullet points

added resampling clean up error handling

removed useless IOError
2025-07-03 14:28:16 +02:00
3e3c6631ff Merge pull request #1146 from ae-utbm/fix-ts
Fix ts
2025-07-02 09:01:24 +02:00
a3ac04fc9e fix TS types 2025-06-30 18:35:53 +02:00
6e724a9c74 extract AlertMessage to its own file 2025-06-30 18:17:29 +02:00
c177ef2a3a Merge pull request #1145 from ae-utbm/xapian
fix: xapian compilation flags
2025-06-30 13:46:02 +02:00
6cf8910626 fix: xapian compilation flags 2025-06-30 13:09:24 +02:00
15 changed files with 132 additions and 61 deletions

View File

@ -0,0 +1,41 @@
import pathlib
from django.apps import apps
from django.core.management.base import BaseCommand
from PIL import Image, UnidentifiedImageError
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument("number", type=int)
parser.add_argument("path", type=pathlib.Path)
parser.add_argument("-f", "--force", action="store_true")
def handle(self, number: int, path: pathlib.Path, force: int, *args, **options):
if not path.exists() or path.is_dir():
self.stderr.write(f"{path} is not a file or does not exist")
return
dest_path = (
pathlib.Path(apps.get_app_config("core").path)
/ "static"
/ "core"
/ "img"
/ f"promo_{number}.png"
)
if dest_path.exists() and not force:
over = input("File already exists, do you want to overwrite it? (y/N):")
if over.lower() != "y":
self.stdout.write("exiting")
return
try:
im = Image.open(path)
im.resize((120, 120), resample=Image.Resampling.LANCZOS).save(
dest_path, format="PNG"
)
self.stdout.write(
f"Promo logo moved and resized successfully at {dest_path}"
)
except UnidentifiedImageError:
self.stderr.write("image cannot be opened and identified.")

View File

@ -4,13 +4,13 @@
VERSION="$1" VERSION="$1"
# Cleanup env vars for auto discovery mechanism # Cleanup env vars for auto discovery mechanism
export CPATH= unset CPATH
export LIBRARY_PATH= unset LIBRARY_PATH
export CFLAGS= unset CFLAGS
export LDFLAGS= unset LDFLAGS
export CCFLAGS= unset CCFLAGS
export CXXFLAGS= unset CXXFLAGS
export CPPFLAGS= unset CPPFLAGS
# prepare # prepare
rm -rf "$VIRTUAL_ENV/packages" rm -rf "$VIRTUAL_ENV/packages"

View File

@ -0,0 +1,38 @@
interface AlertParams {
success?: boolean;
duration?: number;
}
export class AlertMessage {
public open: boolean;
public success: boolean;
public content: string;
private timeoutId?: number;
private readonly defaultDuration: number;
constructor(params?: { defaultDuration: number }) {
this.open = false;
this.content = "";
this.timeoutId = null;
this.defaultDuration = params?.defaultDuration ?? 2000;
}
public display(message: string, params: AlertParams) {
this.clear();
this.open = true;
this.content = message;
this.success = params.success ?? true;
this.timeoutId = setTimeout(() => {
this.open = false;
this.timeoutId = null;
}, params.duration ?? this.defaultDuration);
}
public clear() {
if (this.timeoutId !== null) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
this.open = false;
}
}

View File

@ -1,5 +1,5 @@
import type { Client, Options, RequestResult, TDataShape } from "@hey-api/client-fetch"; import type { Client, RequestResult, TDataShape } from "#openapi:client";
import { client } from "#openapi"; import { type Options, client } from "#openapi";
export interface PaginatedResponse<T> { export interface PaginatedResponse<T> {
count: number; count: number;

View File

@ -1,3 +1,4 @@
import { AlertMessage } from "#core:utils/alert-message";
import { BasketItem } from "#counter:counter/basket"; import { BasketItem } from "#counter:counter/basket";
import type { CounterConfig, ErrorMessage } from "#counter:counter/types"; import type { CounterConfig, ErrorMessage } from "#counter:counter/types";
import type { CounterProductSelect } from "./components/counter-product-select-index.ts"; import type { CounterProductSelect } from "./components/counter-product-select-index.ts";
@ -5,14 +6,9 @@ import type { CounterProductSelect } from "./components/counter-product-select-i
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("counter", (config: CounterConfig) => ({ Alpine.data("counter", (config: CounterConfig) => ({
basket: {} as Record<string, BasketItem>, basket: {} as Record<string, BasketItem>,
errors: [],
customerBalance: config.customerBalance, customerBalance: config.customerBalance,
codeField: null as CounterProductSelect | null, codeField: null as CounterProductSelect | null,
alertMessage: { alertMessage: new AlertMessage({ defaultDuration: 2000 }),
content: "",
show: false,
timeout: null,
},
init() { init() {
// Fill the basket with the initial data // Fill the basket with the initial data
@ -77,22 +73,10 @@ document.addEventListener("alpine:init", () => {
return total; return total;
}, },
showAlertMessage(message: string) {
if (this.alertMessage.timeout !== null) {
clearTimeout(this.alertMessage.timeout);
}
this.alertMessage.content = message;
this.alertMessage.show = true;
this.alertMessage.timeout = setTimeout(() => {
this.alertMessage.show = false;
this.alertMessage.timeout = null;
}, 2000);
},
addToBasketWithMessage(id: string, quantity: number) { addToBasketWithMessage(id: string, quantity: number) {
const message = this.addToBasket(id, quantity); const message = this.addToBasket(id, quantity);
if (message.length > 0) { if (message.length > 0) {
this.showAlertMessage(message); this.alertMessage.display(message, { success: false });
} }
}, },
@ -109,7 +93,9 @@ document.addEventListener("alpine:init", () => {
finish() { finish() {
if (this.getBasketSize() === 0) { if (this.getBasketSize() === 0) {
this.showAlertMessage(gettext("You can't send an empty basket.")); this.alertMessage.display(gettext("You can't send an empty basket."), {
success: false,
});
return; return;
} }
this.$refs.basketForm.submit(); this.$refs.basketForm.submit();

View File

@ -167,7 +167,7 @@ document.addEventListener("alpine:init", () => {
}); });
// if products to download are already in-memory, directly take them. // if products to download are already in-memory, directly take them.
// If not, fetch them. // If not, fetch them.
const products = const products: ProductSchema[] =
this.nbPages > 1 this.nbPages > 1
? await paginated(productSearchProductsDetailed, this.getQueryParams()) ? await paginated(productSearchProductsDetailed, this.getQueryParams())
: Object.values<ProductSchema[]>(this.products).flat(); : Object.values<ProductSchema[]>(this.products).flat();

View File

@ -1,15 +1,11 @@
import { AlertMessage } from "#core:utils/alert-message";
import Alpine from "alpinejs"; import Alpine from "alpinejs";
import { producttypeReorder } from "#openapi"; import { producttypeReorder } from "#openapi";
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("productTypesList", () => ({ Alpine.data("productTypesList", () => ({
loading: false, loading: false,
alertMessage: { alertMessage: new AlertMessage({ defaultDuration: 2000 }),
open: false,
success: true,
content: "",
timeout: null,
},
async reorder(itemId: number, newPosition: number) { async reorder(itemId: number, newPosition: number) {
// The sort plugin of Alpine doesn't manage dynamic lists with x-sort // The sort plugin of Alpine doesn't manage dynamic lists with x-sort
@ -41,23 +37,14 @@ document.addEventListener("alpine:init", () => {
}, },
openAlertMessage(response: Response) { openAlertMessage(response: Response) {
if (response.ok) { const success = response.ok;
this.alertMessage.success = true; const content = response.ok
this.alertMessage.content = gettext("Products types reordered!"); ? gettext("Products types reordered!")
} else { : interpolate(
this.alertMessage.success = false;
this.alertMessage.content = interpolate(
gettext("Product type reorganisation failed with status code : %d"), gettext("Product type reorganisation failed with status code : %d"),
[response.status], [response.status],
); );
} this.alertMessage.display(content, { success: success });
this.alertMessage.open = true;
if (this.alertMessage.timeout !== null) {
clearTimeout(this.alertMessage.timeout);
}
this.alertMessage.timeout = setTimeout(() => {
this.alertMessage.open = false;
}, 2000);
this.loading = false; this.loading = false;
}, },
})); }));

View File

@ -1,4 +1,4 @@
type ErrorMessage = string; export type ErrorMessage = string;
export interface InitialFormData { export interface InitialFormData {
/* Used to refill the form when the backend raises an error */ /* Used to refill the form when the backend raises an error */

View File

@ -12,6 +12,15 @@ nouveau logo d'une promo. C'est un processus manuel.
de faire cette opération manuellement, ça prend quelques de faire cette opération manuellement, ça prend quelques
minutes et on est certain de la qualité à la fin. minutes et on est certain de la qualité à la fin.
### avec une commande django
```bash
./manage.py add_promo_logo numero_de_promo chemin_dacces_du_logo
```
options:
* `--force/-f` pour automatiquement écraser les logos de promo avec le même nom.
### manuellement
Les logos de promo sont à manuellement ajouter dans le projet. Les logos de promo sont à manuellement ajouter dans le projet.
Ils se situent dans le dossier `core/static/core/img/`. Ils se situent dans le dossier `core/static/core/img/`.

8
package-lock.json generated
View File

@ -48,6 +48,7 @@
"@types/cytoscape-cxtmenu": "^3.4.4", "@types/cytoscape-cxtmenu": "^3.4.4",
"@types/cytoscape-klay": "^3.1.4", "@types/cytoscape-klay": "^3.1.4",
"@types/jquery": "^3.5.31", "@types/jquery": "^3.5.31",
"@types/js-cookie": "^3.0.6",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.2.5", "vite": "^6.2.5",
"vite-bundle-visualizer": "^1.2.1", "vite-bundle-visualizer": "^1.2.1",
@ -2865,6 +2866,13 @@
"@types/sizzle": "*" "@types/sizzle": "*"
} }
}, },
"node_modules/@types/js-cookie": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",

View File

@ -10,7 +10,7 @@
"openapi": "openapi-ts", "openapi": "openapi-ts",
"analyse-dev": "vite-bundle-visualizer --mode development", "analyse-dev": "vite-bundle-visualizer --mode development",
"analyse-prod": "vite-bundle-visualizer --mode production", "analyse-prod": "vite-bundle-visualizer --mode production",
"check": "biome check --write" "check": "tsc && biome check --write"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -30,9 +30,10 @@
"@hey-api/openapi-ts": "^0.73.0", "@hey-api/openapi-ts": "^0.73.0",
"@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-inject": "^5.0.5",
"@types/alpinejs": "^3.13.10", "@types/alpinejs": "^3.13.10",
"@types/jquery": "^3.5.31",
"@types/cytoscape-cxtmenu": "^3.4.4", "@types/cytoscape-cxtmenu": "^3.4.4",
"@types/cytoscape-klay": "^3.1.4", "@types/cytoscape-klay": "^3.1.4",
"@types/jquery": "^3.5.31",
"@types/js-cookie": "^3.0.6",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.2.5", "vite": "^6.2.5",
"vite-bundle-visualizer": "^1.2.1", "vite-bundle-visualizer": "^1.2.1",

View File

@ -92,7 +92,7 @@ docs = [
default-groups = ["dev", "tests", "docs"] default-groups = ["dev", "tests", "docs"]
[tool.xapian] [tool.xapian]
version = "1.4.25" version = "1.4.29"
[tool.ruff] [tool.ruff]
output-format = "concise" # makes ruff error logs easier to read output-format = "concise" # makes ruff error logs easier to read

View File

@ -83,7 +83,6 @@ document.addEventListener("alpine:init", () => {
Alpine.data("pictureUpload", (albumId: number) => ({ Alpine.data("pictureUpload", (albumId: number) => ({
errors: [] as string[], errors: [] as string[],
pictures: [],
sending: false, sending: false,
progress: null as HTMLProgressElement, progress: null as HTMLProgressElement,

View File

@ -1,3 +1,4 @@
import type { UserAjaxSelect } from "#core:core/components/ajax-select-index";
import { paginated } from "#core:utils/api"; import { paginated } from "#core:utils/api";
import { exportToHtml } from "#core:utils/globals"; import { exportToHtml } from "#core:utils/globals";
import { History } from "#core:utils/history"; import { History } from "#core:utils/history";
@ -130,7 +131,7 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
currentPicture: { currentPicture: {
// biome-ignore lint/style/useNamingConvention: api is in snake_case // biome-ignore lint/style/useNamingConvention: api is in snake_case
is_moderated: true, is_moderated: true,
id: null, id: null as number,
name: "", name: "",
// biome-ignore lint/style/useNamingConvention: api is in snake_case // biome-ignore lint/style/useNamingConvention: api is in snake_case
display_name: "", display_name: "",
@ -142,7 +143,7 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
full_size_url: "", full_size_url: "",
owner: "", owner: "",
date: new Date(), date: new Date(),
identifications: [], identifications: [] as IdentifiedUserSchema[],
}, },
/** /**
* The picture which will be displayed next if the user press the "next" button * The picture which will be displayed next if the user press the "next" button
@ -155,7 +156,7 @@ exportToHtml("loadViewer", (config: ViewerConfig) => {
/** /**
* The select2 component used to identify users * The select2 component used to identify users
**/ **/
selector: undefined, selector: undefined as UserAjaxSelect,
/** /**
* Error message when a moderation operation fails * Error message when a moderation operation fails
**/ **/

View File

@ -14,6 +14,7 @@
"types": ["jquery", "alpinejs"], "types": ["jquery", "alpinejs"],
"paths": { "paths": {
"#openapi": ["./staticfiles/generated/openapi/client/index.ts"], "#openapi": ["./staticfiles/generated/openapi/client/index.ts"],
"#openapi:*": ["./staticfiles/generated/openapi/client/*"],
"#core:*": ["./core/static/bundled/*"], "#core:*": ["./core/static/bundled/*"],
"#pedagogy:*": ["./pedagogy/static/bundled/*"], "#pedagogy:*": ["./pedagogy/static/bundled/*"],
"#counter:*": ["./counter/static/bundled/*"], "#counter:*": ["./counter/static/bundled/*"],