diff options
-rw-r--r-- | .travis.yml | 5 | ||||
-rw-r--r-- | online_test/settings.py | 3 | ||||
-rw-r--r-- | requirements/requirements-common.txt | 1 | ||||
-rw-r--r-- | yaksh/decorators.py | 40 | ||||
-rw-r--r-- | yaksh/fixtures/demo_questions.zip | bin | 4430 -> 3055 bytes | |||
-rw-r--r-- | yaksh/forms.py | 17 | ||||
-rw-r--r-- | yaksh/live_server_tests/selenium_test.py | 24 | ||||
-rw-r--r-- | yaksh/models.py | 133 | ||||
-rw-r--r-- | yaksh/send_emails.py | 32 | ||||
-rw-r--r-- | yaksh/static/yaksh/js/course.js | 31 | ||||
-rw-r--r-- | yaksh/static/yaksh/js/requesthandler.js | 8 | ||||
-rw-r--r-- | yaksh/static/yaksh/js/show_question.js | 14 | ||||
-rw-r--r-- | yaksh/templates/yaksh/course_detail.html | 103 | ||||
-rw-r--r-- | yaksh/templates/yaksh/login.html | 8 | ||||
-rw-r--r-- | yaksh/templates/yaksh/moderator_dashboard.html | 2 | ||||
-rw-r--r-- | yaksh/templates/yaksh/question.html | 4 | ||||
-rw-r--r-- | yaksh/templates/yaksh/quizzes_user.html | 11 | ||||
-rw-r--r-- | yaksh/templates/yaksh/showquestions.html | 125 | ||||
-rw-r--r-- | yaksh/test_models.py | 25 | ||||
-rw-r--r-- | yaksh/test_views.py | 180 | ||||
-rw-r--r-- | yaksh/urls.py | 5 | ||||
-rw-r--r-- | yaksh/views.py | 673 |
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 Binary files differindex c68e7ef..4e86485 100644 --- a/yaksh/fixtures/demo_questions.zip +++ b/yaksh/fixtures/demo_questions.zip 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"/> <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"/> <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"/> <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"/> <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> <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> {% elif question.type == "upload" %} <br><button class="btn btn-primary" type="submit" name="check" id="check" onClick="return validate();">Upload</button> 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">×</a> + {{ message }} +</div> +{%else %} +<div class="alert alert-danger alert-dismissable"> + <a href="#" class="close" data-dismiss="alert" aria-label="close">×</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">×</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 }}"> <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> {% if questions %} <button class="btn btn-primary" type="submit" name='download' value='download'>Download Selected <span class="glyphicon glyphicon-save"></span></button> <button class="btn btn-primary" type="submit" name="test" value="test">Test Selected</button> {% 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 + |