def get_lti_params(request, *keys): jwt_params, = utils.optional_params(request.data, 'jwt_params') if jwt_params: lti_params = lti.decode_lti_params(jwt_params) else: lti_params = {'empty': ''} values = utils.optional_params(lti_params, *keys) values.append(settings.ROLES['Teacher'] in lti_launch.roles_to_list(lti_params)) return values
def create(self, request): """Create a new course group. Arguments: request -- the request that was send with name -- name of the course group course_id -- course ID of the course lti_id -- (optional) lti_id to link the course to Returns: On failure: unauthorized -- when the user is not logged in forbidden -- when the user has no permission to create new groups On success, with the course group. """ name, course_id = utils.required_params(request.data, "name", "course_id") lti_id, = utils.optional_params(request.data, 'lti_id') course = Course.objects.get(pk=course_id) request.user.check_permission('can_add_course_user_group', course) if lti_id and Group.objects.filter(lti_id=lti_id, course=course).exists(): return response.bad_request('Course group with the desired name already exists.') course_group = factory.make_course_group(name, course, lti_id) serializer = GroupSerializer(course_group, many=False) return response.created({'group': serializer.data})
def update_lti_course_if_exists(request, user, role): """Update a course with lti request. If no course exists, return None If it does exist: 1. If the to be processed user is a test student, remove any other test students from the course, 1 max. 2. Put the user in the course 3. Add groups to the user """ course_lti_id = request.get('custom_course_id', None) course = Course.objects.filter(active_lti_id=course_lti_id) if course_lti_id is None or not course.exists(): return None # There can only be one actively linked course. course = course.first() # Can only have one active test student per course at a time. if user.is_test_student: User.objects.filter(participation__course=course, is_test_student=True).exclude(pk=user.pk).delete() # If the user not is a participant, add participation with possibly the role given by the LTI instance. if not user.is_participant(course): participation = _make_lti_participation(user, course, role) else: participation = Participation.objects.get(course=course, user=user) group_ids, = utils.optional_params(request, 'custom_section_id') if group_ids: add_groups_if_not_exists(participation, group_ids.split(',')) return course
def create(self, request): """Create a new course. Arguments: request -- request data name -- name of the course abbreviation -- abbreviation of the course startdate -- (optional) date when the course starts enddate -- (optional) date when the course ends lti_id -- (optional) lti_id to link the course to Returns: On failure: unauthorized -- when the user is not logged in forbidden -- when the user has no permission to create new courses On succes: success -- with the course data """ request.user.check_permission('can_add_course') name, abbr = utils.required_params(request.data, 'name', 'abbreviation') startdate, enddate, lti_id = utils.optional_params( request.data, 'startdate', 'enddate', 'lti_id') course = factory.make_course(name, abbr, startdate, enddate, request.user, lti_id) serializer = self.serializer_class(course, many=False) return response.created({'course': serializer.data})
def forgot_password(request): """Handles a forgot password request. Arguments: username -- User claimed username. email -- User claimed email. token -- Django stateless token, invalidated after password change or after a set time (by default three days). Generates a recovery token if a matching user can be found by either the prodived username or email. """ username, email = utils.optional_params(request.data, 'username', 'email') # We are retrieving the username based on either the username or email try: user = User.objects.get(username=username) email = 'your recovery address' except User.DoesNotExist: if email is None or email == '': return response.not_found('Invalid email address provided.') user = User.objects.get(email=email) if not user.email: return response.bad_request( description='The provided account has no known email address.') send_password_recovery_link.delay(user.pk) return response.success( description='An email was sent to {}, please check your inbox for further instructions.'.format(email))
def partial_update(self, request, *args, **kwargs): """Update an existing assignment. Arguments: request -- request data data -- the new data for the assignment pk -- assignment ID Returns: On failure: unauthorized -- when the user is not logged in not found -- when the assignment does not exist forbidden -- User not allowed to edit this assignment unauthorized -- when the user is unauthorized to edit the assignment bad_request -- when there is invalid data in the request On success: success -- with the new assignment data """ # Get data pk, = utils.required_typed_params(kwargs, (int, 'pk')) assignment = Assignment.objects.get(pk=pk) published, = utils.optional_params(request.data, 'published') # Remove data that must not be changed by the serializer req_data = request.data req_data.pop('published', None) if not (request.user.is_superuser or request.user == assignment.author): req_data.pop('author', None) response_data = {} # Publish is possible and asked for if published: request.user.check_permission('can_publish_grades', assignment) self.publish(request, assignment) response_data['published'] = published # Update assignment data is possible and asked for if req_data: if 'lti_id' in req_data: factory.make_lti_ids(lti_id=req_data['lti_id'], for_model=Lti_ids.ASSIGNMENT, assignment=assignment) # If a entry has been submitted to one of the journals of the journal it cannot be unpublished if assignment.is_published and 'is_published' in req_data and not req_data['is_published'] and \ Entry.objects.filter(node__journal__assignment=assignment).exists(): return response.bad_request( 'You are not allowed to unpublish an assignment that already has submissions.') serializer = AssignmentSerializer(assignment, data=req_data, context={'user': request.user}, partial=True) if not serializer.is_valid(): response.bad_request() serializer.save() response_data['assignment'] = serializer.data return response.success(response_data)
def partial_update(self, request, *args, **kwargs): """Update an existing journal. Arguments: request -- request data data -- the new data for the journal pk -- journal ID Returns: On failure: unauthorized -- when the user is not logged in not found -- when the journal does not exist forbidden -- User not allowed to edit this journal unauthorized -- when the user is unauthorized to edit the journal bad_request -- when there is invalid data in the request On success: success -- with the new journal data """ pk, = utils.required_typed_params(kwargs, (int, 'pk')) journal = Journal.objects.get(pk=pk) request.user.check_can_view(journal.assignment) req_data = request.data published, = utils.optional_params(request.data, 'published') if published: request.user.check_permission('can_publish_grades', journal.assignment) req_data.pop('published', None) return self.publish(request, journal) bonus_points, = utils.optional_typed_params(request.data, (float, 'bonus_points')) if bonus_points is not None: request.user.check_permission('can_grade', journal.assignment) req_data.pop('bonus_points', None) journal.bonus_points = bonus_points journal.save() lti_grade.replace_result(journal) return response.success( {'journal': JournalSerializer(journal).data}) if not request.user.is_superuser: return response.forbidden( 'You are not allowed to edit this journal.') return self.admin_update(request, journal)
def partial_update(self, request, pk): """Update user role in a course. Arguments: request -- request data user_id -- user ID role -- name of the role (default: Student) pk -- course ID Returns: On failure: unauthorized -- when the user is not logged in not found -- when the perticipation is not found forbidden -- when the user is not connected to the course forbidden -- when the user is not allowed to change the perticipation On success: success -- with the new role name """ user_id, = utils.required_params(request.data, 'user_id') role_name, group_name = utils.optional_params(request.data, 'role', 'group') user = User.objects.get(pk=user_id) course = Course.objects.get(pk=pk) participation = Participation.objects.get(user=user, course=course) request.user.check_permission('can_edit_course_user_group', course) if role_name: request.user.check_permission('can_edit_course_roles', course) participation.role = Role.objects.get(name=role_name, course=course) if group_name: participation.group = Group.objects.get(name=group_name, course=course) elif 'group' in request.data: participation.group = None participation.save() serializer = UserSerializer(participation.user, context={'course': course}) return response.success( {'user': serializer.data}, description='Successfully updated participation.')
def create(self, request): """Create a new assignment. Arguments: request -- request data name -- name of the assignment description -- description of the assignment course_id -- id of the course the assignment belongs to points_possible -- the possible amount of points for the assignment unlock_date -- (optional) date the assignment becomes available on due_date -- (optional) date the assignment is due for lock_date -- (optional) date the assignment becomes unavailable on lti_id -- id labeled link to LTI instance Returns: On failure: unauthorized -- when the user is not logged in not_found -- could not find the course with the given id key_error -- missing keys forbidden -- the user is not allowed to create assignments in this course On success: succes -- with the assignment data """ name, description, course_id = utils.required_params(request.data, "name", "description", "course_id") points_possible, unlock_date, due_date, lock_date, lti_id, is_published = \ utils.optional_params(request.data, "points_possible", "unlock_date", "due_date", "lock_date", "lti_id", "is_published") course = Course.objects.get(pk=course_id) request.user.check_permission('can_add_assignment', course) assignment = factory.make_assignment(name, description, courses=[course], author=request.user, lti_id=lti_id, points_possible=points_possible, unlock_date=unlock_date, due_date=due_date, lock_date=lock_date, is_published=is_published) for user in course.users.all(): factory.make_journal(assignment, user) serializer = AssignmentSerializer(assignment, context={'user': request.user, 'course': course}) return response.created({'assignment': serializer.data})
def partial_update(self, request, *args, **kwargs): """Update an existing journal. Arguments: request -- request data data -- the new data for the journal pk -- journal ID Returns: On failure: unauthorized -- when the user is not logged in not found -- when the journal does not exist forbidden -- User not allowed to edit this journal unauthorized -- when the user is unauthorized to edit the journal bad_request -- when there is invalid data in the request On success: success -- with the new journal data """ pk, = utils.required_typed_params(kwargs, (int, 'pk')) journal = Journal.objects.get(pk=pk) request.user.check_can_view(journal) published, = utils.optional_params(request.data, 'published') if published: request.user.check_permission('can_publish_grades', journal.assignment) return self.publish(request, journal) if not request.user.is_superuser: return response.forbidden( 'You are not allowed to edit this journal.') req_data = request.data if 'published' in req_data: del req_data['published'] serializer = JournalSerializer(journal, data=req_data, partial=True) if not serializer.is_valid(): response.bad_request() serializer.save() return response.success({'journal': serializer.data})
def request_email_verification(request): """Request an email with a verifcation link for the user's email address.""" if request.user.verified_email: return response.success(description='Email address already verified.') email, = utils.optional_params(request.data, 'email') if not email and not request.user.email: return response.bad_request(description='Please provide an email address.') if email and request.user.email != email: request.user.email = email request.user.verified_email = False request.user.save() send_email_verification_link.delay(request.user.pk) return response.success( description='An email was sent to {}, please check your inbox for further \ instructions.'.format(request.user.email))
def check_course_lti(request, user, role): """Check if a course with the lti_id exists. If it does, put the user in the group with the right group and role.""" course_id = request['custom_course_id'] lti_couple = Lti_ids.objects.filter(lti_id=course_id, for_model=Lti_ids.COURSE).first() if not lti_couple: return None course = lti_couple.course lti_id, = utils.optional_params(request, 'custom_section_id') # If the user is participatant, but not yet in a group, put the user in the Canvas related group. if user.is_participant(course): participation = Participation.objects.get(course=course, user=user) if not participation.group and lti_id: groups = Group.objects.filter(lti_id=lti_id, course=course) if groups.exists(): participation.group = groups[0] else: group = factory.make_course_group(lti_id, course, lti_id) participation.group = group participation.save() return course participation = None for r in settings.ROLES: if r in role: participation = factory.make_participation(user, course, Role.objects.get(name=r, course=course)) break if not participation: participation = factory.make_participation(user, course, Role.objects.get(name='Student', course=course)) groups = Group.objects.filter(lti_id=lti_id, course=course) if groups.exists(): participation.group = groups[0] else: group = factory.make_course_group(lti_id, course, lti_id) participation.group = group participation.save() return course
def test_rest(obj, url, create_params=None, delete_params=None, update_params=None, get_params=None, get_is_create=True, user=None, password=userfactory.DEFAULT_PASSWORD, create_status=201, get_status=200, delete_status=200, get_status_when_unauthorized=401): # Create the object that is given create_object = create(obj, url, params=create_params, user=user, password=password, status=create_status) if create_status != 201: return create_object.pop('description', None) pk, = utils.required_typed_params(list(create_object.values())[0], (int, 'id')) # Get that same object if get_params is None: get_params = dict() get(obj, url, params={'pk': pk, **get_params}, status=get_status_when_unauthorized) get_object = get(obj, url, params={'pk': pk, **get_params}, user=user, password=password, status=get_status) get_object.pop('description', None) # Check if the created object is the same as the one it got if get_is_create and get_status == 200: assert create_object == get_object, 'Created object does not equal the get result.' # Update the object if update_params is not None: if not isinstance(update_params, list): update_params = [update_params] for to_update in update_params: changes, status = utils.optional_params(to_update, 'changes', 'status') if changes is None and status is None: changes = to_update if status is None: status = 200 changes['pk'] = pk update_object = update(obj, url, params=changes, user=user, password=password, status=status) update_object.pop('description', None) # Delete the object if delete_params is None: delete_params = dict() delete(obj, url, params={'pk': pk, **delete_params}, user=user, password=password, status=delete_status)
def forgot_password(request): """Handles a forgot password request. Arguments: username -- User claimed username. email -- User claimed email. token -- Django stateless token, invalidated after password change or after a set time (by default three days). Generates a recovery token if a matching user can be found by either the prodived username or email. """ username, email = utils.optional_params(request.data, 'username', 'email') # We are retrieving the username based on either the username or email try: user = User.objects.get(username=username) email = 'your recovery address' except User.DoesNotExist: user = User.objects.get(email=email) email_handling.send_password_recovery_link(user) return response.success( description= 'An email was sent to {}, please follow the email for instructions.'. format(email))
def partial_update(self, request, *args, **kwargs): """Update an existing assignment. Arguments: request -- request data data -- the new data for the assignment pk -- assignment ID Returns: On failure: unauthorized -- when the user is not logged in not found -- when the assignment does not exist forbidden -- User not allowed to edit this assignment unauthorized -- when the user is unauthorized to edit the assignment bad_request -- when there is invalid data in the request On success: success -- with the new assignment data """ pk, = utils.required_typed_params(kwargs, (int, 'pk')) assignment = Assignment.objects.get(pk=pk) request.user.check_permission('can_edit_assignment', assignment) response_data = {} # Remove data that must not be changed by the serializer req_data = request.data if not (request.user.is_superuser or request.user == assignment.author): req_data.pop('author', None) # Check if the assignment can be unpublished is_published, = utils.optional_params(request.data, 'is_published') if not assignment.can_unpublish() and is_published is False: return response.bad_request( 'You cannot unpublish an assignment that already has submissions.' ) active_lti_course, = utils.optional_typed_params( request.data, (int, 'active_lti_course')) if active_lti_course is not None: course = Course.objects.get(pk=active_lti_course) active_lti_id = assignment.get_course_lti_id(course) if active_lti_id: assignment.active_lti_id = active_lti_id assignment.save() # Rename lti id key for serializer if 'lti_id' in req_data: course_id, = utils.required_params(request.data, 'course_id') course = Course.objects.get(pk=course_id) request.user.check_permission('can_add_assignment', course) if course in assignment.courses.all(): return response.bad_request( 'Assignment already used in course.') course.set_assignment_lti_id_set(req_data['lti_id']) course.save() assignment.courses.add(course) assignment.save() for user in User.objects.filter( participation__course=course).exclude( journal__assignment=assignment): factory.make_journal(assignment, user) req_data['active_lti_id'] = req_data.pop('lti_id') # Update the other data serializer = AssignmentSerializer(assignment, data=req_data, context={'user': request.user}, partial=True) if not serializer.is_valid(): return response.bad_request() serializer.save() response_data['assignment'] = serializer.data return response.success(response_data)
def create(self, request): """Create a new entry. Deletes remaining temporary user files if successful. Arguments: request -- the request that was send with journal_id -- the journal id template_id -- the template id to create the entry with node_id -- optional: the node to bind the entry to (only for entrydeadlines) content -- the list of {tag, data} tuples to bind data to a template field. """ journal_id, template_id, content_list = utils.required_params( request.data, "journal_id", "template_id", "content") node_id, = utils.optional_params(request.data, "node_id") journal = Journal.objects.get(pk=journal_id, user=request.user) assignment = journal.assignment template = Template.objects.get(pk=template_id) request.user.check_permission('can_have_journal', assignment) if assignment.is_locked(): return response.forbidden( 'The assignment is locked, entries can no longer be edited/changed.' ) if journal.needs_lti_link(): return response.forbidden(journal.outdated_link_warning_msg) # Check if the template is available if not (node_id or assignment.format.template_set.filter( archived=False, preset_only=False, pk=template.pk).exists()): return response.forbidden('Entry template is not available.') entry_utils.check_fields(template, content_list) # Node specific entry if node_id: node = Node.objects.get(pk=node_id, journal=journal) entry = entry_utils.add_entry_to_node(node, template) # Template specific entry else: entry = factory.make_entry(template) node = factory.make_node(journal, entry) for content in content_list: field_id, = utils.required_typed_params(content, (int, 'id')) data, = utils.required_params(content, 'data') field = Field.objects.get(pk=field_id) created_content = factory.make_content(node.entry, data, field) if field.type in field.FILE_TYPES: # Image, file or PDF user_file = file_handling.get_temp_user_file( request.user, assignment, content['data']) if user_file is None and field.required: node.entry.delete() # If there is a newly created node, delete that as well if not node_id: node.delete() return response.bad_request( 'One of your files was not correctly uploaded, please try again.' ) elif user_file: file_handling.make_permanent_file_content( user_file, created_content, node) # Notify teacher on new entry if (node.journal.sourcedid and node.entry.vle_coupling == Entry.NEED_SUBMISSION): lti_tasks.needs_grading.delay(node.pk) # Delete old user files file_handling.remove_temp_user_files(request.user) return response.created({ 'added': entry_utils.get_node_index(journal, node, request.user), 'nodes': timeline.get_nodes(journal, request.user), 'entry': serialize.EntrySerializer(entry, context={ 'user': request.user }).data })
def create(self, request): """Create a new user. Arguments: request -- request data username -- username password -- password first_name -- (optinal) first name last_name -- (optinal) last name email -- (optinal) email jwt_params -- (optinal) jwt params to get the lti information from user_id -- id of the user user_image -- user image roles -- role of the user Returns: On failure: unauthorized -- when the user is not logged in bad request -- when email/username/lti id already exists bad request -- when email/password is invalid On succes: success -- with the newly created user data """ lti_id, user_image, full_name, email, is_teacher = get_lti_params( request, 'user_id', 'custom_user_image', 'custom_user_full_name', 'custom_user_email') if full_name: first_name, last_name = lti.split_fullname(full_name) if lti_id is None: # Check if instance allows standalone registration if user did not register through some LTI instance try: instance = Instance.objects.get(pk=1) if not instance.allow_standalone_registration: return response.bad_request(('{} does not allow you to register through the website,' + ' please use an LTI instance.').format(instance.name)) except Instance.DoesNotExist: pass first_name, last_name, email = utils.optional_params(request.data, 'first_name', 'last_name', 'email') username, password = utils.required_params(request.data, 'username', 'password') if email and User.objects.filter(email=email).exists(): return response.bad_request('User with this email already exists.') validate_email(email) if User.objects.filter(username=username).exists(): return response.bad_request('User with this username already exists.') if lti_id is not None and User.objects.filter(lti_id=lti_id).exists(): return response.bad_request('User with this lti id already exists.') validators.validate_password(password) user = factory.make_user(username, password, email=email, lti_id=lti_id, is_teacher=is_teacher, first_name=first_name, last_name=last_name, profile_picture=user_image, verified_email=True if lti_id else False) if lti_id is None: try: email_handling.send_email_verification_link(user) except SMTPAuthenticationError as err: user.delete() raise err return response.created({'user': UserSerializer(user).data})
def partial_update(self, request, *args, **kwargs): """Update an existing user. Arguments: request -- request data jwt_params -- jwt params to get the lti information from user_id -- id of the user user_image -- user image roles -- role of the user pk -- user ID Returns: On failure: unauthorized -- when the user is not logged in forbidden -- when the user is not superuser or pk is not the same as the logged in user not found -- when the user doesnt exists bad request -- when the data is invalid On success: success -- with the updated user """ pk, = utils.required_typed_params(kwargs, (int, 'pk')) if pk == 0: pk = request.user.pk if not (request.user.pk == pk or request.user.is_superuser): return response.forbidden() user = User.objects.get(pk=pk) lti_id, user_email, user_full_name, user_image, is_teacher = get_lti_params( request, 'user_id', 'custom_user_email', 'custom_user_full_name', 'custom_user_image') if user_image is not None: user.profile_picture = user_image if user_email is not None: user.email = user_email user.verified_email = True if user_full_name is not None: user.first_name, user.last_name = lti.split_fullname(user_full_name) if is_teacher: user.is_teacher = is_teacher if lti_id is not None: if User.objects.filter(lti_id=lti_id).exists(): return response.bad_request('User with this lti id already exists.') user.lti_id = lti_id user.save() if user.lti_id is not None: gn, cn, pp = utils.optional_params( request.data, 'grade_notifications', 'comment_notifications', 'profile_picture') data = { 'grade_notifications': gn if gn else user.grade_notifications, 'comment_notifications': cn if cn else user.comment_notifications, 'profile_picture': pp if pp else user.profile_picture } else: data = request.data serializer = OwnUserSerializer(user, data=data, partial=True) if not serializer.is_valid(): return response.bad_request() serializer.save() return response.success({'user': serializer.data})
def partial_update(self, request, pk): """Update an existing journal format. Arguments: request -- request data templates -- the list of templates to bind to the format presets -- the list of presets to bind to the format removed_presets -- presets to be removed removed_templates -- templates to be removed pk -- assignment ID Returns: On failure: unauthorized -- when the user is not logged in not found -- when the assignment does not exist forbidden -- User not allowed to edit this assignment unauthorized -- when the user is unauthorized to edit the assignment bad_request -- when there is invalid data in the request On success: success -- with the new assignment data """ assignment_details, templates, presets, removed_templates, removed_presets \ = utils.required_params(request.data, 'assignment_details', 'templates', 'presets', 'removed_templates', 'removed_presets') assignment = Assignment.objects.get(pk=pk) format = assignment.format request.user.check_permission('can_edit_assignment', assignment) # Check if the assignment can be unpublished is_published, = utils.optional_params(assignment_details, 'is_published') if not assignment.can_unpublish() and is_published is False: return response.bad_request("You cannot unpublish an assignment that already has submissions.") # Remove data that must not be changed by the serializer req_data = assignment_details or {} req_data.pop('published', None) req_data.pop('author', None) for key in req_data: if req_data[key] == '': req_data[key] = None # Update the assignment details serializer = AssignmentDetailsSerializer(assignment, data=req_data, context={'user': request.user}, partial=True) if not serializer.is_valid(): return response.bad_request('Invalid data.') serializer.save() new_ids = utils.update_templates(format, templates) utils.update_presets(assignment, presets, new_ids) utils.delete_presets(removed_presets) utils.archive_templates(removed_templates) serializer = FormatSerializer(format) assignment_details = AssignmentDetailsSerializer(assignment, context={'user': request.user}) return response.success({'format': serializer.data, 'assignment_details': assignment_details.data})
def partial_update(self, request, *args, **kwargs): """Update an existing user. Arguments: request -- request data jwt_params -- jwt params to get the lti information from user_id -- id of the user user_image -- user image roles -- role of the user pk -- user ID Returns: On failure: unauthorized -- when the user is not logged in forbidden -- when the user is not superuser or pk is not the same as the logged in user not found -- when the user doesnt exists bad request -- when the data is invalid On success: success -- with the updated user """ pk, = utils.required_typed_params(kwargs, (int, 'pk')) if pk == 0: pk = request.user.pk if not (request.user.pk == pk or request.user.is_superuser): return response.forbidden() user = User.objects.get(pk=pk) lti_id, user_email, user_full_name, user_image, is_teacher = get_lti_params( request, 'user_id', 'custom_user_email', 'custom_user_full_name', 'custom_user_image') if user_image is not None: user.profile_picture = user_image if user_email: if User.objects.filter(email=user_email).exclude( pk=user.pk).exists(): return response.bad_request( '{} is taken by another account. Link to that account or contact support.' .format(user_email)) user.email = user_email user.verified_email = True if user_full_name is not None: user.full_name = user_full_name if is_teacher: user.is_teacher = is_teacher if lti_id is not None: if User.objects.filter(lti_id=lti_id).exists(): return response.bad_request( 'User with this lti id already exists.') elif (bool(lti_id) and not bool(user_email) and user_full_name == settings.LTI_TEST_STUDENT_FULL_NAME or user.is_test_student): return response.forbidden( 'You are not allowed to link a test account to an existing account.' ) user.lti_id = lti_id user.save() if user.lti_id is not None: pp, = utils.optional_params(request.data, 'profile_picture') data = {'profile_picture': pp if pp else user.profile_picture} else: data = request.data serializer = OwnUserSerializer(user, data=data, partial=True) if not serializer.is_valid(): return response.bad_request() serializer.save() return response.success({'user': serializer.data})