started migrating file editor page to generic model list

This commit is contained in:
vabene1111 2021-10-13 15:03:59 +02:00
parent 38c0d01dc1
commit 1b1945d954
11 changed files with 247 additions and 87 deletions

View File

@ -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):

View File

@ -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('_', '-')

View File

@ -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

View File

@ -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:

View File

@ -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:

View 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>

View 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>

View File

@ -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__

View File

@ -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",

View File

@ -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': ''
},
}
},
}
} }

View File

@ -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() {