diff --git a/.flake8 b/.flake8 index 79a16af7e..fe70d6365 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,3 @@ [flake8] -max-line-length = 120 \ No newline at end of file +max-line-length = 120 +extend-ignore = F403 \ No newline at end of file diff --git a/api/resources/login.py b/api/resources/login.py index ad8e79f8b..d7252d57e 100644 --- a/api/resources/login.py +++ b/api/resources/login.py @@ -69,10 +69,8 @@ def obj_create(self, bundle, **kwargs): tracker = Tracker() tracker.user = u tracker.type = 'login' - tracker.ip = bundle.request.META.get('REMOTE_ADDR', - DEFAULT_IP_ADDRESS) - tracker.agent = bundle.request.META.get('HTTP_USER_AGENT', - 'unknown') + tracker.ip = bundle.request.META.get('REMOTE_ADDR', DEFAULT_IP_ADDRESS) + tracker.agent = bundle.request.META.get('HTTP_USER_AGENT', 'unknown') tracker.save() else: raise BadRequest(_(u'Authentication failure')) @@ -82,15 +80,17 @@ def obj_create(self, bundle, **kwargs): bundle.data['api_key'] = key.key try: - up = UserProfile.objects.get(user__username=username) - job_title = up.job_title - organisation = up.organisation + profile = UserProfile.objects.get(user=bundle.obj) + bundle.data['job_title'] = profile.job_title + bundle.data['organisation'] = profile.organisation + + customfields = profile.get_customfields_dict() + bundle.data.update(customfields) + except UserProfile.DoesNotExist: - job_title = "" - organisation = "" + bundle.data['job_title'] = '' + bundle.data['organisation'] = '' - bundle.data['job_title'] = job_title - bundle.data['organisation'] = organisation bundle.obj = u return bundle diff --git a/api/resources/profile.py b/api/resources/profile.py index 4c0975f5b..a649e9dde 100644 --- a/api/resources/profile.py +++ b/api/resources/profile.py @@ -1,6 +1,7 @@ from django.contrib.auth.forms import SetPasswordForm from django.contrib.auth.models import User from django.utils.translation import gettext_lazy as _ +from django.conf import settings from tastypie import fields from tastypie.authentication import ApiKeyAuthentication from tastypie.authorization import Authorization @@ -10,9 +11,11 @@ from api.serializers import UserJSONSerializer from api.utils import check_required_params from datarecovery.models import DataRecovery -from oppia.models import Participant +from oppia.models import Participant, Points, Award from profile.forms import ProfileForm from profile.models import UserProfile, CustomField +from settings.models import SettingProperties +from settings import constants class ProfileUpdateResource(ModelResource): @@ -125,6 +128,78 @@ def get_list(self, request, **kwargs): return self.create_response(request, cohorts) +class UserProfileResource(ModelResource): + + badges = fields.IntegerField(readonly=True) + course_points = fields.CharField(readonly=True) + cohorts = fields.CharField(readonly=True) + scoring = fields.BooleanField(readonly=True) + badging = fields.BooleanField(readonly=True) + metadata = fields.CharField(readonly=True) + points = fields.IntegerField(readonly=True) + + class Meta: + queryset = User.objects.all() + resource_name = 'profile' + allowed_methods = ['get'] + authorization = Authorization() + authentication = ApiKeyAuthentication() + always_return_data = True + include_resource_uri = False + fields = ['first_name', + 'last_name', + 'username', + 'email', + 'job_title', + 'organisation'] + + def dispatch(self, request_type, request, **kwargs): + # Force this to be a single User object + return super().dispatch('detail', request, **kwargs) + + def obj_get(self, bundle, **kwargs): + return bundle.request.user + + def dehydrate(self, bundle): + bundle = super().dehydrate(bundle) + + try: + profile = UserProfile.objects.get(user=bundle.obj) + bundle.data['job_title'] = profile.job_title + bundle.data['organisation'] = profile.organisation + + customfields = profile.get_customfields_dict() + bundle.data.update(customfields) + + except UserProfile.DoesNotExist: + bundle.data['job_title'] = '' + bundle.data['organisation'] = '' + + return bundle + + def dehydrate_cohorts(self, bundle): + return Participant.get_user_cohorts(bundle.request.user) + + def dehydrate_metadata(self, bundle): + return settings.OPPIA_METADATA + + def dehydrate_points(self, bundle): + return Points.get_userscore(bundle.request.user) + + def dehydrate_badges(self, bundle): + return Award.get_userawards(bundle.request.user) + + def dehydrate_scoring(self, bundle): + return SettingProperties.get_bool( + constants.OPPIA_POINTS_ENABLED, + settings.OPPIA_POINTS_ENABLED) + + def dehydrate_badging(self, bundle): + return SettingProperties.get_bool( + constants.OPPIA_BADGES_ENABLED, + settings.OPPIA_BADGES_ENABLED) + + class ChangePasswordResource(ModelResource): ''' For resetting user password diff --git a/api/urls.py b/api/urls.py index 6012d8e18..49526805d 100644 --- a/api/urls.py +++ b/api/urls.py @@ -11,7 +11,8 @@ from api.resources.course import CourseResource, CourseStructureResource from api.resources.login import UserResource as UserResource from api.resources.points import PointsResource -from api.resources.profile import ProfileUpdateResource, ChangePasswordResource, UserCohortsResource +from api.resources.profile import ProfileUpdateResource, ChangePasswordResource, UserCohortsResource, \ + UserProfileResource from api.resources.v1.register import RegisterResource as RegisterResourceV1 from api.resources.v2.register import RegisterResource as RegisterResourceV2 from api.resources.reset_password import ResetPasswordResource @@ -60,6 +61,7 @@ def get_api_v2(): api.register(DownloadDataResource()) api.register(ChangePasswordResource()) api.register(UserCohortsResource()) + api.register(UserProfileResource()) return api diff --git a/av/views.py b/av/views.py index 5e93c3ceb..24f8dad30 100644 --- a/av/views.py +++ b/av/views.py @@ -11,9 +11,8 @@ from av.models import UploadedMedia from helpers.mixins.AjaxTemplateResponseMixin import AjaxTemplateResponseMixin from helpers.mixins.ListItemUrlMixin import ListItemUrlMixin - from oppia.models import Media, Course -from oppia.permissions import can_view_course +from oppia.permissions import permission_view_course STR_UPLOAD_MEDIA = _(u'Upload Media') @@ -63,8 +62,9 @@ def download_media_file(request, media_id): return response +@permission_view_course def download_course_media(request, course_id): - course = can_view_course(request, course_id) + course = get_object_or_404(Course, pk=course_id) media = Media.objects.filter(course=course) uploaded = UploadedMedia.objects.filter( md5__in=media.values_list('digest', flat=True)) diff --git a/oppia/__init__.py b/oppia/__init__.py index c83d17d1e..202ccdb75 100644 --- a/oppia/__init__.py +++ b/oppia/__init__.py @@ -1,5 +1,5 @@ import warnings -VERSION = (0, 14, 9, 'core', '1', 'release') +VERSION = (0, 14, 10, 'core', '1', 'release') DEFAULT_IP_ADDRESS = None warnings.simplefilter('default') diff --git a/oppia/mixins/PermissionMixins.py b/oppia/mixins/PermissionMixins.py index e17e3c9b0..5ccaa510a 100644 --- a/oppia/mixins/PermissionMixins.py +++ b/oppia/mixins/PermissionMixins.py @@ -3,7 +3,26 @@ from oppia.models import Participant, Course -class CanViewUserDetailsPermissionMixin(LoginRequiredMixin, UserPassesTestMixin): +class ObjectPermissionRequiredMixin(LoginRequiredMixin): + """ + Checks that the current user has permission to access this object, raising a PermissionDenied if not. + For this, the object to check against is obtained from the self.object attribute from SingleObjectMixin, + so this mixin must be put **after** the view that inherits from that one + """ + + def has_object_permission(self, obj): + raise NotImplementedError( + '{} is missing the implementation of the has_object_permission() method.'.format(self.__class__.__name__) + ) + + def get_object(self, *args, **kwargs): + obj = super().get_object(*args, **kwargs) + if not self.has_object_permission(obj): + return self.handle_no_permission() + return obj + + +class CanViewUserDetailsMixin(LoginRequiredMixin, UserPassesTestMixin): """ Verify that the current user can view details of another user. For this, the other user pk is get from the URL kwargs. If it is not defined with the SingleObjectMixin `pk_url_kwarg`, the specific kwarg @@ -31,3 +50,11 @@ def test_func(self): coursecohort__cohort__participant__role=Participant.TEACHER) \ return courses_teached_by.exists() + + +class CanEditUserMixin(ObjectPermissionRequiredMixin): + """ + Verify that the current user can edit the current view user. + """ + def has_object_permission(self, obj): + return self.request.user.is_staff or (self.request.user.id == obj.pk) diff --git a/oppia/models/main.py b/oppia/models/main.py index 13f2bf27d..9eac2af76 100644 --- a/oppia/models/main.py +++ b/oppia/models/main.py @@ -179,6 +179,29 @@ def get_no_trackers(self): # ------------ Permissions management ---------------- + def user_can_view(self, user): + if user.is_staff: + return True + + if self.status is CourseStatus.ARCHIVED: + return False + + if self.status != CourseStatus.DRAFT: + return True + + if user.is_anonymous: + return False + + try: + Course.objects.get( + pk=self.pk, + coursepermissions__course=self, + coursepermissions__user=user, + coursepermissions__role=CoursePermissions.VIEWER) + return True + except Course.DoesNotExist: + return False + def user_can_view_detail(self, user): if user.is_staff: return True diff --git a/oppia/models/points.py b/oppia/models/points.py index df3944c51..e62e6627c 100644 --- a/oppia/models/points.py +++ b/oppia/models/points.py @@ -81,7 +81,6 @@ def get_leaderboard_filtered(request_user, .order_by('-points') # check if there's going to be overlap or not - print(users_points.count()) if users_points.count() <= (count_top + above + below + 1): return Points.get_leaderboard_top(users_points, users_points.count()) diff --git a/oppia/permissions.py b/oppia/permissions.py index 5a976dae8..3dd257805 100644 --- a/oppia/permissions.py +++ b/oppia/permissions.py @@ -2,7 +2,6 @@ import functools from itertools import chain -from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied from django.http import Http404, HttpResponseForbidden from django.shortcuts import get_object_or_404 @@ -38,6 +37,7 @@ def can_edit_user(request, view_user_id): else: return False + def get_user_courses(request, view_user): if request.user.is_staff or request.user == view_user: @@ -69,44 +69,47 @@ def is_manager_only(user): if user.is_staff: return False else: - courses = Course.objects.filter(user=user).count() - if courses > 0: + courses = Course.objects.filter(user=user) + if courses.exists(): return True courses = Course.objects.filter( coursepermissions__user=user, - coursepermissions__role=CoursePermissions.MANAGER).count() - if courses > 0: + coursepermissions__role=CoursePermissions.MANAGER) + if courses.exists(): return True return False -def can_add_cohort(request): - if request.user.is_staff: - return True - return False - - -def can_edit_cohort(request): - if request.user.is_staff: - return True - return False - +def permission_edit_cohort(view_func): + """ + this decorator ensures that only the users who have permission to + view a course can view it, raising a 403 otherwise + """ + @functools.wraps(view_func) + def wrapper(request, *args, **kwargs): + if not request.user.is_staff: + raise PermissionDenied + return view_func(request, *args, **kwargs) + return wrapper -def can_view_cohort(request, cohort_id): - try: - cohort = Cohort.objects.get(pk=cohort_id) - except Cohort.DoesNotExist: - raise Http404 - try: - if request.user.is_staff: - return cohort - return Cohort.objects.get(pk=cohort_id, - participant__user=request.user, - participant__role=Participant.TEACHER) - except Cohort.DoesNotExist: - raise PermissionDenied +def permission_view_cohort(view_func): + """ + this decorator ensures that only the users who have permission to + view a course can view it, raising a 403 otherwise + """ + @functools.wraps(view_func) + def wrapper(request, *args, **kwargs): + get_object_or_404(Cohort, pk=kwargs['cohort_id']) + if not request.user.is_staff: + cohort = Cohort.objects.filter(pk=kwargs['cohort_id'], + participant__user=request.user, + participant__role=Participant.TEACHER) + if not cohort.exists(): + raise PermissionDenied + return view_func(request, *args, **kwargs) + return wrapper def get_cohorts(request): @@ -123,26 +126,6 @@ def get_cohorts(request): return cohorts -def can_view_course(request, course_id): - try: - if request.user.is_staff: - course = Course.objects.get(pk=course_id) - else: - try: - course = Course.objects.filter(CourseFilter.IS_NOT_ARCHIVED).get(pk=course_id) - except Course.DoesNotExist: - course = Course.objects \ - .filter(CourseFilter.IS_NOT_ARCHIVED) \ - .get( - pk=course_id, - coursepermissions__course__id=course_id, - coursepermissions__user__id=request.user.id, - coursepermissions__role=CoursePermissions.VIEWER) - except Course.DoesNotExist: - raise Http404 - return course - - def can_download_course(request, course_id): try: if request.user.is_staff: @@ -168,10 +151,24 @@ def can_download_course(request, course_id): return course +def permission_view_course(view_func): + """ + this decorator ensures that only the users who have permission to + view a course can view it, raising a 403 otherwise + """ + @functools.wraps(view_func) + def wrapper(request, *args, **kwargs): + course = get_object_or_404(Course, pk=kwargs['course_id']) + if not course.user_can_view(request.user): + raise PermissionDenied + return view_func(request, *args, **kwargs) + return wrapper + + def permission_view_course_detail(view_func): """ this decorator ensures that only the users who have permission to - access a course can view it, raising a 403 otherwise + access a course detail can view it, raising a 403 otherwise """ @functools.wraps(view_func) def wrapper(request, *args, **kwargs): diff --git a/oppia/urls.py b/oppia/urls.py index e8e955b8f..10e5a09b4 100644 --- a/oppia/urls.py +++ b/oppia/urls.py @@ -44,10 +44,10 @@ path('cohort/', views.CohortListView.as_view(), name="cohorts"), path('cohort/add/', views.AddCohortView.as_view(), name="cohort_add"), - path('cohort//edit/', views.CohortEditView.as_view(), name="cohort_edit"), - path('cohort//view/', views.CohortDetailView.as_view(), name="cohort_view"), + path('cohort//edit/', views.CohortEditView.as_view(), name="cohort_edit"), + path('cohort//view/', views.CohortDetailView.as_view(), name="cohort_view"), path('cohort///view/', views.cohort_course_view, name="cohort_course_view"), - path('cohort//leaderboard', views.CohortLeaderboardView.as_view(), name="cohort_leaderboard"), + path('cohort//leaderboard', views.CohortLeaderboardView.as_view(), name="cohort_leaderboard"), path('certificate/preview//', views.PreviewCertificateView.as_view(), diff --git a/oppia/views/activity.py b/oppia/views/activity.py index f08e64755..454c17827 100644 --- a/oppia/views/activity.py +++ b/oppia/views/activity.py @@ -20,6 +20,7 @@ from profile import utils from summary.models import CourseDailyStats, UserCourseSummary + @method_decorator(permission_view_course_detail, name='dispatch') class CourseActivityDetail(DateRangeFilterMixin, DetailView): template_name = 'course/detail.html' @@ -63,6 +64,7 @@ def get_activity(self, start_date, end_date, interval): return generate_graph_data(monthly_stats, True) + @method_decorator(permission_view_course_detail, name='dispatch') class CourseActivityDetailList(DateRangeFilterMixin, SafePaginatorMixin, ListView): template_name = 'course/activity/list.html' @@ -104,7 +106,6 @@ def get_context_data(self, **kwargs): return context - class ExportCourseTrackers(TemplateView): @method_decorator(permission_view_course_detail) diff --git a/oppia/views/certificate.py b/oppia/views/certificate.py index 2451120cd..e0106a382 100644 --- a/oppia/views/certificate.py +++ b/oppia/views/certificate.py @@ -1,10 +1,9 @@ import datetime import uuid -from django.forms import ValidationError +from django.core.exceptions import ValidationError from django.http import FileResponse -from django.shortcuts import render -from django.views.generic import TemplateView +from django.views.generic import TemplateView, DetailView from oppia.badges.certificates import generate_certificate_pdf from oppia.models import Award, Course @@ -25,17 +24,27 @@ def get(self, request, certificate_template_id): return FileResponse(buffer, filename='certificate.pdf') -class ValidateCertificateView(TemplateView): +class ValidateCertificateView(DetailView): + pk_url_kwarg = 'validation_uuid' + model = Award - def get(self, request, validation_uuid): + def get_object(self, queryset=None): try: - award = Award.objects.get(validation_uuid=validation_uuid) - except (Award.DoesNotExist, ValidationError): - return render(request, - 'oppia/certificates/invalid.html', - {'validation_uuid': validation_uuid}) - course = Course.objects.filter(awardcourse__award=award).first() - return render(request, - 'oppia/certificates/valid.html', - {'award': award, - 'course': course}) + return Award.objects.filter(validation_uuid=self.kwargs['validation_uuid']).first() + except ValidationError: + return None + + def get_template_names(self): + if self.object: + return 'oppia/certificates/valid.html' + else: + return 'oppia/certificates/invalid.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.object: + context['course'] = Course.objects.filter(awardcourse__award=self.object).first() + context['award'] = self.object + else: + context['validation_uuid'] = self.kwargs['validation_uuid'] + return context diff --git a/oppia/views/cohort.py b/oppia/views/cohort.py index 9be1df354..1dd9c8a8d 100644 --- a/oppia/views/cohort.py +++ b/oppia/views/cohort.py @@ -2,12 +2,12 @@ import datetime import operator -from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.models import User from django.http import Http404 from django.shortcuts import render from django.urls import reverse_lazy, reverse from django.utils import timezone +from django.utils.decorators import method_decorator from django.views.generic import ListView, UpdateView, DetailView from django.views.generic.edit import FormView @@ -22,9 +22,7 @@ Participant, \ Course, \ Cohort, CohortCritera -from oppia.permissions import can_add_cohort, \ - can_view_cohort, \ - can_edit_cohort +from oppia.permissions import permission_view_cohort, permission_edit_cohort from oppia.views.utils import get_paginated_courses, filter_trackers from profile.models import CustomField from profile.utils import get_paginated_users @@ -70,7 +68,7 @@ def get_context_data(self, **kwargs): return context def get_named_formsets(self): - return{ + return { 'student_criteria': {'form': CohortCriteriaForm, 'kwargs': {'prefix': 'student'}}, 'teacher_criteria': {'form': CohortCriteriaForm, 'kwargs': {'prefix': 'teacher'}}, } @@ -107,15 +105,12 @@ def save_criteria(self, criteria_form, role): ) -class AddCohortView(FormView, UserPassesTestMixin, EditCohortMixin): +@method_decorator(permission_edit_cohort, name='dispatch') +class AddCohortView(FormView, EditCohortMixin): template_name = STR_COHORT_TEMPLATE_FORM success_url = reverse_lazy('oppia:cohorts') form_class = CohortForm - # Permissions check - def test_func(self): - return can_add_cohort(self.request) - def form_valid(self, form): cohort = Cohort( description=form.cleaned_data.get("description").strip(), @@ -150,14 +145,12 @@ def get_context_data(self, **kwargs): return context -class CohortDetailView(UserPassesTestMixin, DetailView): +@method_decorator(permission_view_cohort, name='dispatch') +class CohortDetailView(DetailView): template_name = 'cohort/activity.html' model = Cohort context_object_name = 'cohort' - - # Permissions check - def test_func(self): - return can_view_cohort(self.request, self.kwargs['pk']) + pk_url_kwarg = 'cohort_id' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -179,21 +172,15 @@ def get_context_data(self, **kwargs): return context -class CohortLeaderboardView(UserPassesTestMixin, - SafePaginatorMixin, - ListView, - AjaxTemplateResponseMixin): +@method_decorator(permission_view_cohort, name='dispatch') +class CohortLeaderboardView(SafePaginatorMixin, ListView, AjaxTemplateResponseMixin): paginate_by = constants.LEADERBOARD_TABLE_RESULTS_PER_PAGE template_name = 'cohort/leaderboard.html' ajax_template_name = 'leaderboard/query.html' - # Permissions check - def test_func(self): - return can_view_cohort(self.request, self.kwargs['pk']) - def get(self, request, *args, **kwargs): - self.object = Cohort.objects.get(pk=kwargs['pk']) + self.object = Cohort.objects.get(pk=kwargs['cohort_id']) return super().get(request, *args, **kwargs) def get_queryset(self): @@ -205,14 +192,12 @@ def get_context_data(self, **kwargs): return context -class CohortEditView(UserPassesTestMixin, UpdateView, EditCohortMixin): +@method_decorator(permission_edit_cohort, name='dispatch') +class CohortEditView(UpdateView, EditCohortMixin): template_name = STR_COHORT_TEMPLATE_FORM model = Cohort form_class = CohortForm - - # Permissions check - def test_func(self): - return can_edit_cohort(self.request) + pk_url_kwarg = 'cohort_id' def get_form_kwargs(self): """Return the keyword arguments for instantiating the form.""" @@ -307,9 +292,9 @@ def form_valid(self, form): return super().form_valid(form) +@permission_view_cohort def cohort_course_view(request, cohort_id, course_id): - cohort = can_view_cohort(request, cohort_id) - + cohort = Cohort.objects.get(pk=cohort_id) try: course = Course.objects.get(pk=course_id, coursecohort__cohort=cohort) except Course.DoesNotExist: diff --git a/oppia/views/course.py b/oppia/views/course.py index fbb4d983b..26cb91302 100644 --- a/oppia/views/course.py +++ b/oppia/views/course.py @@ -1,7 +1,7 @@ import os from django.conf import settings -from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.contrib.auth.mixins import UserPassesTestMixin from django.http import HttpResponseRedirect, HttpResponse from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ @@ -108,7 +108,7 @@ def get(self, request, course_id): return response -class CanUploadCoursePermission(LoginRequiredMixin, UserPassesTestMixin): +class CanUploadCoursePermission(UserPassesTestMixin): def test_func(self): return can_upload(self.request.user) @@ -136,7 +136,7 @@ def form_valid(self, form): return super().form_invalid(form) -class CanEditCoursePermission(LoginRequiredMixin, UserPassesTestMixin): +class CanEditCoursePermission(UserPassesTestMixin): def test_func(self): return can_edit_course(self.request, self.kwargs[self.pk_url_kwarg]) diff --git a/oppia/views/feedback.py b/oppia/views/feedback.py index dbec01130..0295236bc 100644 --- a/oppia/views/feedback.py +++ b/oppia/views/feedback.py @@ -9,6 +9,7 @@ from oppia.permissions import permission_view_course_detail from quiz.models import QuizAttempt, Quiz, QuizProps + @method_decorator(permission_view_course_detail, name='dispatch') class CourseFeedbackActivitiesList(ListView, ListItemUrlMixin, @@ -40,6 +41,7 @@ def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) + @method_decorator(permission_view_course_detail, name='dispatch') class CourseFeedbackResponsesList(ListView, ListItemUrlMixin, AjaxTemplateResponseMixin): diff --git a/oppia/views/home.py b/oppia/views/home.py index 6ca21b82b..84b983ff4 100644 --- a/oppia/views/home.py +++ b/oppia/views/home.py @@ -7,7 +7,7 @@ from django.core.exceptions import PermissionDenied from django.db.models import Count, Sum from django.db.models.functions import TruncDay, TruncMonth, TruncYear -from django.http import HttpResponseRedirect, Http404 +from django.http import HttpResponseRedirect from django.urls import reverse from django.utils import timezone from django.views.generic import TemplateView, ListView @@ -231,18 +231,15 @@ def get_context_data(self, **kwargs): context['course_shortname'] = course_shortname course = Course.objects.filter(shortname=course_shortname).first() if course: - try: - permissions.can_view_course(self.request, course.id) + if course.user_can_view(self.request.user): context['course'] = course context['download_stats'] = UserCourseSummary.objects \ .filter(course=course) \ .aggregated_stats('total_downloads', single=True) - - except Http404: + else: # The user does not have permissions to view this course context['misconfigured'] = True context['course_notpermissions'] = True - else: context['misconfigured'] = True context['course_notfound'] = True diff --git a/profile/mixins/ExportAsCSVMixin.py b/profile/mixins/ExportAsCSVMixin.py index 509338ab4..ac5ef51e1 100644 --- a/profile/mixins/ExportAsCSVMixin.py +++ b/profile/mixins/ExportAsCSVMixin.py @@ -61,7 +61,7 @@ def get_field_label_from_attributes(self, field_name): if field_name in self.field_labels: return self.field_labels[field_name] return None - + def export_csv(self, request, object_list, filter_list=None, *args, **kwargs): now = datetime.datetime.now() diff --git a/profile/models.py b/profile/models.py index 9617cb53a..25ec32b5b 100644 --- a/profile/models.py +++ b/profile/models.py @@ -75,6 +75,16 @@ def update_customfields(self, fields_dict): return errors + def get_customfields_dict(self): + profile_fields = {} + custom_fields = CustomField.objects.all() + for custom_field in custom_fields: + value = UserProfileCustomField.get_user_value(self.user, custom_field) + if value is not None: + profile_fields[custom_field.id] = value + + return profile_fields + class CustomField(models.Model): diff --git a/profile/urls.py b/profile/urls.py index 839cfed85..776d00ccd 100644 --- a/profile/urls.py +++ b/profile/urls.py @@ -23,14 +23,14 @@ path('points/', profile_views.PointsView.as_view(), name="points"), path('badges/', profile_views.BadgesView.as_view(), name="badges"), - path('/activity/', profile_views.UserScorecard.as_view(), name="user_activity"), + path('/activity/', profile_views.UserScorecardDetails.as_view(), name="user_activity"), path('/activity/detail/', - profile_views.UserActivityDetailList.as_view(), + profile_views.UserActivityDetailListDetails.as_view(), name="user_activity_detail"), path('//activity/', - profile_views.UserCourseScorecard.as_view(), + profile_views.UserCourseScorecardDetails.as_view(), name="user_course_activity"), - path('/quizattempts/', profile_views.UserAttemptsList.as_view(), name="user_all_attempts"), + path('/quizattempts/', profile_views.UserAttemptsListDetails.as_view(), name="user_all_attempts"), path('//quiz//attempts/', profile_views.QuizAttemptsList.as_view(), name="user_quiz_attempts"), @@ -38,7 +38,7 @@ profile_views.QuizAttemptDetail.as_view(), name="quiz_attempt_detail"), path('/feedback/', - profile_views.UserFeedbackResponsesList.as_view(), + profile_views.UserFeedbackResponsesListDetails.as_view(), name="user_all_feedback_responses"), path('/regeneratecertificates/', profile_views.RegenerateCertificatesView.as_view(), diff --git a/profile/views/activity.py b/profile/views/activity.py index 6a409a4fd..6bddf50ad 100644 --- a/profile/views/activity.py +++ b/profile/views/activity.py @@ -4,14 +4,16 @@ from django.contrib.auth.models import User from django.db.models import Max, Min, Avg +from django.shortcuts import get_object_or_404 +from django.utils.decorators import method_decorator from django.views.generic import ListView, DetailView from helpers.mixins.DateRangeFilterMixin import DateRangeFilterMixin from helpers.mixins.SafePaginatorMixin import SafePaginatorMixin from oppia.forms.activity_search import ActivitySearchForm -from oppia.mixins.PermissionMixins import CanViewUserDetailsPermissionMixin -from oppia.models import Activity, Tracker -from oppia.permissions import get_user_courses, can_view_course, can_view_course_activity +from oppia.mixins.PermissionMixins import CanViewUserDetailsMixin +from oppia.models import Activity, Tracker, Course +from oppia.permissions import get_user_courses, can_view_course_activity, permission_view_course from oppia.views import filter_trackers from quiz.models import Quiz, QuizAttempt, QuizProps from summary.models import UserCourseSummary @@ -26,7 +28,7 @@ def get_tracker_activities(user, course_ids=[], course=None): return trackers.filter(user=user) -class UserScorecard(CanViewUserDetailsPermissionMixin, DateRangeFilterMixin, DetailView): +class UserScorecardDetails(CanViewUserDetailsMixin, DateRangeFilterMixin, DetailView): template_name = 'profile/user-scorecard.html' context_object_name = 'view_user' pk_url_kwarg = 'user_id' @@ -73,7 +75,8 @@ def get_context_data(self, **kwargs): return context -class UserCourseScorecard(CanViewUserDetailsPermissionMixin, DateRangeFilterMixin, DetailView): +@method_decorator(permission_view_course, name='dispatch') +class UserCourseScorecardDetails(CanViewUserDetailsMixin, DateRangeFilterMixin, DetailView): template_name = 'profile/user-course-scorecard.html' context_object_name = 'view_user' pk_url_kwarg = 'user_id' @@ -81,8 +84,7 @@ class UserCourseScorecard(CanViewUserDetailsPermissionMixin, DateRangeFilterMixi def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - course = can_view_course(self.request, self.kwargs['course_id']) - + course = get_object_or_404(Course, pk=self.kwargs['course_id']) act_quizzes = Activity.objects \ .filter(section__course=course, type=Activity.QUIZ) \ .order_by('section__order', 'order') @@ -198,7 +200,7 @@ def process_quiz_activity(view_user, return quiz, course_pretest, quizzes_attempted, quizzes_passed -class UserActivityDetailList(CanViewUserDetailsPermissionMixin, DateRangeFilterMixin, SafePaginatorMixin, ListView): +class UserActivityDetailListDetails(CanViewUserDetailsMixin, DateRangeFilterMixin, SafePaginatorMixin, ListView): template_name = 'profile/activity/list.html' paginate_by = 25 daterange_form_class = ActivitySearchForm @@ -213,6 +215,12 @@ def get_queryset(self): start_date, end_date = self.get_daterange() trackers = trackers.filter(tracker_date__gte=start_date, tracker_date__lte=end_date) + form = self.get_daterange_form() + if form.is_valid(): + act_type = form.cleaned_data.get("type") + if act_type: + trackers = trackers.filter(type=act_type) + return trackers.order_by('-tracker_date') def get_context_data(self, **kwargs): diff --git a/profile/views/feedback.py b/profile/views/feedback.py index d65950745..50146eb12 100644 --- a/profile/views/feedback.py +++ b/profile/views/feedback.py @@ -3,12 +3,12 @@ from helpers.mixins.AjaxTemplateResponseMixin import AjaxTemplateResponseMixin from helpers.mixins.ListItemUrlMixin import ListItemUrlMixin -from oppia.mixins.PermissionMixins import CanViewUserDetailsPermissionMixin +from oppia.mixins.PermissionMixins import CanViewUserDetailsMixin from oppia.models import Course, Activity from quiz.models import QuizAttempt, Quiz -class FeedbackAttemptsList(CanViewUserDetailsPermissionMixin, ListView, ListItemUrlMixin, AjaxTemplateResponseMixin): +class FeedbackAttemptsList(CanViewUserDetailsMixin, ListView, ListItemUrlMixin, AjaxTemplateResponseMixin): model = QuizAttempt @@ -36,9 +36,9 @@ def get_context_data(self, **kwargs): return context -class UserFeedbackResponsesList(CanViewUserDetailsPermissionMixin, ListView, - ListItemUrlMixin, - AjaxTemplateResponseMixin): +class UserFeedbackResponsesListDetails(CanViewUserDetailsMixin, ListView, + ListItemUrlMixin, + AjaxTemplateResponseMixin): model = QuizAttempt objects_url_name = 'oppia:feedback_response_detail' diff --git a/profile/views/quiz.py b/profile/views/quiz.py index 4b6f2c10d..51d25685d 100644 --- a/profile/views/quiz.py +++ b/profile/views/quiz.py @@ -3,12 +3,12 @@ from helpers.mixins.AjaxTemplateResponseMixin import AjaxTemplateResponseMixin from helpers.mixins.ListItemUrlMixin import ListItemUrlMixin -from oppia.mixins.PermissionMixins import CanViewUserDetailsPermissionMixin +from oppia.mixins.PermissionMixins import CanViewUserDetailsMixin from oppia.models import Course, Activity from quiz.models import QuizAttempt, Quiz -class QuizAttemptsList(CanViewUserDetailsPermissionMixin, ListView, ListItemUrlMixin, AjaxTemplateResponseMixin): +class QuizAttemptsList(CanViewUserDetailsMixin, ListView, ListItemUrlMixin, AjaxTemplateResponseMixin): model = QuizAttempt objects_url_name = 'quiz_attempt_detail' @@ -35,7 +35,7 @@ def get_context_data(self, **kwargs): return context -class UserAttemptsList(CanViewUserDetailsPermissionMixin, ListView, ListItemUrlMixin, AjaxTemplateResponseMixin): +class UserAttemptsListDetails(CanViewUserDetailsMixin, ListView, ListItemUrlMixin, AjaxTemplateResponseMixin): model = QuizAttempt objects_url_name = 'quiz_attempt_detail' @@ -59,7 +59,7 @@ def get_context_data(self, **kwargs): return context -class QuizAttemptDetail(CanViewUserDetailsPermissionMixin, DetailView): +class QuizAttemptDetail(CanViewUserDetailsMixin, DetailView): model = QuizAttempt template_name = 'quiz/attempt.html' diff --git a/profile/views/user.py b/profile/views/user.py index 4cf5ce339..ecec49443 100644 --- a/profile/views/user.py +++ b/profile/views/user.py @@ -5,19 +5,18 @@ from django.contrib import messages from django.contrib.auth import authenticate, login from django.contrib.auth.models import User -from django.core.exceptions import PermissionDenied from django.core.management import call_command from django.http import HttpResponseRedirect, Http404 from django.shortcuts import render from django.urls import reverse from django.utils.translation import gettext as _ -from django.views.generic import ListView, UpdateView, FormView, TemplateView +from django.views.generic import ListView, UpdateView, FormView, TemplateView, DetailView from tastypie.models import ApiKey from helpers.mixins.SafePaginatorMixin import SafePaginatorMixin from helpers.mixins.TitleViewMixin import TitleViewMixin +from oppia.mixins.PermissionMixins import CanEditUserMixin from oppia.models import Points, Award, Tracker, Course, CertificateTemplate -from oppia.permissions import can_edit_user from profile.forms import LoginForm, \ RegisterForm, \ ProfileForm, \ @@ -98,12 +97,13 @@ def form_valid(self, form): return super().form_valid(form) -class EditView(UpdateView): +class EditView(CanEditUserMixin, UpdateView): model = User form_class = ProfileForm - context_object_name = 'user' + context_object_name = 'view_user' template_name = 'profile/profile.html' + pk_url_kwarg = 'user_id' def __init__(self, **kwargs): super().__init__(**kwargs) @@ -112,12 +112,8 @@ def __init__(self, **kwargs): settings.OPPIA_ALLOW_PROFILE_EDITING) def get_object(self, queryset=None): - user_id = self.kwargs.get('user_id', ) - if user_id: - if can_edit_user(self.request, user_id): - return User.objects.get(pk=user_id) - else: - raise PermissionDenied + if self.pk_url_kwarg in self.kwargs: + return super().get_object() else: return self.request.user @@ -287,21 +283,30 @@ def get_queryset(self): user=self.request.user).order_by('-award_date') -class RegenerateCertificatesView(TemplateView): +class RegenerateCertificatesView(CanEditUserMixin, DetailView, FormView): + model = User + form_class = RegenerateCertificatesForm + context_object_name = 'user' + template_name = 'profile/certificates/regenerate.html' + pk_url_kwarg = 'user_id' + success_url = 'success/' - def get(self, request, user_id=None): - if user_id: - if can_edit_user(request, user_id): - user = User.objects.get(pk=user_id) - else: - raise PermissionDenied + def get_initial(self): + user = self.get_object() + return { + 'email': user.email, + 'old_email': user.email + } + + def get_object(self, queryset=None): + if self.pk_url_kwarg in self.kwargs: + return super().get_object() else: - user = request.user + return self.request.user - initial = {'email': user.email, - 'old_email': user.email} - form = RegenerateCertificatesForm(initial=initial) - awards = Award.objects.filter(user=user) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + awards = Award.objects.filter(user=self.object) certificates = [] for award in awards: try: @@ -309,41 +314,28 @@ def get(self, request, user_id=None): except Course.DoesNotExist: continue badge = award.badge - certs = CertificateTemplate.objects.filter(course=course, - badge=badge, - enabled=True) - + certs = CertificateTemplate.objects.filter(course=course, badge=badge, enabled=True) for cert in certs: certificate = {} certificate['course'] = course certificate['badge'] = badge - valid, display_name = cert.display_name(user) + valid, display_name = cert.display_name(self.object) certificate['display_name'] = display_name certificate['cert_link'] = award.certificate_pdf certificates.append(certificate) - return render(request, 'profile/certificates/regenerate.html', - {'user': user, - 'form': form, - 'certificates': certificates}) - - def post(self, request, user_id=None): - if user_id: - if can_edit_user(request, user_id): - user = User.objects.get(pk=user_id) - else: - raise PermissionDenied - else: - user = request.user + context['user'] = self.object + context['certificates'] = certificates + return context - # update email address if changed - old_email = request.POST.get("old_email") - new_email = request.POST.get("email") + def form_valid(self, form): + user = self.get_object() + old_email = form.cleaned_data.get("old_email") + new_email = form.cleaned_data.get("email") if old_email != new_email: user.email = new_email user.save() user_command = "--user=" + str(user.id) call_command('generate_certificates', user_command, stdout=StringIO()) - - return HttpResponseRedirect('success/') + return super().form_valid(form) diff --git a/quiz/views.py b/quiz/views.py index 775b409ca..40ee6f949 100644 --- a/quiz/views.py +++ b/quiz/views.py @@ -85,6 +85,7 @@ def get_quiz_data(quiz_id): return data + @permission_view_course_detail def feedback_download(request, course_id, feedback_id): activity = get_object_or_404(Activity, @@ -102,6 +103,7 @@ def feedback_download(request, course_id, feedback_id): return response + @permission_view_course_detail def old_feedback_download(request, course_id, feedback_id): get_object_or_404(Quiz, pk=feedback_id) @@ -114,6 +116,7 @@ def old_feedback_download(request, course_id, feedback_id): return response + @permission_view_course_detail def old_quiz_download(request, course_id, quiz_id): get_object_or_404(Quiz, pk=quiz_id) @@ -126,6 +129,7 @@ def old_quiz_download(request, course_id, quiz_id): return response + @permission_view_course_detail def quiz_download(request, course_id, quiz_id): activity = get_object_or_404(Activity, diff --git a/requirements.txt b/requirements.txt index 045d930d3..5ccff640f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ django-tastypie==0.14.4 django-crispy-forms==1.14.0 pytz defusedxml==0.7.1 -Pillow==9.4.0 +Pillow==9.5.0 sorl-thumbnail==12.9.0 libsass==0.22.0 django-compressor==4.3.1 @@ -11,8 +11,8 @@ django-sass-processor==1.2.2 pycodestyle pytest pytest-django -openpyxl==3.1.1 -tablib==3.3.0 +openpyxl==3.1.2 +tablib==3.4.0 httpretty==1.1.4 xmltodict==0.13.0 django-ses==3.3.0 diff --git a/setup.py b/setup.py index 69c4a53a7..f955f31a8 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name='django-oppia', - version='0.14.7', + version='0.14.10', packages=[ 'oppia', 'quiz', @@ -45,17 +45,17 @@ install_requires=[ "django == 3.2.18", "django-tastypie == 0.14.4", - "tablib == 3.3.0", + "tablib == 3.4.0", "django-crispy-forms == 1.14.0", "pytz", "defusedxml==0.7.1", - "Pillow==9.4.0", + "Pillow==9.5.0", "sorl-thumbnail==12.9.0", "pycodestyle", "pytest", "pytest-django", "django-ses==3.3.0", - "openpyxl==3.1.1", + "openpyxl==3.1.2", "reportlab==3.6.12", "django-compressor==4.3.1", "httpretty==1.1.4", diff --git a/templates/common/form/date-range.html b/templates/common/form/date-range.html index 6898daf47..5aee4cf49 100644 --- a/templates/common/form/date-range.html +++ b/templates/common/form/date-range.html @@ -1,3 +1,4 @@ +{% load i18n %} {% if not omit_form_tag %}
{% if dateform.form_method != 'get' %} @@ -30,7 +31,7 @@ {% endif %} {% if not omit_form_tag %} - + {% endif %} diff --git a/templates/profile/profile.html b/templates/profile/profile.html index f0d625527..f97206b9b 100644 --- a/templates/profile/profile.html +++ b/templates/profile/profile.html @@ -12,17 +12,25 @@
-

