diff options
Diffstat (limited to 'yaksh/models.py')
-rw-r--r-- | yaksh/models.py | 318 |
1 files changed, 186 insertions, 132 deletions
diff --git a/yaksh/models.py b/yaksh/models.py index 7e0ce16..b917889 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -1,14 +1,10 @@ from __future__ import unicode_literals from datetime import datetime, timedelta import json -from random import sample, shuffle -from itertools import islice, cycle +from random import sample from collections import Counter -from textwrap import dedent from django.db import models -from django.db.models import Q from django.contrib.auth.models import User -from django.forms.models import model_to_dict from django.contrib.contenttypes.models import ContentType from taggit.managers import TaggableManager from django.utils import timezone @@ -20,7 +16,7 @@ except ImportError: import pytz import os import stat -from os.path import join, abspath, dirname, exists +from os.path import join, exists import shutil import zipfile import tempfile @@ -39,7 +35,6 @@ languages = ( ("scilab", "Scilab"), ) - question_types = ( ("mcq", "Multiple Choice"), ("mcc", "Multiple Correct Choices"), @@ -68,8 +63,12 @@ test_status = ( ('completed', 'Completed'), ) + def get_assignment_dir(instance, filename): - return '%s/%s/%s' % (instance.user.user, instance.assignmentQuestion.id, filename) + return os.sep.join(( + instance.user.user, instance.assignmentQuestion.id, filename + )) + def get_model_class(model): ctype = ContentType.objects.get(app_label="yaksh", model=model) @@ -77,12 +76,16 @@ def get_model_class(model): return model_class + def has_profile(user): """ check if user has profile """ return True if hasattr(user, 'profile') else False + def get_upload_dir(instance, filename): - return "question_%s/%s" % (instance.question.id, filename) + return os.sep.join(( + 'question_%s' % (instance.question.id), filename + )) ############################################################################### @@ -194,11 +197,13 @@ class Course(models.Model): def __str__(self): return self.name + ############################################################################### class ConcurrentUser(models.Model): concurrent_user = models.OneToOneField(User) session_key = models.CharField(max_length=40) + ############################################################################### class Profile(models.Model): """Profile for a user to store roll number and other details.""" @@ -207,15 +212,19 @@ class Profile(models.Model): institute = models.CharField(max_length=128) department = models.CharField(max_length=64) position = models.CharField(max_length=64) - timezone = models.CharField(max_length=64, - default=pytz.utc.zone, - choices=[(tz, tz) for tz in pytz.common_timezones] - ) + timezone = models.CharField( + max_length=64, + default=pytz.utc.zone, + choices=[(tz, tz) for tz in pytz.common_timezones] + ) def get_user_dir(self): """Return the output directory for the user.""" user_dir = join(settings.OUTPUT_DIR, str(self.user.username)) + if not exists(user_dir): + os.makedirs(user_dir) + os.chmod(user_dir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) return user_dir @@ -273,32 +282,36 @@ class Question(models.Model): files = FileUpload.objects.filter(question=self) if files: metadata['file_paths'] = [(file.file.path, file.extract) - for file in files] + for file in files] question_data['metadata'] = metadata - return json.dumps(question_data) def dump_questions(self, question_ids, user): - questions = Question.objects.filter(id__in=question_ids, user_id=user.id, active=True) + questions = Question.objects.filter( + id__in=question_ids, user_id=user.id, active=True + ) questions_dict = [] zip_file_name = string_io() zip_file = zipfile.ZipFile(zip_file_name, "a") for question in questions: test_case = question.get_test_cases() file_names = question._add_and_get_files(zip_file) - q_dict = {'summary': question.summary, - 'description': question.description, - 'points': question.points, 'language': question.language, - 'type': question.type, 'active': question.active, - 'snippet': question.snippet, - 'testcase': [case.get_field_value() for case in test_case], - 'files': file_names} + q_dict = { + 'summary': question.summary, + 'description': question.description, + 'points': question.points, 'language': question.language, + 'type': question.type, 'active': question.active, + 'snippet': question.snippet, + 'testcase': [case.get_field_value() for case in test_case], + 'files': file_names + } questions_dict.append(q_dict) question._add_json_to_zip(zip_file, questions_dict) return zip_file_name - def load_questions(self, questions_list, user, file_path=None, files_list=None): + def load_questions(self, questions_list, user, file_path=None, + files_list=None): questions = json.loads(questions_list) for question in questions: question['user'] = user @@ -310,7 +323,10 @@ class Question(models.Model): for test_case in test_cases: test_case_type = test_case.pop('test_case_type') model_class = get_model_class(test_case_type) - new_test_case, obj_create_status = model_class.objects.get_or_create(question=que, **test_case) + new_test_case, obj_create_status = \ + model_class.objects.get_or_create( + question=que, **test_case + ) new_test_case.type = test_case_type new_test_case.save() if files_list: @@ -331,7 +347,8 @@ class Question(models.Model): def get_test_case(self, **kwargs): for tc in self.testcase_set.all(): test_case_type = tc.type - test_case_ctype = ContentType.objects.get(app_label="yaksh", + test_case_ctype = ContentType.objects.get( + app_label="yaksh", model=test_case_type ) test_case = test_case_ctype.get_object_for_this_type( @@ -387,11 +404,12 @@ class Question(models.Model): self.load_questions(questions_list, user, file_path, files) def create_demo_questions(self, user): - zip_file_path = os.path.join(settings.FIXTURE_DIRS, 'demo_questions.zip') + zip_file_path = os.path.join( + settings.FIXTURE_DIRS, 'demo_questions.zip' + ) files, extract_path = extract_files(zip_file_path) self.read_json(extract_path, user, files) - def __str__(self): return self.summary @@ -424,6 +442,7 @@ class FileUpload(models.Model): self.hide = True self.save() + ############################################################################### class Answer(models.Model): """Answers submitted by the users.""" @@ -474,13 +493,14 @@ class QuizManager(models.Manager): def create_trial_from_quiz(self, original_quiz_id, user, godmode): """Creates a trial quiz from existing quiz""" - trial_quiz_name = "Trial_orig_id_{0}_{1}".format(original_quiz_id, - "godmode" if godmode else "usermode" - ) - + trial_quiz_name = "Trial_orig_id_{0}_{1}".format( + original_quiz_id, + "godmode" if godmode else "usermode" + ) + if self.filter(description=trial_quiz_name).exists(): trial_quiz = self.get(description=trial_quiz_name) - + else: trial_quiz = self.get(id=original_quiz_id) trial_quiz.course.enroll(False, user) @@ -494,12 +514,13 @@ class QuizManager(models.Manager): trial_quiz.attempts_allowed = -1 trial_quiz.active = True trial_quiz.start_date_time = timezone.now() - trial_quiz.end_date_time = datetime(2199, 1, 1, 0, 0, 0, 0, - tzinfo=pytz.utc - ) + trial_quiz.end_date_time = datetime( + 2199, 1, 1, 0, 0, 0, 0, tzinfo=pytz.utc + ) trial_quiz.save() return trial_quiz + ############################################################################### class Quiz(models.Model): """A quiz that students will participate in. One can think of this @@ -509,18 +530,21 @@ class Quiz(models.Model): course = models.ForeignKey(Course) # The start date of the quiz. - start_date_time = models.DateTimeField("Start Date and Time of the quiz", - default=timezone.now(), - null=True) + start_date_time = models.DateTimeField( + "Start Date and Time of the quiz", + default=timezone.now(), + null=True + ) # The end date and time of the quiz - end_date_time = models.DateTimeField("End Date and Time of the quiz", - default=datetime(2199, 1, 1, - tzinfo=pytz.timezone - (timezone. - get_current_timezone_name()) - ), - null=True) + end_date_time = models.DateTimeField( + "End Date and Time of the quiz", + default=datetime( + 2199, 1, 1, + tzinfo=pytz.timezone(timezone.get_current_timezone_name()) + ), + null=True + ) # This is always in minutes. duration = models.IntegerField("Duration of quiz in minutes", default=20) @@ -544,8 +568,9 @@ class Quiz(models.Model): # Number of attempts for the quiz attempts_allowed = models.IntegerField(default=1, choices=attempts) - time_between_attempts = models.IntegerField("Number of Days",\ - choices=days_between_attempts) + time_between_attempts = models.IntegerField( + "Number of Days", choices=days_between_attempts + ) is_trial = models.BooleanField(default=False) @@ -560,7 +585,6 @@ class Quiz(models.Model): class Meta: verbose_name_plural = "Quizzes" - def is_expired(self): return not self.start_date_time <= timezone.now() < self.end_date_time @@ -568,16 +592,18 @@ class Quiz(models.Model): return True if self.prerequisite else False def create_demo_quiz(self, course): - demo_quiz = Quiz.objects.create(start_date_time=timezone.now(), - end_date_time=timezone.now() + timedelta(176590), - duration=30, active=True, - attempts_allowed=-1, - time_between_attempts=0, - description='Yaksh Demo quiz', pass_criteria=0, - language='Python', prerequisite=None, - course=course) + demo_quiz = Quiz.objects.create( + start_date_time=timezone.now(), + end_date_time=timezone.now() + timedelta(176590), + duration=30, active=True, + attempts_allowed=-1, + time_between_attempts=0, + description='Yaksh Demo quiz', pass_criteria=0, + language='Python', prerequisite=None, + course=course + ) return demo_quiz - + def __str__(self): desc = self.description or 'Quiz' return '%s: on %s for %d minutes' % (desc, self.start_date_time, @@ -614,8 +640,8 @@ class QuestionPaperManager(models.Manager): if self.filter(quiz=trial_quiz).exists(): trial_questionpaper = self.get(quiz=trial_quiz) else: - trial_questionpaper, trial_questions = self._create_trial_from_questionpaper\ - (original_quiz_id) + trial_questionpaper, trial_questions = \ + self._create_trial_from_questionpaper(original_quiz_id) trial_questionpaper.quiz = trial_quiz trial_questionpaper.fixed_questions\ .add(*trial_questions["fixed_questions"]) @@ -665,7 +691,8 @@ class QuestionPaper(models.Model): def make_answerpaper(self, user, ip, attempt_num): """Creates an answer paper for the user to attempt the quiz""" - ans_paper = AnswerPaper(user=user, + ans_paper = AnswerPaper( + user=user, user_ip=ip, attempt_number=attempt_num ) @@ -680,8 +707,8 @@ class QuestionPaper(models.Model): return ans_paper def _is_questionpaper_passed(self, user): - return AnswerPaper.objects.filter(question_paper=self, user=user, - passed = True).exists() + return AnswerPaper.objects.filter(question_paper=self, user=user, + passed=True).exists() def _is_attempt_allowed(self, user): attempts = AnswerPaper.objects.get_total_attempt(questionpaper=self, @@ -690,8 +717,9 @@ class QuestionPaper(models.Model): def can_attempt_now(self, user): if self._is_attempt_allowed(user): - last_attempt = AnswerPaper.objects.get_user_last_attempt(user=user, - questionpaper=self) + last_attempt = AnswerPaper.objects.get_user_last_attempt( + user=user, questionpaper=self + ) if last_attempt: time_lag = (timezone.now() - last_attempt.start_time).days return time_lag >= self.quiz.time_between_attempts @@ -719,10 +747,11 @@ class QuestionPaper(models.Model): # add fixed set of questions to the question paper for question in questions: question_paper.fixed_questions.add(question) - + def __str__(self): return "Question Paper for " + self.quiz.description + ############################################################################### class QuestionSet(models.Model): """Question set contains a set of questions from which random questions @@ -777,16 +806,21 @@ class AnswerPaperManager(models.Manager): ).values_list('attempt_number', flat=True).distinct() return attempt_numbers - def has_attempt(self, questionpaper_id, attempt_number, status='completed'): + def has_attempt(self, questionpaper_id, attempt_number, + status='completed'): ''' Whether question paper is attempted''' - return self.filter(question_paper_id=questionpaper_id, - attempt_number=attempt_number, status=status).exists() + return self.filter( + question_paper_id=questionpaper_id, + attempt_number=attempt_number, status=status + ).exists() def get_count(self, questionpaper_id, attempt_number, status='completed'): ''' Return count of answerpapers for a specfic question paper and attempt number''' - return self.filter(question_paper_id=questionpaper_id, - attempt_number=attempt_number, status=status).count() + return self.filter( + question_paper_id=questionpaper_id, + attempt_number=attempt_number, status=status + ).count() def get_question_statistics(self, questionpaper_id, attempt_number, status='completed'): @@ -815,8 +849,7 @@ class AnswerPaperManager(models.Manager): return self.filter(question_paper_id=questionpaper_id) else: return self.filter(question_paper_id=questionpaper_id, - status="completed") - + status="completed") def _get_answerpapers_users(self, answerpapers): return answerpapers.values_list('user', flat=True).distinct() @@ -830,7 +863,9 @@ class AnswerPaperManager(models.Manager): return latest_attempts def _get_latest_attempt(self, answerpapers, user_id): - return answerpapers.filter(user_id=user_id).order_by('-attempt_number')[0] + return answerpapers.filter( + user_id=user_id + ).order_by('-attempt_number')[0] def get_user_last_attempt(self, questionpaper, user): attempts = self.filter(question_paper=questionpaper, @@ -856,10 +891,11 @@ class AnswerPaperManager(models.Manager): def get_user_data(self, user, questionpaper_id, attempt_number=None): if attempt_number is not None: papers = self.filter(user=user, question_paper_id=questionpaper_id, - attempt_number=attempt_number) + attempt_number=attempt_number) else: - papers = self.filter(user=user, question_paper_id=questionpaper_id)\ - .order_by("-attempt_number") + papers = self.filter( + user=user, question_paper_id=questionpaper_id + ).order_by("-attempt_number") data = {} profile = user.profile if hasattr(user, 'profile') else None data['user'] = user @@ -876,6 +912,7 @@ class AnswerPaperManager(models.Manager): best_attempt = max([marks["marks_obtained"] for marks in papers]) return best_attempt + ############################################################################### class AnswerPaper(models.Model): """A answer paper for a student -- one per student typically. @@ -901,12 +938,14 @@ class AnswerPaper(models.Model): user_ip = models.CharField(max_length=15) # The questions unanswered - questions_unanswered = models.ManyToManyField(Question, - related_name='questions_unanswered') + questions_unanswered = models.ManyToManyField( + Question, related_name='questions_unanswered' + ) # The questions answered - questions_answered = models.ManyToManyField(Question, - related_name='questions_answered') + questions_answered = models.ManyToManyField( + Question, related_name='questions_answered' + ) # All the submitted answers. answers = models.ManyToManyField(Answer) @@ -924,8 +963,10 @@ class AnswerPaper(models.Model): passed = models.NullBooleanField() # Status of the quiz attempt - status = models.CharField(max_length=20, choices=test_status,\ - default='inprogress') + status = models.CharField( + max_length=20, choices=test_status, + default='inprogress' + ) objects = AnswerPaperManager() @@ -960,7 +1001,7 @@ class AnswerPaper(models.Model): if len(questions) == 0: return None try: - index = questions.index(int(question_id)) + index = questions.index(int(question_id)) next_id = questions[index+1] except (ValueError, IndexError): next_id = questions[0] @@ -982,17 +1023,17 @@ class AnswerPaper(models.Model): """Updates the total marks earned by student for this paper.""" marks = 0 for question in self.questions.all(): - marks_list = [a.marks for a in self.answers.filter(question=question)] + marks_list = [a.marks + for a in self.answers.filter(question=question)] max_marks = max(marks_list) if marks_list else 0.0 marks += max_marks self.marks_obtained = marks - def _update_percent(self): """Updates the percent gained by the student for this paper.""" total_marks = self.question_paper.total_marks if self.marks_obtained is not None: - percent = self.marks_obtained/self.question_paper.total_marks*100 + percent = self.marks_obtained/total_marks*100 self.percent = round(percent, 2) def _update_passed(self): @@ -1031,15 +1072,15 @@ class AnswerPaper(models.Model): for answer in self.answers.all(): question = answer.question if question in q_a: - q_a[question].append({'answer': answer, - 'error_list': [e for e in json.loads(answer.error)] - } - ) + q_a[question].append({ + 'answer': answer, + 'error_list': [e for e in json.loads(answer.error)] + }) else: - q_a[question] = [{'answer': answer, - 'error_list': [e for e in json.loads(answer.error)] - } - ] + q_a[question] = [{ + 'answer': answer, + 'error_list': [e for e in json.loads(answer.error)] + }] return q_a def get_questions(self): @@ -1058,7 +1099,7 @@ class AnswerPaper(models.Model): def is_attempt_inprogress(self): if self.status == 'inprogress': - return self.time_left()> 0 + return self.time_left() > 0 def get_previous_answers(self, question): if question.type == 'code': @@ -1074,7 +1115,8 @@ class AnswerPaper(models.Model): For code questions success is True only if the answer is correct. """ - result = {'success': False, 'error': ['Incorrect answer'], 'weight': 0.0} + result = {'success': False, 'error': ['Incorrect answer'], + 'weight': 0.0} if user_answer is not None: if question.type == 'mcq': expected_answer = question.get_test_case(correct=True).options @@ -1090,7 +1132,9 @@ class AnswerPaper(models.Model): result['error'] = ['Correct answer'] elif question.type == 'code': user_dir = self.user.profile.get_user_dir() - json_result = code_server.run_code(question.language, json_data, user_dir) + json_result = code_server.run_code( + question.language, json_data, user_dir + ) result = json.loads(json_result) return result @@ -1101,7 +1145,9 @@ class AnswerPaper(models.Model): self.user, self.question_paper.quiz.description, question) except Question.DoesNotExist: msg = 'User: {0}; Quiz: {1} Question id: {2}.\n'.format( - self.user, self.question_paper.quiz.description, question_id) + self.user, self.question_paper.quiz.description, + question_id + ) return False, msg + 'Question not in the answer paper.' user_answer = self.answers.filter(question=question).last() if not user_answer: @@ -1111,23 +1157,29 @@ class AnswerPaper(models.Model): answer = eval(user_answer.answer) if type(answer) is not list: return False, msg + 'MCC answer not a list.' - except Exception as e: + except Exception: return False, msg + 'MCC answer submission error' else: answer = user_answer.answer json_data = question.consolidate_answer_data(answer) \ - if question.type == 'code' else None + if question.type == 'code' else None result = self.validate_answer(answer, question, json_data) user_answer.correct = result.get('success') user_answer.error = result.get('error') if result.get('success'): - user_answer.marks = (question.points * result['weight'] / - question.get_maximum_test_case_weight()) \ - if question.partial_grading and question.type == 'code' else question.points + if question.partial_grading and question.type == 'code': + max_weight = question.get_maximum_test_case_weight() + factor = result['weight']/max_weight + user_answer.marks = question.points * factor + else: + user_answer.marks = question.points else: - user_answer.marks = (question.points * result['weight'] / - question.get_maximum_test_case_weight()) \ - if question.partial_grading and question.type == 'code' else 0 + if question.partial_grading and question.type == 'code': + max_weight = question.get_maximum_test_case_weight() + factor = result['weight']/max_weight + user_answer.marks = question.points * factor + else: + user_answer.marks = 0 user_answer.save() self.update_marks('completed') return True, msg @@ -1146,11 +1198,12 @@ class AssignmentUpload(models.Model): assignmentFile = models.FileField(upload_to=get_assignment_dir) -################################################################################ +############################################################################### class TestCase(models.Model): - question = models.ForeignKey(Question, blank=True, null = True) + question = models.ForeignKey(Question, blank=True, null=True) type = models.CharField(max_length=24, choices=test_case_types, null=True) + class StandardTestCase(TestCase): test_case = models.TextField() weight = models.FloatField(default=1.0) @@ -1173,14 +1226,15 @@ class StdIOBasedTestCase(TestCase): def get_field_value(self): return {"test_case_type": "stdiobasedtestcase", - "expected_output": self.expected_output, - "expected_input": self.expected_input, - "weight": self.weight} + "expected_output": self.expected_output, + "expected_input": self.expected_input, + "weight": self.weight} def __str__(self): - return u'StdIO Based Testcase | Exp. Output: {0} | Exp. Input: {1}'.format( - self.expected_output, self.expected_input - ) + return u'StdIO Based Testcase | Exp. Output: {0} | Exp. Input: {1}'.\ + format( + self.expected_output, self.expected_input + ) class McqTestCase(TestCase): @@ -1188,32 +1242,33 @@ class McqTestCase(TestCase): correct = models.BooleanField(default=False) def get_field_value(self): - return {"test_case_type": "mcqtestcase", "options": self.options, "correct": self.correct} + return {"test_case_type": "mcqtestcase", + "options": self.options, "correct": self.correct} def __str__(self): return u'MCQ Testcase | Correct: {0}'.format(self.correct) class HookTestCase(TestCase): - hook_code = models.TextField(default=dedent\ - ("""\ - def check_answer(user_answer): - ''' Evaluates user answer to return - - success - Boolean, indicating if code was executed correctly - mark_fraction - Float, indicating fraction of the - weight to a test case - error - String, error message if success is false''' - success = False - err = "Incorrect Answer" # Please make the error message more specific - mark_fraction = 0.0 + hook_code = models.TextField(default=dedent( + """\ + def check_answer(user_answer): + ''' Evaluates user answer to return - + success - Boolean, indicating if code was executed correctly + mark_fraction - Float, indicating fraction of the + weight to a test case + error - String, error message if success is false''' + success = False + err = "Incorrect Answer" # Please make this more specific + mark_fraction = 0.0 - # write your code here + # write your code here - return success, err, mark_fraction + return success, err, mark_fraction - """) + """) - ) + ) weight = models.FloatField(default=1.0) def get_field_value(self): @@ -1222,4 +1277,3 @@ class HookTestCase(TestCase): def __str__(self): return u'Hook Testcase | Correct: {0}'.format(self.hook_code) - |