Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #13 from richo/wip/modal_new_objects

modal new objects
  • Loading branch information...
commit 9a471011940c1a23c22c129ae2195a557ec78552 2 parents d05e532 + f8b1775
@richo authored
View
5 Makefile
@@ -35,4 +35,7 @@ clean:
groundstation_test:
GROUNDSTATION_DEBUG=WARN python -m unittest discover test
-test: groundstation_test
+airship_test:
+ cd airship; ../node_modules/mocha/bin/mocha
+
+test: groundstation_test airship_test
View
33 airship/__init__.py
@@ -14,6 +14,7 @@
import pygit2
from groundstation.utils import oid2hex
+from groundstation.objects.root_object import RootObject
from groundstation.objects.update_object import UpdateObject
def jsonate(obj, escaped):
@@ -90,4 +91,36 @@ def update_gref(channel, identifier):
station.update_gref(gref, [oid], parents)
return jsonate({"response": "ok"}, False)
+ @app.route("/grefs/<channel>", methods=['PUT'])
+ def create_gref(channel):
+ def _write_object(obj):
+ return station.write(obj.as_object())
+
+ name = request.form["name"]
+ protocol = request.form["protocol"]
+ user = request.form["user"]
+ body = request.form["body"]
+ title = request.form["title"]
+ gref = Gref(station.store, channel, name)
+ root = RootObject(name, channel, protocol)
+ root_oid = _write_object(root)
+
+ _title = UpdateObject([root_oid], json.dumps({
+ "type": "title",
+ "id": None,
+ "body": title,
+ "user": user
+ }))
+ title_oid = _write_object(_title)
+
+ _body = UpdateObject([title_oid], json.dumps({
+ "type": "body",
+ "id": None,
+ "body": body
+ }))
+ body_oid = _write_object(_body)
+
+ station.update_gref(gref, [body_oid], [])
+ return ""
+
return app
View
41 airship/static/airship-gref-validation.js
@@ -0,0 +1,41 @@
+groundstation.validators.gref = (function() {
+ func = function(name, title, body) {
+ return func.valid_name_p(name) &&
+ func.valid_title_p(title) &&
+ func.valid_body_p(body);
+ };
+ return function(d_name, d_title, d_body) {
+ func.valid_name_p = function(name) {
+ if ((typeof name) !== 'string')
+ return false;
+ if (name.length === 0)
+ return false;
+ if (name === d_name)
+ return false;
+ if (name.indexOf(" ") >= 0)
+ return false;
+ return true;
+ };
+ func.valid_title_p = function(title) {
+ if ((typeof title) !== 'string')
+ return false;
+ if (title.length === 0)
+ return false;
+ if (title === d_title)
+ return false;
+ // TODO
+ return true;
+ };
+ func.valid_body_p = function(body) {
+ if ((typeof body) !== 'string')
+ return false;
+ if (body.length === 0)
+ return false;
+ if (body === d_body)
+ return false;
+ // TODO
+ return true;
+ };
+ return func;
+ };
+})();
View
76 airship/static/airship.js
@@ -7,12 +7,13 @@ function Groundstation() {
this.username = localStorage.getItem("airship.committer") || "Anonymous Coward";
this.renderers = {};
+ this.validators = {};
}
function init_airship(groundstation) {
_.each(groundstation.channels.models, function(channel) {
new ChannelTab({
model: channel,
- id: channel.attributes["name"]
+ id: channel.attributes["name"]
});
});
rendered_gref_content = new RenderedGref({
@@ -34,6 +35,52 @@ function init_airship(groundstation) {
}
}
});
+ var new_gref = {
+ title: $("#new-gref-title").html(),
+ body: $("#new-gref-body").html(),
+ name: $("#new-gref-name").html()
+ };
+ var new_gref_validator = groundstation.validators.gref(new_gref.name, new_gref.title, new_gref.body);
+
+ $("#new-gref").on('click', function() {
+ $("#new-gref-title").html(new_gref.title);
+ $("#new-gref-body").html(new_gref.body);
+ $("#new-gref-name").html(new_gref.name);
+ var modal = $("#new-gref-modal");
+ modal.modal();
+ });
+ $("#new-gref-cancel").on('click', function() {
+ $("#new-gref-modal").modal('hide');
+ });
+ $("#new-gref-create").on('click', function() {
+ var title = $("#new-gref-title").html(),
+ body = $("#new-gref-body").html(),
+ name = $("#new-gref-name").html(),
+ protocol = $("#new-gref-protocol").html();
+
+ if (new_gref_validator(name, title, body)) {
+ $.ajax({
+ type: "PUT",
+ url: groundstation.active_grefs.url,
+ data: {
+ title: title,
+ body: body,
+ name: name,
+ protocol: protocol,
+
+ user: groundstation.username
+
+ },
+ success: function(data, st, xhr) {
+ $("#new-gref-modal").modal('hide');
+ groundstation.active_grefs.redraw();
+ }
+ });
+ } else {
+ // TODO Actually give the user something to go with
+ alert("Validation failed!");
+ }
+ });
}
var Channel = Backbone.Model.extend();
@@ -108,18 +155,21 @@ var ChannelTab = Backbone.View.extend({
var self = this;
var current_grefs = $("#current-grefs")[0];
groundstation.active_grefs.url = '/grefs/' + this.model.attributes["name"];
- groundstation.active_grefs.fetch({
- success: function(collection, response, options) {
- $("#active-channel").html(self.model.attributes["name"]);
- $("#gref-container").show();
- _.each(visible_grefs, function(el) { el.remove(); });
- _.each(collection.models, function(gref) {
- visible_grefs.push(new GrefMenuItem({
- model: gref
- }));
- });
- }
- });
+ groundstation.active_grefs.redraw = function() {
+ groundstation.active_grefs.fetch({
+ success: function(collection, response, options) {
+ $("#active-channel").html(self.model.attributes["name"]);
+ $("#gref-container").show();
+ _.each(visible_grefs, function(el) { el.remove(); });
+ _.each(collection.models, function(gref) {
+ visible_grefs.push(new GrefMenuItem({
+ model: gref
+ }));
+ });
+ }
+ });
+ };
+ groundstation.active_grefs.redraw();
},
events: {
View
251 airship/static/bootstrap-modal.js
@@ -0,0 +1,251 @@
+/* =========================================================
+ * bootstrap-modal.js v3.0.0
+ * http://twitter.github.com/bootstrap/javascript.html#modals
+ * =========================================================
+ * Copyright 2012 Twitter, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================= */
+
+
+!function ($) {
+
+ "use strict"; // jshint ;_;
+
+
+ /* MODAL CLASS DEFINITION
+ * ====================== */
+
+ var Modal = function (element, options) {
+ this.options = options
+ this.$element = $(element)
+ .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this))
+ this.options.remote && this.$element.find('.modal-body').load(this.options.remote)
+ }
+
+ Modal.prototype = {
+
+ constructor: Modal
+
+ , toggle: function () {
+ return this[!this.isShown ? 'show' : 'hide']()
+ }
+
+ , show: function () {
+ var that = this
+ , e = $.Event('show')
+
+ this.$element.trigger(e)
+
+ if (this.isShown || e.isDefaultPrevented()) return
+
+ this.isShown = true
+
+ this.escape()
+
+ this.backdrop(function () {
+ var transition = $.support.transition && that.$element.hasClass('fade')
+
+ if (!that.$element.parent().length) {
+ that.$element.appendTo(document.body) //don't move modals dom position
+ }
+
+ that.$element.show()
+
+ if (transition) {
+ that.$element[0].offsetWidth // force reflow
+ }
+
+ that.$element
+ .addClass('in')
+ .attr('aria-hidden', false)
+
+ that.enforceFocus()
+
+ transition ?
+ that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) :
+ that.$element.focus().trigger('shown')
+
+ })
+ }
+
+ , hide: function (e) {
+ e && e.preventDefault()
+
+ var that = this
+
+ e = $.Event('hide')
+
+ this.$element.trigger(e)
+
+ if (!this.isShown || e.isDefaultPrevented()) return
+
+ this.isShown = false
+
+ this.escape()
+
+ $(document).off('focusin.modal')
+
+ this.$element
+ .removeClass('in')
+ .attr('aria-hidden', true)
+
+ $.support.transition && this.$element.hasClass('fade') ?
+ this.hideWithTransition() :
+ this.hideModal()
+ }
+
+ , enforceFocus: function () {
+ var that = this
+ $(document).on('focusin.modal', function (e) {
+ if (that.$element[0] !== e.target && !that.$element.has(e.target).length) {
+ that.$element.focus()
+ }
+ })
+ }
+
+ , escape: function () {
+ var that = this
+ if (this.isShown && this.options.keyboard) {
+ this.$element.on('keyup.dismiss.modal', function ( e ) {
+ e.which == 27 && that.hide()
+ })
+ } else if (!this.isShown) {
+ this.$element.off('keyup.dismiss.modal')
+ }
+ }
+
+ , hideWithTransition: function () {
+ var that = this
+ , timeout = setTimeout(function () {
+ that.$element.off($.support.transition.end)
+ that.hideModal()
+ }, 500)
+
+ this.$element.one($.support.transition.end, function () {
+ clearTimeout(timeout)
+ that.hideModal()
+ })
+ }
+
+ , hideModal: function () {
+ var that = this
+ this.$element.hide()
+ this.backdrop(function () {
+ that.removeBackdrop()
+ that.$element.trigger('hidden')
+ })
+ }
+
+ , removeBackdrop: function () {
+ this.$backdrop && this.$backdrop.remove()
+ this.$backdrop = null
+ }
+
+ , backdrop: function (callback) {
+ var that = this
+ , animate = this.$element.hasClass('fade') ? 'fade' : ''
+
+ if (this.isShown && this.options.backdrop) {
+ var doAnimate = $.support.transition && animate
+
+ this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
+ .appendTo(document.body)
+
+ this.$backdrop.click(
+ this.options.backdrop == 'static' ?
+ $.proxy(this.$element[0].focus, this.$element[0])
+ : $.proxy(this.hide, this)
+ )
+
+ if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
+
+ this.$backdrop.addClass('in')
+
+ if (!callback) return
+
+ doAnimate ?
+ this.$backdrop.one($.support.transition.end, callback) :
+ callback()
+
+ } else if (!this.isShown && this.$backdrop) {
+ this.$backdrop.removeClass('in')
+
+ $.support.transition && this.$element.hasClass('fade')?
+ this.$backdrop.one($.support.transition.end, callback) :
+ callback()
+
+ } else if (callback) {
+ callback()
+ }
+ }
+ }
+
+
+ /* MODAL PLUGIN DEFINITION
+ * ======================= */
+
+ var old = $.fn.modal
+
+ $.fn.modal = function (option) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('modal')
+ , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option)
+ if (!data) $this.data('modal', (data = new Modal(this, options)))
+ if (typeof option == 'string') data[option]()
+ else if (options.show) data.show()
+ })
+ }
+
+ $.fn.modal.defaults = {
+ backdrop: true
+ , keyboard: true
+ , show: true
+ }
+
+ $.fn.modal.Constructor = Modal
+
+
+ /* MODAL NO CONFLICT
+ * ================= */
+
+ $.fn.modal.noConflict = function () {
+ $.fn.modal = old
+ return this
+ }
+
+
+ /* MODAL DATA-API
+ * ============== */
+
+ $(document).on('click.modal.data-api', '[data-toggle="modal"]', function (e) {
+ var $this = $(this)
+ , href = $this.attr('href')
+ , $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
+ , option = $target.data('modal') ? 'toggle' : $.extend({ remote:!/#/.test(href) && href }, $target.data(), $this.data())
+
+ e.preventDefault()
+
+ $target
+ .modal(option)
+ .one('hide', function () {
+ $this.focus()
+ })
+ })
+
+ var $body = $(document.body)
+ .on('shown', '.modal', function () { $body.addClass('modal-open') })
+ .on('hidden', '.modal', function () { $body.removeClass('modal-open') })
+
+}(window.jQuery);
View
31 airship/templates/index.html
@@ -5,6 +5,7 @@
<script src="/static/underscore-1.4.4.js"></script>
<script src="/static/backbone-0.9.9.js"></script>
<script src="/static/bootstrap-dropdown.js"></script>
+ <script src="/static/bootstrap-modal.js"></script>
<script src="/static/airship.js?bust={{current_time}}"></script>
<script src="/static/markdown.js"></script>
<link rel="stylesheet" href="/static/bootstrap.css">
@@ -17,6 +18,7 @@
init_airship(groundstation);
});
</script>
+ <script src="/static/airship-gref-validation.js"></script>
<!-- For stupid reasons, load order matters. -->
<script src="/static/protocol/github.js?bust={{current_time}}"></script>
<script src="/static/protocol/jira.js?bust={{current_time}}"></script>
@@ -48,12 +50,39 @@
<div class="span3">
<div class="sidebar-nav">
<ul class="nav nav-list" style="text-shadow: none;" id="current-grefs">
- <li class="nav-header">grefs</li>
+ <li class="nav-header">grefs<span class="pull-right"><a id="new-gref">New</a></span></li>
</ul>
</div>
</div>
<div class="span9" id="gref-content">
</div>
</div>
+
+
+ <div class="modal" id="new-gref-modal">
+ <!-- Modal -->
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+ <h6 id="new-gref-name" class="modal-name" contenteditable>GrefName</h6>
+ </div>
+ <div class="modal-header">
+ <h4 id="new-gref-title" class="modal-title" contenteditable>Gref Title</h4>
+ </div>
+ <div id="new-gref-body" class="modal-body" contenteditable>
+ Gref body
+ </div>
+ <div class="modal-footer">
+ <a id="new-gref-cancel" href="#" class="btn">Close</a>
+ <a id="new-gref-create" href="#" class="btn btn-primary">Save changes</a>
+ </div>
+ <div class="hidden" id="new-gref-protocol">richo@psych0tik.net:github:0.0.0</div>
+ </div>
+ </div>
+ </div><!-- /.modal -->
+
+
+
</body>
</html>
View
0  airship/test/test.js
No changes.
View
54 airship/test/test_gref_validator.js
@@ -0,0 +1,54 @@
+var fs = require("fs");
+var assert = require("assert");
+var groundstation = {
+ "validators": {}
+};
+
+// Oh yes.
+eval(fs.readFileSync("static/airship-gref-validation.js").toString());
+
+describe('groundstation.validators.gref', function(){
+ var gref_validator = groundstation.validators.gref("defaultname", "defaulttitle", "defaultbody");
+ describe('name', function(){
+ var validator = gref_validator.valid_name_p;
+ it('should return false for non strings', function(){
+ assert.equal(false, validator(123));
+ });
+ it('should return false if spaces in name', function(){
+ assert.equal(false, validator("butts lol"));
+ });
+ it ('should return false for empty strings', function(){
+ assert.equal(false, validator(""));
+ });
+ it('should return true for short valid names', function(){
+ assert.equal(true, validator("buttslol"));
+ });
+ it('should return false for the default name', function(){
+ assert.equal(false, validator("defaultname"));
+ });
+ });
+ describe('title', function(){
+ var validator = gref_validator.valid_title_p;
+ it('should return false for non strings', function(){
+ assert.equal(false, validator(123));
+ });
+ it ('should return false for empty strings', function(){
+ assert.equal(false, validator(""));
+ });
+ it('should return false for the default title', function(){
+ assert.equal(false, validator("defaulttitle"));
+ });
+ });
+ describe('body', function(){
+ var validator = gref_validator.valid_body_p;
+ it('should return false for non strings', function(){
+ assert.equal(false, validator(123));
+ });
+ it ('should return false for empty strings', function(){
+ assert.equal(false, validator(""));
+ });
+ it('should return false for the default body', function(){
+ assert.equal(false, validator("defaultbody"));
+ });
+ });
+});
View
20 package.json
@@ -0,0 +1,20 @@
+{
+ "name": "groundstation",
+ "preferGlobal": false,
+ "version": "0.0.0",
+ "author": "Richo Healey <richo@psych0tik.net>",
+ "description": "Test dependencies for groundstation's airship",
+ "contributors": [
+ {
+ "name": "Richo Healey",
+ "email": "richo@psych0tik.net"
+ }
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/richo/groundstation.git"
+ },
+ "dependencies" : {
+ "mocha" : "~1.8.1"
+ }
+}
View
1  script/cibuild
@@ -8,5 +8,6 @@ fi
. env/bin/activate
pip install -r requirements.txt
+npm install
make test
Please sign in to comment.
Something went wrong with that request. Please try again.