diff options
-rw-r--r-- | CHANGELOG.txt | 1 | ||||
-rw-r--r-- | README.md | 18 | ||||
-rw-r--r-- | tasks.py | 17 | ||||
-rw-r--r-- | yaksh/file_utils.py | 18 | ||||
-rw-r--r-- | yaksh/fixtures/sample_user_upload.csv | 2 | ||||
-rw-r--r-- | yaksh/fixtures/users_add_update_reject.csv | 4 | ||||
-rw-r--r-- | yaksh/fixtures/users_correct.csv | 2 | ||||
-rw-r--r-- | yaksh/fixtures/users_some_headers_missing.csv | 2 | ||||
-rw-r--r-- | yaksh/fixtures/users_some_values_missing.csv | 4 | ||||
-rw-r--r-- | yaksh/fixtures/users_with_no_values.csv | 1 | ||||
-rw-r--r-- | yaksh/models.py | 16 | ||||
-rw-r--r-- | yaksh/scripts/yaksh_script.sh | 5 | ||||
-rw-r--r-- | yaksh/templates/yaksh/course_detail.html | 30 | ||||
-rw-r--r-- | yaksh/templates/yaksh/grade_user.html | 11 | ||||
-rw-r--r-- | yaksh/templates/yaksh/user_data.html | 4 | ||||
-rw-r--r-- | yaksh/templates/yaksh/view_answerpaper.html | 4 | ||||
-rw-r--r-- | yaksh/test_views.py | 159 | ||||
-rw-r--r-- | yaksh/urls.py | 3 | ||||
-rw-r--r-- | yaksh/views.py | 156 |
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. @@ -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``` @@ -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) |