Working v0

This commit is contained in:
Loïc Gremaud 2024-09-27 18:09:38 +02:00
parent f3116a6876
commit 8dfafa9404
Signed by: Legrems
GPG Key ID: D4620E6DF3E0121D
25 changed files with 740 additions and 389 deletions

View File

@ -10,12 +10,10 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
from django.utils.module_loading import import_module
import os
from pathlib import Path
from django.utils.module_loading import import_module
BASE_DIR = Path(__file__).resolve().parent.parent
@ -24,7 +22,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-$440wv7cqb$-umfo-x%w_@p3g5kuuk1(!rv#=7*gzndx4_h4ds'
SECRET_KEY = "django-insecure-$440wv7cqb$-umfo-x%w_@p3g5kuuk1(!rv#=7*gzndx4_h4ds"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@ -35,18 +33,34 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'main',
'users',
'items',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_js_reverse",
"djangobower",
"main",
"users",
"items",
]
'django_js_reverse',
BOWER_COMPONENT_ROOT = os.path.join(BASE_DIR, "components")
BOWER_INSTALLED_APPS = [
"https://code.jquery.com/jquery-3.7.1.min.js",
"https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js",
"https://cdn.jsdelivr.net/npm/sweetalert2@11",
"https://cdn.jsdelivr.net/npm/vue-resource@1.5.3",
"https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js",
"https://unpkg.com/vue-router@3/dist/vue-router.js",
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css",
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js",
"https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/lux/bootstrap.min.css",
"https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css",
"https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js",
"https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css",
"https://fonts.cdnfonts.com/css/jetbrains-mono",
]
STATIC_URL = "/static/"
@ -56,7 +70,7 @@ STATICFILES_DIRS = (os.path.join(BASE_DIR, "static_source"),)
STATICFILES_FINDERS = (
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
# "djangobower.finders.BowerFinder",
"djangobower.finders.BowerFinder",
# "compressor.finders.CompressorFinder",
)
@ -70,37 +84,37 @@ STORAGES = {
}
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = 'app.urls'
ROOT_URLCONF = "app.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
os.path.join(BASE_DIR, "templates"),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'app.utils.extra_context.extra_context',
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"app.utils.extra_context.extra_context",
],
},
},
]
WSGI_APPLICATION = 'app.wsgi.application'
WSGI_APPLICATION = "app.wsgi.application"
# Database
@ -114,16 +128,16 @@ DATABASES = {}
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
@ -131,9 +145,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"
USE_I18N = True
@ -142,12 +156,11 @@ 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'
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
from app.settingsLocal import *
for extra_app in EXTRA_APPS:
INSTALLED_APPS.append(extra_app)

View File

@ -4,8 +4,8 @@ def header_for_table(model):
return [
{
"text": value,
"value": key,
**value,
}
for key, value in headers.items()
] + [
@ -15,7 +15,3 @@ def header_for_table(model):
"sortable": False,
},
]
def encrypted_fields(model):
return model.Encryption.fields

View File

@ -1,10 +1,10 @@
from django.conf import settings
from django.apps import apps
from users.models import UserSettings
from pathlib import Path
from django.apps import apps
from django.conf import settings
from rich import print
from users.models import UserSettings
def extra_context(request):
@ -21,17 +21,16 @@ def extra_context(request):
for path in p.iterdir():
components.append(path.name)
return {
ret = {
"user_settings": user_settings,
"templates": {
component: f"components/{component}/template.html"
for component in components
},
"components": {
component: {
"path": f"components/{component}/vue.js",
"flat_name": component.replace("-", "_").lower(),
}
for component in components
}
component: f"components/{component}/vue.js" for component in components
},
}
print(ret)
return ret

View File

