Add history + more components

This commit is contained in:
Loïc Gremaud 2024-10-03 22:29:32 +02:00
parent 8f9c83dc2a
commit d7f0d2a7f2
Signed by: Legrems
GPG Key ID: D4620E6DF3E0121D
18 changed files with 352 additions and 30 deletions

View File

@ -157,10 +157,8 @@ USE_I18N = True
USE_TZ = True USE_TZ = True
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" SIMPLE_HISTORY_HISTORY_ID_USE_UUID = True
from app.settingsLocal import * from app.settingsLocal import *

View File

@ -1,6 +1,9 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.postgres.expressions import ArraySubquery
from django.db import models from django.db import models
from django.db.models import OuterRef
from django.db.models.fields.related import RelatedField from django.db.models.fields.related import RelatedField
from django.db.models.functions import JSONObject
from app.utils.helpers import recursive_getattr from app.utils.helpers import recursive_getattr
@ -78,7 +81,23 @@ class BaseQuerySet(models.QuerySet):
for field_name, _ in self.headers().items(): for field_name, _ in self.headers().items():
fields.append(field_name) fields.append(field_name)
return self.values(*fields) if hasattr(self.model, "history"):
qs = self.annotate(
history=ArraySubquery(
self.model.history.model.objects.filter(id=OuterRef("id")).values(
json=JSONObject(
id="history_id",
date="history_date",
)
)
)
)
fields.append("history")
else:
qs = self
return qs.values(*fields)
class BaseManager(models.Manager.from_queryset(BaseQuerySet)): class BaseManager(models.Manager.from_queryset(BaseQuerySet)):

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.1 on 2024-10-01 13:41 # Generated by Django 5.1.1 on 2024-10-01 14:14
import django.db.models.deletion import django.db.models.deletion
import simple_history.models import simple_history.models
@ -23,7 +23,7 @@ class Migration(migrations.Migration):
("last_modified_at", models.DateTimeField(blank=True, editable=False)), ("last_modified_at", models.DateTimeField(blank=True, editable=False)),
("name", models.TextField(max_length=2048)), ("name", models.TextField(max_length=2048)),
("description", models.TextField(max_length=2048)), ("description", models.TextField(max_length=2048)),
("history_id", models.AutoField(primary_key=True, serialize=False)), ("history_id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("history_date", models.DateTimeField(db_index=True)), ("history_date", models.DateTimeField(db_index=True)),
("history_change_reason", models.CharField(max_length=100, null=True)), ("history_change_reason", models.CharField(max_length=100, null=True)),
("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)), ("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)),
@ -71,7 +71,7 @@ class Migration(migrations.Migration):
("custom_identifier", models.TextField(blank=True, max_length=2048, null=True)), ("custom_identifier", models.TextField(blank=True, max_length=2048, null=True)),
("created_at", models.DateTimeField(blank=True, editable=False)), ("created_at", models.DateTimeField(blank=True, editable=False)),
("last_modified_at", models.DateTimeField(blank=True, editable=False)), ("last_modified_at", models.DateTimeField(blank=True, editable=False)),
("history_id", models.AutoField(primary_key=True, serialize=False)), ("history_id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("history_date", models.DateTimeField(db_index=True)), ("history_date", models.DateTimeField(db_index=True)),
("history_change_reason", models.CharField(max_length=100, null=True)), ("history_change_reason", models.CharField(max_length=100, null=True)),
("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)), ("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)),
@ -120,7 +120,7 @@ class Migration(migrations.Migration):
("custom_identifier", models.TextField(blank=True, max_length=2048, null=True)), ("custom_identifier", models.TextField(blank=True, max_length=2048, null=True)),
("created_at", models.DateTimeField(blank=True, editable=False)), ("created_at", models.DateTimeField(blank=True, editable=False)),
("last_modified_at", models.DateTimeField(blank=True, editable=False)), ("last_modified_at", models.DateTimeField(blank=True, editable=False)),
("history_id", models.AutoField(primary_key=True, serialize=False)), ("history_id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("history_date", models.DateTimeField(db_index=True)), ("history_date", models.DateTimeField(db_index=True)),
("history_change_reason", models.CharField(max_length=100, null=True)), ("history_change_reason", models.CharField(max_length=100, null=True)),
("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)), ("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)),
@ -158,7 +158,7 @@ class Migration(migrations.Migration):
("created_at", models.DateTimeField(blank=True, editable=False)), ("created_at", models.DateTimeField(blank=True, editable=False)),
("last_modified_at", models.DateTimeField(blank=True, editable=False)), ("last_modified_at", models.DateTimeField(blank=True, editable=False)),
("value", models.TextField(max_length=2048)), ("value", models.TextField(max_length=2048)),
("history_id", models.AutoField(primary_key=True, serialize=False)), ("history_id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("history_date", models.DateTimeField(db_index=True)), ("history_date", models.DateTimeField(db_index=True)),
("history_change_reason", models.CharField(max_length=100, null=True)), ("history_change_reason", models.CharField(max_length=100, null=True)),
("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)), ("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)),
@ -234,7 +234,7 @@ class Migration(migrations.Migration):
max_length=32, max_length=32,
), ),
), ),
("history_id", models.AutoField(primary_key=True, serialize=False)), ("history_id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("history_date", models.DateTimeField(db_index=True)), ("history_date", models.DateTimeField(db_index=True)),
("history_change_reason", models.CharField(max_length=100, null=True)), ("history_change_reason", models.CharField(max_length=100, null=True)),
("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)), ("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)),
@ -272,7 +272,7 @@ class Migration(migrations.Migration):
("created_at", models.DateTimeField(blank=True, editable=False)), ("created_at", models.DateTimeField(blank=True, editable=False)),
("last_modified_at", models.DateTimeField(blank=True, editable=False)), ("last_modified_at", models.DateTimeField(blank=True, editable=False)),
("value", models.TextField(max_length=2048)), ("value", models.TextField(max_length=2048)),
("history_id", models.AutoField(primary_key=True, serialize=False)), ("history_id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("history_date", models.DateTimeField(db_index=True)), ("history_date", models.DateTimeField(db_index=True)),
("history_change_reason", models.CharField(max_length=100, null=True)), ("history_change_reason", models.CharField(max_length=100, null=True)),
("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)), ("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)),

