, allowing + # to index only the main text of the page. + main = soup.find(role='main') + if main: + soup = main text = ' '.join(get_text(soup)) return text From b3134c5f83d8f22f8bedf62859c82f5b84781b87 Mon Sep 17 00:00:00 2001 From: Justin Stollsteimer Date: Mon, 10 Jun 2013 13:33:42 -0600 Subject: [PATCH 0610/1061] minor improvements to inline table editor --- widgy/static/widgy/css/widgy.scss | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/widgy/static/widgy/css/widgy.scss b/widgy/static/widgy/css/widgy.scss index 6dbd1023a..f3e3b4c47 100644 --- a/widgy/static/widgy/css/widgy.scss +++ b/widgy/static/widgy/css/widgy.scss @@ -4,7 +4,7 @@ /* Mixins -----------------------------------------*/ -$table-cell-min-width: 250px; +$table-cell-min-width: 120px; @mixin button { @include rounded(3px); @@ -593,9 +593,9 @@ li.node_drop_target { } span.title { - font-size: 12px; - display: block; - padding: 0 0 0 15px; +// font-size: 12px; + display: none; +// padding: 0 0 0 15px; } button { @@ -689,9 +689,7 @@ li.node_drop_target { min-height: 20px; > p.drag-row { - background: white; - height: 0px; - padding: 0px; + display: none; } } } @@ -702,15 +700,15 @@ li.node_drop_target { p.drag-row { span.dragHandle { font-size: 13px; - left: 0px; + left: 2px; position: absolute; - top: 5px; + top: 3px; } span.title { - font-size: 12px; - display: block; - padding: 0 0 0 15px; +// font-size: 12px; + display: none; +// padding: 0 0 0 15px; } button { @@ -734,6 +732,10 @@ li.node_drop_target { } } } + + textarea { + height: 40px; + } } } } From feeff752ed6a4b4e68232f78502c4eb7fb4690b8 Mon Sep 17 00:00:00 2001 From: Gavin Wahl Date: Mon, 10 Jun 2013 16:30:50 -0600 Subject: [PATCH 0611/1061] Unicode support for build_url --- widgy/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/widgy/utils.py b/widgy/utils.py index 01a42d672..9cdcdd7e0 100644 --- a/widgy/utils.py +++ b/widgy/utils.py @@ -1,7 +1,6 @@ """ Some utility functions used throughout the project. """ -import urllib from itertools import ifilterfalse import bs4 @@ -10,6 +9,7 @@ from django.utils.safestring import mark_safe from django.template import Context from django.db import models +from django.utils.http import urlencode try: from django.contrib.auth import get_user_model @@ -94,7 +94,7 @@ def update_context(context, dict): def build_url(path, **kwargs): if kwargs: - path += '?' + urllib.urlencode(kwargs) + path += '?' + urlencode(kwargs) return path From ce9b46574f01781bd8b505f556d15142302f631d Mon Sep 17 00:00:00 2001 From: Justin Stollsteimer Date: Tue, 11 Jun 2013 12:08:57 -0600 Subject: [PATCH 0612/1061] form field position and style updates --- widgy/static/widgy/css/widgy.scss | 100 ++++++++++++++++-------------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/widgy/static/widgy/css/widgy.scss b/widgy/static/widgy/css/widgy.scss index f3e3b4c47..2bc623165 100644 --- a/widgy/static/widgy/css/widgy.scss +++ b/widgy/static/widgy/css/widgy.scss @@ -296,6 +296,50 @@ li.node_drop_target { } } + &.image { + div.widget { + div { + + div { + + &.formField { + position: relative; + + label { + display: none; + } + + a { + img { + float: left; + margin-right: 10px; + } + } + + span { + float: left; + + } + + a.related-lookup { + position: absolute; + height: 10px; + left: 53px; + top: 25px; + } + + input { + float: left; + margin-top: 6px; + width: 86%; + } + + } + } + } + } + } + div.widget { background: white; padding: 0px; @@ -352,42 +396,9 @@ li.node_drop_target { &.formField { margin-bottom: 10px; - - &.image { - padding-left: 80px; - position: relative; - - label { - display: none; - } - - img { - left: 5px; - position: relative; - - &.quiet { - left: 10px; - position: absolute; - top: 10px; - } - } - - a.related-lookup { - float: right; - height: 10px; - top: 21px; - } - - input { - float: left; - margin-top: 6px; - width: 86%; - } - - } .cke { - float: right; + float: left; } &.required { @@ -424,6 +435,13 @@ li.node_drop_target { } } } + + input { + &[type='checkbox'], + &[type='radio'] { + margin-left: 0px; + } + } label { clear: left; @@ -985,20 +1003,6 @@ li.shelfItem { -/* Mezzanine Override */ - -input[type="text"], -input[type="password"], -input.vDateField, -input.vTimeField, -.change-list form table .vAutocompleteSearchField, -.change-list form table .vM2MAutocompleteSearchField, -.inline-tabular .vAutocompleteSearchField, -.inline-tabular .vM2MAutocompleteSearchField { - width: 200px; -} - - /*| HTML and Markdown Preview Pane -----------------------------------------*/ From a48a1fe7475c4b4f553719ecffba7ce27a8c45f3 Mon Sep 17 00:00:00 2001 From: Gavin Wahl Date: Tue, 11 Jun 2013 13:33:17 -0600 Subject: [PATCH 0613/1061] Lazily populate changed_anything in the template context This isn't always used by the template, and is expensive to calculate. --- widgy/views/versioning.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/widgy/views/versioning.py b/widgy/views/versioning.py index c4910fe47..4041e06d9 100644 --- a/widgy/views/versioning.py +++ b/widgy/views/versioning.py @@ -62,7 +62,9 @@ def get_context_data(self, **kwargs): kwargs['diff_url'] = diff_url(self.site, self.object.working_copy, self.object.head.root_node) - kwargs['changed_anything'] = self.object.has_changes() + + # lazy because the template doesn't always use it + kwargs['changed_anything'] = lambda: self.object.has_changes() return kwargs From 4ff34d771be29bce24e628b64ca47fb7e301403e Mon Sep 17 00:00:00 2001 From: Rocky Meza Date: Tue, 21 May 2013 14:09:27 -0600 Subject: [PATCH 0614/1061] Static drop targets --- widgy/static/widgy/css/widgy.scss | 30 ++++++++++++++++------ widgy/static/widgy/js/nodes/nodes.js | 38 +++++++++++++++++++--------- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/widgy/static/widgy/css/widgy.scss b/widgy/static/widgy/css/widgy.scss index 2bc623165..ae6763b0c 100644 --- a/widgy/static/widgy/css/widgy.scss +++ b/widgy/static/widgy/css/widgy.scss @@ -166,29 +166,44 @@ textarea { li.node_drop_target { @include rounded(3px); @include transition; - border: 1px dashed lighten($green,10%) !important; + + border: none !important; background: lighten($green,25%) !important; font-size: 1.0em !important; font-style: italic !important; - height: auto !important; - margin: 0.75em 1em !important; - padding: 0.25em !important; + height: 1em !important; + margin: -1em 1em !important; + padding: 0em !important; text-align: center; - + span { display: none; } + &:first-child { + margin-top: 0 !important; + } + + &:last-child, + &.previous.nm { + margin-bottom: 0 !important; + } + &.active { border: 1px dashed $green !important; background: lighten($green,40%) !important; color: darken($green,10%) !important; - margin: 0.25em 1em !important; - + height: 3em !important; + padding: 1em !important; + span { display: block; } } + + &.previous { + background: #FF9900 !important; + } } @mixin widgetNodeChildren { @@ -235,7 +250,6 @@ li.node_drop_target { // drag & drop styles &.being_dragged { @include shadow(#999999, 2px, 2px, 5px); - background-color: blue; margin: 0 !important; position: fixed; } diff --git a/widgy/static/widgy/js/nodes/nodes.js b/widgy/static/widgy/js/nodes/nodes.js index 4ff6d6879..29c4cd36a 100644 --- a/widgy/static/widgy/js/nodes/nodes.js +++ b/widgy/static/widgy/js/nodes/nodes.js @@ -298,23 +298,33 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ }); } - if (this.canAcceptChild(view)) - { - $children.prepend(this.createDropTarget().el); + if (this.canAcceptChild(view)) { + $children.prepend(this.createDropTarget(view).el); this.list.each(function(node_view) { - var drop_target = that.createDropTarget().$el.insertAfter(node_view.el); - - if ( mine && view == node_view ) - drop_target.hide(); - }); + var drop_target = that.createDropTarget(view).$el.insertAfter(node_view.el); + }, this); } }, - createDropTarget: function() { + createDropTarget: function(view) { var drop_target = new DropTargetView(); drop_target.on('dropped', this.dropChildView); this.drop_targets_list.push(drop_target); + if ( this.list.contains(view) ) { + var view_index = this.list.list.indexOf(view), + target_index = this.drop_targets_list.list.length - 1; + if ( view_index == target_index ) { + drop_target.activate().$el + .addClass('previous') + .attr('style', function(i, s) { return (s||'') + ' height: '+ view.$el.height() +'px !important;'; }); + if ( view_index == this.list.list.length - 1 ) + drop_target.$el.addClass('nm'); + } else if ( view_index == target_index - 1 ) { + drop_target.$el.hide(); + } + } + return drop_target.render(); }, @@ -549,9 +559,10 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ 'opacity': 0, 'width': '100%', 'height': '100%', + 'padding': '20px 40px', 'position': 'absolute', - 'top': 0, - 'left': 0 + 'top': '-20px', + 'left': '-40px' }); this.$el.prepend($pointerEventsCatcher); @@ -560,10 +571,13 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ activate: function(event) { this.$el.addClass('active'); + return this; }, deactivate: function(event) { - this.$el.removeClass('active'); + this.$el.removeClass('active') + .css('height', ''); + return this; } }); From 7c29da869d089f6c4cf2c7cb2ed0127a499a2177 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Mon, 10 Jun 2013 16:51:10 -0600 Subject: [PATCH 0615/1061] Move History Link from the page to the Widgy field --- .../templates/widgy/page_builder/widgypage_change_form.html | 3 --- widgy/contrib/widgy_mezzanine/admin.py | 2 -- widgy/forms.py | 1 + widgy/templates/widgy/versioned_widgy_field_base.html | 1 + 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/widgy/contrib/page_builder/templates/widgy/page_builder/widgypage_change_form.html b/widgy/contrib/page_builder/templates/widgy/page_builder/widgypage_change_form.html index 9c9ec0cd5..34e9f4bdf 100644 --- a/widgy/contrib/page_builder/templates/widgy/page_builder/widgypage_change_form.html +++ b/widgy/contrib/page_builder/templates/widgy/page_builder/widgypage_change_form.html @@ -2,9 +2,6 @@ {% load i18n %} {% block object-tools %}
    - {% if history_url %} -
  • {% trans "History" %}
  • - {% endif %} {% if has_absolute_url %}
  • {% trans "View on site" %}
  • {% endif%} diff --git a/widgy/contrib/widgy_mezzanine/admin.py b/widgy/contrib/widgy_mezzanine/admin.py index bae918f48..16f1f4931 100644 --- a/widgy/contrib/widgy_mezzanine/admin.py +++ b/widgy/contrib/widgy_mezzanine/admin.py @@ -52,8 +52,6 @@ def render_change_form(self, request, context, *args, **kwargs): # we are rendering a change form obj = context['original'] site = self.get_site() - context['history_url'] = site.reverse(site.history_view, - kwargs={'pk': obj.root_node.pk}) return super(WidgyPageAdmin, self).render_change_form(request, context, *args, **kwargs) diff --git a/widgy/forms.py b/widgy/forms.py index fc498f791..50b3cc363 100644 --- a/widgy/forms.py +++ b/widgy/forms.py @@ -142,6 +142,7 @@ def render(self, name, value, attrs=None): context = { 'commit_url': self.site.reverse(self.site.commit_view, kwargs={'pk': value}), 'reset_url': self.site.reverse(self.site.reset_view, kwargs={'pk': value}), + 'history_url': self.site.reverse(self.site.history_view, kwargs={'pk': value}), } return super(VersionedWidgyWidget, self).render(name, value, attrs, context) diff --git a/widgy/templates/widgy/versioned_widgy_field_base.html b/widgy/templates/widgy/versioned_widgy_field_base.html index 229bc8590..d2a757190 100644 --- a/widgy/templates/widgy/versioned_widgy_field_base.html +++ b/widgy/templates/widgy/versioned_widgy_field_base.html @@ -9,6 +9,7 @@ {% block widgy_tools %}
  • {% trans "Commit" %}
  • {% trans "Reset" %}
  • +
  • {% trans "History" %}
  • {% endblock %}
{% compress js %} From 72f78fb7932a9c120b61532ec9a5b2fed04c600e Mon Sep 17 00:00:00 2001 From: Rocky Meza Date: Fri, 17 May 2013 16:49:53 -0600 Subject: [PATCH 0616/1061] Made TabbedContainer children editable. --- widgy/contrib/page_builder/models.py | 5 +- widgy/static/widgy/css/widgy.scss | 9 +- widgy/static/widgy/css/widgy_common.scss | 5 + .../widgy/js/components/tabbed/component.js | 52 ++++++--- widgy/static/widgy/js/nodes/nodes.js | 31 +++-- widgy/static/widgy/mixins/tabbed.admin.scss | 107 +++++++++++------- widgy/templates/widgy/preview.html | 6 +- 7 files changed, 142 insertions(+), 73 deletions(-) diff --git a/widgy/contrib/page_builder/models.py b/widgy/contrib/page_builder/models.py index b66484cbe..57f5debf2 100644 --- a/widgy/contrib/page_builder/models.py +++ b/widgy/contrib/page_builder/models.py @@ -13,7 +13,7 @@ from widgy.models import Content from widgy.models.mixins import ( StrictDefaultChildrenMixin, InvisibleMixin, TitleDisplayNameMixin, - DisplayNameMixin, + DisplayNameMixin, TabbedContainer ) from widgy.models.links import LinkField, LinkFormField, LinkFormMixin from widgy.db.fields import WidgyField @@ -191,7 +191,8 @@ class Meta: @widgy.register -class Tabs(Accordion): +class Tabs(TabbedContainer, Accordion): + class Meta: proxy = True verbose_name = _('tabs') diff --git a/widgy/static/widgy/css/widgy.scss b/widgy/static/widgy/css/widgy.scss index ae6763b0c..d0c4b5a7a 100644 --- a/widgy/static/widgy/css/widgy.scss +++ b/widgy/static/widgy/css/widgy.scss @@ -385,10 +385,11 @@ li.node_drop_target { top: 3px; right: 3px; z-index: 10; - - &.edit { - right: 74px; - } + } + + // If you nest this in the previous statement the selector becomes .edit.edit, which is too specific. + button.edit { + right: 74px; } .dragHandle { diff --git a/widgy/static/widgy/css/widgy_common.scss b/widgy/static/widgy/css/widgy_common.scss index 65c836791..6455e187f 100644 --- a/widgy/static/widgy/css/widgy_common.scss +++ b/widgy/static/widgy/css/widgy_common.scss @@ -122,6 +122,11 @@ table { display: table; width: 100% !important; } } +@mixin box-sizing($box-sizing) { + box-sizing: $box-sizing; + -moz-box-sizing: $box-sizing; +} + /* Variables -----------------------------------------------*/ diff --git a/widgy/static/widgy/js/components/tabbed/component.js b/widgy/static/widgy/js/components/tabbed/component.js index 2efa2576f..182c37497 100644 --- a/widgy/static/widgy/js/components/tabbed/component.js +++ b/widgy/static/widgy/js/components/tabbed/component.js @@ -5,6 +5,14 @@ define([ 'underscore', 'widgy.backbone', 'components/widget/component' ], functi 'click .tabbed > .widget > .node_children .drag-row': 'showTabClick' }), + initialize: function() { + widget.View.prototype.initialize.apply(this, arguments); + + _.bindAll(this, + 'stealThingsFromChild' + ); + }, + showTabClick: function(event) { if ( $(event.target).is('.preview') ) return; @@ -42,28 +50,40 @@ define([ 'underscore', 'widgy.backbone', 'components/widget/component' ], functi }); }, - renderPromise: function() { - return widget.View.prototype.renderPromise.apply(this, arguments).then(function(view) { + renderChildren: function() { + var parent = this; + return widget.View.prototype.renderChildren.apply(this, arguments).then(function() { // Show the first tab on first render. // Does this always work? - view.showTab(view.list.list[0]); + if ( parent.list.list.length ) + parent.showTab(parent.list.list[0]); - return view; + return parent; }); }, - addChildPromise: function() { - var parent = this; - return widget.View.prototype.addChildPromise.apply(this, arguments).then(function(child_view) { - if ( child_view.hasShelf() ) { - console.log('addChildPromise'); - child_view.shelf.$el.hide().appendTo(parent.$current); - } - child_view.$preview.hide().appendTo(parent.$current); - child_view.$children.hide().appendTo(parent.$current); - - return child_view; - }); + stealThingsFromChild: function(child_view) { + if ( child_view.hasShelf() ) { + child_view.shelf.$el.hide().appendTo(this.$current); + } + child_view.$preview.hide().appendTo(this.$current); + child_view.$children.hide().appendTo(this.$current); + + this.showTab(child_view); + + return child_view; + }, + + listenToChildEvents: function(child_view) { + widget.View.prototype.listenToChildEvents.apply(this, arguments); + this.listenTo(child_view, 'rendered', this.stealThingsFromChild); + return child_view; + }, + + createDropTarget: function(view) { + var drop_target = widget.View.prototype.createDropTarget.apply(this, arguments); + drop_target.$el.css('height', ''); + return drop_target; } }); diff --git a/widgy/static/widgy/js/nodes/nodes.js b/widgy/static/widgy/js/nodes/nodes.js index 29c4cd36a..8f4e3a7bf 100644 --- a/widgy/static/widgy/js/nodes/nodes.js +++ b/widgy/static/widgy/js/nodes/nodes.js @@ -55,6 +55,7 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ 'rerender', 'popOut', 'popIn', + 'listenToChildEvents', 'closeSubwindow' ); @@ -112,6 +113,14 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ }); }, + listenToChildEvents: function(child_view) { + this + .listenTo(child_view, 'startDrag', this.startDrag) + .listenTo(child_view, 'stopDrag', this.stopDrag); + + return child_view; + }, + addChildPromise: function(node, collection, options) { var parent = this; @@ -121,16 +130,14 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ }); return node.ready(function(model) { - var node_view = new model.component.View({ + return new model.component.View({ model: node, parent: parent, app: parent.app }); - - parent - .listenTo(node_view, 'startDrag', parent.startDrag) - .listenTo(node_view, 'stopDrag', parent.stopDrag); - + }) + .then(this.listenToChildEvents) + .then(function(node_view) { parent.app.node_view_list.push(node_view); if ( options && options.index ) { parent.list.list.splice(options.index, 0, node_view); @@ -388,9 +395,17 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ }, rerender: function() { + this.cleanUp(); this.renderPromise().done(); }, + cleanUp: function() { + this.$children.remove(); + this.$preview.remove(); + if (this.shelf) + this.shelf.close(); + }, + renderNode: function() { return DraggableView.prototype.renderPromise.apply(this, arguments).then(function(view) { view.$children = view.$(' > .widget > .node_children'); @@ -410,6 +425,8 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ if ( view.app.compatibility_data ) view.app.updateCompatibility(view.app.compatibility_data); + view.trigger('rendered', view); + return view; }); }); @@ -433,8 +450,6 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ renderShelf: function() { console.log('renderShelf'); - if (this.shelf) - this.shelf.remove(); var shelf = this.shelf = new shelves.ShelfView({ collection: new shelves.ShelfCollection({ diff --git a/widgy/static/widgy/mixins/tabbed.admin.scss b/widgy/static/widgy/mixins/tabbed.admin.scss index 16fe24b48..1a8be20b5 100644 --- a/widgy/static/widgy/mixins/tabbed.admin.scss +++ b/widgy/static/widgy/mixins/tabbed.admin.scss @@ -1,10 +1,4 @@ -@mixin rounded($radius) { - border-radius: $radius !important; - -o-border-radius: $radius !important; - -ms-border-radius: $radius !important; - -moz-border-radius: $radius !important; - -webkit-border-radius: $radius !important; -} +@import "widgy_common"; /* div.widgy div.editor section.node div.widget > ul.nodeChildren { @@ -12,60 +6,93 @@ div.widgy div.editor section.node div.widget > ul.nodeChildren { } */ -div.widgy div.editor section.tabbed > div.widget > ul.nodeChildren { - float: left !important; - width: 63% !important; +div.widgy div.editor .tabbed { + > .widget { + @include clearfix; + } +} + +div.widgy div.editor .tabbed > div.widget > ul.nodeChildren { + @include box-sizing(border-box); > li { @include rounded(4px 4px 0px 0px); - background: #eeeeee !important; - border-color: #cccccc; - border-width: 1px 1px 0px 1px; - color: #999999; display: inline-block; float: left; margin: 0 0 0 .5em !important; padding: 0px 5px !important; position: relative; top: 2px; - z-index: 10; - .widget { - background: transparent !important; + &.node { + background: #eeeeee !important; + border-color: #cccccc; + border-width: 1px 1px 0px 1px; + color: #999999; - .drag-row { - @include rounded(0px); - background: transparent !important; + &.active { + background-color: #fafafa !important; + border-bottom: 2px solid #fafafa; + color: #555555; - span.title { - color: inherit; + .edit, .delete { + display: inline; } + } - .edit, // functionality for editing the tab doesn't exist yet. - .delete span { - display: none; - } + .widget { + background: transparent !important; - .preview { - background: transparent; - display: inline; + .drag-row { + @include rounded(0px); + background: transparent !important; + + span.title { + color: inherit; + } + + .edit span, // functionality for editing the tab doesn't exist yet. + .delete span { + display: none; + } + + button.edit, + button.delete { + position: static !important; + } + + .preview { + background: transparent; + display: inline; + } } } - } - .preview, - .nodeChildren { - display: none; - } + .preview, + .nodeChildren { + display: none; + } + + &:hover { + background-color: white !important; + } - &:hover { - background-color: white !important; + .edit, .delete { + display: none; + } } - &.active { - background-color: #fafafa !important; - border-bottom: 2px solid #fafafa; - color: #555555; + &.node_drop_target { + height: 24px !important; + width: 50px; + + &.active { + width: 100px; + + span { + display: none; + } + } } } } diff --git a/widgy/templates/widgy/preview.html b/widgy/templates/widgy/preview.html index 2be6fe3af..b16546407 100644 --- a/widgy/templates/widgy/preview.html +++ b/widgy/templates/widgy/preview.html @@ -10,12 +10,12 @@ {{ self.display_name|capfirst }} {% endblock %} - {% if self.deletable %} - - {% endif %} {% if self.editable %} {% endif %} + {% if self.deletable %} + + {% endif %} {% if self.pop_out %} {% trans "pop out" %} {% endif %} From 7bd3d94c87a9ebb5362fe8d7a04890221bc046ba Mon Sep 17 00:00:00 2001 From: Rocky Meza Date: Mon, 20 May 2013 11:38:33 -0600 Subject: [PATCH 0617/1061] Optional dragTimeout for DraggableView. This allows us to delay dragging for TabbedContainer children. --- .../widgy/js/components/tabbed/component.js | 5 +++-- widgy/static/widgy/js/nodes/base.js | 19 +++++++++++++++++-- widgy/static/widgy/js/nodes/nodes.js | 6 +++--- widgy/static/widgy/js/shelves/shelves.js | 6 +----- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/widgy/static/widgy/js/components/tabbed/component.js b/widgy/static/widgy/js/components/tabbed/component.js index 182c37497..56891343f 100644 --- a/widgy/static/widgy/js/components/tabbed/component.js +++ b/widgy/static/widgy/js/components/tabbed/component.js @@ -74,9 +74,10 @@ define([ 'underscore', 'widgy.backbone', 'components/widget/component' ], functi return child_view; }, - listenToChildEvents: function(child_view) { - widget.View.prototype.listenToChildEvents.apply(this, arguments); + prepareChild: function(child_view) { + widget.View.prototype.prepareChild.apply(this, arguments); this.listenTo(child_view, 'rendered', this.stealThingsFromChild); + child_view.dragTimeout = 100; return child_view; }, diff --git a/widgy/static/widgy/js/nodes/base.js b/widgy/static/widgy/js/nodes/base.js index f7548cfcc..6c8d7848d 100644 --- a/widgy/static/widgy/js/nodes/base.js +++ b/widgy/static/widgy/js/nodes/base.js @@ -15,9 +15,10 @@ define(['jquery', 'underscore', 'widgy.backbone'], function( var DraggablewView = Backbone.View.extend({ tagName: 'li', className: 'node', + dragTimeout: 0, events: Backbone.extendEvents(Backbone.View, { - 'mousedown .drag-row': 'startBeingDragged' + 'mousedown .drag-row': 'onMouseDown' }), initialize: function(options) { @@ -53,7 +54,7 @@ define(['jquery', 'underscore', 'widgy.backbone'], function( }); }, - startBeingDragged: function(event) { + onMouseDown: function(event) { event.preventDefault(); event.stopPropagation(); @@ -64,6 +65,20 @@ define(['jquery', 'underscore', 'widgy.backbone'], function( if ( ! this.app.ready() ) return; + var _continue = true; + + this.$el.on('mouseup.draggable-timeout', function() { + _continue = false; + }); + + setTimeout(_.bind(function() { + this.$el.off('.draggable-timeout'); + if ( _continue ) + this.startBeingDragged(event); + }, this), this.dragTimeout); + }, + + startBeingDragged: function(event) { // Store the mouse offset in this container for followMouse to use. We // need to get this before `this.app.startDrag`, otherwise the drop // targets screw everything up. diff --git a/widgy/static/widgy/js/nodes/nodes.js b/widgy/static/widgy/js/nodes/nodes.js index 8f4e3a7bf..ad44bc8b5 100644 --- a/widgy/static/widgy/js/nodes/nodes.js +++ b/widgy/static/widgy/js/nodes/nodes.js @@ -55,7 +55,7 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ 'rerender', 'popOut', 'popIn', - 'listenToChildEvents', + 'prepareChild', 'closeSubwindow' ); @@ -113,7 +113,7 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ }); }, - listenToChildEvents: function(child_view) { + prepareChild: function(child_view) { this .listenTo(child_view, 'startDrag', this.startDrag) .listenTo(child_view, 'stopDrag', this.stopDrag); @@ -136,7 +136,7 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ app: parent.app }); }) - .then(this.listenToChildEvents) + .then(this.prepareChild) .then(function(node_view) { parent.app.node_view_list.push(node_view); if ( options && options.index ) { diff --git a/widgy/static/widgy/js/shelves/shelves.js b/widgy/static/widgy/js/shelves/shelves.js index 8fc05a942..fb9c39ef4 100644 --- a/widgy/static/widgy/js/shelves/shelves.js +++ b/widgy/static/widgy/js/shelves/shelves.js @@ -141,7 +141,7 @@ define([ 'exports', 'underscore', 'widgy.backbone', 'nodes/base', // Override the DraggableView events, I want the whole thing draggable, not // just the drag handle. events: Backbone.extendEvents(DraggableView, { - 'mousedown': 'startBeingDragged' + 'mousedown': 'onMouseDown' }), canAcceptParent: function(parent) { @@ -151,10 +151,6 @@ define([ 'exports', 'underscore', 'widgy.backbone', 'nodes/base', startBeingDragged: function(event) { DraggableView.prototype.startBeingDragged.apply(this, arguments); - // only on a left click. - if ( event.which !== 1 ) - return; - var placeholder = this.placeholder = $('
  •  
  • ').css({ width: this.$el.width() }); From 497dbf381cd181d2c5d2d8bb58bebde48e83b22e Mon Sep 17 00:00:00 2001 From: Rocky Meza Date: Mon, 10 Jun 2013 11:00:47 -0600 Subject: [PATCH 0618/1061] Got JS tests running again. - remove depedencies on node versions of modules - switched to amdjs/backbone and underscore - upgraded to backbone 1.0 --- js_tests/package.json | 4 - js_tests/tests/setup.js | 7 +- js_tests/tests/test_node.js | 2 +- widgy/static/widgy/js/lib/backbone.js | 1371 +++++++++++----------- widgy/static/widgy/js/lib/underscore.js | 71 +- widgy/static/widgy/js/main.js | 19 +- widgy/static/widgy/js/nodes/models.js | 6 +- widgy/static/widgy/js/shelves/shelves.js | 2 +- widgy/static/widgy/js/widgy.backbone.js | 2 +- 9 files changed, 761 insertions(+), 723 deletions(-) diff --git a/js_tests/package.json b/js_tests/package.json index d0b64a816..18711126f 100644 --- a/js_tests/package.json +++ b/js_tests/package.json @@ -7,10 +7,6 @@ "test": "tests" }, "dependencies": { - "jquery": ">=1.8.3", - "mustache": ">=0.7.2", - "underscore": "1.4.4", - "backbone": ">=0.9.10", "jsdom": "~0.3.4", "requirejs": "~2.1.4", "chai": "~1.4.2" diff --git a/js_tests/tests/setup.js b/js_tests/tests/setup.js index 2b59180f1..6ebf1cc81 100644 --- a/js_tests/tests/setup.js +++ b/js_tests/tests/setup.js @@ -10,13 +10,16 @@ global.window = global.window = global.document.createWindow(); requirejs.config({ baseUrl: path.join(__dirname, "../../widgy/static/widgy/js/"), paths: { + 'jquery': './lib/jquery', + 'underscore': './lib/underscore', + 'backbone': './lib/backbone', 'text': 'require/text' } }); // Backbone expects window.jQuery to be set. -var Backbone = requirejs('backbone'), - jQuery = requirejs('jquery'); +var Backbone = requirejs('lib/backbone'), + jQuery = requirejs('lib/jquery'); Backbone.$ = jQuery; diff --git a/js_tests/tests/test_node.js b/js_tests/tests/test_node.js index 1587a15a6..f67321430 100644 --- a/js_tests/tests/test_node.js +++ b/js_tests/tests/test_node.js @@ -177,7 +177,7 @@ describe('NodeCollection', function() { assert.equal(node1.children.length, 1); return Q.all([node1.ready(), node2.ready()]).then(function() { - x.update2([ + x.set2([ { url: 1, children: [ diff --git a/widgy/static/widgy/js/lib/backbone.js b/widgy/static/widgy/js/lib/backbone.js index 7391faac3..8b4f60c45 100644 --- a/widgy/static/widgy/js/lib/backbone.js +++ b/widgy/static/widgy/js/lib/backbone.js @@ -1,47 +1,46 @@ -// Backbone.js 0.9.9 +// Backbone.js 1.0.0 -// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc. // Backbone may be freely distributed under the MIT license. // For all details and documentation: // http://backbonejs.org -(function(){ +(function(root, factory) { + // Set up Backbone appropriately for the environment. + if (typeof exports !== 'undefined') { + // Node/CommonJS, no need for jQuery in that case. + factory(root, exports, require('underscore')); + } else if (typeof define === 'function' && define.amd) { + // AMD + define(['underscore', 'jquery', 'exports'], function(_, $, exports) { + // Export global even in AMD case in case this script is loaded with + // others that may still expect a global Backbone. + root.Backbone = factory(root, exports, _, $); + }); + } else { + // Browser globals + root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); + } +}(this, function(root, Backbone, _, $) { // Initial Setup // ------------- - // Save a reference to the global object (`window` in the browser, `exports` - // on the server). - var root = this; - // Save the previous value of the `Backbone` variable, so that it can be // restored later on, if `noConflict` is used. var previousBackbone = root.Backbone; - // Create a local reference to array methods. + // Create local references to array methods we'll want to use later. var array = []; var push = array.push; var slice = array.slice; var splice = array.splice; - // The top-level namespace. All public Backbone classes and modules will - // be attached to this. Exported for both CommonJS and the browser. - var Backbone; - if (typeof exports !== 'undefined') { - Backbone = exports; - } else { - Backbone = root.Backbone = {}; - } - // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '0.9.9'; - - // Require Underscore, if we're on the server, and it's not already present. - var _ = root._; - if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); + Backbone.VERSION = '1.0.0'; // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable. - Backbone.$ = root.jQuery || root.Zepto || root.ender; + Backbone.$ = $; // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable // to its previous owner. Returns a reference to this Backbone object. @@ -64,45 +63,6 @@ // Backbone.Events // --------------- - // Regular expression used to split event strings. - var eventSplitter = /\s+/; - - // Implement fancy features of the Events API such as multiple event - // names `"change blur"` and jQuery-style event maps `{change: action}` - // in terms of the existing API. - var eventsApi = function(obj, action, name, rest) { - if (!name) return true; - if (typeof name === 'object') { - for (var key in name) { - obj[action].apply(obj, [key, name[key]].concat(rest)); - } - } else if (eventSplitter.test(name)) { - var names = name.split(eventSplitter); - for (var i = 0, l = names.length; i < l; i++) { - obj[action].apply(obj, [names[i]].concat(rest)); - } - } else { - return true; - } - }; - - // Optimized internal dispatch function for triggering events. Tries to - // keep the usual cases speedy (most Backbone events have 3 arguments). - var triggerEvents = function(obj, events, args) { - var ev, i = -1, l = events.length; - switch (args.length) { - case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); - return; - case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]); - return; - case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]); - return; - case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]); - return; - default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); - } - }; - // A module that can be mixed in to *any object* in order to provide it with // custom events. You may bind with `on` or remove with `off` callback // functions to an event; `trigger`-ing an event fires all callbacks in @@ -115,37 +75,35 @@ // var Events = Backbone.Events = { - // Bind one or more space separated events, or an events map, - // to a `callback` function. Passing `"all"` will bind the callback to - // all events fired. + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. on: function(name, callback, context) { - if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this; + if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; this._events || (this._events = {}); - var list = this._events[name] || (this._events[name] = []); - list.push({callback: callback, context: context, ctx: context || this}); + var events = this._events[name] || (this._events[name] = []); + events.push({callback: callback, context: context, ctx: context || this}); return this; }, - // Bind events to only be triggered a single time. After the first time + // Bind an event to only be triggered a single time. After the first time // the callback is invoked, it will be removed. once: function(name, callback, context) { - if (!(eventsApi(this, 'once', name, [callback, context]) && callback)) return this; + if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; var self = this; var once = _.once(function() { self.off(name, once); callback.apply(this, arguments); }); once._callback = callback; - this.on(name, once, context); - return this; + return this.on(name, once, context); }, // Remove one or many callbacks. If `context` is null, removes all // callbacks with that function. If `callback` is null, removes all - // callbacks for the event. If `events` is null, removes all bound + // callbacks for the event. If `name` is null, removes all bound // callbacks for all events. off: function(name, callback, context) { - var list, ev, events, names, i, l, j, k; + var retain, ev, events, names, i, l, j, k; if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; if (!name && !callback && !context) { this._events = {}; @@ -155,18 +113,18 @@ names = name ? [name] : _.keys(this._events); for (i = 0, l = names.length; i < l; i++) { name = names[i]; - if (list = this._events[name]) { - events = []; + if (events = this._events[name]) { + this._events[name] = retain = []; if (callback || context) { - for (j = 0, k = list.length; j < k; j++) { - ev = list[j]; - if ((callback && callback !== (ev.callback._callback || ev.callback)) || + for (j = 0, k = events.length; j < k; j++) { + ev = events[j]; + if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || (context && context !== ev.context)) { - events.push(ev); + retain.push(ev); } } } - this._events[name] = events; + if (!retain.length) delete this._events[name]; } } @@ -183,39 +141,87 @@ if (!eventsApi(this, 'trigger', name, args)) return this; var events = this._events[name]; var allEvents = this._events.all; - if (events) triggerEvents(this, events, args); - if (allEvents) triggerEvents(this, allEvents, arguments); - return this; - }, - - // An inversion-of-control version of `on`. Tell *this* object to listen to - // an event in another object ... keeping track of what it's listening to. - listenTo: function(object, events, callback) { - var listeners = this._listeners || (this._listeners = {}); - var id = object._listenerId || (object._listenerId = _.uniqueId('l')); - listeners[id] = object; - object.on(events, callback || this, this); + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, arguments); return this; }, // Tell this object to stop listening to either specific events ... or // to every object it's currently listening to. - stopListening: function(object, events, callback) { + stopListening: function(obj, name, callback) { var listeners = this._listeners; - if (!listeners) return; - if (object) { - object.off(events, callback, this); - if (!events && !callback) delete listeners[object._listenerId]; - } else { - for (var id in listeners) { - listeners[id].off(null, null, this); - } - this._listeners = {}; + if (!listeners) return this; + var deleteListener = !name && !callback; + if (typeof name === 'object') callback = this; + if (obj) (listeners = {})[obj._listenerId] = obj; + for (var id in listeners) { + listeners[id].off(name, callback, this); + if (deleteListener) delete this._listeners[id]; } return this; } + + }; + + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // Implement fancy features of the Events API such as multiple event + // names `"change blur"` and jQuery-style event maps `{change: action}` + // in terms of the existing API. + var eventsApi = function(obj, action, name, rest) { + if (!name) return true; + + // Handle event maps. + if (typeof name === 'object') { + for (var key in name) { + obj[action].apply(obj, [key, name[key]].concat(rest)); + } + return false; + } + + // Handle space separated event names. + if (eventSplitter.test(name)) { + var names = name.split(eventSplitter); + for (var i = 0, l = names.length; i < l; i++) { + obj[action].apply(obj, [names[i]].concat(rest)); + } + return false; + } + + return true; + }; + + // A difficult-to-believe, but optimized internal dispatch function for + // triggering events. Tries to keep the usual cases speedy (most internal + // Backbone events have 3 arguments). + var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + switch (args.length) { + case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; + case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; + case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; + case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); + } }; + var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; + + // Inversion-of-control versions of `on` and `once`. Tell *this* object to + // listen to an event in another object ... keeping track of what it's + // listening to. + _.each(listenMethods, function(implementation, method) { + Events[method] = function(obj, name, callback) { + var listeners = this._listeners || (this._listeners = {}); + var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); + listeners[id] = obj; + if (typeof name === 'object') callback = this; + obj[implementation](name, callback, this); + return this; + }; + }); + // Aliases for backwards compatibility. Events.bind = Events.on; Events.unbind = Events.off; @@ -227,30 +233,41 @@ // Backbone.Model // -------------- - // Create a new model, with defined attributes. A client id (`cid`) + // Backbone **Models** are the basic data object in the framework -- + // frequently representing a row in a table in a database on your server. + // A discrete chunk of data and a bunch of useful, related methods for + // performing computations and transformations on that data. + + // Create a new model with the specified attributes. A client id (`cid`) // is automatically generated and assigned for you. var Model = Backbone.Model = function(attributes, options) { var defaults; var attrs = attributes || {}; + options || (options = {}); this.cid = _.uniqueId('c'); - this.changed = {}; this.attributes = {}; - this._changes = []; - if (options && options.collection) this.collection = options.collection; - if (options && options.parse) attrs = this.parse(attrs); - if (defaults = _.result(this, 'defaults')) _.defaults(attrs, defaults); - this.set(attrs, {silent: true}); - this._currentAttributes = _.clone(this.attributes); - this._previousAttributes = _.clone(this.attributes); + _.extend(this, _.pick(options, modelOptions)); + if (options.parse) attrs = this.parse(attrs, options) || {}; + if (defaults = _.result(this, 'defaults')) { + attrs = _.defaults({}, attrs, defaults); + } + this.set(attrs, options); + this.changed = {}; this.initialize.apply(this, arguments); }; + // A list of options to be attached directly to the model, if provided. + var modelOptions = ['url', 'urlRoot', 'collection']; + // Attach all inheritable methods to the Model prototype. _.extend(Model.prototype, Events, { // A hash of attributes whose current and previous value differ. changed: null, + // The value returned during the last failed validation. + validationError: null, + // The default name for the JSON `id` attribute is `"id"`. MongoDB and // CouchDB users may want to set this to `"_id"`. idAttribute: 'id', @@ -264,7 +281,8 @@ return _.clone(this.attributes); }, - // Proxy `Backbone.sync` by default. + // Proxy `Backbone.sync` by default -- but override this if you need + // custom syncing semantics for *this* particular model. sync: function() { return Backbone.sync.apply(this, arguments); }, @@ -285,76 +303,140 @@ return this.get(attr) != null; }, - // Set a hash of model attributes on the object, firing `"change"` unless - // you choose to silence it. + // Set a hash of model attributes on the object, firing `"change"`. This is + // the core primitive operation of a model, updating the data and notifying + // anyone who needs to know about the change in state. The heart of the beast. set: function(key, val, options) { - var attr, attrs; + var attr, attrs, unset, changes, silent, changing, prev, current; if (key == null) return this; // Handle both `"key", value` and `{key: value}` -style arguments. - if (_.isObject(key)) { + if (typeof key === 'object') { attrs = key; options = val; } else { (attrs = {})[key] = val; } - // Extract attributes and options. - var silent = options && options.silent; - var unset = options && options.unset; + options || (options = {}); // Run validation. if (!this._validate(attrs, options)) return false; + // Extract attributes and options. + unset = options.unset; + silent = options.silent; + changes = []; + changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = _.clone(this.attributes); + this.changed = {}; + } + current = this.attributes, prev = this._previousAttributes; + // Check for changes of `id`. if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; - var now = this.attributes; - - // For each `set` attribute... + // For each `set` attribute, update or delete the current value. for (attr in attrs) { val = attrs[attr]; - - // Update or delete the current value, and track the change. - unset ? delete now[attr] : now[attr] = val; - this._changes.push(attr, val); + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { + this.changed[attr] = val; + } else { + delete this.changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; } - // Signal that the model's state has potentially changed, and we need - // to recompute the actual changes. - this._hasComputed = false; + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = true; + for (var i = 0, l = changes.length; i < l; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); + } + } - // Fire the `"change"` events. - if (!silent) this.change(options); + // You might be wondering why there's a `while` loop here. Changes can + // be recursively nested within `"change"` events. + if (changing) return this; + if (!silent) { + while (this._pending) { + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; return this; }, - // Remove an attribute from the model, firing `"change"` unless you choose - // to silence it. `unset` is a noop if the attribute doesn't exist. + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. unset: function(attr, options) { return this.set(attr, void 0, _.extend({}, options, {unset: true})); }, - // Clear all attributes on the model, firing `"change"` unless you choose - // to silence it. + // Clear all attributes on the model, firing `"change"`. clear: function(options) { var attrs = {}; for (var key in this.attributes) attrs[key] = void 0; return this.set(attrs, _.extend({}, options, {unset: true})); }, + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var val, changed = false; + var old = this._changing ? this._previousAttributes : this.attributes; + for (var attr in diff) { + if (_.isEqual(old[attr], (val = diff[attr]))) continue; + (changed || (changed = {}))[attr] = val; + } + return changed; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + // Fetch the model from the server. If the server's representation of the - // model differs from its current attributes, they will be overriden, + // model differs from its current attributes, they will be overridden, // triggering a `"change"` event. fetch: function(options) { options = options ? _.clone(options) : {}; if (options.parse === void 0) options.parse = true; var model = this; var success = options.success; - options.success = function(resp, status, xhr) { - if (!model.set(model.parse(resp), options)) return false; + options.success = function(resp) { + if (!model.set(model.parse(resp, options), options)) return false; if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); }; + wrapError(this, options); return this.sync('read', this, options); }, @@ -362,55 +444,53 @@ // If the server returns an attributes hash that differs, the model's // state will be `set` again. save: function(key, val, options) { - var attrs, current, done; + var attrs, method, xhr, attributes = this.attributes; // Handle both `"key", value` and `{key: value}` -style arguments. - if (key == null || _.isObject(key)) { + if (key == null || typeof key === 'object') { attrs = key; options = val; - } else if (key != null) { + } else { (attrs = {})[key] = val; } - options = options ? _.clone(options) : {}; - // If we're "wait"-ing to set changed attributes, validate early. - if (options.wait) { - if (attrs && !this._validate(attrs, options)) return false; - current = _.clone(this.attributes); - } + // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. + if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; - // Regular saves `set` attributes before persisting to the server. - var silentOptions = _.extend({}, options, {silent: true}); - if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) { - return false; - } + options = _.extend({validate: true}, options); // Do not persist invalid models. - if (!attrs && !this._validate(null, options)) return false; + if (!this._validate(attrs, options)) return false; + + // Set temporary attributes if `{wait: true}`. + if (attrs && options.wait) { + this.attributes = _.extend({}, attributes, attrs); + } // After a successful server-side save, the client is (optionally) // updated with the server-side state. + if (options.parse === void 0) options.parse = true; var model = this; var success = options.success; - options.success = function(resp, status, xhr) { - done = true; - var serverAttrs = model.parse(resp); + options.success = function(resp) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = model.parse(resp, options); if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); - if (!model.set(serverAttrs, options)) return false; + if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { + return false; + } if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); }; + wrapError(this, options); - // Finish configuring and sending the Ajax request. - var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); - if (method == 'patch') options.attrs = attrs; - var xhr = this.sync(method, this, options); + method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); + if (method === 'patch') options.attrs = attrs; + xhr = this.sync(method, this, options); - // When using `wait`, reset attributes to original values unless - // `success` has been called already. - if (!done && options.wait) { - this.clear(silentOptions); - this.set(current, silentOptions); - } + // Restore attributes. + if (attrs && options.wait) this.attributes = attributes; return xhr; }, @@ -430,12 +510,14 @@ options.success = function(resp) { if (options.wait || model.isNew()) destroy(); if (success) success(model, resp, options); + if (!model.isNew()) model.trigger('sync', model, resp, options); }; if (this.isNew()) { options.success(); return false; } + wrapError(this, options); var xhr = this.sync('delete', this, options); if (!options.wait) destroy(); @@ -453,7 +535,7 @@ // **parse** converts a response into the hash of attributes to be `set` on // the model. The default implementation is just to pass the response along. - parse: function(resp) { + parse: function(resp, options) { return resp; }, @@ -467,128 +549,52 @@ return this.id == null; }, - // Call this method to manually fire a `"change"` event for this model and - // a `"change:attribute"` event for each changed attribute. - // Calling this will cause all objects observing the model to update. - change: function(options) { - var changing = this._changing; - this._changing = true; - - // Generate the changes to be triggered on the model. - var triggers = this._computeChanges(true); - - this._pending = !!triggers.length; - - for (var i = triggers.length - 2; i >= 0; i -= 2) { - this.trigger('change:' + triggers[i], this, triggers[i + 1], options); - } - - if (changing) return this; - - // Trigger a `change` while there have been changes. - while (this._pending) { - this._pending = false; - this.trigger('change', this, options); - this._previousAttributes = _.clone(this.attributes); - } - - this._changing = false; - return this; - }, - - // Determine if the model has changed since the last `"change"` event. - // If you specify an attribute name, determine if that attribute has changed. - hasChanged: function(attr) { - if (!this._hasComputed) this._computeChanges(); - if (attr == null) return !_.isEmpty(this.changed); - return _.has(this.changed, attr); - }, - - // Return an object containing all the attributes that have changed, or - // false if there are no changed attributes. Useful for determining what - // parts of a view need to be updated and/or what attributes need to be - // persisted to the server. Unset attributes will be set to undefined. - // You can also pass an attributes object to diff against the model, - // determining if there *would be* a change. - changedAttributes: function(diff) { - if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; - var val, changed = false, old = this._previousAttributes; - for (var attr in diff) { - if (_.isEqual(old[attr], (val = diff[attr]))) continue; - (changed || (changed = {}))[attr] = val; - } - return changed; - }, - - // Looking at the built up list of `set` attribute changes, compute how - // many of the attributes have actually changed. If `loud`, return a - // boiled-down list of only the real changes. - _computeChanges: function(loud) { - this.changed = {}; - var already = {}; - var triggers = []; - var current = this._currentAttributes; - var changes = this._changes; - - // Loop through the current queue of potential model changes. - for (var i = changes.length - 2; i >= 0; i -= 2) { - var key = changes[i], val = changes[i + 1]; - if (already[key]) continue; - already[key] = true; - - // Check if the attribute has been modified since the last change, - // and update `this.changed` accordingly. If we're inside of a `change` - // call, also add a trigger to the list. - if (current[key] !== val) { - this.changed[key] = val; - if (!loud) continue; - triggers.push(key, val); - current[key] = val; - } - } - if (loud) this._changes = []; - - // Signals `this.changed` is current to prevent duplicate calls from `this.hasChanged`. - this._hasComputed = true; - return triggers; - }, - - // Get the previous value of an attribute, recorded at the time the last - // `"change"` event was fired. - previous: function(attr) { - if (attr == null || !this._previousAttributes) return null; - return this._previousAttributes[attr]; - }, - - // Get all of the attributes of the model at the time of the previous - // `"change"` event. - previousAttributes: function() { - return _.clone(this._previousAttributes); + // Check if the model is currently in a valid state. + isValid: function(options) { + return this._validate({}, _.extend(options || {}, { validate: true })); }, // Run validation against the next complete set of model attributes, - // returning `true` if all is well. If a specific `error` callback has - // been passed, call that instead of firing the general `"error"` event. + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. _validate: function(attrs, options) { - if (!this.validate) return true; + if (!options.validate || !this.validate) return true; attrs = _.extend({}, this.attributes, attrs); - var error = this.validate(attrs, options); + var error = this.validationError = this.validate(attrs, options) || null; if (!error) return true; - if (options && options.error) options.error(this, error, options); - this.trigger('error', this, error, options); + this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error})); return false; } }); + // Underscore methods that we want to implement on the Model. + var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; + + // Mix in each Underscore method as a proxy to `Model#attributes`. + _.each(modelMethods, function(method) { + Model.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.attributes); + return _[method].apply(_, args); + }; + }); + // Backbone.Collection // ------------------- - // Provides a standard collection class for our sets of models, ordered - // or unordered. If a `comparator` is specified, the Collection will maintain + // If models tend to represent a single row of data, a Backbone Collection is + // more analagous to a table full of data ... or a small slice or page of that + // table, or a collection of rows that belong together for a particular reason + // -- all of the messages in this particular folder, all of the documents + // belonging to this particular author, and so on. Collections maintain + // indexes of their models, both in order, and for lookup by `id`. + + // Create a new **Collection**, perhaps to contain a specific type of `model`. + // If a `comparator` is specified, the Collection will maintain // its models in sort order, as they're added and removed. var Collection = Backbone.Collection = function(models, options) { options || (options = {}); + if (options.url) this.url = options.url; if (options.model) this.model = options.model; if (options.comparator !== void 0) this.comparator = options.comparator; this._reset(); @@ -596,6 +602,10 @@ if (models) this.reset(models, _.extend({silent: true}, options)); }; + // Default options for `Collection#set`. + var setOptions = {add: true, remove: true, merge: true}; + var addOptions = {add: true, merge: false, remove: false}; + // Define the Collection's inheritable methods. _.extend(Collection.prototype, Events, { @@ -618,83 +628,120 @@ return Backbone.sync.apply(this, arguments); }, - // Add a model, or list of models to the set. Pass **silent** to avoid - // firing the `add` event for every new model. + // Add a model, or list of models to the set. add: function(models, options) { - var i, args, length, model, existing, needsSort; - var at = options && options.at; - var sort = ((options && options.sort) == null ? true : options.sort); + return this.set(models, _.defaults(options || {}, addOptions)); + }, + + // Remove a model, or a list of models from the set. + remove: function(models, options) { models = _.isArray(models) ? models.slice() : [models]; + options || (options = {}); + var i, l, index, model; + for (i = 0, l = models.length; i < l; i++) { + model = this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byId[model.cid]; + index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + this._removeReference(model); + } + return this; + }, + + // Update a collection by `set`-ing a new list of models, adding new ones, + // removing models that are no longer present, and merging models that + // already exist in the collection, as necessary. Similar to **Model#set**, + // the core operation for updating the data contained by the collection. + set: function(models, options) { + options = _.defaults(options || {}, setOptions); + if (options.parse) models = this.parse(models, options); + if (!_.isArray(models)) models = models ? [models] : []; + var i, l, model, attrs, existing, sort; + var at = options.at; + var sortable = this.comparator && (at == null) && options.sort !== false; + var sortAttr = _.isString(this.comparator) ? this.comparator : null; + var toAdd = [], toRemove = [], modelMap = {}; // Turn bare objects into model references, and prevent invalid models // from being added. - for (i = models.length - 1; i >= 0; i--) { - if(!(model = this._prepareModel(models[i], options))) { - this.trigger("error", this, models[i], options); - models.splice(i, 1); - continue; - } - models[i] = model; + for (i = 0, l = models.length; i < l; i++) { + if (!(model = this._prepareModel(models[i], options))) continue; - existing = model.id != null && this._byId[model.id]; // If a duplicate is found, prevent it from being added and // optionally merge it into the existing model. - if (existing || this._byCid[model.cid]) { - if (options && options.merge && existing) { + if (existing = this.get(model)) { + if (options.remove) modelMap[existing.cid] = true; + if (options.merge) { existing.set(model.attributes, options); - needsSort = sort; + if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; } - models.splice(i, 1); - continue; + + // This is a new model, push it to the `toAdd` list. + } else if (options.add) { + toAdd.push(model); + + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + model.on('all', this._onModelEvent, this); + this._byId[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; } + } - // Listen to added models' events, and index models for lookup by - // `id` and by `cid`. - model.on('all', this._onModelEvent, this); - this._byCid[model.cid] = model; - if (model.id != null) this._byId[model.id] = model; + // Remove nonexistent models if appropriate. + if (options.remove) { + for (i = 0, l = this.length; i < l; ++i) { + if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); + } + if (toRemove.length) this.remove(toRemove, options); } // See if sorting is needed, update `length` and splice in new models. - if (models.length) needsSort = sort; - this.length += models.length; - args = [at != null ? at : this.models.length, 0]; - push.apply(args, models); - splice.apply(this.models, args); + if (toAdd.length) { + if (sortable) sort = true; + this.length += toAdd.length; + if (at != null) { + splice.apply(this.models, [at, 0].concat(toAdd)); + } else { + push.apply(this.models, toAdd); + } + } - // Sort the collection if appropriate. - if (needsSort && this.comparator && at == null) this.sort({silent: true}); + // Silently sort the collection if appropriate. + if (sort) this.sort({silent: true}); - if (options && options.silent) return this; + if (options.silent) return this; // Trigger `add` events. - while (model = models.shift()) { - model.trigger('add', model, this, options); + for (i = 0, l = toAdd.length; i < l; i++) { + (model = toAdd[i]).trigger('add', model, this, options); } + // Trigger `sort` if the collection was sorted. + if (sort) this.trigger('sort', this, options); return this; }, - // Remove a model, or a list of models from the set. Pass silent to avoid - // firing the `remove` event for every model removed. - remove: function(models, options) { - var i, l, index, model; + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any granular `add` or `remove` events. Fires `reset` when finished. + // Useful for bulk operations and optimizations. + reset: function(models, options) { options || (options = {}); - models = _.isArray(models) ? models.slice() : [models]; - for (i = 0, l = models.length; i < l; i++) { - model = this.get(models[i]); - if (!model) continue; - delete this._byId[model.id]; - delete this._byCid[model.cid]; - index = this.indexOf(model); - this.models.splice(index, 1); - this.length--; - if (!options.silent) { - options.index = index; - model.trigger('remove', model, this, options); - } - this._removeReference(model); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i]); } + options.previousModels = this.models; + this._reset(); + this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); return this; }, @@ -734,7 +781,7 @@ // Get a model from the set by id. get: function(obj) { if (obj == null) return void 0; - return this._byId[obj.id != null ? obj.id : obj] || this._byCid[obj.cid || obj]; + return this._byId[obj.id != null ? obj.id : obj.cid || obj]; }, // Get the model at the given index. @@ -742,10 +789,11 @@ return this.models[index]; }, - // Return models with matching attributes. Useful for simple cases of `filter`. - where: function(attrs) { - if (_.isEmpty(attrs)) return []; - return this.filter(function(model) { + // Return models with matching attributes. Useful for simple cases of + // `filter`. + where: function(attrs, first) { + if (_.isEmpty(attrs)) return first ? void 0 : []; + return this[first ? 'find' : 'filter'](function(model) { for (var key in attrs) { if (attrs[key] !== model.get(key)) return false; } @@ -753,95 +801,60 @@ }); }, + // Return the first model with matching attributes. Useful for simple cases + // of `find`. + findWhere: function(attrs) { + return this.where(attrs, true); + }, + // Force the collection to re-sort itself. You don't need to call this under // normal circumstances, as the set will maintain sort order as each item // is added. sort: function(options) { - if (!this.comparator) { - throw new Error('Cannot sort a set without a comparator'); - } + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + options || (options = {}); + // Run sort based on type of `comparator`. if (_.isString(this.comparator) || this.comparator.length === 1) { this.models = this.sortBy(this.comparator, this); } else { this.models.sort(_.bind(this.comparator, this)); } - if (!options || !options.silent) this.trigger('sort', this, options); + if (!options.silent) this.trigger('sort', this, options); return this; }, + // Figure out the smallest index at which a model should be inserted so as + // to maintain order. + sortedIndex: function(model, value, context) { + value || (value = this.comparator); + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _.sortedIndex(this.models, model, iterator, context); + }, + // Pluck an attribute from each model in the collection. pluck: function(attr) { return _.invoke(this.models, 'get', attr); }, - // Smartly update a collection with a change set of models, adding, - // removing, and merging as necessary. - update: function(models, options) { - var model, i, l, existing; - var add = [], remove = [], modelMap = {}; - var idAttr = this.model.prototype.idAttribute; - options = _.extend({add: true, merge: true, remove: true}, options); - if (options.parse) models = this.parse(models); - - // Allow a single model (or no argument) to be passed. - if (!_.isArray(models)) models = models ? [models] : []; - - // Proxy to `add` for this case, no need to iterate... - if (options.add && !options.remove) return this.add(models, options); - - // Determine which models to add and merge, and which to remove. - for (i = 0, l = models.length; i < l; i++) { - model = models[i]; - existing = this.get(model.id || model.cid || model[idAttr]); - if (options.remove && existing) modelMap[existing.cid] = true; - if ((options.add && !existing) || (options.merge && existing)) { - add.push(model); - } - } - if (options.remove) { - for (i = 0, l = this.models.length; i < l; i++) { - model = this.models[i]; - if (!modelMap[model.cid]) remove.push(model); - } - } - - // Remove models (if applicable) before we add and merge the rest. - if (remove.length) this.remove(remove, options); - if (add.length) this.add(add, options); - return this; - }, - - // When you have more items than you want to add or remove individually, - // you can reset the entire set with a new list of models, without firing - // any `add` or `remove` events. Fires `reset` when finished. - reset: function(models, options) { - options || (options = {}); - if (options.parse) models = this.parse(models); - for (var i = 0, l = this.models.length; i < l; i++) { - this._removeReference(this.models[i]); - } - options.previousModels = this.models; - this._reset(); - if (models) this.add(models, _.extend({silent: true}, options)); - if (!options.silent) this.trigger('reset', this, options); - return this; - }, - // Fetch the default set of models for this collection, resetting the - // collection when they arrive. If `add: true` is passed, appends the - // models to the collection instead of resetting. + // collection when they arrive. If `reset: true` is passed, the response + // data will be passed through the `reset` method instead of `set`. fetch: function(options) { options = options ? _.clone(options) : {}; if (options.parse === void 0) options.parse = true; - var collection = this; var success = options.success; - options.success = function(resp, status, xhr) { - var method = options.update ? 'update' : 'reset'; + var collection = this; + options.success = function(resp) { + var method = options.reset ? 'reset' : 'set'; collection[method](resp, options); if (success) success(collection, resp, options); + collection.trigger('sync', collection, resp, options); }; + wrapError(this, options); return this.sync('read', this, options); }, @@ -849,13 +862,12 @@ // collection immediately, unless `wait: true` is passed, in which case we // wait for the server to agree. create: function(model, options) { - var collection = this; options = options ? _.clone(options) : {}; - model = this._prepareModel(model, options); - if (!model) return false; - if (!options.wait) collection.add(model, options); + if (!(model = this._prepareModel(model, options))) return false; + if (!options.wait) this.add(model, options); + var collection = this; var success = options.success; - options.success = function(model, resp, options) { + options.success = function(resp) { if (options.wait) collection.add(model, options); if (success) success(model, resp, options); }; @@ -865,7 +877,7 @@ // **parse** converts a response into a list of models to be added to the // collection. The default implementation is just to pass it through. - parse: function(resp) { + parse: function(resp, options) { return resp; }, @@ -874,22 +886,16 @@ return new this.constructor(this.models); }, - // Proxy to _'s chain. Can't be proxied the same way the rest of the - // underscore methods are proxied because it relies on the underscore - // constructor. - chain: function() { - return _(this.models).chain(); - }, - - // Reset all internal state. Called when the collection is reset. + // Private method to reset all internal state. Called when the collection + // is first initialized or reset. _reset: function() { this.length = 0; this.models = []; this._byId = {}; - this._byCid = {}; }, - // Prepare a model or hash of attributes to be added to this collection. + // Prepare a hash of attributes (or other model) to be added to this + // collection. _prepareModel: function(attrs, options) { if (attrs instanceof Model) { if (!attrs.collection) attrs.collection = this; @@ -898,11 +904,14 @@ options || (options = {}); options.collection = this; var model = new this.model(attrs, options); - if (!model._validate(attrs, options)) return false; + if (!model._validate(attrs, options)) { + this.trigger('invalid', this, attrs, options); + return false; + } return model; }, - // Internal method to remove a model's ties to a collection. + // Internal method to sever a model's ties to a collection. _removeReference: function(model) { if (this === model.collection) delete model.collection; model.off('all', this._onModelEvent, this); @@ -925,12 +934,14 @@ }); // Underscore methods that we want to implement on the Collection. + // 90% of the core usefulness of Backbone Collections is actually implemented + // right here: var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', - 'max', 'min', 'sortedIndex', 'toArray', 'size', 'first', 'head', 'take', - 'initial', 'rest', 'tail', 'last', 'without', 'indexOf', 'shuffle', - 'lastIndexOf', 'isEmpty']; + 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', + 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', + 'isEmpty', 'chain']; // Mix in each Underscore method as a proxy to `Collection#models`. _.each(methods, function(method) { @@ -954,6 +965,241 @@ }; }); + // Backbone.View + // ------------- + + // Backbone Views are almost more convention than they are actual code. A View + // is simply a JavaScript object that represents a logical chunk of UI in the + // DOM. This might be a single item, an entire list, a sidebar or panel, or + // even the surrounding frame which wraps your whole app. Defining a chunk of + // UI as a **View** allows you to define your DOM events declaratively, without + // having to worry about render order ... and makes it easy for the view to + // react to specific changes in the state of your models. + + // Creating a Backbone.View creates its initial element outside of the DOM, + // if an existing element is not provided... + var View = Backbone.View = function(options) { + this.cid = _.uniqueId('view'); + this._configure(options || {}); + this._ensureElement(); + this.initialize.apply(this, arguments); + this.delegateEvents(); + }; + + // Cached regex to split keys for `delegate`. + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + + // List of view options to be merged as properties. + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; + + // Set up all inheritable **Backbone.View** properties and methods. + _.extend(View.prototype, Events, { + + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', + + // jQuery delegate for element lookup, scoped to DOM elements within the + // current view. This should be prefered to global lookups where possible. + $: function(selector) { + return this.$el.find(selector); + }, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. + render: function() { + return this; + }, + + // Remove this view by taking the element out of the DOM, and removing any + // applicable Backbone.Events listeners. + remove: function() { + this.$el.remove(); + this.stopListening(); + return this; + }, + + // Change the view's element (`this.el` property), including event + // re-delegation. + setElement: function(element, delegate) { + if (this.$el) this.undelegateEvents(); + this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); + this.el = this.$el[0]; + if (delegate !== false) this.delegateEvents(); + return this; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save' + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + // This only works for delegate-able events: not `focus`, `blur`, and + // not `change`, `submit`, and `reset` in Internet Explorer. + delegateEvents: function(events) { + if (!(events || (events = _.result(this, 'events')))) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!_.isFunction(method)) method = this[events[key]]; + if (!method) continue; + + var match = key.match(delegateEventSplitter); + var eventName = match[1], selector = match[2]; + method = _.bind(method, this); + eventName += '.delegateEvents' + this.cid; + if (selector === '') { + this.$el.on(eventName, method); + } else { + this.$el.on(eventName, selector, method); + } + } + return this; + }, + + // Clears all callbacks previously bound to the view with `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + undelegateEvents: function() { + this.$el.off('.delegateEvents' + this.cid); + return this; + }, + + // Performs the initial configuration of a View with a set of options. + // Keys with special meaning *(e.g. model, collection, id, className)* are + // attached directly to the view. See `viewOptions` for an exhaustive + // list. + _configure: function(options) { + if (this.options) options = _.extend({}, _.result(this, 'options'), options); + _.extend(this, _.pick(options, viewOptions)); + this.options = options; + }, + + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + if (!this.el) { + var attrs = _.extend({}, _.result(this, 'attributes')); + if (this.id) attrs.id = _.result(this, 'id'); + if (this.className) attrs['class'] = _.result(this, 'className'); + var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); + this.setElement($el, false); + } else { + this.setElement(_.result(this, 'el'), false); + } + } + + }); + + // Backbone.sync + // ------------- + + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, makes a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` + // instead of `application/json` with the model in a param named `model`. + // Useful when interfacing with server-side languages like **PHP** that make + // it difficult to read the body of `PUT` requests. + Backbone.sync = function(method, model, options) { + var type = methodMap[method]; + + // Default options, unless specified. + _.defaults(options || (options = {}), { + emulateHTTP: Backbone.emulateHTTP, + emulateJSON: Backbone.emulateJSON + }); + + // Default JSON-request options. + var params = {type: type, dataType: 'json'}; + + // Ensure that we have a URL. + if (!options.url) { + params.url = _.result(model, 'url') || urlError(); + } + + // Ensure that we have the appropriate request data. + if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { + params.contentType = 'application/json'; + params.data = JSON.stringify(options.attrs || model.toJSON(options)); + } + + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (options.emulateJSON) { + params.contentType = 'application/x-www-form-urlencoded'; + params.data = params.data ? {model: params.data} : {}; + } + + // For older servers, emulate HTTP by mimicking the HTTP method with `_method` + // And an `X-HTTP-Method-Override` header. + if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { + params.type = 'POST'; + if (options.emulateJSON) params.data._method = type; + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-HTTP-Method-Override', type); + if (beforeSend) return beforeSend.apply(this, arguments); + }; + } + + // Don't process data on a non-GET request. + if (params.type !== 'GET' && !options.emulateJSON) { + params.processData = false; + } + + // If we're sending a `PATCH` request, and we're in an old Internet Explorer + // that still has ActiveX enabled by default, override jQuery to use that + // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. + if (params.type === 'PATCH' && window.ActiveXObject && + !(window.external && window.external.msActiveXFilteringEnabled)) { + params.xhr = function() { + return new ActiveXObject("Microsoft.XMLHTTP"); + }; + } + + // Make the request, allowing the user to override any Ajax options. + var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); + model.trigger('request', model, xhr, options); + return xhr; + }; + + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + 'create': 'POST', + 'update': 'PUT', + 'patch': 'PATCH', + 'delete': 'DELETE', + 'read': 'GET' + }; + + // Set the default implementation of `Backbone.ajax` to proxy through to `$`. + // Override this if you'd like to use a different library. + Backbone.ajax = function() { + return Backbone.$.ajax.apply(Backbone.$, arguments); + }; + // Backbone.Router // --------------- @@ -969,7 +1215,7 @@ // Cached regular expressions for matching named param parts and splatted // parts of route strings. var optionalParam = /\((.*?)\)/g; - var namedParam = /:\w+/g; + var namedParam = /(\(\?)?:\w+/g; var splatParam = /\*\w+/g; var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; @@ -988,13 +1234,19 @@ // route: function(route, name, callback) { if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ''; + } if (!callback) callback = this[name]; - Backbone.history.route(route, _.bind(function(fragment) { - var args = this._extractParameters(route, fragment); - callback && callback.apply(this, args); - this.trigger.apply(this, ['route:' + name].concat(args)); - Backbone.history.trigger('route', this, name, args); - }, this)); + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + callback && callback.apply(router, args); + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + }); return this; }, @@ -1009,6 +1261,7 @@ // routes can be defined at the bottom of the route map. _bindRoutes: function() { if (!this.routes) return; + this.routes = _.result(this, 'routes'); var route, routes = _.keys(this.routes); while ((route = routes.pop()) != null) { this.route(route, this.routes[route]); @@ -1020,15 +1273,21 @@ _routeToRegExp: function(route) { route = route.replace(escapeRegExp, '\\$&') .replace(optionalParam, '(?:$1)?') - .replace(namedParam, '([^\/]+)') + .replace(namedParam, function(match, optional){ + return optional ? match : '([^\/]+)'; + }) .replace(splatParam, '(.*?)'); return new RegExp('^' + route + '$'); }, // Given a route, and a URL fragment that it matches, return the array of - // extracted parameters. + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. _extractParameters: function(route, fragment) { - return route.exec(fragment).slice(1); + var params = route.exec(fragment).slice(1); + return _.map(params, function(param) { + return param ? decodeURIComponent(param) : null; + }); } }); @@ -1036,13 +1295,16 @@ // Backbone.History // ---------------- - // Handles cross-browser history management, based on URL fragments. If the - // browser does not support `onhashchange`, falls back to polling. + // Handles cross-browser history management, based on either + // [pushState](http://diveintohtml5.info/history.html) and real URLs, or + // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) + // and URL fragments. If the browser supports neither (old IE, natch), + // falls back to polling. var History = Backbone.History = function() { this.handlers = []; _.bindAll(this, 'checkUrl'); - // #1653 - Ensure that `History` can be used outside of the browser. + // Ensure that `History` can be used outside of the browser. if (typeof window !== 'undefined') { this.location = window.location; this.history = window.history; @@ -1121,9 +1383,9 @@ // Depending on whether we're using pushState or hashes, and whether // 'onhashchange' is supported, determine how we check the URL state. if (this._hasPushState) { - Backbone.$(window).bind('popstate', this.checkUrl); + Backbone.$(window).on('popstate', this.checkUrl); } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { - Backbone.$(window).bind('hashchange', this.checkUrl); + Backbone.$(window).on('hashchange', this.checkUrl); } else if (this._wantsHashChange) { this._checkUrlInterval = setInterval(this.checkUrl, this.interval); } @@ -1155,7 +1417,7 @@ // Disable Backbone.history, perhaps temporarily. Not useful in a real app, // but possibly useful for unit testing Routers. stop: function() { - Backbone.$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl); + Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); clearInterval(this._checkUrlInterval); History.started = false; }, @@ -1238,7 +1500,7 @@ var href = location.href.replace(/(javascript:|#).*$/, ''); location.replace(href + '#' + fragment); } else { - // #1649 - Some browsers require that `hash` contains a leading #. + // Some browsers require that `hash` contains a leading #. location.hash = '#' + fragment; } } @@ -1248,241 +1510,6 @@ // Create the default Backbone.history. Backbone.history = new History; - // Backbone.View - // ------------- - - // Creating a Backbone.View creates its initial element outside of the DOM, - // if an existing element is not provided... - var View = Backbone.View = function(options) { - this.cid = _.uniqueId('view'); - this._configure(options || {}); - this._ensureElement(); - this.initialize.apply(this, arguments); - this.delegateEvents(); - }; - - // Cached regex to split keys for `delegate`. - var delegateEventSplitter = /^(\S+)\s*(.*)$/; - - // List of view options to be merged as properties. - var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; - - // Set up all inheritable **Backbone.View** properties and methods. - _.extend(View.prototype, Events, { - - // The default `tagName` of a View's element is `"div"`. - tagName: 'div', - - // jQuery delegate for element lookup, scoped to DOM elements within the - // current view. This should be prefered to global lookups where possible. - $: function(selector) { - return this.$el.find(selector); - }, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // **render** is the core function that your view should override, in order - // to populate its element (`this.el`), with the appropriate HTML. The - // convention is for **render** to always return `this`. - render: function() { - return this; - }, - - // Remove this view by taking the element out of the DOM, and removing any - // applicable Backbone.Events listeners. - remove: function() { - this.$el.remove(); - this.stopListening(); - return this; - }, - - // For small amounts of DOM Elements, where a full-blown template isn't - // needed, use **make** to manufacture elements, one at a time. - // - // var el = this.make('li', {'class': 'row'}, this.model.escape('title')); - // - make: function(tagName, attributes, content) { - var el = document.createElement(tagName); - if (attributes) Backbone.$(el).attr(attributes); - if (content != null) Backbone.$(el).html(content); - return el; - }, - - // Change the view's element (`this.el` property), including event - // re-delegation. - setElement: function(element, delegate) { - if (this.$el) this.undelegateEvents(); - this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); - this.el = this.$el[0]; - if (delegate !== false) this.delegateEvents(); - return this; - }, - - // Set callbacks, where `this.events` is a hash of - // - // *{"event selector": "callback"}* - // - // { - // 'mousedown .title': 'edit', - // 'click .button': 'save' - // 'click .open': function(e) { ... } - // } - // - // pairs. Callbacks will be bound to the view, with `this` set properly. - // Uses event delegation for efficiency. - // Omitting the selector binds the event to `this.el`. - // This only works for delegate-able events: not `focus`, `blur`, and - // not `change`, `submit`, and `reset` in Internet Explorer. - delegateEvents: function(events) { - if (!(events || (events = _.result(this, 'events')))) return; - this.undelegateEvents(); - for (var key in events) { - var method = events[key]; - if (!_.isFunction(method)) method = this[events[key]]; - if (!method) throw new Error('Method "' + events[key] + '" does not exist'); - var match = key.match(delegateEventSplitter); - var eventName = match[1], selector = match[2]; - method = _.bind(method, this); - eventName += '.delegateEvents' + this.cid; - if (selector === '') { - this.$el.bind(eventName, method); - } else { - this.$el.delegate(selector, eventName, method); - } - } - }, - - // Clears all callbacks previously bound to the view with `delegateEvents`. - // You usually don't need to use this, but may wish to if you have multiple - // Backbone views attached to the same DOM element. - undelegateEvents: function() { - this.$el.unbind('.delegateEvents' + this.cid); - }, - - // Performs the initial configuration of a View with a set of options. - // Keys with special meaning *(model, collection, id, className)*, are - // attached directly to the view. - _configure: function(options) { - if (this.options) options = _.extend({}, _.result(this, 'options'), options); - _.extend(this, _.pick(options, viewOptions)); - this.options = options; - }, - - // Ensure that the View has a DOM element to render into. - // If `this.el` is a string, pass it through `$()`, take the first - // matching element, and re-assign it to `el`. Otherwise, create - // an element from the `id`, `className` and `tagName` properties. - _ensureElement: function() { - if (!this.el) { - var attrs = _.extend({}, _.result(this, 'attributes')); - if (this.id) attrs.id = _.result(this, 'id'); - if (this.className) attrs['class'] = _.result(this, 'className'); - this.setElement(this.make(_.result(this, 'tagName'), attrs), false); - } else { - this.setElement(_.result(this, 'el'), false); - } - } - - }); - - // Backbone.sync - // ------------- - - // Map from CRUD to HTTP for our default `Backbone.sync` implementation. - var methodMap = { - 'create': 'POST', - 'update': 'PUT', - 'patch': 'PATCH', - 'delete': 'DELETE', - 'read': 'GET' - }; - - // Override this function to change the manner in which Backbone persists - // models to the server. You will be passed the type of request, and the - // model in question. By default, makes a RESTful Ajax request - // to the model's `url()`. Some possible customizations could be: - // - // * Use `setTimeout` to batch rapid-fire updates into a single request. - // * Send up the models as XML instead of JSON. - // * Persist models via WebSockets instead of Ajax. - // - // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests - // as `POST`, with a `_method` parameter containing the true HTTP method, - // as well as all requests with the body as `application/x-www-form-urlencoded` - // instead of `application/json` with the model in a param named `model`. - // Useful when interfacing with server-side languages like **PHP** that make - // it difficult to read the body of `PUT` requests. - Backbone.sync = function(method, model, options) { - var type = methodMap[method]; - - // Default options, unless specified. - _.defaults(options || (options = {}), { - emulateHTTP: Backbone.emulateHTTP, - emulateJSON: Backbone.emulateJSON - }); - - // Default JSON-request options. - var params = {type: type, dataType: 'json'}; - - // Ensure that we have a URL. - if (!options.url) { - params.url = _.result(model, 'url') || urlError(); - } - - // Ensure that we have the appropriate request data. - if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { - params.contentType = 'application/json'; - params.data = JSON.stringify(options.attrs || model.toJSON(options)); - } - - // For older servers, emulate JSON by encoding the request into an HTML-form. - if (options.emulateJSON) { - params.contentType = 'application/x-www-form-urlencoded'; - params.data = params.data ? {model: params.data} : {}; - } - - // For older servers, emulate HTTP by mimicking the HTTP method with `_method` - // And an `X-HTTP-Method-Override` header. - if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { - params.type = 'POST'; - if (options.emulateJSON) params.data._method = type; - var beforeSend = options.beforeSend; - options.beforeSend = function(xhr) { - xhr.setRequestHeader('X-HTTP-Method-Override', type); - if (beforeSend) return beforeSend.apply(this, arguments); - }; - } - - // Don't process data on a non-GET request. - if (params.type !== 'GET' && !options.emulateJSON) { - params.processData = false; - } - - var success = options.success; - options.success = function(resp, status, xhr) { - if (success) success(resp, status, xhr); - model.trigger('sync', model, resp, options); - }; - - var error = options.error; - options.error = function(xhr, status, thrown) { - if (error) error(model, xhr, options); - model.trigger('error', model, xhr, options); - }; - - // Make the request, allowing the user to override any Ajax options. - var xhr = Backbone.ajax(_.extend(params, options)); - model.trigger('request', model, xhr, options); - return xhr; - }; - - // Set the default implementation of `Backbone.ajax` to proxy through to `$`. - Backbone.ajax = function() { - return Backbone.$.ajax.apply(Backbone.$, arguments); - }; - // Helpers // ------- @@ -1499,7 +1526,7 @@ if (protoProps && _.has(protoProps, 'constructor')) { child = protoProps.constructor; } else { - child = function(){ parent.apply(this, arguments); }; + child = function(){ return parent.apply(this, arguments); }; } // Add static properties to the constructor function, if supplied. @@ -1530,4 +1557,14 @@ throw new Error('A "url" property or function must be specified'); }; -}).call(this); + // Wrap an optional error callback with a fallback error event. + var wrapError = function (model, options) { + var error = options.error; + options.error = function(resp) { + if (error) error(model, resp, options); + model.trigger('error', model, resp, options); + }; + }; + + return Backbone; +})); diff --git a/widgy/static/widgy/js/lib/underscore.js b/widgy/static/widgy/js/lib/underscore.js index 4d83099fd..5574d1495 100644 --- a/widgy/static/widgy/js/lib/underscore.js +++ b/widgy/static/widgy/js/lib/underscore.js @@ -1,6 +1,6 @@ -// Underscore.js 1.4.3 +// Underscore.js 1.4.4 // http://underscorejs.org -// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. +// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. // Underscore may be freely distributed under the MIT license. (function() { @@ -64,7 +64,7 @@ } // Current version. - _.VERSION = '1.4.3'; + _.VERSION = '1.4.4'; // Collection Functions // -------------------- @@ -224,8 +224,9 @@ // Invoke a method (with arguments) on every item in a collection. _.invoke = function(obj, method) { var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); return _.map(obj, function(value) { - return (_.isFunction(method) ? method : value[method]).apply(value, args); + return (isFunc ? method : value[method]).apply(value, args); }); }; @@ -235,10 +236,10 @@ }; // Convenience version of a common use case of `filter`: selecting only objects - // with specific `key:value` pairs. - _.where = function(obj, attrs) { - if (_.isEmpty(attrs)) return []; - return _.filter(obj, function(value) { + // containing specific `key:value` pairs. + _.where = function(obj, attrs, first) { + if (_.isEmpty(attrs)) return first ? null : []; + return _[first ? 'find' : 'filter'](obj, function(value) { for (var key in attrs) { if (attrs[key] !== value[key]) return false; } @@ -246,6 +247,12 @@ }); }; + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.where(obj, attrs, true); + }; + // Return the maximum element or (element-based computation). // Can't optimize arrays of integers longer than 65,535 elements. // See: https://bugs.webkit.org/show_bug.cgi?id=80797 @@ -567,26 +574,23 @@ // Function (ahem) Functions // ------------------ - // Reusable constructor function for prototype setting. - var ctor = function(){}; - // Create a function bound to a given object (assigning `this`, and arguments, - // optionally). Binding with arguments is also known as `curry`. - // Delegates to **ECMAScript 5**'s native `Function.bind` if available. - // We check for `func.bind` first, to fail fast when `func` is undefined. + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. _.bind = function(func, context) { - var args, bound; if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); - if (!_.isFunction(func)) throw new TypeError; - args = slice.call(arguments, 2); - return bound = function() { - if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); - ctor.prototype = func.prototype; - var self = new ctor; - ctor.prototype = null; - var result = func.apply(self, args.concat(slice.call(arguments))); - if (Object(result) === result) return result; - return self; + var args = slice.call(arguments, 2); + return function() { + return func.apply(context, args.concat(slice.call(arguments))); + }; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. + _.partial = function(func) { + var args = slice.call(arguments, 1); + return function() { + return func.apply(this, args.concat(slice.call(arguments))); }; }; @@ -594,7 +598,7 @@ // all callbacks defined on an object belong to it. _.bindAll = function(obj) { var funcs = slice.call(arguments, 1); - if (funcs.length == 0) funcs = _.functions(obj); + if (funcs.length === 0) funcs = _.functions(obj); each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); return obj; }; @@ -1019,7 +1023,7 @@ max = min; min = 0; } - return min + (0 | Math.random() * (max - min + 1)); + return min + Math.floor(Math.random() * (max - min + 1)); }; // List of HTML entities for escaping. @@ -1075,7 +1079,7 @@ // Useful for temporary DOM ids. var idCounter = 0; _.uniqueId = function(prefix) { - var id = '' + ++idCounter; + var id = ++idCounter + ''; return prefix ? prefix + id : id; }; @@ -1110,6 +1114,7 @@ // Underscore templating handles arbitrary delimiters, preserves whitespace, // and correctly escapes quotes within interpolated code. _.template = function(text, data, settings) { + var render; settings = _.defaults({}, settings, _.templateSettings); // Combine delimiters into one regular expression via alternation. @@ -1148,7 +1153,7 @@ source + "return __p;\n"; try { - var render = new Function(settings.variable || 'obj', '_', source); + render = new Function(settings.variable || 'obj', '_', source); } catch (e) { e.source = source; throw e; @@ -1218,4 +1223,12 @@ }); + // AMD define happens at the end for compatibility with AMD loaders + // that don't enforce next-turn semantics on modules. + if (typeof define === 'function' && define.amd) { + define('underscore', function() { + return _; + }); + } + }).call(this); diff --git a/widgy/static/widgy/js/main.js b/widgy/static/widgy/js/main.js index 1043e4e51..0e7d39b0e 100644 --- a/widgy/static/widgy/js/main.js +++ b/widgy/static/widgy/js/main.js @@ -10,21 +10,10 @@ if ( typeof window.console == 'undefined' ) } require.config({ - // ordered dependencies (example: jquery plugins) - shim: { - 'underscore': { - exports: '_' - }, - 'backbone': { - deps: ['underscore', 'jquery'], - exports: 'Backbone' - } - }, paths: { - 'jquery': './lib/jquery', - 'underscore': './lib/underscore', - 'backbone': './lib/backbone', - 'mustache': './lib/mustache', - 'text': 'require/text' + 'jquery': './lib/jquery', + 'underscore': './lib/underscore', + 'backbone': './lib/backbone', + 'text': 'require/text' } }); diff --git a/widgy/static/widgy/js/nodes/models.js b/widgy/static/widgy/js/nodes/models.js index 0526b5b87..c68ee33fa 100644 --- a/widgy/static/widgy/js/nodes/models.js +++ b/widgy/static/widgy/js/nodes/models.js @@ -105,7 +105,7 @@ define([ 'underscore', 'widgy.backbone', 'lib/q' if (ret) { if (children) { - this.children.update2(children, options); + this.children.set2(children, options); if ( options && (options.resort || options.sort_silently) ) { this.children.sortByRight(options); } @@ -185,7 +185,7 @@ define([ 'underscore', 'widgy.backbone', 'lib/q' * collection. * - else, remove the old model from the collection. */ - update2: function(data, options) { + set2: function(data, options) { var models = []; _.each(data, function(child) { @@ -199,7 +199,7 @@ define([ 'underscore', 'widgy.backbone', 'lib/q' } }, this); - this.update(models, options); + this.set(models, options); }, /** diff --git a/widgy/static/widgy/js/shelves/shelves.js b/widgy/static/widgy/js/shelves/shelves.js index fb9c39ef4..a9fadcdda 100644 --- a/widgy/static/widgy/js/shelves/shelves.js +++ b/widgy/static/widgy/js/shelves/shelves.js @@ -121,7 +121,7 @@ define([ 'exports', 'underscore', 'widgy.backbone', 'nodes/base', else return this.collection._prepareModel(value); }, this); - this.collection.update(instances); + this.collection.set(instances); this.collection.sort(); this.content_classes = null; }, diff --git a/widgy/static/widgy/js/widgy.backbone.js b/widgy/static/widgy/js/widgy.backbone.js index abd363e52..4bd38b586 100644 --- a/widgy/static/widgy/js/widgy.backbone.js +++ b/widgy/static/widgy/js/widgy.backbone.js @@ -1,4 +1,4 @@ -define([ 'jquery', 'underscore', 'backbone', 'mustache', 'lib/q' ], function($, _, Backbone, Mustache, Q) { +define([ 'jquery', 'underscore', 'backbone', 'lib/mustache', 'lib/q' ], function($, _, Backbone, Mustache, Q) { Mustache.tags = ['<%', '%>']; From b238945b79f9963fbc79adfa3c098b4a61028504 Mon Sep 17 00:00:00 2001 From: Rocky Meza Date: Tue, 11 Jun 2013 15:22:35 -0600 Subject: [PATCH 0619/1061] test ShelfItem event bubbling --- js_tests/package.json | 3 +- js_tests/tests/setup.js | 14 +++++++++ js_tests/tests/test_node.js | 9 ------ js_tests/tests/test_nodeview.js | 38 ++++++++++++++++++++++++ js_tests/tests/test_widget_component.js | 14 --------- widgy/static/widgy/js/nodes/nodes.js | 18 +++++++---- widgy/static/widgy/js/shelves/shelves.js | 19 +++++++----- widgy/static/widgy/js/widgy.backbone.js | 3 +- 8 files changed, 79 insertions(+), 39 deletions(-) create mode 100644 js_tests/tests/test_nodeview.js diff --git a/js_tests/package.json b/js_tests/package.json index 18711126f..5622efc41 100644 --- a/js_tests/package.json +++ b/js_tests/package.json @@ -9,7 +9,8 @@ "dependencies": { "jsdom": "~0.3.4", "requirejs": "~2.1.4", - "chai": "~1.4.2" + "chai": "~1.4.2", + "sinon": "~1.7.1" }, "devDependencies": { "mocha": "~1.8.1", diff --git a/js_tests/tests/setup.js b/js_tests/tests/setup.js index 6ebf1cc81..40d7b5b59 100644 --- a/js_tests/tests/setup.js +++ b/js_tests/tests/setup.js @@ -38,6 +38,20 @@ test = { } }; +requirejs.define('components/testcomponent/component', ['widgy.contents'], function(contents) { + var TestContent = contents.Model.extend(); + var EditorView = contents.EditorView.extend(); + + var WidgetView = contents.View.extend({ + editorClass: EditorView + }); + + return _.extend({}, contents, { + Model: TestContent, + View: WidgetView + }); +}); + module.exports = { test: test }; diff --git a/js_tests/tests/test_node.js b/js_tests/tests/test_node.js index f67321430..089fde9ad 100644 --- a/js_tests/tests/test_node.js +++ b/js_tests/tests/test_node.js @@ -17,15 +17,6 @@ var assertListsEqual = function(a, b, message) { } }; -// define a TestComponent -requirejs.define('components/testcomponent/component', ['widgy.contents'], function(contents) { - var TestContent = contents.Model.extend(); - - return _.extend({}, contents, { - Model: TestContent - }); -}); - var TestComponent = requirejs('components/testcomponent/component'); diff --git a/js_tests/tests/test_nodeview.js b/js_tests/tests/test_nodeview.js new file mode 100644 index 000000000..e88356360 --- /dev/null +++ b/js_tests/tests/test_nodeview.js @@ -0,0 +1,38 @@ +var test = require('./setup').test, + requirejs = require('requirejs'), + assert = require('chai').assert, + sinon = require('sinon'); + +var nodes = requirejs('nodes/nodes'), + shelves = requirejs('shelves/shelves'), + _ = requirejs('underscore'), + Q = requirejs('lib/q'); + + +describe('ShelfView', function() { + beforeEach(function() { + this.node = new nodes.Node({ + content: { + component: 'testcomponent', + shelf: true + } + }); + }); + + it('bubbles ShelfItemView events', function() { + return this.node.ready(function(node) { + var node_view = new nodes.NodeView({model: node}), + callback = sinon.spy(), + // this would happen in renderShelf + shelf = node_view.makeShelf(); + + shelf.collection.add({}); + var shelf_item = shelf.list.list[0]; + + shelf.on('foo', callback); + assert.isFalse(callback.called); + shelf_item.trigger('foo'); + assert.isTrue(callback.called); + }); + }); +}); diff --git a/js_tests/tests/test_widget_component.js b/js_tests/tests/test_widget_component.js index 6d771a40c..1cf4806cd 100644 --- a/js_tests/tests/test_widget_component.js +++ b/js_tests/tests/test_widget_component.js @@ -8,20 +8,6 @@ var _ = requirejs('underscore'), contents = requirejs('widgy.contents'), nodes = requirejs('nodes/nodes'); -// define a TestComponent -requirejs.define('components/testcomponent/component', ['widgy.contents'], function(contents) { - var EditorView = contents.EditorView.extend({ - }); - - var WidgetView = contents.View.extend({ - editorClass: EditorView - }); - - return _.extend({}, contents, { - View: WidgetView - }); -}); - var TestComponent = requirejs('components/testcomponent/component'); diff --git a/widgy/static/widgy/js/nodes/nodes.js b/widgy/static/widgy/js/nodes/nodes.js index ad44bc8b5..86ffb9316 100644 --- a/widgy/static/widgy/js/nodes/nodes.js +++ b/widgy/static/widgy/js/nodes/nodes.js @@ -451,12 +451,7 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ renderShelf: function() { console.log('renderShelf'); - var shelf = this.shelf = new shelves.ShelfView({ - collection: new shelves.ShelfCollection({ - node: this.node - }), - app: this.app - }); + var shelf = this.shelf = this.makeShelf(); this.listenTo(shelf, 'startDrag', this.startDrag) .listenTo(shelf, 'stopDrag', this.stopDrag); @@ -479,6 +474,17 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ shelf.el.style.marginTop = margin_top + 'px'; }); } + + return shelf; + }, + + makeShelf: function() { + return new shelves.ShelfView({ + collection: new shelves.ShelfCollection({ + node: this.node + }), + app: this.app + }); }, toJSON: function() { diff --git a/widgy/static/widgy/js/shelves/shelves.js b/widgy/static/widgy/js/shelves/shelves.js index a9fadcdda..24db6d896 100644 --- a/widgy/static/widgy/js/shelves/shelves.js +++ b/widgy/static/widgy/js/shelves/shelves.js @@ -1,8 +1,8 @@ -define([ 'exports', 'underscore', 'widgy.backbone', 'nodes/base', +define([ 'jquery', 'underscore', 'widgy.backbone', 'nodes/base', 'nodes/models', 'text!nodes/preview.html', 'text!./shelf.html' - ], function(exports, _, Backbone, DraggableView, + ], function($, _, Backbone, DraggableView, node_models, shelf_item_view_template, shelf_view_template @@ -32,14 +32,11 @@ define([ 'exports', 'underscore', 'widgy.backbone', 'nodes/base', 'resizeShelf' ); - this.collection.on('add', this.addOne) - .on('sort', this.resort); + this.collection.on('add', this.addOne); this.app = options.app; this.list = new Backbone.ViewList(); - - $(window).resize(this.resizeShelf); }, addOne: function(model) { @@ -51,7 +48,6 @@ define([ 'exports', 'underscore', 'widgy.backbone', 'nodes/base', view.on('all', this.bubble); this.list.push(view); - this.$list.append(view.render().el); }, resort: function(collection) { @@ -69,8 +65,15 @@ define([ 'exports', 'underscore', 'widgy.backbone', 'nodes/base', render: function() { Backbone.View.prototype.render.apply(this, arguments); - this.$list = this.$el.children('.list'); + var $list = this.$list = this.$el.children('.list'); + this.collection.on('sort', this.resort); + + this.list.on('push', function(view) { + $list.append(view.render().el); + }); + this.resizeShelf(); + $(window).resize(this.resizeShelf); return this; }, diff --git a/widgy/static/widgy/js/widgy.backbone.js b/widgy/static/widgy/js/widgy.backbone.js index 4bd38b586..874f0bd3a 100644 --- a/widgy/static/widgy/js/widgy.backbone.js +++ b/widgy/static/widgy/js/widgy.backbone.js @@ -171,10 +171,11 @@ define([ 'jquery', 'underscore', 'backbone', 'lib/mustache', 'lib/q' ], function ); } - _.extend(ViewList.prototype, { + _.extend(ViewList.prototype, Backbone.Events, { push: function(view) { view.on('close', this.remove); this.list.push(view); + this.trigger('push', view); }, remove: function(view) { From d810b1851d0c6e4e8fb376648c36b6e2fe6cf407 Mon Sep 17 00:00:00 2001 From: Rocky Meza Date: Tue, 11 Jun 2013 17:46:42 -0600 Subject: [PATCH 0620/1061] Replaced dragTimeout with distanceTrigger Now, dragging a widget doesn't begin until you have moved at least 5 pixels. --- .../widgy/js/components/tabbed/component.js | 1 - widgy/static/widgy/js/nodes/base.js | 99 ++++++++++++++----- widgy/static/widgy/js/nodes/nodes.js | 34 +------ widgy/static/widgy/js/shelves/shelves.js | 12 +++ 4 files changed, 93 insertions(+), 53 deletions(-) diff --git a/widgy/static/widgy/js/components/tabbed/component.js b/widgy/static/widgy/js/components/tabbed/component.js index 56891343f..9720495b0 100644 --- a/widgy/static/widgy/js/components/tabbed/component.js +++ b/widgy/static/widgy/js/components/tabbed/component.js @@ -77,7 +77,6 @@ define([ 'underscore', 'widgy.backbone', 'components/widget/component' ], functi prepareChild: function(child_view) { widget.View.prototype.prepareChild.apply(this, arguments); this.listenTo(child_view, 'rendered', this.stealThingsFromChild); - child_view.dragTimeout = 100; return child_view; }, diff --git a/widgy/static/widgy/js/nodes/base.js b/widgy/static/widgy/js/nodes/base.js index 6c8d7848d..48302172a 100644 --- a/widgy/static/widgy/js/nodes/base.js +++ b/widgy/static/widgy/js/nodes/base.js @@ -6,6 +6,8 @@ define(['jquery', 'underscore', 'widgy.backbone'], function( document.body.scrollTop += amount; }, 100); + var BACKTICK = 96; + /** * Provides an interface for a draggable NodeView. See NodeView for more @@ -15,7 +17,7 @@ define(['jquery', 'underscore', 'widgy.backbone'], function( var DraggablewView = Backbone.View.extend({ tagName: 'li', className: 'node', - dragTimeout: 0, + distanceTrigger: 5, events: Backbone.extendEvents(Backbone.View, { 'mousedown .drag-row': 'onMouseDown' @@ -28,7 +30,11 @@ define(['jquery', 'underscore', 'widgy.backbone'], function( 'startBeingDragged', 'followMouse', 'stopBeingDragged', - 'canAcceptParent' + 'canAcceptParent', + 'bindDragEvents', + 'unbindDocument', + 'checkDistance', + 'debugMode' ); this @@ -59,26 +65,13 @@ define(['jquery', 'underscore', 'widgy.backbone'], function( event.stopPropagation(); // only on a left click. - if ( event.which !== 1 ) - return; - - if ( ! this.app.ready() ) - return; - - var _continue = true; - - this.$el.on('mouseup.draggable-timeout', function() { - _continue = false; - }); + if ( event.which !== 1 || ! this.app.ready() ) + return false; - setTimeout(_.bind(function() { - this.$el.off('.draggable-timeout'); - if ( _continue ) - this.startBeingDragged(event); - }, this), this.dragTimeout); - }, + // this is for checkDistance + this.originalX = event.clientX; + this.originalY = event.clientY; - startBeingDragged: function(event) { // Store the mouse offset in this container for followMouse to use. We // need to get this before `this.app.startDrag`, otherwise the drop // targets screw everything up. @@ -86,8 +79,39 @@ define(['jquery', 'underscore', 'widgy.backbone'], function( this.cursorOffsetX = event.clientX - offset.left + (event.pageX - event.clientX); this.cursorOffsetY = event.clientY - offset.top + (event.pageY - event.clientY); - // follow mouse really quick, just in case they haven't moved their mouse - // yet. + $(document) + .on('mouseup.' + this.cid, this.unbindDocument) + .on('mousemove.' + this.cid, this.checkDistance); + + $(window).on('scroll.' + this.cid, _.bind(function() { + // pass in a fake mouse event. + this.startBeingDragged({ + clientY: this.originalY, + clientX: this.originalX + }); + }, this)); + + return true; + }, + + unbindDocument: function(event) { + $(document).off('.' + this.cid); + $(window).off('.' + this.cid); + }, + + checkDistance: function(event) { + var distance = Math.sqrt(Math.pow(event.clientY - this.originalY, 2) + Math.pow(event.clientX - this.originalX, 2)); + + if ( distance > this.distanceTrigger ) { + this.startBeingDragged(event); + } + }, + + startBeingDragged: function(event) { + this.unbindDocument(); + + // follow mouse really quick, just in case they haven't moved their + // mouse yet. this.followMouse(event); this.$el.css({ @@ -96,9 +120,36 @@ define(['jquery', 'underscore', 'widgy.backbone'], function( }); this.$el.addClass('being_dragged'); + this.bindDragEvents(); this.trigger('startDrag', this); }, + bindDragEvents: function() { + var view = this; + + $(document) + .on('mouseup.' + this.cid, this.stopBeingDragged) + .on('mousemove.' + this.cid, this.followMouse) + .on('selectstart.' + this.cid, function(){ return false; }) + // debugging helper + .one('keypress.' + this.cid, function(event) { + if ( event.which === BACKTICK ) + view.debugMode(); + }); + }, + + debugMode: function(event) { + var view = this; + view.unbindDocument(); + + $(document) + // resume dragging + .one('keypress.' + this.cid, function(event) { + if ( event.which === BACKTICK ) + view.bindDragEvents(); + }); + }, + stopBeingDragged: function() { this.$el.css({ top: '', @@ -107,9 +158,11 @@ define(['jquery', 'underscore', 'widgy.backbone'], function( 'z-index': '' }); + this.unbindDocument(); this.$el.removeClass('being_dragged'); - clearInterval(this.bumpInterval); + + this.trigger('stopDrag'); }, bumpAmount: function(clientY) { diff --git a/widgy/static/widgy/js/nodes/nodes.js b/widgy/static/widgy/js/nodes/nodes.js index 86ffb9316..b2607a2c0 100644 --- a/widgy/static/widgy/js/nodes/nodes.js +++ b/widgy/static/widgy/js/nodes/nodes.js @@ -94,9 +94,9 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ return this.content.get('pop_out') === 2 && ! this.isRootNode(); }, - startBeingDragged: function(event) { + onMouseDown: function(event) { if ( $(event.target).is('.title, .drag-row, .drag_handle') && this.content.get('draggable') ) { - return DraggableView.prototype.startBeingDragged.apply(this, arguments); + return DraggableView.prototype.onMouseDown.apply(this, arguments); } else { return false; } @@ -207,27 +207,6 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ startDrag: function(dragged_view) { if ( ( this.hasShelf() && ! dragged_view.model.id || this.isRootNode() )) { this.dragged_view = dragged_view; - - var bindToDocument = _.bind(function() { - $(document) - .on('mouseup.' + dragged_view.cid, this.stopDragging) - .on('mousemove.' + dragged_view.cid, dragged_view.followMouse) - .on('selectstart.' + dragged_view.cid, function(){ return false; }) - // debugging helper - .one('keypress.' + dragged_view.cid, function(event) { - if ( event.which === 96 ) { - $(document) - .off('.' + dragged_view.cid) - // resume dragging - .one('keypress.' + dragged_view.cid, function(event) { - if ( event.which === 96 ) bindToDocument(); - }); - } - }); - }, this); - - bindToDocument(); - this.addDropTargets(dragged_view); } else { // propagate event @@ -239,11 +218,6 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ var dragged_view = this.dragged_view; delete this.dragged_view; - $(document).off('.' + dragged_view.cid); - - if ( dragged_view.placeholder ) - dragged_view.placeholder.remove(); - this.clearDropTargets(); dragged_view.stopBeingDragged(); @@ -253,7 +227,9 @@ define([ 'exports', 'jquery', 'underscore', 'widgy.backbone', 'lib/q', 'shelves/ stopDrag: function(callback) { if ( this.hasShelf() && this.dragged_view ) { - callback(this.stopDragging()); + var dragged_view = this.stopDragging(); + if ( callback ) + callback(dragged_view); } else { // propagate event this.trigger('stopDrag', callback); diff --git a/widgy/static/widgy/js/shelves/shelves.js b/widgy/static/widgy/js/shelves/shelves.js index 24db6d896..bf2674af4 100644 --- a/widgy/static/widgy/js/shelves/shelves.js +++ b/widgy/static/widgy/js/shelves/shelves.js @@ -151,6 +151,13 @@ define([ 'jquery', 'underscore', 'widgy.backbone', 'nodes/base', return this.app.validateRelationship(parent, this.model); }, + onMouseDown: function(event) { + var ret = DraggableView.prototype.onMouseDown.apply(this, arguments); + + if (ret) + this.startBeingDragged(event); + }, + startBeingDragged: function(event) { DraggableView.prototype.startBeingDragged.apply(this, arguments); @@ -161,6 +168,11 @@ define([ 'jquery', 'underscore', 'widgy.backbone', 'nodes/base', this.$el.after(placeholder); }, + stopBeingDragged: function() { + DraggableView.prototype.stopBeingDragged.apply(this, arguments); + this.placeholder.remove(); + }, + cssClasses: function() { return this.model.get('css_classes'); } From 7739178479aa616743b0e27dd1e716ba7f3c5b7f Mon Sep 17 00:00:00 2001 From: Gavin Wahl Date: Wed, 12 Jun 2013 14:15:33 -0600 Subject: [PATCH 0621/1061] Use 'here' classes in the top nav menu The li and the anchor for current_or_ascendant pages should have a 'here' class. The anchor doesn't strictly need it, but it's more convenient for the designers that way. --- .../widgy_mezzanine/templates/pages/menus/dropdown.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/widgy/contrib/widgy_mezzanine/templates/pages/menus/dropdown.html b/widgy/contrib/widgy_mezzanine/templates/pages/menus/dropdown.html index 0677e244f..ba4a37ac5 100644 --- a/widgy/contrib/widgy_mezzanine/templates/pages/menus/dropdown.html +++ b/widgy/contrib/widgy_mezzanine/templates/pages/menus/dropdown.html @@ -4,8 +4,9 @@
      {% for page in page_branch %} {% if page.in_menu %} -
    • - {{ page.title }} + + {{ page.title }} {% if page.branch_level == 0 and page.has_children_in_menu %} {% page_menu page %} {% endif %} From 305803966504baeb5b5dff44c85b36dc081b2b54 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Wed, 12 Jun 2013 09:44:52 -0600 Subject: [PATCH 0622/1061] Make Mezzanine links linkable --- tests/modeltests/core_tests/models.py | 10 ++-- tests/modeltests/core_tests/tests/links.py | 25 +++++----- widgy/contrib/widgy_mezzanine/admin.py | 4 ++ widgy/contrib/widgy_mezzanine/models.py | 6 +-- widgy/models/links.py | 58 ++++++++++++++-------- 5 files changed, 61 insertions(+), 42 deletions(-) diff --git a/tests/modeltests/core_tests/models.py b/tests/modeltests/core_tests/models.py index a13d59f5f..c99ffa75d 100644 --- a/tests/modeltests/core_tests/models.py +++ b/tests/modeltests/core_tests/models.py @@ -4,7 +4,7 @@ from widgy.db.fields import WidgyField, VersionedWidgyField from widgy import registry -from widgy.models.links import LinkableMixin, LinkField +from widgy.models import links from widgy.models.mixins import InvisibleMixin @@ -156,19 +156,21 @@ class ForeignKeyWidget(Content): registry.register(ForeignKeyWidget) -class LinkableThing(LinkableMixin, models.Model): +@links.register +class LinkableThing(models.Model): name = models.CharField(max_length=255, default='') def __unicode__(self): return self.name -class AnotherLinkableThing(LinkableMixin, models.Model): +@links.register +class AnotherLinkableThing(models.Model): pass class ThingWithLink(models.Model): - link = LinkField('linkable_content_type', 'linkable_object_id') + link = links.LinkField('linkable_content_type', 'linkable_object_id') class MyInvisibleBucket(InvisibleMixin, Content): diff --git a/tests/modeltests/core_tests/tests/links.py b/tests/modeltests/core_tests/tests/links.py index 4b01c0236..93c5a8f40 100644 --- a/tests/modeltests/core_tests/tests/links.py +++ b/tests/modeltests/core_tests/tests/links.py @@ -2,8 +2,7 @@ from django.test import TestCase from widgy.models.links import ( - get_all_linkable_classes, get_all_linker_classes, - get_link_field_from_model, LinkFormMixin, LinkFormField, + link_registry, get_link_field_from_model, LinkFormMixin, LinkFormField, get_composite_key, convert_linkable_to_choice, ) @@ -15,24 +14,24 @@ class TestLinkRelations(TestCase): def test_get_all_linkable_classes(self): - self.assertIn(LinkableThing, get_all_linkable_classes()) - self.assertIn(AnotherLinkableThing, get_all_linkable_classes()) + self.assertIn(LinkableThing, link_registry) + self.assertIn(AnotherLinkableThing, link_registry) def test_get_all_linker_classes(self): - self.assertIn(ThingWithLink, get_all_linker_classes()) - self.assertNotIn(Bucket, get_all_linker_classes()) - self.assertNotIn(VersionPageThrough, get_all_linker_classes()) + self.assertIn(ThingWithLink, link_registry.get_all_linker_classes()) + self.assertNotIn(Bucket, link_registry.get_all_linker_classes()) + self.assertNotIn(VersionPageThrough, link_registry.get_all_linker_classes()) def test_get_all_links_for_obj(self): linkable = LinkableThing.objects.create() - self.assertEqual(len(list(linkable.get_links())), 0) + self.assertEqual(len(list(link_registry.get_links(linkable))), 0) thing = ThingWithLink.objects.create( link=linkable, ) - self.assertEqual(list(linkable.get_links()), [thing]) + self.assertEqual(list(link_registry.get_links(linkable)), [thing]) linkable2 = AnotherLinkableThing.objects.create() @@ -40,7 +39,7 @@ def test_get_all_links_for_obj(self): link=linkable2, ) - self.assertEqual(list(linkable2.get_links()), [thing2]) + self.assertEqual(list(link_registry.get_links(linkable2)), [thing2]) def test_get_all_possible_linkables(self): l1 = LinkableThing.objects.create() @@ -87,11 +86,11 @@ def test_choices(self): # TODO: this has an implicit ordering check, that might be brittle. self.assertEqual(form.fields['link'].choices, [ + (AnotherLinkableThing._meta.verbose_name_plural, [ + convert_linkable_to_choice(page3), + ]), (LinkableThing._meta.verbose_name_plural, [ convert_linkable_to_choice(page2), convert_linkable_to_choice(page1), ]), - (AnotherLinkableThing._meta.verbose_name_plural, [ - convert_linkable_to_choice(page3), - ]), ]) diff --git a/widgy/contrib/widgy_mezzanine/admin.py b/widgy/contrib/widgy_mezzanine/admin.py index 16f1f4931..f278dabe8 100644 --- a/widgy/contrib/widgy_mezzanine/admin.py +++ b/widgy/contrib/widgy_mezzanine/admin.py @@ -14,10 +14,12 @@ from mezzanine.core.models import (CONTENT_STATUS_PUBLISHED, CONTENT_STATUS_DRAFT) +from mezzanine.pages.models import Link from widgy.forms import WidgyFormMixin from widgy.contrib.widgy_mezzanine import get_widgypage_model from widgy.utils import fancy_import, format_html +from widgy.models import links WidgyPage = get_widgypage_model() @@ -133,3 +135,5 @@ def __init__(self, *args, **kwargs): admin.site.register(WidgyPage, WidgyPageAdmin) admin.site.register(UndeletePage, UndeletePageAdmin) + +links.register(Link) diff --git a/widgy/contrib/widgy_mezzanine/models.py b/widgy/contrib/widgy_mezzanine/models.py index 6d3301a7f..dc511f39a 100644 --- a/widgy/contrib/widgy_mezzanine/models.py +++ b/widgy/contrib/widgy_mezzanine/models.py @@ -2,10 +2,8 @@ from django.conf import settings from django.core import urlresolvers -from widgy.models.links import LinkableMixin - -class WidgyPageMixin(LinkableMixin): +class WidgyPageMixin(object): base_template = 'widgy/mezzanine_base.html' @property @@ -42,7 +40,9 @@ def get_content_model(self): if getattr(settings, 'WIDGY_MEZZANINE_PAGE_MODEL', None) is None: from mezzanine.pages.models import Page from widgy.db.fields import VersionedWidgyField + from widgy.models import links + @links.register class WidgyPage(WidgyPageMixin, Page): root_node = VersionedWidgyField( site=settings.WIDGY_MEZZANINE_SITE, diff --git a/widgy/models/links.py b/widgy/models/links.py index 7428cd639..74af59692 100644 --- a/widgy/models/links.py +++ b/widgy/models/links.py @@ -3,26 +3,51 @@ from django.db import models from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ImproperlyConfigured from django import forms from widgy.generic import WidgyGenericForeignKey -def is_linkable(model): - return issubclass(model, LinkableMixin) and not model._meta.proxy +class LinkRegistry(set): + def register(self, model): + if model in self: + raise ImproperlyConfigured(("You cannot register the same " + "content ('{0}') twice.").format(model)) + if not issubclass(model, models.Model): + raise ImproperlyConfigured(("{0} is not a subclass of django.db.models.Model, " + "so it cannot be registered").format(model)) + self.add(model) -def has_link(model): - return any(isinstance(field, LinkField) - for field in model._meta.virtual_fields) + # This allow LinkRegistry.register to be used as a decorator + return model + def unregister(self, model): + self.remove(model) + + def get_links(self, obj): + if not isinstance(obj, tuple(self)): + raise ValueError("The object class is not registered linkable") + all_qs = (linker._default_manager.filter(points_to_links(linker, obj)) + for linker in self.get_all_linker_classes()) + + return itertools.chain.from_iterable(all_qs) + + @classmethod + def get_all_linker_classes(cls): + return filter(cls.has_link, models.get_models()) + + @classmethod + def has_link(cls, model): + return any(isinstance(field, LinkField) + for field in model._meta.virtual_fields) -def get_all_linkable_classes(): - return filter(is_linkable, models.get_models()) -def get_all_linker_classes(): - return filter(has_link, models.get_models()) +link_registry = LinkRegistry() +register = link_registry.register +unregister = link_registry.register def points_to_links(linker, linkable): @@ -37,18 +62,6 @@ def points_to_links(linker, linkable): if isinstance(field, LinkField))) -class LinkableMixin(object): - def get_links(self): - """ - Returns a heterogenous list of all things that have a LinkField that - points to self. - """ - all_qs = (linker._default_manager.filter(points_to_links(linker, self)) - for linker in get_all_linker_classes()) - - return itertools.chain.from_iterable(all_qs) - - class LinkField(WidgyGenericForeignKey): """ TODO: Explore the consequences of using add_field in contribute_to_class to @@ -56,6 +69,7 @@ class LinkField(WidgyGenericForeignKey): """ def __init__(self, ct_field=None, fk_field=None, *args, **kwargs): self.null = kwargs.pop('null', False) + self._link_registry = kwargs.pop('link_registry', link_registry) super(LinkField, self).__init__(ct_field, fk_field, *args, **kwargs) def get_choices(self): @@ -63,7 +77,7 @@ def get_choices(self): def get_choices_by_class(self): return ((Model, Model._default_manager.all()) - for Model in get_all_linkable_classes()) + for Model in self._link_registry) def contribute_to_class(self, cls, name): if self.ct_field is None: From 6a5a7f40107a92a63de6f7fd04d9b0a944417ca8 Mon Sep 17 00:00:00 2001 From: Gavin Wahl Date: Thu, 13 Jun 2013 10:14:18 -0600 Subject: [PATCH 0623/1061] Set section_behavior when rendering accordion This needs to be set when render accordions in addition to tabs. Otherwise, an accordion inside a tab will still have section_behavior=tabs. --- .../templates/widgy/page_builder/accordion/render.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/widgy/contrib/page_builder/templates/widgy/page_builder/accordion/render.html b/widgy/contrib/page_builder/templates/widgy/page_builder/accordion/render.html index f95fd2b70..33a0ffb8e 100644 --- a/widgy/contrib/page_builder/templates/widgy/page_builder/accordion/render.html +++ b/widgy/contrib/page_builder/templates/widgy/page_builder/accordion/render.html @@ -1,7 +1,9 @@ {% load widgy_tags %}
      - {% for child in self.get_children %} - {% render child %} - {% endfor %} + {% with section_behavior='accordion' %} + {% for child in self.get_children %} + {% render child %} + {% endfor %} + {% endwith %}
      From e791e48d7da3da2b44d3c4723210f4c67101c6ac Mon Sep 17 00:00:00 2001 From: Rocky Meza Date: Thu, 13 Jun 2013 11:15:25 -0600 Subject: [PATCH 0624/1061] Use a relative protocol link for the font file. --- widgy/templates/widgy/widgy_field.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widgy/templates/widgy/widgy_field.html b/widgy/templates/widgy/widgy_field.html index 6f0f86796..945c539d6 100644 --- a/widgy/templates/widgy/widgy_field.html +++ b/widgy/templates/widgy/widgy_field.html @@ -11,7 +11,7 @@ {% endcomment %} - + {% compress css %} From 2281f04152ef74c157e0d74fc0a9f1dddf3ab03f Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Thu, 13 Jun 2013 11:42:53 -0600 Subject: [PATCH 0625/1061] Fix uncaptcha template (Localization and HTML validation) --- .../templates/widgy/form_builder/uncaptcha/render.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/widgy/contrib/form_builder/templates/widgy/form_builder/uncaptcha/render.html b/widgy/contrib/form_builder/templates/widgy/form_builder/uncaptcha/render.html index 9392f385b..1c465e9ce 100644 --- a/widgy/contrib/form_builder/templates/widgy/form_builder/uncaptcha/render.html +++ b/widgy/contrib/form_builder/templates/widgy/form_builder/uncaptcha/render.html @@ -1,8 +1,8 @@ -{% load fusionbox_tags %}{{ field.errors }} +{% load fusionbox_tags i18n %}{{ field.errors }}
      - + -

      Please copy "{{ csrf_token }}" into the field labeled "Uncaptcha" +

      {% blocktrans %}Please copy "{{ csrf_token }}" into the field labeled "Uncaptcha".{% endblocktrans %}
      diff --git a/widgy/templates/widgy/revert.html b/widgy/templates/widgy/revert.html index 2baa2a941..9d18aa7f7 100644 --- a/widgy/templates/widgy/revert.html +++ b/widgy/templates/widgy/revert.html @@ -3,15 +3,7 @@
    {% compress js %} - + {% endcompress %} {{ block.super }} {% endblock %} From 3966e4aadfcaac5d206376b9afc32b2c242f8004 Mon Sep 17 00:00:00 2001 From: Rocky Meza Date: Mon, 8 Jul 2013 16:39:31 -0600 Subject: [PATCH 0677/1061] Fixed showActivity bug. The code that calls showActivity/hideActivity accessed a jQuery reference that doesn't have fancybox. We need to close over a jQuery value that does have the fancybox object. --- widgy/static/widgy/js/widgy.admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widgy/static/widgy/js/widgy.admin.js b/widgy/static/widgy/js/widgy.admin.js index a69b43d46..7d6c79ebd 100644 --- a/widgy/static/widgy/js/widgy.admin.js +++ b/widgy/static/widgy/js/widgy.admin.js @@ -1,4 +1,4 @@ -$(function() { +jQuery(function($) { $('a.widgy-fancybox').fancybox({ width: 700, type: 'iframe', From 92bc51cf3ba747c216af71d37fa31e6b567e0987 Mon Sep 17 00:00:00 2001 From: Justin Stollsteimer Date: Tue, 9 Jul 2013 14:59:43 -0600 Subject: [PATCH 0678/1061] deleted item highlight --- widgy/static/widgy/css/widgy.scss | 14 +++++++++++--- widgy/static/widgy/css/widgy_common.scss | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/widgy/static/widgy/css/widgy.scss b/widgy/static/widgy/css/widgy.scss index b445e3783..dc89c8637 100644 --- a/widgy/static/widgy/css/widgy.scss +++ b/widgy/static/widgy/css/widgy.scss @@ -214,9 +214,17 @@ li.node_drop_target { cursor: pointer; } - &.deleting * { - // fixme! - background-color: red; + &.deleting { + @include shadow($black50,0px,5px,15px); + border-color: $red; + + div.widget { + p.drag-row { + span.title { + color: $red; + } + } + } } // specific node styles diff --git a/widgy/static/widgy/css/widgy_common.scss b/widgy/static/widgy/css/widgy_common.scss index aa7570cc9..e390c13c2 100644 --- a/widgy/static/widgy/css/widgy_common.scss +++ b/widgy/static/widgy/css/widgy_common.scss @@ -132,6 +132,7 @@ -----------------------------------------------*/ $black10: rgba(0,0,0,0.1); +$black50: rgba(0,0,0,0.5); $white20: rgba(255,255,255,0.2); $reallylightgrey: #fafafa; $lightgrey: #bbbbbb; From 4f3a55ac5081ed5a828aa399fff16143e72dacf7 Mon Sep 17 00:00:00 2001 From: Justin Stollsteimer Date: Tue, 9 Jul 2013 15:05:32 -0600 Subject: [PATCH 0679/1061] rounded corner cutoff fix --- widgy/static/widgy/css/widgy.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/widgy/static/widgy/css/widgy.scss b/widgy/static/widgy/css/widgy.scss index dc89c8637..273f3b088 100644 --- a/widgy/static/widgy/css/widgy.scss +++ b/widgy/static/widgy/css/widgy.scss @@ -358,12 +358,14 @@ li.node_drop_target { } div.widget { + @include rounded(4px); background: white; padding: 0px; p.drag-row { @include clearfix; @include gradient(#eee,#fff); + @include rounded(4px); display: block; float: none; height: auto; From 51d144ef931567c7d09cc1a0987097ed38ad4ddc Mon Sep 17 00:00:00 2001 From: Gavin Wahl Date: Tue, 9 Jul 2013 15:57:42 -0600 Subject: [PATCH 0680/1061] fix intersphinx --- docs/conf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 3e08d7221..17b9d84d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -249,3 +249,8 @@ # all teh paths! sys.path.insert(0, os.path.abspath('../..')) os.environ['DJANGO_SETTINGS_MODULE'] = 'demo.settings' + +intersphinx_mapping = { + 'django': ('https://docs.djangoproject.com/en/1.5/', 'https://docs.djangoproject.com/en/1.5/_objects/'), + 'treebeard': ('https://tabo.pe/projects/django-treebeard/docs/2.0b1/', None), +} From ce54b195b49fe95a74c68f9c227a89ddc8874771 Mon Sep 17 00:00:00 2001 From: Gavin Wahl Date: Tue, 9 Jul 2013 16:59:18 -0600 Subject: [PATCH 0681/1061] doc outlines --- docs/index.rst | 3 ++ docs/models.rst | 136 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/owners.rst | 40 ++++++++++++++ docs/site.rst | 67 ++++++++++++++++++++++++ 4 files changed, 246 insertions(+) create mode 100644 docs/models.rst create mode 100644 docs/owners.rst create mode 100644 docs/site.rst diff --git a/docs/index.rst b/docs/index.rst index 92a2567e3..8f0a2c877 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,6 +20,9 @@ many different uses. versioning contrib/index links + owners + models + site Development diff --git a/docs/models.rst b/docs/models.rst new file mode 100644 index 000000000..66125e5f8 --- /dev/null +++ b/docs/models.rst @@ -0,0 +1,136 @@ +Base Models +=========== + +.. currentmodule:: widgy.models.base + +.. class:: Content + + .. attribute:: draggable = True + + .. attribute:: deletable = True + + .. attribute:: editable = False + + .. attribute:: accepting_children = False + + .. attribute:: shelf = False + + .. attribute:: component_name = 'widget' + + .. attribute:: pop_out = CANNOT_POP_OUT + + .. attribute:: CANNOT_POP_OUT + + .. attribute:: CAN_POP_OUT + + .. attribute:: MUST_POP_OUT + + .. attribute:: form = ModelForm + + .. attribute:: formfield_overrides = {} + + .. attribute:: node + + .. attribute:: class_name + + .. attribute:: display_name + + + .. method:: to_json(self, site) + + .. method:: get_attributes(self) + + .. classmethod:: class_to_json(cls, site) + + .. method:: css_classes(self) + + .. method:: get_root(self) + + .. method:: get_ancestors(self) + + .. method:: depth_first_order(self) + + .. method:: get_children(self) + + .. method:: get_next_sibling(self) + + .. method:: get_parent(self) + + .. method:: get_form_class(self, request) + + .. method:: get_form(self, request, **form_kwargs) + + .. method:: valid_parent_of(self, cls, obj=None) + + .. classmethod:: valid_child_of(cls, parent, obj=None) + + .. classmethod:: add_root(cls, site, **kwargs) + + .. method:: add_child(self, site, cls, **kwargs) + + .. method:: add_sibling(self, site, cls, **kwargs) + + .. method:: post_create(self, site) + + .. classmethod:: get_templates_hierarchy(cls, **kwargs) + + .. classmethod:: get_template_kwargs(cls, **kwargs) + + .. method:: preview_templates(self) + + .. method:: edit_templates(self) + + .. method:: get_render_templates(self, context) + + .. method:: get_form_template(self, request, template=None, context=None) + + .. method:: get_preview_template(self, site) + + .. method:: render(self, context, template=None) + + .. method:: formfield_for_dbfield(self, db_field, **kwargs) + + .. method:: get_templates(self, request) + + .. method:: reposition(self, site, right=None, parent=None) + + .. method:: delete(self, raw=False) + + .. method:: clone(self) + + .. method:: save(self, *args, **kwargs) + + +.. class:: Node + + .. attribute:: content + + .. attribute:: is_frozen + + .. method:: to_json(self, site) + + .. method:: render(self, *args, **kwargs) + + .. method:: depth_first_order(self) + + .. classmethod:: prefetch_trees(cls, *root_nodes) + + .. method:: prefetch_tree(self) + + .. method:: maybe_prefetch_tree(self) + + .. method:: filter_child_classes(self, site, classes) + + .. method:: filter_child_classes_recursive(self, site, classes) + + .. method:: possible_parents(self, site, root_node) + + .. method:: clone_tree(self, freeze=True) + + .. method:: check_frozen(self) + + .. method:: delete(self, *args, **kwargs) + + .. method:: trees_equal(self, other) + + .. classmethod:: find_widgy_problems(cls, site=None) diff --git a/docs/owners.rst b/docs/owners.rst new file mode 100644 index 000000000..2dc18b613 --- /dev/null +++ b/docs/owners.rst @@ -0,0 +1,40 @@ +Owners +====== + +A Widgy owner is a model that has a :class:`~widgy.db.fields.WidgyField`. + +Owners should + + - Use a ``WidgyField`` + - Use :class:`~widgy.admin.WidgyAdmin` (or a :class:`~widgy.forms.WidgyForm` + for your admin form) + - ``widgy.contrib.form_builder`` requires a ``get_form_action`` method on the + owner. It accepts the form widget and the widgy context, and returns a URL + for forms to submit to. You normally submit to your own view and mix in + :class:`~widgy.contrib.form_builder.views.HandleFormMixin` to help with + handling the form submission. Make sure rerendering after a validation + error works. + - It's probably a good idea to render the entire page through widgy, so I've + used a template like this: + + .. code-block:: html+django + + {# product_list.html #} + + {% include widgy_tags %}{% render_root category 'content' %} + + I have been inserting the 'view' style functionality, in this case a list + of products in a category, with ``ProductList`` widget. + + - If layouts should extend something other than ``widgy_base.html``, set the + ``base_template`` property on your owner. + - Use ``get_action_links`` to add a preview button to the editor. You'll + probably have to add support for ``root_node_override`` to your view, like + this:: + + root_node_pk = self.kwargs.get('root_node_pk') + if root_node_pk: + site.authorize_view(self.request, self) + kwargs['root_node_override'] = get_object_or_404(Node, pk=root_node_pk) + elif hasattr(self, 'form_node'): + kwargs['root_node_override'] = self.form_node.get_root() diff --git a/docs/site.rst b/docs/site.rst new file mode 100644 index 000000000..077b50002 --- /dev/null +++ b/docs/site.rst @@ -0,0 +1,67 @@ +Widgy Site +========== + + +.. currentmodule:: widgy.site + +.. class:: WidgySite + + .. attribute:: scss_files + + .. attribute:: js_files + + .. attribute:: admin_scss_files + + .. method:: get_registry(self) + + .. method:: get_all_content_classes(self) + + .. method:: get_urls(self) + + .. method:: urls(self) + + .. method:: reverse(self, *args, **kwargs) + + .. method:: get_view_instance(self, view) + + .. method:: authorize_view(self, request, view) + + .. method:: has_add_permission(self, request, content_class) + + .. method:: has_change_permission(self, request, obj_or_class) + + .. method:: has_delete_permission(self, request, obj_or_class) + + .. method:: node_view(self) + + .. method:: content_view(self) + + .. method:: shelf_view(self) + + .. method:: node_edit_view(self) + + .. method:: node_templates_view(self) + + .. method:: node_parents_view(self) + + .. method:: commit_view(self) + + .. method:: history_view(self) + + .. method:: revert_view(self) + + .. method:: diff_view(self) + + .. method:: reset_view(self) + + .. method:: valid_parent_of(self, parent, child_class, child=None) + + .. method:: valid_child_of(self, parent, child_class, child=None) + + .. method:: validate_relationship(self, parent, child) + + .. method:: get_version_tracker_model(self) + + .. method:: filter_existing_staticfiles(self, filename) + + .. method:: find_media_files(self, extension, hierarchy=['widgy/{app_label}/{module_name}{extension}']) From 20115a00030d00dfb5dd5167847d3e720b503caf Mon Sep 17 00:00:00 2001 From: Aaron Merriam Date: Wed, 10 Jul 2013 10:09:55 -0600 Subject: [PATCH 0682/1061] Fun Fact: The internet is comprised mostly of cats --- widgy/cats.py | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 widgy/cats.py diff --git a/widgy/cats.py b/widgy/cats.py new file mode 100644 index 000000000..91c1ec22f --- /dev/null +++ b/widgy/cats.py @@ -0,0 +1,105 @@ +# * ,MMM8&&&. * +# MMMM88&&&&& . +# MMMM88&&&&&&& +# * MMM88&&&&&&&& +# MMM88&&&&&&&& +# 'MMM88&&&&&&' +# 'MMM8&&&' * +# |\___/| +# ) ( . ' +# =\ /= +# )===( * +# / \ +# | | +# / \ +# \ / +# _/\_/\_/\__ _/_/\_/\_/\_/\_/\_/\_/\_/\_/\_ +# | | | |( ( | | | | | | | | | | +# | | | | ) ) | | | | | | | | | | +# | | | |(_( | | | | | | | | | | +# | | | | | | | | | | | | | | | +# | | | | | | | | | | | | | | | +# * ,MMM8&&&. * +# MMMM88&&&&& . +# MMMM88&&&&&&& +# * MMM88&&&&&&&& +# MMM88&&&&&&&& +# 'MMM88&&&&&&' +# 'MMM8&&&' * +# |\___/| +# =) ^Y^ (= . ' +# \ ^ / +# )=*=( * +# / \ +# | | +# /| | | |\ +# \| | |_|/\ +# /\_/\_//_// ___/\_/\_/\_/\_/\_/\_/\_/\_/\_ +# | | | | \_) | | | | | | | | | | +# | | | | | | | | | | | | | | | +# | | | | | | | | | | | | | | | +# | | | | | | | | | | | | | | | +# | | | | | | | | | | | | | | | +# * ,MMM8&&&. * +# MMMM88&&&&& . +# MMMM88&&&&&&& +# * MMM88&&&&&&&& +# MMM88&&&&&&&& +# 'MMM88&&&&&&' +# 'MMM8&&&' * _ +# |\___/| \\ +# =) ^Y^ (= |\_/| || ' +# \ ^ / )a a '._.-""""-. // +# )=*=( =\T_= / ~ ~ \// +# / \ `"`\ ~ / ~ / +# | | |~ \ | ~/ +# /| | | |\ \ ~/- \ ~\ +# \| | |_|/| || | // /` +# /\__/\_//_// __//\_/\_/\_((_|\((_//\_/\_/\_ +# | | | | \_) | | | | | | | | | | +# | | | | | | | | | | | | | | | +# | | | | | | | | | | | | | | | +# | | | | | | | | | | | | | | | +# | | | | | | | | | | | | | | | +# * ,MMM8&&&. * +# MMMM88&&&&& . +# MMMM88&&&&&&& +# * MMM88&&&&&&&& +# MMM88&&&&&&&& +# 'MMM88&&&&&&' +# 'MMM8&&&' * +# |\___/| /\___/\ +# ) ( ) ~( . ' +# =\ /= =\~ /= +# )===( ) ~ ( +# / \ / \ +# | | ) ~ ( +# / \ / ~ \ +# \ / \~ ~/ +# _/\_/\_/\__ _/_/\_/\__~__/_/\_/\_/\_/\_/\_ +# | | | |( ( | | | )) | | | | | | +# | | | | ) ) | | |//| | | | | | | +# | | | |(_( | | (( | | | | | | | +# | | | | | | | |\)| | | | | | | +# | | | | | | | | | | | | | | | +# * ,MMM8&&&. * +# MMMM88&&&&& . +# MMMM88&&&&&&& +# * MMM88&&&&&&&& +# MMM88&&&&&&&& +# 'MMM88&&&&&&' +# 'MMM8&&&' * +# /\/|_ __/\\ +# / -\ /- ~\ . ' +# \ = Y =T_ = / +# )==*(` `) ~ \ +# / \ / \ +# | | ) ~ ( +# / \ / ~ \ +# \ / \~ ~/ +# _/\_/\_/\__ _/_/\_/\__~__/_/\_/\_/\_/\_/\_ +# | | | | ) ) | | | (( | | | | | | +# | | | |( ( | | | \\ | | | | | | +# | | | | )_) | | | |))| | | | | | +# | | | | | | | | (/ | | | | | | +# | | | | | | | | | | | | | | | From 91f4f2bbc9b8136e770d5a3b70d9a5264dcc59c4 Mon Sep 17 00:00:00 2001 From: Justin Stollsteimer Date: Wed, 10 Jul 2013 10:34:55 -0600 Subject: [PATCH 0683/1061] for tables with an edit option on each table row, the edit button is now displayed, reduced, and alongside the 'delete row' button --- widgy/static/widgy/css/widgy.scss | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/widgy/static/widgy/css/widgy.scss b/widgy/static/widgy/css/widgy.scss index 273f3b088..0dcc1b921 100644 --- a/widgy/static/widgy/css/widgy.scss +++ b/widgy/static/widgy/css/widgy.scss @@ -541,7 +541,7 @@ li.node_drop_target { border: 0px; margin: 0 1em; overflow: visible; - padding: 0px 0px 0px 40px; + padding: 0px 0px 0px 60px; > div.widget { > p.drag-row { @@ -675,7 +675,7 @@ li.node_drop_target { border: 0px; margin: 0 1em; overflow: visible; - padding: 0px 0px 0px 40px; + padding: 0px 0px 0px 60px; div.widget { ul.nodeChildren { @@ -718,6 +718,26 @@ li.node_drop_target { display: none; } } + + button.edit { + font-size: 9px; + line-height: 9px; + height: 15px; + padding: 0px; + position: absolute; + top: 3px; + left: -60px; + width: 15px; + z-index: 10; + + i { + width: 1em; + } + + span { + display: none; + } + } } ul.nodeChildren { @@ -845,7 +865,7 @@ li.node_drop_target { width: auto !important; span.title { - background-position: 5px 5px; + background-position: 5px 3px; font-size: 11px; padding: 2px 0px 2px 26px; text-transform: capitalize; From fedc37a916aa4dd7e96eddeeb7ff2d870ebaae40 Mon Sep 17 00:00:00 2001 From: Justin Stollsteimer Date: Wed, 10 Jul 2013 10:26:08 -0600 Subject: [PATCH 0684/1061] unsafe HTML widget icon --- .../static/widgy/page_builder/admin.scss | 1 + .../widgy/image/widget-skull-and-crossbones.png | Bin 0 -> 1320 bytes 2 files changed, 1 insertion(+) create mode 100644 widgy/static/widgy/image/widget-skull-and-crossbones.png diff --git a/widgy/contrib/page_builder/static/widgy/page_builder/admin.scss b/widgy/contrib/page_builder/static/widgy/page_builder/admin.scss index b795ae7e3..de3300176 100644 --- a/widgy/contrib/page_builder/static/widgy/page_builder/admin.scss +++ b/widgy/contrib/page_builder/static/widgy/page_builder/admin.scss @@ -16,4 +16,5 @@ li.shelfItem { @include node-icon("page_builder.tablerow", "../image/widget-tablerow.png"); @include node-icon("page_builder.tabs", "../image/widget-tab.png"); @include node-icon("page_builder.video", "../image/widget-video.gif"); + @include node-icon("page_builder.unsafehtml", "../image/widget-skull-and-crossbones.png"); } diff --git a/widgy/static/widgy/image/widget-skull-and-crossbones.png b/widgy/static/widgy/image/widget-skull-and-crossbones.png new file mode 100644 index 0000000000000000000000000000000000000000..3e85c1e8c65a24e75eed331e7fec29e20ff775cf GIT binary patch literal 1320 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+nA0*tB1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxKsVXI%s|1+P|wiV z#N6CmN5ROz&_Lh7NZ-&%*U;R`*vQJjKmiJrfVLH-q*(>IxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8EkR}&8R-I5=oVMzl_XZ^<`pZ$OmImpPA){ffi_eM3D1{oGuTzrd=COM+4n&cLd=IHa;5RX-@T zIKQ+g85kdF$}r8qu)}W=NFmTQR{lkqz(`5Vami0E%}vcK@pQ3O0?O#6WTsdd7+M$@ z8k(CJm>U_GSr{5RS(q5O8X1`wTAG_VnVXxy%)qAC$j!vU*~!4g)zA@WhO3dKld*}L zsU^@f3s*~LV^f%3&%EN2#JuEGn7x@md!c%r@#?j5E=o--$uA1Y&(DFSfPjqrlKkR~ z`~n5%U{eL(#N^C85FZph5Z}6_7G;*DrnnX5=PH21*D4c>{RWO`{x)&_yNiK=F~ZZu zF{I+wl*?zeT>>SJf3$qO^oEDd8l5!PjT<)0M*4`AIcI77W7O`Nw3qdoP*_)vN0?Wy zi0aKVPD!ns93wjlYUS&Kzt7uQerlzEas0gBbDqz6-oxFb@Ra{~Oy7LQEsv$Q-%$Ow zL+OUl6vJ5?^81C?v_G!j|53;%{HMi_+4{2--iltC{OkD#VVkV8lV@C9bAHAFryRC? z&dNKlce!!$mCipnbwA7V@dlOEqKA|>?ckY?V9@1asH`LiDTC58>n)~Rg*{(lt+#47fmTT+u T%c(iXg32yWS3j3^P6 Date: Wed, 10 Jul 2013 12:02:28 -0600 Subject: [PATCH 0685/1061] form styles for top-node editors --- widgy/static/widgy/css/widgy.scss | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/widgy/static/widgy/css/widgy.scss b/widgy/static/widgy/css/widgy.scss index 0dcc1b921..184bcadf7 100644 --- a/widgy/static/widgy/css/widgy.scss +++ b/widgy/static/widgy/css/widgy.scss @@ -925,11 +925,48 @@ div.widgy { padding: 5px 0; } + p.drag-row { + button { + position: relative; + top: -5px; + } + } + > ul.nodeChildren { clear: left; float: left; width: 63% !important; } + + > div.preview { + // top-level forms + width: 63%; + + > div { + margin: 0px 0px 10px 0px; + } + + .formField { + clear: both; + display: block; + margin: 0px 0px 10px 0px; + } + + button, input[type='submit'] { + @include button; + float: left; + margin-left: 10px; + + &.cancel { + line-height: 11px; + margin-left: 130px; + } + } + + p.help_text { + padding-left: 130px; + } + } } // yet another wrapper From 0f128f7ef6a19b29bb2ff06f2fd4e639a79ba149 Mon Sep 17 00:00:00 2001 From: Gavin Wahl Date: Fri, 12 Jul 2013 11:17:13 -0600 Subject: [PATCH 0686/1061] Make qs argument optional in PatchUrlconfMiddleware.get_urlconf signalhandlers.patch_url_conf calls get_urlconf with a single argument. --- widgy/contrib/urlconf_include/middleware.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/widgy/contrib/urlconf_include/middleware.py b/widgy/contrib/urlconf_include/middleware.py index e8b000cf2..5ae163f87 100644 --- a/widgy/contrib/urlconf_include/middleware.py +++ b/widgy/contrib/urlconf_include/middleware.py @@ -29,7 +29,9 @@ def get_pages(cls, logged_in): return qs @classmethod - def get_urlconf(cls, root_urlconf, qs): + def get_urlconf(cls, root_urlconf, qs=None): + if qs is None: + qs = cls.get_pages(logged_in=False) urlconf_pages = sorted(qs, key=lambda p: len(p.slug)) new_urlconf = imp.new_module('urlconf') From 0aa8360cd3df65c5f8d7ee2878eed836124084b6 Mon Sep 17 00:00:00 2001 From: Rocky Meza Date: Fri, 12 Jul 2013 17:57:16 -0600 Subject: [PATCH 0687/1061] reorganized documentation --- docs/api/index.rst | 10 +++ docs/{ => api}/links.rst | 0 docs/{ => api}/site.rst | 0 docs/contrib/form-builder/index.rst | 14 +++- docs/contrib/page-builder/index.rst | 9 ++- docs/contrib/widgy-mezzanine/index.rst | 18 ++++++ docs/customizing.rst | 5 -- .../_images/interface-example.png | Bin docs/design/data-model.rst | 38 +++++++++++ docs/design/index.rst | 20 ++++++ docs/design/javascript.rst | 12 ++++ docs/{ => design}/models.rst | 0 docs/design/owners.rst | 58 +++++++++++++++++ docs/design/site.rst | 48 ++++++++++++++ docs/{ => design}/versioning.rst | 5 ++ docs/index.rst | 12 +--- docs/intro.rst | 61 ------------------ docs/owners.rst | 40 ------------ docs/roadmap.md | 42 ++++++++++++ docs/tutorial.rst | 5 -- docs/tutorials/index.rst | 10 +++ .../widgy-mezzanine-tutorial.rst} | 0 22 files changed, 284 insertions(+), 123 deletions(-) create mode 100644 docs/api/index.rst rename docs/{ => api}/links.rst (100%) rename docs/{ => api}/site.rst (100%) delete mode 100644 docs/customizing.rst rename docs/{ => design}/_images/interface-example.png (100%) create mode 100644 docs/design/data-model.rst create mode 100644 docs/design/index.rst create mode 100644 docs/design/javascript.rst rename docs/{ => design}/models.rst (100%) create mode 100644 docs/design/owners.rst create mode 100644 docs/design/site.rst rename docs/{ => design}/versioning.rst (96%) delete mode 100644 docs/intro.rst delete mode 100644 docs/owners.rst create mode 100644 docs/roadmap.md delete mode 100644 docs/tutorial.rst create mode 100644 docs/tutorials/index.rst rename docs/{quickstart.rst => tutorials/widgy-mezzanine-tutorial.rst} (100%) diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 000000000..a350c878f --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,10 @@ +API Reference +============= + +.. toctree:: + :maxdepth: 1 + + fields + forms + site + links diff --git a/docs/links.rst b/docs/api/links.rst similarity index 100% rename from docs/links.rst rename to docs/api/links.rst diff --git a/docs/site.rst b/docs/api/site.rst similarity index 100% rename from docs/site.rst rename to docs/api/site.rst diff --git a/docs/contrib/form-builder/index.rst b/docs/contrib/form-builder/index.rst index 612a1db85..298d2d760 100644 --- a/docs/contrib/form-builder/index.rst +++ b/docs/contrib/form-builder/index.rst @@ -53,11 +53,21 @@ Views even more robust example implementation. +.. currentmodule:: widgy.contrib.form_builder.models + +Success Handlers +---------------- + +When a user submits a :class:`Form`, the :class:`Form` will loop through all of +the success handler widgets to do the things that you would normally put in the +``form_valid`` method of a :class:`~django:django.views.generic.FormView`, for +example. Form Builder provides a couple of built-in success handlers that do +things like saving the data, sending emails, or submitting to Salesforce. + + Widgets ------- -.. currentmodule:: widgy.contrib.form_builder.models - .. class:: Form This widget corresponds to the HTML ``
    `` tag. It acts as a container diff --git a/docs/contrib/page-builder/index.rst b/docs/contrib/page-builder/index.rst index 9f9dac43c..a18231853 100644 --- a/docs/contrib/page-builder/index.rst +++ b/docs/contrib/page-builder/index.rst @@ -29,7 +29,7 @@ Page builder is a collection of widgets for the purpose of creating HTML pages. capability to add images or tables, because there are already widgets that the developer can control. - .. warning:: + .. note:: There is a possible permission escalation vulnerability with allowing any admin user to add HTML. For this reason, the :class:`Html` widget @@ -42,6 +42,13 @@ Page builder is a collection of widgets for the purpose of creating HTML pages. unsafe because a non-superuser could gain publishing the unsafe HTML on the website with XSS code to cause permission escalation. + .. warning:: + + The ``page_builder.add_unsafehtml`` and ``page_builder.edit_unsafehtml`` + permissions are equivalent to ``is_superuser`` status because of the + possibility of a staff user inserting JavaScript that a superuser will + execute. + .. class:: CalloutWidget diff --git a/docs/contrib/widgy-mezzanine/index.rst b/docs/contrib/widgy-mezzanine/index.rst index 6388b9d58..b3d9abdf6 100644 --- a/docs/contrib/widgy-mezzanine/index.rst +++ b/docs/contrib/widgy-mezzanine/index.rst @@ -7,5 +7,23 @@ providing a subclass of Mezzanine's Page model called :class:`~widgy.contrib.widgy_mezzanine.models.WidgyPage` which delegates to Page Builder for all content. +In order to use Widgy Mezzanine, you must provide ``WIDGY_MEZZANINE_SITE`` in +your settings. This is a fully-qualified import path to an instance of +:class:`~widgy.site.WidgySite`. You also need to install the URLs. :: + + url(r'^widgy-mezzanine/', include('widgy.contrib.widgy_mezzanine.urls')), + + +.. class:: widgy.contrib.widgy_mezzanine.models.WidgyPage + + The :class:`~widgy.content.widgy_mezzanine.models.WidgyPage` class is + ``swappable`` like :class:`~django.contrib.auth.models.User`. If you want to + override it, specify a ``WIDGY_MEZZANINE_PAGE_MODEL`` in your settings. the + :class:`widgy.contrib.widgy_mezzanine.models.WidgyPageMixin` mixin is + provided for ease of overriding. Any code that references a + :class:`~widgy.contrib.widgy_mezzanine.models.WidgyPage` should use the + :func:`widgy.contrib.widgy_mezzanine.get_widgypage_model` to get the + correct class. + .. _Mezzanine: http://mezzanine.jupo.org/ diff --git a/docs/customizing.rst b/docs/customizing.rst deleted file mode 100644 index eadf7d02f..000000000 --- a/docs/customizing.rst +++ /dev/null @@ -1,5 +0,0 @@ -Customization -============= - -.. outline customization options here, mention things like draggable/deletable, - also mention proxying, also mention JavaScript components diff --git a/docs/_images/interface-example.png b/docs/design/_images/interface-example.png similarity index 100% rename from docs/_images/interface-example.png rename to docs/design/_images/interface-example.png diff --git a/docs/design/data-model.rst b/docs/design/data-model.rst new file mode 100644 index 000000000..4405f16c1 --- /dev/null +++ b/docs/design/data-model.rst @@ -0,0 +1,38 @@ +Data Model +========== + +Central to Widgy are Nodes, Contents, and Widgets. :class:`~widgy.models.Node` +is a subclass of Treebeard's :class:`~treebeard:treebeard.mp_tree.MP_Node`. +Nodes concern themselves with the tree structure. Each Node is associated with +an instance of :class:`~widgy.models.Content` subclass. A Node + Content +combination is called a Widget. + +Storing all the structure data in Node and having that point to any subclass of +Content allows us to have all the benefits of a tree, but also the flexibility +to store very different data within a tree. + +:class:`Nodes ` are associated with their +:class:`~widgy.models.Content` through a +:class:`~django:django.contrib.contenttypes.generic.GenericForeignKey`. + +This is what a hypothetical Widgy tree might look like.:: + + Node (TwoColumnLayout) + | + +-- Node (MainBucket) + | | + | +-- Node (Text) + | | + | +-- Node (Image) + | | + | +-- Node (Form) + | | + | +-- Node (Input) + | | + | +-- Node (Checkboxes) + | | + | +-- Node (SubmitButton) + | + +-- Node (SidebarBucket) + | + +-- Node (CallToAction) diff --git a/docs/design/index.rst b/docs/design/index.rst new file mode 100644 index 000000000..b74d65180 --- /dev/null +++ b/docs/design/index.rst @@ -0,0 +1,20 @@ +Design +====== + +django-widgy is a heterogeneous tree editor for Django. It enables you to +combine different model of different types into a tree structure. + +The django-widgy project is split into two main pieces. Widgy core provides +:class:`~widgy.models.Node`, the :class:`~widgy.models.Content` abstract class, +versioning models, views, configuration helpers, and the JavaScript editor +code. Much like in Django, django-widgy has many contrib packages that provide +the batteries. + +.. toctree:: + :maxdepth: 1 + + data-model + versioning + site + owners + javascript diff --git a/docs/design/javascript.rst b/docs/design/javascript.rst new file mode 100644 index 000000000..f26a94800 --- /dev/null +++ b/docs/design/javascript.rst @@ -0,0 +1,12 @@ +Editor +====== + +Widgy provides a drag and drop JavaScript editor interface to the tree in the +form of a Django formfield. + +.. figure:: _images/interface-example.png + :scale: 50 % + :alt: Interface example + +The editor is built on Backbone.js and RequireJS to provide a modular and +customizable interface. diff --git a/docs/models.rst b/docs/design/models.rst similarity index 100% rename from docs/models.rst rename to docs/design/models.rst diff --git a/docs/design/owners.rst b/docs/design/owners.rst new file mode 100644 index 000000000..bbbeca488 --- /dev/null +++ b/docs/design/owners.rst @@ -0,0 +1,58 @@ +Owners +====== + +A Widgy owner is a model that has a :class:`~widgy.db.fields.WidgyField`. + +Admin +----- + +Use :class:`~widgy.admin.WidgyAdmin` (or a :class:`~widgy.forms.WidgyForm` for +your admin form) + +Use ``get_action_links`` to add a preview button to the editor. + + +Page Builder +------------ +If layouts should extend something other than ``layout_base.html``, set the +``base_template`` property on your owner. + + +Form Builder +------------ +``widgy.contrib.form_builder`` requires a ``get_form_action`` method on the +owner. It accepts the form widget and the Widgy context, and returns a URL for +forms to submit to. You normally submit to your own view and mix in +:class:`~widgy.contrib.form_builder.views.HandleFormMixin` to help with handling +the form submission. Make sure re-rendering after a validation error works. + + +.. todo:: + + tutorials/owner + + +Tutorial +-------- + - It's probably a good idea to render the entire page through Widgy, so I've + used a template like this: + + .. code-block:: html+django + + {# product_list.html #} + + {% include widgy_tags %}{% render_root category 'content' %} + + I have been inserting the 'view' style functionality, in this case a list + of products in a category, with ``ProductList`` widget. + + +You'll probably have to add support for ``root_node_override`` to your view, +like this:: + + root_node_pk = self.kwargs.get('root_node_pk') + if root_node_pk: + site.authorize_view(self.request, self) + kwargs['root_node_override'] = get_object_or_404(Node, pk=root_node_pk) + elif hasattr(self, 'form_node'): + kwargs['root_node_override'] = self.form_node.get_root() diff --git a/docs/design/site.rst b/docs/design/site.rst new file mode 100644 index 000000000..886ca251c --- /dev/null +++ b/docs/design/site.rst @@ -0,0 +1,48 @@ +Customization +============= + +.. outline customization options here, mention proxying + + +There are two main ways to customize the behavior of Widgy and existing widgets. +The first is through the :class:`~widgy.site.WidgySite`. +:class:`~widgy.site.WidgySite` is a centralized source of configuration for +a Widgy instance, much like Django's +:class:`~django:django.contrib.admin.AdminSite`. You can also configure each +widget's behavior by subclassing it with a proxy. + + +WidgySite +--------- + +- tracks installed widgets +- stores URLs +- authorization +- allows centralized overriding of compatibility +- allows having more than one instance of widgy + +Proxying a Widget +----------------- + +Widgy uses a special subclass of +:class:`~django.contrib.contenttypes.generic.GenericForeignKey` that supports +retrieving proxy models. Subclassing a model as a proxy is a lightweight method +for providing custom behavior for widgets that you don't control. For example, +if you wanted to override the compatibility and ``verbose_name`` for Page +Builder's :class:`~widgy.contrib.page_builder.models.CalloutBucket`, you could +do the following:: + + import widgy + from widgy.contrib.page_builder.models import CalloutBucket + + widgy.unregister(CalloutBucket) + + @widgy.register + class MyCalloutBucket(CalloutBucket): + class Meta: + proxy = True + verbose_name = 'Awesome Callout' + + def valid_parent_of(self, cls, obj=None): + return issubclass(cls, (MyWidget)) or \ + super(MyCalloutBucket, self).valid_parent_of(self, cls, obj) diff --git a/docs/versioning.rst b/docs/design/versioning.rst similarity index 96% rename from docs/versioning.rst rename to docs/design/versioning.rst index 48f1ff892..c8c88fa5e 100644 --- a/docs/versioning.rst +++ b/docs/design/versioning.rst @@ -16,3 +16,8 @@ publish time for a commit that allows a user to future publish content. To enable versioning, all you need to do is use :class:`widgy.db.fields.VersionedWidgyfield` instead of :class:`widgy.db.fields.WidgyField`. + + +.. todo:: + + diagram diff --git a/docs/index.rst b/docs/index.rst index 8f0a2c877..b0749e2d7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,16 +13,10 @@ many different uses. .. toctree:: :maxdepth: 2 - intro - quickstart - tutorial - customizing - versioning + design/index contrib/index - links - owners - models - site + api/index + tutorials/index Development diff --git a/docs/intro.rst b/docs/intro.rst deleted file mode 100644 index cc6901580..000000000 --- a/docs/intro.rst +++ /dev/null @@ -1,61 +0,0 @@ -Introduction -============ - -django-widgy is a heterogeneous tree editor for Django. It enables you to -combine different model of different types into a tree structure. - -The django-widgy project is split into two main pieces. Widgy core provides -:class:`~widgy.models.Node`, the :class:`~widgy.models.Content` abstract class, -versioning models, views, configuration helpers, and the JavaScript editor -code. Much like in Django, django-widgy has many contrib packages that provide -the batteries. - - -Data Model ----------- - -Central to Widgy are Nodes, Contents, and Widgets. :class:`~widgy.models.Node` -is a subclass of Treebeard's :class:`~treebeard:treebeard.mp_tree.MP_Node`. -Nodes concern themselves with the tree structure. Each Node is associated with -an instance of :class:`~widgy.models.Content` subclass. A Node + Content -combination is called a Widget. - -Storing all the structure data in Node and having that point to any subclass of -Content allows us to have all the benefits of a tree, but also the flexibility -to store very different data within a tree. - -.. todo:: - - Give an example, maybe something with this sort of diagram:: - - Node (TwoColumnLayout) - | - +-- Node (MainBucket) - | | - | +-- Node (Text) - | | - | +-- Node (Image) - | | - | +-- Node (Form) - | | - | +-- Node (Input) - | | - | +-- Node (Checkboxes) - | - +-- Node (SidebarBucket) - | - +-- Node (CallToAction) - - -Editor ------- - -Widgy provides a drag and drop JavaScript editor interface to the tree in the -form of a Django formfield. - -.. figure:: _images/interface-example.png - :scale: 50 % - :alt: Interface example - -The editor is built on Backbone.js and RequireJS to provide a modular and -customizable interface. diff --git a/docs/owners.rst b/docs/owners.rst deleted file mode 100644 index 2dc18b613..000000000 --- a/docs/owners.rst +++ /dev/null @@ -1,40 +0,0 @@ -Owners -====== - -A Widgy owner is a model that has a :class:`~widgy.db.fields.WidgyField`. - -Owners should - - - Use a ``WidgyField`` - - Use :class:`~widgy.admin.WidgyAdmin` (or a :class:`~widgy.forms.WidgyForm` - for your admin form) - - ``widgy.contrib.form_builder`` requires a ``get_form_action`` method on the - owner. It accepts the form widget and the widgy context, and returns a URL - for forms to submit to. You normally submit to your own view and mix in - :class:`~widgy.contrib.form_builder.views.HandleFormMixin` to help with - handling the form submission. Make sure rerendering after a validation - error works. - - It's probably a good idea to render the entire page through widgy, so I've - used a template like this: - - .. code-block:: html+django - - {# product_list.html #} - - {% include widgy_tags %}{% render_root category 'content' %} - - I have been inserting the 'view' style functionality, in this case a list - of products in a category, with ``ProductList`` widget. - - - If layouts should extend something other than ``widgy_base.html``, set the - ``base_template`` property on your owner. - - Use ``get_action_links`` to add a preview button to the editor. You'll - probably have to add support for ``root_node_override`` to your view, like - this:: - - root_node_pk = self.kwargs.get('root_node_pk') - if root_node_pk: - site.authorize_view(self.request, self) - kwargs['root_node_override'] = get_object_or_404(Node, pk=root_node_pk) - elif hasattr(self, 'form_node'): - kwargs['root_node_override'] = self.form_node.get_root() diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 000000000..43d7b4acf --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,42 @@ +Design + + Data Model + Base Model structure + Versioning + WidgySite (rationale) + Owner Contract + JavaScript (low priority) + +Contrib Packages + + Page Builder (list of widgets) + Form Builder + - installation (HandleFormMixin) + - list of widgets + - Form API + - success handlers + Widgy Mezzanine + - installation + Review Queue + - installation / permissions + +API Reference + + Model Fields + Forms/Formfields + Node API + Content API + WidgySite (API) + Links API + + +Tutorials + + Tutorial with Widgy Mezzanine + - start at startproject + - install Mezzanine + - end at creating your own widget. + Creating your first Widget + Creating a custom widgy owner + - views + Creating a component (low priority) diff --git a/docs/tutorial.rst b/docs/tutorial.rst deleted file mode 100644 index aafde12b5..000000000 --- a/docs/tutorial.rst +++ /dev/null @@ -1,5 +0,0 @@ -Tutorial: Creating Your First Widget -==================================== - -.. scenario, you have a blog app, but are tired of CKEditor and want - to add a photo gallery or something. diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst new file mode 100644 index 000000000..77fc1380b --- /dev/null +++ b/docs/tutorials/index.rst @@ -0,0 +1,10 @@ +Tutorials +========= + + +.. toctree:: + :maxdepth: 1 + + widgy-mezzanine-tutorial + first-widget + custom-owner diff --git a/docs/quickstart.rst b/docs/tutorials/widgy-mezzanine-tutorial.rst similarity index 100% rename from docs/quickstart.rst rename to docs/tutorials/widgy-mezzanine-tutorial.rst From f035b98f2ab5f107259f53a2221f25e8dfc08037 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Mon, 15 Jul 2013 12:46:28 -0600 Subject: [PATCH 0688/1061] Make FormSuccesHandler draggable It's awkward to be able to drop the success handler anywhere, but not be able to move it aftewards. --- widgy/contrib/form_builder/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widgy/contrib/form_builder/models.py b/widgy/contrib/form_builder/models.py index 6cfad3df7..24dfab8a6 100644 --- a/widgy/contrib/form_builder/models.py +++ b/widgy/contrib/form_builder/models.py @@ -50,7 +50,7 @@ def valid_child_of(cls, parent, obj=None): class FormSuccessHandler(FormElement): - draggable = False + draggable = True class Meta: abstract = True From ece03620eeffd34b6e1938af70542ceda8cb960a Mon Sep 17 00:00:00 2001 From: Gavin Wahl Date: Mon, 15 Jul 2013 11:37:06 -0600 Subject: [PATCH 0689/1061] Preserve submit-button data in popup forms If we disable the submit button in the submit handler, the browser won't submit its value. We need this value to determine which button was pressed. --- widgy/static/widgy/js/popup.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/widgy/static/widgy/js/popup.js b/widgy/static/widgy/js/popup.js index b8863bab6..e4567ea05 100644 --- a/widgy/static/widgy/js/popup.js +++ b/widgy/static/widgy/js/popup.js @@ -22,8 +22,14 @@ jQuery(function($) { var $form = $(this); var $submits = $form.find('[type=submit]'); if ( $form.find('.loading').length == 0 ) { + // enter pressed $submits.first().addClass('loading'); } - $submits.attr('disabled', true); + // We want to disable the submit buttons to prevent double-submission, but + // we need the data from the submit button to be submitted, which won't + // happen if it's disabled in this frame. + setTimeout(function() { + $submits.attr('disabled', true); + }, 0); }); }); From a243efbd9a939289b46ae327bac6598e60900702 Mon Sep 17 00:00:00 2001 From: Gavin Wahl Date: Fri, 5 Jul 2013 16:33:30 -0600 Subject: [PATCH 0690/1061] Add get_action_links to Widgy owners Models with WidgyFields should define a get_action_links method. It takes a root_node and returns a list of links. This is used for preview links. - Removes the dependency in core on widgy_mezzanine for preview links - Removes the VersionCommitAdmin.get_commit_name and get_commit_preview_url methods. - Removes an attempt at making forms.WidgyField work for non-model forms. We'll revisit this when it comes up. - Adds preview and diff links for every owner of a VersionTracker. --- tests/modeltests/core_tests/tests/fields.py | 4 ++ widgy/contrib/review_queue/admin.py | 63 +++++++++++------- .../review_queue/commit_preview.html | 12 +++- widgy/contrib/widgy_mezzanine/admin.py | 33 +++------- widgy/contrib/widgy_mezzanine/models.py | 14 +++- .../templates/widgy/history.html | 13 ++-- .../widgy/page_builder/layout/preview.html | 7 -- widgy/contrib/widgy_mezzanine/tests.py | 4 ++ widgy/contrib/widgy_mezzanine/urls.py | 5 +- widgy/contrib/widgy_mezzanine/views.py | 33 ++++++---- widgy/forms.py | 14 ++-- widgy/models/versioning.py | 16 +++++ widgy/site.py | 2 +- widgy/static/widgy/css/django.fusionbox.scss | 1 + widgy/templates/widgy/_commit_form.html | 15 +++-- widgy/templates/widgy/commit.html | 2 +- widgy/templates/widgy/revert.html | 2 +- .../widgy/versioned_widgy_field_base.html | 19 ++---- widgy/templates/widgy/widgy_field.html | 16 ++++- widgy/templatetags/widgy_tags.py | 10 +++ widgy/utils.py | 7 +- widgy/views/versioning.py | 65 ++++++++++--------- 22 files changed, 219 insertions(+), 138 deletions(-) delete mode 100644 widgy/contrib/widgy_mezzanine/templates/widgy/page_builder/layout/preview.html diff --git a/tests/modeltests/core_tests/tests/fields.py b/tests/modeltests/core_tests/tests/fields.py index bac168847..2cd3ec2f9 100644 --- a/tests/modeltests/core_tests/tests/fields.py +++ b/tests/modeltests/core_tests/tests/fields.py @@ -4,6 +4,7 @@ from django import forms from django.contrib.contenttypes.models import ContentType from django.template import Context +from django.utils import unittest import mock @@ -62,6 +63,8 @@ def test_add_root(self): self.assertEqual(root_node.content.pk, 1337) + +@unittest.skip("We want WidgyFields to work with non-modelforms, but we haven't designed an API yet.") class TestPlainForm(TestCase): def setUp(self): # WidgyForms cannot be at the root level of a test because they make @@ -110,6 +113,7 @@ def test_second_save(self): root_node = Layout.add_root(widgy_site) x = self.form(initial={'widgy_field': root_node}) + class TestModelForm(TestCase): def setUp(self): class WidgiedModelForm(WidgyFormMixin, forms.ModelForm): diff --git a/widgy/contrib/review_queue/admin.py b/widgy/contrib/review_queue/admin.py index cf6d9eb17..bc8984b22 100644 --- a/widgy/contrib/review_queue/admin.py +++ b/widgy/contrib/review_queue/admin.py @@ -4,7 +4,7 @@ from django.contrib.admin.views.main import ChangeList from django.contrib import messages from django.core.exceptions import ValidationError -from django.utils.translation import ugettext_lazy as _, ugettext, ungettext +from django.utils.translation import ugettext_lazy as _, ungettext from django.conf import settings from django.template.loader import render_to_string from django.utils.safestring import mark_safe @@ -14,6 +14,7 @@ from widgy.admin import AuthorizedAdminMixin from .forms import ApproveForm +from .models import ReviewedVersionTracker # BBB: Before django 1.5, only session storage supported safe string in @@ -31,12 +32,30 @@ class VersionCommitChangeList(ChangeList): def get_ordering(self, request, queryset): return ['tracker__pk', ] + super(VersionCommitChangeList, self).get_ordering(request, queryset) or [] + def get_results(self, request): + ret = super(VersionCommitChangeList, self).get_results(request) + + # Manually prefetch owners. We can't do a normal + # ReviewedVersionCommit.objects.prefetch_related('tracker__widgypage_set') + # because ReviewedVersionCommit.tracker is a VersionTracker (which + # doesn't have the reverse relationship), not a ReviewedVersionTracker. + trackers = ReviewedVersionTracker.objects.prefetch_related( + *ReviewedVersionTracker.get_owner_related_names() + ).in_bulk( + set(commit.tracker_id for commit in self.result_list) + ) + for commit in self.result_list: + commit.tracker = trackers[commit.tracker_id] + + return ret + class VersionCommitAdminBase(AuthorizedAdminMixin, ModelAdmin): list_display = ('commit_name', 'author', 'publish_at', 'commit_preview') readonly_fields = ('preview', ) list_select_related = True form = ApproveForm + actions = ['approve_selected'] def get_queryset(self, request): try: @@ -44,8 +63,7 @@ def get_queryset(self, request): except AttributeError: # BBB: In django 1.5 queryset changed to get_queryset qs = super(VersionCommitAdminBase, self).queryset(request) - return qs.unapproved() - + return qs.unapproved().select_related('author', 'root_node') queryset = get_queryset def get_object(self, request, object_id): @@ -65,8 +83,6 @@ def has_add_permission(self, request): def has_delete_permission(self, request, obj=None): return False - actions = ['approve_selected'] - def get_actions(self, request): actions = super(VersionCommitAdminBase, self).get_actions(request) del actions['delete_selected'] @@ -107,36 +123,33 @@ def save_form(self, request, form, change): # request.user during the saving. return form.save(request, commit=False) + def commit_name(self, commit): + assert isinstance(commit.tracker, ReviewedVersionTracker), ( + "We rely on the VersionCommitChangeList to set commit.tracker" + " to a ReviewedVersionTracker." + ) + return ', '.join(unicode(owner) for owner in commit.tracker.owners) + commit_name.short_description = _("Commit name") + def commit_preview(self, commit): - url = self.get_commit_preview_url(commit) - return format_html('{preview}', - url=url, preview=ugettext('preview')) + res = [] + for owner in commit.tracker.owners: + if hasattr(owner, 'get_action_links'): + for link in owner.get_action_links(commit.root_node): + res.append(format_html('{text}', **link)) + return ' '.join(res) commit_preview.short_description = '' commit_preview.allow_tags = True def preview(self, commit): context = { - 'commit_url': self.get_commit_preview_url(commit) + 'owners': ReviewedVersionTracker.objects.get(pk=commit.tracker_id).owners, + 'node': commit.root_node, } return mark_safe(render_to_string('review_queue/commit_preview.html', context)) preview.short_description = _('Preview') - def commit_name(self, commit): - return self.get_commit_name(commit) - commit_name.short_description = _("Commit name") - # Override this method in subclasses def get_site(self): - raise NotImplementedError('get_site should be implemented in ' - 'in subclasses') - - # Override this method in subclasses - def get_commit_name(self, commit): - raise NotImplementedError('get_commit_name should be implemented ' - 'in subclasses') - - # Override this method in subclasses - def get_commit_preview_url(self, commit): - raise NotImplementedError('get_commit_preview_url should be ' - 'in subclasses') + raise NotImplementedError('get_site should be implemented in subclasses') diff --git a/widgy/contrib/review_queue/templates/review_queue/commit_preview.html b/widgy/contrib/review_queue/templates/review_queue/commit_preview.html index 6cb8e43d2..826535fa7 100644 --- a/widgy/contrib/review_queue/templates/review_queue/commit_preview.html +++ b/widgy/contrib/review_queue/templates/review_queue/commit_preview.html @@ -1,5 +1,13 @@ -{% load compress %} - +{% load compress widgy_tags %} +{% for owner in owners %} + {% get_action_links owner node as links %} + {% for link in links %} + {% if link.type == 'preview' %} + {{ link.text }} + + {% endif %} + {% endfor %} +{% endfor %} {% compress css %} {% endcompress %} diff --git a/widgy/contrib/widgy_mezzanine/admin.py b/widgy/contrib/widgy_mezzanine/admin.py index 2c4620c61..dd9cc807b 100644 --- a/widgy/contrib/widgy_mezzanine/admin.py +++ b/widgy/contrib/widgy_mezzanine/admin.py @@ -18,7 +18,6 @@ from widgy.forms import WidgyFormMixin from widgy.contrib.widgy_mezzanine import get_widgypage_model -from widgy.contrib.widgy_mezzanine.views import get_page_from_node from widgy.utils import fancy_import, format_html from widgy.models import links @@ -27,19 +26,18 @@ WidgyPage = get_widgypage_model() -class GetSiteMixin(object): - def get_site(self): - return fancy_import(settings.WIDGY_MEZZANINE_SITE) - - class WidgyPageAdminForm(WidgyFormMixin, PageAdminForm): class Meta: model = WidgyPage def __init__(self, *args, **kwargs): super(WidgyPageAdminForm, self).__init__(*args, **kwargs) - self.fields['publish_date'].help_text = _("If you enter a date here, the page will not be viewable on the site until then") - self.fields['expiry_date'].help_text = _("If you enter a date here, the page will not be viewable after this time") + self.fields['publish_date'].help_text = _( + "If you enter a date here, the page will not be viewable on the site until then" + ) + self.fields['expiry_date'].help_text = _( + "If you enter a date here, the page will not be viewable after this time" + ) self.fields['status'].initial = CONTENT_STATUS_DRAFT def clean_status(self): @@ -50,17 +48,10 @@ def clean_status(self): return status -class WidgyPageAdmin(PageAdmin, GetSiteMixin): +class WidgyPageAdmin(PageAdmin): change_form_template = 'widgy/page_builder/widgypage_change_form.html' form = WidgyPageAdminForm - def render_change_form(self, request, context, *args, **kwargs): - if 'original' in context and context['original'].root_node: - # we are rendering a change form - obj = context['original'] - site = self.get_site() - return super(WidgyPageAdmin, self).render_change_form(request, context, *args, **kwargs) - class UndeleteField(forms.ModelChoiceField): widget = forms.RadioSelect @@ -133,13 +124,9 @@ def __init__(self, *args, **kwargs): return super(UndeletePage, self).__init__(*args, **kwargs) -class VersionCommitAdmin(GetSiteMixin, VersionCommitAdminBase): - def get_commit_name(self, commit): - return get_page_from_node(commit.root_node).title - - def get_commit_preview_url(self, commit): - return reverse('widgy.contrib.widgy_mezzanine.views.preview', - kwargs={'node_pk': commit.root_node.pk}) +class VersionCommitAdmin(VersionCommitAdminBase): + def get_site(self): + return fancy_import(settings.WIDGY_MEZZANINE_SITE) # Remove built in Mezzanine models from the admin center diff --git a/widgy/contrib/widgy_mezzanine/models.py b/widgy/contrib/widgy_mezzanine/models.py index 8b80cebc9..0be8ac3b8 100644 --- a/widgy/contrib/widgy_mezzanine/models.py +++ b/widgy/contrib/widgy_mezzanine/models.py @@ -26,9 +26,21 @@ def get_form_action_url(self, form, widgy): 'widgy.contrib.widgy_mezzanine.views.handle_form', kwargs={ 'form_node_pk': form.node.pk, - 'root_node_pk': widgy['root_node'].pk, + 'slug': self.slug, }) + def get_action_links(self, root_node): + return [ + { + 'type': 'preview', + 'text': _('Preview'), + 'url': urlresolvers.reverse( + 'widgy.contrib.widgy_mezzanine.views.preview', + kwargs={'slug': self.slug, 'node_pk': root_node.pk} + ) + }, + ] + def get_content_model(self): """ This is needed to render an unsaved WidgyPage. The template diff --git a/widgy/contrib/widgy_mezzanine/templates/widgy/history.html b/widgy/contrib/widgy_mezzanine/templates/widgy/history.html index cde68ea75..6a4d65298 100644 --- a/widgy/contrib/widgy_mezzanine/templates/widgy/history.html +++ b/widgy/contrib/widgy_mezzanine/templates/widgy/history.html @@ -49,10 +49,15 @@

    {% trans "Page History" %}

    {% elif can_revert %} {% trans "Can't revert; this is the same as the current version." %} {% endif %} - {% if commit.diff_url %} - {% trans "Diff" %} - {% endif %} - {% trans "View" %} + {% for diff_url in commit.diff_urls %} + {% trans "Diff" %} + {% endfor %} + {% for owner in object.owners %} + {% get_action_links owner commit.root_node as links %} + {% for link in links %} + {{ link.text }} + {% endfor %} + {% endfor %} {% endblock %}