mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-09 19:40:19 +00:00
use google convention for docstrings
This commit is contained in:
@ -19,9 +19,7 @@ class TwoDigitMonthConverter:
|
||||
|
||||
|
||||
class BooleanStringConverter:
|
||||
"""
|
||||
Converter whose regex match either True or False
|
||||
"""
|
||||
"""Converter whose regex match either True or False."""
|
||||
|
||||
regex = r"(True)|(False)"
|
||||
|
||||
|
@ -90,12 +90,15 @@ def list_tags(s):
|
||||
yield parts[1][len(tag_prefix) :]
|
||||
|
||||
|
||||
def parse_semver(s):
|
||||
"""
|
||||
Turns a semver string into a 3-tuple or None if the parsing failed, it is a
|
||||
prerelease or it has build metadata.
|
||||
def parse_semver(s) -> tuple[int, int, int] | None:
|
||||
"""Parse a semver string.
|
||||
|
||||
See https://semver.org
|
||||
|
||||
Returns:
|
||||
A tuple, if the parsing was successful, else None.
|
||||
In the latter case, it must probably be a prerelease
|
||||
or include build metadata.
|
||||
"""
|
||||
m = semver_regex.match(s)
|
||||
|
||||
@ -106,7 +109,7 @@ def parse_semver(s):
|
||||
):
|
||||
return None
|
||||
|
||||
return (int(m.group("major")), int(m.group("minor")), int(m.group("patch")))
|
||||
return int(m.group("major")), int(m.group("minor")), int(m.group("patch"))
|
||||
|
||||
|
||||
def semver_to_s(t):
|
||||
|
@ -29,9 +29,7 @@ from django.core.management.commands import compilemessages
|
||||
|
||||
|
||||
class Command(compilemessages.Command):
|
||||
"""
|
||||
Wrap call to compilemessages to avoid building whole env
|
||||
"""
|
||||
"""Wrap call to compilemessages to avoid building whole env."""
|
||||
|
||||
help = """
|
||||
The usage is the same as the real compilemessages
|
||||
|
@ -30,9 +30,7 @@ from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Compiles scss in static folder for production
|
||||
"""
|
||||
"""Compiles scss in static folder for production."""
|
||||
|
||||
help = "Compile scss files from static folder"
|
||||
|
||||
|
@ -53,13 +53,14 @@ _threadlocal = threading.local()
|
||||
|
||||
|
||||
def get_signal_request():
|
||||
"""
|
||||
!!! Do not use if your operation is asynchronus !!!
|
||||
Allow to access current request in signals
|
||||
This is a hack that looks into the thread
|
||||
Mainly used for log purpose
|
||||
"""
|
||||
"""Allow to access current request in signals.
|
||||
|
||||
This is a hack that looks into the thread
|
||||
Mainly used for log purpose.
|
||||
|
||||
!!!danger
|
||||
Do not use if your operation is asynchronous.
|
||||
"""
|
||||
return getattr(_threadlocal, "request", None)
|
||||
|
||||
|
||||
|
224
core/models.py
224
core/models.py
@ -21,11 +21,13 @@
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import unicodedata
|
||||
from datetime import date, timedelta
|
||||
from typing import List, Optional, Union
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import (
|
||||
@ -56,6 +58,9 @@ from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
from core import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from club.models import Club
|
||||
|
||||
|
||||
class RealGroupManager(AuthGroupManager):
|
||||
def get_queryset(self):
|
||||
@ -68,8 +73,7 @@ class MetaGroupManager(AuthGroupManager):
|
||||
|
||||
|
||||
class Group(AuthGroup):
|
||||
"""
|
||||
Implement both RealGroups and Meta groups
|
||||
"""Implement both RealGroups and Meta groups.
|
||||
|
||||
Groups are sorted by their is_meta property
|
||||
"""
|
||||
@ -87,9 +91,6 @@ class Group(AuthGroup):
|
||||
ordering = ["name"]
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""
|
||||
This is needed for black magic powered UpdateView's children
|
||||
"""
|
||||
return reverse("core:group_list")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@ -104,8 +105,8 @@ class Group(AuthGroup):
|
||||
|
||||
|
||||
class MetaGroup(Group):
|
||||
"""
|
||||
MetaGroups are dynamically created groups.
|
||||
"""MetaGroups are dynamically created groups.
|
||||
|
||||
Generally used with clubs where creating a club creates two groups:
|
||||
|
||||
* club-SITH_BOARD_SUFFIX
|
||||
@ -123,14 +124,14 @@ class MetaGroup(Group):
|
||||
self.is_meta = True
|
||||
|
||||
@cached_property
|
||||
def associated_club(self):
|
||||
"""
|
||||
Return the group associated with this meta group
|
||||
def associated_club(self) -> Club | None:
|
||||
"""Return the group associated with this meta group.
|
||||
|
||||
The result of this function is cached
|
||||
|
||||
:return: The associated club if it exists, else None
|
||||
:rtype: club.models.Club | None
|
||||
|
||||
Returns:
|
||||
The associated club if it exists, else None
|
||||
"""
|
||||
from club.models import Club
|
||||
|
||||
@ -150,8 +151,8 @@ class MetaGroup(Group):
|
||||
|
||||
|
||||
class RealGroup(Group):
|
||||
"""
|
||||
RealGroups are created by the developer.
|
||||
"""RealGroups are created by the developer.
|
||||
|
||||
Most of the time they match a number in settings to be easily used for permissions.
|
||||
"""
|
||||
|
||||
@ -173,22 +174,26 @@ def validate_promo(value):
|
||||
|
||||
|
||||
def get_group(*, pk: int = None, name: str = None) -> Optional[Group]:
|
||||
"""
|
||||
Search for a group by its primary key or its name.
|
||||
"""Search for a group by its primary key or its name.
|
||||
Either one of the two must be set.
|
||||
|
||||
The result is cached for the default duration (should be 5 minutes).
|
||||
|
||||
:param pk: The primary key of the group
|
||||
:param name: The name of the group
|
||||
:return: The group if it exists, else None
|
||||
:raise ValueError: If no group matches the criteria
|
||||
Args:
|
||||
pk: The primary key of the group
|
||||
name: The name of the group
|
||||
|
||||
Returns:
|
||||
The group if it exists, else None
|
||||
|
||||
Raises:
|
||||
ValueError: If no group matches the criteria
|
||||
"""
|
||||
if pk is None and name is None:
|
||||
raise ValueError("Either pk or name must be set")
|
||||
|
||||
# replace space characters to hide warnings with memcached backend
|
||||
pk_or_name: Union[str, int] = pk if pk is not None else name.replace(" ", "_")
|
||||
pk_or_name: str | int = pk if pk is not None else name.replace(" ", "_")
|
||||
group = cache.get(f"sith_group_{pk_or_name}")
|
||||
|
||||
if group == "not_found":
|
||||
@ -211,8 +216,7 @@ def get_group(*, pk: int = None, name: str = None) -> Optional[Group]:
|
||||
|
||||
|
||||
class User(AbstractBaseUser):
|
||||
"""
|
||||
Defines the base user class, useable in every app
|
||||
"""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
|
||||
@ -382,9 +386,6 @@ class User(AbstractBaseUser):
|
||||
return self.is_active and self.is_superuser
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""
|
||||
This is needed for black magic powered UpdateView's children
|
||||
"""
|
||||
return reverse("core:user_profile", kwargs={"user_id": self.pk})
|
||||
|
||||
def __str__(self):
|
||||
@ -412,8 +413,7 @@ class User(AbstractBaseUser):
|
||||
return 0
|
||||
|
||||
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
|
||||
"""
|
||||
Check if this user is in the given group.
|
||||
"""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.
|
||||
|
||||
@ -421,7 +421,8 @@ class User(AbstractBaseUser):
|
||||
If no group is found, return False.
|
||||
If a group is found, check if this user is in the latter.
|
||||
|
||||
:return: True if the user is the group, else False
|
||||
Returns:
|
||||
True if the user is the group, else False
|
||||
"""
|
||||
if pk is not None:
|
||||
group: Optional[Group] = get_group(pk=pk)
|
||||
@ -454,11 +455,12 @@ class User(AbstractBaseUser):
|
||||
return group in self.cached_groups
|
||||
|
||||
@property
|
||||
def cached_groups(self) -> List[Group]:
|
||||
"""
|
||||
Get the list of groups this user is in.
|
||||
def cached_groups(self) -> list[Group]:
|
||||
"""Get the list of groups this user is in.
|
||||
|
||||
The result is cached for the default duration (should be 5 minutes)
|
||||
:return: A list of all the groups this user is in
|
||||
|
||||
Returns: A list of all the groups this user is in.
|
||||
"""
|
||||
groups = cache.get(f"user_{self.id}_groups")
|
||||
if groups is None:
|
||||
@ -523,9 +525,8 @@ class User(AbstractBaseUser):
|
||||
|
||||
@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
|
||||
"""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
|
||||
@ -576,31 +577,27 @@ class User(AbstractBaseUser):
|
||||
}
|
||||
|
||||
def get_full_name(self):
|
||||
"""
|
||||
Returns the first_name plus the last_name, with a space in between.
|
||||
"""
|
||||
"""Returns the first_name plus the last_name, with a space in between."""
|
||||
full_name = "%s %s" % (self.first_name, self.last_name)
|
||||
return full_name.strip()
|
||||
|
||||
def get_short_name(self):
|
||||
"Returns the short name for the user."
|
||||
"""Returns the short name for the user."""
|
||||
if self.nick_name:
|
||||
return self.nick_name
|
||||
return self.first_name + " " + self.last_name
|
||||
|
||||
def get_display_name(self):
|
||||
"""
|
||||
Returns the display name of the user.
|
||||
A nickname if possible, otherwise, the full name
|
||||
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()
|
||||
|
||||
def get_age(self):
|
||||
"""
|
||||
Returns the age
|
||||
"""
|
||||
"""Returns the age."""
|
||||
today = timezone.now()
|
||||
born = self.date_of_birth
|
||||
return (
|
||||
@ -608,18 +605,18 @@ class User(AbstractBaseUser):
|
||||
)
|
||||
|
||||
def email_user(self, subject, message, from_email=None, **kwargs):
|
||||
"""
|
||||
Sends an email to this User.
|
||||
"""
|
||||
"""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)
|
||||
|
||||
def generate_username(self):
|
||||
"""
|
||||
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 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):
|
||||
@ -644,9 +641,7 @@ class User(AbstractBaseUser):
|
||||
return user_name
|
||||
|
||||
def is_owner(self, obj):
|
||||
"""
|
||||
Determine if the object is owned by the user
|
||||
"""
|
||||
"""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):
|
||||
@ -656,9 +651,7 @@ class User(AbstractBaseUser):
|
||||
return False
|
||||
|
||||
def can_edit(self, obj):
|
||||
"""
|
||||
Determine if the object can be edited by the user
|
||||
"""
|
||||
"""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"):
|
||||
@ -672,9 +665,7 @@ class User(AbstractBaseUser):
|
||||
return False
|
||||
|
||||
def can_view(self, obj):
|
||||
"""
|
||||
Determine if the object can be viewed by the user
|
||||
"""
|
||||
"""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"):
|
||||
@ -730,11 +721,8 @@ class User(AbstractBaseUser):
|
||||
return infos
|
||||
|
||||
@cached_property
|
||||
def clubs_with_rights(self):
|
||||
"""
|
||||
:return: the list of clubs where the user has rights
|
||||
:rtype: list[club.models.Club]
|
||||
"""
|
||||
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]
|
||||
|
||||
@ -796,9 +784,7 @@ class AnonymousUser(AuthAnonymousUser):
|
||||
raise PermissionDenied
|
||||
|
||||
def is_in_group(self, *, pk: int = None, name: str = None) -> bool:
|
||||
"""
|
||||
The anonymous user is only in the public group
|
||||
"""
|
||||
"""The anonymous user is only in the public group."""
|
||||
allowed_id = settings.SITH_GROUP_PUBLIC_ID
|
||||
if pk is not None:
|
||||
return pk == allowed_id
|
||||
@ -957,16 +943,15 @@ class SithFile(models.Model):
|
||||
).save()
|
||||
|
||||
def can_be_managed_by(self, user: User) -> bool:
|
||||
"""
|
||||
Tell if the user can manage the file (edit, delete, etc.) or not.
|
||||
"""Tell if the user can manage the file (edit, delete, etc.) or not.
|
||||
Apply the following rules:
|
||||
- If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone -> return True
|
||||
- If the file is in the SAS, only the SAS admins (or roots) can manage it -> return True if the user is in the SAS admin group or is a root
|
||||
- If the file is in the profiles directory, only the roots can manage it -> return True if the user is a root
|
||||
- If the file is in the profiles directory, only the roots can manage it -> return True if the user is a root.
|
||||
|
||||
:returns: True if the file is managed by the SAS or within the profiles directory, False otherwise
|
||||
Returns:
|
||||
True if the file is managed by the SAS or within the profiles directory, False otherwise
|
||||
"""
|
||||
|
||||
# If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone
|
||||
profiles_dir = SithFile.objects.filter(name="profiles").first()
|
||||
if not self.is_in_sas and not profiles_dir in self.get_parent_list():
|
||||
@ -1017,9 +1002,7 @@ class SithFile(models.Model):
|
||||
return super().delete()
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Cleans up the file
|
||||
"""
|
||||
"""Cleans up the file."""
|
||||
super().clean()
|
||||
if "/" in self.name:
|
||||
raise ValidationError(_("Character '/' not authorized in name"))
|
||||
@ -1070,15 +1053,14 @@ class SithFile(models.Model):
|
||||
c.apply_rights_recursively(only_folders=only_folders)
|
||||
|
||||
def copy_rights(self):
|
||||
"""Copy, if possible, the rights of the parent folder"""
|
||||
"""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())
|
||||
self.save()
|
||||
|
||||
def move_to(self, parent):
|
||||
"""
|
||||
Move a file to a new 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
|
||||
@ -1091,10 +1073,7 @@ class SithFile(models.Model):
|
||||
self.save()
|
||||
|
||||
def _repair_fs(self):
|
||||
"""
|
||||
This function rebuilds recursively the filesystem as it should be
|
||||
regarding the DB tree.
|
||||
"""
|
||||
"""Rebuilds recursively the filesystem as it should be regarding the DB tree."""
|
||||
if self.is_folder:
|
||||
for c in self.children.all():
|
||||
c._repair_fs()
|
||||
@ -1197,19 +1176,19 @@ class SithFile(models.Model):
|
||||
|
||||
|
||||
class LockError(Exception):
|
||||
"""There was a lock error on the object"""
|
||||
"""There was a lock error on the object."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AlreadyLocked(LockError):
|
||||
"""The object is already locked"""
|
||||
"""The object is already locked."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NotLocked(LockError):
|
||||
"""The object is not locked"""
|
||||
"""The object is not locked."""
|
||||
|
||||
pass
|
||||
|
||||
@ -1220,12 +1199,11 @@ def get_default_owner_group():
|
||||
|
||||
|
||||
class Page(models.Model):
|
||||
"""
|
||||
The page class to build a Wiki
|
||||
"""The page class to build a Wiki
|
||||
Each page may have a parent and it's URL is of the form my.site/page/<grd_pa>/<parent>/<mypage>
|
||||
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()
|
||||
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!
|
||||
@ -1294,9 +1272,7 @@ class Page(models.Model):
|
||||
return self.get_full_name()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Performs some needed actions before and after saving a page in database
|
||||
"""
|
||||
"""Performs some needed actions before and after saving a page in database."""
|
||||
locked = kwargs.pop("force_lock", False)
|
||||
if not locked:
|
||||
locked = self.is_locked()
|
||||
@ -1317,22 +1293,15 @@ class Page(models.Model):
|
||||
self.unset_lock()
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""
|
||||
This is needed for black magic powered UpdateView's children
|
||||
"""
|
||||
return reverse("core:page", kwargs={"page_name": self._full_name})
|
||||
|
||||
@staticmethod
|
||||
def get_page_by_full_name(name):
|
||||
"""
|
||||
Quicker to get a page with that method rather than building the request every time
|
||||
"""
|
||||
"""Quicker to get a page with that method rather than building the request every time."""
|
||||
return Page.objects.filter(_full_name=name).first()
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Cleans up only the name for the moment, but this can be used to make any treatment before saving the object
|
||||
"""
|
||||
"""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 (
|
||||
@ -1367,10 +1336,11 @@ class Page(models.Model):
|
||||
return l
|
||||
|
||||
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
|
||||
"""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)
|
||||
@ -1384,9 +1354,7 @@ class Page(models.Model):
|
||||
)
|
||||
|
||||
def set_lock(self, user):
|
||||
"""
|
||||
Sets a lock on the current page or raise an AlreadyLocked exception
|
||||
"""
|
||||
"""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
|
||||
@ -1395,41 +1363,34 @@ class Page(models.Model):
|
||||
# print("Locking page")
|
||||
|
||||
def set_lock_recursive(self, user):
|
||||
"""
|
||||
Locks recursively all the child pages for editing properties
|
||||
"""
|
||||
"""Locks recursively all the child pages for editing properties."""
|
||||
for p in self.children.all():
|
||||
p.set_lock_recursive(user)
|
||||
self.set_lock(user)
|
||||
|
||||
def unset_lock_recursive(self):
|
||||
"""
|
||||
Unlocks recursively all the child pages
|
||||
"""
|
||||
"""Unlocks recursively all the child pages."""
|
||||
for p in self.children.all():
|
||||
p.unset_lock_recursive()
|
||||
self.unset_lock()
|
||||
|
||||
def unset_lock(self):
|
||||
"""Always try to unlock, even if there is no lock"""
|
||||
"""Always try to unlock, even if there is no lock."""
|
||||
self.lock_user = None
|
||||
self.lock_timeout = None
|
||||
super().save()
|
||||
# print("Unlocking page")
|
||||
|
||||
def get_lock(self):
|
||||
"""
|
||||
Returns the page's mutex containing the time and the user in a dict
|
||||
"""
|
||||
"""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")
|
||||
|
||||
def get_full_name(self):
|
||||
"""
|
||||
Computes the real full_name of the page based on its name and its parent's 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)
|
||||
(For example when treating a Page object coming from a form).
|
||||
"""
|
||||
if self.parent is None:
|
||||
return self.name
|
||||
@ -1463,8 +1424,8 @@ class Page(models.Model):
|
||||
|
||||
|
||||
class PageRev(models.Model):
|
||||
"""
|
||||
This is the true content of the page.
|
||||
"""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.
|
||||
@ -1492,9 +1453,6 @@ class PageRev(models.Model):
|
||||
self.page.unset_lock()
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""
|
||||
This is needed for black magic powered UpdateView's children
|
||||
"""
|
||||
return reverse("core:page", kwargs={"page_name": self.page._full_name})
|
||||
|
||||
def __getattribute__(self, attr):
|
||||
@ -1573,9 +1531,7 @@ class Gift(models.Model):
|
||||
|
||||
|
||||
class OperationLog(models.Model):
|
||||
"""
|
||||
General purpose log object to register operations
|
||||
"""
|
||||
"""General purpose log object to register operations."""
|
||||
|
||||
date = models.DateTimeField(_("date"), auto_now_add=True)
|
||||
label = models.CharField(_("label"), max_length=255)
|
||||
|
@ -21,24 +21,26 @@
|
||||
#
|
||||
#
|
||||
|
||||
"""
|
||||
This page is useful for custom migration tricks.
|
||||
Sometimes, when you need to have a migration hack and you think it can be
|
||||
useful again, put it there, we never know if we might need the hack again.
|
||||
"""Collection of utils for custom migration tricks.
|
||||
|
||||
Sometimes, when you need to have a migration hack,
|
||||
and you think it can be useful again,
|
||||
put it there, we never know if we might need the hack again.
|
||||
"""
|
||||
|
||||
from django.db import connection, migrations
|
||||
|
||||
|
||||
class PsqlRunOnly(migrations.RunSQL):
|
||||
"""
|
||||
This is an SQL runner that will launch the given command only if
|
||||
the used DBMS is PostgreSQL.
|
||||
"""SQL runner for PostgreSQL-only queries.
|
||||
|
||||
It may be useful to run Postgres' specific SQL, or to take actions
|
||||
that would be non-senses with backends other than Postgre, such
|
||||
as disabling particular constraints that would prevent the migration
|
||||
to run successfully.
|
||||
|
||||
If used on another DBMS than Postgres, it will be a noop.
|
||||
|
||||
See `club/migrations/0010_auto_20170912_2028.py` as an example.
|
||||
Some explanations can be found here too:
|
||||
https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes
|
||||
|
@ -31,9 +31,7 @@ from django.core.files.storage import FileSystemStorage
|
||||
|
||||
|
||||
class ScssFinder(FileSystemFinder):
|
||||
"""
|
||||
Find static *.css files compiled on the fly
|
||||
"""
|
||||
"""Find static *.css files compiled on the fly."""
|
||||
|
||||
locations = []
|
||||
|
||||
|
@ -35,10 +35,9 @@ from core.scss.storage import ScssFileStorage, find_file
|
||||
|
||||
|
||||
class ScssProcessor(object):
|
||||
"""
|
||||
If DEBUG mode enabled : compile the scss file
|
||||
"""If DEBUG mode enabled : compile the scss file
|
||||
Else : give the path of the corresponding css supposed to already be compiled
|
||||
Don't forget to use compilestatics to compile scss for production
|
||||
Don't forget to use compilestatics to compile scss for production.
|
||||
"""
|
||||
|
||||
prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/"))
|
||||
|
@ -91,9 +91,9 @@ class IndexSignalProcessor(signals.BaseSignalProcessor):
|
||||
|
||||
|
||||
class BigCharFieldIndex(indexes.CharField):
|
||||
"""
|
||||
Workaround to avoid xapian.InvalidArgument: Term too long (> 245)
|
||||
See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion
|
||||
"""Workaround to avoid xapian.InvalidArgument: Term too long (> 245).
|
||||
|
||||
See https://groups.google.com/forum/#!topic/django-haystack/hRJKcPNPXqw/discussion.
|
||||
"""
|
||||
|
||||
def prepare(self, term):
|
||||
|
@ -7,9 +7,7 @@ from core.models import User
|
||||
|
||||
@receiver(m2m_changed, sender=User.groups.through, dispatch_uid="user_groups_changed")
|
||||
def user_groups_changed(sender, instance: User, **kwargs):
|
||||
"""
|
||||
Clear the cached groups of the user
|
||||
"""
|
||||
"""Clear the cached groups of the user."""
|
||||
# As a m2m relationship doesn't live within the model
|
||||
# but rather on an intermediary table, there is no
|
||||
# model method to override, meaning we must use
|
||||
|
@ -30,11 +30,11 @@ from jinja2.parser import Parser
|
||||
|
||||
|
||||
class HoneypotExtension(Extension):
|
||||
"""
|
||||
Wrapper around the honeypot extension tag
|
||||
Known limitation: doesn't support arguments
|
||||
"""Wrapper around the honeypot extension tag.
|
||||
|
||||
Usage: {% render_honeypot_field %}
|
||||
Known limitation: doesn't support arguments.
|
||||
|
||||
Usage: `{% render_honeypot_field %}`
|
||||
"""
|
||||
|
||||
tags = {"render_honeypot_field"}
|
||||
|
@ -46,9 +46,7 @@ def markdown(text):
|
||||
def phonenumber(
|
||||
value, country="FR", number_format=phonenumbers.PhoneNumberFormat.NATIONAL
|
||||
):
|
||||
"""
|
||||
This filter is kindly borrowed from https://github.com/foundertherapy/django-phonenumber-filter
|
||||
"""
|
||||
# collectivised from https://github.com/foundertherapy/django-phonenumber-filter.
|
||||
value = str(value)
|
||||
try:
|
||||
parsed = phonenumbers.parse(value, country)
|
||||
@ -59,6 +57,12 @@ def phonenumber(
|
||||
|
||||
@register.filter(name="truncate_time")
|
||||
def truncate_time(value, time_unit):
|
||||
"""Remove everything in the time format lower than the specified unit.
|
||||
|
||||
Args:
|
||||
value: the value to truncate
|
||||
time_unit: the lowest unit to display
|
||||
"""
|
||||
value = str(value)
|
||||
return {
|
||||
"millis": lambda: value.split(".")[0],
|
||||
@ -81,8 +85,6 @@ def format_timedelta(value: datetime.timedelta) -> str:
|
||||
|
||||
@register.simple_tag()
|
||||
def scss(path):
|
||||
"""
|
||||
Return path of the corresponding css file after compilation
|
||||
"""
|
||||
"""Return path of the corresponding css file after compilation."""
|
||||
processor = ScssProcessor(path)
|
||||
return processor.get_converted_scss()
|
||||
|
@ -105,7 +105,7 @@ class TestUserRegistration:
|
||||
def test_register_fail_with_not_existing_email(
|
||||
self, client: Client, valid_payload, monkeypatch
|
||||
):
|
||||
"""Test that, when email is valid but doesn't actually exist, registration fails"""
|
||||
"""Test that, when email is valid but doesn't actually exist, registration fails."""
|
||||
|
||||
def always_fail(*_args, **_kwargs):
|
||||
raise SMTPException
|
||||
@ -127,10 +127,7 @@ class TestUserLogin:
|
||||
return User.objects.first()
|
||||
|
||||
def test_login_fail(self, client, user):
|
||||
"""
|
||||
Should not login a user correctly
|
||||
"""
|
||||
|
||||
"""Should not login a user correctly."""
|
||||
response = client.post(
|
||||
reverse("core:login"),
|
||||
{
|
||||
@ -158,9 +155,7 @@ class TestUserLogin:
|
||||
assert response.wsgi_request.user.is_anonymous
|
||||
|
||||
def test_login_success(self, client, user):
|
||||
"""
|
||||
Should login a user correctly
|
||||
"""
|
||||
"""Should login a user correctly."""
|
||||
response = client.post(
|
||||
reverse("core:login"),
|
||||
{
|
||||
@ -210,7 +205,7 @@ class TestUserLogin:
|
||||
],
|
||||
)
|
||||
def test_custom_markdown_syntax(md, html):
|
||||
"""Test the homemade markdown syntax"""
|
||||
"""Test the homemade markdown syntax."""
|
||||
assert markdown(md) == f"<p>{html}</p>\n"
|
||||
|
||||
|
||||
@ -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 '<a href="/page/guy/bibou/">' 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('<a href="/page/guy/bibou/edit/">', 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('<a href="/page/create/?page=swagg">', 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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
||||
|
Reference in New Issue
Block a user