summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--yaksh/file_utils.py29
-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/templates/yaksh/course_detail.html20
-rw-r--r--yaksh/test_views.py137
-rw-r--r--yaksh/urls.py1
-rw-r--r--yaksh/views.py65
9 files changed, 259 insertions, 2 deletions
diff --git a/yaksh/file_utils.py b/yaksh/file_utils.py
index f41c531..361b29c 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,30 @@ def extract_files(zip_file, path=None):
zip_file.close()
return zfiles, extract_path
+
+def is_csv(document):
+ 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)
+ document.seek(0)
+ except (csv.Error, UnicodeDecodeError):
+ return False
+ return True
+
+
+def headers_present(dict_reader, headers):
+ fields = dict_reader.fieldnames
+ clist = set()
+ for field in fields:
+ if field.strip() in headers.keys():
+ headers[field.strip()] = field
+ clist.add(field.strip())
+ if clist != headers.keys():
+ return False
+ return True
+
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/templates/yaksh/course_detail.html b/yaksh/templates/yaksh/course_detail.html
index d8aeb2e..3f13ea7 100644
--- a/yaksh/templates/yaksh/course_detail.html
+++ b/yaksh/templates/yaksh/course_detail.html
@@ -41,6 +41,18 @@
<a href="{{URL_ROOT}}/exam/manage/send_mail/{{ course.id }}/">
Send Mail</a>
</li>
+ <li>
+ <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">
+ The uploaded csv should have headers exactly same as mentioned below:<br>
+ <b>firstname, lastname, email</b>
+ </div>
+ </li>
</ul>
</div>
</div>
@@ -53,6 +65,13 @@
</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 %}
{% if state == 'mail' %}
<div id="enrolled-students">
<center><b><u>Send Mails to Students</u></b></center><br>
@@ -218,6 +237,7 @@
{% endif %}
</div>
</div>
+
<!-- Dialog to display error message -->
<div id="dialog" title="Alert">
<p id="error_msg"></p>
diff --git a/yaksh/test_views.py b/yaksh/test_views.py
index dc06126..e27d5ef 100644
--- a/yaksh/test_views.py
+++ b/yaksh/test_views.py
@@ -1507,6 +1507,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
@@ -1758,6 +1760,141 @@ 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(settings.FIXTURE_DIRS, "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_with_wrong_csv(self):
+ # Given
+ self.client.login(
+ username=self.user1.username,
+ password=self.user1_plaintext_pass
+ )
+ csv_file_path = os.path.join(settings.FIXTURE_DIRS, "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(settings.FIXTURE_DIRS, "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(settings.FIXTURE_DIRS, "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(settings.FIXTURE_DIRS, "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 4c5593a..a7aeecc 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"),
diff --git a/yaksh/views.py b/yaksh/views.py
index b4cb844..838041e 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -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, headers_present
from .send_emails import send_user_mail, generate_activation_key, send_bulk_mail
from .decorators import email_verified, has_profile
@@ -1796,6 +1796,69 @@ 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']
+ if not is_csv(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)
+ headers = {'firstname':'', 'lastname':'', 'email':''}
+ reader = csv.DictReader(csv_file.read().decode('utf-8').splitlines())
+ if not headers_present(reader, headers):
+ context['message'] = "The CSV file does not contain the required headers"
+ return my_render_to_response('yaksh/course_detail.html', context,
+ context_instance=ci)
+ context['upload_details'] = _read_user_csv(reader, headers, course)
+ return my_render_to_response('yaksh/course_detail.html', context,
+ context_instance=ci)
+
+
+def _read_user_csv(reader, headers, course):
+ upload_details = ["Upload Summary:"]
+ counter = 0;
+ for row in reader:
+ counter += 1
+ email, first_name, last_name = map(str.strip, [row[headers['email']],
+ row[headers['firstname']], row[headers['lastname']]])
+ if not email or not first_name or not last_name:
+ upload_details.append("{0} -- Missing Values".format(counter))
+ continue
+ user = User.objects.filter(email=email)
+ if user.exists():
+ upload_details.append("{0} -- {1} -- Email Already Exists".format(
+ counter, email))
+ else:
+ try:
+ user = User.objects.create_user(username=email, password=email,
+ email=email, first_name=first_name, last_name=last_name)
+ except IntegrityError:
+ upload_details.append("{0} -- {1} -- User Already Exists".format(
+ counter, email))
+ else:
+ Profile.objects.create(user=user, is_email_verified=True)
+ course.students.add(user)
+ upload_details.append("{0} -- {1} -- User Added Successfully".format(
+ counter, email))
+ if counter == 0:
+ upload_details.append("No rows in the CSV file")
+ return upload_details
+
+
+@login_required
+@email_verified
def duplicate_course(request, course_id):
user = request.user
course = get_object_or_404(Course, pk=course_id)