summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.txt24
-rw-r--r--yaksh/fixtures/user_existing_email.csv2
-rw-r--r--yaksh/fixtures/users_add_update_reject.csv4
-rw-r--r--yaksh/migrations/0021_auto_20200703_1556.py25
-rw-r--r--yaksh/migrations/0022_release_0_22_1.py30
-rw-r--r--yaksh/migrations/0023_release_0_23_0.py46
-rw-r--r--yaksh/models.py149
-rw-r--r--yaksh/static/yaksh/css/custom.css6
-rw-r--r--yaksh/static/yaksh/js/lesson.js9
-rw-r--r--yaksh/static/yaksh/js/question_paper_creation.js33
-rw-r--r--yaksh/templates/yaksh/add_lesson.html28
-rw-r--r--yaksh/templates/yaksh/add_module.html51
-rw-r--r--yaksh/templates/yaksh/add_quiz.html12
-rw-r--r--yaksh/templates/yaksh/design_questionpaper.html72
-rw-r--r--yaksh/templates/yaksh/micromanaged.html22
-rw-r--r--yaksh/templates/yaksh/micromonitor.html9
-rw-r--r--yaksh/templates/yaksh/monitor.html31
-rw-r--r--yaksh/templates/yaksh/question.html10
-rw-r--r--yaksh/templates/yaksh/quit.html4
-rw-r--r--yaksh/templates/yaksh/quizzes_user.html2
-rw-r--r--yaksh/templates/yaksh/show_video.html6
-rw-r--r--yaksh/templates/yaksh/statistics_question.html117
-rw-r--r--yaksh/templatetags/custom_filters.py43
-rw-r--r--yaksh/test_models.py151
-rw-r--r--yaksh/test_views.py81
-rw-r--r--yaksh/urls.py9
-rw-r--r--yaksh/views.py178
27 files changed, 1004 insertions, 150 deletions
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index d37f7c7..275624f 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,8 +1,26 @@
-== 0.21.0 (02-07-2020) ===
+=== 0.23.0 (09-09-2020) ===
+
+* Allow a single user or multiple users to reattempt a quiz
+
+=== 0.22.1 (28-08-2020) ===
+
+* Avoid duplicate user entry with same email address during upload.
+* Fix a bug where user cannot submit zero as answer
+* Fix UI in question statistics.
+* Fix a bug where the trial question paper was not updated.
+* Fix a bug where answers for fill in the blanks type is not shown.
+
+=== 0.22.0 (27-08-2020) ===
+
+* Fix zero division error if the course does not have any quizzes.
+* Improve question statistics
+* Add Mathjax support to lesson and module.
+
+=== 0.21.0 (02-07-2020) ===
* Added support for hiding test cases for code questions
-== 0.20.2 (02-06-2020) ===
+=== 0.20.2 (02-06-2020) ===
* Added a custom filter to convert str objects to int in templates
* Fixed a bug that prevented users from seeing the last submitted MCQ answers
@@ -12,7 +30,7 @@
* Display question solution in view answer paper
* Fixed bug to check if attempts are allowed and spare time is available before answer is checked
-== 0.20.1 (21-05-2020) ===
+=== 0.20.1 (21-05-2020) ===
* Rename celery.py to celery_settings.py to avoid conflicts with the celery app
diff --git a/yaksh/fixtures/user_existing_email.csv b/yaksh/fixtures/user_existing_email.csv
new file mode 100644
index 0000000..ee5fcd0
--- /dev/null
+++ b/yaksh/fixtures/user_existing_email.csv
@@ -0,0 +1,2 @@
+firstname, lastname, email
+abc, abc, demo_student@test.com
diff --git a/yaksh/fixtures/users_add_update_reject.csv b/yaksh/fixtures/users_add_update_reject.csv
index 1990179..2b8fcf6 100644
--- a/yaksh/fixtures/users_add_update_reject.csv
+++ b/yaksh/fixtures/users_add_update_reject.csv
@@ -1,4 +1,4 @@
firstname, lastname, email, institute,department,roll_no,remove,password,username
test, test, test@g.com, TEST, TEST, TEST101, FALSE, TEST, test
-test2, test, test@g.com, TEST, TEST, TEST101, FALSE, TEST, test2
-test2, test, test@g.com, TEST, TEST, TEST101, TRUE, TEST, test2
+test2, test, test2@g.com, TEST, TEST, TEST101, FALSE, TEST, test2
+test2, test, test2@g.com, TEST, TEST, TEST101, TRUE, TEST, test2
diff --git a/yaksh/migrations/0021_auto_20200703_1556.py b/yaksh/migrations/0021_auto_20200703_1556.py
new file mode 100644
index 0000000..713b2d8
--- /dev/null
+++ b/yaksh/migrations/0021_auto_20200703_1556.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.0.7 on 2020-07-03 10:26
+
+import datetime
+from django.db import migrations, models
+from django.utils.timezone import utc
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('yaksh', '0020_release_0_21_0'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='course',
+ name='end_enroll_time',
+ field=models.DateTimeField(default=datetime.datetime(2198, 12, 31, 18, 7, tzinfo=utc), null=True, verbose_name='End Date and Time for enrollment of course'),
+ ),
+ migrations.AlterField(
+ model_name='quiz',
+ name='end_date_time',
+ field=models.DateTimeField(default=datetime.datetime(2198, 12, 31, 18, 7, tzinfo=utc), null=True, verbose_name='End Date and Time of the quiz'),
+ ),
+ ]
diff --git a/yaksh/migrations/0022_release_0_22_1.py b/yaksh/migrations/0022_release_0_22_1.py
new file mode 100644
index 0000000..5275b86
--- /dev/null
+++ b/yaksh/migrations/0022_release_0_22_1.py
@@ -0,0 +1,30 @@
+# Generated by Django 3.0.7 on 2020-08-28 07:17
+
+import datetime
+from django.db import migrations, models
+from django.utils.timezone import utc
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('yaksh', '0021_auto_20200703_1556'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='course',
+ name='end_enroll_time',
+ field=models.DateTimeField(default=datetime.datetime(2199, 1, 1, 0, 0, tzinfo=utc), null=True, verbose_name='End Date and Time for enrollment of course'),
+ ),
+ migrations.AlterField(
+ model_name='questionpaper',
+ name='fixed_question_order',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AlterField(
+ model_name='quiz',
+ name='end_date_time',
+ field=models.DateTimeField(default=datetime.datetime(2199, 1, 1, 0, 0, tzinfo=utc), null=True, verbose_name='End Date and Time of the quiz'),
+ ),
+ ]
diff --git a/yaksh/migrations/0023_release_0_23_0.py b/yaksh/migrations/0023_release_0_23_0.py
new file mode 100644
index 0000000..0666fb8
--- /dev/null
+++ b/yaksh/migrations/0023_release_0_23_0.py
@@ -0,0 +1,46 @@
+# Generated by Django 3.0.7 on 2020-09-09 02:25
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('yaksh', '0022_release_0_22_1'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='answerpaper',
+ name='extra_time',
+ field=models.FloatField(default=0.0, verbose_name='Additional time in mins'),
+ ),
+ migrations.AddField(
+ model_name='answerpaper',
+ name='is_special',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.CreateModel(
+ name='MicroManager',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('special_attempt', models.BooleanField(default=False)),
+ ('attempts_permitted', models.IntegerField(default=0)),
+ ('permitted_time', models.DateTimeField(default=django.utils.timezone.now)),
+ ('attempts_utilised', models.IntegerField(default=0)),
+ ('wait_time', models.IntegerField(default=0, verbose_name='Days to wait before special attempt')),
+ ('attempt_valid_for', models.IntegerField(default=90, verbose_name='Validity days')),
+ ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yaksh.Course')),
+ ('manager', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='micromanaging', to=settings.AUTH_USER_MODEL)),
+ ('quiz', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='yaksh.Quiz')),
+ ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='micromanaged', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'unique_together': {('student', 'course', 'quiz')},
+ },
+ ),
+ ]
diff --git a/yaksh/models.py b/yaksh/models.py
index 1fee87b..aa0b916 100644
--- a/yaksh/models.py
+++ b/yaksh/models.py
@@ -1045,6 +1045,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)
@@ -1695,17 +1703,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
@@ -1729,7 +1735,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",
@@ -1778,7 +1784,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,
@@ -1797,6 +1803,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)
@@ -2141,6 +2148,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:
@@ -2223,11 +2234,16 @@ class AnswerPaper(models.Model):
questions = list(self.questions.all())
return questions
+ def set_extra_time(self, time=0):
+ self.extra_time = time
+ 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):
@@ -2712,3 +2728,108 @@ class Comment(ForumBase):
def __str__(self):
return 'Comment by {0}: {1}'.format(self.creator.username,
self.post_field.title)
+
+
+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)
diff --git a/yaksh/static/yaksh/css/custom.css b/yaksh/static/yaksh/css/custom.css
index f995c61..7756478 100644
--- a/yaksh/static/yaksh/css/custom.css
+++ b/yaksh/static/yaksh/css/custom.css
@@ -131,3 +131,9 @@ body, .dropdown-menu {
#question_card {
border: none;
}
+
+
+iframe {
+ display:block;
+ width:100%;
+}
diff --git a/yaksh/static/yaksh/js/lesson.js b/yaksh/static/yaksh/js/lesson.js
index 55d4846..6eaf6c6 100644
--- a/yaksh/static/yaksh/js/lesson.js
+++ b/yaksh/static/yaksh/js/lesson.js
@@ -32,13 +32,10 @@ $(document).ready(function(){
});
});
- function preview_text(data){
- var preview_div = $("#preview_text_div");
- if (!preview_div.is(":visible")){
- $("#preview_text_div").toggle();
- }
+ function preview_text(data) {
$("#description_body").empty();
- $("#description_body").append(data);
+ $("#description_body").html(data);
+ MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
}
$("#embed").click(function() {
diff --git a/yaksh/static/yaksh/js/question_paper_creation.js b/yaksh/static/yaksh/js/question_paper_creation.js
index 9d04728..1159dd3 100644
--- a/yaksh/static/yaksh/js/question_paper_creation.js
+++ b/yaksh/static/yaksh/js/question_paper_creation.js
@@ -57,7 +57,37 @@ $(document).ready(function(){
$('#design_q').submit(function(eventObj) {
$(this).append('<input type="hidden" name="checked_ques" value='+checked_vals+'>');
return true;
-});
+ });
+
+ $('#add_checkall').on("change", function () {
+ if($(this).prop("checked")) {
+ $("#fixed-available input:checkbox").each(function(index, element) {
+ if(isNaN($(this).val())) {return};
+ $(this).prop("checked", true);
+ checked_vals.push(parseInt($(this).val()))
+ });
+ } else {
+ $("#fixed-available input:checkbox").each(function(index, element){
+ $(this).prop('checked', false);
+ checked_vals.pop(parseInt($(this).val()));
+ });
+ }
+ });
+
+ $('#remove_checkall').on("change", function () {
+ if($(this).prop("checked")) {
+ $("#fixed-added input:checkbox").each(function (index, element) {
+ if(isNaN($(this).val())) { return };
+ $(this).prop('checked', true);
+ checked_vals.push(parseInt($(this).val()));
+ });
+ } else {
+ $("#fixed-added input:checkbox").each(function (index, element) {
+ $(this).prop('checked', false);
+ checked_vals.pop(parseInt($(this).val()));
+ });
+ }
+ });
});//document
function append_tag(tag){
@@ -69,3 +99,4 @@ function append_tag(tag){
tag_name.value = tag.value;
}
}
+
diff --git a/yaksh/templates/yaksh/add_lesson.html b/yaksh/templates/yaksh/add_lesson.html
index b984db0..4211b1b 100644
--- a/yaksh/templates/yaksh/add_lesson.html
+++ b/yaksh/templates/yaksh/add_lesson.html
@@ -9,6 +9,8 @@
</script>
<script type="text/javascript" src="{% static 'yaksh/js/jquery-ui.js' %}">
</script>
+<script type="text/javascript" src="{% static 'yaksh/js/mathjax/MathJax.js' %}?config=TeX-MML-AM_CHTML">
+</script>
{% endblock %}
{% block css %}
@@ -17,15 +19,15 @@
{% endblock %}
{% block content %}
-<div class="container">
+<div class="container-fluid">
{% if error %}
<div class="alert alert-danger">
{{error}}
</div>
{% endif %}
-<div class="container">
+<div class="container-fluid">
<div class="row justify-content-center form-group">
- <div class="col-md-9 col-md-offset-4">
+ <div class="col-md-5 col-md-offset-4">
<a class="btn btn-primary" href="{% url 'yaksh:get_course_modules' course_id %}">
<i class="fa fa-arrow-left"></i>&nbsp;Back
</a>
@@ -137,18 +139,20 @@
</center>
</form>
<hr>
- <div class="card" id="preview_text_div" style="display: none;">
- <div class="card-heading">
- <center>
- <h3>Description Preview</h3>
- </center>
- </div>
- <div class="card-body" id="description_body">
- </div>
- </div>
</fieldset>
</form>
</div>
+ <div class="col-md-6">
+ <div class="card" id="preview_text_div">
+ <div class="card-header">
+ <center>
+ <h3>Description Preview</h3>
+ </center>
+ </div>
+ <div class="card-body" id="description_body">
+ </div>
+ </div>
+ </div>
</div>
</div>
{% endblock %} \ No newline at end of file
diff --git a/yaksh/templates/yaksh/add_module.html b/yaksh/templates/yaksh/add_module.html
index 262c009..7112485 100644
--- a/yaksh/templates/yaksh/add_module.html
+++ b/yaksh/templates/yaksh/add_module.html
@@ -11,6 +11,8 @@
</script>
<script type="text/javascript" src="{% static 'yaksh/js/jquery-ui.js' %}">
</script>
+<script type="text/javascript" src="{% static 'yaksh/js/mathjax/MathJax.js' %}?config=TeX-MML-AM_CHTML">
+</script>
{% endblock %}
{% block css %}
@@ -19,17 +21,7 @@
{% endblock %}
{% block content %}
-<div class="container">
-{% if messages %}
- {% for message in messages %}
- <div class="alert alert-dismissible alert-{{ message.tags }}">
- <button type="button" class="close" data-dismiss="alert">
- <i class="fa fa-close"></i>
- </button>
- <strong>{{ message }}</strong>
- </div>
- {% endfor %}
-{% endif %}
+<div class="container-fluid">
{% if course_id %}
<a class="btn btn-primary" href="{% url 'yaksh:get_course_modules' course_id %}">
<i class="fa fa-arrow-left"></i>
@@ -43,10 +35,23 @@
{% endif %}
</div>
<br>
-{% if status == "add" %}
<div class="container">
+{% if messages %}
+ {% for message in messages %}
+ <div class="alert alert-dismissible alert-{{ message.tags }}">
+ <button type="button" class="close" data-dismiss="alert">
+ <i class="fa fa-close"></i>
+ </button>
+ <strong>{{ message }}</strong>
+ </div>
+ {% endfor %}
+{% endif %}
+</div>
+<br>
+{% if status == "add" %}
+<div class="container-fluid">
<div class="row justify-content-center form-group">
- <div class="col-md-9 col-md-offset-4">
+ <div class="col-md-5 col-md-offset-4">
<form name=frm id=frm action="" method="post">
<fieldset>
{% csrf_token %}
@@ -92,18 +97,20 @@
</center>
</form>
<hr>
- <div class="card" id="preview_text_div" style="display: none;">
- <div class="card-heading">
- <center>
- <h3>Description Preview</h3>
- </center>
- </div>
- <div class="card-body" id="description_body">
- </div>
- </div>
</fieldset>
</form>
</div>
+ <div class="col-md-5">
+ <div class="card" id="preview_text_div">
+ <div class="card-header">
+ <center>
+ <h3>Description Preview</h3>
+ </center>
+ </div>
+ <div class="card-body" id="description_body">
+ </div>
+ </div>
+ </div>
</div>
</div>
{% endif %}
diff --git a/yaksh/templates/yaksh/add_quiz.html b/yaksh/templates/yaksh/add_quiz.html
index 55e3bd6..9b80e0d 100644
--- a/yaksh/templates/yaksh/add_quiz.html
+++ b/yaksh/templates/yaksh/add_quiz.html
@@ -55,7 +55,7 @@
{% if quiz and course_id %}
{% if quiz.questionpaper_set.get.id %}
<center>
- <a href="{% url 'yaksh:designquestionpaper' quiz.id quiz.questionpaper_set.get.id course_id %}" class="btn btn-primary">
+ <a href="{% url 'yaksh:designquestionpaper' course_id quiz.id quiz.questionpaper_set.get.id %}" class="btn btn-primary">
<i class="fa fa-edit"></i> Edit Question Paper
</a>
<a href="{% url 'yaksh:preview_questionpaper' quiz.questionpaper_set.get.id %}" class="btn btn-info" target="_blank">
@@ -65,11 +65,11 @@
<br>
<h4>You can check the quiz by attempting it in the following modes:</h4>
<a class="btn btn-outline-info" name="button" href="{% url 'yaksh:test_quiz' 'usermode' quiz.id course_id %}" target="blank">
- User Mode
+ Try as student
</a>
- <a class="btn btn-outline-info" name="button" href="{% url 'yaksh:test_quiz' 'godmode' quiz.id course_id %}" target="blank">
- God Mode
+ <a class="btn btn-outline-primary" name="button" href="{% url 'yaksh:test_quiz' 'godmode' quiz.id course_id %}" target="blank">
+ Try as teacher
</a>
<a data-toggle="modal" data-target="#help">
<span class="text-info"><i class="fa fa-info-circle"></i> Help</span></a>
@@ -88,13 +88,13 @@
</div>
<div class="modal-body">
<p>
- <b>User Mode:</b> Attempt quiz the way normal users will attempt i.e. -
+ <b>Try as student:</b> Attempt quiz the way students will attempt i.e. -
<ul class="list-group list-group-flush">
<li class="list-group-item">Quiz will have the same duration as that of the original quiz.</li>
<li class="list-group-item">Quiz won't start if the course is inactive or the quiz time has expired.</li>
<li class="list-group-item">You will be notified about quiz prerequisites.(You can still attempt the quiz though)</li>
</ul>
- <b>God Mode:</b> Attempt quiz without any time or eligibilty constraints.
+ <b>Try as teacher:</b> Attempt quiz without any time or eligibilty constraints.
</p>
</div>
</div>
diff --git a/yaksh/templates/yaksh/design_questionpaper.html b/yaksh/templates/yaksh/design_questionpaper.html
index ffbdf5f..fcc3ed5 100644
--- a/yaksh/templates/yaksh/design_questionpaper.html
+++ b/yaksh/templates/yaksh/design_questionpaper.html
@@ -114,12 +114,47 @@
<div id="fixed-available-wrapper">
<p><u>Select questions to add:</u></p>
<div id="fixed-available">
- {% if state == "fixed" or state == "None" %}
+ {% if questions %}
+ {% if state == "fixed" or state == "None" %}
+ <ul class="inputs-list">
+ <h5><input id="add_checkall" name="add_checkall" type="checkbox"> Select All </h5>
+ {% for question in questions %}
+ <li>
+ <label>
+ <input type="checkbox" name="questions" data-qid="{{question.id}}" value={{question.id}}>
+ <span>
+ {% if user == question.user %}
+ <a href="{% url 'yaksh:add_question' question.id %}" target="_blank">{{ question.summary }}</a>
+ {% else %}
+ {{question.summary}}
+ {% endif %}
+ </span>
+ <span> {{ question.points }}</span>
+ </label>
+ </li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ {% endif %}
+ </div>
+ </div>
+ <br />
+ <button id="add-fixed" name="add-fixed" class="btn btn-success pull-right" type="submit">
+ <i class="fa fa-plus-square"></i>&nbsp;Add to paper
+ </button>
+ </div>
+ <div class="col-md-6">
+ <div id="fixed-added-wrapper">
+ <p><u>Fixed questions currently in paper:</u></p>
+ <div id="fixed-added">
+ {% if fixed_questions %}
<ul class="inputs-list">
- {% for question in questions %}
+ <h5><input id="remove_checkall" type="checkbox"> Select All </h5>
+ {% for question in fixed_questions %}
<li>
<label>
- <input type="checkbox" name="questions" data-qid="{{question.id}}" value={{question.id}}>
+ <input type="checkbox" name="added-questions"
+ data-qid="{{question.id}}" value={{question.id}}>
<span>
{% if user == question.user %}
<a href="{% url 'yaksh:add_question' question.id %}" target="_blank">{{ question.summary }}</a>
@@ -127,7 +162,7 @@
{{question.summary}}
{% endif %}
</span>
- <span> {{ question.points }}</span>
+ <span> {{ question.points }} </span>
</label>
</li>
{% endfor %}
@@ -136,35 +171,6 @@
</div>
</div>
<br />
- <button id="add-fixed" name="add-fixed" class="btn btn-success pull-right" type="submit">
- <i class="fa fa-plus-square"></i>&nbsp;Add to paper
- </button>
- </div>
- <div class="col-md-6">
- <div id="fixed-added-wrapper">
- <p><u>Fixed questions currently in paper:</u></p>
- <div id="fixed-added">
- <ul class="inputs-list">
- {% for question in fixed_questions %}
- <li>
- <label>
- <input type="checkbox" name="added-questions"
- data-qid="{{question.id}}" value={{question.id}}>
- <span>
- {% if user == question.user %}
- <a href="{% url 'yaksh:add_question' question.id %}" target="_blank">{{ question.summary }}</a>
- {% else %}
- {{question.summary}}
- {% endif %}
- </span>
- <span> {{ question.points }} </span>
- </label>
- </li>
- {% endfor %}
- </ul>
- </div>
- </div>
- <br />
<button id="remove-fixed" name="remove-fixed" class="btn btn-danger pull-right" type="submit">
<i class="fa fa-minus-square"></i>&nbsp;Remove from paper
</button>
diff --git a/yaksh/templates/yaksh/micromanaged.html b/yaksh/templates/yaksh/micromanaged.html
new file mode 100644
index 0000000..336feec
--- /dev/null
+++ b/yaksh/templates/yaksh/micromanaged.html
@@ -0,0 +1,22 @@
+{% if micromanagers %}
+<hr>
+<div class="row">
+ {% for micromanager in micromanagers %}
+ {% if micromanager.attempts_permitted > 0 %}
+ <div class="col-md-8">
+ <p> You have been given a special attempt to the {{ micromanager.quiz.description }} by the course creator</p>
+ </div>
+ <div class="col-md-3">
+ {% if micromanager.can_student_attempt %}
+ <a class="btn btn-success" href="{% url 'yaksh:special_start' micromanager.id %}">
+ Start Special Attempt
+ </a>
+ {% else %}
+ <span class="badge badge-secondary">Exhausted</span>
+ {% endif %}
+ </div>
+ {% endif %}
+ {% endfor %}
+{% endif %}
+</div>
+
diff --git a/yaksh/templates/yaksh/micromonitor.html b/yaksh/templates/yaksh/micromonitor.html
new file mode 100644
index 0000000..cc059aa
--- /dev/null
+++ b/yaksh/templates/yaksh/micromonitor.html
@@ -0,0 +1,9 @@
+{% if micromanager %}
+ {% if micromanager.can_student_attempt %}
+ <a class="btn btn-danger" href="{% url 'yaksh:revoke_special_attempt' micromanager.id %}">Revoke</a>
+ {% else %}
+ <a class="btn btn-success" href="{% url 'yaksh:allow_special_attempt' user_id course_id quiz_id %}">Allow </a>
+ {% endif %}
+{% else %}
+ <a class="btn btn-success" href="{% url 'yaksh:allow_special_attempt' user_id course_id quiz_id %}">Allow </a>
+{% endif %}
diff --git a/yaksh/templates/yaksh/monitor.html b/yaksh/templates/yaksh/monitor.html
index ef7b033..183ba99 100644
--- a/yaksh/templates/yaksh/monitor.html
+++ b/yaksh/templates/yaksh/monitor.html
@@ -74,6 +74,18 @@ $(document).ready(function()
</div>
</div>
<br>
+ <br>
+ {% if messages %}
+ {% for message in messages %}
+ <div class="alert alert-dismissible alert-{{ message.tags }}">
+ <button type="button" class="close" data-dismiss="alert">
+ <i class="fa fa-close"></i>
+ </button>
+ <strong>{{ message }}</strong>
+ </div>
+ {% endfor %}
+ {% endif %}
+ <br>
<div class="row">
<div class="col-md-4">
<a href="{% url 'yaksh:show_statistics' papers.0.question_paper.id course.id %}" class="btn btn-primary">
@@ -102,8 +114,9 @@ $(document).ready(function()
<th> Institute&nbsp;<i class="fa fa-sort"></i> </th>
<th> Marks&nbsp;<i class="fa fa-sort"></i> </th>
<th> Attempts&nbsp;<i class="fa fa-sort"></i> </th>
- <th> Time&nbsp;<i class="fa fa-sort"></i> </th>
+ <th> Time Left&nbsp;<i class="fa fa-sort"></i> </th>
<th> Status&nbsp;<i class="fa fa-sort"></i> </th>
+ <th> Special Attempt&nbsp;<i class="fa fa-sort"></i> </th>
</tr>
</thead>
<tbody>
@@ -118,7 +131,20 @@ $(document).ready(function()
<td> {{ paper.marks_obtained }} </td>
<td> {{ paper.answers.count }} </td>
<td id="time_left{{forloop.counter0}}"> {{ paper.time_left }} </td>
- <td id="status{{forloop.counter0}}">{{ paper.status }}</td>
+ <td> {% if paper.is_attempt_inprogress %}
+ <form method="post" action="{% url 'yaksh:extend_time' paper.id %}">
+ {% csrf_token %}
+ <div class="form-group">
+ <label for="extra_time"> Time in mins </label>
+ <input type="number" class="form-control" id="extra_time" name="extra_time" required>
+ </div>
+ <button type="submit" class="btn btn-primary">Extend Time</button>
+ </form>
+ {% else %}
+ <span class="badge badge-secondary"> Completed </span>
+ {% endif %}
+ </td>
+ <td>{% specail_attempt_monitor paper.user.id course.id quiz.id %}</td>
</tr>
{% endfor %}
</tbody>
@@ -126,7 +152,6 @@ $(document).ready(function()
<!-- CSV Modal -->
<div class="modal fade" id="csvModal" role="dialog">
<div class="modal-dialog">
-
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html
index ae2f9f4..3f7e67e 100644
--- a/yaksh/templates/yaksh/question.html
+++ b/yaksh/templates/yaksh/question.html
@@ -127,14 +127,14 @@ question_type = "{{ question.type }}";
<button type="button" class="close" data-dismiss="alert">
<i class="fa fa-close"></i>
</button>
- <strong>Note:</strong> {{ notification }}
+ {{ notification }}
</div>
{% else %}
<div id="notification" class="alert alert-info col-md-8" role="alert">
<button type="button" class="close" data-dismiss="alert">
<i class="fa fa-close"></i>
</button>
- <strong>Note:</strong> {{ notification }}
+ {{ notification }}
</div>
{% endif %}
{% else %}
@@ -239,21 +239,21 @@ question_type = "{{ question.type }}";
<!-- Integer type question -->
{% if question.type == "integer" %}
Enter Integer:<br/>
- <input autofocus class="form-control" name="answer" type="number" id="integer" value="{{ last_attempt|safe }}" />
+ <input autofocus class="form-control" name="answer" type="number" id="integer" value="{{ last_attempt|to_integer }}" />
<br><br>
{% endif %}
<!-- String type question -->
{% if question.type == "string" %}
Enter Text:<br/>
- <textarea autofocus name="answer" id="string" class="form-control" style="width: 100%">{{ last_attempt|safe }}</textarea>
+ <textarea autofocus name="answer" id="string" class="form-control" style="width: 100%">{{ last_attempt|to_str }}</textarea>
<br/><br/>
{% endif %}
<!-- Float type question -->
{% if question.type == "float" %}
Enter Decimal Value :<br/>
- <input autofocus class="form-control" name="answer" type="number" step="any" id="float" value="{{ last_attempt|safe }}" />
+ <input autofocus class="form-control" name="answer" type="number" step="any" id="float" value="{{ last_attempt|to_float }}" />
<br/><br/>
{% endif %}
diff --git a/yaksh/templates/yaksh/quit.html b/yaksh/templates/yaksh/quit.html
index ccb0893..a801ea8 100644
--- a/yaksh/templates/yaksh/quit.html
+++ b/yaksh/templates/yaksh/quit.html
@@ -56,7 +56,11 @@
{% csrf_token %}
<center>
<button class="btn btn-outline-success btn-lg" type="submit" name="yes">Yes</button>
+ {% if paper.is_special %}
+ <a class="btn btn-outline-danger btn-lg" name="no" href="{% url 'yaksh:skip_question' paper.questions.first.id paper.attempt_number module_id paper.question_paper.id course_id %}">No</a>
+ {% else %}
<a class="btn btn-outline-danger btn-lg" name="no" href="{% url 'yaksh:start_quiz' paper.attempt_number module_id paper.question_paper.id course_id %}">No</a>
+ {% endif %}
</center>
</form>
{% endblock content %}
diff --git a/yaksh/templates/yaksh/quizzes_user.html b/yaksh/templates/yaksh/quizzes_user.html
index a9f5a43..e28cb69 100644
--- a/yaksh/templates/yaksh/quizzes_user.html
+++ b/yaksh/templates/yaksh/quizzes_user.html
@@ -1,4 +1,5 @@
{% extends "user.html" %}
+{% load custom_filters %}
{% block title %} Student Dashboard {% endblock %}
{% block script %}
@@ -104,6 +105,7 @@
{% endif %}
</div>
</div>
+ {% show_special_attempt user.id course.data.id %}
</div>
<div id="collapse{{course.data.id}}" class="collapse hide" data-parent="#accordion">
<div class="card-body">
diff --git a/yaksh/templates/yaksh/show_video.html b/yaksh/templates/yaksh/show_video.html
index a2edbe0..9c8d133 100644
--- a/yaksh/templates/yaksh/show_video.html
+++ b/yaksh/templates/yaksh/show_video.html
@@ -1,8 +1,12 @@
{% extends "user.html" %}
+{% load static %}
{% load custom_filters %}
{% block title %} {{ learning_module.name }} {% endblock %}
-
+{% block script %}
+<script type="text/javascript" src="{% static 'yaksh/js/mathjax/MathJax.js' %}?config=TeX-MML-AM_CHTML">
+</script>
+{% endblock %}
{% block main %}
<div class="wrapper">
<!-- Sidebar -->
diff --git a/yaksh/templates/yaksh/statistics_question.html b/yaksh/templates/yaksh/statistics_question.html
index 58fd8db..52c29d3 100644
--- a/yaksh/templates/yaksh/statistics_question.html
+++ b/yaksh/templates/yaksh/statistics_question.html
@@ -1,9 +1,10 @@
{% extends "manage.html" %}
+{% block title %} Question Statistics {% endblock %}
{% block pagetitle %} Statistics for {{ quiz.description }}{% endblock pagetitle %}
{% block content %}
-<div class="container">
+<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<ul class="list-group">
@@ -18,13 +19,121 @@
<div class="col-md-9">
{% if question_stats %}
<p><b>Total number of participants: {{ total }}</b></p>
- <table class="table table-bordered table-responsive-sm">
- <tr class="bg-light yakshred"><th>Question</th><th>Type</th><th>Total</th><th>Answered</th></tr>
+ <table class="table table-responsive-sm">
+ <tr class="bg-light yakshred"><th>Question</th><th></th><th>Type</th><th>Total</th><th>Answered Correctly</th></tr>
{% for question, value in question_stats.items %}
- <tr><td>{{ question.summary }}</td><td>{{ question.type }}</td><td>{{value.1}}</td><td>{{ value.0 }} ({% widthratio value.0 value.1 100 %}%)</td></tr>
+ <tr>
+ <td width="45%">{{ question.summary }}
+ <div class="collapse" id="collapse_question_{{question.id}}">
+ <br>
+ <div class="card card-body">
+ <strong>
+ Summary:
+ </strong>
+ <p>
+ {{ question.summary }}
+ </p>
+ <strong>
+ Description:
+ </strong>
+ <p>
+ {{ question.description|safe }}
+ </p>
+ <strong>
+ Points:
+ </strong>
+ <p>
+ {{ question.points }}
+ </p>
+ <strong>
+ Type:
+ </strong>
+ <p>
+ {{ question.get_type_display }}
+ </p>
+ {% if question.type in 'mcq mcc' %}
+ <strong>
+ Test Cases:
+ </strong>
+ <p>
+ <ol>
+ {% for tc in question.testcase_set.all %}
+ <li>
+ {{ tc.mcqtestcase.options }}
+ {% if tc.mcqtestcase.correct %}
+ <span class="badge badge-primary">Correct</span>
+ {% endif %}
+ </li>
+ {% endfor %}
+ </ol>
+ </p>
+ {% endif %}
+ </div>
+ </div>
+ </td>
+ <td>
+ <button class="btn btn-outline-primary" type="button" data-toggle="collapse" data-target="#collapse_question_{{question.id}}" aria-expanded="false" aria-controls="collapseExample">
+ <i class="fa fa-angle-down"></i>&nbsp;More
+ </button>
+ </td>
+ <td>{{ question.type }}</td>
+ <td>{{value.1}}</td><td>{{ value.0 }} ({% widthratio value.0 value.1 100 %}%)</td>
+
+
+ </tr>
{% endfor %}
</table>
{% endif %}
+
+ <!-- The Modal -->
+ <div class="modal" id="question_detail_modal">
+ <div class="modal-dialog">
+ <div class="modal-content">
+
+ <!-- Modal Header -->
+ <div class="modal-header">
+ <h4 class="modal-title">Question Details</h4>
+ <button type="button" class="close" data-dismiss="modal">&times;</button>
+ </div>
+
+ <!-- Modal body -->
+ <div class="modal-body">
+ <table>
+ <tr>
+ <td>Summary</td>
+ <td>{{ question.summary }}</td>
+ </tr>
+ <tr>
+ <td>Description</td>
+ <td>{{ question.description }}</td>
+ </tr> <tr>
+ <td>Type</td>
+ <td>{{ question.type }}</td>
+ </tr> <tr>
+ <td>Points</td>
+ <td>{{ question.points }}</td>
+ </tr>
+ <tr>
+ {% for tc in question.testcase_set.all %}
+ tc
+ {% endfor %}
+ <br><br>
+ </tr>
+ </table>
+ </div>
+
+ <!-- Modal footer -->
+ <div class="modal-footer">
+ <button type="button" class="btn btn-danger" data-dismiss="modal">Close</button>
+ </div>
+
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ <!-- end Modal outer -->
</div>
</div>
</div>
diff --git a/yaksh/templatetags/custom_filters.py b/yaksh/templatetags/custom_filters.py
index 7a065eb..2a01787 100644
--- a/yaksh/templatetags/custom_filters.py
+++ b/yaksh/templatetags/custom_filters.py
@@ -10,6 +10,7 @@ except ImportError:
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter
+from yaksh.models import User, Course, Quiz
register = template.Library()
@@ -122,3 +123,45 @@ def highlight_spaces(text):
return text.replace(
" ", '<span style="background-color:#ffb6db">&nbsp</span>'
)
+
+
+@register.filter(name="to_integer")
+def to_integer(text):
+ try:
+ value = int(text)
+ except ValueError:
+ value = ''
+ return value
+
+
+@register.filter(name="to_float")
+def to_float(text):
+ try:
+ value = float(text)
+ except ValueError:
+ value = ''
+ return value
+
+
+@register.filter(name="to_str")
+def to_str(text):
+ return text.decode("utf-8")
+
+
+@register.inclusion_tag('yaksh/micromanaged.html')
+def show_special_attempt(user_id, course_id):
+ user = User.objects.get(pk=user_id)
+ micromanagers = user.micromanaged.filter(course_id=course_id)
+ context = {'micromanagers': micromanagers}
+ return context
+
+
+@register.inclusion_tag('yaksh/micromonitor.html')
+def specail_attempt_monitor(user_id, course_id, quiz_id):
+ user = User.objects.get(pk=user_id)
+ micromanagers = user.micromanaged.filter(course_id=course_id,
+ quiz_id=quiz_id)
+ context = {'user_id': user_id, 'course_id': course_id, 'quiz_id': quiz_id}
+ if micromanagers.exists():
+ context['micromanager'] = micromanagers.first()
+ return context
diff --git a/yaksh/test_models.py b/yaksh/test_models.py
index 37baf6e..e24a38e 100644
--- a/yaksh/test_models.py
+++ b/yaksh/test_models.py
@@ -5,7 +5,7 @@ from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\
QuestionSet, AnswerPaper, Answer, Course, StandardTestCase,\
StdIOBasedTestCase, FileUpload, McqTestCase, AssignmentUpload,\
LearningModule, LearningUnit, Lesson, LessonFile, CourseStatus, \
- create_group, legend_display_types, Post, Comment
+ create_group, legend_display_types, Post, Comment, MicroManager
from yaksh.code_server import (
ServerPool, get_result as get_result_from_code_server
)
@@ -27,7 +27,7 @@ from yaksh import settings
def setUpModule():
- Group.objects.create(name='moderator')
+ Group.objects.get_or_create(name='moderator')
# create user profile
user = User.objects.create_user(username='creator',
@@ -103,7 +103,8 @@ def setUpModule():
course.save()
LessonFile.objects.create(lesson=lesson)
CourseStatus.objects.create(course=course, user=course_user)
-
+ MicroManager.objects.create(manager=user, course=course, quiz=quiz,
+ student=course_user)
def tearDownModule():
User.objects.all().delete()
@@ -116,6 +117,7 @@ def tearDownModule():
LearningUnit.objects.all().delete()
LearningModule.objects.all().delete()
AnswerPaper.objects.all().delete()
+ MicroManager.objects.all().delete()
Group.objects.all().delete()
@@ -129,6 +131,141 @@ class GlobalMethodsTestCases(unittest.TestCase):
###############################################################################
+class MicroManagerTestCase(unittest.TestCase):
+ def setUp(self):
+ self.micromanager = MicroManager.objects.first()
+ self.course = self.micromanager.course
+ quiz = self.micromanager.quiz
+ self.questionpaper = QuestionPaper.objects.create(quiz=quiz)
+ question = Question.objects.get(summary='Q1')
+ self.questionpaper.fixed_questions.add(question)
+ self.questionpaper.update_total_marks()
+ self.student = User.objects.get(username='course_user')
+
+ def tearDown(self):
+ self.questionpaper.delete()
+
+ def test_micromanager(self):
+ # Given
+ user = User.objects.get(username='creator')
+ course = Course.objects.get(name='Python Course', creator=user)
+ quiz = Quiz.objects.get(description='demo quiz 1')
+ student = User.objects.get(username='course_user')
+
+ # When
+ micromanager = MicroManager.objects.first()
+
+ # Then
+ self.assertIsNotNone(micromanager)
+ self.assertEqual(micromanager.manager, user)
+ self.assertEqual(micromanager.student, student)
+ self.assertEqual(micromanager.course, course)
+ self.assertEqual(micromanager.quiz, quiz)
+ self.assertFalse(micromanager.special_attempt)
+ self.assertEqual(micromanager.attempts_permitted, 0)
+ self.assertEqual(micromanager.attempts_utilised, 0)
+ self.assertEqual(micromanager.wait_time, 0)
+ self.assertEqual(micromanager.attempt_valid_for, 90)
+ self.assertEqual(user.micromanaging.first(), micromanager)
+ self.assertEqual(student.micromanaged.first(), micromanager)
+
+ def test_set_wait_time(self):
+ # Given
+ micromanager = self.micromanager
+
+ # When
+ micromanager.set_wait_time(days=2)
+
+ # Then
+ self.assertEqual(micromanager.wait_time, 2)
+
+ def self_increment_attempts_permitted(self):
+ # Given
+ micromanager = self.micromanager
+
+ # When
+ micromanager.increment_attempts_permitted()
+
+ # Then
+ self.assertEqual(micromanager.attempts_permitted, 1)
+
+ def test_update_permitted_time(self):
+ # Given
+ micromanager = self.micromanager
+ permit_time = timezone.now()
+
+ # When
+ micromanager.update_permitted_time(permit_time)
+
+ # Then
+ self.assertEqual(micromanager.permitted_time, permit_time)
+
+ def test_has_student_attempts_exhausted(self):
+ # Given
+ micromanager = self.micromanager
+
+ # Then
+ self.assertFalse(micromanager.has_student_attempts_exhausted())
+
+ def test_has_quiz_time_exhausted(self):
+ # Given
+ micromanager = self.micromanager
+
+ # Then
+ self.assertFalse(micromanager.has_quiz_time_exhausted())
+
+ def test_is_special_attempt_required(self):
+ # Given
+ micromanager = self.micromanager
+ attempt = 1
+ ip = '127.0.0.1'
+
+ # Then
+ self.assertFalse(micromanager.is_special_attempt_required())
+
+ # When
+ answerpaper = self.questionpaper.make_answerpaper(self.student, ip,
+ attempt,
+ self.course.id)
+ answerpaper.update_marks(state='completed')
+
+ # Then
+ self.assertTrue(micromanager.is_special_attempt_required())
+
+ answerpaper.delete()
+
+ def test_allow_special_attempt(self):
+ # Given
+ micromanager = self.micromanager
+
+ # When
+ micromanager.allow_special_attempt()
+
+ # Then
+ self.assertFalse(micromanager.special_attempt)
+
+ def test_has_special_attempt(self):
+ # Given
+ micromanager = self.micromanager
+
+ # Then
+ self.assertFalse(micromanager.has_special_attempt())
+
+ def test_is_attempt_time_valid(self):
+ # Given
+ micromanager = self.micromanager
+
+ # Then
+ self.assertTrue(micromanager.is_attempt_time_valid())
+
+ def test_can_student_attempt(self):
+ # Given
+ micromanager = self.micromanager
+
+ # Then
+ self.assertFalse(micromanager.can_student_attempt())
+
+
class LessonTestCases(unittest.TestCase):
def setUp(self):
self.lesson = Lesson.objects.get(name='L1')
@@ -842,7 +979,11 @@ class QuestionPaperTestCases(unittest.TestCase):
total_marks=0.0,
shuffle_questions=True
)
-
+ self.question_paper_with_time_between_attempts.fixed_question_order = \
+ "{0}, {1}".format(self.questions[3].id, self.questions[5].id)
+ self.question_paper_with_time_between_attempts.fixed_questions.add(
+ self.questions[3], self.questions[5]
+ )
self.question_paper.fixed_question_order = "{0}, {1}".format(
self.questions[3].id, self.questions[5].id
)
@@ -1030,7 +1171,7 @@ class QuestionPaperTestCases(unittest.TestCase):
qu_list = [str(self.questions_list[0]), str(self.questions_list[1])]
trial_paper = \
QuestionPaper.objects.create_trial_paper_to_test_quiz(
- self.trial_quiz, self.quiz.id
+ self.trial_quiz, self.quiz_with_time_between_attempts.id
)
trial_paper.random_questions.add(self.question_set_1)
trial_paper.random_questions.add(self.question_set_2)
diff --git a/yaksh/test_views.py b/yaksh/test_views.py
index a7ccac2..df408bb 100644
--- a/yaksh/test_views.py
+++ b/yaksh/test_views.py
@@ -2755,6 +2755,33 @@ class TestCourseDetail(TestCase):
id=uploaded_users.first().id).exists()
)
+ def test_upload_existing_user_email(self):
+ # Given
+ self.client.login(
+ username=self.user1.username, password=self.user1_plaintext_pass)
+ csv_file_path = os.path.join(FIXTURES_DIR_PATH,
+ 'user_existing_email.csv')
+ csv_file = open(csv_file_path, 'rb')
+ upload_file = SimpleUploadedFile(csv_file_path, csv_file.read())
+ csv_file.close()
+
+ # When
+ response = self.client.post(
+ reverse('yaksh:upload_users',
+ kwargs={'course_id': self.user1_course.id}),
+ data={'csv_file': upload_file})
+
+ # Then
+ uploaded_users = User.objects.filter(email='demo_student@test.com')
+ self.assertEqual(response.status_code, 302)
+ messages = [m.message for m in get_messages(response.wsgi_request)]
+ self.assertIn('demo_student', messages[0])
+ self.assertTrue(
+ self.user1_course.students.filter(
+ id=uploaded_users.first().id).exists()
+ )
+ self.assertEqual(uploaded_users.count(), 1)
+
def test_upload_users_add_update_reject(self):
# Given
self.client.login(
@@ -5937,12 +5964,11 @@ class TestQuestionPaper(TestCase):
'add-random': ['']}
)
- self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'yaksh/design_questionpaper.html')
- random_set = response.context['random_sets'][0]
- added_random_ques = random_set.questions.all()
- self.assertIn(self.random_que1, added_random_ques)
- self.assertIn(self.random_que2, added_random_ques)
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue(
+ self.question_paper.random_questions.filter(
+ id__in=[self.random_que1.id, self.random_que2.id]).exists()
+ )
# Check if questions already exists
self.client.login(
@@ -5969,10 +5995,11 @@ class TestQuestionPaper(TestCase):
data={'checked_ques': [self.fixed_que.id],
'add-fixed': ''}
)
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context['qpaper'], self.fixed_question_paper)
- self.assertEqual(response.context['fixed_questions'][0],
- self.fixed_que)
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue(
+ self.fixed_question_paper.fixed_questions.filter(
+ id=self.fixed_que.id).exists()
+ )
# Add one more fixed question in question paper
response = self.client.post(
@@ -5983,10 +6010,11 @@ class TestQuestionPaper(TestCase):
data={'checked_ques': [self.question_float.id],
'add-fixed': ''}
)
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context['qpaper'], self.fixed_question_paper)
- self.assertEqual(response.context['fixed_questions'],
- [self.fixed_que, self.question_float])
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue(
+ self.fixed_question_paper.fixed_questions.filter(
+ id=self.question_float.id).exists()
+ )
# Remove fixed question from question paper
response = self.client.post(
@@ -5997,10 +6025,11 @@ class TestQuestionPaper(TestCase):
data={'added-questions': [self.fixed_que.id],
'remove-fixed': ''}
)
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context['qpaper'], self.fixed_question_paper)
- self.assertEqual(response.context['fixed_questions'],
- [self.question_float])
+ self.assertEqual(response.status_code, 302)
+ self.assertFalse(
+ self.fixed_question_paper.fixed_questions.filter(
+ id=self.fixed_que.id).exists()
+ )
# Remove one more fixed question from question paper
response = self.client.post(
@@ -6011,9 +6040,11 @@ class TestQuestionPaper(TestCase):
data={'added-questions': [self.question_float.id],
'remove-fixed': ''}
)
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context['qpaper'], self.fixed_question_paper)
- self.assertEqual(response.context['fixed_questions'], [])
+ self.assertEqual(response.status_code, 302)
+ self.assertFalse(
+ self.fixed_question_paper.fixed_questions.filter(
+ id=self.question_float.id).exists()
+ )
# Remove random questions from question paper
random_que_set = self.question_paper.random_questions.all().first()
@@ -6025,9 +6056,11 @@ class TestQuestionPaper(TestCase):
data={'random_sets': random_que_set.id,
'remove-random': ''}
)
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context['qpaper'], self.question_paper)
- self.assertEqual(len(response.context['random_sets']), 0)
+ self.assertEqual(response.status_code, 302)
+ self.assertFalse(
+ self.question_paper.random_questions.filter(
+ id=random_que_set.id).exists()
+ )
class TestLearningModule(TestCase):
diff --git a/yaksh/urls.py b/yaksh/urls.py
index 0639b25..13e46fc 100644
--- a/yaksh/urls.py
+++ b/yaksh/urls.py
@@ -237,4 +237,13 @@ urlpatterns = [
views.mark_notification, name="mark_notification"),
path('mark/notifications', views.mark_notification,
name="mark_notification"),
+ url(r'^manage/micromanager/allow_special_attempt/(?P<user_id>\d+)/'
+ '(?P<course_id>\d+)/(?P<quiz_id>\d+)/$',
+ views.allow_special_attempt, name='allow_special_attempt'),
+ url(r'^micromanager/special_start/(?P<micromanager_id>\d+)/$',
+ views.special_start, name='special_start'),
+ url(r'^manage/micromanager/special_revoke/(?P<micromanager_id>\d+)/$',
+ views.revoke_special_attempt, name='revoke_special_attempt'),
+ url(r'^manage/extend_time/(?P<paper_id>\d+)/$',
+ views.extend_time, name='extend_time'),
]
diff --git a/yaksh/views.py b/yaksh/views.py
index 3adb536..01f5e4e 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -3,7 +3,7 @@ import csv
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
from django.contrib.auth import login, logout, authenticate
from django.shortcuts import render, get_object_or_404, redirect
-from django.template import Context, Template
+from django.template import Context, Template, loader
from django.http import Http404
from django.db.models import Max, Q, F
from django.db import models
@@ -23,7 +23,7 @@ from django.urls import reverse
import json
from textwrap import dedent
import zipfile
-from markdown import Markdown
+import markdown
try:
from StringIO import StringIO as string_io
except ImportError:
@@ -37,7 +37,8 @@ from yaksh.models import (
QuestionPaper, QuestionSet, Quiz, Question, StandardTestCase,
StdIOBasedTestCase, StringTestCase, TestCase, User,
get_model_class, FIXTURES_DIR_PATH, MOD_GROUP_NAME, Lesson, LessonFile,
- LearningUnit, LearningModule, CourseStatus, question_types, Post, Comment
+ LearningUnit, LearningModule, CourseStatus, question_types, Post, Comment,
+ MicroManager
)
from yaksh.forms import (
UserRegisterForm, UserLoginForm, QuizForm, QuestionForm,
@@ -101,7 +102,9 @@ CSV_FIELDS = ['name', 'username', 'roll_number', 'institute', 'department',
def get_html_text(md_text):
"""Takes markdown text and converts it to html"""
- return Markdown().convert(md_text)
+ return markdown.markdown(
+ md_text, extensions=['tables', 'fenced_code']
+ )
def formfield_callback(field):
@@ -481,6 +484,46 @@ def user_login(request):
@login_required
@email_verified
+def special_start(request, micromanager_id=None):
+ user = request.user
+ micromanager = get_object_or_404(MicroManager, pk=micromanager_id,
+ student=user)
+ course = micromanager.course
+ quiz = micromanager.quiz
+ module = course.get_learning_module(quiz)
+ quest_paper = get_object_or_404(QuestionPaper, quiz=quiz)
+
+ if not course.is_enrolled(user):
+ msg = 'You are not enrolled in {0} course'.format(course.name)
+ return quizlist_user(request, msg=msg)
+
+ if not micromanager.can_student_attempt():
+ msg = 'Your special attempts are exhausted for {0}'.format(
+ quiz.description)
+ return quizlist_user(request, msg=msg)
+
+ last_attempt = AnswerPaper.objects.get_user_last_attempt(
+ quest_paper, user, course.id)
+
+ if last_attempt:
+ if last_attempt.is_attempt_inprogress():
+ return show_question(
+ request, last_attempt.current_question(), last_attempt,
+ course_id=course.id, module_id=module.id,
+ previous_question=last_attempt.current_question()
+ )
+
+ attempt_num = micromanager.get_attempt_number()
+ ip = request.META['REMOTE_ADDR']
+ new_paper = quest_paper.make_answerpaper(user, ip, attempt_num, course.id,
+ special=True)
+ micromanager.increment_attempts_utilised()
+ return show_question(request, new_paper.current_question(), new_paper,
+ course_id=course.id, module_id=module.id)
+
+
+@login_required
+@email_verified
def start(request, questionpaper_id=None, attempt_num=None, course_id=None,
module_id=None):
"""Check the user cedentials and if any quiz is available,
@@ -641,7 +684,7 @@ def show_question(request, question, paper, error_message=None,
request, msg, paper.attempt_number, paper.question_paper.id,
course_id=course_id, module_id=module_id
)
- if not quiz.active:
+ if not quiz.active and not paper.is_special:
reason = 'The quiz has been deactivated!'
return complete(
request, reason, paper.attempt_number, paper.question_paper.id,
@@ -819,7 +862,7 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None,
previous_question=current_question)
else:
user_answer = request.POST.get('answer')
- if not user_answer:
+ if not str(user_answer):
msg = "Please submit a valid answer."
return show_question(
request, current_question, paper, notification=msg,
@@ -1444,6 +1487,13 @@ def design_questionpaper(request, course_id, quiz_id, questionpaper_id=None):
question_paper.save()
question_paper.fixed_questions.add(*questions)
messages.success(request, "Questions added successfully")
+ return redirect(
+ 'yaksh:designquestionpaper',
+ course_id=course_id,
+ quiz_id=quiz_id,
+ questionpaper_id=questionpaper_id
+ )
+
else:
messages.warning(request, "Please select atleast one question")
@@ -1462,6 +1512,12 @@ def design_questionpaper(request, course_id, quiz_id, questionpaper_id=None):
question_paper.save()
question_paper.fixed_questions.remove(*question_ids)
messages.success(request, "Questions removed successfully")
+ return redirect(
+ 'yaksh:designquestionpaper',
+ course_id=course_id,
+ quiz_id=quiz_id,
+ questionpaper_id=questionpaper_id
+ )
else:
messages.warning(request, "Please select atleast one question")
@@ -1476,6 +1532,12 @@ def design_questionpaper(request, course_id, quiz_id, questionpaper_id=None):
random_set.questions.add(*random_ques)
question_paper.random_questions.add(random_set)
messages.success(request, "Questions removed successfully")
+ return redirect(
+ 'yaksh:designquestionpaper',
+ course_id=course_id,
+ quiz_id=quiz_id,
+ questionpaper_id=questionpaper_id
+ )
else:
messages.warning(request, "Please select atleast one question")
@@ -1484,6 +1546,12 @@ def design_questionpaper(request, course_id, quiz_id, questionpaper_id=None):
if random_set_ids:
question_paper.random_questions.remove(*random_set_ids)
messages.success(request, "Questions removed successfully")
+ return redirect(
+ 'yaksh:designquestionpaper',
+ course_id=course_id,
+ quiz_id=quiz_id,
+ questionpaper_id=questionpaper_id
+ )
else:
messages.warning(request, "Please select question set")
@@ -1500,8 +1568,8 @@ def design_questionpaper(request, course_id, quiz_id, questionpaper_id=None):
if questions:
questions = _remove_already_present(questionpaper_id, questions)
- question_paper.update_total_marks()
- question_paper.save()
+ question_paper.update_total_marks()
+ question_paper.save()
random_sets = question_paper.random_questions.all()
fixed_questions = question_paper.get_ordered_questions()
context = {
@@ -2409,8 +2477,10 @@ def _read_user_csv(request, reader, course):
messages.info(request, "{0} -- Missing Values".format(counter))
continue
users = User.objects.filter(username=username)
+ if not users.exists():
+ users = User.objects.filter(email=email)
if users.exists():
- user = users[0]
+ user = users.last()
if remove.strip().lower() == 'true':
_remove_from_course(user, course)
messages.info(request, "{0} -- {1} -- User rejected".format(
@@ -3420,3 +3490,93 @@ def hide_comment(request, course_id, uuid):
comment.active = False
comment.save()
return redirect('yaksh:post_comments', course_id, post_uid)
+
+
+@login_required
+@email_verified
+def allow_special_attempt(request, user_id, course_id, quiz_id):
+ user = request.user
+
+ if not is_moderator(user):
+ raise Http404('You are not allowed to view this page')
+
+ course = get_object_or_404(Course, pk=course_id)
+ if not course.is_creator(user) and not course.is_teacher(user):
+ raise Http404('This course does not belong to you')
+
+ quiz = get_object_or_404(Quiz, pk=quiz_id)
+ student = get_object_or_404(User, pk=user_id)
+
+ if not course.is_enrolled(student):
+ raise Http404('The student is not enrolled for this course')
+
+ micromanager, created = MicroManager.objects.get_or_create(
+ course=course, student=student, quiz=quiz
+ )
+ micromanager.manager = user
+ micromanager.save()
+
+ if (not micromanager.is_special_attempt_required() or
+ micromanager.is_last_attempt_inprogress()):
+ name = student.get_full_name()
+ msg = '{} can attempt normally. No special attempt required!'.format(
+ name)
+ elif micromanager.can_student_attempt():
+ msg = '{} already has a special attempt!'.format(
+ student.get_full_name())
+ else:
+ micromanager.allow_special_attempt()
+ msg = 'A special attempt is provided to {}!'.format(
+ student.get_full_name())
+
+ messages.info(request, msg)
+ return my_redirect('/exam/manage/monitor/{0}/{1}/'.format(quiz_id,
+ course_id))
+
+
+@login_required
+@email_verified
+def revoke_special_attempt(request, micromanager_id):
+ user = request.user
+
+ if not is_moderator(user):
+ raise Http404('You are not allowed to view this page')
+
+ micromanager = get_object_or_404(MicroManager, pk=micromanager_id)
+ course = micromanager.course
+ if not course.is_creator(user) and not course.is_teacher(user):
+ raise Http404('This course does not belong to you')
+ micromanager.revoke_special_attempt()
+ msg = 'Revoked special attempt for {}'.format(
+ micromanager.student.get_full_name())
+ messages.info(request, msg)
+ return my_redirect('/exam/manage/monitor/{0}/{1}/'.format(
+ micromanager.quiz.id, course.id))
+
+
+@login_required
+@email_verified
+def extend_time(request, paper_id):
+ user = request.user
+
+ if not is_moderator(user):
+ raise Http404('You are not allowed to view this page')
+
+ anspaper = get_object_or_404(AnswerPaper, pk=paper_id)
+ course = anspaper.course
+ if not course.is_creator(user) and not course.is_teacher(user):
+ raise Http404('This course does not belong to you')
+
+ if request.method == "POST":
+ extra_time = request.POST.get('extra_time', None)
+ if extra_time is None:
+ msg = 'Please provide time'
+ else:
+ anspaper.set_extra_time(extra_time)
+ msg = 'Extra {0} minutes given to {1}'.format(
+ extra_time, anspaper.user.get_full_name())
+ else:
+ msg = 'Bad Request'
+ messages.info(request, msg)
+ return my_redirect('/exam/manage/monitor/{0}/{1}/'.format(
+ anspaper.question_paper.quiz.id, course.id))