summaryrefslogtreecommitdiff
path: root/yaksh/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'yaksh/models.py')
-rw-r--r--yaksh/models.py355
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()
+
+
+##############################################################################