Skip to content

Commit

Permalink
add m2m fields and restore migrations
Browse files Browse the repository at this point in the history
  • Loading branch information
riceyrice committed Oct 18, 2023
1 parent d3c93bf commit 9badc0c
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 14 deletions.
2 changes: 1 addition & 1 deletion api/audit/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
USER_UPDATED_MESSAGE = "User %s updated: %s"
USER_DELETED_MESSAGE = "User deleted: %s"
GROUP_CREATED_MESSAGE = "New Permission Group created: %s"
GROUP_UPDATED_MESSAGE = "Permission Group updated: %s"
GROUP_UPDATED_MESSAGE = "Permission Group %s updated: %s"
GROUP_DELETED_MESSAGE = "Permission Group deleted: %s"
FEATURE_STATE_SCHEDULED_MESSAGE = (
"Flag state / Remote Config value update scheduled for %s for feature: %s"
Expand Down
44 changes: 44 additions & 0 deletions api/projects/migrations/0020_historicalproject.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 3.2.20 on 2023-10-18 15:58

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import simple_history.models
import uuid


class Migration(migrations.Migration):

dependencies = [
('organisations', '0046_allow_allowed_projects_to_be_null'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('api_keys', '0003_masterapikey_is_admin'),
('projects', '0019_add_limits'),
]

operations = [
migrations.CreateModel(
name='HistoricalProject',
fields=[
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('deleted_at', models.DateTimeField(blank=True, db_index=True, default=None, editable=False, null=True)),
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False)),
('name', models.CharField(max_length=2000)),
('created_date', models.DateTimeField(blank=True, editable=False, verbose_name='DateCreated')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('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)),
('master_api_key', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='api_keys.masterapikey')),
('organisation', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='organisations.organisation')),
],
options={
'verbose_name': 'historical project',
'verbose_name_plural': 'historical projects',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
]
6 changes: 3 additions & 3 deletions api/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
environment_cache = caches[settings.ENVIRONMENT_CACHE_NAME]


UNAUDITED_FIELDS = [
UNAUDITED_PROJECT_FIELDS = (
"hide_disabled_flags",
"enable_dynamo_db",
"prevent_flag_defaults",
Expand All @@ -47,13 +47,13 @@
"max_segments_allowed",
"max_features_allowed",
"max_segment_overrides_allowed",
]
)


class Project(
LifecycleModelMixin,
abstract_base_auditable_model_factory(UNAUDITED_FIELDS),
SoftDeleteExportableModel,
abstract_base_auditable_model_factory(UNAUDITED_PROJECT_FIELDS),
):
history_record_class_path = "projects.models.HistoricalProject"
related_object_type = RelatedObjectType.PROJECT
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Generated by Django 3.2.20 on 2023-10-18 16:56

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import simple_history.models


class Migration(migrations.Migration):

dependencies = [
('organisations', '0046_allow_allowed_projects_to_be_null'),
('api_keys', '0003_masterapikey_is_admin'),
('users', '0034_add_user_permission_group_membership_through_model'),
]

operations = [
migrations.CreateModel(
name='HistoricalFFAdminUser',
fields=[
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('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')),
('email', models.EmailField(db_index=True, max_length=254)),
('username', models.CharField(blank=True, db_index=True, max_length=150, null=True)),
('first_name', models.CharField(max_length=30, verbose_name='first name')),
('last_name', models.CharField(max_length=150, verbose_name='last name')),
('google_user_id', models.CharField(blank=True, max_length=50, null=True)),
('github_user_id', models.CharField(blank=True, max_length=50, null=True)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('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)),
('master_api_key', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='api_keys.masterapikey')),
],
options={
'verbose_name': 'historical Feature flag admin user',
'verbose_name_plural': 'historical Feature flag admin users',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalUserPermissionGroup',
fields=[
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('is_default', models.BooleanField(default=False, help_text='If set to true, all new users will be added to this group')),
('external_id', models.CharField(blank=True, help_text='Unique ID of the group in an external system', max_length=255, null=True)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('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)),
('master_api_key', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='api_keys.masterapikey')),
('organisation', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='organisations.organisation')),
],
options={
'verbose_name': 'historical user permission group',
'verbose_name_plural': 'historical user permission groups',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalUserPermissionGroupMembership',
fields=[
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('group_admin', models.BooleanField(default=False)),
('m2m_history_id', models.AutoField(primary_key=True, serialize=False)),
('ffadminuser', 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', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='users.historicaluserpermissiongroup')),
('userpermissiongroup', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='users.userpermissiongroup')),
],
options={
'verbose_name': 'HistoricalUserPermissionGroupMembership',
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalUserOrganisation',
fields=[
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('date_joined', models.DateTimeField(blank=True, editable=False)),
('role', models.CharField(choices=[('ADMIN', 'Admin'), ('USER', 'User')], max_length=50)),
('m2m_history_id', models.AutoField(primary_key=True, serialize=False)),
('history', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.DO_NOTHING, to='users.historicalffadminuser')),
('organisation', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='organisations.organisation')),
('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'HistoricalUserOrganisation',
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
]
42 changes: 32 additions & 10 deletions api/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
from django_lifecycle import AFTER_CREATE, LifecycleModel, hook

from audit.constants import (
GROUP_CREATED_MESSAGE,
GROUP_DELETED_MESSAGE,
GROUP_UPDATED_MESSAGE,
USER_CREATED_MESSAGE,
USER_DELETED_MESSAGE,
USER_UPDATED_MESSAGE,
GROUP_CREATED_MESSAGE,
GROUP_DELETED_MESSAGE,
)
from audit.models import RelatedObjectType
from environments.models import Environment
Expand Down Expand Up @@ -100,17 +101,23 @@ def get_by_natural_key(self, email):
return self.get(email__iexact=email)


UNAUDITED_USER_FIELDS = [
"date_joined",
# NOTE date_joined cannot be excluded because there is also a date_joined field on
# the organisations through model, and currently simple history would create a
# historical through model without the field but try to store the value anyway
UNAUDITED_USER_FIELDS = (
# "date_joined",
"marketing_consent_given",
"sign_up_type",
"last_login",
]
)
AUDITED_USER_M2M_FIELDS = ("organisations",)


class FFAdminUser(
LifecycleModel,
abstract_base_auditable_model_factory(UNAUDITED_USER_FIELDS),
abstract_base_auditable_model_factory(
UNAUDITED_USER_FIELDS, AUDITED_USER_M2M_FIELDS
),
AbstractUser,
):
history_record_class_path = "users.models.HistoricalFFAdminUser"
Expand Down Expand Up @@ -365,7 +372,7 @@ def get_delete_log_message(self, history_instance) -> str | None:
return USER_DELETED_MESSAGE % self

def _get_organisations(self) -> typing.Iterable[Organisation]:
# TODO IS THIS CAUSING A DEADLOCK?
# TODO #2797 IS THIS CAUSING A DEADLOCK?
return self.organisations.all()


Expand All @@ -387,7 +394,12 @@ class Meta:
db_table = "users_userpermissiongroup_users"


class UserPermissionGroup(abstract_base_auditable_model_factory(), models.Model):
AUDITED_GROUP_M2M_FIELDS = ("users",)


class UserPermissionGroup(
abstract_base_auditable_model_factory(audited_m2m_fields=AUDITED_GROUP_M2M_FIELDS)
):
"""
Model to group users within an organisation for the purposes of permissioning.
"""
Expand All @@ -401,7 +413,7 @@ class UserPermissionGroup(abstract_base_auditable_model_factory(), models.Model)
blank=True,
related_name="permission_groups",
through=UserPermissionGroupMembership,
through_fields=["userpermissiongroup", "ffadminuser"],
through_fields=("userpermissiongroup", "ffadminuser"),
)
organisation = models.ForeignKey(
Organisation, on_delete=models.CASCADE, related_name="permission_groups"
Expand All @@ -410,7 +422,6 @@ class UserPermissionGroup(abstract_base_auditable_model_factory(), models.Model)
default=False,
help_text="If set to true, all new users will be added to this group",
)

external_id = models.CharField(
blank=True,
null=True,
Expand Down Expand Up @@ -439,6 +450,17 @@ def remove_users_by_id(self, user_ids: list):
def get_create_log_message(self, history_instance) -> str | None:
return GROUP_CREATED_MESSAGE % self.name

def get_update_log_message(self, history_instance) -> str | None:
changed_fields = history_instance.diff_against(
history_instance.prev_record
).changed_fields
return (
"; ".join(
GROUP_UPDATED_MESSAGE % (field, self.name) for field in changed_fields
)
or None
)

def get_delete_log_message(self, history_instance) -> str | None:
return GROUP_DELETED_MESSAGE % self.name

Expand Down

0 comments on commit 9badc0c

Please sign in to comment.