Aller au contenu

Views

OperationLog

Bases: Model

General purpose log object to register operations.

SithFile

Bases: Model

clean()

Cleans up the file.

Source code in core/models.py
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:
                import imghdr

                if imghdr.what(None, self.file.read()) not in [
                    "gif",
                    "png",
                    "jpeg",
                ]:
                    self.file.delete()
                    self.file = None
            except:  # noqa E722 I don't know the exception that can be raised
                self.file = None
        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
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
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
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()

User

Bases: AbstractBaseUser

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

cached_groups: list[Group] property

Get the list of groups this user is in.

The result is cached for the default duration (should be 5 minutes)

Returns: A list of all the groups this user is in.

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
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 pk is not None:
        group: Optional[Group] = get_group(pk=pk)
    elif name is not None:
        group: Optional[Group] = get_group(name=name)
    else:
        raise ValueError("You must either provide the id or the name of the group")
    if group is None:
        return False
    if group.id == settings.SITH_GROUP_PUBLIC_ID:
        return True
    if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:
        return self.is_subscribed
    if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:
        return self.was_subscribed
    if group.id == settings.SITH_GROUP_ROOT_ID:
        return self.is_root
    if group.is_meta:
        # check if this group is associated with a club
        group.__class__ = MetaGroup
        club = group.associated_club
        if club is None:
            return False
        membership = club.get_membership_for(self)
        if membership is None:
            return False
        if group.name.endswith(settings.SITH_MEMBER_SUFFIX):
            return True
        return membership.role > settings.SITH_MAXIMUM_FREE_ROLE
    return group in self.cached_groups

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
@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_full_name()

Returns the first_name plus the last_name, with a space in between.

Source code in core/models.py
def get_full_name(self):
    """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()

get_short_name()

Returns the short name for the user.

Source code in core/models.py
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
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_age()

Returns the age.

Source code in core/models.py
def get_age(self):
    """Returns the age."""
    today = timezone.now()
    born = self.date_of_birth
    return (
        today.year - born.year - ((today.month, today.day) < (born.month, born.day))
    )

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
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
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
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")
    )
    un_set = [u.username for u in User.objects.all()]
    if user_name in un_set:
        i = 1
        while user_name + str(i) in un_set:
            i += 1
        user_name += str(i)
    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
def is_owner(self, obj):
    """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
def can_edit(self, obj):
    """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"):
        for pk in obj.edit_groups.values_list("pk", flat=True):
            if self.is_in_group(pk=pk):
                return True
    if isinstance(obj, User) and obj == self:
        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
def can_view(self, obj):
    """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"):
        for pk in obj.view_groups.values_list("pk", flat=True):
            if self.is_in_group(pk=pk):
                return True
    return self.can_edit(obj)

clubs_with_rights()

The list of clubs where the user has rights

Source code in core/models.py
@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]

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

Customer

Bases: Model

Customer data of a User.

It adds some basic customers' information, such as the account ID, and is used by other accounting classes as reference to the customer, rather than using User.

can_buy: bool property

Check if whether this customer has the right to purchase any item.

