Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ build/
dist/
.vscode/
ai_aside.egg-info/
.DS_Store
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ Change Log
Unreleased
**********

3.1.0 – 2023-07-20
**********************************************

Features
=========

* Added API endpoints for updating settings for courses and modules (enable/disable for now) (Has migrations)

3.0.1 – 2023-07-20
**********************************************

Expand Down
4 changes: 3 additions & 1 deletion ai_aside/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
A plugin containing xblocks and apps supporting GPT and other LLM use on edX.
"""

__version__ = '3.0.1'
__version__ = '3.1.0'

default_app_config = "ai_aside.apps.AiAsideConfig"
Empty file added ai_aside/api/__init__.py
Empty file.
17 changes: 17 additions & 0 deletions ai_aside/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
implements a simple REST API for updating unit and course settings
"""
from django.urls import re_path

from ai_aside.api.views import CourseEnabledAPIView, UnitEnabledAPIView
from ai_aside.constants import COURSE_ID_PATTERN, UNIT_ID_PATTERN

urlpatterns = [
re_path(r'^v1/{course_id}/?$'.format(
course_id=COURSE_ID_PATTERN
), CourseEnabledAPIView.as_view(), name='api-course-settings'),
re_path(r'^v1/{course_id}/{unit_id}/?$'.format(
course_id=COURSE_ID_PATTERN,
unit_id=UNIT_ID_PATTERN
), UnitEnabledAPIView.as_view(), name='api-unit-settings'),
]
191 changes: 191 additions & 0 deletions ai_aside/api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"""
Implements a simple REST API for updating unit and course settings.

Setters:
POST: ai_aside/v1/:course_id - (payload: { enabled: True/False })
POST: ai_aside/v1/:course_id/:unit_id - (payload: { enabled: True/False })

Getters:
GET: ai_aside/v1/:course_id - (response: { success: True/False, enabled: True/False })
GET: ai_aside/v1/:course_id/:unit_id - (response: { success: True/False, enabled: True/False })

Delete:
DELETE: ai_aside/v1/:course_id - (response: { success: True/False })
DELETE: ai_aside/v1/:course_id/:unit_id - (response: { success: True/False })

Both GET and DELETE methods respond with a 404 if the setting cannot be found.
"""
import ast

from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from ai_aside.models import AIAsideCourseEnabled, AIAsideUnitEnabled


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 CourseEnabledAPIView(APIView):
"""Handlers for course level settings"""

def get(self, request, course_id=None):
"""Gets the enabled state for a course"""
if course_id is None:
return APIResponse(http_status=status.HTTP_404_NOT_FOUND)

try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
data = {'message': 'Invalid Key'}
return APIResponse(http_status=status.HTTP_400_BAD_REQUEST, data=data)

try:
record = AIAsideCourseEnabled.objects.get(
course_key=course_key,
)
except AIAsideCourseEnabled.DoesNotExist:
return APIResponse(http_status=status.HTTP_404_NOT_FOUND)

enabled = record.enabled

data = {'enabled': enabled}
return APIResponse(success=True, data=data)

def post(self, request, course_id=None):
"""Sets the enabled state for a course"""
if (enabledStr := request.data.get('enabled')) is None:
data = {'message': 'Invalid parameters'}
return APIResponse(http_status=status.HTTP_400_BAD_REQUEST, data=data)

enabled = ast.literal_eval(enabledStr)

if course_id is None:
return APIResponse(http_status=status.HTTP_404_NOT_FOUND)

try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
data = {'message': 'Invalid Key'}
return APIResponse(http_status=status.HTTP_400_BAD_REQUEST, data=data)

AIAsideCourseEnabled.objects.update_or_create(
course_key=course_key,
defaults={'enabled': enabled}
)

data = {'enabled': enabled}

return APIResponse(success=True, data=data)

def delete(self, request, course_id=None):
"""Deletes the settings for a module"""
if course_id is None:
return APIResponse(http_status=status.HTTP_404_NOT_FOUND)

try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
data = {'message': 'Invalid Key'}
return APIResponse(http_status=status.HTTP_400_BAD_REQUEST, data=data)

try:
AIAsideCourseEnabled.objects.get(
course_key=course_key,
).delete()
except AIAsideCourseEnabled.DoesNotExist:
return APIResponse(http_status=status.HTTP_404_NOT_FOUND)

return APIResponse(success=True)


class UnitEnabledAPIView(APIView):
"""Handlers for module level settings"""
def get(self, request, course_id=None, unit_id=None):
"""Gets the enabled state for a module"""
if course_id is None or unit_id is None:
return APIResponse(http_status=status.HTTP_404_NOT_FOUND)

try:
course_key = CourseKey.from_string(course_id)
unit_key = UsageKey.from_string(unit_id)
except InvalidKeyError:
data = {'message': 'Invalid Key'}
return APIResponse(http_status=status.HTTP_400_BAD_REQUEST, data=data)

try:
record = AIAsideUnitEnabled.objects.get(
course_key=course_key,
unit_key=unit_key,
)
except AIAsideUnitEnabled.DoesNotExist:
return APIResponse(http_status=status.HTTP_404_NOT_FOUND)

if record is None:
return APIResponse(http_status=status.HTTP_404_NOT_FOUND)

enabled = record.enabled

data = {'enabled': enabled}
return APIResponse(success=True, data=data)

def post(self, request, course_id=None, unit_id=None):

"""Sets the enabled state for a module"""
if course_id is None or unit_id is None:
return APIResponse(http_status=status.HTTP_404_NOT_FOUND)

if (enabledStr := request.data.get('enabled')) is None:
data = {'message': 'Invalid parameters'}
return APIResponse(http_status=status.HTTP_400_BAD_REQUEST, data=data)

