From 60907f2db05fe76ab39153a7125ea4655efb8791 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Fri, 17 Jul 2015 20:29:12 +0100 Subject: [PATCH 01/36] Upped JWT expiration to 30 minutes --- api/auth.py | 4 +--- api/helpers.py | 3 +-- api/urls.py | 25 ++++++++++++++++++++----- dss/settings.py | 7 +++++++ spa/models/session.py | 7 +++++++ 5 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 spa/models/session.py diff --git a/api/auth.py b/api/auth.py index 201ce3a..4246dd8 100644 --- a/api/auth.py +++ b/api/auth.py @@ -18,8 +18,6 @@ from dss import settings @psa() def auth_by_token(request, backend): - token = request.data.get('access_token') - user = request.user user = request.backend.do_auth( access_token=request.data.get('access_token') ) @@ -40,7 +38,7 @@ class FacebookView(APIView): except Exception, e: return Response({ 'status': 'Bad request', - 'message': 'Could not authenticate with the provided token' if settings.DEBUG else e.message + 'message': 'Could not authenticate with the provided token' if not settings.DEBUG else e.message }, status=status.HTTP_400_BAD_REQUEST) if user: diff --git a/api/helpers.py b/api/helpers.py index 32b3e32..8680609 100644 --- a/api/helpers.py +++ b/api/helpers.py @@ -22,8 +22,7 @@ class ChatHelper(ActivityHelper): # do some persistence stuff with the chat from core.realtime import chat - user = self.get_session(request) - + #user = self.get_session(request) chat.post_chat(request.data['user'], request.data['message']) return Response(request.data['message'], HTTP_201_CREATED) diff --git a/api/urls.py b/api/urls.py index 043c535..ce510ef 100755 --- a/api/urls.py +++ b/api/urls.py @@ -1,21 +1,35 @@ from django.conf.urls import patterns, url, include +from rest_framework import permissions from rest_framework.routers import DefaultRouter +from rest_framework.views import APIView from api import views, auth, helpers from api.auth import FacebookView +from rest_framework.views import status +from rest_framework.response import Response router = DefaultRouter() # trailing_slash=True) router.register(r'user', views.UserProfileViewSet) router.register(r'mix', views.MixViewSet) - router.register(r'notification', views.NotificationViewSet) router.register(r'hitlist', views.HitlistViewSet) router.register(r'comments', views.CommentViewSet) router.register(r'activity', views.ActivityViewSet, base_name='activity') router.register(r'genre', views.GenreViewSet, base_name='genre') + +class DebugView(APIView): + permission_classes = (permissions.AllowAny,) + + def post(self, request, format=None): + return Response({ + 'status': 'Hello', + 'message': 'Sailor' + }, status=status.HTTP_200_OK) + + urlpatterns = patterns( '', url(r'^', include(router.urls)), @@ -26,17 +40,18 @@ urlpatterns = patterns( url(r'_search/$', views.SearchResultsView.as_view()), url(r'^', include(router.urls)), - #url(r'^login/', auth.ObtainAuthToken.as_view()), - #url(r'^logout/', auth.ObtainLogout.as_view()), + url(r'^_login/', FacebookView.as_view()), + url(r'^token-refresh/', 'rest_framework_jwt.views.refresh_jwt_token'), # url(r'^_tr/', RefreshToken.as_view()), url(r'^__u/checkslug', helpers.UserSlugCheckHelper.as_view()), url(r'^__u/', auth.ObtainUser.as_view()), - url(r'^_act/play', helpers.ActivityPlayHelper.as_view()), url(r'^_chat/', helpers.ChatHelper.as_view()), - url(r'^_login/', FacebookView.as_view()), + + + url(r'^__debug/', DebugView.as_view()), url('', include('social.apps.django_app.urls', namespace='social')), ) diff --git a/dss/settings.py b/dss/settings.py index 605277d..e7bc4ed 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -1,6 +1,7 @@ # e Django settings for dss project. import os import mimetypes +from datetime import timedelta from django.core.urlresolvers import reverse_lazy import djcelery from django.conf import global_settings @@ -217,3 +218,9 @@ DEFAULT_USER_TITLE = 'Just another DSS lover' SITE_NAME = 'Deep South Sounds' THUMBNAIL_PREFIX = 'cache/_tn/' + +JWT_AUTH = { + 'JWT_EXPIRATION_DELTA': timedelta(seconds=1800), + 'JWT_ALLOW_REFRESH': True, + 'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=30), +} \ No newline at end of file diff --git a/spa/models/session.py b/spa/models/session.py new file mode 100644 index 0000000..2d78ba5 --- /dev/null +++ b/spa/models/session.py @@ -0,0 +1,7 @@ +from django.db import models +from spa.models import BaseModel, UserProfile + + +class Session(BaseModel): + jwt_token = models.CharField(max_length=2048) + user = models.ForeignKey(UserProfile) From 013d0f37be1da41a714ec48a19f7f34240add0a4 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Fri, 17 Jul 2015 23:58:16 +0100 Subject: [PATCH 02/36] Extended JWT expiry time --- api/urls.py | 7 +++++-- dss/settings.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/api/urls.py b/api/urls.py index ce510ef..edd15f8 100755 --- a/api/urls.py +++ b/api/urls.py @@ -1,7 +1,9 @@ from django.conf.urls import patterns, url, include from rest_framework import permissions +from rest_framework.permissions import IsAuthenticated from rest_framework.routers import DefaultRouter from rest_framework.views import APIView +from rest_framework_jwt.authentication import JSONWebTokenAuthentication from api import views, auth, helpers from api.auth import FacebookView @@ -21,11 +23,12 @@ router.register(r'genre', views.GenreViewSet, base_name='genre') class DebugView(APIView): - permission_classes = (permissions.AllowAny,) + permission_classes = (IsAuthenticated, ) + authentication_classes = (JSONWebTokenAuthentication, ) def post(self, request, format=None): return Response({ - 'status': 'Hello', + 'status': request.user.first_name, 'message': 'Sailor' }, status=status.HTTP_200_OK) diff --git a/dss/settings.py b/dss/settings.py index e7bc4ed..a91bf7a 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -220,7 +220,7 @@ SITE_NAME = 'Deep South Sounds' THUMBNAIL_PREFIX = 'cache/_tn/' JWT_AUTH = { - 'JWT_EXPIRATION_DELTA': timedelta(seconds=1800), + 'JWT_EXPIRATION_DELTA': timedelta(seconds=900), 'JWT_ALLOW_REFRESH': True, 'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=30), } \ No newline at end of file From ec1c744148e16ad946257fda6b4f432fd74c651b Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Sun, 19 Jul 2015 21:40:16 +0100 Subject: [PATCH 03/36] Added richer payload to redis post --- api/helpers.py | 10 +++++++++- core/realtime/chat.py | 12 ++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/api/helpers.py b/api/helpers.py index 8680609..e3e50d5 100644 --- a/api/helpers.py +++ b/api/helpers.py @@ -23,7 +23,15 @@ class ChatHelper(ActivityHelper): from core.realtime import chat #user = self.get_session(request) - chat.post_chat(request.data['user'], request.data['message']) + u = request.user + if not u.is_anonymous(): + image = u.userprofile.get_sized_avatar_image(32, 32) + user = u.userprofile.get_nice_name() + else: + image = settings.DEFAULT_USER_IMAGE + user = settings.DEFAULT_USER_NAME + + chat.post_chat(request.data['user'], image, user, request.data['message']) return Response(request.data['message'], HTTP_201_CREATED) diff --git a/core/realtime/chat.py b/core/realtime/chat.py index e981a5e..f8a7d8a 100644 --- a/core/realtime/chat.py +++ b/core/realtime/chat.py @@ -1,8 +1,16 @@ import json +import datetime import redis -def post_chat(session, message): +def post_chat(session, image, user, message): r = redis.StrictRedis(host='localhost', port=6379, db=0) - response = r.publish('chat', json.dumps({'session': session, 'message': message})) + payload = json.dumps({ + 'session': session, + 'message': message, + 'user': user, + 'image': image, + 'date': datetime.datetime.now().isoformat() + }) + response = r.publish('chat', payload) print "Message sent: {0}".format(response) From 3b19f19708a853844750ac6d7190bf7eebcac3e4 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Sun, 19 Jul 2015 22:08:16 +0100 Subject: [PATCH 04/36] Removed 500 handler --- dss/urls.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dss/urls.py b/dss/urls.py index 61c8f58..c1db362 100755 --- a/dss/urls.py +++ b/dss/urls.py @@ -15,7 +15,6 @@ urlpatterns = patterns( (r'^grappelli/', include('grappelli.urls')), (r'^social/', include('spa.social.urls')), ) -handler500 = 'spa.views.debug_500' if settings.DEBUG: from django.views.static import serve From df4d14a6f7d0943ad74d210c69ca56d85535afd9 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Thu, 23 Jul 2015 18:23:43 +0000 Subject: [PATCH 05/36] Added docker files --- Dockerfile | 13 +++++++++++++ docker-compose.yml | 11 +++++++++++ dss/settings.py | 3 +-- requirements.txt | 3 +-- 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1f9677d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:2.7 +ENV PYTHONBUFFERED 1 + +RUN mkdir /code +RUN mkdir /files/static +RUN mkdir /files/media +RUN mkdir /files/cache +RUN mkdir /files/tmp + +WORKDIR /code +ADD requirements.txt /code/ +RUN pip install -r requirements.txt +ADD . /code/ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..25af2bd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +db: + image: postgres +web: + build: . + command: python manage.py runserver 0.0.0.0:8000 + volumes: + - .:/code + ports: + - "8000:8000" + links: + - db diff --git a/dss/settings.py b/dss/settings.py index a91bf7a..1aed694 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -117,7 +117,6 @@ INSTALLED_APPS = ( 'corsheaders', 'sorl.thumbnail', 'spa', - 'tinymce', 'gunicorn', 'spa.signals', 'core', @@ -223,4 +222,4 @@ JWT_AUTH = { 'JWT_EXPIRATION_DELTA': timedelta(seconds=900), 'JWT_ALLOW_REFRESH': True, 'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=30), -} \ No newline at end of file +} diff --git a/requirements.txt b/requirements.txt index c8d12d2..83dc88d 100755 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,6 @@ 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 django-allauth -django-tinymce apache-libcloud mandrill djrill @@ -43,4 +42,4 @@ mutagen django-enumfield ipython -ipdb \ No newline at end of file +ipdb From a98f3731eed1b24acda1137d9994947f079e40be Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Fri, 24 Jul 2015 19:32:43 +0100 Subject: [PATCH 06/36] Pre docker commit --- Dockerfile | 1 + docker-compose.yml | 11 ----------- dss/settings.py | 7 ------- dss/urls.py | 1 - dss/wsgi.py | 22 ---------------------- requirements.txt | 10 +++++----- 6 files changed, 6 insertions(+), 46 deletions(-) delete mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile index 1f9677d..ae29385 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ FROM python:2.7 ENV PYTHONBUFFERED 1 RUN mkdir /code +RUN mkdir /files RUN mkdir /files/static RUN mkdir /files/media RUN mkdir /files/cache diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 25af2bd..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,11 +0,0 @@ -db: - image: postgres -web: - build: . - command: python manage.py runserver 0.0.0.0:8000 - volumes: - - .:/code - ports: - - "8000:8000" - links: - - db diff --git a/dss/settings.py b/dss/settings.py index 1aed694..faba7de 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -71,12 +71,6 @@ TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + ( '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", ) @@ -138,7 +132,6 @@ INSTALLED_APPS = ( 'djrill', 'rest_framework', 'rest_framework.authtoken', - 'rest_framework_swagger', ) # where to redirect users to after logging in diff --git a/dss/urls.py b/dss/urls.py index c1db362..4625528 100755 --- a/dss/urls.py +++ b/dss/urls.py @@ -9,7 +9,6 @@ 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')), diff --git a/dss/wsgi.py b/dss/wsgi.py index 8765705..c998099 100755 --- a/dss/wsgi.py +++ b/dss/wsgi.py @@ -1,28 +1,6 @@ -""" -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/requirements.txt b/requirements.txt index 83dc88d..1246684 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django>=1.6,<1.7 +Django==1.6.11 django-extensions django-sendfile Werkzeug @@ -9,9 +9,9 @@ django-dirtyfields django-storages django-user-sessions django-cors-headers -django-rest-swagger +six==1.6.0 django-filter -django-grappelli +django-grappelli==2.5.7 django-model_utils django-dbbackup django-user-agents @@ -30,8 +30,8 @@ apache-libcloud mandrill djrill -djangorestframework -djangorestframework-jwt +djangorestframework==3.1.3 +djangorestframework-jwt==1.6.0 drf-nested-routers django-celery pillow From 59cb70b120aac36fb53afe9c42088100e3b22db9 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Sun, 26 Jul 2015 00:34:18 +0100 Subject: [PATCH 07/36] Added var for redis host --- core/realtime/chat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/realtime/chat.py b/core/realtime/chat.py index f8a7d8a..ccfa0ff 100644 --- a/core/realtime/chat.py +++ b/core/realtime/chat.py @@ -1,10 +1,10 @@ import json import datetime import redis - +import settings def post_chat(session, image, user, message): - r = redis.StrictRedis(host='localhost', port=6379, db=0) + r = redis.StrictRedis(host=settings.REDIS_HOST, port=6379, db=0) payload = json.dumps({ 'session': session, 'message': message, From 70b627d59f8c0a451d2f07cd95b211f1dcf974a0 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Sun, 26 Jul 2015 00:48:04 +0100 Subject: [PATCH 08/36] Fixed settings import --- core/realtime/chat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/realtime/chat.py b/core/realtime/chat.py index ccfa0ff..fffefe1 100644 --- a/core/realtime/chat.py +++ b/core/realtime/chat.py @@ -1,7 +1,8 @@ import json import datetime import redis -import settings +from dss import settings + def post_chat(session, image, user, message): r = redis.StrictRedis(host=settings.REDIS_HOST, port=6379, db=0) From ffddf5101c0b9c3515ea2321011d42fdd5437eef Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Sun, 26 Jul 2015 16:41:38 +0100 Subject: [PATCH 09/36] Settings changes and logging --- Dockerfile | 1 + api/views.py | 15 ++++++++++++--- dss/settings.py | 1 - 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index ae29385..f7d9704 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ FROM python:2.7 ENV PYTHONBUFFERED 1 RUN mkdir /code +RUN mkdir /srv/logs RUN mkdir /files RUN mkdir /files/static RUN mkdir /files/media diff --git a/api/views.py b/api/views.py index 12948a5..0ca3e42 100755 --- a/api/views.py +++ b/api/views.py @@ -161,16 +161,23 @@ class PartialMixUploadView(views.APIView): # noinspection PyBroadException def post(self, request): try: + logger.info("Received post file") uid = request.META.get('HTTP_UPLOAD_HASH') - in_file = request.FILES['file'] if request.FILES else None + in_file = request.data['file'] if request.data else None file_name, extension = os.path.splitext(in_file.name) + logger.info("Constructing storage") 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' + logger.info("Storage constructed") + try: + logger.debug("Received input file") + logger.debug("Storage is %s".format(file_storate.base_location)) input_file = os.path.join(file_storage.base_location, cache_file) + logger.debug("Input file generating") # Chain the waveform & archive tasks together # Probably not the best place for them but will do for now @@ -178,7 +185,8 @@ class PartialMixUploadView(views.APIView): (create_waveform_task.s(input_file, uid) | archive_mix_task.s(filetype='mp3', uid=uid)).delay() - except Exception: + except Exception, ex: + logger.error("Unable to connect to celery: %s".format(ex.message)) response = \ 'Unable to connect to waveform generation task, there may be a delay in getting your mix online' @@ -209,12 +217,13 @@ class ActivityViewSet(viewsets.ModelViewSet): ret = ActivityPlay.objects.filter(mix__user=user).order_by("-id") - if len(ret) >0: + if len(ret) > 0: logger.debug("Activity returned: %s".format(ret[0].get_object_slug())) return ret else: return [] + class DownloadItemView(views.APIView): def get(self, request, *args, **kwargs): try: diff --git a/dss/settings.py b/dss/settings.py index faba7de..fac71dd 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -39,7 +39,6 @@ DATABASES = { 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' From 5f61fb280c20f1e5bedaa6ab0932636bfd491158 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Sun, 26 Jul 2015 16:42:57 +0100 Subject: [PATCH 10/36] Altered settings --- dss/settings.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dss/settings.py b/dss/settings.py index faba7de..e35ebba 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -116,7 +116,7 @@ INSTALLED_APPS = ( 'core', #'schedule', 'django_user_agents', - + 'storages', 'social.apps.django_app.default', # TODO: remove @@ -209,7 +209,8 @@ DEFAULT_USER_NAME = 'Anonymouse' DEFAULT_USER_TITLE = 'Just another DSS lover' SITE_NAME = 'Deep South Sounds' -THUMBNAIL_PREFIX = 'cache/_tn/' +THUMBNAIL_PREFIX = '_tn/' +THUMBNAIL_STORAGE = 'storages.backends.azure_storage.AzureStorage' JWT_AUTH = { 'JWT_EXPIRATION_DELTA': timedelta(seconds=900), From d4cfae3addfdebd8e1d7c506429d20b1fef0977d Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Sun, 26 Jul 2015 20:43:24 +0100 Subject: [PATCH 11/36] Fixed console logging --- api/views.py | 8 ++++---- dss/logsettings.py | 10 +++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/api/views.py b/api/views.py index 0ca3e42..9bf1d9c 100755 --- a/api/views.py +++ b/api/views.py @@ -27,7 +27,7 @@ from spa.models.comment import Comment from spa.models.notification import Notification from spa.models.userprofile import UserProfile -logger = logging.getLogger(__name__) +logger = logging.getLogger('spa') class AnonymousWriteUserDelete(BasePermission): @@ -175,7 +175,7 @@ class PartialMixUploadView(views.APIView): try: logger.debug("Received input file") - logger.debug("Storage is %s".format(file_storate.base_location)) + logger.debug("Storage is {0}".format(file_storate.base_location)) input_file = os.path.join(file_storage.base_location, cache_file) logger.debug("Input file generating") @@ -186,7 +186,7 @@ class PartialMixUploadView(views.APIView): archive_mix_task.s(filetype='mp3', uid=uid)).delay() except Exception, ex: - logger.error("Unable to connect to celery: %s".format(ex.message)) + logger.error("Unable to connect to celery: {0}".format(ex.message)) response = \ 'Unable to connect to waveform generation task, there may be a delay in getting your mix online' @@ -218,7 +218,7 @@ class ActivityViewSet(viewsets.ModelViewSet): ret = ActivityPlay.objects.filter(mix__user=user).order_by("-id") if len(ret) > 0: - logger.debug("Activity returned: %s".format(ret[0].get_object_slug())) + logger.debug("Activity returned: {0}".format(ret[0].get_object_slug())) return ret else: return [] diff --git a/dss/logsettings.py b/dss/logsettings.py index e7d88c2..a98888a 100755 --- a/dss/logsettings.py +++ b/dss/logsettings.py @@ -1,4 +1,5 @@ import os +import sys from dss import localsettings if os.name == 'posix': @@ -20,11 +21,14 @@ LOGGING = { }, 'handlers': { 'file': { - 'level': 'DEBUG', + 'level': 'INFO', 'class': 'logging.FileHandler', 'filename': LOG_FILE, 'formatter': 'verbose' - }, + }, 'console': { + 'class': 'logging.StreamHandler', + 'stream': sys.stdout, + } }, 'loggers': { 'django': { @@ -33,7 +37,7 @@ LOGGING = { 'level': 'DEBUG', }, 'spa': { - 'handlers': ['file'], + 'handlers': ['file', 'console'], 'level': 'DEBUG', }, } From cb14778dfe5e78ad75cf681df21aa3d04544490a Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Sun, 26 Jul 2015 20:48:32 +0100 Subject: [PATCH 12/36] Fixed typo --- api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/views.py b/api/views.py index 9bf1d9c..9b8ba6e 100755 --- a/api/views.py +++ b/api/views.py @@ -175,7 +175,7 @@ class PartialMixUploadView(views.APIView): try: logger.debug("Received input file") - logger.debug("Storage is {0}".format(file_storate.base_location)) + logger.debug("Storage is {0}".format(file_storage.base_location)) input_file = os.path.join(file_storage.base_location, cache_file) logger.debug("Input file generating") From 69c779c90e10812d984a15ea326c0bf093041bd0 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Sun, 26 Jul 2015 21:06:14 +0100 Subject: [PATCH 13/36] Altered logging --- api/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/views.py b/api/views.py index 9b8ba6e..5a70774 100755 --- a/api/views.py +++ b/api/views.py @@ -177,18 +177,18 @@ class PartialMixUploadView(views.APIView): logger.debug("Received input file") logger.debug("Storage is {0}".format(file_storage.base_location)) input_file = os.path.join(file_storage.base_location, cache_file) - logger.debug("Input file generating") # Chain the waveform & archive tasks together # Probably not the best place for them but will do for now # First argument to archive_mix_task is not specified as it is piped from create_waveform_task (create_waveform_task.s(input_file, uid) | archive_mix_task.s(filetype='mp3', uid=uid)).delay() + logger.debug("Waveform task started") except Exception, ex: - logger.error("Unable to connect to celery: {0}".format(ex.message)) + logger.error("Unable to connect to rabbitmq: {0}".format(ex.message)) response = \ - 'Unable to connect to waveform generation task, there may be a delay in getting your mix online' + 'Unable to connect to rabbitmq, there may be a delay in getting your mix online' file_dict = { 'response': response, From 8e0d15399f57a2c5c480cee3e26083a7ab0f9a42 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Mon, 27 Jul 2015 22:04:10 +0100 Subject: [PATCH 14/36] Azure all the things --- spa/management/commands/get_avatars.py | 17 +++++++++++++---- spa/models/fields.py | 4 +++- spa/models/userprofile.py | 14 ++------------ 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/spa/management/commands/get_avatars.py b/spa/management/commands/get_avatars.py index 2ff750d..eccda8d 100755 --- a/spa/management/commands/get_avatars.py +++ b/spa/management/commands/get_avatars.py @@ -1,7 +1,10 @@ from allauth.socialaccount.models import SocialAccount +from azure.storage import BlobService from django.core.files.base import ContentFile from django.core.management.base import NoArgsCommand from requests import request, ConnectionError +from dss import storagesettings + from spa.models.userprofile import UserProfile @@ -12,10 +15,16 @@ def save_image(profile, url): except ConnectionError: pass else: - profile.avatar_image.save(u'', - ContentFile(response.content), - save=False) - profile.save() + service = BlobService( + account_name=storagesettings.AZURE_ACCOUNT_NAME, + account_key=storagesettings.AZURE_ACCOUNT_KEY) + + service.put_block_blob_from_bytes( + 'avatars', + profile.id, + response.content, + x_ms_blob_content_type=response.headers['content-type'] + ) class Command(NoArgsCommand): diff --git a/spa/models/fields.py b/spa/models/fields.py index f44c3b2..27896e6 100755 --- a/spa/models/fields.py +++ b/spa/models/fields.py @@ -102,4 +102,6 @@ try: from south.modelsinspector import add_introspection_rules add_introspection_rules([], ['^spa\.models.fields\.MultiSelectField']) except ImportError: - pass \ No newline at end of file + pass + + diff --git a/spa/models/userprofile.py b/spa/models/userprofile.py index f3f2aa5..5898e01 100755 --- a/spa/models/userprofile.py +++ b/spa/models/userprofile.py @@ -168,6 +168,7 @@ class UserProfile(BaseModel): return self.display_name or self.first_name + ' ' + self.last_name def get_sized_avatar_image(self, width, height): + return self.get_avatar_image() try: image = self.get_avatar_image() sized = thumbnail.get_thumbnail(image, "%sx%s" % (width, height), crop="center") @@ -178,18 +179,7 @@ class UserProfile(BaseModel): return UserProfile.get_default_avatar_image() def get_avatar_image(self): - avatar_type = self.avatar_type - if avatar_type == 'gravatar': - gravatar_exists = has_gravatar(self.email) - if gravatar_exists: - return get_gravatar_url(self.email) - else: - if os.path.exists(self.avatar_image.file.name): - return self.avatar_image - else: - return self.get_default_avatar_image() - - return UserProfile.get_default_avatar_image() + return (settings.CDN_URL + 'avatars/{0}').format(self.id) def get_profile_url(self): return '/user/%s' % (self.slug) From de4ac4dadddd8d7840b305692ac5e913f03832dc Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Mon, 27 Jul 2015 22:04:57 +0100 Subject: [PATCH 15/36] Celery and stuff --- Dockerfile | 4 ++++ api/auth.py | 6 ++++-- api/views.py | 5 ++++- dss/settings.py | 1 - 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index f7d9704..a81128d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,3 +13,7 @@ WORKDIR /code ADD requirements.txt /code/ RUN pip install -r requirements.txt ADD . /code/ + +RUN adduser --disabled-password --gecos '' djworker +RUN chown djworker /files -R +RUN chown djworker /srv/logs -R diff --git a/api/auth.py b/api/auth.py index 4246dd8..0403173 100644 --- a/api/auth.py +++ b/api/auth.py @@ -1,5 +1,6 @@ from calendar import timegm import datetime +import logging from rest_framework import permissions from rest_framework.authtoken.serializers import AuthTokenSerializer from rest_framework.response import Response @@ -15,7 +16,7 @@ from rest_framework import parsers from social.apps.django_app.utils import psa from dss import settings - +logger = logging.getLogger('spa') @psa() def auth_by_token(request, backend): user = request.backend.do_auth( @@ -36,9 +37,10 @@ class FacebookView(APIView): try: user = auth_by_token(request, backend) except Exception, e: + logger.exception(e) return Response({ 'status': 'Bad request', - 'message': 'Could not authenticate with the provided token' if not settings.DEBUG else e.message + 'message': e.message }, status=status.HTTP_400_BAD_REQUEST) if user: diff --git a/api/views.py b/api/views.py index 5a70774..1ab0fad 100755 --- a/api/views.py +++ b/api/views.py @@ -181,12 +181,15 @@ class PartialMixUploadView(views.APIView): # Chain the waveform & archive tasks together # Probably not the best place for them but will do for now # First argument to archive_mix_task is not specified as it is piped from create_waveform_task + + logger.debug("Processing input_file: {0}".format(input_file)) + logger.debug("Connecting to broker: {0}".format(settings.BROKER_URL)) (create_waveform_task.s(input_file, uid) | archive_mix_task.s(filetype='mp3', uid=uid)).delay() logger.debug("Waveform task started") except Exception, ex: - logger.error("Unable to connect to rabbitmq: {0}".format(ex.message)) + logger.exception(ex) response = \ 'Unable to connect to rabbitmq, there may be a delay in getting your mix online' diff --git a/dss/settings.py b/dss/settings.py index 6cfb3c8..2904ec6 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -139,7 +139,6 @@ LOGOUT_URL = reverse_lazy('home') FACEBOOK_APP_ID = '154504534677009' -djcelery.setup_loader() AVATAR_STORAGE_DIR = MEDIA_ROOT + '/avatars/' ACCOUNT_LOGOUT_REDIRECT_URL = '/' From a6e70a929f1d27a57d81683edc5059e19d199d8e Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Mon, 27 Jul 2015 22:05:38 +0100 Subject: [PATCH 16/36] Added untracked --- dss/celeryconf.py | 13 +++++++++++++ run_celery.sh | 4 ++++ run_web.sh | 2 ++ 3 files changed, 19 insertions(+) create mode 100644 dss/celeryconf.py create mode 100755 run_celery.sh create mode 100755 run_web.sh diff --git a/dss/celeryconf.py b/dss/celeryconf.py new file mode 100644 index 0000000..11b75a8 --- /dev/null +++ b/dss/celeryconf.py @@ -0,0 +1,13 @@ +import os + +from celery import Celery +from django.conf import settings + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dss.settings") + +app = Celery('dss') + +CELERY_TIMEZONE = 'UTC' + +app.config_from_object('django.conf:settings') +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) diff --git a/run_celery.sh b/run_celery.sh new file mode 100755 index 0000000..a2e8345 --- /dev/null +++ b/run_celery.sh @@ -0,0 +1,4 @@ +#!/bin/sh +su -m djworker -c "celery worker -A dss.celeryconf -Q default" +chown djworker /files -R +chown djworker /tmp/dss.log \ No newline at end of file diff --git a/run_web.sh b/run_web.sh new file mode 100755 index 0000000..45e0d8f --- /dev/null +++ b/run_web.sh @@ -0,0 +1,2 @@ +#!/bin/sh +su -m djworker -c "python manage.py runserver_plus 0.0.0.0:8000" From bcf0b37f819ef786fc18e24808dc686a4c0baaa2 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Tue, 28 Jul 2015 22:03:30 +0100 Subject: [PATCH 17/36] Altered some celery stuff --- api/views.py | 3 +-- dss/__init__.py | 2 ++ dss/celeryconf.py | 14 +++++++++----- dss/settings.py | 2 -- requirements.txt | 3 ++- spa/api/v1/UserResource.py | 8 +++----- spa/management/commands/waveforms.py | 9 +++++---- {core => spa}/tasks.py | 3 ++- 8 files changed, 24 insertions(+), 20 deletions(-) rename {core => spa}/tasks.py (93%) diff --git a/api/views.py b/api/views.py index 1ab0fad..53349de 100755 --- a/api/views.py +++ b/api/views.py @@ -17,9 +17,8 @@ from rest_framework.status import HTTP_202_ACCEPTED, HTTP_401_UNAUTHORIZED, HTTP HTTP_200_OK, HTTP_204_NO_CONTENT from api import serializers -from core.utils.cdn import upload_to_azure from dss import settings -from core.tasks import create_waveform_task, archive_mix_task +from spa.tasks import create_waveform_task, archive_mix_task from spa.models.genre import Genre from spa.models.activity import ActivityPlay from spa.models.mix import Mix diff --git a/dss/__init__.py b/dss/__init__.py index e69de29..d1956f9 100755 --- a/dss/__init__.py +++ b/dss/__init__.py @@ -0,0 +1,2 @@ +from __future__ import absolute_import +from .celeryconf import app as celery_app diff --git a/dss/celeryconf.py b/dss/celeryconf.py index 11b75a8..2625d86 100644 --- a/dss/celeryconf.py +++ b/dss/celeryconf.py @@ -1,13 +1,17 @@ +from __future__ import absolute_import + import os from celery import Celery -from django.conf import settings -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dss.settings") +# set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proj.settings') + +from django.conf import settings app = Celery('dss') -CELERY_TIMEZONE = 'UTC' - +# Using a string here means the worker will not have to +# pickle the object when using Windows. app.config_from_object('django.conf:settings') -app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) \ No newline at end of file diff --git a/dss/settings.py b/dss/settings.py index 2904ec6..40e662e 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -3,7 +3,6 @@ import os import mimetypes from datetime import timedelta from django.core.urlresolvers import reverse_lazy -import djcelery from django.conf import global_settings from utils import here @@ -106,7 +105,6 @@ INSTALLED_APPS = ( #'django_facebook', 'django_extensions', 'django_gravatar', - 'djcelery', 'corsheaders', 'sorl.thumbnail', 'spa', diff --git a/requirements.txt b/requirements.txt index 1246684..e3dd54e 100755 --- a/requirements.txt +++ b/requirements.txt @@ -30,10 +30,11 @@ apache-libcloud mandrill djrill +celery + djangorestframework==3.1.3 djangorestframework-jwt==1.6.0 drf-nested-routers -django-celery pillow django-gravatar2 diff --git a/spa/api/v1/UserResource.py b/spa/api/v1/UserResource.py index 16757f2..3e8f7ea 100755 --- a/spa/api/v1/UserResource.py +++ b/spa/api/v1/UserResource.py @@ -1,21 +1,19 @@ from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned -from django.db.models import Count, Q, F +from django.db.models import Count, Q +from django.conf.urls import url + 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): diff --git a/spa/management/commands/waveforms.py b/spa/management/commands/waveforms.py index 8824b7c..3d90b59 100755 --- a/spa/management/commands/waveforms.py +++ b/spa/management/commands/waveforms.py @@ -1,10 +1,11 @@ from optparse import make_option import os -from django.core.management.base import NoArgsCommand, BaseCommand -from spa.management.commands import helpers +from django.core.management.base import BaseCommand + +from spa.management.commands import helpers from spa.models.mix import Mix -from core.tasks import create_waveform_task +from spa.tasks import create_waveform_task class Command(BaseCommand): @@ -40,7 +41,7 @@ class Command(BaseCommand): return processed_file except Exception, ex: - print "Error generating waveform: %s" % ex.message + print "Error generating waveform: {0}".format(ex.message) return "" diff --git a/core/tasks.py b/spa/tasks.py similarity index 93% rename from core/tasks.py rename to spa/tasks.py index 89a37e2..a823dc1 100755 --- a/core/tasks.py +++ b/spa/tasks.py @@ -37,7 +37,8 @@ def archive_mix_task(in_file, filetype, uid): try: upload_to_azure(in_file, filetype, uid) except Exception, ex: - print "Unable to upload: %s".format(ex.message) + print "Unable to upload: {0}".format(ex.message) + @task def update_geo_info_task(ip_address, profile_id): From 364ad61431f23d917cc7eadcf72176770b191ad0 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Tue, 28 Jul 2015 22:04:38 +0100 Subject: [PATCH 18/36] Updated tasks --- core/tasks.py | 53 --------------------------------------------------- run_celery.sh | 2 +- 2 files changed, 1 insertion(+), 54 deletions(-) delete mode 100755 core/tasks.py diff --git a/core/tasks.py b/core/tasks.py deleted file mode 100755 index 89a37e2..0000000 --- a/core/tasks.py +++ /dev/null @@ -1,53 +0,0 @@ -import shutil -from celery.task import task -import os -from core.utils.cdn import upload_to_azure -from spa.signals import waveform_generated_signal - -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 - waveform_generated_signal.send(sender=None, uid=uid) - return new_file - else: - print "Outfile is missing" - - -@task(time_limit=3600) -def archive_mix_task(in_file, filetype, uid): - print "Sending {0} to azure".format(uid) - try: - upload_to_azure(in_file, filetype, uid) - except Exception, ex: - print "Unable to upload: %s".format(ex.message) - -@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/run_celery.sh b/run_celery.sh index a2e8345..7199e4d 100755 --- a/run_celery.sh +++ b/run_celery.sh @@ -1,4 +1,4 @@ #!/bin/sh -su -m djworker -c "celery worker -A dss.celeryconf -Q default" +su -m djworker -c "sleep 3 && celery worker -A dss.celeryconf -Q default" chown djworker /files -R chown djworker /tmp/dss.log \ No newline at end of file From 535b60759bd4169023cb2e90b98cd6922ac16243 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Tue, 28 Jul 2015 22:40:41 +0100 Subject: [PATCH 19/36] Added bin dir --- bin/sox | Bin 0 -> 68896 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100755 bin/sox diff --git a/bin/sox b/bin/sox new file mode 100755 index 0000000000000000000000000000000000000000..438a437da5a0d064755b5e31983b267977ec3670 GIT binary patch literal 68896 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 literal 0 HcmV?d00001 From 4d3353e39629acd8c3055c69f20fd1002b0c573e Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Wed, 29 Jul 2015 12:01:32 +0100 Subject: [PATCH 20/36] Fixed wave generation --- Dockerfile | 1 + bin/sox | Bin 68896 -> 0 bytes bin/wav2png | Bin 0 -> 177848 bytes dss/celeryconf.py | 2 +- run_celery.sh | 2 -- 5 files changed, 2 insertions(+), 3 deletions(-) delete mode 100755 bin/sox create mode 100755 bin/wav2png diff --git a/Dockerfile b/Dockerfile index a81128d..9c07f99 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ RUN mkdir /files/tmp WORKDIR /code ADD requirements.txt /code/ RUN pip install -r requirements.txt +RUN apt-get update && apt-get install -y sox lame libboost-program-options-dev libsox-fmt-mp3 ADD . /code/ RUN adduser --disabled-password --gecos '' djworker diff --git a/bin/sox b/bin/sox deleted file mode 100755 index 438a437da5a0d064755b5e31983b267977ec3670..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68896 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 diff --git a/bin/wav2png b/bin/wav2png new file mode 100755 index 0000000000000000000000000000000000000000..3caced9c78e3258a3f83b3ddc45126d60700ef85 GIT binary patch literal 177848 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 Date: Wed, 29 Jul 2015 16:48:31 +0100 Subject: [PATCH 21/36] Fixed some waveform/cdn stuff --- api/views.py | 12 +++++++++--- core/utils/cdn.py | 14 ++++++++++++-- dss/settings.py | 1 + spa/models/mix.py | 10 ++++------ spa/tasks.py | 18 +++++++----------- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/api/views.py b/api/views.py index 53349de..63cdc3f 100755 --- a/api/views.py +++ b/api/views.py @@ -18,7 +18,7 @@ from rest_framework.status import HTTP_202_ACCEPTED, HTTP_401_UNAUTHORIZED, HTTP from api import serializers from dss import settings -from spa.tasks import create_waveform_task, archive_mix_task +from spa.tasks import create_waveform_task, upload_to_cdn_task from spa.models.genre import Genre from spa.models.activity import ActivityPlay from spa.models.mix import Mix @@ -179,12 +179,18 @@ class PartialMixUploadView(views.APIView): # Chain the waveform & archive tasks together # Probably not the best place for them but will do for now - # First argument to archive_mix_task is not specified as it is piped from create_waveform_task + # First argument to upload_to_cdn_task is not specified as it is piped from create_waveform_task logger.debug("Processing input_file: {0}".format(input_file)) logger.debug("Connecting to broker: {0}".format(settings.BROKER_URL)) + + from celery import group (create_waveform_task.s(input_file, uid) | - archive_mix_task.s(filetype='mp3', uid=uid)).delay() + group( + upload_to_cdn_task.s(filetype='mp3', uid=uid, container_name='mixes'), + upload_to_cdn_task.s(filetype='png', uid=uid, container_name='waveforms') + ) + ).delay() logger.debug("Waveform task started") except Exception, ex: diff --git a/core/utils/cdn.py b/core/utils/cdn.py index 09f5a80..4ea6d82 100755 --- a/core/utils/cdn.py +++ b/core/utils/cdn.py @@ -8,13 +8,13 @@ from libcloud.storage.types import Provider from libcloud.storage.providers import get_driver -def upload_to_azure(in_file, filetype, uid): +def upload_to_azure(in_file, filetype, uid, container_name=settings.AZURE_CONTAINER): if os.path.isfile(in_file): print "Uploading file for: %s" % in_file file_name = "%s.%s" % (uid, filetype) 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) + container = driver.get_container(container_name) with open(in_file, 'rb') as iterator: obj = driver.upload_object_via_stream( @@ -47,3 +47,13 @@ def set_azure_details(blob_name, download_name): print "No blob found for: %s" % download_name except Exception, ex: print "Error processing blob %s: %s" % (download_name, ex.message) + +def file_exists(url): + import httplib + from urlparse import urlparse + p = urlparse(url) + c = httplib.HTTPConnection(p.netloc) + c.request("HEAD", p.path) + + r = c.getresponse() + return r.status == 200 diff --git a/dss/settings.py b/dss/settings.py index 40e662e..6dc6b9d 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -107,6 +107,7 @@ INSTALLED_APPS = ( 'django_gravatar', 'corsheaders', 'sorl.thumbnail', + 'djcelery', 'spa', 'gunicorn', 'spa.signals', diff --git a/spa/models/mix.py b/spa/models/mix.py index 1ed76d6..5a70c03 100755 --- a/spa/models/mix.py +++ b/spa/models/mix.py @@ -1,8 +1,7 @@ -import os import rfc822 -import urlparse -from django.utils.encoding import smart_str +import os +from django.utils.encoding import smart_str from sorl.thumbnail import get_thumbnail from django.contrib.sites.models import Site from django.db import models @@ -19,8 +18,7 @@ from dss import settings, localsettings from spa.models.userprofile import UserProfile from spa.models.basemodel import BaseModel from core.utils.file import generate_save_file_name -from PIL import Image -import glob +from core.utils import cdn class Engine(Engine): @@ -104,7 +102,7 @@ class Mix(BaseModel): self.clean_image('mix_image', Mix) # Check for the unlikely event that the waveform has been generated - if os.path.isfile(self.get_waveform_path()): + if cdn.file_exists('{0}{1}.png'.format(localsettings.WAVEFORM_URL, self.uid)): self.waveform_generated = True try: self.duration = mp3_length(self.get_absolute_path()) diff --git a/spa/tasks.py b/spa/tasks.py index a823dc1..c2bfeaa 100755 --- a/spa/tasks.py +++ b/spa/tasks.py @@ -15,31 +15,27 @@ 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) + out_file = os.path.join(settings.CACHE_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 waveform_generated_signal.send(sender=None, uid=uid) - return new_file + return out_file else: print "Outfile is missing" -@task(time_limit=3600) -def archive_mix_task(in_file, filetype, uid): +@task(timse_limit=3600) +def upload_to_cdn_task(in_file, filetype, uid, container_name): + source_file = os.path.join(settings.CACHE_ROOT, '{0}/{1}.{2}'.format(container_name, uid, filetype)) print "Sending {0} to azure".format(uid) try: - upload_to_azure(in_file, filetype, uid) + upload_to_azure(source_file, filetype, uid, container_name) + return source_file except Exception, ex: print "Unable to upload: {0}".format(ex.message) - @task def update_geo_info_task(ip_address, profile_id): try: From 66a8195141c4c7d15fcddb1ec484679724606562 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Wed, 29 Jul 2015 17:11:22 +0100 Subject: [PATCH 22/36] Removed djcelery --- dss/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dss/settings.py b/dss/settings.py index 6dc6b9d..40e662e 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -107,7 +107,6 @@ INSTALLED_APPS = ( 'django_gravatar', 'corsheaders', 'sorl.thumbnail', - 'djcelery', 'spa', 'gunicorn', 'spa.signals', From 71fa592f4b75b3a5a799c19e268206b7e5c3181e Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Wed, 29 Jul 2015 21:49:53 +0100 Subject: [PATCH 23/36] Fixed some celery stuff --- Dockerfile | 2 ++ dss/celeryconf.py | 9 +++++++-- dss/settings.py | 1 + requirements.txt | 1 + run_celery.sh | 2 +- spa/tasks.py | 15 +++++++++------ 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9c07f99..9179e4b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,8 @@ RUN mkdir /files RUN mkdir /files/static RUN mkdir /files/media RUN mkdir /files/cache +RUN mkdir /files/cache/mixes +RUN mkdir /files/cache/waveforms RUN mkdir /files/tmp WORKDIR /code diff --git a/dss/celeryconf.py b/dss/celeryconf.py index 22abefa..dcd124d 100644 --- a/dss/celeryconf.py +++ b/dss/celeryconf.py @@ -1,17 +1,22 @@ from __future__ import absolute_import import os +import logging from celery import Celery +logger = logging.getLogger('dss') + # set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dss.settings') from django.conf import settings - +print 'Connecting to celery app' app = Celery('dss') +print 'Connected' # Using a string here means the worker will not have to # pickle the object when using Windows. app.config_from_object('django.conf:settings') -app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) \ No newline at end of file +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) +print 'Discovered tasks' diff --git a/dss/settings.py b/dss/settings.py index 40e662e..6dc6b9d 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -107,6 +107,7 @@ INSTALLED_APPS = ( 'django_gravatar', 'corsheaders', 'sorl.thumbnail', + 'djcelery', 'spa', 'gunicorn', 'spa.signals', diff --git a/requirements.txt b/requirements.txt index e3dd54e..68682e5 100755 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ django-dbbackup django-user-agents south redis +django-celery sorl-thumbnail diff --git a/run_celery.sh b/run_celery.sh index 4af3a17..fe41a9e 100755 --- a/run_celery.sh +++ b/run_celery.sh @@ -1,2 +1,2 @@ #!/bin/sh -su -m djworker -c "sleep 3 && celery worker -A dss.celeryconf -Q default" +su -m djworker -c "python manage.py celeryd" \ No newline at end of file diff --git a/spa/tasks.py b/spa/tasks.py index c2bfeaa..ad1fffb 100755 --- a/spa/tasks.py +++ b/spa/tasks.py @@ -1,6 +1,8 @@ import shutil from celery.task import task import os +import logging + from core.utils.cdn import upload_to_azure from spa.signals import waveform_generated_signal @@ -12,29 +14,30 @@ except ImportError: from core.utils.waveform import generate_waveform from dss import settings +logger = logging.getLogger('dss') @task(time_limit=3600) def create_waveform_task(in_file, uid): out_file = os.path.join(settings.CACHE_ROOT, 'waveforms/%s.png' % uid) - print "Creating waveform \n\tIn: %s\n\tOut: %s" % (in_file, out_file) + logger.info("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" + logger.info("Waveform generated successfully") waveform_generated_signal.send(sender=None, uid=uid) return out_file else: - print "Outfile is missing" + logger.error("Outfile is missing") @task(timse_limit=3600) def upload_to_cdn_task(in_file, filetype, uid, container_name): source_file = os.path.join(settings.CACHE_ROOT, '{0}/{1}.{2}'.format(container_name, uid, filetype)) - print "Sending {0} to azure".format(uid) + logger.info("Sending {0} to azure".format(uid)) try: upload_to_azure(source_file, filetype, uid, container_name) return source_file except Exception, ex: - print "Unable to upload: {0}".format(ex.message) + logger.error("Unable to upload: {0}".format(ex.message)) @task def update_geo_info_task(ip_address, profile_id): @@ -46,5 +49,5 @@ def update_geo_info_task(ip_address, profile_id): country = g.country(ip) print "Updated user location" except Exception, e: - print e.message + logger.exception(e) pass From c636c4afa251488aa77b15fdd84df71bd00a8b8a Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Thu, 30 Jul 2015 22:06:43 +0100 Subject: [PATCH 24/36] Updated some CDN urls --- core/utils/cdn.py | 16 ++++++++++++++++ dss/celeryconf.py | 3 --- spa/management/commands/tidy_cdn.py | 22 ++++++++++++++++++++++ spa/models/basemodel.py | 2 +- spa/models/mix.py | 11 +++-------- 5 files changed, 42 insertions(+), 12 deletions(-) create mode 100755 spa/management/commands/tidy_cdn.py diff --git a/core/utils/cdn.py b/core/utils/cdn.py index 4ea6d82..55da434 100755 --- a/core/utils/cdn.py +++ b/core/utils/cdn.py @@ -48,6 +48,7 @@ def set_azure_details(blob_name, download_name): except Exception, ex: print "Error processing blob %s: %s" % (download_name, ex.message) + def file_exists(url): import httplib from urlparse import urlparse @@ -57,3 +58,18 @@ def file_exists(url): r = c.getresponse() return r.status == 200 + + +def enumerate_objects(container): + blob_service = BlobService(AZURE_ACCOUNT_NAME, AZURE_ACCOUNT_KEY) + blobs = blob_service.list_blobs(container) + items = [] + for blob in blobs: + items.append(blob.name) + + return items + + +def delete_object(container, name): + blob_service = BlobService(AZURE_ACCOUNT_NAME, AZURE_ACCOUNT_KEY) + blob_service.delete_blob(container, name) \ No newline at end of file diff --git a/dss/celeryconf.py b/dss/celeryconf.py index dcd124d..198334e 100644 --- a/dss/celeryconf.py +++ b/dss/celeryconf.py @@ -11,12 +11,9 @@ logger = logging.getLogger('dss') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dss.settings') from django.conf import settings -print 'Connecting to celery app' app = Celery('dss') -print 'Connected' # Using a string here means the worker will not have to # pickle the object when using Windows. app.config_from_object('django.conf:settings') app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) -print 'Discovered tasks' diff --git a/spa/management/commands/tidy_cdn.py b/spa/management/commands/tidy_cdn.py new file mode 100755 index 0000000..0e54b5f --- /dev/null +++ b/spa/management/commands/tidy_cdn.py @@ -0,0 +1,22 @@ +import os +from django.core.management.base import NoArgsCommand +from core.utils import cdn +from spa.models import Mix + + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + print 'Enumerating items' + items = cdn.enumerate_objects('mixes') + for item in items: + # Check if we have a corresponding mix + uid, type = os.path.splitext(item) + try: + Mix.objects.get(uid=uid) + except Mix.DoesNotExist: + # no mix found - delete the blob + cdn.delete_object('mixes', item) + print "Deleting blob: {0}".format(uid) + except Exception, ex: + print "Debug exception: %s" % ex.message diff --git a/spa/models/basemodel.py b/spa/models/basemodel.py index 7f15512..dcf593e 100755 --- a/spa/models/basemodel.py +++ b/spa/models/basemodel.py @@ -10,7 +10,7 @@ from dss import localsettings, settings class BaseModel(models.Model): - logger = logging.getLogger(__name__) + logger = logging.getLogger('dss') object_created = models.DateTimeField(auto_now_add=True, default=datetime.now()) object_updated = models.DateTimeField(auto_now=True, default=datetime.now(), db_index=True) diff --git a/spa/models/mix.py b/spa/models/mix.py index 5a70c03..342f7a1 100755 --- a/spa/models/mix.py +++ b/spa/models/mix.py @@ -166,15 +166,10 @@ class Mix(BaseModel): def get_image_url(self, size='200x200', default=''): try: - if self.mix_image.name and self.mix_image.storage.exists(self.mix_image.name): - ret = get_thumbnail(self.mix_image, size, crop='center') - return "%s/%s" % (settings.MEDIA_URL, ret.name) - else: - return self.user.get_sized_avatar_image(170, 170) + return "{0}{1}".format(settings.MIXIMAGE_URL, os.path.basename(self.mix_image.name)) except Exception, ex: - pass - - return super(Mix, self).get_image_url(self.mix_image, settings.DEFAULT_TRACK_IMAGE) + self.logger.exception(ex) + return super(Mix, self).get_image_url(self.mix_image, settings.DEFAULT_TRACK_IMAGE) def get_stream_url(self): if self.archive_path in [None, '']: From 0b7d65e716a8840bdaf556ad4476b0079a28c4c6 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Thu, 30 Jul 2015 22:27:30 +0100 Subject: [PATCH 25/36] Fixed mix image url --- spa/models/mix.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spa/models/mix.py b/spa/models/mix.py index 342f7a1..59a3add 100755 --- a/spa/models/mix.py +++ b/spa/models/mix.py @@ -166,10 +166,13 @@ class Mix(BaseModel): def get_image_url(self, size='200x200', default=''): try: - return "{0}{1}".format(settings.MIXIMAGE_URL, os.path.basename(self.mix_image.name)) + filename = os.path.basename(self.mix_image.name) + if cdn.file_exists('{0}{1}'.format(localsettings.MIXIMAGE_URL, filename)): + return "{0}{1}".format(settings.MIXIMAGE_URL, filename) except Exception, ex: self.logger.exception(ex) - return super(Mix, self).get_image_url(self.mix_image, settings.DEFAULT_TRACK_IMAGE) + + return super(Mix, self).get_image_url(self.mix_image, settings.DEFAULT_TRACK_IMAGE) def get_stream_url(self): if self.archive_path in [None, '']: From fa9c6ba3d790809b53665c70a3d9ccffa49f8f0b Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Fri, 31 Jul 2015 22:53:15 +0100 Subject: [PATCH 26/36] Fixed media serving --- Dockerfile | 5 +++- api/views.py | 17 ++++++++------ core/utils/cdn.py | 34 ++++++++++++++------------- dss/settings.py | 4 ++-- spa/management/commands/azure_util.py | 6 ++--- spa/models/basemodel.py | 5 +--- spa/models/mix.py | 10 ++++---- spa/tasks.py | 7 ++++-- 8 files changed, 49 insertions(+), 39 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9179e4b..b5db2af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,9 +14,12 @@ RUN mkdir /files/tmp WORKDIR /code ADD requirements.txt /code/ RUN pip install -r requirements.txt -RUN apt-get update && apt-get install -y sox lame libboost-program-options-dev libsox-fmt-mp3 +RUN apt-get update && apt-get install -y sox lame vim \ + libboost-program-options-dev libsox-fmt-mp3 postgresql-client + ADD . /code/ RUN adduser --disabled-password --gecos '' djworker RUN chown djworker /files -R RUN chown djworker /srv/logs -R +RUN export PATH=$PATH:/mnt/bin/ \ No newline at end of file diff --git a/api/views.py b/api/views.py index 63cdc3f..81823a0 100755 --- a/api/views.py +++ b/api/views.py @@ -3,7 +3,7 @@ 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.core.files.storage import FileSystemStorage, default_storage from django.db.models import Count from django.http.response import HttpResponse from rest_framework import viewsets @@ -17,6 +17,7 @@ from rest_framework.status import HTTP_202_ACCEPTED, HTTP_401_UNAUTHORIZED, HTTP HTTP_200_OK, HTTP_204_NO_CONTENT from api import serializers +from core.utils import cdn from dss import settings from spa.tasks import create_waveform_task, upload_to_cdn_task from spa.models.genre import Genre @@ -120,10 +121,10 @@ class AttachedImageUploadView(views.APIView): parser_classes = (FileUploadParser,) def post(self, request): - if request.FILES['file'] is None or request.data.get('data') is None: + if request.data['file'] is None or request.data.get('data') is None: return Response(status=HTTP_400_BAD_REQUEST) - file_obj = request.FILES['file'] + file_obj = request.data['file'] file_hash = request.data.get('data') try: mix = Mix.objects.get(uid=file_hash) @@ -133,6 +134,8 @@ class AttachedImageUploadView(views.APIView): return Response(HTTP_202_ACCEPTED) except ObjectDoesNotExist: return Response(status=HTTP_404_NOT_FOUND) + except Exception, ex: + logger.exception(ex) return Response(status=HTTP_401_UNAUTHORIZED) @@ -186,10 +189,10 @@ class PartialMixUploadView(views.APIView): from celery import group (create_waveform_task.s(input_file, uid) | - group( - upload_to_cdn_task.s(filetype='mp3', uid=uid, container_name='mixes'), - upload_to_cdn_task.s(filetype='png', uid=uid, container_name='waveforms') - ) + group( + upload_to_cdn_task.s(filetype='mp3', uid=uid, container_name='mixes'), + upload_to_cdn_task.s(filetype='png', uid=uid, container_name='waveforms') + ) ).delay() logger.debug("Waveform task started") diff --git a/core/utils/cdn.py b/core/utils/cdn.py index 55da434..5852349 100755 --- a/core/utils/cdn.py +++ b/core/utils/cdn.py @@ -1,34 +1,36 @@ 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 +from dss import settings +from dss.storagesettings import AZURE_ACCOUNT_NAME, AZURE_ACCOUNT_KEY, AZURE_CONTAINER -def upload_to_azure(in_file, filetype, uid, container_name=settings.AZURE_CONTAINER): + +def upload_file_to_azure(in_file, file_name, container_name=settings.AZURE_CONTAINER): if os.path.isfile(in_file): print "Uploading file for: %s" % in_file - file_name = "%s.%s" % (uid, filetype) - cls = get_driver(Provider.AZURE_BLOBS) - driver = cls(settings.AZURE_ACCOUNT_NAME, settings.AZURE_ACCOUNT_KEY) - container = driver.get_container(container_name) - 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 upload_stream_to_azure(iterator, file_name, container_name=container_name) else: print "infile not found" return None +def upload_stream_to_azure(iterator, file_name, container_name=settings.AZURE_CONTAINER): + cls = get_driver(Provider.AZURE_BLOBS) + driver = cls(settings.AZURE_ACCOUNT_NAME, settings.AZURE_ACCOUNT_KEY) + container = driver.get_container(container_name) + obj = driver.upload_object_via_stream( + iterator=iterator, + container=container, + object_name=file_name + ) + return obj + + def set_azure_details(blob_name, download_name): try: blob_service = BlobService(AZURE_ACCOUNT_NAME, AZURE_ACCOUNT_KEY) diff --git a/dss/settings.py b/dss/settings.py index 6dc6b9d..f651b78 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -29,7 +29,6 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': DATABASE_NAME, - 'ADMINUSER': 'postgres', 'USER': DATABASE_USER, 'PASSWORD': DATABASE_PASSWORD, 'HOST': DATABASE_HOST, @@ -207,7 +206,8 @@ DEFAULT_USER_TITLE = 'Just another DSS lover' SITE_NAME = 'Deep South Sounds' THUMBNAIL_PREFIX = '_tn/' -THUMBNAIL_STORAGE = 'storages.backends.azure_storage.AzureStorage' + +# THUMBNAIL_STORAGE = 'storages.backends.azure_storage.AzureStorage' JWT_AUTH = { 'JWT_EXPIRATION_DELTA': timedelta(seconds=900), diff --git a/spa/management/commands/azure_util.py b/spa/management/commands/azure_util.py index d86f2b2..40a22ab 100755 --- a/spa/management/commands/azure_util.py +++ b/spa/management/commands/azure_util.py @@ -1,6 +1,6 @@ -from django.core.management.base import NoArgsCommand, BaseCommand +from django.core.management.base import BaseCommand -from core.utils.cdn import upload_to_azure +from core.utils import cdn from spa.models import Mix @@ -18,7 +18,7 @@ class Command(BaseCommand): mixes = Mix.objects.filter(archive_updated=False) for mix in mixes: blob_name, download_name = mix.get_cdn_details() - upload_to_azure(blob_name, "mp3", download_name) + cdn.upload_file_to_azure(blob_name, "mp3", download_name) mix.archive_updated = True mix.save() diff --git a/spa/models/basemodel.py b/spa/models/basemodel.py index dcf593e..0f9fc00 100755 --- a/spa/models/basemodel.py +++ b/spa/models/basemodel.py @@ -33,11 +33,8 @@ class BaseModel(models.Model): def get_image_url(self, image, default): try: if os.path.isfile(image.path): - images_root = localsettings.IMAGE_URL if hasattr(localsettings, - 'IMAGE_URL') else "%s" % settings.MEDIA_URL - ret = "%s/%s/%s" % (settings.STATIC_URL, images_root, image) + ret = "{0}/{1}/{2}".format(settings.STATIC_URL, settings.MEDIA_URL, image) return url.urlclean(ret) - except Exception, ex: pass diff --git a/spa/models/mix.py b/spa/models/mix.py index 59a3add..f5a6201 100755 --- a/spa/models/mix.py +++ b/spa/models/mix.py @@ -166,11 +166,13 @@ class Mix(BaseModel): def get_image_url(self, size='200x200', default=''): try: - filename = os.path.basename(self.mix_image.name) - if cdn.file_exists('{0}{1}'.format(localsettings.MIXIMAGE_URL, filename)): - return "{0}{1}".format(settings.MIXIMAGE_URL, filename) + if self.mix_image.name and self.mix_image.storage.exists(self.mix_image.name): + ret = get_thumbnail(self.mix_image, size, crop='center') + return url.urlclean("%s/%s" % (settings.MEDIA_URL, ret.name)) + else: + return self.user.get_sized_avatar_image(170, 170) except Exception, ex: - self.logger.exception(ex) + pass return super(Mix, self).get_image_url(self.mix_image, settings.DEFAULT_TRACK_IMAGE) diff --git a/spa/tasks.py b/spa/tasks.py index ad1fffb..633aed0 100755 --- a/spa/tasks.py +++ b/spa/tasks.py @@ -3,7 +3,7 @@ from celery.task import task import os import logging -from core.utils.cdn import upload_to_azure +from core.utils import cdn from spa.signals import waveform_generated_signal try: @@ -16,6 +16,7 @@ from dss import settings logger = logging.getLogger('dss') + @task(time_limit=3600) def create_waveform_task(in_file, uid): out_file = os.path.join(settings.CACHE_ROOT, 'waveforms/%s.png' % uid) @@ -34,11 +35,13 @@ def upload_to_cdn_task(in_file, filetype, uid, container_name): source_file = os.path.join(settings.CACHE_ROOT, '{0}/{1}.{2}'.format(container_name, uid, filetype)) logger.info("Sending {0} to azure".format(uid)) try: - upload_to_azure(source_file, filetype, uid, container_name) + file_name = "{0}.{1}".format(uid, filetype) + cdn.upload_file_to_azure(source_file, file_name, container_name) return source_file except Exception, ex: logger.error("Unable to upload: {0}".format(ex.message)) + @task def update_geo_info_task(ip_address, profile_id): try: From 058e01cbe1d3e92275ce4959457ff0c46c053002 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Fri, 31 Jul 2015 22:59:48 +0100 Subject: [PATCH 27/36] Removed unused imports --- spa/tasks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/spa/tasks.py b/spa/tasks.py index 633aed0..0eceea3 100755 --- a/spa/tasks.py +++ b/spa/tasks.py @@ -1,4 +1,3 @@ -import shutil from celery.task import task import os import logging From 4b181d848241d84270a0cdbaf2f7ec1dbf53df4f Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Fri, 31 Jul 2015 23:32:10 +0100 Subject: [PATCH 28/36] Fixed basemodel static url --- spa/models/basemodel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spa/models/basemodel.py b/spa/models/basemodel.py index 0f9fc00..779b744 100755 --- a/spa/models/basemodel.py +++ b/spa/models/basemodel.py @@ -33,7 +33,7 @@ class BaseModel(models.Model): def get_image_url(self, image, default): try: if os.path.isfile(image.path): - ret = "{0}/{1}/{2}".format(settings.STATIC_URL, settings.MEDIA_URL, image) + ret = "{0}/{1}".format(settings.MEDIA_URL, image) return url.urlclean(ret) except Exception, ex: pass From baa2d87ec099216674d420387e50f3416a4bfe1d Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Sun, 2 Aug 2015 15:37:19 +0100 Subject: [PATCH 29/36] Added path as parameter to waveform_generated_signal --- spa/signals.py | 3 ++- spa/tasks.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spa/signals.py b/spa/signals.py index 5c65bba..4030307 100755 --- a/spa/signals.py +++ b/spa/signals.py @@ -18,11 +18,12 @@ def _waveform_generated_callback(sender, **kwargs): print "Updating model with waveform" try: uid = kwargs['uid'] + path = kwargs['uid'] if uid is not None: mix = Mix.objects.get(uid=uid) if mix is not None: mix.waveform_generated = True - mix.duration = mp3_length(mix.get_absolute_path()) + mix.duration = mp3_length(path) mix.save(update_fields=["waveform_generated", "duration"]) except ObjectDoesNotExist: diff --git a/spa/tasks.py b/spa/tasks.py index 0eceea3..c921c5a 100755 --- a/spa/tasks.py +++ b/spa/tasks.py @@ -23,7 +23,7 @@ def create_waveform_task(in_file, uid): generate_waveform(in_file, out_file) if os.path.isfile(out_file): logger.info("Waveform generated successfully") - waveform_generated_signal.send(sender=None, uid=uid) + waveform_generated_signal.send(sender=None, uid=uid, path=in_file) return out_file else: logger.error("Outfile is missing") From bacb9b5e2e6ca91a7d7e04df13aabfaae1799361 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Sun, 2 Aug 2015 15:56:50 +0100 Subject: [PATCH 30/36] Fixed type in kwargs extraction --- spa/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spa/signals.py b/spa/signals.py index 4030307..cb131ba 100755 --- a/spa/signals.py +++ b/spa/signals.py @@ -18,7 +18,7 @@ def _waveform_generated_callback(sender, **kwargs): print "Updating model with waveform" try: uid = kwargs['uid'] - path = kwargs['uid'] + path = kwargs['path'] if uid is not None: mix = Mix.objects.get(uid=uid) if mix is not None: From 967f0d0d2a2600f73346593459dba86ac01d2605 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Mon, 3 Aug 2015 20:53:07 +0100 Subject: [PATCH 31/36] Made avatar images local again --- spa/management/commands/get_avatars.py | 17 +++++++++++++++-- spa/models/userprofile.py | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/spa/management/commands/get_avatars.py b/spa/management/commands/get_avatars.py index eccda8d..66ed4e9 100755 --- a/spa/management/commands/get_avatars.py +++ b/spa/management/commands/get_avatars.py @@ -1,14 +1,26 @@ +import urllib2 + from allauth.socialaccount.models import SocialAccount from azure.storage import BlobService -from django.core.files.base import ContentFile +from django.core.files.base import File +from django.core.files.temp import NamedTemporaryFile from django.core.management.base import NoArgsCommand from requests import request, ConnectionError -from dss import storagesettings +from dss import storagesettings from spa.models.userprofile import UserProfile def save_image(profile, url): + + img = NamedTemporaryFile(delete=True) + img.write(urllib2.urlopen(url).read()) + + img.flush() + profile.avatar_image.save(str(profile.id), File(img)) + + +def save_image_to_azure(profile, url): try: response = request('GET', url) response.raise_for_status() @@ -40,6 +52,7 @@ class Command(NoArgsCommand): if provider_account: avatar_url = provider_account.get_avatar_url() save_image(user, avatar_url) + user.save() except Exception, ex: print ex.message else: diff --git a/spa/models/userprofile.py b/spa/models/userprofile.py index 5898e01..f3f2aa5 100755 --- a/spa/models/userprofile.py +++ b/spa/models/userprofile.py @@ -168,7 +168,6 @@ class UserProfile(BaseModel): return self.display_name or self.first_name + ' ' + self.last_name def get_sized_avatar_image(self, width, height): - return self.get_avatar_image() try: image = self.get_avatar_image() sized = thumbnail.get_thumbnail(image, "%sx%s" % (width, height), crop="center") @@ -179,7 +178,18 @@ class UserProfile(BaseModel): return UserProfile.get_default_avatar_image() def get_avatar_image(self): - return (settings.CDN_URL + 'avatars/{0}').format(self.id) + avatar_type = self.avatar_type + if avatar_type == 'gravatar': + gravatar_exists = has_gravatar(self.email) + if gravatar_exists: + return get_gravatar_url(self.email) + else: + if os.path.exists(self.avatar_image.file.name): + return self.avatar_image + else: + return self.get_default_avatar_image() + + return UserProfile.get_default_avatar_image() def get_profile_url(self): return '/user/%s' % (self.slug) From cb1a08ce5e273c2391848b39cbfcd50a0933845f Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Mon, 3 Aug 2015 22:27:53 +0100 Subject: [PATCH 32/36] Fixed api url --- core/utils/url.py | 16 ++++++++++------ dss/urls.py | 3 ++- spa/management/commands/azure_util.py | 26 +++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/core/utils/url.py b/core/utils/url.py index 3583a9f..4c59ab5 100755 --- a/core/utils/url.py +++ b/core/utils/url.py @@ -5,19 +5,21 @@ 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), '') + 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 + # remove double slashes ret = urlparse.urljoin(url, urlparse.urlparse(url).path.replace('//', '/')) return ret @@ -90,11 +92,13 @@ def _slug_strip(value, 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 + return url diff --git a/dss/urls.py b/dss/urls.py index 4625528..99d9603 100755 --- a/dss/urls.py +++ b/dss/urls.py @@ -9,10 +9,11 @@ admin.autodiscover() urlpatterns = patterns( '', url(r'^admin/', include(admin.site.urls)), - url(r'^api/v2/', include('api.urls')), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), (r'^grappelli/', include('grappelli.urls')), (r'^social/', include('spa.social.urls')), + (r'^arges/', include('spa.social.urls')), + url(r'^', include('api.urls')), ) if settings.DEBUG: diff --git a/spa/management/commands/azure_util.py b/spa/management/commands/azure_util.py index 40a22ab..aa66717 100755 --- a/spa/management/commands/azure_util.py +++ b/spa/management/commands/azure_util.py @@ -1,9 +1,28 @@ +import os from django.core.management.base import BaseCommand from core.utils import cdn from spa.models import Mix +def _check_missing_mixes(): + ms = Mix.objects.all() + found = 0 + for m in ms: + url = m.get_download_url() + if not cdn.file_exists(url): + file = '/mnt/dev/deepsouthsounds.com/media/mixes/{0}.mp3'.format(m.uid) + if os.path.isfile(file): + print '* {0}'.format(file) + cdn.upload_file_to_azure(file, '{0}.mp3'.format(m.uid), 'mixes') + found += 1 + else: + found += 1 + print '({0}){1} - {2}'.format(found, m.slug, m.uid) + + print '{0} of {1} missing'.format(found, Mix.objects.count()) + + class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( @@ -26,4 +45,9 @@ class Command(BaseCommand): print "Fatal error, bailing. {0}".format(ex.message) def handle(self, *args, **options): - pass + if len(args) == 0: + print "Commands are \n\t_check_missing_mixes" + elif args[0] == 'check_missing_mix': + _check_missing_mixes() + else: + print "Commands are \n\tcheck_missing_mix" From 9766ba2c4c1332f9fb34e3e7bdffd2345b013f38 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Fri, 7 Aug 2015 11:32:56 +0100 Subject: [PATCH 33/36] Altered content management command --- api/auth.py | 5 +++-- api/urls.py | 10 ++++++++-- core/realtime/activity.py | 3 ++- core/utils/cdn.py | 6 +++--- dss/settings.py | 6 +++--- dss/urls.py | 1 + spa/management/commands/azure_util.py | 11 +++++++++-- spa/models/userprofile.py | 3 +++ 8 files changed, 32 insertions(+), 13 deletions(-) diff --git a/api/auth.py b/api/auth.py index 0403173..df3d5b7 100644 --- a/api/auth.py +++ b/api/auth.py @@ -26,7 +26,7 @@ def auth_by_token(request, backend): return user if user else None -class FacebookView(APIView): +class SocialLoginHandler(APIView): permission_classes = (permissions.AllowAny,) def post(self, request, format=None): @@ -57,7 +57,8 @@ class FacebookView(APIView): ) response_data = { - 'token': jwt_encode_handler(payload) + 'token': jwt_encode_handler(payload), + 'session': user.userprofile.get_session_id() } return Response(response_data) diff --git a/api/urls.py b/api/urls.py index edd15f8..75701bf 100755 --- a/api/urls.py +++ b/api/urls.py @@ -6,9 +6,10 @@ from rest_framework.views import APIView from rest_framework_jwt.authentication import JSONWebTokenAuthentication from api import views, auth, helpers -from api.auth import FacebookView +from api.auth import SocialLoginHandler from rest_framework.views import status from rest_framework.response import Response +from core.realtime import activity router = DefaultRouter() # trailing_slash=True) @@ -27,6 +28,11 @@ class DebugView(APIView): authentication_classes = (JSONWebTokenAuthentication, ) def post(self, request, format=None): + try: + activity.post_activity('user:message', request.user.userprofile.get_session_id(), 'Hello Sailor') + except Exception, ex: + print ex.message + return Response({ 'status': request.user.first_name, 'message': 'Sailor' @@ -43,7 +49,7 @@ urlpatterns = patterns( url(r'_search/$', views.SearchResultsView.as_view()), url(r'^', include(router.urls)), - url(r'^_login/', FacebookView.as_view()), + url(r'^_login/', SocialLoginHandler.as_view()), url(r'^token-refresh/', 'rest_framework_jwt.views.refresh_jwt_token'), # url(r'^_tr/', RefreshToken.as_view()), diff --git a/core/realtime/activity.py b/core/realtime/activity.py index 1ed2ef0..3af0813 100755 --- a/core/realtime/activity.py +++ b/core/realtime/activity.py @@ -1,9 +1,10 @@ import redis import json +from dss import settings def post_activity(channel, session, message): - r = redis.StrictRedis(host='localhost', port=6379, db=0) + r = redis.StrictRedis(host=settings.REDIS_HOST, port=6379, db=0) response = r.publish(channel, json.dumps({'session': session, 'message': message})) print "Message sent: {0}".format(response) diff --git a/core/utils/cdn.py b/core/utils/cdn.py index 5852349..5e51b1b 100755 --- a/core/utils/cdn.py +++ b/core/utils/cdn.py @@ -31,13 +31,13 @@ def upload_stream_to_azure(iterator, file_name, container_name=settings.AZURE_CO return obj -def set_azure_details(blob_name, download_name): +def set_azure_details(blob_name, download_name, container_name=AZURE_CONTAINER): try: blob_service = BlobService(AZURE_ACCOUNT_NAME, AZURE_ACCOUNT_KEY) - blob = blob_service.get_blob(AZURE_CONTAINER, blob_name) + blob = blob_service.get_blob(container_name, blob_name) if blob: blob_service.set_blob_properties( - AZURE_CONTAINER, + container_name, blob_name, x_ms_blob_content_type='application/octet-stream', x_ms_blob_content_disposition='attachment;filename="{0}"'.format(download_name) diff --git a/dss/settings.py b/dss/settings.py index f651b78..3caa6ea 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -74,7 +74,7 @@ TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + ( MIDDLEWARE_CLASSES = ( 'django.middleware.gzip.GZipMiddleware', 'django.middleware.common.CommonMiddleware', - 'user_sessions.middleware.SessionMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', @@ -96,7 +96,7 @@ INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', - 'user_sessions', + 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', @@ -149,7 +149,7 @@ TASTYPIE_ALLOW_MISSING_SLASH = True SENDFILE_ROOT = os.path.join(MEDIA_ROOT, 'mixes') SENDFILE_URL = '/media/mixes' -SESSION_ENGINE = 'user_sessions.backends.db' +#SESSION_ENGINE = 'user_sessions.backends.db' mimetypes.add_type("text/xml", ".plist", False) diff --git a/dss/urls.py b/dss/urls.py index 99d9603..78fdc2f 100755 --- a/dss/urls.py +++ b/dss/urls.py @@ -13,6 +13,7 @@ urlpatterns = patterns( (r'^grappelli/', include('grappelli.urls')), (r'^social/', include('spa.social.urls')), (r'^arges/', include('spa.social.urls')), + url(r'', include('user_sessions.urls', 'user_sessions')), url(r'^', include('api.urls')), ) diff --git a/spa/management/commands/azure_util.py b/spa/management/commands/azure_util.py index aa66717..c310264 100755 --- a/spa/management/commands/azure_util.py +++ b/spa/management/commands/azure_util.py @@ -5,16 +5,21 @@ from core.utils import cdn from spa.models import Mix +def _update_azure_headers(): + ms = Mix.objects.all() + for m in ms: + cdn.set_azure_details('{0}.mp3'.format(m.uid), 'Deep South Sounds - {0}'.format(m.title), 'mixes') + def _check_missing_mixes(): ms = Mix.objects.all() found = 0 for m in ms: url = m.get_download_url() if not cdn.file_exists(url): - file = '/mnt/dev/deepsouthsounds.com/media/mixes/{0}.mp3'.format(m.uid) + file = '/mnt/dev/working/Dropbox/Development/deepsouthsounds.com/media/mixes/{0}.mp3'.format(m.uid) if os.path.isfile(file): print '* {0}'.format(file) - cdn.upload_file_to_azure(file, '{0}.mp3'.format(m.uid), 'mixes') + #cdn.upload_file_to_azure(file, '{0}.mp3'.format(m.uid), 'mixes') found += 1 else: found += 1 @@ -49,5 +54,7 @@ class Command(BaseCommand): print "Commands are \n\t_check_missing_mixes" elif args[0] == 'check_missing_mix': _check_missing_mixes() + elif args[0] == 'update_azure_headers': + _update_azure_headers() else: print "Commands are \n\tcheck_missing_mix" diff --git a/spa/models/userprofile.py b/spa/models/userprofile.py index f3f2aa5..79b9d14 100755 --- a/spa/models/userprofile.py +++ b/spa/models/userprofile.py @@ -139,6 +139,9 @@ class UserProfile(BaseModel): except Exception, e: self.logger.error("Unable to create profile slug: %s", e.message) + def get_session_id(self): + return str(self.id) + def toggle_favourite(self, mix, value): try: if value: From 275ca511e2fb1e9bd3871217fc9c0903f972f0b4 Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Mon, 10 Aug 2015 21:25:13 +0100 Subject: [PATCH 34/36] Fixed waveform flow and socket.io push --- api/views.py | 19 +++++++++---------- spa/signals.py | 1 + spa/tasks.py | 9 ++++++++- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/api/views.py b/api/views.py index 81823a0..ec9a7a4 100755 --- a/api/views.py +++ b/api/views.py @@ -3,7 +3,7 @@ import os from django.core.exceptions import PermissionDenied, ObjectDoesNotExist from django.core.files.base import ContentFile -from django.core.files.storage import FileSystemStorage, default_storage +from django.core.files.storage import FileSystemStorage from django.db.models import Count from django.http.response import HttpResponse from rest_framework import viewsets @@ -17,9 +17,8 @@ from rest_framework.status import HTTP_202_ACCEPTED, HTTP_401_UNAUTHORIZED, HTTP HTTP_200_OK, HTTP_204_NO_CONTENT from api import serializers -from core.utils import cdn from dss import settings -from spa.tasks import create_waveform_task, upload_to_cdn_task +from spa import tasks from spa.models.genre import Genre from spa.models.activity import ActivityPlay from spa.models.mix import Mix @@ -187,13 +186,13 @@ class PartialMixUploadView(views.APIView): logger.debug("Processing input_file: {0}".format(input_file)) logger.debug("Connecting to broker: {0}".format(settings.BROKER_URL)) - from celery import group - (create_waveform_task.s(input_file, uid) | - group( - upload_to_cdn_task.s(filetype='mp3', uid=uid, container_name='mixes'), - upload_to_cdn_task.s(filetype='png', uid=uid, container_name='waveforms') - ) - ).delay() + from celery import group, chain + ( + tasks.create_waveform_task.s(input_file, uid) | + tasks.upload_to_cdn_task.subtask(('mp3', uid, 'mixes'), immutable=True) | + tasks.upload_to_cdn_task.subtask(('png', uid, 'waveforms'), immutable=True) | + tasks.notify_subscriber.subtask((request.user.userprofile.get_session_id(), uid), immutable=True) + ).delay() logger.debug("Waveform task started") except Exception, ex: diff --git a/spa/signals.py b/spa/signals.py index cb131ba..1390a55 100755 --- a/spa/signals.py +++ b/spa/signals.py @@ -4,6 +4,7 @@ from django.db.models.signals import post_save, pre_save, m2m_changed from django.dispatch import Signal, receiver from django.contrib.auth.models import User +from core.realtime import activity from core.utils.audio.mp3 import mp3_length from spa.models.activity import ActivityFollow diff --git a/spa/tasks.py b/spa/tasks.py index c921c5a..10dbd05 100755 --- a/spa/tasks.py +++ b/spa/tasks.py @@ -1,6 +1,7 @@ from celery.task import task import os import logging +from core.realtime import activity from core.utils import cdn from spa.signals import waveform_generated_signal @@ -30,7 +31,7 @@ def create_waveform_task(in_file, uid): @task(timse_limit=3600) -def upload_to_cdn_task(in_file, filetype, uid, container_name): +def upload_to_cdn_task(filetype, uid, container_name): source_file = os.path.join(settings.CACHE_ROOT, '{0}/{1}.{2}'.format(container_name, uid, filetype)) logger.info("Sending {0} to azure".format(uid)) try: @@ -53,3 +54,9 @@ def update_geo_info_task(ip_address, profile_id): except Exception, e: logger.exception(e) pass + + +@task +def notify_subscriber(session_id, uid): + if session_id is not None: + activity.post_activity('user:message', session_id, {'type': 'waveform', 'target': uid}) From a8292b621c1df24515ab224321c8f3b4678cdf0f Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Wed, 12 Aug 2015 20:41:16 +0100 Subject: [PATCH 35/36] Cleaned up notifications --- api/serializers.py | 12 +++++++----- api/views.py | 5 ++++- spa/models/notification.py | 6 ++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index e94b610..3ad5b36 100755 --- a/api/serializers.py +++ b/api/serializers.py @@ -449,9 +449,11 @@ class ActivitySerializer(serializers.HyperlinkedModelSerializer): 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') + from_user = InlineUserProfileSerializer(source='get_from_user', read_only=True) + notification_url = serializers.ReadOnlyField() + verb = serializers.ReadOnlyField() + target = serializers.ReadOnlyField() + date = serializers.ReadOnlyField() class Meta: model = Notification @@ -459,10 +461,10 @@ class NotificationSerializer(serializers.HyperlinkedModelSerializer): 'id', 'notification_url', 'from_user', - 'display_name', - 'avatar_image', 'verb', 'target', + 'date', + 'accepted_date', ) def get_display_name(self, obj): diff --git a/api/views.py b/api/views.py index ec9a7a4..01b8038 100755 --- a/api/views.py +++ b/api/views.py @@ -253,7 +253,10 @@ class NotificationViewSet(viewsets.ModelViewSet): if not user.is_authenticated(): raise PermissionDenied("Not allowed") - return Notification.objects.filter(to_user=user).order_by('-date')[0:5] + return Notification.objects.filter(to_user=user).order_by('-date') + + def perform_update(self, serializer): + return super(NotificationViewSet, self).perform_update(serializer) class GenreViewSet(viewsets.ModelViewSet): diff --git a/spa/models/notification.py b/spa/models/notification.py index cb7c71d..e29d9d8 100755 --- a/spa/models/notification.py +++ b/spa/models/notification.py @@ -29,8 +29,7 @@ class Notification(BaseModel): def get_notification_url(self): return '/api/v1/notification/%s' % self.id - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): + def __save(self, force_insert=False, force_update=False, using=None, update_fields=None): if self._activity.should_send_email(): self.send_notification_email() @@ -81,3 +80,6 @@ class Notification(BaseModel): except mandrill.Error, e: # Mandrill errors are thrown as exceptions print 'A mandrill error occurred: %s - %s' % (e.__class__, e) + + def get_from_user(self): + return UserProfile.get_user(self.from_user) From 1b76a0817063d415d08e01c118d22fd290f8e65f Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Wed, 12 Aug 2015 23:12:43 +0100 Subject: [PATCH 36/36] Fixed migrations --- api/serializers.py | 2 + .../commands/create_notifications.py | 15 + ...tification_text__del_field_notification.py | 305 ++++++++++++++++++ ...uto__add_field_notification_target_desc.py | 274 ++++++++++++++++ spa/models/activity.py | 27 +- spa/models/notification.py | 6 +- 6 files changed, 607 insertions(+), 22 deletions(-) create mode 100755 spa/management/commands/create_notifications.py create mode 100644 spa/migrations/0071_auto__del_field_notification_notification_text__del_field_notification.py create mode 100644 spa/migrations/0072_auto__add_field_notification_target_desc.py diff --git a/api/serializers.py b/api/serializers.py index 3ad5b36..b51fc02 100755 --- a/api/serializers.py +++ b/api/serializers.py @@ -463,6 +463,8 @@ class NotificationSerializer(serializers.HyperlinkedModelSerializer): 'from_user', 'verb', 'target', + 'target_desc', + 'type', 'date', 'accepted_date', ) diff --git a/spa/management/commands/create_notifications.py b/spa/management/commands/create_notifications.py new file mode 100755 index 0000000..9584e90 --- /dev/null +++ b/spa/management/commands/create_notifications.py @@ -0,0 +1,15 @@ +from django.core.management.base import NoArgsCommand +from spa import models + + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + models.Notification.objects.all().delete() + act = models.Activity.objects.all().order_by('-id').select_subclasses() + for a in act: + print "Creating for: {0}".format(a) + a.create_notification(accept=True) + + except Exception, ex: + print "Debug exception: %s" % ex.message \ No newline at end of file diff --git a/spa/migrations/0071_auto__del_field_notification_notification_text__del_field_notification.py b/spa/migrations/0071_auto__del_field_notification_notification_text__del_field_notification.py new file mode 100644 index 0000000..7ecbaa3 --- /dev/null +++ b/spa/migrations/0071_auto__del_field_notification_notification_text__del_field_notification.py @@ -0,0 +1,305 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Deleting field 'Notification.notification_text' + db.delete_column(u'spa_notification', 'notification_text') + + # Deleting field 'Notification.notification_html' + db.delete_column(u'spa_notification', 'notification_html') + + # Deleting field 'Notification.notification_url' + db.delete_column(u'spa_notification', 'notification_url') + + # Adding field 'Notification.type' + db.add_column(u'spa_notification', 'type', + self.gf('django.db.models.fields.CharField')(max_length=200, null=True), + keep_default=False) + + + def backwards(self, orm): + + # User chose to not deal with backwards NULL issues for 'Notification.notification_text' + raise RuntimeError("Cannot reverse this migration. 'Notification.notification_text' and its values cannot be restored.") + + # The following code is provided here to aid in writing a correct migration # Adding field 'Notification.notification_text' + db.add_column(u'spa_notification', 'notification_text', + self.gf('django.db.models.fields.CharField')(max_length=1024), + keep_default=False) + + + # User chose to not deal with backwards NULL issues for 'Notification.notification_html' + raise RuntimeError("Cannot reverse this migration. 'Notification.notification_html' and its values cannot be restored.") + + # The following code is provided here to aid in writing a correct migration # Adding field 'Notification.notification_html' + db.add_column(u'spa_notification', 'notification_html', + self.gf('django.db.models.fields.CharField')(max_length=1024), + keep_default=False) + + # Adding field 'Notification.notification_url' + db.add_column(u'spa_notification', 'notification_url', + self.gf('django.db.models.fields.URLField')(max_length=200, null=True), + keep_default=False) + + # Deleting field 'Notification.type' + db.delete_column(u'spa_notification', 'type') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'spa._lookup': { + 'Meta': {'object_name': '_Lookup'}, + 'description': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}) + }, + 'spa.activity': { + 'Meta': {'object_name': 'Activity'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['spa.UserProfile']", 'null': 'True', 'blank': 'True'}) + }, + 'spa.activitycomment': { + 'Meta': {'object_name': 'ActivityComment', '_ormbases': ['spa.Activity']}, + u'activity_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa.Activity']", 'unique': 'True', 'primary_key': 'True'}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_comments'", 'to': "orm['spa.Mix']"}) + }, + 'spa.activitydownload': { + 'Meta': {'object_name': 'ActivityDownload', '_ormbases': ['spa.Activity']}, + u'activity_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa.Activity']", 'unique': 'True', 'primary_key': 'True'}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_downloads'", 'to': "orm['spa.Mix']"}) + }, + 'spa.activityfavourite': { + 'Meta': {'object_name': 'ActivityFavourite', '_ormbases': ['spa.Activity']}, + u'activity_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa.Activity']", 'unique': 'True', 'primary_key': 'True'}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_favourites'", 'to': "orm['spa.Mix']"}) + }, + 'spa.activityfollow': { + 'Meta': {'object_name': 'ActivityFollow', '_ormbases': ['spa.Activity']}, + u'activity_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa.Activity']", 'unique': 'True', 'primary_key': 'True'}), + 'to_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_follow'", 'to': "orm['spa.UserProfile']"}) + }, + 'spa.activitylike': { + 'Meta': {'object_name': 'ActivityLike', '_ormbases': ['spa.Activity']}, + u'activity_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa.Activity']", 'unique': 'True', 'primary_key': 'True'}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_likes'", 'to': "orm['spa.Mix']"}) + }, + 'spa.activityplay': { + 'Meta': {'object_name': 'ActivityPlay', '_ormbases': ['spa.Activity']}, + u'activity_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa.Activity']", 'unique': 'True', 'primary_key': 'True'}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_plays'", 'to': "orm['spa.Mix']"}) + }, + 'spa.chatmessage': { + 'Meta': {'object_name': 'ChatMessage'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'chat_messages'", 'null': 'True', 'to': "orm['spa.UserProfile']"}) + }, + 'spa.comment': { + 'Meta': {'object_name': 'Comment'}, + 'comment': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'likes': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'liked_comments'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['spa.UserProfile']"}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['spa.Mix']"}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'time_index': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + 'spa.genre': { + 'Meta': {'object_name': 'Genre'}, + 'description': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}) + }, + 'spa.label': { + 'Meta': {'object_name': 'Label'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}) + }, + 'spa.mix': { + 'Meta': {'ordering': "('-id',)", 'object_name': 'Mix'}, + 'archive_path': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'null': 'True', 'blank': 'True'}), + 'archive_updated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'download_allowed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'duration': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'favourites': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'favourites'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['spa.UserProfile']"}), + 'filetype': ('django.db.models.fields.CharField', [], {'default': "'mp3'", 'max_length': '10'}), + 'genres': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['spa.Genre']", 'symmetrical': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_featured': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'likes': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'likes'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['spa.UserProfile']"}), + 'mix_image': ('django.db.models.fields.files.ImageField', [], {'max_length': '1024', 'blank': 'True'}), + 'mp3tags_updated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'uid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '38', 'blank': 'True'}), + 'upload_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'mixes'", 'to': "orm['spa.UserProfile']"}), + 'waveform_generated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'waveform_version': ('django.db.models.fields.IntegerField', [], {'default': '1'}) + }, + 'spa.notification': { + 'Meta': {'ordering': "('-id',)", 'object_name': 'Notification'}, + 'accepted_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'from_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'notifications'", 'null': 'True', 'to': "orm['spa.UserProfile']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'target': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}), + 'to_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'to_notications'", 'to': "orm['spa.UserProfile']"}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}), + 'verb': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}) + }, + 'spa.playlist': { + 'Meta': {'object_name': 'Playlist'}, + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mixes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['spa.Mix']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'playlists'", 'to': "orm['spa.UserProfile']"}) + }, + 'spa.purchaselink': { + 'Meta': {'object_name': 'PurchaseLink'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'track': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'purchase_link'", 'to': "orm['spa.Tracklist']"}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + }, + 'spa.recurrence': { + 'Meta': {'object_name': 'Recurrence', '_ormbases': ['spa._Lookup']}, + u'_lookup_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa._Lookup']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'spa.release': { + 'Meta': {'object_name': 'Release'}, + 'embed_code': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'release_artist': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'release_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)'}), + 'release_description': ('django.db.models.fields.TextField', [], {}), + 'release_image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'blank': 'True'}), + 'release_label': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['spa.Label']"}), + 'release_title': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['spa.UserProfile']"}) + }, + 'spa.releaseaudio': { + 'Meta': {'object_name': 'ReleaseAudio'}, + 'description': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'release_audio'", 'null': 'True', 'to': "orm['spa.Release']"}) + }, + 'spa.tracklist': { + 'Meta': {'object_name': 'Tracklist'}, + 'artist': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.SmallIntegerField', [], {}), + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tracklist'", 'to': "orm['spa.Mix']"}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'remixer': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'timeindex': ('django.db.models.fields.TimeField', [], {'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'spa.userprofile': { + 'Meta': {'object_name': 'UserProfile'}, + 'activity_sharing_facebook': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'activity_sharing_networks': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'activity_sharing_twitter': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'avatar_image': ('django.db.models.fields.files.ImageField', [], {'max_length': '1024', 'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'social'", 'max_length': '15'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}), + 'display_name': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}), + 'email_notifications': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'following': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'followers'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['spa.UserProfile']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_known_session': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'default': 'None', 'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'userprofile'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + 'spa.venue': { + 'Meta': {'object_name': 'Venue'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + 'venue_address': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'venue_image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'blank': 'True'}), + 'venue_name': ('django.db.models.fields.CharField', [], {'max_length': '250'}) + } + } + + complete_apps = ['spa'] \ No newline at end of file diff --git a/spa/migrations/0072_auto__add_field_notification_target_desc.py b/spa/migrations/0072_auto__add_field_notification_target_desc.py new file mode 100644 index 0000000..6dbd40d --- /dev/null +++ b/spa/migrations/0072_auto__add_field_notification_target_desc.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Notification.target_desc' + db.add_column(u'spa_notification', 'target_desc', + self.gf('django.db.models.fields.CharField')(max_length=200, null=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Notification.target_desc' + db.delete_column(u'spa_notification', 'target_desc') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'spa._lookup': { + 'Meta': {'object_name': '_Lookup'}, + 'description': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}) + }, + 'spa.activity': { + 'Meta': {'object_name': 'Activity'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['spa.UserProfile']", 'null': 'True', 'blank': 'True'}) + }, + 'spa.activitycomment': { + 'Meta': {'object_name': 'ActivityComment', '_ormbases': ['spa.Activity']}, + u'activity_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa.Activity']", 'unique': 'True', 'primary_key': 'True'}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_comments'", 'to': "orm['spa.Mix']"}) + }, + 'spa.activitydownload': { + 'Meta': {'object_name': 'ActivityDownload', '_ormbases': ['spa.Activity']}, + u'activity_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa.Activity']", 'unique': 'True', 'primary_key': 'True'}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_downloads'", 'to': "orm['spa.Mix']"}) + }, + 'spa.activityfavourite': { + 'Meta': {'object_name': 'ActivityFavourite', '_ormbases': ['spa.Activity']}, + u'activity_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa.Activity']", 'unique': 'True', 'primary_key': 'True'}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_favourites'", 'to': "orm['spa.Mix']"}) + }, + 'spa.activityfollow': { + 'Meta': {'object_name': 'ActivityFollow', '_ormbases': ['spa.Activity']}, + u'activity_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa.Activity']", 'unique': 'True', 'primary_key': 'True'}), + 'to_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_follow'", 'to': "orm['spa.UserProfile']"}) + }, + 'spa.activitylike': { + 'Meta': {'object_name': 'ActivityLike', '_ormbases': ['spa.Activity']}, + u'activity_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa.Activity']", 'unique': 'True', 'primary_key': 'True'}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_likes'", 'to': "orm['spa.Mix']"}) + }, + 'spa.activityplay': { + 'Meta': {'object_name': 'ActivityPlay', '_ormbases': ['spa.Activity']}, + u'activity_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa.Activity']", 'unique': 'True', 'primary_key': 'True'}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_plays'", 'to': "orm['spa.Mix']"}) + }, + 'spa.chatmessage': { + 'Meta': {'object_name': 'ChatMessage'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'chat_messages'", 'null': 'True', 'to': "orm['spa.UserProfile']"}) + }, + 'spa.comment': { + 'Meta': {'object_name': 'Comment'}, + 'comment': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'likes': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'liked_comments'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['spa.UserProfile']"}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['spa.Mix']"}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'time_index': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + 'spa.genre': { + 'Meta': {'object_name': 'Genre'}, + 'description': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}) + }, + 'spa.label': { + 'Meta': {'object_name': 'Label'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}) + }, + 'spa.mix': { + 'Meta': {'ordering': "('-id',)", 'object_name': 'Mix'}, + 'archive_path': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'null': 'True', 'blank': 'True'}), + 'archive_updated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'download_allowed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'duration': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'favourites': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'favourites'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['spa.UserProfile']"}), + 'filetype': ('django.db.models.fields.CharField', [], {'default': "'mp3'", 'max_length': '10'}), + 'genres': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['spa.Genre']", 'symmetrical': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_featured': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'likes': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'likes'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['spa.UserProfile']"}), + 'mix_image': ('django.db.models.fields.files.ImageField', [], {'max_length': '1024', 'blank': 'True'}), + 'mp3tags_updated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'uid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '38', 'blank': 'True'}), + 'upload_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'mixes'", 'to': "orm['spa.UserProfile']"}), + 'waveform_generated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'waveform_version': ('django.db.models.fields.IntegerField', [], {'default': '1'}) + }, + 'spa.notification': { + 'Meta': {'ordering': "('-id',)", 'object_name': 'Notification'}, + 'accepted_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'from_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'notifications'", 'null': 'True', 'to': "orm['spa.UserProfile']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'target': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}), + 'target_desc': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}), + 'to_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'to_notications'", 'to': "orm['spa.UserProfile']"}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}), + 'verb': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}) + }, + 'spa.playlist': { + 'Meta': {'object_name': 'Playlist'}, + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mixes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['spa.Mix']", 'symmetrical': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'playlists'", 'to': "orm['spa.UserProfile']"}) + }, + 'spa.purchaselink': { + 'Meta': {'object_name': 'PurchaseLink'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'provider': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'track': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'purchase_link'", 'to': "orm['spa.Tracklist']"}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + }, + 'spa.recurrence': { + 'Meta': {'object_name': 'Recurrence', '_ormbases': ['spa._Lookup']}, + u'_lookup_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['spa._Lookup']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'spa.release': { + 'Meta': {'object_name': 'Release'}, + 'embed_code': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'release_artist': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'release_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)'}), + 'release_description': ('django.db.models.fields.TextField', [], {}), + 'release_image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'blank': 'True'}), + 'release_label': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['spa.Label']"}), + 'release_title': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['spa.UserProfile']"}) + }, + 'spa.releaseaudio': { + 'Meta': {'object_name': 'ReleaseAudio'}, + 'description': ('django.db.models.fields.TextField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'release': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'release_audio'", 'null': 'True', 'to': "orm['spa.Release']"}) + }, + 'spa.tracklist': { + 'Meta': {'object_name': 'Tracklist'}, + 'artist': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index': ('django.db.models.fields.SmallIntegerField', [], {}), + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tracklist'", 'to': "orm['spa.Mix']"}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'remixer': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'timeindex': ('django.db.models.fields.TimeField', [], {'null': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'spa.userprofile': { + 'Meta': {'object_name': 'UserProfile'}, + 'activity_sharing_facebook': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'activity_sharing_networks': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'activity_sharing_twitter': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'avatar_image': ('django.db.models.fields.files.ImageField', [], {'max_length': '1024', 'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'social'", 'max_length': '15'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}), + 'display_name': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}), + 'email_notifications': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}), + 'following': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'followers'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['spa.UserProfile']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_known_session': ('django.db.models.fields.CharField', [], {'max_length': '250', 'null': 'True', 'blank': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'default': 'None', 'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'userprofile'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + 'spa.venue': { + 'Meta': {'object_name': 'Venue'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now_add': 'True', 'blank': 'True'}), + 'object_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 8, 12, 0, 0)', 'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + 'venue_address': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'venue_image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'blank': 'True'}), + 'venue_name': ('django.db.models.fields.CharField', [], {'max_length': '250'}) + } + } + + complete_apps = ['spa'] \ No newline at end of file diff --git a/spa/models/activity.py b/spa/models/activity.py index 0790fe2..c82e607 100755 --- a/spa/models/activity.py +++ b/spa/models/activity.py @@ -1,11 +1,9 @@ import abc -from django.contrib.auth.models import AnonymousUser, User +from datetime import datetime from allauth.socialaccount.models import SocialToken -from datetime import datetime from django.db import models from model_utils.managers import InheritanceManager - from open_facebook import OpenFacebook from core.utils.url import wrap_full @@ -62,27 +60,20 @@ class Activity(BaseModel): print ex.message pass - def create_notification(self): + def create_notification(self, accept=False): try: notification = Notification() notification.from_user = self.user notification.to_user = self.get_target_user() - notification.notification_text = "%s %s %s" % ( - self.user.get_nice_name() if self.user is not None else "Anonymouse", - self.get_verb_past(), - self.get_object_name_for_notification()) - notification.notification_html = "%s %s %s" % ( - wrap_full(self.user.get_profile_url() if self.user is not None else ""), - self.user.get_nice_name() if self.user is not None else "Anonymouse", - self.get_verb_past(), - wrap_full(self.get_object_url()), - self.get_object_name_for_notification() - ) - - notification.notification_url = self.get_object_url() notification.verb = self.get_verb_past() - notification.target = self.get_object_name() + notification.type = self.get_object_type() + notification.target = self.get_object_slug() + notification.target_desc = self.get_object_name() + + if accept: + notification.accepted_date = datetime.now() + notification.save() except Exception, ex: print "Error creating activity notification: %s" % ex.message diff --git a/spa/models/notification.py b/spa/models/notification.py index e29d9d8..2968c71 100755 --- a/spa/models/notification.py +++ b/spa/models/notification.py @@ -13,12 +13,10 @@ class Notification(BaseModel): from_user = models.ForeignKey('spa.UserProfile', related_name='notifications', null=True, blank=True) date = models.DateTimeField(auto_now_add=True) - notification_text = models.CharField(max_length=1024) - notification_html = models.CharField(max_length=1024) - notification_url = models.URLField(null=True) - verb = models.CharField(max_length=200, null=True) + type = models.CharField(max_length=200, null=True) target = models.CharField(max_length=200, null=True) + target_desc = models.CharField(max_length=200, null=True) accepted_date = models.DateTimeField(null=True)