summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml5
-rw-r--r--online_test/settings.py3
-rw-r--r--requirements/requirements-common.txt1
-rw-r--r--yaksh/decorators.py40
-rw-r--r--yaksh/fixtures/demo_questions.zipbin4430 -> 3055 bytes
-rw-r--r--yaksh/forms.py17
-rw-r--r--yaksh/live_server_tests/selenium_test.py24
-rw-r--r--yaksh/models.py133
-rw-r--r--yaksh/send_emails.py32
-rw-r--r--yaksh/static/yaksh/js/course.js31
-rw-r--r--yaksh/static/yaksh/js/requesthandler.js8
-rw-r--r--yaksh/static/yaksh/js/show_question.js14
-rw-r--r--yaksh/templates/yaksh/course_detail.html103
-rw-r--r--yaksh/templates/yaksh/login.html8
-rw-r--r--yaksh/templates/yaksh/moderator_dashboard.html2
-rw-r--r--yaksh/templates/yaksh/question.html4
-rw-r--r--yaksh/templates/yaksh/quizzes_user.html11
-rw-r--r--yaksh/templates/yaksh/showquestions.html125
-rw-r--r--yaksh/test_models.py25
-rw-r--r--yaksh/test_views.py180
-rw-r--r--yaksh/urls.py5
-rw-r--r--yaksh/views.py673
22 files changed, 1068 insertions, 376 deletions
diff --git a/.travis.yml b/.travis.yml
index c242e62..3759ca2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -25,3 +25,8 @@ script:
after_success:
- coverage combine
- coverage report
+
+dist: precise
+
+addons:
+ firefox: "46.0"
diff --git a/online_test/settings.py b/online_test/settings.py
index 4ca2967..90cce9d 100644
--- a/online_test/settings.py
+++ b/online_test/settings.py
@@ -113,7 +113,8 @@ EMAIL_HOST_USER = 'email_host_user'
EMAIL_HOST_PASSWORD = 'email_host_password'
-# Set EMAIL_BACKEND to 'django.core.mail.backends.smtp.EmailBackend' in production
+# Set EMAIL_BACKEND to 'django.core.mail.backends.smtp.EmailBackend'
+# in production
EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
# SENDER_EMAIL, REPLY_EMAIL, PRODUCTION_URL, IS_DEVELOPMENT are used in email
diff --git a/requirements/requirements-common.txt b/requirements/requirements-common.txt
index 53a44a4..100d693 100644
--- a/requirements/requirements-common.txt
+++ b/requirements/requirements-common.txt
@@ -6,3 +6,4 @@ tornado
selenium==2.53.6
coverage
psutil
+ruamel.yaml==0.15.23
diff --git a/yaksh/decorators.py b/yaksh/decorators.py
index f0d354c..9e9bc6d 100644
--- a/yaksh/decorators.py
+++ b/yaksh/decorators.py
@@ -1,12 +1,42 @@
-from django.shortcuts import render_to_response
+from django.shortcuts import render_to_response, redirect
from django.conf import settings
from django.template import RequestContext
+# Local imports
+from yaksh.forms import ProfileForm
+
+
+def user_has_profile(user):
+ return hasattr(user, 'profile')
+
+
+def has_profile(func):
+ """
+ This decorator is used to check if the user account has a profile.
+ If the user does not have a profile then redirect the user to
+ profile edit page.
+ """
+
+ def _wrapped_view(request, *args, **kwargs):
+ if user_has_profile(request.user):
+ return func(request, *args, **kwargs)
+ ci = RequestContext(request)
+ if request.user.groups.filter(name='moderator').exists():
+ template = 'manage.html'
+ else:
+ template = 'user.html'
+ form = ProfileForm(user=request.user, instance=None)
+ context = {'template': template, 'form': form}
+ return render_to_response('yaksh/editprofile.html', context,
+ context_instance=ci)
+ return _wrapped_view
+
def email_verified(func):
- """ This decorator is used to check if email is verified.
- If email is not verified then redirect user for email
- verification
+ """
+ This decorator is used to check if email is verified.
+ If email is not verified then redirect user for email
+ verification.
"""
def is_email_verified(request, *args, **kwargs):
@@ -14,7 +44,7 @@ def email_verified(func):
user = request.user
context = {}
if not settings.IS_DEVELOPMENT:
- if user.is_authenticated() and hasattr(user, 'profile'):
+ if user.is_authenticated() and user_has_profile(user):
if not user.profile.is_email_verified:
context['success'] = False
context['msg'] = "Your account is not verified. \
diff --git a/yaksh/fixtures/demo_questions.zip b/yaksh/fixtures/demo_questions.zip
index c68e7ef..4e86485 100644
--- a/yaksh/fixtures/demo_questions.zip
+++ b/yaksh/fixtures/demo_questions.zip
Binary files differ
diff --git a/yaksh/forms.py b/yaksh/forms.py
index 3459be9..2740497 100644
--- a/yaksh/forms.py
+++ b/yaksh/forms.py
@@ -181,9 +181,13 @@ class QuizForm(forms.ModelForm):
user = kwargs.pop('user')
course_id = kwargs.pop('course')
super(QuizForm, self).__init__(*args, **kwargs)
- self.fields['prerequisite'] = forms.ModelChoiceField(
- queryset=Quiz.objects.filter(course__id=course_id,
- is_trial=False))
+
+ prerequisite_list = Quiz.objects.filter(
+ course__id=course_id,
+ is_trial=False
+ ).exclude(id=self.instance.id)
+
+ self.fields['prerequisite'] = forms.ModelChoiceField(prerequisite_list)
self.fields['prerequisite'].required = False
self.fields['course'] = forms.ModelChoiceField(
queryset=Course.objects.filter(id=course_id), empty_label=None)
@@ -240,6 +244,13 @@ class QuizForm(forms.ModelForm):
</p>
""")
+ def clean_prerequisite(self):
+ prereq = self.cleaned_data['prerequisite']
+ if prereq and prereq.prerequisite:
+ if prereq.prerequisite.id == self.instance.id:
+ raise forms.ValidationError("Please set another prerequisite quiz")
+ return prereq
+
class Meta:
model = Quiz
exclude = ["is_trial"]
diff --git a/yaksh/live_server_tests/selenium_test.py b/yaksh/live_server_tests/selenium_test.py
index 859d032..31efcac 100644
--- a/yaksh/live_server_tests/selenium_test.py
+++ b/yaksh/live_server_tests/selenium_test.py
@@ -17,11 +17,8 @@ class ElementDisplay(object):
def __call__(self, driver):
try:
element = EC._find_element(driver, self.locator)
- a = element.value_of_css_property("display") == "none"
- print(a)
- return a
+ return element.value_of_css_property("display") == "none"
except Exception as e:
- print(e)
return False
@@ -40,6 +37,7 @@ class SeleniumTest():
self.driver.get(self.url)
self.login(username, password)
self.open_quiz()
+ self.quit_quiz()
self.close_quiz()
self.logout()
self.driver.close()
@@ -65,7 +63,6 @@ class SeleniumTest():
def submit_answer(self, question_label, answer, loop_count=1):
self.driver.implicitly_wait(2)
for count in range(loop_count):
- print("in")
self.driver.find_element_by_link_text(question_label).click()
submit_answer_elem = self.driver.find_element_by_id("check")
self.driver.execute_script('global_editor.editor.setValue({});'.format(answer))
@@ -131,9 +128,20 @@ class SeleniumTest():
)
start_exam_elem.click()
- self.test_c_question(question_label=2)
- self.test_python_question(question_label=3)
- self.test_bash_question(question_label=1)
+ self.test_c_question(question_label=7)
+ self.test_python_question(question_label=5)
+ self.test_bash_question(question_label=4)
+
+ def quit_quiz(self):
+ quit_link_elem = WebDriverWait(self.driver, 5).until(
+ EC.presence_of_element_located((By.NAME, "quit"))
+ )
+ quit_link_elem.click()
+
+ quit_link_elem = WebDriverWait(self.driver, 5).until(
+ EC.presence_of_element_located((By.NAME, "yes"))
+ )
+ quit_link_elem.click()
def close_quiz(self):
quit_link_elem = WebDriverWait(self.driver, 5).until(
diff --git a/yaksh/models.py b/yaksh/models.py
index 9b3cabe..7198e69 100644
--- a/yaksh/models.py
+++ b/yaksh/models.py
@@ -1,6 +1,9 @@
from __future__ import unicode_literals
from datetime import datetime, timedelta
import json
+import ruamel.yaml
+from ruamel.yaml.scalarstring import PreservedScalarString
+from ruamel.yaml.comments import CommentedMap
from random import sample
from collections import Counter
from django.db import models
@@ -23,9 +26,11 @@ import shutil
import zipfile
import tempfile
from textwrap import dedent
+from ast import literal_eval
from .file_utils import extract_files, delete_files
from yaksh.code_server import submit, SERVER_POOL_PORT
from django.conf import settings
+from django.forms.models import model_to_dict
languages = (
@@ -92,16 +97,23 @@ def get_model_class(model):
return model_class
-def has_profile(user):
- """ check if user has profile """
- return True if hasattr(user, 'profile') else False
-
-
def get_upload_dir(instance, filename):
return os.sep.join((
'question_%s' % (instance.question.id), filename
))
+def dict_to_yaml(dictionary):
+ for k,v in dictionary.items():
+ if isinstance(v, list):
+ for nested_v in v:
+ if isinstance(nested_v, dict):
+ dict_to_yaml(nested_v)
+ elif v and isinstance(v,str):
+ dictionary[k] = PreservedScalarString(v)
+ return ruamel.yaml.round_trip_dump(dictionary, explicit_start=True,
+ default_flow_style=False,
+ allow_unicode=True,
+ )
###############################################################################
class CourseManager(models.Manager):
@@ -380,52 +392,57 @@ class Question(models.Model):
return json.dumps(question_data)
def dump_questions(self, question_ids, user):
- questions = Question.objects.filter(
- id__in=question_ids, user_id=user.id, active=True
- )
+ questions = Question.objects.filter(id__in=question_ids,
+ user_id=user.id, active=True
+ )
questions_dict = []
zip_file_name = string_io()
zip_file = zipfile.ZipFile(zip_file_name, "a")
for question in questions:
test_case = question.get_test_cases()
file_names = question._add_and_get_files(zip_file)
- q_dict = {
- 'summary': question.summary,
- 'description': question.description,
- 'points': question.points, 'language': question.language,
- 'type': question.type, 'active': question.active,
- 'snippet': question.snippet,
- 'testcase': [case.get_field_value() for case in test_case],
- 'files': file_names
- }
+ q_dict = model_to_dict(question, exclude=['id', 'user'])
+ testcases = []
+ for case in test_case:
+ testcases.append(case.get_field_value())
+ q_dict['testcase'] = testcases
+ q_dict['files'] = file_names
+ q_dict['tags'] = [tags.tag.name for tags in q_dict['tags']]
questions_dict.append(q_dict)
- question._add_json_to_zip(zip_file, questions_dict)
+ question._add_yaml_to_zip(zip_file, questions_dict)
return zip_file_name
def load_questions(self, questions_list, user, file_path=None,
files_list=None):
try:
- questions = json.loads(questions_list)
- except ValueError as exc_msg:
- msg = "Error Parsing Json: {0}".format(exc_msg)
- return msg
- for question in questions:
- question['user'] = user
- file_names = question.pop('files')
- test_cases = question.pop('testcase')
- que, result = Question.objects.get_or_create(**question)
- if file_names:
- que._add_files_to_db(file_names, file_path)
- for test_case in test_cases:
- test_case_type = test_case.pop('test_case_type')
- model_class = get_model_class(test_case_type)
- new_test_case, obj_create_status = \
- model_class.objects.get_or_create(
- question=que, **test_case
- )
- new_test_case.type = test_case_type
- new_test_case.save()
- return "Questions Uploaded Successfully"
+ questions = ruamel.yaml.safe_load_all(questions_list)
+ msg = "Questions Uploaded Successfully"
+ for question in questions:
+ question['user'] = user
+ file_names = question.pop('files')
+ test_cases = question.pop('testcase')
+ tags = question.pop('tags')
+ que, result = Question.objects.get_or_create(**question)
+ if file_names:
+ que._add_files_to_db(file_names, file_path)
+ if tags:
+ que.tags.add(*tags)
+ for test_case in test_cases:
+ try:
+ test_case_type = test_case.pop('test_case_type')
+ model_class = get_model_class(test_case_type)
+ new_test_case, obj_create_status = \
+ model_class.objects.get_or_create(
+ question=que, **test_case
+ )
+ new_test_case.type = test_case_type
+ new_test_case.save()
+
+ except:
+ msg = "File not correct."
+ except Exception as exc_msg:
+ msg = "Error Parsing Yaml: {0}".format(exc_msg)
+ return msg
def get_test_cases(self, **kwargs):
tc_list = []
@@ -481,25 +498,30 @@ class Question(models.Model):
file_upload.extract = extract
file_upload.file.save(file_name, django_file, save=True)
- def _add_json_to_zip(self, zip_file, q_dict):
- json_data = json.dumps(q_dict, indent=2)
+ def _add_yaml_to_zip(self, zip_file, q_dict,path_to_file=None):
+
tmp_file_path = tempfile.mkdtemp()
- json_path = os.path.join(tmp_file_path, "questions_dump.json")
- with open(json_path, "w") as json_file:
- json_file.write(json_data)
- zip_file.write(json_path, os.path.basename(json_path))
+ yaml_path = os.path.join(tmp_file_path, "questions_dump.yaml")
+ for elem in q_dict:
+ sorted_dict = CommentedMap(sorted(elem.items(), key=lambda x:x[0]))
+ yaml_block = dict_to_yaml(sorted_dict)
+ with open(yaml_path, "a") as yaml_file:
+ yaml_file.write(yaml_block)
+ zip_file.write(yaml_path, os.path.basename(yaml_path))
zip_file.close()
shutil.rmtree(tmp_file_path)
- def read_json(self, file_path, user, files=None):
- json_file = os.path.join(file_path, "questions_dump.json")
+ def read_yaml(self, file_path, user, files=None):
+ yaml_file = os.path.join(file_path, "questions_dump.yaml")
msg = ""
- if os.path.exists(json_file):
- with open(json_file, 'r') as q_file:
+ 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, file_path, files)
+ msg = self.load_questions(questions_list, user,
+ file_path, files
+ )
else:
- msg = "Please upload zip file with questions_dump.json in it."
+ msg = "Please upload zip file with questions_dump.yaml in it."
if files:
delete_files(files, file_path)
@@ -510,7 +532,7 @@ class Question(models.Model):
settings.FIXTURE_DIRS, 'demo_questions.zip'
)
files, extract_path = extract_files(zip_file_path)
- self.read_json(extract_path, user, files)
+ self.read_yaml(extract_path, user, files)
def __str__(self):
return self.summary
@@ -885,8 +907,13 @@ class QuestionPaper(models.Model):
total_marks=6.0,
shuffle_questions=True
)
+ summaries = ['Roots of quadratic equation', 'Print Output',
+ 'Adding decimals', 'For Loop over String',
+ 'Hello World in File', 'Extract columns from files',
+ 'Check Palindrome', 'Add 3 numbers', 'Reverse a string'
+ ]
questions = Question.objects.filter(active=True,
- summary="Yaksh Demo Question",
+ summary__in=summaries,
user=user)
q_order = [str(que.id) for que in questions]
question_paper.fixed_question_order = ",".join(q_order)
diff --git a/yaksh/send_emails.py b/yaksh/send_emails.py
index 24215dd..ae49f23 100644
--- a/yaksh/send_emails.py
+++ b/yaksh/send_emails.py
@@ -7,11 +7,14 @@ from string import digits, punctuation
import hashlib
from textwrap import dedent
import smtplib
+import os
# Django imports
from django.utils.crypto import get_random_string
from django.conf import settings
-from django.core.mail import send_mass_mail, send_mail
+from django.core.mail import EmailMultiAlternatives, send_mail
+from django.core.files.storage import default_storage
+from django.core.files.base import ContentFile
def generate_activation_key(username):
@@ -57,3 +60,30 @@ def send_user_mail(user_mail, key):
success = False
return success, msg
+
+def send_bulk_mail(subject, email_body, recipients, attachments):
+ try:
+ text_msg = ""
+ msg = EmailMultiAlternatives(subject, text_msg, settings.SENDER_EMAIL,
+ recipients
+ )
+ msg.attach_alternative(email_body, "text/html")
+ if attachments:
+ for file in attachments:
+ path = default_storage.save('attachments/'+file.name,
+ ContentFile(file.read())
+ )
+ msg.attach_file(os.sep.join((settings.MEDIA_ROOT, path)),
+ mimetype="text/html"
+ )
+ default_storage.delete(path)
+ msg.send()
+
+ message = "Email Sent Successfully"
+
+ except Exception as exc_msg:
+ message = """Error: {0}. Please check email address.\
+ If email address is correct then
+ Please contact {1}.""".format(exc_msg, settings.REPLY_EMAIL)
+
+ return message
diff --git a/yaksh/static/yaksh/js/course.js b/yaksh/static/yaksh/js/course.js
index 5b79e68..8fb2773 100644
--- a/yaksh/static/yaksh/js/course.js
+++ b/yaksh/static/yaksh/js/course.js
@@ -35,4 +35,35 @@ $(".reject").change( function(){
});
}
});
+
+$(function() {
+ $('textarea#email_body').froalaEditor({
+ heightMin: 200,
+ heightMax: 200
+ })
+ });
+
+$("#send_mail").click(function(){
+ var subject = $("#subject").val();
+ var body = $('#email_body').val();
+ var status = false;
+ var selected = [];
+ $('#reject input:checked').each(function() {
+ selected.push($(this).attr('value'));
+ });
+
+ if (subject == '' || body == ''){
+ $("#error_msg").html("Please enter mail details");
+ $("#dialog").dialog();
+ }
+ else if (selected.length == 0){
+ $("#error_msg").html("Please select atleast one user");
+ $("#dialog").dialog();
+ }
+ else {
+ status = true;
+ }
+ return status;
+});
+
});
diff --git a/yaksh/static/yaksh/js/requesthandler.js b/yaksh/static/yaksh/js/requesthandler.js
index c5629ab..9890b54 100644
--- a/yaksh/static/yaksh/js/requesthandler.js
+++ b/yaksh/static/yaksh/js/requesthandler.js
@@ -53,13 +53,13 @@ function get_result(uid){
dataType: "html", // Your server can response html, json, xml format.
success: function(data, status, xhr) {
content_type = xhr.getResponseHeader("content-type");
- if(content_type.includes("text/html")) {
+ if(content_type.indexOf("text/html") !== -1) {
clearInterval(checker);
unlock_screen();
document.open();
document.write(data);
document.close();
- } else if(content_type.includes("application/json")) {
+ } else if(content_type.indexOf("application/json") !== -1) {
res = JSON.parse(data);
request_status = res.status;
check_state(request_status, uid);
@@ -125,7 +125,7 @@ $(document).ready(function(){
dataType: "html", // Your server can response html, json, xml format.
success: function(data, status, xhr) {
content_type = xhr.getResponseHeader("content-type");
- if(content_type.includes("text/html")) {
+ if(content_type.indexOf("text/html") !== -1) {
request_status = "initial"
count = 0;
clearInterval(checker);
@@ -133,7 +133,7 @@ $(document).ready(function(){
document.open();
document.write(data);
document.close();
- } else if(content_type.includes("application/json")) {
+ } else if(content_type.indexOf("application/json") !== -1) {
res = JSON.parse(data);
var uid = res.uid;
request_status = res.state;
diff --git a/yaksh/static/yaksh/js/show_question.js b/yaksh/static/yaksh/js/show_question.js
index e3ed1cc..e6825a0 100644
--- a/yaksh/static/yaksh/js/show_question.js
+++ b/yaksh/static/yaksh/js/show_question.js
@@ -37,3 +37,17 @@ function confirm_edit(frm)
else
return true;
}
+
+function append_tag(tag){
+ var tag_name = document.getElementById("question_tags");
+ if (tag_name.value != null){
+ tag_name.value = tag.value+", "+tag_name.value;
+ }
+ else{
+ tag_name.value = tag.value;
+ }
+}
+$(document).ready(function()
+ {
+ $("#questions-table").tablesorter({sortList: [[0,0], [4,0]]});
+ });
diff --git a/yaksh/templates/yaksh/course_detail.html b/yaksh/templates/yaksh/course_detail.html
index cd4144f..bcada42 100644
--- a/yaksh/templates/yaksh/course_detail.html
+++ b/yaksh/templates/yaksh/course_detail.html
@@ -6,6 +6,13 @@
{% block script %}
<script language="JavaScript" type="text/javascript" src="{{ URL_ROOT }}/static/yaksh/js/course.js"></script>
+<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/froala-editor/2.5.1/js/froala_editor.min.js"></script>
+<script src="https://code.jquery.com/ui/1.9.1/jquery-ui.js"></script>
+{% endblock %}
+{% block css %}
+<link rel="stylesheet" href="https://code.jquery.com/ui/1.9.1/themes/base/jquery-ui.css">
+<link href="https://cdnjs.cloudflare.com/ajax/libs/froala-editor/2.5.1/css/froala_editor.min.css" rel="stylesheet" type="text/css" />
+<link href="https://cdnjs.cloudflare.com/ajax/libs/froala-editor/2.5.1/css/froala_style.min.css" rel="stylesheet" type="text/css" />
{% endblock %}
{% block content %}
<br/>
@@ -13,9 +20,17 @@
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
- <li><a href="#student-requests" id="request"> Requested Students </a></li>
- <li><a href="#enrolled-students" id="enroll-students"> Enrolled Students </a></li>
- <li><a href="#rejected-students" id="reject-students"> Rejected Students </a></li>
+ {% if state == 'mail'%}
+ <li><a href="{{URL_ROOT}}/exam/manage/course_detail/{{course.id}}/">
+ Go to Course Details</a></li>
+ {% else %}
+ <li><a href="#student-requests" id="request">
+ Requested Students </a></li>
+ <li><a href="#enrolled-students" id="enroll-students">
+ Enrolled Students </a></li>
+ <li><a href="#rejected-students" id="reject-students">
+ Rejected Students </a></li>
+ {% endif %}
<li>
<a href="{{URL_ROOT}}/exam/manage/toggle_status/{{ course.id }}/">
{% if course.active %}Deactivate Course {% else %} Activate Course {% endif %}</a>
@@ -24,16 +39,70 @@
<a href="{{URL_ROOT}}/exam/manage/duplicate_course/{{ course.id }}/">
Clone Course</a>
</li>
+ <li>
+ <a href="{{URL_ROOT}}/exam/manage/send_mail/{{ course.id }}/">
+ Send Mail</a>
+ </li>
</ul>
</div>
</div>
<div class="col-md-9 col-md-offset-2 main">
<div class="row">
+ {% if message %}
+ <div class="alert alert-warning" role="alert">
+ <center>
+ <strong> {{ message }} </strong>
+ </center>
+ </div>
+ {% endif %}
+ {% if state == 'mail' %}
+ <div id="enrolled-students">
+ <center><b><u>Send Mails to Students</u></b></center><br>
+ {% if course.get_enrolled %}
+ <input type="checkbox" class="reject"/>&nbsp;<font size="2">Select all</font>
+ <div id="reject">
+ <form action="{{URL_ROOT}}/exam/manage/send_mail/{{ course.id }}/" method="post" id="send_mail_form">
+ {% csrf_token %}
+ <table class="table table-striped">
+ <th></th>
+ <th></th>
+ <th>Full Name</th>
+ <th>Email</th>
+ <th>Roll Number</th>
+ <th>Institute</th>
+ <th>Department</th>
+ {% for enrolled in course.get_enrolled %}
+ <tr>
+ <td><input type="checkbox" name="check" value="{{ enrolled.id }}"></td>
+ <td>{{ forloop.counter }}.</td>
+ <td> {{ enrolled.get_full_name|title }} </td>
+ <td> {{enrolled.email}}</td>
+ <td> {{enrolled.profile.roll_number}}</td>
+ <td> {{enrolled.profile.institute}}</td>
+ <td> {{enrolled.profile.department}}</td>
+ </tr>
+ {% endfor %}
+ </table>
+ <br>
+ <textarea name="subject" id="subject" placeholder="Email Subject" cols="50"></textarea>
+ <br><br>
+ <textarea name="body" id="email_body"></textarea><br>
+ Attachments: <input type="file" name="email_attach" multiple="">
+ <br>
+ <button class="btn btn-success" type="submit" name='send_mail' value='send_mail' id="send_mail">
+ Send Mail to Selected Students</button>
+ </div>
+ {% endif %}
+ </form>
+ </div>
+ {% else %}
<div id="student-requests">
<center><b><u>Requests</u></b></center><br>
{% if course.get_requests %}
<input type="checkbox" class="checkall"/>&nbsp;<font size="2">Select all</font>
<div id="enroll-all">
+ <form action="{{URL_ROOT}}/exam/manage/enroll/{{ course.id }}/" method="post">
+ {% csrf_token %}
<table class="table table-striped">
<th></th>
<th></th>
@@ -43,9 +112,7 @@
<th>Institute</th>
<th>Department</th>
<th>Enroll/Reject</th>
- <form action="{{URL_ROOT}}/exam/manage/enroll/{{ course.id }}/" method="post">
- {% csrf_token %}
- {% for request in course.get_requests %}
+ {% for request in course.get_requests %}
<tr>
<td><input type="checkbox" name="check" value="{{ request.id }}"></td>
<td>{{ forloop.counter }}.</td>
@@ -76,6 +143,8 @@
{% if course.get_enrolled %}
<input type="checkbox" class="reject"/>&nbsp;<font size="2">Select all</font>
<div id="reject">
+ <form action="{{URL_ROOT}}/exam/manage/enrolled/reject/{{ course.id }}/" method="post" id="reject-form">
+ {% csrf_token %}
<table class="table table-striped">
<th></th>
<th></th>
@@ -86,9 +155,7 @@
<th>Department</th>
<th>Reject</th>
{% for enrolled in course.get_enrolled %}
- <form action="{{URL_ROOT}}/exam/manage/enrolled/reject/{{ course.id }}/" method="post">
- {% csrf_token %}
- <tr>
+ <tr>
<td><input type="checkbox" name="check" value="{{ enrolled.id }}"></td>
<td>{{ forloop.counter }}.</td>
<td> {{ enrolled.get_full_name|title }} </td>
@@ -99,11 +166,12 @@
<td><a class="btn btn-danger"
href="{{URL_ROOT}}/exam/manage/enrolled/reject/{{ course.id }}/{{ enrolled.id }}/">
Reject </a>
- </td>
- </tr>
+ </td>
+ </tr>
{% endfor %}
</table>
- <button class="btn btn-danger" type="submit" name='reject' value='reject'>Reject Selected</button>
+ <button class="btn btn-danger" type="submit" name='reject' value='reject'>
+ Reject Selected</button>
</div>
{% endif %}
</form>
@@ -114,6 +182,8 @@
{% if course.get_rejected %}
<input type="checkbox" class="enroll"/>&nbsp;<font size="2">Select all</font>
<div id="enroll">
+ <form action="{{URL_ROOT}}/exam/manage/enroll/rejected/{{ course.id }}/" method="post">
+ {% csrf_token %}
<table class="table table-striped">
<th></th>
<th></th>
@@ -123,9 +193,7 @@
<th>Institute</th>
<th>Department</th>
<th>Enroll</th>
- {% for rejected in course.get_rejected %}
- <form action="{{URL_ROOT}}/exam/manage/enroll/rejected/{{ course.id }}/" method="post">
- {% csrf_token %}
+ {% for rejected in course.get_rejected %}
<tr>
<td><input type="checkbox" name="check" value="{{ rejected.id }}"></td>
<td>{{ forloop.counter }}.</td>
@@ -149,6 +217,11 @@
{% endif %}
</form>
</div>
+ {% endif %}
</div>
</div>
+<!-- Dialog to display error message -->
+<div id="dialog" title="Alert">
+ <p id="error_msg"></p>
+</div>
{% endblock %}
diff --git a/yaksh/templates/yaksh/login.html b/yaksh/templates/yaksh/login.html
index e4b5933..f40b12f 100644
--- a/yaksh/templates/yaksh/login.html
+++ b/yaksh/templates/yaksh/login.html
@@ -36,6 +36,14 @@
<li>Scales to over 500+ simultaneous users.</li>
</ul>
</p>
+ <br/>
+ <p><b>Fork us at:</b>
+ <a class = "btn btn-social-icon btn-github"
+ href ="https://github.com/fossee/online_test">
+
+ <span class="fa fa-github" style="font-size:48px"></span>
+ </p>
+ </a>
</div>
</div>
diff --git a/yaksh/templates/yaksh/moderator_dashboard.html b/yaksh/templates/yaksh/moderator_dashboard.html
index faccffe..c61675d 100644
--- a/yaksh/templates/yaksh/moderator_dashboard.html
+++ b/yaksh/templates/yaksh/moderator_dashboard.html
@@ -20,7 +20,7 @@
{{ paper.quiz.course.name }}
</td>
<td>
- <a href="{{URL_ROOT}}/exam/manage/monitor/{{paper.id}}/">{{ paper.quiz.description }}</a>
+ <a href="{{URL_ROOT}}/exam/manage/monitor/{{ paper.quiz.id }}/">{{ paper.quiz.description }}</a>
</td>
<td>
{{ answer_papers|length }} user(s)
diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html
index 9789d25..3a3066c 100644
--- a/yaksh/templates/yaksh/question.html
+++ b/yaksh/templates/yaksh/question.html
@@ -202,8 +202,8 @@ lang = "{{ question.language }}"
{% endif %}
<div class="from-group">
- {% if question.type == "mcq" or "mcc" or "integer" or "float" or "string" %}
- <br><button class="btn btn-primary" type="submit" name="check" id="check" >Submit Answer</button>&nbsp;&nbsp;<br/>
+ {% if question.type == "mcq" or question.type == "mcc" or question.type == "integer" or question.type == "float" or question.type == "string" %}
+ <br><button class="btn btn-primary" type="submit" name="check" id="check">Submit Answer</button>&nbsp;&nbsp;
{% elif question.type == "upload" %}
<br><button class="btn btn-primary" type="submit" name="check" id="check" onClick="return validate();">Upload</button>&nbsp;&nbsp;
diff --git a/yaksh/templates/yaksh/quizzes_user.html b/yaksh/templates/yaksh/quizzes_user.html
index 90d7f8e..b90db18 100644
--- a/yaksh/templates/yaksh/quizzes_user.html
+++ b/yaksh/templates/yaksh/quizzes_user.html
@@ -28,8 +28,9 @@ No Courses to display
<div class="col-md-4">
<h4><b><u> {{ course.name }} by {{ course.creator.get_full_name }}</u></b></h4>
</div>
- <div class="col-md-4">
- {% if course.hidden %}<span class="label label-info">Open Course</span>
+ <div class="col-md-4">
+ {% if not course.active %}
+ <span class="label label-danger">Closed</span>
{% endif %}
{% if user in course.requests.all %} <span class="label label-warning">Request Pending </span>
{% elif user in course.rejected.all %}<span class="label label-danger">Request Rejected</span>
@@ -45,8 +46,8 @@ No Courses to display
<span class="label label-danger">Enrollment Closed</span>
{% endif %}
{% endif %}
- </div>
- </div>
+ </div>
+ </div>
<div class="row">
{% if user in course.students.all %}
@@ -57,7 +58,7 @@ No Courses to display
{% for quiz in course.get_quizzes %}
{% if quiz.active and quiz.course_id == course.id %}
<tr>
- {% if not quiz.is_expired %}
+ {% if not quiz.is_expired and course.active %}
<td>
<a href="{{ URL_ROOT }}/exam/start/{{quiz.questionpaper_set.get.id}}">{{ quiz.description }}</a><br>
</td>
diff --git a/yaksh/templates/yaksh/showquestions.html b/yaksh/templates/yaksh/showquestions.html
index a136ddf..a8983bd 100644
--- a/yaksh/templates/yaksh/showquestions.html
+++ b/yaksh/templates/yaksh/showquestions.html
@@ -2,33 +2,67 @@
{% block title %} Questions {% endblock %}
-{% block pagetitle %} List of Questions {% endblock pagetitle %}
+{% block pagetitle %} Questions {% endblock pagetitle %}
{% block script %}
<script src="{{ URL_ROOT }}/static/yaksh/js/show_question.js"></script>
<script src="{{ URL_ROOT }}/static/yaksh/js/question_filter.js"></script>
+<script src="{{ URL_ROOT }}/static/yaksh/js/jquery.tablesorter.min.js"></script>
{% endblock %}
{% block content %}
-
-<h4>Upload ZIP file for adding questions</h4>
+<div class="row">
+ <div class="col-sm-3 col-md-2 sidebar">
+ <ul class="nav nav-sidebar nav-stacked">
+ <li class="active"><a href="#show" data-toggle="pill" > Show all Questions</a></li>
+ <li><a href="#updown" data-toggle="pill" > Upload and Download Questions</a></li>
+ </ul>
+ </div>
+<div class="tab-content">
+<!-- Upload Questions -->
+<div id="updown" class="tab-pane fade">
+<a class="btn btn-primary" href="{{URL_ROOT}}/exam/manage/courses/download_yaml_template/"> Download Template</a>
+<br/>
+<h4> Or </h4>
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
-{{ upload_form.as_p }}
-<button class="btn btn-primary" type="submit" name="upload" value="upload">
-Upload File <span class="glyphicon glyphicon-open"></span></button>
+ {{ upload_form.as_p }}
+<br/>
+<h4> And </h4>
+<button class="btn btn-success" type="submit" name="upload" value="upload">
+Upload File <span class="glyphicon glyphicon-open"/></button>
</form>
+</div>
+<!-- End of upload questions -->
+
+<!-- Show questions -->
+<div id="show" class= "tab-pane fade in active">
+<form name=frm action="" method="post">
+{% csrf_token %}
{% if message %}
-<h4>{{ message }}</h4>
+{%if message == "Questions Uploaded Successfully"%}
+<div class="alert alert-success alert-dismissable">
+<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
+ {{ message }}
+</div>
+{%else %}
+<div class="alert alert-danger alert-dismissable">
+ <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
+ {{ message }}
+</div>
+{% endif %}
{% endif %}
{% if msg %}
-<h4>{{ msg }}</h4>
+<div class="alert alert-danger alert-dismissable">
+ <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
+ {{ msg }}
+</div>
{% endif %}
<br><br>
<form name=frm action="" method="post">
-{% csrf_token %}
+<!-- Filtering Questions -->
<div class="row" id="selectors">
- <h5 style="padding-left: 20px;">Filters</h5>
+ <h4 style="padding-left: 20px;">Filters Questions: </h4>
<div class="col-md-3">
{{ form.question_type }}
</div>
@@ -38,25 +72,84 @@ Upload File <span class="glyphicon glyphicon-open"></span></button>
<div class="col-md-3">
{{ form.marks }}
</div>
-</div>
-<br>
- <button class="btn btn-primary" type="button" onClick='location.replace("{{URL_ROOT}}");'>Clear Filters</button>
<br>
+<h4 style="padding-left: 20px;">Or</h4>
+
+<h4 style="padding-left: 20px;">Search using Tags: </h4>
+</div>
+<!-- Searching Tags -->
+{% csrf_token %}
+ <div class="col-md-14">
+ <div class="input-group">
+ <span class="input-group-addon" id="basic-addon1">Search Questions </span>
+ <input type="text" id="question_tags" name="question_tags" class="form-control"
+ placeholder="Search using comma separated Tags">
+ <span class="input-group-btn">
+ <button class="btn btn-default" type="submit">Search</button>
+ </span>
+ <div class="col-md-6">
+ <select class="form-control" id="sel1" onchange="append_tag(this);">
+ {% if all_tags %}
+ <option value="" disabled selected>Available Tags</option>
+ {% for tag in all_tags %}
+ <option>
+ {{tag}}
+ </option>
+ {% endfor %}
+ {% else %}
+ <option value="" disabled selected>No Available Tags</option>
+ {% endif %}
+ </select>
+ </div>
+ </div>
+ </div>
+<br><br>
+<button class="btn btn-primary" type="button" onClick='location.replace("{{URL_ROOT}}");'>
+ Clear Filters</button>
<div id="filtered-questions">
{% if questions %}
<h5><input id="checkall" type="checkbox"> Select All </h5>
-{% for i in questions %}
-<input type="checkbox" name="question" value="{{ i.id }}">&nbsp;&nbsp;<a href="{{URL_ROOT}}/exam/manage/addquestion/{{ i.id }}">{{ i }}</a><br>
+
+<table id="questions-table" class="tablesorter table table table-striped">
+ <thead>
+ <tr>
+ <th> Select </th>
+ <th> Summary </th>
+ <th> Language </th>
+ <th> Type </th>
+ <th> Marks </th>
+ </tr>
+ </thead>
+ <tbody>
+
+{% for question in questions %}
+<tr>
+<td>
+<input type="checkbox" name="question" value="{{ question.id }}">
+</td>
+<td><a href="{{URL_ROOT}}/exam/manage/addquestion/{{ question.id }}">{{question.summary|capfirst}}</a></td>
+<td>{{question.language|capfirst}}</td>
+<td>{{question.type|capfirst}}</td>
+<td>{{question.points}}</td>
+</tr>
{% endfor %}
+</tbody>
+</table>
{% endif %}
</div>
<br>
+<center>
<button class="btn btn-primary" type="button" onclick='location.replace("{{URL_ROOT}}/exam/manage/addquestion/");'>Add Question <span class="glyphicon glyphicon-plus"></span></button>&nbsp;&nbsp;
{% if questions %}
<button class="btn btn-primary" type="submit" name='download' value='download'>Download Selected <span class="glyphicon glyphicon-save"></span></button>&nbsp;&nbsp;
<button class="btn btn-primary" type="submit" name="test" value="test">Test Selected</button>&nbsp;&nbsp;
{% endif %}
<button class="btn btn-danger" type="submit" onClick="return confirm_delete(frm);" name='delete' value='delete'>Delete Selected <span class="glyphicon glyphicon-minus"></span></button>
+</center>
</form>
-{% endblock %}
+</div>
+</div>
+</div>
+<!-- End of Show questions -->
+{% endblock %} \ No newline at end of file
diff --git a/yaksh/test_models.py b/yaksh/test_models.py
index c86d9a3..a940c0f 100644
--- a/yaksh/test_models.py
+++ b/yaksh/test_models.py
@@ -1,8 +1,9 @@
import unittest
from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\
QuestionSet, AnswerPaper, Answer, Course, StandardTestCase,\
- StdIOBasedTestCase, FileUpload, McqTestCase, AssignmentUpload
+ StdIOBasedTestCase, FileUpload, McqTestCase, AssignmentUpload
import json
+import ruamel.yaml as yaml
from datetime import datetime, timedelta
from django.utils import timezone
import pytz
@@ -111,7 +112,7 @@ class QuestionTestCases(unittest.TestCase):
user=self.user1
)
- self.question2 = Question.objects.create(summary='Demo Json',
+ self.question2 = Question.objects.create(summary='Yaml Json',
language='python',
type='code',
active=True,
@@ -159,8 +160,10 @@ class QuestionTestCases(unittest.TestCase):
"language": "Python", "type": "Code",
"testcase": self.test_case_upload_data,
"files": [[file1, 0]],
- "summary": "Json Demo"}]
- self.json_questions_data = json.dumps(questions_data)
+ "summary": "Yaml Demo",
+ "tags": ['yaml_demo']
+ }]
+ self.yaml_questions_data = yaml.safe_dump_all(questions_data)
def tearDown(self):
shutil.rmtree(self.load_tmp_path)
@@ -191,7 +194,7 @@ class QuestionTestCases(unittest.TestCase):
self.assertIn(tag, ['python', 'function'])
def test_dump_questions(self):
- """ Test dump questions into json """
+ """ Test dump questions into Yaml """
question = Question()
question_id = [self.question2.id]
questions_zip = question.dump_questions(question_id, self.user2)
@@ -200,8 +203,8 @@ class QuestionTestCases(unittest.TestCase):
tmp_path = tempfile.mkdtemp()
zip_file.extractall(tmp_path)
test_case = self.question2.get_test_cases()
- with open("{0}/questions_dump.json".format(tmp_path), "r") as f:
- questions = json.loads(f.read())
+ with open("{0}/questions_dump.yaml".format(tmp_path), "r") as f:
+ questions = yaml.safe_load_all(f.read())
for q in questions:
self.assertEqual(self.question2.summary, q['summary'])
self.assertEqual(self.question2.language, q['language'])
@@ -216,13 +219,13 @@ class QuestionTestCases(unittest.TestCase):
os.remove(os.path.join(tmp_path, file))
def test_load_questions(self):
- """ Test load questions into database from json """
+ """ Test load questions into database from Yaml """
question = Question()
- result = question.load_questions(self.json_questions_data, self.user1)
- question_data = Question.objects.get(summary="Json Demo")
+ result = question.load_questions(self.yaml_questions_data, self.user1)
+ question_data = Question.objects.get(summary="Yaml Demo")
file = FileUpload.objects.get(question=question_data)
test_case = question_data.get_test_cases()
- self.assertEqual(question_data.summary, 'Json Demo')
+ self.assertEqual(question_data.summary, 'Yaml Demo')
self.assertEqual(question_data.language, 'Python')
self.assertEqual(question_data.type, 'Code')
self.assertEqual(question_data.description, 'factorial of a no')
diff --git a/yaksh/test_views.py b/yaksh/test_views.py
index ef222e2..064c39d 100644
--- a/yaksh/test_views.py
+++ b/yaksh/test_views.py
@@ -21,8 +21,9 @@ from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\
- QuestionSet, AnswerPaper, Answer, Course, StandardTestCase, has_profile,\
+ QuestionSet, AnswerPaper, Answer, Course, StandardTestCase,\
AssignmentUpload, FileUpload
+from yaksh.decorators import user_has_profile
class TestUserRegistration(TestCase):
@@ -90,18 +91,18 @@ class TestProfile(TestCase):
self.user2.delete()
- def test_has_profile_for_user_without_profile(self):
+ def test_user_has_profile_for_user_without_profile(self):
"""
If no profile exists for user passed as argument return False
"""
- has_profile_status = has_profile(self.user1)
+ has_profile_status = user_has_profile(self.user1)
self.assertFalse(has_profile_status)
- def test_has_profile_for_user_with_profile(self):
+ def test_user_has_profile_for_user_with_profile(self):
"""
If profile exists for user passed as argument return True
"""
- has_profile_status = has_profile(self.user2)
+ has_profile_status = user_has_profile(self.user2)
self.assertTrue(has_profile_status)
@@ -206,6 +207,30 @@ class TestProfile(TestCase):
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'yaksh/editprofile.html')
+ def test_edit_profile_get_for_user_without_profile(self):
+ """
+ If no profile exists a blank profile form will be displayed
+ """
+ self.client.login(
+ username=self.user1.username,
+ password=self.user1_plaintext_pass
+ )
+ response = self.client.get(reverse('yaksh:edit_profile'))
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'yaksh/editprofile.html')
+
+ def test_edit_profile_get_for_user_with_profile(self):
+ """
+ If profile exists a editprofile.html template will be rendered
+ """
+ self.client.login(
+ username=self.user2.username,
+ password=self.user2_plaintext_pass
+ )
+ response = self.client.get(reverse('yaksh:edit_profile'))
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'yaksh/editprofile.html')
+
def test_update_email_for_user_post(self):
""" POST request to update email if multiple users with same email are
found
@@ -249,6 +274,16 @@ class TestStudentDashboard(TestCase):
timezone='UTC'
)
+ # student without profile
+ self.student_no_profile_plaintext_pass = 'student2'
+ self.student_no_profile = User.objects.create_user(
+ username='student_no_profile',
+ password=self.student_no_profile_plaintext_pass,
+ first_name='first_name',
+ last_name='last_name',
+ email='student_no_profile@test.com'
+ )
+
# moderator
self.user_plaintext_pass = 'demo'
self.user = User.objects.create_user(
@@ -291,6 +326,30 @@ class TestStudentDashboard(TestCase):
redirection_url = '/exam/login/?next=/exam/quizzes/'
self.assertRedirects(response, redirection_url)
+ def test_student_dashboard_get_for_user_without_profile(self):
+ """
+ If no profile exists a blank profile form will be displayed
+ """
+ self.client.login(
+ username=self.student_no_profile.username,
+ password=self.student_no_profile_plaintext_pass
+ )
+ response = self.client.get(reverse('yaksh:quizlist_user'))
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'yaksh/editprofile.html')
+
+ def test_student_dashboard_get_for_user_with_profile(self):
+ """
+ If profile exists a editprofile.html template will be rendered
+ """
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ response = self.client.get(reverse('yaksh:quizlist_user'))
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'yaksh/quizzes_user.html')
+
def test_student_dashboard_all_courses_get(self):
"""
Check student dashboard for all non hidden courses
@@ -1195,6 +1254,7 @@ class TestAddTeacher(TestCase):
username=self.user.username,
password=self.user_plaintext_pass
)
+
teacher_id_list = []
for i in range(5):
@@ -1833,6 +1893,70 @@ class TestCourseDetail(TestCase):
self.assertEqual(response.status_code, 200)
course = Course.objects.get(name="Python Course")
self.assertFalse(course.active)
+ self.assertEqual(self.user1_course, response.context['course'])
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'yaksh/course_detail.html')
+
+ def test_send_mail_to_course_students(self):
+ """ Check if bulk mail is sent to multiple students enrolled in a course
+ """
+ self.client.login(
+ username=self.user1.username,
+ password=self.user1_plaintext_pass
+ )
+ self.student2 = User.objects.create_user(
+ username='demo_student2',
+ password=self.student_plaintext_pass,
+ first_name='student_first_name',
+ last_name='student_last_name',
+ email='demo_student2@test.com'
+ )
+ self.student3 = User.objects.create_user(
+ username='demo_student3',
+ password=self.student_plaintext_pass,
+ first_name='student_first_name',
+ last_name='student_last_name',
+ email='demo_student3@test.com'
+ )
+ self.student4 = User.objects.create_user(
+ username='demo_student4',
+ password=self.student_plaintext_pass,
+ first_name='student_first_name',
+ last_name='student_last_name',
+ email='demo_student4@test.com'
+ )
+ user_ids = [self.student.id, self.student2.id, self.student3.id,
+ self.student4.id]
+ user_emails = [self.student.email, self.student2.email,
+ self.student3.email, self.student4.email]
+
+ self.user1_course.students.add(*user_ids)
+ attachment = SimpleUploadedFile("file.txt", b"Test")
+ email_data = {
+ 'send_mail': 'send_mail', 'email_attach': [attachment],
+ 'subject': 'test_bulk_mail', 'body': 'Test_Mail',
+ 'check': user_ids
+ }
+ self.client.post(reverse(
+ 'yaksh:send_mail', kwargs={'course_id': self.user1_course.id}),
+ data=email_data
+ )
+ attachment_file = mail.outbox[0].attachments[0][0]
+ subject = mail.outbox[0].subject
+ body = mail.outbox[0].alternatives[0][0]
+ recipients = mail.outbox[0].recipients()
+ self.assertEqual(attachment_file, "file.txt")
+ self.assertEqual(subject, "test_bulk_mail")
+ self.assertEqual(body, "Test_Mail")
+ self.assertSequenceEqual(recipients, user_emails)
+
+ # Test for get request in send mail
+ get_response = self.client.get(reverse(
+ 'yaksh:send_mail', kwargs={'course_id': self.user1_course.id})
+ )
+ self.assertEqual(get_response.status_code, 200)
+ self.assertEqual(get_response.context['course'], self.user1_course)
+ self.assertEqual(get_response.context['state'], 'mail')
class TestEnrollRequest(TestCase):
@@ -2496,6 +2620,16 @@ class TestModeratorDashboard(TestCase):
position='Moderator',
timezone='UTC'
)
+
+ self.mod_no_profile_plaintext_pass = 'demo2'
+ self.mod_no_profile = User.objects.create_user(
+ username='demo_user2',
+ password=self.mod_no_profile_plaintext_pass,
+ first_name='user_first_name22',
+ last_name='user_last_name',
+ email='demo2@test.com'
+ )
+
self.mod_group.user_set.add(self.user)
self.course = Course.objects.create(name="Python Course",
enrollment="Enroll Request", creator=self.user)
@@ -2589,6 +2723,30 @@ class TestModeratorDashboard(TestCase):
self.assertEqual(response.status_code, 200)
self.assertRedirects(response, '/exam/quizzes/')
+ def test_moderator_dashboard_get_for_user_without_profile(self):
+ """
+ If no profile exists a blank profile form will be displayed
+ """
+ self.client.login(
+ username=self.mod_no_profile.username,
+ password=self.mod_no_profile_plaintext_pass
+ )
+ response = self.client.get(reverse('yaksh:quizlist_user'))
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'yaksh/editprofile.html')
+
+ def test_moderator_dashboard_get_for_user_with_profile(self):
+ """
+ If profile exists a editprofile.html template will be rendered
+ """
+ self.client.login(
+ username=self.user.username,
+ password=self.user_plaintext_pass
+ )
+ response = self.client.get(reverse('yaksh:quizlist_user'))
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'yaksh/quizzes_user.html')
+
def test_moderator_dashboard_get_all_quizzes(self):
"""
Check moderator dashboard to get all the moderator created quizzes
@@ -2983,7 +3141,7 @@ class TestShowQuestions(TestCase):
zip_file = string_io(response.content)
zipped_file = zipfile.ZipFile(zip_file, 'r')
self.assertIsNone(zipped_file.testzip())
- self.assertIn('questions_dump.json', zipped_file.namelist())
+ self.assertIn('questions_dump.yaml', zipped_file.namelist())
zip_file.close()
zipped_file.close()
@@ -3012,12 +3170,18 @@ class TestShowQuestions(TestCase):
data={'file': questions_file,
'upload': 'upload'}
)
+ summaries = ['Roots of quadratic equation', 'Print Output',
+ 'Adding decimals', 'For Loop over String',
+ 'Hello World in File', 'Extract columns from files',
+ 'Check Palindrome', 'Add 3 numbers', 'Reverse a string'
+ ]
+
uploaded_ques = Question.objects.filter(active=True,
- summary="Yaksh Demo Question",
+ summary__in=summaries,
user=self.user).count()
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'yaksh/showquestions.html')
- self.assertEqual(uploaded_ques, 3)
+ self.assertEqual(uploaded_ques, 9)
f.close()
dummy_file = SimpleUploadedFile("test.txt", b"test")
response = self.client.post(reverse('yaksh:show_questions'),
diff --git a/yaksh/urls.py b/yaksh/urls.py
index 6daaf46..3a15f99 100644
--- a/yaksh/urls.py
+++ b/yaksh/urls.py
@@ -68,6 +68,7 @@ urlpatterns = [
name="enroll_user"),
url(r'manage/enroll/rejected/(?P<course_id>\d+)/(?P<user_id>\d+)/$',
views.enroll, {'was_rejected': True}),
+ url(r'manage/send_mail/(?P<course_id>\d+)/$', views.send_mail, name="send_mail"),
url(r'manage/reject/(?P<course_id>\d+)/(?P<user_id>\d+)/$', views.reject,
name="reject_user"),
url(r'manage/enrolled/reject/(?P<course_id>\d+)/(?P<user_id>\d+)/$',
@@ -106,5 +107,7 @@ urlpatterns = [
url(r'^manage/download/user_assignment/(?P<question_id>\d+)/(?P<user_id>\d+)/(?P<quiz_id>\d+)/$',
views.download_assignment_file, name="download_user_assignment"),
url(r'^manage/download/quiz_assignments/(?P<quiz_id>\d+)/$',
- views.download_assignment_file, name="download_quiz_assignment")
+ views.download_assignment_file, name="download_quiz_assignment"),
+ url(r'^manage/courses/download_yaml_template/',
+ views.download_yaml_template, name="download_yaml_template"),
]
diff --git a/yaksh/views.py b/yaksh/views.py
index fc550ed..af7951f 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -29,22 +29,25 @@ try:
from StringIO import StringIO as string_io
except ImportError:
from io import BytesIO as string_io
+import re
# Local imports.
from yaksh.code_server import get_result, SERVER_POOL_PORT
-from yaksh.models import get_model_class, Quiz, Question, QuestionPaper, QuestionSet, Course
-from yaksh.models import Profile, Answer, AnswerPaper, User, TestCase, FileUpload,\
- has_profile, StandardTestCase, McqTestCase,\
- StdIOBasedTestCase, HookTestCase, IntegerTestCase,\
- FloatTestCase, StringTestCase
-from yaksh.forms import UserRegisterForm, UserLoginForm, QuizForm,\
- QuestionForm, RandomQuestionForm,\
- QuestionFilterForm, CourseForm, ProfileForm, UploadFileForm,\
- get_object_form, FileForm, QuestionPaperForm
+from yaksh.models import (
+ Answer, AnswerPaper, AssignmentUpload, Course, FileUpload, FloatTestCase,
+ HookTestCase, IntegerTestCase, McqTestCase, Profile,
+ QuestionPaper, QuestionSet, Quiz, Question, StandardTestCase,
+ StdIOBasedTestCase, StringTestCase, TestCase, User,
+ get_model_class
+)
+from yaksh.forms import (
+ UserRegisterForm, UserLoginForm, QuizForm, QuestionForm,
+ RandomQuestionForm, QuestionFilterForm, CourseForm, ProfileForm,
+ UploadFileForm, get_object_form, FileForm, QuestionPaperForm
+)
from .settings import URL_ROOT
-from yaksh.models import AssignmentUpload
from .file_utils import extract_files
-from .send_emails import send_user_mail, generate_activation_key
-from .decorators import email_verified
+from .send_emails import send_user_mail, generate_activation_key, send_bulk_mail
+from .decorators import email_verified, has_profile
def my_redirect(url):
@@ -76,6 +79,7 @@ def add_to_group(users):
if not is_moderator(user):
user.groups.add(group)
+
@email_verified
def index(request, next_url=None):
"""The start page.
@@ -108,16 +112,18 @@ def user_register(request):
if user_email and key:
success, msg = send_user_mail(user_email, key)
context = {'activation_msg': msg}
- return my_render_to_response('yaksh/activation_status.html',
- context)
+ return my_render_to_response(
+ 'yaksh/activation_status.html', context
+ )
return index(request)
else:
return my_render_to_response('yaksh/register.html', {'form': form},
context_instance=ci)
else:
form = UserRegisterForm()
- return my_render_to_response('yaksh/register.html', {'form': form},
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/register.html', {'form': form}, context_instance=ci
+ )
def user_logout(request):
@@ -128,6 +134,7 @@ def user_logout(request):
@login_required
+@has_profile
@email_verified
def quizlist_user(request, enrolled=None):
"""Show All Quizzes that is available to logged-in user."""
@@ -144,13 +151,18 @@ def quizlist_user(request, enrolled=None):
courses = user.students.all()
title = 'Enrolled Courses'
else:
- courses = Course.objects.filter(active=True, is_trial=False, hidden=False)
+ courses = Course.objects.filter(
+ active=True, is_trial=False
+ ).exclude(
+ ~Q(requests=user), ~Q(rejected=user), hidden=True
+ )
title = 'All Courses'
context = {'user': user, 'courses': courses, 'title': title}
- return my_render_to_response("yaksh/quizzes_user.html", context,
- context_instance=ci)
+ return my_render_to_response(
+ "yaksh/quizzes_user.html", context, context_instance=ci
+ )
@login_required
@@ -204,7 +216,10 @@ def add_question(request, question_id=None):
for testcase in TestCase.__subclasses__():
formset = inlineformset_factory(Question, testcase, extra=0,
fields='__all__')
- formsets.append(formset(request.POST, request.FILES, instance=question))
+ formsets.append(formset(
+ request.POST, request.FILES, instance=question
+ )
+ )
files = request.FILES.getlist('file_field')
uploaded_files = FileUpload.objects.filter(question_id=question.id)
if qform.is_valid():
@@ -218,17 +233,23 @@ def add_question(request, question_id=None):
formset.save()
test_case_type = request.POST.get('case_type', None)
else:
- context = {'qform': qform, 'fileform': fileform, 'question': question,
- 'formsets': formsets, 'uploaded_files': uploaded_files}
- return my_render_to_response("yaksh/add_question.html", context,
- context_instance=ci)
+ context = {
+ 'qform': qform,
+ 'fileform': fileform,
+ 'question': question,
+ 'formsets': formsets,
+ 'uploaded_files': uploaded_files
+ }
+ return my_render_to_response(
+ "yaksh/add_question.html", context, context_instance=ci
+ )
qform = QuestionForm(instance=question)
fileform = FileForm()
uploaded_files = FileUpload.objects.filter(question_id=question.id)
formsets = []
for testcase in TestCase.__subclasses__():
- if test_case_type == testcase.__name__.lower():
+ if test_case_type == testcase.__name__.lower():
formset = inlineformset_factory(Question, testcase, extra=1,
fields='__all__')
else:
@@ -237,7 +258,9 @@ def add_question(request, question_id=None):
formsets.append(formset(instance=question))
context = {'qform': qform, 'fileform': fileform, 'question': question,
'formsets': formsets, 'uploaded_files': uploaded_files}
- return my_render_to_response("yaksh/add_question.html", context, context_instance=ci)
+ return my_render_to_response(
+ "yaksh/add_question.html", context, context_instance=ci
+ )
@login_required
@@ -257,32 +280,28 @@ def add_quiz(request, course_id, quiz_id=None):
if form.is_valid():
form.save()
return my_redirect("/exam/manage/courses/")
- else:
- context["form"] = form
- return my_render_to_response('yaksh/add_quiz.html',
- context,
- context_instance=ci)
+
else:
quiz = Quiz.objects.get(id=quiz_id)
form = QuizForm(request.POST, user=user, course=course_id,
- instance=quiz)
+ instance=quiz
+ )
if form.is_valid():
form.save()
- context["quiz_id"] = quiz_id
return my_redirect("/exam/manage/courses/")
+
else:
- if quiz_id is None:
- form = QuizForm(course=course_id, user=user)
- else:
- quiz = Quiz.objects.get(id=quiz_id)
- form = QuizForm(user=user,course=course_id, instance=quiz)
- context["quiz_id"] = quiz_id
- context["form"] = form
- return my_render_to_response('yaksh/add_quiz.html',
- context,
- context_instance=ci)
+ quiz = Quiz.objects.get(id=quiz_id) if quiz_id else None
+ form = QuizForm(user=user,course=course_id, instance=quiz)
+ context["quiz_id"] = quiz_id
+ context["form"] = form
+ return my_render_to_response(
+ 'yaksh/add_quiz.html', context, context_instance=ci
+ )
+
@login_required
+@has_profile
@email_verified
def prof_manage(request, msg=None):
"""Take credentials of the user with professor/moderator
@@ -291,19 +310,19 @@ def prof_manage(request, msg=None):
ci = RequestContext(request)
if user.is_authenticated() and is_moderator(user):
question_papers = QuestionPaper.objects.filter(
- Q(quiz__course__creator=user) |
- Q(quiz__course__teachers=user),
- quiz__is_trial=False
- ).distinct()
- trial_paper = AnswerPaper.objects.filter(user=user,
- question_paper__quiz__is_trial=True
- )
+ Q(quiz__course__creator=user) |
+ Q(quiz__course__teachers=user),
+ quiz__is_trial=False
+ ).distinct()
+ trial_paper = AnswerPaper.objects.filter(
+ user=user, question_paper__quiz__is_trial=True
+ )
if request.method == "POST":
delete_paper = request.POST.getlist('delete_paper')
for answerpaper_id in delete_paper:
answerpaper = AnswerPaper.objects.get(id=answerpaper_id)
qpaper = answerpaper.question_paper
- if qpaper.quiz.course.is_trial == True:
+ if qpaper.quiz.course.is_trial:
qpaper.quiz.course.delete()
else:
if qpaper.answerpaper_set.count() == 1:
@@ -313,16 +332,21 @@ def prof_manage(request, msg=None):
users_per_paper = []
for paper in question_papers:
answer_papers = AnswerPaper.objects.filter(question_paper=paper)
- users_passed = AnswerPaper.objects.filter(question_paper=paper,
- passed=True).count()
- users_failed = AnswerPaper.objects.filter(question_paper=paper,
- passed=False).count()
+ users_passed = AnswerPaper.objects.filter(
+ question_paper=paper, passed=True
+ ).count()
+ users_failed = AnswerPaper.objects.filter(
+ question_paper=paper,
+ passed=False
+ ).count()
temp = paper, answer_papers, users_passed, users_failed
users_per_paper.append(temp)
context = {'user': user, 'users_per_paper': users_per_paper,
'trial_paper': trial_paper, 'msg': msg
}
- return my_render_to_response('yaksh/moderator_dashboard.html', context, context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/moderator_dashboard.html', context, context_instance=ci
+ )
return my_redirect('/exam/login/')
@@ -376,7 +400,7 @@ def start(request, questionpaper_id=None, attempt_num=None):
if not quest_paper.quiz.course.is_enrolled(user):
raise Http404('You are not allowed to view this page!')
# prerequisite check and passing criteria
- if quest_paper.quiz.is_expired():
+ if quest_paper.quiz.is_expired() or not quest_paper.quiz.course.active:
if is_moderator(user):
return redirect("/exam/manage")
return redirect("/exam/quizzes")
@@ -388,16 +412,24 @@ def start(request, questionpaper_id=None, attempt_num=None):
last_attempt = AnswerPaper.objects.get_user_last_attempt(
questionpaper=quest_paper, user=user)
if last_attempt and last_attempt.is_attempt_inprogress():
- return show_question(request, last_attempt.current_question(), last_attempt)
+ return show_question(
+ request, last_attempt.current_question(), last_attempt
+ )
# allowed to start
if not quest_paper.can_attempt_now(user):
if is_moderator(user):
return redirect("/exam/manage")
return redirect("/exam/quizzes")
if attempt_num is None:
- attempt_number = 1 if not last_attempt else last_attempt.attempt_number +1
- context = {'user': user, 'questionpaper': quest_paper,
- 'attempt_num': attempt_number}
+ if not last_attempt:
+ attempt_number = 1
+ else:
+ last_attempt.attempt_number + 1
+ context = {
+ 'user': user,
+ 'questionpaper': quest_paper,
+ 'attempt_num': attempt_number
+ }
if is_moderator(user):
context["user"] = "moderator"
return my_render_to_response('yaksh/intro.html', context,
@@ -418,22 +450,36 @@ def show_question(request, question, paper, error_message=None, notification=Non
user = request.user
if not question:
msg = 'Congratulations! You have successfully completed the quiz.'
- return complete(request, msg, paper.attempt_number, paper.question_paper.id)
+ return complete(
+ request, msg, paper.attempt_number, paper.question_paper.id
+ )
if not paper.question_paper.quiz.active:
reason = 'The quiz has been deactivated!'
- return complete(request, reason, paper.attempt_number, paper.question_paper.id)
+ return complete(
+ request, reason, paper.attempt_number, paper.question_paper.id
+ )
if paper.time_left() <= 0:
- reason='Your time is up!'
- return complete(request, reason, paper.attempt_number, paper.question_paper.id)
+ reason = 'Your time is up!'
+ return complete(
+ request, reason, paper.attempt_number, paper.question_paper.id
+ )
if question in paper.questions_answered.all():
- notification = 'You have already attempted this question successfully' \
- if question.type == "code" else \
+ notification = (
+ 'You have already attempted this question successfully'
+ if question.type == "code" else
'You have already attempted this question'
+ )
test_cases = question.get_test_cases()
files = FileUpload.objects.filter(question_id=question.id, hide=False)
- context = {'question': question, 'paper': paper, 'error_message': error_message,
- 'test_cases': test_cases, 'files': files, 'notification': notification,
- 'last_attempt': question.snippet.encode('unicode-escape')}
+ context = {
+ 'question': question,
+ 'paper': paper,
+ 'error_message': error_message,
+ 'test_cases': test_cases,
+ 'files': files,
+ 'notification': notification,
+ 'last_attempt': question.snippet.encode('unicode-escape')
+ }
answers = paper.get_previous_answers(question)
if answers:
last_attempt = answers[0].answer
@@ -447,8 +493,10 @@ def show_question(request, question, paper, error_message=None, notification=Non
@email_verified
def skip(request, q_id, next_q=None, attempt_num=None, questionpaper_id=None):
user = request.user
- paper = get_object_or_404(AnswerPaper, user=request.user, attempt_number=attempt_num,
- question_paper=questionpaper_id)
+ paper = get_object_or_404(
+ AnswerPaper, user=request.user, attempt_number=attempt_num,
+ question_paper=questionpaper_id
+ )
question = get_object_or_404(Question, pk=q_id)
if request.method == 'POST' and question.type == 'code':
@@ -470,8 +518,12 @@ def skip(request, q_id, next_q=None, attempt_num=None, questionpaper_id=None):
def check(request, q_id, attempt_num=None, questionpaper_id=None):
"""Checks the answers of the user for particular question"""
user = request.user
- paper = get_object_or_404(AnswerPaper, user=request.user, attempt_number=attempt_num,
- question_paper=questionpaper_id)
+ paper = get_object_or_404(
+ AnswerPaper,
+ user=request.user,
+ attempt_number=attempt_num,
+ question_paper=questionpaper_id
+ )
current_question = get_object_or_404(Question, pk=q_id)
if request.method == 'POST':
@@ -483,9 +535,9 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None):
user_answer = int(request.POST.get('answer'))
except ValueError:
msg = "Please enter an Integer Value"
- return show_question(request, current_question,
- paper, notification=msg
- )
+ return show_question(
+ request, current_question, paper, notification=msg
+ )
elif current_question.type == 'float':
try:
user_answer = float(request.POST.get('answer'))
@@ -504,8 +556,9 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None):
assignment_filename = request.FILES.getlist('assignment')
if not assignment_filename:
msg = "Please upload assignment file"
- return show_question(request, current_question, paper, notification=msg)
-
+ return show_question(
+ request, current_question, paper, notification=msg
+ )
for fname in assignment_filename:
assignment_files = AssignmentUpload.objects.filter(
assignmentQuestion=current_question,
@@ -518,14 +571,16 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None):
question_paper=questionpaper_id)
os.remove(assign_file.assignmentFile.path)
assign_file.delete()
- AssignmentUpload.objects.create(user=user,
- assignmentQuestion=current_question, assignmentFile=fname,
- question_paper_id=questionpaper_id
- )
+ AssignmentUpload.objects.create(
+ user=user, assignmentQuestion=current_question,
+ assignmentFile=fname, question_paper_id=questionpaper_id
+ )
user_answer = 'ASSIGNMENT UPLOADED'
if not current_question.grade_assignment_upload:
- new_answer = Answer(question=current_question, answer=user_answer,
- correct=False, error=json.dumps([]))
+ new_answer = Answer(
+ question=current_question, answer=user_answer,
+ correct=False, error=json.dumps([])
+ )
new_answer.save()
paper.answers.add(new_answer)
next_q = paper.add_completed_question(current_question.id)
@@ -534,9 +589,13 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None):
user_answer = request.POST.get('answer')
if not user_answer:
msg = ["Please submit a valid option or code"]
- return show_question(request, current_question, paper, notification=msg)
- new_answer = Answer(question=current_question, answer=user_answer,
- correct=False, error=json.dumps([]))
+ return show_question(
+ request, current_question, paper, notification=msg
+ )
+ new_answer = Answer(
+ question=current_question, answer=user_answer,
+ correct=False, error=json.dumps([])
+ )
new_answer.save()
uid = new_answer.id
paper.answers.add(new_answer)
@@ -544,11 +603,11 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None):
# questions, we obtain the results via XML-RPC with the code executed
# safely in a separate process (the code_server.py) running as nobody.
json_data = current_question.consolidate_answer_data(user_answer, user) \
- if current_question.type == 'code' or \
- current_question.type == 'upload' else None
- result = paper.validate_answer(user_answer, current_question,
- json_data, uid
- )
+ if current_question.type == 'code' or \
+ current_question.type == 'upload' else None
+ result = paper.validate_answer(
+ user_answer, current_question, json_data, uid
+ )
if current_question.type in ['code', 'upload']:
if paper.time_left() <= 0:
url = 'http://localhost:%s' % SERVER_POOL_PORT
@@ -634,12 +693,14 @@ def complete(request, reason=None, attempt_num=None, questionpaper_id=None):
return my_render_to_response('yaksh/complete.html', context)
else:
q_paper = QuestionPaper.objects.get(id=questionpaper_id)
- paper = AnswerPaper.objects.get(user=user, question_paper=q_paper,
- attempt_number=attempt_num)
+ paper = AnswerPaper.objects.get(
+ user=user, question_paper=q_paper,
+ attempt_number=attempt_num
+ )
paper.update_marks()
paper.set_end_time(timezone.now())
message = reason or "Quiz has been submitted"
- context = {'message': message, 'paper': paper}
+ context = {'message': message, 'paper': paper}
return my_render_to_response('yaksh/complete.html', context)
@@ -663,13 +724,14 @@ def add_course(request, course_id=None):
new_course.save()
return my_redirect('/exam/manage/')
else:
- return my_render_to_response('yaksh/add_course.html',
- {'form': form},
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/add_course.html', {'form': form}, context_instance=ci
+ )
else:
form = CourseForm(instance=course)
- return my_render_to_response('yaksh/add_course.html', {'form': form},
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/add_course.html', {'form': form}, context_instance=ci
+ )
@login_required
@@ -679,8 +741,10 @@ def enroll_request(request, course_id):
ci = RequestContext(request)
course = get_object_or_404(Course, pk=course_id)
if not course.is_active_enrollment and course.hidden:
- msg = 'Unable to add enrollments for this course, please contact your '\
+ msg = (
+ 'Unable to add enrollments for this course, please contact your '
'instructor/administrator.'
+ )
return complete(request, msg, attempt_num=None, questionpaper_id=None)
course.request(user)
@@ -732,8 +796,9 @@ def course_detail(request, course_id):
if not course.is_creator(user) and not course.is_teacher(user):
raise Http404('This course does not belong to you')
- return my_render_to_response('yaksh/course_detail.html', {'course': course},
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/course_detail.html', {'course': course}, context_instance=ci
+ )
@login_required
@@ -746,8 +811,11 @@ def enroll(request, course_id, user_id=None, was_rejected=False):
course = get_object_or_404(Course, pk=course_id)
if not course.is_active_enrollment:
- msg = 'Enrollment for this course has been closed, please contact your '\
+ msg = (
+ 'Enrollment for this course has been closed,'
+ ' please contact your '
'instructor/administrator.'
+ )
return complete(request, msg, attempt_num=None, questionpaper_id=None)
if not course.is_creator(user) and not course.is_teacher(user):
@@ -758,8 +826,9 @@ def enroll(request, course_id, user_id=None, was_rejected=False):
else:
enroll_ids = [user_id]
if not enroll_ids:
- return my_render_to_response('yaksh/course_detail.html', {'course': course},
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/course_detail.html', {'course': course}, context_instance=ci
+ )
users = User.objects.filter(id__in=enroll_ids)
course.enroll(was_rejected, *users)
return course_detail(request, course_id)
@@ -767,6 +836,39 @@ def enroll(request, course_id, user_id=None, was_rejected=False):
@login_required
@email_verified
+def send_mail(request, course_id, user_id=None):
+ user = request.user
+ ci = RequestContext(request)
+ 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')
+
+ message = None
+ if request.method == 'POST':
+ user_ids = request.POST.getlist('check')
+ if request.POST.get('send_mail') == 'send_mail':
+ users = User.objects.filter(id__in=user_ids)
+ recipients = [student.email for student in users]
+ email_body = request.POST.get('body')
+ subject = request.POST.get('subject')
+ attachments = request.FILES.getlist('email_attach')
+ message = send_bulk_mail(
+ subject, email_body, recipients, attachments
+ )
+ context = {
+ 'course': course, 'message': message,
+ 'state': 'mail'
+ }
+ return my_render_to_response(
+ 'yaksh/course_detail.html', context, context_instance=ci
+ )
+
+
+@login_required
+@email_verified
def reject(request, course_id, user_id=None, was_enrolled=False):
user = request.user
ci = RequestContext(request)
@@ -782,8 +884,11 @@ def reject(request, course_id, user_id=None, was_enrolled=False):
else:
reject_ids = [user_id]
if not reject_ids:
- return my_render_to_response('yaksh/course_detail.html', {'course': course},
- context_instance=ci)
+ message = "Please select atleast one User"
+ return my_render_to_response(
+ 'yaksh/course_detail.html', {'course': course, 'message': message},
+ context_instance=ci
+ )
users = User.objects.filter(id__in=reject_ids)
course.reject(was_enrolled, *users)
return course_detail(request, course_id)
@@ -846,13 +951,17 @@ def monitor(request, quiz_id=None):
raise Http404('You are not allowed to view this page!')
if quiz_id is None:
- course_details = Course.objects.filter(Q(creator=user) |
- Q(teachers=user),
- is_trial=False).distinct()
- context = {'papers': [], "course_details": course_details,
- "msg": "Monitor"}
- return my_render_to_response('yaksh/monitor.html', context,
- context_instance=ci)
+ course_details = Course.objects.filter(
+ Q(creator=user) | Q(teachers=user),
+ is_trial=False
+ ).distinct()
+ context = {
+ "papers": [], "course_details": course_details,
+ "msg": "Monitor"
+ }
+ return my_render_to_response(
+ 'yaksh/monitor.html', context, context_instance=ci
+ )
# quiz_id is not None.
try:
quiz = get_object_or_404(Quiz, id=quiz_id)
@@ -869,15 +978,25 @@ def monitor(request, quiz_id=None):
else:
latest_attempts = []
papers = AnswerPaper.objects.filter(question_paper=q_paper).order_by(
- 'user__profile__roll_number')
+ 'user__profile__roll_number'
+ )
users = papers.values_list('user').distinct()
for auser in users:
last_attempt = papers.filter(user__in=auser).aggregate(
- last_attempt_num=Max('attempt_number'))
- latest_attempts.append(papers.get(user__in=auser,
- attempt_number=last_attempt['last_attempt_num']))
- context = {'papers': papers, "quiz": quiz, "msg": "Quiz Results",
- 'latest_attempts': latest_attempts}
+ last_attempt_num=Max('attempt_number')
+ )
+ latest_attempts.append(
+ papers.get(
+ user__in=auser,
+ attempt_number=last_attempt['last_attempt_num']
+ )
+ )
+ context = {
+ "papers": papers,
+ "quiz": quiz,
+ "msg": "Quiz Results",
+ "latest_attempts": latest_attempts
+ }
return my_render_to_response('yaksh/monitor.html', context,
context_instance=ci)
@@ -902,17 +1021,19 @@ def ajax_questions_filter(request):
filter_dict['language'] = str(language)
questions = list(Question.objects.filter(**filter_dict))
- return my_render_to_response('yaksh/ajax_question_filter.html',
- {'questions': questions})
+ return my_render_to_response(
+ 'yaksh/ajax_question_filter.html', {'questions': questions}
+ )
def _get_questions(user, question_type, marks):
if question_type is None and marks is None:
return None
if question_type:
- questions = Question.objects.filter(type=question_type,
- user=user,
- active=True
+ questions = Question.objects.filter(
+ type=question_type,
+ user=user,
+ active=True
)
if marks:
questions = questions.filter(points=marks)
@@ -1011,11 +1132,20 @@ def design_questionpaper(request, quiz_id, questionpaper_id=None):
question_paper.save()
random_sets = question_paper.random_questions.all()
fixed_questions = question_paper.get_ordered_questions()
- context = {'qpaper_form': qpaper_form, 'filter_form': filter_form, 'qpaper':
- question_paper, 'questions': questions, 'fixed_questions': fixed_questions,
- 'state': state, 'random_sets': random_sets}
- return my_render_to_response('yaksh/design_questionpaper.html', context,
- context_instance=RequestContext(request))
+ context = {
+ 'qpaper_form': qpaper_form,
+ 'filter_form': filter_form,
+ 'qpaper': question_paper,
+ 'questions': questions,
+ 'fixed_questions': fixed_questions,
+ 'state': state,
+ 'random_sets': random_sets
+ }
+ return my_render_to_response(
+ 'yaksh/design_questionpaper.html',
+ context,
+ context_instance=RequestContext(request)
+ )
@login_required
@@ -1029,6 +1159,18 @@ def show_all_questions(request):
if not is_moderator(user):
raise Http404("You are not allowed to view this page !")
+ questions = Question.objects.filter(user_id=user.id, active=True)
+ form = QuestionFilterForm(user=user)
+ user_tags = questions.values_list('tags', flat=True).distinct()
+ all_tags = Tag.objects.filter(id__in = user_tags)
+ upload_form = UploadFileForm()
+ context['questions'] = questions
+ context['all_tags'] = all_tags
+ context['papers'] = []
+ context['question'] = None
+ context['form'] = form
+ context['upload_form'] = upload_form
+
if request.method == 'POST':
if request.POST.get('delete') == 'delete':
data = request.POST.getlist('question')
@@ -1047,7 +1189,7 @@ def show_all_questions(request):
if file_name[-1] == "zip":
ques = Question()
files, extract_path = extract_files(questions_file)
- context['message'] = ques.read_json(extract_path, user,
+ context['message'] = ques.read_yaml(extract_path, user,
files)
else:
message = "Please Upload a ZIP file"
@@ -1059,8 +1201,9 @@ def show_all_questions(request):
question = Question()
zip_file = question.dump_questions(question_ids, user)
response = HttpResponse(content_type='application/zip')
- response['Content-Disposition'] = dedent(\
- '''attachment; filename={0}_questions.zip'''.format(user))
+ response['Content-Disposition'] = dedent(
+ '''attachment; filename={0}_questions.zip'''.format(user)
+ )
zip_file.seek(0)
response.write(zip_file.read())
return response
@@ -1077,17 +1220,19 @@ def show_all_questions(request):
else:
context["msg"] = "Please select atleast one question to test"
- questions = Question.objects.filter(user_id=user.id, active=True)
- form = QuestionFilterForm(user=user)
- upload_form = UploadFileForm()
- context['papers'] = []
- context['question'] = None
- context['questions'] = questions
- context['form'] = form
- context['upload_form'] = upload_form
+ if request.POST.get('question_tags'):
+ question_tags = request.POST.getlist("question_tags")
+ search_tags = []
+ for tags in question_tags:
+ search_tags.extend(re.split('[; |, |\*|\n]',tags))
+ search_result = Question.objects.filter(tags__name__in=search_tags,
+ user=user).distinct()
+ context['questions'] = search_result
+
return my_render_to_response('yaksh/showquestions.html', context,
context_instance=ci)
+
@login_required
@email_verified
def user_data(request, user_id, questionpaper_id=None):
@@ -1102,6 +1247,7 @@ def user_data(request, user_id, questionpaper_id=None):
return my_render_to_response('yaksh/user_data.html', context,
context_instance=RequestContext(request))
+
@login_required
@email_verified
def download_csv(request, questionpaper_id):
@@ -1148,6 +1294,7 @@ def download_csv(request, questionpaper_id):
writer.writerow(row)
return response
+
@login_required
@email_verified
def grade_user(request, quiz_id=None, user_id=None, attempt_number=None):
@@ -1163,10 +1310,12 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None):
is_trial=False).distinct()
context = {"course_details": course_details}
if quiz_id is not None:
- questionpaper_id = QuestionPaper.objects.filter(quiz_id=quiz_id)\
- .values("id")
- user_details = AnswerPaper.objects\
- .get_users_for_questionpaper(questionpaper_id)
+ questionpaper_id = QuestionPaper.objects.filter(
+ quiz_id=quiz_id
+ ).values("id")
+ user_details = AnswerPaper.objects.get_users_for_questionpaper(
+ questionpaper_id
+ )
quiz = get_object_or_404(Quiz, id=quiz_id)
if not quiz.course.is_creator(current_user) and not \
quiz.course.is_teacher(current_user):
@@ -1175,13 +1324,16 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None):
has_quiz_assignments = AssignmentUpload.objects.filter(
question_paper_id=questionpaper_id
).exists()
- context = {"users": user_details, "quiz_id": quiz_id, "quiz":quiz,
- "has_quiz_assignments": has_quiz_assignments
- }
+ context = {
+ "users": user_details,
+ "quiz_id": quiz_id,
+ "quiz": quiz,
+ "has_quiz_assignments": has_quiz_assignments
+ }
if user_id is not None:
-
- attempts = AnswerPaper.objects.get_user_all_attempts\
- (questionpaper_id, user_id)
+ attempts = AnswerPaper.objects.get_user_all_attempts(
+ questionpaper_id, user_id
+ )
try:
if attempt_number is None:
attempt_number = attempts[0].attempt_number
@@ -1192,14 +1344,18 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None):
user_id=user_id
).exists()
user = User.objects.get(id=user_id)
- data = AnswerPaper.objects.get_user_data(user, questionpaper_id,
- attempt_number
- )
- context = {'data': data, "quiz_id": quiz_id, "users": user_details,
- "attempts": attempts, "user_id": user_id,
- "has_user_assignments": has_user_assignments,
- "has_quiz_assignments": has_quiz_assignments
- }
+ data = AnswerPaper.objects.get_user_data(
+ user, questionpaper_id, attempt_number
+ )
+ context = {
+ "data": data,
+ "quiz_id": quiz_id,
+ "users": user_details,
+ "attempts": attempts,
+ "user_id": user_id,
+ "has_user_assignments": has_user_assignments,
+ "has_quiz_assignments": has_quiz_assignments
+ }
if request.method == "POST":
papers = data['papers']
for paper in papers:
@@ -1213,13 +1369,13 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None):
'comments_%d' % paper.question_paper.id, 'No comments')
paper.save()
-
- return my_render_to_response('yaksh/grade_user.html',
- context, context_instance=ci
- )
+ return my_render_to_response(
+ 'yaksh/grade_user.html', context, context_instance=ci
+ )
@login_required
+@has_profile
@email_verified
def view_profile(request):
""" view moderators and users profile """
@@ -1229,20 +1385,12 @@ def view_profile(request):
template = 'manage.html'
else:
template = 'user.html'
- context = {'template': template}
- if has_profile(user):
- context['user'] = user
- return my_render_to_response('yaksh/view_profile.html', context)
- else:
- form = ProfileForm(user=user)
- msg = True
- context['form'] = form
- context['msg'] = msg
- return my_render_to_response('yaksh/editprofile.html', context,
- context_instance=ci)
+ context = {'template': template, 'user': user}
+ return my_render_to_response('yaksh/view_profile.html', context)
@login_required
+@has_profile
@email_verified
def edit_profile(request):
""" edit profile details facility for moderator and students """
@@ -1254,10 +1402,7 @@ def edit_profile(request):
else:
template = 'user.html'
context = {'template': template}
- if has_profile(user):
- profile = Profile.objects.get(user_id=user.id)
- else:
- profile = None
+ profile = Profile.objects.get(user_id=user.id)
if request.method == 'POST':
form = ProfileForm(request.POST, user=user, instance=profile)
@@ -1268,17 +1413,20 @@ def edit_profile(request):
form_data.user.last_name = request.POST['last_name']
form_data.user.save()
form_data.save()
- return my_render_to_response('yaksh/profile_updated.html',
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/profile_updated.html', context_instance=ci
+ )
else:
context['form'] = form
- return my_render_to_response('yaksh/editprofile.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/editprofile.html', context, context_instance=ci
+ )
else:
form = ProfileForm(user=user, instance=profile)
context['form'] = form
- return my_render_to_response('yaksh/editprofile.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/editprofile.html', context, context_instance=ci
+ )
@login_required
@@ -1296,19 +1444,26 @@ def search_teacher(request, course_id):
context['course'] = course
if user != course.creator and user not in course.teachers.all():
- raise Http404('You are not allowed to view this page!')
+ raise Http404('You are not allowed to view this page!')
if request.method == 'POST':
u_name = request.POST.get('uname')
if not len(u_name) == 0:
- teachers = User.objects.filter(Q(username__icontains=u_name)|
- Q(first_name__icontains=u_name)|Q(last_name__icontains=u_name)|
- Q(email__icontains=u_name)).exclude(Q(id=user.id)|Q(is_superuser=1)|
- Q(id=course.creator.id))
+ teachers = User.objects.filter(
+ Q(username__icontains=u_name) |
+ Q(first_name__icontains=u_name) |
+ Q(last_name__icontains=u_name) |
+ Q(email__icontains=u_name)
+ ).exclude(
+ Q(id=user.id) |
+ Q(is_superuser=1) |
+ Q(id=course.creator.id)
+ )
context['success'] = True
context['teachers'] = teachers
- return my_render_to_response('yaksh/addteacher.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/addteacher.html', context, context_instance=ci
+ )
@login_required
@@ -1336,18 +1491,20 @@ def add_teacher(request, course_id):
course.add_teachers(*teachers)
context['status'] = True
context['teachers_added'] = teachers
- return my_render_to_response('yaksh/addteacher.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/addteacher.html', context, context_instance=ci
+ )
@login_required
@email_verified
def remove_teachers(request, course_id):
- """ remove user from a course """
+ """ remove user from a course """
user = request.user
course = get_object_or_404(Course, pk=course_id)
- if not is_moderator(user) and (user != course.creator and user not in course.teachers.all()):
+ if not is_moderator(user) and (user != course.creator and user
+ not in course.teachers.all()):
raise Http404('You are not allowed to view this page!')
if request.method == "POST":
@@ -1363,14 +1520,16 @@ def test_mode(user, godmode=False, questions_list=None, quiz_id=None):
if questions_list is not None:
trial_course = Course.objects.create_trial_course(user)
trial_quiz = Quiz.objects.create_trial_quiz(trial_course, user)
- trial_questionpaper = QuestionPaper.objects\
- .create_trial_paper_to_test_questions\
- (trial_quiz, questions_list)
+ trial_questionpaper = QuestionPaper.objects.create_trial_paper_to_test_questions(
+ trial_quiz, questions_list
+ )
else:
- trial_quiz = Quiz.objects.create_trial_from_quiz(quiz_id, user, godmode)
- trial_questionpaper = QuestionPaper.objects\
- .create_trial_paper_to_test_quiz\
- (trial_quiz, quiz_id)
+ trial_quiz = Quiz.objects.create_trial_from_quiz(
+ quiz_id, user, godmode
+ )
+ trial_questionpaper = QuestionPaper.objects.create_trial_paper_to_test_quiz(
+ trial_quiz, quiz_id
+ )
return trial_questionpaper
@@ -1395,10 +1554,12 @@ def view_answerpaper(request, questionpaper_id):
quiz = get_object_or_404(QuestionPaper, pk=questionpaper_id).quiz
if quiz.view_answerpaper and user in quiz.course.students.all():
data = AnswerPaper.objects.get_user_data(user, questionpaper_id)
- has_user_assignment = AssignmentUpload.objects.filter(user=user,
- question_paper_id=questionpaper_id).exists()
+ has_user_assignment = AssignmentUpload.objects.filter(
+ user=user,
+ question_paper_id=questionpaper_id
+ ).exists()
context = {'data': data, 'quiz': quiz,
- "has_user_assignment":has_user_assignment}
+ "has_user_assignment": has_user_assignment}
return my_render_to_response('yaksh/view_answerpaper.html', context)
else:
return my_redirect('/exam/quizzes/')
@@ -1457,24 +1618,25 @@ def regrade(request, course_id, question_id=None, answerpaper_id=None, questionp
details.append(answerpaper.regrade(question_id))
return grader(request, extra_context={'details': details})
+
@login_required
@email_verified
def download_course_csv(request, course_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)
+ course = get_object_or_404(Course, pk=course_id)
if not course.is_creator(user) and not course.is_teacher(user):
raise Http404('The question paper does not belong to your course')
- students = course.get_only_students().annotate(roll_number=F('profile__roll_number'),
- institute=F('profile__institute')
- )\
- .values("id", "first_name", "last_name",
- "email","institute",
- "roll_number"
- )
+ students = course.get_only_students().annotate(
+ roll_number=F('profile__roll_number'),
+ institute=F('profile__institute')
+ ).values(
+ "id", "first_name", "last_name",
+ "email", "institute", "roll_number"
+ )
quizzes = Quiz.objects.filter(course=course, is_trial=False)
-
+
for student in students:
total_course_marks = 0.0
user_course_marks = 0.0
@@ -1482,24 +1644,23 @@ def download_course_csv(request, course_id):
quiz_best_marks = AnswerPaper.objects.get_user_best_of_attempts_marks\
(quiz, student["id"])
user_course_marks += quiz_best_marks
- total_course_marks += quiz.questionpaper_set.values_list\
- ("total_marks", flat=True)[0]
+ total_course_marks += quiz.questionpaper_set.values_list(
+ "total_marks", flat=True)[0]
student["{}".format(quiz.description)] = quiz_best_marks
student["total_scored"] = user_course_marks
student["out_of"] = total_course_marks
-
-
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="{0}.csv"'.format(
(course.name).lower().replace('.', ''))
- header = ['first_name', 'last_name', "roll_number","email", "institute"]\
- +[quiz.description for quiz in quizzes] + ['total_scored', 'out_of']
- writer = csv.DictWriter(response,fieldnames=header, extrasaction='ignore')
+ header = ['first_name', 'last_name', "roll_number", "email", "institute"]\
+ + [quiz.description for quiz in quizzes] + ['total_scored', 'out_of']
+ writer = csv.DictWriter(response, fieldnames=header, extrasaction='ignore')
writer.writeheader()
for student in students:
writer.writerow(student)
return response
+
def activate_user(request, key):
ci = RequestContext(request)
profile = get_object_or_404(Profile, activation_key=key)
@@ -1507,8 +1668,9 @@ def activate_user(request, key):
context['success'] = False
if profile.is_email_verified:
context['activation_msg'] = "Your account is already verified"
- return my_render_to_response('yaksh/activation_status.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/activation_status.html', context, context_instance=ci
+ )
if timezone.now() > profile.key_expiry_time:
context['msg'] = dedent("""
@@ -1520,8 +1682,10 @@ def activate_user(request, key):
profile.is_email_verified = True
profile.save()
context['msg'] = "Your account is activated"
- return my_render_to_response('yaksh/activation_status.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/activation_status.html', context, context_instance=ci
+ )
+
def new_activation(request, email=None):
ci = RequestContext(request)
@@ -1534,19 +1698,21 @@ def new_activation(request, email=None):
except MultipleObjectsReturned:
context['email_err_msg'] = "Multiple entries found for this email"\
"Please change your email"
- return my_render_to_response('yaksh/activation_status.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/activation_status.html', context, context_instance=ci
+ )
except ObjectDoesNotExist:
context['success'] = False
context['msg'] = "Your account is not verified. \
Please verify your account"
- return render_to_response('yaksh/activation_status.html',
- context, context_instance=ci)
+ return render_to_response(
+ 'yaksh/activation_status.html', context, context_instance=ci
+ )
if not user.profile.is_email_verified:
user.profile.activation_key = generate_activation_key(user.username)
user.profile.key_expiry_time = timezone.now() + \
- timezone.timedelta(minutes=20)
+ timezone.timedelta(minutes=20)
user.profile.save()
new_user_data = User.objects.get(email=email)
success, msg = send_user_mail(new_user_data.email,
@@ -1559,8 +1725,10 @@ def new_activation(request, email=None):
else:
context['activation_msg'] = "Your account is already verified"
- return my_render_to_response('yaksh/activation_status.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/activation_status.html', context, context_instance=ci
+ )
+
def update_email(request):
context = {}
@@ -1574,8 +1742,10 @@ def update_email(request):
return new_activation(request, email)
else:
context['email_err_msg'] = "Please Update your email"
- return my_render_to_response('yaksh/activation_status.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ 'yaksh/activation_status.html', context, context_instance=ci
+ )
+
@login_required
@email_verified
@@ -1584,10 +1754,9 @@ def download_assignment_file(request, quiz_id, question_id=None, user_id=None):
if not is_moderator(user):
raise Http404("You are not allowed to view this page")
qp = QuestionPaper.objects.get(quiz_id=quiz_id)
- assignment_files, file_name = AssignmentUpload.objects.get_assignments(qp,
- question_id,
- user_id
- )
+ assignment_files, file_name = AssignmentUpload.objects.get_assignments(
+ qp, question_id, user_id
+ )
zipfile_name = string_io()
zip_file = zipfile.ZipFile(zipfile_name, "w")
for f_name in assignment_files:
@@ -1596,8 +1765,9 @@ def download_assignment_file(request, quiz_id, question_id=None, user_id=None):
folder_name = os.sep.join((folder, sub_folder, os.path.basename(
f_name.assignmentFile.name))
)
- zip_file.write(f_name.assignmentFile.path, folder_name
- )
+ zip_file.write(
+ f_name.assignmentFile.path, folder_name
+ )
zip_file.close()
zipfile_name.seek(0)
response = HttpResponse(content_type='application/zip')
@@ -1607,6 +1777,7 @@ def download_assignment_file(request, quiz_id, question_id=None, user_id=None):
response.write(zipfile_name.read())
return response
+
@login_required
@email_verified
def duplicate_course(request, course_id):
@@ -1622,3 +1793,21 @@ def duplicate_course(request, course_id):
'instructor/administrator.'
return complete(request, msg, attempt_num=None, questionpaper_id=None)
return my_redirect('/exam/manage/courses/')
+
+@login_required
+@email_verified
+def download_yaml_template(request):
+ user = request.user
+ if not is_moderator(user):
+ raise Http404('You are not allowed to view this page!')
+ template_path = os.path.join(os.path.dirname(__file__), "fixtures",
+ "demo_questions.zip"
+ )
+ yaml_file = zipfile.ZipFile(template_path, 'r')
+ template_yaml = yaml_file.open('questions_dump.yaml', 'r')
+ response = HttpResponse(template_yaml, content_type='text/yaml')
+ response['Content-Disposition'] = 'attachment;\
+ filename="questions_dump.yaml"'
+
+ return response
+