diff --git a/core/utils/url.py b/core/utils/url.py index 55da954..a3cfddd 100755 --- a/core/utils/url.py +++ b/core/utils/url.py @@ -1,13 +1,17 @@ import urlparse import re +from django.contrib.sites.models import Site from django.template.defaultfilters import slugify __author__ = 'fergalm' + + def urlclean(url): #remove double slashes - ret = urlparse.urljoin(url, urlparse.urlparse(url).path.replace('//','/')) + ret = urlparse.urljoin(url, urlparse.urlparse(url).path.replace('//', '/')) return ret + def unique_slugify(instance, value, slug_field_name='slug', queryset=None, slug_separator='-'): """ Calculates and stores a unique slug of ``value`` for an instance. @@ -44,7 +48,7 @@ def unique_slugify(instance, value, slug_field_name='slug', queryset=None, slug_ slug = original_slug end = '%s%s' % (slug_separator, next) if slug_len and len(slug) + len(end) > slug_len: - slug = slug[:slug_len-len(end)] + slug = slug[:slug_len - len(end)] slug = _slug_strip(slug, slug_separator) slug = '%s%s' % (slug, end) next += 1 @@ -74,4 +78,13 @@ def _slug_strip(value, separator='-'): if separator != '-': re_sep = re.escape(separator) value = re.sub(r'^%s+|%s+$' % (re_sep, re_sep), '', value) - return value \ No newline at end of file + return value + +def is_absolute(url): + return bool(urlparse.urlparse(url).scheme) + +def wrap_full(url): + if not is_absolute(url): + url = "http://%s%s" % (Site.objects.get_current().domain, url) + + return url \ No newline at end of file diff --git a/dss/settings.py b/dss/settings.py index a16fe6a..c28f449 100755 --- a/dss/settings.py +++ b/dss/settings.py @@ -171,6 +171,7 @@ INSTALLED_APPS = ( 'django_jenkins', 'dbbackup', 'jfu', + 'djrill', #'backbone_tastypie', ) @@ -229,17 +230,20 @@ SENDFILE_ROOT = os.path.join(MEDIA_ROOT, 'mixes') SENDFILE_URL = '/media/mixes' import mimetypes + mimetypes.add_type("text/xml", ".plist", False) HTML_MINIFY = not localsettings.DEBUG -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = localsettings.EMAIL_HOST EMAIL_PORT = localsettings.EMAIL_PORT DEFAULT_FROM_EMAIL = 'DSS ChatBot ' DEFAULT_HTTP_PROTOCOL = 'http' +EMAIL_BACKEND = 'djrill.mail.backends.djrill.DjrillBackend' +MANDRILL_API_KEY = localsettings.MANDRILL_API_KEY + if DEBUG: import mimetypes @@ -272,3 +276,4 @@ GEOIP_PATH = localsettings.GEOIP_PATH from pipelinesettings import * + diff --git a/spa/ajax.py b/spa/ajax.py index 2c4d2a8..2ff6b3b 100755 --- a/spa/ajax.py +++ b/spa/ajax.py @@ -29,262 +29,262 @@ logger = logging.getLogger(__name__) class AjaxHandler(object): - # Get an instance of a logger + # Get an instance of a logger - def __init__(self, api_name="v1"): - self.api_name = api_name + def __init__(self, api_name="v1"): + self.api_name = api_name - @property - def urls(self): - pattern_list = [ - url(r'^mix/add_comment/$', 'spa.ajax.mix_add_comment', name='mix_add_comment'), - url(r'^mix/comments/(?P\d+)/$', 'spa.ajax.mix_comments', name='ajax_mix_comments'), - url(r'^header/$', 'spa.ajax.header', name='header'), - url(r'^session_play_count/$', 'spa.ajax.session_play_count'), - url(r'^mix_stream_url/(?P\d+)/$', 'spa.ajax.get_mix_stream_url'), - url(r'^release_player/(?P\d+)/$', 'spa.ajax.release_player'), - url(r'^live_now_playing/$', 'spa.ajax.live_now_playing'), - url(r'^mark_read/$', 'spa.ajax.mark_read'), - url(r'^facebook_post_likes_allowed/$', 'spa.ajax.facebook_post_likes_allowed', - name='ajax_facebook_post_likes_allowed'), - url(r'^upload_mix_image/(?P\d+)/$', 'spa.ajax.upload_mix_image', name='ajax_upload_mix_image'), - url(r'^upload_release_image/(?P\d+)/$', 'spa.ajax.upload_release_image', - name='ajax_upload_release_image'), - url(r'^upload_avatar_image/$', 'spa.ajax.upload_avatar_image', name='ajax_upload_avatar_image'), - url(r'^lookup/search/$', 'spa.ajax.lookup_search', name='ajax_lookup'), - url(r'^lookup/(?P\w+)/$', 'spa.ajax.lookup', name='ajax_lookup'), - ] - return pattern_list + @property + def urls(self): + pattern_list = [ + url(r'^mix/add_comment/$', 'spa.ajax.mix_add_comment', name='mix_add_comment'), + url(r'^mix/comments/(?P\d+)/$', 'spa.ajax.mix_comments', name='ajax_mix_comments'), + url(r'^header/$', 'spa.ajax.header', name='header'), + url(r'^session_play_count/$', 'spa.ajax.session_play_count'), + url(r'^mix_stream_url/(?P\d+)/$', 'spa.ajax.get_mix_stream_url'), + url(r'^release_player/(?P\d+)/$', 'spa.ajax.release_player'), + url(r'^live_now_playing/$', 'spa.ajax.live_now_playing'), + url(r'^mark_read/$', 'spa.ajax.mark_read'), + url(r'^facebook_post_likes_allowed/$', 'spa.ajax.facebook_post_likes_allowed', + name='ajax_facebook_post_likes_allowed'), + url(r'^upload_mix_image/(?P\d+)/$', 'spa.ajax.upload_mix_image', name='ajax_upload_mix_image'), + url(r'^upload_release_image/(?P\d+)/$', 'spa.ajax.upload_release_image', + name='ajax_upload_release_image'), + url(r'^upload_avatar_image/$', 'spa.ajax.upload_avatar_image', name='ajax_upload_avatar_image'), + url(r'^lookup/search/$', 'spa.ajax.lookup_search', name='ajax_lookup'), + url(r'^lookup/(?P\w+)/$', 'spa.ajax.lookup', name='ajax_lookup'), + ] + return pattern_list def _get_json(payload, key='value'): - data = { - key: payload - } - return simplejson.dumps(data) + data = { + key: payload + } + return simplejson.dumps(data) @render_to('inc/header.html') def header(request): - return HttpResponse(render_to_response('inc/header.html')) + return HttpResponse(render_to_response('inc/header.html')) def session_play_count(request): - """ + """ - :param request: - :return: Number of tracks played in this session - """ - if not request.user.is_authenticated(): - if 'play_count' in request.session: - result = simplejson.dumps({ - 'play_count': request.session['play_count'] - }) - else: - result = simplejson.dumps({ - 'play_count': '0' - }) - else: - result = simplejson.dumps({ - 'play_count': '0' - }) - return HttpResponse(result, mimetype='application/json') + :param request: + :return: Number of tracks played in this session + """ + if not request.user.is_authenticated(): + if 'play_count' in request.session: + result = simplejson.dumps({ + 'play_count': request.session['play_count'] + }) + else: + result = simplejson.dumps({ + 'play_count': '0' + }) + else: + result = simplejson.dumps({ + 'play_count': '0' + }) + return HttpResponse(result, mimetype='application/json') def get_mix_stream_url(request, mix_id): - try: - if not request.user.is_authenticated(): - if 'play_count' in request.session: - request.session['play_count'] += 1 - else: - request.session['play_count'] = 1 + try: + if not request.user.is_authenticated(): + if 'play_count' in request.session: + request.session['play_count'] += 1 + else: + request.session['play_count'] = 1 - mix = Mix.objects.get(pk=mix_id) - mix.add_play(request.user) - data = { - 'stream_url': mix.get_stream_path(), - 'description': mix.description, - 'item_url': mix.get_absolute_url(), - 'title': mix.title - } - return HttpResponse(simplejson.dumps(data), mimetype="application/json") - except Exception, e: - logger.exception("Error getting mix stream url") - return HttpResponse("Error getting mix stream url", status=401) + mix = Mix.objects.get(pk=mix_id) + mix.add_play(request.user) + data = { + 'stream_url': mix.get_stream_path(), + 'description': mix.description, + 'item_url': mix.get_absolute_url(), + 'title': mix.title + } + return HttpResponse(simplejson.dumps(data), mimetype="application/json") + except Exception, e: + logger.exception("Error getting mix stream url") + return HttpResponse("Error getting mix stream url", status=401) def live_now_playing(request): - now_playing_info = live.get_now_playing( - localsettings.JS_SETTINGS['LIVE_STREAM_URL'], - localsettings.JS_SETTINGS['LIVE_STREAM_PORT'], - localsettings.JS_SETTINGS['LIVE_STREAM_MOUNT']) - try: - if now_playing_info is not None: - return HttpResponse( - simplejson.dumps({ - 'stream_url': 'http://%s:%s/%s' % ( - localsettings.JS_SETTINGS['LIVE_STREAM_URL'], - localsettings.JS_SETTINGS['LIVE_STREAM_PORT'], - localsettings.JS_SETTINGS['LIVE_STREAM_MOUNT']), - 'description': now_playing_info['stream_description'], - 'title': now_playing_info['current_song'] - }), mimetype="application/json") - except: - return HttpResponseNotFound(now_playing_info) + now_playing_info = live.get_now_playing( + localsettings.JS_SETTINGS['LIVE_STREAM_URL'], + localsettings.JS_SETTINGS['LIVE_STREAM_PORT'], + localsettings.JS_SETTINGS['LIVE_STREAM_MOUNT']) + try: + if now_playing_info is not None: + return HttpResponse( + simplejson.dumps({ + 'stream_url': 'http://%s:%s/%s' % ( + localsettings.JS_SETTINGS['LIVE_STREAM_URL'], + localsettings.JS_SETTINGS['LIVE_STREAM_PORT'], + localsettings.JS_SETTINGS['LIVE_STREAM_MOUNT']), + 'description': now_playing_info['stream_description'], + 'title': now_playing_info['current_song'] + }), mimetype="application/json") + except: + return HttpResponseNotFound(now_playing_info) - return None + return None @render_to('inc/release_player.html') def release_player(request, release_id): - return HttpResponse('Hello Sailor') + return HttpResponse('Hello Sailor') def mix_add_comment(request): - if request.POST: - comment = Comment() - comment.mix_id = request.POST['mixid'] - comment.user = request.user - comment.comment = request.POST['comment'] - comment.time_index = request.POST['position'] - comment.save() + if request.POST: + comment = Comment() + comment.mix_id = request.POST['mixid'] + comment.user = request.user + comment.comment = request.POST['comment'] + comment.time_index = request.POST['position'] + comment.save() - return HttpResponse(_get_json('Comment posted', 'description')) - else: - return HttpResponse(_get_json('Error posting', 'description')) + return HttpResponse(_get_json('Comment posted', 'description')) + else: + return HttpResponse(_get_json('Error posting', 'description')) @render_to('inc/comment_list.html') def mix_comments(request, mix_id): - return { - "results": Comment.objects.filter(mix_id=mix_id), - } + return { + "results": Comment.objects.filter(mix_id=mix_id), + } @login_required def mark_read(request): - profile = request.user.get_profile() - if profile is not None: - Notification.objects.filter(to_user=profile).update(accepted_date=datetime.now()) - return HttpResponse('Success', status=200) - pass + profile = request.user.get_profile() + if profile is not None: + Notification.objects.filter(to_user=profile).update(accepted_date=datetime.now()) + return HttpResponse('Success', status=200) + pass - return HttpResponse('Unauthorized', status=401) + return HttpResponse('Unauthorized', status=401) @login_required() def facebook_post_likes_allowed(request): - profile = request.user.get_profile() - if profile is not None: - likes_allowed = profile.activity_sharing & UserProfile.ACTIVITY_SHARE_LIKES - facebook_allowed = profile.activity_sharing_networks & UserProfile.ACTIVITY_SHARE_NETWORK_FACEBOOK + profile = request.user.get_profile() + if profile is not None: + likes_allowed = profile.activity_sharing & UserProfile.ACTIVITY_SHARE_LIKES + facebook_allowed = profile.activity_sharing_networks & UserProfile.ACTIVITY_SHARE_NETWORK_FACEBOOK - return HttpResponse(_get_json(bool(likes_allowed & facebook_allowed)), mimetype="application/json") + return HttpResponse(_get_json(bool(likes_allowed & facebook_allowed)), mimetype="application/json") - return HttpResponse(_get_json(False), mimetype="application/json") + return HttpResponse(_get_json(False), mimetype="application/json") @csrf_exempt def upload_release_image(request, release_id): - try: - if 'release_image' in request.FILES and release_id is not None: - release = Release.objects.get(pk=release_id) - if release is not None: - release.release_image = request.FILES['release_image'] - release.save() - return HttpResponse(_get_json("Success")) - except Exception, ex: - logger.exception("Error uploading release image") - return HttpResponse(_get_json("Failed")) + try: + if 'release_image' in request.FILES and release_id is not None: + release = Release.objects.get(pk=release_id) + if release is not None: + release.release_image = request.FILES['release_image'] + release.save() + return HttpResponse(_get_json("Success")) + except Exception, ex: + logger.exception("Error uploading release image") + return HttpResponse(_get_json("Failed")) def upload_mix_image(request, mix_id): - try: - if len(request.FILES) != 0: - mix = Mix.objects.get(pk=mix_id) - if mix: - mix.mix_image = request.FILES[request.FILES.keys()[0]] - mix.save() - return HttpResponse(json.dumps({'status': 'OK', 'url': mix.get_image_url()})) - except Exception, ex: - logger.exception("Error uploading avatar: %s", ex.message) - return HttpResponse(json.dumps({'status': 'failed', 'message': ex.message})) + try: + if len(request.FILES) != 0: + mix = Mix.objects.get(pk=mix_id) + if mix: + mix.mix_image = request.FILES[request.FILES.keys()[0]] + mix.save() + return HttpResponse(json.dumps({'status': 'OK', 'url': mix.get_image_url()})) + except Exception, ex: + logger.exception("Error uploading avatar: %s", ex.message) + return HttpResponse(json.dumps({'status': 'failed', 'message': ex.message})) - return HttpResponse(json.dumps({'status': 'failed', 'message': 'No image file found'})) + return HttpResponse(json.dumps({'status': 'failed', 'message': 'No image file found'})) def upload_avatar_image(request): - # TODO: fergal.moran@gmail.com - # Problem here that only the current user can update avatar - # Might need to allow staff to change other's avatars? - try: - if len(request.FILES) != 0: - profile = request.user.get_profile() - if profile: - profile.avatar_image = request.FILES[request.FILES.keys()[0]] - profile.avatar_type = 'custom' - profile.save() - return HttpResponse(json.dumps({'status': 'OK', 'url': profile.get_avatar_image()})) - except Exception, ex: - logger.exception("Error uploading avatar: %s", ex.message) - return HttpResponse(json.dumps({'status': 'failed', 'message': ex.message})) + # TODO: fergal.moran@gmail.com + # Problem here that only the current user can update avatar + # Might need to allow staff to change other's avatars? + try: + if len(request.FILES) != 0: + profile = request.user.get_profile() + if profile: + profile.avatar_image = request.FILES[request.FILES.keys()[0]] + profile.avatar_type = 'custom' + profile.save() + return HttpResponse(json.dumps({'status': 'OK', 'url': profile.get_avatar_image()})) + except Exception, ex: + logger.exception("Error uploading avatar: %s", ex.message) + return HttpResponse(json.dumps({'status': 'failed', 'message': ex.message})) - return HttpResponse(json.dumps({'status': 'failed', 'message': 'No image file found'})) + return HttpResponse(json.dumps({'status': 'failed', 'message': 'No image file found'})) @require_POST @login_required def upload(request): - # The assumption here is that jQuery File Upload - # has been configured to send files one at a time. - # If multiple files can be uploaded simultaneously, - # 'file' may be a list of files. - try: - uid = request.POST['upload-hash'] - in_file = request.FILES['file'] if request.FILES else None - fileName, extension = os.path.splitext(in_file.name) + # The assumption here is that jQuery File Upload + # has been configured to send files one at a time. + # If multiple files can be uploaded simultaneously, + # 'file' may be a list of files. + try: + uid = request.POST['upload-hash'] + in_file = request.FILES['file'] if request.FILES else None + fileName, extension = os.path.splitext(in_file.name) - file_storage = FileSystemStorage(location=os.path.join(settings.CACHE_ROOT, "mixes")) - cache_file = file_storage.save("%s%s" % (uid, extension), ContentFile(in_file.read())) + file_storage = FileSystemStorage(location=os.path.join(settings.CACHE_ROOT, "mixes")) + cache_file = file_storage.save("%s%s" % (uid, extension), ContentFile(in_file.read())) - create_waveform_task.delay(in_file=os.path.join(file_storage.base_location, cache_file), uid=uid) + create_waveform_task.delay(in_file=os.path.join(file_storage.base_location, cache_file), uid=uid) - file_dict = { - 'size': in_file.size, - 'uid': uid - } + file_dict = { + 'size': in_file.size, + 'uid': uid + } - return UploadResponse(request, file_dict) - except Exception, ex: - logger.exception(ex.message) - raise + return UploadResponse(request, file_dict) + except Exception, ex: + logger.exception(ex.message) + raise @csrf_exempt def lookup_search(request): - query = request.GET['query'] if 'query' in request.GET else request.GET['q'] if 'q' in request.GET else '' - if query != '': - filter_field = Mix.get_lookup_filter_field() - kwargs = { - '{0}__{1}'.format(filter_field, 'icontains'): query, - } - rows = Mix.objects.values("title").filter(**kwargs) - #results = serializers.serialize("json", rows, fields="title",) - results = json.dumps(rows) - return HttpResponse(results, mimetype='application/json') + query = request.GET['query'] if 'query' in request.GET else request.GET['q'] if 'q' in request.GET else '' + if query != '': + filter_field = Mix.get_lookup_filter_field() + kwargs = { + '{0}__{1}'.format(filter_field, 'icontains'): query, + } + rows = Mix.objects.values("title").filter(**kwargs) + #results = serializers.serialize("json", rows, fields="title",) + results = json.dumps(rows) + return HttpResponse(results, mimetype='application/json') @csrf_exempt def lookup(request, source): - query = request.GET['query'] if 'query' in request.GET else request.GET['q'] if 'q' in request.GET else '' - if query != '': - model = get_model('spa', source) - if model is not None: - filter_field = model.get_lookup_filter_field() - kwargs = { - '{0}__{1}'.format(filter_field, 'icontains'): query, - } - rows = model.objects.filter(**kwargs) - results = json.to_ajax(rows, filter_field) - return HttpResponse(simplejson.dumps(results), mimetype='application/json') - return HttpResponse(_get_json("Key failure in lookup"), mimetype='application/json') + query = request.GET['query'] if 'query' in request.GET else request.GET['q'] if 'q' in request.GET else '' + if query != '': + model = get_model('spa', source) + if model is not None: + filter_field = model.get_lookup_filter_field() + kwargs = { + '{0}__{1}'.format(filter_field, 'icontains'): query, + } + rows = model.objects.filter(**kwargs) + results = json.to_ajax(rows, filter_field) + return HttpResponse(simplejson.dumps(results), mimetype='application/json') + return HttpResponse(_get_json("Key failure in lookup"), mimetype='application/json') diff --git a/spa/api/v1/UserResource.py b/spa/api/v1/UserResource.py index 42c0199..743cb20 100755 --- a/spa/api/v1/UserResource.py +++ b/spa/api/v1/UserResource.py @@ -61,18 +61,6 @@ class UserResource(BackboneCompatibleResource): return semi_filtered - def _patch_resource(self, bundle): - #Handle the patched items from backbone - if 'is_following' in bundle.data: - if bundle.data['is_following']: - bundle.obj.add_follower(bundle.request.user.get_profile()) - activity = ActivityFollow() - activity.user = bundle.request.user.get_profile() - activity.to_user = bundle.obj - activity.save() - else: - bundle.obj.remove_follower(bundle.request.user.get_profile()) - def obj_update(self, bundle, skip_errors=False, **kwargs): """ diff --git a/spa/migrations/0044_auto__add_field_notification_notification_html.py b/spa/migrations/0044_auto__add_field_notification_notification_html.py new file mode 100644 index 0000000..9136d6e --- /dev/null +++ b/spa/migrations/0044_auto__add_field_notification_notification_html.py @@ -0,0 +1,240 @@ +# -*- 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.notification_html' + db.add_column(u'spa_notification', 'notification_html', + self.gf('django.db.models.fields.CharField')(default='', max_length=1024), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Notification.notification_html' + db.delete_column(u'spa_notification', 'notification_html') + + + 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'}) + }, + 'spa.activity': { + 'Meta': {'object_name': 'Activity'}, + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': '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', [], {}), + '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': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mix': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['spa.Mix']"}), + '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.event': { + 'Meta': {'object_name': 'Event'}, + 'attendees': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'attendees'", 'symmetrical': 'False', 'to': u"orm['auth.User']"}), + 'date_created': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2014, 1, 25, 0, 0)'}), + 'event_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2014, 1, 25, 0, 0)'}), + 'event_description': ('tinymce.models.HTMLField', [], {}), + 'event_recurrence': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['spa.Recurrence']"}), + 'event_time': ('django.db.models.fields.TimeField', [], {'default': 'datetime.datetime(2014, 1, 25, 0, 0)'}), + 'event_title': ('django.db.models.fields.CharField', [], {'max_length': '250'}), + 'event_venue': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['spa.Venue']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': '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'}), + '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'}) + }, + 'spa.mix': { + 'Meta': {'object_name': 'Mix'}, + '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'}), + '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', [], {'default': 'datetime.datetime(2014, 1, 25, 0, 0)'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'mixes'", 'to': "orm['spa.UserProfile']"}), + 'waveform_generated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'spa.notification': { + 'Meta': {'object_name': 'Notification'}, + 'accepted_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now': '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'}), + 'notification_html': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'notification_text': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'notification_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': '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']"}), + 'verb': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}) + }, + 'spa.purchaselink': { + 'Meta': {'object_name': 'PurchaseLink'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': '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'}), + 'release_artist': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'release_date': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2014, 1, 25, 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'}), + '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']"}), + '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': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'activity_sharing_networks': ('django.db.models.fields.IntegerField', [], {'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'}), + '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'}), + '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'}), + '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 b51fb8b..3bbbba0 100755 --- a/spa/models/activity.py +++ b/spa/models/activity.py @@ -1,11 +1,13 @@ -import threading +import abc + from django.db import models from model_utils.managers import InheritanceManager -from core.realtime.activity import post_activity +from core.utils.url import wrap_full + from spa.models.notification import Notification from spa.models.userprofile import UserProfile from spa.models._basemodel import _BaseModel -import abc + ACTIVITYTYPES = ( ('p', 'played'), @@ -30,7 +32,15 @@ class Activity(_BaseModel): notification.notification_text = "%s %s %s" % ( self.user.get_nice_name() or "Anonymouse", self.get_verb_past(), self.get_object_name_for_notification()) - notification.notification_url = self.get_object_url() + notification.notification_html = "%s %s %s" % ( + wrap_full(self.user.get_profile_url() or "http://deepsounds.com"), + self.user.get_nice_name() or "Anonymouse", + self.get_verb_past(), + wrap_full(self.get_object_url()), + self.get_object_name_for_notification() + ) + + notification.notification_url = wrap_full(self.get_object_url()) notification.verb = self.get_verb_past() notification.target = self.get_object_name() notification.save() diff --git a/spa/models/comment.py b/spa/models/comment.py index f1a5541..3ad3b81 100755 --- a/spa/models/comment.py +++ b/spa/models/comment.py @@ -1,7 +1,7 @@ from django.contrib.auth.models import User from django.db import models -from spa.models import _BaseModel, UserProfile -from spa.models.notification import Notification + +from spa.models import _BaseModel from spa.models.mix import Mix diff --git a/spa/models/notification.py b/spa/models/notification.py index 16812da..8eb2841 100644 --- a/spa/models/notification.py +++ b/spa/models/notification.py @@ -1,8 +1,12 @@ -from django.contrib.auth.models import User import threading +import mandrill + from django.db import models +from django.template import loader, Context + from core.realtime.notification import post_notification -from spa.models import _BaseModel, UserProfile +from dss import localsettings +from spa.models import _BaseModel class NotificationThread(threading.Thread): @@ -18,11 +22,12 @@ class NotificationThread(threading.Thread): class Notification(_BaseModel): - to_user = models.ForeignKey(UserProfile, related_name='to_notications') - from_user = models.ForeignKey(UserProfile, related_name='notifications', null=True, blank=True) + to_user = models.ForeignKey('spa.UserProfile', related_name='to_notications') + from_user = models.ForeignKey('spa.UserProfile', related_name='notifications', null=True, blank=True) date = models.DateTimeField(auto_now=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) @@ -32,3 +37,40 @@ 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): + + self.send_notification_email() + return super(Notification, self).save(force_insert, force_update, using, update_fields) + + def send_notification_email(self): + try: + t = loader.get_template('email/notification/new.html') + c = Context({ + 'user_name': self.to_user.get_nice_name(), + 'notification_html': self.notification_html, + 'title': self.notification_html + }) + rendered = t.render(c) + + mandrill_client = mandrill.Mandrill(localsettings.MANDRILL_API_KEY) + message = { + 'inline_css': True, + 'from_email': 'chatbot@deepsouthsounds.com', + 'from_name': 'DSS ChatBot', + 'headers': {'Reply-To': 'chatbot@deepsouthsounds.com'}, + 'metadata': {'website': 'www.deepsouthsounds.com'}, + 'subject': self.notification_text, + 'to': [{'email': 'fergal.moran@gmail.com', + 'name': 'Fergal Moran', + 'type': 'to'}], + 'html': rendered, + 'text': 'Get yourself some HTML man!', + } + + result = mandrill_client.messages.send(message=message, async=False) + print result + + except mandrill.Error, e: # Mandrill errors are thrown as exceptions + print 'A mandrill error occurred: %s - %s' % (e.__class__, e) diff --git a/spa/models/userprofile.py b/spa/models/userprofile.py index b78ca9d..03fc22d 100755 --- a/spa/models/userprofile.py +++ b/spa/models/userprofile.py @@ -7,15 +7,17 @@ from django.db import models from django.db.models import Count from django_gravatar.helpers import has_gravatar, get_gravatar_url from sorl.thumbnail import get_thumbnail - from allauth.socialaccount.models import SocialAccount -from core.utils.file import generate_save_file_name -from core.utils.url import unique_slugify -from dss import settings -from spa.models._basemodel import _BaseModel -from templated_email import send_templated_mail from sorl import thumbnail from sorl.thumbnail.helpers import ThumbnailError + +from core.utils.file import generate_save_file_name +from core.utils.url import unique_slugify, wrap_full +from dss import settings +from spa.models.notification import Notification +from spa.models._basemodel import _BaseModel + + logger = logging.getLogger(__name__) @@ -109,24 +111,6 @@ class UserProfile(_BaseModel): except Exception, ex: self.logger.error("Exception updating favourite: %s" % ex.message) - def add_follower(self, user): - self.followers.add(user) - user.following.add(self) - - if not settings.DEBUG: - try: - send_templated_email([user.user], "notification/new_follower", {"profile": self.user}) - except Exception, ex: - self.logger.error("Unable to send email for new follower") - self.logger.error("Exception: %s" % ex.message) - self.logger.error("Host: %s" % settings.EMAIL_HOST) - self.logger.error("Port: %s" % settings.EMAIL_PORT) - self.logger.error("Backend: %s" % settings.EMAIL_BACKEND) - - def remove_follower(self, user): - if user in self.followers.all(): - self.followers.remove(user) - def is_follower(self, user): try: return user.get_profile() in self.followers.all() diff --git a/spa/signals.py b/spa/signals.py index 5dad522..dec362a 100755 --- a/spa/signals.py +++ b/spa/signals.py @@ -1,10 +1,12 @@ from django.contrib.sessions.models import Session from django.core import signals from django.core.exceptions import ObjectDoesNotExist -from django.db.models.signals import post_save, pre_save +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.utils.audio.mp3 import mp3_length +from core.utils.url import wrap_full +from spa.models import Notification from spa.models.userprofile import UserProfile from spa.models.mix import Mix @@ -97,3 +99,29 @@ def session_pre_save(sender, **kwargs): except ObjectDoesNotExist: pass + +@receiver(m2m_changed, sender=UserProfile.following.through, dispatch_uid='user_followers_changed') +def user_followers_changed(sender, **kwargs): + try: + if kwargs['action'] == 'post_add': + source_user = kwargs['instance'] + if source_user: + for i in kwargs['pk_set']: + target_user = UserProfile.objects.get(pk=i) + if target_user: + notification = Notification() + notification.from_user = source_user + notification.to_user = target_user + notification.notification_text = "You have a new follower on Deep South Sounds" + + notification.notification_html = "%s followed you on Deep South Sounds" % ( + wrap_full(source_user.get_profile_url()), + source_user.get_nice_name() + ) + + notification.notification_url = wrap_full(source_user.get_absolute_url()) + notification.verb = "followed" + notification.target = "Lick my balls" + notification.save() + except Exception, ex: + print "Error sending new follower: %s" % ex.message \ No newline at end of file diff --git a/templates/notification/email_body.txt b/templates/email/email_body.txt old mode 100755 new mode 100644 similarity index 100% rename from templates/notification/email_body.txt rename to templates/email/email_body.txt diff --git a/templates/email/notification/new.html b/templates/email/notification/new.html new file mode 100644 index 0000000..1e6a06a --- /dev/null +++ b/templates/email/notification/new.html @@ -0,0 +1,404 @@ +{% block content %} + + + + + + +{{ title }} + + + + + + + + + + +
+
+ + + + + +
New Notice From Deep South Sounds
+
+
+ + + + + + + + +
+
+ + + + +
+

