from rest_framework import viewsets from rest_framework import mixins import django_filters from rest_framework import filters from rest_framework.response import Response from rest_framework import status from rest_framework.renderers import JSONRenderer from rest_framework.authentication import SessionAuthentication, BasicAuthentication from sms_drill.models import ( Word, Form, Semtype, Tag, Tagset, Tagname ) # from .data_permissions import * from .serializers import * # from .data_utils import * class WordView(viewsets.ModelViewSet): """ This view powers the display of goal progress when the user is submitting answers for a course goal. This is mainly intended as an internal API for learning exercises within Oahpa. """ # permission_classes = (IsAuthenticated, GetOnly) model = Word serializer_class = WordSerializer filter_backends = (filters.SearchFilter,) search_fields = ('=lemma', '=pos') def list(self, request): """ GET request. """ rq = super(WordView, self).list(request) return rq def filter_queryset(self, queryset): """ This view should return a list of all the goals for the currently authenticated user. """ print self.request.QUERY_PARAMS semtypes = False sems = self.request.QUERY_PARAMS.get('semtypes', None) if sems: semtypes = sems.split(',') ps = { 'pos': self.request.QUERY_PARAMS.get('pos', None), 'form__tag__string': self.request.QUERY_PARAMS.get('tag', None), } if semtypes: ps['semtype__semtype__in'] = semtypes kwargs = {} for k, v in ps.iteritems(): if v is not None: kwargs[k] = v print kwargs return queryset.filter(**kwargs) def metadata(self, request): """ This returns stuff when the OPTIONS verb is used, but we're coopting it slightly to include the parameters for the form, which need to be automatically constructed for each installation. If the user is an instructor in any courses, these are included and rendered in the form. """ data = super(WordView, self).metadata(request) poses = list(set([ t.tagname for t in Tagname.objects.filter(tagset__tagset='Wordclass') ])) vals = { 'note': "Not for validation purposes.", 'semtypes': Semtype.objects.all().values_list('semtype', flat=True), 'pos': poses, 'tags': Tag.objects.values_list('string', flat=True) } data['possible_values'] = vals return data class FormView(viewsets.ModelViewSet): """ This view powers the display of goal progress when the user is submitting answers for a course goal. This is mainly intended as an internal API for learning exercises within Oahpa. @param fullform @param tag @param lemma """ # permission_classes = (IsAuthenticated, GetOnly) model = Form serializer_class = FormSerializer # filter_backends = (filters.SearchFilter,) # search_fields = ('fullform', 'tag__string', 'word__lemma') def filter_queryset(self, queryset): """ This view should return a list of all the goals for the currently authenticated user. """ ps = { 'fullform': self.request.QUERY_PARAMS.get('fullform', None), 'tag__string': self.request.QUERY_PARAMS.get('tag', None), 'word__lemma': self.request.QUERY_PARAMS.get('lemma', None), } kwargs = {} for k, v in ps.iteritems(): if v is not None: kwargs[k] = v return queryset.filter(**kwargs) ### class FeedbackLogView(viewsets.ModelViewSet): ### """ These views are for adding feedback logs when the user clicks on ### a feedback link. ### ### This is mainly intended as an internal API for learning exercises ### within Oahpa. ### """ ### authentication_classes = (SessionAuthentication, BasicAuthentication) ### permission_classes = (CanCreateAndUpdateFeedbackLog, ) ### model = UserFeedbackLog ### serializer_class = FeedbackLogSerializer ### ### def create(self, request): ### data = request.DATA ### ### with_ids = [] ### ### for d in data: ### _d = d.copy() ### _d['user'] = request.user.id ### with_ids.append(_d) ### ### fb_texts = list(set([a.get('feedback_texts') for a in with_ids if a.get('feedback_texts', False)])) ### ### if len(fb_texts) > 0: ### print UserFeedbackLog.levels.get_user_level(request.user, fb_texts) ### ### serialized = self.serializer_class(data=with_ids, many=True) ### ### if serialized.is_valid(): ### serialized.save() ### headers = self.get_success_headers(serialized.data) ### return Response(serialized.data, status=status.HTTP_201_CREATED, headers=headers) ### ### return Response(serialized.errors, status=status.HTTP_400_BAD_REQUEST) ### ### def get_queryset(self): ### user = self.request.user ### return self.model.objects.filter(user=user) ### ### class CourseGoalView(viewsets.ModelViewSet): ### """ This is intended as an internal view for Oahpa only. ### ### This view set allows a client to manage Course Goals. ### """ ### ### authentication_classes = (SessionAuthentication, BasicAuthentication) ### permission_classes = (CanCreateAndUpdateCourseGoal, ) ### queryset = CourseGoal.objects.all() ### ### serializer_class = CourseGoalSerializer ### ### def get_queryset(self): ### user = self.request.user ### # TODO: also return coursegoals for users instructorships ### return self.queryset.filter(created_by=user) ### ### def update(self, request, pk=None, partial=False): ### response_params = {} ### errors = [] ### ### success = True ### data = request.DATA ### ### try: data.pop('id') ### except: pass ### ### goals = data.pop('goals') ### ### try: ### obj = self.queryset.filter(pk=pk).update(**data) ### except Exception, e: ### success = False ### errors.append(repr(e)) ### ### obj = self.queryset.get(pk=pk) ### ### goal_objs = Goal.objects.filter(id__in=goals) ### ### try: ### CourseGoalGoal.objects.filter(coursegoal=obj).delete() ### for g in goal_objs: ### CourseGoalGoal.objects.create(coursegoal=obj, goal=g) ### except Exception, e: ### errors.append(repr(e)) ### ### success = True ### if not success: ### response_params['errors'] = errors ### ### response_params['success'] = success ### ### return Response(response_params) ### ### def create(self, request): ### # TODO: this can probably be generalized with the serializer now ### ### # TODO: doublecheck that course_id within ### # request.user.get_profile().instructorships if course is ### # specified? ### ### success = True ### response_parameters = {} ### ### new_obj = request.DATA ### new_obj['created_by'] = request.user ### new_obj['course_id'] = new_obj.pop('course') ### new_goal = CourseGoal.objects.create(**new_obj) ### ### response_parameters = { ### 'success': success, ### } ### ### if success: ### response_parameters['id'] = new_goal.id ### response_parameters['goal'] = self.serializer_class(data=new_goal).data ### ### return Response(response_parameters) ### ### def metadata(self, request): ### """ This returns stuff when the OPTIONS verb is used, ### but we're coopting it slightly to include the parameters for the ### form, which need to be automatically constructed for each ### installation. ### ### If the user is an instructor in any courses, these are included ### and rendered in the form. ### """ ### ### data = super(CourseGoalView, self).metadata(request) ### ### data['courses'] = [ ### {'value': c.id, 'label': c.name} ### for c in request.user.get_profile().instructorships ### ] ### ### return data ### ### class GoalParametersView(viewsets.ModelViewSet): ### """ These views are for creating, editing and deleting users' ### personal goals, as well as goals connected to courses. ### ### Django admin wasn't really enough to manage this kind of thing. ### OPTIONS provides a set of possible parameters, and POST and PUT are ### available for creating and editing. DELETE is also possible, and ### permissions are checked. ### ### This is intended as an internal API for Oahpa only. ### """ ### ### authentication_classes = (SessionAuthentication, BasicAuthentication) ### permission_classes = (CanCreateAndUpdateGoal, ) ### queryset = Goal.objects.all() ### ### serializer_class = GoalSerializer ### ### def get_queryset(self): ### """ Always filter by user, because this only concerns ### creating/editing goals, not exhaustively listing all goals ### available to the user via classes. ### """ ### user = self.request.user ### return self.queryset.filter(created_by=user) ### ### def list(self, request): ### """ ### This view should return a list of all the goals ### for the currently authenticated user. ### """ ### ### goals = self.get_queryset() ### ### response_parameters = {} ### response_parameters['success'] = True ### response_parameters['goals'] = self.serializer_class(goals).data ### ### return Response(response_parameters) ### ### @property ### def exercise_type_url_bases(self): ### return dict([ ### (subtype.get('value'), subtype.get('path')) ### for k, v in prepare_goal_params()[0].iteritems() ### for subtype in v.get('subtypes', []) ### ]) ### ### def update(self, request, pk=None): ### """ When the PUT verb is used, this is called. ### """ ### success = True ### ### response_parameters = {} ### ### new_obj = request.DATA ### ### # Pop some kwargs not available in the model ### new_obj.pop('id') ### new_obj.pop('begin_url') ### ### # grab the URL base of the exercise ### url_base = self.exercise_type_url_bases.get(new_obj.get('sub_type')) ### new_obj['url_base'] = url_base ### ### # Pop params, because we iterate later. ### params = new_obj.pop('params') ### ### # NB: queryset already filtered for user ### ### # Actually update the main object, easier to do as a queryset. ### try: ### goal = self.queryset.filter(pk=pk) ### goal.update(**new_obj) ### except Exception, e: ### success = False ### ### goal = goal[0] ### ### # If the update worked, remove the GoalParameters and re-add ### # them. ### if success: ### GoalParameter.objects.filter(goal=goal).delete() ### for p_k, p_v in params.iteritems(): ### goal.params.create(parameter=p_k, value=p_v) ### ### # Reset user progress. ### goal.usergoalinstance_set.all().delete() ### ### if success: ### # re-serialize the created goal for return. ### response_parameters['goal'] = self.serializer_class(goal).data ### else: ### response_parameters['error'] = "Could not update the goal." ### ### response_parameters['success'] = success ### ### return Response(response_parameters) ### ### def create(self, request): ### # TODO: this can probably be generalized with the serializer now ### ### # TODO: doublecheck that course_id within ### # request.user.get_profile().instructorships if course is ### # specified? ### ### response_parameters = {} ### new_obj = request.DATA ### params = new_obj.pop('params') ### ### errors = [] ### ### # use main_type to get url path ### url_base = self.exercise_type_url_bases.get(new_obj.get('sub_type')) ### new_obj['url_base'] = url_base ### ### success = True ### ### goal = Goal.objects.create(created_by=request.user, **new_obj) ### ### if success: ### for p_k, p_v in params.iteritems(): ### goal.params.create(parameter=p_k, value=p_v) ### ### from django.core.urlresolvers import reverse ### ### if success: ### response_parameters['goal'] = {} ### response_parameters['goal']['id'] = goal.id ### response_parameters['goal']['short_name'] = goal.short_name ### response_parameters['goal']['begin_url'] = goal.begin_url ### else: ### response_parameters['error'] = "Could not create the goal." ### response_parameters['errors'] = errors ### ### response_parameters['success'] = success ### ### return Response(response_parameters) ### ### def metadata(self, request): ### """ This returns stuff when the OPTIONS verb is used, ### but we're coopting it slightly to include the parameters for the ### form, which need to be automatically constructed for each ### installation. ### ### If the user is an instructor in any courses, these are included ### and rendered in the form. ### """ ### ### data = super(GoalParametersView, self).metadata(request) ### choice_tree, parameter_values = prepare_goal_params(request) ### parameters = {'tree': choice_tree, 'values': parameter_values} ### data['parameters'] = parameters ### # data['courses'] = [(c.id, c.name) for c in request.user.get_profile().instructorships] ### return data ### ### from notifications.models import Notification ### ### # TODO: test permissions ### # - allows to delete also ? ### class NotificationsView(viewsets.ModelViewSet): ### """ This is intended as an internal API for Oahpa only. ### ### This is for listing and creating notifications, to be displayed to ### users on their course overview page. ### """ ### ### model = Notification() ### queryset = Notification.objects.all() ### serializer_class = NotificationSerializer ### permission_classes = (CanCreateAndUpdateNotification, ) ### ### def get_queryset(self): ### return self.queryset.filter(recipient=self.request.user).unread() ### ### def create(self, request): ### from notifications import notify ### ### notify.send(request.user, ### recipient=request.user, ### description=request.DATA.get('description'), ### verb=u'tested notifications',) ### ### return Response({'success': True}) ### ### def equal_url_base(a, b): ### """ A helper function for equating URL strings via the urlparse ### library. For a URL to match, the scheme, location, and path must all ### be the same, i.e., the following will be equivalent: ### ### http://oahpa.no/path/to/page/ ### http://oahpa.no/path/to/page/?param=foo ### ### TODO: do we require a.query == b.query as option? ### """ ### ### import urlparse ### ### _a = urlparse.urlparse(a) ### _b = urlparse.urlparse(b) ### ### return all([ ### _a.scheme == _b.scheme, ### _a.netloc == _b.netloc, ### _a.path == _b.path, ### ]) ### ### ### from data_authentication import CookieAuthentication ### from rest_framework.authentication import SessionAuthentication ### ### from schematics.models import Model as SchematicsModel ### from schematics.types import StringType, DecimalType, BooleanType ### from schematics.exceptions import ModelValidationError ### ### from univ_drill.models import Log ### ### class SubmissionMixin(object): ### """ This contains some helper functions not immediately related to ### REST operations. Functions here fall outside of important functions ### for developers working with the Submission API. ### ### For documentation pertaining to the Submission API, inspect the ### `SubmissionView` class. """ ### ### def create_logs_for_request(self, submission): ### import datetime ### ### request = self.request ### ### today = datetime.date.today() ### ### log_kwargs = { ### 'userinput': submission.user_input, ### 'correct': ','.join(submission.correct), ### 'iscorrect': submission.iscorrect, ### # 'example': self.example, ### 'game': "Task: %d" % submission.task_id, ### 'date': today ### } ### ### if request.session.has_key('country'): ### log_kwargs['user_country'] = request.session['country'] ### else: ### log_kwargs['user_country'] = False ### ### if self.request.user: ### log_kwargs['username'] = self.request.user.username ### ### log = Log.objects.create(**log_kwargs) ### ### if log.correct not in request.session['question_try_count']: ### request.session['question_try_count'][log.correct] = 1 ### ### request.session['question_try_count'][log.correct] += 1 ### request.session['answered'][log.correct] = True ### request.session['question_try_count'][log.correct] = 1 ### ### print request.session ### ### return [log] ### ### def validate_goal_request(self): ### """ This checks URL parameters, and validates the JSON passed in ### the requests. If the JSON is invalid, or the referer does not ### match the goal's referer URL, a response will be returned with ### the errors. ### ### """ ### import urlparse ### ### from schematics.types.compound import ListType, ModelType ### ### class Submission(SchematicsModel): ### task_id = DecimalType(required=True) ### user_input = StringType(required=True) ### correct = ListType(StringType, required=True) ### iscorrect = BooleanType(required=True) ### ### if 'HTTP_REFERER' not in self.request.META: ### msg = ['Missing referer address'] ### return False, Response({'success': False, 'errors': msg}) ### ### refer = self.request.META['HTTP_REFERER'] ### task_id = self.request.DATA['task_id'] ### json = self.request.DATA ### ### self.task = Goal.objects.get(id=task_id) ### task_url = self.task.remote_page ### ### if self.task.remote_task == True and equal_url_base(self.task.remote_page, refer): ### sub = Submission(self.request.DATA) ### try: ### sub.validate() ### except ModelValidationError, e: ### return False, Response({'success': False, 'errors': e.messages}) ### return sub, None ### else: ### self.errors = [ ### 'Referer does not match task whitelist.', ### ] ### return False, Response({'success': False, 'errors': errors}) ### ### ### def evaluate_user_response(self, submission): ### # TODO: iscorrect validation ### ### self.request.user_logs_generated = self.create_logs_for_request(submission) ### ### goal_instance = self.get_or_create_goal_instance() ### ### ual = create_activity_log_from_drill_logs( ### self.request, ### self.request.user, ### self.request.user_logs_generated, ### current_user_goal=goal_instance.id) ### ### result = goal_instance.evaluate_instance() ### ### if result is not None: ### try: result.pop('progress_pretty') ### except: pass ### ### try: result.pop('correct_minus_first') ### except: pass ### ### try: result.pop('correct_later_tries') ### except: pass ### ### # This also marks the user goal instance as complete. ### complete = self.task.is_complete(goal_instance) ### ### response_data = { ### 'success': True, ### 'evaluation': result, ### 'complete': complete, ### } ### ### return response_data ### ### def get_or_create_goal_instance(self): ### ### if self.request.method == 'POST': ### prev = UserGoalInstance.objects.filter(goal=self.task, ### opened=True) ### prev.update(opened=False) ### ### ugi = UserGoalInstance.objects.create(user=self.request.user, ### goal=self.task) ### ### self.request.session['current_user_goal'] = int(ugi.id) ### self.request.session['max_rounds'] = self.task.minimum_sets_attempted ### self.request.session['rounds'] = 1 ### ### elif self.request.method == 'PUT': ### self.current_user_goal = self.request.session['current_user_goal'] ### ugi = UserGoalInstance.objects.get(id=self.current_user_goal, ### opened=True) ### self.request.session['rounds'] += 1 ### ugi.attempt_count += 1 ### ugi.save() ### ### # TODO: rounds isn't showing up in the output, bu that's an ### # evaluation problem, not a problem with this variable ### # print self.request.session['rounds'] ### # print self.request.session['max_rounds'] ### return ugi ### ### class SubmissionView(SubmissionMixin, viewsets.ModelViewSet): ### """ This view is for logging user progress on external activities. ### The activities must be registered first in the database with a Task, ### marked as external, and the URL that the task will be completed on ### must be registered as well. The referrer will be used in order to ### construct the list of Tasks available from any given page. ### ### In constructing the reporting system on the client side, using ### JavaScript, a typical workflow would be the following: ### ### 1.) a GET request to the submission endpoint to determine what ### activities are available for the page. ### ### 2.) Storing the Task ID for the desired Task to associate with ### the user's activity. ### ### 3.) A POST request with the first set of user data. ### ### 4.) PUT requests for any subsequent activity on for the same ### instance of the work. ### ### This means thus, that if the user resets their progress or restarts, ### a _new_ POST request must be made to mark the new beginning. ### ### **Authentication** ### ### Use of this API relies on a valid cookie: the user must have logged ### into Oahpa! Courses before navigating away to perform the intended ### exercise. ### ### In order to submit however, you will still need proof of ### authentication and the CSRF token. This can be included by adding ### the `X-CSRFToken` header to POST and PUT requests. This token will ### be set on the cookie when the user logs in, and the cookie will be ### accessible via JavaScript. An example from Angular.js with ng-cookies: ### ### $http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken; ### $http.defaults.headers.put['X-CSRFToken'] = $cookies.csrftoken; ### ### **Notes** ### ### NB: end users are always free to intercept requests and submit ### whatever they want. ### ### NB: since this is a "public" (=used by other apps) API for use ### within the Oahpa subdomain, note the authentication_classes. ### ### TODO: submitting sub-tasks. e.g., if external goal contains 3 ### questions, how to track these ### ### """ ### ### model = UserGoalInstance ### queryset = UserGoalInstance.objects.all() ### ### def get_queryset(self): ### return self.queryset.filter(user=self.request.user) ### ### def put(self, request): ### """ This updates an existing goal instance for work in progress. ### ### PUT: json including the following data ### ### * `task_id` - this is the task that will be submitted to ### ### NB: task ID can either be found through API, or found ### within the courses admin interface ### ### * `user_input` - this is the user's response ### * `correct` - a comma separated list of correct answers to ### the question prompt ### * `iscorrect` - did the external app find the response to be ### correct? ### ### """ ### ### submission, resp = self.validate_goal_request() ### if not submission: ### return resp ### ### response_data = self.evaluate_user_response(submission) ### return Response(response_data) ### ### def list(self, request): ### """ Sending a GET request to this path will return a list of ### activities available for the current referrer address. If the ### referrer address is not available, it is also possible to ### include an `address` parameter in the URL path. """ ### ### if 'HTTP_REFERER' not in request.META and 'address' not in request.QUERY_PARAMS: ### msg = ['Missing referer address'] ### return Response({'success': False, 'errors': msg}) ### ### if 'HTTP_REFERER' in request.META: ### refer = request.META['HTTP_REFERER'] ### elif 'address' in request.QUERY_PARAMS: ### refer = request.QUERY_PARAMS['address'] ### ### tasks = Goal.objects.filter(remote_task=True).values_list('id', 'remote_page') ### matching_tasks = [] ### ### for _id, _base in tasks: ### base = _base ### if equal_url_base(refer, base): ### matching_tasks.append(_id) ### ### if len(matching_tasks) > 0: ### matching_objs = Goal.objects.filter(id__in=matching_tasks) ### serialized = GoalSerializer(matching_objs).data ### else: ### serialized = [] ### ### return Response({ ### 'success': True, ### 'goals_available': serialized ### }) ### ### def get(self, request): ### """ There is no need to retrieve individual objects by ID. ### """ ### pass ### ### def create(self, request): ### """ For new tasks. ### ### POST: json including the following data ### ### * `user_input` - this is the user's response ### * `correct` - a comma separated list of correct answers to ### the question prompt ### * `iscorrect` - did the external app find the response to be ### correct? ### ### """ ### ### request.session['question_try_count'] = {} ### request.session['question_set_count'] = 1 ### request.session['answered'] = {} ### ### submission, resp = self.validate_goal_request() ### if not submission: ### return resp ### ### response_data = self.evaluate_user_response(submission) ### ### return Response(response_data)