Aller au contenu

# Views

## `DetailFormView`

Bases: `FormView`, `BaseDetailView`

Class that allow both a detail view and a form view.

## `SithFile`

Bases: `Model`

### `clean()`

Cleans up the file.

Source code in `core/models.py`

    943
    944
    945
    946
    947
    948
    949
    950
    951
    952
    953
    954
    955
    956
    957
    958
    959
    960
    961
    962
    963
    964
    965
    966
    967
    968
    969
    970
    971
    972
    973
    974
    975
    976
    977
    978
    def clean(self):
        """Cleans up the file."""
        super().clean()
        if "/" in self.name:
            raise ValidationError(_("Character '/' not authorized in name"))
        if self == self.parent:
            raise ValidationError(_("Loop in folder tree"), code="loop")
        if self == self.parent or (
            self.parent is not None and self in self.get_parent_list()
        ):
            raise ValidationError(_("Loop in folder tree"), code="loop")
        if self.parent and self.parent.is_file:
            raise ValidationError(
                _("You can not make a file be a children of a non folder file")
            )
        if (
            self.parent is None
            and SithFile.objects.exclude(id=self.id)
            .filter(parent=None, name=self.name)
            .exists()
        ) or (
            self.parent
            and self.parent.children.exclude(id=self.id).filter(name=self.name).exists()
        ):
            raise ValidationError(_("Duplicate file"), code="duplicate")
        if self.is_folder:
            if self.file:
                try:
                    Image.open(BytesIO(self.file.read()))
                except Image.UnidentifiedImageError as e:
                    raise ValidationError(
                        _("This is not a valid folder thumbnail")
                    ) from e
            self.mime_type = "inode/directory"
        if self.is_file and (self.file is None or self.file == ""):
            raise ValidationError(_("You must provide a file"))

### `apply_rights_recursively(*, only_folders=False)`

Apply the rights of this file to all children recursively.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`only_folders` | `bool` | If True, only apply the rights to SithFiles that are folders. | `False`

Source code in `core/models.py`

     980
     981
     982
     983
     984
     985
     986
     987
     988
     989
     990
     991
     992
     993
     994
     995
     996
     997
     998
     999
    1000
    1001
    1002
    1003
    1004
    1005
    def apply_rights_recursively(self, *, only_folders: bool = False) -> None:
        """Apply the rights of this file to all children recursively.

        Args:
            only_folders: If True, only apply the rights to SithFiles that are folders.
        """
        file_ids = []
        explored_ids = [self.id]
        while len(explored_ids) > 0:  # find all children recursively
            file_ids.extend(explored_ids)
            next_level = SithFile.objects.filter(parent_id__in=explored_ids)
            if only_folders:
                next_level = next_level.filter(is_folder=True)
            explored_ids = list(next_level.values_list("id", flat=True))
        for through in (SithFile.view_groups.through, SithFile.edit_groups.through):
            # force evaluation. Without this, the iterator yields nothing
            groups = list(
                through.objects.filter(sithfile_id=self.id).values_list(
                    "group_id", flat=True
                )
            )
            # delete previous rights
            through.objects.filter(sithfile_id__in=file_ids).delete()
            through.objects.bulk_create(  # create new rights
                [through(sithfile_id=f, group_id=g) for f in file_ids for g in groups]
            )

### `copy_rights()`

Copy, if possible, the rights of the parent folder.

Source code in `core/models.py`

    1007
    1008
    1009
    1010
    1011
    def copy_rights(self):
        """Copy, if possible, the rights of the parent folder."""
        if self.parent is not None:
            self.edit_groups.set(self.parent.edit_groups.all())
            self.view_groups.set(self.parent.view_groups.all())

### `move_to(parent)`