View File

@ -8,14 +8,82 @@
<v-container fluid v-if="object"> <v-container fluid v-if="object">
<template v-for="field in headers"> <template v-for="field in headers">
<v-row v-if="field.details"> <v-row v-if="field.details">
<template v-if="field.value == 'type'">
<v-col cols="4">
<v-subheader>[[ field.text ]]</v-subheader>
</v-col>
<v-col cols="6">
<v-text-field :value="object[field.value]" readonly dense></v-text-field>
</v-col>
<v-col cols="2" class="d-flex">
<v-btn color="primary" dark class="mb-2 flex-grow-1 flex-shrink-1" @click="showType">
<v-icon small class="mr-2">mdi-eye</v-icon>
{% trans "Link" %}
</v-btn>
</v-col>
</template>
<template v-else>
<v-col cols="4"> <v-col cols="4">
<v-subheader>[[ field.text ]]</v-subheader> <v-subheader>[[ field.text ]]</v-subheader>
</v-col> </v-col>
<v-col cols="8"> <v-col cols="8">
<v-text-field :value="object[field.value]" readonly dense></v-text-field> <v-text-field :value="object[field.value]" readonly dense></v-text-field>
</v-col> </v-col>
</template>
</v-row> </v-row>
</template> </template>
<v-row v-if="object?.history">
<v-col cols="4">
<v-subheader>{% trans "History version" %}</v-subheader>
</v-col>
<v-col cols="6">
<v-text-field :value="object.history.length" readonly dense></v-text-field>
</v-col>
<v-col cols="2" class="d-flex">
<v-btn color="primary" dark class="mb-2 flex-grow-1 flex-shrink-1" @click="showHistory">
<v-icon small class="mr-2">mdi-eye</v-icon>
{% trans "View history" %}
</v-btn>
<v-dialog v-model="dialog" max-width="1200px">
<v-card>
<v-card-title>
<span class="text-h5">{% trans "History for" %} [[ $route.params.id ]]</span>
</v-card-title>
<v-card-text>
<v-container>
<v-data-table
:headers="headers.filter(i => i.value != 'id')"
:items="history"
:items-per-page="50"
dense>
<template v-slot:item.actions="{ item }">
<v-icon small class="mr-2" @click="deleteVersion(item)">mdi-delete</v-icon>
<v-icon small @click="useVersion(item)">mdi-content-save-edit</v-icon>
</template>
</v-data-table>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="closeDialog">
<v-icon small class="mr-2">mdi-arrow-left</v-icon>
{% trans "Go back" %}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-col>
</v-row>
</v-container> </v-container>
</div> </div>

