Skip to content

Commit

Permalink
Namechanges permissions and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
rafalp committed May 31, 2024
1 parent 1460b9c commit cda26f3
Show file tree
Hide file tree
Showing 12 changed files with 476 additions and 40 deletions.
5 changes: 5 additions & 0 deletions frontend/src/datetimeFormats.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const locale = window.misago_locale || "en-us"
export const momentAgo = pgettext("time ago", "moment ago")
export const momentAgoNarrow = pgettext("time ago", "now")
export const dayAt = pgettext("day at time", "%(day)s at %(time)s")
export const soonAt = pgettext("day at time", "at %(time)s")
export const tomorrowAt = pgettext("day at time", "Tomorrow at %(time)s")
export const yesterdayAt = pgettext("day at time", "Yesterday at %(time)s")

Expand Down Expand Up @@ -103,6 +104,10 @@ export function formatRelative(date) {
}

if (isSameDay(now, date)) {
if (diff > 0) {
return soonAt.replace("%(time)s", shortTime.format(date))
}

return shortTime.format(date)
}

Expand Down
25 changes: 20 additions & 5 deletions misago/account/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from django.contrib.auth import get_user_model
from django.utils.translation import pgettext, pgettext_lazy

from ..users.namechanges import get_username_options
from ..users.validators import validate_username
from .namechanges import get_available_username_changes

User = get_user_model()

Expand Down Expand Up @@ -157,7 +157,7 @@ class AccountUsernameForm(forms.Form):
def __init__(self, *args, **kwargs):
request = kwargs.pop("request")
self.instance = kwargs.pop("instance")
self.acl = request.user_acl
self.permissions = request.user_permissions
self.settings = request.settings
self.username_cache = None

Expand All @@ -166,15 +166,29 @@ def __init__(self, *args, **kwargs):
self.fields["username"].max_length = self.settings.username_length_max

@cached_property
def options(self):
return get_username_options(self.settings, self.instance, self.acl)
def available_changes(self):
return get_available_username_changes(self.instance, self.permissions)

def clean_username(self):
data = self.cleaned_data["username"]
if data == self.instance.username:
return data

print(self.options)
if not self.permissions.can_change_username:
raise forms.ValidationError(
pgettext_lazy(
"account username help",
"You can't change your username.",
),
)

if not self.available_changes.can_change_username:
raise forms.ValidationError(
pgettext_lazy(
"account username help",
"You can't change your username at the moment.",
),
)

validate_username(self.settings, data, self.instance)
return data
Expand All @@ -190,5 +204,6 @@ def save(self):
if username != self.instance.username:
self.instance.set_username(username, changed_by=self.instance)
self.instance.save()
del self.available_changes

return self.instance
111 changes: 111 additions & 0 deletions misago/account/namechanges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from dataclasses import dataclass
from datetime import datetime, timedelta

from django.utils import timezone


@dataclass
class AvailableUsernameChanges:
unlimited: bool
changes_left: int | None
next_change: datetime | None

@property
def can_change_username(self) -> bool:
if self.unlimited or self.changes_left > 0:
return bool(self.next_change is None or self.next_change < timezone.now())

return False


def get_available_username_changes(user, permissions) -> AvailableUsernameChanges:
if not permissions.can_change_username:
return AvailableUsernameChanges(
unlimited=False,
changes_left=0,
next_change=None,
)

changes_left = get_username_changes_left(user, permissions)
next_change = get_username_next_change(user, permissions, changes_left)

return AvailableUsernameChanges(
unlimited=permissions.username_changes_limit == 0,
changes_left=changes_left,
next_change=next_change,
)


def get_username_changes_left(user, permissions) -> int | None:
if permissions.username_changes_limit == 0:
return None

queryset = user.namechanges.filter(changed_by=user)

if permissions.username_changes_expire:
expired_cutoff = timezone.now() - timedelta(
hours=permissions.username_changes_expire,
)
queryset = queryset.filter(changed_on__gt=expired_cutoff)

return max((permissions.username_changes_limit - queryset.count(), 0))


def get_username_next_change(user, permissions, changes_left: int) -> datetime | None:
if changes_left == 0:
return get_username_next_change_if_out_of_changes(user, permissions)

# User has either unlimited changes or some changes left
if permissions.username_changes_span:
return get_newest_change(user, permissions)

return None


def get_username_next_change_if_out_of_changes(user, permissions) -> datetime | None:
# Changes don't expire, no more changes will be possible for the user
if not permissions.username_changes_expire:
return None

oldest_change = get_oldest_expiring_change(user, permissions)
if oldest_change is None:
return None

# We'll be able to change username when oldest change expires
if not permissions.username_changes_span:
return oldest_change

newest_change = get_newest_change(user, permissions)
if newest_change is None:
return None

return max((oldest_change, newest_change))


def get_oldest_expiring_change(user, permissions) -> datetime | None:
expired_cutoff = timezone.now() - timedelta(
hours=permissions.username_changes_expire,
)

change = user.namechanges.filter(
changed_by=user,
changed_on__gt=expired_cutoff,
).first()

if not change:
return None

return change.changed_on + timedelta(
hours=permissions.username_changes_expire,
)


def get_newest_change(user, permissions) -> datetime | None:
change = user.namechanges.filter(changed_by=user).last()

if not change:
return None

return change.changed_on + timedelta(
hours=permissions.username_changes_span,
)
Loading

0 comments on commit cda26f3

Please sign in to comment.