Move a file to a new parent. `parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change anything. This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify SithFiles recursively, so it stays efficient even with top-level folders.

Source code in `core/models.py`

    1013
    1014
    1015
    1016
    1017
    1018
    1019
    1020
    1021
    1022
    1023
    1024
    def move_to(self, parent):
        """Move a file to a new parent.
        `parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change
        anything.
        This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify
        SithFiles recursively, so it stays efficient even with top-level folders.
        """
        if not parent.is_folder:
            return
        self.parent = parent
        self.clean()
        self.save()

## `AllowFragment`

Add `is_fragment` to templates. It's only True if the request is emitted by htmx

## `MultipleFileInput`

Bases: `ClearableFileInput`

## `MultipleFileField(*args, **kwargs)`

Bases: `_MultipleFieldMixin`, `FileField`

Source code in `core/views/files.py`

    113
    114
    115
    def __init__(self, *args, **kwargs):
        kwargs.setdefault("widget", MultipleFileInput())
        super().__init__(*args, **kwargs)

## `MultipleImageField(*args, **kwargs)`

Bases: `_MultipleFieldMixin`, `ImageField`

Source code in `core/views/files.py`

    113
    114
    115
    def __init__(self, *args, **kwargs):
        kwargs.setdefault("widget", MultipleFileInput())
        super().__init__(*args, **kwargs)

## `AddFilesForm`

Bases: `Form`

## `FileListView`

Bases: `ListView`

## `FileEditView`

Bases: `CanEditMixin`, `UpdateView`

## `FileEditPropForm`

Bases: `ModelForm`

## `FileEditPropView`

Bases: `CanEditPropMixin`, `UpdateView`

## `FileView`

Bases: `CanViewMixin`, `DetailView`, `FormMixin`

Handle the upload of new files into a folder.

### `handle_clipboard(request, obj)` `staticmethod`

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:

    FileView.handle_clipboard(request, self.object)

`request` is usually the self.request obj in your view `obj` is the SithFile object you want to put in the clipboard, or where you want to paste the clipboard

Source code in `core/views/files.py`

    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    @staticmethod
    def handle_clipboard(request, obj):
        """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:

            FileView.handle_clipboard(request, self.object)

        `request` is usually the self.request obj in your view
        `obj` is the SithFile object you want to put in the clipboard, or
                 where you want to paste the clipboard
        """
        if "delete" in request.POST:
            for f_id in request.POST.getlist("file_list"):
                file = SithFile.objects.filter(id=f_id).first()
                if file:
                    file.delete()
        if "clear" in request.POST:
            request.session["clipboard"] = []
        if "cut" in request.POST:
            for f_id_str in request.POST.getlist("file_list"):
                f_id = int(f_id_str)
                if (
                    f_id in [c.id for c in obj.children.all()]
                    and f_id not in request.session["clipboard"]
                ):
                    request.session["clipboard"].append(f_id)
        if "paste" in request.POST:
            for f_id in request.session["clipboard"]:
                file = SithFile.objects.filter(id=f_id).first()
                if file:
                    file.move_to(obj)
            request.session["clipboard"] = []
        request.session.modified = True

## `FileDeleteView`

Bases: `AllowFragment`, `CanEditPropMixin`, `DeleteView`

## `FileModerationView`

Bases: `AllowFragment`, `ListView`

## `FileModerateView`

Bases: `CanEditPropMixin`, `SingleObjectMixin`

## `Group`

Bases: `Group`

Wrapper around django.auth.Group

## `PermissionGroupsForm(instance, **kwargs)`

Bases: `ModelForm`

Manage the groups that have a specific permission.

Source code in `core/views/forms.py`

    480
    481
    482
    def __init__(self, instance: Permission, **kwargs):
        super().__init__(instance=instance, **kwargs)
        self.fields["groups"].initial = instance.group_set.all()

## `EditMembersForm(*args, **kwargs)`

Bases: `Form`

Add and remove members from a Group.

Source code in `core/views/group.py`

    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    def __init__(self, *args, **kwargs):
        self.current_users = kwargs.pop("users", [])
        super().__init__(*args, **kwargs)

        self.fields["users_added"] = forms.ModelMultipleChoiceField(
            label=_("Users to add to group"),
            help_text=_("Search users to add (one or more)."),
            required=False,
            widget=AutoCompleteSelectMultipleUser,
            queryset=User.objects.exclude(id__in=self.current_users).all(),
        )

        self.fields["users_removed"] = forms.ModelMultipleChoiceField(
            User.objects.filter(id__in=self.current_users).all(),
            label=_("Users to remove from group"),
            required=False,
            widget=forms.CheckboxSelectMultiple,
        )

## `GroupListView`

Bases: `CanEditMixin`, `ListView`

Displays the Group list.

## `GroupEditView`

Bases: `CanEditMixin`, `UpdateView`

Edit infos of a Group.

## `GroupCreateView`

Bases: `PermissionRequiredMixin`, `CreateView`

Add a new Group.

## `GroupTemplateView`

Bases: `CanEditMixin`, `DetailFormView`

Display all users in a given Group Allow adding and removing users from it.

## `GroupDeleteView`

Bases: `CanEditMixin`, `DeleteView`

Delete a Group.

## `PermissionGroupsUpdateView`

Bases: `PermissionRequiredMixin`, `SuccessMessageMixin`, `UpdateView`

Manage the groups that have a specific permission.

Notes

This is an `UpdateView`, but unlike typical `UpdateView`, it doesn't accept url arguments to retrieve the object to update. As such, a `PermissionGroupsUpdateView` can only deal with a single hardcoded permission.

This is not a limitation, but an on-purpose design, mainly for security matters.

Example

    class SubscriptionPermissionView(PermissionGroupsUpdateView):
        permission = "subscription.add_subscription"

## `Notification`

Bases: `Model`

## `Club`

Bases: `Model`

The Club class, made as a tree to allow nice tidy organization.

### `president()`

Fetch the membership of the current president of this club.

Source code in `club/models.py`

    138
    139
    140
    141
    @cached_property
    def president(self) -> Membership | None:
        """Fetch the membership of the current president of this club."""
        return self.members.filter(end_date=None).order_by("role__order").first()

### `check_loop()`

Raise a validation error when a loop is found within the parent list.

Source code in `club/models.py`

    143
    144
    145
    146
    147
    148
    149
    150
    151
    def check_loop(self):
        """Raise a validation error when a loop is found within the parent list."""
        objs = []
        cur = self
        while cur.parent is not None:
            if cur in objs:
                raise ValidationError(_("You can not make loops in clubs"))
            objs.append(cur)
            cur = cur.parent

### `create_default_roles()`

Create some roles that should exist by default for this club.

The created roles are : president, treasurer, active member and curious.

Source code in `club/models.py`

    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    def create_default_roles(self):
        """Create some roles that should exist by default for this club.

        The created roles are : president, treasurer, active member and curious.

        Warnings:
            When calling this method, no club must exist yet for this club.
        """
        if self.roles.exists():
            raise ProgrammingError(
                "Default roles can be created only for clubs "
                "that don't have associated roles yet"
            )
        # The names are written in French, because there is no gettext involved
        # for strings stored in database, and the majority of users are french.
        roles = [
            ClubRole(name="Président⸱e", is_board=True, is_presidency=True),
            ClubRole(name="Trésorier⸱e", is_board=True, is_presidency=False),
            ClubRole(name="Membre actif⸱ve", is_board=False, is_presidency=False),
            ClubRole(
                name="Curieux⸱euse",
                description=(
                    "Les gens qui suivent l'activité "
                    "du club sans forcément y participer"
                ),
                is_board=False,
                is_presidency=False,
            ),
        ]
        for i, role in enumerate(roles):
            role.club = self
            role.order = i
        ClubRole.objects.bulk_create(roles)

### `is_owned_by(user)`

Method to see if that object can be super edited by the given user.

Source code in `club/models.py`

    228
    229
    230
    231
    232
    def is_owned_by(self, user: User) -> bool:
        """Method to see if that object can be super edited by the given user."""
        if user.is_anonymous:
            return False
        return user.is_root or user.is_board_member

### `can_be_edited_by(user)`

Method to see if that object can be edited by the given user.

Source code in `club/models.py`

    237
    238
    239
    def can_be_edited_by(self, user: User) -> bool:
        """Method to see if that object can be edited by the given user."""
        return self.has_rights_in_club(user)

### `get_membership_for(user)`

Return the current membership of the given user.

Source code in `club/models.py`

    247
    248
    249
    250
    251
    def get_membership_for(self, user: User) -> Membership | None:
        """Return the current membership of the given user."""
        if user.is_anonymous:
            return None
        return next((m for m in self.current_members if m.user_id == user.id), None)

## `UserFilterSchema`

Bases: `FilterSchema`

## `NotificationList`

Bases: `LoginRequiredMixin`, `ListView`

## `SearchView`

Bases: `LoginRequiredMixin`, `TemplateView`

## `PageNotFound(page_name)`

Bases: `Http404`

Http404 Exception, but specifically for when the not found object is a Page.

Source code in `core/views/page.py`

    34
    35
    def __init__(self, page_name: str):
        self.page_name = page_name

## `Page`

Bases: `Model`

The page class to build a Wiki Each page may have a parent and it's URL is of the form my.site/page/// It has an ID field, but don't use it, since it's only there for DB part, and because compound primary key is awkward! Prefere querying pages with Page.get_page_by_full_name().

Be careful with the _full_name attribute: this field may not be valid until you call save(). It's made for fast query, but don't rely on it when playing with a Page object, use get_full_name() instead!

### `save(*args, force_lock=False, **kwargs)`

Performs some needed actions before and after saving a page in database.

Source code in `core/models.py`

    1230
    1231
    1232
    1233
    1234
    1235
    1236
    1237
    1238
    1239
    1240
    1241
    1242
    1243
    1244
    1245
    1246
    def save(self, *args, force_lock: bool = False, **kwargs):
        """Performs some needed actions before and after saving a page in database."""
        if not force_lock and not self.is_locked():
            raise NotLocked("The page is not locked and thus can not be saved")
        self.full_clean()
        if not self.id:
            super().save(
                *args, **kwargs
            )  # Save a first time to correctly set _full_name
        # This reset the _full_name just before saving to maintain a coherent field quicker for queries than the
        # recursive method
        # It also update all the children to maintain correct names
        self._full_name = self.get_full_name()
        for c in self.children.all():
            c.save(force_lock=force_lock)
        super().save(*args, **kwargs)
        self.unset_lock()

### `get_page_by_full_name(name)` `staticmethod`

Quicker to get a page with that method rather than building the request every time.

Source code in `core/models.py`

    1251
    1252
    1253
    1254
    @staticmethod
    def get_page_by_full_name(name):
        """Quicker to get a page with that method rather than building the request every time."""
        return Page.objects.filter(_full_name=name).first()

### `clean()`

Cleans up only the name for the moment, but this can be used to make any treatment before saving the object.

Source code in `core/models.py`

    1256
    1257
    1258
    1259
    1260
    1261
    1262
    1263
    1264
    1265
    1266
    1267
    1268
    def clean(self):
        """Cleans up only the name for the moment, but this can be used to make any treatment before saving the object."""
        if "/" in self.name:
            self.name = self.name.split("/")[-1]
        if (
            Page.objects.exclude(pk=self.pk)
            .filter(_full_name=self.get_full_name())
            .exists()
        ):
            raise ValidationError(_("Duplicate page"), code="duplicate")
        super().clean()
        if self.parent is not None and self in self.get_parent_list():
            raise ValidationError(_("Loop in page tree"), code="loop")

### `is_locked()`

Is True if the page is locked, False otherwise.

This is where the timeout is handled, so a locked page for which the timeout is reach will be unlocked and this function will return False.

Source code in `core/models.py`

    1287
    1288
    1289
    1290
    1291
    1292
    1293
    1294
    1295
    1296
    1297
    1298
    1299
    1300
    1301
    1302
    def is_locked(self):
        """Is True if the page is locked, False otherwise.

        This is where the timeout is handled,
        so a locked page for which the timeout is reach will be unlocked and this
        function will return False.
        """
        if self.lock_timeout and (
            timezone.now() - self.lock_timeout > timedelta(minutes=5)
        ):
            self.unset_lock()
        return (
            self.lock_user
            and self.lock_timeout
            and (timezone.now() - self.lock_timeout < timedelta(minutes=5))
        )

### `set_lock(user)`

Sets a lock on the current page or raise an AlreadyLocked exception.

Source code in `core/models.py`

    1304
    1305
    1306
    1307
    1308
    1309
    1310
    def set_lock(self, user):
        """Sets a lock on the current page or raise an AlreadyLocked exception."""
        if self.is_locked() and self.get_lock() != user:
            raise AlreadyLocked("The page is already locked by someone else")
        self.lock_user = user
        self.lock_timeout = timezone.now()
        super().save()

### `set_lock_recursive(user)`

Locks recursively all the child pages for editing properties.

Source code in `core/models.py`

    1312
    1313
    1314
    1315
    1316
    def set_lock_recursive(self, user):
        """Locks recursively all the child pages for editing properties."""
        for p in self.children.all():
            p.set_lock_recursive(user)
        self.set_lock(user)

### `unset_lock_recursive()`

Unlocks recursively all the child pages.

Source code in `core/models.py`

    1318
    1319
    1320
    1321
    1322
    def unset_lock_recursive(self):
        """Unlocks recursively all the child pages."""
        for p in self.children.all():
            p.unset_lock_recursive()
        self.unset_lock()

### `unset_lock()`

Always try to unlock, even if there is no lock.

Source code in `core/models.py`

    1324
    1325
    1326
    1327
    1328
    def unset_lock(self):
        """Always try to unlock, even if there is no lock."""
        self.lock_user = None
        self.lock_timeout = None
        super().save()

### `get_lock()`

Returns the page's mutex containing the time and the user in a dict.

Source code in `core/models.py`

    1330
    1331
    1332
    1333
    1334
    def get_lock(self):
        """Returns the page's mutex containing the time and the user in a dict."""
        if self.lock_user:
            return self.lock_user
        raise NotLocked("The page is not locked and thus can not return its user")