enabled = ast.literal_eval(enabledStr)

try:
course_key = CourseKey.from_string(course_id)
unit_key = UsageKey.from_string(unit_id)
except InvalidKeyError:
data = {'message': 'Invalid Key'}
return APIResponse(http_status=status.HTTP_400_BAD_REQUEST, data=data)

AIAsideUnitEnabled.objects.update_or_create(
course_key=course_key,
unit_key=unit_key,
defaults={'enabled': enabled}
)

data = {'enabled': enabled}

return APIResponse(success=True, data=data)

def delete(self, request, course_id=None, unit_id=None):
"""Deletes the settings for a module"""
if course_id is None or unit_id is None:
return APIResponse(http_status=status.HTTP_404_NOT_FOUND)

try:
course_key = CourseKey.from_string(course_id)
unit_key = UsageKey.from_string(unit_id)
except InvalidKeyError:
data = {'message': 'Invalid Key'}
return APIResponse(http_status=status.HTTP_400_BAD_REQUEST, data=data)

try:
AIAsideUnitEnabled.objects.get(
course_key=course_key,
unit_key=unit_key,
).delete()
except AIAsideUnitEnabled.DoesNotExist:
return APIResponse(http_status=status.HTTP_404_NOT_FOUND)

return APIResponse(success=True)
7 changes: 7 additions & 0 deletions ai_aside/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ class AiAsideConfig(AppConfig):

name = 'ai_aside'
plugin_app = {
'url_config': {
'lms.djangoapp': {
'namespace': 'ai_aside',
'regex': '^ai_aside/',
'relative_path': 'urls',
},
},
PluginSettings.CONFIG: {
'lms.djangoapp': {
'common': {
Expand Down
9 changes: 9 additions & 0 deletions ai_aside/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Constants for AI-Aside."""

# Regex for Course ID URL patterns
COURSE_ID_REGEX = r'[^/+]+(/|\+)[^/+]+(/|\+)[^/]+'
COURSE_ID_PATTERN = r'(?P<course_id>{})'.format(COURSE_ID_REGEX)

# Regex for Usage ID URL patterns (Unit IDs)
UNIT_ID_REGEX = r'(?:i4x://?[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+)'
UNIT_ID_PATTERN = r'(?P<unit_id>{})'.format(UNIT_ID_REGEX)
31 changes: 31 additions & 0 deletions ai_aside/migrations/0002_auto_20230720_1544.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 3.2.20 on 2023-07-20 15:44

from django.db import migrations
import opaque_keys.edx.django.models


class Migration(migrations.Migration):

dependencies = [
('ai_aside', '0001_initial'),
]

operations = [
migrations.AlterModelOptions(
name='aiasidecourseenabled',
options={},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to figure out why it was doing this and it must be removing the default sort 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, probably

),
migrations.AlterModelOptions(
name='aiasideunitenabled',
options={},
),
migrations.AlterField(
model_name='aiasidecourseenabled',
name='course_key',
field=opaque_keys.edx.django.models.CourseKeyField(db_index=True, max_length=255, unique=True),
),
migrations.AlterUniqueTogether(
name='aiasideunitenabled',
unique_together={('course_key', 'unit_key')},
),
]
11 changes: 3 additions & 8 deletions ai_aside/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,12 @@ class AIAsideCourseEnabled(models.Model):
Maps Course Key to enabled boolean.
"""

course_key = CourseKeyField(db_index=True, max_length=255)
course_key = CourseKeyField(db_index=True, max_length=255, unique=True)
enabled = models.BooleanField(default=False, null=False)

created = models.DateTimeField(auto_now_add=True, db_index=True)
modified = models.DateTimeField(auto_now=True)

class Meta:
"""Default order."""

ordering = ['-created']

def __str__(self):
"""Query."""
return (
Expand Down Expand Up @@ -50,9 +45,9 @@ class AIAsideUnitEnabled(models.Model):
modified = models.DateTimeField(auto_now=True)

class Meta:
"""Default order."""
"""Course and unit are unique together."""

ordering = ['-created']
unique_together = ('course_key', 'unit_key')

def __str__(self):
"""Query."""
Expand Down
13 changes: 8 additions & 5 deletions ai_aside/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""
URLs for ai_aside.
"""
from django.urls import re_path # pylint: disable=unused-import
from django.urls import path, re_path # pylint: disable=unused-import
from django.views.generic import TemplateView # pylint: disable=unused-import

urlpatterns = [
# TODO: Fill in URL patterns and views here.
# re_path(r'', TemplateView.as_view(template_name="ai_aside/base.html")),
]
from ai_aside.api.urls import urlpatterns as apipatterns

app_name = 'ai_aside'

urlpatterns = []

urlpatterns += apipatterns
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Django # Web application framework
edx_django_utils
xblock
edx-opaque-keys
djangorestframework
8 changes: 6 additions & 2 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,21 @@ asgiref==3.7.2
# via django
cffi==1.15.1
# via pynacl
click==8.1.3
click==8.1.6
# via edx-django-utils
django==3.2.20
# via
# -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
# -r requirements/base.in
# django-crum
# djangorestframework
# edx-django-utils
django-crum==0.7.9
# via edx-django-utils
django-waffle==3.0.0
# via edx-django-utils
djangorestframework==3.14.0
# via -r requirements/base.in
edx-django-utils==5.5.0
# via -r requirements/base.in
edx-opaque-keys==2.3.0
Expand Down Expand Up @@ -49,8 +52,9 @@ python-dateutil==2.8.2
pytz==2023.3
# via
# django
# djangorestframework
# xblock
pyyaml==6.0
pyyaml==6.0.1
# via xblock
six==1.16.0
# via
Expand Down
Loading