summaryrefslogtreecommitdiff
path: root/yaksh/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'yaksh/models.py')
-rw-r--r--yaksh/models.py557
1 files changed, 506 insertions, 51 deletions
diff --git a/yaksh/models.py b/yaksh/models.py
index 87a6877..686d0e6 100644
--- a/yaksh/models.py
+++ b/yaksh/models.py
@@ -1,3 +1,4 @@
+# Python Imports
from __future__ import unicode_literals, division
from datetime import datetime, timedelta
import uuid
@@ -8,16 +9,9 @@ from ruamel.yaml.scalarstring import PreservedScalarString
from ruamel.yaml.comments import CommentedMap
from random import sample
from collections import Counter, defaultdict
-
-from django.db import models
-from django.contrib.auth.models import User, Group, Permission
-from django.core.exceptions import ValidationError
-from django.contrib.contenttypes.models import ContentType
-from taggit.managers import TaggableManager
-from django.utils import timezone
-from django.core.files import File
import glob
-
+import sys
+import traceback
try:
from StringIO import StringIO as string_io
except ImportError:
@@ -31,14 +25,31 @@ import zipfile
import tempfile
from textwrap import dedent
from ast import literal_eval
-from .file_utils import extract_files, delete_files
+import pandas as pd
+
+# Django Imports
+from django.db import models
+from django.contrib.auth.models import User, Group, Permission
+from django.core.exceptions import ValidationError
+from django.contrib.contenttypes.models import ContentType
+from taggit.managers import TaggableManager
+from django.utils import timezone
+from django.core.files import File
+from django.contrib.contenttypes.fields import (
+ GenericForeignKey, GenericRelation
+)
+from django.contrib.contenttypes.models import ContentType
from django.template import Context, Template
+from django.conf import settings
+from django.forms.models import model_to_dict
+from django.db.models import Count
+
+# Local Imports
from yaksh.code_server import (
submit, get_result as get_result_from_code_server
)
from yaksh.settings import SERVER_POOL_PORT, SERVER_HOST_NAME
-from django.conf import settings
-from django.forms.models import model_to_dict
+from .file_utils import extract_files, delete_files
from grades.models import GradingSystem
languages = (
@@ -248,6 +259,15 @@ def get_image_dir(instance, filename):
))
+def is_valid_time_format(time):
+ try:
+ hh, mm, ss = time.split(":")
+ status = True
+ except ValueError:
+ status = False
+ return status
+
+
###############################################################################
class CourseManager(models.Manager):
@@ -281,11 +301,16 @@ class Lesson(models.Model):
# A video file
video_file = models.FileField(
- upload_to=get_file_dir, default=None,
+ upload_to=get_file_dir, max_length=255, default=None,
null=True, blank=True,
help_text="Please upload video files in mp4, ogv, webm format"
)
+ video_path = models.CharField(
+ max_length=255, default=None, null=True, blank=True,
+ help_text="Youtube id, vimeo id, others"
+ )
+
def __str__(self):
return "{0}".format(self.name)
@@ -612,7 +637,7 @@ class LearningUnit(models.Model):
on_delete=models.CASCADE)
quiz = models.ForeignKey(Quiz, null=True, blank=True,
on_delete=models.CASCADE)
- check_prerequisite = models.BooleanField(default=True)
+ check_prerequisite = models.BooleanField(default=False)
def get_lesson_or_quiz(self):
unit = None
@@ -691,7 +716,7 @@ class LearningModule(models.Model):
order = models.IntegerField(default=0)
creator = models.ForeignKey(User, related_name="module_creator",
on_delete=models.CASCADE)
- check_prerequisite = models.BooleanField(default=True)
+ check_prerequisite = models.BooleanField(default=False)
check_prerequisite_passes = models.BooleanField(default=False)
html_data = models.TextField(null=True, blank=True)
active = models.BooleanField(default=True)
@@ -1044,6 +1069,14 @@ class Course(models.Model):
def get_learning_modules(self):
return self.learning_module.order_by("order")
+ def get_learning_module(self, quiz):
+ modules = self.get_learning_modules()
+ for module in modules:
+ for unit in module.get_learning_units():
+ if unit.quiz == quiz:
+ break
+ return module
+
def get_unit_completion_status(self, module, user, unit):
course_module = self.learning_module.get(id=module.id)
learning_unit = course_module.learning_unit.get(id=unit.id)
@@ -1069,6 +1102,25 @@ class Course(models.Model):
learning_units.extend(module.get_learning_units())
return learning_units
+ def get_lesson_posts(self):
+ learning_units = self.get_learning_units()
+ comments = []
+ for unit in learning_units:
+ if unit.lesson is not None:
+ lesson_ct = ContentType.objects.get_for_model(unit.lesson)
+ title = unit.lesson.name
+ try:
+ post = Post.objects.get(
+ target_ct=lesson_ct,
+ target_id=unit.lesson.id,
+ active=True, title=title
+ )
+ except Post.DoesNotExist:
+ post = None
+ if post is not None:
+ comments.append(post)
+ return comments
+
def remove_trial_modules(self):
learning_modules = self.learning_module.all()
for module in learning_modules:
@@ -1193,8 +1245,8 @@ class CourseStatus(models.Model):
self.save()
def calculate_percentage(self):
- if self.is_course_complete():
- quizzes = self.course.get_quizzes()
+ quizzes = self.course.get_quizzes()
+ if self.is_course_complete() and quizzes:
total_weightage = 0
sum = 0
for quiz in quizzes:
@@ -1278,7 +1330,7 @@ class Profile(models.Model):
super(Profile, self).save(*args, **kwargs)
def __str__(self):
- return '%s' % (self.user.get_full_name())
+ return '%s' % (self.user.get_full_name() or self.user.username)
###############################################################################
@@ -1328,6 +1380,8 @@ class Question(models.Model):
# Solution for the question.
solution = models.TextField(blank=True)
+ content = GenericRelation("TableOfContents")
+
tc_code_types = {
"python": [
("standardtestcase", "Standard TestCase"),
@@ -1656,12 +1710,17 @@ class Answer(models.Model):
# Whether skipped or not.
skipped = models.BooleanField(default=False)
+ comment = models.TextField(null=True, blank=True)
+
def set_marks(self, marks):
if marks > self.question.points:
self.marks = self.question.points
else:
self.marks = marks
+ def set_comment(self, comments):
+ self.comment = comments
+
def __str__(self):
return "Answer for question {0}".format(self.question.summary)
@@ -1694,17 +1753,15 @@ class QuestionPaperManager(models.Manager):
def create_trial_paper_to_test_quiz(self, trial_quiz, original_quiz_id):
"""Creates a trial question paper to test quiz."""
- 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.quiz = trial_quiz
- trial_questionpaper.fixed_questions\
- .add(*trial_questions["fixed_questions"])
- trial_questionpaper.random_questions\
- .add(*trial_questions["random_questions"])
- trial_questionpaper.save()
+ trial_quiz.questionpaper_set.all().delete()
+ 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"])
+ trial_questionpaper.random_questions\
+ .add(*trial_questions["random_questions"])
+ trial_questionpaper.save()
return trial_questionpaper
@@ -1728,7 +1785,7 @@ class QuestionPaper(models.Model):
total_marks = models.FloatField(default=0.0, blank=True)
# Sequence or Order of fixed questions
- fixed_question_order = models.CharField(max_length=255, blank=True)
+ fixed_question_order = models.TextField(blank=True)
# Shuffle testcase order.
shuffle_testcases = models.BooleanField("Shuffle testcase for each user",
@@ -1762,6 +1819,8 @@ class QuestionPaper(models.Model):
for question in questions:
marks += question.points
for question_set in self.random_questions.all():
+ question_set.marks = question_set.questions.first().points
+ question_set.save()
marks += question_set.marks * question_set.num_questions
self.total_marks = marks
self.save()
@@ -1777,7 +1836,7 @@ class QuestionPaper(models.Model):
all_questions = questions
return all_questions
- def make_answerpaper(self, user, ip, attempt_num, course_id):
+ def make_answerpaper(self, user, ip, attempt_num, course_id, special=False):
"""Creates an answer paper for the user to attempt the quiz"""
try:
ans_paper = AnswerPaper.objects.get(user=user,
@@ -1796,6 +1855,7 @@ class QuestionPaper(models.Model):
ans_paper.end_time = ans_paper.start_time + \
timedelta(minutes=self.quiz.duration)
ans_paper.question_paper = self
+ ans_paper.is_special = special
ans_paper.save()
questions = self._get_questions_for_answerpaper()
ans_paper.questions.add(*questions)
@@ -2140,6 +2200,10 @@ class AnswerPaper(models.Model):
# set question order
questions_order = models.TextField(blank=True, default='')
+ extra_time = models.FloatField('Additional time in mins', default=0.0)
+
+ is_special = models.BooleanField(default=False)
+
objects = AnswerPaperManager()
class Meta:
@@ -2222,11 +2286,23 @@ class AnswerPaper(models.Model):
questions = list(self.questions.all())
return questions
+ def set_extra_time(self, time=0):
+ now = timezone.now()
+ self.extra_time += time
+ if self.status == 'completed' and self.end_time < now:
+ self.extra_time = time
+ quiz_time = self.question_paper.quiz.duration
+ self.start_time = now - timezone.timedelta(minutes=quiz_time)
+ self.end_time = now + timezone.timedelta(minutes=time)
+ self.status = 'inprogress'
+ self.save()
+
def time_left(self):
"""Return the time remaining for the user in seconds."""
secs = self._get_total_seconds()
+ extra_time = self.extra_time * 60
total = self.question_paper.quiz.duration*60.0
- remain = max(total - secs, 0)
+ remain = max(total - (secs - extra_time), 0)
return int(remain)
def time_left_on_question(self, question):
@@ -2244,14 +2320,20 @@ class AnswerPaper(models.Model):
secs = dt.seconds + dt.days*24*3600
return secs
+ def _get_marks_for_question(self, question):
+ marks = 0.0
+ answers = question.answer_set.filter(answerpaper=self)
+ if answers.exists():
+ marks = [answer.marks for answer in answers]
+ max_marks = max(marks)
+ marks = max_marks
+ return marks
+
def _update_marks_obtained(self):
"""Updates the total marks earned by student for this paper."""
- marks = 0
+ marks = 0.0
for question in self.questions.all():
- 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
+ marks += self._get_marks_for_question(question)
self.marks_obtained = marks
def _update_percent(self):
@@ -2288,24 +2370,30 @@ class AnswerPaper(models.Model):
self.end_time = datetime
self.save()
+ def get_answer_comment(self, question_id):
+ answer = self.answers.filter(question_id=question_id).last()
+ if answer:
+ return answer.comment
+
def get_question_answers(self):
"""
Return a dictionary with keys as questions and a list of the
corresponding answers.
"""
q_a = {}
- for answer in self.answers.all():
- question = answer.question
- if question in q_a:
- q_a[question].append({
+ for question in self.questions.all():
+ answers = question.answer_set.filter(answerpaper=self)
+ if not answers.exists():
+ q_a[question] = [None, 0.0]
+ continue
+ ans_errs = []
+ for answer in answers:
+ ans_errs.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] = ans_errs
+ q_a[question].append(self._get_marks_for_question(question))
return q_a
def get_latest_answer(self, question_id):
@@ -2315,7 +2403,7 @@ class AnswerPaper(models.Model):
return self.questions.filter(active=True)
def get_questions_answered(self):
- return self.questions_answered.all()
+ return self.questions_answered.all().distinct()
def get_questions_unanswered(self):
return self.questions_unanswered.all()
@@ -2514,7 +2602,7 @@ class AssignmentUploadManager(models.Manager):
class AssignmentUpload(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
assignmentQuestion = models.ForeignKey(Question, on_delete=models.CASCADE)
- assignmentFile = models.FileField(upload_to=get_assignment_dir)
+ assignmentFile = models.FileField(upload_to=get_assignment_dir, max_length=255)
question_paper = models.ForeignKey(QuestionPaper, blank=True, null=True,
on_delete=models.CASCADE)
course = models.ForeignKey(Course, null=True, blank=True,
@@ -2533,11 +2621,13 @@ class StandardTestCase(TestCase):
test_case = models.TextField()
weight = models.FloatField(default=1.0)
test_case_args = models.TextField(blank=True)
+ hidden = models.BooleanField(default=False)
def get_field_value(self):
return {"test_case_type": "standardtestcase",
"test_case": self.test_case,
"weight": self.weight,
+ "hidden": self.hidden,
"test_case_args": self.test_case_args}
def __str__(self):
@@ -2548,11 +2638,13 @@ class StdIOBasedTestCase(TestCase):
expected_input = models.TextField(default=None, blank=True, null=True)
expected_output = models.TextField(default=None)
weight = models.IntegerField(default=1.0)
+ hidden = models.BooleanField(default=False)
def get_field_value(self):
return {"test_case_type": "stdiobasedtestcase",
"expected_output": self.expected_output,
"expected_input": self.expected_input,
+ "hidden": self.hidden,
"weight": self.weight}
def __str__(self):
@@ -2598,10 +2690,11 @@ class HookTestCase(TestCase):
)
weight = models.FloatField(default=1.0)
+ hidden = models.BooleanField(default=False)
def get_field_value(self):
return {"test_case_type": "hooktestcase", "hook_code": self.hook_code,
- "weight": self.weight}
+ "hidden": self.hidden, "weight": self.weight}
def __str__(self):
return u'Hook Testcase | Correct: {0}'.format(self.hook_code)
@@ -2682,12 +2775,20 @@ class ForumBase(models.Model):
image = models.ImageField(upload_to=get_image_dir, blank=True,
null=True, validators=[validate_image])
active = models.BooleanField(default=True)
+ anonymous = models.BooleanField(default=False)
class Post(ForumBase):
title = models.CharField(max_length=200)
- course = models.ForeignKey(Course,
- on_delete=models.CASCADE, related_name='post')
+ target_ct = models.ForeignKey(ContentType,
+ blank=True,
+ null=True,
+ related_name='target_obj',
+ on_delete=models.CASCADE)
+ target_id = models.PositiveIntegerField(null=True,
+ blank=True,
+ db_index=True)
+ target = GenericForeignKey('target_ct', 'target_id')
def __str__(self):
return self.title
@@ -2706,3 +2807,357 @@ class Comment(ForumBase):
def __str__(self):
return 'Comment by {0}: {1}'.format(self.creator.username,
self.post_field.title)
+
+
+class TOCManager(models.Manager):
+
+ def get_data(self, course_id, lesson_id):
+ contents = TableOfContents.objects.filter(
+ course_id=course_id, lesson_id=lesson_id, content__in=[2, 3, 4]
+ )
+ data = {}
+ for toc in contents:
+ data[toc] = LessonQuizAnswer.objects.filter(
+ toc_id=toc.id).values_list(
+ "student_id", flat=True).distinct().count()
+ return data
+
+ def get_question_stats(self, toc_id):
+ answers = LessonQuizAnswer.objects.get_queryset().filter(
+ toc_id=toc_id).order_by('id')
+ question = TableOfContents.objects.get(id=toc_id).content_object
+ if answers.exists():
+ answers = answers.values(
+ "student__first_name", "student__last_name", "student__email",
+ "student_id", "student__profile__roll_number", "toc_id"
+ )
+ df = pd.DataFrame(answers)
+ answers = df.drop_duplicates().to_dict(orient='records')
+ return question, answers
+
+ def get_per_tc_ans(self, toc_id, question_type, is_percent=True):
+ answers = LessonQuizAnswer.objects.filter(toc_id=toc_id).values(
+ "student_id", "answer__answer"
+ ).order_by("id")
+ data = None
+ if answers.exists():
+ df = pd.DataFrame(answers)
+ grp = df.groupby(["student_id"]).tail(1)
+ total_count = grp.count().answer__answer
+ data = grp.groupby(["answer__answer"]).count().to_dict().get(
+ "student_id")
+ if question_type == "mcc":
+ tc_ids = []
+ mydata = {}
+ for i in data.keys():
+ tc_ids.extend(literal_eval(i))
+ for j in tc_ids:
+ if j not in mydata:
+ mydata[j] = 1
+ else:
+ mydata[j] +=1
+ data = mydata.copy()
+ if is_percent:
+ for key, value in data.items():
+ data[key] = (value/total_count)*100
+ return data, total_count
+
+ def get_answer(self, toc_id, user_id):
+ submission = LessonQuizAnswer.objects.filter(
+ toc_id=toc_id, student_id=user_id).last()
+ question = submission.toc.content_object
+ attempted_answer = submission.answer
+ if question.type == "mcq":
+ submitted_answer = literal_eval(attempted_answer.answer)
+ answers = [
+ tc.options
+ for tc in question.get_test_cases(id=submitted_answer)
+ ]
+ answer = ",".join(answers)
+ elif question.type == "mcc":
+ submitted_answer = literal_eval(attempted_answer.answer)
+ answers = [
+ tc.options
+ for tc in question.get_test_cases(id__in=submitted_answer)
+ ]
+ answer = ",".join(answers)
+ else:
+ answer = attempted_answer.answer
+ return answer, attempted_answer.correct
+
+ def add_contents(self, course_id, lesson_id, user, contents):
+ toc = []
+ messages = []
+ for content in contents:
+ name = content.get('name') or content.get('summary')
+ if "content_type" not in content or "time" not in content:
+ messages.append(
+ (False,
+ f"content_type or time key is missing in {name}")
+ )
+ else:
+ content_type = content.pop('content_type')
+ time = content.pop('time')
+ if not is_valid_time_format(time):
+ messages.append(
+ (False,
+ f"Invalid time format in {name}. "
+ "Format should be 00:00:00")
+ )
+ else:
+ if content_type == 1:
+ topic = Topic.objects.create(**content)
+ toc.append(TableOfContents(
+ course_id=course_id, lesson_id=lesson_id, time=time,
+ content_object=topic, content=content_type
+ ))
+ messages.append((True, f"{topic.name} added successfully"))
+ else:
+ content['user'] = user
+ test_cases = content.pop("testcase")
+ que_type = content.get('type')
+ if "files" in content:
+ content.pop("files")
+ if "tags" in content:
+ content.pop("tags")
+ if (que_type in ['code', 'upload']):
+ messages.append(
+ (False, f"{que_type} question is not allowed. "
+ f"{content.get('summary')} is not added")
+ )
+ else:
+ que = Question.objects.create(**content)
+ for test_case in test_cases:
+ test_case_type = test_case.pop('test_case_type')
+ model_class = get_model_class(test_case_type)
+ model_class.objects.get_or_create(
+ question=que, **test_case, type=test_case_type
+ )
+ toc.append(TableOfContents(
+ course_id=course_id, lesson_id=lesson_id,
+ time=time, content_object=que,
+ content=content_type
+ ))
+ messages.append(
+ (True, f"{que.summary} added successfully")
+ )
+ if toc:
+ TableOfContents.objects.bulk_create(toc)
+ return messages
+
+
+class TableOfContents(models.Model):
+ toc_types = ((1, "Topic"), (2, "Graded Quiz"), (3, "Exercise"), (4, "Poll"))
+ course = models.ForeignKey(Course, on_delete=models.CASCADE,
+ related_name='course')
+ lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE,
+ related_name='contents')
+ time = models.CharField(max_length=100, default=0)
+ content = models.IntegerField(choices=toc_types)
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
+ object_id = models.PositiveIntegerField()
+ content_object = GenericForeignKey()
+
+ objects = TOCManager()
+
+ class Meta:
+ verbose_name_plural = "Table Of Contents"
+
+ def get_toc_text(self):
+ if self.content == 1:
+ content_name = self.content_object.name
+ else:
+ content_name = self.content_object.summary
+ return content_name
+
+ def __str__(self):
+ return f"TOC for {self.lesson.name} with {self.get_content_display()}"
+
+
+class Topic(models.Model):
+ name = models.CharField(max_length=255)
+ description = models.TextField(null=True, blank=True)
+ content = GenericRelation(TableOfContents)
+
+ def __str__(self):
+ return f"{self.name}"
+
+
+class LessonQuizAnswer(models.Model):
+ toc = models.ForeignKey(TableOfContents, on_delete=models.CASCADE)
+ student = models.ForeignKey(User, on_delete=models.CASCADE)
+ answer = models.ForeignKey(Answer, on_delete=models.CASCADE)
+
+ def check_answer(self, user_answer):
+ result = {'success': False, 'error': ['Incorrect answer'],
+ 'weight': 0.0}
+ question = self.toc.content_object
+ if question.type == 'mcq':
+ expected_answer = question.get_test_case(correct=True).id
+ if user_answer.strip() == str(expected_answer).strip():
+ result['success'] = True
+ result['error'] = ['Correct answer']
+
+ elif question.type == 'mcc':
+ expected_answers = [
+ str(opt.id) for opt in question.get_test_cases(correct=True)
+ ]
+ if set(user_answer) == set(expected_answers):
+ result['success'] = True
+ result['error'] = ['Correct answer']
+
+ elif question.type == 'integer':
+ expected_answers = [
+ int(tc.correct) for tc in question.get_test_cases()
+ ]
+ if int(user_answer) in expected_answers:
+ result['success'] = True
+ result['error'] = ['Correct answer']
+
+ elif question.type == 'string':
+ tc_status = []
+ for tc in question.get_test_cases():
+ if tc.string_check == "lower":
+ if tc.correct.lower().splitlines()\
+ == user_answer.lower().splitlines():
+ tc_status.append(True)
+ else:
+ if tc.correct.splitlines()\
+ == user_answer.splitlines():
+ tc_status.append(True)
+ if any(tc_status):
+ result['success'] = True
+ result['error'] = ['Correct answer']
+
+ elif question.type == 'float':
+ user_answer = float(user_answer)
+ tc_status = []
+ for tc in question.get_test_cases():
+ if abs(tc.correct - user_answer) <= tc.error_margin:
+ tc_status.append(True)
+ if any(tc_status):
+ 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']
+ self.answer.error = result
+ ans_status = result.get("success")
+ self.answer.correct = ans_status
+ if ans_status:
+ self.answer.marks = self.answer.question.points
+ self.answer.save()
+ return result
+
+ def __str__(self):
+ return f"Lesson answer of {self.toc} by {self.student.get_full_name()}"
+
+
+class MicroManager(models.Model):
+ manager = models.ForeignKey(User, on_delete=models.CASCADE,
+ related_name='micromanaging', null=True)
+ student = models.ForeignKey(User, on_delete=models.CASCADE,
+ related_name='micromanaged')
+ course = models.ForeignKey(Course, on_delete=models.CASCADE)
+ quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, null=True)
+ special_attempt = models.BooleanField(default=False)
+ attempts_permitted = models.IntegerField(default=0)
+ permitted_time = models.DateTimeField(default=timezone.now)
+ attempts_utilised = models.IntegerField(default=0)
+ wait_time = models.IntegerField('Days to wait before special attempt',
+ default=0)
+ attempt_valid_for = models.IntegerField('Validity days', default=90)
+
+ class Meta:
+ unique_together = ('student', 'course', 'quiz')
+
+ def set_wait_time(self, days=0):
+ self.wait_time = days
+ self.save()
+
+ def increment_attempts_permitted(self):
+ self.attempts_permitted += 1
+ self.save()
+
+ def update_permitted_time(self, permit_time=None):
+ time_now = timezone.now()
+ self.permitted_time = time_now if not permit_time else permit_time
+ self.save()
+
+ def has_student_attempts_exhausted(self):
+ if self.quiz.attempts_allowed == -1:
+ return False
+ question_paper = self.quiz.questionpaper_set.first()
+ attempts = AnswerPaper.objects.get_total_attempt(
+ question_paper, self.student, course_id=self.course.id
+ )
+ last_attempt = AnswerPaper.objects.get_user_last_attempt(
+ question_paper, self.student, self.course.id
+ )
+ if last_attempt:
+ if last_attempt.is_attempt_inprogress():
+ return False
+ return attempts >= self.quiz.attempts_allowed
+
+ def is_last_attempt_inprogress(self):
+ question_paper = self.quiz.questionpaper_set.first()
+ last_attempt = AnswerPaper.objects.get_user_last_attempt(
+ question_paper, self.student, self.course.id
+ )
+ if last_attempt:
+ return last_attempt.is_attempt_inprogress()
+ return False
+
+ def has_quiz_time_exhausted(self):
+ return not self.quiz.active or self.quiz.is_expired()
+
+ def is_course_exhausted(self):
+ return not self.course.active or not self.course.is_active_enrollment()
+
+ def is_special_attempt_required(self):
+ return (self.has_student_attempts_exhausted() or
+ self.has_quiz_time_exhausted() or self.is_course_exhausted())
+
+ def allow_special_attempt(self, wait_time=0):
+ if (self.is_special_attempt_required() and
+ not self.is_last_attempt_inprogress()):
+ self.special_attempt = True
+ if self.attempts_utilised >= self.attempts_permitted:
+ self.increment_attempts_permitted()
+ self.update_permitted_time()
+ self.set_wait_time(days=wait_time)
+ self.save()
+
+ def has_special_attempt(self):
+ return (self.special_attempt and
+ (self.attempts_utilised < self.attempts_permitted))
+
+ def is_attempt_time_valid(self):
+ permit_time = self.permitted_time
+ wait_time = permit_time + timezone.timedelta(days=self.wait_time)
+ valid_time = permit_time + timezone.timedelta(
+ days=self.attempt_valid_for)
+ return wait_time <= timezone.now() <= valid_time
+
+ def can_student_attempt(self):
+ return self.has_special_attempt() and self.is_attempt_time_valid()
+
+ def get_attempt_number(self):
+ return self.quiz.attempts_allowed + self.attempts_utilised + 1
+
+ def increment_attempts_utilised(self):
+ self.attempts_utilised += 1
+ self.save()
+
+ def revoke_special_attempt(self):
+ self.special_attempt = False
+ self.save()
+
+ def __str__(self):
+ return 'MicroManager for {0} - {1}'.format(self.student.username,
+ self.course.name)