From 398f419a139dce9ca425bb81600641d32ff1fd63 Mon Sep 17 00:00:00 2001 From: Katharine Berry Date: Mon, 3 Aug 2015 14:29:58 -0700 Subject: [PATCH 001/184] yay. --- ide/static/ide/css/ide.css | 2 + ide/static/ide/js/editor.js | 6 +- ide/static/ide/js/monkeyscript.js | 137 ++++++++++++++++++++ ide/static/ide/js/test_editor.js | 0 ide/templates/ide/project.html | 4 + ide/templates/ide/project/monkeyscript.html | 8 ++ 6 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 ide/static/ide/js/monkeyscript.js create mode 100644 ide/static/ide/js/test_editor.js create mode 100644 ide/templates/ide/project/monkeyscript.html diff --git a/ide/static/ide/css/ide.css b/ide/static/ide/css/ide.css index 02b09231..ec47b84e 100644 --- a/ide/static/ide/css/ide.css +++ b/ide/static/ide/css/ide.css @@ -1004,3 +1004,5 @@ span.cm-autofilled-end { padding: 10px 25px 10px 10px; } +/* Monkeytest stuff. */ + diff --git a/ide/static/ide/js/editor.js b/ide/static/ide/js/editor.js index 444d872b..b067c521 100644 --- a/ide/static/ide/js/editor.js +++ b/ide/static/ide/js/editor.js @@ -67,7 +67,7 @@ CloudPebble.Editor = (function() { //highlightSelectionMatches: true, smartIndent: true, indentWithTabs: !USER_SETTINGS.use_spaces, - mode: (is_js ? 'javascript' : CloudPebble.Editor.PebbleMode), + mode: (is_js ? 'javascript' : 'MonkeyScript'), styleActiveLine: true, value: source, theme: USER_SETTINGS.theme, @@ -344,8 +344,8 @@ CloudPebble.Editor = (function() { sChecking = false; }); }, 2000); - code_mirror.on('change', throttled_check); - throttled_check(); + //code_mirror.on('change', throttled_check); + //throttled_check(); code_mirror.on('mousedown', function(cm, e) { if(e.ctrlKey || e.metaKey) { diff --git a/ide/static/ide/js/monkeyscript.js b/ide/static/ide/js/monkeyscript.js new file mode 100644 index 00000000..c042496c --- /dev/null +++ b/ide/static/ide/js/monkeyscript.js @@ -0,0 +1,137 @@ +/** + * Created by katharine on 7/23/15. + */ + +$(function() { + EditorModeMonkeyScript = function() { + return { + startState: function() { + return {current_block: null, did_setup: false, did_test: false, keyword: null, command: null}; + }, + token: function(stream, state) { + if (stream.peek() == '#') { + stream.skipToEnd(); + return 'comment'; + } + if(stream.eatSpace()) { + return; + } + if (state.current_block === null) { + var block; + if ((block = stream.match(/^[a-zA-Z0-9_]+/))) { + if (!block) { + stream.skipToEnd(); + return 'error'; + } + block = block[0]; + if (block == 'setup') { + var result = state.did_setup ? 'error' : 'keyword'; + state.current_block = 'setup-brace'; + return result; + } else if (block == 'test') { + var result = state.did_test ? 'error' : 'keyword'; + state.current_block = 'test-name'; + return result; + } else { + stream.eatWhile(/^[^\s]/); + return 'error'; + } + } else { + stream.skipToEnd(); + return 'error'; + } + } else if (state.current_block == 'test-name') { + if (stream.match(/^[\w\s]+/)) { + state.current_block = 'test-brace'; + return 'string'; + } else { + stream.eatSpace(); + if (stream.eat('{')) { + state.current_block = 'test'; + } + return 'error'; + } + } else if (state.current_block == 'test-brace') { + if (stream.eat('{')) { + state.current_block = 'test'; + state.did_test = true; + return 'bracket'; + } + } else if (state.current_block == 'setup-brace') { + if (stream.eat('{')) { + state.current_block = 'setup'; + state.did_setup = true; + return 'bracket'; + } + } else if (state.current_block == 'setup' || state.current_block == 'test') { + if (stream.sol()) { + state.keyword = null; + } + if (stream.eat('}')) { + state.current_block = null; + return 'bracket'; + } + if (state.keyword === null) { + var command = stream.match(/^[a-z_]+/i); + if (!command) { + stream.skipToEnd(); + return 'error'; + } + command = command[0]; + if (command == 'do' || command == 'expect' || command == 'context') { + state.keyword = command; + return 'keyword'; + } else { + return 'error'; + } + } else { + console.log(state.keyword, state.command); + if (state.command === null) { + var thing = stream.match(/^[a-z_]+/i); + if (!thing) { + stream.skipToEnd(); + return 'error'; + } + thing = thing[0]; + var list = []; + if (state.keyword == 'do') { + list = ['single_click', 'long_click', 'wait', 'screenshot', 'reset', 'set_time', + 'install_app', 'remove_app', 'launch_app', 'airplane_mode', 'command', + 'factory_reset', 'charging', 'power_testing_enable', + 'power_testing_disable', 'multi_click']; + } else if (state.keyword == 'expect') { + list = ['equal', 'not_equal', 'power-between', 'screenshot_app', 'screenshot', + 'reset_output', 'captured_output', 'window']; + } else if (state.keyword == 'context') { + list = ['bigboard']; + } + if (_.contains(list, thing)) { + if (stream.eol()) { + state.command = null; + state.keyword = null; + } else { + state.command = thing; + } + return 'variable'; + } else { + stream.skipToEnd(); + state.command = null; + state.keyword = null; + return 'error'; + } + } else { + stream.skipToEnd(); + state.keyword = null; + state.command = null; + return null; + } + } + } + }, + + lineComment: '//' + } + }; + + CodeMirror.defineMode('MonkeyScript', EditorModeMonkeyScript); +}); diff --git a/ide/static/ide/js/test_editor.js b/ide/static/ide/js/test_editor.js new file mode 100644 index 00000000..e69de29b diff --git a/ide/templates/ide/project.html b/ide/templates/ide/project.html index c5d963a9..1a5612e0 100644 --- a/ide/templates/ide/project.html +++ b/ide/templates/ide/project.html @@ -64,6 +64,8 @@

  • + @@ -118,6 +120,7 @@

    {% include "ide/project/github.html" %} {% include "ide/project/ui-editor.html" %} {% include "ide/project/timeline.html" %} +{% include "ide/project/monkeyscript.html" %} {% endblock %} {% block modals %} @@ -483,6 +486,7 @@

    {% trans 'Compass and Accelerometer' %}

    + diff --git a/ide/templates/ide/project/monkeyscript.html b/ide/templates/ide/project/monkeyscript.html new file mode 100644 index 00000000..e31bd167 --- /dev/null +++ b/ide/templates/ide/project/monkeyscript.html @@ -0,0 +1,8 @@ +{% load i18n %} +
    + +
    +
    + +
    +
    From 0b01fb18938b3ec60c144751bd84ca26bb39413f Mon Sep 17 00:00:00 2001 From: Joseph Atkins-Turkish Date: Sat, 10 Oct 2015 15:43:01 -0700 Subject: [PATCH 002/184] Built an interface for uploading test screenshots --- ide/static/ide/css/ide.css | 88 +++- ide/static/ide/js/live_settings_form.js | 71 +++- ide/static/ide/js/screenshot_manager.js | 396 ++++++++++++++++++ ide/templates/ide/project.html | 1 + ide/templates/ide/project/monkeyscript.html | 48 ++- ide/views/project.py | 1 + root/static/common/img/screenshot-aplite.png | Bin 0 -> 2264 bytes .../{screenshot.png => screenshot-basalt.png} | Bin root/static/common/img/screenshot-chalk.png | Bin 0 -> 7781 bytes root/static/common/img/screenshot-empty.png | Bin 0 -> 2055 bytes 10 files changed, 558 insertions(+), 47 deletions(-) create mode 100644 ide/static/ide/js/screenshot_manager.js create mode 100644 root/static/common/img/screenshot-aplite.png rename root/static/common/img/{screenshot.png => screenshot-basalt.png} (100%) create mode 100644 root/static/common/img/screenshot-chalk.png create mode 100644 root/static/common/img/screenshot-empty.png diff --git a/ide/static/ide/css/ide.css b/ide/static/ide/css/ide.css index b38c23b3..6c5972bf 100644 --- a/ide/static/ide/css/ide.css +++ b/ide/static/ide/css/ide.css @@ -1028,17 +1028,11 @@ span.cm-autofilled-end { /* Monkey editor */ #main-pane { - right: 700px; -} - -.monkey-pane { - padding: 10px; - overflow: scroll; - height: 100%; + right: 640px; } #right-pane { - left: calc(100% - 700px); + left: calc(100% - 640px); right: 0; top: 0; bottom: 0; @@ -1046,17 +1040,89 @@ span.cm-autofilled-end { border-left: #444 3px solid; } +.monkey-pane { + padding: 10px; + overflow: scroll; + height: 100%; + background-color: #333; +} + + .monkey-pane h2 { padding: 10px; } -.monkey-screenshots .image-resource-preview { - padding-top: 30px; +.monkey-screenshot-set { + margin-bottom: 30px; +} + +.monkey-pane .image-resource-preview { width: 200px; display: inline-block; /* line-height: 280px; */ } -.monkey-screenshots .image-resource-preview img { +.monkey-pane .image-resource-preview img { width: 180px; +} + +.monkey-platforms div { + width: 200px; + color: white; + font-size: 30px; + display: inline-block; + text-align: center; +} + +.monkey-screenshot-name { + color: white; + font-weight: bold; + font-size: 20px; +} + +.aplite-only .platform-chalk, .aplite-only .platform-basalt, .basalt-only .platform-aplite, .basalt-only .platform-chalk, .chalk-only .platform-aplite, .chalk-only .platform-basalt { + display: none; +} + +.aplite-only .monkey-platforms, .chalk-only .monkey-platforms, .basalt-only .monkey-platforms { + display: none; +} + +.monkey-inline .monkey-screenshot-set, #monkey-upload-previews .image-resource-preview { + display: inline-block; +} + +.monkey-pane h2 > span { + display: none; +} + +.monkey-inline h2 > span { + display: inline; +} + +.monkey-inline .monkey-screenshot-set { + width: 200px; + height: 270px; + vertical-align: top; +} + +.monkey-screenshot-set input { + width: calc(100% - 50px); +} + +.monkey-screenshots .image-resource-preview img { + cursor: pointer; + background-color: rgba(100, 100, 100, 0.5); +} + +.monkey-screenshots .monkey-modified { + border: 2px white dashed; +} + +.monkey-screenshots .monkey-hover { + border: 2px #5bc0de dashed; +} + +.monkey-screenshots .settings-status-icons { + padding-right: 10px; } \ No newline at end of file diff --git a/ide/static/ide/js/live_settings_form.js b/ide/static/ide/js/live_settings_form.js index f54e62d8..d16e05c9 100644 --- a/ide/static/ide/js/live_settings_form.js +++ b/ide/static/ide/js/live_settings_form.js @@ -4,28 +4,32 @@ function make_live_settings_form(options) { save_function: null, form: null, error_function: console.log, - control_selector: 'input, select', - changeable_control_selector: "input[type!='number']", + on_change_function: null, + control_selector: 'input, select, textarea', + changeable_control_selector: "input[type!='number'], textarea", label_selector: '.control-group label', group_selector: '.control-group' }); if (!_.isFunction(opts.save_function) || (!_.isObject(opts.form)) - || (!_.isFunction(opts.error_function))) { + || (!_.isFunction(opts.error_function)) + || (!!opts.on_change_function && !_.isFunction(opts.on_change_function))) { throw "Invalid arguments to make_live_settings_form"; } - var save = function(element) { + var save = function(element, event) { // After the form is saved, flash the 'tick' icon on success or keep a 'changed' icon on error. - opts.save_function() - .done(function() { + var promise = opts.save_function(event); + + if (promise) { + promise.done(function () { clear_changed_icons(); if (element) flash_tick_icon(element); - }) - .fail(function(error) { + }).fail(function (error) { opts.error_function(error); if (element) show_changed_icon(element); }); + } }; var clear_changed_icons = function() { @@ -39,34 +43,47 @@ function make_live_settings_form(options) { element.parents(opts.group_selector).find('.setting-saved').show().delay(1000).hide('fast'); }; - var show_changed_icon = function(element) { + var show_changed_icon = function(element, speed) { // Show the 'changed' icon for an element - element.parents(opts.group_selector).find('.setting-changed').show('fast'); + speed = (speed === undefined ? 'fast' : speed); + element.parents(opts.group_selector).find('.setting-changed').show(speed); + if (_.isFunction(opts.on_change_function)) { + opts.on_change_function(element); + } }; var hookup_elements = function(elements) { - // Set up a hook for any changed form elements - elements.find(opts.control_selector).change(function() { - save($(this)); + elements.on("change", opts.control_selector, function(e) { + console.log("GHCAFAS"); + if (_.isFunction(opts.on_change_function)) { + opts.on_change_function(this); + } + save($(this), e); }); // While typing in text forms, show the changed icon - elements.find(opts.changeable_control_selector).on('input', function() { - show_changed_icon($(this)); + elements.on('input', opts.changeable_control_selector, function(e) { + console.log("OMG"); + show_changed_icon($(this), e); }); }; - var init = function() { - // Add status icons to every form element + var add_status_icon = function(element, changed) { $("" + "" + "" + "") - .insertAfter(opts.form.find(opts.label_selector)) + .insertAfter(element) .children().hide(); + if (changed === true) { + show_changed_icon(element, null); + } + }; - + var init = function() { + // Add status icons to every form element + add_status_icon(opts.form.find(opts.label_selector)); // When a form-reset button is clicked, submit the form instantly $(opts.form).bind("reset", function() { var values = {}; @@ -101,15 +118,25 @@ function make_live_settings_form(options) { hookup_elements(opts.form); }; - return { + var self = { + clearIcons: function() { + console.log("Clearing icons!"); + clear_changed_icons(); + }, addElement: function(elements) { hookup_elements(elements); }, init: function() { init(); + return self; }, save: function(element) { save(element); + }, + addStatusIcon: function(element, changed) { + add_status_icon(element, changed); } - } -} + }; + + return self; +} \ No newline at end of file diff --git a/ide/static/ide/js/screenshot_manager.js b/ide/static/ide/js/screenshot_manager.js new file mode 100644 index 00000000..d3de6fe2 --- /dev/null +++ b/ide/static/ide/js/screenshot_manager.js @@ -0,0 +1,396 @@ +(function() { + var current_platforms = ['aplite', 'basalt', 'chalk']; + + + function ScreenshotFile(options) { + var options = _.defaults(options || {}, { + is_new: false, + id: null, + file: null, + src: "" + }); + this.is_new = options.is_new; + this.id = options.id; + this.file = options.file; + this.src = options.src; + } + + /** + * A mock API (for now) + * @type {{get_screenshots}} + */ + var MockAPI = function() { + var screenshots = [{ + name: "Screenshot set 1", + id: 0, + images: { + aplite: new ScreenshotFile({src: "/static/common/img/screenshot-aplite.png", id: 0}), + basalt: new ScreenshotFile({src: "/static/common/img/screenshot-basalt.png", id: 1}), + chalk: new ScreenshotFile({src: "/static/common/img/screenshot-chalk.png", id: 2}) + } + }, { + name: "Screenshot set 2", + id: 1, + images: { + aplite: new ScreenshotFile({src: "/static/common/img/screenshot-aplite.png", id: 3}), + basalt: new ScreenshotFile({src: "/static/common/img/screenshot-basalt.png", id: 4}), + chalk: new ScreenshotFile({src: "/static/common/img/screenshot-chalk.png", id: 5}) + } + }]; + + /** + * Put screenshot data in a format ready to be sent. + * @param screenshots + * @returns {{screenshots: Array, files: Array}} + */ + var process_screenshots = function(screenshots) { + var screenshots_data = []; + var files = []; + _.each(screenshots, function(screenshot) { + var shot_data = {name: screenshot.name, id: screenshot.id}; + _.each(screenshot.images, function(image, platform) { + shot_data[platform] = {id: image.id}; + if (image.file !== null) { + shot_data[platform].uploadId = files.length; + files.push(image.file); + } + }, this); + screenshots_data.push(shot_data); + }, this); + return { + screenshots: screenshots_data, + files: files + } + }; + + /** + * Get the current list of existing test screenshots + * @param test_name name of test + * @returns {jQuery.Deferred} + */ + this.getScreenshots = function(test_name) { + var defer = $.Deferred(); + setTimeout(function () { + defer.resolve(_.map(screenshots, _.clone)); + }, 700); + return defer.promise(); + }; + + /** + * Save the current state of the screenshots + * @param test_name name of test + * @param new_screenshots + * @returns {*} + */ + this.saveScreenshots = function(test_name, new_screenshots) { + var defer = $.Deferred(); + var data = process_screenshots(new_screenshots); + var form_data = new FormData(); + var screenshot_json = JSON.stringify(data.screenshots); + form_data.append('screenshots', screenshot_json); + _.each(data.files, function(file) { + form_data.append('files[]', file); + }); + + // Made the form data, now we just have to send it. + + setTimeout(function() { + // TODO: AJAX request + screenshots = _.map(new_screenshots, function(shot) { + var new_shot = _.clone(shot); + new_shot.images = _.mapObject(_.clone(new_shot.images), _.partial(_.extend, _, {is_new: false, file: null})); + new_shot._changed = false; + return new_shot; + }); + defer.resolve(); + }, 700); + return defer.promise(); + }; + }; + + var API = new MockAPI(); + + /** + * ScreenshotsModel manages a list of new screenshot files to be uploaded + * @fires ScreenshotsModel.change when files are added or modified + * @constructor + */ + function ScreenshotsModel(test_name) { + var self = this; + var screenshots = []; + var original_screenshots = []; + _.extend(this, Backbone.Events); + + /** + * Update the list of screenshots to be uploaded with some new files. If multiple files are added at one index, + * each file[i] is added to the screenshot[index+i] + * @param files an Array of File objects + * @param index the screenshot index to update, or null for new screenshots + * @param platform a string naming the platform for all of the new screenshots + */ + this.addUploadedFiles = function(files, index, platform) { + if (index === null) { + // Append all new screenshots, given them no name + _.each(files, function(file) { + var upload = { + name: "", + images: {}, + _changed: true + }; + upload.images[platform] = new ScreenshotFile({file: file, is_new: true}); + screenshots.push(upload); + }); + } + else { + _.each(files, function(file, i) { + var upload = screenshots[index + i]; + if (upload) { + // Update existing screenshots at the current index + upload.images[platform] = new ScreenshotFile({file:file, id: upload.images[platform].id, is_new: true}); + } + else { + // If there was no screenshot to update, add the remaining files as new screenshots. + this.addUploadedFiles(files.slice(i), null, platform); + } + }, this); + } + this.trigger('changeScreenshots', screenshots); + }; + + /** + * ScreenshotsModel stores the currently uploaded screenshots + * @constructor + */ + this.loadScreenshots = function() { + API.getScreenshots(test_name).then(function(result) { + screenshots = result; + original_screenshots = _.map(result, _.clone); + self.trigger('changeScreenshots', result); + }, function() { + self.trigger('error', gettext("Error getting screenshots")); + }) + }; + + /** + * Set the screenshot names for each new upload. + * @param names Array of names to apply to each upload + */ + this.setNames = function(names) { + _.each(names, function(name, idx) { + this.setName(idx, name); + }, this); + }; + + this.setName = function(index, name) { + if (_.isString(name)) { + var changed = (!(_.has(original_screenshots, index)) || (name != original_screenshots[index].name)); + screenshots[index].name = name; + screenshots[index]._changed = changed; + self.trigger('changeName', index, name, changed); + } + }; + + this.save = function() { + API.saveScreenshots(test_name, screenshots).then(function() { + console.log("saved1"); + self.trigger('saved'); + }, function() { + // Error? + }); + }; + } + + /** + * Manages a user interface for uploading new screenshots + * @param pane to render the user interface in + * @fires ScreenshotsView.filesSelected when a user tries to upload new files + * @constructor + */ + function ScreenshotsView(pane) { + var self = this; + var screenshot_set_template = pane.find('.monkey-screenshot-set').detach().removeClass('hide'); + var img_selector = '.image-resource-preview img'; + var file_selector = 'input[type="file"]'; + + _.extend(this, Backbone.Events); + + var selected_files = function(files, elm) { + var row = $(elm).parents('.monkey-screenshot-set'); + var col = $(elm).parents('.image-resource-preview'); + self.trigger('filesSelected', files, row.data('index'), col.data('platform')); + }; + + // Enable drag and drop uploads + pane.on('dragover', img_selector, function(e) { + e.preventDefault(); + e.stopPropagation(); + }); + pane.on('dragenter', img_selector, function(e) { + $(this).addClass('monkey-hover'); + e.preventDefault(); + e.stopPropagation(); + }); + pane.on('dragleave', img_selector, function(e) { + $(this).removeClass('monkey-hover'); + e.preventDefault(); + e.stopPropagation(); + }); + pane.on('drop', img_selector, function(e) { + e.preventDefault(); + e.stopPropagation(); + $(this).removeClass('monkey-hover'); + var files = e.originalEvent.dataTransfer.files; + selected_files(files, this); + }); + // Or, upload files by clicking + pane.on('click', img_selector, function(e) { + $(this).siblings('input').click(); + e.stopPropagation(); + e.preventDefault(); + }); + pane.on('change', file_selector, function(e) { + var files = e.target.files; + selected_files(files, this); + }); + pane.on('input', 'input[type="text"]', function(e) { + var index = $(this).parents('.monkey-screenshot-set').data('index'); + var value = $(this).val(); + self.trigger('inputName', index, value); + }); + /* + * Render the list of screenshots + * @param screenshots + */ + this.renderScreenshots = function(screenshots) { + pane.empty(); + _.each(screenshots, function(screenshot, index) { + var template = screenshot_set_template.clone(); + template.find('.monkey-screenshot-name').text(screenshot.name); + + _.each(screenshot.images, function(file, platform) { + var img = template.find('.platform-'+platform+' img'); + if (file.src) { + img.attr('src', file.src); + } + else if (file.file) { + var reader = new FileReader(); + // TODO: loading image + reader.onload = function() { + img.attr('src', reader.result); + file.src = reader.result; + }; + reader.readAsDataURL(file.file); + } + img.toggleClass('monkey-modified', file.is_new); + }); + template.find('.monkey-screenshot-title').removeClass('hide'); + template.find('.monkey-screenshot-title input').val(screenshot.name); + template.find('.monkey-changed').toggleClass('hide', !screenshot._changed); + template.data('index', index); + pane.append(template); + }); + var new_screenshot_pane = screenshot_set_template.clone(); + new_screenshot_pane.data('index', null); + new_screenshot_pane.find('.monkey-screenshot-title').remove(); + pane.append(new_screenshot_pane) + }; + + this.showAsChanged = function(index, changed) { + pane.find('.monkey-screenshot-set').filter(function() { + return $(this).data('index') == index; + }).find('.monkey-changed').toggleClass('hide', !changed); + }; + + this.getNames = function() { + var names = []; + pane.find('.monkey-screenshot-set').each(function() { + var idx = $(this).data('index'); + if (idx !== null) { + names[$(this).data('index')] = $(this).find('.monkey-screenshot-title input').val(); + } + }); + return names; + }; + + /** + * Render an error + * @param error message to render + */ + this.renderError = function(error) { + pane.html(interpolate('
    %s
    ', [error])); + }; + } + + /** + * This class manages the save and reset buttons + * @param pane + * @constructor + */ + function FormButtonsView(pane) { + var self = this; + _.extend(this, Backbone.Events); + + pane.on('click', '.btn-cancel', function() { + CloudPebble.Prompts.Confirm(gettext("Reset all changes?"), gettext("This cannot be undone."), function() { + self.trigger('reset'); + }); + }); + + pane.on('click', '.btn-affirmative', function() { + self.trigger('save'); + }); + } + + /** + * Set up the screenshot manager in a pane, and connect models to views. + * @param test_name name of test associated with screenshots + * @param pane HTML element containing monkey screenshot uploader + */ + function init(test_name, pane) { + var screenshots = new ScreenshotsModel(test_name); + var screenshots_view = new ScreenshotsView(pane.find('.monkey-screenshots')); + var buttons_view = new FormButtonsView(pane.find('.monkey-form-buttons')); + + // Render screenshots whenever we fetch them + screenshots.on('changeScreenshots', function(screenshots) { + screenshots_view.renderScreenshots(screenshots); + }).on('error', function(error) { + screenshots_view.renderError(error); + }).on('changeName', function(index, name, changed) { + screenshots_view.showAsChanged(index, changed); + }).on('saved', function() { + console.log("loading"); + screenshots.loadScreenshots(); + }); + + // Update list of uploads when user selects files + screenshots_view.on('filesSelected', function(fileList, index, platform) { + screenshots.setNames(screenshots_view.getNames()); + var files = []; + _.each(fileList, function(file, i) { + files[i] = file; + }); + screenshots.addUploadedFiles(files, index, platform); + }); + screenshots_view.on('inputName', function(index, value) { + screenshots.setName(index, value); + }); + + // Perform actions when form buttons are clicked + buttons_view.on('reset', function() { + screenshots.loadScreenshots(); + }); + buttons_view.on('save', function() { + screenshots.setNames(screenshots_view.getNames()); + screenshots.save(); + }); + + screenshots_view.renderScreenshots([]); + screenshots.loadScreenshots(); + + + } + + init("TEST", $('.monkey-pane')); + +})(); \ No newline at end of file diff --git a/ide/templates/ide/project.html b/ide/templates/ide/project.html index 666f911d..5ac869fb 100644 --- a/ide/templates/ide/project.html +++ b/ide/templates/ide/project.html @@ -504,6 +504,7 @@

    {% trans 'Compass and Accelerometer' %}

    + diff --git a/ide/templates/ide/project/monkeyscript.html b/ide/templates/ide/project/monkeyscript.html index 07b5079d..a64d6654 100644 --- a/ide/templates/ide/project/monkeyscript.html +++ b/ide/templates/ide/project/monkeyscript.html @@ -1,20 +1,40 @@ {% load i18n %} +{#
    #}
    -

    Test Screenshots

    +

    Test Screenshots + {% for platform in current_platforms %} + - {{ platform }} + {% endfor %} +

    +
    + {% for platform in current_platforms %} +
    {{ platform }}
    + {% endfor %} +
    -
    - - -
    -
    -
    +
    +
    +
    + {% for platform in current_platforms %} +
    + + +
    + {% endfor %} +
    + + + + +
    +
    +
    +
    -
    -
    -
    -
    - -
    \ No newline at end of file +
    + + +
    +
    diff --git a/ide/views/project.py b/ide/views/project.py index e7ddd0b8..8b9243e9 100644 --- a/ide/views/project.py +++ b/ide/views/project.py @@ -43,6 +43,7 @@ def view_project(request, project_id): 'libpebble_proxy': json.dumps(settings.LIBPEBBLE_PROXY), 'token': token, 'phone_shorturl': settings.PHONE_SHORTURL, + 'current_platforms': ['aplite', 'basalt', 'chalk'] }) diff --git a/root/static/common/img/screenshot-aplite.png b/root/static/common/img/screenshot-aplite.png new file mode 100644 index 0000000000000000000000000000000000000000..2c7dc948c9745f81e76e684a9f0695e3a8cf27ac GIT binary patch literal 2264 zcmcImdpO%!8cq}wX^R?)q9U4gP2yIU)NNAtDnVF;wo0f=h$M<`T$|REA{4DC8rq_6 z!*;5Q-lhf#ZKYOn43&;9We2==Kf@27Xppcl8 zctiq$C`17OmI)|fNWfEqAPI!1Xfi6n3i=s=62_u55(@bYp+s6ieUD-whhs>1$U%e& z!T@RwhCm>eq>xaQr=7#sw?tC{!f$ZG7=b2@n03QZ)H%S;7L5 zqCZGOgaPs!xA3T?D2s9@h2w?FB7SQ_%g@OF$bHqZM2f`!&CHidKWBxdT7xZ-->%IX z?D|;C4*>WM?rir1HUapd)WdTUC2McoI3}YHPWA(4g-C2Xt{5kmm7p5~)Z*&8hx6g( zSdSl?2lI2>>i!aR-sUoUnz!7!LrhL=+o?iUTXxU5U&r4yiYJ3Pfeuuf+K z+GC&&kd{AjKneX@Y5@T494i--F z4-eOlv>vi;c;nrrqJYcwHk!>hU4l)`T+Iqf8sZn{;?_En%-c&ErFxIA^T{mEEO%y; z6uYq!P!cd2P?~!X`Fge0`u@|)mB~7%86FUyi>0sb`l(RVzR9MT;O{a?s z8UqK*)*bmZGxNEfL!L5!%kU)7jdI>b;iIw&LrT@Nak+ED2G=oCb;sxTP7gE<{2HCu zwb+!Dn6u>?2(fvC9Wy&(@gM z?suyWodQehDl|X_gBYqV+S|IzcvzhJ>&vU=Mdg)9y;;@HHRHgMKAYuU;1zX6*zoL! zN_$+Av;NzZ{jb-avF{U193sqkZ6=oo!)o3PtjLGGpRJWSSp}5wGXoM#c}2l$;WpB7 zQ-n*8;U=K%m8&r5i__QJI-e3V$Kpy@uneIo|;nB#=?X9`Rg9TO3za2{?Y4I zjw4XjSp*P8kyUg-iD25+shx3%Kfgdrg%4J%OcK!?_@!g@Aff36xBA=F>S2l&1xIaQ zuflayBQHGpSXPp>b&%P*FH&xftA5%cZfR>M^w`XL^nhIkR_Ky;n6Gd65_T zIpU`^u&8?j6I2V%QhSQn8?rV##TA0wli0sC-P1>ZJJRdHY+yV@;f|2Afu6;{VPoD`Fkl#j(l zjKh!A2xIrm9WtU$R5-ch}LRReo4?OnaNHLuoT?ZwA}_-fiks&3wMt8)KpoIrj%;-*`(*G_!5$g>QS z3v%rcMlEs+j%U^mVbOa<=INk9E6qVzgB8!F8BXAade-e!+~%Y1qTqT-V~KI;IIYf6$+yZCcN5u7nOZ+Trym*=yGCi9 zp1Gr{SMR>kt9C0jguOf3!spQdFFGBwv)ezK$ZpzO6qr=3iL@Oik#RW+`hWH5Q`0@m zACjz1VMJQ^gx2tNX(T6{2svtGrU%7p_^*`C4gCb~x_2;_5eb^E7WCb2ZX^fZ%CcT4 z3ifvAKg|IZ0*g#hR$+|pTLVgSetYkFL1^HIEu-T_!4m93Z_R>Tu(XQ+^lxcj+V3tV z(bfgPylyUa9%BxHrD8m$s(B$V)p(K?>xx9?62g`!n7A4|ESx^x(p_ lzwm#c{vSL2uj$gZ0h}XhuOen8ibZb_XL~oh2K0&a-vOsK-#-8V literal 0 HcmV?d00001 diff --git a/root/static/common/img/screenshot.png b/root/static/common/img/screenshot-basalt.png similarity index 100% rename from root/static/common/img/screenshot.png rename to root/static/common/img/screenshot-basalt.png diff --git a/root/static/common/img/screenshot-chalk.png b/root/static/common/img/screenshot-chalk.png new file mode 100644 index 0000000000000000000000000000000000000000..33d9cbf4335f4d7bd9a74a6b6586b775de6d0a0e GIT binary patch literal 7781 zcmY*;1y~$Qvo`L*B|y+Xa5gN#7G0b`SllJJ1z+4D1Oh=4+!l8W!6i6>K+pw3(8V1V zclgOU-~Il3=Xqwjr~0k8s=BAAr>8!usVY3dqrpQ#L3yI2D68>UCO)1OI9QL@EQ)aQ z#{$(|LqQs)YUJ7WV_n})OAo5|Mg?Ty?8s|o>1=Mr>+R_Bh(bXT^9DUu9j%~djNXp# zoZLa);>`abK#%pmVgNJaKM<&cIJ4dxHAWd{H!DT~-dDUYnI-TT85zagEUiHrvhx2? zf4mcCwuM4nKmdT3mlv-WKd-Z!4S-KXL2mk_k9uYk5K2A_GZyqQ2=l_!Y509*s zyM>#b3)IfpiSaM5nYptERGgXluc7}Q|N05Fv;O~rIW;E4hL+ienfD&1`QC@9Zb zlw`qL-l+S=xZX)qY4@Fb@!?X;hBIgwVR@0vuZ@3j=#@JbYer2s#5zz_>O7l^t>?EY z+CSYcO4Mm>5u4Qk!(0s-Iex7+>By2}{J}M3d`n9D1w~0Q_-ZkFbMa8}>fz!3qT}Iv z^UxIx`+mRT@Ma^^eH0WNsd_AFffXV}{Pd|J`wQZ*&Ip{RU}_GGC@IyP+(Nsz;pmYg zKZkN>^U%^?mBfG8+)%d)B-BRMvv@JCKY6Nnh3-lx)vpRGI3Q!Y^bMW$Q&rO(<+I;A zC`9*P@GM9Wq~Fi0@Aoa`SEeN;u=1?WjbB#AciVn$)dvmSehX@BN|oQ-#JVs3+SnSg zxWARtI7bNIjR0*WRlzDyKwKCCZU~eXYdwx(9)J~7(gXtj#T3pU5f~aiB|l_1Ypdi= zE08(xi<#R3Q+s3z9z#+1s@m=8@Y)x4dfYR&t8n|O2)X#771=W?h~Y9uL}Qe4l~v5} zsjVh$y^BCPN3v^Zx@M|^X(f%SE)lJCGk2LS7UwRg)pl-Nb#`z+n@6F=)=QU4z(v#+ za)_(@jcJxo1l^9MvEv6Nx!Q0JTKXqXJ#{8+DoVA}nUB7DDx(686V*FJV^Jd(<6kn4 zm-waC26`*k`=A#ypS5}{Tl4{3w%UH!-{&V(gB%=+Q{p_~pE1aZ!-UmSDAwU}g{)A0 zAKYPXj-pIeD zo9C)Kp3t-=BE^N`*Ld4-3YqP)C^t?|yZ-(CgCgwny<%({qQb1N6FO=_1k@_{Y+A0` z7J!0P6+~26rIy=??$(O)ys~X0%WN{Q@#~Y#(KwnXv+gH)MnbU_1tKyLV|IiRo?agC zZo0Emyj|R1apCpql3xba%q^$1Kuu)% zM>(T6InZN!PuDKM;_gP_>G+*2z74~J+q0kt!1mN?zuoP|nO5!f8&1%qJ?_Td3bXH7 z8~^svhZimGy6ECleEzQ-bA4~unyo>w=Ceqic@%=#5dwW zEouJcx6rCAv5>_--}!G%76(oTFMhT`N;x4RSXvXLYKuV=fgzpxelb3EJQLUpu7@{am`P4pI#S%dV;^=AqiD1H_B?mS46x*PMd zAiXd#2c-hd>&3?<5nl9W`Hd zHL}gcjhQYjN(Gfr%jml4hJEw@VdpyRJ$n^2BqM&JT*p8Ljx^|7Urz_JYT=@v%ud>0 z-{Lq>{O)s%tae;fayGgmqGoylC=hP$N?q!2E;l=)$Ywz>JKnZTopyO-=NeWuzt945 zG08b_4ZaL*g5ZL+-K&pU;SRQf5RT6uV!uc0c0h>e-z8M%FW&HA=3%{C?Q}4T(vz!u zqXskG&vdiKp+Y>bH1{&bQ9$70GxJinr`!ZmP3nsjkMGVm^}a2uOEH+U+iA_P^?-1H ze+_&;*-WIN!l~Pdd5OYi*Gb}y_WS;8AjKs2efaV5Cj%OoO%_7O>p?I`8FPFKL(#aJs<(4-KdNjc*Z z2~M2J>TBqf`^gBF@8O|@HMJsE6_bBPg*n+z$be%9j=k@J-Ra6HKlk&4k{PEtEro|G zu(C98mLbHK!id;z4zxc{9QZ9HnEDBoJco$=*7kfsxTWN^m^dE4eCsF_ckaZPzj5&HU4(#bP>&F(21WJH4P z_E30f>6N9+TUY*^kZ_7836?)7cz8UQLb#Ag-snNdx0@0rUP|T1uqbavmz=b8TmcEg8;^p1&FsKl^!y$-Fw6e3PxCJs}xa zV;+Yaur!LiXftI7Z21DP8Urprb;)U zYsM8l^(OO9!VbT0Xqf$$(wWf~`yc@up?RMR1)`nCv3R2}3^Jl1DYSl)Hsw&p(W$QX zt_*n|RDQR%$a>w;T!e!5`^Ku0vY?w3Jc1|_u(T(_C(hpvV<>B1;6d(v2Ug!HPmSrMy_!mwX6DmH0<*80p^8fE zXo=j2!%Ppu)u@+WN%6PtDgU!+|yLb3FjdOSJ+AKk{8f@#}j|1EK--Uj^B|(uyDA9OhWC!vPg9Ebrqt|{7 z8H@Scx87uk7b;ODWU*<2{^XMW5=COPCx$2>w|}oV$(TJ10%E=i_8$Ie;X*FDe8-dh zFeM7eJH=Nkx{(RTZcje)E(ap7p3{q+w-VVJomjw9Uk(B{H_l3@_`>`1zIq&4C_`QslP1hv1PfNAJ;RLKtU zxQyS0U(9HeGCBtRhv|i7KlfDz)LV1x z>IvxLx{wb;LXL}d7u%&8z13Dj&{l9GpXfTxQ4J!nKd3 zBqJhOgM?wfa8DZs35%J#8Pch0TN1R_3j47lgVGjiYK5S?Rj7>wNq5rHxGEDL44OR(Pf4Wd39t@a4?xZg)R<}Lxkp7Y=I{0YF>!h!tq6~jDd)oY%( z|4QGwGe(GVGD3}t#D^t?1Z{N=i{bBpBu?Ipc<8&nY~a_PCnY%)+AJHbD_-fM9D3SW z3q%spHKo~r16qI+PZfe6`fkVyVsm%=;DRDW;#T<4n<=2t7Q)kDF}8~lm2l8n zY2cop#&-PgBrb;dxRu4aVvMJJOQAMCiv&bX`M&iVQS+8c(*E^RfN=pDk?8dm6ax*eo% zC~Ksnis=0zXURvo@Z?t&N^nkyRd-!6Iq7{B=%Gb|*q$6LcTMvW;GPwy-F5#3CTu#Q zoxPnM^#OF5a^lWLDR-KC+k$6bG2?%RQXMK>Z-;wcw zl9PKb4p1!YipF|!NZJK?lwGS<=J07oEZ|g;fr2GY=?tD>3YdtZG@IEQO_@U9y7`sLFQ3+&9rscJPJLpq}-zlJ- zoz;782O9HOVJ{&`&pG#$F9OWSB~~3~&T3khMT_UR5JIzr@$402jaEaa#YYkbRotB* zX;m0muK&_$Cp%C~c9Cam72AkS+iadErh&L?COy=Cv@R;SlWUdgNN=P5lmf0UmIe)~ z&?VpQfY^tZ-KWDv@Ly}1kR3LrZd=c4j#E#2sFiI$x_y-|T1J5cM(eGael z9CZCU#jMWa{G8)nu@V?Jdlt*5B5~I7EkD2+qX$|_Z0I00@f8dD7Onq_$8qNn(kY}b zP(`ri(ePl1#Qx5(VkOUUv4_~bv|7)f44cI5bRpN$VJ@_qtrE8ai3!BlDJX#TqXn9Q z5PYT{hO_g;`^@D(jl(9fmt8MB(E52pqu#wLq;hDevD{Ps1M5x(dyhCJ_-=_%kK?M< zct+r7`t~lS0a2D&9GoEa#hDHGgEUXOsgr!;K_TeYX?Q6N@ou`Ej;Dws7c5$rl*|+(K;2 zoMBZADBTvw^<84)xxuDnR;gyHu_s;Ei%Mf1;3TsndOaC7JGv{pou z%CGBF@${z0Lp`%qLKCP{7*zjhC7)DvB=kwrliOos+TLMH!(ij*?eO3F`{?McPaQpT z`vfmB8Mn{y*b5{}BE0{i<>!+s%OC5>gm)Pu4}xzPTpm#ugoYQBnI>Y2anDy_ z9>AlD4t!3yZ_~$5T#>JKZ>!;_A;LzOXF9FgYH%!@_8gQbW&`6RUiG3CwcoORWU@{F zOz4R6oaqyt?ql0>{wuBUcQuxznZKG=mLbPP#!?I|&3JgRLQ&?Gr*&{!PG)!=9`hxK_8T{86zp?{c{ z)fhU|%tjp(69TB0jWy^$Vf!%4;27@w14tACA*?;F+y!sUB;=U9YrwOwB1!Chxq7&n zXoz_hT7-=o%mxmJYe98Fn|rqSTpCGRP0KP z*4u#ZN(-~rK&c`!L%;0J>4pjXeN~rJUTUt7Ck~A5A zT%w*&q)!A}aelR)%Kp2I-;r!cruVV1KN~qp`H}3Biu9jDnl0P7Cy3w#>v9kx57u2M zxBJOx;sjqA%r6L+O#nw{j>p9cSkY1+cWH&&Z9~q-64qdzLoxZe-kC)l6U-w_I2$Y9 zn;SnQu1QjeGxDI%oRS>v?rae~`KvR7c=z!;`UzUpEJ&7f6yQg!IH5lHx6!ZN?Oz3+ad6FZc5O?R0vat+o)|02>N`$m0iVB*L(jrtYU# z-)Y*)-U50T{#NM*mj5s>7a<#d!d7QFL$sk>7w-oN(xlBtu0q3bjjVW4EsWGE`Z)UBFB;6k^o7 zRA)DkKIHPl(lg1Z4RUh}bBnuh6eKp$1SLNN!Nnf!z&*-Cf^(T=m!Rl$`ez=lyouCX zYJZCTY+;3Osf{UE4%@+}!b#6*ts+c-PQms^i{m}EyJMcJB5t_%SOQ;=3>_UM*-?C& z&1j~r@3>>6;u=>x!F*wAC;sKp(yC?l%6nYAamP*BrMj+bs$f!wp9P#Zlc|lkU=;76 zbTno(0D{d{Ml;B4EMyQVc+4a#=TF}H%)f$8OSW(Ro#|qrEJ)x2R|-+Aodg{kfb%dt zSbCw~iH#7dCyQrbQA;%^*)-q73Y)fCM%pWoAiG-#bGT_XMRD8P6 zpEaPp9qp2V_QVEANS|!5n#H(@``)%23OE9@c+NozA%43*_D>ae%@&${V#z`>yiG%w zrVQrFpBjh+E$DoshtS)|reC~o$h9=OGy{3v36>qkR!GXNbu6ox8k(P{f6?pnBN7WK zXQ66Be~^vB-A;D>kr7vNPwPW|W8u&^0X~pW`@m2WcLE@;)ze}kU7`?18&%vy2}ib@ z59^@ae{3kg!%O=!aN@X8@kchI_m<3BApMqKE@RA?M%T$@V<}E51nCepG-ZGii4FsX01eO_9t-DgPFDj z*kCuG6jbNle9(L76Y5*|aN)R66GohuYe=sM>?V<%9QZ0B2$fgdK_gobt(|AH=Tm!| z2hslPPFs;~2y2f<@sbL?jC)@dJ7<8c#mHl3L|f^ewRokmwWuN|enwh#;jkI6ZN%C_ zFVm+rQel`%@Ks-3&}TZF(n~^%Y2)^{KkeCZj?1o(ngAK1la**j-&@kLQyTgx7I&a zjcAsBX6W_Se$x$4DRG#^d5!u~R5F6TBJFJ&R(|_()CW=DSt|a^?U!D3?%xmBBuP)9 zLmVwYNUCS3Tq?LCBVqZ)io`xfp;Xi7%^QC&=_JQ|Iw!T)m53^=z3T53-{IdXl*7kO zbrZjyJf;)mDaW53pvA8`1K-#}0rTpRxWaM&nmP*dV1*Sb7lE42+;p|Ix5U?1MU=ip z+l+T@-m~01xdkYcPP3`L@sApPW#<;l!;$Zfv#MT zg{F)kk}2|LXV2*&f=c_toRq87s*d`4_>e*?8PFuswP7ggcsp-`w56i$y>&9VCL!sL zxnZp5#NF-CnHLY89UDths$pVmgcc#x9oM!O$%Rk+|!qe*lTu>$Vjo#7ToucwUXeyguM~(FIGk2(ix3CRw_CXAK8g^@) zX+a0$Va-?4%`;wSyL{HR_dgGl!@xxxBxY9OIV>&%$U@9!?h12IwsMtwbae9)+ogrv z`SJAZp(k`~&WQB;NG*!V_Kwn==jUF)Y``?`AR#fqQD%=tgeEq{hiO8>(c%2pPE`Nq z?T9pp3dE{5j+vL>4nJ za0`PlBg3pY5)2G1YMCJsB@w}FfdWk9dNvV1jxdlMg3=B3ERzPNMYDuC(MQ%=B zu~mhw5?F;5kPQ;nS5g2gDap1~itr6kaLzAERWQ{v(KAr8<5EyiuqjGOvkG!?gK7uz zY?U%fN(!v>^~=l4^~#O)@{7{-4J|D#^$m>ljf`}GDs+o0^GXscbn}XpA%?)raY-#s zF3Kz@$;{7F0GXSZlwVq6tE2?72o50bEXhnm*pycc^%l^B`XCv7Lp=k1Y}!&QGI3}F z$>7wMk_`7%L1|GA*iR|R`l&goxv6<2#Xxrw z!M`XI=+i`yyX*|179fkEtB=HH0kR;H22jvj1!PvF=0vz;0s|E63qunN8-1)Y=sKPA zbMlLV3lfu4K`sMpK@vh&4Ynr|$sUmR5%wU-An8m=wkj@7%1TWxL5eS6tbtR%Q)0S4 zlCX_FR@FAZv|#0%pOTqY>5^EIYG-6%XsT;yplfIxVrXb(XkcYzW}}a$2FV2=8Rw$Z z#FG4?ko^1{JFqu$L0ko6Js^bWMOFdTY@-j#Nk};hl5N4Fzor{c3S7urly!+C%fLqBZ{Gm}ybM}Vp4Ke~V{zM2h zabECRY*}XdUPUGB&AI)bpG}^i_d{Q=hC@KXp@D&siDhsws<~vo?J2vv)_?is{=mPt zj|lIR<7=-M{vDP3^UA8*JJa%yZ#yp>y1zJ7rLx?E zn63Z)+tiO%b6;QlxN*zt@7?$J{(ruGzufWI`S-$}|9$$sV(NwYpY4j?WuD$EpT+Ys zE#~k0J6~_VtCDH_wJ+)Wzo%<0ruM#936SQ`Kf0RxN>}jBAAU?*KYj??v)UTrZ zQ}~xW|M>YsW5oBzH{;dj{r`3J(f`g`(dfERkNsS zmW++3ep<@1uxK?zJOh%?pO**-tYAo*DJ-yJ&iu=c4hxt%QvDnj6xI2$Gd6M!0uA&{ dxnBe-7@sV2`qaAn#V=6p?CI*~vd$@?2>?c0q{#pP literal 0 HcmV?d00001 From 3288b05b4310cb1a79dae0dae1f82b41af2131af Mon Sep 17 00:00:00 2001 From: Joseph Atkins-Turkish Date: Thu, 15 Oct 2015 13:59:31 -0700 Subject: [PATCH 003/184] Added SidePane to house the screenshot window in. --- ide/static/ide/css/ide.css | 8 +- ide/static/ide/js/cloudpebble.js | 2 + ide/static/ide/js/editor.js | 21 +++- ide/static/ide/js/screenshot_manager.js | 130 +++++++++++--------- ide/static/ide/js/sidepane.js | 118 ++++++++++++++++++ ide/templates/ide/project.html | 3 +- ide/templates/ide/project/monkeyscript.html | 4 +- 7 files changed, 219 insertions(+), 67 deletions(-) create mode 100644 ide/static/ide/js/sidepane.js diff --git a/ide/static/ide/css/ide.css b/ide/static/ide/css/ide.css index 666e5c77..b4c58c0e 100644 --- a/ide/static/ide/css/ide.css +++ b/ide/static/ide/css/ide.css @@ -1216,12 +1216,9 @@ button#add-filter { /* Monkey editor */ -#main-pane { - right: 640px; -} - #right-pane { - left: calc(100% - 640px); + box-sizing: border-box; + width: 0; right: 0; top: 0; bottom: 0; @@ -1248,7 +1245,6 @@ button#add-filter { .monkey-pane .image-resource-preview { width: 200px; display: inline-block; - /* line-height: 280px; */ } .monkey-pane .image-resource-preview img { diff --git a/ide/static/ide/js/cloudpebble.js b/ide/static/ide/js/cloudpebble.js index 08e98816..85eac4fa 100644 --- a/ide/static/ide/js/cloudpebble.js +++ b/ide/static/ide/js/cloudpebble.js @@ -40,6 +40,8 @@ CloudPebble.Init = function() { CloudPebble.GitHub.Init(); CloudPebble.Documentation.Init(); CloudPebble.FuzzyPrompt.Init(); + CloudPebble.MonkeyScreenshots.Init(); + CloudPebble.SidePane.Init(); CloudPebble.ProgressBar.Hide(); diff --git a/ide/static/ide/js/editor.js b/ide/static/ide/js/editor.js index 03a8b3ba..22f21b06 100644 --- a/ide/static/ide/js/editor.js +++ b/ide/static/ide/js/editor.js @@ -33,6 +33,12 @@ CloudPebble.Editor = (function() { // See if we already had it open. CloudPebble.Sidebar.SuspendActive(); if(CloudPebble.Sidebar.Restore('source-'+file.id)) { + if (CloudPebble.SidePane.RightPane.restorePane('monkey-screenshots', file.id)) { + CloudPebble.SidePane.RightPane.setSize('640px'); + } + else { + CloudPebble.SidePane.RightPane.setSize('0'); + } if(callback) { callback(open_codemirrors[file.id]); } @@ -51,7 +57,9 @@ CloudPebble.Editor = (function() { error.text(interpolate(gettext("Something went wrong: %s"), [data.error])); CloudPebble.Sidebar.SetActivePane(error, ''); } else { + var screenshot_pane; var is_js = file.name.substr(-3) == '.js'; + var is_monkey = !is_js; var source = data.source; var lastModified = data.modified; var pane = $('
    '); @@ -67,12 +75,13 @@ CloudPebble.Editor = (function() { //highlightSelectionMatches: true, smartIndent: true, indentWithTabs: !USER_SETTINGS.use_spaces, - mode: (is_js ? 'javascript' : 'MonkeyScript'), + mode: (is_js ? 'javascript' : (is_monkey ? 'MonkeyScript' : CloudPebble.Editor.PebbleMode)), styleActiveLine: true, value: source, theme: USER_SETTINGS.theme, foldGutter: true }; + if(USER_SETTINGS.keybinds !== '') { settings.keyMap = USER_SETTINGS.keybinds; } @@ -408,9 +417,19 @@ CloudPebble.Editor = (function() { if(!was_clean) { --unsaved_files; } + screenshot_pane.destroy(); + //CloudPebble.SidePane.RightPane.destroyPane('monkey-screenshots', file.id); delete open_codemirrors[file.id]; }); + if (is_monkey) { + // TODO: test name + screenshot_pane = new CloudPebble.MonkeyScreenshots.ScreenshotPane(file.name); + CloudPebble.SidePane.RightPane.addPane(screenshot_pane.getPane(), 'monkey-screenshots', file.id); + CloudPebble.SidePane.RightPane.setSize('640px'); + } + + var was_clean = true; code_mirror.on('change', function() { if(was_clean) { diff --git a/ide/static/ide/js/screenshot_manager.js b/ide/static/ide/js/screenshot_manager.js index d3de6fe2..a3d2002a 100644 --- a/ide/static/ide/js/screenshot_manager.js +++ b/ide/static/ide/js/screenshot_manager.js @@ -1,16 +1,14 @@ -(function() { +CloudPebble.MonkeyScreenshots = (function() { var current_platforms = ['aplite', 'basalt', 'chalk']; - + var screenshot_editor_template; function ScreenshotFile(options) { var options = _.defaults(options || {}, { is_new: false, - id: null, file: null, src: "" }); this.is_new = options.is_new; - this.id = options.id; this.file = options.file; this.src = options.src; } @@ -24,17 +22,17 @@ name: "Screenshot set 1", id: 0, images: { - aplite: new ScreenshotFile({src: "/static/common/img/screenshot-aplite.png", id: 0}), - basalt: new ScreenshotFile({src: "/static/common/img/screenshot-basalt.png", id: 1}), - chalk: new ScreenshotFile({src: "/static/common/img/screenshot-chalk.png", id: 2}) + aplite: new ScreenshotFile({src: "/static/common/img/screenshot-aplite.png"}), + basalt: new ScreenshotFile({src: "/static/common/img/screenshot-basalt.png"}), + chalk: new ScreenshotFile({src: "/static/common/img/screenshot-chalk.png"}) } }, { name: "Screenshot set 2", id: 1, images: { - aplite: new ScreenshotFile({src: "/static/common/img/screenshot-aplite.png", id: 3}), - basalt: new ScreenshotFile({src: "/static/common/img/screenshot-basalt.png", id: 4}), - chalk: new ScreenshotFile({src: "/static/common/img/screenshot-chalk.png", id: 5}) + aplite: new ScreenshotFile({src: "/static/common/img/screenshot-aplite.png"}), + basalt: new ScreenshotFile({src: "/static/common/img/screenshot-basalt.png"}), + chalk: new ScreenshotFile({src: "/static/common/img/screenshot-chalk.png"}) } }]; @@ -49,7 +47,7 @@ _.each(screenshots, function(screenshot) { var shot_data = {name: screenshot.name, id: screenshot.id}; _.each(screenshot.images, function(image, platform) { - shot_data[platform] = {id: image.id}; + shot_data[platform] = {}; if (image.file !== null) { shot_data[platform].uploadId = files.length; files.push(image.file); @@ -146,7 +144,7 @@ var upload = screenshots[index + i]; if (upload) { // Update existing screenshots at the current index - upload.images[platform] = new ScreenshotFile({file:file, id: upload.images[platform].id, is_new: true}); + upload.images[platform] = new ScreenshotFile({file:file, is_new: true}); } else { // If there was no screenshot to update, add the remaining files as new screenshots. @@ -341,56 +339,74 @@ }); } - /** - * Set up the screenshot manager in a pane, and connect models to views. - * @param test_name name of test associated with screenshots - * @param pane HTML element containing monkey screenshot uploader - */ - function init(test_name, pane) { - var screenshots = new ScreenshotsModel(test_name); - var screenshots_view = new ScreenshotsView(pane.find('.monkey-screenshots')); - var buttons_view = new FormButtonsView(pane.find('.monkey-form-buttons')); - - // Render screenshots whenever we fetch them - screenshots.on('changeScreenshots', function(screenshots) { - screenshots_view.renderScreenshots(screenshots); - }).on('error', function(error) { - screenshots_view.renderError(error); - }).on('changeName', function(index, name, changed) { - screenshots_view.showAsChanged(index, changed); - }).on('saved', function() { - console.log("loading"); - screenshots.loadScreenshots(); - }); - - // Update list of uploads when user selects files - screenshots_view.on('filesSelected', function(fileList, index, platform) { - screenshots.setNames(screenshots_view.getNames()); - var files = []; - _.each(fileList, function(file, i) { - files[i] = file; - }); - screenshots.addUploadedFiles(files, index, platform); - }); - screenshots_view.on('inputName', function(index, value) { - screenshots.setName(index, value); - }); + function ScreenshotPane(test_name) { + var pane = screenshot_editor_template.clone(); + var screenshots; + var screenshots_view; + var buttons_view; + /** + * Set up the screenshot manager in a pane, and connect models to views. + * @param test_name name of test associated with screenshots + * @param pane HTML element containing monkey screenshot uploader + */ + function setup_pane(test_name, pane) { + screenshots = new ScreenshotsModel(test_name); + screenshots_view = new ScreenshotsView(pane.find('.monkey-screenshots')); + buttons_view = new FormButtonsView(pane.find('.monkey-form-buttons')); + // Render screenshots whenever we fetch them + screenshots.on('changeScreenshots', function(screenshots) { + screenshots_view.renderScreenshots(screenshots); + }).on('error', function(error) { + screenshots_view.renderError(error); + }).on('changeName', function(index, name, changed) { + screenshots_view.showAsChanged(index, changed); + }).on('saved', function() { + console.log("loading"); + screenshots.loadScreenshots(); + }); - // Perform actions when form buttons are clicked - buttons_view.on('reset', function() { - screenshots.loadScreenshots(); - }); - buttons_view.on('save', function() { - screenshots.setNames(screenshots_view.getNames()); - screenshots.save(); - }); + // Update list of uploads when user selects files + screenshots_view.on('filesSelected', function(fileList, index, platform) { + screenshots.setNames(screenshots_view.getNames()); + var files = []; + _.each(fileList, function(file, i) { + files[i] = file; + }); + screenshots.addUploadedFiles(files, index, platform); + }); + screenshots_view.on('inputName', function(index, value) { + screenshots.setName(index, value); + }); - screenshots_view.renderScreenshots([]); - screenshots.loadScreenshots(); + // Perform actions when form buttons are clicked + buttons_view.on('reset', function() { + screenshots.loadScreenshots(); + }); + buttons_view.on('save', function() { + screenshots.setNames(screenshots_view.getNames()); + screenshots.save(); + }); + screenshots_view.renderScreenshots([]); + screenshots.loadScreenshots(); + } + setup_pane(test_name, pane); + this.getPane = function() { + return pane; + }; + this.destroy = function() { + // TODO: what else should we destroy? + pane.trigger('destroy'); + } } - init("TEST", $('.monkey-pane')); + //setup_pane("TEST", $('.monkey-pane')); + return { + Init: function() { + screenshot_editor_template = $('#monkey-screenshot-manager-template').detach().removeClass('hide'); + }, + ScreenshotPane: ScreenshotPane + } })(); \ No newline at end of file diff --git a/ide/static/ide/js/sidepane.js b/ide/static/ide/js/sidepane.js new file mode 100644 index 00000000..52cc0671 --- /dev/null +++ b/ide/static/ide/js/sidepane.js @@ -0,0 +1,118 @@ +CloudPebble.SidePane = (function() { + var vertical = 1; + var horizontal = 2; + + /** + * A pane container + * @param orientation either CloudPebble.SidePane.Horizontal or CloudPebble.SidePane.Vertical + * @constructor + */ + function SidePane(orientation) { + var self = this; + var active_pane; + var suspended_panes = {}; + var active_kind; + var active_id; + var container; + var main_pane; + + var get_suspended_pane = function(kind, id) { + return suspended_panes[kind+'-'+id]; + }; + var set_suspended_pane = function(kind, id, pane) { + console.log("setting pane", kind, id); + suspended_panes[kind+'-'+id] = pane; + }; + var destroy_suspended_pane = function(pane) { + console.log("de panes", suspended_panes); + delete suspended_panes[_.findKey(suspended_panes, function(p) {return !p.is(pane);})]; + }; + + this.getSuspendedPanes = function() { + return suspended_panes; + }; + + this.attachPane = function(pane, kind, id) { + $(container).append(pane); + active_kind = kind; + active_id = id; + active_pane = pane; + pane.trigger('attached'); + }; + + this.suspendActivePane = function() { + if (active_pane) { + active_pane.detach(); + active_pane.trigger('detached'); + set_suspended_pane(active_kind, active_id, active_pane); + } + }; + + this.restorePane = function(kind, id) { + var pane = get_suspended_pane(kind, id); + if (!pane) { + return false; + } + else if (pane == active_pane) { + return pane; + } + else { + this.suspendActivePane(); + this.attachPane(pane); + pane.trigger('restored'); + return pane; + } + }; + + this.addPane = function(pane, kind, id) { + this.suspendActivePane(); + this.attachPane(pane, kind, id); + }; + + this.setSize = function(size) { + if (orientation === vertical) { + console.log(size); + $(container).css({width: size}); + $(main_pane).css({right: size}); + } + else if (orientation === horizontal) { + $(container).css({height: size}); + $(main_pane).css({bottom: size}); + } + else { + throw "Invalid orientation"; + } + $(container).trigger('resize', [size]); + $(main_pane).trigger('resize', [size]); + }; + + this.init = function(set_container, page_main_pane) { + container = set_container; + main_pane = page_main_pane; + + $(container).on('destroy', ':first-child', function(event) { + if ($(event.target).is(active_pane)) { + // The user is destroying the active pane + self.suspendActivePane(); + self.setSize(0); + } + destroy_suspended_pane($(event.target)); + + }); + }; + + } + + return { + Orientations: { + Vertical: vertical, + Horizontal: horizontal + }, + SidePane: SidePane, + RightPane: new SidePane(vertical), + Init: function() { + CloudPebble.SidePane.RightPane.init('#right-pane', '#main-pane'); + CloudPebble.SidePane.RightPane.setSize('0'); + } + } +})(); \ No newline at end of file diff --git a/ide/templates/ide/project.html b/ide/templates/ide/project.html index 5a9bbdcf..40724b9e 100644 --- a/ide/templates/ide/project.html +++ b/ide/templates/ide/project.html @@ -82,7 +82,6 @@

    - {% include "ide/project/monkeyscript.html" %}
    @@ -132,6 +131,7 @@

    {% include "ide/project/github.html" %} {% include "ide/project/ui-editor.html" %} {% include "ide/project/timeline.html" %} +{% include "ide/project/monkeyscript.html" %} {% endblock %} {% block modals %} @@ -505,6 +505,7 @@

    {% trans 'Compass and Accelerometer' %}

    + diff --git a/ide/templates/ide/project/monkeyscript.html b/ide/templates/ide/project/monkeyscript.html index 6cb5bff1..a7694ce9 100644 --- a/ide/templates/ide/project/monkeyscript.html +++ b/ide/templates/ide/project/monkeyscript.html @@ -1,7 +1,7 @@ {% load i18n %} {#
    #} -
    +

    Test Screenshots {% for platform in supported_platforms %} - {{ platform }} @@ -11,7 +11,7 @@

    Test Screenshots {% for platform in supported_platforms %}
    {{ platform }}
    {% endfor %} -

    +
    From ea269492ce27d2604b2919248aa8eda1b350fd4b Mon Sep 17 00:00:00 2001 From: Joseph Atkins-Turkish Date: Mon, 19 Oct 2015 15:38:01 -0700 Subject: [PATCH 004/184] Django models for tests & screenshots --- ...testfile_project_file_name__add_screens.py | 225 +++++++++++++ ide/models/files.py | 309 +++++++++++------- 2 files changed, 420 insertions(+), 114 deletions(-) create mode 100644 ide/migrations/0039_auto__add_testfile__add_unique_testfile_project_file_name__add_screens.py diff --git a/ide/migrations/0039_auto__add_testfile__add_unique_testfile_project_file_name__add_screens.py b/ide/migrations/0039_auto__add_testfile__add_unique_testfile_project_file_name__add_screens.py new file mode 100644 index 00000000..ad80ba45 --- /dev/null +++ b/ide/migrations/0039_auto__add_testfile__add_unique_testfile_project_file_name__add_screens.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'TestFile' + db.create_table(u'ide_testfile', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('last_modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, null=True, blank=True)), + ('folded_lines', self.gf('django.db.models.fields.TextField')(default='[]')), + ('file_name', self.gf('django.db.models.fields.CharField')(max_length=100)), + ('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='test_files', to=orm['ide.Project'])), + )) + db.send_create_signal('ide', ['TestFile']) + + # Adding unique constraint on 'TestFile', fields ['project', 'file_name'] + db.create_unique(u'ide_testfile', ['project_id', 'file_name']) + + # Adding model 'ScreenshotSet' + db.create_table(u'ide_screenshotset', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('test', self.gf('django.db.models.fields.related.ForeignKey')(related_name='screenshot_sets', to=orm['ide.TestFile'])), + ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), + )) + db.send_create_signal('ide', ['ScreenshotSet']) + + # Adding model 'ScreenshotFile' + db.create_table(u'ide_screenshotfile', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('screenshot_set', self.gf('django.db.models.fields.related.ForeignKey')(related_name='screenshots', to=orm['ide.ScreenshotSet'])), + ('platform', self.gf('django.db.models.fields.CharField')(max_length=10)), + )) + db.send_create_signal('ide', ['ScreenshotFile']) + + # Adding unique constraint on 'ScreenshotFile', fields ['platform', 'screenshot_set'] + db.create_unique(u'ide_screenshotfile', ['platform', 'screenshot_set_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'ScreenshotFile', fields ['platform', 'screenshot_set'] + db.delete_unique(u'ide_screenshotfile', ['platform', 'screenshot_set_id']) + + # Removing unique constraint on 'TestFile', fields ['project', 'file_name'] + db.delete_unique(u'ide_testfile', ['project_id', 'file_name']) + + # Deleting model 'TestFile' + db.delete_table(u'ide_testfile') + + # Deleting model 'ScreenshotSet' + db.delete_table(u'ide_screenshotset') + + # Deleting model 'ScreenshotFile' + db.delete_table(u'ide_screenshotfile') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'ide.buildresult': { + 'Meta': {'object_name': 'BuildResult'}, + 'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'builds'", 'to': "orm['ide.Project']"}), + 'started': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "'88a3e74d-2864-47ae-a07d-40a8eaab2de2'", 'max_length': '36'}) + }, + 'ide.buildsize': { + 'Meta': {'object_name': 'BuildSize'}, + 'binary_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sizes'", 'to': "orm['ide.BuildResult']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'platform': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'resource_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'total_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'worker_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'ide.project': { + 'Meta': {'object_name': 'Project'}, + 'app_capabilities': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'app_company_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'app_is_hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'app_is_shown_on_communication': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'app_is_watchface': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'app_jshint': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'app_keys': ('django.db.models.fields.TextField', [], {'default': "'{}'"}), + 'app_long_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'app_platforms': ('django.db.models.fields.TextField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'app_short_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'app_uuid': ('django.db.models.fields.CharField', [], {'default': "'4d482d40-e5e7-4002-b690-9b78bf8b6cba'", 'max_length': '36', 'null': 'True', 'blank': 'True'}), + 'app_version_label': ('django.db.models.fields.CharField', [], {'default': "'1.0'", 'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'github_branch': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'github_hook_build': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'github_hook_uuid': ('django.db.models.fields.CharField', [], {'max_length': '36', 'null': 'True', 'blank': 'True'}), + 'github_last_commit': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'github_last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'github_repo': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'optimisation': ('django.db.models.fields.CharField', [], {'default': "'s'", 'max_length': '1'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + 'project_type': ('django.db.models.fields.CharField', [], {'default': "'native'", 'max_length': '10'}), + 'sdk_version': ('django.db.models.fields.CharField', [], {'default': "'2'", 'max_length': '6'}) + }, + 'ide.resourcefile': { + 'Meta': {'unique_together': "(('project', 'file_name'),)", 'object_name': 'ResourceFile'}, + 'file_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_menu_icon': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'kind': ('django.db.models.fields.CharField', [], {'max_length': '9'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'resources'", 'to': "orm['ide.Project']"}), + 'target_platforms': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True', 'blank': 'True'}) + }, + 'ide.resourceidentifier': { + 'Meta': {'unique_together': "(('resource_file', 'resource_id'),)", 'object_name': 'ResourceIdentifier'}, + 'character_regex': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'compatibility': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource_file': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'identifiers'", 'to': "orm['ide.ResourceFile']"}), + 'resource_id': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'tracking': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'ide.resourcevariant': { + 'Meta': {'unique_together': "(('resource_file', 'tags'),)", 'object_name': 'ResourceVariant'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_legacy': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'resource_file': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'variants'", 'to': "orm['ide.ResourceFile']"}), + 'tags': ('django.db.models.fields.CommaSeparatedIntegerField', [], {'max_length': '50', 'blank': 'True'}) + }, + 'ide.screenshotfile': { + 'Meta': {'unique_together': "(('platform', 'screenshot_set'),)", 'object_name': 'ScreenshotFile'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'platform': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'screenshot_set': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'screenshots'", 'to': "orm['ide.ScreenshotSet']"}) + }, + 'ide.screenshotset': { + 'Meta': {'object_name': 'ScreenshotSet'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'screenshot_sets'", 'to': "orm['ide.TestFile']"}) + }, + 'ide.sourcefile': { + 'Meta': {'unique_together': "(('project', 'file_name'),)", 'object_name': 'SourceFile'}, + 'file_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'folded_lines': ('django.db.models.fields.TextField', [], {'default': "'[]'"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'null': 'True', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'source_files'", 'to': "orm['ide.Project']"}), + 'target': ('django.db.models.fields.CharField', [], {'default': "'app'", 'max_length': '10'}) + }, + 'ide.templateproject': { + 'Meta': {'object_name': 'TemplateProject', '_ormbases': ['ide.Project']}, + u'project_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['ide.Project']", 'unique': 'True', 'primary_key': 'True'}), + 'template_kind': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}) + }, + 'ide.testfile': { + 'Meta': {'unique_together': "(('project', 'file_name'),)", 'object_name': 'TestFile'}, + 'file_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'folded_lines': ('django.db.models.fields.TextField', [], {'default': "'[]'"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'null': 'True', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_files'", 'to': "orm['ide.Project']"}) + }, + 'ide.usergithub': { + 'Meta': {'object_name': 'UserGithub'}, + 'avatar': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'nonce': ('django.db.models.fields.CharField', [], {'max_length': '36', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'github'", 'unique': 'True', 'primary_key': 'True', 'to': u"orm['auth.User']"}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}) + }, + 'ide.usersettings': { + 'Meta': {'object_name': 'UserSettings'}, + 'accepted_terms': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'autocomplete': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'keybinds': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '20'}), + 'tab_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '2'}), + 'theme': ('django.db.models.fields.CharField', [], {'default': "'cloudpebble'", 'max_length': '50'}), + 'use_spaces': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'whats_new': ('django.db.models.fields.PositiveIntegerField', [], {'default': '18'}) + } + } + + complete_apps = ['ide'] \ No newline at end of file diff --git a/ide/models/files.py b/ide/models/files.py index db9b7e25..072e070f 100644 --- a/ide/models/files.py +++ b/ide/models/files.py @@ -18,6 +18,125 @@ __author__ = 'katharine' +class TextFile(IdeModel): + last_modified = models.DateTimeField(blank=True, null=True, auto_now=True) + folded_lines = models.TextField(default="[]") + folder = None + + def get_local_filename(self): + padded_id = '%05d' % self.id + return '%s%s/%s/%s/%s' % (settings.FILE_STORAGE, self.folder, padded_id[0], padded_id[1], padded_id) + + def get_s3_path(self): + return '%s/%d' % (self.folder, self.id) + + local_filename = property(get_local_filename) + s3_path = property(get_s3_path) + + def get_contents(self): + if not settings.AWS_ENABLED: + try: + return open(self.local_filename).read() + except IOError: + return '' + else: + return s3.read_file(self.bucket_name, self.s3_path) + + def save_file(self, content, folded_lines=None): + if not settings.AWS_ENABLED: + if not os.path.exists(os.path.dirname(self.local_filename)): + os.makedirs(os.path.dirname(self.local_filename)) + open(self.local_filename, 'w').write(content.encode('utf-8')) + else: + s3.save_file(self.bucket_name, self.s3_path, content.encode('utf-8')) + if folded_lines: + self.folded_lines = folded_lines + self.save() + + def copy_to_path(self, path): + if not settings.AWS_ENABLED: + try: + shutil.copy(self.local_filename, path) + except IOError as err: + if err.errno == 2: + open(path, 'w').close() # create the file if it's missing. + else: + raise + else: + s3.read_file_to_filesystem(self.bucket_name, self.s3_path, path) + + def was_modified_since(self, expected_modification_time): + if isinstance(expected_modification_time, int): + expected_modification_time = datetime.datetime.fromtimestamp(expected_modification_time) + assert isinstance(expected_modification_time, datetime.datetime) + return self.last_modified.replace(tzinfo=None, microsecond=0) > expected_modification_time + + def save(self, *args, **kwargs): + self.full_clean() + self.project.last_modified = now() + self.project.save() + super(TextFile, self).save(*args, **kwargs) + + class Meta(IdeModel.Meta): + abstract = True + + +class BinFile(IdeModel): + bucket_name = '' + folder = None + + def get_local_filename(self, create=False): + padded_id = self.padded_id + filename = '%s%s/%s/%s/%s' % (settings.FILE_STORAGE, self.folder, padded_id[0], padded_id[1], padded_id) + if create: + if not os.path.exists(os.path.dirname(filename)): + os.makedirs(os.path.dirname(filename)) + return filename + + def get_s3_path(self): + return '%s/%s' % (self.folder, self.s3_id) + + local_filename = property(get_local_filename) + s3_path = property(get_s3_path) + + def save_file(self, stream, file_size=0): + if file_size > 5*1024*1024: + raise Exception(_("Uploaded file too big.")) + if not settings.AWS_ENABLED: + if not os.path.exists(os.path.dirname(self.local_filename)): + os.makedirs(os.path.dirname(self.local_filename)) + with open(self.local_filename, 'wb') as out: + out.write(stream.read()) + else: + s3.save_file(self.bucket_name, self.s3_path, stream.read()) + + self.save_project() + + def save_string(self, string): + if not settings.AWS_ENABLED: + if not os.path.exists(os.path.dirname(self.local_filename)): + os.makedirs(os.path.dirname(self.local_filename)) + with open(self.local_filename, 'wb') as out: + out.write(string) + else: + s3.save_file(self.bucket_name, self.s3_path, string) + + def copy_to_path(self, path): + if not settings.AWS_ENABLED: + shutil.copy(self.local_filename, path) + else: + s3.read_file_to_filesystem(self.bucket_name, self.s3_path, path) + + def get_contents(self): + if not settings.AWS_ENABLED: + return open(self.local_filename).read() + else: + return s3.read_file(self.bucket_name, self.s3_path) + + class Meta(IdeModel.Meta): + abstract = True + + class ResourceFile(IdeModel): project = models.ForeignKey('Project', related_name='resources') RESOURCE_KINDS = ( @@ -88,7 +207,8 @@ class Meta(IdeModel.Meta): unique_together = (('project', 'file_name'),) -class ResourceVariant(IdeModel): +class ResourceVariant(BinFile): + bucket_name = 'source' resource_file = models.ForeignKey(ResourceFile, related_name='variants') VARIANT_DEFAULT = 0 @@ -115,6 +235,10 @@ class ResourceVariant(IdeModel): tags = models.CommaSeparatedIntegerField(max_length=50, blank=True) is_legacy = models.BooleanField(default=False) # True for anything migrated out of ResourceFile + def save_project(self): + self.resource_file.project.last_modified = now() + self.resource_file.project.save() + def get_tags(self): return [int(tag) for tag in self.tags.split(",") if tag] @@ -127,64 +251,17 @@ def get_tag_names(self): def get_tags_string(self): return "".join(self.get_tag_names()) - def get_local_filename(self, create=False): - if self.is_legacy: - padded_id = '%05d' % self.resource_file.id - filename = '%sresources/%s/%s/%s' % (settings.FILE_STORAGE, padded_id[0], padded_id[1], padded_id) - else: - padded_id = '%09d' % self.id - filename = '%sresources/variants/%s/%s/%s' % (settings.FILE_STORAGE, padded_id[0], padded_id[1], padded_id) - if create: - if not os.path.exists(os.path.dirname(filename)): - os.makedirs(os.path.dirname(filename)) - return filename - - def get_s3_path(self): - if self.is_legacy: - return 'resources/%s' % self.resource_file.id - else: - return 'resources/variants/%s' % self.id - - local_filename = property(get_local_filename) - s3_path = property(get_s3_path) - - def save_file(self, stream, file_size=0): - if file_size > 5*1024*1024: - raise Exception(_("Uploaded file too big.")) - if not settings.AWS_ENABLED: - if not os.path.exists(os.path.dirname(self.local_filename)): - os.makedirs(os.path.dirname(self.local_filename)) - with open(self.local_filename, 'wb') as out: - out.write(stream.read()) - else: - s3.save_file('source', self.s3_path, stream.read()) - - self.resource_file.project.last_modified = now() - self.resource_file.project.save() - - def save_string(self, string): - if not settings.AWS_ENABLED: - if not os.path.exists(os.path.dirname(self.local_filename)): - os.makedirs(os.path.dirname(self.local_filename)) - with open(self.local_filename, 'wb') as out: - out.write(string) - else: - s3.save_file('source', self.s3_path, string) - - self.resource_file.project.last_modified = now() - self.resource_file.project.save() + @property + def padded_id(self): + return '%05d' % self.resource_file.id if self.is_legacy else '%09d' % self.id - def copy_to_path(self, path): - if not settings.AWS_ENABLED: - shutil.copy(self.local_filename, path) - else: - s3.read_file_to_filesystem('source', self.s3_path, path) + @property + def s3_id(self): + return self.resource_file.id if self.is_legacy else self.id - def get_contents(self): - if not settings.AWS_ENABLED: - return open(self.local_filename).read() - else: - return s3.read_file('source', self.s3_path) + @property + def folder(self): + return 'resources' if self.is_legacy else 'resources/variants' def save(self, *args, **kwargs): self.full_clean() @@ -208,8 +285,7 @@ def get_root_path(self): path = property(get_path) root_path = property(get_root_path) - - class Meta(IdeModel.Meta): + class Meta(BinFile.Meta): unique_together = (('resource_file', 'tags'),) @@ -229,11 +305,11 @@ class Meta(IdeModel.Meta): unique_together = (('resource_file', 'resource_id'),) -class SourceFile(IdeModel): - project = models.ForeignKey('Project', related_name='source_files') +class SourceFile(TextFile): file_name = models.CharField(max_length=100, validators=[RegexValidator(r"^[/a-zA-Z0-9_-]+\.(c|h|js)$")]) - last_modified = models.DateTimeField(blank=True, null=True, auto_now=True) - folded_lines = models.TextField(default="[]") + project = models.ForeignKey('Project', related_name='source_files') + bucket_name = 'source' + folder = 'sources' TARGETS = ( ('app', _('App')), @@ -241,74 +317,79 @@ class SourceFile(IdeModel): ) target = models.CharField(max_length=10, choices=TARGETS, default='app') - def get_local_filename(self): - padded_id = '%05d' % self.id - return '%ssources/%s/%s/%s' % (settings.FILE_STORAGE, padded_id[0], padded_id[1], padded_id) + @property + def project_path(self): + if self.target == 'app': + return 'src/%s' % self.file_name + else: + return 'worker_src/%s' % self.file_name - def get_s3_path(self): - return 'sources/%d' % self.id + class Meta(TextFile.Meta): + unique_together = (('project', 'file_name'),) - def get_contents(self): - if not settings.AWS_ENABLED: - try: - return open(self.local_filename).read() - except IOError: - return '' - else: - return s3.read_file('source', self.s3_path) +class TestFile(TextFile): + file_name = models.CharField(max_length=100, validators=[RegexValidator(r"^[/a-zA-Z0-9_-]$")]) + project = models.ForeignKey('Project', related_name='test_files') + bucket_name = 'source' + folder = 'tests/scripts' - def was_modified_since(self, expected_modification_time): - if isinstance(expected_modification_time, int): - expected_modification_time = datetime.datetime.fromtimestamp(expected_modification_time) - assert isinstance(expected_modification_time, datetime.datetime) - return self.last_modified.replace(tzinfo=None, microsecond=0) > expected_modification_time + @property + def project_path(self): + return 'integration_tests/%s' % self.file_name + # TODO: verify - def save_file(self, content, folded_lines=None): - if not settings.AWS_ENABLED: - if not os.path.exists(os.path.dirname(self.local_filename)): - os.makedirs(os.path.dirname(self.local_filename)) - open(self.local_filename, 'w').write(content.encode('utf-8')) - else: - s3.save_file('source', self.s3_path, content.encode('utf-8')) - if folded_lines: - self.folded_lines = folded_lines - self.save() + class Meta(TextFile.Meta): + unique_together = (('project', 'file_name'),) - def copy_to_path(self, path): - if not settings.AWS_ENABLED: - try: - shutil.copy(self.local_filename, path) - except IOError as err: - if err.errno == 2: - open(path, 'w').close() # create the file if it's missing. - else: - raise - else: - s3.read_file_to_filesystem('source', self.s3_path, path) + +class ScreenshotSet(IdeModel): + test = models.ForeignKey('TestFile', related_name='screenshot_sets') + name = models.CharField(max_length=100, validators=[RegexValidator(r"^[/a-zA-Z0-9_-]+\.(png)$")]) def save(self, *args, **kwargs): - self.full_clean() + self.clean_fields() self.project.last_modified = now() self.project.save() - super(SourceFile, self).save(*args, **kwargs) + super(ScreenshotSet, self).save(*args, **kwargs) + + class Metha(IdeModel.Meta): + unique_together = (('test', 'name'),) + +class ScreenshotFile(BinFile): + bucket_name = 'source' + folder = 'tests/screenshots' + screenshot_set = models.ForeignKey('ScreenshotSet', related_name='screenshots') + PLATFORMS = ( + ('aplite', 'Aplite'), + ('basalt', 'Basalt'), + ('chalk', 'Chalk') + ) + platform = models.CharField(max_length=10, choices=PLATFORMS) @property - def project_path(self): - if self.target == 'app': - return 'src/%s' % self.file_name - else: - return 'worker_src/%s' % self.file_name + def padded_id(self): + return '%09d' % self.id - local_filename = property(get_local_filename) - s3_path = property(get_s3_path) + @property + def s3_id(self): + return self.id - class Meta(IdeModel.Meta): - unique_together = (('project', 'file_name')) + def save_project(self): + self.screenshot_set.project.last_modified = now() + self.screenshot_set.project.save() + + def save(self, *args, **kwargs): + self.full_clean() + self.screenshot_set.save() + super(ScreenshotFile, self).save(*args, **kwargs) + + class Meta(BinFile.Meta): + unique_together = (('platform', 'screenshot_set'),) @receiver(post_delete) def delete_file(sender, instance, **kwargs): - if sender == SourceFile or sender == ResourceVariant: + if sender == SourceFile or sender == ResourceVariant or sender == ScreenshotFile or sender == TestFile: if settings.AWS_ENABLED: try: s3.delete_file(instance.s3_path) From c7da37cede7d6d08d16e14c2954bdd5ce6ffafd4 Mon Sep 17 00:00:00 2001 From: Joseph Atkins-Turkish Date: Mon, 19 Oct 2015 21:55:41 -0700 Subject: [PATCH 005/184] Screenshots and tests APIs with django test --- cloudpebble/settings.py | 4 +- ide/api/screenshots.py | 119 ++++++++++++++++++++++ ide/api/source.py | 64 ++++++++---- ide/models/files.py | 17 ++-- ide/static/ide/js/screenshot_manager.js | 19 ++-- ide/test_screenshots.py | 128 ++++++++++++++++++++++++ ide/urls.py | 17 ++-- 7 files changed, 328 insertions(+), 40 deletions(-) create mode 100644 ide/api/screenshots.py create mode 100644 ide/test_screenshots.py diff --git a/cloudpebble/settings.py b/cloudpebble/settings.py index 6578aa1b..df3e61d7 100644 --- a/cloudpebble/settings.py +++ b/cloudpebble/settings.py @@ -3,10 +3,12 @@ import os import dj_database_url +import sys _environ = os.environ DEBUG = _environ.get('DEBUG', '') != '' TEMPLATE_DEBUG = DEBUG +TESTING = 'test' in sys.argv ADMINS = ( ('Administrator', 'example@example.com'), @@ -138,7 +140,7 @@ # 'django.template.loaders.eggs.Loader', ) -if not DEBUG: +if not DEBUG and not TESTING: STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.CachedStaticFilesStorage' diff --git a/ide/api/screenshots.py b/ide/api/screenshots.py new file mode 100644 index 00000000..fd866729 --- /dev/null +++ b/ide/api/screenshots.py @@ -0,0 +1,119 @@ +import json +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.shortcuts import get_object_or_404 +from django.http import HttpResponse, HttpResponseRedirect +from django.db import transaction +from django.views.decorators.http import require_POST, require_safe +from django.core.urlresolvers import reverse +from utils.keen_helper import send_keen_event + +from ide.api import json_failure, json_response +from ide.models.project import Project +from ide.models.files import TestFile, ScreenshotSet, ScreenshotFile +import utils.s3 as s3 + +__author__ = 'joe' + +def make_screenshot_dict(screenshot_set, project_id): + return { + "name": screenshot_set.name, + "id": screenshot_set.id, + "files": dict([(screenshot_file.platform, { + "id": screenshot_file.id, + # "src": "project/%s/screenshot/%s" % (project_id, screenshot_file.id) + "src": reverse('ide:show_screenshot', kwargs={ + 'project_id': project_id, + 'test_id': screenshot_set.test.id, + 'screenshot_id': screenshot_set.id, + 'platform_name': 'basalt' + }) + }) for screenshot_file in screenshot_set.files.all()]) + } + +@require_safe +@login_required +def load_screenshots(request, project_id, test_id): + project = get_object_or_404(Project, pk=project_id, owner=request.user) + test = get_object_or_404(TestFile, pk=test_id) + screenshots = test.screenshot_sets.all() + + send_keen_event('cloudpebble', 'cloudpebble_load_screenshots', data={'data': { + 'test': test.id + }}, project=project, request=request) + + return json_response({"screenshots": [make_screenshot_dict(screenshot, project_id) for screenshot in screenshots]}) + +@require_POST +@login_required +def save_screenshots(request, project_id, test_id): + project = get_object_or_404(Project, pk=project_id, owner=request.user) + screenshot_data = json.loads(request.POST['screenshots']) + uploaded_files = request.FILES.getlist('files') + test = get_object_or_404(TestFile, pk=test_id) + screenshots = test.screenshot_sets.all() + # get set of screenshot IDs + deleted_ids = [shot.id for shot in screenshots] + try: + with transaction.atomic(): + # go through uploaded screenshots + for screenshot_info in screenshot_data: + # if uploaded screenshot has an ID + id = screenshot_info.get('id', None) + if id: + # Remove ID from set + deleted_ids.remove(id) + # Fetch the screenshot + screenshot_set = get_object_or_404(ScreenshotSet, pk=id) + # edit name + screenshot_set.name = screenshot_info['name'] + # delete removed or re-uploaded screenshot files + files = screenshot_set.files.all() + for screenshot_file in files: + if screenshot_file.platform not in screenshot_info['files'] or screenshot_info['files'][screenshot_file.platform].get('uploadId', None): + screenshot_file.delete() + else: + # create a new ScreenshotSet + screenshot_set = ScreenshotSet.objects.create(test=test, name=screenshot_info['name']) + screenshot_set.save() + + # add new uploads + for platform, upload_info in screenshot_info['files'].iteritems(): + uploadId = upload_info.get('uploadId', None) + if isinstance(uploadId, int): + screenshot_file = ScreenshotFile.objects.create(screenshot_set=screenshot_set, platform=platform) + posted_file = uploaded_files[uploadId] + screenshot_file.save() + screenshot_file.save_file(posted_file, posted_file.size) + + screenshot_set.save() + + # delete all screenshots missing from POST request + for screenshot in screenshots: + if screenshot.id in deleted_ids: + screenshot.delete() + except FloatingPointError as e: + return json_failure(str(e)) + else: + screenshots = ScreenshotSet.objects.filter(test=test) + return json_response({"screenshots": [make_screenshot_dict(screenshot, project_id) for screenshot in screenshots]}) + +@require_safe +@login_required +def show_screenshot(request, project_id, test_id, screenshot_id, platform_name): + screenshot_set = get_object_or_404(ScreenshotSet, pk=screenshot_id, test__project__owner=request.user) + screenshot_file = get_object_or_404(ScreenshotFile, platform=platform_name, screenshot_set=screenshot_set) + file_name = screenshot_set.name+".png" + content_type = 'image/png' + content_disposition = "attachment; filename=\"%s\"" % file_name + + if settings.AWS_ENABLED: + headers = { + 'response-content-disposition': content_disposition, + 'Content-Type': content_type + } + return HttpResponseRedirect(s3.get_signed_url('source', screenshot_file.s3_path, headers=headers)) + else: + response = HttpResponse(open(screenshot_file.local_filename), content_type=content_type) + response['Content-Disposition'] = content_disposition + return response diff --git a/ide/api/source.py b/ide/api/source.py index 5fd854a7..eef3c83c 100644 --- a/ide/api/source.py +++ b/ide/api/source.py @@ -9,7 +9,7 @@ from django.utils.translation import ugettext as _ from ide.api import json_failure, json_response from ide.models.project import Project -from ide.models.files import SourceFile +from ide.models.files import SourceFile, TestFile from utils.keen_helper import send_keen_event __author__ = 'katharine' @@ -38,12 +38,40 @@ def create_source_file(request, project_id): return json_response({"file": {"id": f.id, "name": f.file_name, "target": f.target}}) +@require_POST +@login_required +def create_test_file(request, project_id): + project = get_object_or_404(Project, pk=project_id, owner=request.user) + try: + f = TestFile.objects.create(project=project, + file_name=request.POST['name']) + f.save_file(request.POST.get('content', '')) + except IntegrityError as e: + return json_failure(str(e)) + else: + send_keen_event('cloudpebble', 'cloudpebble_create_file', data={ + 'data': { + 'filename': request.POST['name'], + 'kind': 'test' + } + }, project=project, request=request) + + return json_response({"file": {"id": f.id, "name": f.file_name}}) + +def get_source_file(kind, pk, project): + if kind == 'source': + return get_object_or_404(SourceFile, pk=pk, project=project) + elif kind == 'test': + return get_object_or_404(TestFile, pk=pk, project=project) + else: + raise ValueError('Invalid source kind %s' % kind) + @require_safe @csrf_protect @login_required -def load_source_file(request, project_id, file_id): +def load_source_file(request, project_id, kind, file_id): project = get_object_or_404(Project, pk=project_id, owner=request.user) - source_file = get_object_or_404(SourceFile, pk=file_id, project=project) + source_file = get_source_file(kind, pk=file_id, project=project) try: content = source_file.get_contents() @@ -55,7 +83,7 @@ def load_source_file(request, project_id, file_id): send_keen_event('cloudpebble', 'cloudpebble_open_file', data={ 'data': { 'filename': source_file.file_name, - 'kind': 'source' + 'kind': kind } }, project=project, request=request) @@ -73,9 +101,9 @@ def load_source_file(request, project_id, file_id): @require_safe @csrf_protect @login_required -def source_file_is_safe(request, project_id, file_id): +def source_file_is_safe(request, project_id, kind, file_id): project = get_object_or_404(Project, pk=project_id, owner=request.user) - source_file = get_object_or_404(SourceFile, pk=file_id, project=project) + source_file = get_source_file(kind, pk=file_id, project=project) client_modified = datetime.datetime.fromtimestamp(int(request.GET['modified'])) server_modified = source_file.last_modified.replace(tzinfo=None, microsecond=0) is_safe = client_modified >= server_modified @@ -84,16 +112,16 @@ def source_file_is_safe(request, project_id, file_id): @require_POST @login_required -def rename_source_file(request, project_id, file_id): +def rename_source_file(request, project_id, kind, file_id): project = get_object_or_404(Project, pk=project_id, owner=request.user) - source_file = get_object_or_404(SourceFile, pk=file_id, project=project) + source_file = get_source_file(kind, pk=file_id, project=project) old_filename = source_file.file_name try: if source_file.file_name != request.POST['old_name']: send_keen_event('cloudpebble', 'cloudpebble_rename_abort_unsafe', data={ 'data': { 'filename': source_file.file_name, - 'kind': 'source' + 'kind': kind } }, project=project, request=request) raise Exception(_("Could not rename, file has been renamed already.")) @@ -101,7 +129,7 @@ def rename_source_file(request, project_id, file_id): send_keen_event('cloudpebble', 'cloudpebble_rename_abort_unsafe', data={ 'data': { 'filename': source_file.file_name, - 'kind': 'source' + 'kind': kind } }, project=project, request=request) raise Exception(_("Could not rename, file has been modified since last save.")) @@ -115,7 +143,7 @@ def rename_source_file(request, project_id, file_id): 'data': { 'old_filename': old_filename, 'new_filename': source_file.file_name, - 'kind': 'source' + 'kind': kind } }, project=project, request=request) return json_response({"modified": time.mktime(source_file.last_modified.utctimetuple())}) @@ -123,15 +151,15 @@ def rename_source_file(request, project_id, file_id): @require_POST @login_required -def save_source_file(request, project_id, file_id): +def save_source_file(request, project_id, kind, file_id): project = get_object_or_404(Project, pk=project_id, owner=request.user) - source_file = get_object_or_404(SourceFile, pk=file_id, project=project) + source_file = get_source_file(kind, pk=file_id, project=project) try: if source_file.was_modified_since(int(request.POST['modified'])): send_keen_event('cloudpebble', 'cloudpebble_save_abort_unsafe', data={ 'data': { 'filename': source_file.file_name, - 'kind': 'source' + 'kind': kind } }, project=project, request=request) raise Exception(_("Could not save: file has been modified since last save.")) @@ -143,7 +171,7 @@ def save_source_file(request, project_id, file_id): send_keen_event('cloudpebble', 'cloudpebble_save_file', data={ 'data': { 'filename': source_file.file_name, - 'kind': 'source' + 'kind': kind } }, project=project, request=request) @@ -152,9 +180,9 @@ def save_source_file(request, project_id, file_id): @require_POST @login_required -def delete_source_file(request, project_id, file_id): +def delete_source_file(request, project_id, kind, file_id): project = get_object_or_404(Project, pk=project_id, owner=request.user) - source_file = get_object_or_404(SourceFile, pk=file_id, project=project) + source_file = get_source_file(kind, pk=file_id, project=project) try: source_file.delete() except Exception as e: @@ -163,7 +191,7 @@ def delete_source_file(request, project_id, file_id): send_keen_event('cloudpebble', 'cloudpebble_delete_file', data={ 'data': { 'filename': source_file.file_name, - 'kind': 'source' + 'kind': kind } }, project=project, request=request) return json_response({}) diff --git a/ide/models/files.py b/ide/models/files.py index 072e070f..04791ab3 100644 --- a/ide/models/files.py +++ b/ide/models/files.py @@ -328,7 +328,7 @@ class Meta(TextFile.Meta): unique_together = (('project', 'file_name'),) class TestFile(TextFile): - file_name = models.CharField(max_length=100, validators=[RegexValidator(r"^[/a-zA-Z0-9_-]$")]) + file_name = models.CharField(max_length=100, validators=[RegexValidator(r"^[/a-zA-Z0-9_-]+$")]) project = models.ForeignKey('Project', related_name='test_files') bucket_name = 'source' folder = 'tests/scripts' @@ -338,18 +338,21 @@ def project_path(self): return 'integration_tests/%s' % self.file_name # TODO: verify + def get_screenshot_sets(self): + return ScreenshotSet.objects.filter(test=self) + class Meta(TextFile.Meta): unique_together = (('project', 'file_name'),) class ScreenshotSet(IdeModel): test = models.ForeignKey('TestFile', related_name='screenshot_sets') - name = models.CharField(max_length=100, validators=[RegexValidator(r"^[/a-zA-Z0-9_-]+\.(png)$")]) + name = models.CharField(max_length=100, validators=[RegexValidator(r"^[/a-zA-Z0-9_-]+$")]) def save(self, *args, **kwargs): self.clean_fields() - self.project.last_modified = now() - self.project.save() + self.test.project.last_modified = now() + self.test.project.save() super(ScreenshotSet, self).save(*args, **kwargs) class Metha(IdeModel.Meta): @@ -358,7 +361,7 @@ class Metha(IdeModel.Meta): class ScreenshotFile(BinFile): bucket_name = 'source' folder = 'tests/screenshots' - screenshot_set = models.ForeignKey('ScreenshotSet', related_name='screenshots') + screenshot_set = models.ForeignKey('ScreenshotSet', related_name='files') PLATFORMS = ( ('aplite', 'Aplite'), ('basalt', 'Basalt'), @@ -375,8 +378,8 @@ def s3_id(self): return self.id def save_project(self): - self.screenshot_set.project.last_modified = now() - self.screenshot_set.project.save() + self.screenshot_set.test.project.last_modified = now() + self.screenshot_set.test.project.save() def save(self, *args, **kwargs): self.full_clean() diff --git a/ide/static/ide/js/screenshot_manager.js b/ide/static/ide/js/screenshot_manager.js index a3d2002a..57348123 100644 --- a/ide/static/ide/js/screenshot_manager.js +++ b/ide/static/ide/js/screenshot_manager.js @@ -5,10 +5,12 @@ CloudPebble.MonkeyScreenshots = (function() { function ScreenshotFile(options) { var options = _.defaults(options || {}, { is_new: false, + id: null, file: null, src: "" }); this.is_new = options.is_new; + this.id = options.id; this.file = options.file; this.src = options.src; } @@ -22,17 +24,17 @@ CloudPebble.MonkeyScreenshots = (function() { name: "Screenshot set 1", id: 0, images: { - aplite: new ScreenshotFile({src: "/static/common/img/screenshot-aplite.png"}), - basalt: new ScreenshotFile({src: "/static/common/img/screenshot-basalt.png"}), - chalk: new ScreenshotFile({src: "/static/common/img/screenshot-chalk.png"}) + aplite: new ScreenshotFile({src: "/static/common/img/screenshot-aplite.png", id: 0}), + basalt: new ScreenshotFile({src: "/static/common/img/screenshot-basalt.png", id: 1}), + chalk: new ScreenshotFile({src: "/static/common/img/screenshot-chalk.png", id: 2}) } }, { name: "Screenshot set 2", id: 1, images: { - aplite: new ScreenshotFile({src: "/static/common/img/screenshot-aplite.png"}), - basalt: new ScreenshotFile({src: "/static/common/img/screenshot-basalt.png"}), - chalk: new ScreenshotFile({src: "/static/common/img/screenshot-chalk.png"}) + aplite: new ScreenshotFile({src: "/static/common/img/screenshot-aplite.png", id: 3}), + basalt: new ScreenshotFile({src: "/static/common/img/screenshot-basalt.png", id: 4}), + chalk: new ScreenshotFile({src: "/static/common/img/screenshot-chalk.png", id: 5}) } }]; @@ -47,7 +49,7 @@ CloudPebble.MonkeyScreenshots = (function() { _.each(screenshots, function(screenshot) { var shot_data = {name: screenshot.name, id: screenshot.id}; _.each(screenshot.images, function(image, platform) { - shot_data[platform] = {}; + shot_data[platform] = {id: image.id}; if (image.file !== null) { shot_data[platform].uploadId = files.length; files.push(image.file); @@ -144,7 +146,8 @@ CloudPebble.MonkeyScreenshots = (function() { var upload = screenshots[index + i]; if (upload) { // Update existing screenshots at the current index - upload.images[platform] = new ScreenshotFile({file:file, is_new: true}); + var id = (upload.images[platform] ? upload.images[platform].id : null); + upload.images[platform] = new ScreenshotFile({file:file, id: id, is_new: true}); } else { // If there was no screenshot to update, add the remaining files as new screenshots. diff --git a/ide/test_screenshots.py b/ide/test_screenshots.py new file mode 100644 index 00000000..10253254 --- /dev/null +++ b/ide/test_screenshots.py @@ -0,0 +1,128 @@ +import json +from django.test import TestCase +from django.test.utils import setup_test_environment +from django.test.client import Client +from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.exceptions import ObjectDoesNotExist +from django.core.urlresolvers import reverse + +from ide.models.files import TestFile, ScreenshotSet, ScreenshotFile + +__author__ = 'joe' + +setup_test_environment() + +class CloudpebbleTestCase(TestCase): + def login(self): + self.client = Client() + self.client.post('/accounts/register', {'username': 'test', 'email': 'test@test.test', 'password1': 'test', 'password2': 'test'}) + self.assertTrue(self.client.login(username='test', password='test')) + self.assertJSONEqual(self.client.post('/ide/project/create', {'name': 'test', 'template': 0, 'type': 'native', 'sdk': 3}).content, + {"id": 1, "success": True}) + self.project_id = 1 + +class ScreenshotsTests(CloudpebbleTestCase): + + def setUp(self): + self.login() + + def make_test(self): + url = reverse('ide:create_test_file', args=[self.project_id]) + return json.loads(self.client.post(url, {"name": "mytest"}).content)['file']['id'] + + def upload_screenshots(self, test_id): + file1 = SimpleUploadedFile("file.png", "file_content", content_type="image/png") + file2 = SimpleUploadedFile("file.png", "file_content", content_type="image/png") + + # Make a screenshot with two files + screenshots = [{ + "name": "Set_1", + "files": { + "aplite": { + "uploadId": 0 + }, + "basalt": { + "uploadId": 1 + } + } + }] + # Save the screenshots + url = reverse('ide:save_screenshots', kwargs={ + 'project_id': self.project_id, + 'test_id': test_id + }) + data = {"screenshots": json.dumps(screenshots), "files": [file1, file2]} + result = json.loads(self.client.post(url, data).content)['screenshots'] + + # Check from the response that they were created properly + self.assertEqual(result[0]['name'], "Set_1") + self.assertEqual(result[0]['id'], 1) + self.assertGreaterEqual(result[0]['files']['basalt']['id'], 0) + self.assertGreaterEqual(result[0]['files']['aplite']['id'], 0) + + return data, result + + def test_edit_and_load_screenshots(self): + # Make a screenshot to play with + test_id = self.make_test() + data, result1 = self.upload_screenshots(test_id) + + # Delete the aplite file, change the name and add a chalk file + screenshots = result1 + screenshots[0]["name"] = "Set_1_edited" + del screenshots[0]['files']["aplite"] + screenshots[0]['files']["chalk"] = {"uploadId": 0} + url = reverse('ide:save_screenshots', args=[self.project_id, test_id]) + data = {"screenshots": json.dumps(screenshots), "files": [data["files"][0]]} + result2 = json.loads(self.client.post(url, data).content)['screenshots'] + + # Check that the name changed, the basalt file remains the same, aplite is gone, and chalk is added + def check(result): + self.assertEqual(result[0]['name'], "Set_1_edited") + self.assertEqual(result1[0]['files']['basalt']['id'], result[0]['files']['basalt']['id']) + self.assertTrue('aplite' not in result[0]['files']) + self.assertGreaterEqual(result[0]['files']['chalk']['id'], 0) + check(result2) + + # Now try the load URL, and re-run the above assertions + url = reverse('ide:load_screenshots', args=[self.project_id, test_id]) + result3 = json.loads(self.client.get(url).content)["screenshots"] + check(result3) + + def test_show_screenshot(self): + # Test that the URL that the server gives us for a screenshot is a valid + # URL which leads to a PNG file. + test_id = self.make_test() + data, result = self.upload_screenshots(test_id) + url = result[0]['files']['aplite']['src'] + result = self.client.get(url) + self.assertEqual(result.get('Content-Type'), 'image/png') + + def test_delete_test(self): + # Make a test, upload some screenshots + test_id = self.make_test() + data, result = self.upload_screenshots(test_id) + set_id = result[0]['id'] + file_id = result[0]['files']['aplite']['id'] + + # Check everything is there + self.assertIsInstance(TestFile.objects.get(pk=test_id), TestFile) + self.assertIsInstance(ScreenshotSet.objects.get(pk=set_id), ScreenshotSet) + self.assertIsInstance(ScreenshotFile.objects.get(pk=file_id), ScreenshotFile) + + # Delete the test + url = reverse('ide:delete_source_file', kwargs={ + 'project_id': self.project_id, + 'kind': 'test', + 'file_id': test_id + }) + self.client.post(url) + + # Check that the test, its screenshot sets and its screenshots have all been deleted + with self.assertRaises(ObjectDoesNotExist): + TestFile.objects.get(pk=test_id) + with self.assertRaises(ObjectDoesNotExist): + ScreenshotSet.objects.get(pk=set_id) + with self.assertRaises(ObjectDoesNotExist): + ScreenshotFile.objects.get(pk=file_id) + diff --git a/ide/urls.py b/ide/urls.py index 604512e9..111893a1 100644 --- a/ide/urls.py +++ b/ide/urls.py @@ -7,8 +7,9 @@ save_project_settings, delete_project, begin_export, import_zip, import_github, do_import_gist from ide.api.resource import create_resource, resource_info, delete_resource, update_resource, show_resource, \ delete_variant -from ide.api.source import create_source_file, load_source_file, source_file_is_safe, save_source_file, \ +from ide.api.source import create_source_file, create_test_file, load_source_file, source_file_is_safe, save_source_file, \ delete_source_file, rename_source_file +from ide.api.screenshots import save_screenshots, load_screenshots, show_screenshot from ide.api.user import transition_accept, transition_export, transition_delete, whats_new from ide.api.ycm import init_autocomplete from ide.api.qemu import launch_emulator, generate_phone_token, handle_phone_token @@ -26,11 +27,15 @@ url(r'^project/(?P\d+)/save_settings', save_project_settings, name='save_project_settings'), url(r'^project/(?P\d+)/delete', delete_project, name='delete_project'), url(r'^project/(?P\d+)/create_source_file', create_source_file, name='create_source_file'), - url(r'^project/(?P\d+)/source/(?P\d+)/load', load_source_file, name='load_source_file'), - url(r'^project/(?P\d+)/source/(?P\d+)/save', save_source_file, name='save_source_file'), - url(r'^project/(?P\d+)/source/(?P\d+)/rename', rename_source_file, name='rename_source_file'), - url(r'^project/(?P\d+)/source/(?P\d+)/is_safe', source_file_is_safe, name='source_file_is_safe'), - url(r'^project/(?P\d+)/source/(?P\d+)/delete', delete_source_file, name='delete_source_file'), + url(r'^project/(?P\d+)/create_test_file', create_test_file, name='create_test_file'), + url(r'^project/(?P\d+)/(?P(source|test))/(?P\d+)/load', load_source_file, name='load_source_file'), + url(r'^project/(?P\d+)/(?P(source|test))/(?P\d+)/save', save_source_file, name='save_source_file'), + url(r'^project/(?P\d+)/test/(?P\d+)/screenshots/save', save_screenshots, name='save_screenshots'), + url(r'^project/(?P\d+)/test/(?P\d+)/screenshots/load', load_screenshots, name='load_screenshots'), + url(r'^project/(?P\d+)/(?P(source|test))/(?P\d+)/rename', rename_source_file, name='rename_source_file'), + url(r'^project/(?P\d+)/(?P(source|test))/(?P\d+)/is_safe', source_file_is_safe, name='source_file_is_safe'), + url(r'^project/(?P\d+)/(?P(source|test))/(?P\d+)/delete', delete_source_file, name='delete_source_file'), + url(r'^project/(?P\d+)/test/(?P\d+)/screenshot/(?P\d+)/(?P\w{1,10})/get/', show_screenshot, name='show_screenshot'), url(r'^project/(?P\d+)/create_resource', create_resource, name='create_resource'), url(r'^project/(?P\d+)/resource/(?P\d+)/info', resource_info, name='resource_info'), url(r'^project/(?P\d+)/resource/(?P\d+)/delete', delete_resource, name='delete_resource'), From b44157becfc7263b81264e7f71379d6b24eac773 Mon Sep 17 00:00:00 2001 From: Joseph Atkins-Turkish Date: Thu, 22 Oct 2015 14:46:21 -0700 Subject: [PATCH 006/184] Connect test-screenshot UI to real API. --- ide/static/ide/js/screenshot_manager.js | 113 ++++++++++++++++-------- 1 file changed, 75 insertions(+), 38 deletions(-) diff --git a/ide/static/ide/js/screenshot_manager.js b/ide/static/ide/js/screenshot_manager.js index 57348123..e5efdb6f 100644 --- a/ide/static/ide/js/screenshot_manager.js +++ b/ide/static/ide/js/screenshot_manager.js @@ -3,18 +3,58 @@ CloudPebble.MonkeyScreenshots = (function() { var screenshot_editor_template; function ScreenshotFile(options) { - var options = _.defaults(options || {}, { + var final = _.defaults(options || {}, { is_new: false, id: null, file: null, src: "" }); - this.is_new = options.is_new; - this.id = options.id; - this.file = options.file; - this.src = options.src; + this.is_new = final.is_new; + this.id = final.id; + this.file = final.file; + this.src = final.src; + } + + function ScreenshotSet(options) { + var final = _.defaults(options || {}, { + name: "", + id: null, + files: [] + }); + this.name = final.name; + this.id = final.id; + this.files = _.map(final.files, function (file){ return new ScreenshotFile(file); }); } + /** + * Put screenshot data in a format ready to be sent. + * @param screenshots + * @returns {{screenshots: Array, files: Array}} + */ + var process_screenshots = function(screenshots) { + var screenshots_data = []; + var files = []; + _.each(screenshots, function(screenshot) { + var shot_data = {name: screenshot.name, id: screenshot.id}; + _.each(screenshot.images, function(image, platform) { + shot_data[platform] = {id: image.id}; + if (image.file !== null) { + shot_data[platform].uploadId = files.length; + files.push(image.file); + } + }, this); + screenshots_data.push(shot_data); + }, this); + + var form_data = new FormData(); + form_data.append('screenshots', JSON.stringify(screenshots_data)); + _.each(files, function(file) { + form_data.append('files[]', file); + }); + + return form_data; + }; + /** * A mock API (for now) * @type {{get_screenshots}} @@ -38,31 +78,6 @@ CloudPebble.MonkeyScreenshots = (function() { } }]; - /** - * Put screenshot data in a format ready to be sent. - * @param screenshots - * @returns {{screenshots: Array, files: Array}} - */ - var process_screenshots = function(screenshots) { - var screenshots_data = []; - var files = []; - _.each(screenshots, function(screenshot) { - var shot_data = {name: screenshot.name, id: screenshot.id}; - _.each(screenshot.images, function(image, platform) { - shot_data[platform] = {id: image.id}; - if (image.file !== null) { - shot_data[platform].uploadId = files.length; - files.push(image.file); - } - }, this); - screenshots_data.push(shot_data); - }, this); - return { - screenshots: screenshots_data, - files: files - } - }; - /** * Get the current list of existing test screenshots * @param test_name name of test @@ -84,13 +99,7 @@ CloudPebble.MonkeyScreenshots = (function() { */ this.saveScreenshots = function(test_name, new_screenshots) { var defer = $.Deferred(); - var data = process_screenshots(new_screenshots); - var form_data = new FormData(); - var screenshot_json = JSON.stringify(data.screenshots); - form_data.append('screenshots', screenshot_json); - _.each(data.files, function(file) { - form_data.append('files[]', file); - }); + var form_data = process_screenshots(new_screenshots); // Made the form data, now we just have to send it. @@ -108,7 +117,35 @@ CloudPebble.MonkeyScreenshots = (function() { }; }; - var API = new MockAPI(); + var AjaxAPI = function() { + this.getScreenshots = function(test_id) { + var url = "/ide/project/" + PROJECT_ID + "/test/" + test_id + "/screenshots/load"; + var defer = $.Deferred() + return $.ajax({ + url: url, + dataType: 'json' + }).done(function(result) { + defer.resolve(_.map(result, function(screenshot_set) {return new ScreenshotSet(screenshot_set)})); + }).fail(function(err) { + defer.reject(err); + }); + }; + + this.saveScreenshots = function(test_id, new_screenshots) { + var form_data = process_screenshots(new_screenshots); + return $.ajax({ + url: url, + type: "POST", + data: form_data, + processData: false, + contentType: false, + dataType: 'json' + }); + + } + }; + + var API = new AjaxAPI(); /** * ScreenshotsModel manages a list of new screenshot files to be uploaded From b480fa5a38f173aabc3e9e38e51292e945d7621e Mon Sep 17 00:00:00 2001 From: Joseph Atkins-Turkish Date: Fri, 23 Oct 2015 11:40:06 -0700 Subject: [PATCH 007/184] Added test file support. - Adding, editing, deleting test files works - Uploading and modifying screenshots per-test works --- .gitignore | 2 + ide/api/project.py | 4 +- ide/api/screenshots.py | 11 +- ide/static/ide/css/ide.css | 36 +++ ide/static/ide/js/cloudpebble.js | 7 +- ide/static/ide/js/editor.js | 123 +++++++-- ide/static/ide/js/monkeyscript.js | 2 +- ide/static/ide/js/screenshot_manager.js | 291 ++++++++++++++------ ide/static/ide/js/sidebar.js | 11 + ide/static/ide/js/sidepane.js | 12 +- ide/templates/ide/project.html | 5 +- ide/templates/ide/project/monkeyscript.html | 23 +- ide/test_screenshots.py | 4 +- 13 files changed, 391 insertions(+), 140 deletions(-) diff --git a/.gitignore b/.gitignore index 522c3481..46fb6384 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ src/ .cache/ .env/ .idea/ +bower_components + diff --git a/ide/api/project.py b/ide/api/project.py index dc7c95e5..59a21589 100644 --- a/ide/api/project.py +++ b/ide/api/project.py @@ -11,7 +11,7 @@ from ide.api import json_response, json_failure from ide.models.build import BuildResult from ide.models.project import Project, TemplateProject -from ide.models.files import SourceFile, ResourceFile +from ide.models.files import SourceFile, ResourceFile, TestFile from ide.tasks.archive import create_archive, do_import_archive from ide.tasks.build import run_compile from ide.tasks.gist import import_gist @@ -27,6 +27,7 @@ def project_info(request, project_id): project = get_object_or_404(Project, pk=project_id, owner=request.user) source_files = SourceFile.objects.filter(project=project).order_by('file_name') resources = ResourceFile.objects.filter(project=project).order_by('file_name') + test_files = TestFile.objects.filter(project=project).order_by('file_name') output = { 'type': project.project_type, 'success': True, @@ -45,6 +46,7 @@ def project_info(request, project_id): 'sdk_version': project.sdk_version, 'app_platforms': project.app_platforms, 'menu_icon': project.menu_icon.id if project.menu_icon else None, + 'test_files': [{'name': f.file_name, 'id': f.id} for f in test_files], 'source_files': [{'name': f.file_name, 'id': f.id, 'target': f.target} for f in source_files], 'resources': [{ 'id': x.id, diff --git a/ide/api/screenshots.py b/ide/api/screenshots.py index fd866729..9a51ab2c 100644 --- a/ide/api/screenshots.py +++ b/ide/api/screenshots.py @@ -26,7 +26,7 @@ def make_screenshot_dict(screenshot_set, project_id): 'project_id': project_id, 'test_id': screenshot_set.test.id, 'screenshot_id': screenshot_set.id, - 'platform_name': 'basalt' + 'platform_name': screenshot_file.platform }) }) for screenshot_file in screenshot_set.files.all()]) } @@ -49,7 +49,8 @@ def load_screenshots(request, project_id, test_id): def save_screenshots(request, project_id, test_id): project = get_object_or_404(Project, pk=project_id, owner=request.user) screenshot_data = json.loads(request.POST['screenshots']) - uploaded_files = request.FILES.getlist('files') + uploaded_files = request.FILES.getlist('files[]') + test = get_object_or_404(TestFile, pk=test_id) screenshots = test.screenshot_sets.all() # get set of screenshot IDs @@ -70,7 +71,9 @@ def save_screenshots(request, project_id, test_id): # delete removed or re-uploaded screenshot files files = screenshot_set.files.all() for screenshot_file in files: - if screenshot_file.platform not in screenshot_info['files'] or screenshot_info['files'][screenshot_file.platform].get('uploadId', None): + was_deleted = lambda: screenshot_file.platform not in screenshot_info['files'] + is_replaced = lambda: screenshot_info['files'][screenshot_file.platform].get('uploadId', None) is not None + if was_deleted() or is_replaced(): screenshot_file.delete() else: # create a new ScreenshotSet @@ -81,7 +84,7 @@ def save_screenshots(request, project_id, test_id): for platform, upload_info in screenshot_info['files'].iteritems(): uploadId = upload_info.get('uploadId', None) if isinstance(uploadId, int): - screenshot_file = ScreenshotFile.objects.create(screenshot_set=screenshot_set, platform=platform) + screenshot_file, did_create = ScreenshotFile.objects.get_or_create(screenshot_set=screenshot_set, platform=platform) posted_file = uploaded_files[uploadId] screenshot_file.save() screenshot_file.save_file(posted_file, posted_file.size) diff --git a/ide/static/ide/css/ide.css b/ide/static/ide/css/ide.css index 98742ff4..89aa8d57 100644 --- a/ide/static/ide/css/ide.css +++ b/ide/static/ide/css/ide.css @@ -1224,7 +1224,11 @@ button#add-filter { bottom: 0; position: absolute; border-left: #444 3px solid; + /*transition:width 300ms ease-in-out, height 300ms ease-in-out;*/ } +/*#main-pane {*/ + /*transition: right 300ms ease-in-out, height 300ms ease-in-out;*/ +/*}*/ .monkey-pane { padding: 10px; @@ -1277,6 +1281,10 @@ button#add-filter { display: inline-block; } +.monkey-screenshot-set .image-resource-preview { + position: relative; +} + .monkey-pane h2 > span { display: none; } @@ -1312,6 +1320,34 @@ button#add-filter { padding-right: 10px; } +.monkey-screenshot-set .image-resource-preview .delete-btn { + position: absolute; + right: 10px; + width: 40px; + height: 40px; + top: 10px; + opacity: 0; +} +.monkey-screenshot-set .image-resource-preview:hover .delete-btn, +.monkey-screenshot-set .image-resource-preview .delete-btn:focus { + opacity: 0.5; +} +.monkey-screenshot-set .image-resource-preview .delete-btn:hover { + opacity: 1; +} + +.screenshot-empty .delete-btn { + display: none; +} + +.monkey-select-platform { + text-decoration: underline; +} +.monkey-select-platform:hover { + cursor: pointer; +} + + /** jquery-textext adjustments **/ /* textext-core */ diff --git a/ide/static/ide/js/cloudpebble.js b/ide/static/ide/js/cloudpebble.js index 85eac4fa..5fd5f118 100644 --- a/ide/static/ide/js/cloudpebble.js +++ b/ide/static/ide/js/cloudpebble.js @@ -50,6 +50,10 @@ CloudPebble.Init = function() { CloudPebble.Editor.Add(value); }); + $.each(data.test_files, function(index, value) { + CloudPebble.Editor.AddTest(value) + }); + $.each(data.resources, function(index, value) { CloudPebble.Resources.Add(value); }); @@ -92,7 +96,7 @@ CloudPebble.Prompts = { $('#modal-text-input-errors').html(''); $('#modal-text-input').modal(); $('#modal-text-input-value').focus(); - var submit = function() { + var submit = function(event) { callback($('#modal-text-input-value').val(), { error: function(message) { $('#modal-text-input-value').removeAttr('disabled'); @@ -109,6 +113,7 @@ CloudPebble.Prompts = { $('#modal-text-input').modal('hide'); } }); + event.preventDefault(); }; $('#modal-text-confirm-button').unbind('click').click(submit); $('#modal-text-input form').unbind('submit').submit(submit); diff --git a/ide/static/ide/js/editor.js b/ide/static/ide/js/editor.js index 22f21b06..f0039ad0 100644 --- a/ide/static/ide/js/editor.js +++ b/ide/static/ide/js/editor.js @@ -13,6 +13,16 @@ CloudPebble.Editor = (function() { project_source_files[file.name] = file; }; + var add_test_file = function(file) { + file.target = 'test'; + CloudPebble.Sidebar.AddTestFile(file, function() { + edit_source_file(file); + }); + + project_source_files[file.name] = file; + }; + + var run = function() { CloudPebble.Prompts.Progress.Show(gettext("Saving...")); CloudPebble.Editor.SaveAll(function() { @@ -48,9 +58,10 @@ CloudPebble.Editor = (function() { return; } CloudPebble.ProgressBar.Show(); - + var url_kind = (file.target == 'test' ? 'test' : 'source'); + var sidebar_id = url_kind + '-' + file.id; // Open it. - $.getJSON('/ide/project/' + PROJECT_ID + '/source/' + file.id + '/load', function(data) { + $.getJSON('/ide/project/' + PROJECT_ID + '/' + url_kind + '/' + file.id + '/load', function(data) { CloudPebble.ProgressBar.Hide(); if(!data.success) { var error = $('
    '); @@ -58,8 +69,22 @@ CloudPebble.Editor = (function() { CloudPebble.Sidebar.SetActivePane(error, ''); } else { var screenshot_pane; - var is_js = file.name.substr(-3) == '.js'; - var is_monkey = !is_js; + var file_kind; + if (file.name.substr(-3) == '.js') { + file_kind = 'js'; + } + else if (file.target == "test") { + file_kind = 'monkey'; + } + else { + file_kind = 'c'; + } + var mode_for_kind = { + monkey: 'MonkeyScript', + js: 'javascript', + c: CloudPebble.Editor.PebbleMode + }; + var source = data.source; var lastModified = data.modified; var pane = $('
    '); @@ -75,7 +100,7 @@ CloudPebble.Editor = (function() { //highlightSelectionMatches: true, smartIndent: true, indentWithTabs: !USER_SETTINGS.use_spaces, - mode: (is_js ? 'javascript' : (is_monkey ? 'MonkeyScript' : CloudPebble.Editor.PebbleMode)), + mode: mode_for_kind[file_kind], styleActiveLine: true, value: source, theme: USER_SETTINGS.theme, @@ -86,10 +111,10 @@ CloudPebble.Editor = (function() { settings.keyMap = USER_SETTINGS.keybinds; } if(!settings.extraKeys) settings.extraKeys = {}; - if(!is_js && USER_SETTINGS.autocomplete === 2) { + if(file_kind == 'c' && USER_SETTINGS.autocomplete === 2) { settings.extraKeys = {'Ctrl-Space': 'autocomplete'}; } - if(!is_js && USER_SETTINGS.autocomplete !== 0) { + if(file_kind == 'c' && USER_SETTINGS.autocomplete !== 0) { settings.extraKeys['Tab'] = function() { var marks = code_mirror.getAllMarks(); var cursor = code_mirror.getCursor(); @@ -179,11 +204,14 @@ CloudPebble.Editor = (function() { settings.extraKeys['Ctrl-/'] = function(cm) { CodeMirror.commands.toggleComment(cm); }; - if(is_js) { - settings.gutters = ['gutter-hint-warnings', 'CodeMirror-linenumbers', 'CodeMirror-foldgutter']; - } else { - settings.gutters = ['gutter-errors', 'CodeMirror-linenumbers', 'CodeMirror-foldgutter']; + settings.gutters = ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']; + + if(file_kind == 'js') { + settings.gutters.unshift('gutter-hint-warnings'); + } else if (file_kind == 'c') { + settings.gutters.unshift('gutter-errors'); } + var code_mirror = CodeMirror(pane[0], settings); code_mirror.file_path = (file.target == 'worker' ? 'worker_src/' : 'src/') + file.name; code_mirror.file_target = file.target; @@ -227,14 +255,14 @@ CloudPebble.Editor = (function() { create_popover(cm, token.string, pos.left, pos.top); }; - if(!is_js && USER_SETTINGS.autocomplete === 1) { + if(file_kind == 'c' && USER_SETTINGS.autocomplete === 1) { code_mirror.on('changes', function(instance, changes) { update_patch_list(instance, changes); if(!is_autocompleting) CodeMirror.commands.autocomplete(code_mirror); }); } - if(is_js) { + if(file_kind == 'js') { var warning_lines = []; var throttled_hint = _.throttle(function() { // Clear things out, even if jslint is off @@ -310,7 +338,7 @@ CloudPebble.Editor = (function() { code_mirror.on('change', throttled_hint); // Make sure we're ready when we start. throttled_hint(); - } else { + } else if (file_kind == 'c') { var clang_lines = []; var sChecking = false; var throttled_check = _.throttle(function() { @@ -389,11 +417,11 @@ CloudPebble.Editor = (function() { } var check_safe = function() { - $.getJSON('/ide/project/' + PROJECT_ID + '/source/' + file.id + '/is_safe?modified=' + lastModified, function(data) { + $.getJSON('/ide/project/' + PROJECT_ID + '/' + url_kind + '/' + file.id + '/is_safe?modified=' + lastModified, function(data) { if(data.success && !data.safe) { if(was_clean) { code_mirror.setOption('readOnly', true); - $.getJSON('/ide/project/' + PROJECT_ID + '/source/' + file.id + '/load', function(data) { + $.getJSON('/ide/project/' + PROJECT_ID + '/' + url_kind + '/' + file.id + '/load', function(data) { if(data.success) { code_mirror.setValue(data.source); lastModified = data.modified; @@ -408,7 +436,7 @@ CloudPebble.Editor = (function() { }); }; - CloudPebble.Sidebar.SetActivePane(pane, 'source-' + file.id, function() { + CloudPebble.Sidebar.SetActivePane(pane, sidebar_id, function() { code_mirror.refresh(); _.defer(function() { code_mirror.focus(); }); check_safe(); @@ -422,9 +450,8 @@ CloudPebble.Editor = (function() { delete open_codemirrors[file.id]; }); - if (is_monkey) { - // TODO: test name - screenshot_pane = new CloudPebble.MonkeyScreenshots.ScreenshotPane(file.name); + if (file_kind == 'monkey') { + screenshot_pane = new CloudPebble.MonkeyScreenshots.ScreenshotPane(file.id); CloudPebble.SidePane.RightPane.addPane(screenshot_pane.getPane(), 'monkey-screenshots', file.id); CloudPebble.SidePane.RightPane.setSize('640px'); } @@ -433,7 +460,7 @@ CloudPebble.Editor = (function() { var was_clean = true; code_mirror.on('change', function() { if(was_clean) { - CloudPebble.Sidebar.SetIcon('source-' + file.id, 'edit'); + CloudPebble.Sidebar.SetIcon(sidebar_id, 'edit'); was_clean = false; ++unsaved_files; } @@ -442,7 +469,7 @@ CloudPebble.Editor = (function() { var mark_clean = function() { was_clean = true; --unsaved_files; - CloudPebble.Sidebar.ClearIcon('source-' + file.id); + CloudPebble.Sidebar.ClearIcon(sidebar_id); }; var save = function(callback) { @@ -456,8 +483,7 @@ CloudPebble.Editor = (function() { } save_btn.attr('disabled','disabled'); delete_btn.attr('disabled','disabled'); - - $.post("/ide/project/" + PROJECT_ID + "/source/" + file.id + "/save", { + $.post("/ide/project/" + PROJECT_ID + "/" + url_kind + "/" + file.id + "/save", { content: code_mirror.getValue(), modified: lastModified, folded_lines: JSON.stringify(code_mirror.get_folded_lines()) @@ -487,7 +513,7 @@ CloudPebble.Editor = (function() { return defer.reject(interpolate(gettext("A file called '%s' already exists."), [new_name])); } - $.post("/ide/project/" + PROJECT_ID + "/source/" + file.id + "/rename", { + $.post("/ide/project/" + PROJECT_ID + "/" + url_kind + "/" + file.id + "/rename", { old_name: file.name, new_name: new_name, modified: lastModified @@ -498,7 +524,7 @@ CloudPebble.Editor = (function() { else { delete project_source_files[file.name]; file.name = new_name; - CloudPebble.Sidebar.SetItemName('source', file.id, new_name); + CloudPebble.Sidebar.SetItemName(url_kind, file.id, new_name); CloudPebble.FuzzyPrompt.SetCurrentItemName(new_name); project_source_files[file.name] = file; defer.resolve(); @@ -619,13 +645,13 @@ CloudPebble.Editor = (function() { CloudPebble.Prompts.Confirm(interpolate(fmt, file, true), gettext("This cannot be undone."), function() { save_btn.attr('disabled','disabled'); delete_btn.attr('disabled','disabled'); - $.post("/ide/project/" + PROJECT_ID + "/source/" + file.id + "/delete", function(data) { + $.post("/ide/project/" + PROJECT_ID + "/" + url_kind + "/" + file.id + "/delete", function(data) { save_btn.removeAttr('disabled'); delete_btn.removeAttr('disabled'); if(data.success) { CloudPebble.Sidebar.DestroyActive(); delete project_source_files[file.name]; - CloudPebble.Sidebar.Remove('source-' + file.id); + CloudPebble.Sidebar.Remove(sidebar_id); CloudPebble.YCM.deleteFile(file); } else { alert(data.error); @@ -939,6 +965,20 @@ CloudPebble.Editor = (function() { ga('send', 'event', 'file', 'create'); } + function create_remote_test(params, callback) { + if (_.isString(params)) { + params = {name: params}; + } + $.post("/ide/project/" + PROJECT_ID + "/create_test_file", params, function(data) { + if(data.success) { + add_test_file(data.file); + } + if (callback) { + callback(data); + } + }); + } + function init_create_prompt() { var prompt = $('#editor-new-file-prompt'); prompt.find('#new-file-type').change(function() { @@ -1076,6 +1116,27 @@ CloudPebble.Editor = (function() { prompt.modal('show'); } + function create_test_file() { + var pattern = "^[a-zA-Z0-9_-]+$"; + CloudPebble.Prompts.Prompt(gettext("Create new test file"), "Test name", "", "", function(value, then) { + if (!(new RegExp(pattern).test(value))) { + then.error("You must enter a valid test name using only alphanumeric characters, dashes (-) and underscores (_)"); + } + else { + create_remote_test(value, function(data) { + if (data.success) { + then.dismiss(); + } else { + then.error(data.error); + //error.text(data.error).show(); + } + }); + + } + + }, pattern); + } + function go_to(filename, line, ch) { var file = project_source_files[filename]; if(!file) return; @@ -1090,9 +1151,15 @@ CloudPebble.Editor = (function() { Create: function() { create_source_file(); }, + CreateTest: function() { + create_test_file(); + }, Add: function(file) { add_source_file(file); }, + AddTest: function(file) { + add_test_file(file) + }, Init: function() { init(); }, diff --git a/ide/static/ide/js/monkeyscript.js b/ide/static/ide/js/monkeyscript.js index c042496c..ac7f6965 100644 --- a/ide/static/ide/js/monkeyscript.js +++ b/ide/static/ide/js/monkeyscript.js @@ -85,7 +85,7 @@ $(function() { return 'error'; } } else { - console.log(state.keyword, state.command); + //console.log(state.keyword, state.command); if (state.command === null) { var thing = stream.match(/^[a-z_]+/i); if (!thing) { diff --git a/ide/static/ide/js/screenshot_manager.js b/ide/static/ide/js/screenshot_manager.js index e5efdb6f..a1dcf645 100644 --- a/ide/static/ide/js/screenshot_manager.js +++ b/ide/static/ide/js/screenshot_manager.js @@ -7,12 +7,14 @@ CloudPebble.MonkeyScreenshots = (function() { is_new: false, id: null, file: null, - src: "" + src: "", + _changed: false }); this.is_new = final.is_new; this.id = final.id; this.file = final.file; this.src = final.src; + this._changed = final._changed; } function ScreenshotSet(options) { @@ -23,7 +25,9 @@ CloudPebble.MonkeyScreenshots = (function() { }); this.name = final.name; this.id = final.id; - this.files = _.map(final.files, function (file){ return new ScreenshotFile(file); }); + this.files = _.mapObject(final.files, function (file) { + return ((file instanceof ScreenshotFile) ? file : new ScreenshotFile(file)); + }); } /** @@ -35,15 +39,20 @@ CloudPebble.MonkeyScreenshots = (function() { var screenshots_data = []; var files = []; _.each(screenshots, function(screenshot) { - var shot_data = {name: screenshot.name, id: screenshot.id}; - _.each(screenshot.images, function(image, platform) { - shot_data[platform] = {id: image.id}; - if (image.file !== null) { - shot_data[platform].uploadId = files.length; - files.push(image.file); + var shot_data = {name: screenshot.name, files: {}}; + if (screenshot.id) {shot_data.id = screenshot.id;} + _.each(screenshot.files, function(image, platform) { + if (image.id || image.file) { + shot_data.files[platform] = {}; + if (image.id) {shot_data.files[platform].id = image.id;} + if (image.file !== null) { + shot_data.files[platform].uploadId = files.length; + files.push(image.file); + } } }, this); - screenshots_data.push(shot_data); + if (_.keys(shot_data.files).length > 0) + screenshots_data.push(shot_data); }, this); var form_data = new FormData(); @@ -63,7 +72,7 @@ CloudPebble.MonkeyScreenshots = (function() { var screenshots = [{ name: "Screenshot set 1", id: 0, - images: { + files: { aplite: new ScreenshotFile({src: "/static/common/img/screenshot-aplite.png", id: 0}), basalt: new ScreenshotFile({src: "/static/common/img/screenshot-basalt.png", id: 1}), chalk: new ScreenshotFile({src: "/static/common/img/screenshot-chalk.png", id: 2}) @@ -71,7 +80,7 @@ CloudPebble.MonkeyScreenshots = (function() { }, { name: "Screenshot set 2", id: 1, - images: { + files: { aplite: new ScreenshotFile({src: "/static/common/img/screenshot-aplite.png", id: 3}), basalt: new ScreenshotFile({src: "/static/common/img/screenshot-basalt.png", id: 4}), chalk: new ScreenshotFile({src: "/static/common/img/screenshot-chalk.png", id: 5}) @@ -104,10 +113,9 @@ CloudPebble.MonkeyScreenshots = (function() { // Made the form data, now we just have to send it. setTimeout(function() { - // TODO: AJAX request screenshots = _.map(new_screenshots, function(shot) { var new_shot = _.clone(shot); - new_shot.images = _.mapObject(_.clone(new_shot.images), _.partial(_.extend, _, {is_new: false, file: null})); + new_shot.files = _.mapObject(_.clone(new_shot.files), _.partial(_.extend, _, {is_new: false, file: null})); new_shot._changed = false; return new_shot; }); @@ -120,19 +128,21 @@ CloudPebble.MonkeyScreenshots = (function() { var AjaxAPI = function() { this.getScreenshots = function(test_id) { var url = "/ide/project/" + PROJECT_ID + "/test/" + test_id + "/screenshots/load"; - var defer = $.Deferred() - return $.ajax({ + var defer = $.Deferred(); + $.ajax({ url: url, dataType: 'json' }).done(function(result) { - defer.resolve(_.map(result, function(screenshot_set) {return new ScreenshotSet(screenshot_set)})); + defer.resolve(_.map(result['screenshots'], function(screenshot_set) {return new ScreenshotSet(screenshot_set)})); }).fail(function(err) { defer.reject(err); }); + return defer.promise(); }; this.saveScreenshots = function(test_id, new_screenshots) { var form_data = process_screenshots(new_screenshots); + var url = "/ide/project/" + PROJECT_ID + "/test/" + test_id + "/screenshots/save"; return $.ajax({ url: url, type: "POST", @@ -169,12 +179,10 @@ CloudPebble.MonkeyScreenshots = (function() { if (index === null) { // Append all new screenshots, given them no name _.each(files, function(file) { - var upload = { - name: "", - images: {}, + var upload = new ScreenshotSet({ _changed: true - }; - upload.images[platform] = new ScreenshotFile({file: file, is_new: true}); + }); + upload.files[platform] = new ScreenshotFile({file: file, is_new: true}); screenshots.push(upload); }); } @@ -183,8 +191,8 @@ CloudPebble.MonkeyScreenshots = (function() { var upload = screenshots[index + i]; if (upload) { // Update existing screenshots at the current index - var id = (upload.images[platform] ? upload.images[platform].id : null); - upload.images[platform] = new ScreenshotFile({file:file, id: id, is_new: true}); + var id = (upload.files[platform] ? upload.files[platform].id : null); + upload.files[platform] = new ScreenshotFile({file:file, id: id, is_new: true}); } else { // If there was no screenshot to update, add the remaining files as new screenshots. @@ -200,13 +208,15 @@ CloudPebble.MonkeyScreenshots = (function() { * @constructor */ this.loadScreenshots = function() { + this.trigger('loadStart'); API.getScreenshots(test_name).then(function(result) { screenshots = result; original_screenshots = _.map(result, _.clone); self.trigger('changeScreenshots', result); - }, function() { + }, function(error) { self.trigger('error', gettext("Error getting screenshots")); - }) + console.log(error); + }); }; /** @@ -219,6 +229,13 @@ CloudPebble.MonkeyScreenshots = (function() { }, this); }; + this.deleteFile = function(index, platform) { + if (_.isObject(screenshots[index].files[platform])) { + screenshots[index].files[platform] = {_changed: true}; + this.trigger('changeScreenshots', screenshots); + } + }; + this.setName = function(index, name) { if (_.isString(name)) { var changed = (!(_.has(original_screenshots, index)) || (name != original_screenshots[index].name)); @@ -230,10 +247,10 @@ CloudPebble.MonkeyScreenshots = (function() { this.save = function() { API.saveScreenshots(test_name, screenshots).then(function() { - console.log("saved1"); - self.trigger('saved'); - }, function() { - // Error? + self.trigger('saved', true); + }, function(error) { + self.trigger('error', gettext("Error saving screenshots")); + console.error(error); }); }; } @@ -295,6 +312,11 @@ CloudPebble.MonkeyScreenshots = (function() { var value = $(this).val(); self.trigger('inputName', index, value); }); + pane.on('click', '.image-resource-preview .delete-btn', function() { + var row = $(this).parents('.monkey-screenshot-set').data('index'); + var col = $(this).parents('.image-resource-preview').data('platform'); + self.trigger('fileDelete', row, col); + }); /* * Render the list of screenshots * @param screenshots @@ -305,10 +327,16 @@ CloudPebble.MonkeyScreenshots = (function() { var template = screenshot_set_template.clone(); template.find('.monkey-screenshot-name').text(screenshot.name); - _.each(screenshot.images, function(file, platform) { - var img = template.find('.platform-'+platform+' img'); + _.each(screenshot.files, function(file, platform) { + var img_container = template.find('.platform-'+platform); + var img = img_container.find('img'); if (file.src) { - img.attr('src', file.src); + var src = file.src; + if (!src.startsWith('data')) { + src += '?'+(Date.now().toString()); + } + img.attr('src', src); + img_container.toggleClass('screenshot-empty', false); } else if (file.file) { var reader = new FileReader(); @@ -318,6 +346,7 @@ CloudPebble.MonkeyScreenshots = (function() { file.src = reader.result; }; reader.readAsDataURL(file.file); + img_container.toggleClass('screenshot-empty', false); } img.toggleClass('monkey-modified', file.is_new); }); @@ -329,7 +358,7 @@ CloudPebble.MonkeyScreenshots = (function() { }); var new_screenshot_pane = screenshot_set_template.clone(); new_screenshot_pane.data('index', null); - new_screenshot_pane.find('.monkey-screenshot-title').remove(); + new_screenshot_pane.find('.monkey-screenshot-title, .delete-btn').remove(); pane.append(new_screenshot_pane) }; @@ -350,21 +379,41 @@ CloudPebble.MonkeyScreenshots = (function() { return names; }; + this.showProgress = function() { + progressbar.show(); + }; + + this.disable = function() { + pane.find('input, button').prop('disabled', true); + }; + + this.enable = function() { + pane.find('input').prop('disabled', false); + }; + + } + + function ErrorView(pane) { /** * Render an error * @param error message to render */ this.renderError = function(error) { - pane.html(interpolate('
    %s
    ', [error])); + pane.append($(interpolate('
    %s
    ', [error]))); }; + + this.empty = function() { + pane.empty(); + } } /** * This class manages the save and reset buttons - * @param pane + * @param pane jQuery element containing Save and Cancel buttons + * @param form jQuery Form associated with the buttons * @constructor */ - function FormButtonsView(pane) { + function FormButtonsView(pane, form) { var self = this; _.extend(this, Backbone.Events); @@ -374,74 +423,134 @@ CloudPebble.MonkeyScreenshots = (function() { }); }); - pane.on('click', '.btn-affirmative', function() { + form.on('submit', function(event) { self.trigger('save'); + event.preventDefault(); + }); + } + + /** + * Manages a progress bar, which only shows up if things are taking a while + * @param pane HTML element contaiing a progress bar + * @constructor + */ + function ProgressView(pane) { + var timeout; + pane.hide(); + this.showProgress = function() { + timeout = setTimeout(function() { + pane.show(); + }, 500); + }; + + this.hideProgress = function() { + clearTimeout(timeout); + pane.hide(); + } + } + + /** + * MainView manages the whole pane. + * Specifically, it enables switching between aplite/basalt/chalk/all modes by clicking on their titles. + * @param pane jQuery element for .monkey-pane + * @constructor + */ + function MainView(pane) { + pane.on('click', '.monkey-select-platform', function() { + pane.toggleClass('monkey-inline'); + pane.toggleClass($(this).data('platform')+'-only'); + var newsize = (pane.hasClass('monkey-inline') ? '300px' : '650px'); + pane.trigger('resize', newsize); }); } function ScreenshotPane(test_name) { - var pane = screenshot_editor_template.clone(); - var screenshots; - var screenshots_view; - var buttons_view; - /** - * Set up the screenshot manager in a pane, and connect models to views. - * @param test_name name of test associated with screenshots - * @param pane HTML element containing monkey screenshot uploader - */ - function setup_pane(test_name, pane) { - screenshots = new ScreenshotsModel(test_name); - screenshots_view = new ScreenshotsView(pane.find('.monkey-screenshots')); - buttons_view = new FormButtonsView(pane.find('.monkey-form-buttons')); + var self = this; + var pane = screenshot_editor_template.clone(); + var screenshots; + var screenshots_view, buttons_view, progress_view, error_view, main_view; + + _.extend(this, Backbone.Events); + + /** + * Set up the screenshot manager in a pane, and connect models to views. + * @param test_name name of test associated with screenshots + * @param pane HTML element containing monkey screenshot uploader + */ + function setup_pane(test_name, pane) { + screenshots = new ScreenshotsModel(test_name); + screenshots_view = new ScreenshotsView(pane.find('.monkey-screenshots')); + progress_view = new ProgressView(pane.find('.progress')); + buttons_view = new FormButtonsView(pane.find('.monkey-form-buttons'), pane.find('form')); + error_view = new ErrorView(pane.find('.errors')); + main_view = new MainView(pane); + + screenshots.on('changeScreenshots', function(screenshots) { // Render screenshots whenever we fetch them - screenshots.on('changeScreenshots', function(screenshots) { - screenshots_view.renderScreenshots(screenshots); - }).on('error', function(error) { - screenshots_view.renderError(error); - }).on('changeName', function(index, name, changed) { - screenshots_view.showAsChanged(index, changed); - }).on('saved', function() { - console.log("loading"); - screenshots.loadScreenshots(); - }); + error_view.empty(); + progress_view.hideProgress(); + screenshots_view.renderScreenshots(screenshots); + }).on('error', function(error) { + // Show an error if we fail to fetch them + error_view.renderError(error); + progress_view.hideProgress(); + }).on('changeName', function(index, name, changed) { + // Show a "changed" icon if the name of a thing changes + screenshots_view.showAsChanged(index, changed); + }).on('saved', function() { + // Reload the screenshots after a save + error_view.empty(); + screenshots_view.enable(); + screenshots.loadScreenshots(); - // Update list of uploads when user selects files - screenshots_view.on('filesSelected', function(fileList, index, platform) { - screenshots.setNames(screenshots_view.getNames()); - var files = []; - _.each(fileList, function(file, i) { - files[i] = file; - }); - screenshots.addUploadedFiles(files, index, platform); - }); - screenshots_view.on('inputName', function(index, value) { - screenshots.setName(index, value); - }); + }).on('loadStart', function() { + progress_view.showProgress(); + }); - // Perform actions when form buttons are clicked - buttons_view.on('reset', function() { - screenshots.loadScreenshots(); - }); - buttons_view.on('save', function() { - screenshots.setNames(screenshots_view.getNames()); - screenshots.save(); + screenshots_view.on('filesSelected', function(fileList, index, platform) { + // Update list of uploads when user selects files + screenshots.setNames(screenshots_view.getNames()); + var files = []; + _.each(fileList, function(file, i) { + files[i] = file; }); + screenshots.addUploadedFiles(files, index, platform); + }).on('fileDelete', function(index, platform) { + screenshots.deleteFile(index, platform); + }).on('inputName', function(index, value) { + // Update the screenshot's model's name when a user types in a name box + screenshots.setName(index, value); + }); - screenshots_view.renderScreenshots([]); + buttons_view.on('reset', function() { + // Reload the screenshots when the reset button is clicked screenshots.loadScreenshots(); - } + }); + buttons_view.on('save', function() { + // Save the screenshot when the save button is clicked + // (ensure that the models names are up to date + screenshots.setNames(screenshots_view.getNames()); + screenshots.save(); + screenshots_view.disable(); + }); - setup_pane(test_name, pane); - this.getPane = function() { - return pane; - }; - this.destroy = function() { - // TODO: what else should we destroy? - pane.trigger('destroy'); - } - } + // When the component is initialised, render an intitial state and load all the screenshots + screenshots_view.renderScreenshots([]); + screenshots.loadScreenshots(); + } - //setup_pane("TEST", $('.monkey-pane')); + setup_pane(test_name, pane); + this.getPane = function() { + return pane; + }; + this.destroy = function() { + // TODO: what else should we destroy? + pane.trigger('destroy'); + _.each([screenshots, screenshots_view, buttons_view, progress_view, error_view, main_view], function(obj) { + obj.off(); + }); + } + } return { Init: function() { diff --git a/ide/static/ide/js/sidebar.js b/ide/static/ide/js/sidebar.js index 97f10611..b96798fd 100644 --- a/ide/static/ide/js/sidebar.js +++ b/ide/static/ide/js/sidebar.js @@ -132,6 +132,16 @@ CloudPebble.Sidebar = (function() { end.before(li); return li; }, + AddTestFile: function(file, on_click) { + var end = $('#end-test-files'); + var link = $(''); + link.text(file.name + ' '); + link.click(on_click); + var li = $(' + +
  • diff --git a/ide/templates/ide/project/monkeyscript.html b/ide/templates/ide/project/monkeyscript.html index a7694ce9..0a19640d 100644 --- a/ide/templates/ide/project/monkeyscript.html +++ b/ide/templates/ide/project/monkeyscript.html @@ -1,29 +1,38 @@ {% load i18n %} -{#
    #} +{#
    #}
    +
    + +

    Test Screenshots {% for platform in supported_platforms %} - - {{ platform }} + - {{ platform }} {% endfor %}

    +
    {% for platform in supported_platforms %} -
    {{ platform }}
    +
    {{ platform }}
    {% endfor %}
    - +
    +
    +
    + +
    {% for platform in supported_platforms %} -
    +
    +
    {% endfor %}
    - + @@ -34,7 +43,7 @@

    Test Screenshots
    - +

    diff --git a/ide/test_screenshots.py b/ide/test_screenshots.py index 10253254..f76e4529 100644 --- a/ide/test_screenshots.py +++ b/ide/test_screenshots.py @@ -51,7 +51,7 @@ def upload_screenshots(self, test_id): 'project_id': self.project_id, 'test_id': test_id }) - data = {"screenshots": json.dumps(screenshots), "files": [file1, file2]} + data = {"screenshots": json.dumps(screenshots), "files[]": [file1, file2]} result = json.loads(self.client.post(url, data).content)['screenshots'] # Check from the response that they were created properly @@ -73,7 +73,7 @@ def test_edit_and_load_screenshots(self): del screenshots[0]['files']["aplite"] screenshots[0]['files']["chalk"] = {"uploadId": 0} url = reverse('ide:save_screenshots', args=[self.project_id, test_id]) - data = {"screenshots": json.dumps(screenshots), "files": [data["files"][0]]} + data = {"screenshots": json.dumps(screenshots), "files[]": [data["files[]"][0]]} result2 = json.loads(self.client.post(url, data).content)['screenshots'] # Check that the name changed, the basalt file remains the same, aplite is gone, and chalk is added From db6c63f8848a7b72416a6db1ba25b64bfbbc4f57 Mon Sep 17 00:00:00 2001 From: Joseph Atkins-Turkish Date: Fri, 30 Oct 2015 10:28:21 -0700 Subject: [PATCH 008/184] Backend and tests for Monkey Test Manager API --- ide/api/monkey.py | 124 ++++++++++ ide/api/source.py | 22 +- ...e_testrun_test_session__add_testsession.py | 228 ++++++++++++++++++ ide/models/__init__.py | 1 + ide/models/monkey.py | 26 ++ ide/static/ide/js/editor.js | 2 +- ide/tasks/__init__.py | 1 + ide/tasks/monkey.py | 61 +++++ ide/tests/__init__.py | 1 + ide/tests/cloudpebble_test.py | 15 ++ ide/{tests.py => tests/test_git.py} | 9 +- ide/{ => tests}/test_screenshots.py | 18 +- ide/tests/test_tests.py | 103 ++++++++ ide/urls.py | 19 +- 14 files changed, 602 insertions(+), 28 deletions(-) create mode 100644 ide/api/monkey.py create mode 100644 ide/migrations/0040_auto__add_testrun__add_unique_testrun_test_session__add_testsession.py create mode 100644 ide/models/monkey.py create mode 100644 ide/tasks/monkey.py create mode 100644 ide/tests/__init__.py create mode 100644 ide/tests/cloudpebble_test.py rename ide/{tests.py => tests/test_git.py} (70%) rename ide/{ => tests}/test_screenshots.py (87%) create mode 100644 ide/tests/test_tests.py diff --git a/ide/api/monkey.py b/ide/api/monkey.py new file mode 100644 index 00000000..d6b1ac22 --- /dev/null +++ b/ide/api/monkey.py @@ -0,0 +1,124 @@ +import json +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.shortcuts import get_object_or_404 +from django.http import HttpResponse, HttpResponseRedirect +from django.db import transaction +from django.views.decorators.http import require_POST, require_safe +from django.core.urlresolvers import reverse +from utils.keen_helper import send_keen_event + +from ide.api import json_failure, json_response +from ide.models.project import Project +from ide.models.files import TestFile, ScreenshotSet, ScreenshotFile +from ide.models.monkey import TestSession, TestRun +from ide.tasks.monkey import run_test_session, setup_test_session +import utils.s3 as s3 + + +__author__ = 'joe' + + +def serialise_run(run, link_test=True, link_session=True): + result = { + 'id': run.id, + } + if link_test: + result['test'] = { + 'id': run.test.id, + 'name': run.test.file_name + } + if link_session: + result['session_id'] = run.session.id + if run.log is not None: + result['log'] = run.log.split('\n') + if run.code is not None: + result['code'] = run.code + if run.date_started is not None: + result['date_started'] = str(run.date_started) + if run.date_completed is not None: + result['date_completed'] = str(run.date_completed) + return result + + +def serialise_session(session, include_runs=False): + result = {'id': session.id} + if session.date_started is not None: + result['date_started'] = str(session.date_started) + if session.date_completed is not None: + result['date_completed'] = str(session.date_completed) + if include_runs: + runs = TestRun.objects.filter(session=session) + result['runs'] = [serialise_run(run, link_session=False) for run in runs] + return result + + +# GET /project//test_sessions/ +@require_safe +@login_required +def get_test_session(request, project_id, session_id): + project = get_object_or_404(Project, pk=project_id, owner=request.user) + session = get_object_or_404(TestSession, pk=session_id, project=project) + # TODO: KEEN + return json_response({"data": serialise_session(session, include_runs=True)}) + + +# GET /project//test_sessions?date_from=&date_to= +@require_safe +@login_required +def get_test_sessions(request, project_id): + project = get_object_or_404(Project, pk=project_id, owner=request.user) + sessions = TestSession.objects.filter(project=project) + # TODO: KEEN + return json_response({"data": [serialise_session(session) for session in sessions]}) + + + +# GET /project//test_runs/ +@require_safe +@login_required +def get_test_run(request, project_id, run_id): + project = get_object_or_404(Project, pk=project_id, owner=request.user) + run = get_object_or_404(TestRun, pk=run_id, session__project=project) + # TODO: KEEN + return json_response({"data": serialise_run(run)}) + + +# GET /project//test_runs?test=&session=&date_from=&date_to= +@require_safe +@login_required +def get_test_runs(request, project_id): + project = get_object_or_404(Project, pk=project_id, owner=request.user) + test_id = request.GET.get('test', None) + session_id = request.GET.get('session', None) + kwargs = {'session__project': project} + if test_id is not None: + kwargs['test__id'] = test_id + link_test = False + if session_id is not None: + kwargs['session__id'] = session_id + link_session = False + runs = TestRun.objects.filter(**kwargs) + + # TODO: KEEN + return json_response({"data": [serialise_run(run, link_test=(test_id is None), link_session=(session_id is None)) for run in runs]}) + + +# POST /project//test_sessions +@require_POST +@login_required +def post_test_session(request, project_id): + project = get_object_or_404(Project, pk=project_id, owner=request.user) + # We may receive a list of particular tests to run + # If not, all tests will be run. + test_ids = request.POST.get('tests', None) + if test_ids is not None: + test_ids = [int(test_id) for test_id in test_ids.split(',')] + + # Make the database objects + session, runs = setup_test_session(project, test_ids) + + # Then run the monkeyscript task + run_test_session.delay(session.id) + # TODO: KEEN + return json_response({"data": serialise_session(session, include_runs=True)}) \ No newline at end of file diff --git a/ide/api/source.py b/ide/api/source.py index eef3c83c..f47daceb 100644 --- a/ide/api/source.py +++ b/ide/api/source.py @@ -61,7 +61,7 @@ def create_test_file(request, project_id): def get_source_file(kind, pk, project): if kind == 'source': return get_object_or_404(SourceFile, pk=pk, project=project) - elif kind == 'test': + elif kind == 'tests': return get_object_or_404(TestFile, pk=pk, project=project) else: raise ValueError('Invalid source kind %s' % kind) @@ -97,6 +97,26 @@ def load_source_file(request, project_id, kind, file_id): "folded_lines": folded_lines }) +@require_safe +@login_required +def get_test_list(request, project_id): + project = get_object_or_404(Project, pk=project_id, owner=request.user) + objects = TestFile.objects.filter(project=project) + + send_keen_event('cloudpebble', 'cloudpebble_list_source', data={ + 'data': { + 'kind': 'tests' + } + }, project=project, request=request) + + return json_response({ + "success": True, + "tests": [{ + "modified": time.mktime(test.last_modified.utctimetuple()), + "id": test.id, + "name": test.file_name + } for test in objects] + }) @require_safe @csrf_protect diff --git a/ide/migrations/0040_auto__add_testrun__add_unique_testrun_test_session__add_testsession.py b/ide/migrations/0040_auto__add_testrun__add_unique_testrun_test_session__add_testsession.py new file mode 100644 index 00000000..c2eff6e1 --- /dev/null +++ b/ide/migrations/0040_auto__add_testrun__add_unique_testrun_test_session__add_testsession.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'TestRun' + db.create_table(u'ide_testrun', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('date_started', self.gf('django.db.models.fields.DateTimeField')(null=True)), + ('date_completed', self.gf('django.db.models.fields.DateTimeField')(null=True)), + ('session', self.gf('django.db.models.fields.related.ForeignKey')(related_name='runs', to=orm['ide.TestSession'])), + ('test', self.gf('django.db.models.fields.related.ForeignKey')(related_name='runs', to=orm['ide.TestFile'])), + ('code', self.gf('django.db.models.fields.IntegerField')()), + ('log', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('ide', ['TestRun']) + + # Adding unique constraint on 'TestRun', fields ['test', 'session'] + db.create_unique(u'ide_testrun', ['test_id', 'session_id']) + + # Adding model 'TestSession' + db.create_table(u'ide_testsession', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('date_started', self.gf('django.db.models.fields.DateTimeField')(null=True)), + ('date_completed', self.gf('django.db.models.fields.DateTimeField')(null=True)), + ('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='test_sessions', to=orm['ide.Project'])), + )) + db.send_create_signal('ide', ['TestSession']) + + + def backwards(self, orm): + # Removing unique constraint on 'TestRun', fields ['test', 'session'] + db.delete_unique(u'ide_testrun', ['test_id', 'session_id']) + + # Deleting model 'TestRun' + db.delete_table(u'ide_testrun') + + # Deleting model 'TestSession' + db.delete_table(u'ide_testsession') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'ide.buildresult': { + 'Meta': {'object_name': 'BuildResult'}, + 'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'builds'", 'to': "orm['ide.Project']"}), + 'started': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "'3de06f40-02ef-422d-a1fc-fc20622b1aa2'", 'max_length': '36'}) + }, + 'ide.buildsize': { + 'Meta': {'object_name': 'BuildSize'}, + 'binary_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sizes'", 'to': "orm['ide.BuildResult']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'platform': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'resource_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'total_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'worker_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'ide.project': { + 'Meta': {'object_name': 'Project'}, + 'app_capabilities': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'app_company_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'app_is_hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'app_is_shown_on_communication': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'app_is_watchface': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'app_jshint': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'app_keys': ('django.db.models.fields.TextField', [], {'default': "'{}'"}), + 'app_long_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'app_platforms': ('django.db.models.fields.TextField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'app_short_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'app_uuid': ('django.db.models.fields.CharField', [], {'default': "'148f2761-36a7-4063-ab36-1148d39cad03'", 'max_length': '36', 'null': 'True', 'blank': 'True'}), + 'app_version_label': ('django.db.models.fields.CharField', [], {'default': "'1.0'", 'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'github_branch': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'github_hook_build': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'github_hook_uuid': ('django.db.models.fields.CharField', [], {'max_length': '36', 'null': 'True', 'blank': 'True'}), + 'github_last_commit': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'github_last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'github_repo': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'optimisation': ('django.db.models.fields.CharField', [], {'default': "'s'", 'max_length': '1'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + 'project_type': ('django.db.models.fields.CharField', [], {'default': "'native'", 'max_length': '10'}), + 'sdk_version': ('django.db.models.fields.CharField', [], {'default': "'2'", 'max_length': '6'}) + }, + 'ide.resourcefile': { + 'Meta': {'unique_together': "(('project', 'file_name'),)", 'object_name': 'ResourceFile'}, + 'file_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_menu_icon': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'kind': ('django.db.models.fields.CharField', [], {'max_length': '9'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'resources'", 'to': "orm['ide.Project']"}), + 'target_platforms': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '30', 'null': 'True', 'blank': 'True'}) + }, + 'ide.resourceidentifier': { + 'Meta': {'unique_together': "(('resource_file', 'resource_id'),)", 'object_name': 'ResourceIdentifier'}, + 'character_regex': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'compatibility': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resource_file': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'identifiers'", 'to': "orm['ide.ResourceFile']"}), + 'resource_id': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'tracking': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'ide.resourcevariant': { + 'Meta': {'unique_together': "(('resource_file', 'tags'),)", 'object_name': 'ResourceVariant'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_legacy': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'resource_file': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'variants'", 'to': "orm['ide.ResourceFile']"}), + 'tags': ('django.db.models.fields.CommaSeparatedIntegerField', [], {'max_length': '50', 'blank': 'True'}) + }, + 'ide.screenshotfile': { + 'Meta': {'unique_together': "(('platform', 'screenshot_set'),)", 'object_name': 'ScreenshotFile'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'platform': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'screenshot_set': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'files'", 'to': "orm['ide.ScreenshotSet']"}) + }, + 'ide.screenshotset': { + 'Meta': {'object_name': 'ScreenshotSet'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'screenshot_sets'", 'to': "orm['ide.TestFile']"}) + }, + 'ide.sourcefile': { + 'Meta': {'unique_together': "(('project', 'file_name'),)", 'object_name': 'SourceFile'}, + 'file_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'folded_lines': ('django.db.models.fields.TextField', [], {'default': "'[]'"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'null': 'True', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'source_files'", 'to': "orm['ide.Project']"}), + 'target': ('django.db.models.fields.CharField', [], {'default': "'app'", 'max_length': '10'}) + }, + 'ide.templateproject': { + 'Meta': {'object_name': 'TemplateProject', '_ormbases': ['ide.Project']}, + u'project_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['ide.Project']", 'unique': 'True', 'primary_key': 'True'}), + 'template_kind': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}) + }, + 'ide.testfile': { + 'Meta': {'unique_together': "(('project', 'file_name'),)", 'object_name': 'TestFile'}, + 'file_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'folded_lines': ('django.db.models.fields.TextField', [], {'default': "'[]'"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'null': 'True', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_files'", 'to': "orm['ide.Project']"}) + }, + 'ide.testrun': { + 'Meta': {'unique_together': "(('test', 'session'),)", 'object_name': 'TestRun'}, + 'code': ('django.db.models.fields.IntegerField', [], {}), + 'date_completed': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_started': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'log': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'session': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'runs'", 'to': "orm['ide.TestSession']"}), + 'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'runs'", 'to': "orm['ide.TestFile']"}) + }, + 'ide.testsession': { + 'Meta': {'object_name': 'TestSession'}, + 'date_completed': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date_started': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_sessions'", 'to': "orm['ide.Project']"}) + }, + 'ide.usergithub': { + 'Meta': {'object_name': 'UserGithub'}, + 'avatar': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'nonce': ('django.db.models.fields.CharField', [], {'max_length': '36', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'github'", 'unique': 'True', 'primary_key': 'True', 'to': u"orm['auth.User']"}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}) + }, + 'ide.usersettings': { + 'Meta': {'object_name': 'UserSettings'}, + 'accepted_terms': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'autocomplete': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'keybinds': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '20'}), + 'tab_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '2'}), + 'theme': ('django.db.models.fields.CharField', [], {'default': "'cloudpebble'", 'max_length': '50'}), + 'use_spaces': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'whats_new': ('django.db.models.fields.PositiveIntegerField', [], {'default': '18'}) + } + } + + complete_apps = ['ide'] \ No newline at end of file diff --git a/ide/models/__init__.py b/ide/models/__init__.py index 2531a67c..04040cc2 100644 --- a/ide/models/__init__.py +++ b/ide/models/__init__.py @@ -4,3 +4,4 @@ from ide.models.files import * from ide.models.project import * from ide.models.user import * +from ide.models.monkey import * diff --git a/ide/models/monkey.py b/ide/models/monkey.py new file mode 100644 index 00000000..ac535923 --- /dev/null +++ b/ide/models/monkey.py @@ -0,0 +1,26 @@ +from django.db import models + +from ide.models.meta import IdeModel + +__author__ = 'joe' + +class TestSession(IdeModel): + date_started = models.DateTimeField(null=True) + date_completed = models.DateTimeField(null=True) + project = models.ForeignKey('Project', related_name='test_sessions') + +class TestCode: + QUEUED = 0 + COMPLETE = 1 + + +class TestRun(IdeModel): + date_started = models.DateTimeField(null=True) + date_completed = models.DateTimeField(null=True) + session = models.ForeignKey('TestSession', related_name='runs') + test = models.ForeignKey('TestFile', related_name='runs') + code = models.IntegerField(default=TestCode.QUEUED) + log = models.TextField(blank=True, null=True) # logfile on server? + + class Meta(IdeModel.Meta): + unique_together = ('test', 'session') diff --git a/ide/static/ide/js/editor.js b/ide/static/ide/js/editor.js index f0039ad0..995bdf50 100644 --- a/ide/static/ide/js/editor.js +++ b/ide/static/ide/js/editor.js @@ -58,7 +58,7 @@ CloudPebble.Editor = (function() { return; } CloudPebble.ProgressBar.Show(); - var url_kind = (file.target == 'test' ? 'test' : 'source'); + var url_kind = (file.target == 'test' ? 'tests' : 'source'); var sidebar_id = url_kind + '-' + file.id; // Open it. $.getJSON('/ide/project/' + PROJECT_ID + '/' + url_kind + '/' + file.id + '/load', function(data) { diff --git a/ide/tasks/__init__.py b/ide/tasks/__init__.py index 7d2dce9a..11739c27 100644 --- a/ide/tasks/__init__.py +++ b/ide/tasks/__init__.py @@ -4,6 +4,7 @@ from ide.tasks.build import run_compile from ide.tasks.git import github_push, github_pull from ide.tasks.gist import import_gist +from ide.tasks.monkey import run_test_session import apptools.addr2lines diff --git a/ide/tasks/monkey.py b/ide/tasks/monkey.py new file mode 100644 index 00000000..5cb7d055 --- /dev/null +++ b/ide/tasks/monkey.py @@ -0,0 +1,61 @@ +from datetime import datetime, timedelta +from django.db import transaction +from celery import task +from time import sleep + +from ide.models.monkey import TestSession, TestRun, TestCode +from ide.models.files import TestFile +__author__ = 'joe' + + +def setup_test_session(project, test_ids = None): + # Create a test session + + with transaction.atomic(): + session = TestSession.objects.create(project=project) + session.save() + runs = [] + + if test_ids is None: + # If test_ids is None, get all tests for the project + tests = TestFile.objects.filter(project=project) + else: + # Otherwise, get all requested test(s) + tests = TestFile.objects.filter(project=project, id__in=test_ids) + + # Then make a test run for every test + for test in tests: + run = TestRun.objects.create(session=session, test=test) + run.save() + runs.append(run) + + # Return the session and its runs + + return session, runs + + +@task(ignore_result=True, acks_late=True) +def run_test_session(session_id): + #TODO: ignore_result? acks_late? + session = TestSession.objects.get(pk=session_id) + runs = TestRun.objects.filter(session=session) + # Set the session as having started now + session.date_started = datetime.now() + session.save() + # Run all the tests + + for run in runs: + run.date_started = datetime.now() + run.save() + + # TODO: automated testing implementation, actually run the tests + sleep(5) + + for run in runs: + run.code = TestCode.COMPLETE + run.log = "Test log\nThis is a fake run :)" + run.date_completed = datetime.now() + run.save() + session.date_completed = datetime.now() + + return True diff --git a/ide/tests/__init__.py b/ide/tests/__init__.py new file mode 100644 index 00000000..3fa5af38 --- /dev/null +++ b/ide/tests/__init__.py @@ -0,0 +1 @@ +__author__ = 'joe' diff --git a/ide/tests/cloudpebble_test.py b/ide/tests/cloudpebble_test.py new file mode 100644 index 00000000..0c222cbf --- /dev/null +++ b/ide/tests/cloudpebble_test.py @@ -0,0 +1,15 @@ +from django.test import TestCase +from django.test.client import Client +from django.test.utils import setup_test_environment + +setup_test_environment() + +class CloudpebbleTestCase(TestCase): + """CloudpebbleTestCase provides convenience functions for other test cases""" + def login(self): + self.client = Client() + self.client.post('/accounts/register', {'username': 'test', 'email': 'test@test.test', 'password1': 'test', 'password2': 'test'}) + self.assertTrue(self.client.login(username='test', password='test')) + self.assertJSONEqual(self.client.post('/ide/project/create', {'name': 'test', 'template': 0, 'type': 'native', 'sdk': 3}).content, + {"id": 1, "success": True}) + self.project_id = 1 diff --git a/ide/tests.py b/ide/tests/test_git.py similarity index 70% rename from ide/tests.py rename to ide/tests/test_git.py index faa5ea97..3d141bb1 100644 --- a/ide/tests.py +++ b/ide/tests/test_git.py @@ -3,15 +3,16 @@ """ from django.test import TestCase -import git +import ide.git +__author__ = 'joe' class UrlToReposTest(TestCase): def test_basic_url_to_repo(self): """ Tests that a simple repo url is correctly recognized. """ - username, reponame = git.url_to_repo("https://github.com/pebble/cloudpebble") + username, reponame = ide.git.url_to_repo("https://github.com/pebble/cloudpebble") self.assertEqual("pebble", username) self.assertEqual("cloudpebble", reponame) @@ -19,7 +20,7 @@ def test_strange_url_to_repo(self): """ Tests that a non-standard repo url is correctly recognized. """ - username, reponame = git.url_to_repo("git://github.com:foo/bar.git") + username, reponame = ide.git.url_to_repo("git://github.com:foo/bar.git") self.assertEqual("foo", username) self.assertEqual("bar", reponame) @@ -27,4 +28,4 @@ def test_bad_url_to_repo(self): """ Tests that a entirely different url returns None. """ - self.assertEqual(None, git.url_to_repo("http://www.cuteoverload.com")) + self.assertEqual(None, ide.git.url_to_repo("http://www.cuteoverload.com")) diff --git a/ide/test_screenshots.py b/ide/tests/test_screenshots.py similarity index 87% rename from ide/test_screenshots.py rename to ide/tests/test_screenshots.py index f76e4529..02eb97db 100644 --- a/ide/test_screenshots.py +++ b/ide/tests/test_screenshots.py @@ -1,7 +1,6 @@ import json -from django.test import TestCase +from cloudpebble_test import CloudpebbleTestCase from django.test.utils import setup_test_environment -from django.test.client import Client from django.core.files.uploadedfile import SimpleUploadedFile from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import reverse @@ -10,19 +9,7 @@ __author__ = 'joe' -setup_test_environment() - -class CloudpebbleTestCase(TestCase): - def login(self): - self.client = Client() - self.client.post('/accounts/register', {'username': 'test', 'email': 'test@test.test', 'password1': 'test', 'password2': 'test'}) - self.assertTrue(self.client.login(username='test', password='test')) - self.assertJSONEqual(self.client.post('/ide/project/create', {'name': 'test', 'template': 0, 'type': 'native', 'sdk': 3}).content, - {"id": 1, "success": True}) - self.project_id = 1 - class ScreenshotsTests(CloudpebbleTestCase): - def setUp(self): self.login() @@ -113,7 +100,7 @@ def test_delete_test(self): # Delete the test url = reverse('ide:delete_source_file', kwargs={ 'project_id': self.project_id, - 'kind': 'test', + 'kind': 'tests', 'file_id': test_id }) self.client.post(url) @@ -125,4 +112,3 @@ def test_delete_test(self): ScreenshotSet.objects.get(pk=set_id) with self.assertRaises(ObjectDoesNotExist): ScreenshotFile.objects.get(pk=file_id) - diff --git a/ide/tests/test_tests.py b/ide/tests/test_tests.py new file mode 100644 index 00000000..ccbab682 --- /dev/null +++ b/ide/tests/test_tests.py @@ -0,0 +1,103 @@ +import json +from cloudpebble_test import CloudpebbleTestCase +from django.core.urlresolvers import reverse +from ide.models.files import TestFile, ScreenshotSet, ScreenshotFile +from django.conf import settings +from django.test.utils import override_settings +from collections import Counter + +__author__ = 'joe' + + +class TestsTests(CloudpebbleTestCase): + """Tests for the Tests models""" + + def setUp(self): + self.login() + + def add_and_run_tests(self, names=None): + url = reverse('ide:create_test_file', args=[self.project_id]) + # Insert some tests + names = ["mytest1", "mytest2"] if names is None else names + tests = {test['id']: test for test in + (json.loads(self.client.post(url, {"name": name}).content)['file'] for name in names)} + # Start a test session + url = reverse('ide:post_test_session', args=[self.project_id]) + result = json.loads(self.client.post(url, {}).content)['data'] + # Check that the server returns a session containing all the tests we added + run_tests = {run['test']['id']: run['test'] for run in result['runs']} + for test_id, test in tests.iteritems(): + self.assertEqual(test['name'], run_tests[test['id']]['name']) + return result + + def test_get_sessions(self): + session_data = self.add_and_run_tests() + url = reverse('ide:get_test_sessions', args=[self.project_id]) + sessions = json.loads(self.client.get(url).content)['data'] + # Check that the list of all sessions contains the session + self.assertEqual(sessions[0]['id'], session_data['id']) + + def test_get_session(self): + session_data = self.add_and_run_tests() + url = reverse('ide:get_test_session', args=[self.project_id, session_data['id']]) + session = json.loads(self.client.get(url).content)['data'] + # Check that we can get the test session + self.assertEqual(session['id'], session_data['id']) + + def test_run_multiple(self): + session_data1 = self.add_and_run_tests(["mytest1", "mytest2"]) + session_data2 = self.add_and_run_tests(["mytest3", "mytest4"]) + # The second session should run all four tests + self.assertEqual(len(session_data2['runs']), 4) + + def test_get_all_runs(self): + # Add two tests and run, then add another two tests and run + self.add_and_run_tests(["mytest1", "mytest2"]) + self.add_and_run_tests(["mytest3", "mytest4"]) + url = reverse('ide:get_test_runs', args=[self.project_id]) + + # When we get all runs, we expect to have run the first two tests twice + # and the second two tests once + runs = json.loads(self.client.get(url).content)['data'] + self.assertDictEqual(dict(Counter(run['test']['name'] for run in runs)), { + 'mytest1': 2, + 'mytest2': 2, + 'mytest3': 1, + 'mytest4': 1 + }) + + def test_get_runs_for_session(self): + # Add two tests and run, then add another two tests and run + session_data1 = self.add_and_run_tests(["mytest1", "mytest2"]) + session_data2 = self.add_and_run_tests(["mytest3", "mytest4"]) + url = reverse('ide:get_test_runs', args=[self.project_id]) + + # We expect each session to run all previously added tests + runs1 = json.loads(self.client.get(url, {'session': session_data1['id']}).content)['data'] + runs2 = json.loads(self.client.get(url, {'session': session_data2['id']}).content)['data'] + self.assertEqual(len(runs1), 2) + self.assertEqual(len(runs2), 4) + + def test_get_runs_for_test(self): + # Add two tests and run, then add another two tests and run + session_data1 = self.add_and_run_tests(["mytest1", "mytest2"]) + session_data2 = self.add_and_run_tests(["mytest3", "mytest4"]) + url = reverse('ide:get_test_runs', args=[self.project_id]) + + # Get details for mytest1, and mytest4. mytest1 should get run twice. + runs1 = json.loads(self.client.get(url, {'test': session_data1['runs'][0]['test']['id']}).content)['data'] + runs2 = json.loads(self.client.get(url, {'test': session_data2['runs'][-1]['test']['id']}).content)['data'] + self.assertEqual(len(runs1), 2) + self.assertEqual(len(runs2), 1) + + def test_list_tests(self): + # Make a collection of test files + post_url = reverse('ide:create_test_file', args=[self.project_id]) + ids = sorted([int(json.loads(self.client.post(post_url, {"name": "mytest"+str(x)}).content)['file']['id']) for x in range(5)]) + + # Get the list test files + url = reverse('ide:get_test_list', args=[self.project_id]) + response = sorted([int(t['id']) for t in json.loads(self.client.get(url).content)['tests']]) + + # Check that all IDs are present + self.assertEqual(ids == response) diff --git a/ide/urls.py b/ide/urls.py index 111893a1..0c3c3a3e 100644 --- a/ide/urls.py +++ b/ide/urls.py @@ -8,8 +8,9 @@ from ide.api.resource import create_resource, resource_info, delete_resource, update_resource, show_resource, \ delete_variant from ide.api.source import create_source_file, create_test_file, load_source_file, source_file_is_safe, save_source_file, \ - delete_source_file, rename_source_file + delete_source_file, rename_source_file, get_test_list from ide.api.screenshots import save_screenshots, load_screenshots, show_screenshot +from ide.api.monkey import get_test_session, get_test_sessions, get_test_run, get_test_runs, post_test_session from ide.api.user import transition_accept, transition_export, transition_delete, whats_new from ide.api.ycm import init_autocomplete from ide.api.qemu import launch_emulator, generate_phone_token, handle_phone_token @@ -28,13 +29,14 @@ url(r'^project/(?P\d+)/delete', delete_project, name='delete_project'), url(r'^project/(?P\d+)/create_source_file', create_source_file, name='create_source_file'), url(r'^project/(?P\d+)/create_test_file', create_test_file, name='create_test_file'), - url(r'^project/(?P\d+)/(?P(source|test))/(?P\d+)/load', load_source_file, name='load_source_file'), - url(r'^project/(?P\d+)/(?P(source|test))/(?P\d+)/save', save_source_file, name='save_source_file'), + url(r'^project/(?P\d+)/tests$', get_test_list, name='get_test_list'), + url(r'^project/(?P\d+)/(?P(source|tests))/(?P\d+)/load', load_source_file, name='load_source_file'), + url(r'^project/(?P\d+)/(?P(source|tests))/(?P\d+)/save', save_source_file, name='save_source_file'), url(r'^project/(?P\d+)/test/(?P\d+)/screenshots/save', save_screenshots, name='save_screenshots'), url(r'^project/(?P\d+)/test/(?P\d+)/screenshots/load', load_screenshots, name='load_screenshots'), - url(r'^project/(?P\d+)/(?P(source|test))/(?P\d+)/rename', rename_source_file, name='rename_source_file'), - url(r'^project/(?P\d+)/(?P(source|test))/(?P\d+)/is_safe', source_file_is_safe, name='source_file_is_safe'), - url(r'^project/(?P\d+)/(?P(source|test))/(?P\d+)/delete', delete_source_file, name='delete_source_file'), + url(r'^project/(?P\d+)/(?P(source|tests))/(?P\d+)/rename', rename_source_file, name='rename_source_file'), + url(r'^project/(?P\d+)/(?P(source|tests))/(?P\d+)/is_safe', source_file_is_safe, name='source_file_is_safe'), + url(r'^project/(?P\d+)/(?P(source|tests))/(?P\d+)/delete', delete_source_file, name='delete_source_file'), url(r'^project/(?P\d+)/test/(?P\d+)/screenshot/(?P\d+)/(?P\w{1,10})/get/', show_screenshot, name='show_screenshot'), url(r'^project/(?P\d+)/create_resource', create_resource, name='create_resource'), url(r'^project/(?P\d+)/resource/(?P\d+)/info', resource_info, name='resource_info'), @@ -42,6 +44,11 @@ url(r'^project/(?P\d+)/resource/(?P\d+)/update', update_resource, name='update_resource'), url(r'^project/(?P\d+)/resource/(?P\d+)/(?P\d+(?:,\d+)*)/get', show_resource, name='show_resource'), url(r'^project/(?P\d+)/resource/(?P\d+)/(?P\d+(?:,\d+)*)/delete', delete_variant, name='delete_variant'), + url(r'^project/(?P\d+)/test_sessions$', get_test_sessions, name='get_test_sessions'), + url(r'^project/(?P\d+)/test_sessions/(?P\d+)$', get_test_session, name='get_test_session'), + url(r'^project/(?P\d+)/test_runs$', get_test_runs, name='get_test_runs'), + url(r'^project/(?P\d+)/test_runs/(?P\d+)$', get_test_run, name='get_test_run'), + url(r'^project/(?P\d+)/test_sessions/run$', post_test_session, name='post_test_session'), url(r'^project/(?P\d+)/build/run', compile_project, name='compile_project'), url(r'^project/(?P\d+)/build/last', last_build, name='get_last_build'), url(r'^project/(?P\d+)/build/history', build_history, name='get_build_history'), From a53e13434918835a03dcc73d733fd6e5c8c6d83f Mon Sep 17 00:00:00 2001 From: Joseph Atkins-Turkish Date: Tue, 3 Nov 2015 13:05:45 -0800 Subject: [PATCH 009/184] Added React to the project --- cloudpebble/settings.py | 1 + ide/static/ide/js/sidebar.js | 1 + ide/static/ide/js/test_manager.js | 8 ++++++++ ide/templates/ide/project.html | 5 +++++ 4 files changed, 15 insertions(+) create mode 100644 ide/static/ide/js/test_manager.js diff --git a/cloudpebble/settings.py b/cloudpebble/settings.py index efccdc89..983e86a8 100644 --- a/cloudpebble/settings.py +++ b/cloudpebble/settings.py @@ -142,6 +142,7 @@ 'alexgorbatchev/jquery-textext', 'codemirror#4.2.0', 'kanaka/noVNC', + 'react' ) # Make this unique, and don't share it with anybody. diff --git a/ide/static/ide/js/sidebar.js b/ide/static/ide/js/sidebar.js index b96798fd..0936007c 100644 --- a/ide/static/ide/js/sidebar.js +++ b/ide/static/ide/js/sidebar.js @@ -162,6 +162,7 @@ CloudPebble.Sidebar = (function() { $('#sidebar-pane-new-resource').click(CloudPebble.Resources.Create); $('#sidebar-pane-compile > a').click(CloudPebble.Compile.Show); $('#sidebar-pane-settings > a').click(CloudPebble.Settings.Show); + $('#sidebar-pane-testmanager > a').click(CloudPebble.TestManager.Show); $('#sidebar-pane-github > a').click(CloudPebble.GitHub.Show); $('#sidebar-pane-timeline > a').click(CloudPebble.Timeline.show); $('#new-source-file').click(CloudPebble.Editor.Create); diff --git a/ide/static/ide/js/test_manager.js b/ide/static/ide/js/test_manager.js new file mode 100644 index 00000000..e68af66f --- /dev/null +++ b/ide/static/ide/js/test_manager.js @@ -0,0 +1,8 @@ +CloudPebble.TestManager = (function() { + + return { + Show: function() { + + } + } +})(); \ No newline at end of file diff --git a/ide/templates/ide/project.html b/ide/templates/ide/project.html index 4ca668ce..58a76b2a 100644 --- a/ide/templates/ide/project.html +++ b/ide/templates/ide/project.html @@ -64,6 +64,7 @@

    -
    +
    @@ -139,7 +139,6 @@

    {% block modals %} -
    +