diff --git a/.pylintrc b/.pylintrc index 01c0522..8b45ce1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,17 +3,22 @@ # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) extension-pkg-whitelist= # Specify a score threshold to be exceeded before program exits with error. fail-under=10.0 -# Add files or directories to the blacklist. They should be base names, not -# paths. +# Files or directories to be skipped. They should be base names, not paths. ignore=CVS -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. ignore-patterns= # Python code to execute, usually for sys.path manipulation such as @@ -181,7 +186,7 @@ max-nested-blocks=5 # inconsistent-return-statements if a never returning function is called then # it will be considered as an explicit return statement and no message will be # printed. -never-returning-functions=sys.exit +never-returning-functions=sys.exit,argparse.parse_error [STRING] @@ -227,6 +232,8 @@ single-line-if-stmt=no [VARIABLES] +django-settings-module=khana.settings + # List of additional names supposed to be defined in builtins. Remember that # you should avoid defining new builtins when possible. additional-builtins= @@ -234,6 +241,9 @@ additional-builtins= # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes +# List of names allowed to shadow builtins +allowed-redefined-builtins= + # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_, @@ -367,6 +377,13 @@ class-attribute-naming-style=any # attribute-naming-style. #class-attribute-rgx= +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. +#class-const-rgx= + # Naming style matching correct class names. class-naming-style=PascalCase @@ -455,9 +472,13 @@ variable-naming-style=snake_case max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: none. To make it work, -# install the python-enchant package. +# install the 'python-enchant' package. spelling-dict= +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + # List of comma separated words that should not be checked. spelling-ignore-words= @@ -519,6 +540,9 @@ min-public-methods=2 [CLASSES] +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, @@ -557,16 +581,17 @@ analyse-fallback-blocks=no # Deprecated modules which should not be used, separated by a comma. deprecated-modules=optparse,tkinter.tix -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). ext-import-graph= -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). import-graph= -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). int-import-graph= # Force import order to recognize a module as part of the standard diff --git a/requirements/dev.txt b/requirements/dev.txt index 47b1c24..95ca46c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -3,7 +3,9 @@ black==19.10b0 coverage==5.5 flake8==3.9.1 +pylint==2.8.2 +pylint-django==2.4.4 django-spaghetti-and-meatballs==0.4.2 docutils==0.16 pytest==6.2.4 -pytest-django==4.2.0 \ No newline at end of file +pytest-django==4.2.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..0f73868 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[flake8] +max-line-length=100 +max-complexity=10 diff --git a/src/base/models.py b/src/base/models.py index 8851c49..1cabe71 100644 --- a/src/base/models.py +++ b/src/base/models.py @@ -20,5 +20,5 @@ class Markdownizable(models.Model): def to_markdown(self): """Convertit le champ `content` en (Github-flavored) Markdown.""" - + return markdown.markdown(self.content) diff --git a/src/base/tests.py b/src/base/tests.py index efcab21..73bcb6a 100644 --- a/src/base/tests.py +++ b/src/base/tests.py @@ -11,6 +11,6 @@ class TestMarkdownizable(TestCase): def test_to_markdown(self): """Vérifie qu'un contenu Markdown est correctement convertit en HTML.""" - m = Markdownizable(information="# Title") + markdown_content = Markdownizable(information="# Title") - self.assertEqual(m.to_markdown(), "

Title

") + self.assertEqual(markdown_content.to_markdown(), "

Title

