view and delete orphaned files

miscelaneous bug fixes discovered during testing
This commit is contained in:
smilerz 2023-11-10 13:33:16 -06:00
parent c654cc469a
commit 46a50d7835
No known key found for this signature in database
GPG Key ID: 39444C7606D47126
6 changed files with 205 additions and 90 deletions

View File

@ -7,7 +7,7 @@ class Round(Func):
def str2bool(v): def str2bool(v):
if type(v) == bool or v is None: if isinstance(v, bool) or v is None:
return v return v
else: else:
return v.lower() in ("yes", "true", "1") return v.lower() in ("yes", "true", "1")

View File

@ -309,7 +309,7 @@ class RecipeSearch():
def _favorite_recipes(self, times_cooked=None): def _favorite_recipes(self, times_cooked=None):
if self._sort_includes('favorite') or times_cooked: if self._sort_includes('favorite') or times_cooked:
less_than = '-' in (times_cooked or []) and not self._sort_includes('-favorite') less_than = '-' in (str(times_cooked) or []) and not self._sort_includes('-favorite')
if less_than: if less_than:
default = 1000 default = 1000
else: else:

View File

@ -84,21 +84,47 @@
{% endif %} {% endif %}
<h4 class="mt-3">{% trans 'Database' %} <span <h4 class="mt-3">{% trans 'Database' %} <span
class="badge badge-{% if postgres %}warning{% else %}success{% endif %}">{% if postgres %} class="badge badge-{% if postgres %}success{% else %}warning{% endif %}">{% if postgres %}
{% trans 'Info' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4> {% trans 'Info' %}{% else %}{% trans 'Ok' %}{% endif %}</span></h4>
{% if postgres %} {% if postgres %}
{% trans 'Everything is fine!' %}
{% else %}
{% blocktrans %} {% blocktrans %}
This application is not running with a Postgres database backend. This is ok but not recommended as some This application is not running with a Postgres database backend. This is ok but not recommended as some
features only work with postgres databases. features only work with postgres databases.
{% endblocktrans %} {% endblocktrans %}
{% else %}
{% trans 'Everything is fine!' %}
{% endif %} {% endif %}
<h4 class="mt-3">
{% trans 'Orphaned Files' %}
<span class="badge badge-{% if orphans|length == 0 %}success{% elif orphans|length <= 25 %}warning{% else %}danger{% endif %}">
{% if orphans|length == 0 %}{% trans 'Success' %}
{% elif orphans|length <= 25 %}{% trans 'Warning' %}
{% else %}{% trans 'Danger' %}
{% endif %}
</span>
</h4>
{% if orphans|length == 0 %}
{% trans 'Everything is fine!' %}
{% else %}
{% blocktrans with orphan_count=orphans|length %}
There are currently {{ orphan_count }} orphaned files.
{% endblocktrans %}
<br>
<button id="toggle-button" class="btn btn-info btn-sm" onclick="toggleOrphans()">{% trans 'Show' %}</button>
<button class="btn btn-info btn-sm" onclick="deleteOrphans()">{% trans 'Delete' %}</button>
{% endif %}
<textarea id="orphans-list" style="display:none;" class="form-control" rows="20">
{% for orphan in orphans %}{{ orphan }}
{% endfor %}
</textarea>
<h4 class="mt-3">Debug</h4> <h4 class="mt-3">Debug</h4>
<textarea class="form-control" rows="20"> <textarea class="form-control" rows="20">
Gunicorn Media: {{ gunicorn_media }} Gunicorn Media: {{ gunicorn_media }}
Sqlite: {{ postgres }} Sqlite: {% if postgres %} {% trans 'False' %} {% else %} {% trans 'True' %} {% endif %}
Debug: {{ debug }} Debug: {{ debug }}
{% for key,value in request.META.items %}{% if key in 'SERVER_PORT,REMOTE_HOST,REMOTE_ADDR,SERVER_PROTOCOL' %}{{ key }}:{{ value }} {% for key,value in request.META.items %}{% if key in 'SERVER_PORT,REMOTE_HOST,REMOTE_ADDR,SERVER_PROTOCOL' %}{{ key }}:{{ value }}
@ -110,4 +136,30 @@ Debug: {{ debug }}
</textarea> </textarea>
<br/> <br/>
<br/> <br/>
{% endblock %} <form method="POST" id="delete-form">
{% csrf_token %}
<input type="hidden" name="delete_orphans" value="false">
</form>
{% block script %}
<script>
function toggleOrphans() {
var orphansList = document.getElementById('orphans-list');
var button = document.getElementById('toggle-button');
if (orphansList.style.display === 'none') {
orphansList.style.display = 'block';
button.innerText = "{% trans 'Hide' %}";
} else {
orphansList.style.display = 'none';
button.innerText = "{% trans 'Show' %}";
}
}
function deleteOrphans() {
document.getElementById('delete-form').delete_orphans.value = 'true';
document.getElementById('delete-form').submit();
}
</script>
{% endblock script %}
{% endblock %}

View File

@ -3,12 +3,14 @@ import re
from datetime import datetime from datetime import datetime
from uuid import UUID from uuid import UUID
from django.apps import apps
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
@ -18,6 +20,7 @@ from django_scopes import scopes_disabled
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, SpaceCreateForm, from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, SpaceCreateForm,
SpaceJoinForm, User, UserCreateForm, UserPreference) SpaceJoinForm, User, UserCreateForm, UserPreference)
from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.permission_helper import (group_required, has_group_permission, from cookbook.helper.permission_helper import (group_required, has_group_permission,
share_link_valid, switch_user_active_space) share_link_valid, switch_user_active_space)
from cookbook.models import (Comment, CookLog, InviteLink, SearchFields, SearchPreference, from cookbook.models import (Comment, CookLog, InviteLink, SearchFields, SearchPreference,
@ -318,13 +321,20 @@ def system(request):
secret_key = False if os.getenv('SECRET_KEY') else True secret_key = False if os.getenv('SECRET_KEY') else True
if request.method == "POST":
del_orphans = request.POST.get('delete_orphans')
orphans = get_orphan_files(delete_orphans=str2bool(del_orphans))
else:
orphans = get_orphan_files()
return render(request, 'system.html', { return render(request, 'system.html', {
'gunicorn_media': settings.GUNICORN_MEDIA, 'gunicorn_media': settings.GUNICORN_MEDIA,
'debug': settings.DEBUG, 'debug': settings.DEBUG,
'postgres': postgres, 'postgres': postgres,
'version_info': VERSION_INFO, 'version_info': VERSION_INFO,
'plugins': PLUGINS, 'plugins': PLUGINS,
'secret_key': secret_key 'secret_key': secret_key,
'orphans': orphans
}) })
@ -448,3 +458,47 @@ def test(request):
def test2(request): def test2(request):
if not settings.DEBUG: if not settings.DEBUG:
return HttpResponseRedirect(reverse('index')) return HttpResponseRedirect(reverse('index'))
def get_orphan_files(delete_orphans=False):
# Get list of all image files in media folder
media_dir = settings.MEDIA_ROOT
def find_orphans():
image_files = []
for root, dirs, files in os.walk(media_dir):
for file in files:
if not file.lower().endswith(('.db')) and not root.lower().endswith(('@eadir')):
full_path = os.path.join(root, file)
relative_path = os.path.relpath(full_path, media_dir)
image_files.append((relative_path, full_path))
# Get list of all image fields in models
image_fields = []
for model in apps.get_models():
for field in model._meta.get_fields():
if isinstance(field, models.ImageField) or isinstance(field, models.FileField):
image_fields.append((model, field.name))
# get all images in the database
# TODO I don't know why, but this completely bypasses scope limitations
image_paths = []
for model, field in image_fields:
image_field_paths = model.objects.values_list(field, flat=True)
image_paths.extend(image_field_paths)
# Check each image file against model image fields
return [img for img in image_files if img[0] not in image_paths]
orphans = find_orphans()
if delete_orphans:
for f in [img[1] for img in orphans]:
try:
os.remove(f)
except FileNotFoundError:
print(f"File not found: {f}")
except Exception as e:
print(f"Error deleting file {f}: {e}")
orphans = find_orphans()
return [img[1] for img in orphans]

View File

@ -438,7 +438,7 @@ for p in PLUGINS:
if p['bundle_name'] != '': if p['bundle_name'] != '':
WEBPACK_LOADER[p['bundle_name']] = { WEBPACK_LOADER[p['bundle_name']] = {
'CACHE': not DEBUG, 'CACHE': not DEBUG,
'BUNDLE_DIR_NAME': f'vue/', # must end with slash 'BUNDLE_DIR_NAME': 'vue/', # must end with slash
'STATS_FILE': os.path.join(p["base_path"], 'vue', 'webpack-stats.json'), 'STATS_FILE': os.path.join(p["base_path"], 'vue', 'webpack-stats.json'),
'POLL_INTERVAL': 0.1, 'POLL_INTERVAL': 0.1,
'TIMEOUT': None, 'TIMEOUT': None,

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="app" style="padding-bottom: 60px"> <div id="app" style="padding-bottom: 60px">
<RecipeSwitcher ref="ref_recipe_switcher"/> <RecipeSwitcher ref="ref_recipe_switcher" />
<div class="row"> <div class="row">
<div class="col-12 col-xl-10 col-lg-10 offset-xl-1 offset-lg-1"> <div class="col-12 col-xl-10 col-lg-10 offset-xl-1 offset-lg-1">
<div class="row"> <div class="row">
@ -153,7 +153,7 @@
<div class="row" style="margin-top: 1vh"> <div class="row" style="margin-top: 1vh">
<div class="col-12" style="text-align: right"> <div class="col-12" style="text-align: right">
<b-button size="sm" variant="secondary" style="margin-right: 8px" @click="$root.$emit('bv::hide::popover')" <b-button size="sm" variant="secondary" style="margin-right: 8px" @click="$root.$emit('bv::hide::popover')"
>{{ $t("Close") }} >{{ $t("Close") }}
</b-button> </b-button>
</div> </div>
</div> </div>
@ -197,17 +197,17 @@
<span <span
class="text-sm-left text-warning" class="text-sm-left text-warning"
v-if="ui.expert_mode && search.keywords_fields > 1 && hasDuplicateFilter(search.search_keywords, search.keywords_fields)" v-if="ui.expert_mode && search.keywords_fields > 1 && hasDuplicateFilter(search.search_keywords, search.keywords_fields)"
>{{ $t("warning_duplicate_filter") }}</span >{{ $t("warning_duplicate_filter") }}</span
> >
<div class="row" v-if="ui.show_keywords"> <div class="row" v-if="ui.show_keywords">
<div class="col-12"> <div class="col-12">
<b-input-group class="mt-2" v-for="(k, a) in keywordFields" :key="a"> <b-input-group class="mt-2" v-for="(k, a) in keywordFields" :key="a">
<template #prepend v-if="ui.expert_mode"> <template #prepend v-if="ui.expert_mode">
<b-input-group-text style="width: 3em" @click="addField('keywords', k)"> <b-input-group-text style="width: 3em" @click="addField('keywords', k)">
<i class="fas fa-plus-circle text-primary" v-if="k == search.keywords_fields && k < 4"/> <i class="fas fa-plus-circle text-primary" v-if="k == search.keywords_fields && k < 4" />
</b-input-group-text> </b-input-group-text>
<b-input-group-text style="width: 3em" @click="removeField('keywords', k)"> <b-input-group-text style="width: 3em" @click="removeField('keywords', k)">
<i class="fas fa-minus-circle text-primary" v-if="k == search.keywords_fields && k > 1"/> <i class="fas fa-minus-circle text-primary" v-if="k == search.keywords_fields && k > 1" />
</b-input-group-text> </b-input-group-text>
</template> </template>
<generic-multiselect <generic-multiselect
@ -258,17 +258,17 @@
<span <span
class="text-sm-left text-warning" class="text-sm-left text-warning"
v-if="ui.expert_mode && search.foods_fields > 1 && hasDuplicateFilter(search.search_foods, search.foods_fields)" v-if="ui.expert_mode && search.foods_fields > 1 && hasDuplicateFilter(search.search_foods, search.foods_fields)"
>{{ $t("warning_duplicate_filter") }}</span >{{ $t("warning_duplicate_filter") }}</span
> >
<div class="row" v-if="ui.show_foods"> <div class="row" v-if="ui.show_foods">
<div class="col-12"> <div class="col-12">
<b-input-group class="mt-2" v-for="(f, i) in foodFields" :key="i"> <b-input-group class="mt-2" v-for="(f, i) in foodFields" :key="i">
<template #prepend v-if="ui.expert_mode"> <template #prepend v-if="ui.expert_mode">
<b-input-group-text style="width: 3em" @click="addField('foods', f)"> <b-input-group-text style="width: 3em" @click="addField('foods', f)">
<i class="fas fa-plus-circle text-primary" v-if="f == search.foods_fields && f < 4"/> <i class="fas fa-plus-circle text-primary" v-if="f == search.foods_fields && f < 4" />
</b-input-group-text> </b-input-group-text>
<b-input-group-text style="width: 3em" @click="removeField('foods', f)"> <b-input-group-text style="width: 3em" @click="removeField('foods', f)">
<i class="fas fa-minus-circle text-primary" v-if="f == search.foods_fields && f > 1"/> <i class="fas fa-minus-circle text-primary" v-if="f == search.foods_fields && f > 1" />
</b-input-group-text> </b-input-group-text>
</template> </template>
<generic-multiselect <generic-multiselect
@ -314,17 +314,17 @@
<span <span
class="text-sm-left text-warning" class="text-sm-left text-warning"
v-if="ui.expert_mode && search.books_fields > 1 && hasDuplicateFilter(search.search_books, search.books_fields)" v-if="ui.expert_mode && search.books_fields > 1 && hasDuplicateFilter(search.search_books, search.books_fields)"
>{{ $t("warning_duplicate_filter") }}</span >{{ $t("warning_duplicate_filter") }}</span
> >
<div class="row" v-if="ui.show_books"> <div class="row" v-if="ui.show_books">
<div class="col-12"> <div class="col-12">
<b-input-group class="mt-2" v-for="(b, i) in bookFields" :key="i"> <b-input-group class="mt-2" v-for="(b, i) in bookFields" :key="i">
<template #prepend v-if="ui.expert_mode"> <template #prepend v-if="ui.expert_mode">
<b-input-group-text style="width: 3em" @click="addField('books', b)"> <b-input-group-text style="width: 3em" @click="addField('books', b)">
<i class="fas fa-plus-circle text-primary" v-if="b == search.books_fields && b < 4"/> <i class="fas fa-plus-circle text-primary" v-if="b == search.books_fields && b < 4" />
</b-input-group-text> </b-input-group-text>
<b-input-group-text style="width: 3em" @click="removeField('books', b)"> <b-input-group-text style="width: 3em" @click="removeField('books', b)">
<i class="fas fa-minus-circle text-primary" v-if="b == search.books_fields && b > 1"/> <i class="fas fa-minus-circle text-primary" v-if="b == search.books_fields && b > 1" />
</b-input-group-text> </b-input-group-text>
</template> </template>
<generic-multiselect <generic-multiselect
@ -558,16 +558,26 @@
<b-input-group-append v-if="ui.show_makenow"> <b-input-group-append v-if="ui.show_makenow">
<b-input-group-text> <b-input-group-text>
{{ $t("make_now") }} {{ $t("make_now") }}
<b-form-checkbox v-model="search.makenow" name="check-button" <b-form-checkbox
@change="refreshData(false)" v-model="search.makenow"
class="shadow-none" switch style="width: 4em"/> name="check-button"
@change="refreshData(false)"
class="shadow-none"
switch
style="width: 4em"
/>
</b-input-group-text> </b-input-group-text>
<b-input-group-text> <b-input-group-text>
<span>{{ $t("make_now_count") }}</span> <span>{{ $t("make_now_count") }}</span>
<b-form-input type="number" min="0" max="20" v-model="search.makenow_count" <b-form-input
@change="refreshData(false)" type="number"
size="sm" class="mt-1"></b-form-input> min="0"
max="20"
v-model="search.makenow_count"
@change="refreshData(false)"
size="sm"
class="mt-1"
></b-form-input>
</b-input-group-text> </b-input-group-text>
</b-input-group-append> </b-input-group-append>
</b-input-group> </b-input-group>
@ -607,11 +617,11 @@
<!-- TODO find a way to localize this that works without explaining localization to each language translator --> <!-- TODO find a way to localize this that works without explaining localization to each language translator -->
Show all recipes that are matched Show all recipes that are matched
<span v-if="search.search_input"> <span v-if="search.search_input">
by <i>{{ search.search_input }}</i> <br/> by <i>{{ search.search_input }}</i> <br />
</span> </span>
<span v-else> without any search term <br/> </span> <span v-else> without any search term <br /> </span>
<span v-if="search.search_internal"> and are <span class="text-success">internal</span> <br/></span> <span v-if="search.search_internal"> and are <span class="text-success">internal</span> <br /></span>
<span v-for="k in search.search_keywords" v-bind:key="k.id"> <span v-for="k in search.search_keywords" v-bind:key="k.id">
<template v-if="k.items.length > 0"> <template v-if="k.items.length > 0">
@ -620,7 +630,7 @@
contain contain
<b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">keywords</span>: <b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">keywords</span>:
<i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i> <i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i>
<br/> <br />
</template> </template>
</span> </span>
@ -631,7 +641,7 @@
contain contain
<b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">foods</span>: <b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">foods</span>:
<i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i> <i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i>
<br/> <br />
</template> </template>
</span> </span>
@ -642,38 +652,38 @@
contain contain
<b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">books</span>: <b v-if="k.operator">any</b><b v-else>all</b> of the following <span class="text-success">books</span>:
<i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i> <i>{{ k.items.flatMap((x) => x.name).join(", ") }}</i>
<br/> <br />
</template> </template>
</span> </span>
<span v-if="search.makenow"> and you can <span class="text-success">make right now</span> (based on the on hand flag) <br/></span> <span v-if="search.makenow"> and you can <span class="text-success">make right now</span> (based on the on hand flag) <br /></span>
<span v-if="search.search_units.length > 0"> <span v-if="search.search_units.length > 0">
and contain <b v-if="search.search_units_or">any</b><b v-else>all</b> of the following <span class="text-success">units</span>: and contain <b v-if="search.search_units_or">any</b><b v-else>all</b> of the following <span class="text-success">units</span>:
<i>{{ search.search_units.flatMap((x) => x.name).join(", ") }}</i <i>{{ search.search_units.flatMap((x) => x.name).join(", ") }}</i
><br/> ><br />
</span> </span>
<span v-if="search.search_rating !== undefined"> <span v-if="search.search_rating !== undefined">
and have a <span class="text-success">rating</span> <template v-if="search.search_rating_gte">greater than</template and have a <span class="text-success">rating</span> <template v-if="search.search_rating_gte">greater than</template
><template v-else> less than</template> or equal to {{ search.search_rating }}<br/> ><template v-else> less than</template> or equal to {{ search.search_rating }}<br />
</span> </span>
<span v-if="search.lastcooked !== undefined"> <span v-if="search.lastcooked !== undefined">
and have been <span class="text-success">last cooked</span> <template v-if="search.lastcooked_gte"> after</template and have been <span class="text-success">last cooked</span> <template v-if="search.lastcooked_gte"> after</template
><template v-else> before</template> <i>{{ search.lastcooked }}</i ><template v-else> before</template> <i>{{ search.lastcooked }}</i
><br/> ><br />
</span> </span>
<span v-if="search.timescooked !== undefined"> <span v-if="search.timescooked !== undefined">
and have <span class="text-success">been cooked</span> <template v-if="search.timescooked_gte"> at least</template and have <span class="text-success">been cooked</span> <template v-if="search.timescooked_gte"> at least</template
><template v-else> less than</template> or equal to<i>{{ search.timescooked }}</i> times <br/> ><template v-else> less than</template> or equal to<i>{{ search.timescooked }}</i> times <br />
</span> </span>
<span v-if="search.sort_order.length > 0"> <span v-if="search.sort_order.length > 0">
<span class="text-success">order</span> by <span class="text-success">order</span> by
<i>{{ search.sort_order.flatMap((x) => x.text).join(", ") }}</i> <i>{{ search.sort_order.flatMap((x) => x.text).join(", ") }}</i>
<br/> <br />
</span> </span>
</div> </div>
</div> </div>
@ -709,19 +719,19 @@
</b-dropdown> </b-dropdown>
<b-button variant="outline-primary" size="sm" class="shadow-none ml-1" @click="resetSearch()" v-if="searchFiltered()" <b-button variant="outline-primary" size="sm" class="shadow-none ml-1" @click="resetSearch()" v-if="searchFiltered()"
><i class="fas fa-file-alt"></i> {{ search.pagination_page }}/{{ Math.ceil(pagination_count / ui.page_size) }} {{ $t("Reset") }} ><i class="fas fa-file-alt"></i> {{ search.pagination_page }}/{{ Math.ceil(pagination_count / ui.page_size) }} {{ $t("Reset") }}
<i class="fas fa-times-circle"></i> <i class="fas fa-times-circle"></i>
</b-button> </b-button>
<b-button variant="outline-primary" size="sm" class="shadow-none ml-1" @click="openRandom()" <b-button variant="outline-primary" size="sm" class="shadow-none ml-1" @click="openRandom()"
><i class="fas fa-dice-five"></i> {{ $t("Random") }} ><i class="fas fa-dice-five"></i> {{ $t("Random") }}
</b-button> </b-button>
</div> </div>
</div> </div>
</div> </div>
<template v-if="!searchFiltered() && ui.show_meal_plan && meal_plan_grid.length > 0"> <template v-if="!searchFiltered() && ui.show_meal_plan && meal_plan_grid.length > 0">
<hr/> <hr />
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
<div <div
@ -736,7 +746,7 @@
</div> </div>
<div class="flex-grow-1 text-right"> <div class="flex-grow-1 text-right">
<b-button class="hover-button btn-outline-primary btn-sm" @click="showMealPlanEditModal(null, day.create_default_date)" <b-button class="hover-button btn-outline-primary btn-sm" @click="showMealPlanEditModal(null, day.create_default_date)"
><i class="fa fa-plus"></i ><i class="fa fa-plus"></i
></b-button> ></b-button>
</div> </div>
</div> </div>
@ -744,8 +754,8 @@
<b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.id" class="hover-div p-0 pr-2"> <b-list-group-item v-for="plan in day.plan_entries" v-bind:key="plan.id" class="hover-div p-0 pr-2">
<div class="d-flex flex-row align-items-center"> <div class="d-flex flex-row align-items-center">
<div> <div>
<img style="height: 50px; width: 50px; object-fit: cover" :src="plan.recipe.image" v-if="plan.recipe?.image"/> <img style="height: 50px; width: 50px; object-fit: cover" :src="plan.recipe.image" v-if="plan.recipe?.image" />
<img style="height: 50px; width: 50px; object-fit: cover" :src="image_placeholder" v-else/> <img style="height: 50px; width: 50px; object-fit: cover" :src="image_placeholder" v-else />
</div> </div>
<div class="flex-grow-1 ml-2" style="text-overflow: ellipsis; overflow-wrap: anywhere"> <div class="flex-grow-1 ml-2" style="text-overflow: ellipsis; overflow-wrap: anywhere">
<span class="two-row-text"> <span class="two-row-text">
@ -755,7 +765,7 @@
</div> </div>
<div class="hover-button"> <div class="hover-button">
<b-button @click="showMealPlanEditModal(plan, null)" class="btn-outline-primary btn-sm" <b-button @click="showMealPlanEditModal(plan, null)" class="btn-outline-primary btn-sm"
><i class="fas fa-pencil-alt"></i ><i class="fas fa-pencil-alt"></i
></b-button> ></b-button>
</div> </div>
</div> </div>
@ -765,7 +775,7 @@
</div> </div>
</div> </div>
</div> </div>
<hr/> <hr />
</template> </template>
<div v-if="recipes.length > 0" class="mt-4"> <div v-if="recipes.length > 0" class="mt-4">
@ -853,24 +863,23 @@
</template> </template>
<script> <script>
import Vue from "vue" import { BootstrapVue } from "bootstrap-vue"
import {BootstrapVue} from "bootstrap-vue"
import VueCookies from "vue-cookies"
import "bootstrap-vue/dist/bootstrap-vue.css" import "bootstrap-vue/dist/bootstrap-vue.css"
import Vue from "vue"
import VueCookies from "vue-cookies"
import moment from "moment"
import _debounce from "lodash/debounce" import _debounce from "lodash/debounce"
import moment from "moment"
import Multiselect from "vue-multiselect" import Multiselect from "vue-multiselect"
import {ApiMixin, ResolveUrlMixin, StandardToasts, ToastMixin} from "@/utils/utils"
import LoadingSpinner from "@/components/LoadingSpinner" // TODO: is this deprecated?
import RecipeCard from "@/components/RecipeCard"
import GenericMultiselect from "@/components/GenericMultiselect"
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
import {ApiApiFactory} from "@/utils/openapi/api"
import {useMealPlanStore} from "@/stores/MealPlanStore"
import BottomNavigationBar from "@/components/BottomNavigationBar.vue" import BottomNavigationBar from "@/components/BottomNavigationBar.vue"
import RecipeSwitcher from "@/components/Buttons/RecipeSwitcher"
import GenericMultiselect from "@/components/GenericMultiselect"
import MealPlanEditModal from "@/components/MealPlanEditModal.vue" import MealPlanEditModal from "@/components/MealPlanEditModal.vue"
import RecipeCard from "@/components/RecipeCard"
import { useMealPlanStore } from "@/stores/MealPlanStore"
import { ApiApiFactory } from "@/utils/openapi/api"
import { ApiMixin, ResolveUrlMixin, StandardToasts, ToastMixin } from "@/utils/utils"
Vue.use(VueCookies) Vue.use(VueCookies)
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
@ -881,7 +890,7 @@ let UI_COOKIE_NAME = "ui_search_settings"
export default { export default {
name: "RecipeSearchView", name: "RecipeSearchView",
mixins: [ResolveUrlMixin, ApiMixin, ToastMixin], mixins: [ResolveUrlMixin, ApiMixin, ToastMixin],
components: {GenericMultiselect, RecipeCard, RecipeSwitcher, Multiselect, BottomNavigationBar, MealPlanEditModal}, components: { GenericMultiselect, RecipeCard, RecipeSwitcher, Multiselect, BottomNavigationBar, MealPlanEditModal },
data() { data() {
return { return {
// this.Models and this.Actions inherited from ApiMixin // this.Models and this.Actions inherited from ApiMixin
@ -898,22 +907,22 @@ export default {
search_input: "", search_input: "",
search_internal: false, search_internal: false,
search_keywords: [ search_keywords: [
{items: [], operator: true, not: false}, { items: [], operator: true, not: false },
{items: [], operator: false, not: false}, { items: [], operator: false, not: false },
{items: [], operator: true, not: true}, { items: [], operator: true, not: true },
{items: [], operator: false, not: true}, { items: [], operator: false, not: true },
], ],
search_foods: [ search_foods: [
{items: [], operator: true, not: false}, { items: [], operator: true, not: false },
{items: [], operator: false, not: false}, { items: [], operator: false, not: false },
{items: [], operator: true, not: true}, { items: [], operator: true, not: true },
{items: [], operator: false, not: true}, { items: [], operator: false, not: true },
], ],
search_books: [ search_books: [
{items: [], operator: true, not: false}, { items: [], operator: true, not: false },
{items: [], operator: false, not: false}, { items: [], operator: false, not: false },
{items: [], operator: true, not: true}, { items: [], operator: true, not: true },
{items: [], operator: false, not: true}, { items: [], operator: false, not: true },
], ],
search_units: [], search_units: [],
search_units_or: true, search_units_or: true,
@ -986,7 +995,7 @@ export default {
date: moment_date, date: moment_date,
create_default_date: moment_date.format("YYYY-MM-DD"), // improve meal plan edit modal to do formatting itself and accept dates create_default_date: moment_date.format("YYYY-MM-DD"), // improve meal plan edit modal to do formatting itself and accept dates
date_label: moment_date.format("dd") + " " + moment_date.format("ll"), date_label: moment_date.format("dd") + " " + moment_date.format("ll"),
plan_entries: this.meal_plan_store.plan_list.filter((m) => moment_date.isBetween(moment(m.from_date), moment(m.to_date), 'day', '[]')) plan_entries: this.meal_plan_store.plan_list.filter((m) => moment_date.isBetween(moment(m.from_date), moment(m.to_date), "day", "[]")),
}) })
} }
} }
@ -1032,12 +1041,12 @@ export default {
} }
return [ return [
{id: 5, label: "⭐⭐⭐⭐⭐ " + label(5)}, { id: 5, label: "⭐⭐⭐⭐⭐ " + label(5) },
{id: 4, label: "⭐⭐⭐⭐ " + label()}, { id: 4, label: "⭐⭐⭐⭐ " + label() },
{id: 3, label: "⭐⭐⭐ " + label()}, { id: 3, label: "⭐⭐⭐ " + label() },
{id: 2, label: "⭐⭐ " + label()}, { id: 2, label: "⭐⭐ " + label() },
{id: 1, label: "⭐ " + label(1)}, { id: 1, label: "⭐ " + label(1) },
{id: 0, label: this.$t("Unrated")}, { id: 0, label: this.$t("Unrated") },
] ]
}, },
keywordFields: function () { keywordFields: function () {
@ -1063,7 +1072,7 @@ export default {
[this.$t("Name"), "name", "A-z", "Z-a"], [this.$t("Name"), "name", "A-z", "Z-a"],
[this.$t("last_cooked"), "lastcooked", "↑", "↓"], [this.$t("last_cooked"), "lastcooked", "↑", "↓"],
[this.$t("Rating"), "rating", "1-5", "5-1"], [this.$t("Rating"), "rating", "1-5", "5-1"],
[this.$t("times_cooked"), "favorite", "*-x", "x-*"], [this.$t("times_cooked"), "favorite", "x-X", "X-x"],
[this.$t("date_created"), "created_at", "↑", "↓"], [this.$t("date_created"), "created_at", "↑", "↓"],
[this.$t("date_viewed"), "lastviewed", "↑", "↓"], [this.$t("date_viewed"), "lastviewed", "↑", "↓"],
] ]
@ -1093,7 +1102,7 @@ export default {
}, },
}, },
mounted() { mounted() {
this.recipes = Array(this.ui.page_size).fill({loading: true}) this.recipes = Array(this.ui.page_size).fill({ loading: true })
this.$nextTick(function () { this.$nextTick(function () {
if (this.$cookies.isKey(UI_COOKIE_NAME)) { if (this.$cookies.isKey(UI_COOKIE_NAME)) {
@ -1162,13 +1171,13 @@ export default {
"ui.expert_mode": function (newVal, oldVal) { "ui.expert_mode": function (newVal, oldVal) {
if (!newVal) { if (!newVal) {
this.search.search_keywords = this.search.search_keywords.map((x) => { this.search.search_keywords = this.search.search_keywords.map((x) => {
return {...x, not: false} return { ...x, not: false }
}) })
this.search.search_foods = this.search.search_foods.map((x) => { this.search.search_foods = this.search.search_foods.map((x) => {
return {...x, not: false} return { ...x, not: false }
}) })
this.search.search_books = this.search.search_books.map((x) => { this.search.search_books = this.search.search_books.map((x) => {
return {...x, not: false} return { ...x, not: false }
}) })
} }
}, },
@ -1177,7 +1186,7 @@ export default {
// this.genericAPI inherited from ApiMixin // this.genericAPI inherited from ApiMixin
refreshData: _debounce(function (random) { refreshData: _debounce(function (random) {
this.recipes_loading = true this.recipes_loading = true
this.recipes = Array(this.ui.page_size).fill({loading: true}) this.recipes = Array(this.ui.page_size).fill({ loading: true })
let params = this.buildParams(random) let params = this.buildParams(random)
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => { this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {
window.scrollTo(0, 0) window.scrollTo(0, 0)
@ -1220,13 +1229,13 @@ export default {
}, },
resetSearch: function (filter = undefined) { resetSearch: function (filter = undefined) {
this.search.search_keywords = this.search.search_keywords.map((x) => { this.search.search_keywords = this.search.search_keywords.map((x) => {
return {...x, items: []} return { ...x, items: [] }
}) })
this.search.search_foods = this.search.search_foods.map((x) => { this.search.search_foods = this.search.search_foods.map((x) => {
return {...x, items: []} return { ...x, items: [] }
}) })
this.search.search_books = this.search.search_books.map((x) => { this.search.search_books = this.search.search_books.map((x) => {
return {...x, items: []} return { ...x, items: [] }
}) })
this.search.search_input = filter?.query ?? "" this.search.search_input = filter?.query ?? ""
this.search.search_internal = filter?.internal ?? false this.search.search_internal = filter?.internal ?? false
@ -1289,7 +1298,7 @@ export default {
return return
}, },
buildParams: function (random) { buildParams: function (random) {
let params = {options: {query: {}}, page: this.search.pagination_page, pageSize: this.ui.page_size} let params = { options: { query: {} }, page: this.search.pagination_page, pageSize: this.ui.page_size }
if (this.search.search_filter) { if (this.search.search_filter) {
params.options.query.filter = this.search.search_filter.id params.options.query.filter = this.search.search_filter.id
return params return params
@ -1414,7 +1423,7 @@ export default {
;["page", "pageSize"].forEach((key) => { ;["page", "pageSize"].forEach((key) => {
delete search[key] delete search[key]
}) })
search = {...search, ...search.options.query} search = { ...search, ...search.options.query }
console.log("after concat", search) console.log("after concat", search)
let params = { let params = {
name: filtername, name: filtername,