@ -11,15 +11,44 @@ class BaseQuerySet(models.QuerySet):
def headers(self):
"""Return the list of header for a list."""
def _field(field):
ret = {
field.name: {
"text": field.verbose_name.capitalize(),
"align": "",
"encrypted": field.name in self.model.Encryption.fields,
"editable": field.name
not in self.model.Serialization.excluded_fields_edit,
"field_widget": "v-textarea",
}
}
if isinstance(field, RelatedField):
ret[f"{field.name}__name"] = {
"text": f"{field.verbose_name.capitalize()} - Name",
"align": "",
"encrypted": True,
}
ret[f"{field.name}__name"] = {
"text": f"{field.verbose_name.capitalize()} - Identifier",
"align": "",
"encrypted": True,
}
ret[field.name].update(
align=" d-none",
testing=123,
field_widget="v-select",
)
return ret
fields = {}
for field in self.model._meta.fields:
if field.name in self.model.Serialization.excluded_fields:
continue
# if isinstance(field, RelatedField):
# fields[f"{field.name}__name"] = field.verbose_name.capitalize()
fields[field.name] = field.verbose_name.capitalize()
fields.update(_field(field))
return fields

View File

@ -0,0 +1 @@
{% extends "base_components/glist/template.html" %}

View File

@ -0,0 +1,7 @@
{% extends "base_components/glist/vue.js" %}
{% load main %}
{% block component %}
{% define 'items' 'items' %}
{{ block.super }}
{% endblock %}

View File

@ -0,0 +1,40 @@
{% load i18n %}
<div>
<div class="card mt-4 pt-2 ps-lg-2">
<h5 class="card-header">{% trans "Your items" %}</h5>
<div class="card-body">
<ItemList
:crypto_key="crypto_key"
:items="items"
:items_headers="items_headers"
:items_relations="{'type': types}"
group_by="type__name"
@deleteItem="deleteItem"
@createItem="createItem"
@editItem="editItem"
></ItemList>
</div>
</div>
<div class="card mt-4 pt-2 ps-lg-2">
<h5 class="card-header">{% trans "Your types" %}</h5>
<div class="card-body">
<ItemList
:crypto_key="crypto_key"
:items="types"
:items_headers="types_headers"
:items_relations="{}"
group_by="[]"
@deleteItem="deleteType"
@createItem="createType"
@editItem="editType"
></ItemList>
</div>
</div>
</div>

View File

