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
+
+
+
+
+
End User License Agreement (EULA)
+
Terms and Conditions
+
+
+
+
+
+
+
+
+
+
+ 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",
+ ],
},
)