Hi, {{ user_name }}

+ +

{{ html_title }}

+

+ {{ notification_html |safe }} +

+ + + + + + +
+
+
+ + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/notification/new_follower/email.html b/templates/notification/new_follower/email.html deleted file mode 100755 index e63d257..0000000 --- a/templates/notification/new_follower/email.html +++ /dev/null @@ -1,5 +0,0 @@ -{{ profile.get_nice_name }} has started following you on Deep South Sounds.
-This means they will get notifications about new mixes you upload.
-
-To see other notices change how you receive notifications, please go to http://{{ current_site }}/me - diff --git a/templates/notification/new_follower/email.txt b/templates/notification/new_follower/email.txt deleted file mode 100755 index 7e47ba9..0000000 --- a/templates/notification/new_follower/email.txt +++ /dev/null @@ -1 +0,0 @@ -{{ profile.get_nice_name }} has started following you. diff --git a/templates/notification/new_follower/full.txt b/templates/notification/new_follower/full.txt deleted file mode 100755 index 8ffcfba..0000000 --- a/templates/notification/new_follower/full.txt +++ /dev/null @@ -1 +0,0 @@ -{{ profile.get_nice_name }} has started following you. diff --git a/templates/notification/new_follower/notice.html b/templates/notification/new_follower/notice.html deleted file mode 100755 index 9bcdbb6..0000000 --- a/templates/notification/new_follower/notice.html +++ /dev/null @@ -1,7 +0,0 @@ -{% load i18n %} -{% url invitations as invitation_page %} -{% url profile_detail username=invitation.from_user.username as user_url %} -{% blocktrans with invitation.from_user as invitation_from_user %} - {{ invitation_from_user }} has started following you on Deep South Sounds - (see followers) -{% endblocktrans %} \ No newline at end of file diff --git a/templates/notification/new_follower/short.txt b/templates/notification/new_follower/short.txt deleted file mode 100755 index b1bdaaa..0000000 --- a/templates/notification/new_follower/short.txt +++ /dev/null @@ -1 +0,0 @@ -You have a new follower \ No newline at end of file