From 0e08a98987539ac986e2f71a1e366b6e2a0c707d Mon Sep 17 00:00:00 2001 From: ievgenii vdovenko Date: Fri, 14 Nov 2025 17:06:25 +0100 Subject: [PATCH] feature: implements user profile quotas update --- rhodecode/apps/admin/__init__.py | 21 ++++++++++ rhodecode/apps/admin/views/users.py | 42 +++++++++++++++++++ rhodecode/model/forms.py | 8 ++++ rhodecode/model/quota_model.py | 27 ++++++++++-- rhodecode/model/user.py | 7 ++++ .../admin/settings/settings_user_quotas.mako | 1 + .../templates/admin/users/user_edit.mako | 3 ++ .../admin/users/user_edit_user_quotas.mako | 34 +++++++++++++++ 8 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 rhodecode/templates/admin/users/user_edit_user_quotas.mako diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py index 453a482a..d43e1077 100644 --- a/rhodecode/apps/admin/__init__.py +++ b/rhodecode/apps/admin/__init__.py @@ -961,6 +961,27 @@ def admin_routes(config): request_method="POST", ) + # user quotas + config.add_route(name="edit_user_quotas", pattern=r"/users/{user_id:\d+}/edit/quotas", user_route=True) + config.add_view( + UsersView, + attr="user_quotas", + route_name="edit_user_quotas", + request_method="GET", + renderer="rhodecode:templates/admin/users/user_edit.mako", + ) + + config.add_route( + name="edit_user_quotas_update", pattern=r"/users/{user_id:\d+}/edit/quotas/update", user_route=True + ) + config.add_view( + UsersView, + attr="user_quotas_update", + route_name="edit_user_quotas_update", + request_method="POST", + renderer="rhodecode:templates/admin/users/user_edit.mako", + ) + # user audit logs config.add_route(name="edit_user_audit_logs", pattern=r"/users/{user_id:\d+}/edit/audit", user_route=True) config.add_view( diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py index 1dee66b8..f9c8f6b5 100644 --- a/rhodecode/apps/admin/views/users.py +++ b/rhodecode/apps/admin/views/users.py @@ -55,8 +55,10 @@ from rhodecode.model.forms import ( UserPermissionsForm, UserExtraEmailForm, UserExtraIpForm, + UserQuotasUpdateForm, ) from rhodecode.model.permission import PermissionModel +from rhodecode.model.quota_model import UserQuotaModel from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.ssh_key import SshKeyModel from rhodecode.model.user import UserModel @@ -283,6 +285,9 @@ class UsersView(UserAppView): ("zh", "Chinese (zh)"), ] + app_settings = c.rc_config + c.quotas_enabled = app_settings.get("rhodecode_user_quotas_enabled", UserQuotaModel.DEFAULT_QUOTAS_ENABLED) + c.allowed_extern_types = [(x.uid, x.get_display_name()) for x in self.get_auth_plugins()] perms = req.registry.settings.get("available_permissions") if not perms: @@ -1122,6 +1127,43 @@ class UsersView(UserAppView): return HTTPFound(h.route_path("edit_user_groups_management", user_id=user_id)) + @LoginRequired() + @HasPermissionAllDecorator("hg.admin") + def user_quotas(self): + _ = self.request.translate + c = self.load_default_context() + c.user = self.db_user + + quota_model = UserQuotaModel(c.user) + + c.active = "user_quotas" + c.user_repo_quotas = quota_model.formatted_user_max_repository_count_allowance() + c.user_repo_count = quota_model.user_repositories_count + + return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAllDecorator("hg.admin") + def user_quotas_update(self): + _ = self.request.translate + c = self.load_default_context() + c.user = self.db_user + + c.active = "user_quotas" + + user_form = UserQuotasUpdateForm()() + try: + quota_model = UserQuotaModel(c.user) + form_result = user_form.to_python(dict(self.request.POST)) + quota_model.user_max_repository_count_allowance = form_result["user_repo_quotas"] + h.flash(_("Updated user '%s' repository quota") % c.user.username, category="success") + except Exception as e: + log.exception("Exception updating user quotas, %s", e) + h.flash( + _("Exception updating user quotas of user %s") % self.request.POST.get("username"), category="error" + ) + raise HTTPFound(h.route_path("edit_user_quotas", user_id=c.user.user_id)) + @LoginRequired() @HasPermissionAllDecorator("hg.admin") def user_audit_logs(self): diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py index 8e68a252..31b7b9dd 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -715,3 +715,11 @@ def UserQuotasSettingsForm(): rhodecode_global_user_repo_quotas = v.Int(strip=True, required=True) return _UserQuotasSettingsForm + + +def UserQuotasUpdateForm(): + class _UserQuotasUpdateForm(formencode.Schema): + allow_extra_fields = True + user_repo_quotas = v.Int(strip=True, required=True) + + return _UserQuotasUpdateForm diff --git a/rhodecode/model/quota_model.py b/rhodecode/model/quota_model.py index aa8a2427..723f0bbb 100644 --- a/rhodecode/model/quota_model.py +++ b/rhodecode/model/quota_model.py @@ -10,7 +10,7 @@ log = logging.getLogger(__name__) class UserQuotaModel: - UNLIMITED = float("inf") + UNLIMITED = -1 DEFAULT_REPO_QUOTA = 5 DEFAULT_QUOTAS_ENABLED = False @@ -44,18 +44,37 @@ class UserQuotaModel: max_repository_count_allowance=repo_count_allowance, ) - def user_repositories_count(self): + @property + def user_repositories_count(self) -> int: return Session().scalar( select(func.count()).select_from(Repository).where(Repository.user_id == self.user.user_id) ) + @property + def user_max_repository_count_allowance(self) -> int: + quotas = self.user.get_quotas() + return quotas.get("max_repository_count_allowance") + + @user_max_repository_count_allowance.setter + def user_max_repository_count_allowance(self, new_value: int): + self.user.update_quotas(max_repository_count_allowance=new_value) + + def formatted_user_max_repository_count_allowance(self) -> str: + max_repos = self.user_max_repository_count_allowance + if max_repos is self.UNLIMITED: + return "Unlimited" + return str(max_repos) + def is_repo_creation_allowed(self): if not self.enabled: return True if self._user_quotas_valid(): - u_quotas = self.user.get_quotas() - return u_quotas.get("max_repository_count_allowance") > self.user_repositories_count() + if self.user_max_repository_count_allowance is self.UNLIMITED: + # since DB can't save float('inf'), we need to use a special case + return True + + return self.user_max_repository_count_allowance > self.user_repositories_count log.warning("User quotas not present, re-initialize quotas.") self.init_quotas() diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py index bf3a8327..d02fd051 100644 --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -412,6 +412,13 @@ class UserModel(BaseModel): self.sa.add(new_user) + # TODO: fix tests and uncomment this, because tests launching pyramid app, this part fails during + # fixture setup, in normal app this works as intended + # user quotas will be lazily initialized anyway, but this is workaround + # if new_user.is_new_user: + # qm = UserQuotaModel(new_user) + # qm.init_quotas() + if not edit and create_repo_group: RepoGroupModel().create_personal_repo_group(new_user, commit_early=False) diff --git a/rhodecode/templates/admin/settings/settings_user_quotas.mako b/rhodecode/templates/admin/settings/settings_user_quotas.mako index 93ce280b..63c17034 100644 --- a/rhodecode/templates/admin/settings/settings_user_quotas.mako +++ b/rhodecode/templates/admin/settings/settings_user_quotas.mako @@ -21,6 +21,7 @@
${h.text('rhodecode_global_user_repo_quotas', c.global_user_repo_quotas)} +

