From b98804e5144fb825f64a129e725cb547fe4fcf8b Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Wed, 28 Aug 2024 15:10:13 -0400 Subject: [PATCH] membershipworks: Use django-simple-history for Member, Flag, and MemberFlag --- cmsmanage/settings.py | 1 + membershipworks/admin.py | 7 +- .../0002_historical_member_and_flags.py | 453 ++++++++++++++++++ membershipworks/models.py | 12 +- pdm.lock | 17 +- pyproject.toml | 1 + 6 files changed, 485 insertions(+), 6 deletions(-) create mode 100644 membershipworks/migrations/0002_historical_member_and_flags.py diff --git a/cmsmanage/settings.py b/cmsmanage/settings.py index 569a94c..0d02ab8 100644 --- a/cmsmanage/settings.py +++ b/cmsmanage/settings.py @@ -54,6 +54,7 @@ class Base(Configuration): "django_db_views", "django_sendfile", "django_bootstrap5", + "simple_history", # "tasks.apps.TasksConfig", "rentals.apps.RentalsConfig", "membershipworks.apps.MembershipworksConfig", diff --git a/membershipworks/admin.py b/membershipworks/admin.py index e1db0f3..8a25c10 100644 --- a/membershipworks/admin.py +++ b/membershipworks/admin.py @@ -10,6 +10,7 @@ from django_object_actions import ( ) from django_q.models import Task from django_q.tasks import async_task +from simple_history.admin import SimpleHistoryAdmin from .models import ( Event, @@ -28,7 +29,7 @@ from .tasks.scrape import ( from .tasks.ucsAccounts import sync_accounts -class ReadOnlyAdmin(admin.ModelAdmin): +class ReadOnlyAdminMixin: def has_add_permission(self, request, obj=None): return False @@ -39,7 +40,9 @@ class ReadOnlyAdmin(admin.ModelAdmin): return False -class BaseMembershipWorksAdmin(DjangoObjectActions, ReadOnlyAdmin): +class BaseMembershipWorksAdmin( + DjangoObjectActions, ReadOnlyAdminMixin, SimpleHistoryAdmin +): changelist_actions = ("refresh_membershipworks_data", "sync_ucs_accounts") # internal method from DjangoObjectActions diff --git a/membershipworks/migrations/0002_historical_member_and_flags.py b/membershipworks/migrations/0002_historical_member_and_flags.py new file mode 100644 index 0000000..84fcf37 --- /dev/null +++ b/membershipworks/migrations/0002_historical_member_and_flags.py @@ -0,0 +1,453 @@ +# Generated by Django 5.1 on 2024-08-28 19:20 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + +import simple_history.models + + +class Migration(migrations.Migration): + dependencies = [ + ("membershipworks", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="HistoricalFlag", + fields=[ + ("id", models.CharField(db_index=True, max_length=24)), + ("name", models.TextField(blank=True, null=True)), + ("type", models.CharField(max_length=6)), + ("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, + ), + ), + ], + options={ + "verbose_name": "historical flag", + "verbose_name_plural": "historical flags", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name="HistoricalMember", + fields=[ + ("uid", models.CharField(db_index=True, max_length=24)), + ( + "year_of_birth", + models.TextField(blank=True, db_column="Year of Birth", null=True), + ), + ( + "account_name", + models.TextField(blank=True, db_column="Account Name", null=True), + ), + ( + "first_name", + models.TextField(blank=True, db_column="First Name", null=True), + ), + ( + "last_name", + models.TextField(blank=True, db_column="Last Name", null=True), + ), + ("phone", models.TextField(blank=True, db_column="Phone", null=True)), + ("email", models.TextField(blank=True, db_column="Email", null=True)), + ( + "volunteer_email", + models.TextField( + blank=True, db_column="Volunteer Email", null=True + ), + ), + ( + "address_street", + models.TextField( + blank=True, db_column="Address (Street)", null=True + ), + ), + ( + "address_city", + models.TextField(blank=True, db_column="Address (City)", null=True), + ), + ( + "address_state_province", + models.TextField( + blank=True, db_column="Address (State/Province)", null=True + ), + ), + ( + "address_postal_code", + models.TextField( + blank=True, db_column="Address (Postal Code)", null=True + ), + ), + ( + "address_country", + models.TextField( + blank=True, db_column="Address (Country)", null=True + ), + ), + ( + "profile_description", + models.TextField( + blank=True, db_column="Profile description", null=True + ), + ), + ( + "website", + models.TextField(blank=True, db_column="Website", null=True), + ), + ("fax", models.TextField(blank=True, db_column="Fax", null=True)), + ( + "contact_person", + models.TextField(blank=True, db_column="Contact Person", null=True), + ), + ( + "password", + models.TextField(blank=True, db_column="Password", null=True), + ), + ( + "position_relation", + models.TextField( + blank=True, db_column="Position/relation", null=True + ), + ), + ( + "parent_account_id", + models.TextField( + blank=True, db_column="Parent Account ID", null=True + ), + ), + ( + "closet_storage", + models.TextField( + blank=True, db_column="Closet Storage #", null=True + ), + ), + ( + "storage_shelf", + models.TextField( + blank=True, db_column="Storage Shelf #", null=True + ), + ), + ( + "personal_studio_space", + models.TextField( + blank=True, db_column="Personal Studio Space #", null=True + ), + ), + ( + "access_permitted_shops_during_extended_hours", + models.BooleanField( + db_column="Access Permitted Shops During Extended Hours?" + ), + ), + ( + "access_front_door_and_studio_space_during_extended_hours", + models.BooleanField( + db_column="Access Front Door and Studio Space During Extended Hours?" + ), + ), + ( + "access_wood_shop", + models.BooleanField(db_column="Access Wood Shop?"), + ), + ( + "access_metal_shop", + models.BooleanField(db_column="Access Metal Shop?"), + ), + ( + "access_storage_closet", + models.BooleanField(db_column="Access Storage Closet?"), + ), + ( + "access_studio_space", + models.BooleanField(db_column="Access Studio Space?"), + ), + ( + "access_front_door", + models.BooleanField(db_column="Access Front Door?"), + ), + ( + "access_card_number", + models.TextField( + blank=True, db_column="Access Card Number", null=True + ), + ), + ( + "access_card_facility_code", + models.TextField( + blank=True, db_column="Access Card Facility Code", null=True + ), + ), + ( + "auto_billing_id", + models.TextField( + blank=True, db_column="Auto Billing ID", null=True + ), + ), + ( + "billing_method", + models.TextField(blank=True, db_column="Billing Method", null=True), + ), + ( + "renewal_date", + models.DateField(blank=True, db_column="Renewal Date", null=True), + ), + ( + "join_date", + models.DateField(blank=True, db_column="Join Date", null=True), + ), + ( + "admin_note", + models.TextField(blank=True, db_column="Admin note", null=True), + ), + ( + "profile_gallery_image_url", + models.TextField( + blank=True, db_column="Profile gallery image URL", null=True + ), + ), + ( + "business_card_image_url", + models.TextField( + blank=True, db_column="Business card image URL", null=True + ), + ), + ( + "instagram", + models.TextField(blank=True, db_column="Instagram", null=True), + ), + ( + "pinterest", + models.TextField(blank=True, db_column="Pinterest", null=True), + ), + ( + "youtube", + models.TextField(blank=True, db_column="Youtube", null=True), + ), + ("yelp", models.TextField(blank=True, db_column="Yelp", null=True)), + ( + "google", + models.TextField(blank=True, db_column="Google+", null=True), + ), + ("bbb", models.TextField(blank=True, db_column="BBB", null=True)), + ( + "twitter", + models.TextField(blank=True, db_column="Twitter", null=True), + ), + ( + "facebook", + models.TextField(blank=True, db_column="Facebook", null=True), + ), + ( + "linked_in", + models.TextField(blank=True, db_column="LinkedIn", null=True), + ), + ( + "do_not_show_street_address_in_profile", + models.TextField( + blank=True, + db_column="Do not show street address in profile", + null=True, + ), + ), + ( + "do_not_list_in_directory", + models.TextField( + blank=True, db_column="Do not list in directory", null=True + ), + ), + ( + "how_did_you_hear", + models.TextField(blank=True, db_column="HowDidYouHear", null=True), + ), + ( + "authorize_charge", + models.TextField( + blank=True, db_column="authorizeCharge", null=True + ), + ), + ( + "policy_agreement", + models.TextField( + blank=True, db_column="policyAgreement", null=True + ), + ), + ( + "waiver_form_signed_and_on_file_date", + models.DateField( + blank=True, + db_column="Waiver form signed and on file date.", + null=True, + ), + ), + ( + "membership_agreement_signed_and_on_file_date", + models.DateField( + blank=True, + db_column="Membership Agreement signed and on file date.", + null=True, + ), + ), + ( + "ip_address", + models.TextField(blank=True, db_column="IP Address", null=True), + ), + ( + "audit_date", + models.DateField(blank=True, db_column="Audit Date", null=True), + ), + ( + "agreement_version", + models.TextField( + blank=True, db_column="Agreement Version", null=True + ), + ), + ( + "paperwork_status", + models.TextField( + blank=True, db_column="Paperwork status", null=True + ), + ), + ( + "membership_agreement_dated", + models.BooleanField(db_column="Membership agreement dated"), + ), + ( + "membership_agreement_acknowledgement_page_filled_out", + models.BooleanField( + db_column="Membership Agreement Acknowledgement Page Filled Out" + ), + ), + ( + "membership_agreement_signed", + models.BooleanField(db_column="Membership Agreement Signed"), + ), + ( + "liability_form_filled_out", + models.BooleanField(db_column="Liability Form Filled Out"), + ), + ("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, + ), + ), + ], + options={ + "verbose_name": "historical member", + "verbose_name_plural": "historical members", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name="HistoricalMemberFlag", + fields=[ + ( + "id", + models.BigIntegerField( + auto_created=True, blank=True, db_index=True, verbose_name="ID" + ), + ), + ("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, + ), + ), + ( + "flag", + simple_history.models.HistoricForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="membershipworks.flag", + ), + ), + ( + "history_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "member", + simple_history.models.HistoricForeignKey( + blank=True, + db_column="uid", + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="membershipworks.member", + ), + ), + ], + options={ + "verbose_name": "historical member flag", + "verbose_name_plural": "historical member flags", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.AlterField( + model_name="memberflag", + name="flag", + field=simple_history.models.HistoricForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="membershipworks.flag" + ), + ), + migrations.AlterField( + model_name="memberflag", + name="member", + field=simple_history.models.HistoricForeignKey( + db_column="uid", + db_constraint=False, + on_delete=django.db.models.deletion.PROTECT, + to="membershipworks.member", + ), + ), + ] diff --git a/membershipworks/models.py b/membershipworks/models.py index 91a0526..e6a13cf 100644 --- a/membershipworks/models.py +++ b/membershipworks/models.py @@ -28,6 +28,7 @@ from django.utils import timezone import nh3 from django_db_views.db_view import DBView from django_stubs_ext import WithAnnotations +from simple_history.models import HistoricalRecords, HistoricForeignKey from reservations.models import Reservation @@ -89,6 +90,8 @@ class Flag(BaseModel): name = models.TextField(null=True, blank=True) type = models.CharField(max_length=6) + history = HistoricalRecords() + class Meta: db_table = "flag" ordering = ("name",) @@ -121,7 +124,6 @@ class MemberQuerySet(models.QuerySet): ) -# TODO: is this still a temporal table? class Member(BaseModel): uid = models.CharField(max_length=24, primary_key=True) year_of_birth = models.TextField(db_column="Year of Birth", null=True, blank=True) @@ -248,6 +250,8 @@ class Member(BaseModel): ) flags = models.ManyToManyField(Flag, through="MemberFlag", related_name="members") + history = HistoricalRecords() + _api_names_override = { "uid": "Account ID", "how_did_you_hear": "Please tell us how you heard about the Claremont MakerSpace and what tools or shops you are most excited to start using:", @@ -300,10 +304,12 @@ class Member(BaseModel): class MemberFlag(BaseModel): - member = models.ForeignKey( + member = HistoricForeignKey( Member, on_delete=models.PROTECT, db_column="uid", db_constraint=False ) - flag = models.ForeignKey(Flag, on_delete=models.PROTECT) + flag = HistoricForeignKey(Flag, on_delete=models.PROTECT) + + history = HistoricalRecords() class Meta: db_table = "memberflag" diff --git a/pdm.lock b/pdm.lock index 3d6611c..e2be7a8 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "debug", "dev", "lint", "server", "typing"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:0c560e1f2a81810e95ba8993538337bfc8b39576b6923280cfbfad91ad5b58f2" +content_hash = "sha256:f908c755f35f0769821be54914ad53a3de6e9a0c654baf877045c161c7dacfc7" [[metadata.targets]] requires_python = "==3.11.*" @@ -650,6 +650,21 @@ files = [ {file = "django_sendfile2-0.7.1-py3-none-any.whl", hash = "sha256:81971df1db77f688c4834b8540930902d0d929c64cf374bd604b81f5ac52c04a"}, ] +[[package]] +name = "django-simple-history" +version = "3.7.0" +requires_python = ">=3.8" +summary = "Store model history and view/revert changes from admin site." +groups = ["default"] +marker = "python_version == \"3.11\"" +dependencies = [ + "django>=4.2", +] +files = [ + {file = "django_simple_history-3.7.0-py3-none-any.whl", hash = "sha256:282cb2c4aa63f51547f17da7f2130abaa81ba01694676d19b88d52c94a57a52c"}, + {file = "django_simple_history-3.7.0.tar.gz", hash = "sha256:ac3b7ca8b0d33f7ea6be8fe7fc98cf43415efa500ff5dfe736fbd1ebc0cf39f9"}, +] + [[package]] name = "django-stubs" version = "5.0.2" diff --git a/pyproject.toml b/pyproject.toml index 52b8918..7787385 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ dependencies = [ "google-auth-oauthlib~=1.2", "django-model-utils~=4.5", "psycopg[binary,pool]~=3.2", + "django-simple-history~=3.7", ] requires-python = ">=3.11"