diff --git a/changelog.d/20250620_113902_sekachev.bs.md b/changelog.d/20250620_113902_sekachev.bs.md new file mode 100644 index 000000000000..aecb01489451 --- /dev/null +++ b/changelog.d/20250620_113902_sekachev.bs.md @@ -0,0 +1,4 @@ +### Added + +- CVAT server tracks `last_activity_date` of a user, the field is updated once a day + () diff --git a/cvat/apps/engine/middleware.py b/cvat/apps/engine/middleware.py index 6b5a7066c43d..d608d251707f 100644 --- a/cvat/apps/engine/middleware.py +++ b/cvat/apps/engine/middleware.py @@ -5,6 +5,9 @@ from typing import Protocol from uuid import uuid4 +from django.conf import settings +from django.utils.timezone import now + class WithUUID(Protocol): uuid: str @@ -24,3 +27,28 @@ def __call__(self, request): response.headers["X-Request-Id"] = request.uuid return response + + +class LastActivityMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + + if request.user.is_authenticated: + profile = getattr(request.user, "profile", None) + if not profile: + return response + + last_activity_date = profile.last_activity_date + if ( + not last_activity_date + or (now() - last_activity_date) > settings.USER_LAST_ACTIVITY_UPDATE_MIN_INTERVAL + ): + # such way we avoid failing and any db updates if the Profile was removed during the request + from cvat.apps.engine.models import Profile + + Profile.objects.filter(user_id=request.user.id).update(last_activity_date=now()) + + return response diff --git a/cvat/apps/engine/migrations/0091_profile_last_activity_date.py b/cvat/apps/engine/migrations/0091_profile_last_activity_date.py new file mode 100644 index 000000000000..a7fc9b552333 --- /dev/null +++ b/cvat/apps/engine/migrations/0091_profile_last_activity_date.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.21 on 2025-06-22 11:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("engine", "0090_asset_content_size_data_content_size"), + ] + + operations = [ + migrations.AddField( + model_name="profile", + name="last_activity_date", + field=models.DateTimeField(blank=True, default=None, null=True), + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index d49d26a131b6..e409f2a0c971 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -1172,6 +1172,7 @@ class TrackedShapeAttributeVal(AttributeVal): class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) rating = models.FloatField(default=0.0) + last_activity_date = models.DateTimeField(null=True, blank=True, default=None) has_analytics_access = models.BooleanField( _("has access to analytics"), default=False, diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 87f65f5f0724..5b8323cc7472 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -195,6 +195,7 @@ def generate_secret_key(): "django.contrib.auth.middleware.AuthenticationMiddleware", "django.middleware.gzip.GZipMiddleware", "cvat.apps.engine.middleware.RequestTrackingMiddleware", + "cvat.apps.engine.middleware.LastActivityMiddleware", "crum.CurrentRequestUserMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", @@ -787,3 +788,5 @@ class CVAT_QUEUES(Enum): "cron_string": "0 8 * * *", } ) + +USER_LAST_ACTIVITY_UPDATE_MIN_INTERVAL = timedelta(days=1)