### `get_full_name()`

Computes the real full_name of the page based on its name and its parent's name You can and must rely on this function when working on a page object that is not freshly fetched from the DB (For example when treating a Page object coming from a form).

Source code in `core/models.py`

    1336
    1337
    1338
    1339
    1340
    1341
    1342
    1343
    def get_full_name(self):
        """Computes the real full_name of the page based on its name and its parent's name
        You can and must rely on this function when working on a page object that is not freshly fetched from the DB
        (For example when treating a Page object coming from a form).
        """
        if self.parent is None:
            return self.name
        return f"{self.parent.get_full_name()}/{self.name}"

## `PageRev`

Bases: `Model`

True content of the page.

Each page object has a revisions field that is a list of PageRev, ordered by date. my_page.revisions.last() gives the PageRev object that is the most up-to-date, and thus, is the real content of the page. The content is in PageRev.title and PageRev.content .

### `similarity_ratio(text)`

Similarity ratio between this revision's content and the given text.

The result is a float in [0; 1], 0 meaning the contents are entirely different, and 1 they are strictly the same.

Source code in `core/models.py`

    1423
    1424
    1425
    1426
    1427
    1428
    1429
    1430
    def similarity_ratio(self, text: str) -> float:
        """Similarity ratio between this revision's content and the given text.

        The result is a float in [0; 1], 0 meaning the contents are entirely different,
        and 1 they are strictly the same.
        """
        # cf. https://docs.python.org/3/library/difflib.html#difflib.SequenceMatcher.ratio
        return difflib.SequenceMatcher(None, self.content, text).quick_ratio()

