diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bdd8a37..dd88b41 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,6 @@ Templates: Markdown: * 4 space indent -* 80 character line-length limit * Continued lines in lists indented so that the first character is vertically in line with the first character (not list item signifier) on previous line. See the source of this doc for examples. diff --git a/Makefile b/Makefile index b28300c..0e1df6e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -APPLICATIONS := admin core promotion publishers social submissions usermgmt +APPLICATIONS := activitystream administration core promotion publishers social submissions usermgmt APPLICATIONS_COMMA := $(shell echo $(APPLICATIONS) | tr ' ' ',') .PHONY: run @@ -33,6 +33,7 @@ cleanmigrations: venv/bin/django-admin @echo "In case @makyo does not delete this before first alpha, do not" @echo "run this target. Migrations are hecka important for dev after" @echo "that point!" + exit 1 # We really shouldn't do this, but may need to in the future @echo @echo "Psst, @makyo, don't forget to delete this target!" @sleep 5 @@ -41,6 +42,10 @@ cleanmigrations: venv/bin/django-admin touch $$i/migrations/__init__.py; \ done +.PHONY: update-revno +update-revno: venv/bin/django-admin + venv/bin/python manage.py git_revno $(TAG) + .PHONY: test test: tox diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..0998e8e --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,11 @@ +# Release Process + +* Ensure that the project is clean across versions by running `make check` +* Tag your release + * ensure master is up to date: `git pull upstream master` + * update the revno file: `TAG= make update-revno` + * relying on semver as an outline, make an empty commit specifying the release: `git commit -am "Releasing "` + * tag and sign the release: `git tag -s ` +* Push the new tag + * push the changes: `git push upstream master` + * push the tags: `git push --tags upstream` diff --git a/activitystream/__init__.py b/activitystream/__init__.py new file mode 100644 index 0000000..b021599 --- /dev/null +++ b/activitystream/__init__.py @@ -0,0 +1 @@ +default_app_config = 'activitystream.apps.ActivitystreamConfig' diff --git a/admin/admin.py b/activitystream/admin.py similarity index 100% rename from admin/admin.py rename to activitystream/admin.py diff --git a/activitystream/apps.py b/activitystream/apps.py new file mode 100644 index 0000000..8e282c4 --- /dev/null +++ b/activitystream/apps.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class ActivitystreamConfig(AppConfig): + name = 'activitystream' + + def ready(self): + import activitystream.signals # noqa: F401 diff --git a/activitystream/migrations/0001_initial.py b/activitystream/migrations/0001_initial.py new file mode 100644 index 0000000..878c8f6 --- /dev/null +++ b/activitystream/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-10-30 04:16 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Activity', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('activity_time', models.DateTimeField(auto_now_add=True)), + ('activity_type', models.CharField(choices=[('USER:REG', 'User: registered'), ('USER:LOGIN', 'User: logged in'), ('USER:PWCHANGE', 'User: password changed'), ('USER:PWRESET', 'User: password reset'), ('USER:UPDATE', 'User: profile updated'), ('USER:VIEW', 'User: profile viewed'), ('GROUP:CREATE', 'Group: created'), ('GROUP:UPDATE', 'Group: updated'), ('GROUP:DELETE', 'Group: deleted'), ('GROUP:VIEW', 'Group: viewed'), ('SOCIAL:WATCH', 'Social: watch user'), ('SOCIAL:UNWATCH', 'Social: unwatch user'), ('SOCIAL:BLOCK', 'Social: block user'), ('SOCIAL:UNBLOCK', 'Social: unblock user'), ('SOCIAL:FAVORITE', 'Social: favorite submission'), ('SOCIAL:UNFAVORITE', 'Social: unfavorite submission'), ('SOCIAL:RATE', 'Social: rate submission'), ('SOCIAL:ENJOY', 'Social: enjoy submission'), ('SOCIAL:COMMENT', 'Social: object received comment'), ('SUBMISSION:CREATE', 'Submission: created'), ('SUBMISSION:UPDATE', 'Submission: updated'), ('SUBMISSION:DELETE', 'Submission: deleted'), ('SUBMISSION:VIEW', 'Submission: viewed'), ('FOLDER:CREATE', 'Folder: created'), ('FOLDER:UPDATE', 'Folder: updated'), ('FOLDER:DELETE', 'Folder: deleted'), ('FOLDER:VIEW', 'Folder: viewed'), ('FOLDER:SORT', 'Folder: sorted')], max_length=3)), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + options={ + 'ordering': ['-activity_type'], + }, + ), + ] diff --git a/activitystream/migrations/0002_auto_20161030_0719.py b/activitystream/migrations/0002_auto_20161030_0719.py new file mode 100644 index 0000000..ae32e47 --- /dev/null +++ b/activitystream/migrations/0002_auto_20161030_0719.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-10-30 07:19 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('activitystream', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='activity', + options={'ordering': ['-activity_time']}, + ), + migrations.AlterField( + model_name='activity', + name='activity_type', + field=models.CharField(choices=[('USER:REG', 'User: registered'), ('USER:LOGIN', 'User: logged in'), ('USER:LOGOUT', 'User: logged out'), ('USER:PWCHANGE', 'User: password changed'), ('USER:PWRESET', 'User: password reset'), ('PROFILE:UPDATE', 'User: profile updated'), ('PROFILE:VIEW', 'User: profile viewed'), ('ADMINFLAG:CREATE', 'Administration flag: created'), ('ADMINFLAG:UPDATE', 'Administration flag: updated'), ('ADMINFLAG:DELETE', 'Administration flag: deleted'), ('ADMINFLAG:VIEW', 'Administration flag: viewed'), ('GROUP:CREATE', 'Group: created'), ('GROUP:UPDATE', 'Group: updated'), ('GROUP:DELETE', 'Group: deleted'), ('GROUP:VIEW', 'Group: viewed'), ('SOCIAL:WATCH', 'Social: watch user'), ('SOCIAL:UNWATCH', 'Social: unwatch user'), ('SOCIAL:BLOCK', 'Social: block user'), ('SOCIAL:UNBLOCK', 'Social: unblock user'), ('SOCIAL:FAVORITE', 'Social: favorite submission'), ('SOCIAL:UNFAVORITE', 'Social: unfavorite submission'), ('SOCIAL:RATE', 'Social: rate submission'), ('SOCIAL:ENJOY', 'Social: enjoy submission'), ('SUBMISSION:CREATE', 'Submission: created'), ('SUBMISSION:UPDATE', 'Submission: updated'), ('SUBMISSION:DELETE', 'Submission: deleted'), ('SUBMISSION:VIEW', 'Submission: viewed'), ('FOLDER:CREATE', 'Folder: created'), ('FOLDER:UPDATE', 'Folder: updated'), ('FOLDER:DELETE', 'Folder: deleted'), ('FOLDER:VIEW', 'Folder: viewed'), ('FOLDER:SORT', 'Folder: sorted'), ('TAG:CREATE', 'Tag: tag created'), ('TAG:TAG', 'Tag: tagged item created'), ('COMMENT:CREATE', 'Comment: created'), ('COMMENT:UPDATE', 'Comment: updated'), ('COMMENT:DELETE', 'Comment: deleted'), ('PROMOTION:CREATE', 'Promotion: created'), ('PROMOTION:RETIRED', 'Promotion: retired'), ('PUBLISHER:CREATE', 'Publisher: created'), ('PUBLISHER:UPDATE', 'Publisher: updated'), ('PUBLISHER:DELETE', 'Publisher: deleted'), ('PUBLISHER:VIEW', 'Publisher: viewed'), ('PUBLISHER:CLAIMED', 'Publisher: claimed'), ('SEARCH:SEARCH', 'Search: run')], max_length=50), + ), + ] diff --git a/activitystream/migrations/0003_auto_20161030_0723.py b/activitystream/migrations/0003_auto_20161030_0723.py new file mode 100644 index 0000000..8d16120 --- /dev/null +++ b/activitystream/migrations/0003_auto_20161030_0723.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-10-30 07:23 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('activitystream', '0002_auto_20161030_0719'), + ] + + operations = [ + migrations.AlterField( + model_name='activity', + name='object_id', + field=models.PositiveIntegerField(blank=True), + ), + ] diff --git a/activitystream/migrations/0004_auto_20161030_0723.py b/activitystream/migrations/0004_auto_20161030_0723.py new file mode 100644 index 0000000..9f83c5f --- /dev/null +++ b/activitystream/migrations/0004_auto_20161030_0723.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-10-30 07:23 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('activitystream', '0003_auto_20161030_0723'), + ] + + operations = [ + migrations.AlterField( + model_name='activity', + name='content_type', + field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), + ), + ] diff --git a/activitystream/migrations/0005_auto_20161030_0724.py b/activitystream/migrations/0005_auto_20161030_0724.py new file mode 100644 index 0000000..3c5e6fb --- /dev/null +++ b/activitystream/migrations/0005_auto_20161030_0724.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-10-30 07:24 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('activitystream', '0004_auto_20161030_0723'), + ] + + operations = [ + migrations.AlterField( + model_name='activity', + name='content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), + ), + migrations.AlterField( + model_name='activity', + name='object_id', + field=models.PositiveIntegerField(blank=True, null=True), + ), + ] diff --git a/admin/__init__.py b/activitystream/migrations/__init__.py similarity index 100% rename from admin/__init__.py rename to activitystream/migrations/__init__.py diff --git a/activitystream/models.py b/activitystream/models.py new file mode 100644 index 0000000..6d64378 --- /dev/null +++ b/activitystream/models.py @@ -0,0 +1,99 @@ +from __future__ import unicode_literals + +from django.db import models +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType + + +class Activity(models.Model): + ACTIVITY_TYPES = ( + # Users and profiles + ('user:reg', 'user: registered'), + ('user:login', 'user: logged in'), + ('user:logout', 'user: logged out'), + ('user:pwchange', 'user: password changed'), # TODO + ('user:pwreset', 'user: password reset'), # TODO + ('profile:update', 'user: profile updated'), + ('profile:view', 'user: profile viewed'), + + # Administration flags + ('adminflag:create', 'administration flag: created'), + ('adminflag:update', 'administration flag: updated'), + ('adminflag:delete', 'administration flag: deleted'), + ('adminflag:view', 'administration flag: viewed'), + + # User groups + ('group:create', 'group: created'), + ('group:update', 'group: updated'), + ('group:delete', 'group: deleted'), + + # Social interactions + ('social:watch', 'social: watch user'), + ('social:unwatch', 'social: unwatch user'), + ('social:block', 'social: block user'), + ('social:unblock', 'social: unblock user'), + ('social:message', 'social: message user'), + ('social:favorite', 'social: favorite submission'), + ('social:unfavorite', 'social: unfavorite submission'), + ('social:rate', 'social: rate submission'), + ('social:enjoy', 'social: enjoy submission'), + + # Submissions + ('submission:create', 'submission: created'), + ('submission:update', 'submission: updated'), + ('submission:delete', 'submission: deleted'), + ('submission:view', 'submission: viewed'), + + # Submission folders + ('folder:create', 'folder: created'), + ('folder:update', 'folder: updated'), + ('folder:delete', 'folder: deleted'), + ('folder:view', 'folder: viewed'), + ('folder:sort', 'folder: sorted'), + + # Tags + ('tag:create', 'tag: tag created'), + ('tag:tag', 'tag: tagged item created'), + + # Comments + ('comment:create', 'comment: created'), + ('comment:update', 'comment: updated'), + ('comment:delete', 'comment: deleted'), + + # Promotions + ('promotion:create', 'promotion: created'), + ('promotion:retire', 'promotion: retired'), + ('ad:create', 'ad: created,'), + ('ad:update', 'ad: update'), + ('ad:golive', 'ad: went live'), + ('ad:retire', 'ad: retired'), + + # Publisher pages + ('publisher:create', 'publisher: created'), + ('publisher:update', 'publisher: updated'), + ('publisher:delete', 'publisher: deleted'), + ('publisher:view', 'publisher: viewed'), + ('publisher:claimed', 'publisher: claimed'), + + # Search + ('search:basic_search', 'search: basic search run'), + ) + + activity_time = models.DateTimeField(auto_now_add=True) + activity_type = models.CharField(max_length=50, choices=ACTIVITY_TYPES) + content_type = models.ForeignKey(ContentType, blank=True, null=True) + object_id = models.PositiveIntegerField(blank=True, null=True) + object_model = GenericForeignKey('content_type', 'object_id') + + @classmethod + def create(cls, app, action, object_model): + item_type = "{}:{}".format(app.lower(), action.lower()) + if item_type not in dict(Activity.ACTIVITY_TYPES): + return None # XXX should we fail silently? + activity = cls(activity_type=item_type) + activity.object_model = object_model + activity.save() + return activity + + class Meta: + ordering = ['-activity_time'] diff --git a/activitystream/signals.py b/activitystream/signals.py new file mode 100644 index 0000000..6293e96 --- /dev/null +++ b/activitystream/signals.py @@ -0,0 +1,74 @@ +from django.contrib.auth.signals import ( + user_logged_in, + user_logged_out, +) +from django.db.models.signals import ( + post_delete, + post_save, +) +from django.dispatch import receiver +from taggit.models import TaggedItem + +from .models import Activity +from usermgmt.models import Profile + + +@receiver(post_save, sender=Profile) +def log_user_register(sender, **kwargs): + if kwargs['created']: + Activity.create('user', 'reg', kwargs['instance'].user) + + +@receiver(user_logged_in) +def log_login(sender, **kwargs): + Activity.create('user', 'login', kwargs['user']) + + +@receiver(user_logged_out) +def log_logout(sender, **kwargs): + Activity.create('user', 'logout', kwargs['user']) + + +@receiver(post_save) +def log_base_create_or_update(sender, **kwargs): + try: + name = { + 'Flag': 'flag', + 'Folder': 'folder', + 'FriendGroup': 'group', + 'Profile': 'profile', + 'PublisherPage': 'publisher', + 'Submission': 'submission', + 'Tag': 'tag', + }[sender.__name__] + except: + return + if name == 'profile' and kwargs['created']: + return + Activity.create( + name, + 'create' if kwargs['created'] else 'update', + kwargs['instance']) + + +@receiver(post_delete) +def log_base_delete(sender, **kwargs): + try: + name = { + 'Flag': 'flag', + 'Folder': 'folder', + 'FriendGroup': 'group', + 'Submission': 'submission', + 'PublisherPage': 'publisher', + }[sender.__name__] + except: + return + Activity.create(name, 'delete', kwargs['instance']) + + +@receiver(post_save, sender=TaggedItem) +def log_tagged_item(sender, **kwargs): + Activity.create( + 'tag', + 'tag' if kwargs['created'] else 'update', + kwargs['instance']) diff --git a/activitystream/tests.py b/activitystream/tests.py new file mode 100644 index 0000000..d3993ad --- /dev/null +++ b/activitystream/tests.py @@ -0,0 +1,149 @@ +import json + +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.test import TestCase + +from .models import Activity +from usermgmt.models import Profile + + +class ActivityBaseTestCase(TestCase): + @classmethod + def setUpTestData(cls): + cls.foo = User.objects.create_user('foo', 'foo@example.com', + 'a good password') + cls.foo.save() + cls.foo.profile = Profile() + cls.foo.profile.save() + cls.bar = User.objects.create_user('bar', 'bar@example.com', + 'another good password') + cls.bar.save() + cls.bar.profile = Profile() + cls.bar.profile.save() + + +class TestModels(ActivityBaseTestCase): + def test_create(self): + self.assertEqual(Activity.objects.count(), 2) + activity = Activity.create('user', 'login', self.foo) + self.assertEqual(activity.activity_type, 'user:login') + self.assertEqual(Activity.objects.count(), 3) + + def test_ignores_unknown_type(self): + self.assertEqual(Activity.objects.count(), 2) + activity = Activity.create('bad', 'wolf', self.foo) + self.assertEqual(activity, None) + self.assertEqual(Activity.objects.count(), 2) + + +class TestGetStreamView(ActivityBaseTestCase): + def generate_activity_items(self): + self.client.get(reverse('core:basic_search'), {'q': 'asdf'}) + self.client.login(username='foo', + password='a good password') + self.client.get(reverse('core:basic_search'), {'q': 'asdf'}) + self.client.logout() + self.client.login(username='bar', + password='another good password') + self.client.get(reverse('core:basic_search'), {'q': 'asdf'}) + + def test_full_stream(self): + self.generate_activity_items() + response = self.client.get(reverse('activitystream:get_stream')) + data = json.loads(response.content.decode('utf-8')) + self.assertEqual(len(data), 8) + self.assertEqual([item['type'] for item in data], [ + 'search:basic_search', + 'user:login', + 'user:logout', + 'search:basic_search', + 'user:login', + 'search:basic_search', + 'user:reg', + 'user:reg', + ]) + + def test_limit_to_content_type(self): + self.generate_activity_items() + response = self.client.get(reverse( + 'activitystream:get_stream', kwargs={ + 'models': 'auth:user', + })) + data = json.loads(response.content.decode('utf-8')) + self.assertEqual(len(data), 7) + self.assertEqual([item['type'] for item in data], [ + 'search:basic_search', + 'user:login', + 'user:logout', + 'search:basic_search', + 'user:login', + 'user:reg', + 'user:reg', + ]) + + def test_limit_to_object(self): + self.generate_activity_items() + response = self.client.get(reverse( + 'activitystream:get_stream', kwargs={ + 'models': 'auth:user', + 'object_id': self.foo.id, + })) + data = json.loads(response.content.decode('utf-8')) + self.assertEqual(len(data), 4) + self.assertEqual([item['type'] for item in data], [ + 'user:logout', + 'search:basic_search', + 'user:login', + 'user:reg', + ]) + + def test_limit_to_activity_type(self): + self.generate_activity_items() + response = self.client.get(reverse('activitystream:get_stream'), { + 'type': 'search:basic_search', + }) + data = json.loads(response.content.decode('utf-8')) + self.assertEqual(len(data), 3) + self.assertEqual([item['type'] for item in data], [ + 'search:basic_search', + 'search:basic_search', + 'search:basic_search', + ]) + + +class TestSitewideDataView(ActivityBaseTestCase): + def test_results(self): + self.maxDiff = None + response = self.client.get(reverse('activitystream:sitewide_data')) + data = json.loads(response.content.decode('utf-8')) + self.assertEqual(len(data['version']['full']), 40) + data['version']['full'] = 'revno' + self.assertEqual(len(data['version']['short']), 7) + data['version']['short'] = 'revno' + self.assertEqual(data, { + u'adminflags': 0, + u'ads': {u'live': 0, u'total': 0}, + u'comments': 0, + u'enjoys': 0, + u'favorites': 0, + u'folders': 0, + u'friendgroups': 0, + u'groups': {}, + u'promotions': {u'highlight': 0, + u'paid_promotions': 0, + u'promotions': 0}, + u'publishers': 0, + u'ratings': {u'1-star': 0, + u'2-star': 0, + u'3-star': 0, + u'4-star': 0, + u'5-star': 0, + u'total': 0}, + u'submissions': 0, + u'tags': {u'taggeditems': 0, u'tags': 0}, + u'users': {u'all': 2, u'staff': 0, u'superusers': 0}, + u'version': {u'full': u'revno', + u'short': u'revno', + u'version': u'pre-release'} + }) diff --git a/activitystream/urls.py b/activitystream/urls.py new file mode 100644 index 0000000..6165c99 --- /dev/null +++ b/activitystream/urls.py @@ -0,0 +1,13 @@ +from django.conf.urls import url + +from . import views + + +app_name = 'activitystream' +urlpatterns = [ + url('^$', views.sitewide_data, name='sitewide_data'), + url('^stream/$', views.get_stream, name='get_stream'), + url('^stream/(?P[\w_:]+)/$', views.get_stream, name='get_stream'), + url('^stream/(?P[\w_:]+)/(?P\d+)/$', + views.get_stream, name='get_stream'), +] diff --git a/activitystream/views.py b/activitystream/views.py new file mode 100644 index 0000000..afb9db8 --- /dev/null +++ b/activitystream/views.py @@ -0,0 +1,120 @@ +import json + +from django.contrib.auth.models import ( + Group, + User, +) +from django.contrib.contenttypes.models import ContentType +from django.http import (HttpResponse) +from django.views.decorators.cache import cache_page +from taggit.models import ( + Tag, + TaggedItem, +) + +from .models import Activity +from administration.models import Flag +from core.templatetags.git_revno import git_revno +from promotion.models import ( + Ad, + AdLifecycle, + Promotion, +) +from publishers.models import PublisherPage +from social.models import ( + Comment, + EnjoyItem, + Rating, +) +from submissions.models import ( + Folder, + Submission, +) +from usermgmt.group_models import FriendGroup + + +def generate_stream_entry(activity): + entry = { + 'time': activity.activity_time.strftime('%Y-%m-%dT%H:%M:%S'), + 'type': activity.activity_type, + } + if activity.content_type: + entry['instance'] = "{}: {}".format( + activity.content_type.name, str(activity.object_model)) + return entry + + +@cache_page(60 * 15) +def get_stream(request, models=None, object_id=None): + stream = Activity.objects.select_related('content_type') + if models: + expanded = [model.split(':') for model in models.split(',')] + ctypes = ContentType.objects.filter( + app_label__in=[i[0] for i in expanded], + model__in=[i[1] for i in expanded]) + stream = stream.filter(content_type__in=ctypes) + if object_id: + if ',' not in models: + stream = stream.filter(object_id=object_id) + if request.GET.get('type') is not None: + stream = stream.filter( + activity_type__in=request.GET['type'].split(',')) + data = [] + for activity in stream: + data.append(generate_stream_entry(activity)) + return HttpResponse( + json.dumps(data, separators=[',', ':']), + content_type='application/json') + + +@cache_page(60 * 60) +def sitewide_data(request): + data = { + 'version': git_revno(), + 'users': { + 'all': User.objects.count(), + 'staff': User.objects.filter(is_staff=True).count(), + 'superusers': User.objects.filter(is_superuser=True).count(), + }, + 'groups': dict([(group.name, group.user_set.count()) + for group in Group.objects.all()]), + 'submissions': Submission.objects.count(), + 'folders': Folder.objects.count(), + 'friendgroups': FriendGroup.objects.count(), + 'ratings': { + 'total': Rating.objects.count(), + '1-star': Rating.objects.filter(rating=1).count(), + '2-star': Rating.objects.filter(rating=2).count(), + '3-star': Rating.objects.filter(rating=3).count(), + '4-star': Rating.objects.filter(rating=4).count(), + '5-star': Rating.objects.filter(rating=5).count(), + }, + 'favorites': + Activity.objects.filter(activity_type='SOCIAL_FAVORITE').count() - + Activity.objects.filter(activity_type='SOCIAL_UNFAVORITE').count(), + 'enjoys': EnjoyItem.objects.count(), + 'comments': Comment.objects.count(), + 'publishers': PublisherPage.objects.count(), + 'promotions': { + 'promotions': + Promotion.objects.filter( + promotion_type=Promotion.PROMOTION).count(), + 'paid_promotions': + Promotion.objects.filter( + promotion_type=Promotion.PAID_PROMOTION).count(), + 'highlight': Promotion.objects.filter( + promotion_type=Promotion.HIGHLIGHT).count(), + }, + 'ads': { + 'total': Ad.objects.count(), + 'live': AdLifecycle.objects.filter(live=True).count(), + }, + 'tags': { + 'tags': Tag.objects.count(), + 'taggeditems': TaggedItem.objects.count(), + }, + 'adminflags': Flag.objects.count(), + } + return HttpResponse( + json.dumps(data, separators=[',', ':']), + content_type='application/json') diff --git a/admin/migrations/__init__.py b/administration/__init__.py similarity index 100% rename from admin/migrations/__init__.py rename to administration/__init__.py diff --git a/administration/admin.py b/administration/admin.py new file mode 100644 index 0000000..4185d36 --- /dev/null +++ b/administration/admin.py @@ -0,0 +1,3 @@ +# from django.contrib import admin + +# Register your models here. diff --git a/admin/apps.py b/administration/apps.py similarity index 100% rename from admin/apps.py rename to administration/apps.py diff --git a/administration/migrations/0001_initial.py b/administration/migrations/0001_initial.py new file mode 100644 index 0000000..16a91c8 --- /dev/null +++ b/administration/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-10-30 04:16 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Flag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('created', models.DateTimeField(auto_now_add=True)), + ('resolved', models.DateTimeField(null=True)), + ('subject', models.CharField(max_length=100)), + ('body', models.TextField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('flagged_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/administration/migrations/__init__.py b/administration/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/admin/models.py b/administration/models.py similarity index 100% rename from admin/models.py rename to administration/models.py diff --git a/admin/tests.py b/administration/tests.py similarity index 100% rename from admin/tests.py rename to administration/tests.py diff --git a/admin/views.py b/administration/views.py similarity index 100% rename from admin/views.py rename to administration/views.py diff --git a/core/management/commands/git_revno.py b/core/management/commands/git_revno.py index e680576..4a4ec7b 100644 --- a/core/management/commands/git_revno.py +++ b/core/management/commands/git_revno.py @@ -5,8 +5,14 @@ class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument('tag', nargs='?', type=str, + default='pre-release') + def handle(self, *args, **kwargs): - output = check_output( - ['git', 'rev-parse', '--verify', 'HEAD']).strip()[-7:] - with open(os.sep.join(['core', 'templates', 'git_revno']), 'w') as f: - f.write(output) + revno = check_output( + ['git', 'rev-parse', '--verify', 'HEAD']).strip() + + with open(os.sep.join(['honeycomb', 'revno.py']), 'w') as f: + f.write("GIT_REVNO = '{}'\nVERSION = '{}'\n".format( + revno, kwargs['tag'])) diff --git a/core/management/commands/rotate_activitystream.py b/core/management/commands/rotate_activitystream.py new file mode 100644 index 0000000..6b64f95 --- /dev/null +++ b/core/management/commands/rotate_activitystream.py @@ -0,0 +1,5 @@ +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + pass diff --git a/core/management/commands/toggle_ads.py b/core/management/commands/toggle_ads.py index 6b64f95..d9a6bd9 100644 --- a/core/management/commands/toggle_ads.py +++ b/core/management/commands/toggle_ads.py @@ -2,4 +2,5 @@ class Command(BaseCommand): + # Don't forget to add ad:golive/ad:retire activities pass diff --git a/core/management/commands/toggle_promotions.py b/core/management/commands/toggle_promotions.py index 6b64f95..e26e6f8 100644 --- a/core/management/commands/toggle_promotions.py +++ b/core/management/commands/toggle_promotions.py @@ -2,4 +2,5 @@ class Command(BaseCommand): + # Don't forget to add a promotion:retire activity pass diff --git a/core/static/app/base.css b/core/static/app/base.css index d6ed2f2..c882a20 100644 --- a/core/static/app/base.css +++ b/core/static/app/base.css @@ -112,4 +112,10 @@ footer img { footer div { text-align: right; } + footer .first-footer { + padding-top: 1em !important; + } + footer .gh-corner svg { + z-index: 1000; + } } diff --git a/core/templates/base.html b/core/templates/base.html index 06db76f..8a674e2 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -153,16 +153,16 @@

{% if flatpage %}{{ flatpage.title }}{% else %}{{ title|safe }}{% endif %}{% {% block content %}{% endblock %}