Skip to content

Commit

Permalink
feat: add patch endpoint to update exams (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
alangsto authored Jun 15, 2022
1 parent 2b52dc6 commit 7a2a28c
Show file tree
Hide file tree
Showing 24 changed files with 828 additions and 28 deletions.
9 changes: 9 additions & 0 deletions edx_exams/apps/api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
""" Permissions for edx-exams API"""
from rest_framework.permissions import BasePermission


class StaffUserPermissions(BasePermission):
""" Permission class to check if user is staff """

def has_permission(self, request, view):
return request.user.is_staff
47 changes: 43 additions & 4 deletions edx_exams/apps/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,43 @@
# Serializers that can be shared across multiple versions of the API
# should be created here. As the API evolves, serializers may become more
# specific to a particular version of the API. In this case, the serializers
# in question should be moved to versioned sub-package.
"""
Serializers for the edx-exams API
"""
from rest_framework import serializers

from edx_exams.apps.core.exam_types import EXAM_TYPES
from edx_exams.apps.core.models import Exam


class ExamSerializer(serializers.ModelSerializer):
"""
Serializer for the Exam Model
"""

exam_name = serializers.CharField(required=True)
course_id = serializers.CharField(required=False)
content_id = serializers.CharField(required=True)
time_limit_mins = serializers.IntegerField(required=True)
due_date = serializers.DateTimeField(required=False, format=None)
exam_type = serializers.CharField(required=True)
hide_after_due = serializers.BooleanField(required=True)
is_active = serializers.BooleanField(required=True)

class Meta:
"""
Meta Class
"""

model = Exam

fields = (
"id", "exam_name", "course_id", "content_id", "time_limit_mins", "due_date", "exam_type",
"hide_after_due", "is_active",
)

def validate_exam_type(self, value):
"""
Validate that exam_type is one of the predefined choices
"""
valid_exam_types = [exam_type.name for exam_type in EXAM_TYPES]
if value not in valid_exam_types:
raise serializers.ValidationError("Must be a valid exam type.")
return value
59 changes: 59 additions & 0 deletions edx_exams/apps/api/test_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
Test Utilities
"""

from rest_framework.test import APIClient, APITestCase

from edx_exams.apps.api.test_utils.factories import UserFactory
from edx_exams.apps.api.test_utils.mixins import JwtMixin
from edx_exams.apps.core.models import ProctoringProvider

TEST_USERNAME = 'api_worker'
TEST_EMAIL = '[email protected]'
TEST_PASSWORD = 'QWERTY'


class ExamsAPITestCase(JwtMixin, APITestCase):
"""
Base class for API Tests
"""

def setUp(self):
"""
Perform operations common to all tests.
"""
super().setUp()
self.create_user(username=TEST_USERNAME, email=TEST_EMAIL, password=TEST_PASSWORD, is_staff=True)
self.client = APIClient()
self.client.login(username=TEST_USERNAME, password=TEST_PASSWORD)

self.test_provider = ProctoringProvider.objects.create(
name='test_provider',
verbose_name='testing_provider',
lti_configuration_id='123456789'
)

def tearDown(self):
"""
Perform common tear down operations to all tests.
"""
# Remove client authentication credentials
self.client.logout()
super().tearDown()

def create_user(self, username=TEST_USERNAME, password=TEST_PASSWORD, is_staff=False, **kwargs):
"""
Create a test user and set its password.
"""
self.user = UserFactory(username=username, is_active=True, is_staff=is_staff, **kwargs)
self.user.set_password(password)
self.user.save()

def build_jwt_headers(self, user):
"""
Set jwt token in cookies.
"""
jwt_payload = self.default_payload(user)
jwt_token = self.generate_token(jwt_payload)
headers = {"HTTP_AUTHORIZATION": "JWT " + jwt_token}
return headers
30 changes: 30 additions & 0 deletions edx_exams/apps/api/test_utils/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Factories for exams tests
"""

import factory
from django.contrib.auth import get_user_model
from factory.django import DjangoModelFactory


class UserFactory(DjangoModelFactory):
"""
Factory to create users to be used in other unit tests
"""

class Meta:
model = get_user_model()
django_get_or_create = (
"email",
"username",
)

_DEFAULT_PASSWORD = "test"

username = factory.Sequence("user{}".format)
email = factory.Sequence("user+test+{}@edx.org".format)
password = factory.PostGenerationMethodCall("set_password", _DEFAULT_PASSWORD)
first_name = factory.Sequence("User{}".format)
last_name = "Test"
is_superuser = False
is_staff = False
50 changes: 50 additions & 0 deletions edx_exams/apps/api/test_utils/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
Mixins for edx-exams API tests.
"""
from time import time

import jwt
from django.conf import settings

JWT_AUTH = "JWT_AUTH"


class JwtMixin:
"""
Mixin with JWT-related helper functions
"""

JWT_SECRET_KEY = getattr(settings, JWT_AUTH)["JWT_SECRET_KEY"]
JWT_ISSUER = getattr(settings, JWT_AUTH)["JWT_ISSUER"]
JWT_AUDIENCE = getattr(settings, JWT_AUTH)["JWT_AUDIENCE"]

def generate_token(self, payload, secret=None):
"""
Generate a JWT token with the provided payload
"""
secret = secret or self.JWT_SECRET_KEY
token = jwt.encode(payload, secret)
return token

def default_payload(self, user, ttl=1):
"""
Generate a bare payload, in case tests need to manipulate
it directly before encoding
"""
now = int(time())

return {
"iss": self.JWT_ISSUER,
"sub": user.pk,
"aud": self.JWT_AUDIENCE,
"nonce": "dummy-nonce",
"exp": now + ttl,
"iat": now,
"preferred_username": user.username,
"administrator": user.is_staff,
"email": user.email,
"locale": "en",
"name": user.full_name,
"given_name": "",
"family_name": "",
}
2 changes: 1 addition & 1 deletion edx_exams/apps/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@

app_name = 'api'
urlpatterns = [
path(r'^v1/', include(v1_urls)),
path('v1/', include(v1_urls)),
]
Loading

0 comments on commit 7a2a28c

Please sign in to comment.