Profile

+

+ {% if view_user == request.user %} + Profile + {% else %} + {{ view_user.first_name }} {{ view_user.last_name }} + {% endif %} +

- - {% endblock header %} @@ -70,6 +79,4 @@
{% trans 'Export data' %}
- - {% endblock %} diff --git a/templates/profile/user-scorecard.html b/templates/profile/user-scorecard.html index f721989e5..d24d8ad07 100644 --- a/templates/profile/user-scorecard.html +++ b/templates/profile/user-scorecard.html @@ -11,11 +11,11 @@ {% block header %}
-
+

{{ view_user.first_name }} {{ view_user.last_name }}

{{view_user.username}}
-
+ @@ -50,8 +50,13 @@

{{ view_user.first_name }} {{ view_user.last_name }}

{% trans 'Quiz attempts' %} {% trans 'Feedback answers' %} + {% if view_user == request.user %} + + {% trans 'Points and badges' %} + {% else %} {% trans 'Points and badges' %} + {% endif %}
diff --git a/tests/api/v2/test_activitylog.py b/tests/api/v2/test_activitylog.py index 2befc68ce..15939c0f6 100644 --- a/tests/api/v2/test_activitylog.py +++ b/tests/api/v2/test_activitylog.py @@ -74,8 +74,7 @@ def test_correct_basic_data(self): tracker_count_end = Tracker.objects.all().count() uploaded_count_end = UploadedActivityLog.objects.all().count() - last_uploaded = UploadedActivityLog.objects.all() \ - .order_by('-created_date').first() + last_uploaded = UploadedActivityLog.objects.all().order_by('-created_date').first() self.assertEqual(tracker_count_start + 2, tracker_count_end) self.assertEqual(uploaded_count_start + 1, uploaded_count_end) self.assertEqual(last_uploaded.create_user.username, 'demo') @@ -97,8 +96,7 @@ def test_new_user_file(self): tracker_count_end = Tracker.objects.all().count() user_count_end = User.objects.all().count() uploaded_count_end = UploadedActivityLog.objects.all().count() - last_uploaded = UploadedActivityLog.objects.all() \ - .order_by('-created_date').first() + last_uploaded = UploadedActivityLog.objects.all().order_by('-created_date').first() self.assertEqual(tracker_count_start + 2, tracker_count_end) self.assertEqual(user_count_start + 1, user_count_end) @@ -121,8 +119,7 @@ def test_multiple_users_data(self): tracker_count_end = Tracker.objects.all().count() uploaded_count_end = UploadedActivityLog.objects.all().count() - last_uploaded = UploadedActivityLog.objects.all() \ - .order_by('-created_date').first() + last_uploaded = UploadedActivityLog.objects.all().order_by('-created_date').first() self.assertEqual(tracker_count_start + 4, tracker_count_end) self.assertEqual(uploaded_count_start + 1, uploaded_count_end) self.assertEqual(last_uploaded.create_user.username, 'demo') diff --git a/tests/api/v2/test_award.py b/tests/api/v2/test_award.py index 5dd5fdebe..484dddc6a 100644 --- a/tests/api/v2/test_award.py +++ b/tests/api/v2/test_award.py @@ -44,11 +44,9 @@ def test_unauthorized(self): format='json', data=data)) - # check authorized - def test_authorized(self): - resp = self.api_client.get(self.url, - format='json', - data=self.auth_data) + # check valid + def test_valid(self): + resp = self.api_client.get(self.url, format='json', data=self.auth_data) self.assertHttpOK(resp) self.assertValidJSON(resp.content) self.assertEqual(len(self.deserialize(resp)['objects']), 1) @@ -57,6 +55,19 @@ def test_authorized(self): self.assertTrue('award_date' in award) self.assertTrue('badge_icon' in award) self.assertTrue('description' in award) + self.assertTrue('badge' in award) + self.assertTrue('emailed' in award) + self.assertTrue('id' in award) + self.assertTrue('validation_uuid' in award) + + badge = award['badge'] + self.assertTrue('allow_multiple_awards' in badge) + self.assertTrue('default_icon' in badge) + self.assertTrue('description' in badge) + self.assertTrue('id' in badge) + self.assertTrue('name' in badge) + self.assertTrue('points' in badge) + self.assertTrue('ref' in badge) # check returning a set of objects - expecting zero def test_no_objects(self): diff --git a/tests/api/v2/test_badges.py b/tests/api/v2/test_badges.py index 0ec68aa37..c2fb4ace2 100644 --- a/tests/api/v2/test_badges.py +++ b/tests/api/v2/test_badges.py @@ -22,8 +22,7 @@ def setUp(self): # check post not allowed def test_post_invalid(self): - self.assertHttpMethodNotAllowed( - self.api_client.post(self.url, format='json', data={})) + self.assertHttpMethodNotAllowed(self.api_client.post(self.url, format='json', data={})) # check unauthorized def test_unauthorized(self): @@ -31,18 +30,23 @@ def test_unauthorized(self): 'username': 'demo', 'api_key': '1234', } - self.assertHttpUnauthorized( - self.api_client.get(self.url, format='json', data=data)) + self.assertHttpUnauthorized(self.api_client.get(self.url, format='json', data=data)) # check correct def test_correct(self): - resp = self.api_client.get(self.url, - format='json', - data=self.auth_data) + resp = self.api_client.get(self.url, format='json', data=self.auth_data) self.assertHttpOK(resp) self.assertValidJSON(resp.content) - # check that the response contains 2 badges + # check that the response contains 1 badge response_data = self.deserialize(resp) self.assertTrue('objects' in response_data) self.assertEqual(len(response_data['objects']), 1) + badge = response_data['objects'][0] + self.assertTrue('allow_multiple_awards' in badge) + self.assertTrue('default_icon' in badge) + self.assertTrue('description' in badge) + self.assertTrue('id' in badge) + self.assertTrue('name' in badge) + self.assertTrue('points' in badge) + self.assertTrue('ref' in badge) diff --git a/tests/api/v2/test_category.py b/tests/api/v2/test_category.py index d2c4c59c2..84caab1b2 100644 --- a/tests/api/v2/test_category.py +++ b/tests/api/v2/test_category.py @@ -41,6 +41,16 @@ def assert_valid_response_and_get_tags(self, resp): self.assertValidJSON(resp.content) response_data = self.deserialize(resp) self.assertTrue('tags' in response_data) + category = response_data['tags'][0] + self.assertTrue('count' in category) + self.assertTrue('count_new_downloads_enabled' in category) + self.assertTrue('course_statuses' in category) + self.assertTrue('description' in category) + self.assertTrue('highlight' in category) + self.assertTrue('icon' in category) + self.assertTrue('id' in category) + self.assertTrue('name' in category) + self.assertTrue('order_priority' in category) return response_data['tags'] def get_category_attr_in_results(self, tags, category, attr=None): @@ -48,8 +58,7 @@ def get_category_attr_in_results(self, tags, category, attr=None): # Post invalid def test_post_invalid(self): - self.assertHttpMethodNotAllowed( - self.api_client.post(self.url, format='json', data={})) + self.assertHttpMethodNotAllowed(self.api_client.post(self.url, format='json', data={})) # test unauthorized def test_unauthorized(self): @@ -57,19 +66,16 @@ def test_unauthorized(self): 'username': 'user', 'api_key': '1234', } - self.assertHttpUnauthorized( - self.api_client.get(self.url, format='json', data=data)) + self.assertHttpUnauthorized(self.api_client.get(self.url, format='json', data=data)) # test authorized def test_authorized(self): - resp = self.api_client.get( - self.url, format='json', data=self.user_auth) + resp = self.api_client.get(self.url, format='json', data=self.user_auth) self.assertHttpOK(resp) # test valid json response and with 5 tags def test_has_categories(self): - resp = self.api_client.get( - self.url, format='json', data=self.user_auth) + resp = self.api_client.get(self.url, format='json', data=self.user_auth) tags = self.assert_valid_response_and_get_tags(resp) # should have 5 tags with the test data set @@ -96,13 +102,22 @@ def test_category_list(self): self.assertTrue('courses' in response_data) self.assertTrue('count' in response_data) self.assertTrue('name' in response_data) - self.assertEqual(len(response_data['courses']), - response_data['count']) + self.assertTrue('id' in response_data) + self.assertEqual(len(response_data['courses']), response_data['count']) for course in response_data['courses']: - self.assertTrue('shortname' in course) + self.assertTrue('resource_uri' in course) + self.assertTrue('id' in course) + self.assertTrue('version' in course) self.assertTrue('title' in course) + self.assertTrue('description' in course) + self.assertTrue('shortname' in course) + self.assertTrue('priority' in course) + self.assertTrue('status' in course) + self.assertTrue('restricted' in course) self.assertTrue('url' in course) - self.assertTrue('version' in course) + self.assertTrue('author' in course) + self.assertTrue('username' in course) + self.assertTrue('organisation' in course) # test getting listing of courses for an invalid tag def test_category_not_found(self): diff --git a/tests/api/v2/test_change_password.py b/tests/api/v2/test_change_password.py index 714c136da..7d447c2ed 100644 --- a/tests/api/v2/test_change_password.py +++ b/tests/api/v2/test_change_password.py @@ -22,8 +22,7 @@ def get_credentials(self): # check get not allowed def test_get_invalid(self): - self.assertHttpMethodNotAllowed(self.api_client.get(self.url, - format='json')) + self.assertHttpMethodNotAllowed(self.api_client.get(self.url, format='json')) # check valid password change def test_valid_change(self): diff --git a/tests/api/v2/test_course.py b/tests/api/v2/test_course.py index e772d2530..52e1efb06 100644 --- a/tests/api/v2/test_course.py +++ b/tests/api/v2/test_course.py @@ -1,6 +1,8 @@ import os import shutil +import xml.etree.ElementTree as ET + from django.conf import settings from django.contrib.auth.models import User @@ -8,17 +10,22 @@ from django.test import TransactionTestCase from tastypie.test import ResourceTestCaseMixin -from tests.utils import get_api_key, \ - get_api_url, \ - update_course_status, \ - update_course_owner +from tests.utils import get_api_key, get_api_url, update_course_status, update_course_owner from oppia.models import Tracker, Course, CourseStatus class CourseResourceTest(ResourceTestCaseMixin, TransactionTestCase): fixtures = ['tests/test_user.json', 'tests/test_oppia.json', - 'tests/test_permissions.json'] + 'tests/test_quiz.json', + 'tests/test_permissions.json', + 'default_badges.json', + 'default_gamification_events.json', + 'tests/awards/award-course.json', + 'tests/test_course_permissions.json', + 'tests/test_cohort.json', + 'tests/test_progress_summary.json', + 'tests/test_tracker.json'] STR_DOWNLOAD = 'download/' STR_ACTIVITY = 'activity/' @@ -59,14 +66,12 @@ def copy_test_courses(self): def perform_request(self, course_id, user, path=''): resource_url = get_api_url('v2', 'course', course_id) + path - resp = self.api_client.get( - resource_url, format='json', data=user) + resp = self.api_client.get(resource_url, format='json', data=user) return resp # Post invalid def test_post_invalid(self): - self.assertHttpMethodNotAllowed( - self.api_client.post(self.url, format='json', data={})) + self.assertHttpMethodNotAllowed(self.api_client.post(self.url, format='json', data={})) # test unauthorized def test_unauthorized(self): @@ -74,32 +79,36 @@ def test_unauthorized(self): 'username': 'demo', 'api_key': '1234', } - self.assertHttpUnauthorized( - self.api_client.get(self.url, format='json', data=data)) + self.assertHttpUnauthorized(self.api_client.get(self.url, format='json', data=data)) # test authorized def test_authorized(self): - resp = self.api_client.get( - self.url, format='json', data=self.user_auth) + resp = self.api_client.get(self.url, format='json', data=self.user_auth) self.assertHttpOK(resp) # test contains courses (and right no of courses) def test_has_courses(self): - resp = self.api_client.get( - self.url, format='json', data=self.user_auth) + resp = self.api_client.get(self.url, format='json', data=self.user_auth) self.assertHttpOK(resp) self.assertValidJSON(resp.content) response_data = self.deserialize(resp) self.assertTrue('courses' in response_data) - # should have 2 courses with the test data set - self.assertEqual(3, len(response_data['courses'])) + # should have 4 courses with the test data set + self.assertEqual(4, len(response_data['courses'])) # check each course had a download url for course in response_data['courses']: - self.assertTrue('url' in course) - self.assertTrue('shortname' in course) - self.assertTrue('title' in course) + self.assertTrue('resource_uri' in course) + self.assertTrue('id' in course) self.assertTrue('version' in course) + self.assertTrue('title' in course) + self.assertTrue('description' in course) + self.assertTrue('shortname' in course) + self.assertTrue('priority' in course) + self.assertTrue('status' in course) + self.assertTrue('restricted' in course) + self.assertTrue('url' in course) self.assertTrue('author' in course) + self.assertTrue('username' in course) self.assertTrue('organisation' in course) def test_course_get_single(self): @@ -108,11 +117,18 @@ def test_course_get_single(self): self.assertValidJSON(resp.content) # check course format course = self.deserialize(resp) - self.assertTrue('shortname' in course) + self.assertTrue('resource_uri' in course) + self.assertTrue('id' in course) + self.assertTrue('version' in course) self.assertTrue('title' in course) self.assertTrue('description' in course) - self.assertTrue('version' in course) + self.assertTrue('shortname' in course) + self.assertTrue('priority' in course) + self.assertTrue('status' in course) + self.assertTrue('restricted' in course) + self.assertTrue('url' in course) self.assertTrue('author' in course) + self.assertTrue('username' in course) self.assertTrue('organisation' in course) def test_course_get_single_not_found(self): @@ -272,9 +288,20 @@ def test_download_course_read_only_admin(self): resp = self.perform_request(1, self.admin_auth, self.STR_DOWNLOAD) self.assertHttpOK(resp) + # Checks the course activity def test_course_get_activity(self): resp = self.perform_request(1, self.user_auth, self.STR_ACTIVITY) self.assertHttpOK(resp) + xml_doc = ET.fromstring(resp.content) + trackers = xml_doc.findall("tracker") + self.assertEqual(276, len(trackers)) + first_tracker = trackers[0] + self.assertEqual('cd646d1148da0f45cd4f097c6761186b17687', first_tracker.get('digest')) + self.assertEqual('2015-04-16 13:01:59', first_tracker.get('submitteddate')) + self.assertTrue(first_tracker.get('completed')) + self.assertEqual('page', first_tracker.get('type')) + self.assertEqual('', first_tracker.get('event')) + self.assertEqual('None', first_tracker.get('points')) def test_course_get_activity_notfound(self): resp = self.perform_request(999, self.user_auth, self.STR_ACTIVITY) @@ -292,8 +319,7 @@ def test_live_course_admin(self): tracker_count_start = Tracker.objects.all().count() response = self.perform_request(1, self.admin_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -301,8 +327,7 @@ def test_live_course_staff(self): tracker_count_start = Tracker.objects.all().count() response = self.perform_request(1, self.staff_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -310,8 +335,7 @@ def test_live_course_teacher(self): tracker_count_start = Tracker.objects.all().count() response = self.perform_request(1, self.teacher_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -319,8 +343,7 @@ def test_live_course_normal(self): tracker_count_start = Tracker.objects.all().count() response = self.perform_request(1, self.user_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -329,8 +352,7 @@ def test_draft_course_admin(self): update_course_status(1, CourseStatus.DRAFT) response = self.perform_request(1, self.admin_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -339,8 +361,7 @@ def test_draft_course_staff(self): update_course_status(1, CourseStatus.DRAFT) response = self.perform_request(1, self.staff_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -358,8 +379,7 @@ def test_draft_course_teacher_owner(self): update_course_owner(1, self.teacher.id) response = self.perform_request(1, self.teacher_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -408,8 +428,7 @@ def test_new_downloads_disabled_course_admin(self): update_course_status(1, CourseStatus.NEW_DOWNLOADS_DISABLED) response = self.perform_request(1, self.admin_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -418,8 +437,7 @@ def test_new_downloads_disabled_course_staff(self): update_course_status(1, CourseStatus.NEW_DOWNLOADS_DISABLED) response = self.perform_request(1, self.staff_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -428,8 +446,7 @@ def test_new_downloads_disabled_course_teacher(self): update_course_status(1, CourseStatus.NEW_DOWNLOADS_DISABLED) response = self.perform_request(1, self.teacher_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -439,8 +456,7 @@ def test_new_downloads_disabled_course_teacher_owner(self): update_course_owner(1, self.teacher.id) response = self.perform_request(1, self.teacher_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -449,8 +465,7 @@ def test_new_downloads_disabled_course_normal(self): update_course_status(1, CourseStatus.NEW_DOWNLOADS_DISABLED) response = self.perform_request(1, self.user_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -459,8 +474,7 @@ def test_read_only_course_admin(self): update_course_status(1, CourseStatus.READ_ONLY) response = self.perform_request(1, self.admin_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -469,8 +483,7 @@ def test_read_only_course_staff(self): update_course_status(1, CourseStatus.READ_ONLY) response = self.perform_request(1, self.staff_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -479,8 +492,7 @@ def test_read_only_course_teacher(self): update_course_status(1, CourseStatus.READ_ONLY) response = self.perform_request(1, self.teacher_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -490,8 +502,7 @@ def test_read_only_course_teacher_owner(self): update_course_owner(1, self.teacher.id) response = self.perform_request(1, self.teacher_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -500,8 +511,7 @@ def test_read_only_course_normal(self): update_course_status(1, CourseStatus.READ_ONLY) response = self.perform_request(1, self.user_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -538,8 +548,7 @@ def test_live_course_shortname_normal(self): tracker_count_start = Tracker.objects.all().count() response = self.perform_request('anc1-all', self.user_auth, self.STR_DOWNLOAD) self.assertHttpOK(response) - self.assertEqual(response['content-type'], - self.STR_ZIP_EXPECTED_CONTENT_TYPE) + self.assertEqual(response['content-type'], self.STR_ZIP_EXPECTED_CONTENT_TYPE) tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start, tracker_count_end) @@ -556,11 +565,18 @@ def test_course_shortname_get_single(self): self.assertValidJSON(resp.content) # check course format course = self.deserialize(resp) - self.assertTrue('shortname' in course) + self.assertTrue('resource_uri' in course) + self.assertTrue('id' in course) + self.assertTrue('version' in course) self.assertTrue('title' in course) self.assertTrue('description' in course) - self.assertTrue('version' in course) + self.assertTrue('shortname' in course) + self.assertTrue('priority' in course) + self.assertTrue('status' in course) + self.assertTrue('restricted' in course) + self.assertTrue('url' in course) self.assertTrue('author' in course) + self.assertTrue('username' in course) self.assertTrue('organisation' in course) def test_course_shortname_get_single_staff(self): @@ -569,11 +585,18 @@ def test_course_shortname_get_single_staff(self): self.assertValidJSON(resp.content) # check course format course = self.deserialize(resp) - self.assertTrue('shortname' in course) + self.assertTrue('resource_uri' in course) + self.assertTrue('id' in course) + self.assertTrue('version' in course) self.assertTrue('title' in course) self.assertTrue('description' in course) - self.assertTrue('version' in course) + self.assertTrue('shortname' in course) + self.assertTrue('priority' in course) + self.assertTrue('status' in course) + self.assertTrue('restricted' in course) + self.assertTrue('url' in course) self.assertTrue('author' in course) + self.assertTrue('username' in course) self.assertTrue('organisation' in course) def test_course_shortname_get_single_not_found(self): diff --git a/tests/api/v2/test_login.py b/tests/api/v2/test_login.py index 8d93139ee..912a5739e 100644 --- a/tests/api/v2/test_login.py +++ b/tests/api/v2/test_login.py @@ -8,7 +8,8 @@ class UserResourceTest(ResourceTestCaseMixin, TestCase): fixtures = ['tests/test_user.json', - 'tests/test_oppia.json'] + 'tests/test_oppia.json', + 'tests/test_customfields.json'] def setUp(self): super(UserResourceTest, self).setUp() @@ -46,8 +47,24 @@ def test_valid_login(self): self.assertTrue('organisation' in response_data) self.assertTrue('first_name' in response_data) self.assertTrue('last_name' in response_data) - # check it doesn't contain the password - self.assertFalse('password' in response_data) + self.assertFalse('password' in response_data) # check it doesn't contain the password + self.assertTrue('cohorts' in response_data) + self.assertTrue('badging' in response_data) + self.assertTrue('scoring' in response_data) + self.assertTrue('username' in response_data) + self.assertTrue('metadata' in response_data) + self.assertTrue('resource_uri' in response_data) + + self.assertEqual(100, response_data['points']) + self.assertEqual(0, response_data['badges']) + self.assertTrue(response_data['badging']) + self.assertTrue(response_data['scoring']) + self.assertEqual("demo@me.com", response_data['email']) + self.assertEqual("", response_data['organisation']) + self.assertEqual("", response_data['job_title']) + self.assertEqual("demo", response_data['first_name']) + self.assertEqual("user", response_data['last_name']) + self.assertEqual("demo", response_data['username']) # check inactive user can't access def test_inactive_username(self): diff --git a/tests/api/v2/test_media_api.py b/tests/api/v2/test_media_api.py index 5b3b7747c..15c365750 100644 --- a/tests/api/v2/test_media_api.py +++ b/tests/api/v2/test_media_api.py @@ -98,6 +98,18 @@ def test_upload_user(self): 'password': 'password', 'media_file': upload_file}) self.assertEqual(response.status_code, 201) + response_data = json.loads(response.content) + + self.assertTrue('digest' in response_data) + self.assertTrue('length' in response_data) + self.assertTrue('filesize' in response_data) + self.assertTrue('download_url' in response_data) + + self.assertEqual("5c414654ad2bd2bc1ea9819435e1f193", response_data['digest']) + self.assertEqual(4, response_data['length']) + self.assertEqual(496995, response_data['filesize']) + self.assertTrue(response_data['download_url'].startswith("http://testserver/media/uploaded/")) + self.assertTrue(response_data['download_url'].endswith(".m4v")) def test_upload_teacher(self): # teacher @@ -154,6 +166,9 @@ def test_digest_valid(self): url = "http://testserver/media/uploaded/2018/02/MH1_Keyboard_480p.mp4" self.assertEqual(json_data['download_url'], url) self.assertEqual(json_data['length'], 170) + self.assertEqual(json_data['title'], None) + self.assertEqual(json_data['organisation'], None) + self.assertEqual(json_data['license'], None) def test_digest_invalid(self): response = self.client.get(self.get_invalid_digest) diff --git a/tests/api/v2/test_points.py b/tests/api/v2/test_points.py index 2499cc394..0b3932e57 100644 --- a/tests/api/v2/test_points.py +++ b/tests/api/v2/test_points.py @@ -10,8 +10,10 @@ class PointsResourceTest(ResourceTestCaseMixin, TestCase): fixtures = ['tests/test_user.json', 'tests/test_oppia.json', - 'tests/test_leaderboard.json' - ] + 'tests/test_leaderboard.json', + 'default_badges.json', + 'default_gamification_events.json', + 'tests/usercoursesummary/course_tracker_v3.json'] STR_LEADERBOARD_FILTERED_URL = "/api/v2/leaderboard/" @@ -22,8 +24,7 @@ def test_get_unauthorized(self): api_key = get_api_key(user=user) self.api_key = api_key.key self.url = get_api_url('v2', 'points') - self.assertHttpUnauthorized(self.api_client.get(self.url, - format='json')) + self.assertHttpUnauthorized(self.api_client.get(self.url, format='json')) # check post not allowed def test_post_not_allowed(self): @@ -32,9 +33,7 @@ def test_post_not_allowed(self): api_key = get_api_key(user=user) self.api_key = api_key.key self.url = get_api_url('v2', 'points') - self.assertHttpMethodNotAllowed(self.api_client.post(self.url, - format='json', - data={})) + self.assertHttpMethodNotAllowed(self.api_client.post(self.url, format='json', data={})) # check get with an invalid apiKey def test_get_apikeyinvalid(self): @@ -43,8 +42,7 @@ def test_get_apikeyinvalid(self): api_key = get_api_key(user=user) self.api_key = api_key.key self.url = get_api_url('v2', 'points') - auth_header = self.create_apikey(username=self.username, - api_key="badbadbad") + auth_header = self.create_apikey(username=self.username, api_key="badbadbad") self.assertHttpUnauthorized( self.api_client.get(self.url, format='json', @@ -52,30 +50,61 @@ def test_get_apikeyinvalid(self): # check a valid get def test_get_points(self): - auth_header = self.create_apikey(username="user4996", - api_key="1234") + user = User.objects.get(username='demo') + api_key = get_api_key(user=user) + auth_header = self.create_apikey(username=user.username, api_key=api_key.key) self.url = get_api_url('v2', 'points') - res = self.api_client.get(self.url, - format='json', - authentication=auth_header) - self.assertHttpOK(res) - self.assertValidJSON(res.content) + resp = self.api_client.get(self.url, format='json', authentication=auth_header) + self.assertHttpOK(resp) + self.assertValidJSON(resp.content) + points = self.deserialize(resp)['objects'] + + self.assertEqual(12, len(points)) + point = points[0] + self.assertTrue('date' in point) + self.assertTrue('description' in point) + self.assertTrue('points' in point) + self.assertTrue('type' in point) def test_get_leaderboard_all(self): - auth_header = self.create_apikey(username="user4996", - api_key="1234") - response = self.api_client.get('/api/v2/leaderboard-all/', - format='json', - authentication=auth_header) + auth_header = self.create_apikey(username="user4996", api_key="1234") + response = self.api_client.get('/api/v2/leaderboard-all/', format='json', authentication=auth_header) self.assertHttpOK(response) self.assertValidJSON(response.content) json_data = json.loads(response.content) + + self.assertTrue('generated_date' in json_data) + self.assertTrue('server' in json_data) + self.assertTrue('leaderboard' in json_data) + self.assertEqual(2037, len(json_data['leaderboard'])) + leader = json_data['leaderboard'][0] + self.assertTrue('position' in leader) + self.assertTrue('username' in leader) + self.assertTrue('first_name' in leader) + self.assertTrue('last_name' in leader) + self.assertTrue('points' in leader) + self.assertTrue('badges' in leader) + + self.assertEqual(1, leader['position']) + self.assertEqual("user4847", leader['username']) + self.assertEqual("User", leader['first_name']) + self.assertEqual("4847", leader['last_name']) + self.assertEqual(169437, leader['points']) + self.assertEqual(7, leader['badges']) + + position567 = json_data['leaderboard'][566] + self.assertEqual(567, position567['position']) + self.assertEqual("user4856", position567['username']) + self.assertEqual("User", position567['first_name']) + self.assertEqual("4856", position567['last_name']) + self.assertEqual(720, position567['points']) + self.assertEqual(1, position567['badges']) + # check top user def test_get_leaderboard_filtered_position1_user(self): - auth_header = self.create_apikey(username="user4847", - api_key="1234") + auth_header = self.create_apikey(username="user4847", api_key="1234") response = self.api_client.get(self.STR_LEADERBOARD_FILTERED_URL, format='json', authentication=auth_header) @@ -84,10 +113,24 @@ def test_get_leaderboard_filtered_position1_user(self): json_data = json.loads(response.content) self.assertEqual(21, len(json_data['leaderboard'])) + leader = json_data['leaderboard'][0] + self.assertTrue('position' in leader) + self.assertTrue('username' in leader) + self.assertTrue('first_name' in leader) + self.assertTrue('last_name' in leader) + self.assertTrue('points' in leader) + self.assertTrue('badges' in leader) + + self.assertEqual(1, leader['position']) + self.assertEqual("user4847", leader['username']) + self.assertEqual("User", leader['first_name']) + self.assertEqual("4847", leader['last_name']) + self.assertEqual(169437, leader['points']) + self.assertEqual(7, leader['badges']) + # Check bottom of top users def test_get_leaderboard_filtered_position20_user(self): - auth_header = self.create_apikey(username="user3263", - api_key="1234") + auth_header = self.create_apikey(username="user3263", api_key="1234") response = self.api_client.get(self.STR_LEADERBOARD_FILTERED_URL, format='json', authentication=auth_header) @@ -96,9 +139,23 @@ def test_get_leaderboard_filtered_position20_user(self): json_data = json.loads(response.content) self.assertEqual(40, len(json_data['leaderboard'])) + position_x = json_data['leaderboard'][37] + self.assertTrue('position' in position_x) + self.assertTrue('username' in position_x) + self.assertTrue('first_name' in position_x) + self.assertTrue('last_name' in position_x) + self.assertTrue('points' in position_x) + self.assertTrue('badges' in position_x) + + self.assertEqual(38, position_x['position']) + self.assertEqual("user2622", position_x['username']) + self.assertEqual("User", position_x['first_name']) + self.assertEqual("2622", position_x['last_name']) + self.assertEqual(8446, position_x['points']) + self.assertEqual(7, position_x['badges']) + def test_get_leaderboard_filtered_position21_user(self): - auth_header = self.create_apikey(username="user3342", - api_key="1234") + auth_header = self.create_apikey(username="user3342", api_key="1234") response = self.api_client.get(self.STR_LEADERBOARD_FILTERED_URL, format='json', authentication=auth_header) @@ -108,8 +165,7 @@ def test_get_leaderboard_filtered_position21_user(self): self.assertEqual(41, len(json_data['leaderboard'])) def test_get_leaderboard_filtered_position40_user(self): - auth_header = self.create_apikey(username="user4351", - api_key="1234") + auth_header = self.create_apikey(username="user4351", api_key="1234") response = self.api_client.get(self.STR_LEADERBOARD_FILTERED_URL, format='json', authentication=auth_header) @@ -119,8 +175,7 @@ def test_get_leaderboard_filtered_position40_user(self): self.assertEqual(60, len(json_data['leaderboard'])) def test_get_leaderboard_filtered_position100_user(self): - auth_header = self.create_apikey(username="user2937", - api_key="1234") + auth_header = self.create_apikey(username="user2937", api_key="1234") response = self.api_client.get(self.STR_LEADERBOARD_FILTERED_URL, format='json', authentication=auth_header) @@ -130,8 +185,7 @@ def test_get_leaderboard_filtered_position100_user(self): self.assertEqual(61, len(json_data['leaderboard'])) def test_get_leaderboard_filtered_position2000_user(self): - auth_header = self.create_apikey(username="user4398", - api_key="1234") + auth_header = self.create_apikey(username="user4398", api_key="1234") response = self.api_client.get(self.STR_LEADERBOARD_FILTERED_URL, format='json', authentication=auth_header) @@ -141,8 +195,7 @@ def test_get_leaderboard_filtered_position2000_user(self): self.assertEqual(61, len(json_data['leaderboard'])) def test_get_leaderboard_filtered_position2017_user(self): - auth_header = self.create_apikey(username="user4234", - api_key="1234") + auth_header = self.create_apikey(username="user4234", api_key="1234") response = self.api_client.get(self.STR_LEADERBOARD_FILTERED_URL, format='json', authentication=auth_header) @@ -153,8 +206,7 @@ def test_get_leaderboard_filtered_position2017_user(self): # position 2037 is last def test_get_leaderboard_filtered_position2037_user(self): - auth_header = self.create_apikey(username="user3169", - api_key="1234") + auth_header = self.create_apikey(username="user3169", api_key="1234") response = self.api_client.get(self.STR_LEADERBOARD_FILTERED_URL, format='json', authentication=auth_header) diff --git a/tests/api/v2/test_profile_get.py b/tests/api/v2/test_profile_get.py new file mode 100644 index 000000000..d2390ba6b --- /dev/null +++ b/tests/api/v2/test_profile_get.py @@ -0,0 +1,47 @@ +# UserResource +from django.contrib.auth.models import User +from django.test import TestCase +from tastypie.test import ResourceTestCaseMixin + +from tests.utils import get_api_key, get_api_url + + +class UserProfileTest(ResourceTestCaseMixin, TestCase): + fixtures = ['tests/test_user.json', + 'tests/test_oppia.json', + 'tests/test_customfields.json'] + + def setUp(self): + super().setUp() + self.url = get_api_url('v2', 'profile') + + user = User.objects.get(username='demo') + self.user_auth = { + 'username': 'demo', + 'api_key': get_api_key(user=user).key, + } + + # check get not allowed + def test_post_invalid(self): + self.assertHttpMethodNotAllowed(self.api_client.post(self.url, format='json')) + + def test_get_invalid_apikey(self): + data = { + 'username': 'user', + 'api_key': '1234', + } + self.assertHttpUnauthorized(self.api_client.get(self.url, format='json', data=data)) + + def test_get_anonymous_user(self): + resp = self.api_client.get(self.url, format='json') + self.assertHttpUnauthorized(resp) + + def test_get_valid_user(self): + resp = self.api_client.get(self.url, format='json', data=self.user_auth) + self.assertValidJSON(resp.content) + + # check return data + response_data = self.deserialize(resp) + # check that cohorts and custom fields are included + self.assertIn('country', response_data) + self.assertIn('cohorts', response_data) diff --git a/tests/api/v2/test_profile_update.py b/tests/api/v2/test_profile_update.py index 4d7f7afe9..cbadea8c4 100644 --- a/tests/api/v2/test_profile_update.py +++ b/tests/api/v2/test_profile_update.py @@ -1,3 +1,5 @@ +import json + from django.contrib.auth.models import User from tests.profile.user_profile_base_test_case import UserProfileBaseTestCase @@ -26,6 +28,19 @@ def test_edit_own_profile_user(self): authentication=self.get_credentials()) self.assertHttpCreated(response) + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("Hernan", json_data['first_name']) + self.assertEqual("Cortez", json_data['last_name']) + self.assertEqual("my organisation", json_data['organisation']) + updated_user = User.objects.get(username=self.username) self.assertNotEqual(orig_firstname, updated_user.first_name) self.assertNotEqual(orig_lastname, updated_user.last_name) diff --git a/tests/api/v2/test_profile_update_custom_fields.py b/tests/api/v2/test_profile_update_custom_fields.py index 15616a974..256376f83 100644 --- a/tests/api/v2/test_profile_update_custom_fields.py +++ b/tests/api/v2/test_profile_update_custom_fields.py @@ -1,3 +1,5 @@ +import json + from django.contrib.auth.models import User from django.test import TestCase from tastypie.test import ResourceTestCaseMixin @@ -86,6 +88,22 @@ def test_edit_profile_req_bool_new(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('bool_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual(True, json_data['bool_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start+1, count_end) @@ -114,6 +132,22 @@ def test_edit_profile_req_bool_no_change(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('bool_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual(True, json_data['bool_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start, count_end) @@ -142,6 +176,22 @@ def test_edit_profile_not_req_bool_change(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('bool_not_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual(False, json_data['bool_not_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start, count_end) @@ -169,6 +219,21 @@ def test_edit_profile_not_req_bool_no_change(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('bool_not_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual(True, json_data['bool_not_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start, count_end) @@ -197,6 +262,21 @@ def test_edit_profile_req_int_change(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('int_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual(999, json_data['int_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start, count_end) @@ -220,6 +300,22 @@ def test_edit_profile_req_int_new(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('int_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual(999, json_data['int_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start+1, count_end) @@ -247,6 +343,22 @@ def test_edit_profile_req_int_no_change(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('int_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual(123, json_data['int_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start, count_end) @@ -275,6 +387,22 @@ def test_edit_profile_not_req_int_change(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('int_not_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual(1234, json_data['int_not_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start, count_end) @@ -302,6 +430,22 @@ def test_edit_profile_not_req_int_no_change(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('int_not_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual(123, json_data['int_not_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start, count_end) @@ -330,6 +474,22 @@ def test_edit_profile_req_str_change(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('str_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual("my new string", json_data['str_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start, count_end) @@ -353,6 +513,22 @@ def test_edit_profile_req_str_new(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('str_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual("my new string", json_data['str_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start+1, count_end) @@ -380,6 +556,22 @@ def test_edit_profile_req_str_no_change(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('str_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual("my string", json_data['str_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start, count_end) @@ -408,6 +600,22 @@ def test_edit_profile_not_req_str_change(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('str_not_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual("my new string", json_data['str_not_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start, count_end) @@ -435,6 +643,22 @@ def test_edit_profile_not_req_str_no_change(self): data=post_data, authentication=self.get_credentials()) self.assertHttpCreated(response) + + self.assertValidJSON(response.content) + json_data = json.loads(response.content) + + self.assertTrue('email' in json_data) + self.assertTrue('first_name' in json_data) + self.assertTrue('last_name' in json_data) + self.assertTrue('organisation' in json_data) + self.assertTrue('str_not_req' in json_data) + + self.assertEqual("demo@me.com", json_data['email']) + self.assertEqual("demo", json_data['first_name']) + self.assertEqual("user", json_data['last_name']) + self.assertEqual("", json_data['organisation']) + self.assertEqual("my string", json_data['str_not_req']) + count_end = UserProfileCustomField.objects.all().count() self.assertEqual(count_start, count_end) diff --git a/tests/api/v2/test_progress.py b/tests/api/v2/test_progress.py index e1f50e177..e92d53d15 100644 --- a/tests/api/v2/test_progress.py +++ b/tests/api/v2/test_progress.py @@ -1,6 +1,4 @@ -import json - from django.contrib.auth.models import User from django.test import TestCase from tastypie.test import ResourceTestCaseMixin @@ -58,166 +56,154 @@ def check_json_content(self, json): # post disallowed def test_post_invalid(self): self.assertHttpMethodNotAllowed( - self.api_client.post(self.url, - format='json', - data=self.admin_auth)) + self.api_client.post(self.url, format='json', data=self.admin_auth)) # admin gets own def test_admin_gets_own(self): url = self.url + "admin/" - response = self.api_client.get( - url, format='json', data=self.admin_auth) + response = self.api_client.get(url, format='json', data=self.admin_auth) self.assertHttpOK(response) self.assertValidJSON(response.content) - content = json.loads(response.content) + content = self.deserialize(response) self.assertEqual(2, len(content)) self.check_json_content(content[0]) # staff gets own def test_staff_gets_own(self): url = self.url + "staff/" - response = self.api_client.get( - url, format='json', data=self.staff_auth) + response = self.api_client.get(url, format='json', data=self.staff_auth) self.assertHttpOK(response) self.assertValidJSON(response.content) - content = json.loads(response.content) + content = self.deserialize(response) self.assertEqual(2, len(content)) self.check_json_content(content[0]) # teacher gets own def test_teacher_gets_own(self): url = self.url + "teacher/" - response = self.api_client.get( - url, format='json', data=self.teacher_auth) + response = self.api_client.get(url, format='json', data=self.teacher_auth) self.assertHttpOK(response) self.assertValidJSON(response.content) - content = json.loads(response.content) + content = self.deserialize(response) self.assertEqual(2, len(content)) self.check_json_content(content[0]) # user gets own def test_user_gets_own(self): url = self.url + "demo/" - response = self.api_client.get( - url, format='json', data=self.user_auth) + response = self.api_client.get(url, format='json', data=self.user_auth) self.assertHttpOK(response) self.assertValidJSON(response.content) - content = json.loads(response.content) + content = self.deserialize(response) self.assertEqual(3, len(content)) self.check_json_content(content[0]) + self.assertEqual('anc1-all', content[0]['shortname']) + self.assertEqual(0, content[0]['points']) + self.assertEqual(278, content[0]['total_activity']) + self.assertEqual(0, content[0]['quizzes_passed']) + self.assertEqual(0, content[0]['badges_achieved']) + self.assertEqual(2, content[0]['media_viewed']) + self.assertEqual(0, content[0]['completed_activities']) + self.assertEqual(0, content[0]['percent_complete']) + # no user specified def test_no_user(self): - response = self.api_client.get( - self.url, format='json', data=self.user_auth) + response = self.api_client.get(self.url, format='json', data=self.user_auth) self.assertEqual(400, response.status_code) # can't get a direct summary resource record, will actually look for a user def test_ucs_id(self): url = self.url + "17/" - response = self.api_client.get( - url, format='json', data=self.user_auth) + response = self.api_client.get(url, format='json', data=self.user_auth) self.assertHttpNotFound(response) # admin gets teacher/staff/user def test_admin_other_users(self): url = self.url + "demo/" - response = self.api_client.get( - url, format='json', data=self.admin_auth) + response = self.api_client.get(url, format='json', data=self.admin_auth) self.assertHttpOK(response) self.assertValidJSON(response.content) - content = json.loads(response.content) + content = self.deserialize(response) self.assertEqual(3, len(content)) self.check_json_content(content[0]) url = self.url + "teacher/" - response = self.api_client.get( - url, format='json', data=self.admin_auth) + response = self.api_client.get(url, format='json', data=self.admin_auth) self.assertHttpOK(response) self.assertValidJSON(response.content) - content = json.loads(response.content) + content = self.deserialize(response) self.assertEqual(2, len(content)) self.check_json_content(content[0]) url = self.url + "staff/" - response = self.api_client.get( - url, format='json', data=self.admin_auth) + response = self.api_client.get(url, format='json', data=self.admin_auth) self.assertHttpOK(response) self.assertValidJSON(response.content) - content = json.loads(response.content) + content = self.deserialize(response) self.assertEqual(2, len(content)) self.check_json_content(content[0]) # staff gets admin/teacher/user def test_staff_other_users(self): url = self.url + "demo/" - response = self.api_client.get( - url, format='json', data=self.staff_auth) + response = self.api_client.get(url, format='json', data=self.staff_auth) self.assertHttpOK(response) self.assertValidJSON(response.content) - content = json.loads(response.content) + content = self.deserialize(response) self.assertEqual(3, len(content)) self.check_json_content(content[0]) url = self.url + "teacher/" - response = self.api_client.get( - url, format='json', data=self.staff_auth) + response = self.api_client.get(url, format='json', data=self.staff_auth) self.assertHttpOK(response) self.assertValidJSON(response.content) - content = json.loads(response.content) + content = self.deserialize(response) self.assertEqual(2, len(content)) self.check_json_content(content[0]) url = self.url + "admin/" - response = self.api_client.get( - url, format='json', data=self.staff_auth) + response = self.api_client.get(url, format='json', data=self.staff_auth) self.assertHttpOK(response) self.assertValidJSON(response.content) - content = json.loads(response.content) + content = self.deserialize(response) self.assertEqual(2, len(content)) self.check_json_content(content[0]) # teacher can't get admin/staff def test_teacher_other_users(self): url = self.url + "admin/" - response = self.api_client.get( - url, format='json', data=self.teacher_auth) + response = self.api_client.get(url, format='json', data=self.teacher_auth) self.assertHttpNotFound(response) url = self.url + "staff/" - response = self.api_client.get( - url, format='json', data=self.teacher_auth) + response = self.api_client.get(url, format='json', data=self.teacher_auth) self.assertHttpNotFound(response) url = self.url + "demo/" - response = self.api_client.get( - url, format='json', data=self.teacher_auth) + response = self.api_client.get(url, format='json', data=self.teacher_auth) self.assertHttpOK(response) self.assertValidJSON(response.content) - content = json.loads(response.content) + content = self.deserialize(response) self.assertEqual(1, len(content)) self.check_json_content(content[0]) # user can;t get admin/teacher/staff def test_user_other_users(self): url = self.url + "admin/" - response = self.api_client.get( - url, format='json', data=self.user_auth) + response = self.api_client.get(url, format='json', data=self.user_auth) self.assertHttpNotFound(response) url = self.url + "staff/" - response = self.api_client.get( - url, format='json', data=self.user_auth) + response = self.api_client.get(url, format='json', data=self.user_auth) self.assertHttpNotFound(response) url = self.url + "teacher/" - response = self.api_client.get( - url, format='json', data=self.user_auth) + response = self.api_client.get(url, format='json', data=self.user_auth) self.assertHttpNotFound(response) # invalid user def test_invalid_user(self): url = self.url + "not-a-user/" - response = self.api_client.get( - url, format='json', data=self.user_auth) + response = self.api_client.get(url, format='json', data=self.user_auth) self.assertHttpNotFound(response) diff --git a/tests/api/v2/test_register.py b/tests/api/v2/test_register.py index a493cc949..914cb7101 100644 --- a/tests/api/v2/test_register.py +++ b/tests/api/v2/test_register.py @@ -57,6 +57,15 @@ def test_post_no_email(self): resp = self.api_client.post(self.url, format='json', data=data) self.assertHttpCreated(resp) self.assertValidJSON(resp.content) + user_data = self.deserialize(resp) + self.assertTrue('api_key' in user_data) + self.assertTrue('email' in user_data) + self.assertTrue('first_name' in user_data) + self.assertTrue('last_name' in user_data) + self.assertTrue('points' in user_data) + self.assertTrue('username' in user_data) + self.assertTrue('password' not in user_data) + self.assertTrue('passwordagain' not in user_data) # check posting with invalid email def test_post_invalid_email(self): @@ -180,6 +189,15 @@ def test_post_created(self): resp = self.api_client.post(self.url, format='json', data=data) self.assertHttpCreated(resp) self.assertValidJSON(resp.content) + user_data = self.deserialize(resp) + self.assertTrue('api_key' in user_data) + self.assertTrue('email' in user_data) + self.assertTrue('first_name' in user_data) + self.assertTrue('last_name' in user_data) + self.assertTrue('points' in user_data) + self.assertTrue('username' in user_data) + self.assertTrue('password' not in user_data) + self.assertTrue('passwordagain' not in user_data) # test username already in use def test_username_in_use(self): @@ -211,8 +229,7 @@ def test_email_in_use(self): def test_self_registration_disabled_cant_view(self): # turn off self registration - SettingProperties.set_bool(constants.OPPIA_ALLOW_SELF_REGISTRATION, - False) + SettingProperties.set_bool(constants.OPPIA_ALLOW_SELF_REGISTRATION, False) data = { 'username': 'demo3', 'password': 'secret', @@ -226,5 +243,4 @@ def test_self_registration_disabled_cant_view(self): self.assertValidJSON(response.content) # turn back on - SettingProperties.set_bool(constants.OPPIA_ALLOW_SELF_REGISTRATION, - True) + SettingProperties.set_bool(constants.OPPIA_ALLOW_SELF_REGISTRATION, True) diff --git a/tests/api/v2/test_reset_password.py b/tests/api/v2/test_reset_password.py index ce4c7e99b..705438d0e 100644 --- a/tests/api/v2/test_reset_password.py +++ b/tests/api/v2/test_reset_password.py @@ -14,8 +14,7 @@ def setUp(self): # check get method not allowed def test_get_invalid(self): - self.assertHttpMethodNotAllowed(self.api_client.get(self.url, - format='json')) + self.assertHttpMethodNotAllowed(self.api_client.get(self.url, format='json')) def test_no_username(self): data = {} @@ -43,6 +42,13 @@ def test_valid_username(self): resp = self.api_client.post(self.url, format='json', data=data) self.assertHttpCreated(resp) self.assertValidJSON(resp.content) + + response_data = self.deserialize(resp) + self.assertTrue('message' in response_data) + self.assertTrue('username' in response_data) + self.assertEqual('demo', response_data['username']) + self.assertFalse('password' in response_data) + self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].to[0], self.demo_email_address) mail.outbox = [] @@ -52,6 +58,13 @@ def test_valid_email(self): resp = self.api_client.post(self.url, format='json', data=data) self.assertHttpCreated(resp) self.assertValidJSON(resp.content) + + response_data = self.deserialize(resp) + self.assertTrue('message' in response_data) + self.assertTrue('username' in response_data) + self.assertEqual('demo@me.com', response_data['username']) + self.assertFalse('password' in response_data) + self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].to[0], self.demo_email_address) mail.outbox = [] diff --git a/tests/api/v2/test_tracker.py b/tests/api/v2/test_tracker.py index 86f23798d..bf75f61ae 100644 --- a/tests/api/v2/test_tracker.py +++ b/tests/api/v2/test_tracker.py @@ -78,6 +78,7 @@ def test_post_digest_found(self): authentication=self.get_credentials()) self.assertHttpCreated(resp) self.assertValidJSON(resp.content) + # check the record was successfully added tracker_count_end = Tracker.objects.all().count() self.assertEqual(tracker_count_start + 1, tracker_count_end) @@ -87,7 +88,16 @@ def test_post_digest_found(self): self.assertTrue('points' in response_data) self.assertTrue('badges' in response_data) self.assertTrue('completed' in response_data) + self.assertTrue('badging' in response_data) + self.assertTrue('metadata' in response_data) + self.assertTrue('scoring' in response_data) + self.assertTrue('tracker_date' in response_data) + self.assertTrue('user' in response_data) + + self.assertEqual(0, response_data['badges']) self.assertFalse(response_data['completed']) + self.assertTrue(response_data['badging']) + self.assertTrue(response_data['scoring']) # check existing trackers can't be overwritten def test_post_no_overwrite(self): @@ -145,6 +155,15 @@ def test_patch_all_valid_digests(self): response_data = self.deserialize(resp) self.assertTrue('points' in response_data) self.assertTrue('badges' in response_data) + self.assertTrue('scoring' in response_data) + self.assertTrue('badging' in response_data) + self.assertTrue('metadata' in response_data) + self.assertTrue('course_points' in response_data) + + self.assertEqual(100, response_data['points']) + self.assertEqual(0, response_data['badges']) + self.assertTrue(response_data['badging']) + self.assertTrue(response_data['scoring']) def test_patch_all_invalid_digests(self): activity1 = { @@ -438,6 +457,15 @@ def test_patch_activity_search_mix_null(self): response_data = self.deserialize(resp) self.assertTrue('points' in response_data) self.assertTrue('badges' in response_data) + self.assertTrue('scoring' in response_data) + self.assertTrue('badging' in response_data) + self.assertTrue('metadata' in response_data) + self.assertTrue('course_points' in response_data) + + self.assertEqual(100, response_data['points']) + self.assertEqual(0, response_data['badges']) + self.assertTrue(response_data['badging']) + self.assertTrue(response_data['scoring']) def test_patch_activity_search_mix_valid(self): activity1 = { diff --git a/tests/api/v2/test_user_cohorts.py b/tests/api/v2/test_user_cohorts.py index 9fc56f3da..6c83161d7 100644 --- a/tests/api/v2/test_user_cohorts.py +++ b/tests/api/v2/test_user_cohorts.py @@ -43,8 +43,7 @@ def assert_valid_response_and_get_list(self, resp): # Post invalid def test_post_invalid(self): - self.assertHttpMethodNotAllowed( - self.api_client.post(self.url, format='json', data={})) + self.assertHttpMethodNotAllowed(self.api_client.post(self.url, format='json', data={})) # test unauthorized def test_unauthorized(self): @@ -52,8 +51,7 @@ def test_unauthorized(self): 'username': 'user', 'api_key': '1234', } - self.assertHttpUnauthorized( - self.api_client.get(self.url, format='json', data=data)) + self.assertHttpUnauthorized(self.api_client.get(self.url, format='json', data=data)) def test_user_with_no_cohorts(self): resp = self.api_client.get(self.url, format='json', data=self.admin_auth) @@ -65,6 +63,8 @@ def test_user_with_cohorts(self): resp = self.api_client.get(self.url, format='json', data=self.user_auth) cohorts = self.assert_valid_response_and_get_list(resp) self.assertEqual(len(cohorts), 2) + self.assertEqual(1, cohorts[0]) + self.assertEqual(2, cohorts[1]) # Test that if a user belongs more than once to a cohort (multiple roles), they are not duplicated def test_no_duplicate_cohorts(self): @@ -84,6 +84,7 @@ def test_cohorts_returned_after_login(self): # check return data response_data = self.deserialize(resp) - print(response_data) self.assertTrue('cohorts' in response_data) self.assertEqual(len(response_data['cohorts']), 2) + self.assertEqual(1, response_data['cohorts'][0]) + self.assertEqual(2, response_data['cohorts'][1]) diff --git a/tests/api/v2/test_username_reminder.py b/tests/api/v2/test_username_reminder.py index bed0a6e73..4f4d91cd0 100644 --- a/tests/api/v2/test_username_reminder.py +++ b/tests/api/v2/test_username_reminder.py @@ -16,8 +16,7 @@ def setUp(self): # get not allowed def test_get_invalid(self): - self.assertHttpMethodNotAllowed(self.api_client.get(self.url, - format='json')) + self.assertHttpMethodNotAllowed(self.api_client.get(self.url, format='json')) # email not in post params def test_no_email(self): @@ -62,5 +61,13 @@ def test_email_one_valid(self): resp = self.api_client.post(self.url, format='json', data=data) self.assertHttpCreated(resp) self.assertValidJSON(resp.content) + + response_data = self.deserialize(resp) + + self.assertTrue('email' in response_data) + self.assertTrue('message' in response_data) + + self.assertEqual("staff@me.com", response_data['email']) + self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].to[0], "staff@me.com") diff --git a/tests/oppia/test_permissions.py b/tests/oppia/test_permissions.py index 74b62ccae..776090f98 100644 --- a/tests/oppia/test_permissions.py +++ b/tests/oppia/test_permissions.py @@ -114,27 +114,27 @@ def test_student_cantview_cohorts(self): def test_anon_cantview_cohort(self): self.assert_must_login('oppia:cohort_view', - view_kwargs={'pk': 1}) + view_kwargs={'cohort_id': 1}) def test_view_nonexisting_cohort(self): self.assert_not_found('oppia:cohort_view', self.admin_user, - view_kwargs={'pk': 1000}) + view_kwargs={'cohort_id': 1000}) def test_admin_canview_cohort(self): self.assert_can_view('oppia:cohort_view', self.admin_user, - view_kwargs={'pk': 1}) + view_kwargs={'cohort_id': 1}) def test_staff_canview_cohort(self): self.assert_can_view('oppia:cohort_view', self.staff_user, - view_kwargs={'pk': 1}) + view_kwargs={'cohort_id': 1}) def test_student_cantview_cohort(self): self.assert_unauthorized('oppia:cohort_view', self.normal_user, - view_kwargs={'pk': 1}) + view_kwargs={'cohort_id': 1}) # TODO: Teacher view cohort s/he is assigned into # View a cohort course activity diff --git a/tests/profile/views/test_export_users.py b/tests/profile/views/test_export_users.py index 8cc5d5b8d..3ea31388c 100644 --- a/tests/profile/views/test_export_users.py +++ b/tests/profile/views/test_export_users.py @@ -15,7 +15,7 @@ class ExportUsersViewsTest(OppiaTestCase): STR_EXPECTED_CONTENT_TYPE = 'text/csv' STR_URL = 'profile:users_list' - + def test_permissions(self): for user in [self.normal_user, self.teacher_user]: @@ -35,7 +35,7 @@ def test_download_all_users_all_data(self): self.assertEqual(response['content-type'], self.STR_EXPECTED_CONTENT_TYPE) csv_file = csv.DictReader(io.StringIO(response.content.decode())) self.assertEqual(6, len(list(csv_file))) - + def test_download_filtered_by_registration_date_all_data(self): for user in [self.admin_user, self.staff_user]: @@ -47,7 +47,7 @@ def test_download_filtered_by_registration_date_all_data(self): self.assertEqual(response['content-type'], self.STR_EXPECTED_CONTENT_TYPE) csv_file = csv.DictReader(io.StringIO(response.content.decode())) self.assertEqual(0, len(list(csv_file))) - + def test_download_all_users_username_only(self): for user in [self.admin_user, self.staff_user]: @@ -71,4 +71,3 @@ def test_download_all_users_invalid_field(self): self.assertEqual(response['content-type'], self.STR_EXPECTED_CONTENT_TYPE) csv_file = csv.DictReader(io.StringIO(response.content.decode())) self.assertEqual(0, len(list(csv_file))) - \ No newline at end of file diff --git a/tests/quiz/test_feedback_download.py b/tests/quiz/test_feedback_download.py index e6568722a..46aedf869 100644 --- a/tests/quiz/test_feedback_download.py +++ b/tests/quiz/test_feedback_download.py @@ -62,7 +62,6 @@ def test_valid_course_invalid_feedback(self): for user in [self.teacher_user, self.normal_user]: self.assert_response_status(user, self.valid_course_invalid_feedback_url, 403) - def test_invalid_course_invalid_feedback(self): users = [self.admin_user, self.staff_user, @@ -73,7 +72,7 @@ def test_invalid_course_invalid_feedback(self): def test_course_feedback_mismatch(self): - for user in [self.admin_user, self.staff_user]: + for user in [self.admin_user, self.staff_user]: self.assert_response_status(user, self.course_feedback_mismatch_url, 404) for user in [self.teacher_user, self.normal_user]: self.assert_response_status(user, self.course_feedback_mismatch_url, 403) diff --git a/tests/quiz/test_quiz_attempt.py b/tests/quiz/test_quiz_attempt.py index ecd61d732..cfb44cd92 100644 --- a/tests/quiz/test_quiz_attempt.py +++ b/tests/quiz/test_quiz_attempt.py @@ -35,12 +35,12 @@ def test_get_invalid(self): format='json')) def test_put_invalid(self): - resource_url = get_api_url('v1', 'quizattempt', 1192) + resource_url = get_api_url('v2', 'quizattempt', 1192) self.assertHttpMethodNotAllowed(self.api_client.put(resource_url, format='json')) def test_delete_invalid(self): - resource_url = get_api_url('v1', 'quizattempt', 1192) + resource_url = get_api_url('v2', 'quizattempt', 1192) self.assertHttpMethodNotAllowed(self.api_client.delete(resource_url, format='json')) @@ -62,21 +62,40 @@ def test_authorized(self): "score": 0, "text": "false"}]} quizattempt_count_start = QuizAttempt.objects.all().count() - quizattemptresponse_count_start = QuizAttemptResponse.objects \ - .all().count() + quizattemptresponse_count_start = QuizAttemptResponse.objects.all().count() resp = self.api_client.post(self.url, format='json', data=data, authentication=self.get_credentials()) self.assertHttpCreated(resp) self.assertValidJSON(resp.content) + content = self.deserialize(resp) + self.assertTrue('agent' in content) + self.assertTrue('attempt_date' in content) + self.assertTrue('badges' in content) + self.assertTrue('event' in content) + self.assertTrue('id' in content) + self.assertTrue('instance_id' in content) + self.assertTrue('ip' in content) + self.assertTrue('maxscore' in content) + self.assertTrue('points' in content) + self.assertTrue('quiz_id' in content) + self.assertTrue('resource_uri' in content) + self.assertTrue('score' in content) + self.assertTrue('submitted_date' in content) + self.assertTrue('time_taken' in content) + + self.assertEqual('2012-12-18T15:35:12', content['attempt_date']) + self.assertEqual(0, content['badges']) + self.assertEqual('30', content['maxscore']) + self.assertEqual(100, content['points']) + self.assertEqual('10', content['score']) + self.assertEqual(0, content['time_taken']) quizattempt_count_end = QuizAttempt.objects.all().count() - quizattemptresponse_count_end = QuizAttemptResponse.objects \ - .all().count() + quizattemptresponse_count_end = QuizAttemptResponse.objects.all().count() self.assertEqual(quizattempt_count_start + 1, quizattempt_count_end) - self.assertEqual(quizattemptresponse_count_start + 3, - quizattemptresponse_count_end) + self.assertEqual(quizattemptresponse_count_start + 3, quizattemptresponse_count_end) def test_time_taken_saved(self): data = { @@ -125,8 +144,7 @@ def test_unauthorized(self): "text": "false"}]} bad_auth = self.create_apikey(username=self.username, api_key="1234") quizattempt_count_start = QuizAttempt.objects.all().count() - quizattemptresponse_count_start = QuizAttemptResponse.objects \ - .all().count() + quizattemptresponse_count_start = QuizAttemptResponse.objects.all().count() resp = self.api_client.post(self.url, format='json', data=data, @@ -134,11 +152,9 @@ def test_unauthorized(self): self.assertHttpUnauthorized(resp) quizattempt_count_end = QuizAttempt.objects.all().count() - quizattemptresponse_count_end = QuizAttemptResponse.objects \ - .all().count() + quizattemptresponse_count_end = QuizAttemptResponse.objects.all().count() self.assertEqual(quizattempt_count_start, quizattempt_count_end) - self.assertEqual(quizattemptresponse_count_start, - quizattemptresponse_count_end) + self.assertEqual(quizattemptresponse_count_start, quizattemptresponse_count_end) def test_invalid_quiz_id(self): data = { @@ -159,8 +175,7 @@ def test_invalid_quiz_id(self): "score": 0, "text": "false"}]} quizattempt_count_start = QuizAttempt.objects.all().count() - quizattemptresponse_count_start = QuizAttemptResponse.objects \ - .all().count() + quizattemptresponse_count_start = QuizAttemptResponse.objects.all().count() resp = self.api_client.post(self.url, format='json', data=data, @@ -169,11 +184,9 @@ def test_invalid_quiz_id(self): self.assertValidJSON(resp.content) quizattempt_count_end = QuizAttempt.objects.all().count() - quizattemptresponse_count_end = QuizAttemptResponse.objects \ - .all().count() + quizattemptresponse_count_end = QuizAttemptResponse.objects.all().count() self.assertEqual(quizattempt_count_start, quizattempt_count_end) - self.assertEqual(quizattemptresponse_count_start, - quizattemptresponse_count_end) + self.assertEqual(quizattemptresponse_count_start, quizattemptresponse_count_end) def test_invalid_question_id(self): data = { @@ -194,8 +207,7 @@ def test_invalid_question_id(self): "score": 0, "text": "false"}]} quizattempt_count_start = QuizAttempt.objects.all().count() - quizattemptresponse_count_start = QuizAttemptResponse.objects \ - .all().count() + quizattemptresponse_count_start = QuizAttemptResponse.objects.all().count() resp = self.api_client.post(self.url, format='json', data=data, @@ -204,11 +216,9 @@ def test_invalid_question_id(self): self.assertValidJSON(resp.content) quizattempt_count_end = QuizAttempt.objects.all().count() - quizattemptresponse_count_end = QuizAttemptResponse.objects \ - .all().count() + quizattemptresponse_count_end = QuizAttemptResponse.objects.all().count() self.assertEqual(quizattempt_count_start, quizattempt_count_end) - self.assertEqual(quizattemptresponse_count_start, - quizattemptresponse_count_end) + self.assertEqual(quizattemptresponse_count_start, quizattemptresponse_count_end) def test_question_is_part_of_quiz(self): data = { @@ -230,8 +240,7 @@ def test_question_is_part_of_quiz(self): "score": 0, "text": "false"}]} quizattempt_count_start = QuizAttempt.objects.all().count() - quizattemptresponse_count_start = QuizAttemptResponse.objects \ - .all().count() + quizattemptresponse_count_start = QuizAttemptResponse.objects.all().count() resp = self.api_client.post(self.url, format='json', data=data, @@ -240,11 +249,9 @@ def test_question_is_part_of_quiz(self): self.assertValidJSON(resp.content) quizattempt_count_end = QuizAttempt.objects.all().count() - quizattemptresponse_count_end = QuizAttemptResponse.objects \ - .all().count() + quizattemptresponse_count_end = QuizAttemptResponse.objects.all().count() self.assertEqual(quizattempt_count_start, quizattempt_count_end) - self.assertEqual(quizattemptresponse_count_start, - quizattemptresponse_count_end) + self.assertEqual(quizattemptresponse_count_start, quizattemptresponse_count_end) def test_duplicate_quiz_attempt(self): @@ -265,8 +272,7 @@ def test_duplicate_quiz_attempt(self): "score": 0, "text": "false"}]} quizattempt_count_start = QuizAttempt.objects.all().count() - quizattemptresponse_count_start = QuizAttemptResponse.objects \ - .all().count() + quizattemptresponse_count_start = QuizAttemptResponse.objects.all().count() resp = self.api_client.post(self.url, format='json', data=data, @@ -275,17 +281,13 @@ def test_duplicate_quiz_attempt(self): self.assertValidJSON(resp.content) quizattempt_count_end = QuizAttempt.objects.all().count() - quizattemptresponse_count_end = QuizAttemptResponse.objects \ - .all().count() - self.assertEqual(quizattempt_count_start + 1, - quizattempt_count_end) - self.assertEqual(quizattemptresponse_count_start + 3, - quizattemptresponse_count_end) + quizattemptresponse_count_end = QuizAttemptResponse.objects.all().count() + self.assertEqual(quizattempt_count_start + 1, quizattempt_count_end) + self.assertEqual(quizattemptresponse_count_start + 3, quizattemptresponse_count_end) # now send same data again quizattempt_count_start = QuizAttempt.objects.all().count() - quizattemptresponse_count_start = QuizAttemptResponse.objects \ - .all().count() + quizattemptresponse_count_start = QuizAttemptResponse.objects.all().count() resp = self.api_client.post(self.url, format='json', data=data, @@ -294,8 +296,6 @@ def test_duplicate_quiz_attempt(self): self.assertValidJSON(resp.content) quizattempt_count_end = QuizAttempt.objects.all().count() - quizattemptresponse_count_end = QuizAttemptResponse.objects \ - .all().count() + quizattemptresponse_count_end = QuizAttemptResponse.objects.all().count() self.assertEqual(quizattempt_count_start, quizattempt_count_end) - self.assertEqual(quizattemptresponse_count_start, - quizattemptresponse_count_end) + self.assertEqual(quizattemptresponse_count_start, quizattemptresponse_count_end) diff --git a/tests/quiz/test_quiz_download.py b/tests/quiz/test_quiz_download.py index c62c19222..1f9faa76c 100644 --- a/tests/quiz/test_quiz_download.py +++ b/tests/quiz/test_quiz_download.py @@ -27,7 +27,6 @@ class QuizDownloadTest(OppiaTestCase): course_quiz_mismatch_url = reverse(STR_URL_TEMPLATE, args=[1, 224]) - def test_admin_download(self): response = self.assert_response_status(self.admin_user, self.valid_course_valid_quiz_url, 200) self.assertEqual(self.STR_EXPECTED_CONTENT_TYPE, response['content-type']) @@ -59,7 +58,6 @@ def test_valid_course_invalid_quiz(self): for user in [self.teacher_user, self.normal_user]: self.assert_response_status(user, self.valid_course_invalid_quiz_url, 403) - def test_invalid_course_invalid_quiz(self): users = [self.admin_user, self.staff_user, @@ -68,9 +66,8 @@ def test_invalid_course_invalid_quiz(self): for user in users: self.assert_response_status(user, self.invalid_course_invalid_quiz_url, 404) - def test_course_quiz_mismatch(self): - for user in [self.admin_user, self.staff_user]: + for user in [self.admin_user, self.staff_user]: self.assert_response_status(user, self.course_quiz_mismatch_url, 404) for user in [self.teacher_user, self.normal_user]: self.assert_response_status(user, self.course_quiz_mismatch_url, 403)