### `should_merge(other)`

Return True if `other` should be merged into `self`, else False.

It's considered the other revision should be merged into this one if :

- it was made less than 20 minutes after
- by the same author
- with a similarity ratio higher than 80%

Source code in `core/models.py`

    1432
    1433
    1434
    1435
    1436
    1437
    1438
    1439
    1440
    1441
    1442
    1443
    1444
    1445
    1446
    1447
    def should_merge(self, other: Self) -> bool:
        """Return True if `other` should be merged into `self`, else False.

        It's considered the other revision should be merged into this one if :

        - it was made less than 20 minutes after
        - by the same author
        - with a similarity ratio higher than 80%
        """
        return (
            not self._state.adding  # cannot merge if the original rev doesn't exist
            and self.author == other.author
            and (other.date - self.date) < self.MERGE_TIME_THRESHOLD
            and (not other._state.adding or other.revision == self.revision + 1)
            and self.similarity_ratio(other.content) >= (1 - other.MERGE_DIFF_THRESHOLD)
        )

## `PageForm(*args, **kwargs)`

Bases: `ModelForm`

Source code in `core/views/forms.py`

    404
    405
    406
    407
    408
    409
    410
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["parent"].queryset = (
            self.fields["parent"]
            .queryset.exclude(name=settings.SITH_CLUB_ROOT_PAGE)
            .filter(club=None)
        )

## `PagePropForm(*arg, **kwargs)`

Bases: `ModelForm`

Source code in `core/views/forms.py`

    387
    388
    389
    390
    def __init__(self, *arg, **kwargs):
        super().__init__(*arg, **kwargs)
        self.fields["edit_groups"].required = False
        self.fields["view_groups"].required = False

## `PageRevisionForm(*args, author, page, instance=None, **kwargs)`

Bases: `ModelForm`

Form to add a new revision to a page.

Notes

Saving this form won't always result in a new revision. If the previous revision on the same page was made :

- less than 20 minutes ago
- by the same author
- with a similarity ratio higher than 80%

then the latter will be edited and the new revision won't be created.

Source code in `core/views/forms.py`

    432
    433
    434
    435
    436
    437
    438
    def __init__(
        self, *args, author: User, page: Page, instance: PageRev | None = None, **kwargs
    ):
        super().__init__(*args, instance=instance, **kwargs)
        self.author = author
        self.page = page
        self.initial_obj: PageRev = copy(self.instance)

## `PageListView`

Bases: `ListView`

## `BasePageDetailView`

Bases: `CanViewMixin`, `DetailView`

## `PageView`

Bases: `BasePageDetailView`

## `PageHistView`

Bases: `BasePageDetailView`

## `PageRevView`

Bases: `BasePageDetailView`

## `PageCreateView`

Bases: `PermissionRequiredMixin`, `CreateView`

## `CanEditPagePropMixin`

Bases: `CanEditPropMixin`

## `PagePropView`

Bases: `CanEditPagePropMixin`, `UpdateView`

## `BasePageEditView`

Bases: `UserPassesTestMixin`, `UpdateView`

## `PageEditView`

Bases: `BasePageEditView`

## `PageDeleteView`

Bases: `CanEditPagePropMixin`, `DeleteView`

## `LoginForm(*arg, **kwargs)`

Bases: `AuthenticationForm`

Source code in `core/views/forms.py`

    112
    113
    114
    def __init__(self, *arg, **kwargs):
        super().__init__(*arg, **kwargs)
        self.fields["username"].label = _("Username, email, or account number")

## `CanEditMixin`

Bases: `GenericContentPermissionMixinBuilder`

Ensure the user has permission to edit this view's object.

Raises:

Type | Description
--- | ---
`PermissionDenied` | if the user cannot edit this view's object.

## `CanEditPropMixin`

Bases: `GenericContentPermissionMixinBuilder`

Ensure the user has owner permissions on the child view object.

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:

Type | Description
--- | ---
`PermissionDenied` | If the user cannot see the object

## `CanViewMixin`

Bases: `GenericContentPermissionMixinBuilder`

Ensure the user has permission to view this view's object.

Raises:

Type | Description
--- | ---
`PermissionDenied` | if the user cannot edit this view's object.

## `User`

Bases: `AbstractUser`

Defines the base user class, useable in every app.

This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).

Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth

### `is_in_group(*, pk=None, name=None)`

Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.

