mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-11-03 18:43:04 +00:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			docker
			...
			windows-up
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1d03fcf6ea | |||
| a6ba65a494 | |||
| c90fcc838e | 
							
								
								
									
										83
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										83
									
								
								.env.example
									
									
									
									
									
								
							@@ -1,83 +0,0 @@
 | 
			
		||||
HTTPS=off
 | 
			
		||||
DEBUG=true
 | 
			
		||||
 | 
			
		||||
# This is not the real key used in prod
 | 
			
		||||
SECRET_KEY=(4sjxvhz@m5$0a$j0_pqicnc$s!vbve)z+&++m%g%bjhlz4+g2
 | 
			
		||||
 | 
			
		||||
DATABASE_URL=sqlite:///db.sqlite3
 | 
			
		||||
# uncomment the next line if you want to use a postgres database
 | 
			
		||||
#DATABASE_URL=postgres://user:password@127.0.0.1:5432/sith
 | 
			
		||||
CACHE_URL=redis://127.0.0.1:6379/0
 | 
			
		||||
 | 
			
		||||
MEDIA_ROOT=data
 | 
			
		||||
STATIC_ROOT=static
 | 
			
		||||
 | 
			
		||||
DEFAULT_FROM_EMAIL=bibou@git.an
 | 
			
		||||
SITH_COM_EMAIL=bibou_com@git.an
 | 
			
		||||
 | 
			
		||||
HONEYPOT_VALUE=content
 | 
			
		||||
HONEYPOT_FIELD_NAME=body2
 | 
			
		||||
HONEYPOT_FIELD_NAME_FORUM=message2
 | 
			
		||||
 | 
			
		||||
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
 | 
			
		||||
EMAIL_HOST=localhost
 | 
			
		||||
EMAIL_PORT=25
 | 
			
		||||
 | 
			
		||||
SITH_URL=127.0.0.1:8000
 | 
			
		||||
SITH_NAME="AE UTBM"
 | 
			
		||||
 | 
			
		||||
SITH_MAIN_CLUB_ID=1
 | 
			
		||||
 | 
			
		||||
SITH_GROUP_ROOT_ID=1
 | 
			
		||||
SITH_GROUP_PUBLIC_ID=2
 | 
			
		||||
SITH_GROUP_SUBSCRIBERS_ID=3
 | 
			
		||||
SITH_GROUP_OLD_SUBSCRIBERS_ID=4
 | 
			
		||||
SITH_GROUP_ACCOUNTING_ADMIN_ID=5
 | 
			
		||||
SITH_GROUP_COM_ADMIN_ID=6
 | 
			
		||||
SITH_GROUP_COUNTER_ADMIN_ID=7
 | 
			
		||||
SITH_GROUP_SAS_ADMIN_ID=8
 | 
			
		||||
SITH_GROUP_FORUM_ADMIN_ID=9
 | 
			
		||||
SITH_GROUP_PEDAGOGY_ADMIN_ID=10
 | 
			
		||||
 | 
			
		||||
SITH_GROUP_BANNED_ALCOHOL_ID=11
 | 
			
		||||
SITH_GROUP_BANNED_COUNTER_ID=12
 | 
			
		||||
SITH_GROUP_BANNED_SUBSCRIPTION_ID=13
 | 
			
		||||
 | 
			
		||||
SITH_CLUB_REFOUND_ID=89
 | 
			
		||||
SITH_COUNTER_REFOUND_ID=38
 | 
			
		||||
SITH_PRODUCT_REFOUND_ID=5
 | 
			
		||||
 | 
			
		||||
# Counter
 | 
			
		||||
 | 
			
		||||
SITH_COUNTER_ACCOUNT_DUMP_ID=39
 | 
			
		||||
 | 
			
		||||
# Defines which product type is the refilling type, and thus increases the account amount
 | 
			
		||||
SITH_COUNTER_PRODUCTTYPE_REFILLING=3
 | 
			
		||||
 | 
			
		||||
SITH_ECOCUP_CONS=1152
 | 
			
		||||
SITH_ECOCUP_DECO=1151
 | 
			
		||||
 | 
			
		||||
# Defines which product is the one year subscription and which one is the six month subscription
 | 
			
		||||
SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER=1
 | 
			
		||||
SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS=2
 | 
			
		||||
SITH_PRODUCTTYPE_SUBSCRIPTION=2
 | 
			
		||||
 | 
			
		||||
# Defines which clubs let its members the ability to see users subscription history
 | 
			
		||||
SITH_CAN_CREATE_SUBSCRIPTION_HISTORY=1
 | 
			
		||||
SITH_CAN_READ_SUBSCRIPTION_HISTORY=1
 | 
			
		||||
 | 
			
		||||
# SAS variables
 | 
			
		||||
SITH_SAS_ROOT_DIR_ID=4
 | 
			
		||||
 | 
			
		||||
# ET variables
 | 
			
		||||
SITH_EBOUTIC_CB_ENABLED=true
 | 
			
		||||
SITH_EBOUTIC_ET_URL="https://preprod-tpeweb.e-transactions.fr/cgi/MYchoix_pagepaiement.cgi"
 | 
			
		||||
SITH_EBOUTIC_PBX_SITE=1999888
 | 
			
		||||
SITH_EBOUTIC_PBX_RANG=32
 | 
			
		||||
SITH_EBOUTIC_PBX_IDENTIFIANT=2
 | 
			
		||||
SITH_EBOUTIC_HMAC_KEY=0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
 | 
			
		||||
SITH_EBOUTIC_PUB_KEY_PATH=sith/et_keys/pubkey.pem
 | 
			
		||||
 | 
			
		||||
SITH_MAILING_FETCH_KEY=IloveMails
 | 
			
		||||
SENTRY_DSN=
 | 
			
		||||
SENTRY_ENV=production
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -7,10 +7,6 @@ on:
 | 
			
		||||
    branches: [master, taiste]
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  SECRET_KEY: notTheRealOne
 | 
			
		||||
  DATABASE_URL: sqlite:///db.sqlite3
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  pre-commit:
 | 
			
		||||
    name: Launch pre-commits checks (ruff)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -21,4 +21,3 @@ node_modules/
 | 
			
		||||
 | 
			
		||||
# compiled documentation
 | 
			
		||||
site/
 | 
			
		||||
.env
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
from django.db.models.signals import post_delete, post_save
 | 
			
		||||
from django.db.models.base import post_save
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
 | 
			
		||||
from com.calendar import IcsCalendar
 | 
			
		||||
from com.models import News
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver([post_save, post_delete], sender=News, dispatch_uid="update_internal_ics")
 | 
			
		||||
@receiver(post_save, sender=News, dispatch_uid="update_internal_ics")
 | 
			
		||||
def update_internal_ics(*args, **kwargs):
 | 
			
		||||
    _ = IcsCalendar.make_internal()
 | 
			
		||||
 
 | 
			
		||||
@@ -125,7 +125,7 @@
 | 
			
		||||
          <i class="fa-brands fa-discord fa-xl"></i>
 | 
			
		||||
          <a rel="nofollow" target="#" href="https://discord.gg/QvTm3XJrHR">{% trans %}Discord AE{% endtrans %}</a>
 | 
			
		||||
          {% if user.was_subscribed %}
 | 
			
		||||
            - <a rel="nofollow" target="#" href="https://discord.gg/u6EuMfyGaJ">{% trans %}Dev Team{% endtrans %}</a>
 | 
			
		||||
            - <a rel="nofollow" target="#" href="https://discord.gg/XK9WfPsUFm">{% trans %}Dev Team{% endtrans %}</a>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
        </li>
 | 
			
		||||
        <li>
 | 
			
		||||
 
 | 
			
		||||
@@ -13,12 +13,42 @@
 | 
			
		||||
#
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
import hashlib
 | 
			
		||||
import multiprocessing
 | 
			
		||||
import os
 | 
			
		||||
import platform
 | 
			
		||||
import shutil
 | 
			
		||||
import subprocess
 | 
			
		||||
import sys
 | 
			
		||||
import tarfile
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Self
 | 
			
		||||
 | 
			
		||||
import tomli
 | 
			
		||||
from django.core.management.base import BaseCommand, CommandParser
 | 
			
		||||
import urllib3
 | 
			
		||||
from django.core.management.base import BaseCommand, CommandParser, OutputWrapper
 | 
			
		||||
