From 434ae7bf3cb61934d13fc4c172ebe20dbc925241 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Wed, 4 Sep 2013 16:05:13 +0100 Subject: [PATCH] Fixed comments and disabled download --- dss/settings.py | 8 +- spa/api/v1/CommentResource.py | 23 +- spa/api/v1/MixResource.py | 3 +- spa/middleware/cors.py | 39 +++ spa/models/comment.py | 2 +- spa/templates.py | 5 +- static/js/app/lib/router.js | 9 +- .../js/app/models/comment/commentItem.coffee | 15 +- static/js/app/models/comment/commentItem.js | 25 +- static/js/app/models/mix/mixItem.coffee | 18 +- static/js/app/models/mix/mixItem.js | 6 +- .../app/views/comment/commentListView.coffee | 28 ++- .../js/app/views/comment/commentListView.js | 41 +++- static/js/app/views/mix/mixItemView.coffee | 2 +- static/js/app/views/mix/mixItemView.js | 2 +- static/js/app/views/mix/mixListLayout.js | 10 +- .../js/libs/backbone/backbone.relational.js | 227 ++++++++++++------ static/js/main.js | 4 + templates/javascript/settings.js | 3 + templates/views/CommentListView.html | 4 +- templates/views/_MixItemInsert.html | 9 +- 21 files changed, 353 insertions(+), 130 deletions(-) create mode 100644 spa/middleware/cors.py diff --git a/dss/settings.py b/dss/settings.py index 2fa5696..5a446d7 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -117,6 +117,7 @@ TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + ( "allauth.account.context_processors.account", "spa.context_processors.debug" ) + AUTHENTICATION_BACKENDS = global_settings.AUTHENTICATION_BACKENDS + ( "allauth.account.auth_backends.AuthenticationBackend", ) @@ -129,6 +130,7 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'spa.middleware.cors.XsSharingMiddleware', #'spa.middleware.uploadify.SWFUploadMiddleware', #'spa.middleware.sqlprinter.SqlPrintingMiddleware' if DEBUG else None, #'debug_toolbar.middleware.DebugToolbarMiddleware', @@ -254,7 +256,11 @@ if DEBUG: import mimetypes mimetypes.add_type("image/png", ".png", True) - mimetypes.add_type("font/woff", ".woff", True) + mimetypes.add_type("image/png", ".png", True) + mimetypes.add_type("application/x-font-woff", ".woff", True) + mimetypes.add_type("application/vnd.ms-fontobject", ".eot", True) + mimetypes.add_type("font/ttf", ".ttf", True) + mimetypes.add_type("font/otf", ".otf", True) # TODO(fergal.moran@gmail.com): #import localsettings - so all localsettings are part of import settings diff --git a/spa/api/v1/CommentResource.py b/spa/api/v1/CommentResource.py index 60c72e2..3bd6b69 100755 --- a/spa/api/v1/CommentResource.py +++ b/spa/api/v1/CommentResource.py @@ -1,7 +1,10 @@ from tastypie import fields from tastypie.authentication import Authentication from tastypie.authorization import Authorization +from tastypie.exceptions import ImmediateHttpResponse +from tastypie.http import HttpForbidden, HttpBadRequest from spa.api.v1.BackboneCompatibleResource import BackboneCompatibleResource +from spa.models import Mix from spa.models.comment import Comment @@ -18,14 +21,20 @@ class CommentResource(BackboneCompatibleResource): authentication = Authentication() always_return_data = True - def obj_create(self, bundle, request=None, **kwargs): - bundle.data['user'] = {'pk': request.user.pk} - return super(CommentResource, self).obj_create(bundle, request, user=request.user) + def obj_create(self, bundle, **kwargs): + bundle.data['user'] = bundle.request.user - """ - def dehydrate_date_created(self, bundle): - return self.humanize_date(bundle.obj.date_created) - """ + try: + if 'mix_id' in bundle.data: + mix = Mix.objects.get(pk=bundle.data['mix_id']) + if mix is not None: + return super(CommentResource, self).obj_create(bundle, user=bundle.request.user, mix=mix) + except Exception, e: + self.logger.error("Error creating comment (%s)" % e.message) + pass + raise ImmediateHttpResponse( + HttpBadRequest("Unable to hydrate comment from supplied data.") + ) def dehydrate(self, bundle): bundle.data['avatar_image'] = bundle.obj.user.get_profile().get_small_profile_image() diff --git a/spa/api/v1/MixResource.py b/spa/api/v1/MixResource.py index db7316b..a0e923d 100755 --- a/spa/api/v1/MixResource.py +++ b/spa/api/v1/MixResource.py @@ -20,7 +20,7 @@ from spa.models.mix import Mix class MixResource(BackboneCompatibleResource): - comments = fields.ToManyField('spa.api.v1.CommentResource.CommentResource', 'comments', null=True) + comments = fields.ToManyField('spa.api.v1.CommentResource.CommentResource', 'comments', null=True, full=True) favourites = fields.ToManyField('spa.api.v1.UserResource.UserResource', 'favourites', related_name='favourites', full=False, null=True) @@ -37,6 +37,7 @@ class MixResource(BackboneCompatibleResource): 'comments': ALL_WITH_RELATIONS, 'favourites': ALL_WITH_RELATIONS, 'likes': ALL_WITH_RELATIONS, + 'slug': ALL_WITH_RELATIONS, } authorization = Authorization() diff --git a/spa/middleware/cors.py b/spa/middleware/cors.py new file mode 100644 index 0000000..8e1452f --- /dev/null +++ b/spa/middleware/cors.py @@ -0,0 +1,39 @@ +from django import http + +try: + import settings + XS_SHARING_ALLOWED_ORIGINS = settings.XS_SHARING_ALLOWED_ORIGINS + XS_SHARING_ALLOWED_METHODS = settings.XS_SHARING_ALLOWED_METHODS +except: + XS_SHARING_ALLOWED_ORIGINS = '*' + XS_SHARING_ALLOWED_METHODS = ['POST','GET','OPTIONS', 'PUT', 'DELETE'] + + +class XsSharingMiddleware(object): + """ + This middleware allows cross-domain XHR using the html5 postMessage API. + + + Access-Control-Allow-Origin: http://foo.example + Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE + """ + def process_request(self, request): + + if 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' in request.META: + response = http.HttpResponse() + response['Access-Control-Allow-Origin'] = XS_SHARING_ALLOWED_ORIGINS + response['Access-Control-Allow-Methods'] = ",".join( XS_SHARING_ALLOWED_METHODS ) + + return response + + return None + + def process_response(self, request, response): + # Avoid unnecessary work + if response.has_header('Access-Control-Allow-Origin'): + return response + + response['Access-Control-Allow-Origin'] = XS_SHARING_ALLOWED_ORIGINS + response['Access-Control-Allow-Methods'] = ",".join( XS_SHARING_ALLOWED_METHODS ) + + return response diff --git a/spa/models/comment.py b/spa/models/comment.py index 85ccb3c..68a913f 100755 --- a/spa/models/comment.py +++ b/spa/models/comment.py @@ -12,7 +12,7 @@ class Comment(_BaseModel): mix = models.ForeignKey(Mix, editable=False, null=True, blank=True, related_name='comments') comment = models.CharField(max_length=1024) date_created = models.DateTimeField(auto_now=True) - time_index = models.IntegerField() + time_index = models.IntegerField(default=0) def get_absolute_url(self): return '/comment/%i' % self.id diff --git a/spa/templates.py b/spa/templates.py index bebfe4c..293fc08 100755 --- a/spa/templates.py +++ b/spa/templates.py @@ -43,7 +43,10 @@ def get_dialog(request, dialog_name, **kwargs): def get_javascript(request, template_name): localsettings.JS_SETTINGS.update({ - 'CURRENT_USER_ID': request.user.get_profile().id if not request.user.is_anonymous() else -1 + 'CURRENT_USER_ID': request.user.get_profile().id if not request.user.is_anonymous() else -1, + 'CURRENT_USER_NAME': request.user.get_profile().get_nice_name() if not request.user.is_anonymous() else -1, + 'CURRENT_USER_URL': request.user.get_profile().get_profile_url() if not request.user.is_anonymous() else -1, + 'AVATAR_IMAGE': request.user.get_profile().get_small_profile_image() if not request.user.is_anonymous() else "" }) return render_to_response( 'javascript/%s.js' % template_name, diff --git a/static/js/app/lib/router.js b/static/js/app/lib/router.js index 00017b4..e33b7fc 100755 --- a/static/js/app/lib/router.js +++ b/static/js/app/lib/router.js @@ -1,16 +1,17 @@ -// Generated by CoffeeScript 1.3.3 +// Generated by CoffeeScript 1.6.2 (function() { var __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; define(['marionette', 'vent', 'app.lib/controller'], function(Marionette, vent, Controller) { - var DssRouter; - return DssRouter = (function(_super) { + var DssRouter, _ref; + return DssRouter = (function(_super) { __extends(DssRouter, _super); function DssRouter() { - return DssRouter.__super__.constructor.apply(this, arguments); + _ref = DssRouter.__super__.constructor.apply(this, arguments); + return _ref; } DssRouter.prototype.controller = new Controller; diff --git a/static/js/app/models/comment/commentItem.coffee b/static/js/app/models/comment/commentItem.coffee index 38271d6..d904f51 100755 --- a/static/js/app/models/comment/commentItem.coffee +++ b/static/js/app/models/comment/commentItem.coffee @@ -1,6 +1,11 @@ -define ['backbone'], -(Backbone) -> - class CommentItem extends Backbone.Model - urlRoot:com.podnoms.settings.urlRoot + "comments/" +define ['app.lib/backbone.dss.model'], \ + (DSSModel) -> + class CommentItem extends DSSModel + urlRoot: com.podnoms.settings.urlRoot + "comments/" + defaults: + avatar_image: com.podnoms.settings.avatarImage + user_name: com.podnoms.settings.userName + user_url: com.podnoms.settings.userUrl + date_created: "" - CommentItem \ No newline at end of file + CommentItem \ No newline at end of file diff --git a/static/js/app/models/comment/commentItem.js b/static/js/app/models/comment/commentItem.js index cc00ec7..2289df8 100755 --- a/static/js/app/models/comment/commentItem.js +++ b/static/js/app/models/comment/commentItem.js @@ -1,24 +1,31 @@ -// Generated by CoffeeScript 1.3.3 +// Generated by CoffeeScript 1.6.2 (function() { var __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; - define(['backbone'], function(Backbone) { - var CommentItem; - CommentItem = (function(_super) { + define(['app.lib/backbone.dss.model'], function(DSSModel) { + var CommentItem, _ref; + CommentItem = (function(_super) { __extends(CommentItem, _super); function CommentItem() { - return CommentItem.__super__.constructor.apply(this, arguments); + _ref = CommentItem.__super__.constructor.apply(this, arguments); + return _ref; } + CommentItem.prototype.urlRoot = com.podnoms.settings.urlRoot + "comments/"; + + CommentItem.prototype.defaults = { + avatar_image: com.podnoms.settings.avatarImage, + user_name: com.podnoms.settings.userName, + user_url: com.podnoms.settings.userUrl, + date_created: "" + }; + return CommentItem; - })(Backbone.Model); - ({ - urlRoot: com.podnoms.settings.urlRoot + "comments/" - }); + })(DSSModel); return CommentItem; }); diff --git a/static/js/app/models/mix/mixItem.coffee b/static/js/app/models/mix/mixItem.coffee index fd1e769..cc61b6e 100755 --- a/static/js/app/models/mix/mixItem.coffee +++ b/static/js/app/models/mix/mixItem.coffee @@ -1,6 +1,16 @@ -define ['backbone', 'app.lib/backbone.dss.model'], \ - (Backbone, DssModel) -> - class MixItem extends Backbone.Model +define ['backbone.relational', 'models/comment/commentCollection', 'models/comment/commentItem', 'app.lib/backbone.dss.model'], \ + (Backbone, CommentCollection, CommentItem, DSSModel) -> + class MixItem extends DSSModel urlRoot: com.podnoms.settings.urlRoot + "mix/" - + """ + relations: [ + type: Backbone.HasMany + key: "comments" + relatedModel: CommentItem + collectionType: CommentCollection + reverseRelation: + key: "hasItems" + includeInJSON: "id" + ] + """ MixItem \ No newline at end of file diff --git a/static/js/app/models/mix/mixItem.js b/static/js/app/models/mix/mixItem.js index 8328e66..6121539 100755 --- a/static/js/app/models/mix/mixItem.js +++ b/static/js/app/models/mix/mixItem.js @@ -3,7 +3,7 @@ var __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; - define(['backbone', 'app.lib/backbone.dss.model'], function(Backbone, DssModel) { + define(['backbone.relational', 'models/comment/commentCollection', 'models/comment/commentItem', 'app.lib/backbone.dss.model'], function(Backbone, CommentCollection, CommentItem, DSSModel) { var MixItem, _ref; MixItem = (function(_super) { @@ -16,9 +16,11 @@ MixItem.prototype.urlRoot = com.podnoms.settings.urlRoot + "mix/"; + "relations: [\n type: Backbone.HasMany\n key: \"comments\"\n relatedModel: CommentItem\n collectionType: CommentCollection\n reverseRelation:\n key: \"hasItems\"\n includeInJSON: \"id\"\n ]"; + return MixItem; - })(Backbone.Model); + })(DSSModel); return MixItem; }); diff --git a/static/js/app/views/comment/commentListView.coffee b/static/js/app/views/comment/commentListView.coffee index 154ab99..969ec43 100755 --- a/static/js/app/views/comment/commentListView.coffee +++ b/static/js/app/views/comment/commentListView.coffee @@ -1,5 +1,5 @@ -define ['marionette', 'views/comment/commentItemView', 'text!/tpl/CommentListView'], -(Marionette, CommentItemView, Template) -> +define ['marionette', 'models/comment/commentItem', 'views/comment/commentItemView', 'text!/tpl/CommentListView'], +(Marionette, CommentItem, CommentItemView, Template) -> class CommentListView extends Marionette.CompositeView template: _.template(Template) @@ -8,7 +8,31 @@ define ['marionette', 'views/comment/commentItemView', 'text!/tpl/CommentListVie itemView: CommentItemView itemViewContainer: "#comment-list-container" + ui: + commentText: '#comment-text' + + events: + "click #btn-add-comment": "addComment" + initialize: -> console.log "CommentListView: initialize" + addComment: -> + console.log "CommentListView: addComment" + @collection.create + mix_id: @collection.mix.get("id") + comment: @ui.commentText.val() + , + success: (newItem) => + @ui.commentText.val "" + true + + error: (a, b, c) -> + console.log a + console.log b + console.log c + true + + true + CommentListView diff --git a/static/js/app/views/comment/commentListView.js b/static/js/app/views/comment/commentListView.js index 3ce168d..fb1b3ba 100755 --- a/static/js/app/views/comment/commentListView.js +++ b/static/js/app/views/comment/commentListView.js @@ -1,16 +1,17 @@ -// Generated by CoffeeScript 1.3.3 +// Generated by CoffeeScript 1.6.2 (function() { var __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; - define(['marionette', 'views/comment/commentItemView', 'text!/tpl/CommentListView'], function(Marionette, CommentItemView, Template) { - var CommentListView; - CommentListView = (function(_super) { + define(['marionette', 'models/comment/commentItem', 'views/comment/commentItemView', 'text!/tpl/CommentListView'], function(Marionette, CommentItem, CommentItemView, Template) { + var CommentListView, _ref; + CommentListView = (function(_super) { __extends(CommentListView, _super); function CommentListView() { - return CommentListView.__super__.constructor.apply(this, arguments); + _ref = CommentListView.__super__.constructor.apply(this, arguments); + return _ref; } CommentListView.prototype.template = _.template(Template); @@ -23,10 +24,40 @@ CommentListView.prototype.itemViewContainer = "#comment-list-container"; + CommentListView.prototype.ui = { + commentText: '#comment-text' + }; + + CommentListView.prototype.events = { + "click #btn-add-comment": "addComment" + }; + CommentListView.prototype.initialize = function() { return console.log("CommentListView: initialize"); }; + CommentListView.prototype.addComment = function() { + var _this = this; + + console.log("CommentListView: addComment"); + this.collection.create({ + mix_id: this.collection.mix.get("id"), + comment: this.ui.commentText.val() + }, { + success: function(newItem) { + _this.ui.commentText.val(""); + return true; + }, + error: function(a, b, c) { + console.log(a); + console.log(b); + console.log(c); + return true; + } + }); + return true; + }; + return CommentListView; })(Marionette.CompositeView); diff --git a/static/js/app/views/mix/mixItemView.coffee b/static/js/app/views/mix/mixItemView.coffee index f9f2926..761b6f9 100755 --- a/static/js/app/views/mix/mixItemView.coffee +++ b/static/js/app/views/mix/mixItemView.coffee @@ -56,7 +56,7 @@ define ['moment', 'app', 'vent', 'marionette', 'utils', 'models/comment/commentC comments = new CommentsCollection() comments.url = @model.get("resource_uri") + "comments/" comments.mix_id = @model.id - comments.mix = @model.get("resource_uri") + comments.mix = @model comments.fetch success: (data) -> console.log(data) content = new CommentsListView(collection: comments).render() diff --git a/static/js/app/views/mix/mixItemView.js b/static/js/app/views/mix/mixItemView.js index 3389fad..d2f95b3 100755 --- a/static/js/app/views/mix/mixItemView.js +++ b/static/js/app/views/mix/mixItemView.js @@ -85,7 +85,7 @@ comments = new CommentsCollection(); comments.url = this.model.get("resource_uri") + "comments/"; comments.mix_id = this.model.id; - comments.mix = this.model.get("resource_uri"); + comments.mix = this.model; comments.fetch({ success: function(data) { var content; diff --git a/static/js/app/views/mix/mixListLayout.js b/static/js/app/views/mix/mixListLayout.js index 58fbe81..7058b17 100644 --- a/static/js/app/views/mix/mixListLayout.js +++ b/static/js/app/views/mix/mixListLayout.js @@ -1,16 +1,17 @@ -// Generated by CoffeeScript 1.3.3 +// Generated by CoffeeScript 1.6.2 (function() { var __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; define(['marionette', 'vent', 'models/user/userItem', 'views/widgets/mixTabHeaderView', 'views/user/userItemView', 'views/mix/mixListView', 'text!/tpl/MixListLayoutView'], function(Marionette, vent, UserItem, MixTabHeaderView, UserItemView, MixListView, Template) { - var MixListRegionView; - MixListRegionView = (function(_super) { + var MixListRegionView, _ref; + MixListRegionView = (function(_super) { __extends(MixListRegionView, _super); function MixListRegionView() { - return MixListRegionView.__super__.constructor.apply(this, arguments); + _ref = MixListRegionView.__super__.constructor.apply(this, arguments); + return _ref; } MixListRegionView.prototype.template = _.template(Template); @@ -36,6 +37,7 @@ MixListRegionView.prototype.showUserView = function(options) { var user, _this = this; + this.bodyRegion.show(new MixListView(options)); user = new UserItem({ id: options.user diff --git a/static/js/libs/backbone/backbone.relational.js b/static/js/libs/backbone/backbone.relational.js index 863af53..a5218e5 100644 --- a/static/js/libs/backbone/backbone.relational.js +++ b/static/js/libs/backbone/backbone.relational.js @@ -1,6 +1,6 @@ /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab: */ /** - * Backbone-relational.js 0.8.5 + * Backbone-relational.js 0.8.6 * (c) 2011-2013 Paul Uithol and contributors (https://github.com/PaulUithol/Backbone-relational/graphs/contributors) * * Backbone-relational may be freely distributed under the MIT license; see the accompanying LICENSE.txt. @@ -17,7 +17,8 @@ if ( typeof window === 'undefined' ) { _ = require( 'underscore' ); Backbone = require( 'backbone' ); - exports = module.exports = Backbone; + exports = Backbone; + typeof module === 'undefined' || ( module.exports = exports ); } else { _ = window._; @@ -86,9 +87,21 @@ } }, + // Some of the queued events may trigger other blocking events. By + // copying the queue here it allows queued events to process closer to + // the natural order. + // + // queue events [ 'A', 'B', 'C' ] + // A handler of 'B' triggers 'D' and 'E' + // By copying `this._queue` this executes: + // [ 'A', 'B', 'D', 'E', 'C' ] + // The same order the would have executed if they didn't have to be + // delayed and queued. process: function() { - while ( this._queue && this._queue.length ) { - this._queue.shift()(); + var queue = this._queue; + this._queue = []; + while ( queue && queue.length ) { + queue.shift()(); } }, @@ -298,7 +311,9 @@ rootModel = rootModel._superModel; } - var coll = _.findWhere( this._collections, { model: rootModel } ); + var coll = _.find( this._collections, function(item) { + return item.model === rootModel; + }); if ( !coll && create !== false ) { coll = this._createCollection( rootModel ); @@ -407,6 +422,7 @@ var modelColl = model.collection; coll.add( model ); this.listenTo( model, 'destroy', this.unregister, this ); + this.listenTo( model, 'relational:unregister', this.unregister, this ); model.collection = modelColl; } }, @@ -446,10 +462,10 @@ * Remove a 'model' from the store. * @param {Backbone.RelationalModel} model */ - unregister: function( model ) { - this.stopListening( model, 'destroy', this.unregister ); + unregister: function( model, collection, options ) { + this.stopListening( model ); var coll = this.getCollection( model ); - coll && coll.remove( model ); + coll && coll.remove( model, options ); }, /** @@ -496,7 +512,12 @@ this.keyDestination = this.options.keyDestination || this.keySource || this.key; this.model = this.options.model || this.instance.constructor; + this.relatedModel = this.options.relatedModel; + + if ( _.isFunction( this.relatedModel ) && !( this.relatedModel.prototype instanceof Backbone.RelationalModel ) ) { + this.relatedModel = _.result( this, 'relatedModel' ); + } if ( _.isString( this.relatedModel ) ) { this.relatedModel = Backbone.Relational.store.getObjectByName( this.relatedModel ); } @@ -528,7 +549,7 @@ // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'. if ( this.keySource !== this.key ) { - this.instance.unset( this.keySource, { silent: true } ); + delete this.instance.attributes[ this.keySource ]; } // Add this Relation to instance._relations @@ -829,6 +850,9 @@ // Handle a custom 'collectionType' this.collectionType = this.options.collectionType; + if ( _.isFunction( this.collectionType ) && this.collectionType !== Backbone.Collection && !( this.collectionType.prototype instanceof Backbone.Collection ) ) { + this.collectionType = _.result( this, 'collectionType' ); + } if ( _.isString( this.collectionType ) ) { this.collectionType = Backbone.Relational.store.getObjectByName( this.collectionType ); } @@ -1064,6 +1088,7 @@ _isInitialized: false, _deferProcessing: false, _queue: null, + _attributeChangeFired: false, // Keeps track of `change` event firing under some conditions (like nested `set`s) subModelTypeAttribute: 'type', subModelTypes: null, @@ -1128,7 +1153,9 @@ // Determine if the `change` event is still valid, now that all relations are populated var changed = true; if ( eventName === 'change' ) { - changed = dit.hasChanged(); + // `hasChanged` may have gotten reset by nested calls to `set`. + changed = dit.hasChanged() || dit._attributeChangeFired; + dit._attributeChangeFired = false; } else { var attr = eventName.slice( 7 ), @@ -1137,7 +1164,7 @@ if ( rel ) { // If `attr` is a relation, `change:attr` get triggered from `Relation.onChange`. // These take precedence over `change:attr` events triggered by `Model.set`. - // The relation set a fourth attribute to `true`. If this attribute is present, + // The relation sets a fourth attribute to `true`. If this attribute is present, // continue triggering this event; otherwise, it's from `Model.set` and should be stopped. changed = ( args[ 4 ] === true ); @@ -1152,6 +1179,9 @@ delete dit.changed[ attr ]; } } + else if ( changed ) { + dit._attributeChangeFired = true; + } } changed && Backbone.Model.prototype.trigger.apply( dit, args ); @@ -1172,7 +1202,7 @@ this.acquire(); // Setting up relations often also involve calls to 'set', and we only want to enter this function once this._relations = {}; - _.each( this.relations || [], function( rel ) { + _.each( _.result( this, 'relations' ) || [], function( rel ) { Backbone.Relational.store.initializeRelation( this, rel, options ); }, this ); @@ -1188,11 +1218,16 @@ updateRelations: function( options ) { if ( this._isInitialized && !this.isLocked() ) { _.each( this._relations, function( rel ) { - // Update from data in `rel.keySource` if set, or `rel.key` otherwise + // Update from data in `rel.keySource` if data got set in there, or `rel.key` otherwise var val = this.attributes[ rel.keySource ] || this.attributes[ rel.key ]; if ( rel.related !== val ) { this.trigger( 'relational:change:' + rel.key, this, val, options || {} ); } + + // Explicitly clear 'keySource', to prevent a leaky abstraction if 'keySource' differs from 'key'. + if ( rel.keySource !== rel.key ) { + delete rel.instance.attributes[ rel.keySource ]; + } }, this ); } }, @@ -1244,7 +1279,7 @@ var setUrl, requests = [], rel = this.getRelation( key ), - idsToFetch = rel && ( rel.keyIds || ( ( rel.keyId || rel.keyId === 0 ) ? [ rel.keyId ] : [] ) ); + idsToFetch = rel && ( ( rel.keyIds && rel.keyIds.slice( 0 ) ) || ( ( rel.keyId || rel.keyId === 0 ) ? [ rel.keyId ] : [] ) ); // On `refresh`, add the ids for current models in the relation to `idsToFetch` if ( refresh ) { @@ -1329,11 +1364,19 @@ // Go through all splits and return the final result var splits = attr.split( '.' ); var result = _.reduce(splits, function( model, split ) { - if ( !( model instanceof Backbone.Model ) ) { - throw new Error( 'Attribute must be an instanceof Backbone.Model. Is: ' + model + ', currentSplit: ' + split ); + if ( _.isNull(model) || _.isUndefined( model ) ) { + // Return undefined if the path cannot be expanded + return undefined; + } + else if ( model instanceof Backbone.Model ) { + return Backbone.Model.prototype.get.call( model, split ); + } + else if ( model instanceof Backbone.Collection ) { + return Backbone.Collection.prototype.at.call( model, split ) + } + else { + throw new Error( 'Attribute must be an instanceof Backbone.Model or Backbone.Collection. Is: ' + model + ', currentSplit: ' + split ); } - - return Backbone.Model.prototype.get.call( model, split ); }, this ); if ( originalResult !== undefined && result !== undefined ) { @@ -1389,30 +1432,6 @@ return result; }, - unset: function( attribute, options ) { - Backbone.Relational.eventQueue.block(); - - var result = Backbone.Model.prototype.unset.apply( this, arguments ); - this.updateRelations( options ); - - // Try to run the global queue holding external events - Backbone.Relational.eventQueue.unblock(); - - return result; - }, - - clear: function( options ) { - Backbone.Relational.eventQueue.block(); - - var result = Backbone.Model.prototype.clear.apply( this, arguments ); - this.updateRelations( options ); - - // Try to run the global queue holding external events - Backbone.Relational.eventQueue.unblock(); - - return result; - }, - clone: function() { var attributes = _.clone( this.attributes ); if ( !_.isUndefined( attributes[ this.idAttribute ] ) ) { @@ -1569,57 +1588,92 @@ * @return {Backbone.Model} */ build: function( attributes, options ) { - var model = this; - // 'build' is a possible entrypoint; it's possible no model hierarchy has been determined yet. this.initializeModelHierarchy(); // Determine what type of (sub)model should be built if applicable. - // Lookup the proper subModelType in 'this._subModels'. - if ( this._subModels && this.prototype.subModelTypeAttribute in attributes ) { - var subModelTypeAttribute = attributes[ this.prototype.subModelTypeAttribute ]; - var subModelType = this._subModels[ subModelTypeAttribute ]; - if ( subModelType ) { - model = subModelType; - } - } + var model = this._findSubModelType(this, attributes) || this; return new model( attributes, options ); }, + /** + * Determines what type of (sub)model should be built if applicable. + * Looks up the proper subModelType in 'this._subModels', recursing into + * types until a match is found. Returns the applicable 'Backbone.Model' + * or null if no match is found. + * @param {Backbone.Model} type + * @param {Object} attributes + * @return {Backbone.Model} + */ + _findSubModelType: function (type, attributes) { + if ( type._subModels && type.prototype.subModelTypeAttribute in attributes ) { + var subModelTypeAttribute = attributes[type.prototype.subModelTypeAttribute]; + var subModelType = type._subModels[subModelTypeAttribute]; + if ( subModelType ) { + return subModelType; + } else { + // Recurse into subModelTypes to find a match + for ( subModelTypeAttribute in type._subModels ) { + subModelType = this._findSubModelType(type._subModels[subModelTypeAttribute], attributes); + if ( subModelType ) { + return subModelType; + } + } + } + } + return null; + }, + /** * */ initializeModelHierarchy: function() { - // If we're here for the first time, try to determine if this modelType has a 'superModel'. - if ( _.isUndefined( this._superModel ) || _.isNull( this._superModel ) ) { - Backbone.Relational.store.setupSuperModel( this ); - - // If a superModel has been found, copy relations from the _superModel if they haven't been - // inherited automatically (due to a redefinition of 'relations'). - // Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail - // the isUndefined/isNull check next time. - if ( this._superModel && this._superModel.prototype.relations ) { - // Find relations that exist on the `_superModel`, but not yet on this model. + // Inherit any relations that have been defined in the parent model. + this.inheritRelations(); + + // If we came here through 'build' for a model that has 'subModelTypes' then try to initialize the ones that + // haven't been resolved yet. + if ( this.prototype.subModelTypes ) { + var resolvedSubModels = _.keys(this._subModels); + var unresolvedSubModels = _.omit(this.prototype.subModelTypes, resolvedSubModels); + _.each( unresolvedSubModels, function( subModelTypeName ) { + var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName ); + subModelType && subModelType.initializeModelHierarchy(); + }); + } + }, + + inheritRelations: function() { + // Bail out if we've been here before. + if (!_.isUndefined( this._superModel ) && !_.isNull( this._superModel )) { + return; + } + // Try to initialize the _superModel. + Backbone.Relational.store.setupSuperModel( this ); + + // If a superModel has been found, copy relations from the _superModel if they haven't been inherited automatically + // (due to a redefinition of 'relations'). + if ( this._superModel ) { + // The _superModel needs a chance to initialize its own inherited relations before we attempt to inherit relations + // from the _superModel. You don't want to call 'initializeModelHierarchy' because that could cause sub-models of + // this class to inherit their relations before this class has had chance to inherit it's relations. + this._superModel.inheritRelations(); + if ( this._superModel.prototype.relations ) { + // Find relations that exist on the '_superModel', but not yet on this model. var inheritedRelations = _.select( this._superModel.prototype.relations || [], function( superRel ) { return !_.any( this.prototype.relations || [], function( rel ) { return superRel.relatedModel === rel.relatedModel && superRel.key === rel.key; }, this ); }, this ); - + this.prototype.relations = inheritedRelations.concat( this.prototype.relations ); } - else { - this._superModel = false; - } } - - // If we came here through 'build' for a model that has 'subModelTypes', and not all of them have been resolved yet, try to resolve each. - if ( this.prototype.subModelTypes && _.keys( this.prototype.subModelTypes ).length !== _.keys( this._subModels ).length ) { - _.each( this.prototype.subModelTypes || [], function( subModelTypeName ) { - var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName ); - subModelType && subModelType.initializeModelHierarchy(); - }); + // Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail the + // isUndefined/isNull check next time. + else { + this._superModel = false; } }, @@ -1638,7 +1692,7 @@ findOrCreate: function( attributes, options ) { options || ( options = {} ); var parsedAttributes = ( _.isObject( attributes ) && options.parse && this.prototype.parse ) ? - this.prototype.parse( attributes ) : attributes; + this.prototype.parse( _.clone( attributes ) ) : attributes; // Try to find an instance of 'this' model type in the store var model = Backbone.Relational.store.find( this, parsedAttributes ); @@ -1647,8 +1701,9 @@ // If not, create an instance (unless 'options.create' is false). if ( _.isObject( attributes ) ) { if ( model && options.merge !== false ) { - // Make sure `options.collection` doesn't cascade to nested models + // Make sure `options.collection` and `options.url` doesn't cascade to nested models delete options.collection; + delete options.url; model.set( parsedAttributes, options ); } @@ -1658,6 +1713,22 @@ } return model; + }, + + /** + * Find an instance of `this` type in 'Backbone.Relational.store'. + * - If `attributes` is a string or a number, `find` will just query the `store` and return a model if found. + * - If `attributes` is an object and is found in the store, the model will be updated with `attributes` unless `options.update` is `false`. + * @param {Object|String|Number} attributes Either a model's id, or the attributes used to create or update a model. + * @param {Object} [options] + * @param {Boolean} [options.merge=true] + * @param {Boolean} [options.parse=false] + * @return {Backbone.RelationalModel} + */ + find: function( attributes, options ) { + options || ( options = {} ); + options.create = false; + return this.findOrCreate( attributes, options ); } }); _.extend( Backbone.RelationalModel.prototype, Backbone.Semaphore ); @@ -1765,14 +1836,14 @@ return remove.apply( this, arguments ); } - models = _.isArray( models ) ? models.slice() : [ models ]; + models = _.isArray( models ) ? models.slice( 0 ) : [ models ]; options || ( options = {} ); var toRemove = []; //console.debug('calling remove on coll=%o; models=%o, options=%o', this, models, options ); _.each( models, function( model ) { - model = this.get( model ) || this.get( model.cid ); + model = this.get( model ) || ( model && this.get( model.cid ) ); model && toRemove.push( model ); }, this ); @@ -1827,7 +1898,7 @@ return trigger.apply( this, arguments ); } - if ( eventName === 'add' || eventName === 'remove' || eventName === 'reset' ) { + if ( eventName === 'add' || eventName === 'remove' || eventName === 'reset' || eventName === 'sort' ) { var dit = this, args = arguments; diff --git a/static/js/main.js b/static/js/main.js index 2d6653c..a7cc461 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -64,6 +64,10 @@ requirejs.config({ exports: 'Backbone', deps: ['jquery', 'underscore'] }, + 'backbone.relational': { + exports: 'Backbone', + deps: ['backbone'] + }, bootstrap: { exports: 'bootstrap', deps: ['jquery'] diff --git a/templates/javascript/settings.js b/templates/javascript/settings.js index 84000f5..f882526 100755 --- a/templates/javascript/settings.js +++ b/templates/javascript/settings.js @@ -15,6 +15,9 @@ com.podnoms.settings = { staticUrl: '{{ STATIC_URL }}', urlArgs: {{ IS_DEBUG }} ? "" : "bust="+ (new Date()).getTime(), currentUser: {{ CURRENT_USER_ID }}, + userName: "{{ CURRENT_USER_NAME }}", + userUrl: "{{ CURRENT_USER_URL }}", + avatarImage: "{{ AVATAR_IMAGE }}", /** simple helper to take an api JSON object and initialise a player item */ setupPlayerWrapper: function (id, stream_url, el) { com.podnoms.player.setupPlayer({ diff --git a/templates/views/CommentListView.html b/templates/views/CommentListView.html index 5b40f0b..e135bc9 100755 --- a/templates/views/CommentListView.html +++ b/templates/views/CommentListView.html @@ -1,7 +1,7 @@ {% if request.user.is_authenticated %}
- - + +
{% endif %}

Comments

diff --git a/templates/views/_MixItemInsert.html b/templates/views/_MixItemInsert.html index a7a5faf..9659fb5 100755 --- a/templates/views/_MixItemInsert.html +++ b/templates/views/_MixItemInsert.html @@ -111,14 +111,19 @@ class="icon-edit">Edit {% endif %} + <% if (download_allowed) { %> {% if user.is_authenticated %} - <% if (download_allowed) { %> - <% } %> + {% else %} + {% endif %} + <% } %>