diff options
Diffstat (limited to 'yaksh/models.py')
-rw-r--r-- | yaksh/models.py | 355 |
1 files changed, 289 insertions, 66 deletions
diff --git a/yaksh/models.py b/yaksh/models.py index f065190..5d17dba 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -19,8 +19,6 @@ except ImportError: from io import BytesIO as string_io import pytz import os -import sys -import traceback import stat from os.path import join, exists import shutil @@ -35,7 +33,7 @@ from yaksh.code_server import ( from yaksh.settings import SERVER_POOL_PORT, SERVER_HOST_NAME from django.conf import settings from django.forms.models import model_to_dict - +from grades.models import GradingSystem languages = ( ("python", "Python"), @@ -54,6 +52,8 @@ question_types = ( ("integer", "Answer in Integer"), ("string", "Answer in String"), ("float", "Answer in Float"), + ("arrange", "Arrange in Correct Order"), + ) enrollment_methods = ( @@ -69,6 +69,7 @@ test_case_types = ( ("integertestcase", "Integer Testcase"), ("stringtestcase", "String Testcase"), ("floattestcase", "Float Testcase"), + ("arrangetestcase", "Arrange Testcase"), ) string_check_type = ( @@ -89,10 +90,10 @@ FIXTURES_DIR_PATH = os.path.join(settings.BASE_DIR, 'yaksh', 'fixtures') def get_assignment_dir(instance, filename): upload_dir = instance.question_paper.quiz.description.replace(" ", "_") - return os.sep.join(( - upload_dir, instance.user.username, str(instance.assignmentQuestion.id), - filename - )) + return os.sep.join((upload_dir, instance.user.username, + str(instance.assignmentQuestion.id), + filename + )) def get_model_class(model): @@ -107,13 +108,14 @@ def get_upload_dir(instance, filename): 'question_%s' % (instance.question.id), filename )) + def dict_to_yaml(dictionary): - for k,v in dictionary.items(): + for k, v in dictionary.items(): if isinstance(v, list): - for nested_v in v: + for nested_v in v: if isinstance(nested_v, dict): dict_to_yaml(nested_v) - elif v and isinstance(v,str): + elif v and isinstance(v, str): dictionary[k] = PreservedScalarString(v) return ruamel.yaml.round_trip_dump(dictionary, explicit_start=True, default_flow_style=False, @@ -163,6 +165,23 @@ class Lesson(models.Model): def get_files(self): return LessonFile.objects.filter(lesson=self) + def _create_lesson_copy(self, user): + lesson_files = self.get_files() + new_lesson = self + new_lesson.id = None + new_lesson.name = "Copy of {0}".format(self.name) + new_lesson.creator = user + new_lesson.save() + for _file in lesson_files: + file_name = os.path.basename(_file.file.name) + if os.path.exists(_file.file.path): + lesson_file = open(_file.file.path, "rb") + django_file = File(lesson_file) + lesson_file_obj = LessonFile() + lesson_file_obj.lesson = new_lesson + lesson_file_obj.file.save(file_name, django_file, save=True) + return new_lesson + ############################################################################# class LessonFile(models.Model): @@ -184,12 +203,10 @@ class QuizManager(models.Manager): def create_trial_quiz(self, user): """Creates a trial quiz for testing questions""" - trial_quiz = self.create(duration=1000, - description="trial_questions", - is_trial=True, - time_between_attempts=0, - creator=user - ) + trial_quiz = self.create( + duration=1000, description="trial_questions", + is_trial=True, time_between_attempts=0, creator=user + ) return trial_quiz def create_trial_from_quiz(self, original_quiz_id, user, godmode, @@ -297,7 +314,7 @@ class Quiz(models.Model): attempts_allowed = models.IntegerField(default=1, choices=attempts) time_between_attempts = models.FloatField( - "Time Between Quiz Attempts in hours" + "Time Between Quiz Attempts in hours", default=0.0 ) is_trial = models.BooleanField(default=False) @@ -311,7 +328,8 @@ class Quiz(models.Model): allow_skip = models.BooleanField("Allow students to skip questions", default=True) - weightage = models.FloatField(default=1.0) + weightage = models.FloatField(help_text='Will be considered as percentage', + default=100) is_exercise = models.BooleanField(default=False) @@ -366,6 +384,31 @@ class Quiz(models.Model): course=course, passed=False ).values_list("user", flat=True).distinct().count() + def get_answerpaper_status(self, user, course): + try: + qp = self.questionpaper_set.get().id + except QuestionPaper.DoesNotExist: + qp = None + ans_ppr = AnswerPaper.objects.filter( + user=user, course=course, question_paper=qp + ).order_by("-attempt_number") + if ans_ppr.exists(): + status = ans_ppr.first().status + else: + status = "not attempted" + return status + + def _create_quiz_copy(self, user): + question_papers = self.questionpaper_set.all() + new_quiz = self + new_quiz.id = None + new_quiz.description = "Copy of {0}".format(self.description) + new_quiz.creator = user + new_quiz.save() + for qp in question_papers: + qp._create_duplicate_questionpaper(new_quiz) + return new_quiz + def __str__(self): desc = self.description or 'Quiz' return '%s: on %s for %d minutes' % (desc, self.start_date_time, @@ -393,6 +436,8 @@ class LearningUnit(models.Model): if course_status.exists(): if self in course_status.first().completed_units.all(): state = "completed" + elif self.type == "quiz": + state = self.quiz.get_answerpaper_status(user, course) elif course_status.first().current_unit == self: state = "inprogress" return state @@ -416,6 +461,17 @@ class LearningUnit(models.Model): success = False return success + def _create_unit_copy(self, user): + if self.type == "quiz": + new_quiz = self.quiz._create_quiz_copy(user) + new_unit = LearningUnit.objects.create( + order=self.order, type="quiz", quiz=new_quiz) + else: + new_lesson = self.lesson._create_lesson_copy(user) + new_unit = LearningUnit.objects.create( + order=self.order, type="lesson", lesson=new_lesson) + return new_unit + ############################################################################### class LearningModule(models.Model): @@ -508,9 +564,21 @@ class LearningModule(models.Model): status_list = [unit.get_completion_status(user, course) for unit in units] count = status_list.count("completed") - percent = round((count / len(units)) * 100) + percent = round((count / units.count()) * 100) return percent + def _create_module_copy(self, user, module_name): + learning_units = self.learning_unit.order_by("order") + new_module = self + new_module.id = None + new_module.name = module_name + new_module.creator = user + new_module.save() + for unit in learning_units: + new_unit = unit._create_unit_copy(user) + new_module.learning_unit.add(new_unit) + return new_module + def __str__(self): return self.name @@ -551,6 +619,8 @@ class Course(models.Model): null=True ) + grading_system = models.ForeignKey(GradingSystem, null=True, blank=True) + objects = CourseManager() def _create_duplicate_instance(self, creator, course_name=None): @@ -562,14 +632,13 @@ class Course(models.Model): return new_course def create_duplicate_course(self, user): - learning_modules = self.learning_module.all() - - new_course_name = "Copy Of {0}".format(self.name) - new_course = self._create_duplicate_instance(user, new_course_name) - - new_course.learning_module.add(*learning_modules) - - return new_course + learning_modules = self.learning_module.order_by("order") + copy_course_name = "Copy Of {0}".format(self.name) + new_course = self._create_duplicate_instance(user, copy_course_name) + for module in learning_modules: + copy_module_name = "Copy of {0}".format(module.name) + new_module = module._create_module_copy(user, copy_module_name) + new_course.learning_module.add(new_module) def request(self, *users): self.requests.add(*users) @@ -711,17 +780,45 @@ class Course(models.Model): next_index = 0 return modules.get(id=module_ids[next_index]) - def percent_completed(self, user): - modules = self.get_learning_modules() + def percent_completed(self, user, modules): if not modules: percent = 0.0 else: status_list = [module.get_module_complete_percent(self, user) for module in modules] count = sum(status_list) - percent = round((count / len(modules))) + percent = round((count / modules.count())) return percent + def get_grade(self, user): + course_status = CourseStatus.objects.filter(course=self, user=user) + if course_status.exists(): + grade = course_status.first().get_grade() + else: + grade = "NA" + return grade + + def get_current_unit(self, user): + course_status = CourseStatus.objects.filter(course=self, user=user) + if course_status.exists(): + return course_status.first().current_unit + + def days_before_start(self): + """ Get the days remaining for the start of the course """ + if timezone.now() < self.start_enroll_time: + remaining_days = (self.start_enroll_time - timezone.now()).days + 1 + else: + remaining_days = 0 + return remaining_days + + def get_completion_percent(self, user): + course_status = CourseStatus.objects.filter(course=self, user=user) + if course_status.exists(): + percentage = course_status.first().percent_completed + else: + percentage = 0 + return percentage + def __str__(self): return self.name @@ -735,7 +832,49 @@ class CourseStatus(models.Model): course = models.ForeignKey(Course) user = models.ForeignKey(User) grade = models.CharField(max_length=255, null=True, blank=True) - total_marks = models.FloatField(default=0.0) + percentage = models.FloatField(default=0.0) + percent_completed = models.IntegerField(default=0) + + def get_grade(self): + return self.grade + + def set_grade(self): + if self.is_course_complete(): + self.calculate_percentage() + if self.course.grading_system is None: + grading_system = GradingSystem.objects.get(name='default') + else: + grading_system = self.course.grading_system + grade = grading_system.get_grade(self.percentage) + self.grade = grade + self.save() + + def calculate_percentage(self): + if self.is_course_complete(): + quizzes = self.course.get_quizzes() + total_weightage = 0 + sum = 0 + for quiz in quizzes: + total_weightage += quiz.weightage + marks = AnswerPaper.objects.get_user_best_of_attempts_marks( + quiz, self.user.id, self.course.id) + out_of = quiz.questionpaper_set.first().total_marks + sum += (marks/out_of)*quiz.weightage + self.percentage = (sum/total_weightage)*100 + self.save() + + def is_course_complete(self): + modules = self.course.get_learning_modules() + complete = False + for module in modules: + complete = module.get_status(self.user, self.course) == 'completed' + if not complete: + break + return complete + + def set_current_unit(self, unit): + self.current_unit = unit + self.save() ############################################################################### @@ -813,8 +952,9 @@ class Question(models.Model): # Check assignment upload based question grade_assignment_upload = models.BooleanField(default=False) - min_time = models.IntegerField("time in minutes", default=0) + min_time = models.IntegerField("time in minutes", default=0) + # Solution for the question. solution = models.TextField(blank=True) def consolidate_answer_data(self, user_answer, user=None): @@ -842,7 +982,7 @@ class Question(models.Model): ) if assignment_files: metadata['assign_files'] = [(file.assignmentFile.path, False) - for file in assignment_files] + for file in assignment_files] question_data['metadata'] = metadata return json.dumps(question_data) @@ -876,8 +1016,7 @@ class Question(models.Model): for question in questions: question['user'] = user file_names = question.pop('files') \ - if 'files' in question \ - else None + if 'files' in question else None tags = question.pop('tags') if 'tags' in question else None test_cases = question.pop('testcase') que, result = Question.objects.get_or_create(**question) @@ -896,8 +1035,8 @@ class Question(models.Model): new_test_case.type = test_case_type new_test_case.save() - except: - msg = "File not correct." + except Exception: + msg = "Unable to parse test case data" except Exception as exc_msg: msg = "Error Parsing Yaml: {0}".format(exc_msg) return msg @@ -928,6 +1067,17 @@ class Question(models.Model): return test_case + def get_ordered_test_cases(self, answerpaper): + try: + order = TestCaseOrder.objects.get(answer_paper=answerpaper, + question=self + ).order.split(",") + return [self.get_test_case(id=int(tc_id)) + for tc_id in order + ] + except TestCaseOrder.DoesNotExist: + return self.get_test_cases() + def get_maximum_test_case_weight(self, **kwargs): max_weight = 0.0 for test_case in self.get_test_cases(): @@ -956,13 +1106,11 @@ class Question(models.Model): file_upload.extract = extract file_upload.file.save(file_name, django_file, save=True) - def _add_yaml_to_zip(self, zip_file, q_dict,path_to_file=None): - + def _add_yaml_to_zip(self, zip_file, q_dict, path_to_file=None): tmp_file_path = tempfile.mkdtemp() yaml_path = os.path.join(tmp_file_path, "questions_dump.yaml") for elem in q_dict: relevant_dict = CommentedMap() - irrelevant_dict = CommentedMap() relevant_dict['summary'] = elem.pop('summary') relevant_dict['type'] = elem.pop('type') relevant_dict['language'] = elem.pop('language') @@ -970,8 +1118,8 @@ class Question(models.Model): relevant_dict['points'] = elem.pop('points') relevant_dict['testcase'] = elem.pop('testcase') relevant_dict.update(CommentedMap(sorted(elem.items(), - key=lambda x:x[0] - )) + key=lambda x: x[0] + )) ) yaml_block = dict_to_yaml(relevant_dict) @@ -987,7 +1135,7 @@ class Question(models.Model): if os.path.exists(yaml_file): with open(yaml_file, 'r') as q_file: questions_list = q_file.read() - msg = self.load_questions(questions_list, user, + msg = self.load_questions(questions_list, user, file_path, files ) else: @@ -1134,6 +1282,11 @@ class QuestionPaper(models.Model): # Sequence or Order of fixed questions fixed_question_order = models.CharField(max_length=255, blank=True) + # Shuffle testcase order. + shuffle_testcases = models.BooleanField("Shuffle testcase for each user", + default=True + ) + objects = QuestionPaperManager() def get_question_bank(self): @@ -1144,8 +1297,8 @@ class QuestionPaper(models.Model): return questions def _create_duplicate_questionpaper(self, quiz): - new_questionpaper = QuestionPaper.objects.create(quiz=quiz, - shuffle_questions=self.shuffle_questions, + new_questionpaper = QuestionPaper.objects.create( + quiz=quiz, shuffle_questions=self.shuffle_questions, total_marks=self.total_marks, fixed_question_order=self.fixed_question_order ) @@ -1197,7 +1350,20 @@ class QuestionPaper(models.Model): ans_paper.save() questions = self._get_questions_for_answerpaper() ans_paper.questions.add(*questions) - question_ids = [str(que.id) for que in questions] + question_ids = [] + for question in questions: + question_ids.append(str(question.id)) + if (question.type == "arrange") or ( + self.shuffle_testcases and + question.type in ["mcq", "mcc"]): + testcases = question.get_test_cases() + random.shuffle(testcases) + testcases_ids = ",".join([str(tc.id) for tc in testcases] + ) + TestCaseOrder.objects.create( + answer_paper=ans_paper, question=question, + order=testcases_ids) + ans_paper.questions_order = ",".join(question_ids) ans_paper.save() ans_paper.questions_unanswered.add(*questions) @@ -1215,12 +1381,20 @@ class QuestionPaper(models.Model): user=user, questionpaper=self, course_id=course_id ) if last_attempt: - time_lag = (timezone.now() - last_attempt.start_time).total_seconds() / 3600 - return time_lag >= self.quiz.time_between_attempts + time_lag = (timezone.now() - last_attempt.start_time) + time_lag = time_lag.total_seconds()/3600 + can_attempt = time_lag >= self.quiz.time_between_attempts + msg = "You cannot start the next attempt for this quiz before"\ + "{0} hour(s)".format(self.quiz.time_between_attempts) \ + if not can_attempt else None + return can_attempt, msg else: - return True + return True, None else: - return False + msg = "You cannot attempt {0} quiz more than {1} time(s)".format( + self.quiz.description, self.quiz.attempts_allowed + ) + return False, msg def create_demo_quiz_ppr(self, demo_quiz, user): question_paper = QuestionPaper.objects.create(quiz=demo_quiz, @@ -1516,11 +1690,12 @@ class AnswerPaper(models.Model): ) def get_per_question_score(self, question_id): - if question_id not in self.get_questions().values_list('id', flat=True): + questions = self.get_questions().values_list('id', flat=True) + if question_id not in questions: return 'NA' answer = self.get_latest_answer(question_id) if answer: - return answer.marks + return answer.marks else: return 0 @@ -1536,7 +1711,8 @@ class AnswerPaper(models.Model): def get_current_question(self, questions): if self.questions_order: available_question_ids = questions.values_list('id', flat=True) - ordered_question_ids = [int(q) for q in self.questions_order.split(',')] + ordered_question_ids = [int(q) + for q in self.questions_order.split(',')] for qid in ordered_question_ids: if qid in available_question_ids: return questions.get(id=qid) @@ -1551,7 +1727,7 @@ class AnswerPaper(models.Model): Adds the completed question to the list of answered questions and returns the next question. """ - if question_id not in self.questions_answered.all(): + if question_id not in self.questions_answered.all(): self.questions_answered.add(question_id) self.questions_unanswered.remove(question_id) @@ -1739,11 +1915,11 @@ class AnswerPaper(models.Model): for tc in question.get_test_cases(): if tc.string_check == "lower": if tc.correct.lower().splitlines()\ - == user_answer.lower().splitlines(): + == user_answer.lower().splitlines(): tc_status.append(True) else: if tc.correct.splitlines()\ - == user_answer.splitlines(): + == user_answer.splitlines(): tc_status.append(True) if any(tc_status): result['success'] = True @@ -1760,6 +1936,14 @@ class AnswerPaper(models.Model): result['success'] = True result['error'] = ['Correct answer'] + elif question.type == 'arrange': + testcase_ids = sorted( + [tc.id for tc in question.get_test_cases()] + ) + if user_answer == testcase_ids: + result['success'] = True + result['error'] = ['Correct answer'] + elif question.type == 'code' or question.type == "upload": user_dir = self.user.profile.get_user_dir() url = '{0}:{1}'.format(SERVER_HOST_NAME, server_port) @@ -1781,13 +1965,21 @@ class AnswerPaper(models.Model): user_answer = self.answers.filter(question=question).last() if not user_answer: return False, msg + 'Did not answer.' - if question.type == 'mcc': + if question.type in ['mcc', 'arrange']: try: - answer = eval(user_answer.answer) + answer = literal_eval(user_answer.answer) if type(answer) is not list: - return False, msg + 'MCC answer not a list.' + return (False, + msg + '{0} answer not a list.'.format( + question.type + ) + ) except Exception: - return False, msg + 'MCC answer submission error' + return (False, + msg + '{0} answer submission error'.format( + question.type + ) + ) else: answer = user_answer.answer json_data = question.consolidate_answer_data(answer) \ @@ -1829,7 +2021,7 @@ class AnswerPaper(models.Model): .format(u.first_name, u.last_name, q.description) -################################################################################ +############################################################################## class AssignmentUploadManager(models.Manager): def get_assignments(self, qp, que_id=None, user_id=None): @@ -1851,7 +2043,7 @@ class AssignmentUploadManager(models.Manager): return assignment_files, file_name -################################################################################ +############################################################################## class AssignmentUpload(models.Model): user = models.ForeignKey(User) assignmentQuestion = models.ForeignKey(Question) @@ -1860,7 +2052,7 @@ class AssignmentUpload(models.Model): objects = AssignmentUploadManager() -############################################################################### +############################################################################## class TestCase(models.Model): question = models.ForeignKey(Question, blank=True, null=True) type = models.CharField(max_length=24, choices=test_case_types, null=True) @@ -1943,6 +2135,7 @@ class HookTestCase(TestCase): def __str__(self): return u'Hook Testcase | Correct: {0}'.format(self.hook_code) + class IntegerTestCase(TestCase): correct = models.IntegerField(default=None) @@ -1955,11 +2148,11 @@ class IntegerTestCase(TestCase): class StringTestCase(TestCase): correct = models.TextField(default=None) - string_check = models.CharField(max_length=200,choices=string_check_type) + string_check = models.CharField(max_length=200, choices=string_check_type) def get_field_value(self): return {"test_case_type": "stringtestcase", "correct": self.correct, - "string_check":self.string_check} + "string_check": self.string_check} def __str__(self): return u'String Testcase | Correct: {0}'.format(self.correct) @@ -1972,9 +2165,39 @@ class FloatTestCase(TestCase): def get_field_value(self): return {"test_case_type": "floattestcase", "correct": self.correct, - "error_margin":self.error_margin} + "error_margin": self.error_margin} def __str__(self): return u'Testcase | Correct: {0} | Error Margin: +or- {1}'.format( self.correct, self.error_margin ) + + +class ArrangeTestCase(TestCase): + options = models.TextField(default=None) + + def get_field_value(self): + return {"test_case_type": "arrangetestcase", + "options": self.options} + + def __str__(self): + return u'Arrange Testcase | Option: {0}'.format(self.options) + + +############################################################################## +class TestCaseOrder(models.Model): + """Testcase order contains a set of ordered test cases for a given question + for each user. + """ + + # Answerpaper of the user. + answer_paper = models.ForeignKey(AnswerPaper, related_name="answer_paper") + + # Question in an answerpaper. + question = models.ForeignKey(Question) + + # Order of the test case for a question. + order = models.TextField() + + +############################################################################## |