The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.

Returns:

Type | Description
--- | ---
`bool` | True if the user is the group, else False

Source code in `core/models.py`

    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    def is_in_group(self, *, pk: int | None = None, name: str | None = None) -> bool:
        """Check if this user is in the given group.
        Either a group id or a group name must be provided.
        If both are passed, only the id will be considered.

        The group will be fetched using the given parameter.
        If no group is found, return False.
        If a group is found, check if this user is in the latter.

        Returns:
             True if the user is the group, else False
        """
        if not pk and not name:
            raise ValueError("You must either provide the id or the name of the group")
        group_id: int | None = (
            pk or Group.objects.filter(name=name).values_list("id", flat=True).first()
        )
        if group_id is None:
            return False
        return group_id in self.all_groups

### `all_groups()`

Get the list of groups this user is in.

Source code in `core/models.py`

    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    @cached_property
    def all_groups(self) -> dict[int, Group]:
        """Get the list of groups this user is in."""
        additional_groups = []
        if self.is_subscribed:
            additional_groups.append(settings.SITH_GROUP_SUBSCRIBERS_ID)
        if self.is_superuser:
            additional_groups.append(settings.SITH_GROUP_ROOT_ID)
        qs = self.groups.all()
        if additional_groups:
            # This is somewhat counter-intuitive, but this query runs way faster with
            # a UNION rather than a OR (in average, 0.25ms vs 14ms).
            # For the why, cf. https://dba.stackexchange.com/questions/293836/why-is-an-or-statement-slower-than-union
            qs = qs.union(Group.objects.filter(id__in=additional_groups))
        return {g.id: g for g in qs}

### `age()`

Return the age this user has the day the method is called. If the user has not filled his age, return 0.

Source code in `core/models.py`

    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    @cached_property
    def age(self) -> int:
        """Return the age this user has the day the method is called.
        If the user has not filled his age, return 0.
        """
        if self.date_of_birth is None:
            return 0
        today = timezone.now()
        age = today.year - self.date_of_birth.year
        # remove a year if this year's birthday is yet to come
        age -= (today.month, today.day) < (
            self.date_of_birth.month,
            self.date_of_birth.day,
        )
        return age

### `get_short_name()`

Returns the short name for the user.

Source code in `core/models.py`

    439
    440
    441
    442
    443
    def get_short_name(self):
        """Returns the short name for the user."""
        if self.nick_name:
            return self.nick_name
        return self.first_name + " " + self.last_name

### `get_display_name()`

Returns the display name of the user.

A nickname if possible, otherwise, the full name.

Source code in `core/models.py`

    445
    446
    447
    448
    449
    450
    451
    452
    def get_display_name(self) -> str:
        """Returns the display name of the user.

        A nickname if possible, otherwise, the full name.
        """
        if self.nick_name:
            return "%s (%s)" % (self.get_full_name(), self.nick_name)
        return self.get_full_name()

### `get_family(godfathers_depth=4, godchildren_depth=4)`

Get the family of the user, with the given depth.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`godfathers_depth` | `NonNegativeInt` | The number of generations of godfathers to fetch | `4`
`godchildren_depth` | `NonNegativeInt` | The number of generations of godchildren to fetch | `4`

Returns:

Type | Description
--- | ---
`set[through]` | A list of family relationships in this user's family

Source code in `core/models.py`

    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    def get_family(
        self,
        godfathers_depth: NonNegativeInt = 4,
        godchildren_depth: NonNegativeInt = 4,
    ) -> set[User.godfathers.through]:
        """Get the family of the user, with the given depth.

        Args:
            godfathers_depth: The number of generations of godfathers to fetch
            godchildren_depth: The number of generations of godchildren to fetch

        Returns:
            A list of family relationships in this user's family
        """
        res = []
        for depth, key, reverse_key in [
            (godfathers_depth, "from_user_id", "to_user_id"),
            (godchildren_depth, "to_user_id", "from_user_id"),
        ]:
            if depth == 0:
                continue
            links = list(User.godfathers.through.objects.filter(**{key: self.id}))
            res.extend(links)
            for _ in range(1, depth):  # noqa: F402 we don't care about gettext here
                ids = [getattr(c, reverse_key) for c in links]
                links = list(
                    User.godfathers.through.objects.filter(
                        **{f"{key}__in": ids}
                    ).exclude(id__in=[r.id for r in res])
                )
                if not links:
                    break
                res.extend(links)
        return set(res)

### `email_user(subject, message, from_email=None, **kwargs)`

Sends an email to this User.

Source code in `core/models.py`

    489
    490
    491
    492
    493
    def email_user(self, subject, message, from_email=None, **kwargs):
        """Sends an email to this User."""
        if from_email is None:
            from_email = settings.DEFAULT_FROM_EMAIL
        send_mail(subject, message, from_email, [self.email], **kwargs)

### `generate_username()`

Generates a unique username based on the first and last names.

For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.

Returns:

Type | Description
--- | ---
`str` | The generated username.

Source code in `core/models.py`

    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    def generate_username(self) -> str:
        """Generates a unique username based on the first and last names.

        For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.

        Returns:
            The generated username.
        """

        def remove_accents(data):
            return "".join(
                x
                for x in unicodedata.normalize("NFKD", data)
                if unicodedata.category(x)[0] == "L"
            ).lower()

        user_name = (
            remove_accents(self.first_name[0] + self.last_name)
            .encode("ascii", "ignore")
            .decode("utf-8")
        )
        # load all usernames which could conflict with the new one.
        # we need to actually load them, instead of performing a count,
        # because we cannot be sure that two usernames refer to the
        # actual same word (eg. tmore and tmoreau)
        possible_conflicts: list[str] = list(
            User.objects.filter(username__startswith=user_name).values_list(
                "username", flat=True
            )
        )
        nb_conflicts = sum(
            1 for name in possible_conflicts if name.rstrip(string.digits) == user_name
        )
        if nb_conflicts > 0:
            user_name += str(nb_conflicts)  # exemple => exemple1
        self.username = user_name
        return user_name