") diff --git a/src/communication/admin.py b/src/communication/admin.py index c454778..888a2eb 100644 --- a/src/communication/admin.py +++ b/src/communication/admin.py @@ -7,6 +7,8 @@ from .models import Message @admin.register(Message) class MessageAdmin(admin.ModelAdmin): + """La classe `MessageAdmin` contrôle la gestion des messages + """ list_display = ("sender", "recipient", "written_at", "is_read", "read_at") ordering = ("written_at", "sender") search_fields = ("sender", "recipient", "message_title") diff --git a/src/communication/forms.py b/src/communication/forms.py index 2870f30..4465aef 100644 --- a/src/communication/forms.py +++ b/src/communication/forms.py @@ -1,14 +1,13 @@ """Configuration et représentation des forms liés aux messages.""" -from datetime import date - from django import forms -from people.models import Gymnast from .models import Message class MessageForm(forms.ModelForm): + """Formulaire de base pour la création et la modification de messages + """ class Meta: model = Message fields = ( diff --git a/src/communication/models.py b/src/communication/models.py index c12bd1e..b449b12 100644 --- a/src/communication/models.py +++ b/src/communication/models.py @@ -1,10 +1,23 @@ +"""Modelisation de tout ce qui touche à la communication entre utilisateurs. + +Cette application gère: + +* Les messages +* Ah, c'est tout en fait :-) + +""" -from django.db import models -from django.contrib.auth.models import User from datetime import datetime +from django.db import models +from django.contrib.auth import get_user_model + from base.models import Markdownizable + +User = get_user_model() + + class Message(Markdownizable): """Représente un message échangé entre deux utilisateurs. diff --git a/src/communication/tests_models.py b/src/communication/tests_models.py index da9c21a..254da67 100644 --- a/src/communication/tests_models.py +++ b/src/communication/tests_models.py @@ -1,12 +1,19 @@ -# coding=UTF-8 +"""Tests liés au modèle de l'application Communication""" from datetime import datetime -from .models import Message -from django.contrib.auth.models import User -import pytest -def test_message_tostring(): +from django.contrib.auth import get_user_model + +from .models import Message + + +User = get_user_model() + + +def test_message_to_string(): + """Vérifie la représentation textuelle d'un message + """ timing = datetime.now() - u = User(username='fred', password='fredpassword') - m = Message(sender=u, written_at=timing, title="test") - assert str(m) == "fred - " + str(timing) + " : test" + user = User(username='fred', password='fredpassword') + message = Message(sender=user, written_at=timing, title="test") + assert str(message) == "fred - " + str(timing) + " : test" diff --git a/src/communication/urls.py b/src/communication/urls.py index 9c958c4..a764a56 100644 --- a/src/communication/urls.py +++ b/src/communication/urls.py @@ -1,6 +1,6 @@ """Définition des routes d'actions permettant de contrôler les messages et la communication.""" -from django.urls import path, re_path +from django.urls import path from . import views diff --git a/src/communication/views.py b/src/communication/views.py index 4fa015a..cd3be7b 100644 --- a/src/communication/views.py +++ b/src/communication/views.py @@ -1,17 +1,18 @@ """Vues et fonctions pour tout ce qui touche à la communication entre plusieurs utilisateurs.""" +from datetime import datetime + from django.contrib.auth.decorators import login_required -from django.contrib.auth.models import User from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render, get_object_or_404 from django.views.decorators.http import require_http_methods from django.urls import reverse -from datetime import datetime from .forms import MessageForm from .models import Message + @login_required def get_number_of_unread_message(request): """Récupère le nombre de messages non lus associés à l'utilisateur en session. @@ -80,7 +81,7 @@ def delete_message(request, messageid): """ try: message = Message.objects.get(pk=messageid) - + if message.sender == request.user or message.recipient == request.user : message.delete() else: @@ -103,8 +104,8 @@ def compose_message(request): if form.is_valid(): form.save() return HttpResponseRedirect(reverse("sent_messages")) - else: - print("Invalid form") + + print("Invalid form") else: form = MessageForm() diff --git a/src/planning/README.md b/src/planning/README.md index 3182577..d33e64e 100644 --- a/src/planning/README.md +++ b/src/planning/README.md @@ -1,7 +1,9 @@ # Application `Planning` ## Saison + Une saison est déinie par : + - un id, - un label, - une date de début et @@ -11,9 +13,12 @@ La date de début est très souvent le : 01/09/xxxx La date de fin est très souvent le : 31/08/xxxy Exemple : 1/9/2015 - 31/8/2016 +NOTE: Le fait que la date de début soit **très souvent** le 01 septembre indique sans doute une date par défaut (modifiable) au niveau du modèle. Idem pour la date de fin. +NOTE: je ne comprends pas la méthode `week_number_from_begin`. Si cela fait référence à la date de début, alors il faut le mentionner dans le nom de la fonction. ## Course + Un cours est un ensemble d'entraînements (`training`) (récurrents ?) défini par : - une heure de début et une heure de fin, - une date de début et une date de fin @@ -26,16 +31,32 @@ Réflexions/questions : - les cours devraient-ils être liés à une saison ? - un cours est considéré comme donné hebdomadairement entre la date de début et la date de fin (hérite de la classe `Temporizable`), mais est-ce une bonne idée ? Est-ce une bonne manière de faire ? +NOTE: Je dirais que oui. D'un côté, tu n'aurais pas de possibilité de déduction entre un cours et le moment où il y a réellement lieu - de ce que je comprends, le *cours* correspond en fait à quelque chose qui est prévu selon une récurrence donnée - eg. "tous les mardis (deuxième jour de la semaine), entre 10h et 12h, avec Machin, Chose et Brol". +La *saison* va juste indiquer la date de début et de fin des cours qui y sont liés. +Même s'il y a moyen de le représenter différement, je pense surtout que le concept de saison parle à beaucoup de monde. + +NOTE: la réflexion va surtout être "est-ce qu'un cours est différent entre deux saison ?" A priori, oui, puisque Bidule peut devenir entraineur pour la saison 2020-2021. + +L'avantage, c'est que Machin pourrait se connecter sur son profil et dire "ah ouais, cette année, je donne cours le jeudi et le samedi." ## Training + Un entraînement est une occurence d'un cours pendant lequel des gmnastes sont présents. + Un objet de cette classe lie donc : -- un cours, -- gymnaste et +- un cours, +- des gymnastes présents et - une date. +NOTE: Techniquement, tu peux ici mettre une contrainte ou un avertissement si l'entrainement est situé à une date différente de ce que la saison devrait autoriser. + +NOTE: dans la classe Training, il est question d'une `ForeignKey` vers Gymnast, mais ce devrait être un ManyToManyField. + +NOTE: De la même manière, je reprendrais aussi l'heure de début et de fin. Entre ce qui est prévu (le cours) et la réalité (l'entraintement), il pourrait y avoir des différences. + +Cela permettrait aussi de planifier les cours - dire en gros que, en début d'année, tu (l'appli) planifies les jours fériés, et *projette* les entrainements pour la saison, sur base de ce qui est prévu. ## Round @@ -43,6 +64,10 @@ Un objet de cette classe lie donc : Classe représentant les passages des élèves lors d'un entrainement. Chaque record représente un passage. Il est donc lié à un record de la classe `Training`. +NOTE: Est-ce qu'il est important de savoir qui est l'entraineur qui a donné une évaluation ? + +NOTE: au niveau du round, il y a un ensemble d'informations chronologiques: `nb_of_realisations` (au pluriel...), `nb_of_success`, ... mais c'est incohérent avec le `round_number`, puisque je suppose qu'il pourrait faire un tour de A, puis B, puis revenir à A. +Cette partie-ci me semble très complexe - sans oublier qu'il va falloir la remplir: si tes entraineurs chipotent sur une tablette ou sur un écran pour chaque action que réalise un gymnaste, ça va pas être sympa pour eux. ## Group @@ -50,6 +75,11 @@ Chaque record représente un passage. Il est donc lié à un record de la classe Classe représentant les groupes (Loisir, D1, D2, A, B, …). Un groupe appartient à un club. +NOTE: pourquoi garder un champ `active` ? Il y a un risque qu'un groupe soit désactivé ? Si oui, ne vaut-il pas mieux garder le moment où il l'a été ? + +NOTE: est-ce que le champ `name` n'est pas un dictionnaire fini ? Loisir, D1, D2, ... ? + +NOTE: est-ce que tu n'as pas une contrainte sur le nom, le club et la saison ? ## Subgroup @@ -61,28 +91,33 @@ Un sous-groupe appartient à un groupe (pour rappel, lui-même lié à un club). De cette manière, quand un gymnaste est mis dans un sous-groupe, en remontant via le groupe, nous pouvons connaître le(s) club(s) du gymnaste pour chaque saison. - +NOTE: re-question sur le `name`. A mon avis, si le nom du groupe est fini, tu peux te passer d'une des classes `Group` ou `Subgroup`, et cela simplifierait pas mal la gestion du club. ## Unavailability Classe représentant les indisponibilités. +NOTE: avec la réflexion ci-dessous, cela pourrait ne plus être utile. Les *Courses* correspondent à la modélisation tandis que les entrainements représentent le planifié/réalisé. Du coup, il suffit qu'un entrainenemnts n'existe pas pour qu'il ne soit pas planifié. ## PlanningLine + Classe représentant les passages prévisionnels (incubating idea). +NOTE: en gros, tu veux proposer un entrainement personnalisé pour chaque gymnaste ;) Je ne vois pas la valeur ajoutée. Le mieux serait d'avoir une forme de proposition au niveau des Rounds et des Trainings, quitte à la modifier pendant l'entrainement. Sinon, je ne vois pas trop l'idée. ## EventType Classe représentant les types d'évènements. + C'est un dictionnaire fini : - compétiton qualificative, - compétition finale, - démonstration, - … +NOTE: tu peux utiliser un champ de type Choice, si le dictionnaire est fini. Cela te fera gagner une jointure. Si le dictionnaire a ***une*** chance d'avoir une nouvelle valeur, garde la table. ## Event @@ -97,6 +132,8 @@ Un évènement est caractérisé par : Je ne me rapelle plus à quoi sert le club. - +NOTE: alors, retire le club :-p ## Event_Participation + +NOTE: Dans Event, tu as déjà un lien avec des gymnastes, que tu reprends dans la classe EventParticipation (pas de "*_*"). Autant ne garder qu'une seule liaison entre un évènement et des gymnastes, et compléter ces enregistrements après (ou pendant) pour dire si Choupidou étant bien placé ou pas (quitte à laisser le `rank` vide si Choupidou n'est finalement pas venu ou s'il a sauté comme une bouse - oui, ça arrive). diff --git a/src/profile/urls.py b/src/profile/urls.py index 7b825c0..2c7bd6b 100644 --- a/src/profile/urls.py +++ b/src/profile/urls.py @@ -7,5 +7,5 @@ from . import views profile_urlpatterns = [ path(r"lookup/", views.user_lookup, name="user_lookup"), - path(r"edit//", views.profile_update, name="profile_update"), + path(r"edit/", views.profile_update, name="profile_update"), ] diff --git a/src/profile/views.py b/src/profile/views.py index e375b3c..0cb8af9 100644 --- a/src/profile/views.py +++ b/src/profile/views.py @@ -60,17 +60,12 @@ def user_lookup(request): @login_required @require_http_methods(["GET", "POST"]) -def profile_update(request, profileid): - """Modification d'un profil utilisateur. +def profile_update(request): + """Modification du profil de l'utilisateur connecté - Args: - profileid (int): L'identifiant du profil utilisateur à modifier. """ - profile = get_object_or_404(Profile, pk=profileid) - - if profile.user != request.user: - raise PermissionDenied("Permission denied : you don't have the permission to update this profile.") + profile = request.user.profile if request.method == "POST": form = ProfileForm(request.POST, instance=profile) @@ -78,7 +73,6 @@ def profile_update(request, profileid): if form.is_valid(): form.save() - request.session["profileid"] = profileid request.session["template"] = profile.template_color request.session["sidebar"] = profile.sidebar_color request.session["is_sidebar_minified"] = profile.is_sidebar_minified @@ -88,5 +82,5 @@ def profile_update(request, profileid): else: form = ProfileForm(instance=profile) - context = {"form": form, "profileid": profileid} + context = {"form": form,} return render(request, "profile_create.html", context) diff --git a/src/templates/base.html b/src/templates/base.html index 0b53ca0..c614494 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -87,93 +87,93 @@ - - Dashboard - - - + Dashboard + + + -
-
+ {% csrf_token %}
@@ -59,10 +59,10 @@
- -{% endblock %} +{% endblock %} \ No newline at end of file