Skip to content

Commit

Permalink
OpenConceptLab/ocl_issues#852 | monthly usage report under admin/repo…
Browse files Browse the repository at this point in the history
…rt/ namespace
  • Loading branch information
snyaggarwal committed Jul 27, 2021
1 parent f41e0de commit 79af082
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 158 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ omit =
core/asgi.py
core/celery.py
core/common/healthcheck/*
core/reports/*
core/wsgi.py
*/v1_dump/*
*/management/commands/*
Expand Down
4 changes: 4 additions & 0 deletions core/reports/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from django.conf import settings


__version__ = settings.VERSION
173 changes: 173 additions & 0 deletions core/reports/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
from dateutil.relativedelta import relativedelta
from django.db.models import Count, F
from django.db.models.functions import TruncMonth
from django.utils import timezone

from core.collections.models import Collection, CollectionReference
from core.common.constants import HEAD
from core.concepts.models import Concept
from core.mappings.models import Mapping
from core.orgs.models import Organization
from core.sources.models import Source
from core.users.models import UserProfile


class MonthlyUsageReport:
def __init__(self, verbose=False, start=None, end=None):
self.verbose = verbose
self.start = start
self.end = end
self.resources = []
self.result = dict()
self.make_resources()

def make_resources(self):
self.resources.append(UserReport(start=self.start, end=self.end, verbose=self.verbose))
self.resources.append(OrganizationReport(start=self.start, end=self.end, verbose=self.verbose))
self.resources.append(SourceReport(start=self.start, end=self.end, verbose=self.verbose))
self.resources.append(CollectionReport(start=self.start, end=self.end, verbose=self.verbose))
if self.verbose:
self.resources.append(SourceVersionReport(start=self.start, end=self.end, verbose=self.verbose))
self.resources.append(CollectionVersionReport(start=self.start, end=self.end, verbose=self.verbose))
self.resources.append(CollectionReferenceReport(start=self.start, end=self.end, verbose=self.verbose))
self.resources.append(ConceptReport(start=self.start, end=self.end, verbose=self.verbose))
self.resources.append(MappingReport(start=self.start, end=self.end, verbose=self.verbose))

def prepare(self):
for resource in self.resources:
self.result[resource.resource] = resource.get_monthly_report()


class ResourceReport:
queryset = None
resource = None

def __init__(self, start=None, end=None, verbose=False):
self.verbose = verbose
now = timezone.now()
self.start = start or (now - relativedelta(months=6))
self.end = end or now
self.total = 0
self.active = 0
self.inactive = 0
self.created_monthly_distribution = None
self.result = dict()
self.set_date_range()

@staticmethod
def get_active_filter(active=True):
return dict(retired=not active)

def set_date_range(self):
self.queryset = self.queryset.filter(created_at__gte=self.start, created_at__lte=self.end)

def set_total(self):
self.total = self.queryset.count()

def set_active(self):
self.active = self.queryset.filter(**self.get_active_filter()).count()

def set_inactive(self):
self.inactive = self.queryset.filter(**self.get_active_filter(False)).count()

def set_created_monthly_distribution(self):
self.created_monthly_distribution = self.get_distribution()

def get_distribution(self, date_attr='created_at', count_by='id'):
return self.queryset.annotate(
month=TruncMonth(date_attr)
).filter(
month__gte=self.start, month__lte=self.end
).values('month').annotate(total=Count(count_by)).values('month', 'total').order_by('-month')

def get_monthly_report(self):
self.set_total()
self.set_created_monthly_distribution()

self.result = dict(
total=self.total, created_monthly=self.format_distribution(self.created_monthly_distribution)
)
if self.resource not in ['collection_references']:
self.set_active()
self.set_inactive()
self.result['active'] = self.active
self.result['inactive'] = self.inactive
return self.result

@staticmethod
def format_distribution(queryset):
formatted = list()
for item in queryset:
month = item['month']
if month:
result = dict()
result[item['month'].strftime('%b %Y')] = item['total']
formatted.append(result)

return formatted


class UserReport(ResourceReport):
queryset = UserProfile.objects
resource = 'users'

def __init__(self, start=None, end=None, verbose=False):
super().__init__(start, end, verbose)
self.last_login_monthly_distribution = None

@staticmethod
def get_active_filter(active=True):
return dict(is_active=active)

def set_last_login_monthly_distribution(self):
self.last_login_monthly_distribution = self.get_distribution('last_login')

def get_monthly_report(self):
self.result = super().get_monthly_report()
self.set_last_login_monthly_distribution()
self.result['last_login_monthly'] = self.format_distribution(self.last_login_monthly_distribution)
return self.result


class OrganizationReport(ResourceReport):
queryset = Organization.objects
resource = 'organizations'

@staticmethod
def get_active_filter(active=True):
return dict(is_active=active)


class SourceReport(ResourceReport):
queryset = Source.objects.filter(version=HEAD)
resource = 'sources'


class CollectionReport(ResourceReport):
queryset = Collection.objects.filter(version=HEAD)
resource = 'collections'


class SourceVersionReport(ResourceReport):
queryset = Source.objects.exclude(version=HEAD)
resource = 'source_versions'


class CollectionVersionReport(ResourceReport):
queryset = Collection.objects.exclude(version=HEAD)
resource = 'collection_versions'


class CollectionReferenceReport(ResourceReport):
queryset = CollectionReference.objects.filter(collections__version=HEAD)
resource = 'collection_references'


class ConceptReport(ResourceReport):
queryset = Concept.objects.filter(id=F('versioned_object_id'))
resource = 'concepts'


class MappingReport(ResourceReport):
queryset = Mapping.objects.filter(id=F('versioned_object_id'))
resource = 'mappings'
19 changes: 19 additions & 0 deletions core/reports/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response

from core.common.views import BaseAPIView
from core.reports.models import MonthlyUsageReport


class MonthlyUsageView(BaseAPIView): # pragma: no cover
permission_classes = (IsAdminUser, )

def get(self, request):
report = MonthlyUsageReport(
verbose=self.is_verbose(),
start=request.query_params.get('start', None),
end=request.query_params.get('end', None)
)
report.prepare()

return Response(report.result)
2 changes: 2 additions & 0 deletions core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from core.common.utils import get_api_base_url
from core.common.views import RootView, FeedbackView, APIVersionView, ChangeLogView
from core.importers.views import BulkImportView
import core.reports.views as report_views

SchemaView = get_schema_view(
openapi.Info(
Expand All @@ -48,6 +49,7 @@
url(r'^redoc/$', SchemaView.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
path('healthcheck/', include('core.common.healthcheck.urls')),
path('admin/', admin.site.urls, name='admin_urls'),
path('admin/reports/monthly-usage/', report_views.MonthlyUsageView.as_view(), name='monthly-usage-report'),
path('users/', include('core.users.urls'), name='users_urls'),
path('user/', include('core.users.user_urls'), name='current_user_urls'),
path('orgs/', include('core.orgs.urls'), name='orgs_url'),
Expand Down
142 changes: 0 additions & 142 deletions core/users/models.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
from datetime import datetime

from dateutil.relativedelta import relativedelta
from django.contrib import admin
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Count, F
from django.db.models.functions import TruncMonth
from django.urls import reverse
from rest_framework.authtoken.models import Token

from core.common.constants import HEAD
from core.common.mixins import SourceContainerMixin
from core.common.models import BaseModel, CommonLogoModel
from core.common.tasks import send_user_verification_email, send_user_reset_password_email
Expand Down Expand Up @@ -152,140 +146,4 @@ def auth_groups(self):
return self.groups.values_list('name', flat=True)


class UserReport: # pragma: no cover
def __init__(self, verbose=False, start=None, end=None):
self.verbose = verbose
now = datetime.now()
self.start = start or (now - relativedelta(months=6))
self.end = end or now
self.total = 0
self.active = 0
self.inactive = 0
self.joining_monthly_distribution = None
self.last_login_monthly_distribution = None
self.organizations_created_by_month = None
self.sources_created_by_month = None
self.source_versions_created_by_month = None
self.collections_created_by_month = None
self.collection_versions_created_by_month = None
self.collection_references_created_by_month = None
self.concepts_created_by_month = None
self.mappings_created_by_month = None
self.result = dict()
self.queryset = self.set_date_range(UserProfile.objects)

def set_date_range(self, queryset):
return queryset.filter(created_at__gte=self.start, created_at__lte=self.end)

def set_total(self):
self.total = self.queryset.count()

def set_active(self):
self.active = self.queryset.filter(is_active=True).count()

def set_inactive(self):
self.inactive = self.queryset.filter(is_active=False).count()

def set_joining_monthly_distribution(self):
self.joining_monthly_distribution = self.get_distribution(self.queryset, 'created_at', 'username')

def set_last_login_monthly_distribution(self):
self.last_login_monthly_distribution = self.get_distribution(self.queryset, 'last_login', 'username')

def set_mappings_created_by_month(self):
from core.mappings.models import Mapping
self.mappings_created_by_month = self.get_distribution(Mapping.objects.filter(id=F('versioned_object_id')))

def set_concepts_created_by_month(self):
from core.concepts.models import Concept
self.concepts_created_by_month = self.get_distribution(Concept.objects.filter(id=F('versioned_object_id')))

def set_collections_created_by_month(self):
from core.collections.models import Collection
self.collections_created_by_month = self.get_distribution(Collection.objects.filter(version=HEAD))

def set_sources_created_by_month(self):
from core.sources.models import Source
self.sources_created_by_month = self.get_distribution(Source.objects.filter(version=HEAD))

def set_source_versions_created_by_month(self):
from core.sources.models import Source
self.source_versions_created_by_month = self.get_distribution(Source.objects.exclude(version=HEAD))

def set_collection_versions_created_by_month(self):
from core.collections.models import Collection
self.collection_versions_created_by_month = self.get_distribution(Collection.objects.exclude(version=HEAD))

def set_collection_references_created_by_month(self):
from core.collections.models import CollectionReference
self.collection_references_created_by_month = self.get_distribution(
CollectionReference.objects.filter(collections__version=HEAD))

def set_organizations_created_by_month(self):
from core.orgs.models import Organization
self.organizations_created_by_month = self.get_distribution(Organization.objects)

def get_distribution(self, queryset, date_attr='created_at', count_by='id'):
return self.set_date_range(queryset).annotate(
month=TruncMonth(date_attr)
).filter(
month__gte=self.start, month__lte=self.end
).values('month').annotate(total=Count(count_by)).values('month', 'total').order_by('-month')

@staticmethod
def __format_distribution(queryset):
formatted = list()
for item in queryset:
month = item['month']
if month:
result = dict()
result[item['month'].strftime('%b %Y')] = item['total']
formatted.append(result)

return formatted

def prepare(self):
self.set_total()
self.set_active()
self.set_inactive()
self.set_joining_monthly_distribution()
self.set_last_login_monthly_distribution()
self.set_organizations_created_by_month()
self.set_sources_created_by_month()
self.set_collections_created_by_month()
if self.verbose:
self.set_source_versions_created_by_month()
self.set_collection_versions_created_by_month()
self.set_collection_references_created_by_month()
self.set_concepts_created_by_month()
self.set_mappings_created_by_month()

def make_result(self):
self.result = dict(
total=self.total,
active=self.active,
inactive=self.inactive,
new_users=self.__format_distribution(self.joining_monthly_distribution),
users_last_login=self.__format_distribution(self.last_login_monthly_distribution),
new_organizations=self.__format_distribution(self.organizations_created_by_month),
new_sources=self.__format_distribution(self.sources_created_by_month),
new_collections=self.__format_distribution(self.collections_created_by_month),
)
if self.verbose:
self.result['new_source_versions'] = self.__format_distribution(
self.source_versions_created_by_month)
self.result['new_collection_versions'] = self.__format_distribution(
self.collection_versions_created_by_month)
self.result['new_collection_references'] = self.__format_distribution(
self.collection_references_created_by_month)
self.result['new_concepts'] = self.__format_distribution(
self.concepts_created_by_month)
self.result['new_mappings'] = self.__format_distribution(
self.mappings_created_by_month)

def generate(self):
self.prepare()
self.make_result()


admin.site.register(UserProfile)
1 change: 0 additions & 1 deletion core/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
re_path(r'^$', views.UserListView.as_view(), name='userprofile-list'),
url('login/', views.TokenAuthenticationView.as_view(), name='user-login'),
url('signup/', views.UserSignup.as_view(), name='user-signup'),
url('report/', views.UserReportView.as_view(), name='user-report'),
re_path(
r'^(?P<user>' + NAMESPACE_PATTERN + ')/$',
views.UserDetailView.as_view(),
Expand Down

0 comments on commit 79af082

Please sign in to comment.