add category context menu

This commit is contained in:
smilerz 2022-01-04 14:00:01 -06:00
parent 25c1689ca0
commit 214db80dac
11 changed files with 74 additions and 44 deletions

View File

@ -35,4 +35,3 @@ class CookbookConfig(AppConfig):
# if DEBUG:
# traceback.print_exc()
# pass # dont break startup just because fix could not run, need to investigate cases when this happens

View File

@ -5,6 +5,7 @@ from rest_framework.authtoken.models import Token
from rest_framework.exceptions import AuthenticationFailed
from cookbook.views import views
from recipes import settings
class ScopeMiddleware:
@ -14,14 +15,15 @@ class ScopeMiddleware:
def __call__(self, request):
if request.user.is_authenticated:
if request.path.startswith('/admin/'):
prefix = settings.JS_REVERSE_SCRIPT_PREFIX or ''
if request.path.startswith(prefix + '/admin/'):
with scopes_disabled():
return self.get_response(request)
if request.path.startswith('/signup/') or request.path.startswith('/invite/'):
if request.path.startswith(prefix + '/signup/') or request.path.startswith(prefix + '/invite/'):
return self.get_response(request)
if request.path.startswith('/accounts/'):
if request.path.startswith(prefix + '/accounts/'):
return self.get_response(request)
with scopes_disabled():

View File

@ -17,8 +17,7 @@ def shopping_helper(qs, request):
supermarket = request.query_params.get('supermarket', None)
checked = request.query_params.get('checked', 'recent')
user = request.user
supermarket_order = ['food__supermarket_category__name', 'food__name']
supermarket_order = [F('food__supermarket_category__name').asc(nulls_first=True), 'food__name']
# TODO created either scheduled task or startup task to delete very old shopping list entries
# TODO create user preference to define 'very old'

View File

@ -65,7 +65,6 @@ def test_related_mixed_space(request, recipe, u1_s2):
reverse(RELATED_URL, args={recipe.id})).content)) == 0
# TODO add tests for mealplan related when thats added
# TODO if/when related recipes includes multiple levels (related recipes of related recipes) add the following tests
# -- step recipes included in step recipes
# -- step recipes included in food recipes

View File

@ -233,7 +233,6 @@ def test_shopping_recipe_mixed_authors(u1_s1, u2_s1):
assert len(json.loads(u2_s1.get(reverse(SHOPPING_LIST_URL)).content)) == 0
# TODO test adding recipe with ingredients that are not food
@pytest.mark.parametrize("recipe", [{'steps__ingredients__header': 1}], indirect=['recipe'])
def test_shopping_with_header_ingredient(u1_s1, recipe):
# with scope(space=recipe.space):

View File

@ -689,8 +689,11 @@ class RecipeViewSet(viewsets.ModelViewSet):
obj = self.get_object()
if obj.get_space() != request.space:
raise PermissionDenied(detail='You do not have the required permission to perform this action', code=403)
qs = obj.get_related_recipes(levels=1) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
# mealplans= TODO get todays mealplans
try:
levels = int(request.query_params.get('levels', 1))
except (ValueError, TypeError):
levels = 1
qs = obj.get_related_recipes(levels=levels) # TODO: make levels a user setting, included in request data?, keep solely in the backend?
return Response(self.serializer_class(qs, many=True).data)

View File

@ -7,7 +7,7 @@
<div class="col-xl-8 col-12">
<div class="container-fluid d-flex flex-column flex-grow-1">
<!-- dynamically loaded header components -->
<div class="row" v-if="header_component_name !== ''">
<div class="row" v-if="header_component_name">
<div class="col-md-12">
<component :is="headerComponent"></component>
</div>

View File

