From 51b8d7f3475fd1d9748839f06a6e1a8e2810ffc8 Mon Sep 17 00:00:00 2001 From: Marnanel Thurman Date: Wed, 5 Jun 2019 00:07:05 +0100 Subject: [PATCH] new Audience model, plus tests. Not yet linked in to Thing. --- .../migrations/0003_auto_20190604_1752.py | 28 +++++ django_kepi/models/__init__.py | 8 +- django_kepi/models/audience.py | 113 ++++++++++++++++++ tests/test_audience.py | 77 ++++++++++++ 4 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 django_kepi/migrations/0003_auto_20190604_1752.py create mode 100644 django_kepi/models/audience.py create mode 100644 tests/test_audience.py diff --git a/django_kepi/migrations/0003_auto_20190604_1752.py b/django_kepi/migrations/0003_auto_20190604_1752.py new file mode 100644 index 0000000..058ecd1 --- /dev/null +++ b/django_kepi/migrations/0003_auto_20190604_1752.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.1 on 2019-06-04 17:52 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_kepi', '0002_auto_20190527_1458'), + ] + + operations = [ + migrations.AlterField( + model_name='thing', + name='f_type', + field=models.CharField(choices=[('Accept', 'Accept'), ('Add', 'Add'), ('Create', 'Create'), ('Delete', 'Delete'), ('Follow', 'Follow'), ('Like', 'Like'), ('Reject', 'Reject'), ('Remove', 'Remove'), ('Undo', 'Undo'), ('Update', 'Update')], max_length=255), + ), + migrations.CreateModel( + name='Audience', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('field', models.PositiveSmallIntegerField(choices=[(1, 'audience'), (2, 'to'), (4, 'cc'), (114, 'bto'), (116, 'bcc')])), + ('recipient', models.CharField(max_length=255)), + ('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_kepi.Thing')), + ], + ), + ] diff --git a/django_kepi/models/__init__.py b/django_kepi/models/__init__.py index 6d58ecf..e09b266 100644 --- a/django_kepi/models/__init__.py +++ b/django_kepi/models/__init__.py @@ -1,11 +1,7 @@ from django_kepi.models.thing import Thing, ThingField, create from django_kepi.models.following import Following from django_kepi.models.actor import Actor - -def new_activity_identifier(): - # we have to keep this in for now, - # to pacify makemigrations - return None +from django_kepi.models.audience import Audience ####################### @@ -15,5 +11,5 @@ __all__ = [ 'ThingField', 'create', 'Following', - 'new_activity_identifier', + 'Audience', ] diff --git a/django_kepi/models/audience.py b/django_kepi/models/audience.py new file mode 100644 index 0000000..1db5c50 --- /dev/null +++ b/django_kepi/models/audience.py @@ -0,0 +1,113 @@ +from django.db import models +from collections import defaultdict +import logging + +logger = logging.getLogger('django_kepi') + +FIELD_AUDIENCE = 0x01 # literally "audience" +FIELD_TO = 0x02 +FIELD_CC = 0x04 + +BLIND = 0x70 +FIELD_BTO = BLIND + FIELD_TO +FIELD_BCC = BLIND + FIELD_CC +# the spec doesn't allow for blind audience; idk why + +FIELD_CHOICES = [ + (FIELD_AUDIENCE, 'audience'), + (FIELD_TO, 'to'), + (FIELD_CC, 'cc'), + (FIELD_BTO, 'bto'), + (FIELD_BCC, 'bcc'), + ] + +FIELD_NAMES = dict([(v,f) for (f,v) in FIELD_CHOICES]) + +class Audience(models.Model): + + parent = models.ForeignKey( + 'django_kepi.Thing', + on_delete = models.CASCADE, + ) + + field = models.PositiveSmallIntegerField( + choices = FIELD_CHOICES, + ) + + recipient = models.CharField( + max_length=255, + ) + + @property + def blind(self): + return (self.field & BLIND) != 0 + + def __str__(self): + return '[%s %12s %s]' % ( + self.parent.number, + self.get_field_display(), + self.recipient, + ) + + @classmethod + def add_audiences_for(cls, thing, + field, value): + + """ + Add new Audiences for a given Thing. + "value" is a list of strings. + + This function only adds Audiences of + a single field type. This is + deliberately asymmetrical to + get_audiences_for(), which returns + all Audiences of all field types. + The difference is because of + where it's needed. + """ + + if field not in FIELD_NAMES: + raise ValueError('%s is not an audience field' % ( + field, + )) + logger.debug('Adding Audiences for %s: %s=%s', + thing.number, field, value) + + field = FIELD_NAMES[field] + + if not isinstance(value, list): + value = [value] + + for line in value: + a = Audience( + parent = thing, + field = field, + recipient = str(line), + ) + a.save() + logger.debug(' -- created %s', + a) + + @classmethod + def get_audiences_for(cls, thing, + hide_blind = False, + ): + + result = defaultdict(lambda: []) + + for a in Audience.objects.filter( + parent=thing, + ): + + if hide_blind and a.blind: + logger.debug('Not counting %s because blind fields are hidden', + a) + continue + + result[a.get_field_display()].append(a.recipient) + + result = dict(result) + + logger.debug('Audience is: %s', result) + + return result diff --git a/tests/test_audience.py b/tests/test_audience.py new file mode 100644 index 0000000..4ae4ac6 --- /dev/null +++ b/tests/test_audience.py @@ -0,0 +1,77 @@ +from django.test import TestCase +from django_kepi.models import create, Audience +from . import create_local_person, REMOTE_FRED, REMOTE_JIM + +class TestAudience(TestCase): + + def test_add_audiences_for(self): + narcissus = create_local_person( + name = 'narcissus', + ) + + like = create( + f_type = 'Like', + f_actor = narcissus, + f_object = narcissus, + ) + + a = Audience.add_audiences_for( + thing = like, + field = 'to', + value = [ + REMOTE_FRED, + REMOTE_JIM, + ], + ) + + results = Audience.objects.filter( + parent = like, + ) + + self.assertEqual(len(results), 2) + self.assertEqual(results[0].recipient, REMOTE_FRED) + self.assertEqual(results[1].recipient, REMOTE_JIM) + + def test_get_audiences_for(self): + narcissus = create_local_person( + name = 'narcissus', + ) + + like = create( + f_type = 'Like', + f_actor = narcissus, + f_object = narcissus, + ) + + for fieldname in ['to', 'cc', 'bcc']: + a = Audience.add_audiences_for( + thing = like, + field = fieldname, + value = [ + REMOTE_FRED, + REMOTE_JIM, + ], + ) + + self.assertDictEqual( + Audience.get_audiences_for(like), + {'to': ['https://remote.example.org/users/fred', + 'https://remote.example.org/users/jim'], + 'cc': ['https://remote.example.org/users/fred', + 'https://remote.example.org/users/jim'], + 'bcc': ['https://remote.example.org/users/fred', + 'https://remote.example.org/users/jim'] + }) + + self.assertDictEqual( + Audience.get_audiences_for(like, + hide_blind = True, + ), + {'to': ['https://remote.example.org/users/fred', + 'https://remote.example.org/users/jim'], + 'cc': ['https://remote.example.org/users/fred', + 'https://remote.example.org/users/jim'], + }) + + +