diff options
author | Prabhu Ramachandran | 2017-04-28 17:14:43 +0530 |
---|---|---|
committer | GitHub | 2017-04-28 17:14:43 +0530 |
commit | b285378b2eca29e7125bf9474cd57c973a202f37 (patch) | |
tree | 726da35ba55acacfde255e31bdc2233600f75395 | |
parent | 2694fd6dd4d37a1a6570792e234998feef21edca (diff) | |
parent | 624e752684125aa525d9b3643cbd7c9b7ba61682 (diff) | |
download | online_test-b285378b2eca29e7125bf9474cd57c973a202f37.tar.gz online_test-b285378b2eca29e7125bf9474cd57c973a202f37.tar.bz2 online_test-b285378b2eca29e7125bf9474cd57c973a202f37.zip |
Merge pull request #273 from adityacp/email_verification
Email verification during registration
-rw-r--r-- | online_test/settings.py | 23 | ||||
-rw-r--r-- | yaksh/decorators.py | 25 | ||||
-rw-r--r-- | yaksh/email_verification.py | 68 | ||||
-rw-r--r-- | yaksh/forms.py | 22 | ||||
-rw-r--r-- | yaksh/middleware/user_time_zone.py | 4 | ||||
-rw-r--r-- | yaksh/models.py | 3 | ||||
-rw-r--r-- | yaksh/templates/yaksh/activation_status.html | 79 | ||||
-rw-r--r-- | yaksh/urls.py | 3 | ||||
-rw-r--r-- | yaksh/views.py | 137 |
9 files changed, 349 insertions, 15 deletions
diff --git a/online_test/settings.py b/online_test/settings.py index dcbbb21..8d0613b 100644 --- a/online_test/settings.py +++ b/online_test/settings.py @@ -102,6 +102,7 @@ MEDIA_URL = "/data/" MEDIA_ROOT = os.path.join(BASE_DIR, "yaksh", "data") +# Set this varable to <True> if smtp-server is not allowing to send email. EMAIL_USE_TLS = False EMAIL_HOST = 'your_email_host' @@ -110,6 +111,28 @@ EMAIL_PORT = 'your_email_port' EMAIL_HOST_USER = 'email_host_user' +EMAIL_HOST_PASSWORD = 'email_host_password' + +# SENDER_EMAIL, REPLY_EMAIL, PRODUCTION_URL, IS_DEVELOPMENT are used in email +# verification. Set the variables accordingly to avoid errors in production + +# This email id will be used as <from address> for sending emails. +# For example no_reply@<your_organization>.in can be used. +SENDER_EMAIL = 'your_email' + +# This email id will be used by users to send their queries +# For example queries@<your_organization>.in can be used. +REPLY_EMAIL = 'your_reply_email' + +# This url will be used in email verification to create activation link. +# Add your hosted url to this variable. +# For example https://127.0.0.1:8000 or 127.0.0.1:8000 +PRODUCTION_URL = 'your_project_url' + +# Set this variable to <False> once the project is in production. +# If this variable is kept <True> in production, email will not be verified. +IS_DEVELOPMENT = True + DEFAULT_FROM_EMAIL = EMAIL_HOST_USER TEMPLATES = [ diff --git a/yaksh/decorators.py b/yaksh/decorators.py new file mode 100644 index 0000000..f0d354c --- /dev/null +++ b/yaksh/decorators.py @@ -0,0 +1,25 @@ +from django.shortcuts import render_to_response +from django.conf import settings +from django.template import RequestContext + + +def email_verified(func): + """ This decorator is used to check if email is verified. + If email is not verified then redirect user for email + verification + """ + + def is_email_verified(request, *args, **kwargs): + ci = RequestContext(request) + user = request.user + context = {} + if not settings.IS_DEVELOPMENT: + if user.is_authenticated() and hasattr(user, 'profile'): + if not user.profile.is_email_verified: + context['success'] = False + context['msg'] = "Your account is not verified. \ + Please verify your account" + return render_to_response('yaksh/activation_status.html', + context, context_instance=ci) + return func(request, *args, **kwargs) + return is_email_verified
\ No newline at end of file diff --git a/yaksh/email_verification.py b/yaksh/email_verification.py new file mode 100644 index 0000000..721fa61 --- /dev/null +++ b/yaksh/email_verification.py @@ -0,0 +1,68 @@ +# Local imports +try: + from string import letters +except ImportError: + from string import ascii_letters as letters +from string import digits, punctuation +import hashlib +from random import randint +from textwrap import dedent +import smtplib + +# Django imports +from django.utils.crypto import get_random_string +from django.conf import settings + +def generate_activation_key(username): + """ Generate hashed secret key for email activation """ + chars = letters + digits + punctuation + secret_key = get_random_string(randint(10, 40), chars) + return hashlib.sha256((secret_key + username).encode('utf-8')).hexdigest() + +def send_user_mail(user_mail, key): + """ Send mail to user whose email is to be verified + This function should get two args i.e user_email and secret_key. + The activation url is generated from settings.PRODUCTION_URL and key. + """ + try: + to = user_mail + subject = 'Yaksh Email Verification' + message = dedent("""\ + To activate your account and verify your email address, + please click the following link: + {0}/exam/activate/{1} + If clicking the link above does not work, + copy and paste the URL in a new browser window instead. + For any issue, please write us on {2} + + Regards + Yaksh Team + """.format(settings.PRODUCTION_URL, key, settings.REPLY_EMAIL) + ) + + user = settings.EMAIL_HOST_USER + pwd = settings.EMAIL_HOST_PASSWORD + smtpserver = smtplib.SMTP(settings.EMAIL_HOST, settings.EMAIL_PORT) + smtpserver.ehlo() + smtpserver.starttls() + smtpserver.ehlo() + smtpserver.esmtp_features['auth']='LOGIN DIGEST-MD5 PLAIN' + + smtpserver.login(user, pwd) + header = 'To:{0}\nFrom:{1}\nSubject:{2}\n'.format(to, + settings.SENDER_EMAIL, subject) + message = '{0}\n{1}\n\n'.format(header, message) + smtpserver.sendmail(user, to, message) + smtpserver.close() + + msg = "An activation link is sent to your registered email.\ + Please activate the link within 20 minutes." + success = True + + except Exception as exc_msg: + msg = """Error: {0}. Please check your email address.\ + If email address is correct then + Please contact {1}.""".format(exc_msg, settings.REPLY_EMAIL) + success = False + + return success, msg diff --git a/yaksh/forms.py b/yaksh/forms.py index d56fb04..5dd56a2 100644 --- a/yaksh/forms.py +++ b/yaksh/forms.py @@ -5,7 +5,7 @@ from yaksh.models import get_model_class, Profile, Quiz, Question, TestCase, Cou from django.contrib.auth import authenticate from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType - +from django.conf import settings from taggit.managers import TaggableManager from taggit.forms import TagField from django.forms.models import inlineformset_factory @@ -18,6 +18,7 @@ except ImportError: from string import punctuation, digits import datetime import pytz +from .email_verification import generate_activation_key languages = ( ("select", "Select Language"), @@ -117,6 +118,12 @@ class UserRegisterForm(forms.Form): return c_pwd + def clean_email(self): + user_email = self.cleaned_data['email'] + if User.objects.filter(email=user_email).exists(): + raise forms.ValidationError("This email already exists") + return user_email + def save(self): u_name = self.cleaned_data["username"] u_name = u_name.lower() @@ -135,9 +142,17 @@ class UserRegisterForm(forms.Form): new_profile.department = cleaned_data["department"] new_profile.position = cleaned_data["position"] new_profile.timezone = cleaned_data["timezone"] + if settings.IS_DEVELOPMENT: + new_profile.is_email_verified = True + else: + new_profile.activation_key = generate_activation_key(new_user.username) + new_profile.key_expiry_time = datetime.datetime.strftime( + datetime.datetime.now() + \ + datetime.timedelta(minutes=20), + "%Y-%m-%d %H:%M:%S" + ) new_profile.save() - - return u_name, pwd + return u_name, pwd, new_user.email, new_profile.activation_key class UserLoginForm(forms.Form): @@ -318,4 +333,3 @@ class QuestionPaperForm(forms.ModelForm): class Meta: model = QuestionPaper fields = ['shuffle_questions'] - diff --git a/yaksh/middleware/user_time_zone.py b/yaksh/middleware/user_time_zone.py index 0bd4831..ff9ec5c 100644 --- a/yaksh/middleware/user_time_zone.py +++ b/yaksh/middleware/user_time_zone.py @@ -5,10 +5,10 @@ from django.utils import timezone class TimezoneMiddleware(object): """ Middleware to get user's timezone and activate timezone - if user timezone is not available default value 'UTC' is activated """ + if user timezone is not available default value 'Asia/Kolkata' is activated """ def process_request(self, request): user = request.user - user_tz = 'UTC' + user_tz = 'Asia/Kolkata' if hasattr(user, 'profile'): if user.profile.timezone: user_tz = user.profile.timezone diff --git a/yaksh/models.py b/yaksh/models.py index 5fa828c..f6867f0 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -256,6 +256,9 @@ class Profile(models.Model): default=pytz.utc.zone, choices=[(tz, tz) for tz in pytz.common_timezones] ) + is_email_verified = models.BooleanField(default=False) + activation_key = models.CharField(max_length=40, blank=True, null=True) + key_expiry_time = models.DateTimeField(blank=True, null=True) def get_user_dir(self): """Return the output directory for the user.""" diff --git a/yaksh/templates/yaksh/activation_status.html b/yaksh/templates/yaksh/activation_status.html new file mode 100644 index 0000000..c196300 --- /dev/null +++ b/yaksh/templates/yaksh/activation_status.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} + +{% block pagetitle %} Yaksh Account Activation {% endblock %} +{% block title %} Yaksh Account Activation {% endblock %} +{% block css%} + <link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/dashboard.css" type="text/css" /> +{% endblock %} +{% block nav %} +<nav class="navbar navbar-fixed-top navbar-inverse"> + <div class="container"> + <div class= "collapse navbar-collapse" id="navbar"> + <ul class="nav navbar-nav navbar-right"> + <li><a href="{{ URL_ROOT }}/exam/viewprofile"> {{ user.get_full_name.title }} </a></li> + <li><a style='cursor:pointer' id='logout' onClick='location.replace("{{URL_ROOT}}/exam/logout/");'> + <span class="glyphicon glyphicon-log-out">Logout </span></a></li> + </ul> + </div> + </div> +</nav> +{% endblock %} +{% block content %} +{% if success %} + <center> + <div class="alert alert-success" role="alert"> + <strong> {{ msg }} </strong> + </div> + <a href="{{URL_ROOT}}/exam/"> Home </a> + </center> +{% else %} + {% if msg %} + <center> + <div class="alert alert-warning" role="alert"> + <strong> {{ msg }} </strong> + </div> + </center> + <form action="{{ URL_ROOT }}/exam/new_activation/" method="post"> + {% csrf_token %} + <center> + Enter Email Address for verification: <input type="email" name="email" required><br><br> + <button class="btn" type="submit">Send Email</button> + </center> + </form> + {% endif %} +{% endif %} +<br/> +{% if activation_msg %} + <center> + <div class="alert alert-info" role="alert"> + <strong> {{ activation_msg }} </strong> + </div> + <a href="{{URL_ROOT}}/exam/"> Home </a> + </center> +{% endif %} + +{% if email_err_msg %} + <center> + <div class="alert alert-warning" role="alert"> + <strong> {{ email_err_msg }} </strong> + </div> + </center> + <form action="{{ URL_ROOT }}/exam/update_email/" method="post"> + {% csrf_token %} + <center> + <table> + <tr> + <td>Enter Username:</td> + <td><input type="text" name="username" required></td> + </tr> + <tr> + <td>Enter New Email Address:</td> + <td><input type="email" name="email" required></td> + </tr> + </table> + <br> + <button class="btn" type="submit">Submit</button> + </center> + </form> +{% endif %} +{% endblock content %} diff --git a/yaksh/urls.py b/yaksh/urls.py index 8ddfe67..20ce918 100644 --- a/yaksh/urls.py +++ b/yaksh/urls.py @@ -5,6 +5,9 @@ urlpatterns = [ url(r'^$', views.index), url(r'^login/$', views.user_login, name='login'), url(r'^logout/$', views.user_logout), + url(r'^update_email/$', views.update_email), + url(r'^activate/(?P<key>.+)$', views.activate_user), + url(r'^new_activation/$', views.new_activation), url(r'^quizzes/$', views.quizlist_user, name='quizlist_user'), url(r'^quizzes/(?P<enrolled>\w+)/$', views.quizlist_user, name='quizlist_user'), url(r'^results/$', views.results_user), diff --git a/yaksh/views.py b/yaksh/views.py index d298e5e..35df9f9 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -1,7 +1,7 @@ import random import string import os -from datetime import datetime +from datetime import datetime, timedelta import collections import csv from django.http import HttpResponse @@ -16,12 +16,14 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.models import Group from django.forms.models import inlineformset_factory from django.utils import timezone -from django.core.exceptions import MultipleObjectsReturned +from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist +from django.conf import settings import pytz from taggit.models import Tag from itertools import chain import json import six +from textwrap import dedent import zipfile try: from StringIO import StringIO as string_io @@ -40,7 +42,8 @@ from yaksh.forms import UserRegisterForm, UserLoginForm, QuizForm,\ from .settings import URL_ROOT from yaksh.models import AssignmentUpload from .file_utils import extract_files - +from .email_verification import send_user_mail, generate_activation_key +from .decorators import email_verified def my_redirect(url): @@ -72,13 +75,13 @@ def add_to_group(users): if not is_moderator(user): user.groups.add(group) - +@email_verified def index(request, next_url=None): """The start page. """ user = request.user if user.is_authenticated(): - if user.groups.filter(name='moderator').count() > 0: + if is_moderator(user): return my_redirect('/exam/manage/' if not next_url else next_url) return my_redirect("/exam/quizzes/" if not next_url else next_url) @@ -93,15 +96,20 @@ def user_register(request): ci = RequestContext(request) if user.is_authenticated(): return my_redirect("/exam/quizzes/") - + context = {} if request.method == "POST": form = UserRegisterForm(request.POST) if form.is_valid(): data = form.cleaned_data - u_name, pwd = form.save() + u_name, pwd, user_email, key = form.save() new_user = authenticate(username=u_name, password=pwd) login(request, new_user) - return my_redirect("/exam/quizzes/") + if user_email and key: + success, msg = send_user_mail(user_email, key) + context = {'activation_msg': msg} + return my_render_to_response('yaksh/activation_status.html', + context) + return index(request) else: return my_render_to_response('yaksh/register.html', {'form': form}, context_instance=ci) @@ -119,6 +127,7 @@ def user_logout(request): @login_required +@email_verified def quizlist_user(request, enrolled=None): """Show All Quizzes that is available to logged-in user.""" user = request.user @@ -144,6 +153,7 @@ def quizlist_user(request, enrolled=None): @login_required +@email_verified def results_user(request): """Show list of Results of Quizzes that is taken by logged-in user.""" user = request.user @@ -153,6 +163,7 @@ def results_user(request): @login_required +@email_verified def add_question(request, question_id=None): user = request.user ci = RequestContext(request) @@ -229,6 +240,7 @@ def add_question(request, question_id=None): @login_required +@email_verified def add_quiz(request, course_id, quiz_id=None): """To add a new quiz in the database. Create a new quiz and store it.""" @@ -271,6 +283,7 @@ def add_quiz(request, course_id, quiz_id=None): @login_required +@email_verified def show_all_questionpapers(request, questionpaper_id=None): user = request.user ci = RequestContext(request) @@ -294,6 +307,7 @@ def show_all_questionpapers(request, questionpaper_id=None): @login_required +@email_verified def prof_manage(request, msg=None): """Take credentials of the user with professor/moderator rights/permissions and log in.""" @@ -341,6 +355,7 @@ def user_login(request): user = request.user ci = RequestContext(request) + context = {} if user.is_authenticated(): return index(request) @@ -363,8 +378,8 @@ def user_login(request): context_instance=ci) - @login_required +@email_verified def start(request, questionpaper_id=None, attempt_num=None): """Check the user cedentials and if any quiz is available, start the exam.""" @@ -421,6 +436,7 @@ def start(request, questionpaper_id=None, attempt_num=None): @login_required +@email_verified def show_question(request, question, paper, error_message=None, notification=None): """Show a question if possible.""" user = request.user @@ -452,6 +468,7 @@ def show_question(request, question, paper, error_message=None, notification=Non @login_required +@email_verified def skip(request, q_id, next_q=None, attempt_num=None, questionpaper_id=None): user = request.user paper = get_object_or_404(AnswerPaper, user=request.user, attempt_number=attempt_num, @@ -473,6 +490,7 @@ def skip(request, q_id, next_q=None, attempt_num=None, questionpaper_id=None): @login_required +@email_verified def check(request, q_id, attempt_num=None, questionpaper_id=None): """Checks the answers of the user for particular question""" user = request.user @@ -598,6 +616,7 @@ def quit(request, reason=None, attempt_num=None, questionpaper_id=None): @login_required +@email_verified def complete(request, reason=None, attempt_num=None, questionpaper_id=None): """Show a page to inform user that the quiz has been compeleted.""" user = request.user @@ -618,6 +637,7 @@ def complete(request, reason=None, attempt_num=None, questionpaper_id=None): @login_required +@email_verified def add_course(request, course_id=None): user = request.user ci = RequestContext(request) @@ -646,6 +666,7 @@ def add_course(request, course_id=None): @login_required +@email_verified def enroll_request(request, course_id): user = request.user ci = RequestContext(request) @@ -663,6 +684,7 @@ def enroll_request(request, course_id): @login_required +@email_verified def self_enroll(request, course_id): user = request.user ci = RequestContext(request) @@ -677,6 +699,7 @@ def self_enroll(request, course_id): @login_required +@email_verified def courses(request): user = request.user ci = RequestContext(request) @@ -690,6 +713,7 @@ def courses(request): @login_required +@email_verified def course_detail(request, course_id): user = request.user ci = RequestContext(request) @@ -706,6 +730,7 @@ def course_detail(request, course_id): @login_required +@email_verified def enroll(request, course_id, user_id=None, was_rejected=False): user = request.user ci = RequestContext(request) @@ -734,6 +759,7 @@ def enroll(request, course_id, user_id=None, was_rejected=False): @login_required +@email_verified def reject(request, course_id, user_id=None, was_enrolled=False): user = request.user ci = RequestContext(request) @@ -757,6 +783,7 @@ def reject(request, course_id, user_id=None, was_enrolled=False): @login_required +@email_verified def toggle_course_status(request, course_id): user = request.user if not is_moderator(user): @@ -775,6 +802,7 @@ def toggle_course_status(request, course_id): @login_required +@email_verified def show_statistics(request, questionpaper_id, attempt_number=None): user = request.user if not is_moderator(user): @@ -801,6 +829,7 @@ def show_statistics(request, questionpaper_id, attempt_number=None): @login_required +@email_verified def monitor(request, quiz_id=None): """Monitor the progress of the papers taken so far.""" @@ -897,6 +926,7 @@ def _remove_already_present(questionpaper_id, questions): @login_required +@email_verified def design_questionpaper(request, quiz_id, questionpaper_id=None): user = request.user @@ -983,6 +1013,7 @@ def design_questionpaper(request, quiz_id, questionpaper_id=None): @login_required +@email_verified def show_all_questions(request): """Show a list of all the questions currently in the database.""" @@ -1052,6 +1083,7 @@ def show_all_questions(request): context_instance=ci) @login_required +@email_verified def user_data(request, user_id, questionpaper_id=None): """Render user data.""" current_user = request.user @@ -1066,6 +1098,7 @@ def user_data(request, user_id, questionpaper_id=None): @login_required +@email_verified def download_csv(request, questionpaper_id): user = request.user if not is_moderator(user): @@ -1111,6 +1144,7 @@ def download_csv(request, questionpaper_id): return response @login_required +@email_verified def grade_user(request, quiz_id=None, user_id=None, attempt_number=None): """Present an interface with which we can easily grade a user's papers and update all their marks and also give comments for each paper. @@ -1181,6 +1215,7 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None): @login_required +@email_verified def view_profile(request): """ view moderators and users profile """ user = request.user @@ -1203,6 +1238,7 @@ def view_profile(request): @login_required +@email_verified def edit_profile(request): """ edit profile details facility for moderator and students """ @@ -1241,6 +1277,7 @@ def edit_profile(request): @login_required +@email_verified def search_teacher(request, course_id): """ search teachers for the course """ user = request.user @@ -1270,6 +1307,7 @@ def search_teacher(request, course_id): @login_required +@email_verified def add_teacher(request, course_id): """ add teachers to the course """ @@ -1298,6 +1336,7 @@ def add_teacher(request, course_id): @login_required +@email_verified def remove_teachers(request, course_id): """ remove user from a course """ @@ -1331,6 +1370,7 @@ def test_mode(user, godmode=False, questions_list=None, quiz_id=None): @login_required +@email_verified def test_quiz(request, mode, quiz_id): """creates a trial quiz for the moderators""" godmode = True if mode == "godmode" else False @@ -1344,6 +1384,7 @@ def test_quiz(request, mode, quiz_id): @login_required +@email_verified def view_answerpaper(request, questionpaper_id): user = request.user quiz = get_object_or_404(QuestionPaper, pk=questionpaper_id).quiz @@ -1359,6 +1400,7 @@ def view_answerpaper(request, questionpaper_id): @login_required +@email_verified def create_demo_course(request): """ creates a demo course for user """ user = request.user @@ -1375,6 +1417,7 @@ def create_demo_course(request): @login_required +@email_verified def grader(request, extra_context=None): user = request.user if not is_moderator(user): @@ -1388,6 +1431,7 @@ def grader(request, extra_context=None): @login_required +@email_verified def regrade(request, course_id, question_id=None, answerpaper_id=None, questionpaper_id=None): user = request.user course = get_object_or_404(Course, pk=course_id) @@ -1409,6 +1453,7 @@ def regrade(request, course_id, question_id=None, answerpaper_id=None, questionp return grader(request, extra_context={'details': details}) @login_required +@email_verified def download_course_csv(request, course_id): user = request.user if not is_moderator(user): @@ -1450,8 +1495,82 @@ def download_course_csv(request, course_id): writer.writerow(student) return response +def activate_user(request, key): + ci = RequestContext(request) + profile = get_object_or_404(Profile, activation_key=key) + context = {} + context['success'] = False + if profile.is_email_verified: + context['activation_msg'] = "Your account is already verified" + return my_render_to_response('yaksh/activation_status.html', context, + context_instance=ci) + + if timezone.now() > profile.key_expiry_time: + context['msg'] = dedent(""" + Your activation time expired. + Please try again. + """) + else: + context['success'] = True + profile.is_email_verified = True + profile.save() + context['msg'] = "Your account is activated" + return my_render_to_response('yaksh/activation_status.html', context, + context_instance=ci) + +def new_activation(request, email=None): + ci = RequestContext(request) + context = {} + if request.method == "POST": + email = request.POST.get('email') + + try: + user = User.objects.get(email=email) + except MultipleObjectsReturned: + context['email_err_msg'] = "Multiple entries found for this email"\ + "Please change your email" + return my_render_to_response('yaksh/activation_status.html', context, + context_instance=ci) + except ObjectDoesNotExist: + context['success'] = False + context['msg'] = "Your account is not verified. \ + Please verify your account" + return render_to_response('yaksh/activation_status.html', + context, context_instance=ci) + + if not user.profile.is_email_verified: + user.profile.activation_key = generate_activation_key(user.username) + user.profile.key_expiry_time = datetime.strftime( + datetime.now() + \ + timedelta(minutes=20), "%Y-%m-%d %H:%M:%S" + ) + user.profile.save() + success, msg = send_user_mail(user.email, user.profile.activation_key) + if success: + context['activation_msg'] = msg + else: + context['msg'] = msg + else: + context['activation_msg'] = "Your account is already verified" + + return my_render_to_response('yaksh/activation_status.html', context, + context_instance=ci) + +def update_email(request): + if request.method == "POST": + email = request.POST.get('email') + username = request.POST.get('username') + user = get_object_or_404(User, username=username) + user.email = email + user.save() + return new_activation(request, email) + else: + context['email_err_msg'] = "Please Update your email" + return my_render_to_response('yaksh/activation_status.html', context, + context_instance=ci) @login_required +@email_verified def download_assignment_file(request, quiz_id, question_id=None, user_id=None): user = request.user qp = QuestionPaper.objects.get(quiz_id=quiz_id) |