Add history + more components

This commit is contained in:
2024-10-03 22:29:32 +02:00
parent 8f9c83dc2a
commit d7f0d2a7f2
18 changed files with 352 additions and 30 deletions
@@ -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 simple_history.models
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
("last_modified_at", models.DateTimeField(blank=True, editable=False)),
("name", 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_change_reason", models.CharField(max_length=100, null=True)),
("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)),
("created_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_change_reason", models.CharField(max_length=100, null=True)),
("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)),
("created_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_change_reason", models.CharField(max_length=100, null=True)),
("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)),
("last_modified_at", models.DateTimeField(blank=True, editable=False)),
("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_change_reason", models.CharField(max_length=100, null=True)),
("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)),
@@ -234,7 +234,7 @@ class Migration(migrations.Migration):
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_change_reason", models.CharField(max_length=100, null=True)),
("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)),
("last_modified_at", models.DateTimeField(blank=True, editable=False)),
("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_change_reason", models.CharField(max_length=100, null=True)),
("history_type", models.CharField(choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], max_length=1)),
@@ -8,14 +8,82 @@
<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>
<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-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>
</template>
</v-row>
</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>
</div>
@@ -3,6 +3,12 @@ ItemDetail = {
router_path: "/ItemDetail/:id",
delimiters: ["[[", "]]"],
data: function() {
return {
dialog: false,
}
},
computed: {
object: function() {
return this.$store.state.items.items.find(i => i.id == this.$route.params.id)
@@ -47,10 +53,51 @@ ItemDetail = {
all_properties: function() {
return this.$store.state.properties.items
},
history: function() {
return this.$store.state.history.items
},
},
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) {
return this.object_edit("items:linked.property.edit", "items:linked.property.create", 'linkedProperties', method, item)
},
@@ -12,11 +12,11 @@
<v-subheader>[[ field.text ]]</v-subheader>
</v-col>
<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-col>
<v-col cols="1">
<v-btn color="primary" dark class="mb-2" @click="showItem(object[field.value])">
<v-col cols="2" class="d-flex">
<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>
{% trans "Link" %}
</v-btn>
@@ -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>
@@ -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)
},
}
}
@@ -26,6 +26,7 @@
:items="types"
:items_headers="types_headers"
group_by="[]"
show_url="ItemTypeDetail"
@deleteItem="deleteType"
@createItem="createType"
@editItem="editType"
+2
View File
@@ -11,6 +11,8 @@ urlpatterns = [
path("list", item_view.item_list, name="list"),
path("<uuid:id>", item_view.item_edit, name="edit"),
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"),
# Type
path("type/<uuid:id>", type_view.type_edit, name="type.edit"),
+59
View File
@@ -55,3 +55,62 @@ def item_details(request, id):
"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)