summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.txt1
-rw-r--r--README.md18
-rw-r--r--tasks.py17
-rw-r--r--yaksh/file_utils.py18
-rw-r--r--yaksh/fixtures/sample_user_upload.csv2
-rw-r--r--yaksh/fixtures/users_add_update_reject.csv4
-rw-r--r--yaksh/fixtures/users_correct.csv2
-rw-r--r--yaksh/fixtures/users_some_headers_missing.csv2
-rw-r--r--yaksh/fixtures/users_some_values_missing.csv4
-rw-r--r--yaksh/fixtures/users_with_no_values.csv1
-rw-r--r--yaksh/models.py16
-rw-r--r--yaksh/scripts/yaksh_script.sh5
-rw-r--r--yaksh/templates/yaksh/course_detail.html30
-rw-r--r--yaksh/templates/yaksh/grade_user.html11
-rw-r--r--yaksh/templates/yaksh/user_data.html4
-rw-r--r--yaksh/templates/yaksh/view_answerpaper.html4
-rw-r--r--yaksh/test_views.py159
-rw-r--r--yaksh/urls.py3
-rw-r--r--yaksh/views.py156
19 files changed, 429 insertions, 28 deletions
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index a227164..824a051 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -11,6 +11,7 @@
* Updated the validation of MCQ/MCC type question.
* Fixed a bug that did not allow expected input in Standard I/O type question to be none.
* 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.
diff --git a/README.md b/README.md
index a0faab2..28f45d0 100644
--- a/README.md
+++ b/README.md
@@ -49,22 +49,15 @@ Quick Start
- For Python 2 use:
- $ pip install -r ./requirements/requirements-py2.txt
+ $ pip install -r ./requirements/requirements-py2.txt
- For Python 3 (recommended) use:
- $ pip install -r ./requirements/requirements-py3.txt
+ $ pip install -r ./requirements/requirements-py3.txt
#### Short instructions
-1. To run the application do the following:
-
- $ invoke serve
-
- - *Note:* The serve command will run the django application server on the 8000 port
- and hence this port will be unavailable to other processes.
-
-1. On another terminal start up the code server that executes the user code safely:
+1. Start up the code server that executes the user code safely:
- To run the code server in a sandboxed docker environment, run the command:
@@ -82,7 +75,12 @@ Quick Start
and is susceptible to malicious code. You will have to install the code
server requirements in sudo mode.
+1. On another terminal, run the application using the following command:
+
+ $ invoke serve
+ - *Note:* The serve command will run the django application server on the 8000 port
+ and hence this port will be unavailable to other processes.
1. Open your browser and open the URL ```http://localhost:8000/exam```
diff --git a/tasks.py b/tasks.py
index eabf8fb..68d9967 100644
--- a/tasks.py
+++ b/tasks.py
@@ -1,3 +1,4 @@
+from __future__ import print_function
import invoke
from invoke import task
import os
@@ -6,11 +7,18 @@ 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'
+CHECK_FILE = 'server_running.txt'
+CHECK_FILE_PATH = os.path.join(SCRIPT_DIR, 'yaksh_data', CHECK_FILE)
+
def create_dir(path):
if not os.path.exists(path):
os.makedirs(path)
+def remove_check_file(path):
+ if os.path.isfile(path):
+ os.remove(path)
+
@task
def setupdb(ctx):
print("** Setting up & migrating database **")
@@ -36,6 +44,7 @@ def getimage(ctx, image=SRC_IMAGE_NAME):
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))
+ print("\n** Done! Successfully pulled latest image <{0}> **".format(image))
@task
def start(ctx, ports=SERVER_POOL_PORT, image=SRC_IMAGE_NAME, unsafe=False,
@@ -58,6 +67,7 @@ def start(ctx, ports=SERVER_POOL_PORT, image=SRC_IMAGE_NAME, unsafe=False,
)
}
+ remove_check_file(CHECK_FILE_PATH)
getimage(ctx, image=SRC_IMAGE_NAME)
print("** Preparing code server **")
@@ -84,12 +94,19 @@ def start(ctx, ports=SERVER_POOL_PORT, image=SRC_IMAGE_NAME, unsafe=False,
{image} {command}".format(**cmd_params)
)
+ while not os.path.isfile(CHECK_FILE_PATH):
+ print("** Checking code server status. Press Ctrl-C to exit **\r", end="")
+ print("** Code server is up and running successfully **")
+
+
@task
def stop(ctx, container=TARGET_CONTAINER_NAME, hide=True):
result = ctx.run("sudo docker ps -q --filter='name={0}'".format(container))
+ remove_check_file(CHECK_FILE_PATH)
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))
+ print ("** Done! Discarded the docker container <{0}>".format(container))
else:
print("** Docker container <{0}> not found **".format(container))
diff --git a/yaksh/file_utils.py b/yaksh/file_utils.py
index f41c531..b178eeb 100644
--- a/yaksh/file_utils.py
+++ b/yaksh/file_utils.py
@@ -2,7 +2,7 @@ import shutil
import os
import zipfile
import tempfile
-
+import csv
def copy_files(file_paths):
""" Copy Files to current directory, takes
@@ -50,3 +50,19 @@ def extract_files(zip_file, path=None):
zip_file.close()
return zfiles, extract_path
+
+def is_csv(document):
+ ''' Check if document is csv with ',' as the delimiter'''
+ try:
+ try:
+ content = document.read(1024).decode('utf-8')
+ except AttributeError:
+ document.seek(0)
+ content = document.read(1024)
+ sniffer = csv.Sniffer()
+ dialect = sniffer.sniff(content, delimiters=',')
+ document.seek(0)
+ except (csv.Error, UnicodeDecodeError):
+ return False, None
+ return True, dialect
+
diff --git a/yaksh/fixtures/sample_user_upload.csv b/yaksh/fixtures/sample_user_upload.csv
new file mode 100644
index 0000000..2edf710
--- /dev/null
+++ b/yaksh/fixtures/sample_user_upload.csv
@@ -0,0 +1,2 @@
+firstname,lastname,email,username,password,institute,department,roll_no,remove
+sample,user,sampleuser@xyz.com,sample_user,sample,sample,sample,sample123,False
diff --git a/yaksh/fixtures/users_add_update_reject.csv b/yaksh/fixtures/users_add_update_reject.csv
new file mode 100644
index 0000000..43881d8
--- /dev/null
+++ b/yaksh/fixtures/users_add_update_reject.csv
@@ -0,0 +1,4 @@
+firstname, lastname, email, institute,department,roll_no,remove,password,username
+test, test, test@g.com, TEST, TEST, TEST101, FALSE, TEST, test
+test2, test, test@g.com, TEST, TEST, TEST101, FALSE, TEST, test
+test2, test, test@g.com, TEST, TEST, TEST101, TRUE, TEST, test
diff --git a/yaksh/fixtures/users_correct.csv b/yaksh/fixtures/users_correct.csv
new file mode 100644
index 0000000..7b2a977
--- /dev/null
+++ b/yaksh/fixtures/users_correct.csv
@@ -0,0 +1,2 @@
+firstname, lastname, email
+abc, abc, abc@xyz.com
diff --git a/yaksh/fixtures/users_some_headers_missing.csv b/yaksh/fixtures/users_some_headers_missing.csv
new file mode 100644
index 0000000..ddcaa88
--- /dev/null
+++ b/yaksh/fixtures/users_some_headers_missing.csv
@@ -0,0 +1,2 @@
+noname, lastname, email
+abc, abc, abc@xyz.com
diff --git a/yaksh/fixtures/users_some_values_missing.csv b/yaksh/fixtures/users_some_values_missing.csv
new file mode 100644
index 0000000..d60ed8d
--- /dev/null
+++ b/yaksh/fixtures/users_some_values_missing.csv
@@ -0,0 +1,4 @@
+firstname, lastname, email
+abc, , abc@xyz.com
+dummy, dummy , dummy@xyz.com
+dummy, dummy , dummy@xyz.com
diff --git a/yaksh/fixtures/users_with_no_values.csv b/yaksh/fixtures/users_with_no_values.csv
new file mode 100644
index 0000000..db08ba2
--- /dev/null
+++ b/yaksh/fixtures/users_with_no_values.csv
@@ -0,0 +1 @@
+firstname, lastname, email
diff --git a/yaksh/models.py b/yaksh/models.py
index d698232..8e7089f 100644
--- a/yaksh/models.py
+++ b/yaksh/models.py
@@ -29,7 +29,9 @@ 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 yaksh.code_server import(submit, SERVER_POOL_PORT,
+ get_result as get_result_from_code_server
+ )
from django.conf import settings
from django.forms.models import model_to_dict
@@ -1435,6 +1437,7 @@ class AnswerPaper(models.Model):
elif question.type == 'float':
tc_status = []
+ user_answer = float(user_answer)
for tc in question.get_test_cases():
if abs(tc.correct - user_answer) <= tc.error_margin:
tc_status.append(True)
@@ -1474,9 +1477,16 @@ class AnswerPaper(models.Model):
answer = user_answer.answer
json_data = question.consolidate_answer_data(answer) \
if question.type == 'code' else None
- result = self.validate_answer(answer, question, json_data)
+ result = self.validate_answer(answer, question,
+ json_data, user_answer.id)
+ if question.type == "code":
+ url = 'http://localhost:%s' % SERVER_POOL_PORT
+ check_result = get_result_from_code_server(url, result['uid'],
+ block=True
+ )
+ result = json.loads(check_result.get('result'))
user_answer.correct = result.get('success')
- user_answer.error = result.get('error')
+ user_answer.error = json.dumps(result.get('error'))
if result.get('success'):
if question.partial_grading and question.type == 'code':
max_weight = question.get_maximum_test_case_weight()
diff --git a/yaksh/scripts/yaksh_script.sh b/yaksh/scripts/yaksh_script.sh
index f39153e..1401d09 100644
--- a/yaksh/scripts/yaksh_script.sh
+++ b/yaksh/scripts/yaksh_script.sh
@@ -5,7 +5,8 @@ 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 **"
+echo "** [CONTAINER] Installing python dependencies **"
pip3 install -r ./requirements-codeserver.txt
-echo "** Running code server **"
+echo "** [CONTAINER] Running code server **"
+touch server_running.txt
/usr/bin/sudo -su nobody python3 -m yaksh.code_server
diff --git a/yaksh/templates/yaksh/course_detail.html b/yaksh/templates/yaksh/course_detail.html
index d8aeb2e..93a7048 100644
--- a/yaksh/templates/yaksh/course_detail.html
+++ b/yaksh/templates/yaksh/course_detail.html
@@ -45,6 +45,27 @@
</div>
</div>
<div class="col-md-9 col-md-offset-2 main">
+ <form id="upload_users" action="{{ URL_ROOT }}/exam/manage/upload_users/{{course.id}}/"
+ method="POST" enctype="multipart/form-data">
+ {% csrf_token %}
+ <input type="file" name="csv_file" />
+ <button class="btn btn-primary" type=submit> Upload Users <span class="glyphicon glyphicon-open"/></button>
+ </form>
+ <div class="alert alert-info" role="alert">
+ <p>
+ - The uploaded csv should have headers exactly same as mentioned below:<br />
+ <b>firstname, lastname, email, username, password, institute, roll_no, department,
+ remove</b><br />
+ - Mandatory fields are <b> firstname, lastname and email. </b><br />
+ - Other fields are optional. <br />
+ - If username and password are not provided then
+ <b>Users created will have username and password same as their email</b>
+ </p>
+ <p>
+ <b> Click <a class="btn btn-success" href="{{ URL_ROOT }}/exam/manage/download_sample_csv/
+">here</a> to download a sample CSV, edit and upload it</b>
+ </p>
+ </div>
<div class="row">
{% if message %}
<div class="alert alert-warning" role="alert">
@@ -53,6 +74,14 @@
</center>
</div>
{% endif %}
+ {% if upload_details %}
+ <div class="alert alert-info" role="info">
+ {% for detail in upload_details %}
+ <strong> {{ detail }} </strong><br>
+ {% endfor %}
+ </div>
+ {% endif %}
+ <hr>
{% if state == 'mail' %}
<div id="enrolled-students">
<center><b><u>Send Mails to Students</u></b></center><br>
@@ -218,6 +247,7 @@
{% endif %}
</div>
</div>
+
<!-- Dialog to display error message -->
<div id="dialog" title="Alert">
<p id="error_msg"></p>
diff --git a/yaksh/templates/yaksh/grade_user.html b/yaksh/templates/yaksh/grade_user.html
index 3339177..1fef026 100644
--- a/yaksh/templates/yaksh/grade_user.html
+++ b/yaksh/templates/yaksh/grade_user.html
@@ -8,7 +8,6 @@
{% block content %}
{% block script %}
-<script src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script src="{{ URL_ROOT }}/static/yaksh/js/jquery.tablesorter.min.js"></script>
<script type="text/javascript">
$(document).ready(function()
@@ -107,10 +106,10 @@ End time: {{ paper.end_time }} <br/>
{%if paper.percent%}
Percentage obtained: {{paper.percent}}% <br/>
{% endif %}
-{% if paper.passed == 0 %}
-Status : <b style="color: red;"> Failed </b><br/>
-{% else %}
+{% if paper.passed %}
Status : <b style="color: green;"> Passed </b><br/>
+{% else %}
+Status : <b style="color: red;"> Failed </b><br/>
{% endif %}
</p>
{% if paper.answers.count %}
@@ -230,7 +229,7 @@ Status : <b style="color: green;"> Passed </b><br/>
</tr>
</table>
{% endif %}
- <table class="table table-bordered" width="100%" id="output">
+ <table class="table table-bordered" width="100%" id="output" style="table-layout: fixed">
<col width="10%">
<col width="40%">
<col width="40%">
@@ -268,7 +267,7 @@ Status : <b style="color: green;"> Passed </b><br/>
{% endif %}
<p> <b>The following error took place: </b></p>
<div class="well well-sm">
- <table class="table table-bordered" width="100%">
+ <table class="table table-bordered" width="100%" style="table-layout: fixed">
<col width="30%">
<tr class = "active">
<td><b>Exception Name: </b></td>
diff --git a/yaksh/templates/yaksh/user_data.html b/yaksh/templates/yaksh/user_data.html
index a0219dd..73157ff 100644
--- a/yaksh/templates/yaksh/user_data.html
+++ b/yaksh/templates/yaksh/user_data.html
@@ -150,7 +150,7 @@ User IP address: {{ paper.user_ip }}
</tr>
</table>
{% endif %}
- <table class="table table-bordered" width="100%" id="output">
+ <table class="table table-bordered" width="100%" id="output" style="table-layout: fixed">
<col width="10%">
<col width="40%">
<col width="40%">
@@ -188,7 +188,7 @@ User IP address: {{ paper.user_ip }}
{% endif %}
<p> <b>The following error took place: </b></p>
<div class="well well-sm">
- <table class="table table-bordered" width="100%">
+ <table class="table table-bordered" width="100%" style="table-layout: fixed">
<col width="30%">
<tr class = "active">
<td><b>Exception Name: </b></td>
diff --git a/yaksh/templates/yaksh/view_answerpaper.html b/yaksh/templates/yaksh/view_answerpaper.html
index fa16a08..b433ad5 100644
--- a/yaksh/templates/yaksh/view_answerpaper.html
+++ b/yaksh/templates/yaksh/view_answerpaper.html
@@ -143,7 +143,7 @@
</tr>
</table>
{% endif %}
- <table class="table table-bordered" width="100%" id="output">
+ <table class="table table-bordered" width="100%" id="output" style="table-layout: fixed">
<col width="10%">
<col width="40%">
<col width="40%">
@@ -181,7 +181,7 @@
{% endif %}
<p> <b>The following error took place: </b></p>
<div class="well well-sm">
- <table class="table table-bordered" width="100%">
+ <table class="table table-bordered" width="100%" style="table-layout: fixed">
<col width="30%">
<tr class = "active">
<td><b>Exception Name: </b></td>
diff --git a/yaksh/test_views.py b/yaksh/test_views.py
index 652f44c..2dddcef 100644
--- a/yaksh/test_views.py
+++ b/yaksh/test_views.py
@@ -1539,6 +1539,8 @@ class TestCourses(TestCase):
self.user1_course.delete()
self.user2_course.delete()
+
+
def test_courses_denies_anonymous(self):
"""
If not logged in redirect to login page
@@ -1790,6 +1792,163 @@ class TestCourseDetail(TestCase):
self.student.delete()
self.user1_course.delete()
+ def test_upload_users_with_correct_csv(self):
+ # Given
+ self.client.login(
+ username=self.user1.username,
+ password=self.user1_plaintext_pass
+ )
+ csv_file_path = os.path.join(FIXTURES_DIR_PATH, "users_correct.csv")
+ csv_file = open(csv_file_path, 'rb')
+ upload_file = SimpleUploadedFile(csv_file_path, csv_file.read())
+
+ # When
+ response = self.client.post(reverse('yaksh:upload_users',
+ kwargs={'course_id': self.user1_course.id}),
+ data={'csv_file': upload_file})
+ csv_file.close()
+
+ # Then
+ uploaded_user = User.objects.filter(email="abc@xyz.com")
+ self.assertEqual(uploaded_user.count(), 1)
+ self.assertEqual(response.status_code, 200)
+ self.assertIn('upload_details', response.context)
+ self.assertTemplateUsed(response, 'yaksh/course_detail.html')
+
+ def test_upload_users_add_update_reject(self):
+ # Given
+ self.client.login(
+ username=self.user1.username,
+ password=self.user1_plaintext_pass
+ )
+ csv_file_path = os.path.join(FIXTURES_DIR_PATH,
+ "users_add_update_reject.csv")
+ csv_file = open(csv_file_path, 'rb')
+ upload_file = SimpleUploadedFile(csv_file_path, csv_file.read())
+
+ # When
+ response = self.client.post(reverse('yaksh:upload_users',
+ kwargs={'course_id': self.user1_course.id}),
+ data={'csv_file': upload_file})
+ csv_file.close()
+
+ # Then
+ uploaded_user = User.objects.filter(username="test")
+ user = uploaded_user[0]
+ self.assertEqual(uploaded_user.count(), 1)
+ self.assertEqual(user.first_name, "test2")
+ self.assertIn(user, self.user1_course.get_rejected())
+ self.assertEqual(response.status_code, 200)
+ self.assertIn('upload_details', response.context)
+ self.assertTemplateUsed(response, 'yaksh/course_detail.html')
+
+ def test_upload_users_with_wrong_csv(self):
+ # Given
+ self.client.login(
+ username=self.user1.username,
+ password=self.user1_plaintext_pass
+ )
+ csv_file_path = os.path.join(FIXTURES_DIR_PATH, "demo_questions.zip")
+ csv_file = open(csv_file_path, 'rb')
+ upload_file = SimpleUploadedFile(csv_file_path, csv_file.read())
+ message = "The file uploaded is not a CSV file."
+
+ # When
+ response = self.client.post(reverse('yaksh:upload_users',
+ kwargs={'course_id': self.user1_course.id}),
+ data={'csv_file': upload_file})
+ csv_file.close()
+
+ # Then
+ self.assertEqual(response.status_code, 200)
+ self.assertNotIn('upload_details', response.context)
+ self.assertIn('message', response.context)
+ self.assertEqual(response.context['message'], message)
+ self.assertTemplateUsed(response, 'yaksh/course_detail.html')
+
+ def test_upload_users_csv_with_missing_headers(self):
+ # Given
+ self.client.login(
+ username=self.user1.username,
+ password=self.user1_plaintext_pass
+ )
+ csv_file_path = os.path.join(FIXTURES_DIR_PATH, "users_some_headers_missing.csv")
+ csv_file = open(csv_file_path, 'rb')
+ upload_file = SimpleUploadedFile(csv_file_path, csv_file.read())
+ message = "The CSV file does not contain the required headers"
+
+ # When
+ response = self.client.post(reverse('yaksh:upload_users',
+ kwargs={'course_id': self.user1_course.id}),
+ data={'csv_file': upload_file})
+ csv_file.close()
+
+ # Then
+ self.assertEqual(response.status_code, 200)
+ self.assertNotIn('upload_details', response.context)
+ self.assertIn('message', response.context)
+ self.assertEqual(response.context['message'], message)
+ self.assertTemplateUsed(response, 'yaksh/course_detail.html')
+
+ def test_upload_users_csv_with_no_values(self):
+ # Given
+ self.client.login(
+ username=self.user1.username,
+ password=self.user1_plaintext_pass
+ )
+ csv_file_path = os.path.join(FIXTURES_DIR_PATH, "users_with_no_values.csv")
+ csv_file = open(csv_file_path, 'rb')
+ upload_file = SimpleUploadedFile(csv_file_path, csv_file.read())
+
+ # When
+ response = self.client.post(reverse('yaksh:upload_users',
+ kwargs={'course_id': self.user1_course.id}),
+ data={'csv_file': upload_file})
+ csv_file.close()
+
+ # Then
+ self.assertEqual(response.status_code, 200)
+ self.assertIn('upload_details', response.context)
+ self.assertNotIn('message', response.context)
+ self.assertIn("No rows in the CSV file", response.context['upload_details'])
+ self.assertTemplateUsed(response, 'yaksh/course_detail.html')
+
+ def test_upload_users_csv_with_missing_values(self):
+ '''
+ This test takes csv with 3 row values.
+ 1st row has a missing row.
+ 2nd has a proper row.
+ 3rd has a same row has 2nd
+
+ Only 2nd user will be added.
+
+ This test proves that:
+ - Row with missing values is ignored and continued with next row.
+ - Duplicate user is not created.
+ '''
+ # Given
+ self.client.login(
+ username=self.user1.username,
+ password=self.user1_plaintext_pass
+ )
+ csv_file_path = os.path.join(FIXTURES_DIR_PATH, "users_some_values_missing.csv")
+ csv_file = open(csv_file_path, 'rb')
+ upload_file = SimpleUploadedFile(csv_file_path, csv_file.read())
+
+ # When
+ response = self.client.post(reverse('yaksh:upload_users',
+ kwargs={'course_id': self.user1_course.id}),
+ data={'csv_file': upload_file})
+ csv_file.close()
+
+ # Then
+ uploaded_user = User.objects.filter(email="dummy@xyz.com")
+ self.assertEqual(uploaded_user.count(), 1)
+ self.assertEqual(response.status_code, 200)
+ self.assertIn('upload_details', response.context)
+ self.assertNotIn('message', response.context)
+ self.assertTemplateUsed(response, 'yaksh/course_detail.html')
+
def test_course_detail_denies_anonymous(self):
"""
If not logged in redirect to login page
diff --git a/yaksh/urls.py b/yaksh/urls.py
index 9288956..7f484b9 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/upload_users/(?P<course_id>\d+)/$', views.upload_users, name="upload_users"),
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"),
@@ -110,4 +111,6 @@ urlpatterns = [
views.download_assignment_file, name="download_quiz_assignment"),
url(r'^manage/courses/download_yaml_template/',
views.download_yaml_template, name="download_yaml_template"),
+ url(r'^manage/download_sample_csv/',
+ views.download_sample_csv, name="download_sample_csv"),
]
diff --git a/yaksh/views.py b/yaksh/views.py
index df002a4..bc03ca2 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -37,7 +37,7 @@ from yaksh.models import (
HookTestCase, IntegerTestCase, McqTestCase, Profile,
QuestionPaper, QuestionSet, Quiz, Question, StandardTestCase,
StdIOBasedTestCase, StringTestCase, TestCase, User,
- get_model_class
+ get_model_class, FIXTURES_DIR_PATH
)
from yaksh.forms import (
UserRegisterForm, UserLoginForm, QuizForm, QuestionForm,
@@ -45,7 +45,7 @@ from yaksh.forms import (
UploadFileForm, get_object_form, FileForm, QuestionPaperForm
)
from .settings import URL_ROOT
-from .file_utils import extract_files
+from .file_utils import extract_files, is_csv
from .send_emails import send_user_mail, generate_activation_key, send_bulk_mail
from .decorators import email_verified, has_profile
@@ -1850,6 +1850,158 @@ def download_assignment_file(request, quiz_id, question_id=None, user_id=None):
@login_required
@email_verified
+def upload_users(request, course_id):
+ user = request.user
+ ci = RequestContext(request)
+ course = get_object_or_404(Course, pk=course_id)
+ context = {'course': course}
+
+ if not (course.is_teacher(user) or course.is_creator(user)):
+ msg = 'You do not have permissions to this course.'
+ return complete(request, reason=msg)
+ if request.method == 'POST':
+ if 'csv_file' not in request.FILES:
+ context['message'] = "Please upload a CSV file."
+ return my_render_to_response('yaksh/course_detail.html', context,
+ context_instance=ci)
+ csv_file = request.FILES['csv_file']
+ is_csv_file, dialect = is_csv(csv_file)
+ if not is_csv_file:
+ context['message'] = "The file uploaded is not a CSV file."
+ return my_render_to_response('yaksh/course_detail.html', context,
+ context_instance=ci)
+ required_fields = ['firstname', 'lastname', 'email']
+ try:
+ reader = csv.DictReader(csv_file.read().decode('utf-8').splitlines(),
+ dialect=dialect)
+ except TypeError:
+ context['message'] = "Bad CSV file"
+ return my_render_to_response('yaksh/course_detail.html', context,
+ context_instance=ci)
+ stripped_fieldnames = [field.strip().lower() for field in reader.fieldnames]
+ for field in required_fields:
+ if field not in stripped_fieldnames:
+ context['message'] = "The CSV file does not contain the required headers"
+ return my_render_to_response('yaksh/course_detail.html', context,
+ context_instance=ci)
+ reader.fieldnames = stripped_fieldnames
+ context['upload_details'] = _read_user_csv(reader, course)
+ return my_render_to_response('yaksh/course_detail.html', context,
+ context_instance=ci)
+
+
+def _read_user_csv(reader, course):
+ fields = reader.fieldnames
+ upload_details = ["Upload Summary:"]
+ counter = 0
+ for row in reader:
+ counter += 1
+ (username, email, first_name, last_name, password, roll_no, institute,
+ department, remove) = _get_csv_values(row, fields)
+ if not email or not first_name or not last_name:
+ upload_details.append("{0} -- Missing Values".format(counter))
+ continue
+ users = User.objects.filter(username=username)
+ if users.exists():
+ user = users[0]
+ if remove.strip().lower() == 'true':
+ if _remove_from_course(user, course):
+ upload_details.append("{0} -- {1} -- User rejected".format(
+ counter, user.username))
+ continue
+ else:
+ if _add_to_course(user, course):
+ upload_details.append("{0} -- {1} -- User rejected".format(
+ counter, user.username))
+ if user not in course.get_enrolled():
+ upload_details.append("{0} -- {1} not added to course".format(
+ counter, user))
+ continue
+ user_defaults = {'email': email, 'first_name': first_name,
+ 'last_name': last_name}
+ user, created = _create_or_update_user(username, password, user_defaults)
+ profile_defaults = {'institute': institute, 'roll_number': roll_no,
+ 'department': department, 'is_email_verified': True}
+ _create_or_update_profile(user, profile_defaults)
+ if created:
+ state = "Added"
+ course.students.add(user)
+ else:
+ state = "Updated"
+ upload_details.append("{0} -- {1} -- User {2} Successfully".format(
+ counter, user.username, state))
+ if counter == 0:
+ upload_details.append("No rows in the CSV file")
+ return upload_details
+
+
+def _get_csv_values(row, fields):
+ roll_no, institute, department = "", "", ""
+ remove = "false"
+ email, first_name, last_name = map(str.strip, [row['email'],
+ row['firstname'],
+ row['lastname']])
+ password = email
+ username = email
+ if 'password' in fields and row['password']:
+ password = row['password'].strip()
+ if 'roll_no' in fields:
+ roll_no = row['roll_no'].strip()
+ if 'institute' in fields:
+ institute = row['institute'].strip()
+ if 'department' in fields:
+ department = row['department'].strip()
+ if 'remove' in fields:
+ remove = row['remove'].strip()
+ if 'username' in fields and row['username']:
+ username = row['username'].strip()
+ if 'remove' in fields:
+ remove = row['remove']
+ return (username, email, first_name, last_name, password, roll_no, institute,
+ department, remove)
+
+
+def _remove_from_course(user, course):
+ if user in course.get_enrolled():
+ course.reject(True, user)
+ return True
+
+
+def _add_to_course(user, course):
+ if user in course.get_rejected():
+ course.enroll(True, user)
+ return True
+
+
+def _create_or_update_user(username, password, defaults):
+ user, created = User.objects.update_or_create(username=username,
+ defaults=defaults)
+ user.set_password(password)
+ user.save()
+ return user, created
+
+
+def _create_or_update_profile(user, defaults):
+ Profile.objects.update_or_create(user=user, defaults=defaults)
+
+
+@login_required
+@email_verified
+def download_sample_csv(request):
+ user = request.user
+ if not is_moderator(user):
+ raise Http404('You are not allowed to view this page!')
+ csv_file_path = os.path.join(FIXTURES_DIR_PATH,
+ "sample_user_upload.csv")
+ with open(csv_file_path, 'rb') as csv_file:
+ response = HttpResponse(csv_file.read(), content_type='text/csv')
+ response['Content-Disposition'] = 'attachment;\
+ filename="sample_user_upload"'
+ return response
+
+
+@login_required
+@email_verified
def duplicate_course(request, course_id):
user = request.user
course = get_object_or_404(Course, pk=course_id)