Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
11 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 38 additions & 13 deletions admin/institutional_storage_quota_control/views.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import logging
from django.contrib.auth.mixins import UserPassesTestMixin
from django.db import connection, transaction, IntegrityError
from django.db.models import Subquery, OuterRef
from django.http import Http404

from admin.institutions.views import QuotaUserList
from osf.models import Institution, OSFUser, UserQuota
from osf.models import Institution, OSFUser, UserQuota, InstitutionDefaultMaxQuota
from admin.base import settings
from addons.osfstorage.models import Region
from django.views.generic import ListView, View
from django.shortcuts import redirect
from admin.rdm.utils import RdmPermissionMixin
from django.core.urlresolvers import reverse
from api.base import settings as api_settings

logger = logging.getLogger(__name__)


class InstitutionStorageList(RdmPermissionMixin, UserPassesTestMixin, ListView):
Expand Down Expand Up @@ -122,6 +125,22 @@ def get_institution(self):
raise Http404
return institution

def get_default_max_quota(self):
"""Get default max quota for the institution, fallback to DEFAULT_MAX_QUOTA when not found."""
try:
institution_default_max_quota = InstitutionDefaultMaxQuota.objects.get(
institution_id=self.institution_id
)
return institution_default_max_quota.default_max_quota
except InstitutionDefaultMaxQuota.DoesNotExist:
return api_settings.DEFAULT_MAX_QUOTA

def get_context_data(self, **kwargs):
""" Add default_max_quota to template context """
context = super().get_context_data(**kwargs)
context['default_max_quota'] = self.get_default_max_quota()
return context


