started migrating file editor page to generic model list
This commit is contained in:
parent
38c0d01dc1
commit
1b1945d954
@ -146,6 +146,7 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
|
|||||||
class UserFileSerializer(serializers.ModelSerializer):
|
class UserFileSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
def check_file_limit(self, validated_data):
|
def check_file_limit(self, validated_data):
|
||||||
|
if 'file' in validated_data:
|
||||||
if self.context['request'].space.max_file_storage_mb == -1:
|
if self.context['request'].space.max_file_storage_mb == -1:
|
||||||
raise ValidationError(_('File uploads are not enabled for this Space.'))
|
raise ValidationError(_('File uploads are not enabled for this Space.'))
|
||||||
|
|
||||||
@ -156,10 +157,8 @@ class UserFileSerializer(serializers.ModelSerializer):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
current_file_size_mb = 0
|
current_file_size_mb = 0
|
||||||
|
|
||||||
if (
|
if ((validated_data['file'].size / 1000 / 1000 + current_file_size_mb - 5)
|
||||||
(validated_data['file'].size / 1000 / 1000 + current_file_size_mb - 5)
|
> self.context['request'].space.max_file_storage_mb != 0):
|
||||||
> self.context['request'].space.max_file_storage_mb != 0
|
|
||||||
):
|
|
||||||
raise ValidationError(_('You have reached your file upload limit.'))
|
raise ValidationError(_('You have reached your file upload limit.'))
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
|
@ -10,7 +10,7 @@ from cookbook.helper import dal
|
|||||||
|
|
||||||
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
|
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
|
||||||
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList,
|
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList,
|
||||||
Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, get_model_name, Automation)
|
Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, get_model_name, Automation, UserFile)
|
||||||
from .views import api, data, delete, edit, import_export, lists, new, views, telegram
|
from .views import api, data, delete, edit, import_export, lists, new, views, telegram
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
@ -179,7 +179,7 @@ for m in generic_models:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory, Automation]
|
vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory, Automation, UserFile]
|
||||||
for m in vue_models:
|
for m in vue_models:
|
||||||
py_name = get_model_name(m)
|
py_name = get_model_name(m)
|
||||||
url_name = py_name.replace('_', '-')
|
url_name = py_name.replace('_', '-')
|
||||||
|
@ -200,6 +200,20 @@ def automation(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@group_required('user')
|
||||||
|
def user_file(request):
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'generic/model_template.html',
|
||||||
|
{
|
||||||
|
"title": _("Files"),
|
||||||
|
"config": {
|
||||||
|
'model': "USERFILE", # *REQUIRED* name of the model in models.js
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
def shopping_list_new(request):
|
def shopping_list_new(request):
|
||||||
# recipe-param is the name of the parameters used when filtering recipes by this attribute
|
# recipe-param is the name of the parameters used when filtering recipes by this attribute
|
||||||
|
@ -121,7 +121,6 @@ export default {
|
|||||||
case this.Actions.UPDATE:
|
case this.Actions.UPDATE:
|
||||||
update = e.form_data
|
update = e.form_data
|
||||||
update.id = this.this_item.id
|
update.id = this.this_item.id
|
||||||
console.log('form', update)
|
|
||||||
this.saveThis(update)
|
this.saveThis(update)
|
||||||
break;
|
break;
|
||||||
case this.Actions.MERGE:
|
case this.Actions.MERGE:
|
||||||
|
@ -237,7 +237,6 @@ export default {
|
|||||||
case this.Actions.UPDATE:
|
case this.Actions.UPDATE:
|
||||||
update = e.form_data
|
update = e.form_data
|
||||||
update.id = this.this_item.id
|
update.id = this.this_item.id
|
||||||
console.log('form', update)
|
|
||||||
this.saveThis(update)
|
this.saveThis(update)
|
||||||
break;
|
break;
|
||||||
case this.Actions.MERGE:
|
case this.Actions.MERGE:
|
||||||
|
39
vue/src/components/FileViewer.vue
Normal file
39
vue/src/components/FileViewer.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div v-if="isImage()">
|
||||||
|
<b-img :src="url" :alt="$t('Image')" class="w-100"></b-img>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="url.toLowerCase().includes('.pdf')">
|
||||||
|
<iframe :src="`/static/pdfjs/viewer.html?file=${url}`" class="w-100" style="border: none; height: 30vh"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="h-20 w-100 border border-primary rounded text-center">
|
||||||
|
<i class="fas fa-eye-slash fa-2x text-primary mt-2"></i>
|
||||||
|
<br/>
|
||||||
|
<a :href="url" target="_blank" rel="noreferrer nofollow" class="mt-4">{{$t('Download')}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FileViewer',
|
||||||
|
props: {
|
||||||
|
url: String,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isImage: function (){
|
||||||
|
let cleaned_url = this.url.toLowerCase()
|
||||||
|
return (cleaned_url.endsWith('.png') || cleaned_url.endsWith('.jpg') || cleaned_url.endsWith('.jpeg') || cleaned_url.endsWith('.gif'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
60
vue/src/components/Modals/FileInput.vue
Normal file
60
vue/src/components/Modals/FileInput.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<b-form-group
|
||||||
|
v-bind:label="label"
|
||||||
|
class="mb-3">
|
||||||
|
<b-form-file
|
||||||
|
v-model="new_value"
|
||||||
|
type="file"
|
||||||
|
:placeholder="placeholder_text"
|
||||||
|
></b-form-file>
|
||||||
|
|
||||||
|
|
||||||
|
</b-form-group>
|
||||||
|
|
||||||
|
<file-viewer :url="preview_url" v-if="preview_url"></file-viewer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import FileViewer from "@/components/FileViewer";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "FileInput",
|
||||||
|
components: {FileViewer},
|
||||||
|
props: {
|
||||||
|
field: {type: String, default: 'You Forgot To Set Field Name'},
|
||||||
|
label: {type: String, default: 'Text Field'},
|
||||||
|
value: {type: String, default: ''},
|
||||||
|
placeholder: {type: String, default: ''},
|
||||||
|
show_merge: {type: Boolean, default: false},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
new_value: undefined,
|
||||||
|
placeholder_text: this.placeholder,
|
||||||
|
preview_url: this.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.placeholder_text === '') {
|
||||||
|
this.placeholder_text = this.$t('Select_File')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'new_value': function () {
|
||||||
|
if (!(typeof this.new_value === 'string' || this.new_value instanceof String)) { // only update file if it was changed
|
||||||
|
this.preview_url = URL.createObjectURL(this.new_value) //TODO improve creating obj url so file viewer know which type to display
|
||||||
|
this.$root.$emit('change', this.field, this.new_value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<b-modal :id="'modal_'+id" @hidden="cancelAction">
|
<b-modal :id="'modal_'+id" @hidden="cancelAction">
|
||||||
<template v-slot:modal-title><h4>{{form.title}}</h4></template>
|
<template v-slot:modal-title><h4>{{ form.title }}</h4></template>
|
||||||
<div v-for="(f, i) in form.fields" v-bind:key=i>
|
<div v-for="(f, i) in form.fields" v-bind:key=i>
|
||||||
<p v-if="f.type=='instruction'">{{f.label}}</p>
|
<p v-if="f.type=='instruction'">{{ f.label }}</p>
|
||||||
<!-- this lookup is single selection -->
|
<!-- this lookup is single selection -->
|
||||||
<lookup-input v-if="f.type=='lookup'"
|
<lookup-input v-if="f.type=='lookup'"
|
||||||
:form="f"
|
:form="f"
|
||||||
@ -30,11 +30,16 @@
|
|||||||
:value="f.value"
|
:value="f.value"
|
||||||
:field="f.field"
|
:field="f.field"
|
||||||
@change="storeValue"/>
|
@change="storeValue"/>
|
||||||
|
<file-input v-if="f.type=='file'"
|
||||||
|
:label="f.label"
|
||||||
|
:value="f.value"
|
||||||
|
:field="f.field"
|
||||||
|
@change="storeValue"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-slot:modal-footer>
|
<template v-slot:modal-footer>
|
||||||
<b-button class="float-right mx-1" variant="secondary" v-on:click="cancelAction">{{$t('Cancel')}}</b-button>
|
<b-button class="float-right mx-1" variant="secondary" v-on:click="cancelAction">{{ $t('Cancel') }}</b-button>
|
||||||
<b-button class="float-right mx-1" variant="primary" v-on:click="doAction">{{form.ok_label}}</b-button>
|
<b-button class="float-right mx-1" variant="primary" v-on:click="doAction">{{ form.ok_label }}</b-button>
|
||||||
</template>
|
</template>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
</div>
|
</div>
|
||||||
@ -44,6 +49,7 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import {BootstrapVue} from 'bootstrap-vue'
|
import {BootstrapVue} from 'bootstrap-vue'
|
||||||
import {getForm} from "@/utils/utils";
|
import {getForm} from "@/utils/utils";
|
||||||
|
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
import {Models} from "@/utils/models";
|
import {Models} from "@/utils/models";
|
||||||
@ -52,15 +58,24 @@ import LookupInput from "@/components/Modals/LookupInput";
|
|||||||
import TextInput from "@/components/Modals/TextInput";
|
import TextInput from "@/components/Modals/TextInput";
|
||||||
import EmojiInput from "@/components/Modals/EmojiInput";
|
import EmojiInput from "@/components/Modals/EmojiInput";
|
||||||
import ChoiceInput from "@/components/Modals/ChoiceInput";
|
import ChoiceInput from "@/components/Modals/ChoiceInput";
|
||||||
|
import FileInput from "@/components/Modals/FileInput";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'GenericModalForm',
|
name: 'GenericModalForm',
|
||||||
components: {CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput},
|
components: {FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput},
|
||||||
props: {
|
props: {
|
||||||
model: {required: true, type: Object},
|
model: {required: true, type: Object},
|
||||||
action: {required: true, type: Object},
|
action: {required: true, type: Object},
|
||||||
item1: {type: Object, default () {return undefined}},
|
item1: {
|
||||||
item2: {type: Object, default () {return undefined}},
|
type: Object, default() {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
item2: {
|
||||||
|
type: Object, default() {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
show: {required: true, type: Boolean, default: false},
|
show: {required: true, type: Boolean, default: false},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -95,27 +110,27 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
doAction: function(){
|
doAction: function () {
|
||||||
this.dirty = false
|
this.dirty = false
|
||||||
this.$emit('finish-action', {'form_data': this.detectOverride(this.form_data) })
|
this.$emit('finish-action', {'form_data': this.detectOverride(this.form_data)})
|
||||||
},
|
},
|
||||||
cancelAction: function() {
|
cancelAction: function () {
|
||||||
if (this.dirty) {
|
if (this.dirty) {
|
||||||
this.dirty = false
|
this.dirty = false
|
||||||
this.$emit('finish-action', 'cancel')
|
this.$emit('finish-action', 'cancel')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
storeValue: function(field, value) {
|
storeValue: function (field, value) {
|
||||||
this.form_data[field] = value
|
this.form_data[field] = value
|
||||||
},
|
},
|
||||||
listModel: function(m) {
|
listModel: function (m) {
|
||||||
if (m === 'self') {
|
if (m === 'self') {
|
||||||
return this.model
|
return this.model
|
||||||
} else {
|
} else {
|
||||||
return Models[m]
|
return Models[m]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
detectOverride: function(form) {
|
detectOverride: function (form) {
|
||||||
for (const [k, v] of Object.entries(form)) {
|
for (const [k, v] of Object.entries(form)) {
|
||||||
if (form[k].__override__) {
|
if (form[k].__override__) {
|
||||||
form[k] = form[k].__override__
|
form[k] = form[k].__override__
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
"Manage_Books": "Manage Books",
|
"Manage_Books": "Manage Books",
|
||||||
"Meal_Plan": "Meal Plan",
|
"Meal_Plan": "Meal Plan",
|
||||||
"Select_Book": "Select Book",
|
"Select_Book": "Select Book",
|
||||||
|
"Select_File": "Select File",
|
||||||
"Recipe_Image": "Recipe Image",
|
"Recipe_Image": "Recipe Image",
|
||||||
"Import_finished": "Import finished",
|
"Import_finished": "Import finished",
|
||||||
"View_Recipes": "View Recipes",
|
"View_Recipes": "View Recipes",
|
||||||
@ -92,6 +93,7 @@
|
|||||||
"Files": "Files",
|
"Files": "Files",
|
||||||
"File": "File",
|
"File": "File",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
|
"Image": "Image",
|
||||||
"Delete": "Delete",
|
"Delete": "Delete",
|
||||||
"Open": "Open",
|
"Open": "Open",
|
||||||
"Ok": "Open",
|
"Ok": "Open",
|
||||||
|
@ -113,6 +113,7 @@ export class Models {
|
|||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static KEYWORD = {
|
static KEYWORD = {
|
||||||
'name': i18n.t('Keyword'), // *OPTIONAL: parameters will be built model -> model_type -> default
|
'name': i18n.t('Keyword'), // *OPTIONAL: parameters will be built model -> model_type -> default
|
||||||
'apiName': 'Keyword',
|
'apiName': 'Keyword',
|
||||||
@ -150,6 +151,7 @@ export class Models {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
static UNIT = {
|
static UNIT = {
|
||||||
'name': i18n.t('Unit'),
|
'name': i18n.t('Unit'),
|
||||||
'apiName': 'Unit',
|
'apiName': 'Unit',
|
||||||
@ -175,10 +177,12 @@ export class Models {
|
|||||||
},
|
},
|
||||||
'merge': true
|
'merge': true
|
||||||
}
|
}
|
||||||
|
|
||||||
static SHOPPING_LIST = {
|
static SHOPPING_LIST = {
|
||||||
'name': i18n.t('Shopping_list'),
|
'name': i18n.t('Shopping_list'),
|
||||||
'apiName': 'ShoppingListEntry',
|
'apiName': 'ShoppingListEntry',
|
||||||
}
|
}
|
||||||
|
|
||||||
static RECIPE_BOOK = {
|
static RECIPE_BOOK = {
|
||||||
'name': i18n.t('Recipe_Book'),
|
'name': i18n.t('Recipe_Book'),
|
||||||
'apiName': 'RecipeBook',
|
'apiName': 'RecipeBook',
|
||||||
@ -208,6 +212,7 @@ export class Models {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
static SHOPPING_CATEGORY = {
|
static SHOPPING_CATEGORY = {
|
||||||
'name': i18n.t('Shopping_Category'),
|
'name': i18n.t('Shopping_Category'),
|
||||||
'apiName': 'SupermarketCategory',
|
'apiName': 'SupermarketCategory',
|
||||||
@ -231,6 +236,7 @@ export class Models {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
static SHOPPING_CATEGORY_RELATION = {
|
static SHOPPING_CATEGORY_RELATION = {
|
||||||
'name': i18n.t('Shopping_Category_Relation'),
|
'name': i18n.t('Shopping_Category_Relation'),
|
||||||
'apiName': 'SupermarketCategoryRelation',
|
'apiName': 'SupermarketCategoryRelation',
|
||||||
@ -254,6 +260,7 @@ export class Models {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
static SUPERMARKET = {
|
static SUPERMARKET = {
|
||||||
'name': i18n.t('Supermarket'),
|
'name': i18n.t('Supermarket'),
|
||||||
'apiName': 'Supermarket',
|
'apiName': 'Supermarket',
|
||||||
@ -395,6 +402,31 @@ export class Models {
|
|||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static USERFILE = {
|
||||||
|
'name': i18n.t('File'),
|
||||||
|
'apiName': 'UserFile',
|
||||||
|
'paginated': false,
|
||||||
|
'create': {
|
||||||
|
'params': ['name', 'file',],
|
||||||
|
'form': {
|
||||||
|
'name': {
|
||||||
|
'form_field': true,
|
||||||
|
'type': 'text',
|
||||||
|
'field': 'name',
|
||||||
|
'label': i18n.t('Name'),
|
||||||
|
'placeholder': ''
|
||||||
|
},
|
||||||
|
'file': {
|
||||||
|
'form_field': true,
|
||||||
|
'type': 'file',
|
||||||
|
'field': 'file',
|
||||||
|
'label': i18n.t('File'),
|
||||||
|
'placeholder': ''
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -165,6 +165,7 @@ import axios from "axios";
|
|||||||
axios.defaults.xsrfCookieName = 'csrftoken'
|
axios.defaults.xsrfCookieName = 'csrftoken'
|
||||||
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
|
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
|
||||||
import { Actions, Models } from './models';
|
import { Actions, Models } from './models';
|
||||||
|
import {RequestArgs} from "@/utils/openapi/base";
|
||||||
|
|
||||||
export const ApiMixin = {
|
export const ApiMixin = {
|
||||||
data() {
|
data() {
|
||||||
|
Loading…
Reference in New Issue
Block a user