summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.txt5
-rw-r--r--Dockerfile18
-rw-r--r--README.md58
-rw-r--r--README_production.md7
-rw-r--r--online_test/settings.py6
-rw-r--r--requirements/requirements-codeserver.txt5
-rw-r--r--requirements/requirements-common.txt6
-rw-r--r--tasks.py95
-rw-r--r--yaksh/models.py20
-rw-r--r--yaksh/scripts/yaksh_script.sh11
-rw-r--r--yaksh/templates/yaksh/add_course.html14
-rw-r--r--yaksh/templates/yaksh/monitor.html47
-rw-r--r--yaksh/test_models.py74
-rw-r--r--yaksh/test_views.py56
-rw-r--r--yaksh/urls.py4
-rw-r--r--yaksh/views.py131
16 files changed, 446 insertions, 111 deletions
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index a687446..824a051 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -13,6 +13,11 @@
* UI changes in grade user, view answerpaper and monitor pages.
* Added the facility to create users by uploading CSV.
* Fixed a bug that would require shebang to be put in for Bash assertion based questions.
+* Bug fixed that did not allow to edit a profile.
+* CSV download for quiz attempts enhanced.
+* Updated Courses Page to show Active Courses on top.
+* Added a Datetime Picker to Add Course Page
+* Added invoke script for quickstart and docker deployment
=== 0.6.0 (11-05-2017) ===
diff --git a/Dockerfile b/Dockerfile
index 72a74f4..6a7d894 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,13 +1,11 @@
-FROM debian:8.2
+FROM ubuntu:16.04
MAINTAINER FOSSEE <pythonsupport@fossee.in>
# Update Packages and Install Python & net-tools
-RUN apt-get update && apt-get install -y python net-tools python-pip && pip install tornado
-
-# Copy the project folder from host into container
-COPY ./yaksh /src/yaksh
-
-WORKDIR /src
-
-# Run Yaksh code server
-CMD ["python", "-m", "yaksh.code_server"]
+RUN apt-get update && \
+apt-get install -y software-properties-common && \
+echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections && \
+add-apt-repository -y ppa:webupd8team/java && \
+apt-get update && \
+apt-get install -y oracle-java8-installer && \
+apt-get install -y sudo python net-tools git python3-pip vim libmysqlclient-dev scilab build-essential python3-numpy python3-scipy ipython3 ipython3-notebook python3-pandas python3-nose
diff --git a/README.md b/README.md
index caae485..a0faab2 100644
--- a/README.md
+++ b/README.md
@@ -37,56 +37,52 @@ Quick Start
#### Installation
1. Install yaksh
- - For latest stable release
+ - Clone the repository
- $ pip install yaksh
+ $ git clone https://github.com/FOSSEE/online_test.git
- - For the development version
+ - Go to the online_test directory
- $ pip install git+https://github.com/FOSSEE/online_test.git
+ $ cd ./online_test
-#### Short instructions
+ - Install the dependencies
+
+ - For Python 2 use:
+
+ $ pip install -r ./requirements/requirements-py2.txt
-To see a quick demo after installing yaksh do the following:
+ - For Python 3 (recommended) use:
- $ yaksh create_demo yaksh_demo
- $ yaksh run yaksh_demo
+ $ pip install -r ./requirements/requirements-py3.txt
+
+#### Short instructions
-On another terminal start up the code server that executes the user code safely:
+1. To run the application do the following:
- $ sudo yaksh run_code_server
+ $ invoke serve
-Now point your browser to ```http://localhost:8000/exam```.
+ - *Note:* The serve command will run the django application server on the 8000 port
+ and hence this port will be unavailable to other processes.
-#### More detailed instructions
+1. On another terminal start up the code server that executes the user code safely:
-1. On the terminal run:
+ - To run the code server in a sandboxed docker environment, run the command:
- $ yaksh create_demo [project_path]
+ $ invoke start
- - `project_path` is the desired directory of the django project the
- basename of which is also the Django project name. This can be a
- relative directory.
+ - Make sure that you have Docker installed on your system beforehand.
+ [Docker Installation](https://docs.docker.com/engine/installation/#desktop)
- - In case a `project_path` is not specified, the project is created
- in a `yaksh_demo` subdirectory of the current directory.
-1. The script does the following;
- 1. Creates a new django project with name as the basename of the specified
- `project_path`
- 1. Creates a new demo database.
- 1. Creates two users, teacher and student.
- 1. Loads demo questions.
- 1. Loads demo quiz.
+ - To run the code server without docker, locally use:
-1. To run the server, run:
+ $ invoke start --unsafe
- $ yaksh run relpath/or/abspath/to/demo
+ - Note this command will run the yaksh code server locally on your machine
+ and is susceptible to malicious code. You will have to install the code
+ server requirements in sudo mode.
-1. In a new terminal run the following command which executes user submitted
- code safely:
- $ sudo yaksh run_code_server
1. Open your browser and open the URL ```http://localhost:8000/exam```
diff --git a/README_production.md b/README_production.md
index ed19523..8b79785 100644
--- a/README_production.md
+++ b/README_production.md
@@ -167,19 +167,18 @@ To install this app follow the steps below:
#### Using Dockerized Code Server
- 1. Install [Docker] (https://github.com/FOSSEE/online_test/blob/master/README.md)
+ 1. Install [Docker](https://github.com/FOSSEE/online_test/blob/master/README.md)
1. Got to the directory where the project is located
cd /path/to/online_test
1. Create a docker image. This may take a few minutes
- docker build -t yaksha:v1 .
+ docker build -t yaksh_code_server .
1. Check if the image has been created using the output of,
docker images
- 1. Run a container using the newly created image.
- sudo docker run -d -p 53579:53579 -p 8001:8001 yaksha:v1
+ 1. Run the invoke script using the command ```invoke start```
The command will create and run a new docker container (that is running the code_server.py within it), it will also bind the ports of the host with those of the container
#### Additional commands available
diff --git a/online_test/settings.py b/online_test/settings.py
index 90cce9d..790083e 100644
--- a/online_test/settings.py
+++ b/online_test/settings.py
@@ -15,7 +15,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# The directory where user data can be saved. This directory will be
# world-writable and all user code will be written and saved here by the
# code server with each user having their own sub-directory.
-OUTPUT_DIR = os.path.join(BASE_DIR, 'output')
+OUTPUT_DIR = os.path.join(BASE_DIR, "yaksh_data", "output")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/
@@ -33,8 +33,6 @@ URL_ROOT = ''
# Application definition
-FIXTURE_DIRS = os.path.join(BASE_DIR, "yaksh", "fixtures")
-
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
@@ -100,7 +98,7 @@ SOCIAL_AUTH_LOGIN_ERROR_URL = '/exam/login/'
MEDIA_URL = "/data/"
-MEDIA_ROOT = os.path.join(BASE_DIR, "yaksh", "data")
+MEDIA_ROOT = os.path.join(BASE_DIR, "yaksh_data", "data")
# Set this varable to <True> if smtp-server is not allowing to send email.
EMAIL_USE_TLS = False
diff --git a/requirements/requirements-codeserver.txt b/requirements/requirements-codeserver.txt
new file mode 100644
index 0000000..a4f419c
--- /dev/null
+++ b/requirements/requirements-codeserver.txt
@@ -0,0 +1,5 @@
+pytest
+six
+requests
+tornado
+psutil
diff --git a/requirements/requirements-common.txt b/requirements/requirements-common.txt
index 100d693..b4d2e5b 100644
--- a/requirements/requirements-common.txt
+++ b/requirements/requirements-common.txt
@@ -1,9 +1,9 @@
+-r requirements-codeserver.txt
+invoke==0.21.0
django==1.9.5
django-taggit==0.18.1
pytz==2016.4
python-social-auth==0.2.19
-tornado
selenium==2.53.6
coverage
-psutil
-ruamel.yaml==0.15.23
+ruamel.yaml==0.15.23 \ No newline at end of file
diff --git a/tasks.py b/tasks.py
new file mode 100644
index 0000000..eabf8fb
--- /dev/null
+++ b/tasks.py
@@ -0,0 +1,95 @@
+import invoke
+from invoke import task
+import os
+from yaksh.settings import SERVER_POOL_PORT
+
+SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
+TARGET_CONTAINER_NAME = 'yaksh_code_server'
+SRC_IMAGE_NAME = 'fossee/yaksh_codeserver'
+
+def create_dir(path):
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+@task
+def setupdb(ctx):
+ print("** Setting up & migrating database **")
+ ctx.run("python manage.py makemigrations")
+ ctx.run("python manage.py migrate")
+ ctx.run("python manage.py loaddata demo_fixtures.json")
+
+@task(setupdb)
+def serve(ctx):
+ print("** Running the Django web server. Press Ctrl-C to Exit **")
+ ctx.run("python manage.py runserver")
+
+@task
+def clean(ctx):
+ print("** Discarding database **")
+ ctx.run("rm -rf {0}".format(os.path.join(SCRIPT_DIR, 'db.sqlite3')))
+
+@task
+def getimage(ctx, image=SRC_IMAGE_NAME):
+ try:
+ result = ctx.run("sudo docker inspect {0}".format(image), hide=True)
+ except invoke.exceptions.Failure:
+ print("The docker image {0} does not exist locally".format(image))
+ print("\n** Pulling latest image <{0}> from docker hub **".format(image))
+ ctx.run("sudo docker pull {0}".format(image))
+
+@task
+def start(ctx, ports=SERVER_POOL_PORT, image=SRC_IMAGE_NAME, unsafe=False,
+ version=3):
+ if unsafe:
+ with ctx.cd(SCRIPT_DIR):
+ print("** Initializing local code server **")
+ ctx.run("sudo python{0} -m yaksh.code_server".format(
+ version
+ )
+ )
+ else:
+ cmd_params = {'ports': ports,
+ 'image': SRC_IMAGE_NAME,
+ 'name': TARGET_CONTAINER_NAME,
+ 'vol_mount': os.path.join(SCRIPT_DIR, 'yaksh_data'),
+ 'command': 'sh {0}'.format(
+ os.path.join(SCRIPT_DIR,
+ 'yaksh_data', 'yaksh', 'scripts', 'yaksh_script.sh')
+ )
+ }
+
+ getimage(ctx, image=SRC_IMAGE_NAME)
+
+ print("** Preparing code server **")
+ create_dir(os.path.join(SCRIPT_DIR, 'yaksh_data', 'data'))
+ create_dir(os.path.join(SCRIPT_DIR, 'yaksh_data', 'output'))
+
+ ctx.run('cp -r {0} {1}'.format(
+ os.path.join(SCRIPT_DIR, 'yaksh'),
+ os.path.join(SCRIPT_DIR, 'yaksh_data')
+ )
+ )
+ ctx.run('cp {0} {1}'.format(
+ os.path.join(SCRIPT_DIR, 'requirements', 'requirements-codeserver.txt'),
+ os.path.join(SCRIPT_DIR, 'yaksh_data')
+ )
+ )
+
+ print("** Initializing code server within docker container **")
+ ctx.run(
+ "sudo docker run \
+ -dp {ports}:{ports} --name={name} \
+ -v {vol_mount}:{vol_mount} \
+ -w {vol_mount} \
+ {image} {command}".format(**cmd_params)
+ )
+
+@task
+def stop(ctx, container=TARGET_CONTAINER_NAME, hide=True):
+ result = ctx.run("sudo docker ps -q --filter='name={0}'".format(container))
+ if result.stdout:
+ print ("** Discarding the docker container <{0}>".format(container))
+ ctx.run("sudo docker stop {0}".format(container))
+ ctx.run("sudo docker rm {0}".format(container))
+ else:
+ print("** Docker container <{0}> not found **".format(container))
diff --git a/yaksh/models.py b/yaksh/models.py
index d02b6d6..d698232 100644
--- a/yaksh/models.py
+++ b/yaksh/models.py
@@ -82,6 +82,8 @@ test_status = (
('completed', 'Completed'),
)
+FIXTURES_DIR_PATH = os.path.join(settings.BASE_DIR, 'yaksh', 'fixtures')
+
def get_assignment_dir(instance, filename):
upload_dir = instance.question_paper.quiz.description.replace(" ", "_")
@@ -544,7 +546,7 @@ class Question(models.Model):
def create_demo_questions(self, user):
zip_file_path = os.path.join(
- settings.FIXTURE_DIRS, 'demo_questions.zip'
+ FIXTURES_DIR_PATH, 'demo_questions.zip'
)
files, extract_path = extract_files(zip_file_path)
self.read_yaml(extract_path, user, files)
@@ -840,6 +842,13 @@ class QuestionPaper(models.Model):
objects = QuestionPaperManager()
+ def get_question_bank(self):
+ ''' Gets all the questions in the question paper'''
+ questions = list(self.fixed_questions.all())
+ for random_set in self.random_questions.all():
+ questions += list(random_set.questions.all())
+ return questions
+
def _create_duplicate_questionpaper(self, quiz):
new_questionpaper = QuestionPaper.objects.create(quiz=quiz,
shuffle_questions=self.shuffle_questions,
@@ -1201,6 +1210,15 @@ class AnswerPaper(models.Model):
objects = AnswerPaperManager()
+ def get_per_question_score(self, question_id):
+ if question_id not in self.get_questions().values_list('id', flat=True):
+ return 'NA'
+ answer = self.get_latest_answer(question_id)
+ if answer:
+ return answer.marks
+ else:
+ return 0
+
def current_question(self):
"""Returns the current active question to display."""
unanswered_questions = self.questions_unanswered.all()
diff --git a/yaksh/scripts/yaksh_script.sh b/yaksh/scripts/yaksh_script.sh
new file mode 100644
index 0000000..f39153e
--- /dev/null
+++ b/yaksh/scripts/yaksh_script.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+# Basic script to install pip packages and run the yaksh code server command
+
+chown -R nobody output
+chmod -R a+rwX output
+chmod -R a+rX data yaksh
+chmod -R o-w data yaksh
+echo "** Installing python dependencies **"
+pip3 install -r ./requirements-codeserver.txt
+echo "** Running code server **"
+/usr/bin/sudo -su nobody python3 -m yaksh.code_server
diff --git a/yaksh/templates/yaksh/add_course.html b/yaksh/templates/yaksh/add_course.html
index 55683f4..b8fc11c 100644
--- a/yaksh/templates/yaksh/add_course.html
+++ b/yaksh/templates/yaksh/add_course.html
@@ -4,6 +4,11 @@
{% block css %}
<link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/question_quiz.css" type="text/css" />
+<link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/jquery.datetimepicker.css" type="text/css" />
+{% endblock %}
+
+{% block script %}
+<script src="{{ URL_ROOT }}/static/yaksh/js/jquery.datetimepicker.full.min.js"></script>
{% endblock %}
{% block content %}
@@ -13,9 +18,14 @@
<table class=span1>
{{ form.as_table }}
</table>
+ <br/>
+ <script type="text/javascript">
+ $("#id_start_enroll_time").datetimepicker({format: 'Y-m-d H:i:s'});
+ $("#id_end_enroll_time").datetimepicker({format: 'Y-m-d H:i:s'});
+ </script>
</center>
- <center><button class="btn primary" type="submit" id="submit" name="questionpaper">Add Course</button>
- <button class="btn primary" type="button" name="button" onClick='location.replace("{{URL_ROOT}}/exam/manage/");'>Cancel</button> </center>
+ <center><button class="btn btn-default" type="submit" id="submit" name="questionpaper">Save </button>
+ <button class="btn btn-default" type="button" name="button" onClick='location.replace("{{URL_ROOT}}/exam/manage/");'>Cancel</button> </center>
</form>
{% endblock %}
diff --git a/yaksh/templates/yaksh/monitor.html b/yaksh/templates/yaksh/monitor.html
index 9ce0dc4..8df2e7d 100644
--- a/yaksh/templates/yaksh/monitor.html
+++ b/yaksh/templates/yaksh/monitor.html
@@ -76,7 +76,10 @@ $(document).ready(function()
<p>Papers in progress:<b> {{ inprogress_papers }} </b></p>
<p><a href="{{URL_ROOT}}/exam/manage/statistics/question/{{papers.0.question_paper.id}}">Question Statisitics</a></p>
-<p><a href="{{URL_ROOT}}/exam/manage/monitor/download_csv/{{papers.0.question_paper.id}}">Download CSV</a></p>
+<p>
+ <button type="button" class="btn btn-info btn-lg" data-toggle="modal" data-target="#csvModal">
+ Download CSV <span class="glyphicon glyphicon-save"></span>
+ </button></p>
<table id="result-table" class="tablesorter table table table-striped">
<thead>
<tr>
@@ -115,4 +118,46 @@ $(document).ready(function()
<h4>No Quiz Found</h4>
{% endif %}
{% endif %}
+
+<!-- CSV Modal -->
+<div class="modal fade" id="csvModal" role="dialog">
+ <div class="modal-dialog">
+
+ <!-- Modal content-->
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal">&times;</button>
+ <h3 class="modal-title">Uncheck unwanted columns</h3>
+ </div>
+ <form action="{{URL_ROOT}}/exam/manage/download_quiz_csv/{{ quiz.course.id }}/{{ quiz.id }}/" method="post">
+ {% csrf_token %}
+ <div class="modal-body">
+ {% for field in csv_fields %}
+ <div class="form-check form-check-inline">
+ <label class="form-check-label">
+ <input class="form-check-input" name="csv_fields" type="checkbox" value="{{ field }}" checked> {{ field }}
+ </label>
+ </div>
+ {% endfor %}
+ <b>Select Attempt Number: Default latest attempt</b>
+ <select class="form-control" name = "attempt_number">
+ {%for attempt_number in attempt_numbers %}
+ {% if forloop.last %}
+ <option value="{{ attempt_number }}" selected>{{ attempt_number }} (Latest)</option>
+ {% else %}
+ <option value = "{{ attempt_number }}"> {{ attempt_number }}</option>
+ {% endif %}
+ {% endfor %}
+ </select>
+ </div>
+ <div class="modal-footer">
+ <button type="submit" class="btn btn-primary"> Download <span class="glyphicon glyphicon-save"></span></button>
+ <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
+ </div>
+ </form>
+ </div>
+ </div>
+</div>
+
+
{% endblock %}
diff --git a/yaksh/test_models.py b/yaksh/test_models.py
index 71969fc..ddacb2a 100644
--- a/yaksh/test_models.py
+++ b/yaksh/test_models.py
@@ -384,6 +384,26 @@ class QuestionPaperTestCases(unittest.TestCase):
self.questions = Question.objects.filter(active=True)
self.quiz = Quiz.objects.get(description="demo quiz 1")
+ # create question paper with only fixed questions
+ self.question_paper_fixed_questions = QuestionPaper.objects.create(
+ quiz=self.quiz)
+ self.question_paper_fixed_questions.fixed_questions.add(
+ self.questions.get(summary='Q11'), self.questions.get(summary='Q10'))
+
+ # create question paper with only random questions
+ self.question_paper_random_questions = QuestionPaper.objects.create(
+ quiz=self.quiz)
+ self.question_set_random = QuestionSet.objects.create(marks=2,
+ num_questions=2)
+ self.question_set_random.questions.add(self.questions.get(summary='Q13'),
+ self.questions.get(summary='Q5'), self.questions.get(summary='Q7'))
+ self.question_paper_random_questions.random_questions.add(
+ self.question_set_random)
+
+ # create question paper with no questions
+ self.question_paper_no_questions = QuestionPaper.objects.create(
+ quiz=self.quiz)
+
# create question paper
self.question_paper = QuestionPaper.objects.create(quiz=self.quiz,
total_marks=0.0,
@@ -441,6 +461,31 @@ class QuestionPaperTestCases(unittest.TestCase):
self.trial_course = Course.objects.create_trial_course(self.user)
self.trial_quiz = Quiz.objects.create_trial_quiz(self.trial_course, self.user)
+
+ def test_get_question_bank(self):
+ # Given
+ summaries = ['Q11', 'Q10']
+ questions = list(Question.objects.filter(summary__in=summaries))
+ # When
+ question_bank = self.question_paper_fixed_questions.get_question_bank()
+ # Then
+ self.assertSequenceEqual(questions, question_bank)
+
+ # Given
+ summaries = ['Q13','Q5','Q7']
+ questions = list(Question.objects.filter(summary__in=summaries))
+ # When
+ question_bank = self.question_paper_random_questions.get_question_bank()
+ # Then
+ self.assertSequenceEqual(questions, question_bank)
+
+ # Given
+ questions = []
+ # When
+ question_bank = self.question_paper_no_questions.get_question_bank()
+ # Then
+ self.assertSequenceEqual(questions, question_bank)
+
def test_questionpaper(self):
""" Test question paper"""
self.assertEqual(self.question_paper.quiz.description, 'demo quiz 1')
@@ -630,7 +675,7 @@ class AnswerPaperTestCases(unittest.TestCase):
error=json.dumps([])
)
self.single_answer.save()
- self.answerpaper.answers.add(self.single_answer)
+ self.answerpaper_single_question.answers.add(self.single_answer)
self.question1.language = 'python'
self.question1.test_case_type = 'standardtestcase'
@@ -691,6 +736,31 @@ class AnswerPaperTestCases(unittest.TestCase):
self.user2, self.ip, 1
)
+ def test_get_per_question_score(self):
+ # Given
+ question_id = self.question4.id
+ expected_score = 1
+ # When
+ score = self.answerpaper_single_question.get_per_question_score(question_id)
+ # Then
+ self.assertEqual(score, expected_score)
+
+ # Given
+ question_id = self.question2.id
+ expected_score = 0
+ # When
+ score = self.answerpaper.get_per_question_score(question_id)
+ # Then
+ self.assertEqual(score, expected_score)
+
+ # Given
+ question_id = 131
+ expected_score = 'NA'
+ # When
+ score = self.answerpaper.get_per_question_score(question_id)
+ # Then
+ self.assertEqual(score, expected_score)
+
def test_returned_question_is_not_none(self):
# Test add_completed_question and next_question
# When all questions are answered
@@ -976,7 +1046,7 @@ class AnswerPaperTestCases(unittest.TestCase):
first_answer_obj = first_answer['answer']
self.assertEqual(first_answer_obj.answer, 'Demo answer')
self.assertTrue(first_answer_obj.correct)
- self.assertEqual(len(answered), 3)
+ self.assertEqual(len(answered), 2)
def test_is_answer_correct(self):
self.assertTrue(self.answerpaper.is_answer_correct(self.questions[0]))
diff --git a/yaksh/test_views.py b/yaksh/test_views.py
index e27d5ef..465fb30 100644
--- a/yaksh/test_views.py
+++ b/yaksh/test_views.py
@@ -23,7 +23,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\
QuestionSet, AnswerPaper, Answer, Course, StandardTestCase,\
AssignmentUpload, FileUpload, McqTestCase, IntegerTestCase, StringTestCase,\
- FloatTestCase
+ FloatTestCase, FIXTURES_DIR_PATH
from yaksh.decorators import user_has_profile
@@ -196,6 +196,38 @@ class TestProfile(TestCase):
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'yaksh/profile_updated.html')
+ def test_edit_profile_post_for_user_without_profile(self):
+ """
+ POST request to edit_profile view should update the user's profile
+ """
+ self.client.login(
+ username=self.user1.username,
+ password=self.user1_plaintext_pass
+ )
+ response = self.client.post(reverse('yaksh:edit_profile'),
+ data={
+ 'user': self.user1,
+ 'first_name': 'new_first_name',
+ 'last_name': 'new_last_name',
+ 'roll_number': 21,
+ 'institute': 'new_institute',
+ 'department': 'Aerospace',
+ 'position': 'new_position',
+ 'timezone': 'UTC'
+ }
+ )
+ updated_profile_user = User.objects.get(id=self.user1.id)
+ updated_profile = Profile.objects.get(user=updated_profile_user)
+ self.assertEqual(updated_profile_user.first_name, 'new_first_name')
+ self.assertEqual(updated_profile_user.last_name, 'new_last_name')
+ self.assertEqual(updated_profile.roll_number, '21')
+ self.assertEqual(updated_profile.institute, 'new_institute')
+ self.assertEqual(updated_profile.department, 'Aerospace')
+ self.assertEqual(updated_profile.position, 'new_position')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'yaksh/profile_updated.html')
+
def test_edit_profile_get(self):
"""
GET request to edit profile should display profile form
@@ -3041,6 +3073,7 @@ class TestDownloadcsv(TestCase):
self.mod_group.user_set.add(self.user)
self.course = Course.objects.create(name="Python Course",
enrollment="Enroll Request", creator=self.user)
+ self.course.students.add(self.student)
self.quiz = Quiz.objects.create(
start_date_time=datetime(2014, 10, 9, 10, 8, 15, 0, tzone),
@@ -3093,8 +3126,9 @@ class TestDownloadcsv(TestCase):
username=self.student.username,
password=self.student_plaintext_pass
)
- response = self.client.get(reverse('yaksh:download_csv',
- kwargs={"questionpaper_id": self.question_paper.id}),
+ response = self.client.get(reverse('yaksh:download_quiz_csv',
+ kwargs={"course_id": self.course.id,
+ "quiz_id": self.quiz.id}),
follow=True
)
self.assertEqual(response.status_code, 404)
@@ -3122,8 +3156,9 @@ class TestDownloadcsv(TestCase):
username=self.student.username,
password=self.student_plaintext_pass
)
- response = self.client.get(reverse('yaksh:download_csv',
- kwargs={"questionpaper_id": self.question_paper.id}),
+ response = self.client.get(reverse('yaksh:download_quiz_csv',
+ kwargs={"course_id": self.course.id,
+ "quiz_id": self.quiz.id}),
follow=True
)
self.assertEqual(response.status_code, 404)
@@ -3168,11 +3203,14 @@ class TestDownloadcsv(TestCase):
username=self.user.username,
password=self.user_plaintext_pass
)
- response = self.client.get(reverse('yaksh:download_csv',
- kwargs={'questionpaper_id': self.question_paper.id}),
+ response = self.client.get(reverse('yaksh:download_quiz_csv',
+ kwargs={"course_id": self.course.id,
+ "quiz_id": self.quiz.id}),
+
follow=True
)
- file_name = "{0}.csv".format(self.quiz.description)
+ file_name = "{0}-{1}-attempt{2}.csv".format(self.course.name.replace('.', ''),
+ self.quiz.description.replace('.', ''), 1)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.get('Content-Disposition'),
'attachment; filename="{0}"'.format(file_name))
@@ -3300,7 +3338,7 @@ class TestShowQuestions(TestCase):
username=self.user.username,
password=self.user_plaintext_pass
)
- ques_file = os.path.join(settings.FIXTURE_DIRS, "demo_questions.zip")
+ ques_file = os.path.join(FIXTURES_DIR_PATH, "demo_questions.zip")
f = open(ques_file, 'rb')
questions_file = SimpleUploadedFile(ques_file, f.read(),
content_type="application/zip")
diff --git a/yaksh/urls.py b/yaksh/urls.py
index a7aeecc..1a50ca2 100644
--- a/yaksh/urls.py
+++ b/yaksh/urls.py
@@ -56,8 +56,8 @@ urlpatterns = [
views.show_statistics, name="show_statistics"),
url(r'^manage/statistics/question/(?P<questionpaper_id>\d+)/(?P<attempt_number>\d+)/$',
views.show_statistics, name="show_statistics"),
- url(r'^manage/monitor/download_csv/(?P<questionpaper_id>\d+)/$',
- views.download_csv, name="download_csv"),
+ url(r'^manage/download_quiz_csv/(?P<course_id>\d+)/(?P<quiz_id>\d+)/$',
+ views.download_quiz_csv, name="download_quiz_csv"),
url(r'^manage/duplicate_course/(?P<course_id>\d+)/$', views.duplicate_course,
name='duplicate_course'),
url(r'manage/courses/$', views.courses, name='courses'),
diff --git a/yaksh/views.py b/yaksh/views.py
index 838041e..71b4abe 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -80,6 +80,10 @@ def add_to_group(users):
user.groups.add(group)
+CSV_FIELDS = ['name', 'username', 'roll_number', 'institute', 'department',
+ 'questions', 'marks_obtained', 'out_of', 'percentage', 'status']
+
+
@email_verified
def index(request, next_url=None):
"""The start page.
@@ -792,8 +796,10 @@ def courses(request):
ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this page')
- courses = Course.objects.filter(creator=user, is_trial=False)
- allotted_courses = Course.objects.filter(teachers=user, is_trial=False)
+ courses = Course.objects.filter(
+ creator=user, is_trial=False).order_by('-active', '-id')
+ allotted_courses = Course.objects.filter(
+ teachers=user, is_trial=False).order_by('-active', '-id')
context = {'courses': courses, "allotted_courses": allotted_courses}
return my_render_to_response('yaksh/courses.html', context,
context_instance=ci)
@@ -991,7 +997,12 @@ def monitor(request, quiz_id=None):
papers = []
q_paper = None
latest_attempts = []
+ attempt_numbers = []
else:
+ if q_paper:
+ attempt_numbers = AnswerPaper.objects.get_attempt_numbers(q_paper.last().id)
+ else:
+ attempt_numbers = []
latest_attempts = []
papers = AnswerPaper.objects.filter(question_paper=q_paper).order_by(
'user__profile__roll_number'
@@ -1007,11 +1018,14 @@ def monitor(request, quiz_id=None):
attempt_number=last_attempt['last_attempt_num']
)
)
+ csv_fields = CSV_FIELDS
context = {
"papers": papers,
"quiz": quiz,
"msg": "Quiz Results",
- "latest_attempts": latest_attempts
+ "latest_attempts": latest_attempts,
+ "csv_fields": csv_fields,
+ "attempt_numbers": attempt_numbers
}
return my_render_to_response('yaksh/monitor.html', context,
context_instance=ci)
@@ -1264,49 +1278,80 @@ def user_data(request, user_id, questionpaper_id=None):
context_instance=RequestContext(request))
+def _expand_questions(questions, field_list):
+ i = field_list.index('questions')
+ field_list.remove('questions')
+ for question in questions:
+ field_list.insert(i, '{0}-{1}'.format(question.summary, question.points))
+ return field_list
+
+
@login_required
@email_verified
-def download_csv(request, questionpaper_id):
- user = request.user
- if not is_moderator(user):
+def download_quiz_csv(request, course_id, quiz_id):
+ current_user = request.user
+ if not is_moderator(current_user):
raise Http404('You are not allowed to view this page!')
- quiz = Quiz.objects.get(questionpaper=questionpaper_id)
+ course = get_object_or_404(Course, id=course_id)
+ quiz = get_object_or_404(Quiz, id=quiz_id)
+ if not course.is_creator(current_user) and not course.is_teacher(current_user):
+ raise Http404('The quiz does not belong to your course')
+ users = course.get_enrolled().order_by('first_name')
+ if not users:
+ return monitor(request, quiz_id)
+ csv_fields = []
+ attempt_number = None
+ question_paper = quiz.questionpaper_set.last()
+ last_attempt_number =AnswerPaper.objects.get_attempt_numbers(question_paper.id).last()
+ if request.method == 'POST':
+ csv_fields = request.POST.getlist('csv_fields')
+ attempt_number = request.POST.get('attempt_number', last_attempt_number)
+ if not csv_fields:
+ csv_fields = CSV_FIELDS
+ if not attempt_number:
+ attempt_number = last_attempt_number
+
+ questions = question_paper.get_question_bank()
+ answerpapers = AnswerPaper.objects.filter(question_paper=question_paper,
+ attempt_number=attempt_number)
+ if not answerpapers:
+ return monitor(request, quiz_id)
- if not quiz.course.is_creator(user) and not quiz.course.is_teacher(user):
- raise Http404('The question paper does not belong to your course')
- papers = AnswerPaper.objects.get_latest_attempts(questionpaper_id)
- if not papers:
- return monitor(request, questionpaper_id)
response = HttpResponse(content_type='text/csv')
- response['Content-Disposition'] = 'attachment; filename="{0}.csv"'.format(
- (quiz.description).replace('.', ''))
+ response['Content-Disposition'] = 'attachment; filename="{0}-{1}-attempt{2}.csv"'.format(
+ course.name.replace('.', ''), quiz.description.replace('.', ''),
+ attempt_number)
writer = csv.writer(response)
- header = [
- 'name',
- 'username',
- 'roll_number',
- 'institute',
- 'marks_obtained',
- 'total_marks',
- 'percentage',
- 'questions',
- 'questions_answered',
- 'status'
- ]
- writer.writerow(header)
- for paper in papers:
- row = [
- '{0} {1}'.format(paper.user.first_name, paper.user.last_name),
- paper.user.username,
- paper.user.profile.roll_number,
- paper.user.profile.institute,
- paper.marks_obtained,
- paper.question_paper.total_marks,
- paper.percent,
- paper.questions.all(),
- paper.questions_answered.all(),
- paper.status
- ]
+ if 'questions' in csv_fields:
+ csv_fields = _expand_questions(questions, csv_fields)
+ writer.writerow(csv_fields)
+
+ csv_fields_values = {'name': 'user.get_full_name().title()',
+ 'roll_number': 'user.profile.roll_number',
+ 'institute': 'user.profile.institute',
+ 'department': 'user.profile.department',
+ 'username': 'user.username',
+ 'marks_obtained': 'answerpaper.marks_obtained',
+ 'out_of': 'question_paper.total_marks',
+ 'percentage': 'answerpaper.percent', 'status': 'answerpaper.status'}
+ questions_scores = {}
+ for question in questions:
+ questions_scores['{0}-{1}'.format(question.summary, question.points)] \
+ = 'answerpaper.get_per_question_score({0})'.format(question.id)
+ csv_fields_values.update(questions_scores)
+
+ users = users.exclude(id=course.creator.id).exclude(id__in=course.teachers.all())
+ for user in users:
+ row = []
+ answerpaper = None
+ papers = answerpapers.filter(user=user)
+ if papers:
+ answerpaper = papers.first()
+ for field in csv_fields:
+ try:
+ row.append(eval(csv_fields_values[field]))
+ except AttributeError:
+ row.append('-')
writer.writerow(row)
return response
@@ -1406,7 +1451,6 @@ def view_profile(request):
@login_required
-@has_profile
@email_verified
def edit_profile(request):
""" edit profile details facility for moderator and students """
@@ -1418,7 +1462,10 @@ def edit_profile(request):
else:
template = 'user.html'
context = {'template': template}
- profile = Profile.objects.get(user_id=user.id)
+ try:
+ profile = Profile.objects.get(user_id=user.id)
+ except Profile.DoesNotExist:
+ profile = None
if request.method == 'POST':
form = ProfileForm(request.POST, user=user, instance=profile)