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(), "
")
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 @@
-