From 1adb4b94cbeb6cfa3bc1367aa3591b75bfab5ca2 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Wed, 27 Mar 2019 16:18:12 -0400 Subject: [PATCH 01/33] Reindent --- incident/models/incident_page.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/incident/models/incident_page.py b/incident/models/incident_page.py index 6bd4590c8..640ba2a5f 100644 --- a/incident/models/incident_page.py +++ b/incident/models/incident_page.py @@ -420,14 +420,14 @@ class IncidentPage(MetadataPageMixin, Page): MultiFieldPanel( heading='Details', children=[ - FieldPanel('date'), - FieldPanel('exact_date_unknown'), - FieldPanel('affiliation'), - FieldPanel('city'), - AutocompletePanel('state', page_type='incident.State'), - AutocompletePanel('targets', 'incident.Target', is_single=False), - AutocompletePanel('tags', 'common.CommonTag', is_single=False), - InlinePanel('categories', label='Incident categories', min_num=1), + FieldPanel('date'), + FieldPanel('exact_date_unknown'), + FieldPanel('affiliation'), + FieldPanel('city'), + AutocompletePanel('state', page_type='incident.State'), + AutocompletePanel('targets', 'incident.Target', is_single=False), + AutocompletePanel('tags', 'common.CommonTag', is_single=False), + InlinePanel('categories', label='Incident categories', min_num=1), ] ), InlinePanel( From 080f047bff14651a4a61878eefebbe78e98001c9 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Mon, 29 Apr 2019 12:44:58 -0400 Subject: [PATCH 02/33] Add models and relationships for Institutions and Journalists Also add a migration to copy data from the old field to the new fields. --- .../migrations/0032_auto_20190429_1621.py | 56 +++++++++++++++++++ .../migrations/0033_auto_20190429_1627.py | 48 ++++++++++++++++ incident/models/incident_page.py | 9 +++ incident/models/items.py | 33 +++++++++++ incident/tests/test_incidents_export.py | 2 + 5 files changed, 148 insertions(+) create mode 100644 incident/migrations/0032_auto_20190429_1621.py create mode 100644 incident/migrations/0033_auto_20190429_1627.py diff --git a/incident/migrations/0032_auto_20190429_1621.py b/incident/migrations/0032_auto_20190429_1621.py new file mode 100644 index 000000000..eec748eb2 --- /dev/null +++ b/incident/migrations/0032_auto_20190429_1621.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-29 16:21 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import modelcluster.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('incident', '0031_auto_20190321_1836'), + ] + + operations = [ + migrations.CreateModel( + name='Institution', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, unique=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Journalist', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TargetedJournalist', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), + ('incident', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='targeted_journalists', to='incident.IncidentPage')), + ('institution', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='incident.Institution')), + ('journalist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='incident.Journalist')), + ], + options={ + 'ordering': ['sort_order'], + 'abstract': False, + }, + ), + migrations.AddField( + model_name='incidentpage', + name='targeted_institutions', + field=models.ManyToManyField(blank=True, related_name='institutions_incidents', to='incident.Institution', verbose_name='Targeted Institutions'), + ), + ] diff --git a/incident/migrations/0033_auto_20190429_1627.py b/incident/migrations/0033_auto_20190429_1627.py new file mode 100644 index 000000000..ba86128c2 --- /dev/null +++ b/incident/migrations/0033_auto_20190429_1627.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-29 16:27 +from __future__ import unicode_literals + +from django.db import migrations + + +def copy_targets(apps, schema_editor): + """Copy Target data into separate tables based on kind + + This function copies all Target data on all Incidents into either + the Journalist or Institution models and establishes appropriate + relationships back to the original Incident. + + """ + + IncidentPage = apps.get_model('incident', 'IncidentPage') + Journalist = apps.get_model('incident', 'Journalist') + TargetedJournalist = apps.get_model('incident', 'TargetedJournalist') + Institution = apps.get_model('incident', 'Institution') + + for incident in IncidentPage.objects.all(): + for target in incident.targets.all(): + if target.kind == 'JOURNALIST': + journalist, _ = Journalist.objects.get_or_create(title=target.title) + TargetedJournalist.objects.create( + incident=incident, + journalist=journalist, + institution=None, + ) + elif target.kind == 'INSTITUTION': + inst, _ = Institution.objects.get_or_create(title=target.title) + incident.targeted_institutions.add(inst) + incident.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('incident', '0032_auto_20190429_1621'), + ] + + operations = [ + migrations.RunPython( + copy_targets, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/incident/models/incident_page.py b/incident/models/incident_page.py index 640ba2a5f..50e840a36 100644 --- a/incident/models/incident_page.py +++ b/incident/models/incident_page.py @@ -134,6 +134,13 @@ class IncidentPage(MetadataPageMixin, Page): related_name='targets_incidents', ) + targeted_institutions = models.ManyToManyField( + 'incident.Institution', + blank=True, + verbose_name='Targeted Institutions', + related_name='institutions_incidents', + ) + tags = ParentalManyToManyField( 'common.CommonTag', blank=True, @@ -424,8 +431,10 @@ class IncidentPage(MetadataPageMixin, Page): FieldPanel('exact_date_unknown'), FieldPanel('affiliation'), FieldPanel('city'), + InlinePanel('targeted_journalists', label='Targeted Journalists'), AutocompletePanel('state', page_type='incident.State'), AutocompletePanel('targets', 'incident.Target', is_single=False), + AutocompletePanel('targeted_institutions', 'incident.Institution', is_single=False), AutocompletePanel('tags', 'common.CommonTag', is_single=False), InlinePanel('categories', label='Incident categories', min_num=1), ] diff --git a/incident/models/items.py b/incident/models/items.py index 525263a39..ddf913b6c 100644 --- a/incident/models/items.py +++ b/incident/models/items.py @@ -1,5 +1,9 @@ from django.db import models + from modelcluster.models import ClusterableModel +from modelcluster.fields import ParentalKey +from wagtail.wagtailcore.models import Orderable +from wagtailautocomplete.edit_handlers import AutocompletePanel class Target(ClusterableModel): @@ -32,6 +36,35 @@ class Meta: ordering = ['title'] +class Journalist(ClusterableModel): + @classmethod + def autocomplete_create(kls, value): + return kls.objects.create(title=value) + + title = models.CharField(max_length=255) + + +class Institution(ClusterableModel): + @classmethod + def autocomplete_create(kls, value): + return kls.objects.create(title=value) + + title = models.CharField(max_length=255, unique=True) + + +class TargetedJournalist(Orderable): + incident = ParentalKey('incident.IncidentPage', on_delete=models.CASCADE, related_name='targeted_journalists') + + journalist = models.ForeignKey(Journalist, on_delete=models.CASCADE, related_name='+') + + institution = models.ForeignKey(Institution, on_delete=models.CASCADE, null=True) + + panels = [ + AutocompletePanel('journalist', 'incident.Journalist'), + AutocompletePanel('institution', 'incident.Institution'), + ] + + class Charge(ClusterableModel): @classmethod def autocomplete_create(kls, value): diff --git a/incident/tests/test_incidents_export.py b/incident/tests/test_incidents_export.py index ec859265b..199f8c261 100644 --- a/incident/tests/test_incidents_export.py +++ b/incident/tests/test_incidents_export.py @@ -108,6 +108,8 @@ def test_GET(self): 'legal_order_type', 'status_of_prior_restraint', 'targets', + 'targeted_journalists', + 'targeted_institutions', 'tags', 'current_charges', 'dropped_charges', From c3544813ea89880fe0c29079b18e696bab7ed404 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Mon, 29 Apr 2019 12:46:57 -0400 Subject: [PATCH 03/33] Add migration to switch `ManyToMany` to `ParentalManyToMany` The `targeted_institutions` field was only a `ManyToMany` field to accomodate a data migration, we actually want it to be a `ParentalManyToMany`. See discussion for more information: https://github.com/freedomofpress/tracker/issues/707#issuecomment-480992735 --- .../migrations/0034_auto_20190429_1646.py | 21 +++++++++++++++++++ incident/models/incident_page.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 incident/migrations/0034_auto_20190429_1646.py diff --git a/incident/migrations/0034_auto_20190429_1646.py b/incident/migrations/0034_auto_20190429_1646.py new file mode 100644 index 000000000..aa2c10ae4 --- /dev/null +++ b/incident/migrations/0034_auto_20190429_1646.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-29 16:46 +from __future__ import unicode_literals + +from django.db import migrations +import modelcluster.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('incident', '0033_auto_20190429_1627'), + ] + + operations = [ + migrations.AlterField( + model_name='incidentpage', + name='targeted_institutions', + field=modelcluster.fields.ParentalManyToManyField(blank=True, related_name='institutions_incidents', to='incident.Institution', verbose_name='Targeted Institutions'), + ), + ] diff --git a/incident/models/incident_page.py b/incident/models/incident_page.py index 50e840a36..3845668d4 100644 --- a/incident/models/incident_page.py +++ b/incident/models/incident_page.py @@ -134,7 +134,7 @@ class IncidentPage(MetadataPageMixin, Page): related_name='targets_incidents', ) - targeted_institutions = models.ManyToManyField( + targeted_institutions = ParentalManyToManyField( 'incident.Institution', blank=True, verbose_name='Targeted Institutions', From 4e687f98fff607051903af66a740c341a1421ac0 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Mon, 29 Apr 2019 17:16:22 -0400 Subject: [PATCH 04/33] Update filter list display for lates autocomplete widget It looks like this line ought to have been fixed as part of #742 but slipped under the radar. --- client/common/js/filtering/FiltersList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/common/js/filtering/FiltersList.js b/client/common/js/filtering/FiltersList.js index 140bf1730..cc246bb4e 100644 --- a/client/common/js/filtering/FiltersList.js +++ b/client/common/js/filtering/FiltersList.js @@ -50,7 +50,7 @@ class FiltersList extends PureComponent { ) } else { - renderedValue = filterValues[filter.name].label + renderedValue = filterValues[filter.name].title } } From a0801fe22418b2e99ce86e0f068c05b3b6bc68e0 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Mon, 29 Apr 2019 17:18:24 -0400 Subject: [PATCH 05/33] Add new type of incident filter for ManyToMany-Through relations As far as I can tell, we don't have a way of expressing a filter that is based on a field related to `IncidentPage` via a `ManyToMany` relationship with a "through" model. This is a fairly basic attempt at providing that feature, but it seems to work in the case of `IncidentPage.targeted_journalists`. --- incident/utils/incident_filter.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/incident/utils/incident_filter.py b/incident/utils/incident_filter.py index 265ce31dd..8dbb75e83 100644 --- a/incident/utils/incident_filter.py +++ b/incident/utils/incident_filter.py @@ -340,6 +340,28 @@ def serialize(self): return serialized +class RelationThroughFilter(ManyRelationFilter): + def __init__(self, name, model_field, relation, lookup=None, verbose_name=None): + lookup = model_field.name + '__' + relation + super(RelationThroughFilter, self).__init__(name, model_field, lookup, verbose_name) + self.relation = relation + + def serialize(self): + serialized = super(ManyRelationFilter, self).serialize() + + related_model = self.model_field.remote_field.model._meta.get_field(self.relation).target_field.model + + if isinstance(self.model_field, ManyToOneRel) and hasattr(related_model, '_autocomplete_model'): + serialized['autocomplete_type'] = related_model._autocomplete_model + else: + serialized['autocomplete_type'] = '{}.{}'.format( + related_model._meta.app_label, + related_model.__name__, + ) + serialized['many'] = True + return serialized + + class CircuitsFilter(ChoiceFilter): def get_choices(self): return set(STATES_BY_CIRCUIT) @@ -431,7 +453,7 @@ class IncidentFilter(object): 'equipment_broken': {'lookup': 'equipment_broken__equipment'}, 'tags': {'verbose_name': 'Has any of these tags'}, 'subpoena_statuses': {'verbose_name': 'Subpoena status'}, - 'targets': {'verbose_name': 'Targeted any of these journalists'}, + 'targeted_journalists': {'verbose_name': 'Targeted any of these journalists', 'filter_cls': RelationThroughFilter, 'relation': 'journalist'}, 'venue': {'filter_cls': RelationFilter, 'verbose_name': 'venue'}, } From 22b39d299c7c8b51fcfbcfd2d02f165475ca337a Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Mon, 29 Apr 2019 17:20:02 -0400 Subject: [PATCH 06/33] Display targeted journalists/institutions on Incident Pages This adds rows to the incident data table for our new fields: targeted institutions and targeted journalists. It doesn't remove the old row for "targets" for the time being. --- .../templates/incident/incident_page.html | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/incident/templates/incident/incident_page.html b/incident/templates/incident/incident_page.html index c8645fa1e..d6e789b1e 100644 --- a/incident/templates/incident/incident_page.html +++ b/incident/templates/incident/incident_page.html @@ -41,18 +41,41 @@

