Expose profiles API, quota update endpoint

pull/1371/head
Piero Toffanin 2023-09-01 16:16:13 -04:00
rodzic b4e54e6406
commit aa737da1a1
7 zmienionych plików z 109 dodań i 10 usunięć

Wyświetl plik

@ -1,7 +1,10 @@
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from rest_framework import serializers, viewsets, generics, status from app.models import Profile
from rest_framework import serializers, viewsets, generics, status, exceptions
from rest_framework.decorators import action
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response from rest_framework.response import Response
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from app import models from app import models
@ -20,6 +23,7 @@ class AdminUserViewSet(viewsets.ModelViewSet):
if email is not None: if email is not None:
queryset = queryset.filter(email=email) queryset = queryset.filter(email=email)
return queryset return queryset
def create(self, request): def create(self, request):
data = request.data.copy() data = request.data.copy()
password = data.get('password') password = data.get('password')
@ -44,3 +48,36 @@ class AdminGroupViewSet(viewsets.ModelViewSet):
if name is not None: if name is not None:
queryset = queryset.filter(name=name) queryset = queryset.filter(name=name)
return queryset return queryset
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
exclude = ('id', )
read_only_fields = ('user', )
class AdminProfileViewSet(viewsets.ModelViewSet):
serializer_class = ProfileSerializer
permission_classes = [IsAdminUser]
lookup_field = 'user'
def get_queryset(self):
return Profile.objects.all()
@action(detail=True, methods=['post'])
def update_quota_deadline(self, request, user=None):
try:
hours = float(request.data.get('hours', ''))
if hours < 0:
raise ValueError("hours must be >= 0")
except ValueError as e:
raise exceptions.ValidationError(str(e))
try:
p = Profile.objects.get(user=user)
except ObjectDoesNotExist:
raise exceptions.NotFound()
return Response({'deadline': p.set_quota_deadline(hours)}, status=status.HTTP_200_OK)

Wyświetl plik

@ -6,7 +6,7 @@ from .projects import ProjectViewSet
from .tasks import TaskViewSet, TaskDownloads, TaskAssets, TaskAssetsImport from .tasks import TaskViewSet, TaskDownloads, TaskAssets, TaskAssetsImport
from .imageuploads import Thumbnail, ImageDownload from .imageuploads import Thumbnail, ImageDownload
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
from .admin import AdminUserViewSet, AdminGroupViewSet from .admin import AdminUserViewSet, AdminGroupViewSet, AdminProfileViewSet
from rest_framework_nested import routers from rest_framework_nested import routers
from rest_framework_jwt.views import obtain_jwt_token from rest_framework_jwt.views import obtain_jwt_token
from .tiler import TileJson, Bounds, Metadata, Tiles, Export from .tiler import TileJson, Bounds, Metadata, Tiles, Export
@ -26,6 +26,7 @@ tasks_router.register(r'tasks', TaskViewSet, basename='projects-tasks')
admin_router = routers.DefaultRouter() admin_router = routers.DefaultRouter()
admin_router.register(r'admin/users', AdminUserViewSet, basename='admin-users') admin_router.register(r'admin/users', AdminUserViewSet, basename='admin-users')
admin_router.register(r'admin/groups', AdminGroupViewSet, basename='admin-groups') admin_router.register(r'admin/groups', AdminGroupViewSet, basename='admin-groups')
admin_router.register(r'admin/profiles', AdminProfileViewSet, basename='admin-groups')
urlpatterns = [ urlpatterns = [
url(r'processingnodes/options/$', ProcessingNodeOptionsView.as_view()), url(r'processingnodes/options/$', ProcessingNodeOptionsView.as_view()),
@ -56,7 +57,7 @@ urlpatterns = [
url(r'^auth/', include('rest_framework.urls')), url(r'^auth/', include('rest_framework.urls')),
url(r'^token-auth/', obtain_jwt_token), url(r'^token-auth/', obtain_jwt_token),
url(r'^plugins/(?P<plugin_name>[^/.]+)/(.*)$', api_view_handler) url(r'^plugins/(?P<plugin_name>[^/.]+)/(.*)$', api_view_handler),
] ]
if settings.ENABLE_USERS_API: if settings.ENABLE_USERS_API:

Wyświetl plik

@ -1,3 +1,4 @@
import time
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -6,6 +7,8 @@ from django.dispatch import receiver
from app.models import Task from app.models import Task
from django.db.models import Sum from django.db.models import Sum
from django.core.cache import cache from django.core.cache import cache
from webodm import settings
class Profile(models.Model): class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE) user = models.OneToOneField(User, on_delete=models.CASCADE)
@ -17,6 +20,13 @@ class Profile(models.Model):
def used_quota(self): def used_quota(self):
return Task.objects.filter(project__owner=self.user).aggregate(total=Sum('size'))['total'] return Task.objects.filter(project__owner=self.user).aggregate(total=Sum('size'))['total']
def has_exceeded_quota(self):
if not self.has_quota():
return False
q = self.used_quota()
return q > self.quota
def used_quota_cached(self): def used_quota_cached(self):
k = f'used_quota_{self.user.id}' k = f'used_quota_{self.user.id}'
cached = cache.get(k) cached = cache.get(k)
@ -37,6 +47,20 @@ class Profile(models.Model):
def clear_used_quota_cache(self): def clear_used_quota_cache(self):
cache.delete(f'used_quota_{self.user.id}') cache.delete(f'used_quota_{self.user.id}')
def get_quota_deadline(self):
return cache.get(f'quota_deadline_{self.user.id}')
def set_quota_deadline(self, hours):
k = f'quota_deadline_{self.user.id}'
seconds = (hours * 60 * 60)
v = time.time() + seconds
cache.set(k, v, int(max(seconds * 10, settings.QUOTA_EXCEEDED_GRACE_PERIOD * 60 * 60)))
return v
def clear_quota_deadline(self):
cache.delete(f'quota_deadline_{self.user.id}')
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs): def create_user_profile(sender, instance, created, **kwargs):
if created: if created:

Wyświetl plik

@ -42,9 +42,9 @@
{% if user.profile.has_exceeded_quota_cached %} {% if user.profile.has_exceeded_quota_cached %}
{% with total=user.profile.quota|disk_size used=user.profile.used_quota_cached|disk_size %} {% with total=user.profile.quota|disk_size used=user.profile.used_quota_cached|disk_size %}
{% quota_exceeded_grace_period as hours %} {% quota_exceeded_grace_period as when %}
<div class="alert alert-warning alert-dismissible"> <div class="alert alert-warning alert-dismissible">
<i class="fas fa-exclamation-triangle"></i> {% blocktrans %}The disk quota is being exceeded ({{ used }} of {{ total }} used). The most recent tasks will be automatically deleted within {{ hours }} hours, until usage falls below {{ total }}.{% endblocktrans %} <i class="fas fa-info-circle"></i> {% blocktrans %}The disk quota is being exceeded ({{ used }} of {{ total }} used). The most recent tasks will be automatically deleted {{ when }}, until usage falls below {{ total }}.{% endblocktrans %}
</div> </div>
{% endwith %} {% endwith %}
{% endif %} {% endif %}

Wyświetl plik

@ -1,8 +1,10 @@
import datetime import datetime
import math import math
import logging import logging
import time
from django import template from django import template
from webodm import settings from webodm import settings
from django.utils.translation import gettext as _
register = template.Library() register = template.Library()
logger = logging.getLogger('app.logger') logger = logging.getLogger('app.logger')
@ -28,9 +30,22 @@ def percentage(num, den, maximum=None):
perc = min(perc, maximum) perc = min(perc, maximum)
return perc return perc
@register.simple_tag @register.simple_tag(takes_context=True)
def quota_exceeded_grace_period(): def quota_exceeded_grace_period(context):
return settings.QUOTA_EXCEEDED_GRACE_PERIOD deadline = context.request.user.profile.get_quota_deadline()
now = time.time()
if deadline is None:
deadline = now + settings.QUOTA_EXCEEDED_GRACE_PERIOD * 60 * 60
diff = max(0, deadline - now)
if diff >= 60*60*24*2:
return _("within %(num)s days") % {"num": math.ceil(diff / (60*60*24))}
elif diff >= 60*60:
return _("within %(num)s hours") % {"num": math.ceil(diff / (60*60))}
elif diff > 0:
return _("within %(num)s minutes") % {"num": math.ceil(diff / 60)}
else:
return _("very soon")
@register.simple_tag @register.simple_tag
def is_single_user_mode(): def is_single_user_mode():

Wyświetl plik

@ -44,6 +44,14 @@ app.conf.beat_schedule = {
'retry': False 'retry': False
} }
}, },
'check-quotas': {
'task': 'worker.tasks.check_quotas',
'schedule': 3600,
'options': {
'expires': 1799,
'retry': False
}
},
} }
# Mock class for handling async results during testing # Mock class for handling async results during testing

Wyświetl plik

@ -11,6 +11,7 @@ from celery.utils.log import get_task_logger
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count from django.db.models import Count
from django.db.models import Q from django.db.models import Q
from app.models import Profile
from app.models import Project from app.models import Project
from app.models import Task from app.models import Task
@ -203,3 +204,16 @@ def export_pointcloud(self, input, **opts):
except Exception as e: except Exception as e:
logger.error(str(e)) logger.error(str(e))
return {'error': str(e)} return {'error': str(e)}
@app.task
def check_quotas():
profiles = Profile.objects.filter(quota__gt=-1)
# for p in profiles:
# deadline_key = "%s_quota_exceeded_deadline" % p.user.id
# if p.has_exceeded_quota():
# now = time.time()
# deadline = redis_client.getset(deadline_key, now + (settings.QUOTA_EXCEEDED_GRACE_PERIOD * 60 * 60))
# # if deadline < now: TODO..
# else:
# redis_client.delete(deadline_key)