### `is_owner(obj)`

Determine if the object is owned by the user.

Source code in `core/models.py`

    533
    534
    535
    536
    537
    538
    539
    def is_owner(self, obj: models.Model):
        """Determine if the object is owned by the user."""
        if hasattr(obj, "is_owned_by") and obj.is_owned_by(self):
            return True
        if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group_id):
            return True
        return self.is_root

### `can_edit(obj)`

Determine if the object can be edited by the user.

Source code in `core/models.py`

    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    555
    def can_edit(self, obj: models.Model):
        """Determine if the object can be edited by the user."""
        if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self):
            return True
        if hasattr(obj, "edit_groups"):
            if (
                hasattr(obj, "_prefetched_objects_cache")
                and "edit_groups" in obj._prefetched_objects_cache
            ):
                pks = [g.id for g in obj.edit_groups.all()]
            else:
                pks = list(obj.edit_groups.values_list("id", flat=True))
            if any(self.is_in_group(pk=pk) for pk in pks):
                return True
        return self.is_owner(obj)

### `can_view(obj)`

Determine if the object can be viewed by the user.

Source code in `core/models.py`

    557
    558
    559
    560
    561
    562
    563
    564
    565
    566
    567
    568
    569
    570
    571
    572
    573
    574
    def can_view(self, obj: models.Model):
        """Determine if the object can be viewed by the user."""
        if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
            return True
        if hasattr(obj, "view_groups"):
            # if "view_groups" has already been prefetched, use
            # the prefetch cache, else fetch only the ids, to make
            # the query lighter.
            if (
                hasattr(obj, "_prefetched_objects_cache")
                and "view_groups" in obj._prefetched_objects_cache
            ):
                pks = [g.id for g in obj.view_groups.all()]
            else:
                pks = list(obj.view_groups.values_list("id", flat=True))
            if any(self.is_in_group(pk=pk) for pk in pks):
                return True
        return self.can_edit(obj)

### `can_be_viewed_by(user)`

Check if the given user can be viewed by this user.

Given users A and B. A can be viewed by B if :

- A and B are the same user
- or B has the permission to view hidden users
- or B can view users in general and A didn't hide its profile
- or B is in A's whitelist.

Source code in `core/models.py`

    579
    580
    581
    582
    583
    584
    585
    586
    587
    588
    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    600
    601
    602
    603
    604
    605
    def can_be_viewed_by(self, user: User) -> bool:
        """Check if the given user can be viewed by this user.

        Given users A and B. A can be viewed by B if :

        - A and B are the same user
        - or B has the permission to view hidden users
        - or B can view users in general and A didn't hide its profile
        - or B is in A's whitelist.
        """

        def is_in_whitelist(u: User):
            if (
                hasattr(self, "_prefetched_objects_cache")
                and "whitelisted_users" in self._prefetched_objects_cache
            ):
                return u in self.whitelisted_users.all()
            return self.whitelisted_users.contains(u)

        return (
            user.id == self.id
            or user.has_perm("core.view_hidden_user")
            or (
                user.has_perm("core.view_user")
                and (self.is_viewable or is_in_whitelist(user))
            )
        )

### `clubs_with_rights()`

The list of clubs where the user has rights

Source code in `core/models.py`

    639
    640
    641
    642
    643
    @cached_property
    def clubs_with_rights(self) -> list[Club]:
        """The list of clubs where the user has rights"""
        memberships = self.memberships.ongoing().board().select_related("club")
        return [m.club for m in memberships]

## `Gift`

Bases: `Model`

## `Preferences`

Bases: `Model`

## `GiftForm(*args, **kwargs)`

Bases: `ModelForm`

Source code in `core/views/forms.py`

    456
    457
    458
    459
    460
    461
    462
    463
    def __init__(self, *args, **kwargs):
        user_id = kwargs.pop("user_id", None)
        super().__init__(*args, **kwargs)
        if user_id:
            self.fields["user"].queryset = self.fields["user"].queryset.filter(
                id=user_id
            )
            self.fields["user"].widget = forms.HiddenInput()

## `RegisteringForm`

Bases: `UserCreationForm`

## `UserGodfathersForm(*args, user, **kwargs)`

Bases: `Form`

Source code in `core/views/forms.py`

    343
    344
    345
    def __init__(self, *args, user: User, **kwargs):
        super().__init__(*args, **kwargs)
        self.target_user = user

## `UserGroupsForm`

Bases: `ModelForm`

## `UserProfileForm(*args, label_suffix='', **kwargs)`

Bases: `ModelForm`

Form handling the user profile, managing the files

Source code in `core/views/forms.py`

    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    def __init__(self, *args, label_suffix: str = "", **kwargs):
        super().__init__(*args, label_suffix=label_suffix, **kwargs)

        # Image fields are injected here to override the file field provided by the model
        # This would be better if we could have a SithImage sort of model input instead of a generic SithFile
        self.fields["profile_pict"] = forms.ImageField(
            required=False,
            label=_(
                "Profile: you need to be visible on the picture, in order to be recognized (e.g. by the barmen)"
            ),
        )
        self.fields["avatar_pict"] = forms.ImageField(
            required=False,
            label=_("Avatar: used on the forum"),
        )
        self.fields["scrub_pict"] = forms.ImageField(
            required=False,
            label=_("Scrub: let other know how your scrub looks like!"),
        )

## `UserVisibilityForm(*args, initial=None, instance=None, **kwargs)`

Bases: `ModelForm`

Source code in `core/views/forms.py`

    283
    284
    285
    286
    287
    288
    289
    def __init__(
        self, *args, initial: dict | None = None, instance: User | None = None, **kwargs
    ):
        if instance:
            initial = initial or {}
            initial["show_my_stats"] = instance.preferences.show_my_stats
        super().__init__(*args, initial=initial, instance=instance, **kwargs)

## `FragmentMixin`

Bases: `TemplateResponseMixin`, `ContextMixin`

