Aller au contenu

Models

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]

Subscription

Bases: Model

compute_start(d=None, duration=1, user=None) staticmethod

Computes the start date of the subscription.

The computation is done with respect to the given date (default is today) and the start date given in settings.SITH_SEMESTER_START_AUTUMN. It takes the nearest past start date. Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15) Today -> Start date 2015-03-17 -> 2015-02-15 2015-01-11 -> 2014-08-15.

Source code in subscription/models.py
@staticmethod
def compute_start(
    d: date | None = None, duration: int = 1, user: User | None = None
) -> date:
    """Computes the start date of the subscription.

    The computation is done with respect to the given date (default is today)
    and the start date given in settings.SITH_SEMESTER_START_AUTUMN.
    It takes the nearest past start date.
    Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15)
        Today      -> Start date
        2015-03-17 -> 2015-02-15
        2015-01-11 -> 2014-08-15.
    """
    if not d:
        d = date.today()
    if user is not None and user.subscriptions.exists():
        last = user.subscriptions.last()
        if last.is_valid_now():
            d = last.subscription_end
    if duration <= 2:  # Sliding subscriptions for 1 or 2 semesters
        return d
    return get_start_of_semester(d)

compute_end(duration, start=None, user=None) staticmethod

Compute the end date of the subscription.

Parameters:

Name Type Description Default
duration int

the duration of the subscription, in semester (for example, 2 => 2 semesters => 1 year)

required
start date | None

The start date of the subscription

None
user User | None

the user which is (or will be) subscribed

None
Exemples

Start - Duration -> End date 2015-09-18 - 1 -> 2016-03-18 2015-09-18 - 2 -> 2016-09-18 2015-09-18 - 3 -> 2017-03-18 2015-09-18 - 4 -> 2017-09-18.

Source code in subscription/models.py
@staticmethod
def compute_end(
    duration: int, start: date | None = None, user: User | None = None
) -> date:
    """Compute the end date of the subscription.

    Args:
        duration:
            the duration of the subscription, in semester
            (for example, 2 => 2 semesters => 1 year)
        start: The start date of the subscription
        user: the user which is (or will be) subscribed

    Exemples:
        Start - Duration -> End date
        2015-09-18 - 1 -> 2016-03-18
        2015-09-18 - 2 -> 2016-09-18
        2015-09-18 - 3 -> 2017-03-18
        2015-09-18 - 4 -> 2017-09-18.
    """
    if start is None:
        start = Subscription.compute_start(duration=duration, user=user)

    return start + relativedelta(
        months=round(6 * duration),
        days=math.ceil((6 * duration - round(6 * duration)) * 30),
    )

get_start_of_semester(today=None)

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:

  • If the date is between 15/08 and 31/12 => Autumn semester.
  • 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

Parameters:

Name Type Description Default
today Optional[date]

the date to use to compute the semester. If None, use today's date.

None

Returns:

Type Description
date

the date of the start of the semester

Source code in core/utils.py
def get_start_of_semester(today: Optional[date] = None) -> 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:

    - If the date is between 15/08 and 31/12  => Autumn semester.
    - 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

    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 = localdate()

    autumn = date(today.year, *settings.SITH_SEMESTER_START_AUTUMN)
    spring = date(today.year, *settings.SITH_SEMESTER_START_SPRING)

    if today >= autumn:  # between 15/08 (included) and 31/12 -> autumn semester
        return autumn
    if today >= spring:  # between 15/02 (included) and 15/08 -> spring semester
        return spring
    # between 01/01 and 15/02 -> autumn semester of the previous year
    return autumn.replace(year=autumn.year - 1)

validate_type(value)

Source code in subscription/models.py
def validate_type(value):
    if value not in settings.SITH_SUBSCRIPTIONS:
        raise ValidationError(_("Bad subscription type"))

validate_payment(value)

Source code in subscription/models.py
def validate_payment(value):
    if value not in settings.SITH_SUBSCRIPTION_PAYMENT_METHOD:
        raise ValidationError(_("Bad payment method"))