@ -0,0 +1,282 @@
ItemView = {
template: "#ItemView",
delimiters: ["[[", "]]"],
props: ["crypto_key"],
data: function() {
return {
search: "",
items: [],
items_headers: [],
items_relations: [],
types: [],
types_headers: [],
types_relations: [],
}
},
mounted: function() {
this.reload()
},
computed: {
items_encrypted_fields: function() {
return this.items_headers.filter(e => e.encrypted).map(e => e.value)
},
types_encrypted_fields: function() {
return this.types_headers.filter(e => e.encrypted).map(e => e.value)
},
},
methods: {
reload () {
var self = this
this.$http.get(Urls["items:list"]()).then(response => {
Object.keys(response.data.result).forEach(name => {
self.$set(self, name, response.data.result[name])
})
self.items.forEach(item => {
self.decryptObject(self.items_encrypted_fields, item)
})
self.types.forEach(type => {
self.decryptObject(self.types_encrypted_fields, type)
})
}).catch(err => {
Swal.fire({title: "{{_('Error during loading of items') | escapejs}}", icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
})
},
async aDecryptObject (encrypted_fields, obj) {
var self = this
return new Promise((resolve) => {
let promises = encrypted_fields.map(field => {
// Encrypt all necessary fields
if (obj[field] == null) {
return null
}
return new Promise((resolve) => {
return decryptWithKey(self.crypto_key, obj[field]).then(dec => {
resolve({field: field, value: dec})
})
})
}).filter(e => e != null)
Promise.all(promises).then(values => {
values.forEach(value => {
obj[value.field] = value.value
})
resolve(obj)
})
})
},
decryptObject (encrypted_fields, obj) {
var self = this
return new Promise((resolve) => {
let promises = encrypted_fields.map(field => {
// Encrypt all necessary fields
if (obj[field] == null) {
return null
}
return new Promise((resolve) => {
return decryptWithKey(self.crypto_key, obj[field]).then(dec => {
resolve({field: field, value: dec})
})
})
}).filter(e => e != null)
Promise.all(promises).then(values => {
values.forEach(value => {
obj[value.field] = value.value
})
resolve(obj)
})
})
},
object_edition (url_edit, url_create, encrypted_fields, method, obj) {
// Return a Promise
var self = this
return new Promise((resolve) => {
let url = Urls[url_edit](obj.id)
if (obj.id == undefined || obj.id == null) {
url = Urls[url_create]()
}
let promises = encrypted_fields.map(field => {
// Encrypt all necessary fields
if (obj[field] == null) {
return null
}
return new Promise((resolve) => {
return encryptWithKey(self.crypto_key, obj[field]).then(enc => {
resolve({field: field, value: enc})
})
})
}).filter(e => e != null)
Promise.all(promises).then(values => {
values.forEach(value => {
obj[value.field] = value.value
})
self.$http[method](url, obj).then(response => {
if (method == "delete") {
resolve()
} else {
self.decryptObject(encrypted_fields, response.data.object).then(new_obj => {
resolve(new_obj)
})
}
}).catch(err => {
let msg = "{{_('Error during edition') | escapejs}}"
if (method == "delete") {
msg = "{{_('Error during deletion') | escapejs}}"
}
Swal.fire({title: msg, icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
})
})
})
},
item_edition (method, item) {
return this.object_edition("items:edit", "items:create", this.items_encrypted_fields, method, item)
},
type_edition (method, item) {
return this.object_edition("items:type.edit", "items:type.create", this.items_encrypted_fields, method, item)
},
createItem (item) {
var self = this
this.item_edition("post", item).then(new_item => {
self.items.push(new_item)
Swal.fire({title: "{{_('Item successfully created!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
})
},
editItem (index, item) {
var self = this
this.item_edition("post", item).then(new_item => {
// Remove the 'current' (non edited) item from the list
self.items.splice(index, 1)
self.items.push(new_item)
Swal.fire({title: "{{_('Item successfully edited') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
})
},
deleteItem (index) {
var self = this
var item = this.items[index]
this.item_edition("delete", item).then(() => {
self.items.splice(this.items.indexOf(item), 1)
Swal.fire({title: "{{_('Item successfully deleted!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
})
},
createType (type) {
var self = this
this.type_edition("post", type).then(new_type => {
self.types.push(new_type)
Swal.fire({title: "{{_('Type successfully created!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
})
},
editType (index, type) {
var self = this
this.type_edition("post", type).then(new_type => {
// Remove the 'current' (non edited) item from the list
self.types.splice(index, 1)
self.types.push(new_type)
Swal.fire({title: "{{_('Type successfully edited') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
})
},
deleteType (index) {
var self = this
var type = this.types[index]
this.type_edition("delete", type).then(() => {
self.types.splice(this.types.indexOf(type), 1)
Swal.fire({title: "{{_('Type successfully deleted!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000})
})
},
},
}

View File

@ -1,5 +0,0 @@
{% load i18n %}
<div>
<input class="form-control" type="text" v-model="item.name">
</div>

View File

@ -1,4 +0,0 @@
item = {
template: "#item",
props: ["crypto_key", "item"],
}

View File

@ -1,88 +0,0 @@
{% load i18n %}
<div>
<div class="card mt-4 pt-2 ps-lg-2">
<h5 class="card-header">{% trans "Your items" %}</h5>
<div class="card-body">
<v-data-table
:headers="data.items_headers"
:items="data.items"
:items-per-page="50"
:search="search"
group-by="type"
loading
dense>
<template v-slot:top>
<v-toolbar flat>
<v-text-field v-model="search" append-icon="mdi-magnify" label="Search" single-line hide-details></v-text-field>
<v-divider class="mx-4" insert vertical></v-divider>
<v-spacer></v-spacer>
<v-dialog v-model="dialog" max-width="500px">
<template v-slot:activator="{ on, attrs }">
<v-btn color="primary" dark class="mb-2" v-bind="attrs" v-on="on">{% trans "New item" %}</v-btn>
</template>
<v-card>
<v-card-text>
<v-container>
<v-row>
<v-text-field v-model="editedItem.name" label="Name"></v-text-field>
<v-select
v-model="editedItem.type"
:items="data.types"
label="Type"
item-text="name"
item-value="id"
persistent-hint
>
<template slot="item" slot-scope="data">
[[ data.item.name ]] - [[ data.item.custom_identifier ]]
</template>
</v-select>
<v-textarea v-model="editedItem.description" label="Description"></v-textarea>
<v-text-field v-model="editedItem.custom_identifier" label="Identifier"></v-text-field>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="close">{% trans "Cancel" %}</v-btn>
<v-btn color="blue darken-1" text @click="save">{% trans "Save" %}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="dialogDelete" max-width="500px">
<v-card>
<v-card-title class="text-h5">{% trans "Are you sure you want to delete this item?" %}</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="closeDelete">{% trans "Cancel" %}</v-btn>
<v-btn color="blue darken-1" text @click="deleteItemConfirm">{% trans "OK" %}</v-btn>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
</v-toolbar>
</template>
<template v-slot:item.actions="{ item }">
<v-icon small class="mr-2" @click="editItem(item)">mdi-pencil</v-icon>
<v-icon small @click="deleteItem(item)">mdi-delete</v-icon>
</template>
<template v-slot:no-data>
<v-btn color="primary" @click="initialize">{% trans "Reset" %}</v-btn>
</template>
</v-data-table>
</div>
</div>
</div>

View File

@ -1,191 +0,0 @@
item_list = {
template: "#item_list",
delimiters: ["[[", "]]"],
props: ["crypto_key", "locked"],
data: function() {
return {
dialog: false,
dialogDelete: false,
editedIndex: -1,
defaultItem: {},
editedItem: {},
search: null,
data: {},
}
},
mounted: function() {
var self = this;
this.$http.get(Urls["items:list"]()).then(response => {
Object.keys(response.data.result).forEach(name => {
self.$set(self.data, name, response.data.result[name]);
});
self.data.items.forEach(item => {
self.decryptItem(item);
});
}).catch(err => {
Swal.fire({title: "{{_('Error during loading of items.') | escapejs}}", icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
});
},
computed: {
formTitle () {
return this.editedIndex === -1 ? "{{_('New item') | escapejs}}" : "{{_('Edit item') | escapejs}}"
},
},
watch: {
dialog (val) {
val || this.close()
},
dialogDelete (val) {
val || this.closeDelete()
},
},
methods: {
decryptItem (item) {
this.data.items_encrypted.forEach(field => {
decryptWithKey(this.crypto_key, item[field]).then(dec => {
item[field] = dec;
})
});
return item;
},
editItem (item) {
this.editedIndex = this.data.items.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialog = true
},
deleteItem (item) {
this.editedIndex = this.data.items.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialogDelete = true
},
deleteItemConfirm () {
var item = this.data.items[this.editedIndex];
this.item_edition("delete", item).then(response => {
this.data.items.splice(this.data.items.indexOf(item), 1)
Swal.fire({title: "{{_('Item successfully deleted!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
});
this.closeDelete()
},
close () {
this.dialog = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
closeDelete () {
this.dialogDelete = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
item_edition (method, item) {
// Return a Promise
var self = this;
return new Promise((resolve) => {
let url = Urls["items:edit"](item.id)
if (item.id == undefined || item.id == null) {
url = Urls["items:create"]()
}
let promises = self.data.items_encrypted.map(field => {
// Encrypt all necessary fields
if (item[field] == null) {
return null;
}
return new Promise((resolve) => {
return encryptWithKey(self.crypto_key, item[field]).then(enc => {
resolve({field: field, value: enc});
});
});
}).filter(e => e != null);
Promise.all(promises).then(values => {
values.forEach(value => {
item[value.field] = value.value;
});
self.$http[method](url, item).then(response => {
resolve(response.data);
}).catch(err => {
let msg = "{{_('Error during edition of item') | escapejs}}";
if (method == "delete") {
msg = "{{_('Error during deletion of item') | escapejs}}";
}
Swal.fire({title: msg, icon: "error", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
});
});
});
},
save () {
if (this.editedIndex > -1) {
var self = this;
this.item_edition("post", this.editedItem).then(data => {
self.data.items.splice(this.data.items.indexOf(self.editedItem), 1)
console.log('pre edit', data.item)
new_item = self.decryptItem(data.item);
console.log('edited item', new_item);
// self.data.items.push(self.decryptItem(data.item));
Swal.fire({title: "{{_('Item successfully edited') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
});
} else {
var self = this;
this.item_edition("post", this.editedItem).then(data => {
new_item = self.decryptItem(data.item);
console.log('new item', new_item);
// self.data.items.push(self.decryptItem(data.item));
// console.log(data.item);
Swal.fire({title: "{{_('Item successfully created!') | escapejs}}", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
});
}
this.close()
},
}
}

View File

@ -1,5 +1,5 @@
from django.urls import path
from items.views import item as item_view
from items.views import item_view, type_view
app_name = "items"
@ -7,4 +7,6 @@ urlpatterns = [
path("", item_view.item_list, name="list"),
path("<uuid:id>", item_view.item_edit, name="edit"),
path("create", item_view.item_edit, {"id": None}, name="create"),
path("type/<uuid:id>", type_view.type_edit, name="type.edit"),
path("type/create", type_view.type_edit, {"id": None}, name="type.create"),
]

View File

@ -1,6 +1,6 @@
import json
from app.utils.api.api_list import encrypted_fields, header_for_table
from app.utils.api.api_list import header_for_table
from django.contrib.auth.decorators import login_required
from django.db import models
from django.db.models.fields.related import RelatedField
@ -12,18 +12,15 @@ from items.models import Item, ItemType
def item_list(request):
items = Item.objects.filter(author=request.user.setting)
types = ItemType.objects.filter(author=request.user.setting)
return JsonResponse(
{
"result": {
"items": list(items.serialize()),
"types": list(types.serialize()),
"items_headers": header_for_table(Item),
"types": list(types.serialize()),
"types_headers": header_for_table(ItemType),
"items_encrypted": encrypted_fields(Item),
"types_encrypted": encrypted_fields(ItemType),
},
"count": items.count(),
}
@ -85,6 +82,6 @@ def item_edit(request, id=None):
return JsonResponse(
{
"item": Item.objects.filter(id=item.id).serialize().first(),
"object": Item.objects.filter(id=item.id).serialize().first(),
}
)

View File

@ -0,0 +1,67 @@
import json
from django.contrib.auth.decorators import login_required
from django.db import models
from django.db.models.fields.related import RelatedField
from django.http import JsonResponse
from items.models import ItemType
@login_required
def type_edit(request, id=None):
"""Create/edit type view."""
if id:
item = ItemType.objects.filter(id=id, author=request.user.setting).first()
else:
item = ItemType(author=request.user.setting)
if not item:
return JsonResponse({}, status=404)
if request.method == "DELETE":
try:
item.delete()
except Exception:
return JsonResponse({"error": "INVALID_DELETE"}, status=401)
return JsonResponse({})
if request.method != "POST":
return JsonResponse({}, status=405)
try:
data = json.loads(request.body)
except Exception:
return JsonResponse({"error": "INVALID_DATA"}, status=401)
for field in item._meta.fields:
if field.name in item.Serialization.excluded_fields_edit:
continue
if isinstance(field, RelatedField):
# For now, disregard related field (fk, m2m, 1-1)
if isinstance(field, models.ForeignKey):
setattr(item, f"{field.name}_id", data[field.name])
continue
if field.name not in data:
continue
setattr(item, field.name, data[field.name])
try:
item.save()
except Exception:
return JsonResponse({"error": "DATA_INVALID"}, status=401)
return JsonResponse(
{
"object": ItemType.objects.filter(id=item.id).serialize().first(),
}
)

View File

@ -1,5 +1,5 @@
encryption_testing = {
template: "#encryption-testing",
EncryptionTesting = {
template: "#EncryptionTesting",
props: ["crypto_key"],
data: function() {
return {

View File

@ -1,7 +1,7 @@
const rvalidate = Vue.resource(Urls["users:k356.validate"]);
k356_loading = {
template: "#k356-loading",
Loading = {
template: "#Loading",
props: ["crypto_key"],
data: function() {

View File

@ -1,10 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans "Home" %}{% endblock %}
{% block body %}
<k356-loading></k356-loading>
{% endblock %}

View File

View File

@ -0,0 +1,9 @@
from django import template
register = template.Library()
@register.simple_tag(takes_context=True)
def define(context, key, value):
context.dicts[0][key] = value
return ""

View File

@ -11,9 +11,7 @@
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/vue-resource/0.6.1/vue-resource.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/vue-resource@1.5.3"></script>
<script src="https://unpkg.com/htmx.org@2.0.2"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
@ -28,14 +26,22 @@
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://fonts.cdnfonts.com/css/jetbrains-mono" rel="stylesheet">
<style>
.font {
font-family: 'JetBrains Mono', sans-serif;
}
</style>
</head>
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<v-app>
<body>
<v-app class="font">
<div id="main" data-app>
<nav class="navbar navbar-expand-lg bg-primary" data-bs-theme="dark">
<div class="container-fluid">
<a href="#" @click="lock_me" class="navbar-brand">{% trans "Lock" %}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
@ -49,13 +55,13 @@
</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/item_list">{% trans "Items" %}</router-link>
<router-link class="nav-link" to="/ItemView">{% trans "Items" %}</router-link>
</li>
<li class="nav-item">
<a class="nav-link" href="#">{% trans "Properties" %}</a>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/encryption-testing">{% trans "Encryption" %}</router-link>
<router-link class="nav-link" to="/EncryptionTesting">{% trans "Encryption" %}</router-link>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{% trans "More" %}</a>
@ -77,7 +83,7 @@
</nav>
<div id="app" class="container">
<k356-loading :crypto_key="key" @update_key="update_key" v-if="locked"></k356-loading>
<Loading :crypto_key="key" @update_key="update_key" v-if="locked"></Loading>
<template v-if="!locked">
<router-view :crypto_key="key"></router-view>
</template>
@ -101,17 +107,17 @@
Vue.config.delimiters = ["[[", "]]"];
{% for name, value in components.items %}
{% include value.path %}
Vue.component("{{ name }}", {{ value.flat_name }});
{% for name, path in components.items %}
{% include path %}
Vue.component("{{ name }}", {{ name }});
{% endfor %}
const routes = [
{ path: '/', component: null },
{% for name, value in components.items %}
{% for name, path in components.items %}
{
path: "/{{ name }}",
component: {{ value.flat_name }},
component: {{ name }},
},
{% endfor %}
];
@ -131,7 +137,6 @@
},
methods: {
update_key: function(key) {
console.log(key);
this.key.key = key;
this.locked = key == null;
},
@ -143,9 +148,9 @@
}
});
router.beforeEach((to, from) => {
router.beforeEach((to, from, next) => {
// Prevent from routing if key is not present.
return approuter.key.key != null;
next(approuter.key.key != null);
});
</script>
@ -159,5 +164,16 @@
refresh_csrftoken();
</script>
<script type="module">
import { useDate } from './vuetify.js'
const date = useDate()
const formatted = date.format('2010-04-13', 'fullDateWithWeekday')
console.log(formatted) // Tuesday, April 13, 2010
</script>
</body>
</html>

View File

@ -0,0 +1,104 @@
{% load i18n %}
{% block component %}
<v-data-table
:headers="items_headers"
:items="items"
:items-per-page="50"
:search="search"
:group-by="group_by"
loading
dense>
<template v-slot:top>
<v-toolbar flat>
<v-text-field v-model="search" append-icon="mdi-magnify" label="Search" single-line hide-details></v-text-field>
<v-divider class="mx-4" insert vertical></v-divider>
<v-spacer></v-spacer>
<v-dialog v-model="dialog" max-width="500px">
<template v-slot:activator="{ on, attrs }">
<v-btn color="primary" dark class="mb-2" v-bind="attrs" v-on="on">
<v-icon small class="mr-2">mdi-plus</v-icon>
{% trans "New" %}
</v-btn>
</template>
<v-card>
<v-card-title>
<span class="text-h5"> [[ editedItem.id ]]</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<template v-for="field in editable_fields">
<template v-if="field.field_widget == 'v-select'">
<v-select
v-model="editedItem[field.value]"
:items="items_relations[field.value]"
:label="field.text"
item-text="name"
item-value="id"
persistent-hint>
<template slot="item" slot-scope="data">
[[ data.item.name ]] - [[ data.item.custom_identifier ]]
</template>
</v-select>
</template>
<template v-else>
<component :is="field.field_widget" v-model="editedItem[field.value]" :label="field.text"></component>
</template>
</template>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="close">
<v-icon small class="mr-2">mdi-cancel</v-icon>
{% trans "Cancel" %}
</v-btn>
<v-btn color="blue darken-1" text @click="save">
<v-icon small class="mr-2">mdi-content-save-edit</v-icon>
{% trans "Save" %}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="dialogDelete" max-width="500px">
<v-card>
<v-card-title class="text-h5">{% trans "Are you sure you want to delete this item?" %}</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue darken-1" text @click="closeDelete">
<v-icon small class="mr-2">mdi-cancel</v-icon>
{% trans "Cancel" %}
</v-btn>
<v-btn color="blue darken-1" text @click="deleteItemConfirm">
<v-icon small class="mr-2">mdi-delete-forever</v-icon>
{% trans "OK" %}
</v-btn>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
</v-toolbar>
</template>
<template v-slot:item.id="{ item }">
[[ item.id.slice(0, 8) ]]...
</template>
<template v-slot:item.actions="{ item }">
<v-icon small class="mr-2" @click="editItem(item)">mdi-pencil</v-icon>
<v-icon small @click="deleteItem(item)">mdi-delete</v-icon>
</template>
<template v-slot:no-data>
<v-btn color="primary" @click="initialize">{% trans "Reset" %}</v-btn>
</template>
</v-data-table>
{% endblock %}

View File

@ -0,0 +1,87 @@
{% block component %}
{{ name }} = {
template: "#{{ name }}",
delimiters: ["[[", "]]"],
props: ["crypto_key", "items", "items_headers", "items_relations", "group_by"],
data: function() {
return {
dialog: false,
dialogDelete: false,
editedIndex: -1,
defaultItem: {},
editedItem: {},
search: null,
}
},
computed: {
editable_fields: function() {
return this.items_headers.filter(e => e.editable)
},
},
watch: {
dialog (val) {
val || this.close()
},
dialogDelete (val) {
val || this.closeDelete()
},
},
methods: {
editItem (item) {
this.editedIndex = this.items.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialog = true
},
deleteItem (item) {
this.editedIndex = this.items.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialogDelete = true
},
deleteItemConfirm () {
var self = this
var item = this.items[this.editedIndex]
this.$emit("deleteItem", this.editedIndex)
this.closeDelete()
},
close () {
this.dialog = false
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
},
closeDelete () {
this.dialogDelete = false
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
},
save () {
if (this.editedIndex > -1) {
this.$emit("editItem", this.editedIndex, this.editedItem)
} else {
console.log('createItem emit', this.editedItem)
this.$emit("createItem", this.editedItem)
}
this.close()
},
}
}
{% endblock %}