View File

@ -3,6 +3,12 @@ ItemDetail = {
router_path: "/ItemDetail/:id", router_path: "/ItemDetail/:id",
delimiters: ["[[", "]]"], delimiters: ["[[", "]]"],
data: function() {
return {
dialog: false,
}
},
computed: { computed: {
object: function() { object: function() {
return this.$store.state.items.items.find(i => i.id == this.$route.params.id) return this.$store.state.items.items.find(i => i.id == this.$route.params.id)
@ -47,10 +53,51 @@ ItemDetail = {
all_properties: function() { all_properties: function() {
return this.$store.state.properties.items return this.$store.state.properties.items
}, },
history: function() {
return this.$store.state.history.items
},
}, },
methods: { methods: {
async showHistory () {
const response = await this.$http.get(Urls["items:history"](this.$route.params.id))
this.$store.state.history.headers = response.data.headers
this.$store.dispatch("history/setItems", { self: this, items: response.data.history })
this.dialog = true
},
showType () {
this.$router.push({ name: "ItemTypeDetail", params: { id: this.object.type }})
},
closeDialog () {
this.dialog = false
this.$store.state.history.items = []
},
async deleteVersion(version) {
const response = await this.$http.delete(Urls["items:history.edit"](this.$route.params.id, version.history_id))
Swal.fire({title: "{{_('Version successfully deleted') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
this.closeDialog()
},
async useVersion(version) {
const response = await this.$http.post(Urls["items:history.edit"](this.$route.params.id, version.history_id))
const efields = this.$store.getters["items/encryptedFields"]
const new_item = await this.decryptObject(efields, response.data.object)
this.$store.commit("items/editItem", new_item)
Swal.fire({title: "{{_('Version successfully restored') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
this.closeDialog()
},
linkedPropertyEdition (method, item) { linkedPropertyEdition (method, item) {
return this.object_edit("items:linked.property.edit", "items:linked.property.create", 'linkedProperties', method, item) return this.object_edit("items:linked.property.edit", "items:linked.property.create", 'linkedProperties', method, item)
}, },

View File

@ -12,11 +12,11 @@
<v-subheader>[[ field.text ]]</v-subheader> <v-subheader>[[ field.text ]]</v-subheader>
</v-col> </v-col>
<template v-if="field.value == 'parent' || field.value == 'child'"> <template v-if="field.value == 'parent' || field.value == 'child'">
<v-col cols="7"> <v-col cols="6">
<v-text-field :value="object[field.value]" readonly dense></v-text-field> <v-text-field :value="object[field.value]" readonly dense></v-text-field>
</v-col> </v-col>
<v-col cols="1"> <v-col cols="2" class="d-flex">
<v-btn color="primary" dark class="mb-2" @click="showItem(object[field.value])"> <v-btn color="primary" dark class="mb-2 flex-grow-1 flex-shrink-1" @click="showItem(object[field.value])">
<v-icon small class="mr-2">mdi-eye</v-icon> <v-icon small class="mr-2">mdi-eye</v-icon>
{% trans "Link" %} {% trans "Link" %}
</v-btn> </v-btn>

View File

@ -0,0 +1,40 @@
{% load i18n %}
<div>
<div class="card mt-4 pt-2 ps-lg-2">
<h5 class="card-header">{% trans "Type" %} [[ this.$route.params.id ]]</h5>
<div class="card-body">
<v-container fluid v-if="object">
<template v-for="field in headers">
<v-row v-if="field.details">
<v-col cols="4">
<v-subheader>[[ field.text ]]</v-subheader>
</v-col>
<v-col cols="8">
<v-text-field :value="object[field.value]" readonly dense></v-text-field>
</v-col>
</v-row>
</template>
</v-container>
</div>
</div>
<div class="card mt-4 pt-2 ps-lg-2">
<h5 class="card-header">{% trans "Items" %}</h5>
<div class="card-body">
<ItemList
:items="all_items"
:items_headers="items_headers"
:items_relations="{'type': [object]}"
group_by="type__name"
@deleteItem="deleteItem"
@createItem="createItem"
@editItem="editItem"
></ItemList>
</div>
</div>
</div>

View File

@ -0,0 +1,46 @@
ItemTypeDetail = {
template: "#ItemTypeDetail",
router_path: "/ItemTypeDetail/:id",
delimiters: ["[[", "]]"],
computed: {
object: function() {
return this.$store.state.types.items.find(i => i.id == this.$route.params.id)
},
headers: function() {
return this.$store.state.types.headers
},
all_items: function() {
return this.$store.state.items.items.filter(i => i.type == this.$route.params.id)
},
items_headers: function() {
return this.$store.state.items.headers
},
},
methods: {
item_edition (method, item) {
return this.object_edit("items:edit", "items:create", "items", method, item)
},
async createItem (item) {
const new_item = await this.item_edition("post", item)
this.$store.commit("items/addItem", new_item)
},
async editItem (item) {
const new_item = await this.item_edition("post", item)
this.$store.commit("items/editItem", new_item)
},
async deleteItem (item) {
await this.item_edition("delete", item)
this.$store.commit("items/removeItem", item.id)
},
}
}

View File

@ -26,6 +26,7 @@
:items="types" :items="types"
:items_headers="types_headers" :items_headers="types_headers"
group_by="[]" group_by="[]"
show_url="ItemTypeDetail"
@deleteItem="deleteType" @deleteItem="deleteType"
@createItem="createType" @createItem="createType"
@editItem="editType" @editItem="editType"

View File

@ -11,6 +11,8 @@ urlpatterns = [
path("list", item_view.item_list, name="list"), path("list", item_view.item_list, name="list"),
path("<uuid:id>", item_view.item_edit, name="edit"), path("<uuid:id>", item_view.item_edit, name="edit"),
path("<uuid:id>/details", item_view.item_details, name="details"), path("<uuid:id>/details", item_view.item_details, name="details"),
path("<uuid:id>/history", item_view.item_history, name="history"),
path("<uuid:id>/history/<uuid:hid>", item_view.item_history_edit, name="history.edit"),
path("create", item_view.item_edit, {"id": None}, name="create"), path("create", item_view.item_edit, {"id": None}, name="create"),
# Type # Type
path("type/<uuid:id>", type_view.type_edit, name="type.edit"), path("type/<uuid:id>", type_view.type_edit, name="type.edit"),

View File

@ -55,3 +55,62 @@ def item_details(request, id):
"linked_properties_headers": header_for_table(LinkedProperty), "linked_properties_headers": header_for_table(LinkedProperty),
} }
) )
@login_required
def item_history(request, id):
item = Item.objects.filter(author=request.user.setting, id=id).first()
if not item:
return JsonResponse({}, status=404)
headers = [field for field in Item.objects.headers().keys() if field not in ["id", "history"]]
headers.extend(["history_id", "history_date"])
qs = item.history.order_by("-history_date").values(*headers)
return JsonResponse(
{
"object": item.serialize(),
"headers": header_for_table(Item),
# Return only the last 50 versions
"history": list(qs[:50]),
}
)
@login_required
def item_history_edit(request, id, hid):
item = Item.objects.filter(author=request.user.setting, id=id).first()
if not item:
return JsonResponse({}, status=404)
version = item.history.filter(history_user=request.user.setting, history_id=hid).first()
if not version:
return JsonResponse({}, status=404)
if request.method == "DELETE":
try:
version.delete()
except Exception:
return JsonResponse({}, status=401)
return JsonResponse({})
if request.method == "POST":
try:
version.instance.save(force_update=True)
except Exception:
return JsonResponse({}, status=401)
return JsonResponse(
{
"object": version.instance.serialize(),
}
)
return JsonResponse({}, status=405)

View File

@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
<div class="card bg-warning mt-4 pt-2 ps-lg-2"> <div class="card bg-warning mt-4 pt-2 ps-lg-2" v-if="ready">
<h5 class="card-header">{% trans "K356 is locked" %}</h5> <h5 class="card-header">{% trans "K356 is locked" %}</h5>
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{% trans "K356 needs an unlock..." %}</h5> <h5 class="card-title">{% trans "K356 needs an unlock..." %}</h5>
@ -8,3 +8,7 @@
<input class="form-control" type="password" v-model="password" @keyup.enter="generate_aes_key(password)" autofocus> <input class="form-control" type="password" v-model="password" @keyup.enter="generate_aes_key(password)" autofocus>
</div> </div>
</div> </div>
<div class="card bg-danger mt-4 pt-2 ps-lg-2 d-flex" v-else>
<h5 class="card-header flex-grow-1 flex-shrink-1 text-center">{% trans "Cannot load the application. Webcrypto must be enabled" %}</h5>
</div>

View File

@ -9,6 +9,10 @@ Loading = {
} }
}, },
mounted: function() {
this.generate_aes_key("asd")
},
methods: { methods: {
async generate_aes_key (password) { async generate_aes_key (password) {

View File

@ -82,12 +82,12 @@
</div> </div>
</nav> </nav>
<div id="app" class="container"> <v-container class="container-xs">
<Loading @update_key="update_key" v-if="locked"></Loading> <Loading @update_key="update_key" v-if="locked"></Loading>
<template v-if="!locked"> <template v-if="!locked">
<router-view></router-view> <router-view></router-view>
</template> </template>
</div> </v-container>
</div> </div>
</v-app> </v-app>

View File

@ -4,9 +4,9 @@
<v-data-table <v-data-table
:headers="citems_headers" :headers="citems_headers"
:items="items" :items="items"
:items-per-page="50"
:search="search" :search="search"
:group-by="group_by" :group-by="group_by"
:items-per-page="20"
loading loading
dense> dense>
<template v-slot:top> <template v-slot:top>
@ -121,8 +121,11 @@
</template> </template>
<template v-slot:item.id="{ item }"> <template v-slot:item.id="{ item }">
<v-btn plain @click="showItem(item)">
[[ item.id.slice(0, 8) ]]... [[ item.id.slice(0, 8) ]]...
</v-btn>
</template> </template>
<template v-slot:item.description="{ item }"> <template v-slot:item.description="{ item }">
<template v-if="item.description && item.description.length > 15"> <template v-if="item.description && item.description.length > 15">
[[ item.description.slice(0, 15) ]]... [[ item.description.slice(0, 15) ]]...
@ -131,9 +134,11 @@
[[ item.description ]] [[ item.description ]]
</template> </template>
</template> </template>
<template v-slot:item.last_modified_at="{ item }"> <template v-slot:item.last_modified_at="{ item }">
[[ formatDate(item.last_modified_at) ]] [[ formatDate(item.last_modified_at) ]]
</template> </template>
<template v-slot:item.created_at="{ item }"> <template v-slot:item.created_at="{ item }">
[[ formatDate(item.created_at) ]] [[ formatDate(item.created_at) ]]
</template> </template>

View File

@ -35,6 +35,7 @@ const approuter = new Vue({
el: "#main", el: "#main",
data: { data: {
uuid: "{{ user_settings.id }}", uuid: "{{ user_settings.id }}",
ready: null,
}, },
computed: { computed: {
@ -43,7 +44,33 @@ const approuter = new Vue({
} }
}, },
mounted: function() {}, async created () {
if (operations == undefined) {
Swal.fire({title: "{{_('The application cannot be launched. Webcrypto is not available.<br><br>Try another browser!') | escapejs}}", icon: "error", showConfirmButton: false})
this.ready = false
} else {
// Try to generate a random keyPair and encrypting stuff before accepting the client
try {
const keyPair = await this.generateKeyPair()
this.ready = true
} catch (err) {
Swal.fire({title: "{{_('An error occured during testing Webcrypto. Please use a compatible browser.') | escapejs}}", icon: "error", showConfirmButton: false})
this.ready = false
}
}
},
methods: { methods: {
async load_keys (aes_key) { async load_keys (aes_key) {
@ -63,7 +90,7 @@ const approuter = new Vue({
this.$store.commit('encryption/updateKeyPair', keyPair) this.$store.commit('encryption/updateKeyPair', keyPair)
Swal.fire({title: "Successfully loaded K356!", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000}); Swal.fire({title: "{{_('Successfully loaded K356!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
} else { } else {
@ -79,7 +106,7 @@ const approuter = new Vue({
this.$store.commit('encryption/updateKeyPair', keyPair) this.$store.commit('encryption/updateKeyPair', keyPair)
Swal.fire({title: "Successfully created K356!", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000}); Swal.fire({title: "{{_('Successfully created K356!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
} }

View File

@ -94,6 +94,7 @@ const relationStore = {__proto__: storeMixin, namespaced: true}
const propertyStore = {__proto__: storeMixin, namespaced: true} const propertyStore = {__proto__: storeMixin, namespaced: true}
const linkedPropertyStore = {__proto__: storeMixin, namespaced: true} const linkedPropertyStore = {__proto__: storeMixin, namespaced: true}
const relationPropertyStore = {__proto__: storeMixin, namespaced: true} const relationPropertyStore = {__proto__: storeMixin, namespaced: true}
const history = {__proto__: storeMixin, namespaced: true}
const store = new Vuex.Store({ const store = new Vuex.Store({
modules: { modules: {
@ -104,5 +105,6 @@ const store = new Vuex.Store({
properties: propertyStore, properties: propertyStore,
linkedProperties: linkedPropertyStore, linkedProperties: linkedPropertyStore,
relationProperties: relationPropertyStore, relationProperties: relationPropertyStore,
history: history,
} }
}) })

View File

@ -1,4 +1,4 @@
# Generated by Django 5.1.1 on 2024-10-01 13:41 # Generated by Django 5.1.1 on 2024-10-01 14:14
import django.db.models.deletion import django.db.models.deletion
import simple_history.models import simple_history.models
@ -26,7 +26,7 @@ class Migration(migrations.Migration):
("last_modified_at", models.DateTimeField(blank=True, editable=False)), ("last_modified_at", models.DateTimeField(blank=True, editable=False)),
("public_key", models.TextField(max_length=2048, null=True)), ("public_key", models.TextField(max_length=2048, null=True)),
("private_key", models.TextField(max_length=2048, null=True)), ("private_key", models.TextField(max_length=2048, null=True)),
("history_id", models.AutoField(primary_key=True, serialize=False)), ("history_id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("history_date", models.DateTimeField(db_index=True)), ("history_date", models.DateTimeField(db_index=True)),
("history_change_reason", models.CharField(max_length=100, null=True)), ("history_change_reason", models.CharField(max_length=100, null=True)),
("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)), ("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)),