Incident Data

- {% if page.targets.all %} + {% if page.targeted_institutions.all %} - Targets + Targeted Institutions - {% with page.targets.all as journalists %} + {% with page.targeted_institutions.all as institutions %} + + {% endwith %} + + + {% endif %} + + {% if page.targeted_journalists.all %} + + + Targeted Journalists + + + {% with page.targeted_journalists.all as journalists%} {% endwith %} From 843a9bf13a065d90746064305eadfc867be2f492 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Mon, 29 Apr 2019 17:22:32 -0400 Subject: [PATCH 07/33] Allow targeted journalists with blank institutions --- .../migrations/0035_auto_20190429_2122.py | 21 +++++++++++++++++++ incident/models/items.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 incident/migrations/0035_auto_20190429_2122.py diff --git a/incident/migrations/0035_auto_20190429_2122.py b/incident/migrations/0035_auto_20190429_2122.py new file mode 100644 index 000000000..2f4b27d2c --- /dev/null +++ b/incident/migrations/0035_auto_20190429_2122.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-29 21:22 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('incident', '0034_auto_20190429_1646'), + ] + + operations = [ + migrations.AlterField( + model_name='targetedjournalist', + name='institution', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='incident.Institution'), + ), + ] diff --git a/incident/models/items.py b/incident/models/items.py index ddf913b6c..23adf8c7d 100644 --- a/incident/models/items.py +++ b/incident/models/items.py @@ -57,7 +57,7 @@ class TargetedJournalist(Orderable): journalist = models.ForeignKey(Journalist, on_delete=models.CASCADE, related_name='+') - institution = models.ForeignKey(Institution, on_delete=models.CASCADE, null=True) + institution = models.ForeignKey(Institution, on_delete=models.CASCADE, null=True, blank=True) panels = [ AutocompletePanel('journalist', 'incident.Journalist'), From 98d4a3fed54da3e74dfcd7bafac7d25bc173743e Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Tue, 30 Apr 2019 10:42:22 -0400 Subject: [PATCH 08/33] Add factories for Journalist, Institution, and TargetedJournalist --- incident/tests/factories.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/incident/tests/factories.py b/incident/tests/factories.py index e1aec19cb..1cbcb63d6 100644 --- a/incident/tests/factories.py +++ b/incident/tests/factories.py @@ -20,6 +20,9 @@ Target, State, choices, + Journalist, + Institution, + TargetedJournalist, ) from common.models import CustomImage from common.tests.factories import CategoryPageFactory @@ -397,3 +400,33 @@ class Meta: model = State django_get_or_create = ('name',) name = factory.Faker('state') + + +class JournalistFactory(factory.DjangoModelFactory): + class Meta: + model = Journalist + django_get_or_create = ('title',) + + title = factory.Faker('name') + + +class InstitutionFactory(factory.DjangoModelFactory): + class Meta: + model = Institution + django_get_or_create = ('title',) + + title = factory.LazyAttribute( + lambda p: 'The {} {}'.format( + fake.city(), + random.choice(['Tribune', 'Herald', 'Sun', 'Daily News', 'Post']) + ) + ) + + +class TargetedJournalistFactory(factory.DjangoModelFactory): + class Meta: + model = TargetedJournalist + + journalist = factory.SubFactory(JournalistFactory) + incident = factory.SubFactory(IncidentPageFactory) + institution = factory.SubFactory(InstitutionFactory) From 54c7dca8421f6a55673ddb05d41b2f080e598fc3 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Tue, 30 Apr 2019 10:42:53 -0400 Subject: [PATCH 09/33] Add tests for the RelationThrough filter This is a new kind of filter that I'm adding specifically for the `targeted_journalists` relationship, so let's add some tests to make sure it's doing what we want it to be doing. --- incident/tests/test_filtering.py | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/incident/tests/test_filtering.py b/incident/tests/test_filtering.py index 5e90f435d..2b4493f0a 100644 --- a/incident/tests/test_filtering.py +++ b/incident/tests/test_filtering.py @@ -24,6 +24,7 @@ InexactDateIncidentPageFactory, StateFactory, TargetFactory, + TargetedJournalistFactory ) from incident.utils.incident_filter import IncidentFilter @@ -1163,3 +1164,37 @@ def test_filters_foreign_key_relationships_by_id(self): incidents = incident_filter.get_queryset() self.assertEqual(set(incidents), {incident1}) + + +class RelationThroughTest(TestCase): + @classmethod + def setUpTestData(cls): + GeneralIncidentFilter.objects.all().delete() + CategoryPage.objects.all().delete() + site = Site.objects.get(is_default_site=True) + settings = IncidentFilterSettings.for_site(site) + GeneralIncidentFilter.objects.create( + incident_filter='targeted_journalists', + incident_filter_settings=settings, + ) + + cls.tj1 = TargetedJournalistFactory() + cls.tj2 = TargetedJournalistFactory() + cls.tj3 = TargetedJournalistFactory() + + def test_filter_should_filter_by_single_journalist(self): + incidents = IncidentFilter({ + 'targeted_journalists': self.tj1.journalist.pk, + }).get_queryset() + + self.assertEqual(incidents.count(), 1) + self.assertIn(self.tj1.incident, incidents) + + def test_filter_should_filter_by_multiple_journalists(self): + incidents = IncidentFilter({ + 'targeted_journalists': '{},{}'.format(self.tj1.journalist.pk, self.tj3.journalist.pk), + }).get_queryset() + + self.assertEqual(incidents.count(), 2) + self.assertIn(self.tj1.incident, incidents) + self.assertIn(self.tj3.incident, incidents) From 900a79c532718a2434aa398c5d65fa0e4a069735 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Tue, 30 Apr 2019 16:22:49 -0400 Subject: [PATCH 10/33] Add related name for journalist relation on TargetedJournalist It turns out we actually do need this related name, for when we are trying to count up how many journalists were targets for a certain set of incidents. In that case we have TargetedJournalist objects and we want to ask for Journalists that belong to that set, so we have to have this name to follow that "thread." If this doesn't make any sense, see a future commit that adds the `num_journalist_targets` statistics tag. --- .../migrations/0036_auto_20190430_2022.py | 21 +++++++++++++++++++ incident/models/items.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 incident/migrations/0036_auto_20190430_2022.py diff --git a/incident/migrations/0036_auto_20190430_2022.py b/incident/migrations/0036_auto_20190430_2022.py new file mode 100644 index 000000000..3d553fd93 --- /dev/null +++ b/incident/migrations/0036_auto_20190430_2022.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-04-30 20:22 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('incident', '0035_auto_20190429_2122'), + ] + + operations = [ + migrations.AlterField( + model_name='targetedjournalist', + name='journalist', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='targeted_incidents', to='incident.Journalist'), + ), + ] diff --git a/incident/models/items.py b/incident/models/items.py index 23adf8c7d..823565fbe 100644 --- a/incident/models/items.py +++ b/incident/models/items.py @@ -55,7 +55,7 @@ def autocomplete_create(kls, value): class TargetedJournalist(Orderable): incident = ParentalKey('incident.IncidentPage', on_delete=models.CASCADE, related_name='targeted_journalists') - journalist = models.ForeignKey(Journalist, on_delete=models.CASCADE, related_name='+') + journalist = models.ForeignKey(Journalist, on_delete=models.CASCADE, related_name='targeted_incidents') institution = models.ForeignKey(Institution, on_delete=models.CASCADE, null=True, blank=True) From a32d14a5c217a7ad39bcdfe72176ec54a507b31f Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Tue, 30 Apr 2019 16:26:18 -0400 Subject: [PATCH 11/33] Add statistics tags for number of journalist/institution targets Basically what we're doing here is providing the journalist and institution analgoue for the `num_targets` tag. These should be a drop-in replacement for that, depending on which type of target you want to count. --- incident/tests/factories.py | 13 ++ statistics/templatetags/statistics_tags.py | 31 +++++ statistics/tests/test_statistics_tags.py | 151 +++++++++++++++++++++ 3 files changed, 195 insertions(+) diff --git a/incident/tests/factories.py b/incident/tests/factories.py index 1cbcb63d6..24ef5a1ec 100644 --- a/incident/tests/factories.py +++ b/incident/tests/factories.py @@ -228,6 +228,19 @@ def targets(self, create, count): if not create: self._prefetched_objects_cache = {'targets': targets} + @factory.post_generation + def institution_targets(self, create, count): + if count is None: + count = 2 + make_target = getattr(InstitutionFactory, 'create' if create else 'build') + targets = [] + for i in range(count): + t = make_target() + self.targeted_institutions.add(t) + targets.append(t) + if not create: + self._prefetched_objects_cache = {'targets': targets} + @factory.post_generation def targets_whose_communications_were_obtained(self, create, count): if count is None: diff --git a/statistics/templatetags/statistics_tags.py b/statistics/templatetags/statistics_tags.py index 21250a8f7..155cae3c9 100644 --- a/statistics/templatetags/statistics_tags.py +++ b/statistics/templatetags/statistics_tags.py @@ -26,6 +26,35 @@ def num_incidents(**kwargs): return incident_filter.get_queryset().count() +@statistics.number +@register.simple_tag +def num_institution_targets(**kwargs): + from incident.models.items import Institution + incident_filter = IncidentFilter(kwargs) + try: + incident_filter.clean(strict=True) + except ValidationError: + return '' + queryset = incident_filter.get_queryset() + return Institution.objects.filter(institutions_incidents__in=queryset).distinct().count() + + +@statistics.number +@register.simple_tag +def num_journalist_targets(**kwargs): + from incident.models.items import Journalist, TargetedJournalist + incident_filter = IncidentFilter(kwargs) + try: + incident_filter.clean(strict=True) + except ValidationError: + return '' + queryset = incident_filter.get_queryset() + tj_queryset = TargetedJournalist.objects.filter(incident__in=queryset) + return Journalist.objects.filter( + targeted_incidents__in=tj_queryset, + ).distinct().count() + + @statistics.number @register.simple_tag def num_targets(**kwargs): @@ -42,6 +71,8 @@ def num_targets(**kwargs): @tag_validator(register, 'num_targets') +@tag_validator(register, 'num_institution_targets') +@tag_validator(register, 'num_journalist_targets') @tag_validator(register, 'num_incidents') def validate_filter_kwargs(parser, token): """Return the count of incidents matching the given filter parameters""" diff --git a/statistics/tests/test_statistics_tags.py b/statistics/tests/test_statistics_tags.py index a067eeb2d..5c6c5f039 100644 --- a/statistics/tests/test_statistics_tags.py +++ b/statistics/tests/test_statistics_tags.py @@ -12,6 +12,8 @@ from incident.tests.factories import ( IncidentPageFactory, TargetFactory, + InstitutionFactory, + TargetedJournalistFactory, ) from statistics.templatetags.statistics_tags import ( incidents_in_year_range_by_month, @@ -237,6 +239,155 @@ def test_target_multiple_choice(self): self.assertEqual(rendered, '8') +class NumInstitutionTargetsTest(TestCase): + def setUp(self): + self.custody = 'CUSTODY' + self.returned_full = 'RETURNED_FULL' + self.category = CategoryPageFactory( + title='Equipment Search or Seizure', + incident_filters=['status_of_seized_equipment'], + ) + self.validator = TemplateValidator() + + def test_invalid_args_should_raise_validation_error(self): + template_string = '{{% num_institution_targets categories={} status_of_seized_equipment={} %}}'.format( + str(self.category.pk), + self.custody, + ) + with self.assertRaises(ValidationError): + self.validator(template_string) + + def test_target_count__filtered(self): + # Matched incident page + IncidentPageFactory( + status_of_seized_equipment=self.custody, + categories=[self.category], + institution_targets=3, + ) + IncidentPageFactory( + status_of_seized_equipment=self.returned_full, + categories=[self.category], + institution_targets=5, + ) + template_string = '{{% num_institution_targets categories={} status_of_seized_equipment="{}" %}}'.format( + str(self.category.id), + self.custody, + ) + self.validator(template_string) + rendered = render_as_template(template_string) + self.assertEqual(rendered, '3') + + def test_target_count__combined(self): + IncidentPageFactory( + categories=[self.category], + institution_targets=3, + ) + IncidentPageFactory( + categories=[self.category], + institution_targets=5, + ) + template_string = '{{% num_institution_targets categories={} %}}'.format( + str(self.category.id), + ) + self.validator(template_string) + rendered = render_as_template(template_string) + self.assertEqual(rendered, '8') + + def test_target_count__deduped(self): + inst1 = InstitutionFactory() + inst2 = InstitutionFactory() + incident1 = IncidentPageFactory( + categories=[self.category], + institution_targets=0, + ) + incident1.targeted_institutions.set([inst1, inst2]) + incident1.save() + + incident2 = IncidentPageFactory( + categories=[self.category], + institution_targets=0, + ) + incident2.targeted_institutions = [inst1] + incident2.save() + + template_string = '{{% num_institution_targets categories={} %}}'.format( + str(self.category.id), + ) + self.validator(template_string) + rendered = render_as_template(template_string) + self.assertEqual(rendered, '2') + + +class NumJournalistTargetsTest(TestCase): + def setUp(self): + self.custody = 'CUSTODY' + self.returned_full = 'RETURNED_FULL' + self.category = CategoryPageFactory( + title='Equipment Search or Seizure', + incident_filters=['status_of_seized_equipment'], + ) + self.validator = TemplateValidator() + + def test_invalid_args_should_raise_validation_error(self): + template_string = '{{% num_journalist_targets categories={} status_of_seized_equipment={} %}}'.format( + str(self.category.pk), + self.custody, + ) + with self.assertRaises(ValidationError): + self.validator(template_string) + + def test_target_count__filtered(self): + TargetedJournalistFactory( + incident__categories=[self.category], + incident__status_of_seized_equipment=self.returned_full, + ) + TargetedJournalistFactory( + incident__categories=[self.category], + incident__status_of_seized_equipment=self.custody, + ) + + template_string = '{{% num_journalist_targets categories={} status_of_seized_equipment="{}" %}}'.format( + str(self.category.id), + self.custody, + ) + self.validator(template_string) + rendered = render_as_template(template_string) + self.assertEqual(rendered, '1') + + def test_target_count__combined(self): + TargetedJournalistFactory.create_batch( + 5, + incident__categories=[self.category], + incident__status_of_seized_equipment=self.returned_full, + ) + + template_string = '{{% num_journalist_targets categories={} %}}'.format( + str(self.category.id), + ) + self.validator(template_string) + rendered = render_as_template(template_string) + self.assertEqual(rendered, '5') + + def test_target_count__deduped(self): + tj = TargetedJournalistFactory( + incident__categories=[self.category], + ) + TargetedJournalistFactory( + incident__categories=[self.category], + ) + TargetedJournalistFactory( + incident__categories=[self.category], + journalist=tj.journalist, + ) + + template_string = '{{% num_journalist_targets categories={} %}}'.format( + str(self.category.id), + ) + self.validator(template_string) + rendered = render_as_template(template_string) + self.assertEqual(rendered, '2') + + class TestIncidentsInYearRangeByMonth(TestCase): """Test that incidents_in_year_range_by_month tag """ @classmethod From 4f8580cbd4b37de90d7bb7decb9b74bb2fbed89f Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Wed, 1 May 2019 13:04:24 -0400 Subject: [PATCH 12/33] Reindent --- common/templates/modeladmin/merge_form.html | 79 ++++++++++----------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/common/templates/modeladmin/merge_form.html b/common/templates/modeladmin/merge_form.html index ad5d8090f..f440f782f 100644 --- a/common/templates/modeladmin/merge_form.html +++ b/common/templates/modeladmin/merge_form.html @@ -2,52 +2,49 @@ {% load render_bundle from webpack_loader %} {% block content %} - {% block header %} -
-
-
-
- {% block h1 %}

