From 59fb9fb860ddcacb0730d3054c0755481799b7d0 Mon Sep 17 00:00:00 2001 From: Ian Ross Date: Fri, 17 Jun 2016 22:22:39 +0200 Subject: [PATCH] Add audit logging using simple-history package --- cadasta/accounts/migrations/0001_initial.py | 30 ++++- cadasta/accounts/models.py | 4 +- cadasta/config/settings/default.py | 2 + cadasta/geography/migrations/0001_initial.py | 2 +- .../organization/migrations/0001_initial.py | 109 +++++++++++++++- cadasta/organization/models.py | 10 +- cadasta/party/migrations/0001_initial.py | 121 +++++++++++++++++- cadasta/party/models.py | 9 ++ .../questionnaires/migrations/0001_initial.py | 99 +++++++++++++- cadasta/questionnaires/models.py | 9 ++ cadasta/resources/migrations/0001_initial.py | 51 +++++++- cadasta/resources/models.py | 5 + cadasta/spatial/migrations/0001_initial.py | 52 +++++++- cadasta/spatial/models.py | 5 + requirements/common.txt | 1 + 15 files changed, 494 insertions(+), 15 deletions(-) diff --git a/cadasta/accounts/migrations/0001_initial.py b/cadasta/accounts/migrations/0001_initial.py index 636f5d239..2617a3ef7 100644 --- a/cadasta/accounts/migrations/0001_initial.py +++ b/cadasta/accounts/migrations/0001_initial.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.4 on 2016-05-04 06:50 +# Generated by Django 1.9.6 on 2016-06-17 19:46 from __future__ import unicode_literals import accounts.manager import accounts.models +from django.conf import settings import django.core.validators from django.db import migrations, models +import django.db.models.deletion import django.utils.timezone @@ -44,4 +46,30 @@ class Migration(migrations.Migration): ('objects', accounts.manager.UserManager()), ], ), + migrations.CreateModel( + name='HistoricalUser', + fields=[ + ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(db_index=True, error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')], verbose_name='username')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('full_name', models.CharField(blank=True, max_length=130, verbose_name='full name')), + ('email_verified', models.BooleanField(default=False)), + ('verify_email_by', models.DateTimeField(default=accounts.models.now_plus_48_hours)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical user', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), ] diff --git a/cadasta/accounts/models.py b/cadasta/accounts/models.py index 9c3be2881..ab0c2414b 100644 --- a/cadasta/accounts/models.py +++ b/cadasta/accounts/models.py @@ -8,7 +8,7 @@ from tutelary.models import Policy from tutelary.decorators import permissioned_model - +from simple_history.models import HistoricalRecords from .manager import UserManager @@ -38,6 +38,8 @@ class User(auth_base.AbstractBaseUser, auth.PermissionsMixin): objects = UserManager() + history = HistoricalRecords() + USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email', 'full_name'] diff --git a/cadasta/config/settings/default.py b/cadasta/config/settings/default.py index ee03e67b5..2940189fa 100644 --- a/cadasta/config/settings/default.py +++ b/cadasta/config/settings/default.py @@ -66,6 +66,7 @@ 'allauth.account', 'allauth.socialaccount', 'sass_processor', + 'simple_history', ) MIDDLEWARE_CLASSES = ( @@ -80,6 +81,7 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', 'audit_log.middleware.UserLoggingMiddleware', + 'simple_history.middleware.HistoryRequestMiddleware' ) REST_FRAMEWORK = { diff --git a/cadasta/geography/migrations/0001_initial.py b/cadasta/geography/migrations/0001_initial.py index 88b2669cd..b15c4f215 100644 --- a/cadasta/geography/migrations/0001_initial.py +++ b/cadasta/geography/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.4 on 2016-05-04 06:50 +# Generated by Django 1.9.6 on 2016-06-17 19:46 from __future__ import unicode_literals import django.contrib.gis.db.models.fields diff --git a/cadasta/organization/migrations/0001_initial.py b/cadasta/organization/migrations/0001_initial.py index d520d1bff..f7007d9e6 100644 --- a/cadasta/organization/migrations/0001_initial.py +++ b/cadasta/organization/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.4 on 2016-05-31 15:43 +# Generated by Django 1.9.6 on 2016-06-17 19:46 from __future__ import unicode_literals import core.models @@ -23,6 +23,88 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='HistoricalOrganization', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('name', models.CharField(max_length=200)), + ('slug', models.SlugField()), + ('description', models.TextField(blank=True, null=True)), + ('archived', models.BooleanField(default=False)), + ('urls', django.contrib.postgres.fields.ArrayField(base_field=models.URLField(), default=[], size=None)), + ('contacts', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=[], null=True, validators=[organization.validators.validate_contact])), + ('logo', models.URLField(null=True)), + ('last_updated', models.DateTimeField(blank=True, editable=False)), + ('access', models.CharField(choices=[('public', 'Public'), ('private', 'Private')], default='public', max_length=8)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical organization', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), + migrations.CreateModel( + name='HistoricalOrganizationRole', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('admin', models.BooleanField(default=False)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical organization role', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), + migrations.CreateModel( + name='HistoricalProject', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField(null=True)), + ('country', django_countries.fields.CountryField(max_length=2, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('archived', models.BooleanField(default=False)), + ('urls', django.contrib.postgres.fields.ArrayField(base_field=models.URLField(), default=[], size=None)), + ('contacts', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=[], null=True, validators=[organization.validators.validate_contact])), + ('last_updated', models.DateTimeField(blank=True, editable=False)), + ('extent', django.contrib.gis.db.models.fields.PolygonField(null=True, srid=4326)), + ('access', models.CharField(choices=[('public', 'Public'), ('private', 'Private')], default='public', max_length=8)), + ('current_questionnaire', models.CharField(blank=True, max_length=24, null=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical project', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), + migrations.CreateModel( + name='HistoricalProjectRole', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('role', models.CharField(choices=[('PU', 'Project User'), ('DC', 'Data Collector'), ('PM', 'Project Manager')], default='PU', max_length=2)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical project role', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), migrations.CreateModel( name='Organization', fields=[ @@ -95,6 +177,31 @@ class Migration(migrations.Migration): name='users', field=models.ManyToManyField(related_name='organizations', through='organization.OrganizationRole', to=settings.AUTH_USER_MODEL), ), + migrations.AddField( + model_name='historicalprojectrole', + name='project', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='organization.Project'), + ), + migrations.AddField( + model_name='historicalprojectrole', + name='user', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='historicalproject', + name='organization', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='organization.Organization'), + ), + migrations.AddField( + model_name='historicalorganizationrole', + name='organization', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='organization.Organization'), + ), + migrations.AddField( + model_name='historicalorganizationrole', + name='user', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL), + ), migrations.AlterUniqueTogether( name='projectrole', unique_together=set([('project', 'user')]), diff --git a/cadasta/organization/models.py b/cadasta/organization/models.py index 44b8d7227..21e18acd2 100644 --- a/cadasta/organization/models.py +++ b/cadasta/organization/models.py @@ -5,7 +5,7 @@ from django.dispatch import receiver from django.utils.translation import ugettext as _ import django.contrib.gis.db.models as gismodels - +from simple_history.models import HistoricalRecords from tutelary.decorators import permissioned_model from tutelary.models import Policy @@ -46,6 +46,8 @@ class Organization(SlugModel, RandomIDModel): default="public", choices=ACCESS_CHOICES, max_length=8 ) + history = HistoricalRecords() + class Meta: ordering = ('name',) @@ -103,6 +105,8 @@ class OrganizationRole(RandomIDModel): user = models.ForeignKey('accounts.User') admin = models.BooleanField(default=False) + history = HistoricalRecords() + def reassign_user_policies(instance, adding): assigned_policies = instance.user.assigned_policies() @@ -165,6 +169,8 @@ class Project(ResourceModelMixin, SlugModel, RandomIDModel): max_length=24, null=True, blank=True ) + history = HistoricalRecords() + class Meta: ordering = ('organization', 'name') @@ -235,6 +241,8 @@ class ProjectRole(RandomIDModel): choices=ROLE_CHOICES, default='PU') + history = HistoricalRecords() + class Meta: unique_together = ('project', 'user') diff --git a/cadasta/party/migrations/0001_initial.py b/cadasta/party/migrations/0001_initial.py index d99436bbc..daa554311 100644 --- a/cadasta/party/migrations/0001_initial.py +++ b/cadasta/party/migrations/0001_initial.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.6 on 2016-06-09 09:02 +# Generated by Django 1.9.6 on 2016-06-17 19:46 from __future__ import unicode_literals import datetime +from django.conf import settings import django.contrib.gis.db.models.fields import django.contrib.postgres.fields.jsonb from django.db import migrations, models @@ -15,11 +16,84 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('organization', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('spatial', '0001_initial'), + ('organization', '0001_initial'), ] operations = [ + migrations.CreateModel( + name='HistoricalParty', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('name', models.CharField(max_length=200)), + ('type', models.CharField(choices=[('IN', 'Individual'), ('CO', 'Corporation'), ('GR', 'Group')], default='IN', max_length=2)), + ('contacts', django.contrib.postgres.fields.jsonb.JSONField(default={}, validators=[organization.validators.validate_contact])), + ('attributes', django.contrib.postgres.fields.jsonb.JSONField(default={})), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('project', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='organization.Project')), + ], + options={ + 'verbose_name': 'historical party', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), + migrations.CreateModel( + name='HistoricalPartyRelationship', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('type', models.CharField(choices=[('S', 'is-spouse-of'), ('C', 'is-child-of'), ('M', 'is-member-of')], max_length=1)), + ('attributes', django.contrib.postgres.fields.jsonb.JSONField(default={})), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical party relationship', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), + migrations.CreateModel( + name='HistoricalTenureRelationship', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('acquired_how', models.CharField(blank=True, choices=[('CS', 'Contractual/Share Crop'), ('CA', 'Customary Arrangement'), ('GF', 'Gift'), ('HS', 'Homestead'), ('IO', 'Informal Occupant'), ('IN', 'Inheritance'), ('LH', 'Leasehold'), ('PF', 'Purchased Freehold'), ('RN', 'Rental'), ('OT', 'Other')], max_length=2, null=True)), + ('acquired_date', models.DateField(default=datetime.date.today)), + ('attributes', django.contrib.postgres.fields.jsonb.JSONField(default={})), + ('geom', django.contrib.gis.db.models.fields.GeometryField(blank=True, null=True, srid=4326)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical tenure relationship', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), + migrations.CreateModel( + name='HistoricalTenureRelationshipType', + fields=[ + ('id', models.CharField(db_index=True, max_length=2)), + ('label', models.CharField(max_length=200)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical tenure relationship type', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), migrations.CreateModel( name='Party', fields=[ @@ -42,7 +116,7 @@ class Migration(migrations.Migration): ('attributes', django.contrib.postgres.fields.jsonb.JSONField(default={})), ('party1', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='party1', to='party.Party')), ('party2', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='party2', to='party.Party')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='organization.Project')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='party_relationships', to='organization.Project')), ], options={ 'abstract': False, @@ -56,9 +130,9 @@ class Migration(migrations.Migration): ('acquired_date', models.DateField(default=datetime.date.today)), ('attributes', django.contrib.postgres.fields.jsonb.JSONField(default={})), ('geom', django.contrib.gis.db.models.fields.GeometryField(blank=True, null=True, srid=4326)), - ('party', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='party', to='party.Party')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='organization.Project')), - ('spatial_unit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='spatial_unit', to='spatial.SpatialUnit')), + ('party', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='party.Party')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tenure_relationships', to='organization.Project')), + ('spatial_unit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='spatial.SpatialUnit')), ], options={ 'abstract': False, @@ -86,4 +160,39 @@ class Migration(migrations.Migration): name='tenure_relationships', field=models.ManyToManyField(related_name='tenure_relationships', through='party.TenureRelationship', to='spatial.SpatialUnit'), ), + migrations.AddField( + model_name='historicaltenurerelationship', + name='party', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='party.Party'), + ), + migrations.AddField( + model_name='historicaltenurerelationship', + name='project', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='organization.Project'), + ), + migrations.AddField( + model_name='historicaltenurerelationship', + name='spatial_unit', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='spatial.SpatialUnit'), + ), + migrations.AddField( + model_name='historicaltenurerelationship', + name='tenure_type', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='party.TenureRelationshipType'), + ), + migrations.AddField( + model_name='historicalpartyrelationship', + name='party1', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='party.Party'), + ), + migrations.AddField( + model_name='historicalpartyrelationship', + name='party2', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='party.Party'), + ), + migrations.AddField( + model_name='historicalpartyrelationship', + name='project', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='organization.Project'), + ), ] diff --git a/cadasta/party/models.py b/cadasta/party/models.py index 843a90b30..9016584df 100644 --- a/cadasta/party/models.py +++ b/cadasta/party/models.py @@ -11,6 +11,7 @@ from organization.validators import validate_contact from spatial.models import SpatialUnit from tutelary.decorators import permissioned_model +from simple_history.models import HistoricalRecords from . import managers, messages @@ -71,6 +72,8 @@ class Party(RandomIDModel): related_name='tenure_relationships' ) + history = HistoricalRecords() + class Meta: ordering = ('name',) @@ -141,6 +144,8 @@ class PartyRelationship(RandomIDModel): objects = managers.PartyRelationshipManager() + history = HistoricalRecords() + class TenureRelationship(RandomIDModel): """TenureRelationship model. @@ -191,9 +196,13 @@ class TenureRelationship(RandomIDModel): objects = managers.TenureRelationshipManager() + history = HistoricalRecords() + class TenureRelationshipType(models.Model): """Defines allowable tenure types.""" id = models.CharField(max_length=2, primary_key=True) label = models.CharField(max_length=200) + + history = HistoricalRecords() diff --git a/cadasta/questionnaires/migrations/0001_initial.py b/cadasta/questionnaires/migrations/0001_initial.py index a18549408..3ffacffc0 100644 --- a/cadasta/questionnaires/migrations/0001_initial.py +++ b/cadasta/questionnaires/migrations/0001_initial.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.4 on 2016-05-31 15:43 +# Generated by Django 1.9.6 on 2016-06-17 19:46 from __future__ import unicode_literals import buckets.fields +from django.conf import settings from django.db import migrations, models import django.db.models.deletion @@ -12,10 +13,86 @@ class Migration(migrations.Migration): initial = True dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('organization', '0001_initial'), ] operations = [ + migrations.CreateModel( + name='HistoricalQuestion', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('name', models.CharField(max_length=100)), + ('label', models.CharField(blank=True, max_length=500, null=True)), + ('type', models.CharField(choices=[('IN', 'integer'), ('DE', 'decimal'), ('TX', 'text'), ('S1', 'select one'), ('SM', 'select all that apply'), ('NO', 'note'), ('GP', 'geopoint'), ('GT', 'geotrace'), ('GS', 'geoshape'), ('DA', 'date'), ('TI', 'time'), ('DT', 'dateTime'), ('CA', 'calculate'), ('AC', 'acknowledge'), ('PH', 'photo'), ('AU', 'audio'), ('VI', 'video'), ('BC', 'barcode'), ('ST', 'start'), ('EN', 'end'), ('TD', 'today'), ('DI', 'deviceid'), ('SI', 'subsciberid'), ('SS', 'simserial'), ('PN', 'phonenumber')], max_length=2)), + ('required', models.BooleanField(default=False)), + ('constraint', models.CharField(blank=True, max_length=50, null=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical question', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), + migrations.CreateModel( + name='HistoricalQuestionGroup', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('name', models.CharField(max_length=100)), + ('label', models.CharField(blank=True, max_length=500, null=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical question group', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), + migrations.CreateModel( + name='HistoricalQuestionnaire', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('name', models.CharField(max_length=100)), + ('title', models.CharField(max_length=500)), + ('id_string', models.CharField(max_length=50)), + ('xls_form', buckets.fields.S3FileField(upload_to='xls-forms')), + ('version', models.IntegerField(default=1)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('project', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='organization.Project')), + ], + options={ + 'verbose_name': 'historical questionnaire', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), + migrations.CreateModel( + name='HistoricalQuestionOption', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('name', models.CharField(max_length=100)), + ('label', models.CharField(max_length=200)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical question option', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), migrations.CreateModel( name='Question', fields=[ @@ -83,4 +160,24 @@ class Migration(migrations.Migration): name='questionnaire', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='questionnaires.Questionnaire'), ), + migrations.AddField( + model_name='historicalquestionoption', + name='question', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='questionnaires.Question'), + ), + migrations.AddField( + model_name='historicalquestiongroup', + name='questionnaire', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='questionnaires.Questionnaire'), + ), + migrations.AddField( + model_name='historicalquestion', + name='question_group', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='questionnaires.QuestionGroup'), + ), + migrations.AddField( + model_name='historicalquestion', + name='questionnaire', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='questionnaires.Questionnaire'), + ), ] diff --git a/cadasta/questionnaires/models.py b/cadasta/questionnaires/models.py index 9ab0b480c..11f3b7aaa 100644 --- a/cadasta/questionnaires/models.py +++ b/cadasta/questionnaires/models.py @@ -2,6 +2,7 @@ from django.utils.translation import ugettext as _ from tutelary.decorators import permissioned_model +from simple_history.models import HistoricalRecords from buckets.fields import S3FileField from core.models import RandomIDModel @@ -20,6 +21,8 @@ class Questionnaire(RandomIDModel): objects = managers.QuestionnaireManager() + history = HistoricalRecords() + class TutelaryMeta: perm_type = 'questionnaire' path_fields = ('project', 'pk') @@ -47,6 +50,8 @@ class QuestionGroup(RandomIDModel): objects = managers.QuestionGroupManager() + history = HistoricalRecords() + class Question(RandomIDModel): TYPE_CHOICES = (('IN', 'integer'), @@ -89,6 +94,8 @@ class Question(RandomIDModel): objects = managers.QuestionManager() + history = HistoricalRecords() + @property def has_options(self): return self.type in ['S1', 'SM'] @@ -98,3 +105,5 @@ class QuestionOption(RandomIDModel): name = models.CharField(max_length=100) label = models.CharField(max_length=200) question = models.ForeignKey(Question, related_name='options') + + history = HistoricalRecords() diff --git a/cadasta/resources/migrations/0001_initial.py b/cadasta/resources/migrations/0001_initial.py index 4d39b1c45..219bc9a34 100644 --- a/cadasta/resources/migrations/0001_initial.py +++ b/cadasta/resources/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.6 on 2016-06-03 10:23 +# Generated by Django 1.9.6 on 2016-06-17 19:46 from __future__ import unicode_literals import buckets.fields @@ -15,9 +15,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('organization', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('contenttypes', '0002_remove_content_type_name'), + ('organization', '0001_initial'), ] operations = [ @@ -32,6 +32,48 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.CreateModel( + name='HistoricalContentObject', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('object_id', models.CharField(blank=True, max_length=24, null=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('content_type', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='contenttypes.ContentType')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical content object', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), + migrations.CreateModel( + name='HistoricalResource', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True, null=True)), + ('file', buckets.fields.S3FileField(upload_to='resources', validators=[resources.validators.validate_file_type])), + ('original_file', models.CharField(max_length=200)), + ('file_versions', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), + ('mime_type', models.CharField(max_length=50)), + ('archived', models.BooleanField(default=False)), + ('last_updated', models.DateTimeField(blank=True, editable=False)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('contributor', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('project', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='organization.Project')), + ], + options={ + 'verbose_name': 'historical resource', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), migrations.CreateModel( name='Resource', fields=[ @@ -51,6 +93,11 @@ class Migration(migrations.Migration): 'abstract': False, }, ), + migrations.AddField( + model_name='historicalcontentobject', + name='resource', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='resources.Resource'), + ), migrations.AddField( model_name='contentobject', name='resource', diff --git a/cadasta/resources/models.py b/cadasta/resources/models.py index c7a2cb14b..d723444e2 100644 --- a/cadasta/resources/models.py +++ b/cadasta/resources/models.py @@ -8,6 +8,7 @@ from django.conf import settings from django.utils.translation import ugettext as _ from django.contrib.postgres.fields import JSONField +from simple_history.models import HistoricalRecords from tutelary.decorators import permissioned_model from buckets.fields import S3FileField @@ -36,6 +37,8 @@ class Resource(RandomIDModel): objects = ResourceManager() + history = HistoricalRecords() + class TutelaryMeta: perm_type = 'resource' path_fields = ('project', 'pk') @@ -135,3 +138,5 @@ class ContentObject(RandomIDModel): null=True, blank=True) content_object = GenericForeignKey('content_type', 'object_id') + + history = HistoricalRecords() diff --git a/cadasta/spatial/migrations/0001_initial.py b/cadasta/spatial/migrations/0001_initial.py index 522790ce2..5cd43cc5d 100644 --- a/cadasta/spatial/migrations/0001_initial.py +++ b/cadasta/spatial/migrations/0001_initial.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.4 on 2016-06-02 10:45 +# Generated by Django 1.9.6 on 2016-06-17 19:46 from __future__ import unicode_literals +from django.conf import settings import django.contrib.gis.db.models.fields import django.contrib.postgres.fields.jsonb from django.db import migrations, models @@ -13,10 +14,49 @@ class Migration(migrations.Migration): initial = True dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('organization', '0001_initial'), ] operations = [ + migrations.CreateModel( + name='HistoricalSpatialUnit', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('name', models.CharField(max_length=200)), + ('geometry', django.contrib.gis.db.models.fields.GeometryField(null=True, srid=4326)), + ('type', models.CharField(choices=[('PA', 'Parcel'), ('CB', 'Community boundary'), ('BU', 'Building'), ('AP', 'Apartment'), ('PX', 'Project extent'), ('RW', 'Right-of-way'), ('UC', 'Utility corridor'), ('NP', 'National park boundary'), ('MI', 'Miscellaneous')], default='PA', max_length=2)), + ('attributes', django.contrib.postgres.fields.jsonb.JSONField(default={})), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('project', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='organization.Project')), + ], + options={ + 'verbose_name': 'historical spatial unit', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), + migrations.CreateModel( + name='HistoricalSpatialUnitRelationship', + fields=[ + ('id', models.CharField(db_index=True, max_length=24)), + ('type', models.CharField(choices=[('C', 'is-contained-in'), ('S', 'is-split-of'), ('M', 'is-merge-of')], max_length=1)), + ('attributes', django.contrib.postgres.fields.jsonb.JSONField(default={})), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('project', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='organization.Project')), + ], + options={ + 'verbose_name': 'historical spatial unit relationship', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + ), migrations.CreateModel( name='SpatialUnit', fields=[ @@ -50,4 +90,14 @@ class Migration(migrations.Migration): name='relationships', field=models.ManyToManyField(related_name='relationships_set', through='spatial.SpatialUnitRelationship', to='spatial.SpatialUnit'), ), + migrations.AddField( + model_name='historicalspatialunitrelationship', + name='su1', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='spatial.SpatialUnit'), + ), + migrations.AddField( + model_name='historicalspatialunitrelationship', + name='su2', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='spatial.SpatialUnit'), + ), ] diff --git a/cadasta/spatial/models.py b/cadasta/spatial/models.py index 45247afac..804c8cb01 100644 --- a/cadasta/spatial/models.py +++ b/cadasta/spatial/models.py @@ -6,6 +6,7 @@ from organization.models import Project from party import managers from tutelary.decorators import permissioned_model +from simple_history.models import HistoricalRecords from . import messages from .choices import TYPE_CHOICES @@ -76,6 +77,8 @@ class TutelaryMeta: related_name='relationships_set', ) + history = HistoricalRecords() + def __str__(self): return "".format(name=self.name) @@ -144,3 +147,5 @@ class SpatialUnitRelationship(RandomIDModel): # JSON attributes field with management of allowed members. attributes = JSONField(default={}) objects = SpatialUnitRelationshipManager() + + history = HistoricalRecords() diff --git a/requirements/common.txt b/requirements/common.txt index fcf62d776..80e5d0b7f 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -16,6 +16,7 @@ rfc3987==1.3.5 drfdocs==0.0.9 django-tutelary==0.1.13 django-audit-log==0.7.0 +django-simple-history==1.8.1 simplejson==3.8.1 django-widget-tweaks==1.4.1 django-buckets==0.1.3