feature: implements user profile quotas update

This commit is contained in:
ievgenii vdovenko 2025-11-14 17:06:25 +01:00
parent a960c9f99c
commit 0e08a98987
8 changed files with 139 additions and 4 deletions

View file

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

View file

@ -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):

View file

@ -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

View file

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

View file

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

View file

@ -21,6 +21,7 @@
</div>
<div class="input">
${h.text('rhodecode_global_user_repo_quotas', c.global_user_repo_quotas)}
<p class="help-block pre-formatting">${_('If you want to set an unlimited quota, enter -1.')}</p>
</div>
</div>

View file

@ -46,6 +46,9 @@
<li class="${h.is_active('groups', c.active)}"><a href="${h.route_path('edit_user_groups_management', user_id=c.user.user_id)}">${_('User Groups Management')}</a></li>
<li class="${h.is_active('audit', c.active)}"><a href="${h.route_path('edit_user_audit_logs', user_id=c.user.user_id)}">${_('Audit logs')}</a></li>
<li class="${h.is_active('caches', c.active)}"><a href="${h.route_path('edit_user_caches', user_id=c.user.user_id)}">${_('Caches')}</a></li>
%if c.quotas_enabled:
<li class="${h.is_active('user_quotas', c.active)}"><a href="${h.route_path('edit_user_quotas', user_id=c.user.user_id)}">${_('User Quotas')}</a></li>
%endif
</ul>
</div>

View file

@ -0,0 +1,34 @@
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">${_('User Quotas')}</h3>
</div>
<div class="panel-body">
${h.secure_form(h.route_path('edit_user_quotas_update', user_id=c.user.user_id), id='user_quotas_form', request=request)}
<div class="fields">
<div class="field" id="user_repo_count">
<div class="label label">
<label for="user_repo_count">${_('Current Repository Count')}</label>
</div>
<div class="input">
${h.text('user_repo_count', c.user_repo_count, readonly=True, class_='no-border')}
</div>
</div>
<div class="field" id="user_repo_quotas">
<div class="label label">
<label for="user_repo_quotas">${_('Maximum repositories')}</label>
</div>
<div class="input">
${h.text('user_repo_quotas', c.user_repo_quotas)}
<p class="help-block pre-formatting">${_('If you want to set an unlimited quota, enter -1.')}</p>
</div>
</div>
<div class="buttons">
${h.submit('save',_('Save'),class_="btn")}
</div>
${h.end_form()}
</div>
</div>
</div>