${_('If you want to set an unlimited quota, enter -1.')}

diff --git a/rhodecode/templates/admin/users/user_edit.mako b/rhodecode/templates/admin/users/user_edit.mako index 1f67bb69..1fef54cc 100644 --- a/rhodecode/templates/admin/users/user_edit.mako +++ b/rhodecode/templates/admin/users/user_edit.mako @@ -46,6 +46,9 @@
  • ${_('User Groups Management')}
  • ${_('Audit logs')}
  • ${_('Caches')}
  • + %if c.quotas_enabled: +
  • ${_('User Quotas')}
  • + %endif diff --git a/rhodecode/templates/admin/users/user_edit_user_quotas.mako b/rhodecode/templates/admin/users/user_edit_user_quotas.mako new file mode 100644 index 00000000..79ca3b81 --- /dev/null +++ b/rhodecode/templates/admin/users/user_edit_user_quotas.mako @@ -0,0 +1,34 @@ +
    +
    +

    ${_('User Quotas')}

    +
    +
    + ${h.secure_form(h.route_path('edit_user_quotas_update', user_id=c.user.user_id), id='user_quotas_form', request=request)} +
    + +
    +
    + +
    +
    + ${h.text('user_repo_count', c.user_repo_count, readonly=True, class_='no-border')} +
    +
    + +
    +
    + +
    +
    + ${h.text('user_repo_quotas', c.user_repo_quotas)} +

    ${_('If you want to set an unlimited quota, enter -1.')}

    +
    +
    + +
    + ${h.submit('save',_('Save'),class_="btn")} +
    + ${h.end_form()} +
    +
    +