{{ view.get_page_title }}

{% endblock %} -
-
-
-
- {% endblock %} - - {% block content_main %} -
-

Merge {{ form.merge_model_name }}

-
{% csrf_token %} -
    - {% for field in form %} - -
    - -
    -
    - {{ field }} -
    -
    -
    - - {% endfor %} -
- -
-
- {% endblock content_main %} + {% block header %} +
+
+
+
+ {% block h1 %}

{{ view.get_page_title }}

{% endblock %} +
+
+
+
+ {% endblock %} + + {% block content_main %} +
+

Merge {{ form.merge_model_name }}

+
{% csrf_token %} +
    + {% for field in form %} + +
    + +
    +
    + {{ field }} +
    +
    +
    + + {% endfor %} +
+ +
+
+ {% endblock content_main %} {% endblock content %} {% block extra_js %} - {% render_bundle 'editor' 'js' %} + {% render_bundle 'editor' 'js' %} {% endblock %} {% block extra_css %} - {% render_bundle 'editor' 'css' %} + {% render_bundle 'editor' 'css' %} {% endblock %} - - - From db122404b017088029d15d96eb7e42f71d6939ef Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Wed, 1 May 2019 13:04:53 -0400 Subject: [PATCH 13/33] Fix autocomplete form for merge model admin This was something that seems to have slipped through the cracks when we upgraded to the latest "official" version of the autocomplete plugin. It seems fine to hard-code the locations of the css and js files, given that we don't really expect them to change anytime soon. --- common/forms.py | 2 +- common/templates/modeladmin/merge_form.html | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/common/forms.py b/common/forms.py index 44dac705b..df3acd778 100644 --- a/common/forms.py +++ b/common/forms.py @@ -21,7 +21,7 @@ def __init__(self, *args, **kwargs): widget=type( '_Autocomplete', (Autocomplete,), - dict(page_type=self.merge_model_type, can_create=False, is_single=False) + dict(target_model=self.merge_model_type, can_create=False, is_single=False) ), label='{} to merge'.format(capfirst(self.merge_model_name)) ) diff --git a/common/templates/modeladmin/merge_form.html b/common/templates/modeladmin/merge_form.html index f440f782f..15bb2bce4 100644 --- a/common/templates/modeladmin/merge_form.html +++ b/common/templates/modeladmin/merge_form.html @@ -43,8 +43,10 @@

Merge {{ form.merge_model_name }}