@ -58,16 +58,28 @@
<div v-for="(s, i) in done" :key="i">
<h5 v-if="Object.entries(s).length > 0">
<b-button
class="btn btn-lg text-decoration-none text-dark px-1 py-0 border-0"
variant="link"
data-toggle="collapse"
:href="'#section-' + sectionID(x, i)"
:aria-expanded="'true' ? x == 'false' : 'true'"
>
<i class="fa fa-chevron-right rotate" />
{{ i }}
</b-button>
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true">
<button
aria-haspopup="true"
aria-expanded="false"
type="button"
class="btn dropdown-toggle btn-link text-decoration-none text-dark pr-2 dropdown-toggle-no-caret"
@click.stop="openContextMenu($event, s, true)"
>
<i class="fas fa-ellipsis-v fa-lg"></i>
</button>
<b-button
class="btn btn-lg text-decoration-none text-dark px-1 py-0 border-0"
variant="link"
data-toggle="collapse"
:href="'#section-' + sectionID(x, i)"
:aria-expanded="'true' ? x == 'false' : 'true'"
>
<i class="fa fa-chevron-right rotate" />
{{ i }}
</b-button>
</div>
</h5>
<div class="collapse" :id="'section-' + sectionID(x, i)" visible role="tabpanel" :class="{ show: x == 'false' }">
@ -594,6 +606,7 @@ export default {
// if a supermarket is selected and filtered to only supermarket categories filter out everything else
if (this.selected_supermarket && this.supermarket_categories_only) {
let shopping_categories = this.supermarkets // category IDs configured on supermarket
.filter((x) => x.id === this.selected_supermarket)
.map((x) => x.category_to_supermarket)
.flat()
.map((x) => x.category.id)
@ -603,7 +616,7 @@ export default {
shopping_list = shopping_list.filter((x) => x?.food?.supermarket_category)
}
let groups = { false: {}, true: {} } // force unchecked to always be first
var groups = { false: {}, true: {} } // force unchecked to always be first
if (this.selected_supermarket) {
let super_cats = this.supermarkets
.filter((x) => x.id === this.selected_supermarket)
@ -611,10 +624,13 @@ export default {
.flat()
.map((x) => x.category.name)
new Set([...super_cats, ...this.shopping_categories.map((x) => x.name)]).forEach((cat) => {
groups["false"][cat.name] = {}
groups["true"][cat.name] = {}
groups["false"][cat] = {}
groups["true"][cat] = {}
})
} else {
// TODO: make nulls_first a user setting
groups.false[this.$t("Undefined")] = {}
groups.true[this.$t("Undefined")] = {}
this.shopping_categories.forEach((cat) => {
groups.false[cat.name] = {}
groups.true[cat.name] = {}
@ -917,8 +933,17 @@ export default {
// TODO make decision - should inheritance always be set manually or give user a choice at front-end or make it a setting?
let food = this.items.filter((x) => x.food.id == item?.[0]?.food.id ?? item.food.id)[0].food
food.supermarket_category = this.shopping_categories.filter((x) => x?.id === this.shopcat)?.[0]
this.updateFood(food, "supermarket_category")
let supermarket_category = this.shopping_categories.filter((x) => x?.id === this.shopcat)?.[0]
food.supermarket_category = supermarket_category
this.updateFood(food, "supermarket_category").then((result) => {
this.items = this.items.map((x) => {
if (x.food.id === food.id) {
return { ...x, food: { ...x.food, supermarket_category: supermarket_category } }
} else {
return x
}
})
})
this.shopcat = null
},
onHand: function (item) {
@ -940,8 +965,13 @@ export default {
})
})
},
openContextMenu(e, value) {
this.shopcat = value?.food?.supermarket_category?.id ?? value?.[0]?.food?.supermarket_category?.id ?? undefined
openContextMenu(e, value, section = false) {
if (section) {
value = Object.values(value).flat()
} else {
this.shopcat = value?.food?.supermarket_category?.id ?? value?.[0]?.food?.supermarket_category?.id ?? undefined
}
this.$refs.menu.open(e, value)
},
saveSettings: function () {
@ -1010,24 +1040,15 @@ export default {
},
updateFood: function (food, field) {
let api = new ApiApiFactory()
let ignore_category
if (field) {
ignore_category = food.inherit_fields
.map((x) => food.inherit_fields.fields)
.flat()
.includes(field)
} else {
ignore_category = true
// assume if field is changing it should no longer be inheritted
food.inherit_fields = food.inherit_fields.filter((x) => x.field !== field)
}
return api
.partialUpdateFood(food.id, food)
.then((result) => {
if (food.supermarket_category && !ignore_category && food.parent) {
makeToast(this.$t("Warning"), this.$t("InheritWarning", { food: food.name }), "warning")
} else {
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
}
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
if (food?.numchild > 0) {
this.getShoppingList() // if food has children, just get the whole list. probably could be more efficient
}

View File

@ -26,7 +26,7 @@
<div class="m-0 text-truncate">{{ item[subtitle] }}</div>
<div class="m-0 text-truncate small text-muted" v-if="getFullname">{{ getFullname }}</div>
<generic-pill v-for="x in itemTags" :key="x.field" :item_list="item[x.field]" :label="x.label" :color="x.color" />
<generic-pill v-for="x in itemTags" :key="x.field" :item_list="itemList(x)" :label="x.label" :color="x.color" />
<generic-ordered-pill
v-for="x in itemOrderedTags"
:key="x.field"
@ -259,6 +259,14 @@ export default {
finishAction: function (e) {
this.$emit("finish-action", e)
},
itemList: function (tag) {
let itemlist = this.item?.[tag?.field] ?? []
if (Array.isArray(itemlist)) {
return itemlist
} else {
return [itemlist]
}
},
},
}
</script>

View File

@ -10,7 +10,7 @@
export default {
name: "GenericPill",
props: {
item_list: { type: Object },
item_list: { type: Array },
label: { type: String, default: "name" },
color: { type: String, default: "light" },
},

View File

@ -108,7 +108,7 @@ export default {
this.shop = false // don't check any boxes until user selects a shopping list to edit
if (count_shopping_ingredient >= 1) {
this.shopping_status = true // ingredient is in the shopping list - probably (but not definitely, this ingredient)
} else if (this.ingredient.food.shopping) {
} else if (this.ingredient?.food?.shopping) {
this.shopping_status = null // food is in the shopping list, just not for this ingredient/recipe
} else {
// food is not in any shopping list
@ -123,7 +123,7 @@ export default {
if (count_shopping_ingredient >= 1) {
// ingredient is in this shopping list (not entirely sure how this could happen?)
this.shopping_status = true
} else if (count_shopping_ingredient == 0 && this.ingredient.food.shopping) {
} else if (count_shopping_ingredient == 0 && this.ingredient?.food?.shopping) {
// food is in the shopping list, just not for this ingredient/recipe
this.shopping_status = null
} else {