Skip to content

Commit

Permalink
feat: learners-count statistics
Browse files Browse the repository at this point in the history
  • Loading branch information
shadinaif committed Mar 12, 2024
1 parent 03d5b4c commit 375adc9
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 3 deletions.
3 changes: 0 additions & 3 deletions futurex_openedx_extensions/dashboard/models.py

This file was deleted.

34 changes: 34 additions & 0 deletions futurex_openedx_extensions/dashboard/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Permissions for the dashboard app"""
from rest_framework.permissions import BasePermission

from futurex_openedx_extensions.dashboard.utils import get_accessible_tenants


class OnlyAccessibleTenants(BasePermission):
"""Validate that the user has access to the tenant IDs provided in the request"""
def has_permission(self, request, view):
"""Validation logic for the permission class"""
tenant_ids = request.query_params.get('tenant_ids')
if not tenant_ids:
# No tenant_ids provided
return False
user = request.user
accessible_tenants = get_accessible_tenants(user)

# Extract tenant_ids from query parameters
if tenant_ids:
tenant_ids = [int(id) for id in tenant_ids.split(',')]
else:
# No tenant_ids provided
self.message = 'No tenant IDs provided.'
return False

# Check which tenant_ids are not in the accessible tenants list
inaccessible_tenants = [str(tid) for tid in tenant_ids if tid not in accessible_tenants]

if inaccessible_tenants:
# Generate a custom message with the inaccessible tenant IDs
self.message = f'User does not have access to tenant IDs: {", ".join(inaccessible_tenants)}.'
return False

return True
Empty file.
12 changes: 12 additions & 0 deletions futurex_openedx_extensions/dashboard/statistics/learners.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""functions for getting statistics about learners"""
from __future__ import annotations
from typing import List


def get_learners_count(tenant_ids: List[int]) -> int:
"""
Get the count of learners in a tenant.
:param tenant_ids: List of tenant IDs to get the count for
"""

15 changes: 15 additions & 0 deletions futurex_openedx_extensions/dashboard/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Views for the dashboard app"""
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status

from futurex_openedx_extensions.helpers.mixins import TenantsCheckMixin


class TotalCountsView(TenantsCheckMixin):
"""View to get the total count statistics"""
def get(self, request, *args, **kwargs):
"""Get the total count statistics"""

# Proceed with your view logic if the permission check passes
return Response({"message": "Access granted"})
9 changes: 9 additions & 0 deletions futurex_openedx_extensions/helpers/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""helpers Django application initialization"""

from django.apps import AppConfig


class HelpersConfig(AppConfig):
"""Configuration for the helpers Django application"""

name = 'helpers'
10 changes: 10 additions & 0 deletions futurex_openedx_extensions/helpers/convertrers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Type conversion helpers"""
from __future__ import annotations
from typing import List


def ids_string_to_list(ids_string: str) -> List[int]:
"""Convert a comma-separated string of ids to a list of integers"""
if not ids_string:
return []
return [int(id_value.strip()) for id_value in ids_string.split(",") if id_value.strip()]
17 changes: 17 additions & 0 deletions futurex_openedx_extensions/helpers/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Base views to be used by other views"""

from django.http import JsonResponse
from rest_framework.views import APIView
from rest_framework import status
from futurex_openedx_extensions.helpers.tenants import check_tenant_access


class TenantsCheckMixin(APIView):
"""Mixin for views that require tenant information"""
def dispatch(self, request, *args, **kwargs):
"""Check if the user has access to the tenant IDs provided"""
has_access, details = check_tenant_access(request.user, request.query_params.get('tenant_ids'))

if not has_access:
return JsonResponse(details, status=status.HTTP_403_FORBIDDEN)
return super().dispatch(request, *args, **kwargs)
68 changes: 68 additions & 0 deletions futurex_openedx_extensions/helpers/tenants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Tenant management helpers"""
from __future__ import annotations

from django.contrib.auth import get_user_model
from eox_tenant.models import Route
from typing import List
from futurex_openedx_extensions.helpers.convertrers import ids_string_to_list
from django.db.models import Count



def get_all_tenants() -> List[int]:
"""
TODO: Cache the result of this function
Get all tenants in the system that have at least one organization associated with them
:return: List of all tenant IDs
:rtype: List[int]
"""
return Route.objects.annotate(
orgs_count=Count('config__organizations')
).filter(
orgs_count__gt=0
).values_list('id', flat=True)


def get_accessible_tenants(user: get_user_model()) -> List[int]:
"""
Get the tenants that the user has access to.
:param user: The user to check.
:type user: get_user_model()
:return: List of accessible tenant IDs
:rtype: List[int]
"""
if user.is_superuser or user.is_staff:
return get_all_tenants()
return []


def check_tenant_access(user: get_user_model(), tenant_ids_string: str) -> tuple[bool, dict]:
"""
Check if the user has access to the tenant IDs provided.
:param user: The user to check.
:type user: get_user_model()
:param tenant_ids_string: Comma-separated string of tenant IDs
:type tenant_ids_string: str
:return: Tuple of a boolean indicating if the user has access and a dictionary with details if access is denied
"""
try:
tenant_ids = ids_string_to_list(tenant_ids_string)
except ValueError as e:
return False, {"reason": "Invalid tenant IDs provided", "details": {
"error": str(e)
}}

accessible_tenants = get_accessible_tenants(user)
inaccessible_tenants = [t_id for t_id in tenant_ids if t_id not in accessible_tenants]

if inaccessible_tenants:
return False, {
"reason": "user does not have access to these tenants",
"details": {"tenant_ids": inaccessible_tenants}
}

return True, {}
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def is_requirement(line):
include=[
'futurex_openedx_extensions', 'futurex_openedx_extensions.*',
'futurex_openedx_extensions.dashboard', 'futurex_openedx_extensions.dashboard.*',
'futurex_openedx_extensions.helpers', 'futurex_openedx_extensions.helpers.*',
],
exclude=["*tests"],
),
Expand All @@ -160,6 +161,7 @@ def is_requirement(line):
entry_points={
'lms.djangoapp': [
'dashboard = futurex_openedx_extensions.dashboard.apps:DashboardConfig',
'helpers = futurex_openedx_extensions.helpers.apps:HelpersConfig',
],
},
)

0 comments on commit 375adc9

Please sign in to comment.