From 6cb67fc694e7cf802e761537d1b0a264cc27f2fe Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Tue, 27 Oct 2015 18:20:37 +0100 Subject: [PATCH 01/33] Add skeleton created by XBlock SDK. --- .gitignore | 2 + setup.py | 39 +++++++++++++++ vectordraw/__init__.py | 1 + vectordraw/static/README.txt | 19 +++++++ vectordraw/static/css/vectordraw.css | 9 ++++ vectordraw/static/html/vectordraw.html | 5 ++ vectordraw/static/js/src/vectordraw.js | 22 +++++++++ vectordraw/vectordraw.py | 68 ++++++++++++++++++++++++++ 8 files changed, 165 insertions(+) create mode 100644 .gitignore create mode 100644 setup.py create mode 100644 vectordraw/__init__.py create mode 100644 vectordraw/static/README.txt create mode 100644 vectordraw/static/css/vectordraw.css create mode 100644 vectordraw/static/html/vectordraw.html create mode 100644 vectordraw/static/js/src/vectordraw.js create mode 100644 vectordraw/vectordraw.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43ae0e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.py[cod] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e888f38 --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +"""Setup for vectordraw XBlock.""" + +import os +from setuptools import setup + + +def package_data(pkg, roots): + """Generic function to find package_data. + + All of the files under each of the `roots` will be declared as package + data for package `pkg`. + + """ + data = [] + for root in roots: + for dirname, _, files in os.walk(os.path.join(pkg, root)): + for fname in files: + data.append(os.path.relpath(os.path.join(dirname, fname), pkg)) + + return {pkg: data} + + +setup( + name='vectordraw-xblock', + version='0.1', + description='vectordraw XBlock', # TODO: write a better description. + packages=[ + 'vectordraw', + ], + install_requires=[ + 'XBlock', + ], + entry_points={ + 'xblock.v1': [ + 'vectordraw = vectordraw:VectorDrawXBlock', + ] + }, + package_data=package_data("vectordraw", ["static", "public"]), +) diff --git a/vectordraw/__init__.py b/vectordraw/__init__.py new file mode 100644 index 0000000..93de7a9 --- /dev/null +++ b/vectordraw/__init__.py @@ -0,0 +1 @@ +from .vectordraw import VectorDrawXBlock diff --git a/vectordraw/static/README.txt b/vectordraw/static/README.txt new file mode 100644 index 0000000..0472ef6 --- /dev/null +++ b/vectordraw/static/README.txt @@ -0,0 +1,19 @@ +This static directory is for files that should be included in your kit as plain +static files. + +You can ask the runtime for a URL that will retrieve these files with: + + url = self.runtime.local_resource_url(self, "static/js/lib.js") + +The default implementation is very strict though, and will not serve files from +the static directory. It will serve files from a directory named "public". +Create a directory alongside this one named "public", and put files there. +Then you can get a url with code like this: + + url = self.runtime.local_resource_url(self, "public/js/lib.js") + +The sample code includes a function you can use to read the content of files +in the static directory, like this: + + frag.add_javascript(self.resource_string("static/js/my_block.js")) + diff --git a/vectordraw/static/css/vectordraw.css b/vectordraw/static/css/vectordraw.css new file mode 100644 index 0000000..127f08a --- /dev/null +++ b/vectordraw/static/css/vectordraw.css @@ -0,0 +1,9 @@ +/* CSS for VectorDrawXBlock */ + +.vectordraw_block .count { + font-weight: bold; +} + +.vectordraw_block p { + cursor: pointer; +} diff --git a/vectordraw/static/html/vectordraw.html b/vectordraw/static/html/vectordraw.html new file mode 100644 index 0000000..4dd9762 --- /dev/null +++ b/vectordraw/static/html/vectordraw.html @@ -0,0 +1,5 @@ +
+

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

+
diff --git a/vectordraw/static/js/src/vectordraw.js b/vectordraw/static/js/src/vectordraw.js new file mode 100644 index 0000000..251c459 --- /dev/null +++ b/vectordraw/static/js/src/vectordraw.js @@ -0,0 +1,22 @@ +/* Javascript for VectorDrawXBlock. */ +function VectorDrawXBlock(runtime, element) { + + function updateCount(result) { + $('.count', element).text(result.count); + } + + var handlerUrl = runtime.handlerUrl(element, 'increment_count'); + + $('p', element).click(function(eventObject) { + $.ajax({ + type: "POST", + url: handlerUrl, + data: JSON.stringify({"hello": "world"}), + success: updateCount + }); + }); + + $(function ($) { + /* Here's where you'd do things on page load. */ + }); +} diff --git a/vectordraw/vectordraw.py b/vectordraw/vectordraw.py new file mode 100644 index 0000000..8cc8f40 --- /dev/null +++ b/vectordraw/vectordraw.py @@ -0,0 +1,68 @@ +"""TO-DO: Write a description of what this XBlock is.""" + +import pkg_resources + +from xblock.core import XBlock +from xblock.fields import Scope, Integer +from xblock.fragment import Fragment + + +class VectorDrawXBlock(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") + + # TO-DO: change this view to display your data your own way. + def student_view(self, context=None): + """ + The primary view of the VectorDrawXBlock, shown to students + when viewing courses. + """ + html = self.resource_string("static/html/vectordraw.html") + frag = Fragment(html.format(self=self)) + frag.add_css(self.resource_string("static/css/vectordraw.css")) + frag.add_javascript(self.resource_string("static/js/src/vectordraw.js")) + frag.initialize_js('VectorDrawXBlock') + 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 [ + ("VectorDrawXBlock", + """ + + + + + """), + ] From 9a9a32419e8abee23f6ac2746e1af0608a80ff83 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Fri, 30 Oct 2015 08:43:48 +0100 Subject: [PATCH 02/33] Allow to display and interact with exercises in the LMS. --- vectordraw/static/css/vectordraw.css | 81 +++- vectordraw/static/html/vectordraw.html | 6 +- vectordraw/static/js/src/vectordraw.js | 557 ++++++++++++++++++++++++- vectordraw/vectordraw.py | 218 +++++++++- 4 files changed, 836 insertions(+), 26 deletions(-) diff --git a/vectordraw/static/css/vectordraw.css b/vectordraw/static/css/vectordraw.css index 127f08a..5172255 100644 --- a/vectordraw/static/css/vectordraw.css +++ b/vectordraw/static/css/vectordraw.css @@ -1,9 +1,84 @@ /* CSS for VectorDrawXBlock */ -.vectordraw_block .count { +.vectordraw_block { + display: inline-block; +} + +.vectordraw_block .jxgboard { + float: left; + border: 2px solid #1f628d; +} + +.vectordraw_block .jxgboard .JXGtext { + pointer-events: none; /* prevents cursor from turning into caret when over a label */ +} + +.vectordraw_block .menu { + float: left; + width: 160px; + margin-left: 15px; +} + +.vectordraw_block .menu .controls { + margin-bottom: 20px; + font-size: 0; +} + +.vectordraw_block .menu .controls select { + width: 160px; + margin-bottom: 8px; + font-size: 18px; +} + +.vectordraw_block .menu .controls button { + display: inline-block; + background-color: #3498db; + border-radius: 5px; + box-shadow: 0 1px 3px #666; + color: #fff; + font-size: 14px; + padding: 5px 10px 5px 10px; + margin: 4px 0; + border: 2px solid #1f628d; + text-decoration: none; + width: 160px; +} + +.vectordraw_block .menu .controls button:hover { + background: #3cb0fd; + background-image: -webkit-linear-gradient(top, #3cb0fd, #3498db); + background-image: -moz-linear-gradient(top, #3cb0fd, #3498db); + background-image: -ms-linear-gradient(top, #3cb0fd, #3498db); + background-image: -o-linear-gradient(top, #3cb0fd, #3498db); + background-image: linear-gradient(to bottom, #3cb0fd, #3498db); + text-decoration: none; +} + +vectordraw_block .menu .controls button.undo, +vectordraw_block .menu .controls button.redo { + width: 78px; +} + +.vectordraw_block .menu .controls button.undo { + margin-right: 4px; +} + +.vectordraw_block .menu .vector-properties { + padding: 10px; + border: 1px solid #1f628d; + font-size: 16px; + line-height: 1.25; +} + +.vectordraw_block .menu .vector-properties h3 { + font-size: 16px; + margin: 0 0 5px; +} + +.vectordraw_block .menu .vector-properties .vector-prop-bold { font-weight: bold; } -.vectordraw_block p { - cursor: pointer; +.vectordraw_block .menu .vector-prop-slope { + display: none; } diff --git a/vectordraw/static/html/vectordraw.html b/vectordraw/static/html/vectordraw.html index 4dd9762..0fc6b19 100644 --- a/vectordraw/static/html/vectordraw.html +++ b/vectordraw/static/html/vectordraw.html @@ -1,5 +1,5 @@
-

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

+ +
+
diff --git a/vectordraw/static/js/src/vectordraw.js b/vectordraw/static/js/src/vectordraw.js index 251c459..217134a 100644 --- a/vectordraw/static/js/src/vectordraw.js +++ b/vectordraw/static/js/src/vectordraw.js @@ -1,22 +1,555 @@ /* Javascript for VectorDrawXBlock. */ -function VectorDrawXBlock(runtime, element) { +function VectorDrawXBlock(runtime, element, init_args) { + 'use strict'; - function updateCount(result) { - $('.count', element).text(result.count); - } + var VectorDraw = function(element_id, settings) { + this.board = null; + this.dragged_vector = null; + this.drawMode = false; + this.history_stack = {undo: [], redo: []}; + this.settings = this.sanitizeSettings(settings); + this.element = $('#' + element_id); - var handlerUrl = runtime.handlerUrl(element, 'increment_count'); + this.element.on('click', '.reset', this.reset.bind(this)); + this.element.on('click', '.add-vector', this.addElementFromList.bind(this)); + this.element.on('click', '.undo', this.undo.bind(this)); + this.element.on('click', '.redo', this.redo.bind(this)); + // Prevents default image drag and drop actions in some browsers. + this.element.on('mousedown', '.jxgboard image', function(evt) { evt.preventDefault(); }); - $('p', element).click(function(eventObject) { - $.ajax({ - type: "POST", - url: handlerUrl, - data: JSON.stringify({"hello": "world"}), - success: updateCount + this.render(); + }; + + VectorDraw.prototype.sanitizeSettings = function(settings) { + // Fill in defaults at top level of settings. + var default_settings = { + width: 550, + height: 400, + axis: false, + background: null, + bounding_box_size: 10, + show_navigation: false, + show_vector_properties: true, + add_vector_label: 'Add Selected Force', + vector_properties_label: 'Vector Properties', + vectors: [], + points: [], + expected_result: {}, + custom_checks: [], + unit_vector_ratio: 1 + }; + _.defaults(settings, default_settings); + var width_scale = settings.width / settings.height, + box_size = settings.bounding_box_size; + settings.bounding_box = [-box_size*width_scale, box_size, box_size*width_scale, -box_size]; + + // Fill in defaults for vectors. + var default_vector = { + type: 'vector', + render: false, + length_factor: 1, + length_units: '', + base_angle: 0, + style: {} + }; + var default_vector_style = { + pointSize: 1, + pointColor: 'red', + width: 4, + color: "blue", + label: null, + labelColor: 'black' + }; + settings.vectors.forEach(function(vector) { + _.defaults(vector, default_vector); + _.defaults(vector.style, default_vector_style); + }); + + // Fill in defaults for points. + var default_point = { + fixed: true, // Default to true for backwards compatibility. + render: true, + style: {} + }; + var default_point_style = { + size: 1, + withLabel: false, + color: 'pink', + showInfoBox: false + }; + settings.points.forEach(function(point) { + _.defaults(point, default_point); + _.defaults(point.style, default_point_style); + point.style.name = point.name; + point.style.fixed = point.fixed; + point.style.strokeColor = point.style.color; + point.style.fillColor = point.style.color; + delete point.style.color; + }); + + return settings; + }; + + VectorDraw.prototype.template = _.template([ + '
', + '' + ].join('\n')); + + VectorDraw.prototype.render = function() { + this.element.html(this.template(this.settings)); + // Assign the jxgboard element a random unique ID, + // because JXG.JSXGraph.initBoard needs it. + this.element.find('.jxgboard').prop('id', _.uniqueId('jxgboard')); + this.createBoard(); + }; + + VectorDraw.prototype.createBoard = function() { + var id = this.element.find('.jxgboard').prop('id'), + self = this; + + this.board = JXG.JSXGraph.initBoard(id, { + keepaspectratio: true, + boundingbox: this.settings.bounding_box, + axis: this.settings.axis, + showCopyright: false, + showNavigation: this.settings.show_navigation + }); + + function getImageRatio(bg, callback) { + $('').attr('src', bg.src).load(function(){ + //technically it's inverse of ratio, but we need it to calculate height + var ratio = this.height / this.width; + callback(bg, ratio); + }); + } + + function drawBackground(bg, ratio) { + var height = (bg.height) ? bg.height : bg.width * ratio; + var coords = (bg.coords) ? bg.coords : [-bg.width/2, -height/2]; + self.board.create('image', [bg.src, coords, [bg.width, height]], {fixed: true}); + } + + if (this.settings.background) { + if (this.settings.background.height) { + drawBackground(this.settings.background); + } + else { + getImageRatio(this.settings.background, drawBackground); + } + } + + this.settings.points.forEach(function(point, idx) { + if (point.render) { + this.renderPoint(idx); + } + }, this); + + this.settings.vectors.forEach(function(vec, idx) { + if (vec.render) { + this.renderVector(idx); + } + }, this); + + this.board.on('down', this.onBoardDown.bind(this)); + this.board.on('move', this.onBoardMove.bind(this)); + this.board.on('up', this.onBoardUp.bind(this)); + }; + + VectorDraw.prototype.renderPoint = function(idx, coords) { + var point = this.settings.points[idx]; + var coords = coords || point.coords; + var board_object = this.board.elementsByName[point.name]; + if (board_object) { + // If the point is already rendered, only update its coordinates. + board_object.setPosition(JXG.COORDS_BY_USER, coords); + return; + } + this.board.create('point', coords, point.style); + if (!point.fixed) { + // Disable the