From cd34bb0de717ceffe42c1ca9fd8bfb026c017862 Mon Sep 17 00:00:00 2001 From: Madison Scott-Clary Date: Sun, 30 Oct 2016 01:38:30 -0600 Subject: [PATCH 1/8] First pass at activitystreams Fixes #27 --- Makefile | 3 +- activitystream/__init__.py | 1 + {admin => activitystream}/admin.py | 0 activitystream/apps.py | 10 ++ activitystream/migrations/0001_initial.py | 31 +++++ .../migrations/0002_auto_20161030_0719.py | 24 ++++ .../migrations/0003_auto_20161030_0723.py | 20 +++ .../migrations/0004_auto_20161030_0723.py | 21 +++ .../migrations/0005_auto_20161030_0724.py | 26 ++++ .../migrations}/__init__.py | 0 activitystream/models.py | 94 ++++++++++++++ activitystream/signals.py | 73 +++++++++++ {admin => activitystream}/tests.py | 0 activitystream/urls.py | 9 ++ activitystream/views.py | 121 ++++++++++++++++++ .../migrations => administration}/__init__.py | 0 administration/admin.py | 3 + {admin => administration}/apps.py | 0 administration/migrations/0001_initial.py | 33 +++++ administration/migrations/__init__.py | 0 {admin => administration}/models.py | 0 administration/tests.py | 3 + {admin => administration}/views.py | 0 .../commands/rotate_activitystream.py | 6 + core/views.py | 10 ++ db.sqlite3 | Bin 479232 -> 499712 bytes honeycomb/settings.py | 28 +++- honeycomb/urls.py | 1 + .../0003_promotion_promotion_end_date.py | 20 +++ promotion/models.py | 3 + tox.ini | 2 +- 31 files changed, 539 insertions(+), 3 deletions(-) create mode 100644 activitystream/__init__.py rename {admin => activitystream}/admin.py (100%) create mode 100644 activitystream/apps.py create mode 100644 activitystream/migrations/0001_initial.py create mode 100644 activitystream/migrations/0002_auto_20161030_0719.py create mode 100644 activitystream/migrations/0003_auto_20161030_0723.py create mode 100644 activitystream/migrations/0004_auto_20161030_0723.py create mode 100644 activitystream/migrations/0005_auto_20161030_0724.py rename {admin => activitystream/migrations}/__init__.py (100%) create mode 100644 activitystream/models.py create mode 100644 activitystream/signals.py rename {admin => activitystream}/tests.py (100%) create mode 100644 activitystream/urls.py create mode 100644 activitystream/views.py rename {admin/migrations => administration}/__init__.py (100%) create mode 100644 administration/admin.py rename {admin => administration}/apps.py (100%) create mode 100644 administration/migrations/0001_initial.py create mode 100644 administration/migrations/__init__.py rename {admin => administration}/models.py (100%) create mode 100644 administration/tests.py rename {admin => administration}/views.py (100%) create mode 100644 core/management/commands/rotate_activitystream.py create mode 100644 promotion/migrations/0003_promotion_promotion_end_date.py diff --git a/Makefile b/Makefile index b28300c..376b39c 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 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..aba8159 --- /dev/null +++ b/activitystream/models.py @@ -0,0 +1,94 @@ +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'), + ('USER:PWRESET', 'User: password reset'), + ('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'), + ('GROUP:VIEW', 'Group: viewed'), + + # Social interactions + ('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'), + + # 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:RETIRED', 'Promotion: 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:SEARCH', '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.upper(), action.upper()) + if item_type not in dict(Activity.ACTIVITY_TYPES): + return # XXX should we fail silently? + activity = cls(activity_type=item_type) + activity.object_model = object_model + activity.save() + + class Meta: + ordering = ['-activity_time'] diff --git a/activitystream/signals.py b/activitystream/signals.py new file mode 100644 index 0000000..8e01099 --- /dev/null +++ b/activitystream/signals.py @@ -0,0 +1,73 @@ +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', + 'Submission': 'submission', + 'PublisherPage': 'publisher', + }[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/admin/tests.py b/activitystream/tests.py similarity index 100% rename from admin/tests.py rename to activitystream/tests.py diff --git a/activitystream/urls.py b/activitystream/urls.py new file mode 100644 index 0000000..ade8f4d --- /dev/null +++ b/activitystream/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import url + +from . import views + + +urlpatterns = [ + url('^$', views.sitewide_data, name='sitewide_data'), + url('^stream/$', views.get_stream, name='get_stream'), +] diff --git a/activitystream/views.py b/activitystream/views.py new file mode 100644 index 0000000..17c35dd --- /dev/null +++ b/activitystream/views.py @@ -0,0 +1,121 @@ +import json + +from django.contrib.auth.models import ( + Group, + User, +) +from django.contrib.contenttypes.models import ContentType +from django.http import (HttpResponse) +from django.shortcuts import get_object_or_404 +from django.views.decorators.cache import cache_page +from taggit.models import ( + Tag, + TaggedItem, +) + +from .models import Activity +from administration.models import Flag +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, ctype, instance): + entry = { + 'time': activity.activity_time.strftime('%Y-%m-%dT%H:%M:%S'), + 'type': activity.activity_type, + } + if instance is not None: + entry['instance'] = "{}: {}".format(ctype.name, str(instance)) + elif activity.content_type: + entry['instance'] = "{}: {}".format( + activity.content_type.name, str(activity.object_model)) + return entry + + +@cache_page(60 * 5) +def get_stream(request, app_label=None, model=None, object_id=None): + ctype = None + instance = None + stream = Activity.objects.select_related('content_type') + if app_label and model: + ctype = get_object_or_404(ContentType, + app_label=app_label, model=model) + stream = stream.filter(content_type=ctype) + if object_id: + stream = stream.filter(object_id=object_id) + instance = ctype.get_object_for_this_type(pk=object_id) + if request.GET.get('type') is not None: + stream = stream.filter(activity_type=request.GET['type']) + data = [] + for activity in stream: + print(ctype) + data.append(generate_stream_entry(activity, ctype, instance)) + return HttpResponse( + json.dumps(data, separators=[',', ':']), + content_type='application/json') + + +@cache_page(60 * 60) +def sitewide_data(request): + data = { + '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/administration/tests.py b/administration/tests.py new file mode 100644 index 0000000..a79ca8b --- /dev/null +++ b/administration/tests.py @@ -0,0 +1,3 @@ +# from django.test import TestCase + +# Create your tests here. 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/rotate_activitystream.py b/core/management/commands/rotate_activitystream.py new file mode 100644 index 0000000..1ba6b5c --- /dev/null +++ b/core/management/commands/rotate_activitystream.py @@ -0,0 +1,6 @@ +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + # Don't forget to save a PROMOTION:RETIRED activity + pass diff --git a/core/views.py b/core/views.py index 62d8975..8b64a0a 100644 --- a/core/views.py +++ b/core/views.py @@ -3,6 +3,8 @@ from haystack.generic_views import SearchView from haystack.forms import SearchForm +from activitystream.models import Activity + def front(request): return render(request, 'front.html', {}) @@ -18,3 +20,11 @@ def flatpage_list(request): class BasicSearchView(SearchView): template_name = 'search/search.html' form_class = SearchForm + + def get(self, request, *args, **kwargs): + if request.GET.get('page') is None: + Activity.create( + 'search', + 'search', + request.user if request.user.is_authenticated else None) + return super(BasicSearchView, self).get(request, *args, **kwargs) diff --git a/db.sqlite3 b/db.sqlite3 index b62035b8b2bd18b08332318e9bfe9b779beb5910..9cdf534ccde393e98348eb97a020679be77e1c7c 100644 GIT binary patch delta 9756 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 O-#&0}(g5;Q*M9?>^L@$y delta 7731 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 I)u`_O23*ps00000 diff --git a/honeycomb/settings.py b/honeycomb/settings.py index 6b76c45..5b1c07b 100644 --- a/honeycomb/settings.py +++ b/honeycomb/settings.py @@ -38,6 +38,8 @@ 'social', 'publishers', 'promotion', + 'activitystream', + 'administration', 'taggit', 'haystack', 'django_nose', @@ -147,13 +149,37 @@ # Additional configuration for Honeycomb +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'dev-cache', + } + # TODO production should use memcached + # 'default': { + # 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + # 'LOCATION': '127.0.0.1:11211', + # } +} HAYSTACK_CONNECTIONS = { 'default': { - 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine', + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', }, + # 'default': { + # 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine', + # }, + # TODO production should use elasticsearch + # 'default': { + # 'ENGINE': + # ('haystack.backends.elasticsearch_backend.' + # 'ElasticsearchSearchEngine'), + # 'URL': 'http://127.0.0.1:9200/', + # 'INDEX_NAME': 'haystack', + # }, } +TAGGIT_CASE_INSENSITIVE = True SUBMISSION_BASE = ('^~(?P[^/]+)/(?P\d+)-' '(?P[-\w]+)/') +ACTIVITYSTREAM_ROTATION = 30 # Rotation period in days LOGOUT_REDIRECT_URL = "/login/" MESSAGE_TAGS = { messages.DEBUG: 'debug', diff --git a/honeycomb/urls.py b/honeycomb/urls.py index d7e59b3..cf38444 100644 --- a/honeycomb/urls.py +++ b/honeycomb/urls.py @@ -11,4 +11,5 @@ url('^', include('social.urls')), url('^', include('core.urls')), url('^', include('submissions.urls')), + url('^activity/', include('activitystream.urls')), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/promotion/migrations/0003_promotion_promotion_end_date.py b/promotion/migrations/0003_promotion_promotion_end_date.py new file mode 100644 index 0000000..d84e515 --- /dev/null +++ b/promotion/migrations/0003_promotion_promotion_end_date.py @@ -0,0 +1,20 @@ +# -*- 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 = [ + ('promotion', '0002_auto_20161028_0540'), + ] + + operations = [ + migrations.AddField( + model_name='promotion', + name='promotion_end_date', + field=models.DateTimeField(null=True), + ), + ] diff --git a/promotion/models.py b/promotion/models.py index baba37b..f05b8f9 100644 --- a/promotion/models.py +++ b/promotion/models.py @@ -26,6 +26,9 @@ class Promotion(models.Model): # The user promoting the submission promoter = models.ForeignKey(User) + # The date the promotion ends + promotion_end_date = models.DateTimeField(null=True) + class Ad(models.Model): # The ad's owner diff --git a/tox.ini b/tox.ini index f285b7d..054a547 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ commands = coverage erase rm -f coverage-badge.svg coverage run \ - --source 'admin,core,promotion,publishers,social,submissions,usermgmt' \ + --source 'activitystream,administration,core,promotion,publishers,social,submissions,usermgmt' \ --omit '*migrations*,*urls.py,*apps.py,*admin.py,*__init__.py,*test.py' \ manage.py test --verbosity=2 --nologcapture coverage report -m --skip-covered From 95d6194c09a60a817cca9fbf8bd7b36d8eeca745 Mon Sep 17 00:00:00 2001 From: Madison Scott-Clary Date: Sun, 30 Oct 2016 02:13:50 -0600 Subject: [PATCH 2/8] Cleanup --- Makefile | 2 +- activitystream/models.py | 6 +++++- activitystream/urls.py | 3 +++ activitystream/views.py | 27 +++++++++++---------------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 376b39c..829fde3 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +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 + 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 diff --git a/activitystream/models.py b/activitystream/models.py index aba8159..8cb1879 100644 --- a/activitystream/models.py +++ b/activitystream/models.py @@ -62,7 +62,11 @@ class Activity(models.Model): # Promotions ('PROMOTION:CREATE', 'Promotion: created'), - ('PROMOTION:RETIRED', 'Promotion: retired'), + ('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'), diff --git a/activitystream/urls.py b/activitystream/urls.py index ade8f4d..38e295d 100644 --- a/activitystream/urls.py +++ b/activitystream/urls.py @@ -6,4 +6,7 @@ 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 index 17c35dd..7915c5f 100644 --- a/activitystream/views.py +++ b/activitystream/views.py @@ -6,7 +6,6 @@ ) from django.contrib.contenttypes.models import ContentType from django.http import (HttpResponse) -from django.shortcuts import get_object_or_404 from django.views.decorators.cache import cache_page from taggit.models import ( Tag, @@ -33,37 +32,33 @@ from usermgmt.group_models import FriendGroup -def generate_stream_entry(activity, ctype, instance): +def generate_stream_entry(activity): entry = { 'time': activity.activity_time.strftime('%Y-%m-%dT%H:%M:%S'), 'type': activity.activity_type, } - if instance is not None: - entry['instance'] = "{}: {}".format(ctype.name, str(instance)) - elif activity.content_type: + if activity.content_type: entry['instance'] = "{}: {}".format( activity.content_type.name, str(activity.object_model)) return entry -@cache_page(60 * 5) -def get_stream(request, app_label=None, model=None, object_id=None): - ctype = None - instance = None +@cache_page(60 * 15) +def get_stream(request, models=None, object_id=None): stream = Activity.objects.select_related('content_type') - if app_label and model: - ctype = get_object_or_404(ContentType, - app_label=app_label, model=model) - stream = stream.filter(content_type=ctype) + 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: stream = stream.filter(object_id=object_id) - instance = ctype.get_object_for_this_type(pk=object_id) if request.GET.get('type') is not None: stream = stream.filter(activity_type=request.GET['type']) data = [] for activity in stream: - print(ctype) - data.append(generate_stream_entry(activity, ctype, instance)) + data.append(generate_stream_entry(activity)) return HttpResponse( json.dumps(data, separators=[',', ':']), content_type='application/json') From 88601d19e0ad5787673d23cca658d71b43433c2d Mon Sep 17 00:00:00 2001 From: Madison Scott-Clary Date: Sun, 30 Oct 2016 12:34:36 -0600 Subject: [PATCH 3/8] revno work --- CONTRIBUTING.md | 1 - Makefile | 4 + RELEASE.md | 11 +++ activitystream/models.py | 99 +++++++++---------- activitystream/views.py | 5 +- core/management/commands/git_revno.py | 12 ++- .../commands/rotate_activitystream.py | 1 - core/management/commands/toggle_ads.py | 1 + core/management/commands/toggle_promotions.py | 1 + core/static/app/base.css | 6 ++ core/templates/base.html | 6 +- core/templatetags/git_revno.py | 22 +++-- honeycomb/revno.py | 2 + honeycomb/settings.py | 21 ++-- 14 files changed, 117 insertions(+), 75 deletions(-) create mode 100644 RELEASE.md create mode 100644 honeycomb/revno.py 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 829fde3..0e1df6e 100644 --- a/Makefile +++ b/Makefile @@ -42,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/models.py b/activitystream/models.py index 8cb1879..c6572a9 100644 --- a/activitystream/models.py +++ b/activitystream/models.py @@ -8,75 +8,74 @@ 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'), - ('USER:PWRESET', 'User: password reset'), - ('PROFILE:UPDATE', 'User: profile updated'), - ('PROFILE:VIEW', 'User: profile viewed'), + ('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'), + ('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'), - ('GROUP:VIEW', 'Group: viewed'), + ('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:FAVORITE', 'Social: favorite submission'), - ('SOCIAL:UNFAVORITE', 'Social: unfavorite submission'), - ('SOCIAL:RATE', 'Social: rate submission'), - ('SOCIAL:ENJOY', 'Social: enjoy submission'), + ('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'), # Submissions - ('SUBMISSION:CREATE', 'Submission: created'), - ('SUBMISSION:UPDATE', 'Submission: updated'), - ('SUBMISSION:DELETE', 'Submission: deleted'), - ('SUBMISSION:VIEW', 'Submission: viewed'), + ('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'), + ('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'), + ('tag:create', 'tag: tag created'), + ('tag:tag', 'tag: tagged item created'), # Comments - ('COMMENT:CREATE', 'Comment: created'), - ('COMMENT:UPDATE', 'Comment: updated'), - ('COMMENT:DELETE', 'Comment: deleted'), + ('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'), + ('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'), + ('publisher:create', 'publisher: created'), + ('publisher:update', 'publisher: updated'), + ('publisher:delete', 'publisher: deleted'), + ('publisher:view', 'publisher: viewed'), + ('publisher:claimed', 'publisher: claimed'), # Search - ('SEARCH:SEARCH', 'Search: run'), + ('search:search', 'search: run'), ) activity_time = models.DateTimeField(auto_now_add=True) @@ -87,7 +86,7 @@ class Activity(models.Model): @classmethod def create(cls, app, action, object_model): - item_type = "{}:{}".format(app.upper(), action.upper()) + item_type = "{}:{}".format(app.lower(), action.lower()) if item_type not in dict(Activity.ACTIVITY_TYPES): return # XXX should we fail silently? activity = cls(activity_type=item_type) diff --git a/activitystream/views.py b/activitystream/views.py index 7915c5f..dfd8e0c 100644 --- a/activitystream/views.py +++ b/activitystream/views.py @@ -14,6 +14,7 @@ from .models import Activity from administration.models import Flag +from core.templatetags.git_revno import git_revno from promotion.models import ( Ad, AdLifecycle, @@ -55,7 +56,8 @@ def get_stream(request, models=None, object_id=None): if object_id: stream = stream.filter(object_id=object_id) if request.GET.get('type') is not None: - stream = stream.filter(activity_type=request.GET['type']) + stream = stream.filter( + activity_type__in=request.GET['type'].split(',')) data = [] for activity in stream: data.append(generate_stream_entry(activity)) @@ -67,6 +69,7 @@ def get_stream(request, models=None, object_id=None): @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(), diff --git a/core/management/commands/git_revno.py b/core/management/commands/git_revno.py index e680576..335254c 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( + revno = check_output( ['git', 'rev-parse', '--verify', 'HEAD']).strip()[-7:] - with open(os.sep.join(['core', 'templates', 'git_revno']), 'w') as f: - f.write(output) + + 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 index 1ba6b5c..6b64f95 100644 --- a/core/management/commands/rotate_activitystream.py +++ b/core/management/commands/rotate_activitystream.py @@ -2,5 +2,4 @@ class Command(BaseCommand): - # Don't forget to save a PROMOTION:RETIRED activity pass diff --git a/core/management/commands/toggle_ads.py b/core/management/commands/toggle_ads.py index 6b64f95..5779689 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 an AD:RETIRED activity pass diff --git a/core/management/commands/toggle_promotions.py b/core/management/commands/toggle_promotions.py index 6b64f95..26ddf72 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:RETIRED 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 %}