mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-24 02:39:20 -05:00
completed migration of file view to generic model list
This commit is contained in:
@@ -179,6 +179,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'list_user_file' %}" class="p-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
<div class="card-body text-center p-0 no-gutters">
|
||||
<i class="fas fa-file fa-2x"></i>
|
||||
</div>
|
||||
<div class="card-body text-break text-center p-0 no-gutters text-muted">
|
||||
{% trans 'Files' %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<a href="{% url 'data_batch_edit' %}" class="p-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
@@ -203,6 +215,9 @@
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row m-0">
|
||||
<div class="col-4">
|
||||
<a href="{% url 'view_export' %}" class="p-1">
|
||||
<div class="card p-0 no-gutters border-0">
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}{% trans 'Files' %}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="{% static 'css/vue-multiselect-bs4.min.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="app" >
|
||||
<user-file-view></user-file-view>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block script %}
|
||||
{% if debug %}
|
||||
<script src="{% url 'js_reverse' %}"></script>
|
||||
{% else %}
|
||||
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||
{% endif %}
|
||||
|
||||
<script type="application/javascript">
|
||||
|
||||
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||
|
||||
window.CURRENT_FILE_SIZE_MB = {{ current_file_size_mb|unlocalize }}
|
||||
window.MAX_FILE_SIZE_MB = {{ max_file_size_mb|unlocalize }}
|
||||
</script>
|
||||
|
||||
{% render_bundle 'user_file_view' %}
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% load l10n %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
@@ -7,7 +8,7 @@
|
||||
|
||||
{% block content_fluid %}
|
||||
|
||||
<div id="app" >
|
||||
<div id="app">
|
||||
<model-list-view></model-list-view>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +18,7 @@
|
||||
|
||||
{% block script %}
|
||||
{{ config | json_script:"model_config" }}
|
||||
|
||||
|
||||
{% if debug %}
|
||||
<script src="{% url 'js_reverse' %}"></script>
|
||||
{% else %}
|
||||
@@ -26,6 +27,11 @@
|
||||
|
||||
<script type="application/javascript">
|
||||
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
|
||||
|
||||
{% if current_file_size_mb %}
|
||||
window.CURRENT_FILE_SIZE_MB = {{ current_file_size_mb|unlocalize }}
|
||||
window.MAX_FILE_SIZE_MB = {{ max_file_size_mb|unlocalize }}
|
||||
{% endif %}
|
||||
</script>
|
||||
|
||||
{% render_bundle 'model_list_view' %}
|
||||
|
||||
@@ -67,7 +67,6 @@ urlpatterns = [
|
||||
path('settings/', views.user_settings, name='view_settings'),
|
||||
path('history/', views.history, name='view_history'),
|
||||
path('supermarket/', views.supermarket, name='view_supermarket'),
|
||||
path('files/', views.files, name='view_files'),
|
||||
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
|
||||
path('test/', views.test, name='view_test'),
|
||||
path('test2/', views.test2, name='view_test2'),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.db.models import Q
|
||||
from django.db.models import Q, Sum
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import gettext as _
|
||||
from django_tables2 import RequestConfig
|
||||
@@ -8,7 +8,7 @@ from django_tables2 import RequestConfig
|
||||
from cookbook.filters import ShoppingListFilter
|
||||
from cookbook.helper.permission_helper import group_required
|
||||
from cookbook.models import (InviteLink, RecipeImport,
|
||||
ShoppingList, Storage, SyncLog)
|
||||
ShoppingList, Storage, SyncLog, UserFile)
|
||||
from cookbook.tables import (ImportLogTable, InviteLinkTable,
|
||||
RecipeImportTable, ShoppingListTable, StorageTable)
|
||||
|
||||
@@ -202,6 +202,12 @@ def automation(request):
|
||||
|
||||
@group_required('user')
|
||||
def user_file(request):
|
||||
try:
|
||||
current_file_size_mb = UserFile.objects.filter(space=request.space).aggregate(Sum('file_size_kb'))[
|
||||
'file_size_kb__sum'] / 1000
|
||||
except TypeError:
|
||||
current_file_size_mb = 0
|
||||
|
||||
return render(
|
||||
request,
|
||||
'generic/model_template.html',
|
||||
@@ -209,7 +215,8 @@ def user_file(request):
|
||||
"title": _("Files"),
|
||||
"config": {
|
||||
'model': "USERFILE", # *REQUIRED* name of the model in models.js
|
||||
}
|
||||
},
|
||||
'current_file_size_mb': current_file_size_mb, 'max_file_size_mb': request.space.max_file_storage_mb
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -230,17 +230,6 @@ def supermarket(request):
|
||||
return render(request, 'supermarket.html', {})
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def files(request):
|
||||
try:
|
||||
current_file_size_mb = UserFile.objects.filter(space=request.space).aggregate(Sum('file_size_kb'))[
|
||||
'file_size_kb__sum'] / 1000
|
||||
except TypeError:
|
||||
current_file_size_mb = 0
|
||||
return render(request, 'files.html',
|
||||
{'current_file_size_mb': current_file_size_mb, 'max_file_size_mb': request.space.max_file_storage_mb})
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def meal_plan_entry(request, pk):
|
||||
plan = MealPlan.objects.filter(space=request.space).get(pk=pk)
|
||||
|
||||
@@ -15,12 +15,10 @@
|
||||
<div class="col-xl-8 col-12">
|
||||
<div class="container-fluid d-flex flex-column flex-grow-1">
|
||||
|
||||
<div class="row" v-if="this_model === Models.AUTOMATION">
|
||||
<!-- dynamically loaded header components -->
|
||||
<div class="row" v-if="header_component_name !== ''">
|
||||
<div class="col-md-12">
|
||||
<b-alert show variant="warning">
|
||||
<b-badge>BETA</b-badge>
|
||||
{{ $t('warning_feature_beta') }}
|
||||
</b-alert>
|
||||
<component :is="headerComponent"></component>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,6 +107,7 @@ import GenericHorizontalCard from "@/components/GenericHorizontalCard";
|
||||
import GenericModalForm from "@/components/Modals/GenericModalForm";
|
||||
import ModelMenu from "@/components/ModelMenu";
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
//import StorageQuota from "@/components/StorageQuota";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
@@ -117,7 +116,9 @@ export default {
|
||||
// or i'm capturing it incorrectly
|
||||
name: 'ModelListView',
|
||||
mixins: [CardMixin, ApiMixin, ToastMixin],
|
||||
components: {GenericHorizontalCard, GenericModalForm, GenericInfiniteCards, ModelMenu},
|
||||
components: {
|
||||
GenericHorizontalCard, GenericModalForm, GenericInfiniteCards, ModelMenu,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// this.Models and this.Actions inherited from ApiMixin
|
||||
@@ -134,6 +135,12 @@ export default {
|
||||
show_modal: false,
|
||||
show_split: false,
|
||||
paginated: false,
|
||||
header_component_name: undefined,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
headerComponent() {
|
||||
return () => import(`@/components/${this.header_component_name}`)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -142,6 +149,7 @@ export default {
|
||||
this.this_model = this.Models[model_config?.model]
|
||||
this.this_recipe_param = model_config?.recipe_param
|
||||
this.paginated = this.this_model?.paginated ?? false
|
||||
this.header_component_name = this.this_model?.list?.header_component?.name ?? undefined
|
||||
this.$nextTick(() => {
|
||||
if (!this.paginated) {
|
||||
this.getItems({page:1},'left')
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
<template>
|
||||
|
||||
<div id="app">
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<h3>{{ $t('Files') }} <span class="float-right"><file-editor @change="loadInitial()"></file-editor></span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 2vh">
|
||||
<div class="col col-md-12">
|
||||
<b-progress :max="max_file_size_mb">
|
||||
<b-progress-bar :value="current_file_size_mb">
|
||||
<span><strong class="text-dark ">{{ current_file_size_mb.toFixed(2) }} / {{ max_file_size_mb }} MB</strong></span>
|
||||
</b-progress-bar>
|
||||
</b-progress>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 2vh">
|
||||
<div class="col col-md-12">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('Name') }}</th>
|
||||
<th>{{ $t('Size') }} (MB)</th>
|
||||
<th>{{ $t('Download') }}</th>
|
||||
<th>{{ $t('Edit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr v-for="f in files" v-bind:key="f.id">
|
||||
<td>{{ f.name }}</td>
|
||||
<td>{{ f.file_size_kb / 1000 }}</td>
|
||||
<td><a :href="f.file" target="_blank" rel="noreferrer nofollow">{{ $t('Download') }}</a></td>
|
||||
<td>
|
||||
|
||||
<file-editor @change="loadInitial()" :file_id="f.id"></file-editor>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
|
||||
import {ResolveUrlMixin, ToastMixin} from "@/utils/utils";
|
||||
|
||||
|
||||
import {ApiApiFactory} from "@/utils/openapi/api.ts";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
// import draggable from 'vuedraggable'
|
||||
|
||||
import axios from 'axios'
|
||||
import FileEditor from "@/components/FileEditor";
|
||||
// import Multiselect from "vue-multiselect";
|
||||
|
||||
axios.defaults.xsrfHeaderName = 'X-CSRFToken'
|
||||
axios.defaults.xsrfCookieName = 'csrftoken'
|
||||
|
||||
export default {
|
||||
name: 'UserFileView',
|
||||
mixins: [
|
||||
ResolveUrlMixin,
|
||||
ToastMixin,
|
||||
],
|
||||
components: {
|
||||
FileEditor
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
|
||||
current_file_size_mb: window.CURRENT_FILE_SIZE_MB,
|
||||
max_file_size_mb: window.MAX_FILE_SIZE_MB
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
this.loadInitial()
|
||||
},
|
||||
methods: {
|
||||
loadInitial: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.listUserFiles().then(results => {
|
||||
this.files = results.data
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,10 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import App from './UserFileView.vue'
|
||||
import i18n from '@/i18n'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
i18n,
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
||||
16
vue/src/components/BetaWarning.vue
Normal file
16
vue/src/components/BetaWarning.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<b-alert show variant="warning">
|
||||
<b-badge>BETA</b-badge>
|
||||
{{ $t('warning_feature_beta') }}
|
||||
</b-alert>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "BetaWarning"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -28,7 +28,11 @@
|
||||
|
||||
<b-dropdown-item :href="resolveDjangoUrl('list_automation')">
|
||||
<i class="fas fa-robot fa-fw"></i> {{ Models['AUTOMATION'].name }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item :href="resolveDjangoUrl('list_user_file')">
|
||||
<i class="fas fa-file fa-fw"></i> {{ Models['USERFILE'].name }}
|
||||
</b-dropdown-item>
|
||||
|
||||
</b-dropdown>
|
||||
</span>
|
||||
|
||||
51
vue/src/components/StorageQuota.vue
Normal file
51
vue/src/components/StorageQuota.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div v-if="max_file_size_mb === -1">
|
||||
<b-alert show variant="warning">
|
||||
{{ $t('file_upload_disabled') }}
|
||||
</b-alert>
|
||||
</div>
|
||||
|
||||
<b-progress :max="progress_max" v-else>
|
||||
<b-progress-bar :value="current_file_size_mb" style="text-align: center">
|
||||
<span><strong class="text-dark ">{{ current_file_size_mb.toFixed(2) }} / {{ display_max }} MB</strong></span>
|
||||
</b-progress-bar>
|
||||
</b-progress>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "StorageQuota",
|
||||
props: {},
|
||||
computed: {
|
||||
progress_max() {
|
||||
if (this.max_file_size_mb === 0) {
|
||||
return this.current_file_size_mb * 4
|
||||
}
|
||||
return this.max_file_size_mb
|
||||
},
|
||||
display_max(){
|
||||
if (this.max_file_size_mb === 0) {
|
||||
return '∞'
|
||||
}
|
||||
return this.max_file_size_mb
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
current_file_size_mb: window.CURRENT_FILE_SIZE_MB,
|
||||
max_file_size_mb: window.MAX_FILE_SIZE_MB
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -8,6 +8,7 @@
|
||||
"success_creating_resource": "Successfully created a resource!",
|
||||
"success_updating_resource": "Successfully updated a resource!",
|
||||
"success_deleting_resource": "Successfully deleted a resource!",
|
||||
"file_upload_disabled": "File upload is not enabled for your space.",
|
||||
"step_time_minutes": "Step time in minutes",
|
||||
"confirm_delete": "Are you sure you want to delete this {object}?",
|
||||
"import_running": "Import running, please wait!",
|
||||
|
||||
@@ -308,6 +308,11 @@ export class Models {
|
||||
'name': i18n.t('Automation'),
|
||||
'apiName': 'Automation',
|
||||
'paginated': true,
|
||||
'list': {
|
||||
'header_component': {
|
||||
'name': 'BetaWarning'
|
||||
},
|
||||
},
|
||||
'create': {
|
||||
'params': [['name', 'description', 'type', 'param_1', 'param_2', 'param_3']],
|
||||
'form': {
|
||||
@@ -407,6 +412,11 @@ export class Models {
|
||||
'name': i18n.t('File'),
|
||||
'apiName': 'UserFile',
|
||||
'paginated': false,
|
||||
'list': {
|
||||
'header_component': {
|
||||
'name': 'StorageQuota'
|
||||
},
|
||||
},
|
||||
'create': {
|
||||
'params': ['name', 'file',],
|
||||
'form': {
|
||||
|
||||
@@ -21,10 +21,6 @@ const pages = {
|
||||
entry: './src/apps/SupermarketView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
},
|
||||
'user_file_view': {
|
||||
entry: './src/apps/UserFileView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
},
|
||||
'model_list_view': {
|
||||
entry: './src/apps/ModelListView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
|
||||
Reference in New Issue
Block a user