{% block extra_js %} {% render_bundle 'editor' 'js' %} + {% endblock %} {% block extra_css %} {% render_bundle 'editor' 'css' %} + {% endblock %} From d1ba0c6ee5394d53b9e0fc114ac0d188e2f42db4 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Wed, 1 May 2019 13:07:01 -0400 Subject: [PATCH 14/33] Add view/form/admin/tests for merging Journalists We're pretty sure it's easy enough to add journalists to the site that we're going to want some mechanism for combining erroneously added ones, i.e. the times when an editor adds a journalist when they intended to reference an existing one. --- incident/forms.py | 7 ++++- incident/tests/test_views.py | 55 ++++++++++++++++++++++++++++++++++-- incident/views.py | 26 +++++++++++++++-- incident/wagtail_hooks.py | 15 +++++++++- 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/incident/forms.py b/incident/forms.py index 400880a4e..a4e997d3c 100644 --- a/incident/forms.py +++ b/incident/forms.py @@ -1,5 +1,5 @@ from common.forms import BaseMergeForm -from incident.models import Target, Charge, Nationality, Venue, PoliticianOrPublic +from incident.models import Target, Charge, Nationality, Venue, PoliticianOrPublic, Journalist class TargetMergeForm(BaseMergeForm): @@ -7,6 +7,11 @@ class TargetMergeForm(BaseMergeForm): merge_model_type = 'incident.Target' +class JournalistMergeForm(BaseMergeForm): + merge_model = Journalist + merge_model_type = 'incident.Journalist' + + class ChargeMergeForm(BaseMergeForm): merge_model = Charge merge_model_type = 'incident.Charge' diff --git a/incident/tests/test_views.py b/incident/tests/test_views.py index 2c7bff48f..e2bec5a21 100644 --- a/incident/tests/test_views.py +++ b/incident/tests/test_views.py @@ -6,9 +6,13 @@ from wagtail.core.models import Site from wagtail.core.rich_text import RichText -from incident.models import Target, Charge, Nationality, PoliticianOrPublic, Venue -from incident.wagtail_hooks import TargetAdmin, ChargeAdmin, NationalityAdmin, VenueAdmin, PoliticianOrPublicAdmin -from incident.tests.factories import IncidentPageFactory, IncidentIndexPageFactory +from incident.models import Target, Charge, Nationality, PoliticianOrPublic, Venue, Journalist +from incident.wagtail_hooks import TargetAdmin, ChargeAdmin, NationalityAdmin, VenueAdmin, PoliticianOrPublicAdmin, JournalistAdmin +from incident.tests.factories import ( + IncidentPageFactory, + IncidentIndexPageFactory, + TargetedJournalistFactory, +) User = get_user_model() @@ -54,6 +58,51 @@ def test_search_title_and_body(self): ) +class JournalistMergeViewTestCase(TestCase): + @classmethod + def setUpTestData(cls): + cls.tj1 = TargetedJournalistFactory() + cls.tj2 = TargetedJournalistFactory() + cls.tj3 = TargetedJournalistFactory() + cls.user = User.objects.create_superuser(username='test', password='test', email='test@test.com') + + def setUp(self): + self.client.force_login(self.user) + self.new_journalist_title = 'Person One' + self.response = self.client.post( + JournalistAdmin().url_helper.merge_url, + { + 'models_to_merge': json.dumps([{ + 'label': self.tj1.journalist.title, + 'pk': self.tj1.journalist.pk + }, { + 'label': self.tj2.journalist.title, + 'pk': self.tj2.journalist.pk + }, { + 'label': self.tj3.journalist.title, + 'pk': self.tj3.journalist.pk + }, + ]), + 'title_for_merged_models': self.new_journalist_title + } + ) + + def test_successful_request_redirects(self): + self.assertEqual(self.response.status_code, 302) + + def test_correct_redirect_url(self): + """Should redirect to the modelAdmin's index page""" + self.assertEqual(self.response['location'], JournalistAdmin().url_helper.index_url) + + def test_new_journalist_should_be_created(self): + journalist = Journalist.objects.get(title=self.new_journalist_title) + + self.assertEqual( + set(journalist.targeted_incidents.all()), + {self.tj1, self.tj2, self.tj3} + ) + + class TargetMergeViewTest(TestCase): @classmethod def setUpTestData(cls): diff --git a/incident/views.py b/incident/views.py index c1cf1337d..5c1a58469 100644 --- a/incident/views.py +++ b/incident/views.py @@ -2,15 +2,16 @@ from django.core.paginator import Paginator from django.shortcuts import render from django.views.decorators.vary import vary_on_headers -from wagtail.admin.forms.search import SearchForm +from django.views.generic.edit import FormView +from wagtail.admin.forms import SearchForm from wagtail.admin.utils import ( user_has_any_page_permission, user_passes_test, ) from common.views import MergeView -from incident.forms import TargetMergeForm, ChargeMergeForm, VenueMergeForm, NationalityMergeForm, PoliticianOrPublicMergeForm -from incident.models.incident_page import IncidentPage +from incident.forms import TargetMergeForm, ChargeMergeForm, VenueMergeForm, NationalityMergeForm, PoliticianOrPublicMergeForm, JournalistMergeForm +from incident.models import IncidentPage, Journalist, TargetedJournalist @vary_on_headers('X-Requested-With') @@ -65,3 +66,22 @@ class VenueMergeView(MergeView): class PoliticianOrPublicMergeView(MergeView): form_class = PoliticianOrPublicMergeForm + + +class JournalistMergeView(FormView): + form_class = JournalistMergeForm + template_name = 'modeladmin/merge_form.html' + model_admin = None + + def get_success_url(self): + return self.model_admin.url_helper.index_url + + def form_valid(self, form): + models_to_merge = form.cleaned_data['models_to_merge'] + new_journalist_title = form.cleaned_data['title_for_merged_models'] + + journalist, _ = Journalist.objects.get_or_create(title=new_journalist_title) + TargetedJournalist.objects.filter(journalist__in=models_to_merge).update(journalist=journalist) + + models_to_merge.delete() + return super().form_valid(form) diff --git a/incident/wagtail_hooks.py b/incident/wagtail_hooks.py index eb7c6dea1..d846028ec 100644 --- a/incident/wagtail_hooks.py +++ b/incident/wagtail_hooks.py @@ -10,6 +10,7 @@ from common.wagtail_hooks import MergeAdmin from incident.models import ( Target, + Journalist, Charge, Nationality, PoliticianOrPublic, @@ -22,6 +23,7 @@ PoliticianOrPublicMergeView, TargetMergeView, VenueMergeView, + JournalistMergeView, ) @@ -52,6 +54,17 @@ class TargetAdmin(MergeAdmin): search_fields = ('title',) +class JournalistAdmin(MergeAdmin): + model = Journalist + merge_view_class = JournalistMergeView + menu_label = 'Journalist' + menu_icon = 'edit' + add_to_settings_menu = False # or True to add your model to the Settings sub-menu + exclude_from_explorer = False # or True to exclude pages of this type from Wagtail's explorer view + list_display = ('title',) + search_fields = ('title',) + + class ChargeAdmin(MergeAdmin): model = Charge merge_view_class = ChargeMergeView @@ -100,7 +113,7 @@ class IncidentGroup(ModelAdminGroup): menu_label = 'Incident M2Ms' menu_icon = 'folder-open-inverse' # change as required menu_order = 600 # will put in 7th place (000 being 1st, 100 2nd) - items = (TargetAdmin, ChargeAdmin, NationalityAdmin, PoliticianOrPublicAdmin, VenueAdmin) + items = (TargetAdmin, ChargeAdmin, NationalityAdmin, PoliticianOrPublicAdmin, VenueAdmin, JournalistAdmin) modeladmin_register(IncidentGroup) From 39731f58e6a27c8e0270dbf17619879a616feb02 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Wed, 1 May 2019 14:56:08 -0400 Subject: [PATCH 15/33] Add form/view/admin/tests for merging Institutions --- incident/forms.py | 7 +++- incident/tests/test_views.py | 66 ++++++++++++++++++++++++++++++++++-- incident/views.py | 31 +++++++++++++++-- incident/wagtail_hooks.py | 15 +++++++- 4 files changed, 113 insertions(+), 6 deletions(-) diff --git a/incident/forms.py b/incident/forms.py index a4e997d3c..9f66e60c0 100644 --- a/incident/forms.py +++ b/incident/forms.py @@ -1,5 +1,5 @@ from common.forms import BaseMergeForm -from incident.models import Target, Charge, Nationality, Venue, PoliticianOrPublic, Journalist +from incident.models import Target, Charge, Nationality, Venue, PoliticianOrPublic, Journalist, Institution class TargetMergeForm(BaseMergeForm): @@ -12,6 +12,11 @@ class JournalistMergeForm(BaseMergeForm): merge_model_type = 'incident.Journalist' +class InstitutionMergeForm(BaseMergeForm): + merge_model = Institution + merge_model_type = 'incident.Institution' + + class ChargeMergeForm(BaseMergeForm): merge_model = Charge merge_model_type = 'incident.Charge' diff --git a/incident/tests/test_views.py b/incident/tests/test_views.py index e2bec5a21..39fd4a9bc 100644 --- a/incident/tests/test_views.py +++ b/incident/tests/test_views.py @@ -6,8 +6,8 @@ from wagtail.core.models import Site from wagtail.core.rich_text import RichText -from incident.models import Target, Charge, Nationality, PoliticianOrPublic, Venue, Journalist -from incident.wagtail_hooks import TargetAdmin, ChargeAdmin, NationalityAdmin, VenueAdmin, PoliticianOrPublicAdmin, JournalistAdmin +from incident.models import Target, Charge, Nationality, PoliticianOrPublic, Venue, Journalist, Institution, TargetedJournalist +from incident.wagtail_hooks import TargetAdmin, ChargeAdmin, NationalityAdmin, VenueAdmin, PoliticianOrPublicAdmin, JournalistAdmin, InstitutionAdmin from incident.tests.factories import ( IncidentPageFactory, IncidentIndexPageFactory, @@ -58,6 +58,68 @@ def test_search_title_and_body(self): ) +class InstitutionMergeViewTestCase(TestCase): + @classmethod + def setUpTestData(cls): + cls.inc1 = IncidentPageFactory(institution_targets=1) + cls.inc2 = IncidentPageFactory(institution_targets=2) + cls.inc3 = IncidentPageFactory(institution_targets=3) + + cls.tj = TargetedJournalistFactory(institution=cls.inc1.targeted_institutions.first()) + + cls.user = User.objects.create_superuser(username='test', password='test', email='test@test.com') + + def setUp(self): + self.client.force_login(self.user) + self.new_institution_title = 'Insitution XIII' + + inst1 = self.inc1.targeted_institutions.first() + inst2, inst3 = self.inc2.targeted_institutions.all() + inst4 = self.inc3.targeted_institutions.first() + + self.response = self.client.post( + InstitutionAdmin().url_helper.merge_url, + { + 'models_to_merge': json.dumps([{ + 'label': inst1.title, + 'pk': inst1.pk, + }, { + 'label': inst2.title, + 'pk': inst2.pk, + }, { + 'label': inst3.title, + 'pk': inst3.pk, + }, { + 'label': inst4.title, + 'pk': inst4.pk, + }, + ]), + 'title_for_merged_models': self.new_institution_title + } + ) + + def test_successful_request_redirects(self): + self.assertEqual(self.response.status_code, 302) + + def test_correct_redirect_url(self): + """Should redirect to the modelAdmin's index page""" + self.assertEqual(self.response['location'], InstitutionAdmin().url_helper.index_url) + + def test_new_institution_should_be_created(self): + institution = Institution.objects.get(title=self.new_institution_title) + + self.assertEqual( + set(institution.institutions_incidents.all()), + {self.inc1, self.inc2, self.inc3} + ) + + def test_targeted_journalists_at_institutions_should_be_updated(self): + self.assertEqual( + TargetedJournalist.objects.get(pk=self.tj.pk).institution, + Institution.objects.get(title=self.new_institution_title) + ) + + class JournalistMergeViewTestCase(TestCase): @classmethod def setUpTestData(cls): diff --git a/incident/views.py b/incident/views.py index 5c1a58469..3c4d99b36 100644 --- a/incident/views.py +++ b/incident/views.py @@ -10,8 +10,8 @@ ) from common.views import MergeView -from incident.forms import TargetMergeForm, ChargeMergeForm, VenueMergeForm, NationalityMergeForm, PoliticianOrPublicMergeForm, JournalistMergeForm -from incident.models import IncidentPage, Journalist, TargetedJournalist +from incident.forms import TargetMergeForm, ChargeMergeForm, VenueMergeForm, NationalityMergeForm, PoliticianOrPublicMergeForm, JournalistMergeForm, InstitutionMergeForm +from incident.models import IncidentPage, Journalist, TargetedJournalist, Institution @vary_on_headers('X-Requested-With') @@ -85,3 +85,30 @@ def form_valid(self, form): models_to_merge.delete() return super().form_valid(form) + + +class InstitutionMergeView(FormView): + form_class = InstitutionMergeForm + template_name = 'modeladmin/merge_form.html' + model_admin = None + + def get_success_url(self): + return self.model_admin.url_helper.index_url + + def form_valid(self, form): + models_to_merge = form.cleaned_data['models_to_merge'] + new_inst_title = form.cleaned_data['title_for_merged_models'] + + new_institution, _ = Institution.objects.get_or_create(title=new_inst_title) + + TargetedJournalist.objects.filter( + institution__in=models_to_merge + ).update(institution=new_institution) + + for incident in IncidentPage.objects.filter(targeted_institutions__in=models_to_merge): + incident.targeted_institutions.add(new_institution) + incident.save() + + models_to_merge.delete() + + return super().form_valid(form) diff --git a/incident/wagtail_hooks.py b/incident/wagtail_hooks.py index d846028ec..0a36de738 100644 --- a/incident/wagtail_hooks.py +++ b/incident/wagtail_hooks.py @@ -15,6 +15,7 @@ Nationality, PoliticianOrPublic, Venue, + Institution, ) from incident.views import ( ChargeMergeView, @@ -24,6 +25,7 @@ TargetMergeView, VenueMergeView, JournalistMergeView, + InstitutionMergeView, ) @@ -65,6 +67,17 @@ class JournalistAdmin(MergeAdmin): search_fields = ('title',) +class InstitutionAdmin(MergeAdmin): + model = Institution + merge_view_class = InstitutionMergeView + menu_label = 'Institution' + menu_icon = 'edit' + add_to_settings_menu = False # or True to add your model to the Settings sub-menu + exclude_from_explorer = False # or True to exclude pages of this type from Wagtail's explorer view + list_display = ('title',) + search_fields = ('title',) + + class ChargeAdmin(MergeAdmin): model = Charge merge_view_class = ChargeMergeView @@ -113,7 +126,7 @@ class IncidentGroup(ModelAdminGroup): menu_label = 'Incident M2Ms' menu_icon = 'folder-open-inverse' # change as required menu_order = 600 # will put in 7th place (000 being 1st, 100 2nd) - items = (TargetAdmin, ChargeAdmin, NationalityAdmin, PoliticianOrPublicAdmin, VenueAdmin, JournalistAdmin) + items = (TargetAdmin, ChargeAdmin, NationalityAdmin, PoliticianOrPublicAdmin, VenueAdmin, JournalistAdmin, InstitutionAdmin) modeladmin_register(IncidentGroup) From e1b9633bb2ab391a97aad58a6b4a86cdc496e546 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Wed, 1 May 2019 16:45:22 -0400 Subject: [PATCH 16/33] Update incident summary for journalist/institution separation --- incident/tests/factories.py | 13 +++- incident/tests/test_filtering.py | 110 ++++++++++++++++++++++-------- incident/utils/incident_filter.py | 7 +- 3 files changed, 99 insertions(+), 31 deletions(-) diff --git a/incident/tests/factories.py b/incident/tests/factories.py index 24ef5a1ec..923b83083 100644 --- a/incident/tests/factories.py +++ b/incident/tests/factories.py @@ -239,7 +239,18 @@ def institution_targets(self, create, count): self.targeted_institutions.add(t) targets.append(t) if not create: - self._prefetched_objects_cache = {'targets': targets} + self._prefetched_objects_cache = {'targeted_institutions': targets} + + @factory.post_generation + def journalist_targets(self, create, count): + if count is None: + count = 0 + make_targeted_journalist = getattr(TargetedJournalistFactory, 'create' if create else 'build') + targets = [] + for i in range(count): + make_targeted_journalist(incident=self) + if not create: + self._prefetched_objects_cache = {'targeted_institutions': targets} @factory.post_generation def targets_whose_communications_were_obtained(self, create, count): diff --git a/incident/tests/test_filtering.py b/incident/tests/test_filtering.py index 2b4493f0a..7c17d302e 100644 --- a/incident/tests/test_filtering.py +++ b/incident/tests/test_filtering.py @@ -23,7 +23,8 @@ IncidentIndexPageFactory, InexactDateIncidentPageFactory, StateFactory, - TargetFactory, + InstitutionFactory, + JournalistFactory, TargetedJournalistFactory ) from incident.utils.incident_filter import IncidentFilter @@ -751,10 +752,10 @@ def test_summary__january(self): "Summary should correctly count incidents in January" # Two incidents in January 2018, two not - IncidentPageFactory(date=date(2018, 1, 15), targets=2) - IncidentPageFactory(date=date(2018, 1, 16), targets=2) - IncidentPageFactory(date=date(2017, 1, 15), targets=2) - IncidentPageFactory(date=date(2018, 2, 15), targets=2) + IncidentPageFactory(date=date(2018, 1, 15), journalist_targets=1, institution_targets=1) + IncidentPageFactory(date=date(2018, 1, 16), journalist_targets=1, institution_targets=1) + IncidentPageFactory(date=date(2017, 1, 15), journalist_targets=1, institution_targets=1) + IncidentPageFactory(date=date(2018, 2, 15), journalist_targets=1, institution_targets=1) with patch('incident.utils.incident_filter.date') as date_: date_.today = lambda: date(2018, 1, 20) @@ -762,7 +763,8 @@ def test_summary__january(self): self.assertCountEqual(summary, ( ('Total Results', 4), - ('Journalists affected', 8), + ('Journalists affected', 4), + ('Institutions affected', 4), ('Results in 2018', 3), ('Results in January', 2) )) @@ -771,10 +773,10 @@ def test_summary__december(self): "Summary should correctly count incidents in December" # Two incidents in December 2018, two not - IncidentPageFactory(date=date(2018, 12, 15), targets=2) - IncidentPageFactory(date=date(2018, 12, 16), targets=2) - IncidentPageFactory(date=date(2019, 1, 1), targets=2) - IncidentPageFactory(date=date(2018, 2, 15), targets=2) + IncidentPageFactory(date=date(2018, 12, 15), journalist_targets=2) + IncidentPageFactory(date=date(2018, 12, 16), journalist_targets=2) + IncidentPageFactory(date=date(2019, 1, 1), journalist_targets=2) + IncidentPageFactory(date=date(2018, 2, 15), journalist_targets=2) with patch('incident.utils.incident_filter.date') as date_: date_.today = lambda: date(2018, 12, 20) @@ -783,6 +785,7 @@ def test_summary__december(self): self.assertCountEqual(summary, ( ('Total Results', 4), ('Journalists affected', 8), + ('Institutions affected', 8), ('Results in 2018', 3), ('Results in December', 2) )) @@ -792,13 +795,15 @@ def test_single_category_excludes_category_count(self): status_of_seized_equipment=self.custody, categories=[self.category], date=timezone.now().date(), - targets=0, + journalist_targets=0, + institution_targets=0, ) IncidentPageFactory( status_of_seized_equipment=self.returned_full, categories=[self.category], date=timezone.now().date(), - targets=0, + journalist_targets=0, + institution_targets=0, ) incident_filter = IncidentFilter(dict( categories=str(self.category.id), @@ -809,6 +814,7 @@ def test_single_category_excludes_category_count(self): self.assertEqual(summary, ( ('Total Results', 1), ('Journalists affected', 0), + ('Institutions affected', 0), ('Results in {}'.format(timezone.now().year), 1), ('Results in {0:%B}'.format(timezone.now().date()), 1), )) @@ -848,6 +854,7 @@ def test_category_incident_count_filtered(self): self.assertEqual(summary, ( ('Total Results', 2), ('Journalists affected', 0), + ('Institutions affected', 4), ('Results in {}'.format(timezone.now().year), 2), ('Results in {0:%B}'.format(timezone.now().date()), 2), (self.category.title, 1), @@ -864,13 +871,15 @@ def test_target_count__filtered(self): status_of_seized_equipment=self.custody, categories=[self.category], date=timezone.now().date(), - targets=3, + journalist_targets=3, + institution_targets=3, ) IncidentPageFactory( status_of_seized_equipment=self.returned_full, categories=[self.category], date=timezone.now().date(), - targets=5, + journalist_targets=5, + institution_targets=5, ) incident_filter = IncidentFilter(dict( categories=str(self.category.id), @@ -881,6 +890,7 @@ def test_target_count__filtered(self): self.assertEqual(summary, ( ('Total Results', 1), ('Journalists affected', 3), + ('Institutions affected', 3), ('Results in {}'.format(timezone.now().year), 1), ('Results in {0:%B}'.format(timezone.now().date()), 1), )) @@ -889,16 +899,18 @@ def test_target_count__filtered(self): 'status_of_seized_equipment': [self.custody], }) - def test_target_count__combined(self): + def test_target_journalist_count__combined(self): IncidentPageFactory( categories=[self.category], date=timezone.now().date(), - targets=3, + journalist_targets=3, + institution_targets=0, ) IncidentPageFactory( categories=[self.category], date=timezone.now().date(), - targets=5, + journalist_targets=5, + institution_targets=0, ) incident_filter = IncidentFilter(dict( categories=str(self.category.id), @@ -908,6 +920,43 @@ def test_target_count__combined(self): self.assertEqual(summary, ( ('Total Results', 2), ('Journalists affected', 8), + ('Institutions affected', 0), + ('Results in {}'.format(timezone.now().year), 2), + ('Results in {0:%B}'.format(timezone.now().date()), 2), + )) + self.assertEqual(incident_filter.cleaned_data, { + 'categories': [self.category.id], + }) + + def test_journalist_count__deduped(self): + journalist1 = JournalistFactory() + journalist2 = JournalistFactory() + incident1 = IncidentPageFactory( + categories=[self.category], + date=timezone.now().date(), + targets=0, + institution_targets=0, + ) + TargetedJournalistFactory(incident=incident1, journalist=journalist1) + TargetedJournalistFactory(incident=incident1, journalist=journalist2) + + incident2 = IncidentPageFactory( + categories=[self.category], + date=timezone.now().date(), + targets=0, + institution_targets=0, + ) + TargetedJournalistFactory(incident=incident2, journalist=journalist1) + + incident_filter = IncidentFilter(dict( + categories=str(self.category.id), + )) + + summary = incident_filter.get_summary() + self.assertEqual(summary, ( + ('Total Results', 2), + ('Journalists affected', 2), + ('Institutions affected', 0), ('Results in {}'.format(timezone.now().year), 2), ('Results in {0:%B}'.format(timezone.now().date()), 2), )) @@ -915,23 +964,25 @@ def test_target_count__combined(self): 'categories': [self.category.id], }) - def test_target_count__deduped(self): - target1 = TargetFactory() - target2 = TargetFactory() + def test_institution_count__deduped(self): + inst1 = InstitutionFactory() + inst2 = InstitutionFactory() incident1 = IncidentPageFactory( categories=[self.category], date=timezone.now().date(), targets=0, + institution_targets=0, ) - incident1.targets = [target1, target2] + incident1.targeted_institutions.set([inst1, inst2]) incident1.save() incident2 = IncidentPageFactory( categories=[self.category], date=timezone.now().date(), targets=0, + institution_targets=0, ) - incident2.targets = [target1] + incident2.targeted_institutions.set([inst1]) incident2.save() incident_filter = IncidentFilter(dict( @@ -941,7 +992,8 @@ def test_target_count__deduped(self): summary = incident_filter.get_summary() self.assertEqual(summary, ( ('Total Results', 2), - ('Journalists affected', 2), + ('Journalists affected', 0), + ('Institutions affected', 2), ('Results in {}'.format(timezone.now().year), 2), ('Results in {0:%B}'.format(timezone.now().date()), 2), )) @@ -954,12 +1006,12 @@ def test_search__no_categories(self): IncidentPageFactory( title='asdf', date=timezone.now().date(), - targets=3, + journalist_targets=3, ) IncidentPageFactory( title='zxcv', date=timezone.now().date(), - targets=5, + journalist_targets=5, ) incident_filter = IncidentFilter({ 'search': 'asdf', @@ -969,6 +1021,7 @@ def test_search__no_categories(self): self.assertEqual(summary, ( ('Total Results', 1), ('Journalists affected', 3), + ('Institutions affected', 2), ('Results in {}'.format(timezone.now().year), 1), ('Results in {0:%B}'.format(timezone.now().date()), 1), )) @@ -985,19 +1038,19 @@ def test_search__filters_categories(self): title='asdf 1', categories=[self.category], date=timezone.now().date(), - targets=3, + journalist_targets=3, ) IncidentPageFactory( title='zxcv', categories=[self.category], date=timezone.now().date(), - targets=5, + journalist_targets=5, ) IncidentPageFactory( title='asdf 2', categories=[category2], date=timezone.now().date(), - targets=7, + journalist_targets=7, ) incident_filter = IncidentFilter({ 'categories': '{},{}'.format(self.category.id, category2.id), @@ -1008,6 +1061,7 @@ def test_search__filters_categories(self): self.assertEqual(summary, ( ('Total Results', 2), ('Journalists affected', 10), + ('Institutions affected', 4), ('Results in {}'.format(timezone.now().year), 2), ('Results in {0:%B}'.format(timezone.now().date()), 2), (self.category.title, 1), diff --git a/incident/utils/incident_filter.py b/incident/utils/incident_filter.py index 8dbb75e83..593f9abd1 100644 --- a/incident/utils/incident_filter.py +++ b/incident/utils/incident_filter.py @@ -670,7 +670,7 @@ def get_summary(self): """ from common.models import CategoryPage - from incident.models.items import Target + from incident.models.items import Institution, TargetedJournalist, Journalist queryset = self._get_queryset() TODAY = date.today() @@ -692,9 +692,12 @@ def get_summary(self): num_this_year = incidents_this_year.count() num_this_month = incidents_this_month.count() + tj_queryset = TargetedJournalist.objects.filter(incident__in=queryset) + summary = ( ('Total Results', total_queryset.count()), - ('Journalists affected', Target.objects.filter(targets_incidents__in=total_queryset).distinct().count()), + ('Journalists affected', Journalist.objects.filter(targeted_incidents__in=tj_queryset).distinct().count()), + ('Institutions affected', Institution.objects.filter(institutions_incidents__in=total_queryset).distinct().count()), ) if num_this_year > 0: From e3af905062b9f80ec7dbefd4a026c153f4be563e Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Tue, 28 May 2019 11:15:44 -0400 Subject: [PATCH 17/33] Add merge migration This is required due to rebasing on top of the wagtail 2.0 upgrade code. --- incident/migrations/0037_merge_20190528_1459.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 incident/migrations/0037_merge_20190528_1459.py diff --git a/incident/migrations/0037_merge_20190528_1459.py b/incident/migrations/0037_merge_20190528_1459.py new file mode 100644 index 000000000..502d004aa --- /dev/null +++ b/incident/migrations/0037_merge_20190528_1459.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.7 on 2019-05-28 14:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('incident', '0032_merge_20190403_2121'), + ('incident', '0036_auto_20190430_2022'), + ] + + operations = [ + ] From 60c48c184b4e3153de395d75982f86c584b86865 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Tue, 28 May 2019 11:16:18 -0400 Subject: [PATCH 18/33] Fix imports to be wagtail 2.0 compatible --- incident/models/items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incident/models/items.py b/incident/models/items.py index 823565fbe..316f644f6 100644 --- a/incident/models/items.py +++ b/incident/models/items.py @@ -2,7 +2,7 @@ from modelcluster.models import ClusterableModel from modelcluster.fields import ParentalKey -from wagtail.wagtailcore.models import Orderable +from wagtail.core.models import Orderable from wagtailautocomplete.edit_handlers import AutocompletePanel From 677fbfe400f2f5da87eb8dccc2db56a4a6871314 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Tue, 28 May 2019 11:16:35 -0400 Subject: [PATCH 19/33] incident tests: Add journalists/institution fields to creation tests These tests were added in master, which we've now rebased on, so let's make them work for us. --- incident/tests/test_pages.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/incident/tests/test_pages.py b/incident/tests/test_pages.py index 2287a3dac..34dda7279 100644 --- a/incident/tests/test_pages.py +++ b/incident/tests/test_pages.py @@ -295,6 +295,8 @@ def test_can_preview_incident_page(self): ]), 'state': 'null', 'targets': 'null', + 'targeted_institutions': 'null', + 'targeted_journalists': inline_formset([]), 'tags': 'null', 'current_charges': 'null', 'dropped_charges': 'null', @@ -333,6 +335,8 @@ def test_can_create_incident_page(self): ]), 'state': 'null', 'targets': 'null', + 'targeted_institutions': 'null', + 'targeted_journalists': inline_formset([]), 'tags': 'null', 'current_charges': 'null', 'dropped_charges': 'null', From f165bbd1dc7b0d445b6e8aa820d7372608ebe8ad Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Tue, 28 May 2019 11:39:59 -0400 Subject: [PATCH 20/33] Add missing migration This I think is needed because the choices for the incident filters and statistics items are dynmically generated, and we've altered the potential options for them by adding the journalists/institution fields, so these need to be updated to take that into account. --- common/migrations/0056_auto_20190528_1539.py | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 common/migrations/0056_auto_20190528_1539.py diff --git a/common/migrations/0056_auto_20190528_1539.py b/common/migrations/0056_auto_20190528_1539.py new file mode 100644 index 000000000..2e869acdc --- /dev/null +++ b/common/migrations/0056_auto_20190528_1539.py @@ -0,0 +1,28 @@ +# Generated by Django 2.1.7 on 2019-05-28 15:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0055_auto_20190318_2031'), + ] + + operations = [ + migrations.AlterField( + model_name='categoryincidentfilter', + name='incident_filter', + field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_status', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), + ), + migrations.AlterField( + model_name='generalincidentfilter', + name='incident_filter', + field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_status', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), + ), + migrations.AlterField( + model_name='statisticsitem', + name='dataset', + field=models.CharField(choices=[('num_incidents', 'num_incidents'), ('num_institution_targets', 'num_institution_targets'), ('num_journalist_targets', 'num_journalist_targets'), ('num_targets', 'num_targets')], max_length=255), + ), + ] From 6e718d03d2072d42e2407427c89256df763d60d9 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Tue, 23 Jul 2019 14:34:41 -0400 Subject: [PATCH 21/33] Add merged migrations for rebase onto master --- common/migrations/0058_merge_20190723_1834.py | 14 ++++++++++++++ incident/migrations/0038_merge_20190723_1834.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 common/migrations/0058_merge_20190723_1834.py create mode 100644 incident/migrations/0038_merge_20190723_1834.py diff --git a/common/migrations/0058_merge_20190723_1834.py b/common/migrations/0058_merge_20190723_1834.py new file mode 100644 index 000000000..6b8cfbeb7 --- /dev/null +++ b/common/migrations/0058_merge_20190723_1834.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.10 on 2019-07-23 18:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0057_auto_20190723_0332'), + ('common', '0056_auto_20190528_1539'), + ] + + operations = [ + ] diff --git a/incident/migrations/0038_merge_20190723_1834.py b/incident/migrations/0038_merge_20190723_1834.py new file mode 100644 index 000000000..b46d0dcd4 --- /dev/null +++ b/incident/migrations/0038_merge_20190723_1834.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.10 on 2019-07-23 18:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('incident', '0034_incidentauthor'), + ('incident', '0037_merge_20190528_1459'), + ] + + operations = [ + ] From ea3b45c1cf01ca2cd456c4db6e77c1b5474f32c1 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Tue, 23 Jul 2019 14:44:37 -0400 Subject: [PATCH 22/33] Improve InstitutionFactory uniqueness --- incident/tests/factories.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/incident/tests/factories.py b/incident/tests/factories.py index 923b83083..4586b9ee7 100644 --- a/incident/tests/factories.py +++ b/incident/tests/factories.py @@ -438,14 +438,19 @@ class InstitutionFactory(factory.DjangoModelFactory): class Meta: model = Institution django_get_or_create = ('title',) + exclude = ('city',) - title = factory.LazyAttribute( - lambda p: 'The {} {}'.format( - fake.city(), - random.choice(['Tribune', 'Herald', 'Sun', 'Daily News', 'Post']) + title = factory.LazyAttributeSequence( + lambda o, n: 'The {city} {paper} {n}'.format( + city=o.city, + paper=random.choice(['Tribune', 'Herald', 'Sun', 'Daily News', 'Post']), + n=n, ) ) + # Lazy values + city = factory.Faker('city') + class TargetedJournalistFactory(factory.DjangoModelFactory): class Meta: From 2d579335ccdc2784174b30efbc4d60714fb0408a Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Tue, 23 Jul 2019 14:51:51 -0400 Subject: [PATCH 23/33] Add missing migration --- common/migrations/0059_auto_20190723_1851.py | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 common/migrations/0059_auto_20190723_1851.py diff --git a/common/migrations/0059_auto_20190723_1851.py b/common/migrations/0059_auto_20190723_1851.py new file mode 100644 index 000000000..1ae8c6e0d --- /dev/null +++ b/common/migrations/0059_auto_20190723_1851.py @@ -0,0 +1,23 @@ +# Generated by Django 2.1.10 on 2019-07-23 18:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0058_merge_20190723_1834'), + ] + + operations = [ + migrations.AlterField( + model_name='categoryincidentfilter', + name='incident_filter', + field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('authors', 'Incident author'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_status', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), + ), + migrations.AlterField( + model_name='generalincidentfilter', + name='incident_filter', + field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('authors', 'Incident author'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_status', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), + ), + ] From 1021f06c8c137448f7f4efe44113c421f87ee5f3 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Mon, 9 Sep 2019 16:27:33 -0400 Subject: [PATCH 24/33] Merge migrations --- common/migrations/0060_merge_20190909_2024.py | 14 ++++++++++++++ incident/migrations/0039_merge_20190909_2024.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 common/migrations/0060_merge_20190909_2024.py create mode 100644 incident/migrations/0039_merge_20190909_2024.py diff --git a/common/migrations/0060_merge_20190909_2024.py b/common/migrations/0060_merge_20190909_2024.py new file mode 100644 index 000000000..d4f64c03f --- /dev/null +++ b/common/migrations/0060_merge_20190909_2024.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.11 on 2019-09-09 20:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0059_auto_20190723_1851'), + ('common', '0059_auto_20190806_2055'), + ] + + operations = [ + ] diff --git a/incident/migrations/0039_merge_20190909_2024.py b/incident/migrations/0039_merge_20190909_2024.py new file mode 100644 index 000000000..30181dc27 --- /dev/null +++ b/incident/migrations/0039_merge_20190909_2024.py @@ -0,0 +1,14 @@ +# Generated by Django 2.1.11 on 2019-09-09 20:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('incident', '0036_incidentpage_suppress_footer'), + ('incident', '0038_merge_20190723_1834'), + ] + + operations = [ + ] From b8cf134a58f8e745f4879e184e86b413156c2cdc Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Mon, 9 Sep 2019 16:46:14 -0400 Subject: [PATCH 25/33] Add missing migration --- common/migrations/0061_auto_20190909_2046.py | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 common/migrations/0061_auto_20190909_2046.py diff --git a/common/migrations/0061_auto_20190909_2046.py b/common/migrations/0061_auto_20190909_2046.py new file mode 100644 index 000000000..86e964f77 --- /dev/null +++ b/common/migrations/0061_auto_20190909_2046.py @@ -0,0 +1,23 @@ +# Generated by Django 2.1.11 on 2019-09-09 20:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0060_merge_20190909_2024'), + ] + + operations = [ + migrations.AlterField( + model_name='categoryincidentfilter', + name='incident_filter', + field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('authors', 'Incident author'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('primary_video', 'Primary video'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_status', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('suppress_footer', 'Suppress Footer Call to Action'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), + ), + migrations.AlterField( + model_name='generalincidentfilter', + name='incident_filter', + field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('authors', 'Incident author'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('primary_video', 'Primary video'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_status', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('suppress_footer', 'Suppress Footer Call to Action'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), + ), + ] From 3034077b99c3fa50bf427ee0833a4f3bd04f2f40 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Mon, 27 Jan 2020 13:22:17 -0500 Subject: [PATCH 26/33] Fix admin forms import path --- incident/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incident/views.py b/incident/views.py index 3c4d99b36..e300a2a08 100644 --- a/incident/views.py +++ b/incident/views.py @@ -3,7 +3,7 @@ from django.shortcuts import render from django.views.decorators.vary import vary_on_headers from django.views.generic.edit import FormView -from wagtail.admin.forms import SearchForm +from wagtail.admin.forms.search import SearchForm from wagtail.admin.utils import ( user_has_any_page_permission, user_passes_test, From 3f3f1fb6485ee55e1ab59b04df199d4261119ad8 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Mon, 27 Jan 2020 13:22:28 -0500 Subject: [PATCH 27/33] Merge migrations from master --- common/migrations/0063_merge_20200127_1819.py | 14 +++++++++++ common/migrations/0064_auto_20200127_1832.py | 23 +++++++++++++++++++ .../migrations/0040_merge_20200127_1819.py | 14 +++++++++++ 3 files changed, 51 insertions(+) create mode 100644 common/migrations/0063_merge_20200127_1819.py create mode 100644 common/migrations/0064_auto_20200127_1832.py create mode 100644 incident/migrations/0040_merge_20200127_1819.py diff --git a/common/migrations/0063_merge_20200127_1819.py b/common/migrations/0063_merge_20200127_1819.py new file mode 100644 index 000000000..9c2a52f6b --- /dev/null +++ b/common/migrations/0063_merge_20200127_1819.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.9 on 2020-01-27 18:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0062_auto_20191023_1738'), + ('common', '0061_auto_20190909_2046'), + ] + + operations = [ + ] diff --git a/common/migrations/0064_auto_20200127_1832.py b/common/migrations/0064_auto_20200127_1832.py new file mode 100644 index 000000000..e35bd2990 --- /dev/null +++ b/common/migrations/0064_auto_20200127_1832.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.9 on 2020-01-27 18:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0063_merge_20200127_1819'), + ] + + operations = [ + migrations.AlterField( + model_name='categoryincidentfilter', + name='incident_filter', + field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('authors', 'Incident author'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('primary_video', 'Primary video'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_statuses', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('suppress_footer', 'Suppress Footer Call to Action'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), + ), + migrations.AlterField( + model_name='generalincidentfilter', + name='incident_filter', + field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('authors', 'Incident author'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('primary_video', 'Primary video'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_statuses', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('suppress_footer', 'Suppress Footer Call to Action'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), + ), + ] diff --git a/incident/migrations/0040_merge_20200127_1819.py b/incident/migrations/0040_merge_20200127_1819.py new file mode 100644 index 000000000..11a6c801c --- /dev/null +++ b/incident/migrations/0040_merge_20200127_1819.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.9 on 2020-01-27 18:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('incident', '0039_auto_20191010_1014'), + ('incident', '0039_merge_20190909_2024'), + ] + + operations = [ + ] From 4450fcb5cb8ba5b6428d88548dfdc833a85f3116 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Wed, 12 Feb 2020 11:50:50 -0500 Subject: [PATCH 28/33] Add human-readable string representations for new models --- incident/models/items.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/incident/models/items.py b/incident/models/items.py index 316f644f6..24c29db33 100644 --- a/incident/models/items.py +++ b/incident/models/items.py @@ -43,6 +43,9 @@ def autocomplete_create(kls, value): title = models.CharField(max_length=255) + def __str__(self): + return self.title + class Institution(ClusterableModel): @classmethod @@ -51,6 +54,9 @@ def autocomplete_create(kls, value): title = models.CharField(max_length=255, unique=True) + def __str__(self): + return self.title + class TargetedJournalist(Orderable): incident = ParentalKey('incident.IncidentPage', on_delete=models.CASCADE, related_name='targeted_journalists') From f9d0164290fba46c476f7b865d8de71bf1f08bf4 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Wed, 12 Feb 2020 11:55:10 -0500 Subject: [PATCH 29/33] Add GovernmentWorker model, data migration, misc. helpers This new model is intended to be a classification of targets whose communications were obtained during leak investigations. A data migration has been added to copy all data from `IncidentPage.targets_whose_communications_were_obtained` into this new model. The target data is not deleted. I've also added the usual "Incident M2Ms" items for managing and merging government workers. --- common/migrations/0065_auto_20200212_1515.py | 23 ++++++++++++++ incident/forms.py | 7 ++++- incident/migrations/0041_governmentworker.py | 30 +++++++++++++++++++ ...py_uncategorized_targets_to_gov_workers.py | 30 +++++++++++++++++++ incident/models/incident_page.py | 8 ++++- incident/models/items.py | 15 ++++++++++ incident/tests/test_incidents_export.py | 1 + incident/tests/test_pages.py | 2 ++ incident/views.py | 6 +++- incident/wagtail_hooks.py | 15 +++++++++- 10 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 common/migrations/0065_auto_20200212_1515.py create mode 100644 incident/migrations/0041_governmentworker.py create mode 100644 incident/migrations/0042_copy_uncategorized_targets_to_gov_workers.py diff --git a/common/migrations/0065_auto_20200212_1515.py b/common/migrations/0065_auto_20200212_1515.py new file mode 100644 index 000000000..697eacb46 --- /dev/null +++ b/common/migrations/0065_auto_20200212_1515.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.9 on 2020-02-12 15:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0064_auto_20200127_1832'), + ] + + operations = [ + migrations.AlterField( + model_name='categoryincidentfilter', + name='incident_filter', + field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('workers_whose_communications_were_obtained', 'Targets whose communications were obtained in leak investigation'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('authors', 'Incident author'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('primary_video', 'Primary video'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_statuses', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('suppress_footer', 'Suppress Footer Call to Action'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), + ), + migrations.AlterField( + model_name='generalincidentfilter', + name='incident_filter', + field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('workers_whose_communications_were_obtained', 'Targets whose communications were obtained in leak investigation'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('authors', 'Incident author'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('primary_video', 'Primary video'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_statuses', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('suppress_footer', 'Suppress Footer Call to Action'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), + ), + ] diff --git a/incident/forms.py b/incident/forms.py index 9f66e60c0..a8636d3b3 100644 --- a/incident/forms.py +++ b/incident/forms.py @@ -1,5 +1,5 @@ from common.forms import BaseMergeForm -from incident.models import Target, Charge, Nationality, Venue, PoliticianOrPublic, Journalist, Institution +from incident.models import Target, Charge, Nationality, Venue, PoliticianOrPublic, Journalist, Institution, GovernmentWorker class TargetMergeForm(BaseMergeForm): @@ -35,3 +35,8 @@ class VenueMergeForm(BaseMergeForm): class PoliticianOrPublicMergeForm(BaseMergeForm): merge_model = PoliticianOrPublic merge_model_type = 'incident.PoliticianOrPublic' + + +class GovernmentWorkerMergeForm(BaseMergeForm): + merge_model = GovernmentWorker + merge_model_type = 'incident.GovernmentWorker' diff --git a/incident/migrations/0041_governmentworker.py b/incident/migrations/0041_governmentworker.py new file mode 100644 index 000000000..b2e907a7d --- /dev/null +++ b/incident/migrations/0041_governmentworker.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.9 on 2020-02-12 15:15 + +from django.db import migrations, models +import modelcluster.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('incident', '0040_merge_20200127_1819'), + ] + + operations = [ + migrations.CreateModel( + name='GovernmentWorker', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, unique=True)), + ], + options={ + 'verbose_name': 'Government employee or contractor', + 'verbose_name_plural': 'Government employees or contractors', + }, + ), + migrations.AddField( + model_name='incidentpage', + name='workers_whose_communications_were_obtained', + field=modelcluster.fields.ParentalManyToManyField(blank=True, related_name='incidents', to='incident.GovernmentWorker', verbose_name='Targets whose communications were obtained in leak investigation'), + ), + ] diff --git a/incident/migrations/0042_copy_uncategorized_targets_to_gov_workers.py b/incident/migrations/0042_copy_uncategorized_targets_to_gov_workers.py new file mode 100644 index 000000000..20e0f9ebc --- /dev/null +++ b/incident/migrations/0042_copy_uncategorized_targets_to_gov_workers.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.9 on 2020-02-11 21:46 + +from django.db import migrations + + +def copy_targets(apps, schema_editor): + """Copy Leak-Case incident Target data into government workers table""" + + IncidentPage = apps.get_model('incident', 'IncidentPage') + GovernmentWorker = apps.get_model('incident', 'GovernmentWorker') + + for incident in IncidentPage.objects.all(): + for target in incident.targets_whose_communications_were_obtained.all(): + worker, _ = GovernmentWorker.objects.get_or_create(title=target.title) + worker.incidents.add(incident) + worker.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('incident', '0041_governmentworker'), + ] + + operations = [ + migrations.RunPython( + copy_targets, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/incident/models/incident_page.py b/incident/models/incident_page.py index 3845668d4..1d55783ab 100644 --- a/incident/models/incident_page.py +++ b/incident/models/incident_page.py @@ -322,6 +322,12 @@ class IncidentPage(MetadataPageMixin, Page): ) # Leak Prosecution + workers_whose_communications_were_obtained = ParentalManyToManyField( + 'incident.GovernmentWorker', + verbose_name='Targets whose communications were obtained in leak investigation', + related_name='incidents', + blank=True, + ) targets_whose_communications_were_obtained = ParentalManyToManyField( 'incident.Target', blank=True, @@ -521,7 +527,7 @@ class IncidentPage(MetadataPageMixin, Page): heading='Leak Prosecution (incl. Legal Case, Arrest/Detention', classname='collapsible collapsed', children=[ - AutocompletePanel('targets_whose_communications_were_obtained', 'incident.Target', is_single=False), + AutocompletePanel('workers_whose_communications_were_obtained', 'incident.GovernmentWorker', is_single=False), FieldPanel('charged_under_espionage_act'), ] ), diff --git a/incident/models/items.py b/incident/models/items.py index 24c29db33..46b638d65 100644 --- a/incident/models/items.py +++ b/incident/models/items.py @@ -58,6 +58,21 @@ def __str__(self): return self.title +class GovernmentWorker(ClusterableModel): + @classmethod + def autocomplete_create(kls, value): + return kls.objects.create(title=value) + + title = models.CharField(max_length=255, unique=True) + + def __str__(self): + return self.title + + class Meta: + verbose_name = 'Government employee or contractor' + verbose_name_plural = 'Government employees or contractors' + + class TargetedJournalist(Orderable): incident = ParentalKey('incident.IncidentPage', on_delete=models.CASCADE, related_name='targeted_journalists') diff --git a/incident/tests/test_incidents_export.py b/incident/tests/test_incidents_export.py index 199f8c261..f4a764d0f 100644 --- a/incident/tests/test_incidents_export.py +++ b/incident/tests/test_incidents_export.py @@ -116,6 +116,7 @@ def test_GET(self): 'venue', 'target_nationality', 'targets_whose_communications_were_obtained', + 'workers_whose_communications_were_obtained', 'politicians_or_public_figures_involved', } self.assertEqual(headers, expected_headers) diff --git a/incident/tests/test_pages.py b/incident/tests/test_pages.py index 34dda7279..229cde9d3 100644 --- a/incident/tests/test_pages.py +++ b/incident/tests/test_pages.py @@ -303,6 +303,7 @@ def test_can_preview_incident_page(self): 'venue': 'null', 'target_nationality': 'null', 'targets_whose_communications_were_obtained': 'null', + 'workers_whose_communications_were_obtained': 'null', 'politicians_or_public_figures_involved': 'null', 'related_incidents': 'null', 'updates': inline_formset([]), @@ -343,6 +344,7 @@ def test_can_create_incident_page(self): 'venue': 'null', 'target_nationality': 'null', 'targets_whose_communications_were_obtained': 'null', + 'workers_whose_communications_were_obtained': 'null', 'politicians_or_public_figures_involved': 'null', 'related_incidents': 'null', 'updates': inline_formset([]), diff --git a/incident/views.py b/incident/views.py index e300a2a08..53f20c6c5 100644 --- a/incident/views.py +++ b/incident/views.py @@ -10,7 +10,7 @@ ) from common.views import MergeView -from incident.forms import TargetMergeForm, ChargeMergeForm, VenueMergeForm, NationalityMergeForm, PoliticianOrPublicMergeForm, JournalistMergeForm, InstitutionMergeForm +from incident.forms import TargetMergeForm, ChargeMergeForm, VenueMergeForm, NationalityMergeForm, PoliticianOrPublicMergeForm, JournalistMergeForm, InstitutionMergeForm, GovernmentWorkerMergeForm from incident.models import IncidentPage, Journalist, TargetedJournalist, Institution @@ -112,3 +112,7 @@ def form_valid(self, form): models_to_merge.delete() return super().form_valid(form) + + +class GovernmentWorkerMergeView(MergeView): + form_class = GovernmentWorkerMergeForm diff --git a/incident/wagtail_hooks.py b/incident/wagtail_hooks.py index 0a36de738..686ebee9d 100644 --- a/incident/wagtail_hooks.py +++ b/incident/wagtail_hooks.py @@ -16,6 +16,7 @@ PoliticianOrPublic, Venue, Institution, + GovernmentWorker, ) from incident.views import ( ChargeMergeView, @@ -26,6 +27,7 @@ VenueMergeView, JournalistMergeView, InstitutionMergeView, + GovernmentWorkerMergeView, ) @@ -56,6 +58,17 @@ class TargetAdmin(MergeAdmin): search_fields = ('title',) +class GovernmentWorkerAdmin(MergeAdmin): + model = GovernmentWorker + merge_view_class = GovernmentWorkerMergeView + menu_label = 'Government Workers' + menu_icon = 'edit' + add_to_settings_menu = False # or True to add your model to the Settings sub-menu + exclude_from_explorer = False # or True to exclude pages of this type from Wagtail's explorer view + list_display = ('title',) + search_fields = ('title',) + + class JournalistAdmin(MergeAdmin): model = Journalist merge_view_class = JournalistMergeView @@ -126,7 +139,7 @@ class IncidentGroup(ModelAdminGroup): menu_label = 'Incident M2Ms' menu_icon = 'folder-open-inverse' # change as required menu_order = 600 # will put in 7th place (000 being 1st, 100 2nd) - items = (TargetAdmin, ChargeAdmin, NationalityAdmin, PoliticianOrPublicAdmin, VenueAdmin, JournalistAdmin, InstitutionAdmin) + items = (TargetAdmin, ChargeAdmin, NationalityAdmin, PoliticianOrPublicAdmin, VenueAdmin, JournalistAdmin, InstitutionAdmin, GovernmentWorkerAdmin) modeladmin_register(IncidentGroup) From 751bb5c4ab934f4c72d14eae7dcd9be99cc571aa Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Wed, 12 Feb 2020 11:58:20 -0500 Subject: [PATCH 30/33] Switch the incident page to display workers over targets In leak cases, we now show the new, more specific classification Government Worker instead of the old Target information. It actually looks like there's a change in behavior here (possibly fixing something unintended) where the link on a target's name now goes to the search page with the filter for other incidents in which that target's comms were obtained. Before, it linked to the filter of all incidents *targeting* this person *generally*, which is not the same thing under our schema. The new behavior seems more correct to me, as the linked-to filter is now the same context as the link. --- incident/templates/incident/_leak_prosecution_rows.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/incident/templates/incident/_leak_prosecution_rows.html b/incident/templates/incident/_leak_prosecution_rows.html index 0aa57d703..b84132e59 100644 --- a/incident/templates/incident/_leak_prosecution_rows.html +++ b/incident/templates/incident/_leak_prosecution_rows.html @@ -1,15 +1,15 @@ {% load wagtailcore_tags %} -{% if page.targets_whose_communications_were_obtained.all %} +{% if page.workers_whose_communications_were_obtained.all %} Targets whose Communications Were Obtained
    - {% for target in page.targets_whose_communications_were_obtained.all %} + {% for target in page.workers_whose_communications_were_obtained.all %}
  • - {{ target.title }} + {{ target.title }}
  • {% endfor %}
