diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d478e690..2ebeca97 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -37,7 +37,7 @@ jobs: pushd ${{secrets.SITH_PATH}} git pull - poetry install + poetry install --with prod --without docs,tests poetry run ./manage.py install_xapian poetry run ./manage.py migrate echo "yes" | poetry run ./manage.py collectstatic diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml new file mode 100644 index 00000000..16adb95a --- /dev/null +++ b/.github/workflows/deploy_docs.yml @@ -0,0 +1,21 @@ +name: deploy_docs +on: + push: + branches: + - master +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/setup_project + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v3 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + - run: poetry run mkdocs gh-deploy --force \ No newline at end of file diff --git a/.github/workflows/taiste.yml b/.github/workflows/taiste.yml index d7e1e9d9..b83682ec 100644 --- a/.github/workflows/taiste.yml +++ b/.github/workflows/taiste.yml @@ -36,7 +36,7 @@ jobs: pushd ${{secrets.SITH_PATH}} git pull - poetry install + poetry install --with prod --without docs,tests poetry run ./manage.py install_xapian poetry run ./manage.py migrate echo "yes" | poetry run ./manage.py collectstatic diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 481160ff..00000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -# Required -version: 2 - -# Allow installing xapian-bindings in pip -build: - apt_packages: - - libxapian-dev - -# Build documentation in the doc/ directory with Sphinx -sphinx: - configuration: doc/conf.py - -# Optionally build your docs in additional formats such as PDF and ePub -formats: all - -# Optionally set the version of Python and requirements required to build your docs -python: - version: "3.8" - install: - - method: pip - path: . - extra_requirements: - - docs diff --git a/README.md b/README.md index bf818ec6..f27dc28d 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,20 @@ -
+# Sith -All documentation is in the docs
directory and online at https://sith-ae.readthedocs.io/. This documentation is written in French because it targets a French audience and it's too much work to maintain two versions. The code and code comments are strictly written in English.
- First, it's advised to read the about part of the project to understand the goals and the mindset of the current and previous maintainers and know what to expect to learn. -
-- If in the first part you realize that you need more background about what we use, we provide some links to tutorials and documentation at the end of our documentation. Feel free to use it and complete it with what you found helpful. -
-- Keep in mind that this documentation is thought to be read in order. -
-{html}
\n" def test_full_markdown_syntax(): - doc_path = Path(settings.BASE_DIR) / "doc" - md = (doc_path / "SYNTAX.md").read_text() - html = (doc_path / "SYNTAX.html").read_text() + syntax_path = Path(settings.BASE_DIR) / "core" / "fixtures" + md = (syntax_path / "SYNTAX.md").read_text() + html = (syntax_path / "SYNTAX.html").read_text() result = markdown(md) assert result == html @@ -233,7 +228,6 @@ class PageHandlingTest(TestCase): def test_create_page_ok(self): """Should create a page correctly.""" - response = self.client.post( reverse("core:page_new"), {"parent": "", "name": "guy", "owner_group": self.root_group.id}, @@ -274,9 +268,7 @@ class PageHandlingTest(TestCase): assert '' in str(response.content) def test_access_child_page_ok(self): - """ - Should display a page correctly - """ + """Should display a page correctly.""" parent = Page(name="guy", owner_group=self.root_group) parent.save(force_lock=True) page = Page(name="bibou", owner_group=self.root_group, parent=parent) @@ -289,18 +281,14 @@ class PageHandlingTest(TestCase): self.assertIn('', html) def test_access_page_not_found(self): - """ - Should not display a page correctly - """ + """Should not display a page correctly.""" response = self.client.get(reverse("core:page", kwargs={"page_name": "swagg"})) assert response.status_code == 200 html = response.content.decode() self.assertIn('', html) def test_create_page_markdown_safe(self): - """ - Should format the markdown and escape html correctly - """ + """Should format the markdown and escape html correctly.""" self.client.post( reverse("core:page_new"), {"parent": "", "name": "guy", "owner_group": "1"} ) @@ -335,13 +323,13 @@ http://git.an class UserToolsTest: def test_anonymous_user_unauthorized(self, client): - """An anonymous user shouldn't have access to the tools page""" + """An anonymous user shouldn't have access to the tools page.""" response = client.get(reverse("core:user_tools")) assert response.status_code == 403 @pytest.mark.parametrize("username", ["guy", "root", "skia", "comunity"]) def test_page_is_working(self, client, username): - """All existing users should be able to see the test page""" + """All existing users should be able to see the test page.""" # Test for simple user client.force_login(User.objects.get(username=username)) response = client.get(reverse("core:user_tools")) @@ -391,9 +379,8 @@ class FileHandlingTest(TestCase): class UserIsInGroupTest(TestCase): - """ - Test that the User.is_in_group() and AnonymousUser.is_in_group() - work as intended + """Test that the User.is_in_group() and AnonymousUser.is_in_group() + work as intended. """ @classmethod @@ -450,30 +437,24 @@ class UserIsInGroupTest(TestCase): assert user.is_in_group(name=meta_groups_members) is False def test_anonymous_user(self): - """ - Test that anonymous users are only in the public group - """ + """Test that anonymous users are only in the public group.""" user = AnonymousUser() self.assert_only_in_public_group(user) def test_not_subscribed_user(self): - """ - Test that users who never subscribed are only in the public group - """ + """Test that users who never subscribed are only in the public group.""" self.assert_only_in_public_group(self.toto) def test_wrong_parameter_fail(self): - """ - Test that when neither the pk nor the name argument is given, - the function raises a ValueError + """Test that when neither the pk nor the name argument is given, + the function raises a ValueError. """ with self.assertRaises(ValueError): self.toto.is_in_group() def test_number_queries(self): - """ - Test that the number of db queries is stable - and that less queries are made when making a new call + """Test that the number of db queries is stable + and that less queries are made when making a new call. """ # make sure Skia is in at least one group self.skia.groups.add(Group.objects.first().pk) @@ -497,9 +478,8 @@ class UserIsInGroupTest(TestCase): self.skia.is_in_group(pk=group_not_in.id) def test_cache_properly_cleared_membership(self): - """ - Test that when the membership of a user end, - the cache is properly invalidated + """Test that when the membership of a user end, + the cache is properly invalidated. """ membership = Membership.objects.create( club=self.club, user=self.toto, end_date=None @@ -515,9 +495,8 @@ class UserIsInGroupTest(TestCase): assert self.toto.is_in_group(name=meta_groups_members) is False def test_cache_properly_cleared_group(self): - """ - Test that when a user is removed from a group, - the is_in_group_method return False when calling it again + """Test that when a user is removed from a group, + the is_in_group_method return False when calling it again. """ # testing with pk self.toto.groups.add(self.com_admin.pk) @@ -534,9 +513,8 @@ class UserIsInGroupTest(TestCase): assert self.toto.is_in_group(name="SAS admin") is False def test_not_existing_group(self): - """ - Test that searching for a not existing group - returns False + """Test that searching for a not existing group + returns False. """ assert self.skia.is_in_group(name="This doesn't exist") is False @@ -557,9 +535,7 @@ class DateUtilsTest(TestCase): cls.spring_first_day = date(2023, cls.spring_month, cls.spring_day) def test_get_semester(self): - """ - Test that the get_semester function returns the correct semester string - """ + """Test that the get_semester function returns the correct semester string.""" assert get_semester_code(self.autumn_semester_january) == "A24" assert get_semester_code(self.autumn_semester_september) == "A24" assert get_semester_code(self.autumn_first_day) == "A24" @@ -568,9 +544,7 @@ class DateUtilsTest(TestCase): assert get_semester_code(self.spring_first_day) == "P23" def test_get_start_of_semester_fixed_date(self): - """ - Test that the get_start_of_semester correctly the starting date of the semester. - """ + """Test that the get_start_of_semester correctly the starting date of the semester.""" automn_2024 = date(2024, self.autumn_month, self.autumn_day) assert get_start_of_semester(self.autumn_semester_january) == automn_2024 assert get_start_of_semester(self.autumn_semester_september) == automn_2024 @@ -581,9 +555,8 @@ class DateUtilsTest(TestCase): assert get_start_of_semester(self.spring_first_day) == spring_2023 def test_get_start_of_semester_today(self): - """ - Test that the get_start_of_semester returns the start of the current semester - when no date is given + """Test that the get_start_of_semester returns the start of the current semester + when no date is given. """ with freezegun.freeze_time(self.autumn_semester_september): assert get_start_of_semester() == self.autumn_first_day @@ -592,8 +565,7 @@ class DateUtilsTest(TestCase): assert get_start_of_semester() == self.spring_first_day def test_get_start_of_semester_changing_date(self): - """ - Test that the get_start_of_semester correctly gives the starting date of the semester, + """Test that the get_start_of_semester correctly gives the starting date of the semester, even when the semester changes while the server isn't restarted. """ spring_2023 = date(2023, self.spring_month, self.spring_day) diff --git a/core/utils.py b/core/utils.py index 01d833a5..87f3ab50 100644 --- a/core/utils.py +++ b/core/utils.py @@ -31,9 +31,7 @@ from PIL.Image import Resampling def get_git_revision_short_hash() -> str: - """ - Return the short hash of the current commit - """ + """Return the short hash of the current commit.""" try: output = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) if isinstance(output, bytes): @@ -44,8 +42,7 @@ def get_git_revision_short_hash() -> str: def get_start_of_semester(today: Optional[date] = None) -> date: - """ - Return the date of the start of the semester of the given date. + """Return the date of the start of the semester of the given date. If no date is given, return the start date of the current semester. The current semester is computed as follows: @@ -54,8 +51,11 @@ def get_start_of_semester(today: Optional[date] = None) -> date: - If the date is between 01/01 and 15/02 => Autumn semester of the previous year. - If the date is between 15/02 and 15/08 => Spring semester - :param today: the date to use to compute the semester. If None, use today's date. - :return: the date of the start of the semester + Args: + today: the date to use to compute the semester. If None, use today's date. + + Returns: + the date of the start of the semester """ if today is None: today = timezone.now().date() @@ -72,16 +72,18 @@ def get_start_of_semester(today: Optional[date] = None) -> date: def get_semester_code(d: Optional[date] = None) -> str: - """ - Return the semester code of the given date. + """Return the semester code of the given date. If no date is given, return the semester code of the current semester. The semester code is an upper letter (A for autumn, P for spring), followed by the last two digits of the year. For example, the autumn semester of 2018 is "A18". - :param d: the date to use to compute the semester. If None, use today's date. - :return: the semester code corresponding to the given date + Args: + d: the date to use to compute the semester. If None, use today's date. + + Returns: + the semester code corresponding to the given date """ if d is None: d = timezone.now().date() @@ -147,8 +149,15 @@ def exif_auto_rotate(image): return image -def doku_to_markdown(text): - """This is a quite correct doku translator""" +def doku_to_markdown(text: str) -> str: + """Convert doku text to the corresponding markdown. + + Args: + text: the doku text to convert + + Returns: + The converted markdown text + """ text = re.sub( r"([^:]|^)\/\/(.*?)\/\/", r"*\2*", text ) # Italic (prevents protocol:// conflict) @@ -235,7 +244,14 @@ def doku_to_markdown(text): def bbcode_to_markdown(text): - """This is a very basic BBcode translator""" + """Convert bbcode text to the corresponding markdown. + + Args: + text: the bbcode text to convert + + Returns: + The converted markdown text + """ text = re.sub(r"\[b\](.*?)\[\/b\]", r"**\1**", text, flags=re.DOTALL) # Bold text = re.sub(r"\[i\](.*?)\[\/i\]", r"*\1*", text, flags=re.DOTALL) # Italic text = re.sub(r"\[u\](.*?)\[\/u\]", r"__\1__", text, flags=re.DOTALL) # Underline diff --git a/core/views/__init__.py b/core/views/__init__.py index 61665129..7439f037 100644 --- a/core/views/__init__.py +++ b/core/views/__init__.py @@ -23,6 +23,7 @@ # import types +from typing import Any from django.core.exceptions import ( ImproperlyConfigured, @@ -39,6 +40,7 @@ from django.views.generic.detail import SingleObjectMixin from django.views.generic.edit import FormView from sentry_sdk import last_event_id +from core.models import User from core.views.forms import LoginForm @@ -60,60 +62,63 @@ def internal_servor_error(request): return HttpResponseServerError(render(request, "core/500.jinja")) -def can_edit_prop(obj, user): - """ - :param obj: Object to test for permission - :param user: core.models.User to test permissions against - :return: if user is authorized to edit object properties - :rtype: bool +def can_edit_prop(obj: Any, user: User) -> bool: + """Can the user edit the properties of the object. - :Example: + Args: + obj: Object to test for permission + user: core.models.User to test permissions against - .. code-block:: python + Returns: + True if user is authorized to edit object properties else False + Examples: + ```python if not can_edit_prop(self.object ,request.user): raise PermissionDenied - + ``` """ if obj is None or user.is_owner(obj): return True return False -def can_edit(obj, user): - """ - :param obj: Object to test for permission - :param user: core.models.User to test permissions against - :return: if user is authorized to edit object - :rtype: bool +def can_edit(obj: Any, user: User): + """Can the user edit the object. - :Example: + Args: + obj: Object to test for permission + user: core.models.User to test permissions against - .. code-block:: python + Returns: + True if user is authorized to edit object else False - if not can_edit(self.object ,request.user): + Examples: + ```python + if not can_edit(self.object, request.user): raise PermissionDenied - + ``` """ if obj is None or user.can_edit(obj): return True return can_edit_prop(obj, user) -def can_view(obj, user): - """ - :param obj: Object to test for permission - :param user: core.models.User to test permissions against - :return: if user is authorized to see object - :rtype: bool +def can_view(obj: Any, user: User): + """Can the user see the object. - :Example: + Args: + obj: Object to test for permission + user: core.models.User to test permissions against - .. code-block:: python + Returns: + True if user is authorized to see object else False + Examples: + ```python if not can_view(self.object ,request.user): raise PermissionDenied - + ``` """ if obj is None or user.can_view(obj): return True @@ -121,20 +126,22 @@ def can_view(obj, user): class GenericContentPermissionMixinBuilder(View): - """ - Used to build permission mixins + """Used to build permission mixins. + This view protect any child view that would be showing an object that is restricted based - on two properties + on two properties. - :prop permission_function: function to test permission with, takes an object and an user an return a bool - :prop raised_error: permission to be raised - - :raises: raised_error + Attributes: + raised_error: permission to be raised """ - permission_function = lambda obj, user: False raised_error = PermissionDenied + @staticmethod + def permission_function(obj: Any, user: User) -> bool: + """Function to test permission with.""" + return False + @classmethod def get_permission_function(cls, obj, user): return cls.permission_function(obj, user) @@ -162,11 +169,12 @@ class GenericContentPermissionMixinBuilder(View): class CanCreateMixin(View): - """ - This view is made to protect any child view that would create an object, and thus, that can not be protected by any - of the following mixin + """Protect any child view that would create an object. - :raises: PermissionDenied + Raises: + PermissionDenied: + If the user has not the necessary permission + to create the object of the view. """ def dispatch(self, request, *arg, **kwargs): @@ -183,55 +191,54 @@ class CanCreateMixin(View): class CanEditPropMixin(GenericContentPermissionMixinBuilder): - """ - This view is made to protect any child view that would be showing some properties of an object that are restricted - to only the owner group of the given object. - In other word, you can make a view with this view as parent, and it would be retricted to the users that are in the - object's owner_group + """Ensure the user has owner permissions on the child view object. - :raises: PermissionDenied + In other word, you can make a view with this view as parent, + and it will be retricted to the users that are in the + object's owner_group or that pass the `obj.can_be_viewed_by` test. + + Raises: + PermissionDenied: If the user cannot see the object """ permission_function = can_edit_prop class CanEditMixin(GenericContentPermissionMixinBuilder): - """ - This view makes exactly the same thing as its direct parent, but checks the group on the edit_groups field of the - object + """Ensure the user has permission to edit this view's object. - :raises: PermissionDenied + Raises: + PermissionDenied: if the user cannot edit this view's object. """ permission_function = can_edit class CanViewMixin(GenericContentPermissionMixinBuilder): - """ - This view still makes exactly the same thing as its direct parent, but checks the group on the view_groups field of - the object + """Ensure the user has permission to view this view's object. - :raises: PermissionDenied + Raises: + PermissionDenied: if the user cannot edit this view's object. """ permission_function = can_view class UserIsRootMixin(GenericContentPermissionMixinBuilder): - """ - This view check if the user is root + """Allow only root admins. - :raises: PermissionDenied + Raises: + PermissionDenied: if the user isn't root """ permission_function = lambda obj, user: user.is_root class FormerSubscriberMixin(View): - """ - This view check if the user was at least an old subscriber + """Check if the user was at least an old subscriber. - :raises: PermissionDenied + Raises: + PermissionDenied: if the user never subscribed. """ def dispatch(self, request, *args, **kwargs): @@ -241,10 +248,10 @@ class FormerSubscriberMixin(View): class UserIsLoggedMixin(View): - """ - This view check if the user is logged + """Check if the user is logged. - :raises: PermissionDenied + Raises: + PermissionDenied: """ def dispatch(self, request, *args, **kwargs): @@ -254,9 +261,7 @@ class UserIsLoggedMixin(View): class TabedViewMixin(View): - """ - This view provide the basic functions for displaying tabs in the template - """ + """Basic functions for displaying tabs in the template.""" def get_tabs_title(self): if hasattr(self, "tabs_title"): @@ -299,7 +304,7 @@ class QuickNotifMixin: return ret def get_context_data(self, **kwargs): - """Add quick notifications to context""" + """Add quick notifications to context.""" kwargs = super().get_context_data(**kwargs) kwargs["quick_notifs"] = [] for n in self.quick_notif_list: @@ -312,21 +317,15 @@ class QuickNotifMixin: class DetailFormView(SingleObjectMixin, FormView): - """ - Class that allow both a detail view and a form view - """ + """Class that allow both a detail view and a form view.""" def get_object(self): - """ - Get current group from id in url - """ + """Get current group from id in url.""" return self.cached_object @cached_property def cached_object(self): - """ - Optimisation on group retrieval - """ + """Optimisation on group retrieval.""" return super().get_object() diff --git a/core/views/files.py b/core/views/files.py index b674267b..83f7e00c 100644 --- a/core/views/files.py +++ b/core/views/files.py @@ -42,8 +42,7 @@ from counter.models import Counter def send_file(request, file_id, file_class=SithFile, file_attr="file"): - """ - Send a file through Django without loading the whole file into + """Send a file through Django without loading the whole file into memory at once. The FileWrapper will turn the file object into an iterator for chunks of 8KB. """ @@ -268,7 +267,7 @@ class FileEditPropView(CanEditPropMixin, UpdateView): class FileView(CanViewMixin, DetailView, FormMixin): - """This class handle the upload of new files into a folder""" + """Handle the upload of new files into a folder.""" model = SithFile pk_url_kwarg = "file_id" @@ -278,8 +277,8 @@ class FileView(CanViewMixin, DetailView, FormMixin): @staticmethod def handle_clipboard(request, obj): - """ - This method handles the clipboard in the view. + """Handle the clipboard in the view. + This method can fail, since it does not catch the exceptions coming from below, allowing proper handling in the calling view. Use this method like this: diff --git a/core/views/forms.py b/core/views/forms.py index 811b13d5..140f5158 100644 --- a/core/views/forms.py +++ b/core/views/forms.py @@ -196,10 +196,9 @@ class RegisteringForm(UserCreationForm): class UserProfileForm(forms.ModelForm): - """ - Form handling the user profile, managing the files + """Form handling the user profile, managing the files This form is actually pretty bad and was made in the rush before the migration. It should be refactored. - TODO: refactor this form + TODO: refactor this form. """ class Meta: diff --git a/core/views/group.py b/core/views/group.py index 70d5c3a4..87c18bae 100644 --- a/core/views/group.py +++ b/core/views/group.py @@ -13,9 +13,7 @@ # # -""" -This module contains views to manage Groups -""" +"""Views to manage Groups.""" from ajax_select.fields import AutoCompleteSelectMultipleField from django import forms @@ -31,9 +29,7 @@ from core.views import CanCreateMixin, CanEditMixin, DetailFormView class EditMembersForm(forms.Form): - """ - Add and remove members from a Group - """ + """Add and remove members from a Group.""" def __init__(self, *args, **kwargs): self.current_users = kwargs.pop("users", []) @@ -53,9 +49,7 @@ class EditMembersForm(forms.Form): ) def clean_users_added(self): - """ - Check that the user is not trying to add an user already in the group - """ + """Check that the user is not trying to add an user already in the group.""" cleaned_data = super().clean() users_added = cleaned_data.get("users_added", None) if not users_added: @@ -77,9 +71,7 @@ class EditMembersForm(forms.Form): class GroupListView(CanEditMixin, ListView): - """ - Displays the Group list - """ + """Displays the Group list.""" model = RealGroup ordering = ["name"] @@ -87,9 +79,7 @@ class GroupListView(CanEditMixin, ListView): class GroupEditView(CanEditMixin, UpdateView): - """ - Edit infos of a Group - """ + """Edit infos of a Group.""" model = RealGroup pk_url_kwarg = "group_id" @@ -98,9 +88,7 @@ class GroupEditView(CanEditMixin, UpdateView): class GroupCreateView(CanCreateMixin, CreateView): - """ - Add a new Group - """ + """Add a new Group.""" model = RealGroup template_name = "core/create.jinja" @@ -108,9 +96,8 @@ class GroupCreateView(CanCreateMixin, CreateView): class GroupTemplateView(CanEditMixin, DetailFormView): - """ - Display all users in a given Group - Allow adding and removing users from it + """Display all users in a given Group + Allow adding and removing users from it. """ model = RealGroup @@ -143,9 +130,7 @@ class GroupTemplateView(CanEditMixin, DetailFormView): class GroupDeleteView(CanEditMixin, DeleteView): - """ - Delete a Group - """ + """Delete a Group.""" model = RealGroup pk_url_kwarg = "group_id" diff --git a/core/views/user.py b/core/views/user.py index 5a01a90b..66abd014 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -74,9 +74,7 @@ from trombi.views import UserTrombiForm @method_decorator(check_honeypot, name="post") class SithLoginView(views.LoginView): - """ - The login View - """ + """The login View.""" template_name = "core/login.jinja" authentication_form = LoginForm @@ -85,33 +83,25 @@ class SithLoginView(views.LoginView): class SithPasswordChangeView(views.PasswordChangeView): - """ - Allows a user to change its password - """ + """Allows a user to change its password.""" template_name = "core/password_change.jinja" success_url = reverse_lazy("core:password_change_done") class SithPasswordChangeDoneView(views.PasswordChangeDoneView): - """ - Allows a user to change its password - """ + """Allows a user to change its password.""" template_name = "core/password_change_done.jinja" def logout(request): - """ - The logout view - """ + """The logout view.""" return views.logout_then_login(request) def password_root_change(request, user_id): - """ - Allows a root user to change someone's password - """ + """Allows a root user to change someone's password.""" if not request.user.is_root: raise PermissionDenied user = User.objects.filter(id=user_id).first() @@ -131,9 +121,7 @@ def password_root_change(request, user_id): @method_decorator(check_honeypot, name="post") class SithPasswordResetView(views.PasswordResetView): - """ - Allows someone to enter an email address for resetting password - """ + """Allows someone to enter an email address for resetting password.""" template_name = "core/password_reset.jinja" email_template_name = "core/password_reset_email.jinja" @@ -141,26 +129,20 @@ class SithPasswordResetView(views.PasswordResetView): class SithPasswordResetDoneView(views.PasswordResetDoneView): - """ - Confirm that the reset email has been sent - """ + """Confirm that the reset email has been sent.""" template_name = "core/password_reset_done.jinja" class SithPasswordResetConfirmView(views.PasswordResetConfirmView): - """ - Provide a reset password form - """ + """Provide a reset password form.""" template_name = "core/password_reset_confirm.jinja" success_url = reverse_lazy("core:password_reset_complete") class SithPasswordResetCompleteView(views.PasswordResetCompleteView): - """ - Confirm the password has successfully been reset - """ + """Confirm the password has successfully been reset.""" template_name = "core/password_reset_complete.jinja" @@ -302,9 +284,7 @@ class UserTabsMixin(TabedViewMixin): class UserView(UserTabsMixin, CanViewMixin, DetailView): - """ - Display a user's profile - """ + """Display a user's profile.""" model = User pk_url_kwarg = "user_id" @@ -321,9 +301,7 @@ class UserView(UserTabsMixin, CanViewMixin, DetailView): class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView): - """ - Display a user's pictures - """ + """Display a user's pictures.""" model = User pk_url_kwarg = "user_id" @@ -361,9 +339,7 @@ def delete_user_godfather(request, user_id, godfather_id, is_father): class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView): - """ - Display a user's godfathers - """ + """Display a user's godfathers.""" model = User pk_url_kwarg = "user_id" @@ -394,9 +370,7 @@ class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView): class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView): - """ - Display a user's family tree - """ + """Display a user's family tree.""" model = User pk_url_kwarg = "user_id" @@ -415,9 +389,7 @@ class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView): class UserGodfathersTreePictureView(CanViewMixin, DetailView): - """ - Display a user's tree as a picture - """ + """Display a user's tree as a picture.""" model = User pk_url_kwarg = "user_id" @@ -489,9 +461,7 @@ class UserGodfathersTreePictureView(CanViewMixin, DetailView): class UserStatsView(UserTabsMixin, CanViewMixin, DetailView): - """ - Display a user's stats - """ + """Display a user's stats.""" model = User pk_url_kwarg = "user_id" @@ -591,9 +561,7 @@ class UserStatsView(UserTabsMixin, CanViewMixin, DetailView): class UserMiniView(CanViewMixin, DetailView): - """ - Display a user's profile - """ + """Display a user's profile.""" model = User pk_url_kwarg = "user_id" @@ -602,18 +570,14 @@ class UserMiniView(CanViewMixin, DetailView): class UserListView(ListView, CanEditPropMixin): - """ - Displays the user list - """ + """Displays the user list.""" model = User template_name = "core/user_list.jinja" class UserUploadProfilePictView(CanEditMixin, DetailView): - """ - Handle the upload of the profile picture taken with webcam in navigator - """ + """Handle the upload of the profile picture taken with webcam in navigator.""" model = User pk_url_kwarg = "user_id" @@ -650,9 +614,7 @@ class UserUploadProfilePictView(CanEditMixin, DetailView): class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView): - """ - Edit a user's profile - """ + """Edit a user's profile.""" model = User pk_url_kwarg = "user_id" @@ -663,9 +625,7 @@ class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView): board_only = [] def remove_restricted_fields(self, request): - """ - Removes edit_once and board_only fields - """ + """Removes edit_once and board_only fields.""" for i in self.edit_once: if getattr(self.form.instance, i) and not ( request.user.is_board_member or request.user.is_root @@ -703,9 +663,7 @@ class UserUpdateProfileView(UserTabsMixin, CanEditMixin, UpdateView): class UserClubView(UserTabsMixin, CanViewMixin, DetailView): - """ - Display the user's club(s) - """ + """Display the user's club(s).""" model = User context_object_name = "profile" @@ -715,9 +673,7 @@ class UserClubView(UserTabsMixin, CanViewMixin, DetailView): class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): - """ - Edit a user's preferences - """ + """Edit a user's preferences.""" model = User pk_url_kwarg = "user_id" @@ -752,9 +708,7 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView): - """ - Edit a user's groups - """ + """Edit a user's groups.""" model = User pk_url_kwarg = "user_id" @@ -767,9 +721,7 @@ class UserUpdateGroupView(UserTabsMixin, CanEditPropMixin, UpdateView): class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateView): - """ - Displays the logged user's tools - """ + """Displays the logged user's tools.""" template_name = "core/user_tools.jinja" current_tab = "tools" @@ -786,9 +738,7 @@ class UserToolsView(QuickNotifMixin, UserTabsMixin, UserIsLoggedMixin, TemplateV class UserAccountBase(UserTabsMixin, DetailView): - """ - Base class for UserAccount - """ + """Base class for UserAccount.""" model = User pk_url_kwarg = "user_id" @@ -809,9 +759,7 @@ class UserAccountBase(UserTabsMixin, DetailView): class UserAccountView(UserAccountBase): - """ - Display a user's account - """ + """Display a user's account.""" template_name = "core/user_account.jinja" @@ -858,9 +806,7 @@ class UserAccountView(UserAccountBase): class UserAccountDetailView(UserAccountBase, YearMixin, MonthMixin): - """ - Display a user's account for month - """ + """Display a user's account for month.""" template_name = "core/user_account_detail.jinja" diff --git a/counter/forms.py b/counter/forms.py index 1adbed68..4b5579b7 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -30,9 +30,8 @@ class BillingInfoForm(forms.ModelForm): class StudentCardForm(forms.ModelForm): - """ - Form for adding student cards - Only used for user profile since CounterClick is to complicated + """Form for adding student cards + Only used for user profile since CounterClick is to complicated. """ class Meta: @@ -48,8 +47,7 @@ class StudentCardForm(forms.ModelForm): class GetUserForm(forms.Form): - """ - The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view, + """The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view, reverse function, or any other use. The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with diff --git a/counter/models.py b/counter/models.py index 8d9db9b4..ebe33404 100644 --- a/counter/models.py +++ b/counter/models.py @@ -44,9 +44,10 @@ from subscription.models import Subscription class Customer(models.Model): - """ - This class extends a user to make a customer. It adds some basic customers' information, such as the account ID, and - is used by other accounting classes as reference to the customer, rather than using User + """Customer data of a User. + + It adds some basic customers' information, such as the account ID, and + is used by other accounting classes as reference to the customer, rather than using User. """ user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE) @@ -63,10 +64,9 @@ class Customer(models.Model): return "%s - %s" % (self.user.username, self.account_id) def save(self, *args, allow_negative=False, is_selling=False, **kwargs): - """ - is_selling : tell if the current action is a selling + """is_selling : tell if the current action is a selling allow_negative : ignored if not a selling. Allow a selling to put the account in negative - Those two parameters avoid blocking the save method of a customer if his account is negative + Those two parameters avoid blocking the save method of a customer if his account is negative. """ if self.amount < 0 and (is_selling and not allow_negative): raise ValidationError(_("Not enough money")) @@ -84,9 +84,8 @@ class Customer(models.Model): @property def can_buy(self) -> bool: - """ - Check if whether this customer has the right to - purchase any item. + """Check if whether this customer has the right to purchase any item. + This must be not confused with the Product.can_be_sold_to(user) method as the present method returns an information about a customer whereas the other tells something @@ -100,8 +99,7 @@ class Customer(models.Model): @classmethod def get_or_create(cls, user: User) -> Tuple[Customer, bool]: - """ - Work in pretty much the same way as the usual get_or_create method, + """Work in pretty much the same way as the usual get_or_create method, but with the default field replaced by some under the hood. If the user has an account, return it as is. @@ -158,9 +156,8 @@ class Customer(models.Model): class BillingInfo(models.Model): - """ - Represent the billing information of a user, which are required - by the 3D-Secure v2 system used by the etransaction module + """Represent the billing information of a user, which are required + by the 3D-Secure v2 system used by the etransaction module. """ customer = models.OneToOneField( @@ -182,10 +179,9 @@ class BillingInfo(models.Model): return f"{self.first_name} {self.last_name}" def to_3dsv2_xml(self) -> str: - """ - Convert the data from this model into a xml usable + """Convert the data from this model into a xml usable by the online paying service of the Crédit Agricole bank. - see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster` + see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster`. """ data = { "Address": { @@ -204,9 +200,9 @@ class BillingInfo(models.Model): class ProductType(models.Model): - """ - This describes a product type - Useful only for categorizing, changes are made at the product level for now + """A product type. + + Useful only for categorizing. """ name = models.CharField(_("name"), max_length=30) @@ -229,9 +225,7 @@ class ProductType(models.Model): return reverse("counter:producttype_list") def is_owned_by(self, user): - """ - Method to see if that object can be edited by the given user - """ + """Method to see if that object can be edited by the given user.""" if user.is_anonymous: return False if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): @@ -240,9 +234,7 @@ class ProductType(models.Model): class Product(models.Model): - """ - This describes a product, with all its related informations - """ + """A product, with all its related information.""" name = models.CharField(_("name"), max_length=64) description = models.TextField(_("description"), blank=True) @@ -297,9 +289,7 @@ class Product(models.Model): return settings.SITH_ECOCUP_DECO == self.id def is_owned_by(self, user): - """ - Method to see if that object can be edited by the given user - """ + """Method to see if that object can be edited by the given user.""" if user.is_anonymous: return False if user.is_in_group( @@ -309,8 +299,7 @@ class Product(models.Model): return False def can_be_sold_to(self, user: User) -> bool: - """ - Check if whether the user given in parameter has the right to buy + """Check if whether the user given in parameter has the right to buy this product or not. This must be not confused with the Customer.can_buy() @@ -319,7 +308,8 @@ class Product(models.Model): whereas the other tells something about a Customer (and not a user, they are not the same model). - :return: True if the user can buy this product else False + Returns: + True if the user can buy this product else False """ if not self.buying_groups.exists(): return True @@ -335,15 +325,16 @@ class Product(models.Model): class CounterQuerySet(models.QuerySet): def annotate_has_barman(self, user: User) -> CounterQuerySet: - """ - Annotate the queryset with the `user_is_barman` field. + """Annotate the queryset with the `user_is_barman` field. + For each counter, this field has value True if the user is a barman of this counter, else False. - :param user: the user we want to check if he is a barman - - Example:: + Args: + user: the user we want to check if he is a barman + Examples: + ```python sli = User.objects.get(username="sli") counters = ( Counter.objects @@ -353,6 +344,7 @@ class CounterQuerySet(models.QuerySet): print("Sli est barman dans les comptoirs suivants :") for counter in counters: print(f"- {counter.name}") + ``` """ subquery = user.counters.filter(pk=OuterRef("pk")) # noinspection PyTypeChecker @@ -417,23 +409,21 @@ class Counter(models.Model): return user.is_board_member or user in self.sellers.all() def gen_token(self): - """Generate a new token for this counter""" + """Generate a new token for this counter.""" self.token = "".join( random.choice(string.ascii_letters + string.digits) for x in range(30) ) self.save() def add_barman(self, user): - """ - Logs a barman in to the given counter - A user is stored as a tuple with its login time + """Logs a barman in to the given counter. + + A user is stored as a tuple with its login time. """ Permanency(user=user, counter=self, start=timezone.now(), end=None).save() def del_barman(self, user): - """ - Logs a barman out and store its permanency - """ + """Logs a barman out and store its permanency.""" perm = Permanency.objects.filter(counter=self, user=user, end=None).all() for p in perm: p.end = p.activity @@ -444,8 +434,7 @@ class Counter(models.Model): return self.get_barmen_list() def get_barmen_list(self): - """ - Returns the barman list as list of User + """Returns the barman list as list of User. Also handle the timeout of the barmen """ @@ -462,16 +451,12 @@ class Counter(models.Model): return bl def get_random_barman(self): - """ - Return a random user being currently a barman - """ + """Return a random user being currently a barman.""" bl = self.get_barmen_list() return bl[random.randrange(0, len(bl))] def update_activity(self): - """ - Update the barman activity to prevent timeout - """ + """Update the barman activity to prevent timeout.""" for p in Permanency.objects.filter(counter=self, end=None).all(): p.save() # Update activity @@ -479,25 +464,18 @@ class Counter(models.Model): return len(self.barmen_list) > 0 def is_inactive(self): - """ - Returns True if the counter self is inactive from SITH_COUNTER_MINUTE_INACTIVE's value minutes, else False - """ + """Returns True if the counter self is inactive from SITH_COUNTER_MINUTE_INACTIVE's value minutes, else False.""" return self.is_open() and ( (timezone.now() - self.permanencies.order_by("-activity").first().activity) > timedelta(minutes=settings.SITH_COUNTER_MINUTE_INACTIVE) ) def barman_list(self): - """ - Returns the barman id list - """ + """Returns the barman id list.""" return [b.id for b in self.get_barmen_list()] def can_refill(self): - """ - Show if the counter authorize the refilling with physic money - """ - + """Show if the counter authorize the refilling with physic money.""" if self.type != "BAR": return False if self.id in SITH_COUNTER_OFFICES: @@ -511,8 +489,7 @@ class Counter(models.Model): return is_ae_member def get_top_barmen(self) -> QuerySet: - """ - Return a QuerySet querying the office hours stats of all the barmen of all time + """Return a QuerySet querying the office hours stats of all the barmen of all time of this counter, ordered by descending number of hours. Each element of the QuerySet corresponds to a barman and has the following data : @@ -535,16 +512,17 @@ class Counter(models.Model): ) def get_top_customers(self, since: datetime | date | None = None) -> QuerySet: - """ - Return a QuerySet querying the money spent by customers of this counter + """Return a QuerySet querying the money spent by customers of this counter since the specified date, ordered by descending amount of money spent. Each element of the QuerySet corresponds to a customer and has the following data : - - the full name (first name + last name) of the customer - - the nickname of the customer - - the amount of money spent by the customer - :param since: timestamp from which to perform the calculation + - the full name (first name + last name) of the customer + - the nickname of the customer + - the amount of money spent by the customer + + Args: + since: timestamp from which to perform the calculation """ if since is None: since = get_start_of_semester() @@ -573,12 +551,15 @@ class Counter(models.Model): ) def get_total_sales(self, since: datetime | date | None = None) -> CurrencyField: - """ - Compute and return the total turnover of this counter - since the date specified in parameter (by default, since the start of the current - semester) - :param since: timestamp from which to perform the calculation - :return: Total revenue earned at this counter + """Compute and return the total turnover of this counter since the given date. + + By default, the date is the start of the current semester. + + Args: + since: timestamp from which to perform the calculation + + Returns: + Total revenue earned at this counter. """ if since is None: since = get_start_of_semester() @@ -591,9 +572,7 @@ class Counter(models.Model): class Refilling(models.Model): - """ - Handle the refilling - """ + """Handle the refilling.""" counter = models.ForeignKey( Counter, related_name="refillings", blank=False, on_delete=models.CASCADE @@ -665,9 +644,7 @@ class Refilling(models.Model): class Selling(models.Model): - """ - Handle the sellings - """ + """Handle the sellings.""" label = models.CharField(_("label"), max_length=64) product = models.ForeignKey( @@ -724,9 +701,7 @@ class Selling(models.Model): ) def save(self, *args, allow_negative=False, **kwargs): - """ - allow_negative : Allow this selling to use more money than available for this user - """ + """allow_negative : Allow this selling to use more money than available for this user.""" if not self.date: self.date = timezone.now() self.full_clean() @@ -864,8 +839,10 @@ class Selling(models.Model): class Permanency(models.Model): - """ - This class aims at storing a traceability of who was barman where and when + """A permanency of a barman, on a counter. + + This aims at storing a traceability of who was barman where and when. + Mainly for ~~dick size contest~~ establishing the top 10 barmen of the semester. """ user = models.ForeignKey( @@ -971,9 +948,7 @@ class CashRegisterSummary(models.Model): return object.__getattribute__(self, name) def is_owned_by(self, user): - """ - Method to see if that object can be edited by the given user - """ + """Method to see if that object can be edited by the given user.""" if user.is_anonymous: return False if user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID): @@ -1010,9 +985,7 @@ class CashRegisterSummaryItem(models.Model): class Eticket(models.Model): - """ - Eticket can be linked to a product an allows PDF generation - """ + """Eticket can be linked to a product an allows PDF generation.""" product = models.OneToOneField( Product, @@ -1041,9 +1014,7 @@ class Eticket(models.Model): return reverse("counter:eticket_list") def is_owned_by(self, user): - """ - Method to see if that object can be edited by the given user - """ + """Method to see if that object can be edited by the given user.""" if user.is_anonymous: return False return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID) @@ -1058,11 +1029,11 @@ class Eticket(models.Model): class StudentCard(models.Model): - """ - Alternative way to connect a customer into a counter + """Alternative way to connect a customer into a counter. + We are using Mifare DESFire EV1 specs since it's used for izly cards https://www.nxp.com/docs/en/application-note/AN10927.pdf - UID is 7 byte long that means 14 hexa characters + UID is 7 byte long that means 14 hexa characters. """ UID_SIZE = 14 diff --git a/counter/tests.py b/counter/tests.py index 3754a842..7f1d3cc5 100644 --- a/counter/tests.py +++ b/counter/tests.py @@ -140,10 +140,7 @@ class CounterTest(TestCase): assert response.status_code == 200 def test_annotate_has_barman_queryset(self): - """ - Test if the custom queryset method ``annotate_has_barman`` - works as intended - """ + """Test if the custom queryset method `annotate_has_barman` works as intended.""" self.sli.counters.set([self.foyer, self.mde]) counters = Counter.objects.annotate_has_barman(self.sli) for counter in counters: @@ -265,15 +262,11 @@ class CounterStatsTest(TestCase): assert response.status_code == 403 def test_get_total_sales(self): - """ - Test the result of the Counter.get_total_sales() method - """ + """Test the result of the Counter.get_total_sales() method.""" assert self.counter.get_total_sales() == 3102 def test_top_barmen(self): - """ - Test the result of Counter.get_top_barmen() is correct - """ + """Test the result of Counter.get_top_barmen() is correct.""" users = [self.skia, self.root, self.sli] perm_times = [ timedelta(days=16, hours=2, minutes=35, seconds=54), @@ -292,9 +285,7 @@ class CounterStatsTest(TestCase): ] def test_top_customer(self): - """ - Test the result of Counter.get_top_customers() is correct - """ + """Test the result of Counter.get_top_customers() is correct.""" users = [self.sli, self.skia, self.krophil, self.root] sale_amounts = [2000, 1000, 100, 2] assert list(self.counter.get_top_customers()) == [ @@ -588,9 +579,8 @@ class BarmanConnectionTest(TestCase): class StudentCardTest(TestCase): - """ - Tests for adding and deleting Stundent Cards - Test that an user can be found with it's student card + """Tests for adding and deleting Stundent Cards + Test that an user can be found with it's student card. """ @classmethod diff --git a/counter/views.py b/counter/views.py index 7e4bfaf1..3cff779e 100644 --- a/counter/views.py +++ b/counter/views.py @@ -75,9 +75,7 @@ from counter.models import ( class CounterAdminMixin(View): - """ - This view is made to protect counter admin section - """ + """Protect counter admin section.""" edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID] edit_club = [] @@ -105,9 +103,7 @@ class CounterAdminMixin(View): class StudentCardDeleteView(DeleteView, CanEditMixin): - """ - View used to delete a card from a user - """ + """View used to delete a card from a user.""" model = StudentCard template_name = "core/delete_confirm.jinja" @@ -210,9 +206,7 @@ class CounterTabsMixin(TabedViewMixin): class CounterMain( CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin ): - """ - The public (barman) view - """ + """The public (barman) view.""" model = Counter template_name = "counter/counter_main.jinja" @@ -239,9 +233,7 @@ class CounterMain( return super().post(request, *args, **kwargs) def get_context_data(self, **kwargs): - """ - We handle here the login form for the barman - """ + """We handle here the login form for the barman.""" if self.request.method == "POST": self.object = self.get_object() self.object.update_activity() @@ -275,9 +267,7 @@ class CounterMain( return kwargs def form_valid(self, form): - """ - We handle here the redirection, passing the user id of the asked customer - """ + """We handle here the redirection, passing the user id of the asked customer.""" self.kwargs["user_id"] = form.cleaned_data["user_id"] return super().form_valid(form) @@ -286,10 +276,9 @@ class CounterMain( class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): - """ - The click view + """The click view This is a detail view not to have to worry about loading the counter - Everything is made by hand in the post method + Everything is made by hand in the post method. """ model = Counter @@ -347,7 +336,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): return super().dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): - """Simple get view""" + """Simple get view.""" if "basket" not in request.session.keys(): # Init the basket session entry request.session["basket"] = {} request.session["basket_total"] = 0 @@ -364,7 +353,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): return ret def post(self, request, *args, **kwargs): - """Handle the many possibilities of the post request""" + """Handle the many possibilities of the post request.""" self.object = self.get_object() self.refill_form = None if (self.object.type != "BAR" and not request.user.is_authenticated) or ( @@ -481,10 +470,9 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): return len(request.POST) == 0 and len(request.body) != 0 def add_product(self, request, q=1, p=None): - """ - Add a product to the basket + """Add a product to the basket q is the quantity passed as integer - p is the product id, passed as an integer + p is the product id, passed as an integer. """ pid = p or parse_qs(request.body.decode())["product_id"][0] pid = str(pid) @@ -543,9 +531,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): return True def add_student_card(self, request): - """ - Add a new student card on the customer account - """ + """Add a new student card on the customer account.""" uid = request.POST["student_card_uid"] uid = str(uid) if not StudentCard.is_valid(uid): @@ -564,7 +550,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): return True def del_product(self, request): - """Delete a product from the basket""" + """Delete a product from the basket.""" pid = parse_qs(request.body.decode())["product_id"][0] product = self.get_product(pid) if pid in request.session["basket"]: @@ -581,11 +567,11 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): request.session.modified = True def parse_code(self, request): - """ - Parse the string entered by the barman + """Parse the string entered by the barman. + This can be of two forms : - -Merci de votre -commande
- -Celle-ci sera traite -dans les meilleurs dlais
- -Cordialement,
- - - -Gardez les rfrences de votre commande et n'hsitez pas -nous contacter si vous avez des questions :
- -tel : 00 00 00 00 00
- -courriel : contact@maboutique.fr
- -- {% trans %}Back to profile{% endtrans %} -
- -Hello World !
- {% endblock %} - -Enfin, on crée l'URL. On veut pouvoir appeler la page depuis https://localhost:8000/hello, le préfixe indiqué précédemment suffit donc. - -.. code-block:: python - - # hello/urls.py - from django.urls import path - from hello.views import HelloView - - urlpatterns = [ - # Le préfixe étant retiré lors du passage du routeur d'URL - # dans le fichier d'URL racine du projet, l'URL à matcher ici est donc vide - path("", HelloView.as_view(), name="hello"), - ] - -Et voilà, c'est fini, il ne reste plus qu'à lancer le serveur et à se rendre sur la page. - -Manipuler les arguments d'URL ------------------------------ - -Dans cette partie, on cherche à détecter les numéros passés dans l'URL pour les passer dans le template. On commence par ajouter cet URL modifiée. - -.. code-block:: python - - # hello/urls.py - from django.urls import path - from hello.views import HelloView - - urlpatterns = [ - path("", HelloView.as_view(), name="hello"), - path("- Hello World ! - {% if hello_id -%} - {{ hello_id }} - {%- endif -%} -
- {% endblock content %} - -.. note:: - - Il est tout à fait possible d'utiliser les arguments GET passés dans l'URL. Dans ce cas, il n'est pas obligatoire de modifier l'URL et il est possible de récupérer l'argument dans le dictionnaire `request.GET`. - -À l'assaut des modèles ----------------------- - -Pour cette dernière partie, nous allons ajouter une entrée dans la base de données et l'afficher dans un template. Nous allons ainsi créer un modèle nommé *Article* qui contiendra une entrée de texte pour le titre et une autre pour le contenu. - -Commençons par le modèle en lui même. - -.. code-block:: python - - # hello/models.py - from django.db import models - - - class Article(models.Model): - - title = models.CharField("titre", max_length=100) - content = models.TextField("contenu") - -Continuons avec une vue qui sera en charge d'afficher l'ensemble des articles présent dans la base. - -.. code-block:: python - - # hello/views.py - - from django.views.generic import ListView - - from hello.models import Article - - ... - - # On hérite de ListView pour avoir plusieurs objets - class ArticlesListView(ListView): - - model = Article # On base la vue sur le modèle Article - template_name = "hello/articles.jinja" - -On n'oublie pas l'URL. - -.. code-block:: python - - from hello.views import HelloView, ArticlesListView - - urlpatterns = [ - ... - path("articles/", ArticlesListView.as_view(), name="articles_list") - ] - -Et enfin le template. - -.. code-block:: html+jinja - - {# hello/templates/hello/articles.jinja #} - {% extends "core/base.jinja" %} - - {% block title %} - Hello World Articles - {% endblock title %} - - {% block content %} - {# Par défaut une liste d'objets venant de ListView s'appelle object_list #} - {% for article in object_list %} -{{ article.content }}
- {% endfor %} - {% endblock content %} - -Maintenant que toute la logique de récupération et d'affichage est terminée, la page est accessible à l'adresse https://localhost:8000/hello/articles. - -Mais, j'ai une erreur ! Il se passe quoi ?! Et bien c'est simple, nous avons créé le modèle mais il n'existe pas dans la base de données. Il est dans un premier temps important de créer un fichier de migrations qui contient des instructions pour la génération de celles-ci. Ce sont les fichiers qui sont enregistrés dans le dossier migration. Pour les générer à partir des classes de modèles qu'on vient de manipuler il suffit d'une seule commande. - -.. code-block:: bash - - ./manage.py makemigrations - -Un fichier *hello/migrations/0001_initial.py* se crée automatiquement, vous pouvez même aller le voir. - -.. note:: - - Il est tout à fait possible de modifier à la main les fichiers de migrations. C'est très intéressant si par exemple il faut appliquer des modifications sur les données d'un modèle existant après cette migration mais c'est bien au delà du sujet de ce tutoriel. Référez vous à la documentation pour ce genre de choses. - -J'ai toujours une erreur ! Mais oui, c'est pas fini, faut pas aller trop vite. Maintenant il faut appliquer les modifications à la base de données. - -.. code-block:: bash - - ./manage.py migrate - -Et voilà, là il n'y a plus d'erreur. Tout fonctionne et on a une superbe page vide puisque aucun contenu pour cette table n'est dans la base. Nous allons en rajouter. Pour cela nous allons utiliser le fichier *core/management/commands/populate.py* qui contient la commande qui initialise les données de la base de données de test. C'est un fichier très important qu'on viendra à modifier assez souvent. Nous allons y ajouter quelques articles. - -.. code-block:: python - - # core/management/commands/populate.py - from hello.models import Article - - ... - - class Command(BaseCommand): - - ... - - def handle(self, *args, **options): - - ... - - # les deux syntaxes ci-dessous sont correctes et donnent le même résultat - Article(title="First hello", content="Bonjour tout le monde").save() - Article.objects.create(title="Tutorial", content="C'était un super tutoriel") - - -On regénère enfin les données de test en lançant la commande que l'on vient de modifier. - -.. code-block:: bash - - ./manage.py setup - -On revient sur https://localhost:8000/hello/articles et cette fois-ci nos deux articles apparaissent correctement. \ No newline at end of file diff --git a/doc/start/install.rst b/doc/start/install.rst deleted file mode 100644 index 257fcf46..00000000 --- a/doc/start/install.rst +++ /dev/null @@ -1,242 +0,0 @@ -Installer le projet -=================== - -Dépendances du système ----------------------- - -Certaines dépendances sont nécessaires niveau système : - -* poetry -* libssl -* libjpeg -* zlib1g-dev -* python -* gettext -* graphviz - -Sur Windows -~~~~~~~~~~~ - -Chers utilisateurs de Windows, quel que soit votre amour de Windows, -de Bill Gates et des bloatwares, je suis désolé -de vous annoncer que, certaines dépendances étant uniquement disponibles sur des sytèmes UNIX, -il n'est pas possible développer le site sur Windows. - -Heureusement, il existe une alternative qui ne requiert pas de désinstaller votre -OS ni de mettre un dual boot sur votre ordinateur : :code:`WSL`. - -- **Prérequis:** vous devez être sur Windows 10 version 2004 ou ultérieure (build 19041 & versions ultérieures) ou Windows 11. -- **Plus d'info:** `docs.microsoft.com