This must be not confused with the Product.can_be_sold_to(user) method as the present method returns an information about a customer whereas the other tells something about the relation between a User (not a Customer, don't mix them) and a Product.

save(*args, allow_negative=False, is_selling=False, **kwargs)

is_selling : tell if the current action is a selling allow_negative : ignored if not a selling. Allow a selling to put the account in negative Those two parameters avoid blocking the save method of a customer if his account is negative.

Source code in counter/models.py
def save(self, *args, allow_negative=False, is_selling=False, **kwargs):
    """is_selling : tell if the current action is a selling
    allow_negative : ignored if not a selling. Allow a selling to put the account in negative
    Those two parameters avoid blocking the save method of a customer if his account is negative.
    """
    if self.amount < 0 and (is_selling and not allow_negative):
        raise ValidationError(_("Not enough money"))
    super().save(*args, **kwargs)

get_or_create(user) classmethod

Work in pretty much the same way as the usual get_or_create method, but with the default field replaced by some under the hood.

If the user has an account, return it as is. Else create a new account with no money on it and a new unique account id

Example : ::

user = User.objects.get(pk=1)
account, created = Customer.get_or_create(user)
if created:
    print(f"created a new account with id {account.id}")
else:
    print(f"user has already an account, with {account.id} € on it"
Source code in counter/models.py
@classmethod
def get_or_create(cls, user: User) -> Tuple[Customer, bool]:
    """Work in pretty much the same way as the usual get_or_create method,
    but with the default field replaced by some under the hood.

    If the user has an account, return it as is.
    Else create a new account with no money on it and a new unique account id

    Example : ::

        user = User.objects.get(pk=1)
        account, created = Customer.get_or_create(user)
        if created:
            print(f"created a new account with id {account.id}")
        else:
            print(f"user has already an account, with {account.id} € on it"
    """
    if hasattr(user, "customer"):
        return user.customer, False

    # account_id are always a number with a letter appended
    account_id = (
        Customer.objects.order_by(Length("account_id"), "account_id")
        .values("account_id")
        .last()
    )
    if account_id is None:
        # legacy from the old site
        account = cls.objects.create(user=user, account_id="1504a")
        return account, True

    account_id = account_id["account_id"]
    account_num = int(account_id[:-1])
    while Customer.objects.filter(account_id=account_id).exists():
        # when entering the first iteration, we are using an already existing account id
        # so the loop should always execute at least one time
        account_num += 1
        account_id = f"{account_num}{random.choice(string.ascii_lowercase)}"

    account = cls.objects.create(user=user, account_id=account_id)
    return account, True

ForumMessageMeta

Bases: Model

MergeForm

Bases: Form

SelectUserForm

Bases: Form

MergeUsersView

Bases: FormView

DeleteAllForumUserMessagesView

Bases: FormView

Delete all forum messages from an user.

Messages are soft deleted and are still visible from admins GUI frontend to the dedicated command.

OperationLogListView

Bases: ListView, CanEditPropMixin

List all logs.

__merge_subscriptions(u1, u2)

Give all the subscriptions of the second user to first one.

If some subscriptions are still active, update their end date to increase the overall subscription time of the first user.

Some examples : - if u1 is not subscribed, his subscription end date become the one of u2 - if u1 is subscribed but not u2, nothing happen - if u1 is subscribed for, let's say, 2 remaining months and u2 is subscribed for 3 remaining months, he shall then be subscribed for 5 months

Source code in rootplace/views.py
def __merge_subscriptions(u1: User, u2: User):
    """Give all the subscriptions of the second user to first one.

    If some subscriptions are still active, update their end date
    to increase the overall subscription time of the first user.

    Some examples :
    - if u1 is not subscribed, his subscription end date become the one of u2
    - if u1 is subscribed but not u2, nothing happen
    - if u1 is subscribed for, let's say,
      2 remaining months and u2 is subscribed for 3 remaining months,
    he shall then be subscribed for 5 months
    """
    last_subscription = (
        u1.subscriptions.filter(
            subscription_start__lte=timezone.now(), subscription_end__gte=timezone.now()
        )
        .order_by("subscription_end")
        .last()
    )
    if last_subscription is not None:
        subscription_end = last_subscription.subscription_end
        for subscription in u2.subscriptions.filter(
            subscription_end__gte=timezone.now()
        ):
            subscription.subscription_start = subscription_end
            if subscription.subscription_start > localdate():
                remaining = subscription.subscription_end - localdate()
            else:
                remaining = (
                    subscription.subscription_end - subscription.subscription_start
                )
            subscription_end += remaining
            subscription.subscription_end = subscription_end
            subscription.save()
    u2.subscriptions.all().update(member=u1)

__merge_pictures(u1, u2)

Source code in rootplace/views.py
def __merge_pictures(u1: User, u2: User) -> None:
    SithFile.objects.filter(owner=u2).update(owner=u1)
    if u1.profile_pict is None and u2.profile_pict is not None:
        u1.profile_pict, u2.profile_pict = u2.profile_pict, None
    if u1.scrub_pict is None and u2.scrub_pict is not None:
        u1.scrub_pict, u2.scrub_pict = u2.scrub_pict, None
    if u1.avatar_pict is None and u2.avatar_pict is not None:
        u1.avatar_pict, u2.avatar_pict = u2.avatar_pict, None
    u2.save()
    u1.save()

merge_users(u1, u2)

Merge u2 into u1.

This means that u1 shall receive everything that belonged to u2 :

- pictures
- refills of the sith account
- purchases of any item bought on the eboutic or the counters
- subscriptions
- godfathers
- godchildren

If u1 had no account id, he shall receive the one of u2. If u1 and u2 were both in the middle of a subscription, the remaining durations stack If u1 had no profile picture, he shall receive the one of u2

Source code in rootplace/views.py
def merge_users(u1: User, u2: User) -> User:
    """Merge u2 into u1.

    This means that u1 shall receive everything that belonged to u2 :

        - pictures
        - refills of the sith account
        - purchases of any item bought on the eboutic or the counters
        - subscriptions
        - godfathers
        - godchildren

    If u1 had no account id, he shall receive the one of u2.
    If u1 and u2 were both in the middle of a subscription, the remaining
    durations stack
    If u1 had no profile picture, he shall receive the one of u2
    """
    for field in u1._meta.fields:
        if not field.is_relation and not u1.__dict__[field.name]:
            u1.__dict__[field.name] = u2.__dict__[field.name]
    for group in u2.groups.all():
        u1.groups.add(group.id)
    for godfather in u2.godfathers.exclude(id=u1.id):
        u1.godfathers.add(godfather)
    for godchild in u2.godchildren.exclude(id=u1.id):
        u1.godchildren.add(godchild)
    __merge_subscriptions(u1, u2)
    __merge_pictures(u1, u2)
    u2.invoices.all().update(user=u1)
    c_src = Customer.objects.filter(user=u2).first()
    if c_src is not None:
        c_dest, created = Customer.get_or_create(u1)
        c_src.refillings.update(customer=c_dest)
        c_src.buyings.update(customer=c_dest)
        c_dest.recompute_amount()
        if created:
            # swap the account numbers, so that the user keep
            # the id he is accustomed to
            tmp_id = c_src.account_id
            # delete beforehand in order not to have a unique constraint violation
            c_src.delete()
            c_dest.account_id = tmp_id
    u1.save()
    u2.delete()  # everything remaining in u2 gets deleted thanks to on_delete=CASCADE
    return u1

delete_all_forum_user_messages(user, moderator, *, verbose=False)

Soft delete all messages of a user.

Parameters:

Name Type Description Default
user User

core.models.User the user to delete messages from

required
moderator User

core.models.User the one marked as the moderator.

required
verbose bool

bool if True, print the deleted messages

False
Source code in rootplace/views.py
def delete_all_forum_user_messages(
    user: User, moderator: User, *, verbose: bool = False
):
    """Soft delete all messages of a user.

    Args:
        user: core.models.User the user to delete messages from
        moderator: core.models.User the one marked as the moderator.
        verbose: bool if True, print the deleted messages
    """
    for message in user.forum_messages.all():
        if message.is_deleted():
            continue

        if verbose:
            logging.getLogger("django").info(message)
        ForumMessageMeta(message=message, user=moderator, action="DELETE").save()