diff --git a/admin/institutional_storage_quota_control/views.py b/admin/institutional_storage_quota_control/views.py index 7b028da5986..725d464053d 100644 --- a/admin/institutional_storage_quota_control/views.py +++ b/admin/institutional_storage_quota_control/views.py @@ -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): @@ -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. """ @@ -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 diff --git a/admin/quota_recalc/views.py b/admin/quota_recalc/views.py index 29484c3d559..abeb1bf6537 100644 --- a/admin/quota_recalc/views.py +++ b/admin/quota_recalc/views.py @@ -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): @@ -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( @@ -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( diff --git a/admin/rdm_custom_storage_location/utils.py b/admin/rdm_custom_storage_location/utils.py index df350ed2ae1..8dc098487b7 100644 --- a/admin/rdm_custom_storage_location/utils.py +++ b/admin/rdm_custom_storage_location/utils.py @@ -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 @@ -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 @@ -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) diff --git a/admin/rdm_custom_storage_location/views.py b/admin/rdm_custom_storage_location/views.py index dff08d2b3ec..d91276ee0fa 100644 --- a/admin/rdm_custom_storage_location/views.py +++ b/admin/rdm_custom_storage_location/views.py @@ -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': @@ -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) diff --git a/admin/templates/institutional_storage_quota_control/user_list_by_institute.html b/admin/templates/institutional_storage_quota_control/user_list_by_institute.html index 2e2050514b0..3d747299e1c 100644 --- a/admin/templates/institutional_storage_quota_control/user_list_by_institute.html +++ b/admin/templates/institutional_storage_quota_control/user_list_by_institute.html @@ -121,7 +121,7 @@

{% trans "Institutional Storage" %} > {{ institution_name }}