from urllib3.response import HTTPException
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class XapianSpec:
 | 
			
		||||
    version: str
 | 
			
		||||
    core_sha1: str
 | 
			
		||||
    bindings_sha1: str
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_pyproject(cls) -> Self:
 | 
			
		||||
        with open(
 | 
			
		||||
            Path(__file__).parent.parent.parent.parent / "pyproject.toml", "rb"
 | 
			
		||||
        ) as f:
 | 
			
		||||
            pyproject = tomli.load(f)
 | 
			
		||||
            spec = pyproject["tool"]["xapian"]
 | 
			
		||||
            return cls(
 | 
			
		||||
                version=spec["version"],
 | 
			
		||||
                core_sha1=spec["core-sha1"],
 | 
			
		||||
                bindings_sha1=spec["bindings-sha1"],
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
@@ -39,13 +69,6 @@ class Command(BaseCommand):
 | 
			
		||||
            return None
 | 
			
		||||
        return xapian.version_string()
 | 
			
		||||
 | 
			
		||||
    def _desired_version(self) -> str:
 | 
			
		||||
        with open(
 | 
			
		||||
            Path(__file__).parent.parent.parent.parent / "pyproject.toml", "rb"
 | 
			
		||||
        ) as f:
 | 
			
		||||
            pyproject = tomli.load(f)
 | 
			
		||||
            return pyproject["tool"]["xapian"]["version"]
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, force: bool, **options):
 | 
			
		||||
        if not os.environ.get("VIRTUAL_ENV", None):
 | 
			
		||||
            self.stdout.write(
 | 
			
		||||
@@ -53,20 +76,185 @@ class Command(BaseCommand):
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        desired = self._desired_version()
 | 
			
		||||
        if desired == self._current_version():
 | 
			
		||||
        desired = XapianSpec.from_pyproject()
 | 
			
		||||
        if desired.version == self._current_version():
 | 
			
		||||
            if not force:
 | 
			
		||||
                self.stdout.write(
 | 
			
		||||
                    f"Version {desired} is already installed, use --force to re-install"
 | 
			
		||||
                    f"Version {desired.version} is already installed, use --force to re-install"
 | 
			
		||||
                )
 | 
			
		||||
                return
 | 
			
		||||
            self.stdout.write(f"Version {desired} is already installed, re-installing")
 | 
			
		||||
            self.stdout.write(
 | 
			
		||||
            f"Installing xapian version {desired} at {os.environ['VIRTUAL_ENV']}"
 | 
			
		||||
        )
 | 
			
		||||
        subprocess.run(
 | 
			
		||||
            [str(Path(__file__).parent / "install_xapian.sh"), desired],
 | 
			
		||||
            env=dict(os.environ),
 | 
			
		||||
            check=True,
 | 
			
		||||
                f"Version {desired.version} is already installed, re-installing"
 | 
			
		||||
            )
 | 
			
		||||
        XapianInstaller(desired, self.stdout, self.stderr).run()
 | 
			
		||||
        self.stdout.write("Installation success")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class XapianInstaller:
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        spec: XapianSpec,
 | 
			
		||||
        stdout: OutputWrapper,
 | 
			
		||||
        stderr: OutputWrapper,
 | 
			
		||||
    ):
 | 
			
		||||
        self._version = spec.version
 | 
			
		||||
        self._core_sha1 = spec.core_sha1
 | 
			
		||||
        self._bindings_sha1 = spec.bindings_sha1
 | 
			
		||||
 | 
			
		||||
        self._stdout = stdout
 | 
			
		||||
        self._stderr = stderr
 | 
			
		||||
        self._virtual_env = os.environ.get("VIRTUAL_ENV", None)
 | 
			
		||||
 | 
			
		||||
        if not self._virtual_env:
 | 
			
		||||
            raise RuntimeError("You are not inside a virtual environment")
 | 
			
		||||
        self._virtual_env = Path(self._virtual_env)
 | 
			
		||||
 | 
			
		||||
        self._dest_dir = Path(self._virtual_env) / "packages"
 | 
			
		||||
        self._core = f"xapian-core-{self._version}"
 | 
			
		||||
        self._bindings = f"xapian-bindings-{self._version}"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def _is_windows(self) -> bool:
 | 
			
		||||
        return platform.system() == "Windows"
 | 
			
		||||
 | 
			
		||||
    def _util_download(self, url: str, dest: Path, sha1_hash: str) -> None:
 | 
			
		||||
        resp = urllib3.request("GET", url)
 | 
			
		||||
        if resp.status != 200:
 | 
			
		||||
            raise HTTPException(f"Could not download {url}")
 | 
			
		||||
        if hashlib.sha1(resp.data).hexdigest() != sha1_hash:
 | 
			
		||||
            raise ValueError(f"File downloaded from {url} is compromised")
 | 
			
		||||
        with open(dest, "wb") as f:
 | 
			
		||||
            f.write(resp.data)
 | 
			
		||||
 | 
			
		||||
    def _setup_env(self):
 | 
			
		||||
        os.environ.update(
 | 
			
		||||
            {
 | 
			
		||||
                "CPATH": "",
 | 
			
		||||
                "LIBRARY_PATH": "",
 | 
			
		||||
                "CFLAGS": "",
 | 
			
		||||
                "LDFLAGS": "",
 | 
			
		||||
                "CCFLAGS": "",
 | 
			
		||||
                "CXXFLAGS": "",
 | 
			
		||||
                "CPPFLAGS": "",
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def _prepare_dest_folder(self):
 | 
			
		||||
        shutil.rmtree(self._dest_dir, ignore_errors=True)
 | 
			
		||||
        self._dest_dir.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
 | 
			
		||||
    def _download(self):
 | 
			
		||||
        self._stdout.write("Downloading source…")
 | 
			
		||||
 | 
			
		||||
        core = self._dest_dir / f"{self._core}.tar.xz"
 | 
			
		||||
        bindings = self._dest_dir / f"{self._bindings}.tar.xz"
 | 
			
		||||
        self._util_download(
 | 
			
		||||
            f"https://oligarchy.co.uk/xapian/{self._version}/{self._core}.tar.xz",
 | 
			
		||||
            core,
 | 
			
		||||
            self._core_sha1,
 | 
			
		||||
        )
 | 
			
		||||
        self._util_download(
 | 
			
		||||
            f"https://oligarchy.co.uk/xapian/{self._version}/{self._bindings}.tar.xz",
 | 
			
		||||
            bindings,
 | 
			
		||||
            self._bindings_sha1,
 | 
			
		||||
        )
 | 
			
		||||
        self._stdout.write("Extracting source …")
 | 
			
		||||
        with tarfile.open(core) as tar:
 | 
			
		||||
            tar.extractall(self._dest_dir)
 | 
			
		||||
        with tarfile.open(bindings) as tar:
 | 
			
		||||
            tar.extractall(self._dest_dir)
 | 
			
		||||
 | 
			
		||||
        os.remove(core)
 | 
			
		||||
        os.remove(bindings)
 | 
			
		||||
 | 
			
		||||
    def _install(self):
 | 
			
		||||
        self._stdout.write("Installing Xapian-core…")
 | 
			
		||||
        def configure() -> list[str]:
 | 
			
		||||
            if self._is_windows:
 | 
			
		||||
                return ["sh", "configure"]
 | 
			
		||||
            return ["./configure"]
 | 
			
		||||
        def enable_static() -> list[str]:
 | 
			
		||||
            if self._is_windows:
 | 
			
		||||
                return ["--enable-shared", "--disable-static"]
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        # Make sure that xapian finds the correct executable
 | 
			
		||||
        os.environ["PYTHON3"] = str(Path(sys.executable).as_posix())
 | 
			
		||||
        
 | 
			
		||||
        subprocess.run(
 | 
			
		||||
            [*configure(), "--prefix", str(self._virtual_env.as_posix()), *enable_static(),],
 | 
			
		||||
            env=dict(os.environ),
 | 
			
		||||
            cwd=self._dest_dir / self._core,
 | 
			
		||||
            check=False,
 | 
			
		||||
            shell=self._is_windows,
 | 
			
		||||
        ).check_returncode()
 | 
			
		||||
        subprocess.run(
 | 
			
		||||
            [
 | 
			
		||||
                "make",
 | 
			
		||||
                "-j",
 | 
			
		||||
                str(multiprocessing.cpu_count()),
 | 
			
		||||
            ],
 | 
			
		||||
            env=dict(os.environ),
 | 
			
		||||
            cwd=self._dest_dir / self._core,
 | 
			
		||||
            check=False,
 | 
			
		||||
            shell=self._is_windows,
 | 
			
		||||
        ).check_returncode()
 | 
			
		||||
        subprocess.run(
 | 
			
		||||
            ["make", "install"],
 | 
			
		||||
            env=dict(os.environ),
 | 
			
		||||
            cwd=self._dest_dir / self._core,
 | 
			
		||||
            check=False,
 | 
			
		||||
            shell=self._is_windows,
 | 
			
		||||
 | 
			
		||||
        ).check_returncode()
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        self._stdout.write("Installing Xapian-bindings")
 | 
			
		||||
        subprocess.run(
 | 
			
		||||
            [
 | 
			
		||||
                *configure(),
 | 
			
		||||
                "--prefix",
 | 
			
		||||
                str(self._virtual_env.as_posix()),
 | 
			
		||||
                "--with-python3",
 | 
			
		||||
                f"XAPIAN_CONFIG={(self._virtual_env / 'bin'/'xapian-config').as_posix()}",
 | 
			
		||||
                *enable_static(),
 | 
			
		||||
            ],
 | 
			
		||||
            env=dict(os.environ),
 | 
			
		||||
            cwd=self._dest_dir / self._bindings,
 | 
			
		||||
            check=False,
 | 
			
		||||
            shell=self._is_windows,
 | 
			
		||||
        ).check_returncode()
 | 
			
		||||
        subprocess.run(
 | 
			
		||||
            [
 | 
			
		||||
                "make",
 | 
			
		||||
                "-j",
 | 
			
		||||
                str(multiprocessing.cpu_count()),
 | 
			
		||||
            ],
 | 
			
		||||
            env=dict(os.environ),
 | 
			
		||||
            cwd=self._dest_dir / self._bindings,
 | 
			
		||||
            check=False,
 | 
			
		||||
            shell=self._is_windows,
 | 
			
		||||
        ).check_returncode()
 | 
			
		||||
        subprocess.run(
 | 
			
		||||
            ["make", "install"],
 | 
			
		||||
            env=dict(os.environ),
 | 
			
		||||
            cwd=self._dest_dir / self._bindings,
 | 
			
		||||
            check=False,
 | 
			
		||||
            shell=self._is_windows,
 | 
			
		||||
        ).check_returncode()
 | 
			
		||||
 | 
			
		||||
    def _post_clean(self):
 | 
			
		||||
        shutil.rmtree(self._dest_dir, ignore_errors=True)
 | 
			
		||||
 | 
			
		||||
    def _test(self):
 | 
			
		||||
        subprocess.run(
 | 
			
		||||
            [sys.executable, "-c", "import xapian"], check=False, shell=self._is_windows,
 | 
			
		||||
        ).check_returncode()
 | 
			
		||||
 | 
			
		||||
    def run(self):
 | 
			
		||||
        self._setup_env()
 | 
			
		||||
        self._prepare_dest_folder()
 | 
			
		||||
        self._download()
 | 
			
		||||
        self._install()
 | 
			
		||||
        self._post_clean()
 | 
			
		||||
        self._test()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,47 +0,0 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
# Originates from https://gist.github.com/jorgecarleitao/ab6246c86c936b9c55fd
 | 
			
		||||
# first argument of the script is Xapian version (e.g. 1.2.19)
 | 
			
		||||
VERSION="$1"
 | 
			
		||||
 | 
			
		||||
# Cleanup env vars for auto discovery mechanism
 | 
			
		||||
export CPATH=
 | 
			
		||||
export LIBRARY_PATH=
 | 
			
		||||
export CFLAGS=
 | 
			
		||||
export LDFLAGS=
 | 
			
		||||
export CCFLAGS=
 | 
			
		||||
export CXXFLAGS=
 | 
			
		||||
export CPPFLAGS=
 | 
			
		||||
 | 
			
		||||
# prepare
 | 
			
		||||
rm -rf "$VIRTUAL_ENV/packages"
 | 
			
		||||
mkdir -p "$VIRTUAL_ENV/packages" && cd "$VIRTUAL_ENV/packages" || exit 1
 | 
			
		||||
 | 
			
		||||
CORE=xapian-core-$VERSION
 | 
			
		||||
BINDINGS=xapian-bindings-$VERSION
 | 
			
		||||
 | 
			
		||||
# download
 | 
			
		||||
echo "Downloading source..."
 | 
			
		||||
curl -O "https://oligarchy.co.uk/xapian/$VERSION/${CORE}.tar.xz"
 | 
			
		||||
curl -O "https://oligarchy.co.uk/xapian/$VERSION/${BINDINGS}.tar.xz"
 | 
			
		||||
 | 
			
		||||
# extract
 | 
			
		||||
echo "Extracting source..."
 | 
			
		||||
tar xf "${CORE}.tar.xz"
 | 
			
		||||
tar xf "${BINDINGS}.tar.xz"
 | 
			
		||||
 | 
			
		||||
# install
 | 
			
		||||
echo "Installing Xapian-core..."
 | 
			
		||||
cd "$VIRTUAL_ENV/packages/${CORE}" || exit 1
 | 
			
		||||
./configure --prefix="$VIRTUAL_ENV" && make -j"$(nproc)" && make install
 | 
			
		||||
 | 
			
		||||
PYTHON_FLAG=--with-python3
 | 
			
		||||
 | 
			
		||||
echo "Installing Xapian-bindings..."
 | 
			
		||||
cd "$VIRTUAL_ENV/packages/${BINDINGS}" || exit 1
 | 
			
		||||
./configure --prefix="$VIRTUAL_ENV" $PYTHON_FLAG XAPIAN_CONFIG="$VIRTUAL_ENV/bin/xapian-config" && make -j"$(nproc)" && make install
 | 
			
		||||
 | 
			
		||||
# clean
 | 
			
		||||
rm -rf "$VIRTUAL_ENV/packages"
 | 
			
		||||
 | 
			
		||||
# test
 | 
			
		||||
python -c "import xapian"
 | 
			
		||||
@@ -460,7 +460,6 @@ Welcome to the wiki page!
 | 
			
		||||
            limit_age=18,
 | 
			
		||||
        )
 | 
			
		||||
        cons = Product.objects.create(
 | 
			
		||||
            id=settings.SITH_ECOCUP_CONS,
 | 
			
		||||
            name="Consigne Eco-cup",
 | 
			
		||||
            code="CONS",
 | 
			
		||||
            product_type=verre,
 | 
			
		||||
@@ -470,7 +469,6 @@ Welcome to the wiki page!
 | 
			
		||||
            club=main_club,
 | 
			
		||||
        )
 | 
			
		||||
        dcons = Product.objects.create(
 | 
			
		||||
            id=settings.SITH_ECOCUP_DECO,
 | 
			
		||||
            name="Déconsigne Eco-cup",
 | 
			
		||||
            code="DECO",
 | 
			
		||||
            product_type=verre,
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,6 @@
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management import call_command
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.db import connection
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
@@ -30,7 +29,7 @@ class Command(BaseCommand):
 | 
			
		||||
        if not data_dir.is_dir():
 | 
			
		||||
            data_dir.mkdir()
 | 
			
		||||
        db_path = settings.BASE_DIR / "db.sqlite3"
 | 
			
		||||
        if db_path.exists() or connection.vendor != "sqlite":
 | 
			
		||||
        if db_path.exists():
 | 
			
		||||
            call_command("flush", "--noinput")
 | 
			
		||||
            self.stdout.write("Existing database reset")
 | 
			
		||||
        call_command("migrate")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,73 +0,0 @@
 | 
			
		||||
import clip from "@arendjr/text-clipper";
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
	This script adds a way to have a 'show more / show less' button
 | 
			
		||||
	on some text content.
 | 
			
		||||
 | 
			
		||||
	The usage is very simple, you just have to add the attribute `show-more`
 | 
			
		||||
	with the desired max size to the element you want to add the button to.
 | 
			
		||||
	This script does html matching and is able to properly cut rendered markdown.
 | 
			
		||||
 | 
			
		||||
	Example usage:
 | 
			
		||||
		<p show-more="20">
 | 
			
		||||
			My very long text will be cut by this script
 | 
			
		||||
		</p>
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
function showMore(element: HTMLElement) {
 | 
			
		||||
  if (!element.hasAttribute("show-more")) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Mark element as loaded so we can hide unloaded
 | 
			
		||||
  // tags with css and avoid blinking text
 | 
			
		||||
  element.setAttribute("show-more-loaded", "");
 | 
			
		||||
 | 
			
		||||
  const fullContent = element.innerHTML;
 | 
			
		||||
  const clippedContent = clip(
 | 
			
		||||
    element.innerHTML,
 | 
			
		||||
    Number.parseInt(element.getAttribute("show-more") as string),
 | 
			
		||||
    {
 | 
			
		||||
      html: true,
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // If already at the desired size, we don't do anything
 | 
			
		||||
  if (clippedContent === fullContent) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const actionLink = document.createElement("a");
 | 
			
		||||
  actionLink.setAttribute("class", "show-more-link");
 | 
			
		||||
 | 
			
		||||
  let opened = false;
 | 
			
		||||
 | 
			
		||||
  const setText = () => {
 | 
			
		||||
    if (opened) {
 | 
			
		||||
      element.innerHTML = fullContent;
 | 
			
		||||
      actionLink.innerText = gettext("Show less");
 | 
			
		||||
    } else {
 | 
			
		||||
      element.innerHTML = clippedContent;
 | 
			
		||||
      actionLink.innerText = gettext("Show more");
 | 
			
		||||
    }
 | 
			
		||||
    element.appendChild(document.createElement("br"));
 | 
			
		||||
    element.appendChild(actionLink);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const toggle = () => {
 | 
			
		||||
    opened = !opened;
 | 
			
		||||
    setText();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  setText();
 | 
			
		||||
  actionLink.addEventListener("click", (event) => {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    toggle();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
  for (const elem of document.querySelectorAll("[show-more]")) {
 | 
			
		||||
    showMore(elem as HTMLElement);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
@@ -1,27 +1,11 @@
 | 
			
		||||
.ts-wrapper.multi .ts-control {
 | 
			
		||||
  min-width: calc(100% - 0.2rem);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* This also requires ajax-select-index.css */
 | 
			
		||||
.ts-dropdown {
 | 
			
		||||
  width: calc(100% - 0.2rem);
 | 
			
		||||
  left: 0.1rem;
 | 
			
		||||
  top: calc(100% - 0.2rem - var(--nf-input-border-bottom-width));
 | 
			
		||||
  border: var(--nf-input-border-color) var(--nf-input-border-width) solid;
 | 
			
		||||
  border-top: none;
 | 
			
		||||
  border-bottom-width: var(--nf-input-border-bottom-width);
 | 
			
		||||
 | 
			
		||||
  .option.active {
 | 
			
		||||
    background-color: #e5eafa;
 | 
			
		||||
    color: inherit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .select-item {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    gap: 10px;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
 | 
			
		||||
    img {
 | 
			
		||||
      height: 40px;
 | 
			
		||||
@@ -32,44 +16,19 @@
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ts-wrapper {
 | 
			
		||||
  margin: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ts-wrapper.single {
 | 
			
		||||
  > .ts-control {
 | 
			
		||||
    box-shadow: none;
 | 
			
		||||
    max-width: 300px;
 | 
			
		||||
    background-color: var(--nf-input-background-color);
 | 
			
		||||
 | 
			
		||||
    &::after {
 | 
			
		||||
      content: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  > .ts-dropdown {
 | 
			
		||||
    max-width: 300px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ts-wrapper input[type="text"] {
 | 
			
		||||
  border: none;
 | 
			
		||||
  border-radius: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ts-wrapper.multi, .ts-wrapper.single {
 | 
			
		||||
  .ts-control:has(input:focus) {
 | 
			
		||||
    outline: none;
 | 
			
		||||
    border-color: var(--nf-input-focus-border-color);
 | 
			
		||||
    box-shadow: none;
 | 
			
		||||
  }
 | 
			
		||||
  width: 263px; // same length as regular text inputs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove {
 | 
			
		||||
  border-left: 1px solid #aaa;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ts-wrapper.multi.has-items .ts-control {
 | 
			
		||||
  padding: calc(var(--nf-input-size) * 0.65);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  gap: calc(var(--nf-input-size) / 3);
 | 
			
		||||
 | 
			
		||||
.ts-wrapper.multi .ts-control {
 | 
			
		||||
  [data-value],
 | 
			
		||||
  [data-value].active {
 | 
			
		||||
    background-image: none;
 | 
			
		||||
@@ -78,17 +37,19 @@
 | 
			
		||||
    border: 1px solid #aaa;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    margin-left: 5px;
 | 
			
		||||
    margin-top: 5px;
 | 
			
		||||
    margin-bottom: 5px;
 | 
			
		||||
    padding-right: 10px;
 | 
			
		||||
    padding-left: 10px;
 | 
			
		||||
    text-shadow: none;
 | 
			
		||||
    box-shadow: none;
 | 
			
		||||
 | 
			
		||||
    .remove {
 | 
			
		||||
      vertical-align: baseline;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ts-wrapper.focus .ts-control {
 | 
			
		||||
  box-shadow: none;
 | 
			
		||||
.ts-dropdown {
 | 
			
		||||
  .option.active {
 | 
			
		||||
    background-color: #e5eafa;
 | 
			
		||||
    color: inherit;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -48,8 +48,7 @@
 | 
			
		||||
 | 
			
		||||
  input,
 | 
			
		||||
  textarea[type="text"],
 | 
			
		||||
  [type="number"],
 | 
			
		||||
  .ts-control {
 | 
			
		||||
  [type="number"] {
 | 
			
		||||
    border: none;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
    background-color: $background-button-color;
 | 
			
		||||
@@ -70,7 +69,7 @@
 | 
			
		||||
    font-family: sans-serif;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  select, .ts-control {
 | 
			
		||||
  select {
 | 
			
		||||
    border: none;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
    font-size: 1.2em;
 | 
			
		||||
@@ -178,7 +177,7 @@ form {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // wrap texts
 | 
			
		||||
  label, legend, ul.errorlist > li, .helptext {
 | 
			
		||||
  label, legend, ul.errorlist>li, .helptext {
 | 
			
		||||
    text-wrap: wrap;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -219,7 +218,6 @@ form {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  :not(.ts-control) > {
 | 
			
		||||
  input[type="text"],
 | 
			
		||||
  input[type="email"],
 | 
			
		||||
  input[type="tel"],
 | 
			
		||||
@@ -229,9 +227,9 @@ form {
 | 
			
		||||
  input[type="date"],
 | 
			
		||||
  input[type="week"],
 | 
			
		||||
  input[type="time"],
 | 
			
		||||
  input[type="month"],
 | 
			
		||||
  input[type="search"],
 | 
			
		||||
  textarea,
 | 
			
		||||
    input[type="month"],
 | 
			
		||||
  select {
 | 
			
		||||
    min-width: 300px;
 | 
			
		||||
 | 
			
		||||
@@ -239,7 +237,6 @@ form {
 | 
			
		||||
      width: 95%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  input[type="text"],
 | 
			
		||||
  input[type="checkbox"],
 | 
			
		||||
@@ -256,8 +253,7 @@ form {
 | 
			
		||||
  input[type="month"],
 | 
			
		||||
  input[type="search"],
 | 
			
		||||
  textarea,
 | 
			
		||||
  select,
 | 
			
		||||
  .ts-control {
 | 
			
		||||
  select {
 | 
			
		||||
    background: var(--nf-input-background-color);
 | 
			
		||||
    font-size: var(--nf-input-font-size);
 | 
			
		||||
    border-color: var(--nf-input-border-color);
 | 
			
		||||
@@ -717,11 +713,7 @@ form {
 | 
			
		||||
 | 
			
		||||
  // ---------------- SELECT
 | 
			
		||||
 | 
			
		||||
  select,
 | 
			
		||||
  .ts-wrapper.multi .ts-control,
 | 
			
		||||
  .ts-wrapper.single .ts-control,
 | 
			
		||||
  .ts-wrapper.single.input-active .ts-control {
 | 
			
		||||
    background-color: var(--nf-input-background-color);
 | 
			
		||||
  select {
 | 
			
		||||
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236B7280'  stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-chevron-down'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
 | 
			
		||||
    background-position: right calc(var(--nf-input-size) * 0.75) bottom 50%;
 | 
			
		||||
    background-repeat: no-repeat;
 | 
			
		||||
 
 | 
			
		||||
@@ -131,10 +131,6 @@ body {
 | 
			
		||||
  display: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[show-more]:not([show-more-loaded]) {
 | 
			
		||||
  display: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*--------------------------------HEADER-------------------------------*/
 | 
			
		||||
 | 
			
		||||
#popupheader {
 | 
			
		||||
 
 | 
			
		||||
@@ -125,14 +125,15 @@
 | 
			
		||||
          navbar.style.setProperty("display", current === "none" ? "block" : "none");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        document.addEventListener("keydown", (e) => {
 | 
			
		||||
          // Looking at the `s` key when not typing in a form
 | 
			
		||||
          if (e.keyCode !== 83 || ["INPUT", "TEXTAREA", "SELECT"].includes(e.target.nodeName)) {
 | 
			
		||||
            return;
 | 
			
		||||
        $(document).keydown(function (e) {
 | 
			
		||||
          if ($(e.target).is('input')) { return }
 | 
			
		||||
          if ($(e.target).is('textarea')) { return }
 | 
			
		||||
          if ($(e.target).is('select')) { return }
 | 
			
		||||
          if (e.keyCode === 83) {
 | 
			
		||||
            $("#search").focus();
 | 
			
		||||
            return false;
 | 
			
		||||
          }
 | 
			
		||||
          document.getElementById("search").focus();
 | 
			
		||||
          e.preventDefault(); // Don't type the character in the focused search input
 | 
			
		||||
        })
 | 
			
		||||
        });
 | 
			
		||||
      </script>
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
  </body>
 | 
			
		||||
 
 | 
			
		||||
@@ -57,4 +57,13 @@
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
  {% endif %}
 | 
			
		||||
 | 
			
		||||
  {% block script %}
 | 
			
		||||
    {{ super() }}
 | 
			
		||||
    {% if popup %}
 | 
			
		||||
      <script>
 | 
			
		||||
        parent.$(".choose_file_widget").css("height", "75%");
 | 
			
		||||
      </script>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
  {% endblock %}
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -244,30 +244,27 @@
 | 
			
		||||
{% block script %}
 | 
			
		||||
  {{ super() }}
 | 
			
		||||
  <script>
 | 
			
		||||
    // Image selection
 | 
			
		||||
    for (const img of document.querySelectorAll("#small_pictures img")){
 | 
			
		||||
      img.addEventListener("click", (e) => {
 | 
			
		||||
        const displayed = document.querySelector("#big_picture img");
 | 
			
		||||
        displayed.src = e.target.src;
 | 
			
		||||
        displayed.alt = e.target.alt;
 | 
			
		||||
        displayed.title = e.target.title;
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let keys = [];
 | 
			
		||||
    const pattern = "71,85,89,71,85,89";
 | 
			
		||||
 | 
			
		||||
    document.addEventListener("keydown", (e) => {
 | 
			
		||||
    $(function () {
 | 
			
		||||
      var keys = [];
 | 
			
		||||
      var pattern = "71,85,89,71,85,89";
 | 
			
		||||
      $(document).keydown(function (e) {
 | 
			
		||||
        keys.push(e.keyCode);
 | 
			
		||||
      if (keys.toString() === pattern) {
 | 
			
		||||
        if (keys.toString() == pattern) {
 | 
			
		||||
          keys = [];
 | 
			
		||||
        document.querySelector("#big_picture img").src = "{{ static('core/img/yug.jpg') }}";
 | 
			
		||||
          $("#big_picture img").attr("src", "{{ static('core/img/yug.jpg') }}");
 | 
			
		||||
        }
 | 
			
		||||
      if (keys.length === 6) {
 | 
			
		||||
        if (keys.length == 6) {
 | 
			
		||||
          keys.shift();
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
    $(function () {
 | 
			
		||||
      $("#small_pictures img").click(function () {
 | 
			
		||||
        $("#big_picture img").attr("src", $(this)[0].src);
 | 
			
		||||
        $("#big_picture img").attr("alt", $(this)[0].alt);
 | 
			
		||||
        $("#big_picture img").attr("title", $(this)[0].title);
 | 
			
		||||
      })
 | 
			
		||||
    });
 | 
			
		||||
    $(function () {
 | 
			
		||||
      $("#drop_gifts").accordion({
 | 
			
		||||
        heightStyle: "content",
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
              <li><a href="{{ url('rootplace:operation_logs') }}">{% trans %}Operation logs{% endtrans %}</a></li>
 | 
			
		||||
              <li><a href="{{ url('rootplace:delete_forum_messages') }}">{% trans %}Delete user's forum messages{% endtrans %}</a></li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if user.has_perm("core.view_userban") %}
 | 
			
		||||
            {% if user.has_perm("core:view_userban") %}
 | 
			
		||||
              <li><a href="{{ url("rootplace:ban_list") }}">{% trans %}Bans{% endtrans %}</a></li>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% if user.can_create_subscription or user.is_root %}
 | 
			
		||||
 
 | 
			
		||||
@@ -76,15 +76,7 @@ export class CounterProductSelect extends AutoCompleteSelectBase {
 | 
			
		||||
    return {
 | 
			
		||||
      ...super.tomSelectSettings(),
 | 
			
		||||
      openOnFocus: false,
 | 
			
		||||
      // We make searching on exact code matching a higher priority
 | 
			
		||||
      // We need to manually set weights or it results on an inconsistent
 | 
			
		||||
      // behavior between production and development environment
 | 
			
		||||
      searchField: [
 | 
			
		||||
        // @ts-ignore documentation says it's fine, specified type is wrong
 | 
			
		||||
        { field: "code", weight: 2 },
 | 
			
		||||
        // @ts-ignore documentation says it's fine, specified type is wrong
 | 
			
		||||
        { field: "text", weight: 0.5 },
 | 
			
		||||
      ],
 | 
			
		||||
      searchField: ["code", "text"],
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -236,10 +236,6 @@ class TestCounterClick(TestFullClickBase):
 | 
			
		||||
            BanGroup.objects.get(pk=settings.SITH_GROUP_BANNED_COUNTER_ID)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        cls.gift = product_recipe.make(
 | 
			
		||||
            selling_price="-1.5",
 | 
			
		||||
            special_selling_price="-1.5",
 | 
			
		||||
        )
 | 
			
		||||
        cls.beer = product_recipe.make(
 | 
			
		||||
            limit_age=18, selling_price="1.5", special_selling_price="1"
 | 
			
		||||
        )
 | 
			
		||||
@@ -257,12 +253,7 @@ class TestCounterClick(TestFullClickBase):
 | 
			
		||||
            limit_age=0, selling_price="1.5", special_selling_price="1"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        cls.cons = Product.objects.get(id=settings.SITH_ECOCUP_CONS)
 | 
			
		||||
        cls.dcons = Product.objects.get(id=settings.SITH_ECOCUP_DECO)
 | 
			
		||||
 | 
			
		||||
        cls.counter.products.add(
 | 
			
		||||
            cls.gift, cls.beer, cls.beer_tap, cls.snack, cls.cons, cls.dcons
 | 
			
		||||
        )
 | 
			
		||||
        cls.counter.products.add(cls.beer, cls.beer_tap, cls.snack)
 | 
			
		||||
 | 
			
		||||
        cls.other_counter.products.add(cls.snack)
 | 
			
		||||
 | 
			
		||||
@@ -603,84 +594,6 @@ class TestCounterClick(TestFullClickBase):
 | 
			
		||||
            else:
 | 
			
		||||
                assert not counter.has_annotated_barman
 | 
			
		||||
 | 
			
		||||
    def test_selling_ordering(self):
 | 
			
		||||
        # Cheaper items should be processed with a higher priority
 | 
			
		||||
        self.login_in_bar(self.barmen)
 | 
			
		||||
 | 
			
		||||
        assert (
 | 
			
		||||
            self.submit_basket(
 | 
			
		||||
                self.customer,
 | 
			
		||||
                [
 | 
			
		||||
                    BasketItem(self.beer.id, 1),
 | 
			
		||||
                    BasketItem(self.gift.id, 1),
 | 
			
		||||
                ],
 | 
			
		||||
            ).status_code
 | 
			
		||||
            == 302
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        assert self.updated_amount(self.customer) == 0
 | 
			
		||||
 | 
			
		||||
    def test_recordings(self):
 | 
			
		||||
        self.refill_user(self.customer, self.cons.selling_price * 3)
 | 
			
		||||
        self.login_in_bar(self.barmen)
 | 
			
		||||
        assert (
 | 
			
		||||
            self.submit_basket(
 | 
			
		||||
                self.customer,
 | 
			
		||||
                [BasketItem(self.cons.id, 3)],
 | 
			
		||||
            ).status_code
 | 
			
		||||
            == 302
 | 
			
		||||
        )
 | 
			
		||||
        assert self.updated_amount(self.customer) == 0
 | 
			
		||||
 | 
			
		||||
        assert (
 | 
			
		||||
            self.submit_basket(
 | 
			
		||||
                self.customer,
 | 
			
		||||
                [BasketItem(self.dcons.id, 3)],
 | 
			
		||||
            ).status_code
 | 
			
		||||
            == 302
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        assert self.updated_amount(self.customer) == self.dcons.selling_price * -3
 | 
			
		||||
 | 
			
		||||
        assert (
 | 
			
		||||
            self.submit_basket(
 | 
			
		||||
                self.customer,
 | 
			
		||||
                [BasketItem(self.dcons.id, settings.SITH_ECOCUP_LIMIT)],
 | 
			
		||||
            ).status_code
 | 
			
		||||
            == 302
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        assert self.updated_amount(self.customer) == self.dcons.selling_price * (
 | 
			
		||||
            -3 - settings.SITH_ECOCUP_LIMIT
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        assert (
 | 
			
		||||
            self.submit_basket(
 | 
			
		||||
                self.customer,
 | 
			
		||||
                [BasketItem(self.dcons.id, 1)],
 | 
			
		||||
            ).status_code
 | 
			
		||||
            == 200
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        assert self.updated_amount(self.customer) == self.dcons.selling_price * (
 | 
			
		||||
            -3 - settings.SITH_ECOCUP_LIMIT
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        assert (
 | 
			
		||||
            self.submit_basket(
 | 
			
		||||
                self.customer,
 | 
			
		||||
                [
 | 
			
		||||
                    BasketItem(self.cons.id, 1),
 | 
			
		||||
                    BasketItem(self.dcons.id, 1),
 | 
			
		||||
                ],
 | 
			
		||||
            ).status_code
 | 
			
		||||
            == 302
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        assert self.updated_amount(self.customer) == self.dcons.selling_price * (
 | 
			
		||||
            -3 - settings.SITH_ECOCUP_LIMIT
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestCounterStats(TestCase):
 | 
			
		||||
    @classmethod
 | 
			
		||||
 
 | 
			
		||||
@@ -194,11 +194,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, SingleObjectMixin, FormView):
 | 
			
		||||
        with transaction.atomic():
 | 
			
		||||
            self.request.session["last_basket"] = []
 | 
			
		||||
 | 
			
		||||
            # We sort items from cheap to expensive
 | 
			
		||||
            # This is important because some items have a negative price
 | 
			
		||||
            # Negative priced items gives money to the customer and should
 | 
			
		||||
            # be processed first so that we don't throw a not enough money error
 | 
			
		||||
            for form in sorted(formset, key=lambda form: form.product.price):
 | 
			
		||||
            for form in formset:
 | 
			
		||||
                self.request.session["last_basket"].append(
 | 
			
		||||
                    f"{form.cleaned_data['quantity']} x {form.product.name}"
 | 
			
		||||
                )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
services:
 | 
			
		||||
  db:
 | 
			
		||||
    image: postgres:16.6
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    deploy:
 | 
			
		||||
      resources:
 | 
			
		||||
        limits:
 | 
			
		||||
          cpus: '0.5'
 | 
			
		||||
          memory: 512M
 | 
			
		||||
    ports:
 | 
			
		||||
      - "5431:5432"
 | 
			
		||||
    environment:
 | 
			
		||||
      POSTGRES_USER: sith
 | 
			
		||||
      POSTGRES_PASSWORD: sith
 | 
			
		||||
      POSTGRES_DB: sith
 | 
			
		||||
 | 
			
		||||
  redis:
 | 
			
		||||
    image: redis:latest
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    deploy:
 | 
			
		||||
      resources:
 | 
			
		||||
        limits:
 | 
			
		||||
          cpus: '0.5'
 | 
			
		||||
          memory: 512M
 | 
			
		||||
    ports:
 | 
			
		||||
      - "6378:6379"
 | 
			
		||||
    command: redis-server
 | 
			
		||||
    volumes:
 | 
			
		||||
      - redis_data:/var/lib/redis/data/
 | 
			
		||||
 | 
			
		||||
volumes:
 | 
			
		||||
  postgres_data:
 | 
			
		||||
    driver: local
 | 
			
		||||
  redis_data:
 | 
			
		||||
    driver: local
 | 
			
		||||
 | 
			
		||||
@@ -2,13 +2,13 @@
 | 
			
		||||
 | 
			
		||||
Pour connecter l'application à une instance de sentry (ex: https://sentry.io),
 | 
			
		||||
il est nécessaire de configurer la variable `SENTRY_DSN`
 | 
			
		||||
dans le fichier `.env`.
 | 
			
		||||
dans le fichier `settings_custom.py`.
 | 
			
		||||
Cette variable est composée d'un lien complet vers votre projet sentry.
 | 
			
		||||
 | 
			
		||||
## Récupérer les statiques
 | 
			
		||||
 | 
			
		||||
Nous utilisons du SCSS dans le projet.
 | 
			
		||||
En environnement de développement (`DEBUG=true`),
 | 
			
		||||
En environnement de développement (`DEBUG=True`),
 | 
			
		||||
le SCSS est compilé à chaque fois que le fichier est demandé.
 | 
			
		||||
Pour la production, le projet considère 
 | 
			
		||||
que chacun des fichiers est déjà compilé.
 | 
			
		||||
 
 | 
			
		||||
@@ -47,19 +47,19 @@ Commencez par installer les dépendances système :
 | 
			
		||||
    === "Debian/Ubuntu"
 | 
			
		||||
 | 
			
		||||
        ```bash
 | 
			
		||||
        sudo apt install postgresql libq-dev nginx
 | 
			
		||||
        sudo apt install postgresql redis libq-dev nginx
 | 
			
		||||
        ```
 | 
			
		||||
 | 
			
		||||
    === "Arch Linux"
 | 
			
		||||
    
 | 
			
		||||
        ```bash
 | 
			
		||||
        sudo pacman -S postgresql nginx
 | 
			
		||||
        sudo pacman -S postgresql redis nginx
 | 
			
		||||
        ```
 | 
			
		||||
 | 
			
		||||
=== "macOS"
 | 
			
		||||
 | 
			
		||||
    ```bash
 | 
			
		||||
    brew install postgresql lipbq nginx
 | 
			
		||||
    brew install postgresql redis lipbq nginx
 | 
			
		||||
    export PATH="/usr/local/opt/libpq/bin:$PATH"
 | 
			
		||||
    source ~/.zshrc
 | 
			
		||||
    ```
 | 
			
		||||
@@ -77,6 +77,34 @@ uv sync --group prod
 | 
			
		||||
    C'est parce que ces dépendances compilent certains modules
 | 
			
		||||
    à l'installation.
 | 
			
		||||
 | 
			
		||||
## Configurer Redis
 | 
			
		||||
 | 
			
		||||
Redis est utilisé comme cache.
 | 
			
		||||
Assurez-vous qu'il tourne :
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
sudo systemctl redis status
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Et s'il ne tourne pas, démarrez-le :
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
sudo systemctl start redis
 | 
			
		||||
sudo systemctl enable redis  # si vous voulez que redis démarre automatiquement au boot
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Puis ajoutez le code suivant à la fin de votre fichier
 | 
			
		||||
`settings_custom.py` :
 | 
			
		||||
 | 
			
		||||
```python
 | 
			
		||||
CACHES = {
 | 
			
		||||
    "default": {
 | 
			
		||||
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
 | 
			
		||||
        "LOCATION": "redis://127.0.0.1:6379",
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Configurer PostgreSQL
 | 
			
		||||
 | 
			
		||||
PostgreSQL est utilisé comme base de données.
 | 
			
		||||
@@ -111,19 +139,26 @@ en étant connecté en tant que postgres :
 | 
			
		||||
psql -d sith -c "GRANT ALL PRIVILEGES ON SCHEMA public to sith";
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Puis modifiez votre `.env`.
 | 
			
		||||
Dedans, décommentez l'url de la base de données
 | 
			
		||||
de postgres et commentez l'url de sqlite :
 | 
			
		||||
Puis ajoutez le code suivant à la fin de votre
 | 
			
		||||
`settings_custom.py` :
 | 
			
		||||
 | 
			
		||||
```dotenv
 | 
			
		||||
#DATABASE_URL=sqlite:///db.sqlite3
 | 
			
		||||
DATABASE_URL=postgres://sith:password@localhost:5432/sith
 | 
			
		||||
```python
 | 
			
		||||
DATABASES = {
 | 
			
		||||
    "default": {
 | 
			
		||||
        "ENGINE": "django.db.backends.postgresql",
 | 
			
		||||
        "NAME": "sith",
 | 
			
		||||
        "USER": "sith",
 | 
			
		||||
        "PASSWORD": "password",
 | 
			
		||||
        "HOST": "localhost",
 | 
			
		||||
        "PORT": "",  # laissez ce champ vide pour que le choix du port soit automatique
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Enfin, créez vos données :
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
uv run ./manage.py setup
 | 
			
		||||
uv run ./manage.py populate
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
!!! note
 | 
			
		||||
@@ -212,7 +247,7 @@ Puis lancez ou relancez nginx :
 | 
			
		||||
sudo systemctl restart nginx
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Dans votre `.env`, remplacez `DEBUG=true` par `DEBUG=false`.
 | 
			
		||||
Dans votre `settings_custom.py`, remplacez `DEBUG=True` par `DEBUG=False`.
 | 
			
		||||
 | 
			
		||||
Enfin, démarrez le serveur Django :
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ Certaines dépendances sont nécessaires niveau système :
 | 
			
		||||
- libjpeg
 | 
			
		||||
- zlib1g-dev
 | 
			
		||||
- gettext
 | 
			
		||||
- redis
 | 
			
		||||
 | 
			
		||||
### Installer WSL
 | 
			
		||||
 | 
			
		||||
@@ -67,7 +66,7 @@ cd /mnt/<la_lettre_du_disque>/vos/fichiers/comme/dhab
 | 
			
		||||
        ```bash
 | 
			
		||||
        sudo apt install curl build-essential libssl-dev \
 | 
			
		||||
        libjpeg-dev zlib1g-dev npm libffi-dev pkg-config \
 | 
			
		||||
            gettext git redis
 | 
			
		||||
        gettext git
 | 
			
		||||
        curl -LsSf https://astral.sh/uv/install.sh | sh
 | 
			
		||||
        ```
 | 
			
		||||
 | 
			
		||||
@@ -76,7 +75,7 @@ cd /mnt/<la_lettre_du_disque>/vos/fichiers/comme/dhab
 | 
			
		||||
        ```bash
 | 
			
		||||
        sudo pacman -Syu  # on s'assure que les dépôts et le système sont à jour
 | 
			
		||||
 | 
			
		||||
        sudo pacman -S uv gcc git gettext pkgconf npm redis
 | 
			
		||||
        sudo pacman -S uv gcc git gettext pkgconf npm
 | 
			
		||||
        ```
 | 
			
		||||
 | 
			
		||||
=== "macOS"
 | 
			
		||||
@@ -85,7 +84,7 @@ cd /mnt/<la_lettre_du_disque>/vos/fichiers/comme/dhab
 | 
			
		||||
    Il est également nécessaire d'avoir installé xcode
 | 
			
		||||
    
 | 
			
		||||
    ```bash    
 | 
			
		||||
    brew install git uv npm redis
 | 
			
		||||
    brew install git uv npm
 | 
			
		||||
    
 | 
			
		||||
    # Pour bien configurer gettext
 | 
			
		||||
    brew link gettext # (suivez bien les instructions supplémentaires affichées)
 | 
			
		||||
@@ -100,24 +99,6 @@ cd /mnt/<la_lettre_du_disque>/vos/fichiers/comme/dhab
 | 
			
		||||
    Python ne fait pas parti des dépendances puisqu'il est automatiquement
 | 
			
		||||
    installé par uv.
 | 
			
		||||
 | 
			
		||||
Parmi les dépendances installées se trouve redis (que nous utilisons comme cache).
 | 
			
		||||
Redis est un service qui doit être activé pour être utilisé.
 | 
			
		||||
Pour cela, effectuez les commandes :
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
sudo systemctl start redis
 | 
			
		||||
sudo systemctl enable redis  # si vous voulez que redis démarre automatiquement au boot
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Parmi les dépendances installées se trouve redis (que nous utilisons comme cache).
 | 
			
		||||
Redis est un service qui doit être activé pour être utilisé.
 | 
			
		||||
Pour cela, effectuez les commandes :
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
sudo systemctl start redis
 | 
			
		||||
sudo systemctl enable redis  # si vous voulez que redis démarre automatiquement au boot
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Finaliser l'installation
 | 
			
		||||
 | 
			
		||||
Clonez le projet (depuis votre console WSL, si vous utilisez WSL)
 | 
			
		||||
@@ -139,24 +120,20 @@ uv run ./manage.py install_xapian
 | 
			
		||||
    de texte à l'écran.
 | 
			
		||||
    C'est normal, il ne faut pas avoir peur.
 | 
			
		||||
 | 
			
		||||
Une fois les dépendances installées, il faut encore
 | 
			
		||||
mettre en place quelques éléments de configuration,
 | 
			
		||||
qui peuvent varier d'un environnement à l'autre.
 | 
			
		||||
Ces variables sont stockées dans un fichier `.env`.
 | 
			
		||||
Pour le créer, vous pouvez copier le fichier `.env.example` :
 | 
			
		||||
Maintenant que les dépendances sont installées, nous
 | 
			
		||||
allons créer la base de données, la remplir avec des données de test,
 | 
			
		||||
et compiler les traductions.
 | 
			
		||||
Cependant, avant de faire cela, il est nécessaire de modifier
 | 
			
		||||
la configuration pour signifier que nous sommes en mode développement.
 | 
			
		||||
Pour cela, nous allons créer un fichier `sith/settings_custom.py`
 | 
			
		||||
et l'utiliser pour surcharger les settings de base.
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
cp .env.example .env
 | 
			
		||||
echo "DEBUG=True" > sith/settings_custom.py
 | 
			
		||||
echo 'SITH_URL = "localhost:8000"' >> sith/settings_custom.py
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Les variables par défaut contenues dans le fichier `.env`
 | 
			
		||||
devraient convenir pour le développement, sans modification.
 | 
			
		||||
 | 
			
		||||
Maintenant que les dépendances sont installées
 | 
			
		||||
et la configuration remplie, nous allons pouvoir générer
 | 
			
		||||
des données utiles pendant le développement.
 | 
			
		||||
 | 
			
		||||
Pour cela, lancez les commandes suivantes :
 | 
			
		||||
Enfin, nous pouvons lancer les commandes suivantes :
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Prépare la base de données
 | 
			
		||||
@@ -194,30 +171,6 @@ uv run ./manage.py runserver
 | 
			
		||||
    [http://localhost:8000/api/docs](http://localhost:8000/api/docs),
 | 
			
		||||
    une interface swagger, avec toutes les routes de l'API.
 | 
			
		||||
 | 
			
		||||
!!! question "Pourquoi l'installation est aussi complexe ?"
 | 
			
		||||
 | 
			
		||||
    Cette question nous a été posée de nombreuses fois par des personnes
 | 
			
		||||
    essayant d'installer le projet.
 | 
			
		||||
    Il y a en effet un certain nombre d'étapes à suivre,
 | 
			
		||||
    de paquets à installer et de commandes à exécuter.
 | 
			
		||||
    
 | 
			
		||||
    Le processus d'installation peut donc sembler complexe.
 | 
			
		||||
 | 
			
		||||
    En réalité, il est difficile de faire plus simple.
 | 
			
		||||
    En effet, un site web a besoin de beaucoup de composants
 | 
			
		||||
    pour être développé : il lui faut au minimum
 | 
			
		||||
    une base de données, un cache, un bundler Javascript
 | 
			
		||||
    et un interpréteur pour le code du serveur.
 | 
			
		||||
    Pour nos besoin particuliers, nous utilisons également
 | 
			
		||||
    un moteur de recherche full-text.
 | 
			
		||||
    
 | 
			
		||||
    Nous avons tenté au maximum de limiter le nombre de dépendances
 | 
			
		||||
    et de sélecionner les plus simples à installer.
 | 
			
		||||
    Cependant, il est impossible de retirer l'intégralité
 | 
			
		||||
    de la complexité du processus.
 | 
			
		||||
    Si vous rencontrez des difficulté lors de l'installation,
 | 
			
		||||
    n'hésitez pas à demander de l'aide.
 | 
			
		||||
 | 
			
		||||
## Générer la documentation
 | 
			
		||||
 | 
			
		||||
La documentation est automatiquement mise en ligne à chaque envoi de code sur GitHub.
 | 
			
		||||
 
 | 
			
		||||
@@ -72,14 +72,12 @@ sith/
 | 
			
		||||
├── .gitattributes
 | 
			
		||||
├── .gitignore
 | 
			
		||||
├── .mailmap
 | 
			
		||||
├── .env (26)
 | 
			
		||||
├── .env.example (27)
 | 
			
		||||
├── manage.py (28)
 | 
			
		||||
├── mkdocs.yml (29)
 | 
			
		||||
├── manage.py (26)
 | 
			
		||||
├── mkdocs.yml (27)
 | 
			
		||||
├── uv.lock
 | 
			
		||||
├── pyproject.toml (30)
 | 
			
		||||
├── .venv/ (31)
 | 
			
		||||
├── .python-version (32)
 | 
			
		||||
├── pyproject.toml (28)
 | 
			
		||||
├── .venv/ (29)
 | 
			
		||||
├── .python-version (30)
 | 
			
		||||
└── README.md
 | 
			
		||||
```
 | 
			
		||||
</div>
 | 
			
		||||
@@ -123,19 +121,15 @@ sith/
 | 
			
		||||
    de manière transparente pour l'utilisateur. 
 | 
			
		||||
24. Fichier de configuration de coverage. 
 | 
			
		||||
25. Fichier de configuration de direnv. 
 | 
			
		||||
26. Contient les variables d'environnement, qui sont susceptibles
 | 
			
		||||
    de varier d'une machine à l'autre.
 | 
			
		||||
27. Contient des valeurs par défaut pour le `.env`
 | 
			
		||||
    pouvant convenir à un environnment de développement local
 | 
			
		||||
28. Fichier généré automatiquement par Django. C'est lui
 | 
			
		||||
26. Fichier généré automatiquement par Django. C'est lui
 | 
			
		||||
    qui permet d'appeler des commandes de gestion du projet
 | 
			
		||||
    avec la syntaxe `python ./manage.py <nom de la commande>`
 | 
			
		||||
29. Le fichier de configuration de la documentation,
 | 
			
		||||
27. Le fichier de configuration de la documentation,
 | 
			
		||||
    avec ses plugins et sa table des matières. 
 | 
			
		||||
30. Le fichier où sont déclarés les dépendances et la configuration
 | 
			
		||||
28. Le fichier où sont déclarés les dépendances et la configuration
 | 
			
		||||
    de certaines d'entre elles.
 | 
			
		||||
31. Dossier d'environnement virtuel généré par uv
 | 
			
		||||
32. Fichier qui contrôle quelle version de python utiliser pour le projet
 | 
			
		||||
29. Dossier d'environnement virtuel généré par uv
 | 
			
		||||
30. Fichier qui contrôle quel version de python utiliser pour le projet
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
## L'application principale
 | 
			
		||||
@@ -150,9 +144,10 @@ Il est organisé comme suit :
 | 
			
		||||
```
 | 
			
		||||
sith/
 | 
			
		||||
├── settings.py (1)
 | 
			
		||||
├── toolbar_debug.py (2)
 | 
			
		||||
├── urls.py (3)
 | 
			
		||||
└── wsgi.py (4)
 | 
			
		||||
├── settings_custom.py (2)
 | 
			
		||||
├── toolbar_debug.py (3)
 | 
			
		||||
├── urls.py (4)
 | 
			
		||||
└── wsgi.py (5)
 | 
			
		||||
```
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@@ -160,10 +155,13 @@ sith/
 | 
			
		||||
   Ce fichier contient les paramètres de configuration du projet.
 | 
			
		||||
   Par exemple, il contient la liste des applications
 | 
			
		||||
   installées dans le projet.
 | 
			
		||||
2. Configuration de la barre de debug.
 | 
			
		||||
2. Configuration maison pour votre environnement.
 | 
			
		||||
   Toute variable que vous définissez dans ce fichier sera prioritaire
 | 
			
		||||
   sur la configuration donnée dans `settings.py`.
 | 
			
		||||
3. Configuration de la barre de debug.
 | 
			
		||||
   C'est inutilisé en prod, mais c'est très pratique en développement.
 | 
			
		||||
3. Fichier de configuration des urls du projet.
 | 
			
		||||
4. Fichier de configuration pour le serveur WSGI.
 | 
			
		||||
4. Fichier de configuration des urls du projet.
 | 
			
		||||
5. Fichier de configuration pour le serveur WSGI.
 | 
			
		||||
   WSGI est un protocole de communication entre le serveur
 | 
			
		||||
   et les applications.
 | 
			
		||||
   Ce fichier ne vous servira sans doute pas sur un environnement
 | 
			
		||||
 
 | 
			
		||||
@@ -15,8 +15,8 @@ $min_col_width: 100px;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  gap: $gap;
 | 
			
		||||
 | 
			
		||||
  >input,
 | 
			
		||||
  >label {
 | 
			
		||||
  > input,
 | 
			
		||||
  > label {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -25,12 +25,12 @@ $min_col_width: 100px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#page #content {
 | 
			
		||||
  overflow-x: scroll;
 | 
			
		||||
.election_vote {
 | 
			
		||||
  overflow-x: scroll !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.election_table {
 | 
			
		||||
  width: inherit;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
 | 
			
		||||
  >.lists {
 | 
			
		||||
    display: flex;
 | 
			
		||||
@@ -93,30 +93,16 @@ $min_col_width: 100px;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        justify-content: space-between;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        row-gap: 10px;
 | 
			
		||||
        padding: $padding;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        >.role_text {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          flex-direction: column;
 | 
			
		||||
 | 
			
		||||
          >h4 {
 | 
			
		||||
            margin: 0;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          .role_description {
 | 
			
		||||
            flex-grow: 1;
 | 
			
		||||
          >p {
 | 
			
		||||
            margin-top: .5em;
 | 
			
		||||
            text-wrap: auto;
 | 
			
		||||
            text-align: left;
 | 
			
		||||
 | 
			
		||||
            // Show more/less element
 | 
			
		||||
            a {
 | 
			
		||||
              text-align: center;
 | 
			
		||||
              display: block;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -126,9 +112,9 @@ $min_col_width: 100px;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
          gap: $gap;
 | 
			
		||||
 | 
			
		||||
          >button,
 | 
			
		||||
          >button>i,
 | 
			
		||||
          >a {
 | 
			
		||||
          > button,
 | 
			
		||||
          > button > i,
 | 
			
		||||
          > a {
 | 
			
		||||
            width: 20px;
 | 
			
		||||
            height: 20px;
 | 
			
		||||
            background-color: #e9e9e9;
 | 
			
		||||
@@ -141,23 +127,23 @@ $min_col_width: 100px;
 | 
			
		||||
            justify-content: center;
 | 
			
		||||
 | 
			
		||||
            &:hover,
 | 
			
		||||
            &:hover>i {
 | 
			
		||||
            &:hover > i {
 | 
			
		||||
              background-color: #fff;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          >button {
 | 
			
		||||
          > button {
 | 
			
		||||
            width: 30px;
 | 
			
		||||
            height: 30px;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          >button[disabled] {
 | 
			
		||||
          > button[disabled] {
 | 
			
		||||
            background-color: #eee;
 | 
			
		||||
            cursor: not-allowed;
 | 
			
		||||
 | 
			
		||||
            >i,
 | 
			
		||||
            &:hover,
 | 
			
		||||
            &:hover>i {
 | 
			
		||||
            &:hover > i {
 | 
			
		||||
              background-color: #eee;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
@@ -192,12 +178,12 @@ $min_col_width: 100px;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            gap: $gap;
 | 
			
		||||
 | 
			
		||||
            >input[type="radio"]:checked+label,
 | 
			
		||||
            >input[type="checkbox"]:checked+label {
 | 
			
		||||
            >input[type="radio"]:checked + label,
 | 
			
		||||
            >input[type="checkbox"]:checked + label {
 | 
			
		||||
              background-color: lightgray;
 | 
			
		||||
              border-radius: 10px;
 | 
			
		||||
 | 
			
		||||
              >figure>.edit_btns>a:hover {
 | 
			
		||||
              >figure>.edit_btns>a:hover{
 | 
			
		||||
                background-color: #fff;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
@@ -229,9 +215,7 @@ $min_col_width: 100px;
 | 
			
		||||
                  margin: 0;
 | 
			
		||||
                  text-align: center;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                .candidate_program {
 | 
			
		||||
                  text-wrap: auto;
 | 
			
		||||
                  margin: 5px 0;
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
@@ -244,7 +228,7 @@ $min_col_width: 100px;
 | 
			
		||||
                right: $gap;
 | 
			
		||||
                gap: $gap;
 | 
			
		||||
 | 
			
		||||
                >a {
 | 
			
		||||
                > a {
 | 
			
		||||
                  width: 20px;
 | 
			
		||||
                  height: 20px;
 | 
			
		||||
                  background-color: #e9e9e9;
 | 
			
		||||
@@ -269,22 +253,20 @@ $min_col_width: 100px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#content {
 | 
			
		||||
 | 
			
		||||
  .election_details {
 | 
			
		||||
.election_details {
 | 
			
		||||
  margin: .5em 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  .buttons {
 | 
			
		||||
.buttons {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  gap: $gap;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  .button {
 | 
			
		||||
.button {
 | 
			
		||||
  border: none;
 | 
			
		||||
  color: black;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
@@ -303,10 +285,8 @@ $min_col_width: 100px;
 | 
			
		||||
 | 
			
		||||
  &_send {
 | 
			
		||||
    background-color: #59aee2;
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background-color: rgb(130, 186, 235);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,12 +4,13 @@
 | 
			
		||||
  {{ object.title }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block additional_js %}
 | 
			
		||||
  <script type="module" src="{{ static('bundled/core/read-more-index.ts') }}"></script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
{% block head %}
 | 
			
		||||
  {{ super() -}}
 | 
			
		||||
  <link rel="stylesheet" href="{{ static('election/css/election.scss') }}">
 | 
			
		||||
{%- endblock %}
 | 
			
		||||
 | 
			
		||||
{% block additional_css %}
 | 
			
		||||
  <link rel="stylesheet" href="{{ static('election/css/election.scss') }}">
 | 
			
		||||
  <script src="{{ static('bundled/vendored/jquery.shorten.min.js') }}"></script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
@@ -46,6 +47,7 @@
 | 
			
		||||
      {% csrf_token %}
 | 
			
		||||
      <table class="election_table">
 | 
			
		||||
        {%- set election_lists = election.election_lists.all() -%}
 | 
			
		||||
        <caption></caption>
 | 
			
		||||
        <thead class="lists">
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th class="column" style="width: {{ 100 / (election_lists.count() + 1) }}%">{% trans %}Blank vote{% endtrans %}</th>
 | 
			
		||||
@@ -68,7 +70,7 @@
 | 
			
		||||
              <td class="role_title">
 | 
			
		||||
                <div class="role_text">
 | 
			
		||||
                  <h4>{{ role.title }}</h4>
 | 
			
		||||
                  <p class="role_description" show-more="300">{{ role.description }}</p>
 | 
			
		||||
                  <p class="role_description">{{ role.description }}</p>
 | 
			
		||||
                  {%- if role.max_choice > 1 and not election.has_voted(user) and election.can_vote(user) %}
 | 
			
		||||
                    <strong>{% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %}</strong>
 | 
			
		||||
                  {%- endif %}
 | 
			
		||||
@@ -139,9 +141,7 @@
 | 
			
		||||
                          <figcaption class="candidate__details">
 | 
			
		||||
                            <h5>{{ candidature.user.first_name }} <em>{{candidature.user.nick_name or ''}} </em>{{ candidature.user.last_name }}</h5>
 | 
			
		||||
                            {%- if not election.is_vote_finished %}
 | 
			
		||||
                              <q class="candidate_program" show-more="200">
 | 
			
		||||
                                {{ candidature.program|markdown or '' }}
 | 
			
		||||
                              </q>
 | 
			
		||||
                              <q class="candidate_program">{{ candidature.program | markdown or '' }}</q>
 | 
			
		||||
                            {%- endif %}
 | 
			
		||||
                          </figcaption>
 | 
			
		||||
                          {%- if user.can_edit(candidature) -%}
 | 
			
		||||
@@ -200,6 +200,18 @@
 | 
			
		||||
 | 
			
		||||
{% block script %}
 | 
			
		||||
  {{ super() }}
 | 
			
		||||
  <script type="text/javascript">
 | 
			
		||||
    $('.role_description').shorten({
 | 
			
		||||
      moreText: "{% trans %}Show more{% endtrans %}",
 | 
			
		||||
      lessText: "{% trans %}Show less{% endtrans %}",
 | 
			
		||||
      showChars: 50
 | 
			
		||||
    });
 | 
			
		||||
    $('.candidate_program').shorten({
 | 
			
		||||
      moreText: "{% trans %}Show more{% endtrans %}",
 | 
			
		||||
      lessText: "{% trans %}Show less{% endtrans %}",
 | 
			
		||||
      showChars: 200
 | 
			
		||||
    });
 | 
			
		||||
  </script>
 | 
			
		||||
  <script type="text/javascript">
 | 
			
		||||
    document.querySelectorAll('.role__multiple-choices').forEach(setupRestrictions);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Report-Msgid-Bugs-To: \n"
 | 
			
		||||
"POT-Creation-Date: 2025-01-08 12:23+0100\n"
 | 
			
		||||
"POT-Creation-Date: 2025-01-05 16:39+0100\n"
 | 
			
		||||
"PO-Revision-Date: 2016-07-18\n"
 | 
			
		||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
 | 
			
		||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
 | 
			
		||||
@@ -935,10 +935,6 @@ msgstr "rôle"
 | 
			
		||||
msgid "description"
 | 
			
		||||
msgstr "description"
 | 
			
		||||
 | 
			
		||||
#: club/models.py
 | 
			
		||||
msgid "past member"
 | 
			
		||||
msgstr "ancien membre"
 | 
			
		||||
 | 
			
		||||
#: club/models.py
 | 
			
		||||
msgid "Email address"
 | 
			
		||||
msgstr "Adresse email"
 | 
			
		||||
@@ -2066,12 +2062,16 @@ msgid "reason"
 | 
			
		||||
msgstr "raison"
 | 
			
		||||
 | 
			
		||||
#: core/models.py
 | 
			
		||||
#, fuzzy
 | 
			
		||||
#| msgid "user"
 | 
			
		||||
msgid "user ban"
 | 
			
		||||
msgstr "utilisateur banni"
 | 
			
		||||
msgstr "utilisateur"
 | 
			
		||||
 | 
			
		||||
#: core/models.py
 | 
			
		||||
#, fuzzy
 | 
			
		||||
#| msgid "user"
 | 
			
		||||
msgid "user bans"
 | 
			
		||||
msgstr "utilisateurs bannis"
 | 
			
		||||
msgstr "utilisateur"
 | 
			
		||||
 | 
			
		||||
#: core/models.py
 | 
			
		||||
msgid "receive the Weekmail"
 | 
			
		||||
@@ -3328,8 +3328,8 @@ msgstr "Nom d'utilisateur, email, ou numéro de compte AE"
 | 
			
		||||
 | 
			
		||||
#: core/views/forms.py
 | 
			
		||||
msgid ""
 | 
			
		||||
"Profile: you need to be visible on the picture, in order to be recognized "
 | 
			
		||||
"(e.g. by the barmen)"
 | 
			
		||||
"Profile: you need to be visible on the picture, in order to be recognized (e."
 | 
			
		||||
"g. by the barmen)"
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Photo de profil: vous devez être visible sur la photo afin d'être reconnu "
 | 
			
		||||
"(par exemple par les barmen)"
 | 
			
		||||
@@ -3935,8 +3935,8 @@ msgstr ""
 | 
			
		||||
#: counter/templates/counter/mails/account_dump.jinja
 | 
			
		||||
msgid "If you think this was a mistake, please mail us at ae@utbm.fr."
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Si vous pensez qu'il s'agit d'une erreur, veuillez envoyer un mail à "
 | 
			
		||||
"ae@utbm.fr."
 | 
			
		||||
"Si vous pensez qu'il s'agit d'une erreur, veuillez envoyer un mail à ae@utbm."
 | 
			
		||||
"fr."
 | 
			
		||||
 | 
			
		||||
#: counter/templates/counter/mails/account_dump.jinja
 | 
			
		||||
msgid ""
 | 
			
		||||
@@ -4456,6 +4456,14 @@ msgstr "Ajouter un nouveau rôle"
 | 
			
		||||
msgid "Submit the vote !"
 | 
			
		||||
msgstr "Envoyer le vote !"
 | 
			
		||||
 | 
			
		||||
#: election/templates/election/election_detail.jinja
 | 
			
		||||
msgid "Show more"
 | 
			
		||||
msgstr "Montrer plus"
 | 
			
		||||
 | 
			
		||||
#: election/templates/election/election_detail.jinja
 | 
			
		||||
msgid "Show less"
 | 
			
		||||
msgstr "Montrer moins"
 | 
			
		||||
 | 
			
		||||
#: election/templates/election/election_list.jinja
 | 
			
		||||
msgid "Election list"
 | 
			
		||||
msgstr "Liste des élections"
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Report-Msgid-Bugs-To: \n"
 | 
			
		||||
"POT-Creation-Date: 2025-01-08 12:23+0100\n"
 | 
			
		||||
"POT-Creation-Date: 2025-01-04 23:07+0100\n"
 | 
			
		||||
"PO-Revision-Date: 2024-09-17 11:54+0200\n"
 | 
			
		||||
"Last-Translator: Sli <antoine@bartuccio.fr>\n"
 | 
			
		||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
 | 
			
		||||
@@ -113,14 +113,6 @@ msgstr "Guide markdown"
 | 
			
		||||
msgid "Unsupported NFC card"
 | 
			
		||||
msgstr "Carte NFC non supportée"
 | 
			
		||||
 | 
			
		||||
#: core/static/bundled/core/read-more-index.ts
 | 
			
		||||
msgid "Show less"
 | 
			
		||||
msgstr "Montrer moins"
 | 
			
		||||
 | 
			
		||||
#: core/static/bundled/core/read-more-index.ts
 | 
			
		||||
msgid "Show more"
 | 
			
		||||
msgstr "Montrer plus"
 | 
			
		||||
 | 
			
		||||
#: core/static/bundled/user/family-graph-index.js
 | 
			
		||||
msgid "family_tree.%(extension)s"
 | 
			
		||||
msgstr "arbre_genealogique.%(extension)s"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -10,7 +10,6 @@
 | 
			
		||||
      "license": "GPL-3.0-only",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@alpinejs/sort": "^3.14.7",
 | 
			
		||||
        "@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
 | 
			
		||||
        "@fortawesome/fontawesome-free": "^6.6.0",
 | 
			
		||||
        "@fullcalendar/core": "^6.1.15",
 | 
			
		||||
        "@fullcalendar/daygrid": "^6.1.15",
 | 
			
		||||
@@ -31,6 +30,7 @@
 | 
			
		||||
        "htmx.org": "^2.0.3",
 | 
			
		||||
        "jquery": "^3.7.1",
 | 
			
		||||
        "jquery-ui": "^1.14.0",
 | 
			
		||||
        "jquery.shorten": "^1.0.0",
 | 
			
		||||
        "native-file-system-adapter": "^3.0.1",
 | 
			
		||||
        "three": "^0.169.0",
 | 
			
		||||
        "three-spritetext": "^1.9.0",
 | 
			
		||||
@@ -85,12 +85,6 @@
 | 
			
		||||
        "url": "https://github.com/sponsors/philsturgeon"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@arendjr/text-clipper": {
 | 
			
		||||
      "name": "@jsr/arendjr__text-clipper",
 | 
			
		||||
      "version": "3.0.0",
 | 
			
		||||
      "resolved": "https://npm.jsr.io/~/11/@jsr/arendjr__text-clipper/3.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-Uu3CYSvFrNdDkYKEaEKHAk0decaxVFlSSqf50Okte/9vJjO2rESzPF1ngQjS9H1aX45RIXRGMYOXJ/LPDFwUdQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@babel/code-frame": {
 | 
			
		||||
      "version": "7.24.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
 | 
			
		||||
@@ -4398,6 +4392,11 @@
 | 
			
		||||
        "jquery": ">=1.12.0 <5.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/jquery.shorten": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/jquery.shorten/-/jquery.shorten-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-49rJlpcyVI/Y2eQRwSexyz6l+fwTKfurO0XttXK4XnG9eQxIuE2Fb4rwNqnsnzStJ8M7ynlhH31fWE9P70B9rg=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/js-tokens": {
 | 
			
		||||
      "version": "4.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,6 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@alpinejs/sort": "^3.14.7",
 | 
			
		||||
    "@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
 | 
			
		||||
    "@fortawesome/fontawesome-free": "^6.6.0",
 | 
			
		||||
    "@fullcalendar/core": "^6.1.15",
 | 
			
		||||
    "@fullcalendar/daygrid": "^6.1.15",
 | 
			
		||||
@@ -57,6 +56,7 @@
 | 
			
		||||
    "htmx.org": "^2.0.3",
 | 
			
		||||
    "jquery": "^3.7.1",
 | 
			
		||||
    "jquery-ui": "^1.14.0",
 | 
			
		||||
    "jquery.shorten": "^1.0.0",
 | 
			
		||||
    "native-file-system-adapter": "^3.0.1",
 | 
			
		||||
    "three": "^0.169.0",
 | 
			
		||||
    "three-spritetext": "^1.9.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -44,8 +44,6 @@ dependencies = [
 | 
			
		||||
    "django-honeypot<2.0.0,>=1.2.1",
 | 
			
		||||
    "pydantic-extra-types<3.0.0,>=2.10.1",
 | 
			
		||||
    "ical<9.0.0,>=8.3.0",
 | 
			
		||||
    "redis[hiredis]<6.0.0,>=5.2.0",
 | 
			
		||||
    "environs[django]<15.0.0,>=14.1.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[project.urls]
 | 
			
		||||
@@ -55,6 +53,7 @@ documentation = "https://sith-ae.readthedocs.io/"
 | 
			
		||||
[dependency-groups]
 | 
			
		||||
prod = [
 | 
			
		||||
    "psycopg[c]<4.0.0,>=3.2.3",
 | 
			
		||||
    "redis[hiredis]<6.0.0,>=5.2.0",
 | 
			
		||||
]
 | 
			
		||||
dev = [
 | 
			
		||||
    "django-debug-toolbar<5.0.0,>=4.4.6",
 | 
			
		||||
@@ -85,6 +84,8 @@ default-groups = ["dev", "tests", "docs"]
 | 
			
		||||
 | 
			
		||||
[tool.xapian]
 | 
			
		||||
version = "1.4.25"
 | 
			
		||||
core-sha1 = "e2b4b4cf6076873ec9402cab7b9a3b71dcf95e20"
 | 
			
		||||
bindings-sha1 = "782f568d2ea3ca751c519a2814a35c7dc86df3a4"
 | 
			
		||||
 | 
			
		||||
[tool.ruff]
 | 
			
		||||
output-format = "concise" # makes ruff error logs easier to read
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
  {% if user.has_perm("core.add_userban") %}
 | 
			
		||||
  {% if user.has_perm("core:add_userban") %}
 | 
			
		||||
    <a href="{{ url("rootplace:ban_create") }}" class="btn btn-red margin-bottom">
 | 
			
		||||
      <i class="fa fa-person-circle-xmark"></i>
 | 
			
		||||
      {% trans %}Ban a user{% endtrans %}
 | 
			
		||||
@@ -44,7 +44,7 @@
 | 
			
		||||
          <summary class="clickable">{% trans %}Reason{% endtrans %}</summary>
 | 
			
		||||
          <p>{{ user_ban.reason }}</p>
 | 
			
		||||
        </details>
 | 
			
		||||
        {% if user.has_perm("core.delete_userban") %}
 | 
			
		||||
        {% if user.has_perm("core:delete_userban") %}
 | 
			
		||||
          <span>
 | 
			
		||||
            <a
 | 
			
		||||
              href="{{ url("rootplace:ban_remove", ban_id=user_ban.id) }}"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										153
									
								
								sith/settings.py
									
									
									
									
									
								
							
							
						
						
									
										153
									
								
								sith/settings.py
									
									
									
									
									
								
							@@ -34,6 +34,7 @@ https://docs.djangoproject.com/en/1.8/ref/settings/
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import binascii
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
@@ -42,32 +43,25 @@ from pathlib import Path
 | 
			
		||||
import sentry_sdk
 | 
			
		||||
from dateutil.relativedelta import relativedelta
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from environs import Env
 | 
			
		||||
from sentry_sdk.integrations.django import DjangoIntegration
 | 
			
		||||
 | 
			
		||||
from .honeypot import custom_honeypot_error
 | 
			
		||||
 | 
			
		||||
env = Env()
 | 
			
		||||
env.read_env()
 | 
			
		||||
 | 
			
		||||
BASE_DIR = Path(__file__).parent.parent.resolve()
 | 
			
		||||
 | 
			
		||||
os.environ["HTTPS"] = "off"
 | 
			
		||||
 | 
			
		||||
# Quick-start development settings - unsuitable for production
 | 
			
		||||
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
 | 
			
		||||
 | 
			
		||||
# SECURITY WARNING: keep the secret key used in production secret!
 | 
			
		||||
SECRET_KEY = env.str("SECRET_KEY")
 | 
			
		||||
SECRET_KEY = "(4sjxvhz@m5$0a$j0_pqicnc$s!vbve)z+&++m%g%bjhlz4+g2"
 | 
			
		||||
 | 
			
		||||
# SECURITY WARNING: don't run with debug turned on in production!
 | 
			
		||||
DEBUG = env.bool("DEBUG", default=False)
 | 
			
		||||
DEBUG = False
 | 
			
		||||
TESTING = "pytest" in sys.modules
 | 
			
		||||
INTERNAL_IPS = ["127.0.0.1"]
 | 
			
		||||
 | 
			
		||||
# force csrf tokens and cookies to be secure when in https
 | 
			
		||||
CSRF_COOKIE_SECURE = env.bool("HTTPS", default=True)
 | 
			
		||||
SESSION_COOKIE_SECURE = env.bool("HTTPS", default=True)
 | 
			
		||||
X_FRAME_OPTIONS = "SAMEORIGIN"
 | 
			
		||||
 | 
			
		||||
ALLOWED_HOSTS = ["*"]
 | 
			
		||||
 | 
			
		||||
# Application definition
 | 
			
		||||
@@ -214,12 +208,12 @@ WSGI_APPLICATION = "sith.wsgi.application"
 | 
			
		||||
# Database
 | 
			
		||||
 | 
			
		||||
DATABASES = {
 | 
			
		||||
    "default": env.dj_db_url("DATABASE_URL", conn_max_age=None, conn_health_checks=True)
 | 
			
		||||
    "default": {
 | 
			
		||||
        "ENGINE": "django.db.backends.sqlite3",
 | 
			
		||||
        "NAME": BASE_DIR / "db.sqlite3",
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if "CACHE_URL" in os.environ:
 | 
			
		||||
    CACHES = {"default": env.dj_cache_url("CACHE_URL")}
 | 
			
		||||
 | 
			
		||||
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
 | 
			
		||||
 | 
			
		||||
# Logging
 | 
			
		||||
@@ -271,13 +265,13 @@ PHONENUMBER_DEFAULT_REGION = "FR"
 | 
			
		||||
 | 
			
		||||
# Medias
 | 
			
		||||
MEDIA_URL = "/data/"
 | 
			
		||||
MEDIA_ROOT = env.path("MEDIA_ROOT", default=BASE_DIR / "data")
 | 
			
		||||
MEDIA_ROOT = BASE_DIR / "data"
 | 
			
		||||
 | 
			
		||||
# Static files (CSS, JavaScript, Images)
 | 
			
		||||
# https://docs.djangoproject.com/en/1.8/howto/static-files/
 | 
			
		||||
 | 
			
		||||
STATIC_URL = "/static/"
 | 
			
		||||
STATIC_ROOT = env.path("STATIC_ROOT", default=BASE_DIR / "static")
 | 
			
		||||
STATIC_ROOT = BASE_DIR / "static"
 | 
			
		||||
 | 
			
		||||
# Static files finders which allow to see static folder in all apps
 | 
			
		||||
STATICFILES_FINDERS = [
 | 
			
		||||
@@ -301,28 +295,24 @@ AUTHENTICATION_BACKENDS = ["core.auth_backends.SithModelBackend"]
 | 
			
		||||
LOGIN_URL = "/login"
 | 
			
		||||
LOGOUT_URL = "/logout"
 | 
			
		||||
LOGIN_REDIRECT_URL = "/"
 | 
			
		||||
DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL", default="bibou@git.an")
 | 
			
		||||
SITH_COM_EMAIL = env.str("SITH_COM_EMAIL", default="bibou_com@git.an")
 | 
			
		||||
DEFAULT_FROM_EMAIL = "bibou@git.an"
 | 
			
		||||
SITH_COM_EMAIL = "bibou_com@git.an"
 | 
			
		||||
 | 
			
		||||
# Those values are to be changed in production to be more effective
 | 
			
		||||
HONEYPOT_FIELD_NAME = env.str("HONEYPOT_FIELD_NAME", default="body2")
 | 
			
		||||
HONEYPOT_VALUE = env.str("HONEYPOT_VALUE", default="content")
 | 
			
		||||
HONEYPOT_FIELD_NAME = "body2"
 | 
			
		||||
HONEYPOT_VALUE = "content"
 | 
			
		||||
HONEYPOT_RESPONDER = custom_honeypot_error  # Make honeypot errors less suspicious
 | 
			
		||||
HONEYPOT_FIELD_NAME_FORUM = env.str(
 | 
			
		||||
    "HONEYPOT_FIELD_NAME_FORUM", default="message2"
 | 
			
		||||
)  # Only used on forum
 | 
			
		||||
HONEYPOT_FIELD_NAME_FORUM = "message2"  # Only used on forum
 | 
			
		||||
 | 
			
		||||
# Email
 | 
			
		||||
EMAIL_BACKEND = env.str(
 | 
			
		||||
    "EMAIL_BACKEND", default="django.core.mail.backends.dummy.EmailBackend"
 | 
			
		||||
)
 | 
			
		||||
EMAIL_HOST = env.str("EMAIL_HOST", default="localhost")
 | 
			
		||||
EMAIL_PORT = env.int("EMAIL_PORT", default=25)
 | 
			
		||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
 | 
			
		||||
EMAIL_HOST = "localhost"
 | 
			
		||||
EMAIL_PORT = 25
 | 
			
		||||
 | 
			
		||||
# Below this line, only Sith-specific variables are defined
 | 
			
		||||
 | 
			
		||||
SITH_URL = env.str("SITH_URL", default="127.0.0.1:8000")
 | 
			
		||||
SITH_NAME = env.str("SITH_NAME", default="AE UTBM")
 | 
			
		||||
SITH_URL = "my.url.git.an"
 | 
			
		||||
SITH_NAME = "Sith website"
 | 
			
		||||
SITH_TWITTER = "@ae_utbm"
 | 
			
		||||
 | 
			
		||||
# Enable experimental features
 | 
			
		||||
@@ -331,7 +321,7 @@ SITH_ENABLE_GALAXY = False
 | 
			
		||||
 | 
			
		||||
# AE configuration
 | 
			
		||||
# TODO: keep only that first setting, with the ID, and do the same for the other clubs
 | 
			
		||||
SITH_MAIN_CLUB_ID = env.int("SITH_MAIN_CLUB_ID", default=1)
 | 
			
		||||
SITH_MAIN_CLUB_ID = 1
 | 
			
		||||
SITH_MAIN_CLUB = {
 | 
			
		||||
    "name": "AE",
 | 
			
		||||
    "unix_name": "ae",
 | 
			
		||||
@@ -366,28 +356,26 @@ SITH_SCHOOL_START_YEAR = 1999
 | 
			
		||||
# id of the Root account
 | 
			
		||||
SITH_ROOT_USER_ID = 0
 | 
			
		||||
 | 
			
		||||
SITH_GROUP_ROOT_ID = env.int("SITH_GROUP_ROOT_ID", default=1)
 | 
			
		||||
SITH_GROUP_PUBLIC_ID = env.int("SITH_GROUP_PUBLIC_ID", default=2)
 | 
			
		||||
SITH_GROUP_SUBSCRIBERS_ID = env.int("SITH_GROUP_SUBSCRIBERS_ID", default=3)
 | 
			
		||||
SITH_GROUP_OLD_SUBSCRIBERS_ID = env.int("SITH_GROUP_OLD_SUBSCRIBERS_ID", default=4)
 | 
			
		||||
SITH_GROUP_ACCOUNTING_ADMIN_ID = env.int("SITH_GROUP_ACCOUNTING_ADMIN_ID", default=5)
 | 
			
		||||
SITH_GROUP_COM_ADMIN_ID = env.int("SITH_GROUP_COM_ADMIN_ID", default=6)
 | 
			
		||||
SITH_GROUP_COUNTER_ADMIN_ID = env.int("SITH_GROUP_COUNTER_ADMIN_ID", default=7)
 | 
			
		||||
SITH_GROUP_SAS_ADMIN_ID = env.int("SITH_GROUP_SAS_ADMIN_ID", default=8)
 | 
			
		||||
SITH_GROUP_FORUM_ADMIN_ID = env.int("SITH_GROUP_FORUM_ADMIN_ID", default=9)
 | 
			
		||||
SITH_GROUP_PEDAGOGY_ADMIN_ID = env.int("SITH_GROUP_PEDAGOGY_ADMIN_ID", default=10)
 | 
			
		||||
SITH_GROUP_ROOT_ID = 1
 | 
			
		||||
SITH_GROUP_PUBLIC_ID = 2
 | 
			
		||||
SITH_GROUP_SUBSCRIBERS_ID = 3
 | 
			
		||||
SITH_GROUP_OLD_SUBSCRIBERS_ID = 4
 | 
			
		||||
SITH_GROUP_ACCOUNTING_ADMIN_ID = 5
 | 
			
		||||
SITH_GROUP_COM_ADMIN_ID = 6
 | 
			
		||||
SITH_GROUP_COUNTER_ADMIN_ID = 7
 | 
			
		||||
SITH_GROUP_SAS_ADMIN_ID = 8
 | 
			
		||||
SITH_GROUP_FORUM_ADMIN_ID = 9
 | 
			
		||||
SITH_GROUP_PEDAGOGY_ADMIN_ID = 10
 | 
			
		||||
 | 
			
		||||
SITH_GROUP_BANNED_ALCOHOL_ID = env.int("SITH_GROUP_BANNED_ALCOHOL_ID", default=11)
 | 
			
		||||
SITH_GROUP_BANNED_COUNTER_ID = env.int("SITH_GROUP_BANNED_COUNTER_ID", default=12)
 | 
			
		||||
SITH_GROUP_BANNED_SUBSCRIPTION_ID = env.int(
 | 
			
		||||
    "SITH_GROUP_BANNED_SUBSCRIPTION_ID", default=13
 | 
			
		||||
)
 | 
			
		||||
SITH_GROUP_BANNED_ALCOHOL_ID = 11
 | 
			
		||||
SITH_GROUP_BANNED_COUNTER_ID = 12
 | 
			
		||||
SITH_GROUP_BANNED_SUBSCRIPTION_ID = 13
 | 
			
		||||
 | 
			
		||||
SITH_CLUB_REFOUND_ID = env.int("SITH_CLUB_REFOUND_ID", default=89)
 | 
			
		||||
SITH_COUNTER_REFOUND_ID = env.int("SITH_COUNTER_REFOUND_ID", default=38)
 | 
			
		||||
SITH_PRODUCT_REFOUND_ID = env.int("SITH_PRODUCT_REFOUND_ID", default=5)
 | 
			
		||||
SITH_CLUB_REFOUND_ID = 89
 | 
			
		||||
SITH_COUNTER_REFOUND_ID = 38
 | 
			
		||||
SITH_PRODUCT_REFOUND_ID = 5
 | 
			
		||||
 | 
			
		||||
SITH_COUNTER_ACCOUNT_DUMP_ID = env.int("SITH_COUNTER_ACCOUNT_DUMP_ID", default=39)
 | 
			
		||||
SITH_COUNTER_ACCOUNT_DUMP_ID = 39
 | 
			
		||||
 | 
			
		||||
# Pages
 | 
			
		||||
SITH_CORE_PAGE_SYNTAX = "Aide_sur_la_syntaxe"
 | 
			
		||||
@@ -397,7 +385,7 @@ SITH_CORE_PAGE_SYNTAX = "Aide_sur_la_syntaxe"
 | 
			
		||||
SITH_FORUM_PAGE_LENGTH = 30
 | 
			
		||||
 | 
			
		||||
# SAS variables
 | 
			
		||||
SITH_SAS_ROOT_DIR_ID = env.int("SITH_SAS_ROOT_DIR_ID", default=4)
 | 
			
		||||
SITH_SAS_ROOT_DIR_ID = 4
 | 
			
		||||
SITH_SAS_IMAGES_PER_PAGE = 60
 | 
			
		||||
 | 
			
		||||
SITH_BOARD_SUFFIX = "-bureau"
 | 
			
		||||
@@ -504,9 +492,9 @@ SITH_LOG_OPERATION_TYPE = [
 | 
			
		||||
 | 
			
		||||
SITH_PEDAGOGY_UTBM_API = "https://extranet1.utbm.fr/gpedago/api/guide"
 | 
			
		||||
 | 
			
		||||
SITH_ECOCUP_CONS = env.int("SITH_ECOCUP_CONS", default=1151)
 | 
			
		||||
SITH_ECOCUP_CONS = 1152
 | 
			
		||||
 | 
			
		||||
SITH_ECOCUP_DECO = env.int("SITH_ECOCUP_DECO", default=1152)
 | 
			
		||||
SITH_ECOCUP_DECO = 1151
 | 
			
		||||
 | 
			
		||||
# The limit is the maximum difference between cons and deco possible for a customer
 | 
			
		||||
SITH_ECOCUP_LIMIT = 3
 | 
			
		||||
@@ -521,31 +509,21 @@ SITH_ACCOUNT_DUMP_DELTA = timedelta(days=30)
 | 
			
		||||
 | 
			
		||||
# Defines which product type is the refilling type,
 | 
			
		||||
# and thus increases the account amount
 | 
			
		||||
SITH_COUNTER_PRODUCTTYPE_REFILLING = env.int(
 | 
			
		||||
    "SITH_COUNTER_PRODUCTTYPE_REFILLING", default=3
 | 
			
		||||
)
 | 
			
		||||
SITH_COUNTER_PRODUCTTYPE_REFILLING = 3
 | 
			
		||||
 | 
			
		||||
# Defines which product is the one year subscription
 | 
			
		||||
# and which one is the six month subscription
 | 
			
		||||
SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER = env.int(
 | 
			
		||||
    "SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER", default=1
 | 
			
		||||
)
 | 
			
		||||
SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS = env.int(
 | 
			
		||||
    "SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS", default=2
 | 
			
		||||
)
 | 
			
		||||
SITH_PRODUCTTYPE_SUBSCRIPTION = env.int("SITH_PRODUCTTYPE_SUBSCRIPTION", default=2)
 | 
			
		||||
SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER = 1
 | 
			
		||||
SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS = 2
 | 
			
		||||
SITH_PRODUCTTYPE_SUBSCRIPTION = 2
 | 
			
		||||
 | 
			
		||||
# Defines which club lets its member the ability to make subscriptions
 | 
			
		||||
# Elements of this list are club's id
 | 
			
		||||
SITH_CAN_CREATE_SUBSCRIPTIONS = env.list(
 | 
			
		||||
    "SITH_CAN_CREATE_SUBSCRIPTION_HISTORY", default=[1]
 | 
			
		||||
)
 | 
			
		||||
SITH_CAN_CREATE_SUBSCRIPTIONS = [1]
 | 
			
		||||
 | 
			
		||||
# Defines which clubs lets its members the ability to see users subscription history
 | 
			
		||||
# Elements of this list are club's id
 | 
			
		||||
SITH_CAN_READ_SUBSCRIPTION_HISTORY = env.list(
 | 
			
		||||
    "SITH_CAN_READ_SUBSCRIPTION_HISTORY", default=[1]
 | 
			
		||||
)
 | 
			
		||||
SITH_CAN_READ_SUBSCRIPTION_HISTORY = []
 | 
			
		||||
 | 
			
		||||
# Number of weeks before the end of a subscription when the subscriber can resubscribe
 | 
			
		||||
SITH_SUBSCRIPTION_END = 10
 | 
			
		||||
@@ -654,29 +632,21 @@ SITH_BARMAN_TIMEOUT = 30
 | 
			
		||||
SITH_LAST_OPERATIONS_LIMIT = 10
 | 
			
		||||
 | 
			
		||||
# ET variables
 | 
			
		||||
SITH_EBOUTIC_CB_ENABLED = env.bool("SITH_EBOUTIC_CB_ENABLED", default=True)
 | 
			
		||||
SITH_EBOUTIC_ET_URL = env.str(
 | 
			
		||||
    "SITH_EBOUTIC_ET_URL",
 | 
			
		||||
    default="https://preprod-tpeweb.e-transactions.fr/cgi/MYchoix_pagepaiement.cgi",
 | 
			
		||||
SITH_EBOUTIC_CB_ENABLED = True
 | 
			
		||||
SITH_EBOUTIC_ET_URL = (
 | 
			
		||||
    "https://preprod-tpeweb.e-transactions.fr/cgi/MYchoix_pagepaiement.cgi"
 | 
			
		||||
)
 | 
			
		||||
SITH_EBOUTIC_PBX_SITE = env.str("SITH_EBOUTIC_PBX_SITE", default="1999888")
 | 
			
		||||
SITH_EBOUTIC_PBX_RANG = env.str("SITH_EBOUTIC_PBX_RANG", default="32")
 | 
			
		||||
SITH_EBOUTIC_PBX_IDENTIFIANT = env.str("SITH_EBOUTIC_PBX_IDENTIFIANT", default="2")
 | 
			
		||||
SITH_EBOUTIC_PBX_SITE = "1999888"
 | 
			
		||||
SITH_EBOUTIC_PBX_RANG = "32"
 | 
			
		||||
SITH_EBOUTIC_PBX_IDENTIFIANT = "2"
 | 
			
		||||
SITH_EBOUTIC_HMAC_KEY = binascii.unhexlify(
 | 
			
		||||
    env.str(
 | 
			
		||||
        "SITH_EBOUTIC_HMAC_KEY",
 | 
			
		||||
        default=(
 | 
			
		||||
    "0123456789ABCDEF0123456789ABCDEF"
 | 
			
		||||
    "0123456789ABCDEF0123456789ABCDEF"
 | 
			
		||||
    "0123456789ABCDEF0123456789ABCDEF"
 | 
			
		||||
    "0123456789ABCDEF0123456789ABCDEF"
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
SITH_EBOUTIC_PUB_KEY = ""
 | 
			
		||||
with open(
 | 
			
		||||
    env.path("SITH_EBOUTIC_PUB_KEY_PATH", default=BASE_DIR / "sith/et_keys/pubkey.pem")
 | 
			
		||||
) as f:
 | 
			
		||||
with open(os.path.join(os.path.dirname(__file__), "et_keys/pubkey.pem")) as f:
 | 
			
		||||
    SITH_EBOUTIC_PUB_KEY = f.read()
 | 
			
		||||
 | 
			
		||||
# Launderette variables
 | 
			
		||||
@@ -718,17 +688,24 @@ SITH_QUICK_NOTIF = {
 | 
			
		||||
# Mailing related settings
 | 
			
		||||
 | 
			
		||||
SITH_MAILING_DOMAIN = "utbm.fr"
 | 
			
		||||
SITH_MAILING_FETCH_KEY = env.str("SITH_MAILING_FETCH_KEY", default="ILoveMails")
 | 
			
		||||
SITH_MAILING_FETCH_KEY = "IloveMails"
 | 
			
		||||
 | 
			
		||||
SITH_GIFT_LIST = [("AE Tee-shirt", _("AE tee-shirt"))]
 | 
			
		||||
 | 
			
		||||
SENTRY_DSN = env.str("SENRY_DSN", default=None)
 | 
			
		||||
SENTRY_ENV = env.str("SENTRY_ENV", default="production")
 | 
			
		||||
SENTRY_DSN = ""
 | 
			
		||||
SENTRY_ENV = "production"
 | 
			
		||||
 | 
			
		||||
TOXIC_DOMAINS_PROVIDERS = [
 | 
			
		||||
    "https://www.stopforumspam.com/downloads/toxic_domains_whole.txt",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    from .settings_custom import *  # noqa F403 (this star-import is actually useful)
 | 
			
		||||
 | 
			
		||||
    logging.getLogger("django").info("Custom settings imported")
 | 
			
		||||
except ImportError:
 | 
			
		||||
    logging.getLogger("django").warning("Custom settings failed")
 | 
			
		||||
 | 
			
		||||
if DEBUG:
 | 
			
		||||
    INSTALLED_APPS += ("debug_toolbar",)
 | 
			
		||||
    MIDDLEWARE = ("debug_toolbar.middleware.DebugToolbarMiddleware", *MIDDLEWARE)
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,7 @@ def sentry_debug(request):
 | 
			
		||||
    The error will be displayed on Sentry
 | 
			
		||||
    inside the "development" environment
 | 
			
		||||
 | 
			
		||||
    NOTE : you need to specify the SENTRY_DSN setting in .env
 | 
			
		||||
    NOTE : you need to specify the SENTRY_DSN setting in settings_custom.py
 | 
			
		||||
    """
 | 
			
		||||
    if settings.SENTRY_ENV != "development" or not settings.SENTRY_DSN:
 | 
			
		||||
        raise Http404
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
import subprocess
 | 
			
		||||
import platform
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from hashlib import sha1
 | 
			
		||||
from itertools import chain
 | 
			
		||||
@@ -94,7 +95,7 @@ class JSBundler:
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def compile():
 | 
			
		||||
        """Bundle js files with the javascript bundler for production."""
 | 
			
		||||
        process = subprocess.Popen(["npm", "run", "compile"])
 | 
			
		||||
        process = subprocess.Popen(["npm", "run", "compile"], shell=platform.system() == "Windows")
 | 
			
		||||
        process.wait()
 | 
			
		||||
        if process.returncode:
 | 
			
		||||
            raise RuntimeError(f"Bundler failed with returncode {process.returncode}")
 | 
			
		||||
@@ -103,7 +104,7 @@ class JSBundler:
 | 
			
		||||
    def runserver() -> subprocess.Popen:
 | 
			
		||||
        """Bundle js files automatically in background when called in debug mode."""
 | 
			
		||||
        logging.getLogger("django").info("Running javascript bundling server")
 | 
			
		||||
        return subprocess.Popen(["npm", "run", "serve"])
 | 
			
		||||
        return subprocess.Popen(["npm", "run", "serve"], shell=platform.system() == "Windows")
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def get_manifest() -> JSBundlerManifest:
 | 
			
		||||
@@ -197,4 +198,4 @@ class OpenApi:
 | 
			
		||||
        with open(out, "w") as f:
 | 
			
		||||
            _ = f.write(schema)
 | 
			
		||||
 | 
			
		||||
        subprocess.run(["npx", "openapi-ts"], check=True)
 | 
			
		||||
        subprocess.run(["npx", "openapi-ts"], check=True, shell=platform.system() == "Windows")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										86
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										86
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							@@ -155,7 +155,7 @@ name = "click"
 | 
			
		||||
version = "8.1.8"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
dependencies = [
 | 
			
		||||
    { name = "colorama", marker = "platform_system == 'Windows'" },
 | 
			
		||||
    { name = "colorama", marker = "sys_platform == 'win32'" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
 | 
			
		||||
wheels = [
 | 
			
		||||
@@ -276,28 +276,6 @@ wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "dj-database-url"
 | 
			
		||||
version = "2.3.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
dependencies = [
 | 
			
		||||
    { name = "django" },
 | 
			
		||||
    { name = "typing-extensions" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/98/9f/fc9905758256af4f68a55da94ab78a13e7775074edfdcaddd757d4921686/dj_database_url-2.3.0.tar.gz", hash = "sha256:ae52e8e634186b57e5a45e445da5dc407a819c2ceed8a53d1fac004cc5288787", size = 10980 }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/e5/91/641a4e5c8903ed59f6cbcce571003bba9c5d2f731759c31db0ba83bb0bdb/dj_database_url-2.3.0-py3-none-any.whl", hash = "sha256:bb0d414ba0ac5cd62773ec7f86f8cc378a9dbb00a80884c2fc08cc570452521e", size = 7793 },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "dj-email-url"
 | 
			
		||||
version = "1.0.6"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/14/ef/8eb478accd9b0369d46a98d1b43027ee0c254096149265c78e6b2e2fa3b0/dj-email-url-1.0.6.tar.gz", hash = "sha256:55ffe3329e48f54f8a75aa36ece08f365e09d61f8a209773ef09a1d4760e699a", size = 15590 }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/8a/f9/fcb9745099d821f9a26092d3d6f4df8f10049885045c3a93ff726d2e40a6/dj_email_url-1.0.6-py2.py3-none-any.whl", hash = "sha256:cbd08327fbb08b104eac160fb4703f375532e4c0243eb230f5b960daee7a96db", size = 6296 },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "django"
 | 
			
		||||
version = "4.2.17"
 | 
			
		||||
@@ -312,15 +290,6 @@ wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/5e/85/457360cb3de496382e35db4c2af054066df5c40e26df31400d0109a0500c/Django-4.2.17-py3-none-any.whl", hash = "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0", size = 7993390 },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "django-cache-url"
 | 
			
		||||
version = "3.4.5"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/20/28/d420aaa89028d2ec0cf17c1510d06ff6a8ed0bf1abfb7f33c999e1c5befa/django-cache-url-3.4.5.tar.gz", hash = "sha256:eb9fb194717524348c95cad9905b70b647452741c1d9e481fac6d2125f0ad917", size = 7230 }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/48/90/01755e4a42558b763f7021e9369aa6aa94c2ede7313deed56cb7483834ab/django_cache_url-3.4.5-py2.py3-none-any.whl", hash = "sha256:5f350759978483ab85dc0e3e17b3d53eed3394a28148f6bf0f53d11d0feb5b3c", size = 4760 },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "django-countries"
 | 
			
		||||
version = "7.6.1"
 | 
			
		||||
@@ -470,26 +439,6 @@ wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/93/69/e391bd51bc08ed9141ecd899a0ddb61ab6465309f1eb470905c0c8868081/docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc", size = 570472 },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "environs"
 | 
			
		||||
version = "14.1.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
dependencies = [
 | 
			
		||||
    { name = "marshmallow" },
 | 
			
		||||
    { name = "python-dotenv" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/3c/8f/952bd034eac79c8b68b6c770cb78c2bdcb3140d31ff224847f3520077d75/environs-14.1.0.tar.gz", hash = "sha256:a5f2afe9d5a21b468e74a3cceacf5d2371fd67dbb9a7e54fe62290c75a09cdfa", size = 30985 }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/d5/ad/57cfa3e8a006df88e723524127dbab2971a4877c97e7bad070257e15cb6c/environs-14.1.0-py3-none-any.whl", hash = "sha256:a7edda1668ddf1fbfcb7662bdc242dac25648eff2c7fdbaa5d959693afed7a3e", size = 15332 },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[package.optional-dependencies]
 | 
			
		||||
django = [
 | 
			
		||||
    { name = "dj-database-url" },
 | 
			
		||||
    { name = "dj-email-url" },
 | 
			
		||||
    { name = "django-cache-url" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "executing"
 | 
			
		||||
version = "2.1.0"
 | 
			
		||||
@@ -759,18 +708,6 @@ wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "marshmallow"
 | 
			
		||||
version = "3.25.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
dependencies = [
 | 
			
		||||
    { name = "packaging" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/bd/5c/cbfa41491d6c83b36471f2a2f75602349d20a8f88afd94f83c1e68bbc298/marshmallow-3.25.0.tar.gz", hash = "sha256:5ba94a4eb68894ad6761a505eb225daf7e5cb7b4c32af62d4a45e9d42665bc31", size = 176751 }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/76/26/b347619b719d4c048e038929769f8f6b28c6d930149b40d950bbdde31d48/marshmallow-3.25.0-py3-none-any.whl", hash = "sha256:50894cd57c6b097a6c6ed2bf216af47d10146990a54db52d03e32edb0448c905", size = 49480 },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "matplotlib-inline"
 | 
			
		||||
version = "0.1.7"
 | 
			
		||||
@@ -807,7 +744,7 @@ version = "1.6.1"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
dependencies = [
 | 
			
		||||
    { name = "click" },
 | 
			
		||||
    { name = "colorama", marker = "platform_system == 'Windows'" },
 | 
			
		||||
    { name = "colorama", marker = "sys_platform == 'win32'" },
 | 
			
		||||
    { name = "ghp-import" },
 | 
			
		||||
    { name = "jinja2" },
 | 
			
		||||
    { name = "markdown" },
 | 
			
		||||
@@ -1292,15 +1229,6 @@ wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "python-dotenv"
 | 
			
		||||
version = "1.0.1"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pyyaml"
 | 
			
		||||
version = "6.0.2"
 | 
			
		||||
@@ -1500,7 +1428,6 @@ dependencies = [
 | 
			
		||||
    { name = "django-ordered-model" },
 | 
			
		||||
    { name = "django-phonenumber-field" },
 | 
			
		||||
    { name = "django-simple-captcha" },
 | 
			
		||||
    { name = "environs", extra = ["django"] },
 | 
			
		||||
    { name = "ical" },
 | 
			
		||||
    { name = "jinja2" },
 | 
			
		||||
    { name = "libsass" },
 | 
			
		||||
@@ -1509,7 +1436,6 @@ dependencies = [
 | 
			
		||||
    { name = "pillow" },
 | 
			
		||||
    { name = "pydantic-extra-types" },
 | 
			
		||||
    { name = "python-dateutil" },
 | 
			
		||||
    { name = "redis", extra = ["hiredis"] },
 | 
			
		||||
    { name = "reportlab" },
 | 
			
		||||
    { name = "sentry-sdk" },
 | 
			
		||||
    { name = "sphinx" },
 | 
			
		||||
@@ -1536,6 +1462,7 @@ docs = [
 | 
			
		||||
]
 | 
			
		||||
prod = [
 | 
			
		||||
    { name = "psycopg", extra = ["c"] },
 | 
			
		||||
    { name = "redis", extra = ["hiredis"] },
 | 
			
		||||
]
 | 
			
		||||
tests = [
 | 
			
		||||
    { name = "freezegun" },
 | 
			
		||||
@@ -1559,7 +1486,6 @@ requires-dist = [
 | 
			
		||||
    { name = "django-ordered-model", specifier = ">=3.7.4,<4.0.0" },
 | 
			
		||||
    { name = "django-phonenumber-field", specifier = ">=8.0.0,<9.0.0" },
 | 
			
		||||
    { name = "django-simple-captcha", specifier = ">=0.6.0,<1.0.0" },
 | 
			
		||||
    { name = "environs", extras = ["django"], specifier = ">=14.1.0,<15.0.0" },
 | 
			
		||||
    { name = "ical", specifier = ">=8.3.0,<9.0.0" },
 | 
			
		||||
    { name = "jinja2", specifier = ">=3.1.4,<4.0.0" },
 | 
			
		||||
    { name = "libsass", specifier = ">=0.23.0,<1.0.0" },
 | 
			
		||||
@@ -1568,7 +1494,6 @@ requires-dist = [
 | 
			
		||||
    { name = "pillow", specifier = ">=11.0.0,<12.0.0" },
 | 
			
		||||
    { name = "pydantic-extra-types", specifier = ">=2.10.1,<3.0.0" },
 | 
			
		||||
    { name = "python-dateutil", specifier = ">=2.9.0.post0,<3.0.0.0" },
 | 
			
		||||
    { name = "redis", extras = ["hiredis"], specifier = ">=5.2.0,<6.0.0" },
 | 
			
		||||
    { name = "reportlab", specifier = ">=4.2.5,<5.0.0" },
 | 
			
		||||
    { name = "sentry-sdk", specifier = ">=2.19.2,<3.0.0" },
 | 
			
		||||
    { name = "sphinx", specifier = ">=5,<6" },
 | 
			
		||||
@@ -1593,7 +1518,10 @@ docs = [
 | 
			
		||||
    { name = "mkdocstrings", specifier = ">=0.27.0,<1.0.0" },
 | 
			
		||||
    { name = "mkdocstrings-python", specifier = ">=1.12.2,<2.0.0" },
 | 
			
		||||
]
 | 
			
		||||
prod = [{ name = "psycopg", extras = ["c"], specifier = ">=3.2.3,<4.0.0" }]
 | 
			
		||||
prod = [
 | 
			
		||||
    { name = "psycopg", extras = ["c"], specifier = ">=3.2.3,<4.0.0" },
 | 
			
		||||
    { name = "redis", extras = ["hiredis"], specifier = ">=5.2.0,<6.0.0" },
 | 
			
		||||
]
 | 
			
		||||
tests = [
 | 
			
		||||
    { name = "freezegun", specifier = ">=1.5.1,<2.0.0" },
 | 
			
		||||
    { name = "model-bakery", specifier = ">=1.20.0,<2.0.0" },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
// biome-ignore lint/correctness/noNodejsModules: this is backend side
 | 
			
		||||
import { parse, resolve } from "node:path";
 | 
			
		||||
import { parse, resolve, sep } from "node:path";
 | 
			
		||||
import inject from "@rollup/plugin-inject";
 | 
			
		||||
import { glob } from "glob";
 | 
			
		||||
import { type AliasOptions, type UserConfig, defineConfig } from "vite";
 | 
			
		||||
@@ -31,7 +31,7 @@ function getAliases(): AliasOptions {
 | 
			
		||||
function getRelativeAssetPath(path: string): string {
 | 
			
		||||
  let relativePath: string[] = [];
 | 
			
		||||
  const fullPath = parse(path);
 | 
			
		||||
  for (const dir of fullPath.dir.split("/").reverse()) {
 | 
			
		||||
  for (const dir of fullPath.dir.split(sep).reverse()) {
 | 
			
		||||
    if (dir === "bundled") {
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -40,7 +40,7 @@ function getRelativeAssetPath(path: string): string {
 | 
			
		||||
  // We collected folders in reverse order, we put them back in the original order
 | 
			
		||||
  relativePath = relativePath.reverse();
 | 
			
		||||
  relativePath.push(fullPath.name);
 | 
			
		||||
  return relativePath.join("/");
 | 
			
		||||
  return relativePath.join(sep);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// biome-ignore lint/style/noDefaultExport: this is recommended by documentation
 | 
			
		||||
@@ -97,6 +97,10 @@ export default defineConfig((config: UserConfig) => {
 | 
			
		||||
            src: resolve(nodeModules, "jquery-ui/dist/jquery-ui.min.js"),
 | 
			
		||||
            dest: vendored,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            src: resolve(nodeModules, "jquery.shorten/src/jquery.shorten.min.js"),
 | 
			
		||||
            dest: vendored,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      }),
 | 
			
		||||
    ],
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user