From 3b2db4163a8c7707773fb1276ec5ab05ca02d83c Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Sat, 21 Nov 2020 00:07:41 -0500 Subject: [PATCH 1/9] basics of @slurms idea for Action/Follow models based on settings --- actstream/feeds.py | 20 ++++++++++------ actstream/models.py | 16 ++++++------- actstream/settings.py | 32 +++++++++++++++++++++++++ actstream/templatetags/activity_tags.py | 12 +++++----- actstream/tests/base.py | 8 +++---- actstream/tests/test_activity.py | 23 +++++++++--------- actstream/tests/test_apps.py | 5 ++-- actstream/tests/test_gfk.py | 19 ++++++++------- runtests/testapp/tests.py | 10 ++++---- 9 files changed, 93 insertions(+), 52 deletions(-) diff --git a/actstream/feeds.py b/actstream/feeds.py index 690366ea..6d92c0df 100644 --- a/actstream/feeds.py +++ b/actstream/feeds.py @@ -12,7 +12,8 @@ from django.http import HttpResponse, Http404 from django.urls import reverse -from actstream.models import Action, model_stream, user_stream, any_stream +from actstream.models import model_stream, user_stream, any_stream +from actstream.settings import get_action_model class AbstractActivityStream: @@ -20,11 +21,12 @@ class AbstractActivityStream: Abstract base class for all stream rendering. Supports hooks for fetching streams and formatting actions. """ + def get_stream(self, *args, **kwargs): """ Returns a stream method to use. """ - raise NotImplementedError + raise NotImplementedError def get_object(self, *args, **kwargs): """ @@ -46,7 +48,7 @@ def get_uri(self, action, obj=None, date=None): date = action.timestamp date = datetime_safe.new_datetime(date).strftime('%Y-%m-%d') return 'tag:{},{}:{}'.format(Site.objects.get_current().domain, date, - self.get_url(action, obj, False)) + self.get_url(action, obj, False)) def get_url(self, action, obj=None, domain=True): """ @@ -119,6 +121,7 @@ class ActivityStreamsAtomFeed(Atom1Feed): """ Feed rendering class for the v1.0 Atom Activity Stream Spec """ + def root_attributes(self): attrs = super(ActivityStreamsAtomFeed, self).root_attributes() attrs['xmlns:activity'] = 'http://activitystrea.ms/spec/1.0/' @@ -208,6 +211,7 @@ class JSONActivityFeed(AbstractActivityStream, View): """ Feed that generates feeds compatible with the v1.0 JSON Activity Stream spec """ + def dispatch(self, request, *args, **kwargs): return HttpResponse(self.serialize(request, *args, **kwargs), content_type='application/json') @@ -242,11 +246,12 @@ def get_object(self, request, content_type_id, object_id): def get_stream(self): return any_stream + class StreamKwargsMixin: - + def items(self, request, *args, **kwargs): - return self.get_stream()(self.get_object(request, *args, **kwargs),**self.get_stream_kwargs(request)) - + return self.get_stream()(self.get_object(request, *args, **kwargs), **self.get_stream_kwargs(request)) + class UserActivityMixin: @@ -263,6 +268,7 @@ def get_stream_kwargs(self, request): stream_kwargs['with_user_activity'] = request.GET['with_user_activity'].lower() == 'true' return stream_kwargs + class CustomStreamMixin: name = None @@ -270,7 +276,7 @@ def get_object(self): return def get_stream(self): - return getattr(Action.objects, self.name) + return getattr(get_action_model().objects, self.name) def items(self, *args, **kwargs): return self.get_stream()(*args[1:], **kwargs) diff --git a/actstream/models.py b/actstream/models.py index 8c122de6..5bbe7f24 100644 --- a/actstream/models.py +++ b/actstream/models.py @@ -166,11 +166,11 @@ def get_absolute_url(self): # convenient accessors -actor_stream = Action.objects.actor -action_object_stream = Action.objects.action_object -target_stream = Action.objects.target -user_stream = Action.objects.user -model_stream = Action.objects.model_actions -any_stream = Action.objects.any -followers = Follow.objects.followers -following = Follow.objects.following +actor_stream = actstream_settings.get_action_model().objects.actor +action_object_stream = actstream_settings.get_action_model().objects.action_object +target_stream = actstream_settings.get_action_model().objects.target +user_stream = actstream_settings.get_action_model().objects.user +model_stream = actstream_settings.get_action_model().objects.model_actions +any_stream = actstream_settings.get_action_model().objects.any +followers = actstream_settings.get_follow_model().objects.followers +following = actstream_settings.get_follow_model().objects.following diff --git a/actstream/settings.py b/actstream/settings.py index a86c1884..3edb6e2f 100644 --- a/actstream/settings.py +++ b/actstream/settings.py @@ -1,4 +1,8 @@ +from functools import lru_cache + +from django.apps import apps from django.conf import settings +from django.core.exceptions import ImproperlyConfigured SETTINGS = getattr(settings, 'ACTSTREAM_SETTINGS', {}) @@ -23,3 +27,31 @@ def get_action_manager(): FETCH_RELATIONS = SETTINGS.get('FETCH_RELATIONS', True) USE_JSONFIELD = SETTINGS.get('USE_JSONFIELD', False) + +FOLLOW_MODEL = SETTINGS.get('ACTSTREAM_FOLLOW_MODEL', 'actstream.Follow') +ACTION_MODEL = SETTINGS.get('ACTSTREAM_ACTION_MODEL', 'actstream.Action') + + +def get_swappable_model(model_lookup): + try: + return apps.get_model(model_lookup, require_ready=False) + except ValueError: + raise ImproperlyConfigured( + "%s must be of the form 'app_label.model_name'" % model_lookup + ) + except LookupError: + raise ImproperlyConfigured( + "Model '%s' has not been installed" % model_lookup + ) + + +@lru_cache(maxsize=None) +def get_follow_model(): + """Return the Follow model that is active.""" + return get_swappable_model(FOLLOW_MODEL) + + +@lru_cache(maxsize=None) +def get_action_model(): + """Return the Action model that is active.""" + return get_swappable_model(ACTION_MODEL) diff --git a/actstream/templatetags/activity_tags.py b/actstream/templatetags/activity_tags.py index 17e94728..f0a1fcba 100644 --- a/actstream/templatetags/activity_tags.py +++ b/actstream/templatetags/activity_tags.py @@ -3,7 +3,7 @@ from django.template.loader import render_to_string from django.urls import reverse -from actstream.models import Follow, Action +from actstream.settings import get_follow_model, get_action_model register = Library() @@ -27,7 +27,7 @@ def render(self, context): if self.flag: kwargs['flag'] = self.flag - if Follow.objects.is_following(context.get('user'), actor_instance, flag=self.flag): + if get_follow_model().objects.is_following(context.get('user'), actor_instance, flag=self.flag): return reverse('actstream_unfollow', kwargs=kwargs) if self.actor_only: return reverse('actstream_follow', kwargs=kwargs) @@ -121,7 +121,7 @@ def is_following(user, actor): You are already following {{ another_user }} {% endif %} """ - return Follow.objects.is_following(user, actor) + return get_follow_model().objects.is_following(user, actor) class IsFollowing(AsNode): @@ -132,7 +132,7 @@ def render_result(self, context): actor = self.args[1].resolve(context) flag = self.args[2].resolve(context) - return Follow.objects.is_following(user, actor, flag=flag) + return get_follow_model().objects.is_following(user, actor, flag=flag) def is_following_tag(parser, token): @@ -252,10 +252,10 @@ def activity_stream(context, stream_type, *args, **kwargs): """ if stream_type == 'model': stream_type = 'model_actions' - if not hasattr(Action.objects, stream_type): + if not hasattr(get_action_model().objects, stream_type): raise TemplateSyntaxError('Action manager has no attribute: %s' % stream_type) ctxvar = kwargs.pop('as', 'stream') - context[ctxvar] = getattr(Action.objects, stream_type)(*args, **kwargs) + context[ctxvar] = getattr(get_action_model().objects, stream_type)(*args, **kwargs) return '' diff --git a/actstream/tests/base.py b/actstream/tests/base.py index cb323345..f1732731 100644 --- a/actstream/tests/base.py +++ b/actstream/tests/base.py @@ -12,7 +12,7 @@ from django.contrib.contenttypes.models import ContentType from django.urls import reverse -from actstream.models import Action, Follow +from actstream.settings import get_follow_model, get_action_model from actstream.registry import register, unregister from actstream.actions import follow from actstream.signals import action @@ -63,12 +63,12 @@ def tearDown(self): model = apps.get_model(*model.split('.')) unregister(model) model.objects.all().delete() - Action.objects.all().delete() - Follow.objects.all().delete() + get_action_model().objects.all().delete() + get_follow_model().objects.all().delete() self.User.objects.all().delete() def capture(self, viewname, *args, query_string=''): - response = self.client.get('{}?{}'.format(reverse(viewname, args=args),query_string)) + response = self.client.get('{}?{}'.format(reverse(viewname, args=args), query_string)) content = response.content.decode() if response['Content-Type'] == 'application/json': return loads(content) diff --git a/actstream/tests/test_activity.py b/actstream/tests/test_activity.py index b9247563..cf09be50 100644 --- a/actstream/tests/test_activity.py +++ b/actstream/tests/test_activity.py @@ -6,11 +6,12 @@ from django.utils.translation import activate, get_language from django.urls import reverse -from actstream.models import (Action, Follow, model_stream, user_stream, +from actstream.models import (model_stream, user_stream, actor_stream, following, followers) from actstream.actions import follow, unfollow from actstream.signals import action from actstream.tests.base import DataTestCase, render +from actstream.settings import get_follow_model, get_action_model class ActivityTestCase(DataTestCase): @@ -120,20 +121,20 @@ def test_doesnt_generate_duplicate_follow_records(self): f1 = follow(s, g) self.assertTrue(f1 is not None, "Should have received a new follow " "record") - self.assertTrue(isinstance(f1, Follow), "Returns a Follow object") + self.assertTrue(isinstance(f1, get_follow_model()), "Returns a Follow object") - follows = Follow.objects.filter(user=s, object_id=g.pk, - content_type=self.group_ct) + follows = get_follow_model().objects.filter(user=s, object_id=g.pk, + content_type=self.group_ct) self.assertEqual(1, follows.count(), "Should only have 1 follow record here") f2 = follow(s, g) - follows = Follow.objects.filter(user=s, object_id=g.pk, - content_type=self.group_ct) + follows = get_follow_model().objects.filter(user=s, object_id=g.pk, + content_type=self.group_ct) self.assertEqual(1, follows.count(), "Should still only have 1 follow record here") self.assertTrue(f2 is not None, "Should have received a Follow object") - self.assertTrue(isinstance(f2, Follow), "Returns a Follow object") + self.assertTrue(isinstance(f2, get_follow_model()), "Returns a Follow object") self.assertEqual(f1, f2, "Should have received the same Follow " "object that I first submitted") @@ -143,9 +144,9 @@ def test_following_models_OR_query(self): following(self.user1, Group, self.User), domap=False) def test_y_no_orphaned_follows(self): - follows = Follow.objects.count() + follows = get_follow_model().objects.count() self.user2.delete() - self.assertEqual(follows - 1, Follow.objects.count()) + self.assertEqual(follows - 1, get_follow_model().objects.count()) def test_z_no_orphaned_actions(self): actions = self.user1.actor_actions.count() @@ -257,7 +258,7 @@ def test_is_following_tag_with_verb_variable(self): self.assertEqual(render(src, user=self.user1, group=self.another_group, verb='liking'), '') def test_none_returns_an_empty_queryset(self): - qs = Action.objects.none() + qs = get_action_model().objects.none() self.assertFalse(qs.exists()) self.assertEqual(qs.count(), 0) @@ -274,5 +275,5 @@ def test_store_untranslated_string(self): self.assertEqual(verb, 'Anglais') action.send(self.user1, verb=verb, action_object=self.comment, target=self.group, timestamp=self.testdate) - self.assertTrue(Action.objects.filter(verb='English').exists()) + self.assertTrue(get_action_model().objects.filter(verb='English').exists()) activate(lang) diff --git a/actstream/tests/test_apps.py b/actstream/tests/test_apps.py index e2b8e2b8..3e8a1508 100644 --- a/actstream/tests/test_apps.py +++ b/actstream/tests/test_apps.py @@ -2,6 +2,8 @@ from django.apps.registry import apps +from actstream.settings import get_action_model + class ActstreamConfigTestCase(TestCase): @@ -10,8 +12,7 @@ def test_data_field_is_added_to_action_class_only_once_even_if_app_is_loaded_aga actstream_config.ready() actstream_config.ready() - from actstream.models import Action - data_fields = [field for field in Action._meta.fields if field.name == 'data'] + data_fields = [field for field in get_action_model()._meta.fields if field.name == 'data'] self.assertEqual( len(data_fields), 1 diff --git a/actstream/tests/test_gfk.py b/actstream/tests/test_gfk.py index 69fddaaf..7fb5a98e 100644 --- a/actstream/tests/test_gfk.py +++ b/actstream/tests/test_gfk.py @@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import Group -from actstream.models import Action +from actstream.settings import get_action_model from actstream.tests.base import LTE @@ -18,28 +18,28 @@ def setUp(self): self.user2, _ = User.objects.get_or_create(username='Two') self.user3, _ = User.objects.get_or_create(username='Three') self.user4, _ = User.objects.get_or_create(username='Four') - Action.objects.get_or_create( + get_action_model().objects.get_or_create( actor_content_type=self.user_ct, actor_object_id=self.user1.id, verb='followed', target_content_type=self.user_ct, target_object_id=self.user2.id ) - Action.objects.get_or_create( + get_action_model().objects.get_or_create( actor_content_type=self.user_ct, actor_object_id=self.user1.id, verb='followed', target_content_type=self.user_ct, target_object_id=self.user3.id ) - Action.objects.get_or_create( + get_action_model().objects.get_or_create( actor_content_type=self.user_ct, actor_object_id=self.user1.id, verb='followed', target_content_type=self.user_ct, target_object_id=self.user4.id ) - Action.objects.get_or_create( + get_action_model().objects.get_or_create( actor_content_type=self.user_ct, actor_object_id=self.user1.id, verb='joined', @@ -49,9 +49,10 @@ def setUp(self): def test_fetch_generic_relations(self): # baseline without fetch_generic_relations - _actions = Action.objects.filter(actor_content_type=self.user_ct, - actor_object_id=self.user1.id) - actions = lambda: _actions._clone() + _actions = get_action_model().objects.filter(actor_content_type=self.user_ct, + actor_object_id=self.user1.id) + + def actions(): return _actions._clone() num_content_types = len(set(actions().values_list( 'target_content_type_id', flat=True))) n = actions().count() @@ -86,7 +87,7 @@ def test_fetch_generic_relations(self): action_actor_targets_fetch_generic_all) # fetch only 1 generic relation, but access both gfks - generic = lambda: actions().fetch_generic_relations('target') + def generic(): return actions().fetch_generic_relations('target') self.assertNumQueries(LTE(n + num_content_types + 2), lambda: [ (a.actor, a.target) for a in generic()]) action_actor_targets_fetch_generic_target = [ diff --git a/runtests/testapp/tests.py b/runtests/testapp/tests.py index 611e9770..35428565 100644 --- a/runtests/testapp/tests.py +++ b/runtests/testapp/tests.py @@ -4,9 +4,9 @@ from actstream.signals import action from actstream.registry import register, unregister -from actstream.models import Action, actor_stream, model_stream +from actstream.models import actor_stream, model_stream from actstream.tests.base import render, ActivityBaseTestCase -from actstream.settings import USE_JSONFIELD +from actstream.settings import USE_JSONFIELD, get_action_model from testapp.models import Abstract, Unregistered @@ -18,9 +18,9 @@ def setUp(self): action.send(self.user, verb='was created') def test_accessor(self): - self.assertEqual(len(Action.objects.testfoo(self.user)), 1) + self.assertEqual(len(get_action_model().objects.testfoo(self.user)), 1) self.assertEqual( - len(Action.objects.testfoo(self.user, datetime(1970, 1, 1))), + len(get_action_model().objects.testfoo(self.user, datetime(1970, 1, 1))), 0 ) @@ -73,7 +73,7 @@ def test_jsonfield(self): tags=['sayings'], more_data={'pk': self.user.pk} ) - newaction = Action.objects.filter(verb='said')[0] + newaction = get_action_model().objects.filter(verb='said')[0] self.assertEqual(newaction.data['text'], 'foobar') self.assertEqual(newaction.data['tags'], ['sayings']) self.assertEqual(newaction.data['more_data'], {'pk': self.user.pk}) From a1267aa6676d89c7425709419bba314410cd1a9e Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Tue, 23 Mar 2021 21:25:23 -0400 Subject: [PATCH 2/9] wip: swappable model refactor --- actstream/actions.py | 17 ++++++++++++----- actstream/apps.py | 2 +- actstream/managers.py | 6 +++--- actstream/models.py | 22 +++++++++++++++++----- actstream/registry.py | 5 +++-- actstream/settings.py | 14 ++++---------- runtests/settings.py | 1 + tox.ini | 2 +- 8 files changed, 42 insertions(+), 27 deletions(-) diff --git a/actstream/actions.py b/actstream/actions.py index 7a18e708..7919d56d 100644 --- a/actstream/actions.py +++ b/actstream/actions.py @@ -1,7 +1,7 @@ -from django.apps import apps from django.utils.translation import ugettext_lazy as _ from django.utils.timezone import now from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import FieldDoesNotExist from actstream import settings from actstream.signals import action @@ -32,7 +32,7 @@ def follow(user, obj, send_action=True, actor_only=True, flag='', **kwargs): follow(request.user, group, actor_only=False, flag='liking') """ check(obj) - instance, created = apps.get_model('actstream', 'follow').objects.get_or_create( + instance, created = settings.get_follow_model().objects.get_or_create( user=user, object_id=obj.pk, flag=flag, content_type=ContentType.objects.get_for_model(obj), actor_only=actor_only @@ -60,7 +60,7 @@ def unfollow(user, obj, send_action=False, flag=''): unfollow(request.user, other_user, flag='watching') """ check(obj) - qs = apps.get_model('actstream', 'follow').objects.filter( + qs = settings.get_follow_model().objects.filter( user=user, object_id=obj.pk, content_type=ContentType.objects.get_for_model(obj) ) @@ -91,7 +91,7 @@ def is_following(user, obj, flag=''): """ check(obj) - qs = apps.get_model('actstream', 'follow').objects.filter( + qs = settings.get_follow_model().objects.filter( user=user, object_id=obj.pk, content_type=ContentType.objects.get_for_model(obj) ) @@ -114,7 +114,7 @@ def action_handler(verb, **kwargs): if hasattr(verb, '_proxy____args'): verb = verb._proxy____args[0] - newaction = apps.get_model('actstream', 'action')( + newaction = settings.get_action_model()( actor_content_type=ContentType.objects.get_for_model(actor), actor_object_id=actor.pk, verb=str(verb), @@ -130,6 +130,13 @@ def action_handler(verb, **kwargs): setattr(newaction, '%s_object_id' % opt, obj.pk) setattr(newaction, '%s_content_type' % opt, ContentType.objects.get_for_model(obj)) + for attr in list(kwargs.keys()): + try: + settings.get_action_model()._meta.get_field(attr) + except FieldDoesNotExist: + pass + else: + setattr(newaction, attr, kwargs.pop(attr)) if settings.USE_JSONFIELD and len(kwargs): newaction.data = kwargs newaction.save(force_insert=True) diff --git a/actstream/apps.py b/actstream/apps.py index de3120aa..868a9663 100644 --- a/actstream/apps.py +++ b/actstream/apps.py @@ -10,7 +10,7 @@ class ActstreamConfig(AppConfig): def ready(self): from actstream.actions import action_handler action.connect(action_handler, dispatch_uid='actstream.models') - action_class = self.get_model('action') + action_class = settings.get_action_model() if settings.USE_JSONFIELD and not hasattr(action_class, 'data'): from actstream.jsonfield import DataField, register_app diff --git a/actstream/managers.py b/actstream/managers.py index 7291e441..72ae8011 100644 --- a/actstream/managers.py +++ b/actstream/managers.py @@ -1,4 +1,3 @@ -from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.db.models import Q @@ -6,6 +5,7 @@ from actstream.gfk import GFKManager from actstream.decorators import stream from actstream.registry import check +from actstream.settings import get_follow_model class ActionManager(GFKManager): @@ -97,10 +97,10 @@ def user(self, obj, with_user_activity=False, follow_flag=None, **kwargs): actor_object_id=obj.pk ) - follows = apps.get_model('actstream', 'follow').objects.filter(user=obj) + follows = get_follow_model().objects.filter(user=obj) if follow_flag: follows = follows.filter(flag=follow_flag) - + content_types = ContentType.objects.filter( pk__in=follows.values('content_type_id') ) diff --git a/actstream/models.py b/actstream/models.py index 5bbe7f24..973bbebb 100644 --- a/actstream/models.py +++ b/actstream/models.py @@ -13,7 +13,7 @@ from actstream.managers import FollowManager -class Follow(models.Model): +class AbstractFollow(models.Model): """ Lets a user follow the activities of any specific actor """ @@ -36,13 +36,14 @@ class Follow(models.Model): objects = FollowManager() class Meta: + abstract = True unique_together = ('user', 'content_type', 'object_id', 'flag') def __str__(self): return '{} -> {} : {}'.format(self.user, self.follow_object, self.flag) -class Action(models.Model): +class AbstractAction(models.Model): """ Action model describing the actor acting out a verb (on an optional target). @@ -72,7 +73,7 @@ class Action(models.Model): """ actor_content_type = models.ForeignKey( - ContentType, related_name='actor', + ContentType, related_name='%(app_label)s_actor', on_delete=models.CASCADE, db_index=True ) actor_object_id = models.CharField(max_length=255, db_index=True) @@ -83,7 +84,7 @@ class Action(models.Model): target_content_type = models.ForeignKey( ContentType, blank=True, null=True, - related_name='target', + related_name='%(app_label)s_target', on_delete=models.CASCADE, db_index=True ) target_object_id = models.CharField( @@ -96,7 +97,7 @@ class Action(models.Model): action_object_content_type = models.ForeignKey( ContentType, blank=True, null=True, - related_name='action_object', + related_name='%(app_label)s_action_object', on_delete=models.CASCADE, db_index=True ) action_object_object_id = models.CharField( @@ -114,6 +115,7 @@ class Action(models.Model): objects = actstream_settings.get_action_manager() class Meta: + abstract = True ordering = ('-timestamp',) def __str__(self): @@ -165,6 +167,16 @@ def get_absolute_url(self): 'actstream.views.detail', [self.pk]) +class Follow(AbstractFollow): + class Meta(AbstractFollow.Meta): + swappable = 'ACTSTREAM_FOLLOW_MODEL' + + +class Action(AbstractAction): + class Meta(AbstractAction.Meta): + swappable = 'ACTSTREAM_ACTION_MODEL' + + # convenient accessors actor_stream = actstream_settings.get_action_model().objects.actor action_object_stream = actstream_settings.get_action_model().objects.action_object diff --git a/actstream/registry.py b/actstream/registry.py index 82515d9a..b14fd509 100644 --- a/actstream/registry.py +++ b/actstream/registry.py @@ -1,11 +1,12 @@ from inspect import isclass -import django from django.apps import apps from django.contrib.contenttypes.fields import GenericRelation from django.db.models.base import ModelBase from django.core.exceptions import ImproperlyConfigured +from actstream.settings import get_action_model + class RegistrationError(Exception): pass @@ -15,7 +16,7 @@ def setup_generic_relations(model_class): """ Set up GenericRelations for actionable models. """ - Action = apps.get_model('actstream', 'action') + Action = get_action_model() if Action is None: raise RegistrationError( diff --git a/actstream/settings.py b/actstream/settings.py index 3edb6e2f..abff808e 100644 --- a/actstream/settings.py +++ b/actstream/settings.py @@ -1,5 +1,3 @@ -from functools import lru_cache - from django.apps import apps from django.conf import settings from django.core.exceptions import ImproperlyConfigured @@ -28,11 +26,9 @@ def get_action_manager(): USE_JSONFIELD = SETTINGS.get('USE_JSONFIELD', False) -FOLLOW_MODEL = SETTINGS.get('ACTSTREAM_FOLLOW_MODEL', 'actstream.Follow') -ACTION_MODEL = SETTINGS.get('ACTSTREAM_ACTION_MODEL', 'actstream.Action') - -def get_swappable_model(model_lookup): +def get_swappable_model(model): + model_lookup = getattr(settings, 'ACTSTREAM_%s_MODEL' % model.upper(), 'actstream.%s' % model) try: return apps.get_model(model_lookup, require_ready=False) except ValueError: @@ -45,13 +41,11 @@ def get_swappable_model(model_lookup): ) -@lru_cache(maxsize=None) def get_follow_model(): """Return the Follow model that is active.""" - return get_swappable_model(FOLLOW_MODEL) + return get_swappable_model('Follow') -@lru_cache(maxsize=None) def get_action_model(): """Return the Action model that is active.""" - return get_swappable_model(ACTION_MODEL) + return get_swappable_model('Action') diff --git a/runtests/settings.py b/runtests/settings.py index b54e4fa1..405d058a 100644 --- a/runtests/settings.py +++ b/runtests/settings.py @@ -99,6 +99,7 @@ 'testapp', 'testapp_nested', + 'custom', ) ACTSTREAM_SETTINGS = { diff --git a/tox.ini b/tox.ini index 3b6524bc..33a963c0 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ deps = coverage>=4.5.1 django22: Django>=2.2,<3.0 django30: Django>=3.0,<3.1 - django30: Django>=3.1,<3.2 + django31: Django>=3.1,<3.2 mysql: mysqlclient>=1.4.2,<1.5 mysql: django-mysql>=2.4.1 postgres,sqlite: django-jsonfield>=1.0.1 From 0e6fe879b2184518ed9bee5796845c37c6495bda Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Tue, 23 Mar 2021 21:26:52 -0400 Subject: [PATCH 3/9] wip: custom test app --- runtests/custom/__init__.py | 0 runtests/custom/migrations/0001_initial.py | 60 ++++++++++++++++++++++ runtests/custom/migrations/__init__.py | 0 runtests/custom/models.py | 10 ++++ runtests/custom/tests.py | 29 +++++++++++ 5 files changed, 99 insertions(+) create mode 100644 runtests/custom/__init__.py create mode 100644 runtests/custom/migrations/0001_initial.py create mode 100644 runtests/custom/migrations/__init__.py create mode 100644 runtests/custom/models.py create mode 100644 runtests/custom/tests.py diff --git a/runtests/custom/__init__.py b/runtests/custom/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/runtests/custom/migrations/0001_initial.py b/runtests/custom/migrations/0001_initial.py new file mode 100644 index 00000000..72a6fd40 --- /dev/null +++ b/runtests/custom/migrations/0001_initial.py @@ -0,0 +1,60 @@ +# Generated by Django 2.0.5 on 2018-05-03 23:51 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='CustomAction', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('actor_object_id', models.CharField(db_index=True, max_length=255)), + ('verb', models.CharField(db_index=True, max_length=255)), + ('description', models.TextField(blank=True, null=True)), + ('target_object_id', models.CharField(blank=True, db_index=True, max_length=255, null=True)), + ('action_object_object_id', models.CharField(blank=True, db_index=True, max_length=255, null=True)), + ('timestamp', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), + ('public', models.BooleanField(db_index=True, default=True)), + ('quest', models.CharField(max_length=200)), + ('action_object_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='custom_customaction_action_objects', related_query_name='custom_(class)s_action_objects', to='contenttypes.ContentType')), + ('actor_content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_customaction_actors', related_query_name='custom_(class)s_actors', to='contenttypes.ContentType')), + ('target_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='custom_customaction_targets', related_query_name='custom_(class)s_targets', to='contenttypes.ContentType')), + ], + options={ + 'ordering': ('-timestamp',), + 'abstract': False, + }, + ), + migrations.CreateModel( + name='CustomFollow', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.CharField(db_index=True, max_length=255)), + ('actor_only', models.BooleanField(default=True, verbose_name='Only follow actions where the object is the target.')), + ('flag', models.CharField(blank=True, db_index=True, default='', max_length=255)), + ('started', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), + ('is_special', models.BooleanField(default=False)), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_customfollow_related', related_query_name='custom_(class)ss', to='contenttypes.ContentType')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customfollows', related_query_name='customfollows', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AlterUniqueTogether( + name='customfollow', + unique_together={('user', 'content_type', 'object_id', 'flag')}, + ), + ] diff --git a/runtests/custom/migrations/__init__.py b/runtests/custom/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/runtests/custom/models.py b/runtests/custom/models.py new file mode 100644 index 00000000..b468da10 --- /dev/null +++ b/runtests/custom/models.py @@ -0,0 +1,10 @@ +from django.db import models +from actstream.models import AbstractAction, AbstractFollow + + +class CustomAction(AbstractAction): + quest = models.CharField(max_length=200) + + +class CustomFollow(AbstractFollow): + is_special = models.BooleanField(default=False) diff --git a/runtests/custom/tests.py b/runtests/custom/tests.py new file mode 100644 index 00000000..71dd0a35 --- /dev/null +++ b/runtests/custom/tests.py @@ -0,0 +1,29 @@ +from django.test.utils import override_settings + +from actstream.settings import get_action_model, get_follow_model +from actstream.signals import action +from actstream.tests.base import ActivityBaseTestCase + +from .models import CustomAction, CustomFollow + + +@override_settings( + ACTSTREAM_ACTION_MODEL='custom.CustomAction', + ACTSTREAM_FOLLOW_MODEL='custom.CustomFollow' +) +class CustomModelTests(ActivityBaseTestCase): + def setUp(self): + super(CustomModelTests, self).setUp() + self.user = self.User.objects.create(username='test') + + def test_custom_action_model(self): + self.assertEqual(get_action_model(), CustomAction) + + def test_custom_follow_model(self): + self.assertEqual(get_follow_model(), CustomFollow) + + def test_custom_data(self): + """Adding custom data to a model field works as expected.""" + action.send(self.user, verb='was created', quest='to be awesome') + self.assertEqual(CustomAction.objects.count(), 1) + self.assertEqual(CustomAction.objects.first().quest, 'to be awesome') From fa0aeb832e4235c6d1f7c195214c23a5c447423d Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Mon, 31 May 2021 18:05:54 -0400 Subject: [PATCH 4/9] use actstream_settings instead of django settings --- actstream/apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actstream/apps.py b/actstream/apps.py index eed51fce..4162ae2c 100644 --- a/actstream/apps.py +++ b/actstream/apps.py @@ -15,7 +15,7 @@ class ActstreamConfig(AppConfig): def ready(self): from actstream.actions import action_handler action.connect(action_handler, dispatch_uid='actstream.models') - action_class = settings.get_action_model() + action_class = actstream_settings.get_action_model() if actstream_settings.USE_JSONFIELD: if not hasattr(action_class, 'data'): From 80114e74cf3eb921365cd881017128d956ce4db6 Mon Sep 17 00:00:00 2001 From: Justin Quick Date: Mon, 31 May 2021 19:02:41 -0400 Subject: [PATCH 5/9] custom following that can send attrs to Follow/Action insances --- actstream/actions.py | 18 ++++++++++++++++-- runtests/custom/tests.py | 22 +++++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/actstream/actions.py b/actstream/actions.py index 7919d56d..7fa11893 100644 --- a/actstream/actions.py +++ b/actstream/actions.py @@ -17,6 +17,7 @@ def follow(user, obj, send_action=True, actor_only=True, flag='', **kwargs): If ``send_action`` is ``True`` (the default) then a `` started following `` action signal is sent. + Kwargs that can be passed to the Follow model instance will be passed. Extra keyword arguments are passed to the action.send call. If ``actor_only`` is ``True`` (the default) then only actions where the @@ -32,11 +33,23 @@ def follow(user, obj, send_action=True, actor_only=True, flag='', **kwargs): follow(request.user, group, actor_only=False, flag='liking') """ check(obj) - instance, created = settings.get_follow_model().objects.get_or_create( + follow_model = settings.get_follow_model() + instance, created = follow_model.objects.get_or_create( user=user, object_id=obj.pk, flag=flag, content_type=ContentType.objects.get_for_model(obj), actor_only=actor_only ) + follow_updated = False + for attr in list(kwargs): + try: + follow_model._meta.get_field(attr) + except FieldDoesNotExist: + pass + else: + follow_updated = True + setattr(instance, attr, kwargs.pop(attr)) + if follow_updated: + instance.save() if send_action and created: if not flag: action.send(user, verb=_('started following'), target=obj, **kwargs) @@ -105,6 +118,7 @@ def is_following(user, obj, flag=''): def action_handler(verb, **kwargs): """ Handler function to create Action instance upon action signal call. + Extra kwargs will be passed to the Action instance """ kwargs.pop('signal', None) actor = kwargs.pop('sender') @@ -130,7 +144,7 @@ def action_handler(verb, **kwargs): setattr(newaction, '%s_object_id' % opt, obj.pk) setattr(newaction, '%s_content_type' % opt, ContentType.objects.get_for_model(obj)) - for attr in list(kwargs.keys()): + for attr in list(kwargs): try: settings.get_action_model()._meta.get_field(attr) except FieldDoesNotExist: diff --git a/runtests/custom/tests.py b/runtests/custom/tests.py index 71dd0a35..e369af95 100644 --- a/runtests/custom/tests.py +++ b/runtests/custom/tests.py @@ -3,6 +3,7 @@ from actstream.settings import get_action_model, get_follow_model from actstream.signals import action from actstream.tests.base import ActivityBaseTestCase +from actstream.actions import follow, unfollow from .models import CustomAction, CustomFollow @@ -14,7 +15,8 @@ class CustomModelTests(ActivityBaseTestCase): def setUp(self): super(CustomModelTests, self).setUp() - self.user = self.User.objects.create(username='test') + self.user1 = self.User.objects.create(username='test1') + self.user2 = self.User.objects.create(username='test2') def test_custom_action_model(self): self.assertEqual(get_action_model(), CustomAction) @@ -24,6 +26,20 @@ def test_custom_follow_model(self): def test_custom_data(self): """Adding custom data to a model field works as expected.""" - action.send(self.user, verb='was created', quest='to be awesome') + action.send(self.user1, verb='was created', quest='to be awesome') self.assertEqual(CustomAction.objects.count(), 1) - self.assertEqual(CustomAction.objects.first().quest, 'to be awesome') + self.assertEqual(CustomAction .objects.first().quest, 'to be awesome') + + def test_custom_follow(self): + follow(self.user1, self.user2, is_special=True, quest='holy grail') + custom_follow = get_follow_model().objects.first() + self.assertEqual(custom_follow.user, self.user1) + self.assertEqual(custom_follow.follow_object, self.user2) + self.assertEqual(custom_follow.is_special, True) + custom_action = get_action_model().objects.first() + self.assertEqual(custom_action.actor, self.user1) + self.assertEqual(custom_action.target, self.user2) + self.assertEqual(custom_action.quest, 'holy grail') + + unfollow(self.user1, self.user2) + self.assertFalse(get_follow_model().objects.exists()) From 370037cd5a684606aa9fbad3c6f79658927d1c68 Mon Sep 17 00:00:00 2001 From: Artush Ghazaryan Date: Fri, 24 Nov 2023 13:03:56 +0400 Subject: [PATCH 6/9] remove unused imports --- actstream/actions.py | 2 -- actstream/apps.py | 5 ----- actstream/tests/test_activity.py | 1 - 3 files changed, 8 deletions(-) diff --git a/actstream/actions.py b/actstream/actions.py index 77df766b..95ae8b13 100644 --- a/actstream/actions.py +++ b/actstream/actions.py @@ -1,5 +1,3 @@ - -from django.apps import apps from django.utils.translation import gettext_lazy as _ from django.utils.timezone import now from django.contrib.contenttypes.models import ContentType diff --git a/actstream/apps.py b/actstream/apps.py index bd8215c7..29db5de6 100644 --- a/actstream/apps.py +++ b/actstream/apps.py @@ -1,9 +1,4 @@ -from collections import OrderedDict - -import django -from django.apps import apps from django.apps import AppConfig -from django.conf import settings from django.db.models.signals import pre_delete from actstream import settings as actstream_settings diff --git a/actstream/tests/test_activity.py b/actstream/tests/test_activity.py index 45a847a9..c9733046 100644 --- a/actstream/tests/test_activity.py +++ b/actstream/tests/test_activity.py @@ -145,7 +145,6 @@ def test_following_models_OR_query(self): def test_y_no_orphaned_follows(self): follows = get_follow_model().objects.count() self.user2.delete() - self.assertEqual(follows - 1, get_follow_model().objects.count()) # 2 Follow objects are deleted: # * "User2 follows group" because of the on_delete=models.CASCADE # * "User1 follows User2" because of the pre_delete signal From 04aeac123e2e2ef57b2a0882bae766e132fa0642 Mon Sep 17 00:00:00 2001 From: Artush Ghazaryan Date: Mon, 27 Nov 2023 16:06:52 +0400 Subject: [PATCH 7/9] add swappable models for drf part --- actstream/admin.py | 6 +++--- actstream/drf/serializers.py | 8 ++++---- actstream/drf/views.py | 12 ++++++------ actstream/follows.py | 4 ++-- actstream/tests/test_drf.py | 7 +++---- actstream/tests/test_views.py | 13 +++++++------ 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/actstream/admin.py b/actstream/admin.py index 14015913..f79e99a7 100644 --- a/actstream/admin.py +++ b/actstream/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from actstream import models +from actstream.settings import get_action_model, get_follow_model # Use django-generic-admin widgets if available try: @@ -25,5 +25,5 @@ class FollowAdmin(ModelAdmin): raw_id_fields = ('user', 'content_type') -admin.site.register(models.Action, ActionAdmin) -admin.site.register(models.Follow, FollowAdmin) +admin.site.register(get_action_model(), ActionAdmin) +admin.site.register(get_follow_model(), FollowAdmin) diff --git a/actstream/drf/serializers.py b/actstream/drf/serializers.py index 991c559e..26e44a0b 100644 --- a/actstream/drf/serializers.py +++ b/actstream/drf/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from generic_relations.relations import GenericRelatedField -from actstream.models import Follow, Action +from actstream import settings as actstream_settings from actstream.registry import registry, label from actstream.settings import DRF_SETTINGS, import_obj @@ -72,7 +72,7 @@ class ActionSerializer(DEFAULT_SERIALIZER): action_object = get_grf() class Meta: - model = Action + model = actstream_settings.get_action_model() fields = 'id verb public description timestamp actor target action_object'.split() @@ -97,7 +97,7 @@ class FollowSerializer(DEFAULT_SERIALIZER): follow_object = get_grf() class Meta: - model = Follow + model = actstream_settings.get_follow_model() fields = 'id flag user follow_object started actor_only'.split() @@ -108,5 +108,5 @@ class FollowingSerializer(DEFAULT_SERIALIZER): follow_object = get_grf() class Meta: - model = Follow + model = actstream_settings.get_follow_model() fields = ['follow_object'] diff --git a/actstream/drf/views.py b/actstream/drf/views.py index bd6b3f8e..241bc52e 100644 --- a/actstream/drf/views.py +++ b/actstream/drf/views.py @@ -13,7 +13,7 @@ from actstream.drf import serializers from actstream import models from actstream.registry import label -from actstream.settings import DRF_SETTINGS, import_obj +from actstream.settings import DRF_SETTINGS, import_obj, get_action_model, get_follow_model from actstream.signals import action as action_signal from actstream.actions import follow as follow_action @@ -52,7 +52,7 @@ def get_permissions(self): class ActionViewSet(DefaultModelViewSet): - queryset = models.Action.objects.public().order_by('-timestamp', '-id').prefetch_related() + queryset = get_action_model().objects.public().order_by('-timestamp', '-id').prefetch_related() serializer_class = serializers.ActionSerializer @action(detail=False, permission_classes=[permissions.IsAuthenticated], methods=['POST'], serializer_class=serializers.SendActionSerializer) @@ -159,7 +159,7 @@ def any_stream(self, request, content_type_id, object_id): class FollowViewSet(DefaultModelViewSet): - queryset = models.Follow.objects.order_by('-started', '-id').prefetch_related() + queryset = get_follow_model().objects.order_by('-started', '-id').prefetch_related() serializer_class = serializers.FollowSerializer permission_classes = [permissions.IsAuthenticated] @@ -185,7 +185,7 @@ def is_following(self, request, content_type_id, object_id): """ ctype = get_object_or_404(ContentType, id=content_type_id) instance = ctype.get_object_for_this_type(pk=object_id) - following = models.Follow.objects.is_following(request.user, instance) + following = get_follow_model().objects.is_following(request.user, instance) data = {'is_following': following} return Response(json.dumps(data)) @@ -195,7 +195,7 @@ def following(self, request): """ Returns a JSON response whether the current user is following the object from content_type_id/object_id pair """ - qs = models.Follow.objects.following_qs(request.user) + qs = get_follow_model().objects.following_qs(request.user) return Response(serializers.FollowingSerializer(qs, many=True).data) @action(detail=False, permission_classes=[permissions.IsAuthenticated], @@ -208,7 +208,7 @@ def followers(self, request): if user_model not in serializers.registered_serializers: raise ModelNotRegistered(f'Auth user "{user_model.__name__}" not registered with actstream') serializer = serializers.registered_serializers[user_model] - followers = models.Follow.objects.followers(request.user) + followers = get_follow_model().objects.followers(request.user) return Response(serializer(followers, many=True).data) diff --git a/actstream/follows.py b/actstream/follows.py index acde3782..6707a4fd 100644 --- a/actstream/follows.py +++ b/actstream/follows.py @@ -1,6 +1,6 @@ from django.core.exceptions import ImproperlyConfigured -from actstream.models import Follow +from actstream.settings import get_follow_model def delete_orphaned_follows(sender, instance=None, **kwargs): @@ -11,6 +11,6 @@ def delete_orphaned_follows(sender, instance=None, **kwargs): return try: - Follow.objects.for_object(instance).delete() + get_follow_model().objects.for_object(instance).delete() except ImproperlyConfigured: # raised by actstream for irrelevant models pass diff --git a/actstream/tests/test_drf.py b/actstream/tests/test_drf.py index b89c7933..41b3d89e 100644 --- a/actstream/tests/test_drf.py +++ b/actstream/tests/test_drf.py @@ -5,8 +5,7 @@ from django.urls import reverse from actstream.tests.base import DataTestCase -from actstream.settings import USE_DRF, DRF_SETTINGS -from actstream.models import Action, Follow +from actstream.settings import USE_DRF, DRF_SETTINGS, get_action_model, get_follow_model from actstream import signals @@ -125,7 +124,7 @@ def test_action_send(self): } post = self.auth_client.post(reverse('action-send'), body) assert post.status_code == 201 - action = Action.objects.first() + action = get_action_model().objects.first() assert action.description == body['description'] assert action.verb == body['verb'] assert action.actor == self.user1 @@ -141,7 +140,7 @@ def test_follow(self): } post = self.auth_client.post(reverse('follow-follow'), body) assert post.status_code == 201 - follow = Follow.objects.order_by('-id').first() + follow = get_follow_model().objects.order_by('-id').first() assert follow.follow_object == self.comment assert follow.user == self.user1 assert follow.user == self.user1 diff --git a/actstream/tests/test_views.py b/actstream/tests/test_views.py index 5ab8ab7a..9c7e23ee 100644 --- a/actstream/tests/test_views.py +++ b/actstream/tests/test_views.py @@ -2,6 +2,7 @@ from django.urls import reverse +from actstream.settings import get_action_model, get_follow_model from actstream import models from actstream.tests.base import DataTestCase @@ -34,13 +35,13 @@ def test_follow_unfollow(self): action = {'actor_content_type': self.user_ct, 'actor_object_id': self.user1.pk, 'target_content_type': self.user_ct, 'target_object_id': self.user3.pk, 'verb': 'started following'} - models.Follow.objects.get(**follow) - models.Action.objects.get(**action) + get_follow_model().objects.get(**follow) + get_action_model().objects.get(**action) response = self.get('actstream_unfollow', self.user_ct.pk, self.user3.pk) self.assertEqual(response.status_code, 204) self.assertEqual(len(response.templates), 0) - self.assertRaises(models.Follow.DoesNotExist, models.Follow.objects.get, **follow) + self.assertRaises(get_follow_model().DoesNotExist, get_follow_model().objects.get, **follow) response = self.get('actstream_unfollow', self.user_ct.pk, self.user3.pk, next='/redirect/') self.assertEqual(response.status_code, 302) @@ -55,13 +56,13 @@ def test_follow_unfollow_with_flag(self): action = {'actor_content_type': self.user_ct, 'actor_object_id': self.user1.pk, 'target_content_type': self.user_ct, 'target_object_id': self.user3.pk, 'verb': 'started watching'} - models.Follow.objects.get(**follow) - models.Action.objects.get(**action) + get_follow_model().objects.get(**follow) + get_action_model().objects.get(**action) response = self.get('actstream_unfollow', self.user_ct.pk, self.user3.pk, 'watching') self.assertEqual(response.status_code, 204) self.assertEqual(len(response.templates), 0) - self.assertRaises(models.Follow.DoesNotExist, models.Follow.objects.get, **follow) + self.assertRaises(get_follow_model().DoesNotExist, get_follow_model().objects.get, **follow) response = self.get('actstream_unfollow', self.user_ct.pk, self.user3.pk, 'watching', next='/redirect/') self.assertEqual(response.status_code, 302) From 3a9370080eeb8611a0ce096843da2905ddfcbdea Mon Sep 17 00:00:00 2001 From: Artush Ghazaryan Date: Mon, 27 Nov 2023 22:53:24 +0400 Subject: [PATCH 8/9] add swappable models for drf part --- actstream/admin.py | 6 +++--- actstream/drf/serializers.py | 8 ++++---- actstream/drf/views.py | 12 ++++++------ actstream/follows.py | 4 ++-- actstream/tests/test_drf.py | 7 +++---- actstream/tests/test_views.py | 13 +++++++------ 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/actstream/admin.py b/actstream/admin.py index 14015913..f79e99a7 100644 --- a/actstream/admin.py +++ b/actstream/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from actstream import models +from actstream.settings import get_action_model, get_follow_model # Use django-generic-admin widgets if available try: @@ -25,5 +25,5 @@ class FollowAdmin(ModelAdmin): raw_id_fields = ('user', 'content_type') -admin.site.register(models.Action, ActionAdmin) -admin.site.register(models.Follow, FollowAdmin) +admin.site.register(get_action_model(), ActionAdmin) +admin.site.register(get_follow_model(), FollowAdmin) diff --git a/actstream/drf/serializers.py b/actstream/drf/serializers.py index 991c559e..26e44a0b 100644 --- a/actstream/drf/serializers.py +++ b/actstream/drf/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from generic_relations.relations import GenericRelatedField -from actstream.models import Follow, Action +from actstream import settings as actstream_settings from actstream.registry import registry, label from actstream.settings import DRF_SETTINGS, import_obj @@ -72,7 +72,7 @@ class ActionSerializer(DEFAULT_SERIALIZER): action_object = get_grf() class Meta: - model = Action + model = actstream_settings.get_action_model() fields = 'id verb public description timestamp actor target action_object'.split() @@ -97,7 +97,7 @@ class FollowSerializer(DEFAULT_SERIALIZER): follow_object = get_grf() class Meta: - model = Follow + model = actstream_settings.get_follow_model() fields = 'id flag user follow_object started actor_only'.split() @@ -108,5 +108,5 @@ class FollowingSerializer(DEFAULT_SERIALIZER): follow_object = get_grf() class Meta: - model = Follow + model = actstream_settings.get_follow_model() fields = ['follow_object'] diff --git a/actstream/drf/views.py b/actstream/drf/views.py index bd6b3f8e..241bc52e 100644 --- a/actstream/drf/views.py +++ b/actstream/drf/views.py @@ -13,7 +13,7 @@ from actstream.drf import serializers from actstream import models from actstream.registry import label -from actstream.settings import DRF_SETTINGS, import_obj +from actstream.settings import DRF_SETTINGS, import_obj, get_action_model, get_follow_model from actstream.signals import action as action_signal from actstream.actions import follow as follow_action @@ -52,7 +52,7 @@ def get_permissions(self): class ActionViewSet(DefaultModelViewSet): - queryset = models.Action.objects.public().order_by('-timestamp', '-id').prefetch_related() + queryset = get_action_model().objects.public().order_by('-timestamp', '-id').prefetch_related() serializer_class = serializers.ActionSerializer @action(detail=False, permission_classes=[permissions.IsAuthenticated], methods=['POST'], serializer_class=serializers.SendActionSerializer) @@ -159,7 +159,7 @@ def any_stream(self, request, content_type_id, object_id): class FollowViewSet(DefaultModelViewSet): - queryset = models.Follow.objects.order_by('-started', '-id').prefetch_related() + queryset = get_follow_model().objects.order_by('-started', '-id').prefetch_related() serializer_class = serializers.FollowSerializer permission_classes = [permissions.IsAuthenticated] @@ -185,7 +185,7 @@ def is_following(self, request, content_type_id, object_id): """ ctype = get_object_or_404(ContentType, id=content_type_id) instance = ctype.get_object_for_this_type(pk=object_id) - following = models.Follow.objects.is_following(request.user, instance) + following = get_follow_model().objects.is_following(request.user, instance) data = {'is_following': following} return Response(json.dumps(data)) @@ -195,7 +195,7 @@ def following(self, request): """ Returns a JSON response whether the current user is following the object from content_type_id/object_id pair """ - qs = models.Follow.objects.following_qs(request.user) + qs = get_follow_model().objects.following_qs(request.user) return Response(serializers.FollowingSerializer(qs, many=True).data) @action(detail=False, permission_classes=[permissions.IsAuthenticated], @@ -208,7 +208,7 @@ def followers(self, request): if user_model not in serializers.registered_serializers: raise ModelNotRegistered(f'Auth user "{user_model.__name__}" not registered with actstream') serializer = serializers.registered_serializers[user_model] - followers = models.Follow.objects.followers(request.user) + followers = get_follow_model().objects.followers(request.user) return Response(serializer(followers, many=True).data) diff --git a/actstream/follows.py b/actstream/follows.py index acde3782..6707a4fd 100644 --- a/actstream/follows.py +++ b/actstream/follows.py @@ -1,6 +1,6 @@ from django.core.exceptions import ImproperlyConfigured -from actstream.models import Follow +from actstream.settings import get_follow_model def delete_orphaned_follows(sender, instance=None, **kwargs): @@ -11,6 +11,6 @@ def delete_orphaned_follows(sender, instance=None, **kwargs): return try: - Follow.objects.for_object(instance).delete() + get_follow_model().objects.for_object(instance).delete() except ImproperlyConfigured: # raised by actstream for irrelevant models pass diff --git a/actstream/tests/test_drf.py b/actstream/tests/test_drf.py index b89c7933..41b3d89e 100644 --- a/actstream/tests/test_drf.py +++ b/actstream/tests/test_drf.py @@ -5,8 +5,7 @@ from django.urls import reverse from actstream.tests.base import DataTestCase -from actstream.settings import USE_DRF, DRF_SETTINGS -from actstream.models import Action, Follow +from actstream.settings import USE_DRF, DRF_SETTINGS, get_action_model, get_follow_model from actstream import signals @@ -125,7 +124,7 @@ def test_action_send(self): } post = self.auth_client.post(reverse('action-send'), body) assert post.status_code == 201 - action = Action.objects.first() + action = get_action_model().objects.first() assert action.description == body['description'] assert action.verb == body['verb'] assert action.actor == self.user1 @@ -141,7 +140,7 @@ def test_follow(self): } post = self.auth_client.post(reverse('follow-follow'), body) assert post.status_code == 201 - follow = Follow.objects.order_by('-id').first() + follow = get_follow_model().objects.order_by('-id').first() assert follow.follow_object == self.comment assert follow.user == self.user1 assert follow.user == self.user1 diff --git a/actstream/tests/test_views.py b/actstream/tests/test_views.py index 5ab8ab7a..9c7e23ee 100644 --- a/actstream/tests/test_views.py +++ b/actstream/tests/test_views.py @@ -2,6 +2,7 @@ from django.urls import reverse +from actstream.settings import get_action_model, get_follow_model from actstream import models from actstream.tests.base import DataTestCase @@ -34,13 +35,13 @@ def test_follow_unfollow(self): action = {'actor_content_type': self.user_ct, 'actor_object_id': self.user1.pk, 'target_content_type': self.user_ct, 'target_object_id': self.user3.pk, 'verb': 'started following'} - models.Follow.objects.get(**follow) - models.Action.objects.get(**action) + get_follow_model().objects.get(**follow) + get_action_model().objects.get(**action) response = self.get('actstream_unfollow', self.user_ct.pk, self.user3.pk) self.assertEqual(response.status_code, 204) self.assertEqual(len(response.templates), 0) - self.assertRaises(models.Follow.DoesNotExist, models.Follow.objects.get, **follow) + self.assertRaises(get_follow_model().DoesNotExist, get_follow_model().objects.get, **follow) response = self.get('actstream_unfollow', self.user_ct.pk, self.user3.pk, next='/redirect/') self.assertEqual(response.status_code, 302) @@ -55,13 +56,13 @@ def test_follow_unfollow_with_flag(self): action = {'actor_content_type': self.user_ct, 'actor_object_id': self.user1.pk, 'target_content_type': self.user_ct, 'target_object_id': self.user3.pk, 'verb': 'started watching'} - models.Follow.objects.get(**follow) - models.Action.objects.get(**action) + get_follow_model().objects.get(**follow) + get_action_model().objects.get(**action) response = self.get('actstream_unfollow', self.user_ct.pk, self.user3.pk, 'watching') self.assertEqual(response.status_code, 204) self.assertEqual(len(response.templates), 0) - self.assertRaises(models.Follow.DoesNotExist, models.Follow.objects.get, **follow) + self.assertRaises(get_follow_model().DoesNotExist, get_follow_model().objects.get, **follow) response = self.get('actstream_unfollow', self.user_ct.pk, self.user3.pk, 'watching', next='/redirect/') self.assertEqual(response.status_code, 302) From 96cac8338cb3990cad61e173cb49137a8c1b066d Mon Sep 17 00:00:00 2001 From: Artush Ghazaryan Date: Wed, 13 Dec 2023 16:29:37 +0400 Subject: [PATCH 9/9] release 2.0.1 --- actstream/__init__.py | 2 +- docs/changelog.rst | 5 +++++ docs/drf.rst | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/actstream/__init__.py b/actstream/__init__.py index 192fd774..7bbe37fe 100644 --- a/actstream/__init__.py +++ b/actstream/__init__.py @@ -12,5 +12,5 @@ default_app_config = 'actstream.apps.ActstreamConfig' -__version__ = '2.0.0' +__version__ = '2.0.1' __author__ = 'Asif Saif Uddin, Justin Quick ' diff --git a/docs/changelog.rst b/docs/changelog.rst index 6544a83c..78e4cfc6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,6 +3,11 @@ Changelog ========= +2.0.1 +----- + + - Custom Action and Follow models support + 2.0.0 ----- diff --git a/docs/drf.rst b/docs/drf.rst index 6e977339..1715b14b 100644 --- a/docs/drf.rst +++ b/docs/drf.rst @@ -3,7 +3,7 @@ Django ReST Framework Integration ================================= -As of version 2.0.0, django-activity-stream now supports integration with `Django ReST Framework `_. +As of version 2.0.1, django-activity-stream now supports integration with `Django ReST Framework `_. DRF provides a standardized way of interacting with models stored in Django. It provides standard create/update/get operations using http standard methods.