Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: receive ora submission created event #7

Merged
merged 14 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions platform_plugin_turnitin/api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,28 @@
app_name = "platform_plugin_turnitin"

urlpatterns = [
path("upload-file/", views.TurnitinUploadFileAPIView.as_view(), name="upload-file"),
path(
"submission/<uuid:submission_id>/",
"upload-file/<uuid:ora_submission_id>/",
views.TurnitinUploadFileAPIView.as_view(),
name="upload-file",
),
path(
"submission/<uuid:ora_submission_id>/",
views.TurnitinSubmissionAPIView.as_view(),
name="get-submission",
),
path(
"similarity-report/<uuid:submission_id>/",
"similarity-report/<uuid:ora_submission_id>/",
views.TurnitinSimilarityReportAPIView.as_view(),
name="generate-similarity-report",
),
path(
"similarity-report/<uuid:submission_id>/",
"similarity-report/<uuid:ora_submission_id>/",
views.TurnitinSimilarityReportAPIView.as_view(),
name="get-similarity-report",
),
path(
"viewer-url/<uuid:submission_id>/",
"viewer-url/<uuid:ora_submission_id>/",
views.TurnitinViewerAPIView.as_view(),
name="viewer-url",
),
Expand Down
229 changes: 148 additions & 81 deletions platform_plugin_turnitin/api/v1/views.py

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions platform_plugin_turnitin/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,15 @@ class PlatformPluginTurnitinConfig(AppConfig):
"production": {"relative_path": "settings.production"},
},
},
"signals_config": {
"lms.djangoapp": {
"relative_path": "handlers",
"receivers": [
{
"receiver_func_name": "ora_submission_created",
"signal_path": "openedx_events.learning.signals.ORA_SUBMISSION_CREATED",
},
],
}
},
}
3 changes: 3 additions & 0 deletions platform_plugin_turnitin/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""This module contains constants used in the Turnitin plugin."""

ALLOWED_FILE_EXTENSIONS = ["doc", "docx", "pdf", "txt"]
2 changes: 1 addition & 1 deletion platform_plugin_turnitin/edxapp_wrapper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

from platform_plugin_turnitin.edxapp_wrapper.authentication import BearerAuthenticationAllowInactiveUser
from platform_plugin_turnitin.edxapp_wrapper.course_overviews import get_course_overview_or_none
from platform_plugin_turnitin.edxapp_wrapper.student import CourseInstructorRole, CourseStaffRole
from platform_plugin_turnitin.edxapp_wrapper.student import CourseInstructorRole, CourseStaffRole, user_by_anonymous_id
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Student definitions for Open edX Quince release.
"""

