Fix after rebase

This commit is contained in:
smilerz 2021-11-21 10:59:11 -06:00
parent a24f09c419
commit 2b41fbc9f8
11 changed files with 6118 additions and 33020 deletions

View File

@ -428,8 +428,10 @@ class UserCreateForm(forms.Form):
class SearchPreferenceForm(forms.ModelForm):
prefix = 'search'
trigram_threshold = forms.DecimalField(min_value=0.01, max_value=1, decimal_places=2, widget=NumberInput(attrs={'class': "form-control-range", 'type': 'range'}),
help_text=_('Determines how fuzzy a search is if it uses trigram similarity matching (e.g. low values mean more typos are ignored).'))
trigram_threshold = forms.DecimalField(min_value=0.01, max_value=1, decimal_places=2,
widget=NumberInput(attrs={'class': "form-control-range", 'type': 'range'}),
help_text=_(
'Determines how fuzzy a search is if it uses trigram similarity matching (e.g. low values mean more typos are ignored).'))
preset = forms.CharField(widget=forms.HiddenInput(), required=False)
class Meta:

View File

@ -37,7 +37,8 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
api_serializer = None
# extended values are computationally expensive and not needed in normal circumstances
try:
if bool(int(self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
if bool(int(
self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
return fields
except (AttributeError, KeyError) as e:
pass

View File

@ -48,7 +48,8 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, FoodI
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
from cookbook.schemas import FilterSchema, QueryParam, QueryParamAutoSchema, TreeSchema
from cookbook.schemas import (FilterSchema, QueryOnlySchema, QueryParam, QueryParamAutoSchema,
RecipeSchema, TreeSchema)
from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializer,
CookLogSerializer, FoodInheritFieldSerializer, FoodSerializer,
FoodShoppingUpdateSerializer, ImportLogSerializer,
@ -536,8 +537,7 @@ class StepViewSet(viewsets.ModelViewSet):
permission_classes = [CustomIsUser]
pagination_class = DefaultPagination
query_params = [
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'),
qtype='int'),
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'), qtype='int'),
QueryParam(name='query', description=_('Query string matched (fuzzy) against object name.'), qtype='string'),
]
schema = QueryParamAutoSchema()

27502
vue/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -84,4 +84,14 @@
"@vue/cli-plugin-pwa/workbox-webpack-plugin": "^5.1.3",
"coa": "2.0.2"
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
],
"resolutions": {
"@vue/cli-plugin-pwa/workbox-webpack-plugin": "^5.1.3",
"coa": "2.0.2"
}
}

View File

@ -4,7 +4,7 @@
<div class="row">
<div class="col col-md-12">
<div class="row justify-content-center">
<div class="col-12 col-lg-10 col-xl-8 mt-3 mb-3">
<div class="col-12 col-lg-10 mt-3 mb-3">
<b-input-group>
<b-input
class="form-control form-control-lg form-control-borderless form-control-search"
@ -26,12 +26,12 @@
<div class="row">
<div class="col-md-12">
<b-card class="d-flex flex-column" v-hover v-on:click="openBook(book.id)">
<b-row no-gutters style="height:inherit;">
<b-col no-gutters md="2" style="height:inherit;">
<b-row no-gutters style="height: inherit">
<b-col no-gutters md="2" style="height: inherit">
<h3>{{ book.icon }}</h3>
</b-col>
<b-col no-gutters md="10" style="height:inherit;">
<b-card-body class="m-0 py-0" style="height:inherit;">
<b-col no-gutters md="10" style="height: inherit">
<b-card-body class="m-0 py-0" style="height: inherit">
<b-card-text class="h-100 my-0 d-flex flex-column" style="text-overflow: ellipsis">
<h5 class="m-0 mt-1 text-truncate">
{{ book.name }} <span class="float-right"><i class="fa fa-book"></i></span>
@ -93,7 +93,11 @@ export default {
})
},
},
mounted() {
createNew: function () {
let apiClient = new ApiApiFactory()
apiClient.createRecipeBook({name: this.$t('New_Cookbook'), description: '', icon: '', shared: []}).then(result => {
let new_book = result.data
this.refreshData()
this.$i18n.locale = window.CUSTOM_LOCALE
},
@ -156,8 +160,7 @@ export default {
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active below version 2.1.8 */
{
/* .slide-fade-leave-active below version 2.1.8 */ {
transform: translateX(10px);
opacity: 0;
}

View File

@ -1,54 +1,102 @@
<template>
<div>
<b-tabs content-class="mt-3">
<b-tabs content-class="mt-3" v-model="current_tab">
<b-tab :title="$t('Planner')" active>
<div class="row">
<div class="col-12 calender-parent">
<calendar-view
:show-date="showDate"
:enable-date-selection="true"
class="theme-default"
:show-date="showDate" :enable-date-selection="true" class="theme-default"
:items="plan_items"
:display-period-uom="settings.displayPeriodUom"
:period-changed-callback="periodChangedCallback"
:enable-drag-drop="true"
:period-changed-callback="periodChangedCallback" :enable-drag-drop="true"
:item-content-height="item_height"
@click-date="createEntryClick"
@drop-on-date="moveEntry"
@click-date="createEntryClick" @drop-on-date="moveEntry"
:display-period-count="settings.displayPeriodCount"
:starting-day-of-week="settings.startingDayOfWeek"
:display-week-numbers="settings.displayWeekNumbers"
>
:display-week-numbers="settings.displayWeekNumbers">
<template #item="{ value, weekStartDate, top }">
<meal-plan-card
:value="value"
:week-start-date="weekStartDate"
:top="top"
:detailed="detailed_items"
:item_height="item_height"
@dragstart="dragged_item = value"
@click-item="entryClick"
@open-context-menu="openContextMenu"
/>
<meal-plan-card :value="value" :week-start-date="weekStartDate" :top="top" :detailed="detailed_items"
:item_height="item_height" @dragstart="dragged_item = value" @click-item="entryClick"
@open-context-menu="openContextMenu"/>
</template>
<template #header="{ headerProps }">
<meal-plan-calender-header
ref="header"
<meal-plan-calender-header ref="header"
:header-props="headerProps"
@input="setShowDate"
@delete-dragged="deleteEntry(dragged_item)"
@input="setShowDate" @delete-dragged="deleteEntry(dragged_item)"
@create-new="createEntryClick(new Date())"
@set-starting-day-back="setStartingDay(-1)"
@set-starting-day-forward="setStartingDay(1)"
:i-cal-url="iCalUrl"
@set-starting-day-forward="setStartingDay(1)" :i-cal-url="iCalUrl"
:options="options"
:settings_prop="settings"
/>
:settings_prop="settings"/>
</template>
</calendar-view>
</div>
</div>
</b-tab>
<b-tab :title="$t('Settings')">
<div class="row mt-3">
<div class="col-12 col-md-3 calender-options">
<h5>{{ $t('Planner_Settings') }}</h5>
<b-form>
<b-form-group id="UomInput"
:label="$t('Period')"
:description="$t('Plan_Period_To_Show')"
label-for="UomInput">
<b-form-select
id="UomInput"
v-model="settings.displayPeriodUom"
:options="options.displayPeriodUom"
></b-form-select>
</b-form-group>
<b-form-group id="PeriodInput"
:label="$t('Periods')"
:description="$t('Plan_Show_How_Many_Periods')"
label-for="PeriodInput">
<b-form-select
id="PeriodInput"
v-model="settings.displayPeriodCount"
:options="options.displayPeriodCount"
></b-form-select>
</b-form-group>
<b-form-group id="DaysInput"
:label="$t('Starting_Day')"
:description="$t('Starting_Day')"
label-for="DaysInput">
<b-form-select
id="DaysInput"
v-model="settings.startingDayOfWeek"
:options="dayNames"
></b-form-select>
</b-form-group>
<b-form-group id="WeekNumInput"
:label="$t('Week_Numbers')">
<b-form-checkbox v-model="settings.displayWeekNumbers" name="week_num">
{{ $t('Show_Week_Numbers') }}
</b-form-checkbox>
</b-form-group>
</b-form>
</div>
<div class="col-12 col-md-9 col-lg-6">
<h5>{{ $t('Meal_Types') }}</h5>
<div>
<draggable :list="meal_types" group="meal_types"
:empty-insert-threshold="10" handle=".handle" @sort="sortMealTypes()">
<b-card no-body class="mt-1" v-for="(meal_type, index) in meal_types" v-hover :key="meal_type.id">
<b-card-header class="p-4">
<div class="row">
<div class="col-2 handle">
<button type="button" class="btn btn-lg shadow-none"><i class="fas fa-arrows-alt-v "></i>
</button>
</div>
<div class="col-10">
<h5>{{ meal_type.icon }} {{ meal_type.name }}<span class="float-right text-primary"><i
class="fa" v-bind:class="{ 'fa-pen': !meal_type.editing, 'fa-save': meal_type.editing }"
@click="editOrSaveMealType(index)"
aria-hidden="true"></i></span></h5>
</div>
</div>
</div>
</b-tab>
<b-tab :title="$t('Settings')">
<div class="row mt-3">
<div class="col-3 calender-options">
@ -227,8 +275,33 @@
{{ $t("Export_To_ICal") }}
</a>
</div>
</div>
</b-sidebar>
</div>
</template>
<transition name="slide-fade">
<div class="row fixed-bottom p-2 b-1 border-top text-center" style="background:rgba(255,255,255,0.6);"
v-if="current_tab === 0">
<div class="col-md-3 col-6">
<button class="btn btn-block btn-primary shadow-none disabled" v-b-tooltip.focus.top :title="$t('Coming-Soon')">{{ $t("Auto-Planner") }}</button>
<button class="btn btn-block btn-success shadow-none" @click="createEntryClick(new Date())"><i
class="fas fa-calendar-plus"></i> {{ $t('Create') }}
</button>
</div>
<div class="col-md-3 col-6">
<button class="btn btn-block btn-primary shadow-none" v-b-toggle.sidebar-shopping><i
class="fas fa-shopping-cart"></i> {{ $t('Shopping_list') }}
</button>
</div>
<div class="col-md-3 col-6">
<a class="btn btn-block btn-primary shadow-none" :href="iCalUrl"><i class="fas fa-download"></i>
{{ $t('Export_To_ICal') }}
</a>
</div>
<div class="col-md-3 col-6">
<button class="btn btn-block btn-primary shadow-none disabled" v-b-tooltip.focus.top
:title="$t('Coming_Soon')">
{{ $t('Auto_Planner') }}
</button>
</div>
<div class="col-12 d-flex justify-content-center mt-2 d-block d-md-none">
<b-button-toolbar key-nav aria-label="Toolbar with button groups">
@ -237,8 +310,12 @@
<b-button v-html="'<'" @click="setStartingDay(-1)"></b-button>
</b-button-group>
<b-button-group class="mx-1">
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i class="fas fa-home"></i> </b-button>
<b-form-datepicker button-only button-variant="secondary"></b-form-datepicker>
<b-button @click="setShowDate($refs.header.headerProps.currentPeriod)"><i class="fas fa-home"></i>
</b-button>
<b-form-datepicker
button-only
button-variant="secondary"
></b-form-datepicker>
</b-button-group>
<b-button-group class="mx-1">
<b-button v-html="'>'" @click="setStartingDay(1)"></b-button>
@ -247,6 +324,7 @@
</b-button-toolbar>
</div>
</div>
</transition>
</div>
</template>
@ -288,32 +366,29 @@ export default {
ContextMenuItem,
MealPlanCalenderHeader,
EmojiInput,
draggable,
draggable
},
mixins: [CalendarMathMixin, ApiMixin],
data: function() {
data: function () {
return {
showDate: new Date(),
plan_entries: [],
recipe_viewed: {},
settings: {
displayPeriodUom: "week",
displayPeriodUom: 'week',
displayPeriodCount: 2,
startingDayOfWeek: 1,
displayWeekNumbers: true,
displayWeekNumbers: true
},
dragged_item: null,
current_tab: 0,
meal_types: [],
current_context_menu_item: null,
options: {
displayPeriodUom: [
{ text: this.$t("Week"), value: "week" },
{
text: this.$t("Month"),
value: "month",
},
{ text: this.$t("Year"), value: "year" },
],
displayPeriodUom: [{text: this.$t('Week'), value: 'week'}, {
text: this.$t('Month'),
value: 'month'
}, {text: this.$t('Year'), value: 'year'}],
displayPeriodCount: [1, 2, 3],
entryEditing: {
date: null,
@ -324,17 +399,154 @@ export default {
recipe: null,
servings: 1,
shared: [],
title: "",
title_placeholder: this.$t("Title"),
},
title: '',
title_placeholder: this.$t('Title')
}
},
shopping_list: [],
current_period: null,
entryEditing: {},
edit_modal_show: false,
ical_url: window.ICAL_URL,
ical_url: window.ICAL_URL
}
},
computed: {
modal_title: function () {
if (this.entryEditing.id === -1) {
return this.$t('Create_Meal_Plan_Entry')
} else {
return this.$t('Edit_Meal_Plan_Entry')
}
},
entryEditing_initial_recipe: function () {
if (this.entryEditing.recipe != null) {
return [this.entryEditing.recipe]
} else {
return []
}
},
entryEditing_initial_meal_type: function () {
if (this.entryEditing.meal_type != null) {
return [this.entryEditing.meal_type]
} else {
return []
}
},
plan_items: function () {
let items = []
this.plan_entries.forEach((entry) => {
items.push(this.buildItem(entry))
})
return items
},
detailed_items: function () {
return this.settings.displayPeriodUom === 'week';
},
dayNames: function () {
let options = []
this.getFormattedWeekdayNames(this.userLocale, "long", 0).forEach((day, index) => {
options.push({text: day, value: index})
})
return options
},
userLocale: function () {
return this.getDefaultBrowserLocale
},
item_height: function () {
if (this.settings.displayPeriodUom === 'week') {
return "10rem"
} else {
return "1.6rem"
}
},
iCalUrl() {
if (this.current_period !== null) {
let start = moment(this.current_period.periodStart).format('YYYY-MM-DD')
let end = moment(this.current_period.periodEnd).format('YYYY-MM-DD')
return this.ical_url.replace(/12345/, start).replace(/6789/, end)
} else {
return ""
}
}
},
mounted() {
this.$nextTick(function () {
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
this.settings = Object.assign({}, this.settings, this.$cookies.get(SETTINGS_COOKIE_NAME))
}
})
this.$root.$on('change', this.updateEmoji);
this.$i18n.locale = window.CUSTOM_LOCALE
},
watch: {
settings: {
handler() {
this.$cookies.set(SETTINGS_COOKIE_NAME, this.settings, '360d')
},
deep: true
},
},
methods: {
addToShopping(entry) {
if (entry.originalItem.entry.recipe !== null) {
this.shopping_list.push(entry.originalItem.entry)
makeToast(this.$t("Success"), this.$t("Added_To_Shopping_List"), 'success')
} else {
makeToast(this.$t("Failure"), this.$t("Cannot_Add_Notes_To_Shopping"), 'danger')
}
},
saveShoppingList() {
let url = window.SHOPPING_URL
let first = true
for (let se of this.shopping_list) {
if (first) {
url += `?r=[${se.recipe.id},${se.servings}]`
first = false
} else {
url += `&r=[${se.recipe.id},${se.servings}]`
}
}
window.open(url)
},
setStartingDay(days) {
if (this.settings.startingDayOfWeek + days < 0) {
this.settings.startingDayOfWeek = 6
} else if (this.settings.startingDayOfWeek + days > 6) {
this.settings.startingDayOfWeek = 0
} else {
this.settings.startingDayOfWeek = this.settings.startingDayOfWeek + days
}
},
newMealType() {
let apiClient = new ApiApiFactory()
apiClient.createMealType({name: this.$t('Meal_Type')}).then(e => {
this.periodChangedCallback(this.current_period)
}).catch(error => {
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
})
this.refreshMealTypes()
},
sortMealTypes() {
this.meal_types.forEach(function (element, index) {
element.order = index
});
let updated = 0
this.meal_types.forEach((meal_type) => {
let apiClient = new ApiApiFactory()
apiClient.updateMealType(meal_type.id, meal_type).then(e => {
if (updated === (this.meal_types.length - 1)) {
this.periodChangedCallback(this.current_period)
} else {
updated++
}
}).catch(error => {
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
})
})
},
computed: {
modal_title: function() {
if (this.entryEditing.id === -1) {

View File

@ -55,6 +55,71 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md-9" style="margin-top: 1vh">
<h3>
<!-- <span><b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button></span> -->
<model-menu/>
<span>{{ this.this_model.name }}</span>
<span v-if="this_model.name !== 'Step'"><b-button variant="link" @click="startAction({'action':'new'})"><i
class="fas fa-plus-circle fa-2x"></i></b-button></span><!-- TODO add proper field to model config to determine if create should be available or not -->
</h3>
</div>
<div class="col-md-3" style="position: relative; margin-top: 1vh">
<b-form-checkbox v-model="show_split" name="check-button" v-if="paginated"
class="shadow-none"
style="position:relative;top: 50%; transform: translateY(-50%);" switch>
{{ $t('show_split_screen') }}
</b-form-checkbox>
</div>
</div>
<div class="row">
<div class="col" :class="{'col-md-6' : show_split}">
<!-- model isn't paginated and loads in one API call -->
<div v-if="!paginated">
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id"
:item=i
:model="this_model"
@item-action="startAction($event, 'left')"
@finish-action="finishAction"/>
</div>
<!-- model is paginated and needs managed -->
<generic-infinite-cards v-if="paginated"
:card_counts="left_counts"
:scroll="show_split"
@search="getItems($event, 'left')"
@reset="resetList('left')">
<template v-slot:cards>
<generic-horizontal-card
v-for="i in items_left" v-bind:key="i.id"
:item=i
:model="this_model"
@item-action="startAction($event, 'left')"
@finish-action="finishAction"/>
</template>
</generic-infinite-cards>
</div>
<div class="col col-md-6" v-if="show_split">
<generic-infinite-cards v-if="this_model"
:card_counts="right_counts"
:scroll="show_split"
@search="getItems($event, 'right')"
@reset="resetList('right')">
<template v-slot:cards>
<generic-horizontal-card
v-for="i in items_right" v-bind:key="i.id"
:item=i
:model="this_model"
@item-action="startAction($event, 'right')"
@finish-action="finishAction"/>
</template>
</generic-infinite-cards>
</div>
</div>
</div>
</div>
</template>
@ -78,19 +143,21 @@ Vue.use(BootstrapVue)
export default {
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
// or i'm capturing it incorrectly
name: "ModelListView",
mixins: [CardMixin, ApiMixin],
components: { GenericHorizontalCard, GenericModalForm, GenericInfiniteCards, ModelMenu },
name: 'ModelListView',
mixins: [CardMixin, ApiMixin, ToastMixin],
components: {
GenericHorizontalCard, GenericModalForm, GenericInfiniteCards, ModelMenu,
},
data() {
return {
// this.Models and this.Actions inherited from ApiMixin
items_left: [],
items_right: [],
right_counts: { max: 9999, current: 0 },
left_counts: { max: 9999, current: 0 },
right_counts: {'max': 9999, 'current': 0},
left_counts: {'max': 9999, 'current': 0},
this_model: undefined,
model_menu: undefined,
this_action: {},
this_action: undefined,
this_recipe_param: undefined,
this_item: {},
this_target: {},
@ -100,6 +167,125 @@ export default {
header_component_name: undefined,
}
},
computed: {
headerComponent() {
// TODO this leads webpack to create one .js file for each component in this folder because at runtime any one of them could be requested
// TODO this is not necessarily bad but maybe there are better options to do this
return () => import(/* webpackChunkName: "header-component" */ `@/components/${this.header_component_name}`)
}
},
mounted() {
// value is passed from lists.py
let model_config = JSON.parse(document.getElementById('model_config').textContent)
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')
}
})
this.$i18n.locale = window.CUSTOM_LOCALE
},
methods: {
// this.genericAPI inherited from ApiMixin
resetList: function (e) {
this['items_' + e] = []
this[e + '_counts'].max = 9999 + Math.random()
this[e + '_counts'].current = 0
},
startAction: function (e, param) {
let source = e?.source ?? {}
let target = e?.target ?? undefined
this.this_item = source
this.this_target = target
switch (e.action) {
case 'delete':
this.this_action = this.Actions.DELETE
this.show_modal = true
break;
case 'new':
this.this_action = this.Actions.CREATE
this.show_modal = true
break;
case 'edit':
this.this_item = e.source
this.this_action = this.Actions.UPDATE
this.show_modal = true
break;
case 'move':
if (target == null) {
this.this_item = e.source
this.this_action = this.Actions.MOVE
this.show_modal = true
} else {
this.moveThis(source.id, target.id)
}
break;
case 'merge':
if (target == null) {
this.this_item = e.source
this.this_action = this.Actions.MERGE
this.show_modal = true
} else {
this.mergeThis(e.source, e.target, false)
}
break;
case 'merge-automate':
if (target == null) {
this.this_item = e.source
this.this_action = this.Actions.MERGE
this.show_modal = true
} else {
this.mergeThis(e.source, e.target, true)
}
break
case 'get-children':
if (source.show_children) {
Vue.set(source, 'show_children', false)
} else {
this.getChildren(param, source)
}
break;
case 'get-recipes':
if (source.show_recipes) {
Vue.set(source, 'show_recipes', false)
} else {
this.getRecipes(param, source)
}
break;
}
},
finishAction: function (e) {
let update = undefined
switch (e?.action) {
case 'save':
this.saveThis(e.form_data)
break;
}
if (e !== 'cancel') {
switch (this.this_action) {
case this.Actions.DELETE:
this.deleteThis(this.this_item.id)
break;
case this.Actions.CREATE:
this.saveThis(e.form_data)
break;
case this.Actions.UPDATE:
update = e.form_data
update.id = this.this_item.id
this.saveThis(update)
break;
case this.Actions.MERGE:
this.mergeThis(this.this_item, e.form_data.target, false)
break;
case this.Actions.MOVE:
this.moveThis(this.this_item.id, e.form_data.target.id)
break;
}
},
computed: {
headerComponent() {
// TODO this leads webpack to create one .js file for each component in this folder because at runtime any one of them could be requested

View File

@ -226,7 +226,6 @@ export default {
updateStartTime: function (e) {
this.start_time = e
},
updateIngredientCheckedState: function (e) {
for (let step of this.recipe.steps) {
for (let ingredient of step.ingredients) {

View File

@ -264,5 +264,10 @@
"SupermarketName": "Supermarket Name",
"CategoryInstruction": "Drag categories to change the order categories appear in shopping list.",
"shopping_recent_days_desc": "Days of recent shopping list entries to display.",
"shopping_recent_days": "Recent Days"
"shopping_recent_days": "Recent Days",
"Coming_Soon": "Coming-Soon",
"Auto_Planner": "Auto-Planner",
"New_Cookbook": "New cookbook",
"Hide_Keyword": "Hide keywords",
"Clear": "Clear"
}

File diff suppressed because it is too large Load Diff