From 6693491728037f7ab10cd189ef15334fc3b50cc1 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Wed, 12 Feb 2020 12:10:31 -0500 Subject: [PATCH 31/33] Fix missing space --- incident/templates/incident/incident_page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/incident/templates/incident/incident_page.html b/incident/templates/incident/incident_page.html index d6e789b1e..92a013f97 100644 --- a/incident/templates/incident/incident_page.html +++ b/incident/templates/incident/incident_page.html @@ -68,7 +68,7 @@

Incident Data

Targeted Journalists - {% with page.targeted_journalists.all as journalists%} + {% with page.targeted_journalists.all as journalists %}
    {% for journalist in journalists %}
  • From 125b6445e9e945819e809784b39e67cf72636805 Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Wed, 12 Feb 2020 12:19:14 -0500 Subject: [PATCH 32/33] Add merge migration from master --- common/migrations/0065_merge_20200212_1734.py | 14 ++++++++++++++ ...20200212_1515.py => 0066_auto_20200212_1734.py} | 8 ++++---- 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 common/migrations/0065_merge_20200212_1734.py rename common/migrations/{0065_auto_20200212_1515.py => 0066_auto_20200212_1734.py} (51%) diff --git a/common/migrations/0065_merge_20200212_1734.py b/common/migrations/0065_merge_20200212_1734.py new file mode 100644 index 000000000..763eb77b8 --- /dev/null +++ b/common/migrations/0065_merge_20200212_1734.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.9 on 2020-02-12 17:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0064_auto_20200127_1832'), + ('common', '0063_auto_20200203_1915'), + ] + + operations = [ + ] diff --git a/common/migrations/0065_auto_20200212_1515.py b/common/migrations/0066_auto_20200212_1734.py similarity index 51% rename from common/migrations/0065_auto_20200212_1515.py rename to common/migrations/0066_auto_20200212_1734.py index 697eacb46..d59cfd963 100644 --- a/common/migrations/0065_auto_20200212_1515.py +++ b/common/migrations/0066_auto_20200212_1734.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.9 on 2020-02-12 15:15 +# Generated by Django 2.2.9 on 2020-02-12 17:34 from django.db import migrations, models @@ -6,18 +6,18 @@ class Migration(migrations.Migration): dependencies = [ - ('common', '0064_auto_20200127_1832'), + ('common', '0065_merge_20200212_1734'), ] operations = [ migrations.AlterField( model_name='categoryincidentfilter', name='incident_filter', - field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('workers_whose_communications_were_obtained', 'Targets whose communications were obtained in leak investigation'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('authors', 'Incident author'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('primary_video', 'Primary video'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_statuses', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('suppress_footer', 'Suppress Footer Call to Action'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), + field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('authors', 'Incident author'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('primary_video', 'Primary video'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_statuses', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('suppress_footer', 'Suppress Footer Call to Action'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('workers_whose_communications_were_obtained', 'Targets whose communications were obtained in leak investigation'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), ), migrations.AlterField( model_name='generalincidentfilter', name='incident_filter', - field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('workers_whose_communications_were_obtained', 'Targets whose communications were obtained in leak investigation'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('authors', 'Incident author'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('primary_video', 'Primary video'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_statuses', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('suppress_footer', 'Suppress Footer Call to Action'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), + field=models.CharField(choices=[('actor', 'Actor'), ('affiliation', 'Affiliation'), ('arrest_status', 'Arrest status'), ('assailant', 'Assailant'), ('border_point', 'Border point'), ('charged_under_espionage_act', 'Charged under espionage act?'), ('charges', 'Charges'), ('circuits', 'Circuits'), ('city', 'City'), ('current_charges', 'Current Charges'), ('denial_of_entry', 'Denied entry?'), ('detention_date', 'Detention date between'), ('detention_status', 'Detention status'), ('did_authorities_ask_for_device_access', 'Did authorities ask for device access?'), ('did_authorities_ask_for_social_media_pass', 'Did authorities ask for social media password?'), ('did_authorities_ask_for_social_media_user', 'Did authorities ask for social media username?'), ('did_authorities_ask_about_work', "Did authorities ask intrusive questions about journalist's work?"), ('dropped_charges', 'Dropped Charges'), ('equipment_broken', 'Equipment Broken'), ('equipment_seized', 'Equipment Seized'), ('tags', 'Has any of these tags'), ('held_in_contempt', 'If subject refused to cooperate, were they held in contempt?'), ('authors', 'Incident author'), ('links', 'Incident page links'), ('targets_whose_communications_were_obtained', 'Journalists/Organizations whose communications were obtained in leak investigation'), ('lawsuit_name', 'Lawsuit name'), ('legal_order_type', 'Legal order type'), ('politicians_or_public_figures_involved', 'Politicians or public officials involved'), ('primary_video', 'Primary video'), ('release_date', 'Release date between'), ('is_search_warrant_obtained', 'Search warrant obtained?'), ('pending_cases', 'Show only pending cases'), ('state', 'State'), ('status_of_charges', 'Status of charges'), ('status_of_prior_restraint', 'Status of prior restraint'), ('status_of_seized_equipment', 'Status of seized equipment'), ('stopped_at_border', 'Stopped at border?'), ('stopped_previously', 'Stopped previously?'), ('subpoena_statuses', 'Subpoena status'), ('subpoena_type', 'Subpoena type'), ('suppress_footer', 'Suppress Footer Call to Action'), ('target_nationality', 'Target Nationality'), ('targeted_institutions', 'Targeted Institutions'), ('targeted_journalists', 'Targeted any of these journalists'), ('targets', 'Targets (Journalists/Organizations)'), ('workers_whose_communications_were_obtained', 'Targets whose communications were obtained in leak investigation'), ('third_party_business', 'Third party business'), ('third_party_in_possession_of_communications', 'Third party in possession of communications'), ('date', 'Took place between'), ('target_us_citizenship_status', 'US Citizenship Status'), ('unnecessary_use_of_force', 'Unnecessary use of force?'), ('venue', 'Venue'), ('was_journalist_targeted', 'Was journalist targeted?'), ('were_devices_searched_or_seized', 'Were devices searched or seized?')], max_length=255, unique=True), ), ] From 7b06c4760c2cb595c31db739e6633b046dbfbe3c Mon Sep 17 00:00:00 2001 From: Cameron Higby-Naquin Date: Wed, 12 Feb 2020 13:18:19 -0500 Subject: [PATCH 33/33] Ensure our data migrations are elidable when squashing --- incident/migrations/0033_auto_20190429_1627.py | 1 + .../migrations/0042_copy_uncategorized_targets_to_gov_workers.py | 1 + 2 files changed, 2 insertions(+) diff --git a/incident/migrations/0033_auto_20190429_1627.py b/incident/migrations/0033_auto_20190429_1627.py index ba86128c2..f82bbf332 100644 --- a/incident/migrations/0033_auto_20190429_1627.py +++ b/incident/migrations/0033_auto_20190429_1627.py @@ -44,5 +44,6 @@ class Migration(migrations.Migration): migrations.RunPython( copy_targets, reverse_code=migrations.RunPython.noop, + elidable=True, ), ] diff --git a/incident/migrations/0042_copy_uncategorized_targets_to_gov_workers.py b/incident/migrations/0042_copy_uncategorized_targets_to_gov_workers.py index 20e0f9ebc..4abfd1bf8 100644 --- a/incident/migrations/0042_copy_uncategorized_targets_to_gov_workers.py +++ b/incident/migrations/0042_copy_uncategorized_targets_to_gov_workers.py @@ -26,5 +26,6 @@ class Migration(migrations.Migration): migrations.RunPython( copy_targets, reverse_code=migrations.RunPython.noop, + elidable=True, ), ]