From 3018c1e0fec8e4ef01e86fa266ff680fcd8637e3 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 8 Apr 2022 17:30:39 +0200 Subject: [PATCH] Modification vers couleur Surviva[list] --- apps/api/filters.py | 42 +++++++ apps/wei/management/commands/make_teams.py | 108 +++++++++++++++++ note_kfet/static/css/custom.css.save | 133 +++++++++++++++++++++ 3 files changed, 283 insertions(+) create mode 100644 apps/api/filters.py create mode 100644 apps/wei/management/commands/make_teams.py create mode 100644 note_kfet/static/css/custom.css.save diff --git a/apps/api/filters.py b/apps/api/filters.py new file mode 100644 index 00000000..cb51c37c --- /dev/null +++ b/apps/api/filters.py @@ -0,0 +1,42 @@ +import re +from functools import lru_cache + +from rest_framework.filters import SearchFilter + + +class RegexSafeSearchFilter(SearchFilter): + @lru_cache + def validate_regex(self, search_term) -> bool: + try: + re.compile(search_term) + return True + except re.error: + return False + + def get_search_fields(self, view, request): + """ + Ensure that given regex are valid. + If not, we consider that the user is trying to search by substring. + """ + search_fields = super().get_search_fields(view, request) + search_terms = self.get_search_terms(request) + + for search_term in search_terms: + if not self.validate_regex(search_term): + # Invalid regex. We assume we don't query by regex but by substring. + search_fields = [f.replace('$', '') for f in search_fields] + break + + return search_fields + + def get_search_terms(self, request): + """ + Ensure that search field is a valid regex query. If not, we remove extra characters. + """ + terms = super().get_search_terms(request) + if not all(self.validate_regex(term) for term in terms): + # Invalid regex. If a ^ is prefixed to the search term, we remove it. + terms = [term[1:] if term[0] == '^' else term for term in terms] + # Same for dollars. + terms = [term[:-1] if term[-1] == '$' else term for term in terms] + return terms diff --git a/apps/wei/management/commands/make_teams.py b/apps/wei/management/commands/make_teams.py new file mode 100644 index 00000000..dc502bab --- /dev/null +++ b/apps/wei/management/commands/make_teams.py @@ -0,0 +1,108 @@ +# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later +from argparse import ArgumentParser, FileType + +from django.core.management import BaseCommand +from django.db import transaction +from django.db.models import Q + +from ...forms import CurrentSurvey +from ...models import Bus, WEIMembership + + +class Command(BaseCommand): + help = "Attribute to each first year member a team for the WEI" + + def add_arguments(self, parser: ArgumentParser): + parser.add_argument('--doit', '-d', action='store_true', help='Finally run the algorithm in non-dry mode.') + parser.add_argument('--output', '-o', nargs='?', type=FileType('w'), default=self.stdout, + help='Output file for the algorithm result. Default is standard output.') + + def attribute_teams(self, bus): + if not bus.size: + return + + teams = bus.teams.all() + + old_members = WEIMembership.objects.filter(registration__first_year=False, bus=bus, team__in=teams) + new_members = WEIMembership.objects.filter(registration__first_year=True, bus=bus) + + n_chef_tot = old_members.count() + n_equipe = teams.count() + size_goal = {} + for team in teams: + size_goal[team] = int(old_members.filter(team=team).count() * new_members.count() / n_chef_tot) + i = 0 + non_men = list(new_members.filter(~Q(registration__gender='male'))) + men = list(new_members.filter(registration__gender='male')) + + print(bus.name, size_goal.values()) + return + + while non_men: + if new_members.filter(team=teams[i]).count() < size_goal[teams[i]]: + non_man = non_men.pop() + non_man.team = teams[i] + non_man.save() + i += 1 + i %= n_equipe + + for i, team in enumerate(teams): + print(i, new_members.filter(team=team).count()) + if new_members.filter(team=team).count() == 1: + # Si une fille est seule on l'enlève. + g_alone = new_members.get(team=team) + g_alone.team = teams[i + 1] + g_alone.save() + remain = True + i = 0 + while men and remain: + if new_members.filter(team=teams[i]).count() < size_goal[teams[i]]: + m = men.pop() + m.team = teams[i] + m.save() + i += 1 + i %= n_equipe + + @transaction.atomic + def handle(self, *args, **options): + """ + Run the WEI algorithm to attribute a bus to each first year member. + """ + sid = transaction.savepoint() + + algorithm = CurrentSurvey.get_algorithm_class()() + + try: + from tqdm import tqdm + display_tqdm = True + except ImportError: + display_tqdm = False + + for bus in Bus.objects.all(): + self.attribute_teams(bus) + + raise Exception() + + output = options['output'] + registrations = algorithm.get_registrations() + per_bus = {bus: [r for r in registrations if 'selected_bus_pk' in r.information + and r.information['selected_bus_pk'] == bus.pk] + for bus in algorithm.get_buses()} + for bus, members in per_bus.items(): + output.write(bus.name + "\n") + output.write("=" * len(bus.name) + "\n") + order = -1 + for r in members: + survey = CurrentSurvey(r) + for order, (b, _score) in enumerate(survey.ordered_buses()): + if b == bus: + break + output.write(f"{r.user.username} ({order + 1})\n") + output.write("\n") + + if not options['doit']: + self.stderr.write(self.style.WARNING("Running in dry mode. " + "Use --doit option to really execute the algorithm.")) + transaction.savepoint_rollback(sid) + return diff --git a/note_kfet/static/css/custom.css.save b/note_kfet/static/css/custom.css.save new file mode 100644 index 00000000..6eb03040 --- /dev/null +++ b/note_kfet/static/css/custom.css.save @@ -0,0 +1,133 @@ +.validate:hover { + cursor: pointer; + text-decoration: underline; +} + +/* Opaque tooltip with white background */ +.tooltip.show { + opacity: 1; +} + +.tooltip-inner { + background-color: #fff; + box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15); + border: 1px solid rgba(0, 0, 0, .250); + color: #000; + margin: 0 .5rem .25rem .5rem; + padding: 0; + width: 200px; +} + +.bs-tooltip-bottom .arrow::before { + border-bottom-color: rgba(0, 0, 0, .250); +} + +/* Fixed width picture column */ +.picture-col { + max-width: 202px; +} + +/* Limit fluid container to a max size */ +.container-fluid { + max-width: 1600px; +} + +/* Apply Bootstrap table-responsive to all Django tables */ +.table-container { + display: block; + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +/* Smaller language selector */ +select.language { + padding: 0; + background: transparent; + border: none; + height: calc(1.5em + .5rem); + color: #6c757d; + -moz-appearance: none; + width: auto; + cursor: pointer; +} + +/* Remove horizontal padding on mark */ +.mark, +mark { + padding: .2em 0; +} + +/* Make navbar more readable */ +.navbar-dark .navbar-nav .nav-link { + color: rgba(255, 255, 255, .75); +} + +/* Last BDE colors */ +.bg-primary { + background-color: rgb(0, 0, 0) !important; +} + +html { + scrollbar-color: rgba(0, 0, 0, 1) rgba(35, 35, 39, 1); +} + +body { + background-color: rgba(64, 64, 64, 1); + background-image: url(../img/background-texture.png); +} + +.btn-outline-primary:hover, +.btn-outline-primary:not(:disabled):not(.disabled).active, +.btn-outline-primary:not(:disabled):not(.disabled):active { + color: #fff; + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); +} + +.btn-outline-primary { + color: rgb(0, 0, 0); + background-color: rgba(248, 249, 250, 0.9); + border-color: rgb(0, 0, 0); +} + +.turbolinks-progress-bar { + background-color: #12432E; +} + +.btn-primary:hover, +.btn-primary:not(:disabled):not(.disabled).active, +.btn-primary:not(:disabled):not(.disabled):active { + color: #fff; + background-color: rgb(0, 0, 0); + border-color: rgb(0, 0, 0); +} + +.btn-primary { + color: rgba(248, 249, 250, 0.9); + background-color: rgb(28, 114, 10); + border-color: rgb0à, 67, 46); +} + +.border-primary { + border-color: rgb(28, 114, 10) !important; +} + +a { + color: rgb(28, 114, 10); +} + +a:hover { + color: rgb(122, 163, 75); +} + +.form-control:focus { + box-shadow: 0 0 0 0.25rem rgba(122, 163, 75, 0.25); + border-color: rgb(122, 163, 75); +} + +.btn-outline-primary.focus { + box-shadow: 0 0 0 0.25rem rgba(122, 163, 75, 0.5); +} + +