diff options
-rw-r--r-- | yaksh/file_utils.py | 29 | ||||
-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/templates/yaksh/course_detail.html | 20 | ||||
-rw-r--r-- | yaksh/test_views.py | 137 | ||||
-rw-r--r-- | yaksh/urls.py | 1 | ||||
-rw-r--r-- | yaksh/views.py | 65 |
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) |