From eabe51ce206cc58d71f69e0a008c4a862432079c Mon Sep 17 00:00:00 2001 From: Cristhian Garcia Date: Wed, 4 Oct 2023 15:25:32 -0500 Subject: [PATCH 01/12] feat: move work from xblock-test-api --- platform_plugin_turnitin/apps.py | 7 + platform_plugin_turnitin/models.py | 12 + .../static/css/turnitin.css | 216 ++++++++++++ platform_plugin_turnitin/static/html/cms.html | 1 + .../static/html/turnitin.html | 71 ++++ .../static/js/src/turnitin.js | 239 +++++++++++++ platform_plugin_turnitin/turnitin.py | 331 ++++++++++++++++++ .../turnitin_client/__init__.py | 0 .../turnitin_client/handlers.py | 230 ++++++++++++ requirements/base.in | 3 +- requirements/base.txt | 68 +++- requirements/dev.txt | 84 +++++ requirements/doc.txt | 90 ++++- requirements/quality.txt | 91 ++++- requirements/test.txt | 98 +++++- setup.py | 3 + 16 files changed, 1538 insertions(+), 6 deletions(-) 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.py diff --git a/platform_plugin_turnitin/apps.py b/platform_plugin_turnitin/apps.py index aeb61ac..323b062 100644 --- a/platform_plugin_turnitin/apps.py +++ b/platform_plugin_turnitin/apps.py @@ -26,3 +26,10 @@ 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 TurnitinXBlock diff --git a/platform_plugin_turnitin/models.py b/platform_plugin_turnitin/models.py index be4ae84..954e3cf 100644 --- a/platform_plugin_turnitin/models.py +++ b/platform_plugin_turnitin/models.py @@ -1,3 +1,15 @@ """ Database models for platform_plugin_turnitin. """ + +from django.db import models +from django.contrib.auth.models import User + +class TurnitinSubmission(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='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) + + def __str__(self): + return f"Submission by {self.user.username} - Turnitin ID: {self.turnitin_submission_id or 'Not Set'}" diff --git a/platform_plugin_turnitin/static/css/turnitin.css b/platform_plugin_turnitin/static/css/turnitin.css new file mode 100644 index 0000000..28f0714 --- /dev/null +++ b/platform_plugin_turnitin/static/css/turnitin.css @@ -0,0 +1,216 @@ +/* CSS for TurnitinXBlock */ + +.turnitin_block .count { + font-weight: bold; +} + +.turnitin_block p { + cursor: pointer; +} + + + +body { + font-family: 'Arial', sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-color: #eef2f7; +} + +/* 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*/ +.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..a7bda65 --- /dev/null +++ b/platform_plugin_turnitin/static/html/turnitin.html @@ -0,0 +1,71 @@ +
+

TurnitinXBlock: count is now + {self.count} (click me to increment). +

+ + +
+

EULA Viewer

+ + + + +
+ + +
+
+

File Upload

+
+
+ + + Ningún archivo seleccionado +
+ +
+
+
+ + + +
+

File Processing Status

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

Similarity Processing Status

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

Text to File

+
+ +

+ +
+
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..c2b6a80 --- /dev/null +++ b/platform_plugin_turnitin/static/js/src/turnitin.js @@ -0,0 +1,239 @@ +/* Javascript for TurnitinXBlock. */ +function TurnitinXBlock(runtime, element) { + + function updateCount(result) { + $('.count', element).text(result.count); + } + + function showEULA(htmlContent) { + $('#eulaModal .modal-content p').html(htmlContent); // Establece el contenido HTML en el párrafo del modal + $('#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) { + showEULA(response); + }, + error: function() { + alert('Error al obtener el contenido del 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) { + alert('EULA accepted!.'); + }, + error: function() { + alert('Error getting EULA content.'); + } + }); + }); + + $('.modal-content button').click(closeModal); + + ////////////////////// UPLOAD FILE CSS + + $('#uploadBtn', element).click(function(event) { + event.preventDefault(); // Evitar que el formulario se envíe por defecto + var handlerUrl = runtime.handlerUrl(element, 'upload_turnitin_submission_file'); + + var fileInput = $('#file')[0]; + var file = fileInput.files[0]; + + if (!file) { + alert('Por favor, selecciona un archivo .doc o .docx primero.'); + return; + } + + var formData = new FormData(); + formData.append('myfile', file); + + $.ajax({ + type: 'POST', + url: handlerUrl, + data: formData, + processData: false, + contentType: false, + success: function(response) { + if (response.success) { + alert('Archivo subido con éxito a Turnitin.'); + } else { + alert('Hubo un error al subir el archivo a Turnitin.'); + } + }, + error: function() { + alert('Error comunicándose con el servidor.'); + } + }); + }); + + 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('Cambiar archivo'); + uploadButton.prop('disabled', false); + } else { + $(this).siblings('.selected-filename').text('Ningún archivo seleccionado'); + $(this).siblings('label').removeClass('file-selected').text('Elegir archivo'); + uploadButton.prop('disabled', true); + } + } else { + alert('Por favor, selecciona un archivo .doc o .docx'); + $(this).val(''); + $(this).siblings('.selected-filename').text('Ningún archivo seleccionado'); + $(this).siblings('label').removeClass('file-selected').text('Elegir archivo'); + uploadButton.prop('disabled', true); + } + } + $('#file', element).on('change', updateSelectedFileName); + + + +//////////////////////////////////////////////////// SIMILARITY + + $('#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) { + updateTrafficLightState('1', response['submission_status']); + }, + error: function() { + alert('Error al obtener status de la entrega.'); + } + }); + }); + + + + $('#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) { + 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) { + updateTrafficLightState('2', response['report_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) { + 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 refreshBtn2 = document.getElementById(`refreshBtn2`); + + redLight.classList.add('off'); + yellowLight.classList.add('off'); + greenLight.classList.add('off'); + generateReportBtn.disabled = true; + generateReportBtn.classList.remove('enabled'); + + switch (state) { + case 'ERROR': + redLight.classList.remove('off'); + break; + case 'PROCESSING': + yellowLight.classList.remove('off'); + break; + case 'COMPLETE': + greenLight.classList.remove('off'); + generateReportBtn.disabled = false; + generateReportBtn.classList.add('enabled'); + refreshBtn2.disabled = false; + refreshBtn2.classList.add('enabled'); + break; + } + } + +} diff --git a/platform_plugin_turnitin/turnitin.py b/platform_plugin_turnitin/turnitin.py new file mode 100644 index 0000000..b645bbb --- /dev/null +++ b/platform_plugin_turnitin/turnitin.py @@ -0,0 +1,331 @@ +"""TO-DO: Write a description of what this XBlock is.""" + +from datetime import datetime +import json +from django.http import JsonResponse +import pkg_resources +from django.utils import translation +from xblock.core import XBlock +from xblock.fields import Integer, Scope +from xblock.fragment import Fragment +from xblockutils.resources import ResourceLoader +from django.contrib.auth.models import User +#from .turnitin_api.models import TurnitinSubmission +from platform_plugin_turnitin.turnitin_client.handlers import (get_eula_page, + post_accept_eula_version, + post_create_submission, + put_upload_submission_file_content, + get_submission_info, + put_generate_similarity_report, + get_similarity_report_info, + post_create_viewer_launch_url + ) + +@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.. + + # TO-DO: delete count, and define your own fields. + count = Integer( + default=0, scope=Scope.user_state, + help="A simple counter, to show something happening", + ) + + 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): + """ + The 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): + """ + The 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 + + # TO-DO: change this handler to perform your own actions. You may need more + # than one handler, or you may not need any handlers at all. + @XBlock.json_handler + def increment_count(self, data, suffix=''): + """ + An example handler, which increments the data. + """ + if suffix: + pass # TO-DO: Use the suffix when storing data. + # Just to show data coming in... + assert data['hello'] == 'world' + + self.count += 1 + return {"count": self.count} + + + + + + + @XBlock.json_handler + def get_eula_agreement(self, data, suffix=''): + return get_eula_page() + + + @XBlock.json_handler + def accept_eula_agreement(self, data, suffix=''): + user_service = self.runtime.service(self, 'user') + user_id = user_service.get_current_user().opt_attrs['edx-platform.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" + } + return post_accept_eula_version(payload) + + def create_turnitin_submission_object(self): + current_user = self.runtime.service(self, 'user').get_current_user() + user_email = current_user.emails[0] + user_name = current_user.full_name.split() + user_id = current_user.opt_attrs['edx-platform.user_id'] + date_now = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + + payload={ + "owner": user_id, + "title": self.location.block_id, + "submitter": user_id, + "owner_default_permission_set": "LEARNER", + "submitter_default_permission_set": "INSTRUCTOR", + "extract_text_only": False, + "metadata": { + "owners": [ + { + "id": user_id, + "given_name": user_name[0] if user_name else "no_name", + "family_name": ' '.join(user_name[1:]) if len(user_name) > 1 else "no_last_name", + "email": user_email + } + ], + "submitter": { + "id": user_id, + "given_name": user_name[0] if user_name else "no_name", + "family_name": ' '.join(user_name[1:]) if len(user_name) > 1 else "no_last_name", + "email": user_email + }, + + "original_submitted_time": date_now, + } + } + return post_create_submission(payload) + + @XBlock.handler + def upload_turnitin_submission_file(self, data, suffix=''): + turnitin_submission = self.create_turnitin_submission_object() + if turnitin_submission.status_code == 201: + turnitin_submission_id = turnitin_submission.json()['id'] + #current_user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs['edx-platform.user_id'] + #current_user = User.objects.get(id=current_user_id) + #submission = TurnitinSubmission(user = current_user, turnitin_submission_id=turnitin_submission_id) + #submission.save() + #print('SUBMISSION CREATED<<<<<<<<<<<<<<<', submission) + myfile = data.params['myfile'].file + #turnitin_submission_id='0a966646-83f9-4ce6-aa47-71e07baf4e30' + response = put_upload_submission_file_content(turnitin_submission_id, myfile) + return response + return turnitin_submission + + @XBlock.json_handler + def get_submission_status(self, data, suffix=''): + current_user = self.runtime.service(self, 'user').get_current_user() + # try: + # last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') + # except TurnitinSubmission.DoesNotExist: + # return None + # return get_submission_info(last_submission.turnitin_submission_id) + status = get_submission_info('0a966646-83f9-4ce6-aa47-71e07baf4e30') + return {'submission_status':status} + + @XBlock.json_handler + def generate_similarity_report(self, data, suffix=''): + payload = { + "indexing_settings": { + "add_to_index": True + }, + "generation_settings": { + "search_repositories": [ + "INTERNET", + "SUBMITTED_WORK", + "PUBLICATION", + "CROSSREF", + "CROSSREF_POSTED_CONTENT" + ], + "submission_auto_excludes": [ + "b84b77d1-da0f-4f45-b002-8aec4f4796d6", + "b86de142-bc44-4f95-8467-84af12b89217" + ], + "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 + } + } + current_user = self.runtime.service(self, 'user').get_current_user() + # try: + # last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') + # except TurnitinSubmission.DoesNotExist: + # return None + # return put_generate_similarity_report(last_submission.turnitin_submission_id) + + response = put_generate_similarity_report('0a966646-83f9-4ce6-aa47-71e07baf4e30', payload) + if response.status_code == 202: + return {'success':True} + return {'success':False} + + @XBlock.json_handler + def get_similarity_report_status(self, data, suffix=''): + current_user = self.runtime.service(self, 'user').get_current_user() + # try: + # last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') + # except TurnitinSubmission.DoesNotExist: + # return None + # return get_similarity_report_info(last_submission.turnitin_submission_id) + status = get_similarity_report_info('0a966646-83f9-4ce6-aa47-71e07baf4e30') + return {'report_status':status} + + @XBlock.json_handler + def create_similarity_viewer(self, data, suffix=''): + current_user = self.runtime.service(self, 'user').get_current_user() + user_email = current_user.emails[0] + user_name = current_user.full_name.split() + user_id = current_user.opt_attrs['edx-platform.user_id'] + payload = { + "viewer_user_id": 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": "Smith", + "given_name": "John" + }, + "sidebar": { + "default_mode": "similarity" + } + } + current_user = self.runtime.service(self, 'user').get_current_user() + # try: + # last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') + # except TurnitinSubmission.DoesNotExist: + # return None + # return post_create_viewer_launch_url(last_submission.turnitin_submission_id) + url = post_create_viewer_launch_url('0a966646-83f9-4ce6-aa47-71e07baf4e30', payload) + return {'viewer_url': url} + + + + + + + # TO-DO: change this to create the scenarios you'd like to see in the + # workbench while developing your XBlock. + @staticmethod + def workbench_scenarios(): + """A canned scenario for display in the workbench.""" + return [ + ("TurnitinXBlock", + """ + """), + ("Multiple TurnitinXBlock", + """ + + + + + """), + ] + + @staticmethod + def _get_statici18n_js_url(): + """ + Returns 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(): + """ + Dummy method 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.py b/platform_plugin_turnitin/turnitin_client/handlers.py new file mode 100644 index 0000000..8edc99e --- /dev/null +++ b/platform_plugin_turnitin/turnitin_client/handlers.py @@ -0,0 +1,230 @@ +import requests +from typing import Optional, Dict +import json + +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. + + Raises: + - ValueError: If an unsupported request method is provided. + """ + + # Configuration variables + TII_API_URL = "https://edunext.tii-sandbox.com" + TCA_INTEGRATION_FAMILY = "MySweetLMS" + TCA_INTEGRATION_VERSION = "3.2.4" + TCA_API_KEY = "[SECRET_KEY]" + + # Headers configuration + headers = { + "X-Turnitin-Integration-Name": TCA_INTEGRATION_FAMILY, + "X-Turnitin-Integration-Version": TCA_INTEGRATION_VERSION, + "Authorization": f"Bearer {TCA_API_KEY}" + } + + # Add Content-Type for methods that typically send JSON payload + 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) + return response + + # Mapping of methods to corresponding functions in the requests library + method_map = { + 'get': requests.get, + 'post': requests.post, + 'put': requests.put, + 'delete': requests.delete, + 'patch': requests.patch + } + + # Retrieving the correct function + method_func = method_map.get(request_method.lower()) + + if not method_func: + raise ValueError(f"Unsupported request method: {request_method}") + + # Calling the appropriate method + if request_method.lower() in ['post', 'put', 'patch']: + response = method_func(f"{TII_API_URL}/api/v1/{url_prefix}", headers=headers, json=data) + else: + response = method_func(f"{TII_API_URL}/api/v1/{url_prefix}", headers=headers, params=data) + + return response + + +def pretty_print_response(response, type_of=''): + content = response.json() + print('\n\n') + print(f'------{type_of}------') + print('\n\n') + print(json.dumps(content, indent=4)) + print('\n\n') + print('------------') + print('\n\n') + +# Returns all the features enabled in Turnitin account. +def get_features_enabled(): + """ + Returns all the features enabled in the Turnitin account. + """ + response = turnitin_api_handler('get', 'features-enabled') + pretty_print_response(response) + + +# *---EULA endpoints ---* +# The EULA is a page of terms and conditions that the owner and the +# submiter has to accept in order to send a file to Turnitin. + +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}') + pretty_print_response(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.text + +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) + pretty_print_response(response, 'ACCEPT EULA') + +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}') + pretty_print_response(response) + + +# *---Submissions endpoints ---* +# Submissions is a Turnithin model that has all relative info to a Assessment send by +# an student. + +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) + pretty_print_response(response, 'CREATE SUBMISSION') + 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) + pretty_print_response(response, 'UPLOAD 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}') + pretty_print_response(response, 'SUBMISSION STATUS') + return response.json()['status'] + +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}') + pretty_print_response(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') + pretty_print_response(response) + + +# *---Similarity endpoints ---* +# Similarity is a Turnithin report with an internet similarity detection +# score of the student submissions. + +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) + pretty_print_response(response, 'REPORT GENERATION') + 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') + pretty_print_response(response, 'REPORT STATUS') + return response.json()['status'] + +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) + pretty_print_response(response, 'URL VIEWER') + return response.json()['viewer_url'] + +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') + pretty_print_response(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}') + pretty_print_response(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') + pretty_print_response(response) + diff --git a/requirements/base.in b/requirements/base.in index a954780..04b2f8d 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -2,4 +2,5 @@ -c constraints.txt Django # Web application framework - +xblock +xblock-utils diff --git a/requirements/base.txt b/requirements/base.txt index 81ee185..fc1d27b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,15 +4,81 @@ # # make upgrade # +appdirs==1.4.4 + # via fs asgiref==3.7.2 # via django +boto3==1.28.60 + # via fs-s3fs +botocore==1.31.60 + # via + # boto3 + # s3transfer 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 +jmespath==1.0.1 + # via + # boto3 + # botocore +lazy==1.6 + # via xblock +lxml==4.9.3 + # via xblock +mako==1.2.4 + # via 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 +s3transfer==0.7.0 + # via boto3 +simplejson==3.19.1 + # via 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 +web-fragments==2.1.0 + # via + # xblock + # xblock-utils +webob==1.8.7 + # via xblock +xblock[django]==1.8.0 + # 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..ee79818 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,6 +19,15 @@ astroid==2.15.8 # pylint-celery black==23.9.1 # via -r requirements/dev.in +boto3==1.28.60 + # via + # -r requirements/quality.txt + # fs-s3fs +botocore==1.31.60 + # via + # -r requirements/quality.txt + # boto3 + # s3transfer build==1.0.3 # via # -r requirements/pip-tools.txt @@ -57,6 +70,7 @@ 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 + # openedx-django-pyfs edx-i18n-tools==1.2.0 # via -r requirements/dev.in edx-lint==5.3.4 @@ -70,6 +84,16 @@ 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 importlib-metadata==6.8.0 # via # -r requirements/pip-tools.txt @@ -87,20 +111,43 @@ 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 + # xblock +mako==1.2.4 + # via + # -r requirements/quality.txt + # 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 +226,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 +239,29 @@ 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 +s3transfer==0.7.0 + # via + # -r requirements/quality.txt + # boto3 +simplejson==3.19.1 + # via + # -r requirements/quality.txt + # 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 +310,23 @@ typing-extensions==4.8.0 # astroid # black # pylint +urllib3==1.26.17 + # via + # -r requirements/quality.txt + # botocore 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 +335,12 @@ wrapt==1.15.0 # via # -r requirements/quality.txt # astroid +xblock[django]==1.8.0 + # 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..3f057b9 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,6 +22,15 @@ babel==2.13.0 # sphinx beautifulsoup4==4.12.2 # via pydata-sphinx-theme +boto3==1.28.60 + # via + # -r requirements/test.txt + # fs-s3fs +botocore==1.31.60 + # via + # -r requirements/test.txt + # boto3 + # s3transfer build==1.0.3 # via -r requirements/doc.in certifi==2023.7.22 @@ -42,6 +55,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,6 +69,16 @@ 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 imagesize==1.4.1 @@ -82,20 +106,43 @@ 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-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 +183,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,10 +197,12 @@ 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 @@ -164,8 +218,22 @@ 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.1 + # via + # -r requirements/test.txt + # 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 +286,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.0 + # 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..2fd0eed 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,15 @@ astroid==2.15.8 # via # pylint # pylint-celery +boto3==1.28.60 + # via + # -r requirements/test.txt + # fs-s3fs +botocore==1.31.60 + # via + # -r requirements/test.txt + # boto3 + # s3transfer click==8.1.7 # via # -r requirements/test.txt @@ -34,12 +47,23 @@ 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 iniconfig==2.0.0 # via # -r requirements/test.txt @@ -52,14 +76,37 @@ 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-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 +148,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 +161,27 @@ pytz==2023.3.post1 # via # -r requirements/test.txt # django + # xblock pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations + # xblock +s3transfer==0.7.0 + # via + # -r requirements/test.txt + # boto3 +simplejson==3.19.1 + # via + # -r requirements/test.txt + # 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 +210,27 @@ typing-extensions==4.8.0 # asgiref # astroid # pylint +urllib3==1.26.17 + # via + # -r requirements/test.txt + # botocore +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.0 + # 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..a7c0234 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,10 +4,23 @@ # # 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.60 + # via + # -r requirements/base.txt + # fs-s3fs +botocore==1.31.60 + # via + # -r requirements/base.txt + # boto3 + # s3transfer click==8.1.7 # via code-annotations code-annotations==1.5.0 @@ -17,14 +30,50 @@ 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 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-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 +88,37 @@ 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 +s3transfer==0.7.0 + # via + # -r requirements/base.txt + # boto3 +simplejson==3.19.1 + # via + # -r requirements/base.txt + # 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 +135,25 @@ typing-extensions==4.8.0 # via # -r requirements/base.txt # asgiref +urllib3==1.26.17 + # via + # -r requirements/base.txt + # botocore +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.0 + # 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..adb1f67 100755 --- a/setup.py +++ b/setup.py @@ -150,5 +150,8 @@ def is_requirement(line): "lms.djangoapp": [ "platform_plugin_turnitin = platform_plugin_turnitin.apps:PlatformPluginTurnitinConfig" ], + 'xblock.v1': [ + 'turnitin = platform_plugin_turnitin.turnitin:TurnitinXBlock', + ], }, ) From 6530484aac0038d1b786a6147a045f979ea5c5f3 Mon Sep 17 00:00:00 2001 From: Cristhian Garcia Date: Wed, 4 Oct 2023 15:38:33 -0500 Subject: [PATCH 02/12] feat: add initial migrations --- .../migrations/0001_initial.py | 27 +++++++++++++++++++ .../migrations/__init__.py | 0 2 files changed, 27 insertions(+) create mode 100644 platform_plugin_turnitin/migrations/0001_initial.py create mode 100644 platform_plugin_turnitin/migrations/__init__.py diff --git a/platform_plugin_turnitin/migrations/0001_initial.py b/platform_plugin_turnitin/migrations/0001_initial.py new file mode 100644 index 0000000..2f7377d --- /dev/null +++ b/platform_plugin_turnitin/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# 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/__init__.py b/platform_plugin_turnitin/migrations/__init__.py new file mode 100644 index 0000000..e69de29 From 292fbd2eadc40d568635cb1a43c986a7366e8574 Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 5 Oct 2023 09:24:28 -0400 Subject: [PATCH 03/12] feat: workflow ajax error handle --- platform_plugin_turnitin/models.py | 2 +- .../static/css/turnitin.css | 29 ++++-- .../static/html/turnitin.html | 2 + .../static/js/src/turnitin.js | 74 +++++++++++---- platform_plugin_turnitin/turnitin.py | 91 ++++++++++--------- .../turnitin_client/handlers.py | 11 ++- 6 files changed, 131 insertions(+), 78 deletions(-) diff --git a/platform_plugin_turnitin/models.py b/platform_plugin_turnitin/models.py index 954e3cf..abfb41f 100644 --- a/platform_plugin_turnitin/models.py +++ b/platform_plugin_turnitin/models.py @@ -12,4 +12,4 @@ class TurnitinSubmission(models.Model): created_at = models.DateTimeField(auto_now_add=True) def __str__(self): - return f"Submission by {self.user.username} - Turnitin ID: {self.turnitin_submission_id or 'Not Set'}" + return f"Submission: {self.turnitin_submission_id or 'Not Set'} - created at: {self.created_at}" diff --git a/platform_plugin_turnitin/static/css/turnitin.css b/platform_plugin_turnitin/static/css/turnitin.css index 28f0714..6a5ec07 100644 --- a/platform_plugin_turnitin/static/css/turnitin.css +++ b/platform_plugin_turnitin/static/css/turnitin.css @@ -9,16 +9,6 @@ } - -body { - font-family: 'Arial', sans-serif; - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - background-color: #eef2f7; -} - /* Estilo base para todas las secciones */ .turnitin-test-section { padding: 20px; @@ -133,6 +123,25 @@ button { } /* CSS PARA STATUS*/ +.status-text { + font-size: 16px; + margin-top: 10px; + font-weight: bold; +} + +.red-text { + color: red; +} + +.yellow-text { + color: yellow; +} + +.green-text { + color: green; +} + + .traffic-light { width: 50px; height: 150px; diff --git a/platform_plugin_turnitin/static/html/turnitin.html b/platform_plugin_turnitin/static/html/turnitin.html index a7bda65..31e2677 100644 --- a/platform_plugin_turnitin/static/html/turnitin.html +++ b/platform_plugin_turnitin/static/html/turnitin.html @@ -43,6 +43,7 @@

File Processing Status

+
@@ -55,6 +56,7 @@

Similarity Processing Status

+
diff --git a/platform_plugin_turnitin/static/js/src/turnitin.js b/platform_plugin_turnitin/static/js/src/turnitin.js index c2b6a80..1fca4c3 100644 --- a/platform_plugin_turnitin/static/js/src/turnitin.js +++ b/platform_plugin_turnitin/static/js/src/turnitin.js @@ -32,11 +32,16 @@ function TurnitinXBlock(runtime, element) { type: "POST", url: handlerUrl, data: JSON.stringify({"hello": "world"}), + success: function(response) { - showEULA(response); + if(response.status === 404) { + alert(`ERROR: ${response.status} - No EULA page for the given version was found`); + } else { + showEULA(response.html); + } }, error: function() { - alert('Error al obtener el contenido del EULA.'); + alert('Error getting EULA.'); } }); }); @@ -49,10 +54,16 @@ function TurnitinXBlock(runtime, element) { url: handlerUrl, data: JSON.stringify({"hello": "world"}), success: function(response) { - alert('EULA accepted!.'); + if(response.success === false && (response.status === 400 || response.status === 404)) { + updateTrafficLightState('1', 'ERROR'); + alert(`ERROR: ${response.status} - ${response.message}`); + } else { + alert('EULA successfully accepted!'); + } + }, error: function() { - alert('Error getting EULA content.'); + alert('Error accepting EULA.'); } }); }); @@ -83,14 +94,15 @@ function TurnitinXBlock(runtime, element) { processData: false, contentType: false, success: function(response) { - if (response.success) { - alert('Archivo subido con éxito a Turnitin.'); - } else { - alert('Hubo un error al subir el archivo a Turnitin.'); - } + if(response.success === false && (response.status === 404 || response.status === 413 || response.status === 422 || response.status === 409)) { + alert(`ERROR: ${response.status} - ${response.message}`); + } else { + alert('File successfully uploaded to Turnitin!'); + } + }, error: function() { - alert('Error comunicándose con el servidor.'); + alert('Error uploading file.'); } }); }); @@ -129,16 +141,22 @@ function TurnitinXBlock(runtime, element) { var handlerUrl = runtime.handlerUrl(element, 'get_submission_status'); $.ajax({ - type: "POST", - url: handlerUrl, - data: JSON.stringify({"hello": "world"}), - success: function(response) { - updateTrafficLightState('1', response['submission_status']); - }, - error: function() { - alert('Error al obtener status de la entrega.'); + type: "POST", + url: handlerUrl, + data: JSON.stringify({"hello": "world"}), + success: function(response) { + if(response.success === false && response.status === 404) { + updateTrafficLightState('1', 'ERROR'); + alert(`ERROR: ${response.status} - ${response.message}`); + } else { + updateTrafficLightState('1', response['status']); } - }); + + }, + error: function() { + alert('Error getting report status.'); + } + }); }); @@ -172,7 +190,13 @@ function TurnitinXBlock(runtime, element) { url: handlerUrl, data: JSON.stringify({"hello": "world"}), success: function(response) { - updateTrafficLightState('2', response['report_status']); + if(response.success === false && response.status === 404) { + updateTrafficLightState('2', 'ERROR'); + alert(`ERROR: ${response.status} - ${response.message}`); + } else { + updateTrafficLightState('2', response['status']); + } + }, error: function() { alert('Error getting report status.'); @@ -211,6 +235,7 @@ function TurnitinXBlock(runtime, element) { 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'); @@ -218,21 +243,30 @@ function TurnitinXBlock(runtime, element) { 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 index b645bbb..6fb8808 100644 --- a/platform_plugin_turnitin/turnitin.py +++ b/platform_plugin_turnitin/turnitin.py @@ -2,7 +2,6 @@ from datetime import datetime import json -from django.http import JsonResponse import pkg_resources from django.utils import translation from xblock.core import XBlock @@ -10,7 +9,7 @@ from xblock.fragment import Fragment from xblockutils.resources import ResourceLoader from django.contrib.auth.models import User -#from .turnitin_api.models import TurnitinSubmission +from .models import TurnitinSubmission from platform_plugin_turnitin.turnitin_client.handlers import (get_eula_page, post_accept_eula_version, post_create_submission, @@ -105,8 +104,8 @@ def increment_count(self, data, suffix=''): @XBlock.json_handler def get_eula_agreement(self, data, suffix=''): - return get_eula_page() - + response = get_eula_page() + return {'html': response.text, 'status':response.status_code} @XBlock.json_handler def accept_eula_agreement(self, data, suffix=''): @@ -116,7 +115,8 @@ def accept_eula_agreement(self, data, suffix=''): payload = { "user_id": str(user_id), "accepted_timestamp": date_now, "language": "en-US" } - return post_accept_eula_version(payload) + response = post_accept_eula_version(payload) + return response.json() def create_turnitin_submission_object(self): current_user = self.runtime.service(self, 'user').get_current_user() @@ -158,11 +158,13 @@ def upload_turnitin_submission_file(self, data, suffix=''): turnitin_submission = self.create_turnitin_submission_object() if turnitin_submission.status_code == 201: turnitin_submission_id = turnitin_submission.json()['id'] - #current_user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs['edx-platform.user_id'] - #current_user = User.objects.get(id=current_user_id) - #submission = TurnitinSubmission(user = current_user, turnitin_submission_id=turnitin_submission_id) - #submission.save() - #print('SUBMISSION CREATED<<<<<<<<<<<<<<<', submission) + current_user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs['edx-platform.user_id'] + current_user = User.objects.get(id=current_user_id) + submission = TurnitinSubmission(user = current_user, turnitin_submission_id=turnitin_submission_id) + submission.save() + print('\n\n') + print('SUBMISSION CREATED<<<<<<<<<<<<<<<', submission) + print('\n\n') myfile = data.params['myfile'].file #turnitin_submission_id='0a966646-83f9-4ce6-aa47-71e07baf4e30' response = put_upload_submission_file_content(turnitin_submission_id, myfile) @@ -171,14 +173,15 @@ def upload_turnitin_submission_file(self, data, suffix=''): @XBlock.json_handler def get_submission_status(self, data, suffix=''): - current_user = self.runtime.service(self, 'user').get_current_user() - # try: - # last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') - # except TurnitinSubmission.DoesNotExist: - # return None - # return get_submission_info(last_submission.turnitin_submission_id) - status = get_submission_info('0a966646-83f9-4ce6-aa47-71e07baf4e30') - return {'submission_status':status} + current_user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs['edx-platform.user_id'] + current_user = User.objects.get(id=current_user_id) + try: + last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') + except TurnitinSubmission.DoesNotExist: + return {'success':False} + # last_submission = '0a966646-83f9-4ce6-aa47-71e07baf4e30' + response = get_submission_info(last_submission.turnitin_submission_id) + return response.json() @XBlock.json_handler def generate_similarity_report(self, data, suffix=''): @@ -217,33 +220,33 @@ def generate_similarity_report(self, data, suffix=''): "exclude_submitted_works": False } } - current_user = self.runtime.service(self, 'user').get_current_user() - # try: - # last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') - # except TurnitinSubmission.DoesNotExist: - # return None - # return put_generate_similarity_report(last_submission.turnitin_submission_id) - - response = put_generate_similarity_report('0a966646-83f9-4ce6-aa47-71e07baf4e30', payload) + current_user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs['edx-platform.user_id'] + current_user = User.objects.get(id=current_user_id) + try: + last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') + except TurnitinSubmission.DoesNotExist: + {'success':False} + # last_submission = '0a966646-83f9-4ce6-aa47-71e07baf4e30' + response = put_generate_similarity_report(last_submission.turnitin_submission_id, payload) if response.status_code == 202: return {'success':True} return {'success':False} @XBlock.json_handler def get_similarity_report_status(self, data, suffix=''): - current_user = self.runtime.service(self, 'user').get_current_user() - # try: - # last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') - # except TurnitinSubmission.DoesNotExist: - # return None - # return get_similarity_report_info(last_submission.turnitin_submission_id) - status = get_similarity_report_info('0a966646-83f9-4ce6-aa47-71e07baf4e30') - return {'report_status':status} + current_user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs['edx-platform.user_id'] + current_user = User.objects.get(id=current_user_id) + try: + last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') + except TurnitinSubmission.DoesNotExist: + return {'success': False} + # last_submission = '0a966646-83f9-4ce6-aa47-71e07baf4e30' + response = get_similarity_report_info(last_submission.turnitin_submission_id) + return response.json() @XBlock.json_handler def create_similarity_viewer(self, data, suffix=''): current_user = self.runtime.service(self, 'user').get_current_user() - user_email = current_user.emails[0] user_name = current_user.full_name.split() user_id = current_user.opt_attrs['edx-platform.user_id'] payload = { @@ -266,20 +269,20 @@ def create_similarity_viewer(self, data, suffix=''): } }, "author_metadata_override": { - "family_name": "Smith", - "given_name": "John" + "family_name": ' '.join(user_name[1:]) if len(user_name) > 1 else "no_last_name", + "given_name": user_name[0] if user_name else "no_name", }, "sidebar": { "default_mode": "similarity" } } - current_user = self.runtime.service(self, 'user').get_current_user() - # try: - # last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') - # except TurnitinSubmission.DoesNotExist: - # return None - # return post_create_viewer_launch_url(last_submission.turnitin_submission_id) - url = post_create_viewer_launch_url('0a966646-83f9-4ce6-aa47-71e07baf4e30', payload) + current_user = User.objects.get(id=user_id) + try: + last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') + except TurnitinSubmission.DoesNotExist: + return {'success': False} + # last_submission = '0a966646-83f9-4ce6-aa47-71e07baf4e30' + url = post_create_viewer_launch_url(last_submission.turnitin_submission_id, payload) return {'viewer_url': url} diff --git a/platform_plugin_turnitin/turnitin_client/handlers.py b/platform_plugin_turnitin/turnitin_client/handlers.py index 8edc99e..7a2c86b 100644 --- a/platform_plugin_turnitin/turnitin_client/handlers.py +++ b/platform_plugin_turnitin/turnitin_client/handlers.py @@ -97,13 +97,15 @@ def get_eula_version_info(version: str = 'latest', language: str = 'EN'): response = turnitin_api_handler('get', f'eula/{version}?lang={language}') pretty_print_response(response) +#TODO: done 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.text + return response +#TODO: done def post_accept_eula_version(payload, version: str = 'v1beta'): """ Accepts a specific EULA version. @@ -111,6 +113,7 @@ def post_accept_eula_version(payload, version: str = 'v1beta'): """ response = turnitin_api_handler('post', f'eula/{version}/accept', payload) pretty_print_response(response, 'ACCEPT EULA') + return response def get_eula_acceptance_by_user(user_id): """ @@ -142,6 +145,7 @@ def put_upload_submission_file_content(submission_id, file): pretty_print_response(response, 'UPLOAD FILE') return response +#TODO: done def get_submission_info(submission_id): """ Fetches all the information related to a specific submission. @@ -155,7 +159,7 @@ def get_submission_info(submission_id): """ response = turnitin_api_handler('get', f'submissions/{submission_id}') pretty_print_response(response, 'SUBMISSION STATUS') - return response.json()['status'] + return response def delete_submission(submission_id, is_hard_delete='false'): """ @@ -185,6 +189,7 @@ def put_generate_similarity_report(submission_id, payload): pretty_print_response(response, 'REPORT GENERATION') return response +#TODO: done def get_similarity_report_info(submission_id): """ Returns summary information about the requested Similarity Report. @@ -194,7 +199,7 @@ def get_similarity_report_info(submission_id): """ response = turnitin_api_handler('get', f'submissions/{submission_id}/similarity') pretty_print_response(response, 'REPORT STATUS') - return response.json()['status'] + return response def post_create_viewer_launch_url(submission_id, payload): """ From f8268a406b5ca0f54381c9f42e5a8ad1c838263a Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 5 Oct 2023 10:31:36 -0400 Subject: [PATCH 04/12] fix: File submission response fixed --- platform_plugin_turnitin/static/css/turnitin.css | 2 +- platform_plugin_turnitin/turnitin.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/platform_plugin_turnitin/static/css/turnitin.css b/platform_plugin_turnitin/static/css/turnitin.css index 6a5ec07..53a583b 100644 --- a/platform_plugin_turnitin/static/css/turnitin.css +++ b/platform_plugin_turnitin/static/css/turnitin.css @@ -134,7 +134,7 @@ button { } .yellow-text { - color: yellow; + color: rgb(168, 168, 14); } .green-text { diff --git a/platform_plugin_turnitin/turnitin.py b/platform_plugin_turnitin/turnitin.py index 6fb8808..d886862 100644 --- a/platform_plugin_turnitin/turnitin.py +++ b/platform_plugin_turnitin/turnitin.py @@ -9,6 +9,7 @@ from xblock.fragment import Fragment from xblockutils.resources import ResourceLoader from django.contrib.auth.models import User +from webob import Response from .models import TurnitinSubmission from platform_plugin_turnitin.turnitin_client.handlers import (get_eula_page, post_accept_eula_version, @@ -168,8 +169,9 @@ def upload_turnitin_submission_file(self, data, suffix=''): myfile = data.params['myfile'].file #turnitin_submission_id='0a966646-83f9-4ce6-aa47-71e07baf4e30' response = put_upload_submission_file_content(turnitin_submission_id, myfile) - return response - return turnitin_submission + return Response(json.dumps(response.json()), content_type='application/json', charset='UTF-8') + + return Response(json.dumps(turnitin_submission.json()), content_type='application/json', charset='UTF-8') @XBlock.json_handler def get_submission_status(self, data, suffix=''): @@ -179,7 +181,7 @@ def get_submission_status(self, data, suffix=''): last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') except TurnitinSubmission.DoesNotExist: return {'success':False} - # last_submission = '0a966646-83f9-4ce6-aa47-71e07baf4e30' + # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' response = get_submission_info(last_submission.turnitin_submission_id) return response.json() @@ -226,7 +228,7 @@ def generate_similarity_report(self, data, suffix=''): last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') except TurnitinSubmission.DoesNotExist: {'success':False} - # last_submission = '0a966646-83f9-4ce6-aa47-71e07baf4e30' + # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' response = put_generate_similarity_report(last_submission.turnitin_submission_id, payload) if response.status_code == 202: return {'success':True} @@ -240,7 +242,7 @@ def get_similarity_report_status(self, data, suffix=''): last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') except TurnitinSubmission.DoesNotExist: return {'success': False} - # last_submission = '0a966646-83f9-4ce6-aa47-71e07baf4e30' + # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' response = get_similarity_report_info(last_submission.turnitin_submission_id) return response.json() @@ -281,7 +283,7 @@ def create_similarity_viewer(self, data, suffix=''): last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') except TurnitinSubmission.DoesNotExist: return {'success': False} - # last_submission = '0a966646-83f9-4ce6-aa47-71e07baf4e30' + # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' url = post_create_viewer_launch_url(last_submission.turnitin_submission_id, payload) return {'viewer_url': url} From 02595b476bf713da1924a65c5338a2bc661bcad5 Mon Sep 17 00:00:00 2001 From: Fernando Date: Thu, 5 Oct 2023 20:12:03 -0400 Subject: [PATCH 05/12] fix: requested changes and docstrings --- .../static/html/turnitin.html | 3 - .../static/js/src/turnitin.js | 29 +-- platform_plugin_turnitin/turnitin.py | 172 ++++++++++++------ .../turnitin_client/handlers.py | 7 +- 4 files changed, 131 insertions(+), 80 deletions(-) diff --git a/platform_plugin_turnitin/static/html/turnitin.html b/platform_plugin_turnitin/static/html/turnitin.html index 31e2677..4fdd9d2 100644 --- a/platform_plugin_turnitin/static/html/turnitin.html +++ b/platform_plugin_turnitin/static/html/turnitin.html @@ -1,7 +1,4 @@
-

TurnitinXBlock: count is now - {self.count} (click me to increment). -

diff --git a/platform_plugin_turnitin/static/js/src/turnitin.js b/platform_plugin_turnitin/static/js/src/turnitin.js index 1fca4c3..6fa21eb 100644 --- a/platform_plugin_turnitin/static/js/src/turnitin.js +++ b/platform_plugin_turnitin/static/js/src/turnitin.js @@ -34,7 +34,7 @@ function TurnitinXBlock(runtime, element) { data: JSON.stringify({"hello": "world"}), success: function(response) { - if(response.status === 404) { + if(response.status >= 400) { alert(`ERROR: ${response.status} - No EULA page for the given version was found`); } else { showEULA(response.html); @@ -54,13 +54,11 @@ function TurnitinXBlock(runtime, element) { url: handlerUrl, data: JSON.stringify({"hello": "world"}), success: function(response) { - if(response.success === false && (response.status === 400 || response.status === 404)) { - updateTrafficLightState('1', 'ERROR'); + if(response.success === false && response.status >= 400) { alert(`ERROR: ${response.status} - ${response.message}`); } else { alert('EULA successfully accepted!'); } - }, error: function() { alert('Error accepting EULA.'); @@ -94,12 +92,11 @@ function TurnitinXBlock(runtime, element) { processData: false, contentType: false, success: function(response) { - if(response.success === false && (response.status === 404 || response.status === 413 || response.status === 422 || response.status === 409)) { + 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.'); @@ -145,13 +142,12 @@ function TurnitinXBlock(runtime, element) { url: handlerUrl, data: JSON.stringify({"hello": "world"}), success: function(response) { - if(response.success === false && response.status === 404) { + 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.'); @@ -170,7 +166,11 @@ function TurnitinXBlock(runtime, element) { url: handlerUrl, data: JSON.stringify({"hello": "world"}), success: function(response) { - alert('Successfully scheduled similarity report generation.'); + 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.'); @@ -190,13 +190,12 @@ function TurnitinXBlock(runtime, element) { url: handlerUrl, data: JSON.stringify({"hello": "world"}), success: function(response) { - if(response.success === false && response.status === 404) { + 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.'); @@ -214,8 +213,12 @@ function TurnitinXBlock(runtime, element) { url: handlerUrl, data: JSON.stringify({"hello": "world"}), success: function(response) { - alert('Redirecting...'); - openInNewTab(response['viewer_url']); + 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.'); diff --git a/platform_plugin_turnitin/turnitin.py b/platform_plugin_turnitin/turnitin.py index d886862..8e731ce 100644 --- a/platform_plugin_turnitin/turnitin.py +++ b/platform_plugin_turnitin/turnitin.py @@ -31,11 +31,6 @@ class TurnitinXBlock(XBlock): # Fields are defined on the class. You can access them in your code as # self.. - # TO-DO: delete count, and define your own fields. - count = Integer( - default=0, scope=Scope.user_state, - help="A simple counter, to show something happening", - ) def resource_string(self, path): """Handy helper for getting resources from our kit.""" @@ -82,36 +77,51 @@ def student_view(self, context=None): frag.add_javascript(self.resource_string("static/js/src/turnitin.js")) frag.initialize_js('TurnitinXBlock') return frag - - # TO-DO: change this handler to perform your own actions. You may need more - # than one handler, or you may not need any handlers at all. - @XBlock.json_handler - def increment_count(self, data, suffix=''): + # ---------------------------------------------------------------------------- + def get_user_data(self): """ - An example handler, which increments the data. - """ - if suffix: - pass # TO-DO: Use the suffix when storing data. - # Just to show data coming in... - assert data['hello'] == 'world' - - self.count += 1 - return {"count": self.count} - - - - + Fetches 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() + data = { + "user_id" : current_user.opt_attrs['edx-platform.user_id'], + "user_email" : current_user.emails[0], + "user_name" : current_user.full_name.split(), + } + return data @XBlock.json_handler def get_eula_agreement(self, data, suffix=''): + """ + Fetches 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=''): - user_service = self.runtime.service(self, 'user') - user_id = user_service.get_current_user().opt_attrs['edx-platform.user_id'] + """ + Submits 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" @@ -120,33 +130,37 @@ def accept_eula_agreement(self, data, suffix=''): return response.json() def create_turnitin_submission_object(self): - current_user = self.runtime.service(self, 'user').get_current_user() - user_email = current_user.emails[0] - user_name = current_user.full_name.split() - user_id = current_user.opt_attrs['edx-platform.user_id'] + """ + Constructs 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() + user_name = user_data['user_name'] date_now = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') payload={ - "owner": user_id, + "owner": user_data['user_id'], "title": self.location.block_id, - "submitter": user_id, + "submitter": user_data['user_id'], "owner_default_permission_set": "LEARNER", "submitter_default_permission_set": "INSTRUCTOR", "extract_text_only": False, "metadata": { "owners": [ { - "id": user_id, + "id": user_data['user_id'], "given_name": user_name[0] if user_name else "no_name", "family_name": ' '.join(user_name[1:]) if len(user_name) > 1 else "no_last_name", - "email": user_email + "email": user_data['user_email'] } ], "submitter": { - "id": user_id, + "id": user_data['user_id'], "given_name": user_name[0] if user_name else "no_name", "family_name": ' '.join(user_name[1:]) if len(user_name) > 1 else "no_last_name", - "email": user_email + "email": user_data['user_email'] }, "original_submitted_time": date_now, @@ -156,26 +170,42 @@ def create_turnitin_submission_object(self): @XBlock.handler def upload_turnitin_submission_file(self, data, suffix=''): + """ + Handles 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 == 201: turnitin_submission_id = turnitin_submission.json()['id'] - current_user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs['edx-platform.user_id'] + current_user_id = self.get_user_data()['user_id'] current_user = User.objects.get(id=current_user_id) submission = TurnitinSubmission(user = current_user, turnitin_submission_id=turnitin_submission_id) submission.save() - print('\n\n') - print('SUBMISSION CREATED<<<<<<<<<<<<<<<', submission) - print('\n\n') myfile = data.params['myfile'].file #turnitin_submission_id='0a966646-83f9-4ce6-aa47-71e07baf4e30' response = put_upload_submission_file_content(turnitin_submission_id, myfile) return Response(json.dumps(response.json()), content_type='application/json', charset='UTF-8') - return Response(json.dumps(turnitin_submission.json()), content_type='application/json', charset='UTF-8') @XBlock.json_handler def get_submission_status(self, data, suffix=''): - current_user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs['edx-platform.user_id'] + """ + Retrieves 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_id = self.get_user_data()['user_id'] current_user = User.objects.get(id=current_user_id) try: last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') @@ -187,6 +217,17 @@ def get_submission_status(self, data, suffix=''): @XBlock.json_handler def generate_similarity_report(self, data, suffix=''): + """ + Initiates 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 = { "indexing_settings": { "add_to_index": True @@ -222,21 +263,29 @@ def generate_similarity_report(self, data, suffix=''): "exclude_submitted_works": False } } - current_user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs['edx-platform.user_id'] + current_user_id = self.get_user_data()['user_id'] current_user = User.objects.get(id=current_user_id) try: last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') except TurnitinSubmission.DoesNotExist: - {'success':False} + return {'success':False} # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' response = put_generate_similarity_report(last_submission.turnitin_submission_id, payload) - if response.status_code == 202: - return {'success':True} - return {'success':False} + return response.json() @XBlock.json_handler def get_similarity_report_status(self, data, suffix=''): - current_user_id = self.runtime.service(self, 'user').get_current_user().opt_attrs['edx-platform.user_id'] + """ + Retrieves 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_id = self.get_user_data()['user_id'] current_user = User.objects.get(id=current_user_id) try: last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') @@ -248,11 +297,20 @@ def get_similarity_report_status(self, data, suffix=''): @XBlock.json_handler def create_similarity_viewer(self, data, suffix=''): - current_user = self.runtime.service(self, 'user').get_current_user() - user_name = current_user.full_name.split() - user_id = current_user.opt_attrs['edx-platform.user_id'] + """ + Creates 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() + user_name = user_data['user_name'] payload = { - "viewer_user_id": user_id, + "viewer_user_id": user_data['user_id'], "locale": "en-EN", "viewer_default_permission_set": "INSTRUCTOR", "viewer_permissions": { @@ -278,19 +336,15 @@ def create_similarity_viewer(self, data, suffix=''): "default_mode": "similarity" } } - current_user = User.objects.get(id=user_id) + 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} # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' - url = post_create_viewer_launch_url(last_submission.turnitin_submission_id, payload) - return {'viewer_url': url} - - - - - + response = post_create_viewer_launch_url(last_submission.turnitin_submission_id, payload) + return response.json() + # ---------------------------------------------------------------------------- # TO-DO: change this to create the scenarios you'd like to see in the # workbench while developing your XBlock. diff --git a/platform_plugin_turnitin/turnitin_client/handlers.py b/platform_plugin_turnitin/turnitin_client/handlers.py index 7a2c86b..a2714a4 100644 --- a/platform_plugin_turnitin/turnitin_client/handlers.py +++ b/platform_plugin_turnitin/turnitin_client/handlers.py @@ -66,6 +66,7 @@ def turnitin_api_handler(request_method: str, url_prefix: str = '', data: Option def pretty_print_response(response, type_of=''): + """debug helper function""" content = response.json() print('\n\n') print(f'------{type_of}------') @@ -97,7 +98,6 @@ def get_eula_version_info(version: str = 'latest', language: str = 'EN'): response = turnitin_api_handler('get', f'eula/{version}?lang={language}') pretty_print_response(response) -#TODO: done def get_eula_page(version: str = 'v1beta', language: str = 'en-US'): """ Returns the HTML content for a specified EULA version. @@ -105,7 +105,6 @@ def get_eula_page(version: str = 'v1beta', language: str = 'en-US'): response = turnitin_api_handler('get', f'/eula/{version}/view?lang={language}') return response -#TODO: done def post_accept_eula_version(payload, version: str = 'v1beta'): """ Accepts a specific EULA version. @@ -145,7 +144,6 @@ def put_upload_submission_file_content(submission_id, file): pretty_print_response(response, 'UPLOAD FILE') return response -#TODO: done def get_submission_info(submission_id): """ Fetches all the information related to a specific submission. @@ -189,7 +187,6 @@ def put_generate_similarity_report(submission_id, payload): pretty_print_response(response, 'REPORT GENERATION') return response -#TODO: done def get_similarity_report_info(submission_id): """ Returns summary information about the requested Similarity Report. @@ -209,7 +206,7 @@ def post_create_viewer_launch_url(submission_id, payload): """ response = turnitin_api_handler('post', f'submissions/{submission_id}/viewer-url',payload) pretty_print_response(response, 'URL VIEWER') - return response.json()['viewer_url'] + return response def post_generate_similarity_report_pdf(submission_id): """ From 50e8d16020b4719bb0ec65b4ab5b762ec15c2b4a Mon Sep 17 00:00:00 2001 From: Fernando Date: Fri, 6 Oct 2023 12:12:52 -0400 Subject: [PATCH 06/12] fix: set environment variables and format code --- .../migrations/0001_initial.py | 34 +- platform_plugin_turnitin/models.py | 5 +- platform_plugin_turnitin/settings/common.py | 6 + .../settings/production.py | 15 + platform_plugin_turnitin/turnitin.py | 339 ++++++++++-------- .../turnitin_client/handlers.py | 158 +++++--- setup.py | 4 +- 7 files changed, 344 insertions(+), 217 deletions(-) diff --git a/platform_plugin_turnitin/migrations/0001_initial.py b/platform_plugin_turnitin/migrations/0001_initial.py index 2f7377d..c987fbf 100644 --- a/platform_plugin_turnitin/migrations/0001_initial.py +++ b/platform_plugin_turnitin/migrations/0001_initial.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ @@ -15,13 +14,34 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='TurnitinSubmission', + 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)), + ( + "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/models.py b/platform_plugin_turnitin/models.py index abfb41f..29aa6fd 100644 --- a/platform_plugin_turnitin/models.py +++ b/platform_plugin_turnitin/models.py @@ -2,11 +2,12 @@ Database models for platform_plugin_turnitin. """ -from django.db import models from django.contrib.auth.models import User +from django.db import models + class TurnitinSubmission(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='submissions') + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="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..e646cf8 100644 --- a/platform_plugin_turnitin/settings/common.py +++ b/platform_plugin_turnitin/settings/common.py @@ -35,3 +35,9 @@ def plugin_settings(settings): # pylint: disable=unused-argument 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.TII_API_URL = "https://edunext.tii-sandbox.com" + settings.TCA_INTEGRATION_FAMILY = "MySweetLMS" + settings.TCA_INTEGRATION_VERSION = "3.2.4" + settings.TCA_API_KEY = "[SECRET_KEY_HERE]" diff --git a/platform_plugin_turnitin/settings/production.py b/platform_plugin_turnitin/settings/production.py index f029aa9..684344e 100644 --- a/platform_plugin_turnitin/settings/production.py +++ b/platform_plugin_turnitin/settings/production.py @@ -8,3 +8,18 @@ def plugin_settings(settings): # pylint: disable=unused-argument 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.TII_API_URL = getattr(settings, "ENV_TOKENS", {}).get( + "TII_API_URL", settings.TII_API_URL + ) + + settings.TCA_INTEGRATION_FAMILY = getattr(settings, "ENV_TOKENS", {}).get( + "TCA_INTEGRATION_FAMILY", settings.TCA_INTEGRATION_FAMILY + ) + + settings.TCA_INTEGRATION_VERSION = getattr(settings, "ENV_TOKENS", {}).get( + "TCA_INTEGRATION_VERSION", settings.TCA_INTEGRATION_VERSION + ) + + settings.TCA_API_KEY = getattr(settings, "ENV_TOKENS", {}).get( + "TCA_API_KEY", settings.TCA_API_KEY + ) diff --git a/platform_plugin_turnitin/turnitin.py b/platform_plugin_turnitin/turnitin.py index 8e731ce..dc07963 100644 --- a/platform_plugin_turnitin/turnitin.py +++ b/platform_plugin_turnitin/turnitin.py @@ -1,25 +1,30 @@ """TO-DO: Write a description of what this XBlock is.""" -from datetime import datetime import json +from datetime import datetime + import pkg_resources +from django.contrib.auth.models import User from django.utils import translation +from webob import Response from xblock.core import XBlock from xblock.fields import Integer, Scope from xblock.fragment import Fragment from xblockutils.resources import ResourceLoader -from django.contrib.auth.models import User -from webob import Response + +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, +) + from .models import TurnitinSubmission -from platform_plugin_turnitin.turnitin_client.handlers import (get_eula_page, - post_accept_eula_version, - post_create_submission, - put_upload_submission_file_content, - get_submission_info, - put_generate_similarity_report, - get_similarity_report_info, - post_create_viewer_launch_url - ) + @XBlock.needs("user") @XBlock.needs("user_state") @@ -31,7 +36,6 @@ class TurnitinXBlock(XBlock): # 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) @@ -51,10 +55,12 @@ def studio_view(self, context=None): # 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_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') + frag.initialize_js("TurnitinXBlock") return frag # TO-DO: change this view to display your data your own way. @@ -72,11 +78,14 @@ def student_view(self, context=None): # 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_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') + frag.initialize_js("TurnitinXBlock") return frag + # ---------------------------------------------------------------------------- def get_user_data(self): """ @@ -85,17 +94,17 @@ def get_user_data(self): Returns: dict: A dictionary containing user ID, email, and name. """ - user_service = self.runtime.service(self, 'user') + user_service = self.runtime.service(self, "user") current_user = user_service.get_current_user() data = { - "user_id" : current_user.opt_attrs['edx-platform.user_id'], - "user_email" : current_user.emails[0], - "user_name" : current_user.full_name.split(), + "user_id": current_user.opt_attrs["edx-platform.user_id"], + "user_email": current_user.emails[0], + "user_name": current_user.full_name.split(), } return data @XBlock.json_handler - def get_eula_agreement(self, data, suffix=''): + def get_eula_agreement(self, data, suffix=""): """ Fetches the End User License Agreement (EULA) content. @@ -107,10 +116,10 @@ def get_eula_agreement(self, data, suffix=''): 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} + return {"html": response.text, "status": response.status_code} @XBlock.json_handler - def accept_eula_agreement(self, data, suffix=''): + def accept_eula_agreement(self, data, suffix=""): """ Submits acceptance of the EULA for the current user. @@ -121,10 +130,12 @@ def accept_eula_agreement(self, data, suffix=''): 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') + 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" + "user_id": str(user_id), + "accepted_timestamp": date_now, + "language": "en-US", } response = post_accept_eula_version(payload) return response.json() @@ -137,39 +148,42 @@ def create_turnitin_submission_object(self): Response: The response from the Turnitin submission API. """ user_data = self.get_user_data() - user_name = user_data['user_name'] - date_now = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') - - payload={ - "owner": user_data['user_id'], - "title": self.location.block_id, - "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_name[0] if user_name else "no_name", - "family_name": ' '.join(user_name[1:]) if len(user_name) > 1 else "no_last_name", - "email": user_data['user_email'] - } - ], - "submitter": { - "id": user_data['user_id'], - "given_name": user_name[0] if user_name else "no_name", - "family_name": ' '.join(user_name[1:]) if len(user_name) > 1 else "no_last_name", - "email": user_data['user_email'] - }, + user_name = user_data["user_name"] + date_now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") - "original_submitted_time": date_now, - } + payload = { + "owner": user_data["user_id"], + "title": self.location.block_id, + "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_name[0] if user_name else "no_name", + "family_name": " ".join(user_name[1:]) + if len(user_name) > 1 + else "no_last_name", + "email": user_data["user_email"], + } + ], + "submitter": { + "id": user_data["user_id"], + "given_name": user_name[0] if user_name else "no_name", + "family_name": " ".join(user_name[1:]) + if len(user_name) > 1 + else "no_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=''): + def upload_turnitin_submission_file(self, data, suffix=""): """ Handles the upload of the user's file to Turnitin. @@ -182,19 +196,31 @@ def upload_turnitin_submission_file(self, data, suffix=''): """ turnitin_submission = self.create_turnitin_submission_object() if turnitin_submission.status_code == 201: - turnitin_submission_id = turnitin_submission.json()['id'] - current_user_id = self.get_user_data()['user_id'] + turnitin_submission_id = turnitin_submission.json()["id"] + current_user_id = self.get_user_data()["user_id"] current_user = User.objects.get(id=current_user_id) - submission = TurnitinSubmission(user = current_user, turnitin_submission_id=turnitin_submission_id) + submission = TurnitinSubmission( + user=current_user, turnitin_submission_id=turnitin_submission_id + ) submission.save() - myfile = data.params['myfile'].file - #turnitin_submission_id='0a966646-83f9-4ce6-aa47-71e07baf4e30' - response = put_upload_submission_file_content(turnitin_submission_id, myfile) - return Response(json.dumps(response.json()), content_type='application/json', charset='UTF-8') - return Response(json.dumps(turnitin_submission.json()), content_type='application/json', charset='UTF-8') + myfile = data.params["myfile"].file + # turnitin_submission_id='0a966646-83f9-4ce6-aa47-71e07baf4e30' + response = put_upload_submission_file_content( + turnitin_submission_id, myfile + ) + return Response( + json.dumps(response.json()), + content_type="application/json", + charset="UTF-8", + ) + return Response( + json.dumps(turnitin_submission.json()), + content_type="application/json", + charset="UTF-8", + ) @XBlock.json_handler - def get_submission_status(self, data, suffix=''): + def get_submission_status(self, data, suffix=""): """ Retrieves the status of the latest Turnitin submission for the user. @@ -205,18 +231,20 @@ def get_submission_status(self, data, suffix=''): Returns: dict: Information related to the user's latest Turnitin submission. """ - current_user_id = self.get_user_data()['user_id'] + current_user_id = self.get_user_data()["user_id"] current_user = User.objects.get(id=current_user_id) try: - last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') + last_submission = TurnitinSubmission.objects.filter( + user=current_user + ).latest("created_at") except TurnitinSubmission.DoesNotExist: - return {'success':False} + return {"success": False} # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' response = get_submission_info(last_submission.turnitin_submission_id) return response.json() @XBlock.json_handler - def generate_similarity_report(self, data, suffix=''): + def generate_similarity_report(self, data, suffix=""): """ Initiates the generation of a similarity report for the user's latest Turnitin submission. @@ -229,52 +257,54 @@ def generate_similarity_report(self, data, suffix=''): """ payload = { - "indexing_settings": { - "add_to_index": True - }, - "generation_settings": { - "search_repositories": [ - "INTERNET", - "SUBMITTED_WORK", - "PUBLICATION", - "CROSSREF", - "CROSSREF_POSTED_CONTENT" - ], - "submission_auto_excludes": [ - "b84b77d1-da0f-4f45-b002-8aec4f4796d6", - "b86de142-bc44-4f95-8467-84af12b89217" - ], - "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 - } + "indexing_settings": {"add_to_index": True}, + "generation_settings": { + "search_repositories": [ + "INTERNET", + "SUBMITTED_WORK", + "PUBLICATION", + "CROSSREF", + "CROSSREF_POSTED_CONTENT", + ], + "submission_auto_excludes": [ + "b84b77d1-da0f-4f45-b002-8aec4f4796d6", + "b86de142-bc44-4f95-8467-84af12b89217", + ], + "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, + }, } - current_user_id = self.get_user_data()['user_id'] + current_user_id = self.get_user_data()["user_id"] current_user = User.objects.get(id=current_user_id) try: - last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') + last_submission = TurnitinSubmission.objects.filter( + user=current_user + ).latest("created_at") except TurnitinSubmission.DoesNotExist: - return {'success':False} + return {"success": False} # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' - response = put_generate_similarity_report(last_submission.turnitin_submission_id, payload) + 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=''): + def get_similarity_report_status(self, data, suffix=""): """ Retrieves the status of the similarity report for the user's latest Turnitin submission. @@ -285,18 +315,20 @@ def get_similarity_report_status(self, data, suffix=''): Returns: dict: Information related to the status of the similarity report. """ - current_user_id = self.get_user_data()['user_id'] + current_user_id = self.get_user_data()["user_id"] current_user = User.objects.get(id=current_user_id) try: - last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') + last_submission = TurnitinSubmission.objects.filter( + user=current_user + ).latest("created_at") except TurnitinSubmission.DoesNotExist: - return {'success': False} + return {"success": False} # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' response = get_similarity_report_info(last_submission.turnitin_submission_id) return response.json() @XBlock.json_handler - def create_similarity_viewer(self, data, suffix=''): + def create_similarity_viewer(self, data, suffix=""): """ Creates a Turnitin similarity viewer for the user's latest submission. @@ -308,42 +340,42 @@ def create_similarity_viewer(self, data, suffix=''): dict: Contains the URL for the similarity viewer. """ user_data = self.get_user_data() - user_name = user_data['user_name'] + user_name = user_data["user_name"] 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": ' '.join(user_name[1:]) if len(user_name) > 1 else "no_last_name", - "given_name": user_name[0] if user_name else "no_name", - }, - "sidebar": { - "default_mode": "similarity" + "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": " ".join(user_name[1:]) + if len(user_name) > 1 + else "no_last_name", + "given_name": user_name[0] if user_name else "no_name", + }, + "sidebar": {"default_mode": "similarity"}, } - } - current_user = User.objects.get(id=user_data['user_id']) + current_user = User.objects.get(id=user_data["user_id"]) try: - last_submission = TurnitinSubmission.objects.filter(user=current_user).latest('created_at') + last_submission = TurnitinSubmission.objects.filter( + user=current_user + ).latest("created_at") except TurnitinSubmission.DoesNotExist: - return {'success': False} + return {"success": False} # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' - response = post_create_viewer_launch_url(last_submission.turnitin_submission_id, payload) + response = post_create_viewer_launch_url( + last_submission.turnitin_submission_id, payload + ) return response.json() + # ---------------------------------------------------------------------------- # TO-DO: change this to create the scenarios you'd like to see in the @@ -352,16 +384,20 @@ def create_similarity_viewer(self, data, suffix=''): def workbench_scenarios(): """A canned scenario for display in the workbench.""" return [ - ("TurnitinXBlock", - """ - """), - ("Multiple TurnitinXBlock", - """ + ( + "TurnitinXBlock", + """ + """, + ), + ( + "Multiple TurnitinXBlock", + """ - """), + """, + ), ] @staticmethod @@ -373,12 +409,13 @@ def _get_statici18n_js_url(): 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'): + 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)): + loader.module_name, text_js.format(locale_code=code) + ): return text_js.format(locale_code=code) return None @@ -387,4 +424,4 @@ def get_dummy(): """ Dummy method to generate initial i18n """ - return translation.gettext_noop('Dummy') + return translation.gettext_noop("Dummy") diff --git a/platform_plugin_turnitin/turnitin_client/handlers.py b/platform_plugin_turnitin/turnitin_client/handlers.py index a2714a4..9dc9e83 100644 --- a/platform_plugin_turnitin/turnitin_client/handlers.py +++ b/platform_plugin_turnitin/turnitin_client/handlers.py @@ -1,8 +1,18 @@ -import requests -from typing import Optional, Dict import json +from typing import Dict, Optional + +import requests -def turnitin_api_handler(request_method: str, url_prefix: str = '', data: Optional[Dict] = None, is_upload: bool = False, uploaded_file = None): +from django.conf import settings + + +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. @@ -19,35 +29,37 @@ def turnitin_api_handler(request_method: str, url_prefix: str = '', data: Option """ # Configuration variables - TII_API_URL = "https://edunext.tii-sandbox.com" - TCA_INTEGRATION_FAMILY = "MySweetLMS" - TCA_INTEGRATION_VERSION = "3.2.4" - TCA_API_KEY = "[SECRET_KEY]" + TII_API_URL = getattr(settings, "TII_API_URL", None) + TCA_INTEGRATION_FAMILY = getattr(settings, "TCA_INTEGRATION_FAMILY", None) + TCA_INTEGRATION_VERSION = getattr(settings, "TCA_INTEGRATION_VERSION", None) + TCA_API_KEY = getattr(settings, "TCA_API_KEY", None) # Headers configuration headers = { "X-Turnitin-Integration-Name": TCA_INTEGRATION_FAMILY, "X-Turnitin-Integration-Version": TCA_INTEGRATION_VERSION, - "Authorization": f"Bearer {TCA_API_KEY}" + "Authorization": f"Bearer {TCA_API_KEY}", } # Add Content-Type for methods that typically send JSON payload - if request_method.lower() in ['post', 'put', 'patch']: + 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) + 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 + ) return response # Mapping of methods to corresponding functions in the requests library method_map = { - 'get': requests.get, - 'post': requests.post, - 'put': requests.put, - 'delete': requests.delete, - 'patch': requests.patch + "get": requests.get, + "post": requests.post, + "put": requests.put, + "delete": requests.delete, + "patch": requests.patch, } # Retrieving the correct function @@ -57,31 +69,36 @@ def turnitin_api_handler(request_method: str, url_prefix: str = '', data: Option raise ValueError(f"Unsupported request method: {request_method}") # Calling the appropriate method - if request_method.lower() in ['post', 'put', 'patch']: - response = method_func(f"{TII_API_URL}/api/v1/{url_prefix}", headers=headers, json=data) + if request_method.lower() in ["post", "put", "patch"]: + response = method_func( + f"{TII_API_URL}/api/v1/{url_prefix}", headers=headers, json=data + ) else: - response = method_func(f"{TII_API_URL}/api/v1/{url_prefix}", headers=headers, params=data) + response = method_func( + f"{TII_API_URL}/api/v1/{url_prefix}", headers=headers, params=data + ) return response -def pretty_print_response(response, type_of=''): +def pretty_print_response(response, type_of=""): """debug helper function""" content = response.json() - print('\n\n') - print(f'------{type_of}------') - print('\n\n') + print("\n\n") + print(f"------{type_of}------") + print("\n\n") print(json.dumps(content, indent=4)) - print('\n\n') - print('------------') - print('\n\n') + print("\n\n") + print("------------") + print("\n\n") + # Returns all the features enabled in Turnitin account. def get_features_enabled(): """ Returns all the features enabled in the Turnitin account. """ - response = turnitin_api_handler('get', 'features-enabled') + response = turnitin_api_handler("get", "features-enabled") pretty_print_response(response) @@ -89,36 +106,40 @@ def get_features_enabled(): # The EULA is a page of terms and conditions that the owner and the # submiter has to accept in order to send a file to Turnitin. -def get_eula_version_info(version: str = 'latest', language: str = 'EN'): + +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}') + response = turnitin_api_handler("get", f"eula/{version}?lang={language}") pretty_print_response(response) -def get_eula_page(version: str = 'v1beta', language: str = 'en-US'): + +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}') + response = turnitin_api_handler("get", f"/eula/{version}/view?lang={language}") return response -def post_accept_eula_version(payload, version: str = 'v1beta'): + +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) - pretty_print_response(response, 'ACCEPT EULA') + response = turnitin_api_handler("post", f"eula/{version}/accept", payload) + pretty_print_response(response, "ACCEPT EULA") 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}') + response = turnitin_api_handler("get", f"eula/v1beta/accept/{user_id}") pretty_print_response(response) @@ -126,24 +147,32 @@ def get_eula_acceptance_by_user(user_id): # Submissions is a Turnithin model that has all relative info to a Assessment send by # an student. + 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) - pretty_print_response(response, 'CREATE SUBMISSION') + response = turnitin_api_handler("post", "submissions", payload) + pretty_print_response(response, "CREATE SUBMISSION") 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) - pretty_print_response(response, 'UPLOAD FILE') + response = turnitin_api_handler( + "put", + f"submissions/{submission_id}/original", + is_upload=True, + uploaded_file=file, + ) + pretty_print_response(response, "UPLOAD FILE") return response + def get_submission_info(submission_id): """ Fetches all the information related to a specific submission. @@ -155,23 +184,27 @@ def get_submission_info(submission_id): ERROR An error occurred during submission processing; see error_code for details """ - response = turnitin_api_handler('get', f'submissions/{submission_id}') - pretty_print_response(response, 'SUBMISSION STATUS') + response = turnitin_api_handler("get", f"submissions/{submission_id}") + pretty_print_response(response, "SUBMISSION STATUS") return response -def delete_submission(submission_id, is_hard_delete='false'): + +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}') + response = turnitin_api_handler( + "delete", f"submissions/{submission_id}/?hard={is_hard_delete}" + ) pretty_print_response(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') + response = turnitin_api_handler("put", f"submissions/{submission_id}/recover") pretty_print_response(response) @@ -179,14 +212,18 @@ def put_recover_submission(submission_id): # Similarity is a Turnithin report with an internet similarity detection # score of the student submissions. + 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) - pretty_print_response(response, 'REPORT GENERATION') + response = turnitin_api_handler( + "put", f"submissions/{submission_id}/similarity", payload + ) + pretty_print_response(response, "REPORT GENERATION") return response + def get_similarity_report_info(submission_id): """ Returns summary information about the requested Similarity Report. @@ -194,39 +231,50 @@ def get_similarity_report_info(submission_id): PROCESSING COMPLETE """ - response = turnitin_api_handler('get', f'submissions/{submission_id}/similarity') - pretty_print_response(response, 'REPORT STATUS') + response = turnitin_api_handler("get", f"submissions/{submission_id}/similarity") + pretty_print_response(response, "REPORT STATUS") 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) - pretty_print_response(response, 'URL VIEWER') + response = turnitin_api_handler( + "post", f"submissions/{submission_id}/viewer-url", payload + ) + pretty_print_response(response, "URL VIEWER") 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') + response = turnitin_api_handler( + "post", f"submissions/{submission_id}/similarity/pdf" + ) pretty_print_response(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}') + response = turnitin_api_handler( + "get", f"submissions/{submission_id}/similarity/pdf/{pdf_id}" + ) pretty_print_response(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') + response = turnitin_api_handler( + "get", f"submissions/{submission_id}/similarity/pdf/{pdf_id}/status" + ) pretty_print_response(response) - diff --git a/setup.py b/setup.py index adb1f67..db68a7e 100755 --- a/setup.py +++ b/setup.py @@ -150,8 +150,8 @@ def is_requirement(line): "lms.djangoapp": [ "platform_plugin_turnitin = platform_plugin_turnitin.apps:PlatformPluginTurnitinConfig" ], - 'xblock.v1': [ - 'turnitin = platform_plugin_turnitin.turnitin:TurnitinXBlock', + "xblock.v1": [ + "turnitin = platform_plugin_turnitin.turnitin:TurnitinXBlock", ], }, ) From 3d6c9db3d2ea7456d9aad0116b688a30481f3372 Mon Sep 17 00:00:00 2001 From: Fernando Date: Fri, 6 Oct 2023 15:29:58 -0400 Subject: [PATCH 07/12] fix: new related_name TurnitinSubmission model, docstings --- .../0002_alter_turnitinsubmission_user.py | 21 ++++++++++++++++ platform_plugin_turnitin/models.py | 17 +++++++++++-- platform_plugin_turnitin/settings/common.py | 8 +++--- .../settings/production.py | 16 ++++++------ .../static/html/turnitin.html | 23 ++++++----------- .../static/js/src/turnitin.js | 25 +++++++------------ platform_plugin_turnitin/turnitin.py | 3 +-- .../turnitin_client/handlers.py | 8 +++--- 8 files changed, 70 insertions(+), 51 deletions(-) create mode 100644 platform_plugin_turnitin/migrations/0002_alter_turnitinsubmission_user.py 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..8ab7e80 --- /dev/null +++ b/platform_plugin_turnitin/migrations/0002_alter_turnitinsubmission_user.py @@ -0,0 +1,21 @@ +# 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/models.py b/platform_plugin_turnitin/models.py index 29aa6fd..c499c83 100644 --- a/platform_plugin_turnitin/models.py +++ b/platform_plugin_turnitin/models.py @@ -2,12 +2,25 @@ Database models for platform_plugin_turnitin. """ -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.db import models +User = get_user_model() class TurnitinSubmission(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="submissions") + """ + 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. + + Methods: + - __str__: Returns a string representation of the submission, showing its ID and creation date. + """ + 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 e646cf8..fe12430 100644 --- a/platform_plugin_turnitin/settings/common.py +++ b/platform_plugin_turnitin/settings/common.py @@ -37,7 +37,7 @@ def plugin_settings(settings): # pylint: disable=unused-argument """ # Configuration variables - settings.TII_API_URL = "https://edunext.tii-sandbox.com" - settings.TCA_INTEGRATION_FAMILY = "MySweetLMS" - settings.TCA_INTEGRATION_VERSION = "3.2.4" - settings.TCA_API_KEY = "[SECRET_KEY_HERE]" + settings.TURNITIN_TII_API_URL = None + settings.TURNITIN_TCA_INTEGRATION_FAMILY = None + settings.TURNITIN_TCA_INTEGRATION_VERSION = None + settings.TURNITIN_TCA_API_KEY = None diff --git a/platform_plugin_turnitin/settings/production.py b/platform_plugin_turnitin/settings/production.py index 684344e..d4d3313 100644 --- a/platform_plugin_turnitin/settings/production.py +++ b/platform_plugin_turnitin/settings/production.py @@ -8,18 +8,18 @@ def plugin_settings(settings): # pylint: disable=unused-argument 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.TII_API_URL = getattr(settings, "ENV_TOKENS", {}).get( - "TII_API_URL", settings.TII_API_URL + settings.TURNITIN_TII_API_URL = getattr(settings, "ENV_TOKENS", {}).get( + "TURNITIN_TII_API_URL", settings.TURNITIN_TII_API_URL ) - settings.TCA_INTEGRATION_FAMILY = getattr(settings, "ENV_TOKENS", {}).get( - "TCA_INTEGRATION_FAMILY", settings.TCA_INTEGRATION_FAMILY + settings.TURNITIN_TCA_INTEGRATION_FAMILY = getattr(settings, "ENV_TOKENS", {}).get( + "TURNITIN_TCA_INTEGRATION_FAMILY", settings.TURNITIN_TCA_INTEGRATION_FAMILY ) - settings.TCA_INTEGRATION_VERSION = getattr(settings, "ENV_TOKENS", {}).get( - "TCA_INTEGRATION_VERSION", settings.TCA_INTEGRATION_VERSION + settings.TURNITIN_TCA_INTEGRATION_VERSION = getattr(settings, "ENV_TOKENS", {}).get( + "TURNITIN_TCA_INTEGRATION_VERSION", settings.TURNITIN_TCA_INTEGRATION_VERSION ) - settings.TCA_API_KEY = getattr(settings, "ENV_TOKENS", {}).get( - "TCA_API_KEY", settings.TCA_API_KEY + settings.TURNITIN_TCA_API_KEY = getattr(settings, "ENV_TOKENS", {}).get( + "TURNITIN_TCA_API_KEY", settings.TURNITIN_TCA_API_KEY ) diff --git a/platform_plugin_turnitin/static/html/turnitin.html b/platform_plugin_turnitin/static/html/turnitin.html index 4fdd9d2..86eb180 100644 --- a/platform_plugin_turnitin/static/html/turnitin.html +++ b/platform_plugin_turnitin/static/html/turnitin.html @@ -1,38 +1,34 @@
-

EULA Viewer

-
-

File Upload

- - Ningún archivo seleccionado + + No file selected
- +
-

File Processing Status

@@ -45,7 +41,6 @@

File Processing Status

-

Similarity Processing Status

@@ -58,13 +53,11 @@

Similarity Processing Status

- - -
+ diff --git a/platform_plugin_turnitin/static/js/src/turnitin.js b/platform_plugin_turnitin/static/js/src/turnitin.js index 6fa21eb..acedbd4 100644 --- a/platform_plugin_turnitin/static/js/src/turnitin.js +++ b/platform_plugin_turnitin/static/js/src/turnitin.js @@ -6,7 +6,7 @@ function TurnitinXBlock(runtime, element) { } function showEULA(htmlContent) { - $('#eulaModal .modal-content p').html(htmlContent); // Establece el contenido HTML en el párrafo del modal + $('#eulaModal .modal-content p').html(htmlContent); $('#eulaModal').show(); } @@ -68,17 +68,15 @@ function TurnitinXBlock(runtime, element) { $('.modal-content button').click(closeModal); - ////////////////////// UPLOAD FILE CSS - $('#uploadBtn', element).click(function(event) { - event.preventDefault(); // Evitar que el formulario se envíe por defecto + event.preventDefault(); var handlerUrl = runtime.handlerUrl(element, 'upload_turnitin_submission_file'); var fileInput = $('#file')[0]; var file = fileInput.files[0]; if (!file) { - alert('Por favor, selecciona un archivo .doc o .docx primero.'); + alert('Please select .doc or .docx files.'); return; } @@ -112,27 +110,23 @@ function TurnitinXBlock(runtime, element) { if (fileExtension === 'doc' || fileExtension === 'docx') { if (fileName) { $(this).siblings('.selected-filename').text(fileName); - $(this).siblings('label').addClass('file-selected').text('Cambiar archivo'); + $(this).siblings('label').addClass('file-selected').text('Change File'); uploadButton.prop('disabled', false); } else { - $(this).siblings('.selected-filename').text('Ningún archivo seleccionado'); - $(this).siblings('label').removeClass('file-selected').text('Elegir archivo'); + $(this).siblings('.selected-filename').text('No file selected'); + $(this).siblings('label').removeClass('file-selected').text('Choose File'); uploadButton.prop('disabled', true); } } else { - alert('Por favor, selecciona un archivo .doc o .docx'); + alert('Please, select .doc or .docx files'); $(this).val(''); - $(this).siblings('.selected-filename').text('Ningún archivo seleccionado'); - $(this).siblings('label').removeClass('file-selected').text('Elegir archivo'); + $(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); - - -//////////////////////////////////////////////////// SIMILARITY - $('#refreshBtn1', element).click(function(event) { event.preventDefault(); var handlerUrl = runtime.handlerUrl(element, 'get_submission_status'); @@ -156,7 +150,6 @@ function TurnitinXBlock(runtime, element) { }); - $('#generateReportBtn1', element).click(function(event) { event.preventDefault(); var handlerUrl = runtime.handlerUrl(element, 'generate_similarity_report'); diff --git a/platform_plugin_turnitin/turnitin.py b/platform_plugin_turnitin/turnitin.py index dc07963..ba2704b 100644 --- a/platform_plugin_turnitin/turnitin.py +++ b/platform_plugin_turnitin/turnitin.py @@ -96,12 +96,11 @@ def get_user_data(self): """ user_service = self.runtime.service(self, "user") current_user = user_service.get_current_user() - data = { + return { "user_id": current_user.opt_attrs["edx-platform.user_id"], "user_email": current_user.emails[0], "user_name": current_user.full_name.split(), } - return data @XBlock.json_handler def get_eula_agreement(self, data, suffix=""): diff --git a/platform_plugin_turnitin/turnitin_client/handlers.py b/platform_plugin_turnitin/turnitin_client/handlers.py index 9dc9e83..f1e674b 100644 --- a/platform_plugin_turnitin/turnitin_client/handlers.py +++ b/platform_plugin_turnitin/turnitin_client/handlers.py @@ -29,10 +29,10 @@ def turnitin_api_handler( """ # Configuration variables - TII_API_URL = getattr(settings, "TII_API_URL", None) - TCA_INTEGRATION_FAMILY = getattr(settings, "TCA_INTEGRATION_FAMILY", None) - TCA_INTEGRATION_VERSION = getattr(settings, "TCA_INTEGRATION_VERSION", None) - TCA_API_KEY = getattr(settings, "TCA_API_KEY", None) + 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) # Headers configuration headers = { From d99be8795a16a8db2fe7eacf1319f2adee63b251 Mon Sep 17 00:00:00 2001 From: Fernando Date: Mon, 9 Oct 2023 16:00:00 -0400 Subject: [PATCH 08/12] fix: handlers refactor and format --- .../0002_alter_turnitinsubmission_user.py | 13 +- platform_plugin_turnitin/models.py | 6 +- .../static/html/turnitin.html | 2 +- .../static/js/src/turnitin.js | 2 +- platform_plugin_turnitin/turnitin.py | 37 +-- .../turnitin_client/handlers.py | 280 ------------------ .../turnitin_client/handlers/__init__.py | 21 ++ .../turnitin_client/handlers/api_handler.py | 82 +++++ .../turnitin_client/handlers/eula.py | 38 +++ .../handlers/similarity_reports.py | 69 +++++ .../turnitin_client/handlers/submissions.py | 62 ++++ .../turnitin_client/handlers/utils.py | 13 + setup.py | 3 + 13 files changed, 318 insertions(+), 310 deletions(-) delete mode 100644 platform_plugin_turnitin/turnitin_client/handlers.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 create mode 100644 platform_plugin_turnitin/turnitin_client/handlers/utils.py diff --git a/platform_plugin_turnitin/migrations/0002_alter_turnitinsubmission_user.py b/platform_plugin_turnitin/migrations/0002_alter_turnitinsubmission_user.py index 8ab7e80..1c76869 100644 --- a/platform_plugin_turnitin/migrations/0002_alter_turnitinsubmission_user.py +++ b/platform_plugin_turnitin/migrations/0002_alter_turnitinsubmission_user.py @@ -6,16 +6,19 @@ class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('platform_plugin_turnitin', '0001_initial'), + ("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), + 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/models.py b/platform_plugin_turnitin/models.py index c499c83..779f20e 100644 --- a/platform_plugin_turnitin/models.py +++ b/platform_plugin_turnitin/models.py @@ -7,6 +7,7 @@ User = get_user_model() + class TurnitinSubmission(models.Model): """ Represents a submission to Turnitin. @@ -20,7 +21,10 @@ class TurnitinSubmission(models.Model): Methods: - __str__: Returns a string representation of the submission, showing its ID and creation date. """ - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="turnitin_submissions") + + 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/static/html/turnitin.html b/platform_plugin_turnitin/static/html/turnitin.html index 86eb180..db1053c 100644 --- a/platform_plugin_turnitin/static/html/turnitin.html +++ b/platform_plugin_turnitin/static/html/turnitin.html @@ -19,7 +19,7 @@

End User License Agreement (EULA)

File Upload

- + No file selected
diff --git a/platform_plugin_turnitin/static/js/src/turnitin.js b/platform_plugin_turnitin/static/js/src/turnitin.js index acedbd4..13ab409 100644 --- a/platform_plugin_turnitin/static/js/src/turnitin.js +++ b/platform_plugin_turnitin/static/js/src/turnitin.js @@ -81,7 +81,7 @@ function TurnitinXBlock(runtime, element) { } var formData = new FormData(); - formData.append('myfile', file); + formData.append('uploaded_file', file); $.ajax({ type: 'POST', diff --git a/platform_plugin_turnitin/turnitin.py b/platform_plugin_turnitin/turnitin.py index ba2704b..adc2702 100644 --- a/platform_plugin_turnitin/turnitin.py +++ b/platform_plugin_turnitin/turnitin.py @@ -2,6 +2,7 @@ import json from datetime import datetime +from http import HTTPStatus import pkg_resources from django.contrib.auth.models import User @@ -96,10 +97,14 @@ def get_user_data(self): """ user_service = self.runtime.service(self, "user") current_user = user_service.get_current_user() + 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], - "user_name": current_user.full_name.split(), + "user_name": user_full_name[0] if user_full_name else "no_name", + "user_last_name": " ".join(user_full_name[1:]) + if len(user_full_name) > 1 + else "no_last_name", } @XBlock.json_handler @@ -147,7 +152,6 @@ def create_turnitin_submission_object(self): Response: The response from the Turnitin submission API. """ user_data = self.get_user_data() - user_name = user_data["user_name"] date_now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") payload = { @@ -161,19 +165,15 @@ def create_turnitin_submission_object(self): "owners": [ { "id": user_data["user_id"], - "given_name": user_name[0] if user_name else "no_name", - "family_name": " ".join(user_name[1:]) - if len(user_name) > 1 - else "no_last_name", + "given_name": user_data["user_name"], + "family_name": user_data["user_last_name"], "email": user_data["user_email"], } ], "submitter": { "id": user_data["user_id"], - "given_name": user_name[0] if user_name else "no_name", - "family_name": " ".join(user_name[1:]) - if len(user_name) > 1 - else "no_last_name", + "given_name": user_data["user_name"], + "family_name": user_data["user_last_name"], "email": user_data["user_email"], }, "original_submitted_time": date_now, @@ -194,7 +194,7 @@ def upload_turnitin_submission_file(self, data, suffix=""): Response: The response after uploading the file to Turnitin. """ turnitin_submission = self.create_turnitin_submission_object() - if turnitin_submission.status_code == 201: + if turnitin_submission.status_code == HTTPStatus.CREATED: turnitin_submission_id = turnitin_submission.json()["id"] current_user_id = self.get_user_data()["user_id"] current_user = User.objects.get(id=current_user_id) @@ -202,10 +202,9 @@ def upload_turnitin_submission_file(self, data, suffix=""): user=current_user, turnitin_submission_id=turnitin_submission_id ) submission.save() - myfile = data.params["myfile"].file - # turnitin_submission_id='0a966646-83f9-4ce6-aa47-71e07baf4e30' + uploaded_file = data.params["uploaded_file"].file response = put_upload_submission_file_content( - turnitin_submission_id, myfile + turnitin_submission_id, uploaded_file ) return Response( json.dumps(response.json()), @@ -238,7 +237,6 @@ def get_submission_status(self, data, suffix=""): ).latest("created_at") except TurnitinSubmission.DoesNotExist: return {"success": False} - # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' response = get_submission_info(last_submission.turnitin_submission_id) return response.json() @@ -296,7 +294,6 @@ def generate_similarity_report(self, data, suffix=""): ).latest("created_at") except TurnitinSubmission.DoesNotExist: return {"success": False} - # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' response = put_generate_similarity_report( last_submission.turnitin_submission_id, payload ) @@ -322,7 +319,6 @@ def get_similarity_report_status(self, data, suffix=""): ).latest("created_at") except TurnitinSubmission.DoesNotExist: return {"success": False} - # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' response = get_similarity_report_info(last_submission.turnitin_submission_id) return response.json() @@ -355,10 +351,8 @@ def create_similarity_viewer(self, data, suffix=""): "view_settings": {"save_changes": True}, }, "author_metadata_override": { - "family_name": " ".join(user_name[1:]) - if len(user_name) > 1 - else "no_last_name", - "given_name": user_name[0] if user_name else "no_name", + "family_name": user_data["user_last_name"], + "given_name": user_data["user_name"], }, "sidebar": {"default_mode": "similarity"}, } @@ -369,7 +363,6 @@ def create_similarity_viewer(self, data, suffix=""): ).latest("created_at") except TurnitinSubmission.DoesNotExist: return {"success": False} - # last_submission = 'de6784c5-471f-4220-aff1-16b6b44dffcf' response = post_create_viewer_launch_url( last_submission.turnitin_submission_id, payload ) diff --git a/platform_plugin_turnitin/turnitin_client/handlers.py b/platform_plugin_turnitin/turnitin_client/handlers.py deleted file mode 100644 index f1e674b..0000000 --- a/platform_plugin_turnitin/turnitin_client/handlers.py +++ /dev/null @@ -1,280 +0,0 @@ -import json -from typing import Dict, Optional - -import requests - -from django.conf import settings - - -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. - - Raises: - - ValueError: If an unsupported request method is provided. - """ - - # Configuration variables - 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) - - # Headers configuration - headers = { - "X-Turnitin-Integration-Name": TCA_INTEGRATION_FAMILY, - "X-Turnitin-Integration-Version": TCA_INTEGRATION_VERSION, - "Authorization": f"Bearer {TCA_API_KEY}", - } - - # Add Content-Type for methods that typically send JSON payload - 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 - ) - return response - - # Mapping of methods to corresponding functions in the requests library - method_map = { - "get": requests.get, - "post": requests.post, - "put": requests.put, - "delete": requests.delete, - "patch": requests.patch, - } - - # Retrieving the correct function - method_func = method_map.get(request_method.lower()) - - if not method_func: - raise ValueError(f"Unsupported request method: {request_method}") - - # Calling the appropriate method - if request_method.lower() in ["post", "put", "patch"]: - response = method_func( - f"{TII_API_URL}/api/v1/{url_prefix}", headers=headers, json=data - ) - else: - response = method_func( - f"{TII_API_URL}/api/v1/{url_prefix}", headers=headers, params=data - ) - - return response - - -def pretty_print_response(response, type_of=""): - """debug helper function""" - content = response.json() - print("\n\n") - print(f"------{type_of}------") - print("\n\n") - print(json.dumps(content, indent=4)) - print("\n\n") - print("------------") - print("\n\n") - - -# Returns all the features enabled in Turnitin account. -def get_features_enabled(): - """ - Returns all the features enabled in the Turnitin account. - """ - response = turnitin_api_handler("get", "features-enabled") - pretty_print_response(response) - - -# *---EULA endpoints ---* -# The EULA is a page of terms and conditions that the owner and the -# submiter has to accept in order to send a file to Turnitin. - - -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}") - pretty_print_response(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) - pretty_print_response(response, "ACCEPT EULA") - 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}") - pretty_print_response(response) - - -# *---Submissions endpoints ---* -# Submissions is a Turnithin model that has all relative info to a Assessment send by -# an student. - - -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) - pretty_print_response(response, "CREATE SUBMISSION") - 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, - ) - pretty_print_response(response, "UPLOAD 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}") - pretty_print_response(response, "SUBMISSION STATUS") - 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}" - ) - pretty_print_response(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") - pretty_print_response(response) - - -# *---Similarity endpoints ---* -# Similarity is a Turnithin report with an internet similarity detection -# score of the student submissions. - - -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 - ) - pretty_print_response(response, "REPORT GENERATION") - 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") - pretty_print_response(response, "REPORT STATUS") - 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 - ) - pretty_print_response(response, "URL VIEWER") - 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" - ) - pretty_print_response(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}" - ) - pretty_print_response(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" - ) - pretty_print_response(response) 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..16e2230 --- /dev/null +++ b/platform_plugin_turnitin/turnitin_client/handlers/__init__.py @@ -0,0 +1,21 @@ +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..5cbd94f --- /dev/null +++ b/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py @@ -0,0 +1,82 @@ +from typing import Dict, Optional + +import requests +from django.conf import settings + +from .utils import pretty_print_response + +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 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. + + Raises: + - ValueError: If an unsupported request method is provided. + """ + 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 + ) + return response + + 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}") + + if request_method.lower() in ["post", "put", "patch"]: + response = method_func( + f"{TII_API_URL}/api/v1/{url_prefix}", headers=headers, json=data + ) + else: + response = method_func( + f"{TII_API_URL}/api/v1/{url_prefix}", headers=headers, params=data + ) + + return response + + +def get_features_enabled(): + """ + Returns all the features enabled in the Turnitin account. + """ + response = turnitin_api_handler("get", "features-enabled") + pretty_print_response(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..25e79d1 --- /dev/null +++ b/platform_plugin_turnitin/turnitin_client/handlers/eula.py @@ -0,0 +1,38 @@ +from .api_handler import turnitin_api_handler +from .utils import pretty_print_response + + +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}") + pretty_print_response(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) + pretty_print_response(response, "ACCEPT EULA") + 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}") + pretty_print_response(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..2f61ef3 --- /dev/null +++ b/platform_plugin_turnitin/turnitin_client/handlers/similarity_reports.py @@ -0,0 +1,69 @@ +from .api_handler import turnitin_api_handler +from .utils import pretty_print_response + + +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 + ) + pretty_print_response(response, "REPORT GENERATION") + 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") + pretty_print_response(response, "REPORT STATUS") + 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 + ) + pretty_print_response(response, "URL VIEWER") + 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" + ) + pretty_print_response(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}" + ) + pretty_print_response(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" + ) + pretty_print_response(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..369b68c --- /dev/null +++ b/platform_plugin_turnitin/turnitin_client/handlers/submissions.py @@ -0,0 +1,62 @@ +from .api_handler import turnitin_api_handler +from .utils import pretty_print_response + + +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) + pretty_print_response(response, "CREATE SUBMISSION") + 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, + ) + pretty_print_response(response, "UPLOAD 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}") + pretty_print_response(response, "SUBMISSION STATUS") + 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}" + ) + pretty_print_response(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") + pretty_print_response(response) diff --git a/platform_plugin_turnitin/turnitin_client/handlers/utils.py b/platform_plugin_turnitin/turnitin_client/handlers/utils.py new file mode 100644 index 0000000..8da4971 --- /dev/null +++ b/platform_plugin_turnitin/turnitin_client/handlers/utils.py @@ -0,0 +1,13 @@ +import json + + +def pretty_print_response(response, type_of=""): + """debug helper function""" + content = response.json() + print("\n\n") + print(f"------{type_of}------") + print("\n\n") + print(json.dumps(content, indent=4)) + print("\n\n") + print("------------") + print("\n\n") diff --git a/setup.py b/setup.py index db68a7e..12578a7 100755 --- a/setup.py +++ b/setup.py @@ -150,6 +150,9 @@ 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", ], From 23a207b8180376c2cbccfd50ab4055ba09c3c38f Mon Sep 17 00:00:00 2001 From: Fernando Date: Tue, 10 Oct 2023 12:45:16 -0400 Subject: [PATCH 09/12] fix: code upgrades - turnitin:get_user_data() : delete 'user_' prefix. - turnitin:get_django_user(): method created. - api_hander: get_request_method_func(): method created. - api_handler: turnitin_api_handler(): changes abour args. --- platform_plugin_turnitin/turnitin.py | 44 +++++++------- .../turnitin_client/handlers/api_handler.py | 57 ++++++++++++------- 2 files changed, 56 insertions(+), 45 deletions(-) diff --git a/platform_plugin_turnitin/turnitin.py b/platform_plugin_turnitin/turnitin.py index adc2702..0e7b69c 100644 --- a/platform_plugin_turnitin/turnitin.py +++ b/platform_plugin_turnitin/turnitin.py @@ -97,16 +97,23 @@ def get_user_data(self): """ user_service = self.runtime.service(self, "user") current_user = user_service.get_current_user() - user_full_name = current_user.full_name.split() + full_name = current_user.full_name.split() return { "user_id": current_user.opt_attrs["edx-platform.user_id"], "user_email": current_user.emails[0], - "user_name": user_full_name[0] if user_full_name else "no_name", - "user_last_name": " ".join(user_full_name[1:]) - if len(user_full_name) > 1 + "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): + """ + Returns 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=""): """ @@ -165,15 +172,15 @@ def create_turnitin_submission_object(self): "owners": [ { "id": user_data["user_id"], - "given_name": user_data["user_name"], - "family_name": user_data["user_last_name"], + "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["user_name"], - "family_name": user_data["user_last_name"], + "given_name": user_data["name"], + "family_name": user_data["last_name"], "email": user_data["user_email"], }, "original_submitted_time": date_now, @@ -196,8 +203,7 @@ def upload_turnitin_submission_file(self, data, suffix=""): turnitin_submission = self.create_turnitin_submission_object() if turnitin_submission.status_code == HTTPStatus.CREATED: turnitin_submission_id = turnitin_submission.json()["id"] - current_user_id = self.get_user_data()["user_id"] - current_user = User.objects.get(id=current_user_id) + current_user = self.get_django_user() submission = TurnitinSubmission( user=current_user, turnitin_submission_id=turnitin_submission_id ) @@ -208,13 +214,9 @@ def upload_turnitin_submission_file(self, data, suffix=""): ) return Response( json.dumps(response.json()), - content_type="application/json", - charset="UTF-8", ) return Response( json.dumps(turnitin_submission.json()), - content_type="application/json", - charset="UTF-8", ) @XBlock.json_handler @@ -229,8 +231,7 @@ def get_submission_status(self, data, suffix=""): Returns: dict: Information related to the user's latest Turnitin submission. """ - current_user_id = self.get_user_data()["user_id"] - current_user = User.objects.get(id=current_user_id) + current_user = self.get_django_user() try: last_submission = TurnitinSubmission.objects.filter( user=current_user @@ -286,8 +287,7 @@ def generate_similarity_report(self, data, suffix=""): "exclude_submitted_works": False, }, } - current_user_id = self.get_user_data()["user_id"] - current_user = User.objects.get(id=current_user_id) + current_user = self.get_django_user() try: last_submission = TurnitinSubmission.objects.filter( user=current_user @@ -311,8 +311,7 @@ def get_similarity_report_status(self, data, suffix=""): Returns: dict: Information related to the status of the similarity report. """ - current_user_id = self.get_user_data()["user_id"] - current_user = User.objects.get(id=current_user_id) + current_user = self.get_django_user() try: last_submission = TurnitinSubmission.objects.filter( user=current_user @@ -335,7 +334,6 @@ def create_similarity_viewer(self, data, suffix=""): dict: Contains the URL for the similarity viewer. """ user_data = self.get_user_data() - user_name = user_data["user_name"] payload = { "viewer_user_id": user_data["user_id"], "locale": "en-EN", @@ -351,8 +349,8 @@ def create_similarity_viewer(self, data, suffix=""): "view_settings": {"save_changes": True}, }, "author_metadata_override": { - "family_name": user_data["user_last_name"], - "given_name": user_data["user_name"], + "family_name": user_data["last_name"], + "given_name": user_data["name"], }, "sidebar": {"default_mode": "similarity"}, } diff --git a/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py b/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py index 5cbd94f..b37b1fb 100644 --- a/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py +++ b/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py @@ -11,6 +11,33 @@ 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 = "", @@ -28,9 +55,6 @@ def turnitin_api_handler( Returns: - Response: A requests.Response object containing the server's response to the request. - - Raises: - - ValueError: If an unsupported request method is provided. """ headers = { "X-Turnitin-Integration-Name": TCA_INTEGRATION_FAMILY, @@ -49,27 +73,16 @@ def turnitin_api_handler( ) return response - 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()) + method_func = get_request_method_func(request_method) - if not method_func: - raise ValueError(f"Unsupported request method: {request_method}") + args = { + "headers": headers, + "json" + if request_method.lower() in ["post", "put", "patch"] + else "params": data, + } - if request_method.lower() in ["post", "put", "patch"]: - response = method_func( - f"{TII_API_URL}/api/v1/{url_prefix}", headers=headers, json=data - ) - else: - response = method_func( - f"{TII_API_URL}/api/v1/{url_prefix}", headers=headers, params=data - ) + response = method_func(f"{TII_API_URL}/api/v1/{url_prefix}", **args) return response From 1a5511e3e9a95d3cb6bbc62f95145b3ed4504128 Mon Sep 17 00:00:00 2001 From: Cristhian Garcia Date: Tue, 10 Oct 2023 14:13:17 -0500 Subject: [PATCH 10/12] chore: reformat for quality checks --- platform_plugin_turnitin/apps.py | 4 +- platform_plugin_turnitin/models.py | 6 +- platform_plugin_turnitin/settings/common.py | 33 +++++- .../settings/production.py | 10 +- platform_plugin_turnitin/turnitin.py | 104 +++++++----------- .../turnitin_client/handlers/__init__.py | 8 +- .../turnitin_client/handlers/api_handler.py | 10 +- .../turnitin_client/handlers/eula.py | 9 +- .../handlers/similarity_reports.py | 13 +-- .../turnitin_client/handlers/submissions.py | 11 +- .../turnitin_client/handlers/utils.py | 13 --- requirements/base.in | 1 + requirements/base.txt | 28 +++-- requirements/dev.txt | 28 ++++- requirements/doc.txt | 23 ++-- requirements/quality.txt | 25 ++++- requirements/test.txt | 25 ++++- 17 files changed, 209 insertions(+), 142 deletions(-) delete mode 100644 platform_plugin_turnitin/turnitin_client/handlers/utils.py diff --git a/platform_plugin_turnitin/apps.py b/platform_plugin_turnitin/apps.py index 323b062..e4acc1e 100644 --- a/platform_plugin_turnitin/apps.py +++ b/platform_plugin_turnitin/apps.py @@ -32,4 +32,6 @@ def ready(self) -> None: Perform application initialization once the Django platform has been initialized. """ super().ready() - from platform_plugin_turnitin.turnitin import TurnitinXBlock + from platform_plugin_turnitin.turnitin import ( # no-qa pylint: disable=import-outside-toplevel,unused-import + TurnitinXBlock, + ) diff --git a/platform_plugin_turnitin/models.py b/platform_plugin_turnitin/models.py index 779f20e..a2f1d60 100644 --- a/platform_plugin_turnitin/models.py +++ b/platform_plugin_turnitin/models.py @@ -18,8 +18,7 @@ class TurnitinSubmission(models.Model): - 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. - Methods: - - __str__: Returns a string representation of the submission, showing its ID and creation date. + .. no_pii: """ user = models.ForeignKey( @@ -28,6 +27,3 @@ class TurnitinSubmission(models.Model): 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) - - def __str__(self): - return f"Submission: {self.turnitin_submission_id or 'Not Set'} - created at: {self.created_at}" diff --git a/platform_plugin_turnitin/settings/common.py b/platform_plugin_turnitin/settings/common.py index fe12430..1a81117 100644 --- a/platform_plugin_turnitin/settings/common.py +++ b/platform_plugin_turnitin/settings/common.py @@ -30,7 +30,7 @@ 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 @@ -41,3 +41,34 @@ def plugin_settings(settings): # pylint: disable=unused-argument 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 d4d3313..d861b05 100644 --- a/platform_plugin_turnitin/settings/production.py +++ b/platform_plugin_turnitin/settings/production.py @@ -3,7 +3,7 @@ """ -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 @@ -23,3 +23,11 @@ def plugin_settings(settings): # pylint: disable=unused-argument 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/turnitin.py b/platform_plugin_turnitin/turnitin.py index 0e7b69c..3fb5f22 100644 --- a/platform_plugin_turnitin/turnitin.py +++ b/platform_plugin_turnitin/turnitin.py @@ -5,14 +5,15 @@ from http import HTTPStatus import pkg_resources -from django.contrib.auth.models import User +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.fields import Integer, Scope 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, @@ -24,7 +25,7 @@ put_upload_submission_file_content, ) -from .models import TurnitinSubmission +User = get_user_model() @XBlock.needs("user") @@ -44,8 +45,7 @@ def resource_string(self, path): def studio_view(self, context=None): """ - The primary view of the TurnitinXBlock, shown to students - when viewing courses. + Show primary view of the TurnitinXBlock, shown to students when viewing courses. """ if context: pass # TO-DO: do something based on the context. @@ -67,8 +67,7 @@ def studio_view(self, context=None): # TO-DO: change this view to display your data your own way. def student_view(self, context=None): """ - The primary view of the TurnitinXBlock, shown to students - when viewing courses. + Show primary view of the TurnitinXBlock, shown to students when viewing courses. """ if context: pass # TO-DO: do something based on the context. @@ -87,10 +86,9 @@ def student_view(self, context=None): frag.initialize_js("TurnitinXBlock") return frag - # ---------------------------------------------------------------------------- def get_user_data(self): """ - Fetches user-related data, including user ID, email, and name. + Fetch user-related data, including user ID, email, and name. Returns: dict: A dictionary containing user ID, email, and name. @@ -109,15 +107,15 @@ def get_user_data(self): def get_django_user(self): """ - Returns the django user. + 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=""): + def get_eula_agreement(self, data, suffix=""): # pylint: disable=unused-argument """ - Fetches the End User License Agreement (EULA) content. + Fetch the End User License Agreement (EULA) content. Args: data (dict): Input data for the request. @@ -130,9 +128,9 @@ def get_eula_agreement(self, data, suffix=""): return {"html": response.text, "status": response.status_code} @XBlock.json_handler - def accept_eula_agreement(self, data, suffix=""): + def accept_eula_agreement(self, data, suffix=""): # pylint: disable=unused-argument """ - Submits acceptance of the EULA for the current user. + Submit acceptance of the EULA for the current user. Args: data (dict): Input data for the request. @@ -153,7 +151,7 @@ def accept_eula_agreement(self, data, suffix=""): def create_turnitin_submission_object(self): """ - Constructs a Turnitin submission object based on the user's data. + Create a Turnitin submission object based on the user's data. Returns: Response: The response from the Turnitin submission API. @@ -163,7 +161,7 @@ def create_turnitin_submission_object(self): payload = { "owner": user_data["user_id"], - "title": self.location.block_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", @@ -189,9 +187,11 @@ def create_turnitin_submission_object(self): return post_create_submission(payload) @XBlock.handler - def upload_turnitin_submission_file(self, data, suffix=""): + def upload_turnitin_submission_file( + self, data, suffix="" + ): # pylint: disable=unused-argument """ - Handles the upload of the user's file to Turnitin. + Handle the upload of the user's file to Turnitin. Args: data (WebRequest): Web request containing the file to be uploaded. @@ -220,9 +220,9 @@ def upload_turnitin_submission_file(self, data, suffix=""): ) @XBlock.json_handler - def get_submission_status(self, data, suffix=""): + def get_submission_status(self, data, suffix=""): # pylint: disable=unused-argument """ - Retrieves the status of the latest Turnitin submission for the user. + Retrieve the status of the latest Turnitin submission for the user. Args: data (dict): Input data for the request. @@ -242,9 +242,11 @@ def get_submission_status(self, data, suffix=""): return response.json() @XBlock.json_handler - def generate_similarity_report(self, data, suffix=""): + def generate_similarity_report( + self, data, suffix="" + ): # pylint: disable=unused-argument """ - Initiates the generation of a similarity report for the user's latest Turnitin submission. + Initialize the generation of a similarity report for the user's latest Turnitin submission. Args: data (dict): Input data for the request. @@ -253,40 +255,7 @@ def generate_similarity_report(self, data, suffix=""): Returns: dict: The status of the similarity report generation process. """ - - payload = { - "indexing_settings": {"add_to_index": True}, - "generation_settings": { - "search_repositories": [ - "INTERNET", - "SUBMITTED_WORK", - "PUBLICATION", - "CROSSREF", - "CROSSREF_POSTED_CONTENT", - ], - "submission_auto_excludes": [ - "b84b77d1-da0f-4f45-b002-8aec4f4796d6", - "b86de142-bc44-4f95-8467-84af12b89217", - ], - "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, - }, - } + payload = getattr(settings, "TURNITIN_SIMILARY_REPORT_PAYLOAD") # pylint: disable=literal-used-as-attribute current_user = self.get_django_user() try: last_submission = TurnitinSubmission.objects.filter( @@ -300,9 +269,11 @@ def generate_similarity_report(self, data, suffix=""): return response.json() @XBlock.json_handler - def get_similarity_report_status(self, data, suffix=""): + def get_similarity_report_status( + self, data, suffix="" + ): # pylint: disable=unused-argument """ - Retrieves the status of the similarity report for the user's latest Turnitin submission. + Retrieve the status of the similarity report for the user's latest Turnitin submission. Args: data (dict): Input data for the request. @@ -322,9 +293,11 @@ def get_similarity_report_status(self, data, suffix=""): return response.json() @XBlock.json_handler - def create_similarity_viewer(self, data, suffix=""): + def create_similarity_viewer( + self, data, suffix="" + ): # pylint: disable=unused-argument """ - Creates a Turnitin similarity viewer for the user's latest submission. + Create a Turnitin similarity viewer for the user's latest submission. Args: data (dict): Input data for the request. @@ -366,13 +339,9 @@ def create_similarity_viewer(self, data, suffix=""): ) return response.json() - # ---------------------------------------------------------------------------- - - # TO-DO: change this to create the scenarios you'd like to see in the - # workbench while developing your XBlock. @staticmethod def workbench_scenarios(): - """A canned scenario for display in the workbench.""" + """Define a workbench scenarios.""" return [ ( "TurnitinXBlock", @@ -393,7 +362,8 @@ def workbench_scenarios(): @staticmethod def _get_statici18n_js_url(): """ - Returns the Javascript translation file for the currently selected language, if any. + Return the Javascript translation file for the currently selected language, if any. + Defaults to English if available. """ locale_code = translation.get_language() @@ -412,6 +382,6 @@ def _get_statici18n_js_url(): @staticmethod def get_dummy(): """ - Dummy method to generate initial i18n + Return a dummy translation to generate initial i18n. """ return translation.gettext_noop("Dummy") diff --git a/platform_plugin_turnitin/turnitin_client/handlers/__init__.py b/platform_plugin_turnitin/turnitin_client/handlers/__init__.py index 16e2230..f2ea4e8 100644 --- a/platform_plugin_turnitin/turnitin_client/handlers/__init__.py +++ b/platform_plugin_turnitin/turnitin_client/handlers/__init__.py @@ -1,9 +1,5 @@ -from .eula import ( - get_eula_acceptance_by_user, - get_eula_page, - get_eula_version_info, - post_accept_eula_version, -) +"""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, diff --git a/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py b/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py index b37b1fb..c36c9b1 100644 --- a/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py +++ b/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py @@ -1,10 +1,11 @@ +""" +API handlers for turnitin integration +""" from typing import Dict, Optional import requests from django.conf import settings -from .utils import pretty_print_response - 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) @@ -69,7 +70,8 @@ def turnitin_api_handler( 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 + f"{TII_API_URL}/api/v1/{url_prefix}", headers=headers, data=uploaded_file, + timeout=settings.TURNITIN_API_TIMEOUT ) return response @@ -92,4 +94,4 @@ def get_features_enabled(): Returns all the features enabled in the Turnitin account. """ response = turnitin_api_handler("get", "features-enabled") - pretty_print_response(response) + return response diff --git a/platform_plugin_turnitin/turnitin_client/handlers/eula.py b/platform_plugin_turnitin/turnitin_client/handlers/eula.py index 25e79d1..07c9645 100644 --- a/platform_plugin_turnitin/turnitin_client/handlers/eula.py +++ b/platform_plugin_turnitin/turnitin_client/handlers/eula.py @@ -1,5 +1,7 @@ +""" +EULA handlers for turnitin xblock +""" from .api_handler import turnitin_api_handler -from .utils import pretty_print_response def get_eula_version_info(version: str = "latest", language: str = "EN"): @@ -9,7 +11,7 @@ def get_eula_version_info(version: str = "latest", language: str = "EN"): have to accept in order to send a file to Turnitin. """ response = turnitin_api_handler("get", f"eula/{version}?lang={language}") - pretty_print_response(response) + return response def get_eula_page(version: str = "v1beta", language: str = "en-US"): @@ -26,7 +28,6 @@ def post_accept_eula_version(payload, version: str = "v1beta"): This method should be invoked after the user has viewed the EULA content. """ response = turnitin_api_handler("post", f"eula/{version}/accept", payload) - pretty_print_response(response, "ACCEPT EULA") return response @@ -35,4 +36,4 @@ 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}") - pretty_print_response(response) + return response diff --git a/platform_plugin_turnitin/turnitin_client/handlers/similarity_reports.py b/platform_plugin_turnitin/turnitin_client/handlers/similarity_reports.py index 2f61ef3..e15ab1a 100644 --- a/platform_plugin_turnitin/turnitin_client/handlers/similarity_reports.py +++ b/platform_plugin_turnitin/turnitin_client/handlers/similarity_reports.py @@ -1,5 +1,7 @@ +""" +Similarity reports handlers +""" from .api_handler import turnitin_api_handler -from .utils import pretty_print_response def put_generate_similarity_report(submission_id, payload): @@ -9,7 +11,6 @@ def put_generate_similarity_report(submission_id, payload): response = turnitin_api_handler( "put", f"submissions/{submission_id}/similarity", payload ) - pretty_print_response(response, "REPORT GENERATION") return response @@ -21,7 +22,6 @@ def get_similarity_report_info(submission_id): COMPLETE """ response = turnitin_api_handler("get", f"submissions/{submission_id}/similarity") - pretty_print_response(response, "REPORT STATUS") return response @@ -34,7 +34,6 @@ def post_create_viewer_launch_url(submission_id, payload): response = turnitin_api_handler( "post", f"submissions/{submission_id}/viewer-url", payload ) - pretty_print_response(response, "URL VIEWER") return response @@ -46,7 +45,7 @@ def post_generate_similarity_report_pdf(submission_id): response = turnitin_api_handler( "post", f"submissions/{submission_id}/similarity/pdf" ) - pretty_print_response(response) + return response def get_similarity_report_pdf(submission_id, pdf_id): @@ -56,7 +55,7 @@ def get_similarity_report_pdf(submission_id, pdf_id): response = turnitin_api_handler( "get", f"submissions/{submission_id}/similarity/pdf/{pdf_id}" ) - pretty_print_response(response) + return response def get_similarity_report_pdf_status(submission_id, pdf_id): @@ -66,4 +65,4 @@ def get_similarity_report_pdf_status(submission_id, pdf_id): response = turnitin_api_handler( "get", f"submissions/{submission_id}/similarity/pdf/{pdf_id}/status" ) - pretty_print_response(response) + return response diff --git a/platform_plugin_turnitin/turnitin_client/handlers/submissions.py b/platform_plugin_turnitin/turnitin_client/handlers/submissions.py index 369b68c..90de133 100644 --- a/platform_plugin_turnitin/turnitin_client/handlers/submissions.py +++ b/platform_plugin_turnitin/turnitin_client/handlers/submissions.py @@ -1,5 +1,7 @@ +""" +Submissions hanlders +""" from .api_handler import turnitin_api_handler -from .utils import pretty_print_response def post_create_submission(payload): @@ -9,7 +11,6 @@ def post_create_submission(payload): related to an assessment sent by a student. """ response = turnitin_api_handler("post", "submissions", payload) - pretty_print_response(response, "CREATE SUBMISSION") return response @@ -23,7 +24,6 @@ def put_upload_submission_file_content(submission_id, file): is_upload=True, uploaded_file=file, ) - pretty_print_response(response, "UPLOAD FILE") return response @@ -39,7 +39,6 @@ def get_submission_info(submission_id): """ response = turnitin_api_handler("get", f"submissions/{submission_id}") - pretty_print_response(response, "SUBMISSION STATUS") return response @@ -51,7 +50,7 @@ def delete_submission(submission_id, is_hard_delete="false"): response = turnitin_api_handler( "delete", f"submissions/{submission_id}/?hard={is_hard_delete}" ) - pretty_print_response(response) + return response def put_recover_submission(submission_id): @@ -59,4 +58,4 @@ def put_recover_submission(submission_id): Recovers a submission that has been soft deleted """ response = turnitin_api_handler("put", f"submissions/{submission_id}/recover") - pretty_print_response(response) + return response diff --git a/platform_plugin_turnitin/turnitin_client/handlers/utils.py b/platform_plugin_turnitin/turnitin_client/handlers/utils.py deleted file mode 100644 index 8da4971..0000000 --- a/platform_plugin_turnitin/turnitin_client/handlers/utils.py +++ /dev/null @@ -1,13 +0,0 @@ -import json - - -def pretty_print_response(response, type_of=""): - """debug helper function""" - content = response.json() - print("\n\n") - print(f"------{type_of}------") - print("\n\n") - print(json.dumps(content, indent=4)) - print("\n\n") - print("------------") - print("\n\n") diff --git a/requirements/base.in b/requirements/base.in index 04b2f8d..c8fa182 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -4,3 +4,4 @@ Django # Web application framework xblock xblock-utils +requests diff --git a/requirements/base.txt b/requirements/base.txt index fc1d27b..dada3f5 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -8,12 +8,16 @@ appdirs==1.4.4 # via fs asgiref==3.7.2 # via django -boto3==1.28.60 +boto3==1.28.62 # via fs-s3fs -botocore==1.31.60 +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 @@ -26,6 +30,8 @@ fs==2.4.16 # xblock fs-s3fs==1.1.1 # via openedx-django-pyfs +idna==3.4 + # via requests jmespath==1.0.1 # via # boto3 @@ -35,7 +41,9 @@ lazy==1.6 lxml==4.9.3 # via xblock mako==1.2.4 - # via xblock-utils + # via + # xblock + # xblock-utils markupsafe==2.1.3 # via # mako @@ -52,10 +60,14 @@ pytz==2023.3.post1 # 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.1 - # via xblock-utils +simplejson==3.19.2 + # via + # xblock + # xblock-utils six==1.16.0 # via # fs @@ -66,14 +78,16 @@ sqlparse==0.4.4 typing-extensions==4.8.0 # via asgiref urllib3==1.26.17 - # via botocore + # via + # botocore + # requests web-fragments==2.1.0 # via # xblock # xblock-utils webob==1.8.7 # via xblock -xblock[django]==1.8.0 +xblock[django]==1.8.1 # via # -r requirements/base.in # xblock-utils diff --git a/requirements/dev.txt b/requirements/dev.txt index ee79818..f727f18 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -19,11 +19,11 @@ astroid==2.15.8 # pylint-celery black==23.9.1 # via -r requirements/dev.in -boto3==1.28.60 +boto3==1.28.62 # via # -r requirements/quality.txt # fs-s3fs -botocore==1.31.60 +botocore==1.31.62 # via # -r requirements/quality.txt # boto3 @@ -32,8 +32,16 @@ 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 @@ -71,7 +79,7 @@ django==3.2.22 # -r requirements/quality.txt # edx-i18n-tools # openedx-django-pyfs -edx-i18n-tools==1.2.0 +edx-i18n-tools==1.3.0 # via -r requirements/dev.in edx-lint==5.3.4 # via -r requirements/quality.txt @@ -94,6 +102,10 @@ 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 @@ -127,10 +139,12 @@ lazy-object-proxy==1.9.0 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 @@ -246,13 +260,16 @@ pyyaml==6.0.1 # 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.1 +simplejson==3.19.2 # via # -r requirements/quality.txt + # xblock # xblock-utils six==1.16.0 # via @@ -314,6 +331,7 @@ urllib3==1.26.17 # via # -r requirements/quality.txt # botocore + # requests virtualenv==20.24.5 # via # -r requirements/ci.txt @@ -335,7 +353,7 @@ wrapt==1.15.0 # via # -r requirements/quality.txt # astroid -xblock[django]==1.8.0 +xblock[django]==1.8.1 # via # -r requirements/quality.txt # xblock-utils diff --git a/requirements/doc.txt b/requirements/doc.txt index 3f057b9..8d94fb3 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -22,11 +22,11 @@ babel==2.13.0 # sphinx beautifulsoup4==4.12.2 # via pydata-sphinx-theme -boto3==1.28.60 +boto3==1.28.62 # via # -r requirements/test.txt # fs-s3fs -botocore==1.31.60 +botocore==1.31.62 # via # -r requirements/test.txt # boto3 @@ -34,11 +34,15 @@ botocore==1.31.60 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 @@ -80,7 +84,9 @@ fs-s3fs==1.1.1 # -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 @@ -124,6 +130,7 @@ lxml==4.9.3 mako==1.2.4 # via # -r requirements/test.txt + # xblock # xblock-utils markdown-it-py==3.0.0 # via rich @@ -207,6 +214,7 @@ readme-renderer==42.0 # via twine requests==2.31.0 # via + # -r requirements/test.txt # requests-toolbelt # sphinx # twine @@ -224,9 +232,10 @@ s3transfer==0.7.0 # boto3 secretstorage==3.3.3 # via keyring -simplejson==3.19.1 +simplejson==3.19.2 # via # -r requirements/test.txt + # xblock # xblock-utils six==1.16.0 # via @@ -301,7 +310,7 @@ webob==1.8.7 # via # -r requirements/test.txt # xblock -xblock[django]==1.8.0 +xblock[django]==1.8.1 # via # -r requirements/test.txt # xblock-utils diff --git a/requirements/quality.txt b/requirements/quality.txt index 2fd0eed..837efd6 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -16,15 +16,23 @@ astroid==2.15.8 # via # pylint # pylint-celery -boto3==1.28.60 +boto3==1.28.62 # via # -r requirements/test.txt # fs-s3fs -botocore==1.31.60 +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 @@ -64,6 +72,10 @@ 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 @@ -94,6 +106,7 @@ lxml==4.9.3 mako==1.2.4 # via # -r requirements/test.txt + # xblock # xblock-utils markupsafe==2.1.3 # via @@ -167,13 +180,16 @@ pyyaml==6.0.1 # -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.1 +simplejson==3.19.2 # via # -r requirements/test.txt + # xblock # xblock-utils six==1.16.0 # via @@ -214,6 +230,7 @@ urllib3==1.26.17 # via # -r requirements/test.txt # botocore + # requests web-fragments==2.1.0 # via # -r requirements/test.txt @@ -225,7 +242,7 @@ webob==1.8.7 # xblock wrapt==1.15.0 # via astroid -xblock[django]==1.8.0 +xblock[django]==1.8.1 # via # -r requirements/test.txt # xblock-utils diff --git a/requirements/test.txt b/requirements/test.txt index a7c0234..33031b4 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -12,15 +12,23 @@ asgiref==3.7.2 # via # -r requirements/base.txt # django -boto3==1.28.60 +boto3==1.28.62 # via # -r requirements/base.txt # fs-s3fs -botocore==1.31.60 +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 @@ -43,6 +51,10 @@ 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 @@ -63,6 +75,7 @@ lxml==4.9.3 mako==1.2.4 # via # -r requirements/base.txt + # xblock # xblock-utils markupsafe==2.1.3 # via @@ -105,13 +118,16 @@ pyyaml==6.0.1 # -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.1 +simplejson==3.19.2 # via # -r requirements/base.txt + # xblock # xblock-utils six==1.16.0 # via @@ -139,6 +155,7 @@ urllib3==1.26.17 # via # -r requirements/base.txt # botocore + # requests web-fragments==2.1.0 # via # -r requirements/base.txt @@ -148,7 +165,7 @@ webob==1.8.7 # via # -r requirements/base.txt # xblock -xblock[django]==1.8.0 +xblock[django]==1.8.1 # via # -r requirements/base.txt # xblock-utils From 2bb6980f18e3a4360ddc6b3c06634ed9b2b2b9ed Mon Sep 17 00:00:00 2001 From: Cristhian Garcia Date: Tue, 10 Oct 2023 14:15:06 -0500 Subject: [PATCH 11/12] docs: fix indentation issues --- .../turnitin_client/handlers/similarity_reports.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/platform_plugin_turnitin/turnitin_client/handlers/similarity_reports.py b/platform_plugin_turnitin/turnitin_client/handlers/similarity_reports.py index e15ab1a..362c01a 100644 --- a/platform_plugin_turnitin/turnitin_client/handlers/similarity_reports.py +++ b/platform_plugin_turnitin/turnitin_client/handlers/similarity_reports.py @@ -17,9 +17,10 @@ def put_generate_similarity_report(submission_id, payload): def get_similarity_report_info(submission_id): """ Returns summary information about the requested Similarity Report. + Status: - PROCESSING - COMPLETE + - PROCESSING + - COMPLETE """ response = turnitin_api_handler("get", f"submissions/{submission_id}/similarity") return response From 77e23569c954ba110bc97a19005e7c3a710f9965 Mon Sep 17 00:00:00 2001 From: Cristhian Garcia Date: Tue, 10 Oct 2023 14:18:31 -0500 Subject: [PATCH 12/12] chore: reformat for quality checks again --- Makefile | 2 +- platform_plugin_turnitin/turnitin.py | 4 +++- .../turnitin_client/handlers/api_handler.py | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) 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/turnitin.py b/platform_plugin_turnitin/turnitin.py index 3fb5f22..2d348f3 100644 --- a/platform_plugin_turnitin/turnitin.py +++ b/platform_plugin_turnitin/turnitin.py @@ -255,7 +255,9 @@ def generate_similarity_report( Returns: dict: The status of the similarity report generation process. """ - payload = getattr(settings, "TURNITIN_SIMILARY_REPORT_PAYLOAD") # pylint: disable=literal-used-as-attribute + 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( diff --git a/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py b/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py index c36c9b1..1c6dba4 100644 --- a/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py +++ b/platform_plugin_turnitin/turnitin_client/handlers/api_handler.py @@ -70,8 +70,10 @@ def turnitin_api_handler( 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 + f"{TII_API_URL}/api/v1/{url_prefix}", + headers=headers, + data=uploaded_file, + timeout=settings.TURNITIN_API_TIMEOUT, ) return response