Skip to content

Commit

Permalink
feat: add studio write access validation
Browse files Browse the repository at this point in the history
  • Loading branch information
germanolleunlp committed Sep 4, 2023
1 parent 2dc69b0 commit 582c157
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 115 deletions.
17 changes: 9 additions & 8 deletions ai_aside/config_api/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""
Implements an API for updating unit and course settings.
"""
from ai_aside.config_api.internal import NotFoundError, _get_course, _get_course_units, _get_unit
from ai_aside.config_api.exceptions import AiAsideNotFoundException
from ai_aside.config_api.internal import _get_course, _get_course_units, _get_unit
from ai_aside.models import AIAsideCourseEnabled, AIAsideUnitEnabled
from ai_aside.waffle import summaries_configuration_enabled

Expand All @@ -28,7 +29,7 @@ def set_course_settings(course_key, settings):
Expects: settings to be a dictionary of the form:
`{'enabled': bool}`
Raises NotFoundError if the settings are not found.
Raises AiAsideNotFoundException if the settings are not found.
"""
enabled = settings['enabled']

Expand All @@ -47,7 +48,7 @@ def delete_course_settings(course_key):
"""
Deletes the settings of a course.
Raises NotFoundError if the settings are not found.
Raises AiAsideNotFoundException if the settings are not found.
"""
reset_course_unit_settings(course_key)
record = _get_course(course_key)
Expand Down Expand Up @@ -84,7 +85,7 @@ def set_unit_settings(course_key, unit_key, settings):
Expects: settings as a dictionary of the form:
`{'enabled': bool}`
Raises NotFoundError if the settings are not found.
Raises AiAsideNotFoundException if the settings are not found.
"""
enabled = settings['enabled']

Expand All @@ -104,7 +105,7 @@ def delete_unit_settings(course_key, unit_key):
"""
Deletes the settings of a unit.
Raises NotFoundError if the settings are not found.
Raises AiAsideNotFoundException if the settings are not found.
"""
record = _get_unit(course_key, unit_key)
record.delete()
Expand All @@ -127,7 +128,7 @@ def is_course_settings_present(course_key):
try:
course = _get_course(course_key)
return course is not None
except NotFoundError:
except AiAsideNotFoundException:
return False


Expand All @@ -147,12 +148,12 @@ def is_summary_enabled(course_key, unit_key=None):

if unit is not None:
return unit.enabled
except NotFoundError:
except AiAsideNotFoundException:
pass

try:
course = _get_course(course_key)
except NotFoundError:
except AiAsideNotFoundException:
return False

if course is not None:
Expand Down
18 changes: 18 additions & 0 deletions ai_aside/config_api/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
Custom exceptions for ai-aside
"""
from rest_framework import status


class AiAsideException(Exception):
"""
A common base class for all exceptions
"""
http_status = status.HTTP_400_BAD_REQUEST


class AiAsideNotFoundException(AiAsideException):
"""
A 404 exception class
"""
http_status = status.HTTP_404_NOT_FOUND
13 changes: 5 additions & 8 deletions ai_aside/config_api/internal.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
"""
Internal methods for the API.
"""
from ai_aside.config_api.exceptions import AiAsideNotFoundException
from ai_aside.models import AIAsideCourseEnabled, AIAsideUnitEnabled


class NotFoundError(Exception):
"Raised when the course/unit is not found in the database"


def _get_course(course_key):
"Private method that gets a course based on an id"
try:
record = AIAsideCourseEnabled.objects.get(
course_key=course_key,
)
except AIAsideCourseEnabled.DoesNotExist as exc:
raise NotFoundError from exc
except AIAsideCourseEnabled.DoesNotExist as error:
raise AiAsideNotFoundException from error

return record

Expand All @@ -27,8 +24,8 @@ def _get_unit(course_key, unit_key):
course_key=course_key,
unit_key=unit_key,
)
except AIAsideUnitEnabled.DoesNotExist as exc:
raise NotFoundError from exc
except AIAsideUnitEnabled.DoesNotExist as error:
raise AiAsideNotFoundException from error

return record

Expand Down
20 changes: 20 additions & 0 deletions ai_aside/config_api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
""" Permissions for ai-aside API"""

from rest_framework.permissions import BasePermission

from ai_aside.config_api.validators import validate_course_key
from ai_aside.platform_imports import can_change_summaries_settings


class HasStudioWriteAccess(BasePermission):
"""
Check if the user has studio write access to a course.
"""
def has_permission(self, request, view):
"""
Check permissions for this class.
"""
course_key_string = view.kwargs.get('course_id')
course_key = validate_course_key(course_key_string)

return can_change_summaries_settings(request.user, course_key)
32 changes: 32 additions & 0 deletions ai_aside/config_api/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
Utilities related to API views
"""

from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey

from ai_aside.config_api.exceptions import AiAsideException


def validate_course_key(course_key_string: str) -> CourseKey:
"""
Validate and parse a course_key string, if supported.
"""
try:
course_key = CourseKey.from_string(course_key_string)
except InvalidKeyError as error:
raise AiAsideException(f"{course_key_string} is not a valid CourseKey") from error
if course_key.deprecated:
raise AiAsideException("Deprecated CourseKeys (Org/Course/Run) are not supported.")
return course_key


def validate_unit_key(unit_key_string: str) -> UsageKey:
"""
Validate and parse a unit_key string, if supported.
"""
try:
usage_key = UsageKey.from_string(unit_key_string)
except InvalidKeyError as error:
raise AiAsideException(f"{unit_key_string} is not a valid UsageKey") from error
return usage_key
45 changes: 45 additions & 0 deletions ai_aside/config_api/view_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
Config API Utilities
"""
import logging

from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from ai_aside.config_api.exceptions import AiAsideException

log = logging.getLogger(__name__)


def handle_ai_aside_exception(exc, name=None): # pylint: disable=inconsistent-return-statements
"""
Converts ai_aside exceptions into restframework responses
"""
if isinstance(exc, AiAsideException):
log.exception(name)
return APIResponse(http_status=exc.http_status, data={'message': str(exc)})


class APIResponse(Response):
"""API Response"""
def __init__(self, data=None, http_status=None, content_type=None, success=False):
_status = http_status or status.HTTP_200_OK
data = data or {}
reply = {'response': {'success': success}}
reply['response'].update(data)
super().__init__(data=reply, status=_status, content_type=content_type)


class AiAsideAPIView(APIView):
"""
Overrides APIView to handle ai_aside exceptions
"""
def handle_exception(self, exc):
"""
Converts ai-aside exceptions into standard restframework responses
"""
resp = handle_ai_aside_exception(exc, name=self.__class__.__name__)
if not resp:
resp = super().handle_exception(exc)
return resp
Loading

0 comments on commit 582c157

Please sign in to comment.