diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 7f6b817..91e7ded 100644 --- a/.gitignore +++ b/.gitignore @@ -115,7 +115,7 @@ venv.bak/ .spyproject # Rope project settings -.ropeproject +.ropeprojectig # mkdocs documentation /site @@ -129,3 +129,4 @@ dmypy.json .pyre/ .DS_Store +.idea/ diff --git a/README.md b/README.md index 131e5e3..205a0e2 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,37 @@ An XBlock for Empowr's course platform that allows for the input and checking of any programming language. +![Code Editor Screenshot](code-editor.png) + + +* [CodeEditorXblock](#codeeditorxblock) + * [TODO](#todo) + * [Installation](#installation) + * [Run the Django development server](#run-the-django-development-server) + * [Supported Languages](#supported-languages) + * [Adding support for a language](#adding-support-for-a-language) + * [To find the mime-type](#to-find-the-mime-type) + * [To find the url](#to-find-the-url) + + + +## TODO + + - Fix Solution Tab. Most work for a solution tab is done, however it doesn't properly display. + - Add support for all CodeMirror languages. This is an easy task, just tedious. + ## Installation 1. Make sure you have have Python 3.8 installed on your computer. -2. Clone the repo with `git clone --recurse-submodules git@github.com:EmpowrOrg/CodeEditorXblock.git`. This will give you the repo including the xblock-sdk submodule. +2. Clone the repo with `git clone --recurse-submodules git@github.com:EmpowrOrg/CodeEditorXblock.git`. This will give + you the repo including the xblock-sdk submodule. 3. Create and Activate the Virtual Environment: -You must have a virtual environment tool installed on your computer. For more information, see [Install XBlock Prerequisites](https://edx.readthedocs.io/projects/xblock-tutorial/en/latest/getting_started/prereqs.html). +You must have a virtual environment tool installed on your computer. For more information, +see [Install XBlock Prerequisites](https://edx.readthedocs.io/projects/xblock-tutorial/en/latest/getting_started/prereqs.html) +. Then create the virtual environment in your CodeEditorXblock directory. @@ -22,11 +44,15 @@ Run the following command to activate the virtual environment. `source venv/bin/activate` +Install the plugin with + +`pip install -e swiftplugin` + 4. Navigate to the xblock-sdk directory and run the following command to install the requirements. `pip install -r requirements/base.txt` -## Run the Django development server +### Run the Django development server Navigate to the xblock-sdk directory and run the following commands. @@ -35,3 +61,65 @@ Navigate to the xblock-sdk directory and run the following commands. `python manage.py migrate` `python manage.py runserver` + +## Supported Languages + +When specifying a language you must put in the correct mime-type for the plugin. Here all the supported languages and +their mime-types + +| Language Name | Mime-Type | +|---------------|-------------------| +| APL | text/apl | +| ASN.1 | text/x-ttcn-asn | +| C | text/x-csrc | +| C++ | text/x-c++src | +| C# | text/x-csharp | +| Java | text/x-java | +| Kotlin | text/x-kotlin | +| Python | text/x-python | +| Scala | text/x-scala | +| Squirrel | text/x-squirrel | +| Swift | text/x-swift | +| Objective-C | text/x-objectivec | + + +Code Mirror supports many more languages. If you do not see your language supported just check out the +[doc](https://codemirror.net/5/mode/). + +### Adding support for a language + +It's really simple to add plugin support for +any [language already supported by CodeMirror](https://codemirror.net/5/mode/). +Open ![swiftplugin.py](/swiftplugin/swiftplugin/swiftplugin.py) and scroll down to the dictionary of mime-types and +urls. + +To add support you need both the mime-type and the url. + +#### To find the mime-type +Add the mime-type you wish to add support for. You can find this by looking at the list of supported languages, clicking +on the langauge you want, and then scrolling to the bottom of the page. +Ex: `text/x-kotlin` + +#### To find the url +You can find the url needed by going to +the [CodeMirror git folder for their supported modes](https://github.com/codemirror/codemirror5/blob/master/mode/index.html) + +Look for the folder name of the language you wish to support. + +Ex: If you want to add Kotlin, then you search for Kotlin in the file, and see it's folder name +is `clike`. +Ex:`
  • Kotlin
  • ` + +Go [here](https://github.com/codemirror/codemirror5/tree/master/mode) and click into the folder name you just discovered +of the language you wish to support. + +Notice the name of the `.js` file in this folder. +Ex: If you were adding Kotlin, the js is `clike.js` + +Now just append the folder name and the .js name to this +path `https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/` + +The final Url for Kotlin would look like: `https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/clike/clike.js` + +And now you can just add the mime-type and url to the dictionary like +`"text/x-kotlin": "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/clike/clike.js",` diff --git a/code-editor.png b/code-editor.png new file mode 100644 index 0000000..175cd42 Binary files /dev/null and b/code-editor.png differ diff --git a/codingxblock/codingxblock/__init__.py b/codingxblock/codingxblock/__init__.py deleted file mode 100644 index c68ff9e..0000000 --- a/codingxblock/codingxblock/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .codingxblock import CodeEditorXBlock diff --git a/codingxblock/codingxblock/codingxblock.py b/codingxblock/codingxblock/codingxblock.py deleted file mode 100644 index 8253c38..0000000 --- a/codingxblock/codingxblock/codingxblock.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -This plugin allows the students to write swift code and submit it as a problem. -The plugin will send the code to an API that will send a correct or failure response. -If correct, the user will see a confirmation message. -If incorrect, the user will see the differences between their answer and the expected answer. -The user may optionally see the solution code as well. - -The teacher will be able to upload what the solution code is. -They will also be able to upload the solution answer. -These two are both required. -The teacher will have a toggle on whether they wish to allow the user see the solution code or not. -""" - -import pkg_resources -from web_fragments.fragment import Fragment -from xblock.core import XBlock -from xblock.fields import String, Scope - - -class CodeEditorXBlock(XBlock): - """ - Students can write and submit code in response to a problem. - Teachers can upload and allow students to view solution code. - """ - - # Fields are defined on the class. You can access them in your code as - # self.. - - # TO-DO: delete count, and define your own fields. - placeholder_text = String( - default="", scope=Scope.user_state, - help="Some text to test as a field", - ) - - def resource_string(self, path): - """Handy helper for getting resources from our kit.""" - data = pkg_resources.resource_string(__name__, path) - return data.decode("utf8") - - # TO-DO: change this view to display your data your own way. - def student_view(self, context=None): - """ - The primary view of the CodeEditorXBlock, shown to students - when viewing courses. - """ - html = self.resource_string("static/html/codingxblock.html") - frag = Fragment(html.format(self=self)) - frag.add_css(self.resource_string("static/css/codingxblock.css")) - frag.add_javascript(self.resource_string("static/js/src/codingxblock.js")) - frag.initialize_js('CodeEditorXBlock') - 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. - """ - # Just to show data coming in... - assert data['hello'] == 'world' - - self.count += 1 - return {"count": self.count} - - # 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 [ - ("CodeEditorXBlock", - """ - """), - ("Multiple CodeEditorXBlock", - """ - - - - - """), - ] diff --git a/codingxblock/codingxblock/static/css/codingxblock.css b/codingxblock/codingxblock/static/css/codingxblock.css deleted file mode 100644 index 7b3c99b..0000000 --- a/codingxblock/codingxblock/static/css/codingxblock.css +++ /dev/null @@ -1 +0,0 @@ -/* CSS for CodeEditorXBlock */ diff --git a/codingxblock/codingxblock/static/html/codingxblock.html b/codingxblock/codingxblock/static/html/codingxblock.html deleted file mode 100644 index 43da393..0000000 --- a/codingxblock/codingxblock/static/html/codingxblock.html +++ /dev/null @@ -1,4 +0,0 @@ -
    -

    Code Editor will go here:

    - +
    +
    +

    Instructions

    +

    + Run Code + + Submit Code + +
    +

    Output

    +
    +
    +

    +
    +
    +
    +
    +
    + + + + + + + + + diff --git a/swiftplugin/swiftplugin/static/js/src/swiftplugin.js b/swiftplugin/swiftplugin/static/js/src/swiftplugin.js new file mode 100644 index 0000000..572d823 --- /dev/null +++ b/swiftplugin/swiftplugin/static/js/src/swiftplugin.js @@ -0,0 +1,177 @@ +/* Javascript for SwiftPluginXBlock. */ + + +function SwiftPluginXBlock(runtime, element) { + function updateResponse(response) { + if (response.response.output) { + const compilation_response = response.response.output + let output_response; + if (response.diff) { + const diff_response = response.diff + output_response = compilation_response + '
    ' + diff_response + } else { + output_response = compilation_response + } + document.getElementById('response-txt').innerHTML = output_response; + } else if (response.response.error) { + document.getElementById('response-txt').innerHTML = response.response.error; + } + } + + function handleError(response) { + console.log("error") + console.log(response) + const compilation_response = response.response + const diff_response = response.diff + const output_response = compilation_response + '
    ' + diff_response + document.getElementById('response-txt').innerHTML = output_response; + } + + function updateProblemDescription(response) { + const myAssigmentTextArea = document.getElementById("assigment-instructions-text"); + const converter = new showdown.Converter(); + const html = converter.makeHtml(response.problem_description); + myAssigmentTextArea.innerHTML = html; + } + + function updateProblemTitle(response) { + const myAssigmentTextArea = document.getElementById("assignment-title"); + const converter = new showdown.Converter(); + const html = converter.makeHtml(response.problem_title); + myAssigmentTextArea.innerHTML = html; + } + + function updateProblemSolution(response) { + solutionCodeMirror.setValue(response.problem_solution) + } + + const handlerUrl = runtime.handlerUrl(element, 'get_button_handler'); + const handlerUrlDescription = runtime.handlerUrl(element, 'get_problem_description'); + //const handlerUrlSolution = runtime.handlerUrl(element, 'get_problem_solution'); + //const handlerUrlHasSolution = runtime.handlerUrl(element, 'has_problem_solution'); + const handlerUrlTitle = runtime.handlerUrl(element, 'get_problem_title'); + const handlerUrlLanguage = runtime.handlerUrl(element, 'get_problem_language'); + + var myCodeMirror = null; + //var solutionCodeMirror = null; + + const run_btn = document.getElementById('run-btn'); + run_btn.onclick = function (eventObject) { + var user_code = myCodeMirror.getValue(); + $.ajax({ + type: "POST", + url: handlerUrl, + data: JSON.stringify({type: 'run', code: user_code}), + success: updateResponse, + error: handleError + }); + } + + const submit_btn = document.getElementById('submit-btn'); + submit_btn.onclick = function (eventObject) { + var user_code = myCodeMirror.getValue(); + $.ajax({ + type: "POST", + url: handlerUrl, + data: JSON.stringify({type: 'submit', code: user_code}), + success: updateResponse + }); + } + + const solution_btn = document.getElementById('btn-solution') + + function init_description() { + $.ajax({ + type: "POST", + url: handlerUrlDescription, + data: JSON.stringify({}), + success: updateProblemDescription + }); + } + + function init_title() { + $.ajax({ + type: "POST", + url: handlerUrlTitle, + data: JSON.stringify({}), + success: updateProblemTitle + }); + } + + /* function init_solution() { + $.ajax({ + type: "POST", + url: handlerUrlHasSolution, + data: JSON.stringify({}), + success: function (data) { + if (data.has_solution_defined) { + solution_btn.onclick = function (eventObject) { + init_solution(); + } + } else { + solution_btn.remove() + } + } + }) + }*/ + + function on_init() { + init_description(); + init_title(); + init_language(); + } + + function init_language() { + $.ajax({ + type: "POST", + url: handlerUrlLanguage, + data: JSON.stringify({}), + success: function (data) { + console.log(data) + init_code_mirror(data.problem_language) + } + }); + } + + function init_code_mirror(mode) { + const codemirror_config = { + value: "// Your code here.", + lineNumbers: true, + mode: mode, + lineWrapping: true, + indentWithTabs: true, + lineWiseCopyCut: true, + autoCloseBrackets: true, + } + console.log(codemirror_config) + var myTextArea = document.getElementById("code-area"); + myCodeMirror = CodeMirror(function (elt) { + myTextArea.parentNode.replaceChild(elt, myTextArea); + }, codemirror_config); + myCodeMirror.setSize('100%'); + solution_btn.remove() + /* const solutionTextArea = document.getElementById("code-solution-area"); + solutionCodeMirror = CodeMirror(function (elt) { + solutionTextArea.parentNode.replaceChild(elt, solutionTextArea); + }, codemirror_config); + solutionCodeMirror.setSize('100%'); + init_solution()*/ + } + + /* + function init_solution() { + $.ajax({ + type: "POST", + url: handlerUrlSolution, + data: JSON.stringify({}), + success: updateProblemSolution + }); + }*/ + + + $(function ($) { + /* Here's where you'd do things on page load. */ + on_init() + }); +} + diff --git a/swiftplugin/swiftplugin/swiftplugin.py b/swiftplugin/swiftplugin/swiftplugin.py new file mode 100644 index 0000000..7c1a145 --- /dev/null +++ b/swiftplugin/swiftplugin/swiftplugin.py @@ -0,0 +1,258 @@ +"""TO-DO: Write a description of what this XBlock is.""" + +import pkg_resources +from web_fragments.fragment import Fragment +from xblock.core import XBlock +from xblock.fields import String, Scope +from xblockutils.studio_editable import StudioEditableXBlockMixin +import difflib +from io import StringIO +import logging +import requests +import json + + +class SwiftPluginXBlock( + StudioEditableXBlockMixin, + XBlock): + """ + TO-DO: document what your XBlock does. + """ + + code = String( + default="", + scope=Scope.user_state, + help="User code", + ) + + problem_id = String( + default="", + scope=Scope.settings, + help="Problem id used by the Api to checkcode" + ) + + api_url_submit = String( + default="", + scope=Scope.settings, + help="URL api used to check the code (submit final response)" + ) + + api_url_run = String( + default="", + scope=Scope.settings, + help="URL api used to run the code (run code by api)" + ) + + problem_title = String( + default="Programming Exercise", + scope=Scope.settings, + help="Problem title", + ) + + problem_description = String( + default="Problem description here!", + scope=Scope.settings, + help="Problem description in Markdown Language", + multiline_editor=True + ) + + # problem_solution = String( + # default="", + # scope=Scope.settings, + # help="Problem solution in code", + # multiline_editor=True + # ) + + problem_language = String( + default="text/x-kotlin", + scope=Scope.settings, + help="Example: text/x-kotlin. Supported languages can be found at https://codemirror.net/5/mode/" + ) + + editable_fields = [ + 'problem_id', + 'problem_description', + 'problem_title', + # 'problem_solution', + 'problem_language', + 'api_url_run', + 'api_url_submit' + ] + + def resource_string(self, path): + """Handy helper for getting resources from our kit.""" + data = pkg_resources.resource_string(__name__, path) + return data.decode("utf8") + + @XBlock.supports('multi_device') + def student_view(self, context=None): + """ + The primary view of the SwiftPluginXBlock, shown to students + when viewing courses. + """ + print(self.problem_language) + html = self.resource_string("static/html/swiftplugin.html") + frag = Fragment(html.format(self=self)) + frag.add_javascript_url("https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.js") + frag.add_javascript_url(self.get_mode_url(self.problem_language)) + frag.add_javascript_url("https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js") + frag.add_css(self.resource_string("static/css/swiftplugin.css")) + frag.add_javascript(self.resource_string("static/js/src/swiftplugin.js")) + frag.add_css_url("https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css") + frag.add_css_url("https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.32.0/codemirror.css") + frag.add_javascript_url("https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js") + frag.add_javascript_url("https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.5/umd/popper.min.js") + frag.add_javascript_url("https://codemirror.net/5/addon/search/search.js") + frag.add_javascript_url("https://codemirror.net/5/addon/edit/closebrackets.js") + frag.add_javascript_url("https://codemirror.net/5/addon/search/searchcursor.js") + frag.add_javascript_url("https://codemirror.net/5/addon/search/jump-to-line.js") + frag.add_javascript_url("https://codemirror.net/5/addon/dialog/dialog.js") + frag.add_javascript_url("https://codemirror.net/5/addon/fold/foldcode.js") + + frag.add_css_url("https://codemirror.net/5/addon/dialog/dialog.css") + frag.add_javascript_url( + "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.0-beta2/js/bootstrap.bundle.min.js") + frag.add_css_url("https://d2l03dhf2zcc6i.cloudfront.net/css/custom.css") + frag.add_css_url("https://d2l03dhf2zcc6i.cloudfront.net/css/style.css") + frag.add_css_url( + "https://fonts.googleapis.com/css2?family=Archivo+Black&family=Roboto:ital,wght@0,400;0,700;1,400;1,700&display=swap") + frag.initialize_js('SwiftPluginXBlock') + return frag + + @XBlock.json_handler + def get_button_handler(self, data, suffix=''): + """ + An example handler, which increments the data. + """ + response = {} + + if "code" not in data.keys(): + logging.error("non code data in request!") + response['status'] = "Empty code!" + return response + + self.code = data['code'] + + response['code'] = self.code + if "type" not in data.keys(): + logging.error("non request type in request") + response["status"] = "Non request type" + return response + + if 'run' in data['type']: + api_respo = self.handle_run_request() + response['status'] = "Executed code" + response['response'] = api_respo + + elif 'submit' in data['type']: + api_respo = self.handle_submit_request() + response['status'] = "Submitted code" + response['response'] = api_respo['message'] + response['diff'] = self.calculate_diff(expected_output=api_respo['expected_output'], + actual_output=api_respo['user_output']) + + else: + response["status"] = "No valid type request" + + return response + + @XBlock.json_handler + def get_problem_description(self, data, suffix=''): + return { + 'problem_id': self.problem_id, + 'problem_description': self.problem_description + } + + @XBlock.json_handler + def get_problem_title(self, data, suffix=''): + return { + 'problem_id': self.problem_id, + 'problem_title': self.problem_title + } + + # @XBlock.json_handler + # def get_problem_solution(self, data, suffix=''): + # return { + # 'problem_id': self.problem_id, + # 'problem_solution': self.problem_solution + # } + + @XBlock.json_handler + def get_problem_language(self, data, suffix=''): + return { + 'problem_id': self.problem_id, + 'problem_language': self.problem_language + } + + # @XBlock.json_handler + # def has_problem_solution(self, data, suffix=''): + # return { + # 'problem_id': self.problem_id, + # 'has_solution_defined': self.problem_solution and self.problem_solution.strip() + # } + + def handle_run_request(self): + r = requests.post(self.get_server_run_url(), json=self.build_request_body()) + return r.json() + + def get_server_run_url(self): + if self.api_url_run.startswith("http"): + return self.api_url_run + else: + return "http://" + self.api_url_run + + def handle_submit_request(self): + r = requests.post(self.api_url_submit, json=self.build_request_body()) + return r.json() + + def build_request_body(self): + body = { + "code": self.code, + "language": self.problem_language + } + return json.dumps(body) + + @staticmethod + def workbench_scenarios(): + """A canned scenario for display in the workbench.""" + return [ + ("SwiftPluginXBlock", + """ + """), + ("Multiple SwiftPluginXBlock", + """ + + + + + """), + ] + + def calculate_diff(self, expected_output: str, actual_output: str): + # To redirect std output + mystdout = StringIO() + d = difflib.Differ() + mystdout.writelines(list(d.compare(expected_output.splitlines(keepends=True), + actual_output.splitlines(keepends=True)))) + # Read from mystdout output + diff = mystdout.getvalue() + return diff + + _modeUrl = { + "text/x-swift": "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/swift/swift.js", + "text/x-csrc": "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/clike/clike.js", + "text/x-c++src": "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/clike/clike.js", + "text/x-csharp": "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/clike/clike.js", + "text/x-java": "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/clike/clike.js", + "text/x-objectivec": "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/clike/clike.js", + "text/x-scala": "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/clike/scala.js", + "text/x-squirrel": "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/clike/clike.js", + "text/apl": "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/apl/apl.js", + "text/x-ttcn-asn": "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/asn.1/asn.1.js", + "text/x-python": "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/python/python.js", + "text/x-kotlin": "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/clike/clike.js", + } + + def get_mode_url(self, mode): + normalized_mode = mode.strip().lower() + return self._modeUrl[normalized_mode] diff --git a/codingxblock/codingxblock/translations/README.txt b/swiftplugin/swiftplugin/translations/README.txt similarity index 100% rename from codingxblock/codingxblock/translations/README.txt rename to swiftplugin/swiftplugin/translations/README.txt