class UpdateQuotaUserListByInstitutionStorageID(RdmPermissionMixin, UserPassesTestMixin, View):
""" Change max quota for an institution's users if that institution is not using NII Storage. """
Expand Down Expand Up @@ -163,17 +182,23 @@ def post(self, request, *args, **kwargs):
min_value, max_value = connection.ops.integer_field_range('PositiveIntegerField')
if min_value <= max_quota <= max_value:
# If max quota value is between 0 and 2147483647, update or create used quota for each user in the institution
for user in OSFUser.objects.filter(
affiliated_institutions=self.institution_id):
try:
with transaction.atomic():
UserQuota.objects.update_or_create(
user=user,
storage_type=UserQuota.CUSTOM_STORAGE,
defaults={'max_quota': max_quota}
)
except IntegrityError:
UserQuota.objects.filter(user=user, storage_type=UserQuota.CUSTOM_STORAGE).update(max_quota=max_quota)
with transaction.atomic():
InstitutionDefaultMaxQuota.objects.update_or_create(
institution_id=self.institution_id,
defaults={'default_max_quota': max_quota}
)
for user in OSFUser.objects.filter(
affiliated_institutions=self.institution_id):
try:
with transaction.atomic():
UserQuota.objects.update_or_create(
user=user,
storage_type=UserQuota.CUSTOM_STORAGE,
defaults={'max_quota': max_quota}
)
except IntegrityError as e:
logger.warning(u'IntegrityError while updating UserQuota: user={}, storage_type={}, max_quota={}: {}.'.format(user.id, UserQuota.CUSTOM_STORAGE, max_quota, str(e)))
UserQuota.objects.filter(user=user, storage_type=UserQuota.CUSTOM_STORAGE).update(max_quota=max_quota)
return redirect(
'institutional_storage_quota_control:institution_user_list',
institution_id=self.institution_id
Expand Down
12 changes: 8 additions & 4 deletions admin/quota_recalc/views.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import logging
from django.http import JsonResponse
from django.db import transaction, IntegrityError

from addons.osfstorage.models import Region
from api.base import settings as api_settings
from osf.models import OSFUser, UserQuota, Node
from osf.models.node import set_project_storage_type
from osf.utils.requests import check_select_for_update
from website.util.quota import used_quota
from website.util.quota import get_default_max_quota, used_quota

logger = logging.getLogger(__name__)


def calculate_quota(user):
Expand All @@ -23,6 +25,7 @@ def calculate_quota(user):
with transaction.atomic():
for storage_type in storage_type_list:
used = used_quota(user._id, storage_type)
max_quota = get_default_max_quota(user, storage_type)
try:
if check_select_for_update():
user_quota = UserQuota.objects.filter(
Expand All @@ -42,10 +45,11 @@ def calculate_quota(user):
UserQuota.objects.create(
user=user,
storage_type=storage_type,
max_quota=api_settings.DEFAULT_MAX_QUOTA,
max_quota=max_quota,
used=used,
)
except IntegrityError:
except IntegrityError as e:
logger.warning(u'IntegrityError while creating UserQuota in calculate_quota: user={}, storage_type={}: {}.'.format(user.id, storage_type, str(e)))
used = used_quota(user._id, storage_type)
if check_select_for_update():
user_quota = UserQuota.objects.filter(
Expand Down
52 changes: 51 additions & 1 deletion admin/rdm_custom_storage_location/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import os
import owncloud
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction

from addons.dropboxbusiness.models import node_post_save as dropboxbusiness_post_save
from addons.onedrivebusiness.models import node_post_save as onedrivebusiness_post_save
Expand Down Expand Up @@ -45,7 +46,8 @@
from framework.exceptions import HTTPError
from osf.models import AbstractNode
from website import settings as osf_settings
from osf.models import Node, OSFUser, ProjectStorageType, UserQuota
from osf.models import Node, OSFUser, ProjectStorageType, UserQuota, InstitutionDefaultMaxQuota
from api.base import settings as api_settings
from osf.models.external import ExternalAccountTemporary, ExternalAccount
from osf.utils import external_util
import datetime
Expand Down Expand Up @@ -1387,3 +1389,51 @@ def add_node_settings_to_projects(institution, provider_name):
onedrivebusiness_post_save(None, project, created=project_has_no_node_settings)
else:
node_post_save(None, project, created=project_has_no_node_settings)

def is_institution_using_nii_storage(institution):
"""Return True if the institution is currently using NII storage (or has no region configured)."""
old_region = Region.objects.filter(_id=institution._id).first()
return (
old_region is None or
old_region.waterbutler_settings.get('storage', {}).get('type') == Region.NII_STORAGE
)


def upsert_user_quota_for_institution(institution, provider_short_name, old_is_nii, old_quota_type=None):
"""Determine quota migration params and upsert UserQuota for all users affiliated with the institution."""
new_quota_type = institution.get_user_quota_type_for_nii_storage()
users = OSFUser.objects.filter(affiliated_institutions=institution.id)

if provider_short_name == 'osfstorage' and not old_is_nii:
# 機関ストレージ → NII Storage: reset all users to default quota
storage_type = new_quota_type
quota_map = {}
elif provider_short_name != 'osfstorage' and old_is_nii:
# NII Storage → 機関ストレージ: set all users to institution default quota
default_max_quota = InstitutionDefaultMaxQuota.objects.filter(institution=institution).values_list('default_max_quota', flat=True).first()
fixed_quota = default_max_quota if default_max_quota is not None else api_settings.DEFAULT_MAX_QUOTA
storage_type = UserQuota.CUSTOM_STORAGE
quota_map = {user.id: fixed_quota for user in users}
elif old_quota_type is not None and old_quota_type != new_quota_type and new_quota_type is not None:
# NII Storage quota type changed (e.g. no-region → has-region): preserve each user's existing quota
storage_type = new_quota_type
quota_map = {
q.user_id: q.max_quota
for q in UserQuota.objects.filter(user__in=users, storage_type=old_quota_type)
}
else:
return

with transaction.atomic():
for user in users:
max_quota = quota_map.get(user.id, api_settings.DEFAULT_MAX_QUOTA)
try:
with transaction.atomic():
UserQuota.objects.update_or_create(
user=user,
storage_type=storage_type,
defaults={'max_quota': max_quota}
)
except IntegrityError as e:
logger.warning(u'IntegrityError while updating UserQuota: user={}, storage_type={}, max_quota={}: {}.'.format(user.id, storage_type, max_quota, str(e)))
UserQuota.objects.filter(user=user, storage_type=storage_type).update(max_quota=max_quota)
4 changes: 4 additions & 0 deletions admin/rdm_custom_storage_location/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ def post(self, request, *args, **kwargs):
'message': 'Storage name is missing.'
}, status=http_status.HTTP_400_BAD_REQUEST)

old_is_nii = utils.is_institution_using_nii_storage(institution)
old_quota_type = institution.get_user_quota_type_for_nii_storage()

result = None

if provider_short_name == 's3':
Expand Down Expand Up @@ -436,6 +439,7 @@ def post(self, request, *args, **kwargs):
utils.change_allowed_for_institutions(
institution, provider_short_name)
utils.add_node_settings_to_projects(institution, provider_short_name)
utils.upsert_user_quota_for_institution(institution, provider_short_name, old_is_nii, old_quota_type)
return JsonResponse(result[0], status=status)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ <h3>{% trans "Institutional Storage" %} &gt; {{ institution_name }} </h3>
<input type="number" name="maxQuota"
id="storageLimit" class="form-control flex-item"
style="width:70px; max-width: 115px; margin-left: 2px;"
min="0" max="2147483647" required="" value="100"
min="0" max="2147483647" required="" value="{{ default_max_quota|default_if_none:100 }}"
oninvalid="this.setCustomValidity('{% trans 'Please enter Each user Quota for Institutional Storage (GB) within 0~2147483647.' %}')"
oninput="validateMaxQuotaInput()"
onKeyDown="preventDotInput(event)"
Expand Down
Loading
Loading