From 542433598aad0efffee7619e1f113425147bcec0 Mon Sep 17 00:00:00 2001
From: adityacp
Date: Mon, 1 Mar 2021 11:13:07 +0530
Subject: Add AWS support for file upload
---
online_test/settings.py | 34 ++++++++++++++++++++++++++++++++--
yaksh/admin.py | 3 ++-
yaksh/models.py | 9 +++++++--
yaksh/storage_backends.py | 18 ++++++++++++++++++
yaksh/templates/yaksh/add_quiz.html | 4 ++--
yaksh/views.py | 18 +++++++++---------
6 files changed, 70 insertions(+), 16 deletions(-)
create mode 100644 yaksh/storage_backends.py
diff --git a/online_test/settings.py b/online_test/settings.py
index e7e19a0..bca24d5 100644
--- a/online_test/settings.py
+++ b/online_test/settings.py
@@ -52,7 +52,8 @@ INSTALLED_APPS = (
'rest_framework',
'api',
'corsheaders',
- 'rest_framework.authtoken'
+ 'rest_framework.authtoken',
+ 'storages'
)
MIDDLEWARE = (
@@ -162,7 +163,7 @@ PRODUCTION_URL = 'your_project_url'
IS_DEVELOPMENT = True
# Video File upload size
-MAX_UPLOAD_SIZE = 52428800
+MAX_UPLOAD_SIZE = 524288000
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
@@ -250,3 +251,32 @@ REST_FRAMEWORK = {
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True
+
+
+# AWS Credentials
+USE_AWS = False
+if USE_AWS:
+ DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
+ STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'
+
+ AWS_ACCESS_KEY_ID = "access-key"
+ AWS_SECRET_ACCESS_KEY = "secret-key"
+ AWS_S3_REGION_NAME = "ap-south-1"
+ AWS_STORAGE_BUCKET_NAME = "yaksh-django"
+ AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
+ AWS_DEFAULT_ACL = 'public-read'
+ AWS_S3_SIGNATURE_VERSION = 's3v4'
+ AWS_S3_ADDRESSING_STYLE = "virtual"
+
+ # Static Location
+ AWS_STATIC_LOCATION = 'static'
+ STATICFILES_STORAGE = 'yaksh.storage_backends.StaticStorage'
+ STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_STATIC_LOCATION}/"
+
+ # Media Public
+ AWS_PUBLIC_MEDIA_LOCATION = 'media/public'
+ DEFAULT_FILE_STORAGE = 'yaksh.storage_backends.PublicMediaStorage'
+
+ # Media Private
+ AWS_PRIVATE_MEDIA_LOCATION = 'media/private'
+ PRIVATE_FILE_STORAGE = 'yaksh.storage_backends.PrivateMediaStorage'
diff --git a/yaksh/admin.py b/yaksh/admin.py
index e98c7c5..011e24f 100644
--- a/yaksh/admin.py
+++ b/yaksh/admin.py
@@ -2,7 +2,7 @@ from yaksh.models import Question, Quiz, QuestionPaper, Profile
from yaksh.models import (TestCase, StandardTestCase, StdIOBasedTestCase,
Course, AnswerPaper, CourseStatus, LearningModule,
Lesson, Post, Comment, Topic, TableOfContents,
- LessonQuizAnswer, Answer
+ LessonQuizAnswer, Answer, AssignmentUpload
)
from django.contrib import admin
@@ -64,3 +64,4 @@ admin.site.register(Topic)
admin.site.register(TableOfContents)
admin.site.register(LessonQuizAnswer)
admin.site.register(Answer)
+admin.site.register(AssignmentUpload)
\ No newline at end of file
diff --git a/yaksh/models.py b/yaksh/models.py
index 11ddf8a..dd3838a 100644
--- a/yaksh/models.py
+++ b/yaksh/models.py
@@ -51,6 +51,8 @@ from yaksh.code_server import (
from yaksh.settings import SERVER_POOL_PORT, SERVER_HOST_NAME
from .file_utils import extract_files, delete_files
from grades.models import GradingSystem
+from yaksh.storage_backends import PublicMediaStorage
+
languages = (
("python", "Python"),
@@ -1442,8 +1444,9 @@ class Question(models.Model):
assignmentQuestion=self, user=user
)
if assignment_files:
- metadata['assign_files'] = [(file.assignmentFile.path, False)
+ metadata['assign_files'] = [(file.assignmentFile.url, False)
for file in assignment_files]
+ print(metadata['assign_files'])
question_data['metadata'] = metadata
return json.dumps(question_data)
@@ -2661,7 +2664,9 @@ class AssignmentUploadManager(models.Manager):
class AssignmentUpload(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
assignmentQuestion = models.ForeignKey(Question, on_delete=models.CASCADE)
- assignmentFile = models.FileField(upload_to=get_assignment_dir, max_length=255)
+ assignmentFile = models.FileField(
+ upload_to=get_assignment_dir, max_length=255
+ )
question_paper = models.ForeignKey(QuestionPaper, blank=True, null=True,
on_delete=models.CASCADE)
course = models.ForeignKey(Course, null=True, blank=True,
diff --git a/yaksh/storage_backends.py b/yaksh/storage_backends.py
new file mode 100644
index 0000000..4d08c8c
--- /dev/null
+++ b/yaksh/storage_backends.py
@@ -0,0 +1,18 @@
+from django.conf import settings
+from storages.backends.s3boto3 import S3Boto3Storage
+
+
+class StaticStorage(S3Boto3Storage):
+ location = settings.AWS_STATIC_LOCATION if settings.USE_AWS else settings.STATIC_URL
+
+
+class PublicMediaStorage(S3Boto3Storage):
+ location = settings.AWS_PUBLIC_MEDIA_LOCATION if settings.USE_AWS else settings.MEDIA_URL
+ file_overwrite = False
+
+
+class PrivateMediaStorage(S3Boto3Storage):
+ location = settings.AWS_PRIVATE_MEDIA_LOCATION if settings.USE_AWS else settings.MEDIA_URL
+ default_acl = 'private'
+ file_overwrite = False
+ custom_domain = False
\ No newline at end of file
diff --git a/yaksh/templates/yaksh/add_quiz.html b/yaksh/templates/yaksh/add_quiz.html
index 1609639..d43c1c6 100644
--- a/yaksh/templates/yaksh/add_quiz.html
+++ b/yaksh/templates/yaksh/add_quiz.html
@@ -65,11 +65,11 @@
You can check the quiz by attempting it in the following modes:
-
+
Try as student
-
+
Try as teacher
diff --git a/yaksh/views.py b/yaksh/views.py
index 1965191..8422110 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -858,15 +858,15 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None,
)
for fname in assignment_filename:
fname._name = fname._name.replace(" ", "_")
- assignment_files = AssignmentUpload.objects.filter(
- assignmentQuestion=current_question, course_id=course_id,
- assignmentFile__icontains=fname, user=user,
- question_paper=questionpaper_id)
- if assignment_files.exists():
- assign_file = assignment_files.first()
- if os.path.exists(assign_file.assignmentFile.path):
- os.remove(assign_file.assignmentFile.path)
- assign_file.delete()
+ # assignment_files = AssignmentUpload.objects.filter(
+ # assignmentQuestion=current_question, course_id=course_id,
+ # assignmentFile__icontains=fname, user=user,
+ # question_paper=questionpaper_id)
+ # if assignment_files.exists():
+ # assign_file = assignment_files.first()
+ # if os.path.exists(assign_file.assignmentFile.path):
+ # os.remove(assign_file.assignmentFile.path)
+ # assign_file.delete()
AssignmentUpload.objects.create(
user=user, assignmentQuestion=current_question,
course_id=course_id,
--
cgit
From cc101bd195cc456f7bace4e9b646e2190975dde3 Mon Sep 17 00:00:00 2001
From: adityacp
Date: Tue, 23 Mar 2021 11:53:39 +0530
Subject: Modify file upload with js client dropzone
---
online_test/settings.py | 10 +---
requirements/requirements-common.txt | 2 +
yaksh/models.py | 66 +++++++++++++++------
yaksh/static/yaksh/js/requesthandler.js | 43 +++++++++++++-
yaksh/storage_backends.py | 20 +++----
yaksh/templates/yaksh/complete.html | 100 ++++++++++++++++----------------
yaksh/templates/yaksh/question.html | 23 ++++++--
yaksh/templates/yaksh/quit.html | 1 +
yaksh/views.py | 67 ++++++++++++---------
9 files changed, 215 insertions(+), 117 deletions(-)
diff --git a/online_test/settings.py b/online_test/settings.py
index bca24d5..4bd800d 100644
--- a/online_test/settings.py
+++ b/online_test/settings.py
@@ -30,6 +30,9 @@ DEBUG = True
ALLOWED_HOSTS = []
+# This is a required field
+DOMAIN_HOST = "http://127.0.0.1:8000"
+
URL_ROOT = ''
# Application definition
@@ -256,9 +259,6 @@ CORS_ALLOW_CREDENTIALS = True
# AWS Credentials
USE_AWS = False
if USE_AWS:
- DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
- STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'
-
AWS_ACCESS_KEY_ID = "access-key"
AWS_SECRET_ACCESS_KEY = "secret-key"
AWS_S3_REGION_NAME = "ap-south-1"
@@ -272,11 +272,7 @@ if USE_AWS:
AWS_STATIC_LOCATION = 'static'
STATICFILES_STORAGE = 'yaksh.storage_backends.StaticStorage'
STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_STATIC_LOCATION}/"
-
# Media Public
AWS_PUBLIC_MEDIA_LOCATION = 'media/public'
DEFAULT_FILE_STORAGE = 'yaksh.storage_backends.PublicMediaStorage'
- # Media Private
- AWS_PRIVATE_MEDIA_LOCATION = 'media/private'
- PRIVATE_FILE_STORAGE = 'yaksh.storage_backends.PrivateMediaStorage'
diff --git a/requirements/requirements-common.txt b/requirements/requirements-common.txt
index 9805dc5..320b145 100644
--- a/requirements/requirements-common.txt
+++ b/requirements/requirements-common.txt
@@ -21,3 +21,5 @@ django-cors-headers==3.1.0
Pillow
pandas
more-itertools==8.4.0
+django-storages==1.11.1
+boto3==1.17.17
diff --git a/yaksh/models.py b/yaksh/models.py
index 613bf00..51c2aa8 100644
--- a/yaksh/models.py
+++ b/yaksh/models.py
@@ -43,7 +43,8 @@ from django.template import Context, Template
from django.conf import settings
from django.forms.models import model_to_dict
from django.db.models import Count
-
+from django.db.models.signals import pre_delete
+from django.db.models.fields.files import FieldFile
# Local Imports
from yaksh.code_server import (
submit, get_result as get_result_from_code_server
@@ -51,7 +52,6 @@ from yaksh.code_server import (
from yaksh.settings import SERVER_POOL_PORT, SERVER_HOST_NAME
from .file_utils import extract_files, delete_files
from grades.models import GradingSystem
-from yaksh.storage_backends import PublicMediaStorage
languages = (
@@ -272,6 +272,17 @@ def is_valid_time_format(time):
return status
+def file_cleanup(sender, instance, *args, **kwargs):
+ '''
+ Deletes the file(s) associated with a model instance. The model
+ is not saved after deletion of the file(s) since this is meant
+ to be used with the pre_delete signal.
+ '''
+ for field_name, _ in instance.__dict__.items():
+ field = getattr(instance, field_name)
+ if issubclass(field.__class__, FieldFile) and field.name:
+ field.delete(save=False)
+
###############################################################################
class CourseManager(models.Manager):
@@ -1424,11 +1435,10 @@ class Question(models.Model):
]
}
- def consolidate_answer_data(self, user_answer, user=None):
+ def consolidate_answer_data(self, user_answer, user=None, regrade=False):
question_data = {}
metadata = {}
test_case_data = []
-
test_cases = self.get_test_cases()
for test in test_cases:
@@ -1441,20 +1451,34 @@ class Question(models.Model):
metadata['partial_grading'] = self.partial_grading
files = FileUpload.objects.filter(question=self)
if files:
- metadata['file_paths'] = [(file.file.path, file.extract)
- for file in files]
- if self.type == "upload":
- assignment_files = AssignmentUpload.objects.filter(
- assignmentQuestion=self
- )
- if assignment_files:
- metadata['assign_files'] = [(file.assignmentFile.url, False)
- for file in assignment_files]
- print(metadata['assign_files'])
+ if settings.USE_AWS:
+ metadata['file_paths'] = [
+ (file.file.url, file.extract)
+ for file in files
+ ]
+ else:
+ metadata['file_paths'] = [
+ (self.get_file_url(file.file.url), file.extract)
+ for file in files
+ ]
+ if self.type == "upload" and regrade:
+ file = AssignmentUpload.objects.only(
+ "assignmentFile").filter(
+ assignmentQuestion_id=self.id, answer_paper__user_id=user.id
+ ).order_by("-id").first()
+ if file:
+ if settings.USE_AWS:
+ metadata['assign_files'] = [file.assignmentFile.url]
+ else:
+ metadata['assign_files'] = [
+ self.get_file_url(file.assignmentFile.url)
+ ]
question_data['metadata'] = metadata
-
return json.dumps(question_data)
+ def get_file_url(self, path):
+ return f'{settings.DOMAIN_HOST}{path}'
+
def dump_questions(self, question_ids, user):
questions = Question.objects.filter(id__in=question_ids,
user_id=user.id, active=True
@@ -1693,7 +1717,7 @@ class FileUpload(models.Model):
def get_filename(self):
return os.path.basename(self.file.name)
-
+pre_delete.connect(file_cleanup, sender=FileUpload)
###############################################################################
class Answer(models.Model):
"""Answers submitted by the users."""
@@ -2599,7 +2623,7 @@ class AnswerPaper(models.Model):
return (False, f'{msg} {question.type} answer submission error')
else:
answer = user_answer.answer
- json_data = question.consolidate_answer_data(answer) \
+ json_data = question.consolidate_answer_data(answer, self.user, True) \
if question.type == 'code' else None
result = self.validate_answer(answer, question,
json_data, user_answer.id,
@@ -2649,7 +2673,11 @@ class AssignmentUploadManager(models.Manager):
answer_paper__question_paper=qp,
answer_paper__course_id=course_id
)
- file_name = User.objects.get(id=user_id).get_full_name()
+ user_name = assignment_files.values_list(
+ "answer_paper__user__first_name",
+ "answer_paper__user__last_name"
+ )[0]
+ file_name = "_".join(user_name)
else:
assignment_files = AssignmentUpload.objects.filter(
answer_paper__question_paper=qp,
@@ -2676,6 +2704,8 @@ class AssignmentUpload(models.Model):
def __str__(self):
return f'Assignment File of the user {self.answer_paper.user}'
+pre_delete.connect(file_cleanup, sender=AssignmentUpload)
+
##############################################################################
class TestCase(models.Model):
diff --git a/yaksh/static/yaksh/js/requesthandler.js b/yaksh/static/yaksh/js/requesthandler.js
index 80b67fb..0a06c79 100644
--- a/yaksh/static/yaksh/js/requesthandler.js
+++ b/yaksh/static/yaksh/js/requesthandler.js
@@ -145,8 +145,49 @@ var global_editor = {};
var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
var err_lineno;
var marker;
+Dropzone.autoDiscover = false;
+var submitfiles;
$(document).ready(function(){
- if(is_exercise == "True" && can_skip == "False"){
+ var filezone = $("div#dropzone_file").dropzone({
+ url: $("#code").attr("action"),
+ parallelUploads: 10,
+ uploadMultiple: true,
+ maxFiles:20,
+ paramName: "assignment",
+ autoProcessQueue: false,
+ init: function() {
+ var submitButton = document.querySelector("#check");
+ myDropzone = this;
+ submitButton.addEventListener("click", function(e) {
+ if (myDropzone.getQueuedFiles().length === 0) {
+ $("#upload_alert").modal("show");
+ e.preventDefault();
+ return;
+ }
+ if (myDropzone.getAcceptedFiles().length > 0) {
+ if (submitfiles === true) {
+ submitfiles = false;
+ return;
+ }
+ e.preventDefault();
+ myDropzone.processQueue();
+ myDropzone.on("complete", function () {
+ submitfiles = true;
+ $('#check').trigger('click');
+ });
+ }
+ });
+ },
+ success: function (file, response) {
+ document.open();
+ document.write(response);
+ document.close();
+ },
+ headers: {
+ "X-CSRFToken": document.getElementById("code").elements[0].value
+ }
+ });
+ if(is_exercise == "True" && can_skip == "False") {
setTimeout(function() {show_solution();}, delay_time*1000);
}
// Codemirror object, language modes and initial content
diff --git a/yaksh/storage_backends.py b/yaksh/storage_backends.py
index 4d08c8c..206e456 100644
--- a/yaksh/storage_backends.py
+++ b/yaksh/storage_backends.py
@@ -3,16 +3,16 @@ from storages.backends.s3boto3 import S3Boto3Storage
class StaticStorage(S3Boto3Storage):
- location = settings.AWS_STATIC_LOCATION if settings.USE_AWS else settings.STATIC_URL
+ if settings.USE_AWS:
+ location = settings.AWS_STATIC_LOCATION
+ else:
+ pass
class PublicMediaStorage(S3Boto3Storage):
- location = settings.AWS_PUBLIC_MEDIA_LOCATION if settings.USE_AWS else settings.MEDIA_URL
- file_overwrite = False
-
-
-class PrivateMediaStorage(S3Boto3Storage):
- location = settings.AWS_PRIVATE_MEDIA_LOCATION if settings.USE_AWS else settings.MEDIA_URL
- default_acl = 'private'
- file_overwrite = False
- custom_domain = False
\ No newline at end of file
+ if settings.USE_AWS:
+ location = settings.AWS_PUBLIC_MEDIA_LOCATION
+ file_overwrite = True
+ default_acl = 'public-read'
+ else:
+ pass
diff --git a/yaksh/templates/yaksh/complete.html b/yaksh/templates/yaksh/complete.html
index 4d921e1..79a1392 100644
--- a/yaksh/templates/yaksh/complete.html
+++ b/yaksh/templates/yaksh/complete.html
@@ -20,58 +20,58 @@
{% endif %}
-{% csrf_token %}
- {% if paper.questions_answered.all or paper.questions_unanswered.all %}
-
-
-
Submission Status
-
-
+{% if paper.questions_answered.all or paper.questions_unanswered.all %}
+
+
+
Submission Status
+
+
+
+ Question |
+ Status |
+
+
+
+ {% for question in paper.questions.all %}
+ {% if question in paper.questions_answered.all %}
- Question |
- Status |
-
-
-
- {% for question in paper.questions.all %}
- {% if question in paper.questions_answered.all %}
-
- {{ question.summary }} |
- Attempted |
- {% else %}
-
- {{ question }} |
- Not Attempted |
- {% endif %}
-
- {% endfor %}
-
-
-
-
- {% endif %}
-
-
-
- {{message}}
-
+ {{ question.summary }} |
+ Attempted |
+ {% else %}
+
+ {{ question }} |
+ Not Attempted |
+ {% endif %}
+
+ {% endfor %}
+
+
+
-
-
- {% if module_id and not paper.course.is_trial %}
- {% if first_unit %}
- Next
-
-
-
- {% else %}
- Next
-
-
-
- {% endif %}
+{% endif %}
+
+
+
+ {{message}}
+
+
+
+
+{% if module_id and not paper.course.is_trial %}
+ {% if first_unit %}
+ Next
+
+
+
{% else %}
- Home
+ Next
+
+
+
{% endif %}
-
+{% else %}
+ Home
+{% endif %}
+
+
{% endblock content %}
diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html
index 56edcf3..2779db2 100644
--- a/yaksh/templates/yaksh/question.html
+++ b/yaksh/templates/yaksh/question.html
@@ -7,6 +7,8 @@
+
+