diff --git a/dss/settings.py b/dss/settings.py index 1678326..fcfa90f 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -218,7 +218,6 @@ AVATAR_STORAGE_DIR = MEDIA_ROOT + '/avatars/' ACCOUNT_LOGOUT_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/' - DSS_TEMP_PATH = localsettings.DSS_TEMP_PATH DSS_LAME_PATH = localsettings.DSS_LAME_PATH DSS_WAVE_PATH = localsettings.DSS_WAVE_PATH @@ -264,6 +263,7 @@ REQUIRE_DEBUG = False #DEBUG if DEBUG: import mimetypes + mimetypes.add_type("image/png", ".png", True) mimetypes.add_type("font/woff", ".woff", True) diff --git a/spa/api/v1/ActivityResource.py b/spa/api/v1/ActivityResource.py index cc0ace4..bf76003 100755 --- a/spa/api/v1/ActivityResource.py +++ b/spa/api/v1/ActivityResource.py @@ -1,6 +1,7 @@ import humanize from tastypie.authentication import Authentication from tastypie.authorization import Authorization +from tastypie.constants import ALL, ALL_WITH_RELATIONS from spa.api.v1.BackboneCompatibleResource import BackboneCompatibleResource from spa.models import UserProfile from spa.models.activity import Activity @@ -13,6 +14,9 @@ class ActivityResource(BackboneCompatibleResource): authorization = Authorization() authentication = Authentication() always_return_data = True + filtering = { + 'user': ALL_WITH_RELATIONS + } def dehydrate(self, bundle): try: @@ -41,3 +45,4 @@ class ActivityResource(BackboneCompatibleResource): except Exception, ee: self.logger.debug("Exception: Error dehydrating activity, %s" % ee.message) return None + diff --git a/spa/api/v1/MixResource.py b/spa/api/v1/MixResource.py index 8666d6f..ef15964 100755 --- a/spa/api/v1/MixResource.py +++ b/spa/api/v1/MixResource.py @@ -1,7 +1,7 @@ from django.conf.urls import url from django.core.exceptions import ObjectDoesNotExist from django.core.paginator import Paginator, InvalidPage -from django.db.models import Count +from django.db.models import Count, Q from django.http import Http404 from django.template.loader import render_to_string from tastypie import fields @@ -22,6 +22,7 @@ from spa.models.mix import Mix class MixResource(BackboneCompatibleResource): comments = fields.ToManyField('spa.api.v1.CommentResource.CommentResource', 'comments', null=True) #downloads = fields.ToManyField('spa.api.v1.ActivityResource.ActivityResource', 'downloads') + favourites = fields.ToManyField('spa.api.v1.UserResource.UserResource', 'favourites', related_name='favourites', full=True) class Meta: queryset = Mix.objects.filter(is_active=True) @@ -30,7 +31,9 @@ class MixResource(BackboneCompatibleResource): detail_uri_name = 'slug' excludes = ['download_url', 'is_active', 'local_file', 'upload_date', 'waveform-generated'] filtering = { - 'comments': ALL_WITH_RELATIONS + 'comments': ALL_WITH_RELATIONS, + 'favourites': ALL_WITH_RELATIONS, + 'activity_likes': ALL_WITH_RELATIONS } authorization = Authorization() @@ -107,10 +110,11 @@ class MixResource(BackboneCompatibleResource): def obj_update(self, bundle, **kwargs): #don't sync the mix_image, this has to be handled separately del bundle.data['mix_image'] + ret = super(MixResource, self).obj_update(bundle, bundle.request) bundle.obj.update_favourite(bundle.request.user, bundle.data['favourited']) bundle.obj.update_liked(bundle.request.user, bundle.data['liked']) - ret = super(MixResource, self).obj_update(bundle, bundle.request) + self._unpackGenreList(ret, bundle.data['genre-list']) return ret @@ -119,28 +123,57 @@ class MixResource(BackboneCompatibleResource): if orderby == 'latest': obj_list = obj_list.order_by('-id') elif orderby == 'toprated': - obj_list = obj_list.annotate(karma=Count('likes')).order_by('-karma') + obj_list = obj_list.annotate(karma=Count('activity_likes')).order_by('-karma') elif orderby == 'mostplayed': - obj_list = obj_list.annotate(karma=Count('plays')).order_by('-karma') + obj_list = obj_list.annotate(karma=Count('activity_plays')).order_by('-karma') elif orderby == 'mostactive': obj_list = obj_list.annotate(karma=Count('comments')).order_by('-karma') elif orderby == 'recommended': - obj_list = obj_list.annotate(karma=Count('likes')).order_by('-karma') + obj_list = obj_list.annotate(karma=Count('activity_likes')).order_by('-karma') return obj_list + """ + def build_filters(self, filters=None): + if filters is None: + filters = {} + + # TODO(Ferg@@lMoran.me): Yet another code smell + # I'm doing this shit everywhere in tastypie - here is my canonical rant about it + + # Either I'm completely missing something or tastypie was the wrong horse to back here + # There has to be a more prescriptive way to do this shit!!! + # I'm seriously considering swapping out tastpie for a more sane REST framework + # Simple stuff like deciding on + # your own url pattern + # ORM level rather than Resource level filtering + # are extremely difficult/undocumented + + + orm_filters = super(MixResource, self).build_filters(filters) + #find the non-resource filters that were stripped but the super's build_filters + #and re add them, will probably need to perform some checks here against Meta.filters + #so we can't be filtered willy-nilly + #also, have no idea how filters became a QueryDict?? + for f in filters: + if f not in ['format', 'order_by', 'sort']: + orm_filters.update({'custom': Q(f=filters.get(f))}) + + return orm_filters + """ def apply_filters(self, request, applicable_filters): - semi_filtered = super(MixResource, self)\ - .apply_filters(request, applicable_filters)\ + semi_filtered = super(MixResource, self) \ + .apply_filters(request, applicable_filters) \ .filter(waveform_generated=True) f_type = request.GET.get('type', None) f_user = request.GET.get('user', None) + """ if f_type == 'favourites': semi_filtered = semi_filtered.filter(favourites__user=request.user.get_profile()) elif f_type == 'likes': semi_filtered = semi_filtered.filter(likes__user=request.user.get_profile()) - + """ if f_user is not None: semi_filtered = semi_filtered.filter(user__slug=f_user) @@ -159,16 +192,20 @@ class MixResource(BackboneCompatibleResource): bundle.data['user_profile_image'] = bundle.obj.user.get_small_profile_image() bundle.data['item_url'] = '/mix/%s' % bundle.obj.slug - bundle.data['play_count'] = bundle.obj.plays.count() - bundle.data['download_count'] = bundle.obj.downloads.count() - bundle.data['like_count'] = bundle.obj.likes.count() bundle.data['favourite_count'] = bundle.obj.favourites.count() + + bundle.data['play_count'] = bundle.obj.activity_plays.count() + bundle.data['download_count'] = bundle.obj.activity_downloads.count() + bundle.data['like_count'] = bundle.obj.activity_likes.count() + + bundle.data['tooltip'] = render_to_string('inc/player_tooltip.html', {'item': bundle.obj}) bundle.data['comment_count'] = bundle.obj.comments.count() bundle.data['genre-list'] = json.to_ajax(bundle.obj.genres.all(), 'description', 'slug') bundle.data['liked'] = bundle.obj.is_liked(bundle.request.user) - bundle.data['favourited'] = bundle.obj.is_favourited(bundle.request.user) + bundle.data['favourited'] = bundle.obj.favourites.filter(user=bundle.request.user).count() != 0 + return bundle diff --git a/spa/api/v1/UserResource.py b/spa/api/v1/UserResource.py index 6e5734a..ee497b3 100755 --- a/spa/api/v1/UserResource.py +++ b/spa/api/v1/UserResource.py @@ -1,14 +1,12 @@ -from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db.models import Count, Q from tastypie import fields from tastypie.authentication import Authentication from tastypie.authorization import DjangoAuthorization from django.conf.urls import url -from tastypie.http import HttpMultipleChoices, HttpGone +from tastypie.constants import ALL from tastypie.utils import trailing_slash from spa.api.v1.BackboneCompatibleResource import BackboneCompatibleResource -from spa.api.v1.MixResource import MixResource from spa.models.activity import ActivityFollow from spa.models.userprofile import UserProfile from spa.models.mix import Mix @@ -16,13 +14,18 @@ from spa.models.mix import Mix class UserResource(BackboneCompatibleResource): class Meta: - queryset = UserProfile.objects.all().annotate(mix_count=Count('mixes')).order_by('-mix_count') + queryset = UserProfile.objects.all().annotate(mix_count=Count('mixes'))\ + .extra(select={'u':'user'}).order_by('-mix_count') + favourites = fields.ToManyField('spa.api.v1.MixResource.MixResource', 'favourites', null=True) resource_name = 'user' excludes = ['is_active', 'is_staff', 'is_superuser', 'password'] ordering = ['mix_count'] + filtering = { + 'slug': ALL, + } authorization = DjangoAuthorization() authentication = Authentication() - favourites = fields.ToManyField(MixResource, 'favourites') + favourites = fields.ToManyField('spa.api.v1.MixResource', 'favourites') def _hydrateBitmapOption(self, source, comparator): return True if (source & comparator) != 0 else False @@ -32,9 +35,6 @@ class UserResource(BackboneCompatibleResource): url(r"^(?P%s)/(?P[\w\d_.-]+)/favourites%s$" % ( self._meta.resource_name, trailing_slash()), self.wrap_view('get_user_favourites'), name="api_get_user_favourites"), - url(r"^(?P%s)/(?P[\w\d_.-]+)/activity%s$" % ( - self._meta.resource_name, trailing_slash()), - self.wrap_view('get_user_activity'), name="api_get_user_activity"), url(r"^(?P%s)/(?P\d+)/$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"), url(r"^(?P%s)/(?P[\w\d_.-]+)/$" % self._meta.resource_name, @@ -49,6 +49,10 @@ class UserResource(BackboneCompatibleResource): """ def apply_filters(self, request, applicable_filters): + semi_filtered = super(UserResource, self).apply_filters(request, applicable_filters) + return semi_filtered + + def __apply_filters(self, request, applicable_filters): semi_filtered = super(UserResource, self).apply_filters(request, applicable_filters) q = request.GET.get('q', None) if q is not None: @@ -60,18 +64,6 @@ class UserResource(BackboneCompatibleResource): return semi_filtered - def get_user_favourites(self, request, **kwargs): - try: - obj = self.cached_obj_get(bundle=self.build_bundle(request=request), - **self.remove_api_resource_names(kwargs)) - except ObjectDoesNotExist: - return HttpGone() - except MultipleObjectsReturned: - return HttpMultipleChoices("More than one resource is found at this URI.") - - mixes = MixResource() - return mixes.get_list(request, favourites__user=obj.get_profile()) - def _patch_resource(self, bundle): #Handle the patched items from backbone if 'following' in bundle.data: @@ -131,7 +123,7 @@ class UserResource(BackboneCompatibleResource): self._hydrateBitmapOption(bundle.obj.activity_sharing_networks, UserProfile.ACTIVITY_SHARE_NETWORK_TWITTER) - bundle.data['like_count'] = Mix.objects.filter(likes__user=bundle.obj).count() + bundle.data['like_count'] = Mix.objects.filter(activity_likes__user=bundle.obj).count() bundle.data['favourite_count'] = Mix.objects.filter(favourites__user=bundle.obj).count() bundle.data['follower_count'] = bundle.obj.followers.count() bundle.data['following_count'] = bundle.obj.following.count() diff --git a/spa/management/commands/debugRelations.py b/spa/management/commands/debugRelations.py new file mode 100644 index 0000000..c1cfeec --- /dev/null +++ b/spa/management/commands/debugRelations.py @@ -0,0 +1,11 @@ +from django.core.management.base import NoArgsCommand +from spa.models import Mix + + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + list = Mix.objects.filter(slug='dss-on-deepvibes-radio-17th-july-jamie-o-sullivan')[0] + pass + except Exception, ex: + print "Debug exception: %s" % ex.message \ No newline at end of file diff --git a/spa/models/activity.py b/spa/models/activity.py index 2357922..7ca3d4d 100755 --- a/spa/models/activity.py +++ b/spa/models/activity.py @@ -71,7 +71,7 @@ class Activity(_BaseModel): class ActivityFollow(Activity): - to_user = models.ForeignKey('spa.UserProfile', related_name='follower_activity') + to_user = models.ForeignKey('spa.UserProfile', related_name='activity_follow') def get_target_user(self): return self.to_user @@ -93,7 +93,7 @@ class ActivityFollow(Activity): class ActivityFavourite(Activity): - mix = models.ForeignKey('spa.Mix', related_name='favourites') + mix = models.ForeignKey('spa.Mix', related_name='activity_favourites') def get_target_user(self): return self.mix.user @@ -112,7 +112,7 @@ class ActivityFavourite(Activity): class ActivityPlay(Activity): - mix = models.ForeignKey('spa.Mix', related_name='plays') + mix = models.ForeignKey('spa.Mix', related_name='activity_plays') def get_target_user(self): return self.mix.user @@ -131,7 +131,7 @@ class ActivityPlay(Activity): class ActivityLike(Activity): - mix = models.ForeignKey('spa.Mix', related_name='likes') + mix = models.ForeignKey('spa.Mix', related_name='activity_likes') def get_target_user(self): return self.mix.user @@ -150,7 +150,7 @@ class ActivityLike(Activity): class ActivityDownload(Activity): - mix = models.ForeignKey('spa.Mix', related_name='downloads') + mix = models.ForeignKey('spa.Mix', related_name='activity_downloads') def get_target_user(self): return self.mix.user diff --git a/spa/models/mix.py b/spa/models/mix.py index a7eeda3..9f0bca7 100755 --- a/spa/models/mix.py +++ b/spa/models/mix.py @@ -51,6 +51,9 @@ class Mix(_BaseModel): genres = models.ManyToManyField(Genre) + #activity based stuff + favourites = models.ManyToManyField(UserProfile) + def __unicode__(self): return self.title @@ -171,7 +174,7 @@ class Mix(_BaseModel): if user is None: return False if user.is_authenticated(): - return self.likes.filter(user=user).count() != 0 + return self.activity_likes.filter(user=user).count() != 0 return False @@ -182,9 +185,12 @@ class Mix(_BaseModel): if user.is_authenticated(): if value: if self.favourites.filter(user=user).count() == 0: - ActivityFavourite(user=user.get_profile(), mix=self).save() + self.favourites.add(user.get_profile()) + self.save() else: - self.favourites.filter(user=user).delete() + self.favourites.remove(user.get_profile()) + self.save() + except Exception, ex: self.logger.error("Exception updating favourite: %s" % ex.message) @@ -194,10 +200,10 @@ class Mix(_BaseModel): return if user.is_authenticated(): if value: - if self.likes.filter(user=user).count() == 0: + if self.activity_likes.filter(user=user).count() == 0: ActivityLike(user=user.get_profile(), mix=self).save() else: - self.likes.filter(user=user).delete() + self.activity_likes.filter(user=user).delete() except Exception, ex: self.logger.error("Exception updating like: %s" % ex.message) diff --git a/static/js/app/lib/backbone.dss.model.collection.js b/static/js/app/lib/backbone.dss.model.collection.js index 19ceecd..3b3978d 100755 --- a/static/js/app/lib/backbone.dss.model.collection.js +++ b/static/js/app/lib/backbone.dss.model.collection.js @@ -4,6 +4,67 @@ define(['backbone'], function (Backbone) { this.meta = response.meta || {}; this.page_count = Math.ceil(this.meta.total_count / this.meta.limit); return response.objects || response; + }, + /* + sync: function (method, model, options) { + var headers = {}; + + if (Backbone.Tastypie.apiKey && Backbone.Tastypie.apiKey.username) { + headers[ 'Authorization' ] = 'ApiKey ' + Backbone.Tastypie.apiKey.username + ':' + Backbone.Tastypie.apiKey.key; + } + + if (Backbone.Tastypie.csrfToken) { + headers[ 'X-CSRFToken' ] = Backbone.Tastypie.csrfToken; + } + + // Keep `headers` for a potential second request + headers = _.extend(headers, options.headers); + options.headers = headers; + + if (( method === 'create' && Backbone.Tastypie.doGetOnEmptyPostResponse ) || + ( method === 'update' && Backbone.Tastypie.doGetOnEmptyPutResponse )) { + var dfd = new $.Deferred(); + + // Set up 'success' handling + var success = options.success; + dfd.done(function (resp, textStatus, xhr) { + _.isFunction(success) && success(resp); + }); + + options.success = function (resp, textStatus, xhr) { + // If create is successful but doesn't return a response, fire an extra GET. + // Otherwise, resolve the deferred (which triggers the original 'success' callbacks). + if (!resp && ( xhr.status === 201 || xhr.status === 202 || xhr.status === 204 )) { // 201 CREATED, 202 ACCEPTED or 204 NO CONTENT; response null or empty. + var location = xhr.getResponseHeader('Location') || model.id; + return Backbone.ajax({ + url: location, + headers: headers, + success: dfd.resolve, + error: dfd.reject + }); + } + else { + return dfd.resolveWith(options.context || options, [ resp, textStatus, xhr ]); + } + }; + + // Set up 'error' handling + var error = options.error; + dfd.fail(function (xhr, textStatus, errorThrown) { + _.isFunction(error) && error(xhr.responseText); + }); + + options.error = function (xhr, textStatus, errorText) { + dfd.rejectWith(options.context || options, [ xhr, textStatus, xhr.responseText ]); + }; + + // Create the request, and make it accessibly by assigning it to the 'request' property on the deferred + dfd.request = Backbone.oldSync(method, model, options); + return dfd; + } + + return Backbone.oldSync(method, model, options); } - }); + */ + }) }); diff --git a/static/js/app/views/mix/mixItemView.js b/static/js/app/views/mix/mixItemView.js index 35ff84a..079ee1b 100755 --- a/static/js/app/views/mix/mixItemView.js +++ b/static/js/app/views/mix/mixItemView.js @@ -1,22 +1,26 @@ -// Generated by CoffeeScript 1.6.2 +// Generated by CoffeeScript 1.3.3 (function() { var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __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(['moment', 'app', 'vent', 'marionette', 'utils', 'models/comment/commentCollection', 'views/comment/commentListView', 'text!/tpl/MixListItemView'], function(moment, App, vent, Marionette, utils, CommentsCollection, CommentsListView, Template) { - var MixItemView, _ref; - + var MixItemView; MixItemView = (function(_super) { + __extends(MixItemView, _super); function MixItemView() { this.doStart = __bind(this.doStart, this); + this.renderComments = __bind(this.renderComments, this); + this.renderGenres = __bind(this.renderGenres, this); + this.onRender = __bind(this.onRender, this); - this.initialize = __bind(this.initialize, this); _ref = MixItemView.__super__.constructor.apply(this, arguments); - return _ref; + + this.initialize = __bind(this.initialize, this); + return MixItemView.__super__.constructor.apply(this, arguments); } MixItemView.prototype.template = _.template(Template); @@ -50,7 +54,6 @@ MixItemView.prototype.onRender = function() { var id, totalDuration, totalDurationText; - id = this.model.get('id'); if (this.model.get('duration')) { totalDuration = moment.duration(this.model.get('duration'), "seconds"); @@ -70,7 +73,6 @@ MixItemView.prototype.renderGenres = function() { var el; - el = this.el; $.each(this.model.get("genre-list"), function(data) { $("#genre-list", el).append('' + this.text + ''); @@ -81,7 +83,6 @@ MixItemView.prototype.renderComments = function() { var comments; - comments = new CommentsCollection(); comments.url = this.model.get("resource_uri") + "comments/"; comments.mix_id = this.model.id; @@ -89,7 +90,6 @@ comments.fetch({ success: function(data) { var content; - console.log(data); content = new CommentsListView({ collection: comments @@ -139,7 +139,6 @@ MixItemView.prototype.mixFavourite = function() { var app; - console.log("MixItemView: favouriteMix"); app = require('app'); vent.trigger("mix:favourite", this.model); @@ -154,7 +153,6 @@ MixItemView.prototype.mixShare = function(e) { var mode; - console.log("MixItemView: shareMix"); mode = $(e.currentTarget).data("mode"); console.log("MixItemView: " + mode); diff --git a/static/js/app/views/user/userItemView.js b/static/js/app/views/user/userItemView.js index 61cd0fd..c28e29e 100755 --- a/static/js/app/views/user/userItemView.js +++ b/static/js/app/views/user/userItemView.js @@ -1,18 +1,18 @@ -// Generated by CoffeeScript 1.6.2 +// Generated by CoffeeScript 1.3.3 (function() { var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __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(['app', 'moment', 'marionette', 'vent', 'text!/tpl/UserListItemView'], function(App, moment, Marionette, vent, Template) { - var UserItemView, _ref; - + var UserItemView; UserItemView = (function(_super) { + __extends(UserItemView, _super); function UserItemView() { - this.initialize = __bind(this.initialize, this); _ref = UserItemView.__super__.constructor.apply(this, arguments); - return _ref; + this.initialize = __bind(this.initialize, this); + return UserItemView.__super__.constructor.apply(this, arguments); } UserItemView.prototype.template = _.template(Template);