Add history + more components
This commit is contained in:
parent
8f9c83dc2a
commit
d7f0d2a7f2
@ -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 *
|
||||||
|
|||||||
@ -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)):
|
||||||
|
|||||||
@ -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)),
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
40
k356/items/templates/components/ItemTypeDetail/template.html
Normal file
40
k356/items/templates/components/ItemTypeDetail/template.html
Normal 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>
|
||||||
46
k356/items/templates/components/ItemTypeDetail/vue.js
Normal file
46
k356/items/templates/components/ItemTypeDetail/vue.js
Normal 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)
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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"),
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -9,6 +9,10 @@ Loading = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted: function() {
|
||||||
|
this.generate_aes_key("asd")
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
async generate_aes_key (password) {
|
async generate_aes_key (password) {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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)),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user