From db586633301911bad5d14e4197e064aa347d208e Mon Sep 17 00:00:00 2001 From: Fernando Gonzalez Date: Tue, 30 Jan 2024 15:14:58 -0500 Subject: [PATCH] feat: Turnitin API workflow test with openedx XBlock (#4) --- Makefile | 2 +- platform_plugin_turnitin/apps.py | 9 + .../migrations/0001_initial.py | 47 +++ .../0002_alter_turnitinsubmission_user.py | 24 ++ .../migrations/__init__.py | 0 platform_plugin_turnitin/models.py | 26 ++ platform_plugin_turnitin/settings/common.py | 39 +- .../settings/production.py | 25 +- .../static/css/turnitin.css | 225 ++++++++++ platform_plugin_turnitin/static/html/cms.html | 1 + .../static/html/turnitin.html | 63 +++ .../static/js/src/turnitin.js | 269 ++++++++++++ platform_plugin_turnitin/turnitin.py | 389 ++++++++++++++++++ .../turnitin_client/__init__.py | 0 .../turnitin_client/handlers/__init__.py | 17 + .../turnitin_client/handlers/api_handler.py | 99 +++++ .../turnitin_client/handlers/eula.py | 39 ++ .../handlers/similarity_reports.py | 69 ++++ .../turnitin_client/handlers/submissions.py | 61 +++ requirements/base.in | 4 +- requirements/base.txt | 82 +++- requirements/dev.txt | 104 ++++- requirements/doc.txt | 105 ++++- requirements/quality.txt | 108 ++++- requirements/test.txt | 115 +++++- setup.py | 6 + 26 files changed, 1915 insertions(+), 13 deletions(-) create mode 100644 platform_plugin_turnitin/migrations/0001_initial.py create mode 100644 platform_plugin_turnitin/migrations/0002_alter_turnitinsubmission_user.py create mode 100644 platform_plugin_turnitin/migrations/__init__.py create mode 100644 platform_plugin_turnitin/static/css/turnitin.css create mode 100644 platform_plugin_turnitin/static/html/cms.html create mode 100644 platform_plugin_turnitin/static/html/turnitin.html create mode 100644 platform_plugin_turnitin/static/js/src/turnitin.js create mode 100644 platform_plugin_turnitin/turnitin.py create mode 100644 platform_plugin_turnitin/turnitin_client/__init__.py create mode 100644 platform_plugin_turnitin/turnitin_client/handlers/__init__.py create mode 100644 platform_plugin_turnitin/turnitin_client/handlers/api_handler.py create mode 100644 platform_plugin_turnitin/turnitin_client/handlers/eula.py create mode 100644 platform_plugin_turnitin/turnitin_client/handlers/similarity_reports.py create mode 100644 platform_plugin_turnitin/turnitin_client/handlers/submissions.py diff --git a/Makefile b/Makefile index 30bed1c..ef180c9 100644 --- a/Makefile +++ b/Makefile @@ -67,8 +67,8 @@ test: clean ## run tests in the current virtualenv pytest format: ## Format code automatically - isort $(SOURCES) black $(BLACK_OPTS) + isort $(SOURCES) diff_cover: test ## find diff lines that need test coverage diff-cover coverage.xml diff --git a/platform_plugin_turnitin/apps.py b/platform_plugin_turnitin/apps.py index aeb61ac..e4acc1e 100644 --- a/platform_plugin_turnitin/apps.py +++ b/platform_plugin_turnitin/apps.py @@ -26,3 +26,12 @@ class PlatformPluginTurnitinConfig(AppConfig): }, }, } + + def ready(self) -> None: + """ + Perform application initialization once the Django platform has been initialized. + """ + super().ready() + from platform_plugin_turnitin.turnitin import ( # no-qa pylint: disable=import-outside-toplevel,unused-import + TurnitinXBlock, + ) diff --git a/platform_plugin_turnitin/migrations/0001_initial.py b/platform_plugin_turnitin/migrations/0001_initial.py new file mode 100644 index 0000000..c987fbf --- /dev/null +++ b/platform_plugin_turnitin/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 3.2.21 on 2023-10-04 20:37 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="TurnitinSubmission", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "turnitin_submission_id", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "turnitin_submission_pdf_id", + models.CharField(blank=True, max_length=255, null=True), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="submissions", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/platform_plugin_turnitin/migrations/0002_alter_turnitinsubmission_user.py b/platform_plugin_turnitin/migrations/0002_alter_turnitinsubmission_user.py new file mode 100644 index 0000000..1c76869 --- /dev/null +++ b/platform_plugin_turnitin/migrations/0002_alter_turnitinsubmission_user.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.20 on 2023-10-06 19:16 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("platform_plugin_turnitin", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="turnitinsubmission", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="turnitin_submissions", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/platform_plugin_turnitin/migrations/__init__.py b/platform_plugin_turnitin/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/platform_plugin_turnitin/models.py b/platform_plugin_turnitin/models.py index be4ae84..a2f1d60 100644 --- a/platform_plugin_turnitin/models.py +++ b/platform_plugin_turnitin/models.py @@ -1,3 +1,29 @@ """ Database models for platform_plugin_turnitin. """ + +from django.contrib.auth import get_user_model +from django.db import models + +User = get_user_model() + + +class TurnitinSubmission(models.Model): + """ + Represents a submission to Turnitin. + + Attributes: + - user (User): The user who made the submission. + - 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. + + .. no_pii: + """ + + user = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="turnitin_submissions" + ) + 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) diff --git a/platform_plugin_turnitin/settings/common.py b/platform_plugin_turnitin/settings/common.py index 5c9f08c..1a81117 100644 --- a/platform_plugin_turnitin/settings/common.py +++ b/platform_plugin_turnitin/settings/common.py @@ -30,8 +30,45 @@ USE_TZ = True -def plugin_settings(settings): # pylint: disable=unused-argument +def plugin_settings(settings): """ Set of plugin settings used by the Open Edx platform. More info: https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/plugins/README.rst """ + + # Configuration variables + settings.TURNITIN_TII_API_URL = None + settings.TURNITIN_TCA_INTEGRATION_FAMILY = None + settings.TURNITIN_TCA_INTEGRATION_VERSION = None + settings.TURNITIN_TCA_API_KEY = None + settings.TURNITIN_SIMILARY_REPORT_PAYLOAD = { + "indexing_settings": {"add_to_index": True}, + "generation_settings": { + "search_repositories": [ + "INTERNET", + "SUBMITTED_WORK", + "PUBLICATION", + "CROSSREF", + "CROSSREF_POSTED_CONTENT", + ], + "submission_auto_excludes": [], + "auto_exclude_self_matching_scope": "ALL", + "priority": "HIGH", + }, + "view_settings": { + "exclude_quotes": True, + "exclude_bibliography": True, + "exclude_citations": False, + "exclude_abstract": False, + "exclude_methods": False, + "exclude_custom_sections": False, + "exclude_preprints": False, + "exclude_small_matches": 8, + "exclude_internet": False, + "exclude_publications": False, + "exclude_crossref": False, + "exclude_crossref_posted_content": False, + "exclude_submitted_works": False, + }, + } + settings.TURNITIN_API_TIMEOUT = 30 diff --git a/platform_plugin_turnitin/settings/production.py b/platform_plugin_turnitin/settings/production.py index f029aa9..d861b05 100644 --- a/platform_plugin_turnitin/settings/production.py +++ b/platform_plugin_turnitin/settings/production.py @@ -3,8 +3,31 @@ """ -def plugin_settings(settings): # pylint: disable=unused-argument +def plugin_settings(settings): """ Set of plugin settings used by the Open Edx platform. More info: https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/plugins/README.rst """ + settings.TURNITIN_TII_API_URL = getattr(settings, "ENV_TOKENS", {}).get( + "TURNITIN_TII_API_URL", settings.TURNITIN_TII_API_URL + ) + + settings.TURNITIN_TCA_INTEGRATION_FAMILY = getattr(settings, "ENV_TOKENS", {}).get( + "TURNITIN_TCA_INTEGRATION_FAMILY", settings.TURNITIN_TCA_INTEGRATION_FAMILY + ) + + settings.TURNITIN_TCA_INTEGRATION_VERSION = getattr(settings, "ENV_TOKENS", {}).get( + "TURNITIN_TCA_INTEGRATION_VERSION", settings.TURNITIN_TCA_INTEGRATION_VERSION + ) + + settings.TURNITIN_TCA_API_KEY = getattr(settings, "ENV_TOKENS", {}).get( + "TURNITIN_TCA_API_KEY", settings.TURNITIN_TCA_API_KEY + ) + + settings.TURNITIN_SIMILARY_REPORT_PAYLOAD = getattr(settings, "ENV_TOKENS", {}).get( + "TURNITIN_SIMILARY_REPORT_PAYLOAD", settings.TURNITIN_SIMILARY_REPORT_PAYLOAD + ) + + settings.TURNITIN_API_TIMEOUT = getattr(settings, "ENV_TOKENS", {}).get( + "TURNITIN_API_TIMEOUT", settings.TURNITIN_API_TIMEOUT + ) diff --git a/platform_plugin_turnitin/static/css/turnitin.css b/platform_plugin_turnitin/static/css/turnitin.css new file mode 100644 index 0000000..53a583b --- /dev/null +++ b/platform_plugin_turnitin/static/css/turnitin.css @@ -0,0 +1,225 @@ +/* CSS for TurnitinXBlock */ + +.turnitin_block .count { + font-weight: bold; +} + +.turnitin_block p { + cursor: pointer; +} + + +/* Estilo base para todas las secciones */ +.turnitin-test-section { + padding: 20px; + margin-bottom: 20px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + box-sizing: border-box; + min-height: 200px; +} +/* Estilo para las secciones pares (gris claro) */ +.turnitin-test-section:nth-child(even) { + background-color: #f4f4f4; /* Gris claro */ +} + +/* Estilo para las secciones impares (blanco) */ +.turnitin-test-section:nth-child(odd) { + background-color: #ffffff; /* Blanco */ +} + + +.file-upload-container { + background-color: #ffffff; + padding: 20px 30px; + border-radius: 8px; + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08); + max-width: 400px; + width: 100%; + text-align: center; +} + +h2 { + margin-bottom: 20px; + color: #333; +} + +.file-upload { + position: relative; + display: block; + margin: 20px 0; +} + +.file-upload input[type="file"] { + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + opacity: 0; + cursor: pointer; +} + +.file-upload label { + padding: 10px 25px; + background-color: #4CAF50; + color: #fff; + border-radius: 5px; + transition: background-color 0.3s ease, transform 0.3s ease, color 0.3s ease; + cursor: pointer; + transform: scale(1); +} + +.file-upload label.file-selected { + background-color: #808080; /* Gris */ + transform: scale(1.05); +} + +/* Este selector garantiza que el cambio a rojo solo ocurra cuando se ha seleccionado un archivo */ +.file-upload label.file-selected:hover { + background-color: #00244d; /* Cambio a rojo al pasar el cursor */ + color: #FFFFFF; /* Letras blancas */ +} + +.selected-filename { + display: block; + margin-top: 10px; + font-size: 14px; + color: #555; + font-weight: bold; +} + + +button { + padding: 10px 25px; + background-color: #4CAF50; + color: #fff; + border: none; + border-radius: 5px; + transition: background-color 0.3s ease; + cursor: pointer; +} + +/* CSS PARA EL MODAL EULA */ +.modal { + display: none; + position: fixed; + top: 10%; + left: 10%; + width: 80%; + min-height: 300px; + max-height: 500px; + overflow-y: auto; + background-color: white; + z-index: 1000; + border: 1px solid #ccc; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} +.modal-content { + margin-top: 0; + padding-top: 20px; /* Ajusta segĂșn lo necesario */ +} + +/* CSS PARA STATUS*/ +.status-text { + font-size: 16px; + margin-top: 10px; + font-weight: bold; +} + +.red-text { + color: red; +} + +.yellow-text { + color: rgb(168, 168, 14); +} + +.green-text { + color: green; +} + + +.traffic-light { + width: 50px; + height: 150px; + background-color: #333; + border-radius: 15px; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 10px 0; + margin: 0 auto; +} + +.light { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: #111; +} + +.red { + background-color: rgb(255, 0, 0); +} + +.yellow { + background-color: #ffee00; +} + +.green { + background-color: #00fb00; +} + +.off { + opacity: 0.3; +} + +.refresh-btn { + margin-top: 20px; + padding: 10px 20px; + border: none; + border-radius: 5px; + background-color: #0077CC; + color: #fff; + cursor: pointer; + font-size: 16px; + outline: none; + transition: background-color 0.3s; +} + +.refresh-btn:hover { + background-color: #0055AA; +} + + +.generate-btn { + margin-top: 10px; + padding: 10px 20px; + border: none; + border-radius: 5px; + background-color: #444; + color: #aaa; + cursor: not-allowed; + font-size: 16px; + outline: none; + transition: background-color 0.3s; +} + +.generate-btn.enabled { + background-color: #0077CC; + color: #fff; + cursor: pointer; +} + +.generate-btn.enabled:hover { + background-color: #0055AA; +} + +/* CSS PARA EL TEXT ASSESSMENT */ +#textsubmission{ + width: 400px; + height: 250px; +} diff --git a/platform_plugin_turnitin/static/html/cms.html b/platform_plugin_turnitin/static/html/cms.html new file mode 100644 index 0000000..cc9fe1b --- /dev/null +++ b/platform_plugin_turnitin/static/html/cms.html @@ -0,0 +1 @@ +

HOLA MUNDO

diff --git a/platform_plugin_turnitin/static/html/turnitin.html b/platform_plugin_turnitin/static/html/turnitin.html new file mode 100644 index 0000000..db1053c --- /dev/null +++ b/platform_plugin_turnitin/static/html/turnitin.html @@ -0,0 +1,63 @@ +
+ +
+

EULA Viewer

+ + + +
+ +
+
+

File Upload

+
+
+ + + No file selected +
+ +
+
+
+ + +
+

File Processing Status

+
+
+
+
+
+
+ + +
+ +
+

Similarity Processing Status

+
+
+
+
+
+
+ + +
+ + diff --git a/platform_plugin_turnitin/static/js/src/turnitin.js b/platform_plugin_turnitin/static/js/src/turnitin.js new file mode 100644 index 0000000..13ab409 --- /dev/null +++ b/platform_plugin_turnitin/static/js/src/turnitin.js @@ -0,0 +1,269 @@ +/* Javascript for TurnitinXBlock. */ +function TurnitinXBlock(runtime, element) { + + function updateCount(result) { + $('.count', element).text(result.count); + } + + function showEULA(htmlContent) { + $('#eulaModal .modal-content p').html(htmlContent); + $('#eulaModal').show(); + } + + function closeModal() { + $('#eulaModal', element).hide(); + } + + var handlerUrl = runtime.handlerUrl(element, 'increment_count'); + + $('p', element).click(function(eventObject) { + $.ajax({ + type: "POST", + url: handlerUrl, + data: JSON.stringify({"hello": "world"}), + success: updateCount + }); + }); + + $('#viewEULA', element).click(function() { + var handlerUrl = runtime.handlerUrl(element, 'get_eula_agreement'); + + $.ajax({ + type: "POST", + url: handlerUrl, + data: JSON.stringify({"hello": "world"}), + + success: function(response) { + if(response.status >= 400) { + alert(`ERROR: ${response.status} - No EULA page for the given version was found`); + } else { + showEULA(response.html); + } + }, + error: function() { + alert('Error getting EULA.'); + } + }); + }); + + $('#acceptEULA', element).click(function() { + var handlerUrl = runtime.handlerUrl(element, 'accept_eula_agreement'); + + $.ajax({ + type: "POST", + url: handlerUrl, + data: JSON.stringify({"hello": "world"}), + success: function(response) { + if(response.success === false && response.status >= 400) { + alert(`ERROR: ${response.status} - ${response.message}`); + } else { + alert('EULA successfully accepted!'); + } + }, + error: function() { + alert('Error accepting EULA.'); + } + }); + }); + + $('.modal-content button').click(closeModal); + + $('#uploadBtn', element).click(function(event) { + event.preventDefault(); + var handlerUrl = runtime.handlerUrl(element, 'upload_turnitin_submission_file'); + + var fileInput = $('#file')[0]; + var file = fileInput.files[0]; + + if (!file) { + alert('Please select .doc or .docx files.'); + return; + } + + var formData = new FormData(); + formData.append('uploaded_file', file); + + $.ajax({ + type: 'POST', + url: handlerUrl, + data: formData, + processData: false, + contentType: false, + success: function(response) { + if(response.success === false && response.status >= 400) { + alert(`ERROR: ${response.status} - ${response.message}`); + } else { + alert('File successfully uploaded to Turnitin!'); + } + }, + error: function() { + alert('Error uploading file.'); + } + }); + }); + + function updateSelectedFileName() { + var fileName = $(this).val().split('\\').pop(); + var fileExtension = fileName.split('.').pop().toLowerCase(); + var uploadButton = $('#uploadBtn'); + + if (fileExtension === 'doc' || fileExtension === 'docx') { + if (fileName) { + $(this).siblings('.selected-filename').text(fileName); + $(this).siblings('label').addClass('file-selected').text('Change File'); + uploadButton.prop('disabled', false); + } else { + $(this).siblings('.selected-filename').text('No file selected'); + $(this).siblings('label').removeClass('file-selected').text('Choose File'); + uploadButton.prop('disabled', true); + } + } else { + alert('Please, select .doc or .docx files'); + $(this).val(''); + $(this).siblings('.selected-filename').text('No file selected'); + $(this).siblings('label').removeClass('file-selected').text('Choose File'); + uploadButton.prop('disabled', true); + } + } + $('#file', element).on('change', updateSelectedFileName); + + $('#refreshBtn1', element).click(function(event) { + event.preventDefault(); + var handlerUrl = runtime.handlerUrl(element, 'get_submission_status'); + + $.ajax({ + type: "POST", + url: handlerUrl, + data: JSON.stringify({"hello": "world"}), + success: function(response) { + if(response.success === false && response.status >= 400) { + updateTrafficLightState('1', 'ERROR'); + alert(`ERROR: ${response.status} - ${response.message}`); + } else { + updateTrafficLightState('1', response['status']); + } + }, + error: function() { + alert('Error getting report status.'); + } + }); + }); + + + $('#generateReportBtn1', element).click(function(event) { + event.preventDefault(); + var handlerUrl = runtime.handlerUrl(element, 'generate_similarity_report'); + + $.ajax({ + type: "POST", + url: handlerUrl, + data: JSON.stringify({"hello": "world"}), + success: function(response) { + if(response.success === false && response.status >= 400) { + alert(`ERROR: ${response.status} - ${response.message}`); + } else { + alert('Successfully scheduled similarity report generation.'); + } + }, + error: function() { + alert('Error in similarity report generation.'); + } + }); + }); + + + + + $('#refreshBtn2', element).click(function(event) { + event.preventDefault(); + var handlerUrl = runtime.handlerUrl(element, 'get_similarity_report_status'); + + $.ajax({ + type: "POST", + url: handlerUrl, + data: JSON.stringify({"hello": "world"}), + success: function(response) { + if(response.success === false && response.status >= 400) { + updateTrafficLightState('2', 'ERROR'); + alert(`ERROR: ${response.status} - ${response.message}`); + } else { + updateTrafficLightState('2', response['status']); + } + }, + error: function() { + alert('Error getting report status.'); + } + }); + }); + + + $('#generateReportBtn2', element).click(function(event) { + event.preventDefault(); + var handlerUrl = runtime.handlerUrl(element, 'create_similarity_viewer'); + + $.ajax({ + type: "POST", + url: handlerUrl, + data: JSON.stringify({"hello": "world"}), + success: function(response) { + if(response.success === false && response.status >= 400) { + alert(`ERROR: ${response.status} - ${response.message}`); + } else { + alert('Redirecting...'); + openInNewTab(response.viewer_url); + } + }, + error: function() { + alert('Error getting Viewer URL.'); + } + }); + }); + + + function openInNewTab(url) { + window.open(url, '_blank'); + } + + + function updateTrafficLightState(semaphoreNumber, state) { + console.log(state); + const redLight = document.getElementById(`redLight${semaphoreNumber}`); + const yellowLight = document.getElementById(`yellowLight${semaphoreNumber}`); + const greenLight = document.getElementById(`greenLight${semaphoreNumber}`); + const generateReportBtn = document.getElementById(`generateReportBtn${semaphoreNumber}`); + const statusText = document.getElementById(`statusText${semaphoreNumber}`); + const refreshBtn2 = document.getElementById(`refreshBtn2`); + + redLight.classList.add('off'); + yellowLight.classList.add('off'); + greenLight.classList.add('off'); + generateReportBtn.disabled = true; + generateReportBtn.classList.remove('enabled'); + statusText.textContent = state; + + statusText.classList.remove('red-text', 'yellow-text', 'green-text'); + + switch (state) { + case 'ERROR': + redLight.classList.remove('off'); + statusText.classList.add('red-text'); + break; + case 'PROCESSING': + yellowLight.classList.remove('off'); + statusText.classList.add('yellow-text'); + break; + case 'COMPLETE': + greenLight.classList.remove('off'); + statusText.classList.add('green-text'); + generateReportBtn.disabled = false; + generateReportBtn.classList.add('enabled'); + refreshBtn2.disabled = false; + refreshBtn2.classList.add('enabled'); + break; + default: + redLight.classList.remove('off'); + statusText.classList.add('red-text'); + } + } + +} diff --git a/platform_plugin_turnitin/turnitin.py b/platform_plugin_turnitin/turnitin.py new file mode 100644 index 0000000..2d348f3 --- /dev/null +++ b/platform_plugin_turnitin/turnitin.py @@ -0,0 +1,389 @@ +"""TO-DO: Write a description of what this XBlock is.""" + +import json +from datetime import datetime +from http import HTTPStatus + +import pkg_resources +from django.conf import settings +from django.contrib.auth import get_user_model +from django.utils import translation +from webob import Response +from xblock.core import XBlock +from xblock.fragment import Fragment +from xblockutils.resources import ResourceLoader + +from platform_plugin_turnitin.models import TurnitinSubmission +from platform_plugin_turnitin.turnitin_client.handlers import ( + get_eula_page, + get_similarity_report_info, + get_submission_info, + post_accept_eula_version, + post_create_submission, + post_create_viewer_launch_url, + put_generate_similarity_report, + put_upload_submission_file_content, +) + +User = get_user_model() + + +@XBlock.needs("user") +@XBlock.needs("user_state") +class TurnitinXBlock(XBlock): + """ + TO-DO: document what your XBlock does. + """ + + # Fields are defined on the class. You can access them in your code as + # self.. + + def resource_string(self, path): + """Handy helper for getting resources from our kit.""" + data = pkg_resources.resource_string(__name__, path) + return data.decode("utf8") + + def studio_view(self, context=None): + """ + Show primary view of the TurnitinXBlock, shown to students when viewing courses. + """ + if context: + pass # TO-DO: do something based on the context. + html = self.resource_string("static/html/cms.html") + frag = Fragment(html.format(self=self)) + frag.add_css(self.resource_string("static/css/turnitin.css")) + + # Add i18n js + statici18n_js_url = self._get_statici18n_js_url() + if statici18n_js_url: + frag.add_javascript_url( + self.runtime.local_resource_url(self, statici18n_js_url) + ) + + frag.add_javascript(self.resource_string("static/js/src/turnitin.js")) + frag.initialize_js("TurnitinXBlock") + return frag + + # TO-DO: change this view to display your data your own way. + def student_view(self, context=None): + """ + Show primary view of the TurnitinXBlock, shown to students when viewing courses. + """ + if context: + pass # TO-DO: do something based on the context. + html = self.resource_string("static/html/turnitin.html") + frag = Fragment(html.format(self=self)) + frag.add_css(self.resource_string("static/css/turnitin.css")) + + # Add i18n js + statici18n_js_url = self._get_statici18n_js_url() + if statici18n_js_url: + frag.add_javascript_url( + self.runtime.local_resource_url(self, statici18n_js_url) + ) + + frag.add_javascript(self.resource_string("static/js/src/turnitin.js")) + frag.initialize_js("TurnitinXBlock") + return frag + + def get_user_data(self): + """ + Fetch user-related data, including user ID, email, and name. + + Returns: + dict: A dictionary containing user ID, email, and name. + """ + user_service = self.runtime.service(self, "user") + current_user = user_service.get_current_user() + full_name = current_user.full_name.split() + return { + "user_id": current_user.opt_attrs["edx-platform.user_id"], + "user_email": current_user.emails[0], + "name": full_name[0] if full_name else "no_name", + "last_name": " ".join(full_name[1:]) + if len(full_name) > 1 + else "no_last_name", + } + + def get_django_user(self): + """ + Return the django user. + """ + current_user_id = self.get_user_data()["user_id"] + return User.objects.get(id=current_user_id) + + @XBlock.json_handler + def get_eula_agreement(self, data, suffix=""): # pylint: disable=unused-argument + """ + Fetch the End User License Agreement (EULA) content. + + Args: + data (dict): Input data for the request. + suffix (str, optional): Additional suffix for the request. Defaults to ''. + + Returns: + dict: A dictionary containing the HTML content of the EULA and the status code. + """ + response = get_eula_page() + return {"html": response.text, "status": response.status_code} + + @XBlock.json_handler + def accept_eula_agreement(self, data, suffix=""): # pylint: disable=unused-argument + """ + Submit acceptance of the EULA for the current user. + + Args: + data (dict): Input data for the request. + suffix (str, optional): Additional suffix for the request. Defaults to ''. + + Returns: + dict: The response after accepting the EULA. + """ + user_id = self.get_user_data()["user_id"] + date_now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + payload = { + "user_id": str(user_id), + "accepted_timestamp": date_now, + "language": "en-US", + } + response = post_accept_eula_version(payload) + return response.json() + + def create_turnitin_submission_object(self): + """ + Create a Turnitin submission object based on the user's data. + + Returns: + Response: The response from the Turnitin submission API. + """ + user_data = self.get_user_data() + date_now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + + payload = { + "owner": user_data["user_id"], + "title": self.location.block_id, # pylint: disable=no-member + "submitter": user_data["user_id"], + "owner_default_permission_set": "LEARNER", + "submitter_default_permission_set": "INSTRUCTOR", + "extract_text_only": False, + "metadata": { + "owners": [ + { + "id": user_data["user_id"], + "given_name": user_data["name"], + "family_name": user_data["last_name"], + "email": user_data["user_email"], + } + ], + "submitter": { + "id": user_data["user_id"], + "given_name": user_data["name"], + "family_name": user_data["last_name"], + "email": user_data["user_email"], + }, + "original_submitted_time": date_now, + }, + } + return post_create_submission(payload) + + @XBlock.handler + def upload_turnitin_submission_file( + self, data, suffix="" + ): # pylint: disable=unused-argument + """ + Handle the upload of the user's file to Turnitin. + + Args: + data (WebRequest): Web request containing the file to be uploaded. + suffix (str, optional): Additional suffix for the request. Defaults to ''. + + Returns: + Response: The response after uploading the file to Turnitin. + """ + turnitin_submission = self.create_turnitin_submission_object() + if turnitin_submission.status_code == HTTPStatus.CREATED: + turnitin_submission_id = turnitin_submission.json()["id"] + current_user = self.get_django_user() + submission = TurnitinSubmission( + user=current_user, turnitin_submission_id=turnitin_submission_id + ) + submission.save() + uploaded_file = data.params["uploaded_file"].file + response = put_upload_submission_file_content( + turnitin_submission_id, uploaded_file + ) + return Response( + json.dumps(response.json()), + ) + return Response( + json.dumps(turnitin_submission.json()), + ) + + @XBlock.json_handler + def get_submission_status(self, data, suffix=""): # pylint: disable=unused-argument + """ + Retrieve the status of the latest Turnitin submission for the user. + + Args: + data (dict): Input data for the request. + suffix (str, optional): Additional suffix for the request. Defaults to ''. + + Returns: + dict: Information related to the user's latest Turnitin submission. + """ + current_user = self.get_django_user() + try: + last_submission = TurnitinSubmission.objects.filter( + user=current_user + ).latest("created_at") + except TurnitinSubmission.DoesNotExist: + return {"success": False} + response = get_submission_info(last_submission.turnitin_submission_id) + return response.json() + + @XBlock.json_handler + def generate_similarity_report( + self, data, suffix="" + ): # pylint: disable=unused-argument + """ + Initialize the generation of a similarity report for the user's latest Turnitin submission. + + Args: + data (dict): Input data for the request. + suffix (str, optional): Additional suffix for the request. Defaults to ''. + + Returns: + dict: The status of the similarity report generation process. + """ + payload = getattr( # pylint: disable=literal-used-as-attribute + settings, "TURNITIN_SIMILARY_REPORT_PAYLOAD" + ) + current_user = self.get_django_user() + try: + last_submission = TurnitinSubmission.objects.filter( + user=current_user + ).latest("created_at") + except TurnitinSubmission.DoesNotExist: + return {"success": False} + response = put_generate_similarity_report( + last_submission.turnitin_submission_id, payload + ) + return response.json() + + @XBlock.json_handler + def get_similarity_report_status( + self, data, suffix="" + ): # pylint: disable=unused-argument + """ + Retrieve the status of the similarity report for the user's latest Turnitin submission. + + Args: + data (dict): Input data for the request. + suffix (str, optional): Additional suffix for the request. Defaults to ''. + + Returns: + dict: Information related to the status of the similarity report. + """ + current_user = self.get_django_user() + try: + last_submission = TurnitinSubmission.objects.filter( + user=current_user + ).latest("created_at") + except TurnitinSubmission.DoesNotExist: + return {"success": False} + response = get_similarity_report_info(last_submission.turnitin_submission_id) + return response.json() + + @XBlock.json_handler + def create_similarity_viewer( + self, data, suffix="" + ): # pylint: disable=unused-argument + """ + Create a Turnitin similarity viewer for the user's latest submission. + + Args: + data (dict): Input data for the request. + suffix (str, optional): Additional suffix for the request. Defaults to ''. + + Returns: + dict: Contains the URL for the similarity viewer. + """ + user_data = self.get_user_data() + payload = { + "viewer_user_id": user_data["user_id"], + "locale": "en-EN", + "viewer_default_permission_set": "INSTRUCTOR", + "viewer_permissions": { + "may_view_submission_full_source": False, + "may_view_match_submission_info": False, + "may_view_document_details_panel": False, + }, + "similarity": { + "default_mode": "match_overview", + "modes": {"match_overview": True, "all_sources": True}, + "view_settings": {"save_changes": True}, + }, + "author_metadata_override": { + "family_name": user_data["last_name"], + "given_name": user_data["name"], + }, + "sidebar": {"default_mode": "similarity"}, + } + current_user = User.objects.get(id=user_data["user_id"]) + try: + last_submission = TurnitinSubmission.objects.filter( + user=current_user + ).latest("created_at") + except TurnitinSubmission.DoesNotExist: + return {"success": False} + response = post_create_viewer_launch_url( + last_submission.turnitin_submission_id, payload + ) + return response.json() + + @staticmethod + def workbench_scenarios(): + """Define a workbench scenarios.""" + return [ + ( + "TurnitinXBlock", + """ + """, + ), + ( + "Multiple TurnitinXBlock", + """ + + + + + """, + ), + ] + + @staticmethod + def _get_statici18n_js_url(): + """ + Return the Javascript translation file for the currently selected language, if any. + + Defaults to English if available. + """ + locale_code = translation.get_language() + if locale_code is None: + return None + text_js = "public/js/translations/{locale_code}/text.js" + lang_code = locale_code.split("-")[0] + for code in (locale_code, lang_code, "en"): + loader = ResourceLoader(__name__) + if pkg_resources.resource_exists( + loader.module_name, text_js.format(locale_code=code) + ): + return text_js.format(locale_code=code) + return None + + @staticmethod + def get_dummy(): + """ + Return a dummy translation to generate initial i18n. + """ + return translation.gettext_noop("Dummy") diff --git a/platform_plugin_turnitin/turnitin_client/__init__.py b/platform_plugin_turnitin/turnitin_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/platform_plugin_turnitin/turnitin_client/handlers/__init__.py b/platform_plugin_turnitin/turnitin_client/handlers/__init__.py new file mode 100644 index 0000000..f2ea4e8 --- /dev/null +++ b/platform_plugin_turnitin/turnitin_client/handlers/__init__.py @@ -0,0 +1,17 @@ +"""Handler module for Turnitin API integration""" +from .eula import get_eula_acceptance_by_user, get_eula_page, get_eula_version_info, post_accept_eula_version +from .similarity_reports import ( + get_similarity_report_info, + get_similarity_report_pdf, + get_similarity_report_pdf_status, + post_create_viewer_launch_url, + post_generate_similarity_report_pdf, + put_generate_similarity_report, +) +from .submissions import ( + delete_submission, + get_submission_info, + post_create_submission, + put_recover_submission, + put_upload_submission_file_content, +) diff --git a/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py b/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py new file mode 100644 index 0000000..1c6dba4 --- /dev/null +++ b/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py @@ -0,0 +1,99 @@ +""" +API handlers for turnitin integration +""" +from typing import Dict, Optional + +import requests +from django.conf import settings + +TII_API_URL = getattr(settings, "TURNITIN_TII_API_URL", None) +TCA_INTEGRATION_FAMILY = getattr(settings, "TURNITIN_TCA_INTEGRATION_FAMILY", None) +TCA_INTEGRATION_VERSION = getattr(settings, "TURNITIN_TCA_INTEGRATION_VERSION", None) +TCA_API_KEY = getattr(settings, "TURNITIN_TCA_API_KEY", None) + + +def get_request_method_func(request_method: str): + """ + Retrieve the appropriate request method function from the `requests` library + based on the provided HTTP request method. + + Parameters: + - request_method (str): The HTTP method as a string (e.g., 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'). + + Returns: + - function: The corresponding function from the `requests` library (e.g., requests.get, requests.post). + + Raises: + - ValueError: If the provided request_method is unsupported or not recognized. + """ + method_map = { + "get": requests.get, + "post": requests.post, + "put": requests.put, + "delete": requests.delete, + "patch": requests.patch, + } + method_func = method_map.get(request_method.lower()) + if not method_func: + raise ValueError(f"Unsupported request method: {request_method}") + return method_func + + +def turnitin_api_handler( + request_method: str, + url_prefix: str = "", + data: Optional[Dict] = None, + is_upload: bool = False, + uploaded_file=None, +): + """ + Handles API requests to the Turnitin service. + + Parameters: + - request_method (str): The HTTP method (e.g., 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'). + - data (dict): The payload to be sent in the request. Use None for methods that don't require a payload. + - url_prefix (str): The endpoint suffix for the API URL. + + Returns: + - Response: A requests.Response object containing the server's response to the request. + """ + headers = { + "X-Turnitin-Integration-Name": TCA_INTEGRATION_FAMILY, + "X-Turnitin-Integration-Version": TCA_INTEGRATION_VERSION, + "Authorization": f"Bearer {TCA_API_KEY}", + } + + if request_method.lower() in ["post", "put", "patch"]: + headers["Content-Type"] = "application/json" + + if is_upload: + headers["Content-Type"] = "binary/octet-stream" + headers["Content-Disposition"] = f'inline; filename="{uploaded_file.name}"' + response = requests.put( + f"{TII_API_URL}/api/v1/{url_prefix}", + headers=headers, + data=uploaded_file, + timeout=settings.TURNITIN_API_TIMEOUT, + ) + return response + + method_func = get_request_method_func(request_method) + + args = { + "headers": headers, + "json" + if request_method.lower() in ["post", "put", "patch"] + else "params": data, + } + + response = method_func(f"{TII_API_URL}/api/v1/{url_prefix}", **args) + + return response + + +def get_features_enabled(): + """ + Returns all the features enabled in the Turnitin account. + """ + response = turnitin_api_handler("get", "features-enabled") + return response diff --git a/platform_plugin_turnitin/turnitin_client/handlers/eula.py b/platform_plugin_turnitin/turnitin_client/handlers/eula.py new file mode 100644 index 0000000..07c9645 --- /dev/null +++ b/platform_plugin_turnitin/turnitin_client/handlers/eula.py @@ -0,0 +1,39 @@ +""" +EULA handlers for turnitin xblock +""" +from .api_handler import turnitin_api_handler + + +def get_eula_version_info(version: str = "latest", language: str = "EN"): + """ + Returns Turnitin's EULA (End User License Agreement) version information. + The EULA is a page of terms and conditions that both the owner and the submitter + have to accept in order to send a file to Turnitin. + """ + response = turnitin_api_handler("get", f"eula/{version}?lang={language}") + return response + + +def get_eula_page(version: str = "v1beta", language: str = "en-US"): + """ + Returns the HTML content for a specified EULA version. + """ + response = turnitin_api_handler("get", f"/eula/{version}/view?lang={language}") + return response + + +def post_accept_eula_version(payload, version: str = "v1beta"): + """ + Accepts a specific EULA version. + This method should be invoked after the user has viewed the EULA content. + """ + response = turnitin_api_handler("post", f"eula/{version}/accept", payload) + return response + + +def get_eula_acceptance_by_user(user_id): + """ + Checks if a specific user has accepted a particular EULA version. + """ + response = turnitin_api_handler("get", f"eula/v1beta/accept/{user_id}") + return response diff --git a/platform_plugin_turnitin/turnitin_client/handlers/similarity_reports.py b/platform_plugin_turnitin/turnitin_client/handlers/similarity_reports.py new file mode 100644 index 0000000..362c01a --- /dev/null +++ b/platform_plugin_turnitin/turnitin_client/handlers/similarity_reports.py @@ -0,0 +1,69 @@ +""" +Similarity reports handlers +""" +from .api_handler import turnitin_api_handler + + +def put_generate_similarity_report(submission_id, payload): + """ + Turnitin begin to process the doc to generate the report. + """ + response = turnitin_api_handler( + "put", f"submissions/{submission_id}/similarity", payload + ) + return response + + +def get_similarity_report_info(submission_id): + """ + Returns summary information about the requested Similarity Report. + + Status: + - PROCESSING + - COMPLETE + """ + response = turnitin_api_handler("get", f"submissions/{submission_id}/similarity") + return response + + +def post_create_viewer_launch_url(submission_id, payload): + """ + So that users can interact with the details of a submission and Similarity Report, + Turnitin provides a purpose-built viewer to enable smooth interaction with the + report details and submitted document. + """ + response = turnitin_api_handler( + "post", f"submissions/{submission_id}/viewer-url", payload + ) + return response + + +def post_generate_similarity_report_pdf(submission_id): + """ + This endpoint generates Similarty Report pdf and returns an ID that can be used in + a subsequent API call to download a pdf file. + """ + response = turnitin_api_handler( + "post", f"submissions/{submission_id}/similarity/pdf" + ) + return response + + +def get_similarity_report_pdf(submission_id, pdf_id): + """ + This endpoint returns the Similarity Report pdf file as stream of bytes. + """ + response = turnitin_api_handler( + "get", f"submissions/{submission_id}/similarity/pdf/{pdf_id}" + ) + return response + + +def get_similarity_report_pdf_status(submission_id, pdf_id): + """ + This endpoint returns the requested Similarity Report pdf status. + """ + response = turnitin_api_handler( + "get", f"submissions/{submission_id}/similarity/pdf/{pdf_id}/status" + ) + return response diff --git a/platform_plugin_turnitin/turnitin_client/handlers/submissions.py b/platform_plugin_turnitin/turnitin_client/handlers/submissions.py new file mode 100644 index 0000000..90de133 --- /dev/null +++ b/platform_plugin_turnitin/turnitin_client/handlers/submissions.py @@ -0,0 +1,61 @@ +""" +Submissions hanlders +""" +from .api_handler import turnitin_api_handler + + +def post_create_submission(payload): + """ + Creates a submission object in Turnitin and returns an associated ID. + This relates to the Turnitin model which contains all information + related to an assessment sent by a student. + """ + response = turnitin_api_handler("post", "submissions", payload) + return response + + +def put_upload_submission_file_content(submission_id, file): + """ + Attaches a document to a student's submission. + """ + response = turnitin_api_handler( + "put", + f"submissions/{submission_id}/original", + is_upload=True, + uploaded_file=file, + ) + return response + + +def get_submission_info(submission_id): + """ + Fetches all the information related to a specific submission. + + Status: + CREATED Submission has been created but no file has been uploaded + PROCESSING File contents have been uploaded and the submission is being processed + COMPLETE Submission processing is complete + ERROR An error occurred during submission processing; see error_code for details + + """ + response = turnitin_api_handler("get", f"submissions/{submission_id}") + return response + + +def delete_submission(submission_id, is_hard_delete="false"): + """ + Deletes a submission by its ID. + The deletion can either be a hard delete or a soft delete based on the parameter provided. + """ + response = turnitin_api_handler( + "delete", f"submissions/{submission_id}/?hard={is_hard_delete}" + ) + return response + + +def put_recover_submission(submission_id): + """ + Recovers a submission that has been soft deleted + """ + response = turnitin_api_handler("put", f"submissions/{submission_id}/recover") + return response diff --git a/requirements/base.in b/requirements/base.in index a954780..c8fa182 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -2,4 +2,6 @@ -c constraints.txt Django # Web application framework - +xblock +xblock-utils +requests diff --git a/requirements/base.txt b/requirements/base.txt index 81ee185..dada3f5 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,15 +4,95 @@ # # make upgrade # +appdirs==1.4.4 + # via fs asgiref==3.7.2 # via django +boto3==1.28.62 + # via fs-s3fs +botocore==1.31.62 + # via + # boto3 + # s3transfer +certifi==2023.7.22 + # via requests +charset-normalizer==3.3.0 + # via requests django==3.2.22 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.in + # openedx-django-pyfs +fs==2.4.16 + # via + # fs-s3fs + # openedx-django-pyfs + # xblock +fs-s3fs==1.1.1 + # via openedx-django-pyfs +idna==3.4 + # via requests +jmespath==1.0.1 + # via + # boto3 + # botocore +lazy==1.6 + # via xblock +lxml==4.9.3 + # via xblock +mako==1.2.4 + # via + # xblock + # xblock-utils +markupsafe==2.1.3 + # via + # mako + # xblock +openedx-django-pyfs==3.4.0 + # via xblock +python-dateutil==2.8.2 + # via + # botocore + # xblock pytz==2023.3.post1 - # via django + # via + # django + # xblock +pyyaml==6.0.1 + # via xblock +requests==2.31.0 + # via -r requirements/base.in +s3transfer==0.7.0 + # via boto3 +simplejson==3.19.2 + # via + # xblock + # xblock-utils +six==1.16.0 + # via + # fs + # fs-s3fs + # python-dateutil sqlparse==0.4.4 # via django typing-extensions==4.8.0 # via asgiref +urllib3==1.26.17 + # via + # botocore + # requests +web-fragments==2.1.0 + # via + # xblock + # xblock-utils +webob==1.8.7 + # via xblock +xblock[django]==1.8.1 + # via + # -r requirements/base.in + # xblock-utils +xblock-utils==4.0.0 + # via -r requirements/base.in + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/dev.txt b/requirements/dev.txt index 55e9f02..f727f18 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,6 +4,10 @@ # # make upgrade # +appdirs==1.4.4 + # via + # -r requirements/quality.txt + # fs asgiref==3.7.2 # via # -r requirements/quality.txt @@ -15,12 +19,29 @@ astroid==2.15.8 # pylint-celery black==23.9.1 # via -r requirements/dev.in +boto3==1.28.62 + # via + # -r requirements/quality.txt + # fs-s3fs +botocore==1.31.62 + # via + # -r requirements/quality.txt + # boto3 + # s3transfer build==1.0.3 # via # -r requirements/pip-tools.txt # pip-tools +certifi==2023.7.22 + # via + # -r requirements/quality.txt + # requests chardet==5.2.0 # via diff-cover +charset-normalizer==3.3.0 + # via + # -r requirements/quality.txt + # requests click==8.1.7 # via # -r requirements/pip-tools.txt @@ -57,7 +78,8 @@ django==3.2.22 # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/quality.txt # edx-i18n-tools -edx-i18n-tools==1.2.0 + # openedx-django-pyfs +edx-i18n-tools==1.3.0 # via -r requirements/dev.in edx-lint==5.3.4 # via -r requirements/quality.txt @@ -70,6 +92,20 @@ filelock==3.12.4 # -r requirements/ci.txt # tox # virtualenv +fs==2.4.16 + # via + # -r requirements/quality.txt + # fs-s3fs + # openedx-django-pyfs + # xblock +fs-s3fs==1.1.1 + # via + # -r requirements/quality.txt + # openedx-django-pyfs +idna==3.4 + # via + # -r requirements/quality.txt + # requests importlib-metadata==6.8.0 # via # -r requirements/pip-tools.txt @@ -87,20 +123,45 @@ jinja2==3.1.2 # -r requirements/quality.txt # code-annotations # diff-cover +jmespath==1.0.1 + # via + # -r requirements/quality.txt + # boto3 + # botocore +lazy==1.6 + # via + # -r requirements/quality.txt + # xblock lazy-object-proxy==1.9.0 # via # -r requirements/quality.txt # astroid +lxml==4.9.3 + # via + # -r requirements/quality.txt + # edx-i18n-tools + # xblock +mako==1.2.4 + # via + # -r requirements/quality.txt + # xblock + # xblock-utils markupsafe==2.1.3 # via # -r requirements/quality.txt # jinja2 + # mako + # xblock mccabe==0.7.0 # via # -r requirements/quality.txt # pylint mypy-extensions==1.0.0 # via black +openedx-django-pyfs==3.4.0 + # via + # -r requirements/quality.txt + # xblock packaging==23.2 # via # -r requirements/ci.txt @@ -179,6 +240,11 @@ pytest-cov==4.1.0 # via -r requirements/quality.txt pytest-django==4.5.2 # via -r requirements/quality.txt +python-dateutil==2.8.2 + # via + # -r requirements/quality.txt + # botocore + # xblock python-slugify==8.0.1 # via # -r requirements/quality.txt @@ -187,16 +253,32 @@ pytz==2023.3.post1 # via # -r requirements/quality.txt # django + # xblock pyyaml==6.0.1 # via # -r requirements/quality.txt # code-annotations # edx-i18n-tools + # xblock +requests==2.31.0 + # via -r requirements/quality.txt +s3transfer==0.7.0 + # via + # -r requirements/quality.txt + # boto3 +simplejson==3.19.2 + # via + # -r requirements/quality.txt + # xblock + # xblock-utils six==1.16.0 # via # -r requirements/ci.txt # -r requirements/quality.txt # edx-lint + # fs + # fs-s3fs + # python-dateutil # tox snowballstemmer==2.2.0 # via @@ -245,10 +327,24 @@ typing-extensions==4.8.0 # astroid # black # pylint +urllib3==1.26.17 + # via + # -r requirements/quality.txt + # botocore + # requests virtualenv==20.24.5 # via # -r requirements/ci.txt # tox +web-fragments==2.1.0 + # via + # -r requirements/quality.txt + # xblock + # xblock-utils +webob==1.8.7 + # via + # -r requirements/quality.txt + # xblock wheel==0.41.2 # via # -r requirements/pip-tools.txt @@ -257,6 +353,12 @@ wrapt==1.15.0 # via # -r requirements/quality.txt # astroid +xblock[django]==1.8.1 + # via + # -r requirements/quality.txt + # xblock-utils +xblock-utils==4.0.0 + # via -r requirements/quality.txt zipp==3.17.0 # via # -r requirements/pip-tools.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index 7f770d4..8d94fb3 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -8,6 +8,10 @@ accessible-pygments==0.0.4 # via pydata-sphinx-theme alabaster==0.7.13 # via sphinx +appdirs==1.4.4 + # via + # -r requirements/test.txt + # fs asgiref==3.7.2 # via # -r requirements/test.txt @@ -18,14 +22,27 @@ babel==2.13.0 # sphinx beautifulsoup4==4.12.2 # via pydata-sphinx-theme +boto3==1.28.62 + # via + # -r requirements/test.txt + # fs-s3fs +botocore==1.31.62 + # via + # -r requirements/test.txt + # boto3 + # s3transfer build==1.0.3 # via -r requirements/doc.in certifi==2023.7.22 - # via requests + # via + # -r requirements/test.txt + # requests cffi==1.16.0 # via cryptography charset-normalizer==3.3.0 - # via requests + # via + # -r requirements/test.txt + # requests click==8.1.7 # via # -r requirements/test.txt @@ -42,6 +59,7 @@ django==3.2.22 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt + # openedx-django-pyfs doc8==1.1.1 # via -r requirements/doc.in docutils==0.19 @@ -55,8 +73,20 @@ exceptiongroup==1.1.3 # via # -r requirements/test.txt # pytest +fs==2.4.16 + # via + # -r requirements/test.txt + # fs-s3fs + # openedx-django-pyfs + # xblock +fs-s3fs==1.1.1 + # via + # -r requirements/test.txt + # openedx-django-pyfs idna==3.4 - # via requests + # via + # -r requirements/test.txt + # requests imagesize==1.4.1 # via sphinx importlib-metadata==6.8.0 @@ -82,20 +112,44 @@ jinja2==3.1.2 # -r requirements/test.txt # code-annotations # sphinx +jmespath==1.0.1 + # via + # -r requirements/test.txt + # boto3 + # botocore keyring==24.2.0 # via twine +lazy==1.6 + # via + # -r requirements/test.txt + # xblock +lxml==4.9.3 + # via + # -r requirements/test.txt + # xblock +mako==1.2.4 + # via + # -r requirements/test.txt + # xblock + # xblock-utils markdown-it-py==3.0.0 # via rich markupsafe==2.1.3 # via # -r requirements/test.txt # jinja2 + # mako + # xblock mdurl==0.1.2 # via markdown-it-py more-itertools==10.1.0 # via jaraco-classes nh3==0.2.14 # via readme-renderer +openedx-django-pyfs==3.4.0 + # via + # -r requirements/test.txt + # xblock packaging==23.2 # via # -r requirements/test.txt @@ -136,6 +190,11 @@ pytest-cov==4.1.0 # via -r requirements/test.txt pytest-django==4.5.2 # via -r requirements/test.txt +python-dateutil==2.8.2 + # via + # -r requirements/test.txt + # botocore + # xblock python-slugify==8.0.1 # via # -r requirements/test.txt @@ -145,14 +204,17 @@ pytz==2023.3.post1 # -r requirements/test.txt # babel # django + # xblock pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations + # xblock readme-renderer==42.0 # via twine requests==2.31.0 # via + # -r requirements/test.txt # requests-toolbelt # sphinx # twine @@ -164,8 +226,23 @@ rfc3986==2.0.0 # via twine rich==13.6.0 # via twine +s3transfer==0.7.0 + # via + # -r requirements/test.txt + # boto3 secretstorage==3.3.3 # via keyring +simplejson==3.19.2 + # via + # -r requirements/test.txt + # xblock + # xblock-utils +six==1.16.0 + # via + # -r requirements/test.txt + # fs + # fs-s3fs + # python-dateutil snowballstemmer==2.2.0 # via sphinx soupsieve==2.5 @@ -218,11 +295,31 @@ typing-extensions==4.8.0 # asgiref # pydata-sphinx-theme # rich -urllib3==2.0.6 +urllib3==1.26.17 # via + # -r requirements/test.txt + # botocore # requests # twine +web-fragments==2.1.0 + # via + # -r requirements/test.txt + # xblock + # xblock-utils +webob==1.8.7 + # via + # -r requirements/test.txt + # xblock +xblock[django]==1.8.1 + # via + # -r requirements/test.txt + # xblock-utils +xblock-utils==4.0.0 + # via -r requirements/test.txt zipp==3.17.0 # via # importlib-metadata # importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/quality.txt b/requirements/quality.txt index c27dfd7..837efd6 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -4,6 +4,10 @@ # # make upgrade # +appdirs==1.4.4 + # via + # -r requirements/test.txt + # fs asgiref==3.7.2 # via # -r requirements/test.txt @@ -12,6 +16,23 @@ astroid==2.15.8 # via # pylint # pylint-celery +boto3==1.28.62 + # via + # -r requirements/test.txt + # fs-s3fs +botocore==1.31.62 + # via + # -r requirements/test.txt + # boto3 + # s3transfer +certifi==2023.7.22 + # via + # -r requirements/test.txt + # requests +charset-normalizer==3.3.0 + # via + # -r requirements/test.txt + # requests click==8.1.7 # via # -r requirements/test.txt @@ -34,12 +55,27 @@ django==3.2.22 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt + # openedx-django-pyfs edx-lint==5.3.4 # via -r requirements/quality.in exceptiongroup==1.1.3 # via # -r requirements/test.txt # pytest +fs==2.4.16 + # via + # -r requirements/test.txt + # fs-s3fs + # openedx-django-pyfs + # xblock +fs-s3fs==1.1.1 + # via + # -r requirements/test.txt + # openedx-django-pyfs +idna==3.4 + # via + # -r requirements/test.txt + # requests iniconfig==2.0.0 # via # -r requirements/test.txt @@ -52,14 +88,38 @@ jinja2==3.1.2 # via # -r requirements/test.txt # code-annotations +jmespath==1.0.1 + # via + # -r requirements/test.txt + # boto3 + # botocore +lazy==1.6 + # via + # -r requirements/test.txt + # xblock lazy-object-proxy==1.9.0 # via astroid +lxml==4.9.3 + # via + # -r requirements/test.txt + # xblock +mako==1.2.4 + # via + # -r requirements/test.txt + # xblock + # xblock-utils markupsafe==2.1.3 # via # -r requirements/test.txt # jinja2 + # mako + # xblock mccabe==0.7.0 # via pylint +openedx-django-pyfs==3.4.0 + # via + # -r requirements/test.txt + # xblock packaging==23.2 # via # -r requirements/test.txt @@ -101,6 +161,11 @@ pytest-cov==4.1.0 # via -r requirements/test.txt pytest-django==4.5.2 # via -r requirements/test.txt +python-dateutil==2.8.2 + # via + # -r requirements/test.txt + # botocore + # xblock python-slugify==8.0.1 # via # -r requirements/test.txt @@ -109,12 +174,30 @@ pytz==2023.3.post1 # via # -r requirements/test.txt # django + # xblock pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations + # xblock +requests==2.31.0 + # via -r requirements/test.txt +s3transfer==0.7.0 + # via + # -r requirements/test.txt + # boto3 +simplejson==3.19.2 + # via + # -r requirements/test.txt + # xblock + # xblock-utils six==1.16.0 - # via edx-lint + # via + # -r requirements/test.txt + # edx-lint + # fs + # fs-s3fs + # python-dateutil snowballstemmer==2.2.0 # via pydocstyle sqlparse==0.4.4 @@ -143,5 +226,28 @@ typing-extensions==4.8.0 # asgiref # astroid # pylint +urllib3==1.26.17 + # via + # -r requirements/test.txt + # botocore + # requests +web-fragments==2.1.0 + # via + # -r requirements/test.txt + # xblock + # xblock-utils +webob==1.8.7 + # via + # -r requirements/test.txt + # xblock wrapt==1.15.0 # via astroid +xblock[django]==1.8.1 + # via + # -r requirements/test.txt + # xblock-utils +xblock-utils==4.0.0 + # via -r requirements/test.txt + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/test.txt b/requirements/test.txt index 878494c..33031b4 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,10 +4,31 @@ # # make upgrade # +appdirs==1.4.4 + # via + # -r requirements/base.txt + # fs asgiref==3.7.2 # via # -r requirements/base.txt # django +boto3==1.28.62 + # via + # -r requirements/base.txt + # fs-s3fs +botocore==1.31.62 + # via + # -r requirements/base.txt + # boto3 + # s3transfer +certifi==2023.7.22 + # via + # -r requirements/base.txt + # requests +charset-normalizer==3.3.0 + # via + # -r requirements/base.txt + # requests click==8.1.7 # via code-annotations code-annotations==1.5.0 @@ -17,14 +38,55 @@ coverage[toml]==7.3.2 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.txt + # openedx-django-pyfs exceptiongroup==1.1.3 # via pytest +fs==2.4.16 + # via + # -r requirements/base.txt + # fs-s3fs + # openedx-django-pyfs + # xblock +fs-s3fs==1.1.1 + # via + # -r requirements/base.txt + # openedx-django-pyfs +idna==3.4 + # via + # -r requirements/base.txt + # requests iniconfig==2.0.0 # via pytest jinja2==3.1.2 # via code-annotations +jmespath==1.0.1 + # via + # -r requirements/base.txt + # boto3 + # botocore +lazy==1.6 + # via + # -r requirements/base.txt + # xblock +lxml==4.9.3 + # via + # -r requirements/base.txt + # xblock +mako==1.2.4 + # via + # -r requirements/base.txt + # xblock + # xblock-utils markupsafe==2.1.3 - # via jinja2 + # via + # -r requirements/base.txt + # jinja2 + # mako + # xblock +openedx-django-pyfs==3.4.0 + # via + # -r requirements/base.txt + # xblock packaging==23.2 # via pytest pbr==5.11.1 @@ -39,14 +101,40 @@ pytest-cov==4.1.0 # via -r requirements/test.in pytest-django==4.5.2 # via -r requirements/test.in +python-dateutil==2.8.2 + # via + # -r requirements/base.txt + # botocore + # xblock python-slugify==8.0.1 # via code-annotations pytz==2023.3.post1 # via # -r requirements/base.txt # django + # xblock pyyaml==6.0.1 - # via code-annotations + # via + # -r requirements/base.txt + # code-annotations + # xblock +requests==2.31.0 + # via -r requirements/base.txt +s3transfer==0.7.0 + # via + # -r requirements/base.txt + # boto3 +simplejson==3.19.2 + # via + # -r requirements/base.txt + # xblock + # xblock-utils +six==1.16.0 + # via + # -r requirements/base.txt + # fs + # fs-s3fs + # python-dateutil sqlparse==0.4.4 # via # -r requirements/base.txt @@ -63,3 +151,26 @@ typing-extensions==4.8.0 # via # -r requirements/base.txt # asgiref +urllib3==1.26.17 + # via + # -r requirements/base.txt + # botocore + # requests +web-fragments==2.1.0 + # via + # -r requirements/base.txt + # xblock + # xblock-utils +webob==1.8.7 + # via + # -r requirements/base.txt + # xblock +xblock[django]==1.8.1 + # via + # -r requirements/base.txt + # xblock-utils +xblock-utils==4.0.0 + # via -r requirements/base.txt + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/setup.py b/setup.py index a954885..12578a7 100755 --- a/setup.py +++ b/setup.py @@ -150,5 +150,11 @@ def is_requirement(line): "lms.djangoapp": [ "platform_plugin_turnitin = platform_plugin_turnitin.apps:PlatformPluginTurnitinConfig" ], + "cms.djangoapp": [ + "platform_plugin_turnitin = platform_plugin_turnitin.apps:PlatformPluginTurnitinConfig" + ], + "xblock.v1": [ + "turnitin = platform_plugin_turnitin.turnitin:TurnitinXBlock", + ], }, )