Make a view buildable as a fragment that can be embedded in a template.

Most fragments are used in two different ways : - in the request/response cycle, like any regular view - in templates, where the rendering is done in another view

This mixin aims to simplify the initial fragment rendering. The rendered fragment will then be able to re-render itself through the request/response cycle if it uses HTMX.

Example

    class MyFragment(FragmentMixin, FormView):
        template_name = "app/fragment.jinja"
        form_class = MyForm
        success_url = reverse_lazy("foo:bar")

    # in another view :
    def some_view(request):
        fragment = MyFragment.as_fragment()
        return render(
            request,
            "app/template.jinja",
            context={"fragment": fragment(request)
        }

    # in urls.py
    urlpatterns = [
        path("foo/view", some_view),
        path("foo/fragment", MyFragment.as_view()),
    ]

### `reload_on_redirect = False` `class-attribute` `instance-attribute`

If True, this fragment will trigger a full page reload on redirect.

## `TabedViewMixin`

Bases: `View`

Basic functions for displaying tabs in the template.

## `UseFragmentsMixin`

Bases: `ContextMixin`

Mark a view as using fragments.

This mixin is not mandatory (you may as well render manually your fragments in the `get_context_data` method). However, the interface of this class bring some distinction between fragments and other context data, which may reduce boilerplate.

Example

    class FooFragment(FragmentMixin, FormView): ...

    class BarFragment(FragmentMixin, FormView): ...

    class AdminFragment(FragmentMixin, FormView): ...

    class MyView(UseFragmentsMixin, TemplateView)
        template_name = "app/view.jinja"
        fragments = {
            "foo": FooFragment
            "bar": BarFragment(template_name="some_template.jinja")
        }
        fragments_data = {
            "foo": {"some": "data"}  # this will be passed to the FooFragment renderer
        }

        def get_fragments(self):
            res = super().get_fragments()
            if self.request.user.is_superuser:
                res["admin_fragment"] = AdminFragment
            return res

### `get_fragment_data()`

Return eventual data used to initialize the fragments.

Source code in `core/views/mixins.py`

    179
    180
    181
    def get_fragment_data(self) -> dict[str, dict[str, Any]]:
        """Return eventual data used to initialize the fragments."""
        return self.fragment_data if self.fragment_data is not None else {}

### `get_fragment_context_data()`

Return the rendered fragments as context data.

Source code in `core/views/mixins.py`

    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    def get_fragment_context_data(self) -> dict[str, SafeString]:
        """Return the rendered fragments as context data."""
        res = {}
        data = self.get_fragment_data()
        for name, fragment in self.get_fragments().items():
            is_cls = inspect.isclass(fragment) and issubclass(fragment, FragmentMixin)
            _fragment = fragment.as_fragment() if is_cls else fragment
            fragment_data = data.get(name, {})
            res[name] = _fragment(self.request, **fragment_data)
        return res

## `SithLoginView`

Bases: `LoginView`

The login View.

## `SithPasswordChangeView`

Bases: `PasswordChangeView`

Allows a user to change its password.

## `SithPasswordChangeDoneView`

Bases: `PasswordChangeDoneView`

Allows a user to change its password.

## `PasswordRootChangeView`

Bases: `UserPassesTestMixin`, `FormView`

Allows a root user to change someone's password.

## `SithPasswordResetView`

Bases: `PasswordResetView`

Allows someone to enter an email address for resetting password.

## `SithPasswordResetDoneView`

Bases: `PasswordResetDoneView`

Confirm that the reset email has been sent.

## `SithPasswordResetConfirmView`

Bases: `PasswordResetConfirmView`

Provide a reset password form.

## `SithPasswordResetCompleteView`

Bases: `PasswordResetCompleteView`

Confirm the password has successfully been reset.

## `UserCreationView`

Bases: `FormView`

## `UserMeRedirect`

Bases: `LoginRequiredMixin`, `RedirectView`

## `UserTabsMixin`

Bases: `TabedViewMixin`

## `UserView`

Bases: `UserTabsMixin`, `CanViewMixin`, `DetailView`

Display a user's profile.

## `UserGodfathersView`

Bases: `UserTabsMixin`, `CanViewMixin`, `DetailView`, `FormView`

Display a user's godfathers.

## `UserGodfathersTreeView`

Bases: `UserTabsMixin`, `CanViewMixin`, `DetailView`

Display a user's family tree.

## `UserStatsView`

Bases: `UserTabsMixin`, `UserPassesTestMixin`, `DetailView`

Display a user's stats.

## `UserMiniView`

Bases: `CanViewMixin`, `DetailView`

Display a user's profile.

## `UserUpdateProfileView`

Bases: `UserTabsMixin`, `CanEditMixin`, `UpdateView`

Edit a user's profile.

### `remove_restricted_fields(request)`

Removes edit_once and board_only fields.

Source code in `core/views/user.py`

    428
    429
    430
    431
    432
    433
    434
    def remove_restricted_fields(self, request):
        """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
            ):
                self.form.fields.pop(i, None)

## `UserClubView`

Bases: `UserTabsMixin`, `CanViewMixin`, `DetailView`

Display the user's club(s).

## `UserVisibilityFormFragment`

Bases: `FragmentMixin`, `SuccessMessageMixin`, `UpdateView`

## `UserPreferencesView`

Bases: `UserTabsMixin`, `UseFragmentsMixin`, `CanEditMixin`, `UpdateView`

Edit a user's preferences.

## `UserUpdateGroupView`

Bases: `UserTabsMixin`, `CanEditPropMixin`, `UpdateView`

Edit a user's groups.

## `UserToolsView`

Bases: `LoginRequiredMixin`, `UserTabsMixin`, `TemplateView`

Displays the logged user's tools.

## `UserAccountBase`

Bases: `UserTabsMixin`, `DetailView`

Base class for UserAccount.

## `UserAccountView`

Bases: `UserAccountBase`

Display a user's account.

## `UserAccountDetailView`

Bases: `UserAccountBase`, `YearMixin`, `MonthMixin`

Display a user's account for month.

## `GiftCreateView`

Bases: `CreateView`

## `GiftDeleteView`

Bases: `CanEditPropMixin`, `DeleteView`

## `forbidden(request, exception)`

Source code in `core/views/__init__.py`

    39
    40
    41
    def forbidden(request: HttpRequest, exception):
        context = {"next": request.path, "form": LoginForm()}
        return HttpResponseForbidden(render(request, "core/403.jinja", context=context))

## `not_found(request, exception)`

Source code in `core/views/__init__.py`

    44
    45
    46
    47
    48
    49
    def not_found(request: HttpRequest, exception: Http404):
        if isinstance(exception, PageNotFound):
            template_name = "core/page/not_found.jinja"
        else:
            template_name = "core/404.jinja"
        return render(request, template_name, context={"exception": exception}, status=404)

## `internal_servor_error(request)`

Source code in `core/views/__init__.py`

    52
    53
    54
    def internal_servor_error(request):
        request.sentry_last_event_id = last_event_id
        return HttpResponseServerError(render(request, "core/500.jinja"))

## `can_view(obj, user)`

Can the user see the object.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`obj` | `Any` | Object to test for permission | required
`user` | `User` | core.models.User to test permissions against | required

Returns:

Type | Description
--- | ---
`bool` | True if user is authorized to see object else False

Example

    if not can_view(self.object ,request.user):
        raise PermissionDenied

Source code in `core/auth/mixins.py`

     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    def can_view(obj: Any, user: User) -> bool:
        """Can the user see the object.

        Args:
            obj: Object to test for permission
            user: core.models.User to test permissions against

        Returns:
            True if user is authorized to see object else False

        Example:
            ```python
            if not can_view(self.object ,request.user):
                raise PermissionDenied
            ```
        """
        if obj is None or user.can_view(obj):
            return True
        return can_edit(obj, user)

## `send_raw_file(path)`

Send a file located in the MEDIA_ROOT

This handles all the logic of using production reverse proxy or debug server.

THIS DOESN'T CHECK ANY PERMISSIONS !

Source code in `core/views/files.py`

    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    def send_raw_file(path: Path) -> HttpResponse:
        """Send a file located in the MEDIA_ROOT

        This handles all the logic of using production reverse proxy or debug server.

        THIS DOESN'T CHECK ANY PERMISSIONS !
        """
        if not path.is_relative_to(settings.MEDIA_ROOT):
            raise Http404

        if not path.is_file() or not path.exists():
            raise Http404

        response = HttpResponse(
            headers={"Content-Disposition": f'inline; filename="{quote(path.name)}"'}
        )
        if not settings.DEBUG:
            # When receiving a response with the Accel-Redirect header,
            # the reverse proxy will automatically handle the file sending.
            # This is really hard to test (thus isn't tested)
            # so please do not mess with this.
            response["Content-Type"] = ""  # automatically set by nginx
            response["X-Accel-Redirect"] = quote(
                urljoin(settings.MEDIA_URL, str(path.relative_to(settings.MEDIA_ROOT)))
            )
            return response

        with open(path, "rb") as filename:
            response.content = FileWrapper(filename)
            response["Content-Type"] = mimetypes.guess_type(path)[0]
            response["Last-Modified"] = http_date(path.stat().st_mtime)
            response["Content-Length"] = path.stat().st_size
            return response

## `send_file(request, file_id, file_class=SithFile, file_attr='file')`

Send a protected file, if the user can see it.

In prod, the server won't handle the download itself, but set the appropriate headers in the response to make the reverse-proxy deal with it. In debug mode, the server will directly send the file.

Source code in `core/views/files.py`

     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    def send_file(
        request: HttpRequest,
        file_id: int | str,
        file_class: type[SithFile] = SithFile,
        file_attr: str = "file",
    ) -> HttpResponse:
        """Send a protected file, if the user can see it.

        In prod, the server won't handle the download itself,
        but set the appropriate headers in the response to make the reverse-proxy
        deal with it.
        In debug mode, the server will directly send the file.
        """
        f = get_object_or_404(file_class, pk=file_id)
        if not can_view(f, request.user) and not is_logged_in_counter(request):
            raise PermissionDenied
        name = getattr(f, file_attr).name

        return send_raw_file(settings.MEDIA_ROOT / name)

## `notification(request, notif_id)`

Source code in `core/views/index.py`

    48
    49
    50
    51
    52
    53
    54
    55
    def notification(request: HttpRequest, notif_id: int):
        notif = get_object_or_404(Notification, id=notif_id)
        if notif.type not in settings.SITH_PERMANENT_NOTIFICATIONS:
            notif.viewed = True
        else:
            notif.callback()
        notif.save()
        return redirect(notif.url)

## `get_page_or_404(full_name)`

Like Django's get_object_or_404, but for Page, and with a custom 404 exception.

Source code in `core/views/page.py`

    38
    39
    40
    41
    42
    43
    def get_page_or_404(full_name: str) -> Page:
        """Like Django's get_object_or_404, but for Page, and with a custom 404 exception."""
        page = Page.objects.filter(_full_name=full_name).first()
        if not page:
            raise PageNotFound(full_name)
        return page

## `logout(request)`

The logout view.

Source code in `core/views/user.py`

     99
    100
    101
    def logout(request):
        """The logout view."""
        return views.logout_then_login(request)

## `delete_user_godfather(request, user_id, godfather_id, is_father)`

Source code in `core/views/user.py`

    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    @require_POST
    @login_required
    def delete_user_godfather(request, user_id, godfather_id, is_father):
        user_is_admin = request.user.is_root or request.user.is_board_member
        if user_id != request.user.id and not user_is_admin:
            raise PermissionDenied
        user = get_object_or_404(User, id=user_id)
        to_remove = get_object_or_404(User, id=godfather_id)
        if is_father:
            user.godfathers.remove(to_remove)
        else:
            user.godchildren.remove(to_remove)
        return redirect("core:user_godfathers", user_id=user_id)