commit 6f9f29f5fc7040ef35782967068ff20a9fd7fdbe Author: Fergal Moran Date: Thu Apr 30 23:17:58 2015 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b68b21f --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +.gitignore~ +.coverage +.keystore +tags +.tags +.tags_sorted_by_file +.idea +*.pyc +*.swp +media/* +build/* +_working/* +static/CACHE/* +dss/localsettings.py +dss/storagesettings.py +dss/celery_settings.py +dss.conf +dss/debugsettings.py +mysql +test.py +mysql2pgsql.yml +downloads.txt +plays.txt +dss_ah +fixtures.json +dss_test.db +dsskeys +.ropeproject/* +*.map +./dds.sublime-project +./dds.sublime-workspace +./__krud/* +reload +reset +__krud/ diff --git a/INSTALL b/INSTALL new file mode 100755 index 0000000..9bd9ae2 --- /dev/null +++ b/INSTALL @@ -0,0 +1,29 @@ +#curl -u hb_client_2862_1:j2CbCM8H -H 'Accept: application/json' -H 'Content-type: application/json' http://c1.lon2.dediserve.com/virtual_machines.xml + +apt-get install git python-virtualenv postgresql-common libsndfile1-dev libpng++-dev libpng12-dev libboost-program-options-dev libjpeg-dev python-dev libsox-fmt-mp3 +virtualenv env +source env/bin/activate + +git clone https://github.com/fergalmoran/dss.git dss +cd dss +pip install -r requirements.txt +#can't get gstreamer working in a virtualenv +#I installed it using apt and did this - it's dirty and horrid. +cp /usr/lib/python2.7/dist-packages/pygst.py /home/fergalm/Dropbox/Private/deepsouthsounds.com/nixenv/lib/python2.7/site-packages/ + +#setup db host + +sudo -u postgres createuser deepsouthsounds --no-superuser --createdb --no-createrole --pwprompt +sudo -u postgres createdb deepsouthsounds --owner deepsouthsounds + +#skip this in production, start with a default +if production: + python manage.py dbrestore +else + python manage.py syncdb + python manage.py migrate + +#re-run compressor (stale paths will be in db) +python manage.py compress --force + +git clone https://github.com/fergalmoran/dss.lib lib diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..39626ac --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2012, Deep South Sounds (inc) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README b/README new file mode 100755 index 0000000..c868092 --- /dev/null +++ b/README @@ -0,0 +1,17 @@ +#Deep South Sounds +an open source audio sharing site + +Technology Stack +-------------- +##### Server +* Django +* Tastypie +* NodeJs (https://github.com/fergalmoran/dss-realtime) +* Celery +* RabbitMq + + +##### Client +* Backbone +* Marionette +* RequireJs diff --git a/angular_upgrade.md b/angular_upgrade.md new file mode 100755 index 0000000..a466134 --- /dev/null +++ b/angular_upgrade.md @@ -0,0 +1,11 @@ +#update comments user_id to be userprofile_id + UPDATE spa_comment SET user_id = spa_userprofile.id + FROM spa_userprofile + WHERE spa_comment.user_id = spa_userprofile.user_id + + +#import the avatars + python manage.py get_avatars + +#jiggle the waveforms + python manage.py zoom_convert_waveforms \ No newline at end of file diff --git a/apache/django_live.wsgi b/apache/django_live.wsgi new file mode 100755 index 0000000..19b139d --- /dev/null +++ b/apache/django_live.wsgi @@ -0,0 +1,16 @@ +import os +import sys + +path = '/var/www/deepsouthsounds.com/dss' +if path not in sys.path: + sys.path.append(path) + +path = '/var/www/deepsouthsounds.com' +if path not in sys.path: + sys.path.append(path) + + +os.environ['DJANGO_SETTINGS_MODULE'] = 'dss.settings' + +import django.core.handlers.wsgi +application = django.core.handlers.wsgi.WSGIHandler() diff --git a/api/__init__.py b/api/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/api/auth.py b/api/auth.py new file mode 100755 index 0000000..51fe2c8 --- /dev/null +++ b/api/auth.py @@ -0,0 +1,94 @@ +from requests import HTTPError +from rest_framework import parsers +from rest_framework.authentication import get_authorization_header +from rest_framework.authtoken.models import Token +from rest_framework.authtoken.serializers import AuthTokenSerializer +from rest_framework.response import Response +from rest_framework.status import * +from rest_framework.views import APIView +from rest_framework import status +from rest_framework import renderers +from social.apps.django_app.utils import strategy, load_strategy, load_backend +from dss import settings + + +class LoginException(Exception): + pass + + +@strategy() +def register_by_access_token(request, backend): + strat = load_strategy(request) + auth = get_authorization_header(request).split() + if not auth or auth[0].lower() != b'social': + raise LoginException("Unable to register_by_access_token: No token header provided") + + access_token = auth[1] + user = request.backend.do_auth(access_token) + return user + + +class ObtainAuthToken(APIView): + serializer_class = AuthTokenSerializer + model = Token + + def post(self, request): + # Here we call PSA to authenticate like we would if we used PSA on server side. + try: + backend = request.META.get('HTTP_AUTH_BACKEND') + if backend is None: + # Work around django test client oddness + return Response("No Auth-Backend header specified", HTTP_400_BAD_REQUEST) + + user = register_by_access_token(request, backend) + + # If user is active we get or create the REST token and send it back with user data + if user and user.is_active: + token, created = Token.objects.get_or_create(user=user) + return Response({ + 'slug': user.userprofile.slug, + 'token': token.key + }) + except LoginException, ex: + return Response(ex.message, HTTP_400_BAD_REQUEST) + except HTTPError, ex: + return Response(ex.message, HTTP_400_BAD_REQUEST) + + +class ObtainUser(APIView): + throttle_classes = () + permission_classes = () + parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) + renderer_classes = (renderers.JSONRenderer,) + serializer_class = AuthTokenSerializer + model = Token + + def get(self, request): + serializer = self.serializer_class(data=request.DATA) + if request.META.get('HTTP_AUTHORIZATION'): + + auth = request.META.get('HTTP_AUTHORIZATION').split() + + if not auth or auth[0].lower() != b'token' or len(auth) != 2: + msg = 'Invalid token header. No credentials provided.' + return Response(msg, status=status.HTTP_401_UNAUTHORIZED) + + token = Token.objects.get(key=auth[1]) + if token and token.user.is_active: + return Response({'id': token.user_id, 'name': token.user.username, 'firstname': token.user.first_name, + 'userRole': 'user', 'token': token.key}) + else: + return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED) + + +class ObtainLogout(APIView): + throttle_classes = () + permission_classes = () + parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) + renderer_classes = (renderers.JSONRenderer,) + serializer_class = AuthTokenSerializer + model = Token + + # Logout le user + def get(self, request): + return Response({'User': ''}) diff --git a/api/pipeline.py b/api/pipeline.py new file mode 100755 index 0000000..5b395cb --- /dev/null +++ b/api/pipeline.py @@ -0,0 +1,34 @@ +from django.core.files.base import ContentFile +from requests import request, ConnectionError + + +def save_profile(backend, user, response, is_new, *args, **kwargs): + if backend.name == 'google-oauth2': + if response.get('image') and response['image'].get('url'): + url = response['image'].get('url') + profile = user.userprofile + + try: + response = request('GET', url) + response.raise_for_status() + except ConnectionError: + pass + else: + profile.avatar_image.save(u'', + ContentFile(response.content), + save=False) + profile.save() + elif backend.name == 'facebook': + profile = user.userprofile + url = 'http://graph.facebook.com/{0}/picture'.format(response['id']) + try: + response = request('GET', url, params={'type': 'large'}) + response.raise_for_status() + except ConnectionError: + pass + else: + profile.avatar_image.save(u'', + ContentFile(response.content), + save=False + ) + profile.save() diff --git a/api/serialisers.py b/api/serialisers.py new file mode 100755 index 0000000..f788741 --- /dev/null +++ b/api/serialisers.py @@ -0,0 +1,392 @@ +import json +# from django.core.serializers import serialize +from django.db.models import Count +from rest_framework import serializers +from dss import settings +from spa.models import Activity +from spa.models.activity import ActivityDownload, ActivityPlay +from spa.models.genre import Genre +from spa.models.notification import Notification +from spa.models.userprofile import UserProfile +from spa.models.mix import Mix +from spa.models.comment import Comment + + +class InlineUserProfileSerializer(serializers.ModelSerializer): + class Meta: + model = UserProfile + fields = ( + 'slug', + 'first_name', + 'last_name', + 'display_name', + 'avatar_image', + 'avatar_image_tiny', + ) + + first_name = serializers.ReadOnlyField(source='get_first_name') + last_name = serializers.ReadOnlyField(source='get_last_name') + display_name = serializers.ReadOnlyField(source='get_nice_name') + avatar_image = serializers.SerializerMethodField() + avatar_image_tiny = serializers.SerializerMethodField() + + def get_avatar_image(self, obj): + return obj.get_sized_avatar_image(64, 64) + + def get_avatar_image_tiny(self, obj): + return obj.get_sized_avatar_image(32, 32) + + def to_representation(self, instance): + if instance.user.is_anonymous(): + return { + 'avatar_image': settings.DEFAULT_USER_IMAGE, + 'display_name': settings.DEFAULT_USER_NAME, + 'slug': '' + } + + return super(serializers.ModelSerializer, self).to_representation(instance) + + +class InlineMixSerializer(serializers.ModelSerializer): + class Meta: + model = Mix + fields = ( + 'title', + 'slug', + 'title', + 'description', + 'mix_image', + ) + + mix_image = serializers.ReadOnlyField(source='get_image_url') + + +class LikeSerializer(serializers.ModelSerializer): + class Meta: + model = UserProfile + fields = ( + 'display_name', + 'slug', + ) + + display_name = serializers.ReadOnlyField(source='get_nice_name') + + +class GenreSerializer(serializers.ModelSerializer): + class Meta: + model = Genre + fields = ( + 'slug', + 'description' + ) + + +class InlineActivitySerializer(serializers.ModelSerializer): + user = serializers.SerializerMethodField() + + def get_user(self, obj): + try: + if obj.user is not None: + return obj.user.get_nice_name() + except: + pass + + return "Anomymouse" + + +class InlineActivityPlaySerializer(InlineActivitySerializer): + class Meta: + model = ActivityPlay + fields = ( + 'user', + 'date' + ) + + +class InlineActivityDownloadSerializer(InlineActivitySerializer): + class Meta: + model = ActivityDownload + fields = ( + 'user', + 'date' + ) + + +class MixSerializer(serializers.ModelSerializer): + class Meta: + model = Mix + fields = [ + 'id', + 'slug', + 'uid', + 'title', + 'description', + 'user', + 'duration', + 'waveform_url', + 'waveform_progress_url', + 'mix_image', + # 'stream_url', + 'download_allowed', + 'can_edit', + 'genres', + 'likes', + 'favourites', + 'activity_plays', + 'activity_downloads', + 'is_liked', + ] + + slug = serializers.ReadOnlyField(required=False) + user = InlineUserProfileSerializer(many=False, required=False, read_only=True) + waveform_url = serializers.ReadOnlyField(source='get_waveform_url') + waveform_progress_url = serializers.ReadOnlyField(source='get_waveform_progress_url') + mix_image = serializers.ReadOnlyField(source='get_image_url') + can_edit = serializers.SerializerMethodField() + + genres = GenreSerializer(many=True, required=False, read_only=True) + likes = LikeSerializer(many=True, required=False, read_only=False) # slug_field='slug', many=True, read_only=True) + favourites = serializers.SlugRelatedField(slug_field='slug', many=True, read_only=True) + activity_plays = InlineActivityPlaySerializer(many=True, read_only=True) + activity_downloads = InlineActivityDownloadSerializer(read_only=True) + is_liked = serializers.SerializerMethodField(read_only=True) + + def update(self, instance, validated_data): + # all nested representations need to be serialized separately here + likes = validated_data['likes'] + + # get any likes that aren't in passed bundle + unliked = instance.likes.exclude(user__userprofile__slug__in=[l['slug'] for l in likes]) + for ul in unliked: + # check that the user removing the like is an instance of the current user + # for now, only the current user can like stuff + if ul == self.context['request'].user.userprofile: + instance.update_liked(ul, False) + + for like in likes: + # check that the user adding the like is an instance of the current user + # for now, only the current user can like stuff + try: + user = UserProfile.objects.get(slug=like['slug']) + if user is not None and user == self.context['request'].user.userprofile: + instance.update_favourite(user, True) + + except UserProfile.DoesNotExist: + pass + validated_data.pop('likes', None) + return super(MixSerializer, self).update(instance, validated_data) + + def is_valid(self, raise_exception=False): + return super(MixSerializer, self).is_valid(raise_exception) + + def get_avatar_image(self, obj): + return obj.user.get_sized_avatar_image(32, 32) + + def get_can_edit(self, obj): + user = self.context['request'].user + if user.is_authenticated(): + return user.is_staff or obj.user.id == user.userprofile.id + + return False + + def get_is_favourited(self, obj): + user = self.context['request'].user + return obj.is_favourited(user) if user.is_authenticated() else False + + def get_validation_exclusions(self, instance=None): + exclusions = super(MixSerializer, self).get_validation_exclusions() + return exclusions + ['genres', 'comments', 'slug', 'user'] + + def get_is_liked(self, obj): + user = self.context['request'].user + return obj.is_liked(user) if user.is_authenticated() else False + + +class UserProfileSerializer(serializers.ModelSerializer): + roles = serializers.SerializerMethodField() + mixes = InlineMixSerializer(many=True, required=False) + likes = serializers.SlugRelatedField(slug_field='slug', many=True, read_only=True) + favourites = serializers.SlugRelatedField(slug_field='slug', many=True, read_only=True) + following = InlineUserProfileSerializer(many=True, read_only=True) + followers = InlineUserProfileSerializer(many=True, read_only=True) + first_name = serializers.ReadOnlyField(source='get_first_name') + last_name = serializers.ReadOnlyField(source='get_last_name') + display_name = serializers.ReadOnlyField(source='get_nice_name') + isme = serializers.SerializerMethodField() + date_joined = serializers.SerializerMethodField() + last_login = serializers.SerializerMethodField() + title = serializers.SerializerMethodField() + profile_image_small = serializers.SerializerMethodField() + profile_image_medium = serializers.SerializerMethodField() + profile_image_header = serializers.SerializerMethodField() + + top_tags = serializers.SerializerMethodField() + + class Meta: + model = UserProfile + lookup_field = 'slug' + fields = ( + 'roles', + 'date_joined', + 'last_login', + 'first_name', + 'last_name', + 'display_name', + 'description', + 'title', + 'profile_image_small', + 'profile_image_medium', + 'profile_image_header', + 'slug', + 'mixes', + 'likes', + 'isme', + 'favourites', + 'following', + 'followers', + 'top_tags', + ) + + def get_title(self, obj): + try: + if obj.description: + return obj.description[:75] + (obj.description[75:] and '..') + else: + return settings.DEFAULT_USER_TITLE + except: + return settings.DEFAULT_USER_TITLE + + def get_roles(self, obj): + return obj.get_roles() + + def get_isme(self, obj): + return self.context['request'].user.pk == obj.user_id + + def get_date_joined(self, obj): + return obj.user.date_joined + + def get_last_login(self, obj): + return obj.user.last_login + + def get_profile_image_small(self, obj): + return obj.get_sized_avatar_image(32, 32) + + def get_profile_image_medium(self, obj): + return obj.get_sized_avatar_image(170, 170) + + def get_profile_image_header(self, obj): + return obj.get_sized_avatar_image(1200, 150) + + def get_top_tags(self, obj): + return list( + Genre.objects.filter(mix__user__slug='fergalmoran'). + annotate(total=Count('mix')). + order_by('-total'). + values('total', 'description', 'slug')[0:3]) + + +class CommentSerializer(serializers.HyperlinkedModelSerializer): + user = serializers.SlugRelatedField(slug_field='slug', read_only=True) + avatar_image = serializers.SerializerMethodField() + user_display_name = serializers.SerializerMethodField('get_display_name') + mix = serializers.PrimaryKeyRelatedField(read_only=True) + can_edit = serializers.SerializerMethodField() + + class Meta: + model = Comment + fields = ( + 'id', + 'comment', + 'time_index', + 'date_created', + 'user', + 'user_display_name', + 'avatar_image', + 'mix', + 'can_edit', + ) + + def get_display_name(self, obj): + if obj.user is not None: + return obj.user.get_nice_name() + else: + return settings.DEFAULT_USER_NAME + + def get_avatar_image(self, obj): + if obj.user is not None: + return obj.user.get_sized_avatar_image(48, 48) + else: + return settings.DEFAULT_USER_IMAGE + + def get_can_edit(self, obj): + user = self.context['request'].user + if user.is_authenticated(): + return user.is_staff or obj.user.id == user.userprofile.id + + return False + + +class HitlistSerializer(serializers.ModelSerializer): + display_name = serializers.SerializerMethodField(method_name='get_display_name') + avatar_image = serializers.SerializerMethodField(method_name='get_avatar_image') + + class Meta: + model = UserProfile + fields = ( + 'display_name', + 'description', + 'slug', + 'avatar_image' + ) + + def get_display_name(self, obj): + return obj.get_nice_name() + + def get_avatar_image(self, obj): + return obj.get_sized_avatar_image(170, 170) + + +class ActivitySerializer(serializers.HyperlinkedModelSerializer): + from_user = InlineUserProfileSerializer(source='get_user') + to_user = InlineUserProfileSerializer(source='get_target_user') + verb = serializers.CharField(source='get_verb_past') + object_type = serializers.CharField(source='get_object_type') + object_name = serializers.CharField(source='get_object_name') + object_slug = serializers.CharField(source='get_object_slug') + + class Meta: + model = Activity + fields = ( + 'id', + 'date', + 'from_user', + 'to_user', + 'verb', + 'object_type', + 'object_name', + 'object_slug', + ) + + +class NotificationSerializer(serializers.HyperlinkedModelSerializer): + from_user = UserProfileSerializer(many=False, required=False) + display_name = serializers.SerializerMethodField(method_name='get_display_name') + avatar_image = serializers.SerializerMethodField(method_name='get_avatar_image') + + class Meta: + model = Notification + fields = ( + 'id', + 'notification_url', + 'from_user', + 'display_name', + 'avatar_image', + 'verb', + 'target', + ) + + def get_display_name(self, obj): + return settings.DEFAULT_USER_NAME if obj.from_user is None else obj.from_user.get_nice_name() + + def get_avatar_image(self, obj): + return settings.DEFAULT_USER_IMAGE if obj.from_user is None else obj.from_user.get_sized_avatar_image(170, 170) diff --git a/api/tests.py b/api/tests.py new file mode 100755 index 0000000..7ce503c --- /dev/null +++ b/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/api/tests/upload.py b/api/tests/upload.py new file mode 100644 index 0000000..8ac64b9 --- /dev/null +++ b/api/tests/upload.py @@ -0,0 +1,6 @@ +import unittest +from spa.models.mix import Mix + +class UploadTest(unittest.TestCase): + def run(self, result=None): + print "Argle Bargle" \ No newline at end of file diff --git a/api/urls.py b/api/urls.py new file mode 100755 index 0000000..5dbf333 --- /dev/null +++ b/api/urls.py @@ -0,0 +1,38 @@ +from api.auth import ObtainAuthToken, ObtainUser, ObtainLogout +from api.views import CommentViewSet, MixViewSet, UserProfileViewSet, NotificationViewSet, PartialMixUploadView, \ + GenreViewSet, ActivityViewSet, HitlistViewset, AttachedImageUploadView, DownloadItemView, SearchResultsView +from django.conf.urls import url, patterns, include +from rest_framework_nested import routers + +router = routers.SimpleRouter(trailing_slash=True) +router.register(r'notification', NotificationViewSet) +router.register(r'hitlist', HitlistViewset) +router.register(r'comments', CommentViewSet) +router.register(r'user', UserProfileViewSet, base_name='userprofile') +router.register(r'activity', ActivityViewSet, base_name='activity') + +router.register(r'mix', MixViewSet) +mix_router = routers.NestedSimpleRouter(router, r'mix', lookup='mix') +mix_router.register('comments', CommentViewSet) + +""" +router.register(r'hitlist', HitlistViewset, base_name='hitlist') +router.register(r'notification', NotificationViewSet, base_name='notification') +""" +router.register(r'genre', GenreViewSet, base_name='genre') +urlpatterns = patterns( + '', + url(r'^', include(router.urls)), + url(r'^', include(mix_router.urls)), + url(r'_download/', DownloadItemView.as_view()), + url(r'_upload/$', PartialMixUploadView.as_view()), + url(r'_image/$', AttachedImageUploadView.as_view()), + url(r'_search/$', SearchResultsView.as_view()), + url(r'^', include(router.urls)), + + url(r'^login/', ObtainAuthToken.as_view()), + url(r'^user/', ObtainUser.as_view()), + url(r'^logout/', ObtainLogout.as_view()), + + url('', include('social.apps.django_app.urls', namespace='social')), +) diff --git a/api/views.py b/api/views.py new file mode 100755 index 0000000..b212fe0 --- /dev/null +++ b/api/views.py @@ -0,0 +1,224 @@ +import logging +import os + +from django.core.exceptions import PermissionDenied, ObjectDoesNotExist +from django.core.files.base import ContentFile +from django.core.files.storage import FileSystemStorage +from django.db.models import Count +from django.http.response import HttpResponse +from django.utils.encoding import smart_str +from rest_framework import viewsets +from rest_framework import views +from rest_framework.decorators import detail_route +from rest_framework.parsers import FileUploadParser +from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly +from rest_framework.response import Response +from rest_framework.status import HTTP_202_ACCEPTED, HTTP_401_UNAUTHORIZED, HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND, \ + HTTP_200_OK, HTTP_204_NO_CONTENT +from rest_framework import filters + +from api.serialisers import MixSerializer, UserProfileSerializer, NotificationSerializer, \ + ActivitySerializer, HitlistSerializer, CommentSerializer, GenreSerializer +from dss import settings +from core.tasks import create_waveform_task, archive_mix_task +from spa.models.genre import Genre +from spa.models.activity import Activity, ActivityPlay +from spa.models.mix import Mix +from spa.models.comment import Comment +from spa.models.notification import Notification +from spa.models.userprofile import UserProfile + + +logger = logging.getLogger(__name__) + + +class CommentViewSet(viewsets.ModelViewSet): + queryset = Comment.objects.all() + serializer_class = CommentSerializer + permission_classes = (IsAuthenticatedOrReadOnly,) + filter_fields = ( + 'comment', + 'mix__slug', + ) + ordering_fields = ( + 'id', + ) + + def perform_create(self, serializer): + if 'mix_id' in self.request.DATA: + try: + mix = Mix.objects.get(pk=self.request.DATA['mix_id']) + if mix is not None: + serializer.save(mix=mix, user=self.request.user.userprofile) + except Mix.DoesNotExist: + pass + + +class UserProfileViewSet(viewsets.ModelViewSet): + queryset = UserProfile.objects.annotate(mix_count=Count('mixes')).order_by('-mix_count') + serializer_class = UserProfileSerializer + permission_classes = (IsAuthenticatedOrReadOnly,) + lookup_field = 'slug' + filter_fields = ( + 'slug', + ) + + +class MixViewSet(viewsets.ModelViewSet): + queryset = Mix.objects.all() + serializer_class = MixSerializer + lookup_field = 'slug' + permission_classes = (IsAuthenticatedOrReadOnly,) + filter_fields = ( + 'waveform_generated', + 'slug', + 'user', + 'is_featured', + ) + + @detail_route() + def stream_url(self, request, **kwargs): + mix = self.get_object() + return Response({'url': mix.get_stream_url()}) + + def get_queryset(self): + if 'friends' in self.request.QUERY_PARAMS: + if self.request.user.is_authenticated(): + rows = Mix.objects.filter(user__in=self.request.user.userprofile.following.all()) + return rows + else: + raise PermissionDenied("Not allowed") + else: + return Mix.objects.all() + + def perform_create(self, serializer): + serializer.save(user=self.request.user.userprofile) + + +class AttachedImageUploadView(views.APIView): + parser_classes = (FileUploadParser,) + + def post(self, request): + if request.FILES['file'] is None or request.DATA.get('data') is None: + return Response(status=HTTP_400_BAD_REQUEST) + + file_obj = request.FILES['file'] + file_hash = request.DATA.get('data') + try: + mix = Mix.objects.get(uid=file_hash) + if mix: + mix.mix_image = file_obj + mix.save() + return Response(HTTP_202_ACCEPTED) + except ObjectDoesNotExist: + return Response(status=HTTP_404_NOT_FOUND) + + return Response(status=HTTP_401_UNAUTHORIZED) + + +class SearchResultsView(views.APIView): + def get(self, request, format=None): + q = request.GET.get('q', '') + if len(q) > 0: + m = [{ + 'title': mix.title, + 'image': mix.get_image_url(), + 'slug': mix.slug, + 'url': mix.get_absolute_url(), + 'description': mix.description + } for mix in Mix.objects.filter(title__icontains=q)[0:10]] + return Response(m) + + return HttpResponse(status=HTTP_204_NO_CONTENT) + + +class PartialMixUploadView(views.APIView): + parser_classes = (FileUploadParser,) + permission_classes = (IsAuthenticated,) + + # noinspection PyBroadException + def post(self, request): + try: + uid = request.META.get('HTTP_UPLOAD_HASH') + in_file = request.FILES['file'] if request.FILES else None + file_name, extension = os.path.splitext(in_file.name) + + file_storage = FileSystemStorage(location=os.path.join(settings.CACHE_ROOT, "mixes")) + cache_file = file_storage.save("%s%s" % (uid, extension), ContentFile(in_file.read())) + response = 'File creation in progress' + + try: + create_waveform_task.delay(in_file=os.path.join(file_storage.base_location, cache_file), uid=uid) + create_waveform_task.delay(in_file=os.path.join(file_storage.base_location, cache_file), uid=uid) + except Exception, ex: + response = \ + 'Unable to connect to waveform generation task, there may be a delay in getting your mix online' + + file_dict = { + 'response': response, + 'size': in_file.size, + 'uid': uid + } + return Response(file_dict, HTTP_202_ACCEPTED) + except Exception, ex: + logger.exception(ex.message) + raise + + +class HitlistViewset(viewsets.ModelViewSet): + queryset = UserProfile.objects.all().annotate(mix_count=Count('mixes')).order_by('-mix_count')[0:10] + serializer_class = HitlistSerializer + + +class ActivityViewSet(viewsets.ModelViewSet): + queryset = ActivityPlay.objects.all() #select_subclasses() + serializer_class = ActivitySerializer + + def get_queryset(self): + user = self.request.user + if not user.is_authenticated(): + raise PermissionDenied("Not allowed") + + return ActivityPlay.objects.filter(mix__user=user).order_by("-id") + + +class DownloadItemView(views.APIView): + def get(self, request, *args, **kwargs): + try: + mix = Mix.objects.get(uid=request.query_params['uid']) + return Response({'url': mix.get_download_url()}, HTTP_200_OK) + + except ObjectDoesNotExist: + return Response("Not Found", HTTP_404_NOT_FOUND) + + +class NotificationViewSet(viewsets.ModelViewSet): + queryset = Notification.objects.all() + serializer_class = NotificationSerializer + + def get_queryset(self): + user = self.request.user + if not user.is_authenticated(): + raise PermissionDenied("Not allowed") + + return Notification.objects.filter(to_user=user).order_by('-date')[0:5] + + +class GenreViewSet(viewsets.ModelViewSet): + queryset = Genre.objects.all() + serializer_class = GenreSerializer + + def get_queryset(self): + if 'q' in self.request.QUERY_PARAMS: + rows = Genre.objects \ + .annotate(used=Count('mix')) \ + .filter(description__icontains=self.request.QUERY_PARAMS['q']) \ + .only('description') \ + .order_by('-used') + return rows + else: + rows = Genre.objects \ + .annotate(used=Count('mix')) \ + .only('description') \ + .order_by('-used') + return rows diff --git a/core/__init__.py b/core/__init__.py new file mode 100755 index 0000000..8133f5e --- /dev/null +++ b/core/__init__.py @@ -0,0 +1 @@ +__author__ = 'fergalm' diff --git a/core/analytics/__init__.py b/core/analytics/__init__.py new file mode 100755 index 0000000..6d4caf8 --- /dev/null +++ b/core/analytics/__init__.py @@ -0,0 +1 @@ +__author__ = 'fergalm' diff --git a/core/analytics/google.py b/core/analytics/google.py new file mode 100755 index 0000000..2ca215d --- /dev/null +++ b/core/analytics/google.py @@ -0,0 +1,25 @@ +from django import template +from dss import settings + +register = template.Library() + + +class ShowGoogleAnalyticsJS(template.Node): + def render(self, context): + code = getattr(settings, "GOOGLE_ANALYTICS_CODE", False) + if not code: + return "" + + if 'user' in context and context['user'] and context['user'].is_staff: + return "" + + if settings.DEBUG: + return "" + + return """""" \ No newline at end of file diff --git a/core/decorators.py b/core/decorators.py new file mode 100755 index 0000000..e803e25 --- /dev/null +++ b/core/decorators.py @@ -0,0 +1,26 @@ +from decorator import decorator +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response +from django.template.context import RequestContext + +@decorator +def render_template(func, *args, **kwargs): + """ + using example: + @render_template + def view(request, template='abc.html'): + slot = "this is a slot" + return template, {'slot' : slot} + """ + request = args[0] + _call = func(*args, **kwargs) + + if isinstance(_call, HttpResponseRedirect): + return _call + + if isinstance(_call, tuple): + template, context = _call + else: + template, context = _call, {} + + return render_to_response(template, context_instance=RequestContext(request, context)) diff --git a/core/realtime/__init__.py b/core/realtime/__init__.py new file mode 100755 index 0000000..6d4caf8 --- /dev/null +++ b/core/realtime/__init__.py @@ -0,0 +1 @@ +__author__ = 'fergalm' diff --git a/core/realtime/activity.py b/core/realtime/activity.py new file mode 100755 index 0000000..9ad8562 --- /dev/null +++ b/core/realtime/activity.py @@ -0,0 +1,16 @@ +import requests +from core.serialisers import json +from dss import localsettings, settings + + +def post_activity(session_id, activity_url): + payload = { + 'sessionid': session_id, + 'message': activity_url + } + data = json.dumps(payload) + r = requests.post(localsettings.REALTIME_HOST + 'activity', data=data, headers=settings.REALTIME_HEADERS) + if r.status_code == 200: + return "" + else: + return r.text diff --git a/core/realtime/notification.py b/core/realtime/notification.py new file mode 100755 index 0000000..dfce7b9 --- /dev/null +++ b/core/realtime/notification.py @@ -0,0 +1,33 @@ +import requests +import logging +from requests.packages.urllib3.exceptions import ConnectionError +from dss import localsettings +import json +# classes to avoid duplicating constants below +HEADERS = { + 'content-type': 'application/json' +} + +logger = logging.getLogger('spa') + + +def post_notification(session_id, image, message): + try: + payload = { + 'sessionid': session_id, + 'image': image, + 'message': message + } + data = json.dumps(payload) + r = requests.post( + localsettings.REALTIME_HOST + 'notification', + data=data, + headers=HEADERS + ) + if r.status_code == 200: + return "" + else: + return r.text + except ConnectionError: + #should probably implement some sort of retry in here + pass diff --git a/core/serialisers/__init__.py b/core/serialisers/__init__.py new file mode 100755 index 0000000..6d4caf8 --- /dev/null +++ b/core/serialisers/__init__.py @@ -0,0 +1 @@ +__author__ = 'fergalm' diff --git a/core/tasks.py b/core/tasks.py new file mode 100755 index 0000000..774149a --- /dev/null +++ b/core/tasks.py @@ -0,0 +1,47 @@ +import shutil +from celery.task import task +import os +from core.utils.cdn import upload_to_azure + +try: + from django.contrib.gis.geoip import GeoIP +except ImportError: + pass + +from core.utils.waveform import generate_waveform +from dss import settings + + +@task(time_limit=3600) +def create_waveform_task(in_file, uid): + out_file = os.path.join(settings.MEDIA_ROOT, 'waveforms/%s.png' % uid) + print "Creating waveform \n\tIn: %s\n\tOut: %s" % (in_file, out_file) + generate_waveform(in_file, out_file) + if os.path.isfile(out_file): + print "Waveform generated successfully" + out_file, extension = os.path.splitext(in_file) + new_file = os.path.join(settings.MEDIA_ROOT, "mixes", "%s%s" % (uid, extension)) + print "Moving cache audio clip from %s to %s" % (in_file, new_file) + shutil.move(in_file, new_file) + print "Uid: %s" % uid + else: + print "Outfile is missing" + + +@task(time_limit=3600) +def archive_mix_task(in_file, uid): + print "Sending {0} to azure".format(uid) + upload_to_azure(in_file, uid) + +@task +def update_geo_info_task(ip_address, profile_id): + try: + ip = '188.141.70.110' if ip_address == '127.0.0.1' else ip_address + if ip: + g = GeoIP() + city = g.city(ip) + country = g.country(ip) + print "Updated user location" + except Exception, e: + print e.message + pass diff --git a/core/utils/__init__.py b/core/utils/__init__.py new file mode 100755 index 0000000..8133f5e --- /dev/null +++ b/core/utils/__init__.py @@ -0,0 +1 @@ +__author__ = 'fergalm' diff --git a/core/utils/audio/__init__.py b/core/utils/audio/__init__.py new file mode 100755 index 0000000..dfa5fc7 --- /dev/null +++ b/core/utils/audio/__init__.py @@ -0,0 +1 @@ +class Mp3FileNotFoundException(Exception): pass diff --git a/core/utils/audio/mp3.py b/core/utils/audio/mp3.py new file mode 100755 index 0000000..602e74f --- /dev/null +++ b/core/utils/audio/mp3.py @@ -0,0 +1,29 @@ +from mutagen.easyid3 import EasyID3, mutagen +from mutagen.id3 import ID3, TPE1, TIT2, TALB, TCON, COMM, TDRC +from mutagen.mp3 import MP3 +from core.utils.audio import Mp3FileNotFoundException + + +def mp3_length(source_file): + try: + audio = MP3(source_file) + return audio.info.length + except IOError: + raise Mp3FileNotFoundException("Audio file not found: %s" % source_file) + + +def tag_mp3(source_file, artist, title, url="", album="", year="", comment="", genres=""): + try: + audio = EasyID3(source_file) + except mutagen.id3.ID3NoHeaderError: + audio = mutagen.File(source_file, easy=True) + audio.add_tags() + + audio["artist"] = artist + audio["title"] = title + audio["genre"] = genres + audio["website"] = url + audio["copyright"] = "Deep South Sounds" + audio["album"] = album + + audio.save(v1=2) diff --git a/core/utils/cdn.py b/core/utils/cdn.py new file mode 100755 index 0000000..58fca12 --- /dev/null +++ b/core/utils/cdn.py @@ -0,0 +1,50 @@ +import os +from azure import WindowsAzureMissingResourceError +from azure.storage import BlobService +from core.utils.url import url_path_join +from dss import settings +from dss.storagesettings import AZURE_ACCOUNT_NAME, AZURE_ACCOUNT_KEY, AZURE_CONTAINER +from libcloud.storage.types import Provider +from libcloud.storage.providers import get_driver + + +def upload_to_azure(in_file, filetype, uid): + if os.path.isfile(in_file): + print "Uploading file for: %s" % in_file + file_name = "%s.%s" % (uid, filetype) + archive_path = url_path_join(settings.AZURE_ITEM_BASE_URL, settings.AZURE_CONTAINER, file_name) + + cls = get_driver(Provider.AZURE_BLOBS) + driver = cls(settings.AZURE_ACCOUNT_NAME, settings.AZURE_ACCOUNT_KEY) + container = driver.get_container(container_name=settings.AZURE_CONTAINER) + + with open(in_file, 'rb') as iterator: + obj = driver.upload_object_via_stream( + iterator=iterator, + container=container, + object_name=file_name + ) + print "Uploaded" + return obj + + return None + + +def set_azure_details(blob_name, download_name): + try: + blob_service = BlobService(AZURE_ACCOUNT_NAME, AZURE_ACCOUNT_KEY) + blob = blob_service.get_blob(AZURE_CONTAINER, blob_name) + if blob: + blob_service.set_blob_properties( + AZURE_CONTAINER, + blob_name, + x_ms_blob_content_type='application/octet-stream', + x_ms_blob_content_disposition='attachment;filename="{0}"'.format(download_name) + ) + print "Processed: %s" % download_name + else: + print "No blob found for: %s" % download_name + except WindowsAzureMissingResourceError: + print "No blob found for: %s" % download_name + except Exception, ex: + print "Error processing blob %s: %s" % (download_name, ex.message) diff --git a/core/utils/file.py b/core/utils/file.py new file mode 100755 index 0000000..85355f7 --- /dev/null +++ b/core/utils/file.py @@ -0,0 +1,6 @@ +import os + +def generate_save_file_name(uid, prefix, filename): + filename, extension = os.path.splitext(filename) + ret = "%s%s" % ('/'.join([prefix, uid]), extension) + return ret diff --git a/core/utils/html.py b/core/utils/html.py new file mode 100755 index 0000000..2bf8848 --- /dev/null +++ b/core/utils/html.py @@ -0,0 +1,18 @@ +from HTMLParser import HTMLParser + +class HTMLStripper(HTMLParser): + """ + Class that cleans HTML, removing all tags and HTML entities. + """ + def __init__(self): + self.reset() + self.fed = [] + def handle_data(self, d): + self.fed.append(d) + def get_data(self): + return ''.join(self.fed) + def strip(self, d): + self.reset() + self.fed = [] + self.feed(d) + return self.get_data().strip() \ No newline at end of file diff --git a/core/utils/ice.py b/core/utils/ice.py new file mode 100755 index 0000000..69b7607 --- /dev/null +++ b/core/utils/ice.py @@ -0,0 +1,80 @@ +#!/usr/bin/python + +"""Returns icecast metadata from a stream as a JSON object. + Optionally posts it to a url.""" + +import socket +import json +import urllib2 +import getopt +import sys + + +def usage (): + print """usage: + -h host to get metadata from + -m mount to get metadata from + [-p port to get metadata from (default 8000)] + [-u url to post metadata to as json]""" + +try: + optlist, cmdline = getopt.getopt(sys.argv[1:],'h:p:m:u:') +except getopt.GetoptError: + sys.stderr.write("invalid options\n") + usage() + sys.exit(1) + +# defaults +port = 8000 + +# check options +for opt in optlist: + if opt[0] == '-h': + host=opt[1] + if opt[0] == '-p': + port=int(opt[1]) + if opt[0] == '-m': + mount=opt[1] + if opt[0] == '-u': + posturl=opt[1] + +# required options +try: + host + port + mount +except NameError: + usage() + sys.exit(1) + + +def get_data(host, port, mount): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect ((host, port)) + s.sendall('GET %s HTTP/1.0\r\n' + 'Host: %s:%d\r\n' + 'User-Agent: Ice Meta Fetcher\r\n' + 'Connection: close\r\n' + 'Icy-Metadata: 1\r\n' + '\r\n' % (mount, host, port)) + data = s.recv(1024).decode('utf-8', 'ignore').encode('utf-8') + s.close() + pdata = dict([d.split(':',1) for d in data.split('\r\n') if d.count("icy")]) + if pdata.has_key("icy-br"): + return json.dumps(pdata) + + + + +jdata = get_data(host, port, mount) +#skip empty crap +if jdata: + print jdata + +try: + # this post is optional + req = urllib2.Request(posturl, data=jdata, headers={'Content-Type': 'application/json', + 'Referer': 'http://%s' % (host)}) + r = urllib2.urlopen(req) +except NameError: + pass diff --git a/core/utils/live.py b/core/utils/live.py new file mode 100755 index 0000000..2e6efc1 --- /dev/null +++ b/core/utils/live.py @@ -0,0 +1,44 @@ +import logging +import urllib2 +from bs4 import BeautifulSoup + +def _parseItem(soup, param): + try: + match = soup.find(text=param) + if match is not None: + return match.findNext('td').contents[0] + except Exception, ex: + logging.getLogger('core').exception("Error parsing ice stream details: " + ex.message) + + return "" + + +def get_server_details(server, port, mount): + server = "http://%s:%s/status.xsl?mount=/%s" % (server, port, mount) + print "Getting info for %s" % server + try: + response = urllib2.urlopen(server) + html = response.read() + if html: + soup = BeautifulSoup(html) + info = { + 'stream_title': _parseItem(soup, "Stream Title:"), + 'stream_description': _parseItem(soup, "Stream Description:"), + 'content_type': _parseItem(soup, "Content Type:"), + 'mount_started': _parseItem(soup, "Mount started:"), + 'quality': _parseItem(soup, "Quality:"), + 'current_listeners': _parseItem(soup, "Current Listeners:"), + 'peak_listeners': _parseItem(soup, "Peak Listeners:"), + 'stream_genre': _parseItem(soup, "Stream Genre:"), + 'current_song': _parseItem(soup, "Current Song:") + } + return info + else: + print "Invalid content found" + return None + + except urllib2.URLError: + return "Unknown stream %s" % server + +def get_now_playing(server, port, mount): + return get_server_details(server, port, mount) diff --git a/core/utils/string.py b/core/utils/string.py new file mode 100755 index 0000000..bce02d6 --- /dev/null +++ b/core/utils/string.py @@ -0,0 +1,41 @@ +__author__ = 'fergalm' +import re + +def lreplace(string, pattern, sub): + """ + Replaces 'pattern' in 'string' with 'sub' if 'pattern' starts 'string'. + """ + return re.sub('^%s' % pattern, sub, string) + +def rreplace(string, pattern, sub): + """ + Replaces 'pattern' in 'string' with 'sub' if 'pattern' ends 'string'. + """ + return re.sub('%s$' % pattern, sub, string) + +def is_number(s): + try: + if len(s) > 0: + float(s) + return True + except ValueError: + pass + except IndexError: + pass + except Exception: + pass + + return False + +def trunc_lines(s, linecount): + ret = "" + cur = 0 + for line in s.splitlines(): + if cur < linecount: + ret += line + "\n" + cur += 1 + else: + break + + return ret + diff --git a/core/utils/url.py b/core/utils/url.py new file mode 100755 index 0000000..3583a9f --- /dev/null +++ b/core/utils/url.py @@ -0,0 +1,100 @@ +import urlparse +import re +from django.contrib.sites.models import Site +from django.template.defaultfilters import slugify + +__author__ = 'fergalm' + +def url_path_join(*parts): + """Join and normalize url path parts with a slash.""" + schemes, netlocs, paths, queries, fragments = zip(*(urlparse.urlsplit(part) for part in parts)) + # Use the first value for everything but path. Join the path on '/' + scheme = next((x for x in schemes if x), '') + netloc = next((x for x in netlocs if x), '') + path = '/'.join(x.strip('/') for x in paths if x) + query = next((x for x in queries if x), '') + fragment = next((x for x in fragments if x), '') + return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) + +def urlclean(url): + #remove double slashes + ret = urlparse.urljoin(url, urlparse.urlparse(url).path.replace('//', '/')) + return ret + + +def unique_slugify(instance, value, slug_field_name='slug', queryset=None, slug_separator='-'): + """ + Calculates and stores a unique slug of ``value`` for an instance. + + ``slug_field_name`` should be a string matching the name of the field to + store the slug in (and the field to check against for uniqueness). + + ``queryset`` usually doesn't need to be explicitly provided - it'll default + to using the ``.all()`` queryset from the model's default manager. + """ + slug_field = instance._meta.get_field(slug_field_name) + + slug = getattr(instance, slug_field.attname) + slug_len = slug_field.max_length + + # Sort out the initial slug, limiting its length if necessary. + slug = slugify(value) + if slug_len: + slug = slug[:slug_len] + slug = _slug_strip(slug, slug_separator) + original_slug = slug + + # Create the queryset if one wasn't explicitly provided and exclude the + # current instance from the queryset. + if queryset is None: + queryset = instance.__class__._default_manager.all() + if instance.pk: + queryset = queryset.exclude(pk=instance.pk) + + # Find a unique slug. If one matches, at '-2' to the end and try again + # (then '-3', etc). + next = 2 + while not slug or queryset.filter(**{slug_field_name: slug}): + slug = original_slug + end = '%s%s' % (slug_separator, next) + if slug_len and len(slug) + len(end) > slug_len: + slug = slug[:slug_len - len(end)] + slug = _slug_strip(slug, slug_separator) + slug = '%s%s' % (slug, end) + next += 1 + + return slug + + +def _slug_strip(value, separator='-'): + """ + Cleans up a slug by removing slug separator characters that occur at the + beginning or end of a slug. + + If an alternate separator is used, it will also replace any instances of + the default '-' separator with the new separator. + """ + separator = separator or '' + if separator == '-' or not separator: + re_sep = '-' + else: + re_sep = '(?:-|%s)' % re.escape(separator) + # Remove multiple instances and if an alternate separator is provided, + # replace the default '-' separator. + if separator != re_sep: + value = re.sub('%s+' % re_sep, separator, value) + # Remove separator from the beginning and end of the slug. + if separator: + if separator != '-': + re_sep = re.escape(separator) + value = re.sub(r'^%s+|%s+$' % (re_sep, re_sep), '', value) + return value + +def is_absolute(url): + return bool(urlparse.urlparse(url).scheme) + +def wrap_full(url): + if not is_absolute(url): + url = "http://%s%s" % (Site.objects.get_current().domain, url) + + return url \ No newline at end of file diff --git a/core/utils/waveform.py b/core/utils/waveform.py new file mode 100755 index 0000000..24a3b08 --- /dev/null +++ b/core/utils/waveform.py @@ -0,0 +1,34 @@ +import subprocess +import traceback +import uuid +import os +from dss import settings + + +def generate_waveform(input_file, output_file): + try: + print "Starting decode : %s\n\tIn: %s\n\tOut: %s" % \ + (settings.DSS_LAME_PATH, input_file, output_file) + convert_command = "%s %s -c 1 -t wav - | %s -w 1170 -h 140 -o %s /dev/stdin" % \ + (settings.DSS_LAME_PATH, input_file, settings.DSS_WAVE_PATH, output_file) + print "Convert command: %s" % convert_command + result = os.system(convert_command) + print result + + if os.path.exists(output_file): + #crop the image as it looks nice with zoom + from PIL import Image + import glob + + im = Image.open(output_file) + w, h = im.size + im.crop((0, 0, w, h / 2)).save(output_file) + + return output_file + else: + print "Unable to find working file, did LAME succeed?" + return "" + + except Exception, ex: + print "Error generating waveform %s" % (ex) + diff --git a/core/widgets/__init__.py b/core/widgets/__init__.py new file mode 100755 index 0000000..6d4caf8 --- /dev/null +++ b/core/widgets/__init__.py @@ -0,0 +1 @@ +__author__ = 'fergalm' diff --git a/core/widgets/upload.py b/core/widgets/upload.py new file mode 100755 index 0000000..b69b707 --- /dev/null +++ b/core/widgets/upload.py @@ -0,0 +1,4 @@ +from django.forms.widgets import ClearableFileInput + +class FileUploadWidget(ClearableFileInput): + pass \ No newline at end of file diff --git a/dss/__init__.py b/dss/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/dss/localsettings.initial.py b/dss/localsettings.initial.py new file mode 100755 index 0000000..0867673 --- /dev/null +++ b/dss/localsettings.initial.py @@ -0,0 +1,41 @@ +import os + +DEBUG = True +if os.name == 'posix': + DSS_TEMP_PATH = "/tmp/" + DSS_LAME_PATH = "sox" + DSS_WAVE_PATH = "wav2png" +else: + DSS_TEMP_PATH = "d:\\temp\\" + DSS_LAME_PATH = "D:\\Apps\\lame\\lame.exe" + DSS_WAVE_PATH = "d:\\Apps\\waveformgen.exe" + +DATABASE_NAME = 'deepsouthsounds' +DATABASE_USER = 'deepsouthsounds' +DATABASE_PASSWORD = '' +# DATABASE_HOST = '' + +PIPELINE_YUI_BINARY = "" +FACEBOOK_APP_SECRET = '' + +JS_SETTINGS = { + 'CHAT_HOST': "ext-test.deepsouthsounds.com:8081", + 'API_URL': "/api/v1/", + 'LIVE_STREAM_URL': "radio.deepsouthsounds.com", + 'LIVE_STREAM_PORT': "8000", + 'LIVE_STREAM_MOUNT': "mp3", + 'DEFAULT_AUDIO_VOLUME': "50", + 'SM_DEBUG_MODE': DEBUG, + 'LIVE_STREAM_INFO_URL': "radio.deepsouthsounds.com:8000/mp3" +} +""" +WAVEFORM_URL = 'http://waveforms.podnoms.com/' +IMAGE_URL = 'http://images.podnoms.com/' +STATIC_URL = 'http://static.podnoms.com/' +""" +IMAGE_URL = 'http://ext-test.deepsouthsounds.com:8000/media/' +GOOGLE_ANALYTICS_CODE = '' +SENDFILE_BACKEND = 'sendfile.backends.development' +#SENDFILE_BACKEND = 'sendfile.backends.xsendfile' +#SENDFILE_BACKEND = 'sendfile.backends.nginx' + diff --git a/dss/logsettings.py b/dss/logsettings.py new file mode 100755 index 0000000..525fc21 --- /dev/null +++ b/dss/logsettings.py @@ -0,0 +1,64 @@ +import os +import localsettings +if os.name == 'posix': + LOG_FILE = '/tmp/dss.log' +else: + LOG_FILE = 'c:\\temp\\dss.log' + +LOGGING = { + 'version': 1, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler', + 'include_html': True + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'simple' + }, + 'file': { + 'level': 'ERROR', + 'class': 'logging.FileHandler', + 'filename': LOG_FILE, + 'formatter': 'simple' + }, + }, + 'loggers': { + 'spa': { + 'handlers': ['file', 'console'], + 'level': 'DEBUG', + 'propagate': True, + }, + 'core': { + 'handlers': ['file', 'console', 'mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + 'django.request': { + 'handlers': ['file', 'console', 'mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + 'django.db.backends': { + 'level': 'DEBUG', + 'handers': ['console'], + }, + } +} + diff --git a/dss/paymentsettings.py b/dss/paymentsettings.py new file mode 100755 index 0000000..2056fd7 --- /dev/null +++ b/dss/paymentsettings.py @@ -0,0 +1,4 @@ +PAYPAL_RECEIVER_EMAIL = "admin@deepsouthsounds.com" +PAYPAL_RETURN_URL = "http://ext-test.deepsouthsounds.com:8080/a9cef/pp-lb/rt" +PAYPAL_NOTIFY_URL = "http://ext-test.deepsouthsounds.com:8080/a9cef/pp-lb/nt" +PAYPAL_CANCEL_URL = "http://ext-test.deepsouthsounds.com:8080/a9cef/pp-lb/ct" diff --git a/dss/pipelinesettings.py b/dss/pipelinesettings.py new file mode 100755 index 0000000..e107b52 --- /dev/null +++ b/dss/pipelinesettings.py @@ -0,0 +1,91 @@ +iPIPELINE_TEMPLATE_FUNC = "_.template" + +PIPELINE_COMPILERS = ( + 'pipeline.compilers.coffee.CoffeeScriptCompiler', +) + +PIPELINE_CSS = { + 'css': { + 'source_filenames': ( + 'css/dss.overrides.css', + + 'css/ace/dropzone.css', + 'css/ace/uncompressed/jquery.gritter.css', + 'css/ace/uncompressed/bootstrap.css', + 'css/ace/uncompressed/ace.css', + 'css/ace/uncompressed/ace-ie.css', + 'css/ace/uncompressed/ace-skins.css', + 'css/ace/uncompressed/font-awesome.css', + 'css/ace/uncompressed/fullcalendar.css', + 'css/ace/uncompressed/bootstrap-editable.css', + + 'css/jasny-bootstrap.css', + 'css/select2.css', + 'css/jquery.fileupload-ui.css', + 'css/peneloplay.css', + 'css/toastr.css', + 'css/dss.main.css', + ), + 'output_filename': 'css/site.css' + } +} + +PIPELINE_JS = { + 'templates': { + 'source_filenames': ( + 'js/dss/templates/*.jst', + ), + 'variant': 'datauri', + 'output_filename': 'js/t.js', + }, + + 'lib': { + 'source_filenames': ( + 'js/lib/jquery.js', + 'js/lib/jquery-ui.js', + + 'js/lib/moment.js', + 'js/lib/typeahead.js', + + 'js/lib/sm/soundmanager2.js', + + 'js/lib/underscore.js', + 'js/lib/underscore.templatehelpers.js', + 'js/lib/backbone.js', + 'js/lib/backbone.syphon.js', + 'js/lib/backbone.associations.js', + 'js/lib/backbone.marionette.js', + + 'js/lib/ace/uncompressed/bootstrap.js', + 'js/lib/ace/uncompressed/ace.js', + 'js/lib/ace/uncompressed/ace-elements.js', + 'js/lib/ace/uncompressed/select2.js', + 'js/lib/ace/uncompressed/fuelux/fuelux.wizard.js', + 'js/lib/ace/ace/elements.wizard.js', + 'js/lib/ace/uncompressed/bootstrap-wysiwyg.js', + 'js/lib/ace/uncompressed/jquery.gritter.js', + 'js/lib/ace/uncompressed/dropzone.js', + 'js/lib/ace/uncompressed/fullcalendar.js', + 'js/lib/ace/uncompressed/x-editable/bootstrap-editable.js', + 'js/lib/ace/uncompressed/x-editable/ace-editable.js', + + 'js/lib/ajaxfileupload.js', + 'js/lib/jasny.fileinput.js', + 'js/lib/jquery.fileupload.js', + 'js/lib/jquery.fileupload-process.js', + 'js/lib/jquery.fileupload-audio.js', + 'js/lib/jquery.fileupload-video.js', + 'js/lib/jquery.fileupload-validate.js', + 'js/lib/jquery.fileupload-ui.js', + 'js/lib/jquery.fileupload-image.js', + 'js/lib/jquery.iframe-transport.js', + 'js/lib/jquery.ui.widget.js', + 'js/lib/toastr.js', + + 'js/dss/*.coffee', + 'js/dss/**/*.coffee', + 'js/dss/apps/**/**/*.coffee', + ), + 'output_filename': 'js/a.js', + }, +} diff --git a/dss/psa.py b/dss/psa.py new file mode 100755 index 0000000..4dff0ff --- /dev/null +++ b/dss/psa.py @@ -0,0 +1,39 @@ +from django.conf import global_settings + +AUTHENTICATION_BACKENDS = global_settings.AUTHENTICATION_BACKENDS + ( + + 'social.backends.open_id.OpenIdAuth', + 'social.backends.google.GoogleOAuth2', + 'social.backends.google.GooglePlusAuth', + 'social.backends.twitter.TwitterOAuth', + 'social.backends.yahoo.YahooOpenId', + 'social.backends.facebook.FacebookOAuth2' + +) + +SOCIAL_AUTH_PIPELINE = ( + 'social.pipeline.social_auth.social_details', + 'social.pipeline.social_auth.social_uid', + 'social.pipeline.social_auth.auth_allowed', + 'social.pipeline.social_auth.social_user', + 'social.pipeline.user.get_username', + 'social.pipeline.social_auth.associate_by_email', + 'social.pipeline.user.create_user', + 'api.pipeline.save_profile', + 'social.pipeline.social_auth.associate_user', + 'social.pipeline.social_auth.load_extra_data', + 'social.pipeline.user.user_details' +) + +#SOCIAL_AUTH_GOOGLE_OAUTH2_USE_DEPRECATED_API = True +#SOCIAL_AUTH_GOOGLE_PLUS_USE_DEPRECATED_API = True +#SOCIAL_AUTH_GOOGLE_OAUTH2_IGNORE_DEFAULT_SCOPE = True +SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [ + 'https://mail.google.com/', + 'https://www.googleapis.com/auth/admin.directory.user.readonly', + 'https://www.googleapis.com/auth/admin.directory.orgunit.readonly', + 'https://www.googleapis.com/auth/admin.directory.group.readonly', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', +] +SOCIAL_AUTH_FACEBOOK_SCOPE = ['email'] diff --git a/dss/settings.py b/dss/settings.py new file mode 100755 index 0000000..869914e --- /dev/null +++ b/dss/settings.py @@ -0,0 +1,217 @@ +# e Django settings for dss project. +import os +import mimetypes +from django.core.urlresolvers import reverse_lazy +import djcelery +from django.conf import global_settings + +from dss import logsettings +from utils import here + +from localsettings import * +from storagesettings import * +from paymentsettings import * +from psa import * + +DEVELOPMENT = DEBUG + +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + ('Fergal Moran', 'fergal.moran@gmail.com'), +) + +MANAGERS = ADMINS +AUTH_PROFILE_MODULE = 'spa.UserProfile' + +ALLOWED_HOSTS = ['*'] +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'deepsouthsounds', + 'ADMINUSER': 'postgres', + 'USER': DATABASE_USER, + 'PASSWORD': DATABASE_PASSWORD, + 'HOST': DATABASE_HOST, + } +} +import sys + +if 'test' in sys.argv or 'test_coverage' in sys.argv: + print "Testing" + DATABASES['default']['ENGINE'] = 'django.db.backends.sqlite3' + +ROOT_URLCONF = 'dss.urls' +TIME_ZONE = 'Europe/Dublin' +LANGUAGE_CODE = 'en-ie' +SITE_ID = 1 +USE_I18N = False +USE_L10N = True +s = True + +SITE_ROOT = here('') + +ADMIN_MEDIA_PREFIX = STATIC_URL + "grappelli/" + +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'compressor.finders.CompressorFinder', +) + +STATICFILES_DIRS = ( + here('static'), +) + +TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + ( + 'django_facebook.context_processors.facebook', + 'django.core.context_processors.request', + 'django.core.context_processors.i18n', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.contrib.auth.context_processors.auth', + + + # TODO: remove.. + # `allauth` specific context processors + "allauth.account.context_processors.account", + "allauth.socialaccount.context_processors.socialaccount", +) + + +MIDDLEWARE_CLASSES = ( + 'django.middleware.gzip.GZipMiddleware', + 'django.middleware.common.CommonMiddleware', + 'user_sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'corsheaders.middleware.CorsMiddleware', + # 'htmlmin.middleware.HtmlMinifyMiddleware', + # 'htmlmin.middleware.MarkRequestMiddleware', + 'django_user_agents.middleware.UserAgentMiddleware', + # 'spa.middleware.uploadify.SWFUploadMiddleware', + #'spa.middleware.sqlprinter.SqlPrintingMiddleware' if DEBUG else None, + # 'debug_toolbar.middleware.DebugToolbarMiddleware', +) + +WSGI_APPLICATION = 'dss.wsgi.application' +TEMPLATE_DIRS = (here('templates'),) + +INSTALLED_APPS = ( + 'grappelli', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'user_sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admindocs', + #'django_facebook', + 'django_extensions', + 'django_gravatar', + 'djcelery', + 'corsheaders', + 'sorl.thumbnail', + 'spa', + 'spa.signals', + 'core', + #'schedule', + 'django_user_agents', + + 'social.apps.django_app.default', + + # TODO: remove + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.facebook', + 'allauth.socialaccount.providers.google', + 'allauth.socialaccount.providers.twitter', + 'south', + + 'dbbackup', + 'djrill', + 'rest_framework', + 'rest_framework.authtoken', + 'rest_framework_swagger', +) + +# where to redirect users to after logging in +LOGIN_REDIRECT_URL = reverse_lazy('home') +LOGOUT_URL = reverse_lazy('home') + +LOGGING = logsettings.LOGGING + +FACEBOOK_APP_ID = '154504534677009' + +djcelery.setup_loader() + +AVATAR_STORAGE_DIR = MEDIA_ROOT + '/avatars/' +ACCOUNT_LOGOUT_REDIRECT_URL = '/' + +INTERNAL_IPS = ('127.0.0.1', '86.44.166.21', '192.168.1.111') + +TASTYPIE_DATETIME_FORMATTING = 'rfc-2822' +TASTYPIE_ALLOW_MISSING_SLASH = True + +SENDFILE_ROOT = os.path.join(MEDIA_ROOT, 'mixes') +SENDFILE_URL = '/media/mixes' + +SESSION_ENGINE = 'user_sessions.backends.db' + +mimetypes.add_type("text/xml", ".plist", False) + +HTML_MINIFY = not DEBUG + +DEFAULT_FROM_EMAIL = 'DSS ChatBot ' +DEFAULT_HTTP_PROTOCOL = 'http' + +EMAIL_BACKEND = 'djrill.mail.backends.djrill.DjrillBackend' + +if DEBUG: + import mimetypes + + mimetypes.add_type("image/png", ".png", 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) + +REALTIME_HEADERS = { + 'content-type': 'application/json' +} +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +if 'test' in sys.argv: + try: + from test_settings import * + except ImportError: + pass + +REST_FRAMEWORK = { + # Use hyperlinked styles by default. + # Only used if the `serializer_class` attribute is not set on a view. + 'DEFAULT_MODEL_SERIALIZER_CLASS': + 'rest_framework.serializers.HyperlinkedModelSerializer', + 'DEFAULT_FILTER_BACKENDS': ( + 'rest_framework.filters.DjangoFilterBackend', + 'rest_framework.filters.OrderingFilter', + ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.TokenAuthentication', + ), + 'PAGINATE_BY': 12, # Default to 10 + 'PAGINATE_BY_PARAM': 'limit', # Allow client to override, using `?page_size=xxx`. + 'MAX_PAGINATE_BY': 100 # Maximum limit allowed when using `?page_size=xxx`.} +} + + +DEFAULT_USER_IMAGE = 'assets/images/default-avatar-32.png' +DEFAULT_USER_NAME = 'Anonymouse' +DEFAULT_USER_TITLE = 'Just another DSS lover' + + +SITE_NAME = 'Deep South Sounds' diff --git a/dss/test_settings.py b/dss/test_settings.py new file mode 100755 index 0000000..5fc6de2 --- /dev/null +++ b/dss/test_settings.py @@ -0,0 +1,10 @@ +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'dss_test.db', + 'USER': '', + 'PASSWORD': '', + 'HOST': '', + 'PORT': '', + } +} diff --git a/dss/urls.py b/dss/urls.py new file mode 100755 index 0000000..de41a44 --- /dev/null +++ b/dss/urls.py @@ -0,0 +1,34 @@ +from django.conf.urls import patterns, include, url +from django.contrib import admin +from django.views.generic import TemplateView, RedirectView + +from dss import settings + +admin.autodiscover() + +# Uncomment the next two lines to enable the admin: +# from django.contrib import admin +# admin.autodiscover() + +urlpatterns = patterns( + '', + url(r'^admin/', include(admin.site.urls)), + url(r'^api/docs/', include('rest_framework_swagger.urls')), + url(r'^api/v2/', include('api.urls')), + url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), + (r'^grappelli/', include('grappelli.urls')), +) +handler500 = 'spa.views.debug_500' + +if settings.DEBUG: + from django.views.static import serve + + _media_url = settings.MEDIA_URL + if _media_url.startswith('/'): + _media_url = _media_url[1:] + urlpatterns += patterns( + '', + (r'^%s(?P.*)$' % _media_url, + serve, + {'document_root': settings.MEDIA_ROOT})) + del (_media_url, serve) diff --git a/dss/warning_settings.py b/dss/warning_settings.py new file mode 100755 index 0000000..e69de29 diff --git a/dss/wsgi.py b/dss/wsgi.py new file mode 100755 index 0000000..8765705 --- /dev/null +++ b/dss/wsgi.py @@ -0,0 +1,28 @@ +""" +WSGI config for dss project. + +This module contains the WSGI application used by Django's development server +and any production WSGI deployments. It should expose a module-level variable +named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover +this application via the ``WSGI_APPLICATION`` setting. + +Usually you will have the standard Django WSGI application here, but it also +might make sense to replace the whole Django WSGI application with a custom one +that later delegates to the Django one. For example, you could introduce WSGI +middleware here, or combine a Django application with an application of another +framework. + +""" +import os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dss.settings") + +# This application object is used by any WSGI server configured to use this +# file. This includes Django's development server, if the WSGI_APPLICATION +# setting points here. +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() + +# Apply WSGI middleware here. +# from helloworld.wsgi import HelloWorldApplication +# application = HelloWorldApplication(application) diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..3de7571 --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dss.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/pip_upgrade.py b/pip_upgrade.py new file mode 100755 index 0000000..2dbb564 --- /dev/null +++ b/pip_upgrade.py @@ -0,0 +1,5 @@ +import pip +from subprocess import call + +for dist in pip.get_installed_distributions(): + call("pip install --upgrade " + dist.project_name, shell=True) diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..b08e5fd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,37 @@ +Django>=1.6,<1.7 +django-extensions +django-sendfile +Werkzeug +psycopg2 +gunicorn +dropbox +django-dirtyfields +django-storages +django-user-sessions +django-cors-headers +django-rest-swagger +django-filter +django-grappelli +django-model_utils +django-dbbackup +django-user-agents +south + +sorl-thumbnail + +git+git://github.com/disqus/django-bitfield.git#django-bitfield +git+git://github.com/Azure/azure-sdk-for-python.git#azure +git+git://github.com/tschellenbach/Django-facebook.git#django-facebook +git+git://github.com/llazzaro/django-scheduler.git#django-scheduler +git+git://github.com/omab/python-social-auth.git#egg=python-social-auth +apache-libcloud +mandrill +djrill + +djangorestframework +drf-nested-routers +django-celery +pillow +django-gravatar2 + +mutagen diff --git a/spa/__init__.py b/spa/__init__.py new file mode 100755 index 0000000..b31856c --- /dev/null +++ b/spa/__init__.py @@ -0,0 +1 @@ +import signals diff --git a/spa/admin.py b/spa/admin.py new file mode 100755 index 0000000..9e91359 --- /dev/null +++ b/spa/admin.py @@ -0,0 +1,27 @@ +from django.contrib import admin +from spa.models.genre import Genre +from spa.models.userprofile import UserProfile +from spa.models.chatmessage import ChatMessage +from spa.models.recurrence import Recurrence +from spa.models.release import Release +from spa.models.label import Label +from spa.models.mix import Mix +from spa.models.release import ReleaseAudio +from spa.models.venue import Venue + + +class DefaultAdmin(admin.ModelAdmin): + def save_model(self, request, obj, form, change): + obj.user = request.user.get_profile() + obj.save() + + +admin.site.register(Mix) +admin.site.register(Genre) +admin.site.register(Label) +admin.site.register(Release, DefaultAdmin) +admin.site.register(ReleaseAudio) +admin.site.register(Venue) +admin.site.register(UserProfile) +admin.site.register(Recurrence) +admin.site.register(ChatMessage) diff --git a/spa/api/__init__.py b/spa/api/__init__.py new file mode 100755 index 0000000..8133f5e --- /dev/null +++ b/spa/api/__init__.py @@ -0,0 +1 @@ +__author__ = 'fergalm' diff --git a/spa/api/v1/ActivityResource.py b/spa/api/v1/ActivityResource.py new file mode 100755 index 0000000..93e91a4 --- /dev/null +++ b/spa/api/v1/ActivityResource.py @@ -0,0 +1,48 @@ +import humanize +from tastypie.authentication import Authentication +from tastypie.authorization import Authorization +from tastypie.constants import ALL, ALL_WITH_RELATIONS +from spa.api.v1.BaseResource import BaseResource +from spa.models import UserProfile +from spa.models.activity import Activity + + +class ActivityResource(BaseResource): + class Meta: + queryset = Activity.objects.select_subclasses().order_by('-id') + resource_name = 'activity' + authorization = Authorization() + authentication = Authentication() + always_return_data = True + filtering = { + 'user': ALL_WITH_RELATIONS + } + + def dehydrate(self, bundle): + try: + if bundle.obj.user is not None: + user_name = bundle.obj.user.get_nice_name() + user_image = bundle.obj.user.get_small_profile_image() + user_profile = bundle.obj.user.get_profile_url() + else: + user_name = UserProfile.get_default_display_name() + user_image = UserProfile.get_default_avatar_image() + user_profile = "" + + bundle.data["verb"] = bundle.obj.get_verb_past(), + bundle.data["object"] = bundle.obj.get_object_singular(), + bundle.data["item_name"] = bundle.obj.get_object_name(), + bundle.data["item_url"] = bundle.obj.get_object_url(), + bundle.data["user_name"] = user_name, + bundle.data["user_profile"] = user_profile, + bundle.data["user_image"] = user_image + return bundle + + except AttributeError, ae: + self.logger.debug("AttributeError: Error dehydrating activity, %s" % ae.message) + except TypeError, te: + self.logger.debug("TypeError: Error dehydrating activity, %s" % te.message) + except Exception, ee: + self.logger.debug("Exception: Error dehydrating activity, %s" % ee.message) + return None + diff --git a/spa/api/v1/BaseResource.py b/spa/api/v1/BaseResource.py new file mode 100755 index 0000000..db1779a --- /dev/null +++ b/spa/api/v1/BaseResource.py @@ -0,0 +1,39 @@ +import logging +from tastypie.resources import ModelResource + + +class BaseResource(ModelResource): + logger = logging.getLogger(__name__) + pass + + def _remove_kwargs(self, *args, **kwargs): + for arg in args: + if arg in kwargs: + del kwargs['activity_sharing_networks_facebook'] + + return kwargs + + @staticmethod + def hydrate_bitfield(field_name, bundle, object_field, choices, remove_field=True): + if not hasattr(bundle, field_name + '____processed'): + mask = 0 + for choice in choices: + if choice[0] in bundle.data[field_name]: + if bundle.data[field_name][choice[0]]: + mask |= getattr(object_field, choice[0]) + + bundle.data[field_name] = mask + setattr(bundle, field_name + '____processed', True) + return bundle + + @staticmethod + def dehydrate_bitfield(field_name, bundle, object_field, choices, remove_field=True): + if remove_field: + del bundle.data[field_name] + + d = {} + for choice in choices: + d[choice[0]] = getattr(object_field, choice[0]).is_set + + bundle.data[field_name] = d + return bundle \ No newline at end of file diff --git a/spa/api/v1/ChatResource.py b/spa/api/v1/ChatResource.py new file mode 100755 index 0000000..97ad1b6 --- /dev/null +++ b/spa/api/v1/ChatResource.py @@ -0,0 +1,7 @@ +from spa.api.v1.BaseResource import BaseResource +from spa.models.chatmessage import ChatMessage + +class ChatResource(BaseResource): + class Meta: + queryset = ChatMessage.objects.all().order_by('-timestamp') + resource_name = 'chat' diff --git a/spa/api/v1/CommentResource.py b/spa/api/v1/CommentResource.py new file mode 100755 index 0000000..8c1870f --- /dev/null +++ b/spa/api/v1/CommentResource.py @@ -0,0 +1,66 @@ +from tastypie import fields +from tastypie.authentication import Authentication +from tastypie.authorization import Authorization +from tastypie.exceptions import ImmediateHttpResponse +from tastypie.http import HttpBadRequest, HttpMethodNotAllowed, HttpUnauthorized, HttpApplicationError, HttpNotImplemented +from spa.api.v1.BaseResource import BaseResource +from spa.models import Mix, UserProfile +from spa.models.activity import ActivityComment +from spa.models.comment import Comment + + +class CommentResource(BaseResource): + mix = fields.ToOneField('spa.api.v1.MixResource.MixResource', 'mix') + likes = fields.ToManyField('spa.api.v1.UserResource.UserResource', + 'likes', related_name='favourites', + full=False, null=True) + + class Meta: + queryset = Comment.objects.all().order_by('-date_created') + resource_name = 'comments' + filtering = { + "mix": ('exact',), + } + authorization = Authorization() + authentication = Authentication() + always_return_data = True + + def dehydrate(self, bundle): + if bundle.obj.user is not None: + bundle.data['avatar_image'] = bundle.obj.user.get_profile().get_avatar_image() + bundle.data['user_url'] = bundle.obj.user.get_profile().get_absolute_url() + bundle.data['user_name'] = bundle.obj.user.get_profile().get_nice_name() + else: + bundle.data['avatar_image'] = UserProfile.get_default_avatar_image() + bundle.data['user_url'] = "/" + bundle.data['user_name'] = "Anonymouse" + + if bundle.request.user.is_authenticated(): + bundle.data['can_edit'] = bundle.request.user.is_staff or bundle.obj.user_id == bundle.request.user.id + else: + bundle.data['can_edit'] = False + + return bundle + + def obj_create(self, bundle, **kwargs): + bundle.data['user'] = bundle.request.user + try: + if 'mix_id' in bundle.data: + mix = Mix.objects.get_by_id_or_slug(bundle.data['mix_id']) + if mix is not None: + if bundle.request.user.is_authenticated(): + ActivityComment(user=bundle.request.user.get_profile(), mix=mix).save() + return super(CommentResource, self).obj_create(bundle, user=bundle.request.user or None, mix=mix) + else: + ActivityComment(mix=mix).save() + return super(CommentResource, self).obj_create(bundle, mix=mix) + else: + return HttpBadRequest("Unable to find mix for supplied mix_id (candidate fields are slug & id).") + + return HttpBadRequest("Missing mix_id field.") + except ImmediateHttpResponse, e: + self.logger.error("Error creating comment (%s)" % e.message) + return HttpUnauthorized("Git tae fuck!") + except Exception, e: + self.logger.error("Error creating comment (%s)" % e.message) + return HttpApplicationError("Unable to hydrate comment from supplied data.") diff --git a/spa/api/v1/DebugResource.py b/spa/api/v1/DebugResource.py new file mode 100755 index 0000000..6d3a740 --- /dev/null +++ b/spa/api/v1/DebugResource.py @@ -0,0 +1,18 @@ +from django.db.models import Count +from tastypie import fields +from tastypie.resources import ModelResource +from spa.models import UserProfile + + +class DebugResource(ModelResource): + total_tickets = fields.IntegerField(readonly=True) + + class Meta: + queryset = UserProfile.objects.all() + ordering = ['total_tickets'] + + def get_object_list(self, request): + return super(DebugResource, self).get_object_list(request).annotate(total_tickets=Count('mixes', distinct=True)) + + def dehydrate_total_tickets(self, bundle): + return bundle.obj.total_tickets \ No newline at end of file diff --git a/spa/api/v1/EventResource.py b/spa/api/v1/EventResource.py new file mode 100755 index 0000000..ad16d9e --- /dev/null +++ b/spa/api/v1/EventResource.py @@ -0,0 +1,41 @@ +from django.core.exceptions import ObjectDoesNotExist +import humanize +from tastypie.authorization import Authorization +from spa.api.v1.BaseResource import BaseResource +from spa.models.recurrence import Recurrence +""" +from spa.views.venue import Venue +from spa.views.event import Event +class EventResource(BackboneCompatibleResource): + class Meta: + queryset = Event.objects.all() + authorization = Authorization() + + def obj_create(self, bundle, request=None, **kwargs): + bundle.data['user'] = {'pk': request.user.pk} + return super(EventResource, self).obj_create(bundle, request, user=request.user.get_profile()) + + def dehydrate(self, bundle): + bundle.data['item_url'] = 'event/%s' % bundle.obj.id + bundle.data['event_venue'] = bundle.obj.event_venue.venue_name + return bundle + + def dehydrate_event_date(self, bundle): + return humanize.naturalday(bundle.obj.event_date) + + def hydrate(self, bundle): + if 'event_venue' in bundle.data: + try: + venue = Venue.objects.get(venue_name__exact=bundle.data['event_venue']) + except ObjectDoesNotExist: + venue = Venue(venue_name=bundle.data['event_venue'], user=bundle.request.user) + venue.save() + + bundle.obj.event_venue = venue + + recurrence = Recurrence.objects.get(pk=bundle.data['event_recurrence_id']) + if recurrence != None: + bundle.obj.event_recurrence = recurrence + + return bundle +""" diff --git a/spa/api/v1/GenreResource.py b/spa/api/v1/GenreResource.py new file mode 100755 index 0000000..ab7a76e --- /dev/null +++ b/spa/api/v1/GenreResource.py @@ -0,0 +1,33 @@ +from tastypie.authentication import Authentication +from tastypie.authorization import Authorization + +from spa.api.v1.BaseResource import BaseResource +from spa.models import Genre + + +class GenreResource(BaseResource): + class Meta: + queryset = Genre.objects.all().order_by('description') + resource_name = 'genres' + + excludes = ['resource_uri'] + filtering = { + 'slug': ('exact',), + } + authorization = Authorization() + authentication = Authentication() + always_return_data = True + + def obj_create(self, bundle, **kwargs): + """ + Check to see if there is an existing genre for what was entered + """ + genre = Genre.objects.get(description=bundle.obj['description']) + if genre is not None: + bundle.obj = genre + return bundle + else: + ret = super(GenreResource, self).obj_create(bundle, bundle.request) + + return ret + diff --git a/spa/api/v1/MixResource.py b/spa/api/v1/MixResource.py new file mode 100755 index 0000000..68bc243 --- /dev/null +++ b/spa/api/v1/MixResource.py @@ -0,0 +1,244 @@ +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.http import Http404 +from django.template.loader import render_to_string +from tastypie import fields +from tastypie.authorization import Authorization +from tastypie.constants import ALL_WITH_RELATIONS +from tastypie.exceptions import ImmediateHttpResponse +from tastypie.fields import ToOneField +from tastypie.http import HttpGone, HttpUnauthorized +from tastypie.utils import trailing_slash +from dss import settings + +from spa.api.v1.BaseResource import BaseResource +from spa.api.v1.CommentResource import CommentResource +from spa.api.v1.ActivityResource import ActivityResource +from spa.models.mix import Mix +from spa.models.show import Show +from spa.models.userprofile import UserProfile + + +class MixResource(BaseResource): + 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) + likes = fields.ToManyField('spa.api.v1.UserResource.UserResource', + 'likes', related_name='likes', + full=False, null=True) + genres = fields.ToManyField('spa.api.v1.GenreResource.GenreResource', + 'genres', related_name='genres', + full=True, null=True) + + class Meta: + queryset = Mix.objects.filter(is_active=True) + user = ToOneField('UserResource', 'user') + always_return_data = True + detail_uri_name = 'slug' + excludes = ['is_active', 'waveform-generated'] + post_excludes = ['comments'] + filtering = {'comments': ALL_WITH_RELATIONS, + 'genres': ALL_WITH_RELATIONS, + 'favourites': ALL_WITH_RELATIONS, + 'likes': ALL_WITH_RELATIONS, + 'title': ALL_WITH_RELATIONS, + 'slug': ALL_WITH_RELATIONS, } + authorization = Authorization() + + def prepend_urls(self): + return [ + url(r"^(?P%s)/search%s$" % + (self._meta.resource_name, trailing_slash()), + self.wrap_view('get_search'), + name="api_get_search"), + url(r"^(?P%s)/(?P[\d]+)%s$" % + (self._meta.resource_name, trailing_slash()), + self.wrap_view('dispatch_detail'), + name="api_dispatch_detail"), + url(r"^(?P%s)/random%s$" % + (self._meta.resource_name, trailing_slash()), + self.wrap_view('dispatch_random'), name="api_dispatch_random"), + url(r"^(?P%s)/(?P[\w\d-]+)%s$" % + (self._meta.resource_name, trailing_slash()), + self.wrap_view('dispatch_detail'), name="api_dispatch_detail"), + url(r"^(?P%s)/(?P\w[\w/-]*)/comments%s$" % + (self._meta.resource_name, trailing_slash()), + self.wrap_view('get_comments'), name="api_get_comments"), + url(r"^(?P%s)/(?P\w[\w/-]*)/activity%s$" % ( + self._meta.resource_name, trailing_slash()), + self.wrap_view('get_activity'), name="api_get_activity"), + ] + + def dispatch_random(self, request, **kwargs): + kwargs['pk'] = \ + self._meta.queryset.values_list('pk', flat=True).order_by('?')[0] + return self.get_detail(request, **kwargs) + + def get_comments(self, request, **kwargs): + try: + basic_bundle = self.build_bundle(request=request) + obj = self.cached_obj_get(bundle=basic_bundle, + **self.remove_api_resource_names(kwargs)) + except ObjectDoesNotExist: + return HttpGone() + + child_resource = CommentResource() + return child_resource.get_list(request, mix=obj) + + def get_activity(self, request, **kwargs): + try: + basic_bundle = self.build_bundle(request=request) + obj = self.cached_obj_get(bundle=basic_bundle, + **self.remove_api_resource_names(kwargs)) + except ObjectDoesNotExist: + return HttpGone() + + child_resource = ActivityResource() + return child_resource.get_list(request, mix=obj) + + def obj_create(self, bundle, **kwargs): + if 'is_featured' not in bundle.data: + bundle.data['is_featured'] = False + + if 'download_allowed' not in bundle.data: + bundle.data['download_allowed'] = False + + #AAAAAH - STOP BEING LAZY AND REMOVE THIS + + if settings.DEBUG and bundle.request.user.is_anonymous(): + bundle.data['user'] = UserProfile.objects.get(pk=2) + else: + bundle.data['user'] = bundle.request.user.get_profile() + + ret = super(MixResource, self).obj_create( + bundle, + user=bundle.data['user'], + uid=bundle.data['upload-hash'], + extension=bundle.data['upload-extension']) + + return ret + + def obj_update(self, bundle, **kwargs): + #don't sync the mix_image, this has to be handled separately + bundle.data.pop('mix_image', None) + + 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']) + + return ret + + def apply_sorting(self, obj_list, options=None): + orderby = options.get('order_by', '') + if orderby == 'latest': + obj_list = obj_list.order_by('-id') + elif orderby == 'toprated': + obj_list = obj_list.annotate( + karma=Count('activity_likes')).order_by('-karma') + elif orderby == 'mostplayed': + 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('activity_likes')).order_by('-karma') + + return obj_list + + def apply_filters(self, request, applicable_filters): + semi_filtered = super(MixResource, self) \ + .apply_filters(request, applicable_filters) \ + .filter(waveform_generated=True) + + f_user = request.GET.get('user', None) + + if request.GET.get('stream'): + if request.user.is_anonymous(): + raise ImmediateHttpResponse( + HttpUnauthorized("Only logged in users have a stream") + ) + semi_filtered = semi_filtered.filter( + user__in=request.user.get_profile().following.all()) + + if request.GET.get('for_show'): + semi_filtered = semi_filtered.filter(show__isnull=True) + + if f_user is not None: + semi_filtered = semi_filtered.filter(user__slug=f_user) + elif len(applicable_filters) == 0: + semi_filtered = semi_filtered.filter(is_featured=True) + + return semi_filtered + + def dehydrate_mix_image(self, bundle): + return bundle.obj.get_image_url(size="160x110") + + def dehydrate(self, bundle): + bundle.data['waveform_url'] = bundle.obj.get_waveform_url() + bundle.data['audio_src'] = bundle.obj.get_stream_url() + bundle.data['user_name'] = bundle.obj.user.get_nice_name() + bundle.data['user_profile_url'] = bundle.obj.user.get_absolute_url() + bundle.data['user_profile_image'] = \ + bundle.obj.user.get_small_profile_image() + bundle.data['item_url'] = '/mix/%s' % bundle.obj.slug + bundle.data['download_allowed'] = bundle.obj.download_allowed + 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['liked'] = bundle.obj.is_liked(bundle.request.user) + + if bundle.request.user.is_authenticated(): + bundle.data['can_edit'] = bundle.request.user.is_staff or \ + bundle.obj.user_id == bundle.request.user.get_profile().id + else: + bundle.data['can_edit'] = False + + if bundle.request.user.is_authenticated(): + bundle.data['favourited'] = bundle.obj.favourites.filter( + user=bundle.request.user).count() != 0 + else: + bundle.data['favourited'] = False + + return bundle + + def get_search(self, request, **kwargs): + self.method_check(request, allowed=['get']) + self.is_authenticated(request) + self.throttle_check(request) + + # Do the query. + sqs = Mix.objects.filter(title__icontains=request.GET.get('q', '')) + paginator = Paginator(sqs, 20) + + try: + page = paginator.page(int(request.GET.get('page', 1))) + except InvalidPage: + raise Http404("Sorry, no results on that page.") + + objects = [] + + for result in page.object_list: + bundle = self.build_bundle(obj=result, request=request) + bundle = self.full_dehydrate(bundle) + objects.append(bundle) + + object_list = {'objects': objects, } + + self.log_throttled_access(request) + return self.create_response(request, object_list) diff --git a/spa/api/v1/NotificationResource.py b/spa/api/v1/NotificationResource.py new file mode 100755 index 0000000..5cab992 --- /dev/null +++ b/spa/api/v1/NotificationResource.py @@ -0,0 +1,33 @@ +from tastypie.authentication import SessionAuthentication +from tastypie.authorization import DjangoAuthorization +from spa.api.v1.BaseResource import BaseResource +from spa.models.notification import Notification +from spa.models.userprofile import UserProfile + + +class NotificationResource(BaseResource): + class Meta: + queryset = Notification.objects.order_by('-id') + resource_name = 'notification' + authentication = SessionAuthentication() + authorization = DjangoAuthorization() + always_return_data = True + excludes = ['accepted_date'] + + def authorized_read_list(self, object_list, bundle): + return object_list.filter(to_user=bundle.request.user) + + def dehydrate(self, bundle): + if bundle.obj.from_user is not None: + bundle.data['user_url'] = bundle.obj.from_user.get_absolute_url() + bundle.data['user_image'] = bundle.obj.from_user.get_sized_avatar_image(42, 42) + bundle.data['user_name'] = bundle.obj.from_user.get_nice_name() + else: + bundle.data['user_url'] = "#" + bundle.data['user_image'] = UserProfile.get_default_avatar_image() + bundle.data['user_name'] = UserProfile.get_default_display_name() + return bundle + + def alter_list_data_to_serialize(self, request, data): + data['meta']['is_new'] = Notification.objects.filter(to_user=request.user, accepted_date__isnull=True).count() + return data \ No newline at end of file diff --git a/spa/api/v1/PlaylistResource.py b/spa/api/v1/PlaylistResource.py new file mode 100755 index 0000000..2cbdf80 --- /dev/null +++ b/spa/api/v1/PlaylistResource.py @@ -0,0 +1,82 @@ +from django.db.models import Count +from django.conf.urls import url +from tastypie.authentication import SessionAuthentication, Authentication +from tastypie.authorization import DjangoAuthorization, Authorization +from tastypie import fields +from tastypie.exceptions import ImmediateHttpResponse +from tastypie.http import HttpUnauthorized +from tastypie.utils import trailing_slash +from spa.api.v1.BaseResource import BaseResource +from spa.models import Playlist, Mix, UserProfile + + +class PlaylistResource(BaseResource): + user = fields.ToOneField('spa.api.v1.UserResource.UserResource', 'user') + mixes = fields.ManyToManyField('spa.api.v1.MixResource.MixResource', 'mixes', full=True, null=True) + + class Meta: + queryset = Playlist.objects.all().annotate(mix_count=Count('mixes')).order_by('-mix_count') + always_return_data = True + + excludes = ['public'] + + authentication = Authentication() + authorization = Authorization() + + def authorized_read_list(self, object_list, bundle): + if bundle.request.user.is_authenticated(): + return object_list.filter(user=bundle.request.user) + + raise ImmediateHttpResponse( + HttpUnauthorized("Git tae fuck") + ) + + def prepend_urls(self): + return [ + url(r"^(?P%s)/(?P[\d]+)%s$" % + (self._meta.resource_name, trailing_slash()), + self.wrap_view('dispatch_detail'), + name="api_dispatch_detail"), + url(r"^(?P%s)/(?P[\w\d-]+)%s$" % + (self._meta.resource_name, trailing_slash()), + self.wrap_view('dispatch_detail'), name="api_dispatch_detail"), + ] + + def hydrate(self, bundle): + bundle.obj.user = bundle.request.user.get_profile() + return bundle + + def dehydrate(self, bundle): + bundle.data['playlist_image'] = bundle.obj.get_image_url() + bundle.data['item_url'] = '/playlist/%s' % bundle.obj.slug + return bundle + + def obj_update(self, bundle, skip_errors=False, **kwargs): + if 'playlist_image' in kwargs: del kwargs['playlist_image'] + if 'item_url' in kwargs: del kwargs['item_url'] + mixes = bundle.data['mixes'] + bundle.data.pop('mixes') + result = super(PlaylistResource, self).obj_update(bundle, **kwargs) + if mixes: + for mix_item in mixes: + result.obj.mixes.add(Mix.objects.get(pk=mix_item['id'])) + + result.obj.save() + + return result + + def obj_create(self, bundle, **kwargs): + try: + mixes = bundle.data['mixes'] + bundle.data.pop('mixes') + result = super(PlaylistResource, self).obj_create(bundle, **kwargs) + + if mixes: + for mix_item in mixes: + result.obj.mixes.add(Mix.objects.get(pk=mix_item['id'])) + + result.obj.save() + + return result + except Exception, ex: + print ex \ No newline at end of file diff --git a/spa/api/v1/ReleaseAudioResource.py b/spa/api/v1/ReleaseAudioResource.py new file mode 100755 index 0000000..55f25ab --- /dev/null +++ b/spa/api/v1/ReleaseAudioResource.py @@ -0,0 +1,17 @@ +from tastypie import fields +from spa.api.v1.BaseResource import BaseResource +from spa.models.release import ReleaseAudio + +class ReleaseAudioResource(BaseResource): + release = fields.ToOneField('spa.api.v1.ReleaseResource.ReleaseResource', 'release') + + class Meta: + queryset = ReleaseAudio.objects.all() + resource_name = 'audio' + filtering = { + "release": ('exact',), + } + + def dehydrate(self, bundle): + bundle.data['waveform_url'] = bundle.obj.get_waveform_url() + return bundle \ No newline at end of file diff --git a/spa/api/v1/ReleaseResource.py b/spa/api/v1/ReleaseResource.py new file mode 100755 index 0000000..180f878 --- /dev/null +++ b/spa/api/v1/ReleaseResource.py @@ -0,0 +1,41 @@ +import datetime +from tastypie import fields +from tastypie.authorization import Authorization +from tastypie.constants import ALL_WITH_RELATIONS +from spa.api.v1.BaseResource import BaseResource +from spa.models import Label +from spa.models.release import Release +from django.core.exceptions import ObjectDoesNotExist +class ReleaseResource(BaseResource): + release_audio = fields.ToManyField('spa.api.v1.ReleaseAudioResource.ReleaseAudioResource', 'release_audio', 'release', null=True, blank=True) + class Meta: + queryset = Release.objects.all() + filtering = { + 'release_audio' : ALL_WITH_RELATIONS + } + authorization = Authorization() + + def obj_create(self, bundle, request=None, **kwargs): + bundle.data['user'] = {'pk': request.user.pk} + return super(ReleaseResource, self).obj_create(bundle, request, user=request.user.get_profile()) + + def hydrate(self, bundle): + if 'release_label' in bundle.data: + try: + label = Label.objects.get(name__exact=bundle.data['release_label']) + except ObjectDoesNotExist: + label = Label(name=bundle.data['release_label']) + label.save() + + bundle.obj.release_label = label + return bundle + + def dehydrate(self, bundle): + bundle.data['release_label'] = bundle.obj.release_label.name + bundle.data['item_url'] = 'release/%s' % bundle.obj.id + bundle.data['mode'] = 'release' + return bundle + + def dehydrate_release_image(self, bundle): + return bundle.obj.get_image_url() + diff --git a/spa/api/v1/ShowResource.py b/spa/api/v1/ShowResource.py new file mode 100755 index 0000000..f84a40f --- /dev/null +++ b/spa/api/v1/ShowResource.py @@ -0,0 +1,34 @@ +from tastypie import fields +from tastypie.authorization import Authorization +from tastypie.exceptions import ImmediateHttpResponse +from tastypie.http import HttpBadRequest + +from spa.api.v1.BaseResource import BaseResource + +from spa.models import Show +from spa.models.show import ShowOverlapException + +DATE_FORMAT = '%d/%m/%Y %H:%M:%S' + + +class ShowResource(BaseResource): + mix = fields.ToOneField('spa.api.v1.MixResource.MixResource', + 'mix', null=False, full=False) + + class Meta: + queryset = Show.objects.all() + authorization = Authorization() + resource_name = 'shows' + + def obj_create(self, bundle, **kwargs): + try: + return super(ShowResource, self).obj_create(bundle, **kwargs) + except ShowOverlapException: + raise ImmediateHttpResponse( + HttpBadRequest("This event overlaps with an existing event") + ) + except Exception, ex: + raise ImmediateHttpResponse( + HttpBadRequest(ex.message) + ) + diff --git a/spa/api/v1/UserResource.py b/spa/api/v1/UserResource.py new file mode 100755 index 0000000..16757f2 --- /dev/null +++ b/spa/api/v1/UserResource.py @@ -0,0 +1,172 @@ +from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned +from django.db.models import Count, Q, F +from tastypie import fields +from tastypie.authentication import Authentication +from tastypie.authorization import Authorization +from django.conf.urls import url +from tastypie.constants import ALL, ALL_WITH_RELATIONS +from tastypie.http import HttpGone, HttpMultipleChoices +from tastypie.utils import trailing_slash +from tastypie_msgpack import Serializer + +from dss import settings +from spa.api.v1.BaseResource import BaseResource +from spa.api.v1.PlaylistResource import PlaylistResource +from spa.models.basemodel import BaseModel +from spa.models.userprofile import UserProfile +from spa.models.mix import Mix +from core.tasks import update_geo_info_task + + +class UserResource(BaseResource): + following = fields.ToManyField(to='self', attribute='following', related_name='following', null=True) + followers = fields.ToManyField(to='self', attribute='followers', related_name='followers', null=True) + + favourites = fields.ToManyField('spa.api.v1.MixResource.MixResource', 'favourites', null=True) + playlists = fields.ToManyField('spa.api.v1.PlaylistResource.PlaylistResource', 'playlists', + related_name='user', null=True, full=True) + + class Meta: + queryset = UserProfile.objects.all().annotate(mix_count=Count('mixes')).order_by('-mix_count') + serializer = Serializer() + resource_name = 'user' + + if not settings.DEBUG: + excludes = ['is_active', 'is_staff', 'is_superuser', 'password'] + ordering = ['mix_count'] + filtering = { + 'slug': ALL, + 'display_name': ALL, + 'following': ALL_WITH_RELATIONS, + 'followers': ALL_WITH_RELATIONS, + 'favourites': ALL_WITH_RELATIONS, + 'playlists': ALL_WITH_RELATIONS, + } + authorization = Authorization() + authentication = Authentication() + + def prepend_urls(self): + return [ + url(r"^(?P%s)/(?P\d+)%s$" % + (self._meta.resource_name, trailing_slash()), + self.wrap_view('dispatch_detail'), name="api_dispatch_detail"), + url(r"^(?P%s)/(?P[\w\d_.-]+)%s$" % + (self._meta.resource_name, trailing_slash()), + self.wrap_view('dispatch_detail'), name="api_dispatch_detail"), + ] + + 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: + semi_filtered = semi_filtered.filter( + Q(user__first_name__icontains=q) | + Q(user__last_name__icontains=q) | + Q(display_name__icontains=q) + ) + + return semi_filtered + + def obj_create(self, bundle, **kwargs): + return super(UserResource, self).obj_create(bundle, **kwargs) + + def obj_update(self, bundle, skip_errors=False, **kwargs): + return super(UserResource, self).obj_update(bundle, skip_errors, **kwargs) + + def _create_playlist(self, request): + pass + + def get_playlists(self, request, **kwargs): + if request.method == 'POST': + return self._create_playlist(request) + try: + basic_bundle = self.build_bundle(request=request) + obj = self.cached_obj_get(bundle=basic_bundle, + **self.remove_api_resource_names(kwargs)) + except ObjectDoesNotExist: + return HttpGone() + + child_resource = PlaylistResource() + return child_resource.get_list(request, mix=obj) + + def dehydrate_description(self, bundle): + return bundle.obj.get_profile_description() + + def dehydrate(self, bundle): + del bundle.data['activity_sharing_networks'] + bundle.data['display_name'] = bundle.obj.get_nice_name() + bundle.data['avatar_image'] = bundle.obj.get_avatar_image() + + bundle = BaseResource.dehydrate_bitfield( + bundle=bundle, + field_name='email_notifications', + object_field=bundle.obj.email_notifications, + choices=UserProfile.NOTIFICATION_CHOICES, + ) + + bundle = BaseResource.dehydrate_bitfield( + bundle=bundle, + field_name='activity_sharing_facebook', + object_field=bundle.obj.activity_sharing_facebook, + choices=UserProfile.NOTIFICATION_CHOICES, + ) + + bundle = BaseResource.dehydrate_bitfield( + bundle=bundle, + field_name='activity_sharing_twitter', + object_field=bundle.obj.activity_sharing_twitter, + choices=UserProfile.NOTIFICATION_CHOICES, + ) + + if bundle.obj.user.id == bundle.request.user.id: + bundle.data['email'] = bundle.obj.email + bundle.data['first_name'] = bundle.obj.first_name + bundle.data['last_name'] = bundle.obj.last_name + + bundle.data['like_count'] = Mix.objects.filter(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() + bundle.data['is_following'] = bundle.obj.is_follower(bundle.request.user) + bundle.data['url'] = bundle.obj.get_profile_url() + bundle.data['date_joined'] = bundle.obj.user.date_joined + bundle.data['last_login'] = bundle.obj.user.last_login + bundle.data['mix_count'] = bundle.obj.mix_count + bundle.data['thumbnail'] = bundle.obj.get_small_profile_image() + + return bundle + + def hydrate(self, bundle): + bundle = BaseResource.hydrate_bitfield( + bundle=bundle, + field_name='email_notifications', + object_field=UserProfile.email_notifications, + choices=UserProfile.NOTIFICATION_CHOICES, + ) + bundle = BaseResource.hydrate_bitfield( + bundle=bundle, + field_name='activity_sharing_facebook', + object_field=UserProfile.activity_sharing_facebook, + choices=UserProfile.NOTIFICATION_CHOICES, + ) + + bundle = BaseResource.hydrate_bitfield( + bundle=bundle, + field_name='activity_sharing_twitter', + object_field=UserProfile.activity_sharing_twitter, + choices=UserProfile.NOTIFICATION_CHOICES, + ) + + return bundle + + def get_followers(self, request, **kwargs): + try: + basic_bundle = self.build_bundle(request=request) + obj = self.cached_obj_get(bundle=basic_bundle, **self.remove_api_resource_names(kwargs)) + except ObjectDoesNotExist: + return HttpGone() + except MultipleObjectsReturned: + return HttpMultipleChoices("More than one resource is found at this URI.") + + child_resource = UserResource() + return child_resource.get_list(request, followers__in=obj) diff --git a/spa/api/v1/__init__.py b/spa/api/v1/__init__.py new file mode 100755 index 0000000..8133f5e --- /dev/null +++ b/spa/api/v1/__init__.py @@ -0,0 +1 @@ +__author__ = 'fergalm' diff --git a/spa/api/v1/auth.py b/spa/api/v1/auth.py new file mode 100755 index 0000000..d1bf67c --- /dev/null +++ b/spa/api/v1/auth.py @@ -0,0 +1,16 @@ +from tastypie.authorization import Authorization + + +class UserOwnsRowAuthorisation(Authorization): + """ + If the user is already authenticated by a django session it will + allow the request (useful for ajax calls) . + In addition, we will check that the user owns the row being updated + or is an admin + """ + + def apply_limits(self, request, object_list): + if request and hasattr(request, 'user'): + return object_list.filter(author__username=request.user.username) + + return object_list.none() \ No newline at end of file diff --git a/spa/embedding/__init__.py b/spa/embedding/__init__.py new file mode 100755 index 0000000..8133f5e --- /dev/null +++ b/spa/embedding/__init__.py @@ -0,0 +1 @@ +__author__ = 'fergalm' diff --git a/spa/embedding/urls.py b/spa/embedding/urls.py new file mode 100755 index 0000000..10a3e67 --- /dev/null +++ b/spa/embedding/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import patterns, url + +urlpatterns = patterns( + '', + url(r'^mix/(?P\d+)/$', 'spa.embedding.views.mix', name='embed_mix'), + url(r'^mix/(?P[\w\d_.-]+)/$', 'spa.embedding.views.mix', name='embed_mix_slug'), +) \ No newline at end of file diff --git a/spa/embedding/views.py b/spa/embedding/views.py new file mode 100755 index 0000000..fd381aa --- /dev/null +++ b/spa/embedding/views.py @@ -0,0 +1,34 @@ +from django.contrib.sites.models import Site +from django.http import Http404 +from django.shortcuts import render_to_response +from django.template import RequestContext +from spa.models import Mix + + +def mix(request, **args): + try: + if 'mix_id' in args: + mix = Mix.objects.get(pk=args['mix_id']) + else: + mix = Mix.objects.get(slug=args['slug']) + except Mix.DoesNotExist: + raise Http404 + + image = mix.get_image_url('1500x1500') + audio_url = mix.get_stream_url() + mix_url = mix.get_absolute_url() + payload = { + "description": mix.description.replace('
', '\n'), + "title": mix.title, + "image_url": image, + "audio_url": audio_url, + "mix_url": 'http://%s%s' % (Site.objects.get_current().domain, mix_url) + } + response = render_to_response( + 'inc/embed/mix.html', + payload, + context_instance=RequestContext(request) + ) + response['X-XSS-Protection'] = 0 + response['X-Frame-Options'] = 'IGNORE' + return response diff --git a/spa/management/__init__.py b/spa/management/__init__.py new file mode 100755 index 0000000..8133f5e --- /dev/null +++ b/spa/management/__init__.py @@ -0,0 +1 @@ +__author__ = 'fergalm' diff --git a/spa/management/commands/__init__.py b/spa/management/commands/__init__.py new file mode 100755 index 0000000..8133f5e --- /dev/null +++ b/spa/management/commands/__init__.py @@ -0,0 +1 @@ +__author__ = 'fergalm' diff --git a/spa/management/commands/__template_Debug.py b/spa/management/commands/__template_Debug.py new file mode 100755 index 0000000..d153efb --- /dev/null +++ b/spa/management/commands/__template_Debug.py @@ -0,0 +1,9 @@ +from django.core.management.base import NoArgsCommand + + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + pass + except Exception, ex: + print "Debug exception: %s" % ex.message \ No newline at end of file diff --git a/spa/management/commands/__timeside_waveforms.py b/spa/management/commands/__timeside_waveforms.py new file mode 100755 index 0000000..aa6caf1 --- /dev/null +++ b/spa/management/commands/__timeside_waveforms.py @@ -0,0 +1,15 @@ +from django.core.management.base import NoArgsCommand +import timeside + + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + audio_file = '/home/fergalm/Dropbox/Private/deepsouthsounds.com/working/sample.mp3' + decoder = timeside.decoder.FileDecoder(audio_file) + grapher = timeside.grapher.Spectrogram(width=1920, height=1080) + (decoder | grapher).run() + grapher.render('d:\spectrogram.png') + + except Exception, ex: + print "Debug exception: %s" % ex.message \ No newline at end of file diff --git a/spa/management/commands/archive_mixes.py b/spa/management/commands/archive_mixes.py new file mode 100755 index 0000000..1b803ed --- /dev/null +++ b/spa/management/commands/archive_mixes.py @@ -0,0 +1,49 @@ +from django.core.management.base import NoArgsCommand +from libcloud.storage.types import Provider +from libcloud.storage.providers import get_driver +from core.utils.url import url_path_join +from dss import settings +from spa.models.mix import Mix +from datetime import datetime, timedelta +from django.db.models import Count +import os +import urlparse +from os.path import isfile, join, basename + + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + cls = get_driver(Provider.AZURE_BLOBS) + driver = cls(settings.AZURE_ACCOUNT_NAME, settings.AZURE_ACCOUNT_KEY) + container = driver.get_container(container_name=settings.AZURE_CONTAINER) + + #.filter(upload_date__lte=datetime.today() - timedelta(days=180)) \ + mixes = Mix.objects \ + .exclude(archive_path__isnull=False) \ + .annotate(num_plays=Count('activity_plays')) \ + .order_by('num_plays') + for mix in mixes: + if os.path.isfile(mix.get_absolute_path()): + print "Uploading file for: %s" % mix.slug + file_name = "%s.%s" % (mix.uid, mix.filetype) + archive_path = url_path_join(settings.AZURE_ITEM_BASE_URL, settings.AZURE_CONTAINER, file_name) + + with open(mix.get_absolute_path(), 'rb') as iterator: + obj = driver.upload_object_via_stream( + iterator=iterator, + container=container, + object_name=file_name + ) + print "Uploaded" + mix.archive_path = archive_path + mix.save() + + expired_path = join(settings.MEDIA_ROOT, "mixes/archived") + new_file = os.path.join(expired_path, basename(iterator.name)) + os.rename(iterator.name, new_file) + + print "done- file is %s" % mix.archive_path + + except Exception, ex: + print "Debug exception: %s" % ex.message diff --git a/spa/management/commands/azure_util.py b/spa/management/commands/azure_util.py new file mode 100755 index 0000000..db5e979 --- /dev/null +++ b/spa/management/commands/azure_util.py @@ -0,0 +1,18 @@ +from django.core.management.base import NoArgsCommand + +from core.utils.cdn import upload_to_azure +from spa.models import Mix + + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + mixes = Mix.objects.filter(archive_updated=False) + for mix in mixes: + blob_name, download_name = mix.get_cdn_details() + upload_to_azure(blob_name, download_name) + mix.archive_updated = True + mix.save() + + except Exception, ex: + print "Fatal error, bailing. {0}".format(ex.message) diff --git a/spa/management/commands/debugHumanize.py b/spa/management/commands/debugHumanize.py new file mode 100755 index 0000000..9d383d9 --- /dev/null +++ b/spa/management/commands/debugHumanize.py @@ -0,0 +1,14 @@ +import humanize +from django.core.management.base import NoArgsCommand +from spa.models.activity import Activity + + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + activity = Activity.objects.get(pk=13437) + if activity is not None: + date = humanize.naturaltime(activity.date.replace(tzinfo=None)) + print date + except Exception, ex: + print "Debug exception: %s" % ex.message \ No newline at end of file diff --git a/spa/management/commands/debugRelations.py b/spa/management/commands/debugRelations.py new file mode 100755 index 0000000..f534eb0 --- /dev/null +++ b/spa/management/commands/debugRelations.py @@ -0,0 +1,14 @@ +from django.core.management.base import NoArgsCommand +from spa.models import Mix + + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + #l = Mix.objects.filter(slug='dss-on-deepvibes-radio-17th-july-jamie-o-sullivan')[0] + l = Mix.objects.filter(favourites__slug='fergalmoran')[0] + for fav in l.favourites.all(): + print fav.slug + pass + except Exception, ex: + print "Debug exception: %s" % ex.message \ No newline at end of file diff --git a/spa/management/commands/debugUserProfile.py b/spa/management/commands/debugUserProfile.py new file mode 100755 index 0000000..e068274 --- /dev/null +++ b/spa/management/commands/debugUserProfile.py @@ -0,0 +1,19 @@ +from django.contrib.auth.models import User +from django.core.management.base import NoArgsCommand + + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + users = User.objects.all() + for user in users: + try: + if user.get_profile() is None: + print "Invalid user: %s" % user.username + except: + print "Invalid user: %s" % user.username + user.save() + + pass + except Exception, ex: + print "Debug exception: %s" % ex.message \ No newline at end of file diff --git a/spa/management/commands/deletefailed.py b/spa/management/commands/deletefailed.py new file mode 100755 index 0000000..7db8534 --- /dev/null +++ b/spa/management/commands/deletefailed.py @@ -0,0 +1,10 @@ +from django.core.management.base import NoArgsCommand +from spa.models import Mix + + +class Command(NoArgsCommand): + def handle(self, *args, **options): + candidates = Mix.objects.filter(waveform_generated=False) + for mix in candidates: + print "Deleting: %s" % mix.title + mix.delete() diff --git a/spa/management/commands/deleteorphanmp3.py b/spa/management/commands/deleteorphanmp3.py new file mode 100755 index 0000000..87bd67b --- /dev/null +++ b/spa/management/commands/deleteorphanmp3.py @@ -0,0 +1,30 @@ +from dircache import listdir +import os +from django.core.management.base import NoArgsCommand +from os.path import isfile, join +from dss import settings +from spa.models import Mix + + +class Command(NoArgsCommand): + def handle(self, *args, **options): + try: + print "Starting" + mixes_path = join(settings.MEDIA_ROOT, "mixes") + expired_path = join(settings.MEDIA_ROOT, "mixes/expired") + files = [f for f in listdir(mixes_path) if isfile(join(mixes_path, f))] + + for f in files: + uid = os.path.splitext(f)[0] + try: + Mix.objects.get(uid=uid) + except Mix.DoesNotExist: + new_file = os.path.join(expired_path, f) + os.rename(os.path.join(mixes_path, f), new_file) + print "Moved %s to %s" % (f, new_file) + except Exception, ex: + print "Error in file: %s" % ex.message + + except Exception, ex: + print "Error: %s" % ex.message + diff --git a/spa/management/commands/fake_comment_timeindex.py b/spa/management/commands/fake_comment_timeindex.py new file mode 100755 index 0000000..d95cead --- /dev/null +++ b/spa/management/commands/fake_comment_timeindex.py @@ -0,0 +1,18 @@ +import random +from django.core.management.base import NoArgsCommand +from spa.models.comment import Comment + + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + comments = Comment.objects.all() + for comment in comments: + mix = comment.mix + time_index = random.randrange(50, comment.mix.duration) + comment.time_index = time_index + + comment.save() + print "Timeindex: %d Mix: %s Comment: %s" % (time_index, comment.mix.slug, comment.comment) + except Exception, ex: + print "Debug exception: %s" % ex.message \ No newline at end of file diff --git a/spa/management/commands/get_avatars.py b/spa/management/commands/get_avatars.py new file mode 100755 index 0000000..2ff750d --- /dev/null +++ b/spa/management/commands/get_avatars.py @@ -0,0 +1,44 @@ +from allauth.socialaccount.models import SocialAccount +from django.core.files.base import ContentFile +from django.core.management.base import NoArgsCommand +from requests import request, ConnectionError +from spa.models.userprofile import UserProfile + + +def save_image(profile, url): + try: + response = request('GET', url) + response.raise_for_status() + except ConnectionError: + pass + else: + profile.avatar_image.save(u'', + ContentFile(response.content), + save=False) + profile.save() + + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + for user in UserProfile.objects.all(): + try: + print "Getting image for {0}".format(user.slug) + social_account = SocialAccount.objects.get(user=user.user) + if social_account: + try: + provider_account = social_account.get_provider_account() + if provider_account: + avatar_url = provider_account.get_avatar_url() + save_image(user, avatar_url) + except Exception, ex: + print ex.message + else: + print "No account for {0}".format(user.slug) + + except SocialAccount.DoesNotExist: + pass + except Exception, ex: + print "Debug exception: %s" % ex.message + except Exception, ex: + print "Debug exception: %s" % ex.message diff --git a/spa/management/commands/processmix.py b/spa/management/commands/processmix.py new file mode 100755 index 0000000..fece7d7 --- /dev/null +++ b/spa/management/commands/processmix.py @@ -0,0 +1,31 @@ +from django.core.management.base import NoArgsCommand, CommandError +from django.template.defaultfilters import slugify +from core.utils.audio import Mp3FileNotFoundException +from core.utils.audio.mp3 import mp3_length +from core.utils.url import unique_slugify +from spa.models import Mix + + +class Command(NoArgsCommand): + help = "Updates audio files with their durations" + + def handle(self, *args, **options): + try: + candidates = Mix.objects.all() + for mix in candidates: + try: + if mix.duration is None: + print "Finding duration for: %s" % mix.title + length = mp3_length(mix.get_absolute_path()) + print "\tLength: %d" % length + mix.duration = length + if mix.slug == 'Invalid': + print "Slugifying mix: %s" % mix.title + mix.slug = unique_slugify(mix, mix.title) + print "\tNew title: %s" % mix.slug + mix.save() + except Mp3FileNotFoundException, me: + mix.delete() + print me.message + except Exception, ex: + raise CommandError(ex.message) diff --git a/spa/management/commands/waveforms.py b/spa/management/commands/waveforms.py new file mode 100755 index 0000000..872edcc --- /dev/null +++ b/spa/management/commands/waveforms.py @@ -0,0 +1,48 @@ +from optparse import make_option +import os +from django.core.management.base import NoArgsCommand, BaseCommand + +from spa.models.mix import Mix +from core.tasks import create_waveform_task + + +class Command(BaseCommand): + help = "Generate all outstanding waveforms" + option_list = BaseCommand.option_list + ( + make_option('--nocelery', + action='store_true', + dest='nocelery', + default=False, + help='Dispatch calls to celery broker'), + ) + + @staticmethod + def _get_file(mix): + #Check for file in mix directory + processed_file = "" + try: + processed_file = mix.get_absolute_path() + if not os.path.isfile(processed_file): + processed_file = mix.get_cache_path() + if not os.path.isfile(processed_file): + print "File for [%s] not found tried\n\t%s\n\t%s" % (mix.title, processed_file, processed_file) + return "" + + except Exception, ex: + print "Error generating waveform: %s" % ex.message + + return processed_file + + def handle(self, *args, **options): + print "Scanning for missing waveforms" + unprocessed = Mix.objects.filter(waveform_generated=False) + for mix in unprocessed: + print "Found %s" % mix.slug + mix_file = self._get_file(mix) + + if mix_file is not "": + if options['nocelery']: + create_waveform_task(in_file=mix_file, uid=mix.uid) + else: + create_waveform_task.delay(in_file=mix_file, uid=mix.uid) + diff --git a/spa/management/commands/zoom_convert_waveforms.py b/spa/management/commands/zoom_convert_waveforms.py new file mode 100755 index 0000000..6fd8e73 --- /dev/null +++ b/spa/management/commands/zoom_convert_waveforms.py @@ -0,0 +1,75 @@ +import os +from django.core.management.base import NoArgsCommand +from core.utils.waveform import generate_waveform +from dss import settings +from spa.models.mix import Mix + + +class Command(NoArgsCommand): + def _download_file(self, url, file_name): + import urllib2 + + u = urllib2.urlopen(url) + f = open(file_name, 'wb') + meta = u.info() + file_size = int(meta.getheaders("Content-Length")[0]) + print "Downloading: %s Bytes: %s" % (file_name, file_size) + + file_size_dl = 0 + block_sz = 8192 + while True: + file_buffer = u.read(block_sz) + if not file_buffer: + break + + file_size_dl += len(file_buffer) + f.write(file_buffer) + status = r"%10d [%3.2f%%]" % (file_size_dl, file_size_dl * 100. / file_size) + status += chr(8) * (len(status) + 1) + print status, + + f.close() + + def _convert_remote(self): + mixes = Mix.objects.exclude(waveform_version=2) + for mix in mixes: + # download audio file to temp path + print "Starting to process: %s" % mix.slug + file_name = "/tmp/%s.mp3" % mix.uid + url = mix.get_stream_url() + print "Downloading: %s To: %s" % (url, file_name) + self._download_file(url, file_name) + if not os.path.isfile(file_name): + print "File failed to download" + else: + # process waveform + generate_waveform(file_name, ) + # update mix.waveform_version to 2 + + # delete cached file + os.remove(file_name) + print "Done %s" % mix.slug + + def handle_noargs(self, **options): + try: + mixes = Mix.objects.exclude(waveform_version=2) + for mix in mixes: + from PIL import Image + import glob + + output_file = '{0}/waveforms/{1}.{2}'.format(settings.MEDIA_ROOT, mix.uid, 'png') + if os.path.exists(output_file): + try: + print 'Processing: %s' % mix.slug + im = Image.open(output_file) + w, h = im.size + im.crop((0, 0, w, h / 2)).save(output_file) + except Exception: + print "Exception with image: %s" % output_file + else: + print "Skipping: %s" % mix.slug + mix.waveform_version = 2 + mix.save() + pass + except Exception, ex: + print "Debug exception: %s" % ex.message \ No newline at end of file diff --git a/spa/middleware/__init__.py b/spa/middleware/__init__.py new file mode 100755 index 0000000..6d4caf8 --- /dev/null +++ b/spa/middleware/__init__.py @@ -0,0 +1 @@ +__author__ = 'fergalm' diff --git a/spa/middleware/sqlprinter.py b/spa/middleware/sqlprinter.py new file mode 100755 index 0000000..72189de --- /dev/null +++ b/spa/middleware/sqlprinter.py @@ -0,0 +1,56 @@ +import os + +from django.db import connection +from django.conf import settings + + +def terminal_width(): + """ + Function to compute the terminal width. + WARNING: This is not my code, but I've been using it forever and + I don't remember where it came from. + """ + width = 0 + try: + import struct, fcntl, termios + + s = struct.pack('HHHH', 0, 0, 0, 0) + x = fcntl.ioctl(1, termios.TIOCGWINSZ, s) + width = struct.unpack('HHHH', x)[1] + except: + pass + if width <= 0: + try: + width = int(os.environ['COLUMNS']) + except: + pass + if width <= 0: + width = 80 + return width + + +class SqlPrintingMiddleware(object): + """ + Middleware which prints out a list of all SQL queries done + for each view that is processed. This is only useful for debugging. + """ + + def process_response(self, request, response): + if not settings.DEBUG: + return + + indentation = 2 + if len(connection.queries) > 0 and settings.DEBUG: + width = 10000 #terminal_width() + total_time = 0.0 + for query in connection.queries: + nice_sql = query['sql'].replace('"', '').replace(',', ', ') + sql = "\033[1;31m[%s]\033[0m %s" % (query['time'], nice_sql) + total_time += float(query['time']) + while len(sql) > width - indentation: + print "%s%s" % (" " * indentation, sql[:width - indentation]) + sql = sql[width - indentation:] + print "%s%s\n" % (" " * indentation, sql) + replace_tuple = (" " * indentation, str(total_time)) + print "%s\033[1;32m[TOTAL TIME: %s seconds]\033[0m" % replace_tuple + return response diff --git a/spa/middleware/stripwhitespace.py b/spa/middleware/stripwhitespace.py new file mode 100755 index 0000000..18c765a --- /dev/null +++ b/spa/middleware/stripwhitespace.py @@ -0,0 +1,39 @@ +""" +Tightens up response content by removed superflous line breaks and whitespace. +By Doug Van Horn + +---- CHANGES ---- +v1.1 - 31st May 2011 +Cal Leeming [Simplicity Media Ltd] +Modified regex to strip leading/trailing white space from every line, not just those with blank \n. + +---- TODO ---- +* Ensure whitespace isn't stripped from within
 or  or