from common.djangoapps.student.models.user import user_by_anonymous_id # pylint: disable=import-error, unused-import
from common.djangoapps.student.roles import ( # pylint: disable=import-error, unused-import
CourseInstructorRole,
CourseStaffRole,
Expand Down
10 changes: 10 additions & 0 deletions platform_plugin_turnitin/edxapp_wrapper/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,15 @@ def get_course_staff_role():
return backend.CourseStaffRole


def user_by_anonymous_id(*args, **kwargs):
"""
Wrapper method of `user_by_anonymous_id` in edx-platform.
"""
backend_function = settings.PLATFORM_PLUGIN_TURNITIN_STUDENT_BACKEND
backend = import_module(backend_function)

return backend.user_by_anonymous_id(*args, **kwargs)


CourseInstructorRole = get_course_instructor_role()
CourseStaffRole = get_course_staff_role()
16 changes: 16 additions & 0 deletions platform_plugin_turnitin/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Event handlers for the Turnitin plugin."""

from platform_plugin_turnitin.tasks import ora_submission_created_task


def ora_submission_created(submission, **kwargs):
"""
Handle the ORA_SUBMISSION_CREATED event.

Args:
submission (ORASubmissionData): The ORA submission data.
"""
ora_submission_created_task.delay(
submission.id,
submission.file_downloads,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2024-02-02 01:56

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("platform_plugin_turnitin", "0002_alter_turnitinsubmission_user"),
]

operations = [
migrations.AddField(
model_name="turnitinsubmission",
name="ora_submission_id",
field=models.CharField(blank=True, max_length=255, null=True),
),
]
2 changes: 2 additions & 0 deletions platform_plugin_turnitin/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class TurnitinSubmission(models.Model):

Attributes:
- user (User): The user who made the submission.
- ora_submission_id (str): The unique identifier for the submission in the Open Response Assessment (ORA) system.
- turnitin_submission_id (str): The unique identifier for the submission in Turnitin.
- turnitin_submission_pdf_id (str): The unique identifier for the PDF version of the submission in Turnitin.
- created_at (datetime): The date and time when the submission was created.
Expand All @@ -24,6 +25,7 @@ class TurnitinSubmission(models.Model):
user = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="turnitin_submissions"
)
ora_submission_id = models.CharField(max_length=255, blank=True, null=True)
turnitin_submission_id = models.CharField(max_length=255, blank=True, null=True)
turnitin_submission_pdf_id = models.CharField(max_length=255, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
120 changes: 120 additions & 0 deletions platform_plugin_turnitin/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""This module contains the tasks that will be run by celery."""

import tempfile
from logging import getLogger
from typing import List
from urllib.parse import urljoin

import requests
from celery import shared_task
from django.conf import settings
from submissions import api as submissions_api

from platform_plugin_turnitin.api.v1.views import TurnitinClient
from platform_plugin_turnitin.constants import ALLOWED_FILE_EXTENSIONS
from platform_plugin_turnitin.edxapp_wrapper import user_by_anonymous_id

log = getLogger(__name__)


@shared_task
def ora_submission_created_task(submission_id: str, file_downloads: List[dict]) -> None:
"""
Task to handle the creation of a new ora submission.

Args:
submission_id (str): The ORA submission ID.
file_downloads (List[dict]): The list of file downloads.
"""
submission_data = dict(submissions_api.get_submission_and_student(submission_id))
user = user_by_anonymous_id(submission_data["student_item"]["student_id"])

send_text_to_turnitin(submission_id, user, submission_data["answer"])
send_uploaded_files_to_turnitin(submission_id, user, file_downloads)
BryanttV marked this conversation as resolved.
Show resolved Hide resolved


def send_text_to_turnitin(submission_id: str, user, answer: dict) -> None:
"""
Task to send text to Turnitin.

Args:
submission_id (str): The ORA submission ID.
user (User): The user who made the submission.
answer (dict): The answer of the submission.
"""
for part in answer["parts"]:
text_content = part.get("text").encode("utf-8")
send_file_to_turnitin(submission_id, user, text_content, "response.txt")


def send_uploaded_files_to_turnitin(
submission_id: str, user, file_downloads: List[dict]
) -> None:
"""
Task to send uploaded files to Turnitin.

Args:
submission_id (str): The ORA submission ID.
user (User): The user who made the submission.
file_downloads (List[dict]): The list of file downloads.
"""
base_url = getattr(settings, "LMS_ROOT_URL", "")

for file in file_downloads:
filename = file.get("name", "")
file_extension = filename.split(".")[-1]
if file_extension in ALLOWED_FILE_EXTENSIONS:
file_link = urljoin(base_url, file.get("download_url"))
response = requests.get(file_link, timeout=5)

if response.ok:
send_file_to_turnitin(submission_id, user, response.content, filename)
else:
raise Exception(f"Failed to download file from {file_link}")
else:
log.info(
f"Skipping uploading file [{filename}] because it has not an allowed extension."
)


def send_file_to_turnitin(
submission_id: str, user, file_content: bytes, filename: str
) -> None:
"""
Send a file to Turnitin.

Create a temporary file with the content and upload it to Turnitin
creating a new submission.

Args:
submission_id (str): The ORA submission ID.
user (User): The user who made the submission.
file_content (bytes): The content of the file.
filename (str): The name of the file.
"""
with tempfile.NamedTemporaryFile() as temp_file:
temp_file.write(file_content)
temp_file.seek(0)
temp_file.name = filename
upload_turnitin_submission(submission_id, user, temp_file)


def upload_turnitin_submission(submission_id: str, user, file) -> None:
"""
Create a new submission in Turnitin.

First, the user must accept the EULA agreement. Then, the file is uploaded to Turnitin.

Args:
submission_id (str): The ORA submission ID.
user (User): The user who made the submission.
file (File): The file to upload.
"""
turnitin_client = TurnitinClient(user, file)

agreement_response = turnitin_client.accept_eula_agreement()

if not agreement_response.ok:
raise Exception("Failed to accept the EULA agreement.")

turnitin_client.upload_turnitin_submission_file(submission_id)
4 changes: 4 additions & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ Django # Web application framework
requests # HTTP requests
djangorestframework # RESTful API framework
edx-drf-extensions # Extensions to Django REST Framework for edX
celery # Asynchronous task queue
edx-submissions # Submissions API for edX
attrs # Classes without boilerplate
openedx-events @ git+https://github.com/edunext/openedx-events.git@9.4.0/edues # Open edX event tracking
Loading
Loading