main.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. /**
  2. * Creates an instance of BlockPy
  3. *
  4. * @constructor
  5. * @this {BlockPy}
  6. * @param {Object} settings - User level settings (e.g., what view mode, whether to mute semantic errors, etc.)
  7. * @param {Object} assignment - Assignment level settings (data about the loaded assignment, user, submission, etc.)
  8. * @param {Object} submission - Unused parameter.
  9. * @param {Object} programs - Includes the source code of any programs to be loaded
  10. */
  11. function BlockPy(settings, assignment, programs) {
  12. this.localSettings = new LocalStorageWrapper('localSettings');
  13. this.initModel(settings);
  14. if (assignment !== undefined) {
  15. this.setAssignment(settings, assignment, programs);
  16. }
  17. this.initModelMethods();
  18. this.initMain();
  19. }
  20. /**
  21. * The default modules to make available to the user.
  22. *
  23. * @type Array.<String>
  24. */
  25. BlockPy.DEFAULT_MODULES = ['Decisions', 'Iteration', 'Calculation', 'Variables', 'Values',
  26. 'Lists', 'Functions', 'Output'];
  27. /**
  28. * Initializes the BlockPy object by initializing its interface,
  29. * model, and components.
  30. *
  31. */
  32. BlockPy.prototype.initMain = function () {
  33. this.turnOnHacks();
  34. this.initInterface();
  35. this.applyModel();
  36. this.initComponents();
  37. if (this.model.settings.developer()) {
  38. this.initDevelopment();
  39. }
  40. }
  41. /**
  42. * Initializes the User Inteface for the instance, by loading in the
  43. * HTML file (which has been manually encoded into a JS string using
  44. * the build.py script). We do this because its a giant hassle to keep
  45. * HTML correct when it's stored in JS strings. We should look into
  46. * more sophisticated templating features, probably.
  47. *
  48. */
  49. BlockPy.prototype.initInterface = function () {
  50. var constants = this.model.constants;
  51. // Refer to interface.js, interface.html, and build.py
  52. var _str = $(constants.attachmentPoint).html();
  53. //$(BlockPyInterface
  54. constants.container = $(constants.attachmentPoint).html($(_str))
  55. }
  56. /**
  57. * Applys the KnockoutJS bindings to the model, instantiating the values into the
  58. * HTML.
  59. */
  60. BlockPy.prototype.applyModel = function () {
  61. ko.applyBindings(this.model);
  62. }
  63. /**
  64. * Initializes each of the relevant components of BlockPy. For more information,
  65. * consult each of the component's relevant JS file in turn.
  66. */
  67. BlockPy.prototype.initComponents = function () {
  68. var container = this.model.constants.container;
  69. this.components = {};
  70. var main = this,
  71. components = this.components;
  72. // Each of these components will take the BlockPy instance, and possibly a
  73. // reference to the relevant HTML location where it will be embedded.
  74. components.dialog = new BlockPyDialog(main, container.find('.blockpy-popup'));
  75. components.toolbar = new BlockPyToolbar(main, container.find('.blockpy-toolbar'));
  76. components.feedback = new BlockPyFeedback(main, container.find('.blockpy-feedback'));
  77. components.editor = new BlockPyEditor(main, container.find('.blockpy-editor'));
  78. components.presentation = new BlockPyPresentation(main, container.find('.blockpy-presentation'));
  79. components.printer = new BlockPyPrinter(main, container.find('.blockpy-printer'));
  80. components.engine = new BlockPyEngine(main);
  81. components.server = new BlockPyServer(main);
  82. components.corgis = new BlockPyCorgis(main);
  83. components.history = new BlockPyHistory(main);
  84. components.english = new BlockPyEnglish(main);
  85. components.editor.setMode();
  86. main.model.status.server('Loaded')
  87. var statusBox = container.find(".blockpy-status-box");
  88. main.model.status.server.subscribe(function (newValue) {
  89. if (newValue == "Error" ||
  90. newValue == "Offline" ||
  91. newValue == "Disconnected") {
  92. if (!statusBox.is(':animated')) {
  93. statusBox.effect("shake");
  94. }
  95. } else if (newValue == "Out of date") {
  96. if (!statusBox.is(':animated')) {
  97. statusBox.effect("shake").effect("shake");
  98. }
  99. }
  100. });
  101. statusBox.tooltip();
  102. }
  103. /**
  104. * Initiailizes certain development data, useful for testing out new modules in
  105. * Skulpt.
  106. */
  107. BlockPy.prototype.initDevelopment = function () {
  108. /*$.get('src/skulpt_ast.js', function(data) {
  109. Sk.builtinFiles['files']['src/lib/ast/__init__.js'] = data;
  110. });*/
  111. }
  112. /**
  113. * Helper function for setting the current code, optionally in the given filename.
  114. *
  115. * @param {String} code - The new Python source code to set.
  116. * @param {String?} name - An optional filename (e.g,. '__main__') to update. Defaults to the currently selected filename.
  117. * @returns {Boolean} - whether the code was updated (i.e. there was a diff between new and old).
  118. */
  119. BlockPy.prototype.setCode = function (code, name) {
  120. if (name === undefined) {
  121. name = this.model.settings.filename();
  122. }
  123. var original = this.model.programs[name]();
  124. this.model.programs[name](code);
  125. return original != this.model.programs[name]();
  126. }
  127. /**
  128. * Initializes the model to its defaults
  129. */
  130. BlockPy.prototype.initModel = function (settings) {
  131. var getDefault = this.localSettings.getDefault.bind(this.localSettings);
  132. this.model = {
  133. // User level settings
  134. 'settings': {
  135. // Default mode when you open the screen is text
  136. // 'Text', 'Blocks', "Split"
  137. 'editor': ko.observable(settings.editor || getDefault('editor', 'Split')),
  138. // Default mode when you open the screen is instructor
  139. // boolean
  140. 'instructor': ko.observable(settings.instructor),
  141. // Track the original value
  142. // boolean
  143. 'instructor_initial': ko.observable(settings.instructor),
  144. // Internal for Refresh mechanism to fix broken logs
  145. // String
  146. 'log_id': ko.observable(null),
  147. // boolean
  148. 'enable_blocks': ko.observable(true),
  149. // Whether the canvas is read-only
  150. // boolean
  151. 'read_only': ko.observable(false),
  152. // The current filename that we are editing
  153. // string
  154. 'filename': ko.observable("__main__"),
  155. // boolean
  156. 'show_settings': ko.observable(false),
  157. // boolean
  158. 'disable_semantic_errors': ko.observable(false),
  159. // boolean
  160. 'disable_variable_types': ko.observable(false),
  161. // boolean
  162. 'disable_timeout': ko.observable(false),
  163. // boolean
  164. 'auto_upload': ko.observable(true),
  165. // boolean
  166. 'developer': ko.observable(false),
  167. // boolean
  168. 'mute_printer': ko.observable(false),
  169. // function
  170. 'completedCallback': settings.completedCallback,
  171. // boolean
  172. 'server_connected': ko.observable(true)
  173. },
  174. // Assignment level settings
  175. 'assignment': {
  176. 'modules': ko.observableArray(BlockPy.DEFAULT_MODULES),
  177. 'files': ko.observableArray([]),
  178. 'assignment_id': ko.observable(null),
  179. 'student_id': null,
  180. 'course_id': null,
  181. 'group_id': null,
  182. 'version': ko.observable(0),
  183. 'name': ko.observable('Untitled'),
  184. 'introduction': ko.observable(''),
  185. "initial_view": ko.observable('Split'),
  186. 'parsons': ko.observable(false),
  187. 'upload': ko.observable(false),
  188. 'importable': ko.observable(false),
  189. 'disable_algorithm_errors': ko.observable(false),
  190. 'disable_timeout': ko.observable(false)
  191. },
  192. // Programs' actual code
  193. 'programs': {
  194. "__main__": ko.observable(''),
  195. "starting_code": ko.observable(''),
  196. "give_feedback": ko.observable(''),
  197. "on_change": ko.observable(''),
  198. "answer": ko.observable('')
  199. },
  200. // Information about the current run of the program
  201. 'execution': {
  202. // 'waiting', 'running'
  203. 'status': ko.observable('waiting'),
  204. // integer
  205. 'step': ko.observable(0),
  206. // integer
  207. 'last_step': ko.observable(0),
  208. // list of string/list of int
  209. 'output': ko.observableArray([]),
  210. // integer
  211. 'line_number': ko.observable(0),
  212. // array of simple objects
  213. 'trace': ko.observableArray([]),
  214. // integer
  215. 'trace_step': ko.observable(0),
  216. // boolean
  217. 'show_trace': ko.observable(false),
  218. // object: strings => objects
  219. 'reports': {},
  220. // objects: strings => boolean
  221. 'suppressions': {}
  222. },
  223. // Internal and external status information
  224. 'status': {
  225. // boolean
  226. 'loaded': ko.observable(false),
  227. // Status text
  228. // string
  229. 'text': ko.observable("Loading"),
  230. // 'none', 'runtime', 'syntax', 'semantic', 'feedback', 'complete', 'editor'
  231. 'error': ko.observable('none'),
  232. // "Loading", "Saving", "Ready", "Disconnected", "Error"
  233. 'server': ko.observable("Loading"),
  234. // Some message from a server error can go here
  235. 'server_error': ko.observable(''),
  236. // Dataset loading
  237. // List of promises
  238. 'dataset_loading': ko.observableArray()
  239. },
  240. // Constant globals for this page, cannot be changed
  241. 'constants': {
  242. // string
  243. 'blocklyPath': settings.blocklyPath,
  244. // boolean
  245. 'blocklyScrollbars': true,
  246. // string
  247. 'attachmentPoint': settings.attachmentPoint,
  248. // JQuery object
  249. 'container': null,
  250. // Maps codes ('log_event', 'save_code') to URLs
  251. 'urls': settings.urls
  252. },
  253. }
  254. }
  255. /**
  256. * Define various helper methods that can be used in the view, based on
  257. * data from the model.
  258. */
  259. BlockPy.prototype.initModelMethods = function () {
  260. // The code for the current active program file (e.g., "__main__")
  261. this.model.program = ko.computed(function () {
  262. return this.programs[this.settings.filename()]();
  263. }, this.model) //.extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 400 } });
  264. // Whether this URL has been specified
  265. this.model.server_is_connected = function (url) {
  266. return this.settings.server_connected() &&
  267. this.constants.urls !== undefined && this.constants.urls[url] !== undefined;
  268. };
  269. var modelSettings = this.model.settings;
  270. this.model.showHideSettings = function () {
  271. modelSettings.show_settings(!modelSettings.show_settings());
  272. };
  273. // Helper function to map error statuses to UI elements
  274. this.model.status_feedback_class = ko.computed(function () {
  275. switch (this.status.error()) {
  276. default: case 'none': return ['label-none', ''];
  277. case 'runtime': return ['label-runtime-error', 'Runtime Error'];
  278. case 'syntax': return ['label-syntax-error', 'Syntax Error'];
  279. case 'editor': return ['label-syntax-error', 'Editor Error'];
  280. case 'internal': return ['label-internal-error', 'Internal Error'];
  281. case 'semantic': return ['label-semantic-error', 'Algorithm Error'];
  282. case 'feedback': return ['label-feedback-error', 'Incorrect Answer'];
  283. case 'complete': return ['label-problem-complete', 'Complete'];
  284. case 'no errors': return ['label-no-errors', 'No errors'];
  285. }
  286. }, this.model);
  287. // Helper function to map Server error statuses to UI elements
  288. this.model.status_server_class = ko.computed(function () {
  289. switch (this.status.server()) {
  290. default: case 'Loading': return ['label-default', 'Loading'];
  291. case 'Offline': return ['label-default', 'Offline'];
  292. case 'Out of date': return ['label-danger', 'Out of Date'];
  293. case 'Loaded': return ['label-success', 'Loaded'];
  294. case 'Logging': return ['label-primary', 'Logging'];
  295. case 'Saving': return ['label-primary', 'Saving'];
  296. case 'Saved': return ['label-success', 'Saved'];
  297. case 'Ungraded': return ['label-warning', 'Ungraded']
  298. case 'Disconnected': return ['label-danger', 'Disconnected'];
  299. case 'Error': return ['label-danger', 'Error'];
  300. }
  301. }, this.model);
  302. // Helper function to map Execution status messages to UI elements
  303. this.model.execution_status_class = ko.computed(function () {
  304. switch (this.execution.status()) {
  305. default: case 'idle': return ['label-success', 'Ready'];
  306. case 'running': return ['label-warning', 'Running'];
  307. case 'changing': return ['label-warning', 'Changing'];
  308. case 'verifying': return ['label-warning', 'Verifying'];
  309. case 'parsing': return ['label-warning', 'Parsing'];
  310. case 'analyzing': return ['label-warning', 'Analyzing'];
  311. case 'student': return ['label-warning', 'Student'];
  312. case 'instructor': return ['label-warning', 'Instructor'];
  313. case 'complete': return ['label-success', 'Idle'];
  314. }
  315. }, this.model);
  316. // Program trace functions
  317. var execution = this.model.execution;
  318. this.model.moveTraceFirst = function (index) {
  319. execution.trace_step(0);
  320. };
  321. this.model.moveTraceBackward = function (index) {
  322. var previous = Math.max(execution.trace_step() - 1, 0);
  323. execution.trace_step(previous);
  324. };
  325. this.model.moveTraceForward = function (index) {
  326. var next = Math.min(execution.trace_step() + 1, execution.last_step());
  327. execution.trace_step(next);
  328. };
  329. this.model.moveTraceLast = function (index) {
  330. execution.trace_step(execution.last_step());
  331. };
  332. this.model.current_trace = ko.pureComputed(function () {
  333. //console.log(execution.trace(), execution.trace().length-1, execution.trace_step())
  334. return execution.trace()[Math.min(execution.trace().length - 1, execution.trace_step())];
  335. });
  336. /**
  337. * Opens a new window to represent the exact value of a Skulpt object.
  338. * Particularly useful for things like lists that can be really, really
  339. * long.
  340. *
  341. * @param {String} type - The type of the value
  342. * @param {Object} exact_value - A Skulpt value to be rendered.
  343. */
  344. this.model.viewExactValue = function (type, exact_value) {
  345. return function () {
  346. if (type == "List") {
  347. var output = exact_value.$r().v;
  348. var newWindow = window.open('about:blank', "_blank");
  349. newWindow.document.body.innerHTML += "<code>" + output + "</code>";
  350. }
  351. }
  352. }
  353. this.model.areBlocksUpdating = ko.pureComputed(function () {
  354. return (!this.assignment.upload() &&
  355. (this.settings.filename() == "__main__" ||
  356. this.settings.filename() == "starting_code"))
  357. }, this.model);
  358. // For performance reasons, batch notifications for execution handling.
  359. // I'm not even sure these have any value any more.
  360. execution.trace.extend({ rateLimit: { timeout: 20, method: "notifyWhenChangesStop" } });
  361. execution.step.extend({ rateLimit: { timeout: 20, method: "notifyWhenChangesStop" } });
  362. execution.last_step.extend({ rateLimit: { timeout: 20, method: "notifyWhenChangesStop" } });
  363. execution.line_number.extend({ rateLimit: { timeout: 20, method: "notifyWhenChangesStop" } });
  364. // Handle Files
  365. var self = this;
  366. this.model.removeFile = function () {
  367. self.model.assignment.files.remove(this.valueOf());
  368. delete self.components.engine.openedFiles[this];
  369. }
  370. this.model.viewFile = function () {
  371. var contents = self.components.engine.openedFiles[this];
  372. var newWindow = window.open('about:blank', "_blank");
  373. newWindow.document.body.innerHTML += "<pre>" + contents + "</pre>";
  374. }
  375. this.model.addFile = function () {
  376. var name = prompt("Please enter the filename.");
  377. if (name !== null) {
  378. self.model.assignment.files.push(name);
  379. self.components.engine.openURL(name, 'file');
  380. }
  381. }
  382. }
  383. /**
  384. * Restores any user interface items to their original states.
  385. * This is used to manually reset anything that isn't tied automatically to
  386. * the model.
  387. */
  388. BlockPy.prototype.resetSystem = function () {
  389. if (this.components) {
  390. this.components.feedback.clear();
  391. this.components.printer.resetPrinter();
  392. }
  393. }
  394. /**
  395. * Function for initializing user, course, and assignment group info.
  396. */
  397. BlockPy.prototype.setUserData = function (student_id, course_id, group_id) {
  398. this.model.assignment['group_id'] = group_id;
  399. this.model.assignment['student_id'] = student_id;
  400. this.model.assignment['course_id'] = course_id;
  401. }
  402. /**
  403. * Helper function for loading in an assignment.
  404. */
  405. BlockPy.prototype.setAssignment = function (settings, assignment, programs) {
  406. this.model.settings.server_connected(false);
  407. this.resetSystem();
  408. // Settings
  409. if (settings.filename) {
  410. this.model.settings['filename'](settings.filename);
  411. }
  412. // Update the current filename ONLY if we're editing the __main__
  413. if (this.model.settings['filename']() == '__main__') {
  414. this.model.settings['editor'](assignment.initial_view);
  415. }
  416. if (settings.instructor) {
  417. this.model.settings['instructor'](settings.instructor);
  418. this.model.settings['instructor_initial'](settings.instructor);
  419. }
  420. this.model.settings['enable_blocks'](settings.blocks_enabled);
  421. this.model.settings['read_only'](settings.read_only);
  422. this.model.settings['show_settings'](settings.show_settings);
  423. this.model.settings['disable_semantic_errors'](
  424. settings.disable_semantic_errors ||
  425. assignment.disable_algorithmic_errors ||
  426. false);
  427. this.model.settings['disable_variable_types'](settings.disable_variable_types);
  428. this.model.settings['disable_timeout'](settings.disable_timeout ||
  429. assignment.disable_timeout);
  430. this.model.settings['developer'](settings.developer);
  431. if (settings.completedCallback) {
  432. this.model.settings['completedCallback'] = settings.completedCallback;
  433. }
  434. // Assignment
  435. if (assignment.modules) {
  436. var new_modules = expandArray(this.model.assignment['modules'](),
  437. assignment.modules.added || [],
  438. assignment.modules.removed || []);
  439. this.model.assignment['modules'](assignment.modules.added);
  440. }
  441. if (assignment.files) {
  442. this.model.assignment['files'](assignment.files);
  443. }
  444. this.model.assignment['assignment_id'](assignment.assignment_id);
  445. this.model.assignment['group_id'] = assignment.group_id;
  446. this.model.assignment['student_id'] = assignment.student_id;
  447. this.model.assignment['course_id'] = assignment.course_id;
  448. this.model.assignment['version'](assignment.version);
  449. this.model.assignment['name'](assignment.name);
  450. this.model.assignment['introduction'](assignment.introduction);
  451. if (assignment.initial_view) {
  452. this.model.assignment['initial_view'](assignment.initial_view);
  453. }
  454. if (assignment.has_files) {
  455. this.model.assignment['has_files'](assignment.has_files);
  456. }
  457. this.model.assignment['parsons'](assignment.parsons);
  458. this.model.assignment['upload'](assignment.upload);
  459. if (assignment.importable) {
  460. this.model.assignment['importable'](assignment.importable);
  461. }
  462. if (assignment.disable_algorithm_errors) {
  463. this.model.assignment['disable_algorithm_errors'](assignment.disable_algorithm_errors);
  464. }
  465. // Programs
  466. if (programs.__main__ !== undefined) {
  467. this.model.programs['__main__'](programs.__main__);
  468. }
  469. if (assignment.starting_code !== undefined) {
  470. this.model.programs['starting_code'](assignment.starting_code);
  471. }
  472. if (assignment.give_feedback !== undefined) {
  473. this.model.programs['give_feedback'](assignment.give_feedback);
  474. }
  475. if (assignment.on_change !== undefined) {
  476. this.model.programs['on_change'](assignment.on_change);
  477. }
  478. this.model.programs['answer'](assignment.answer);
  479. // Update Model
  480. // Reload blockly
  481. // Reload CodeMirror
  482. // Reload summernote
  483. this.components.editor.reloadIntroduction();
  484. this.model.settings.server_connected(true)
  485. this.components.corgis.loadDatasets(true);
  486. this.components.engine.loadAllFiles(true);
  487. this.components.server.setStatus('Loaded');
  488. //const _lang = getUrlLanguage();
  489. //addLibButton(_lang);
  490. }
  491. /**
  492. * Function for running any code that fixes bugs and stuff upstream.
  493. * Not pleasant that this exists, but better to have it isolated than
  494. * just lying about randomly...
  495. *
  496. */
  497. BlockPy.prototype.turnOnHacks = function () {
  498. /*
  499. * jQuery UI shake - Padding disappears
  500. * Courtesy: http://stackoverflow.com/questions/22301972/jquery-ui-shake-padding-disappears
  501. */
  502. if ($.ui) {
  503. (function () {
  504. var oldEffect = $.fn.effect;
  505. $.fn.effect = function (effectName) {
  506. if (effectName === "shake" || effectName.effect == "shake") {
  507. var old = $.effects.createWrapper;
  508. $.effects.createWrapper = function (element) {
  509. var result;
  510. var oldCSS = $.fn.css;
  511. $.fn.css = function (size) {
  512. var _element = this;
  513. var hasOwn = Object.prototype.hasOwnProperty;
  514. return _element === element && hasOwn.call(size, "width") && hasOwn.call(size, "height") && _element || oldCSS.apply(this, arguments);
  515. };
  516. result = old.apply(this, arguments);
  517. $.fn.css = oldCSS;
  518. return result;
  519. };
  520. }
  521. return oldEffect.apply(this, arguments);
  522. };
  523. })();
  524. }
  525. // Fix Function#name on browsers that do not support it (IE):
  526. // Courtesy: http://stackoverflow.com/a/17056530/1718155
  527. if (!(function f() { }).name) {
  528. Object.defineProperty(Function.prototype, 'name', {
  529. get: function () {
  530. var name = (this.toString().match(/^function\s*([^\s(]+)/) || [])[1];
  531. // For better performance only parse once, and then cache the
  532. // result through a new accessor for repeated access.
  533. Object.defineProperty(this, 'name', { value: name });
  534. return name;
  535. }
  536. });
  537. }
  538. }