From 85aca8acf641cfbc9d12f6f96fd646e43dc5ad36 Mon Sep 17 00:00:00 2001 From: smilerz Date: Mon, 13 Sep 2021 14:30:03 -0500 Subject: [PATCH] shopping_list_category --- cookbook/serializer.py | 9 +- cookbook/urls.py | 4 +- cookbook/views/api.py | 2 +- cookbook/views/lists.py | 36 ++++- vue/src/apps/ModelListView/ModelListView.vue | 135 +++++++++++++------ vue/src/apps/RecipeView/RecipeView.vue | 1 - vue/src/components/GenericInfiniteCards.vue | 92 +++++++++++++ vue/src/components/GenericPill.vue | 44 ++++++ vue/src/components/Ingredient.vue | 17 ++- vue/src/components/ModelMenu.vue | 61 +++++++++ vue/src/locales/en.json | 2 +- vue/src/utils/models.js | 59 +++++++- 12 files changed, 407 insertions(+), 55 deletions(-) create mode 100644 vue/src/components/GenericInfiniteCards.vue create mode 100644 vue/src/components/GenericPill.vue create mode 100644 vue/src/components/ModelMenu.vue diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 37b43afa..72cb9069 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -276,8 +276,9 @@ class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer): class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer): def create(self, validated_data): - obj, created = SupermarketCategory.objects.get_or_create(name=validated_data['name'], - space=self.context['request'].space) + validated_data['name'] = validated_data['name'].strip() + validated_data['space'] = self.context['request'].space + obj, created = SupermarketCategory.objects.get_or_create(**validated_data) return obj def update(self, instance, validated_data): @@ -285,7 +286,7 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial class Meta: model = SupermarketCategory - fields = ('id', 'name') + fields = ('id', 'name', 'description') class SupermarketCategoryRelationSerializer(WritableNestedModelSerializer): @@ -301,7 +302,7 @@ class SupermarketSerializer(UniqueFieldsMixin, SpacedModelSerializer): class Meta: model = Supermarket - fields = ('id', 'name', 'category_to_supermarket') + fields = ('id', 'name', 'description', 'category_to_supermarket') class RecipeSimpleSerializer(serializers.ModelSerializer): diff --git a/cookbook/urls.py b/cookbook/urls.py index 7ea95e99..e3f8fac5 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -10,7 +10,7 @@ from cookbook.helper import dal from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, - Storage, Sync, SyncLog, Unit, get_model_name) + Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, get_model_name) from .views import api, data, delete, edit, import_export, lists, new, views, telegram router = routers.DefaultRouter() @@ -176,7 +176,7 @@ for m in generic_models: ) ) -vue_models = [Food, Keyword, Unit] +vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory] for m in vue_models: py_name = get_model_name(m) url_name = py_name.replace('_', '-') diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 72188250..54292e89 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -359,7 +359,7 @@ class SupermarketCategoryRelationViewSet(viewsets.ModelViewSet, StandardFilterMi pagination_class = DefaultPagination def get_queryset(self): - self.queryset = self.queryset.filter(supermarket__space=self.request.space) + self.queryset = self.queryset.filter(supermarket__space=self.request.space).order_by('order') return super().get_queryset() diff --git a/cookbook/views/lists.py b/cookbook/views/lists.py index 71ff6ed5..d67e320c 100644 --- a/cookbook/views/lists.py +++ b/cookbook/views/lists.py @@ -146,7 +146,39 @@ def unit(request): "title": _("Units"), "config": { 'model': "UNIT", # *REQUIRED* name of the model in models.js - 'recipe_param': 'units' # *OPTIONAL* name of the listRecipes parameter if filtering on this attribute + 'recipe_param': 'units', # *OPTIONAL* name of the listRecipes parameter if filtering on this attribute } } - ) \ No newline at end of file + ) + + +@group_required('user') +def supermarket(request): + # recipe-param is the name of the parameters used when filtering recipes by this attribute + # model-name is the models.js name of the model, probably ALL-CAPS + return render( + request, + 'generic/model_template.html', + { + "title": _("Supermarkets"), + "config": { + 'model': "SUPERMARKET", # *REQUIRED* name of the model in models.js + } + } + ) + + +@group_required('user') +def supermarket_category(request): + # recipe-param is the name of the parameters used when filtering recipes by this attribute + # model-name is the models.js name of the model, probably ALL-CAPS + return render( + request, + 'generic/model_template.html', + { + "title": _("Shopping Categories"), + "config": { + 'model': "SHOPPING_CATEGORY", # *REQUIRED* name of the model in models.js + } + } + ) diff --git a/vue/src/apps/ModelListView/ModelListView.vue b/vue/src/apps/ModelListView/ModelListView.vue index 04de3933..3f7058df 100644 --- a/vue/src/apps/ModelListView/ModelListView.vue +++ b/vue/src/apps/ModelListView/ModelListView.vue @@ -1,37 +1,80 @@ @@ -46,9 +89,10 @@ import 'bootstrap-vue/dist/bootstrap-vue.css' import {CardMixin, ApiMixin} from "@/utils/utils"; import {StandardToasts, ToastMixin} from "@/utils/utils"; -import GenericSplitLists from "@/components/GenericSplitLists"; +import GenericInfiniteCards from "@/components/GenericInfiniteCards"; import GenericHorizontalCard from "@/components/GenericHorizontalCard"; import GenericModalForm from "@/components/Modals/GenericModalForm"; +import ModelMenu from "@/components/ModelMenu"; Vue.use(BootstrapVue) @@ -57,7 +101,7 @@ export default { // or i'm capturing it incorrectly name: 'ModelListView', mixins: [CardMixin, ApiMixin, ToastMixin], - components: {GenericHorizontalCard, GenericSplitLists, GenericModalForm}, + components: {GenericHorizontalCard, GenericModalForm, GenericInfiniteCards, ModelMenu}, data() { return { // this.Models and this.Actions inherited from ApiMixin @@ -66,11 +110,14 @@ export default { right_counts: {'max': 9999, 'current': 0}, left_counts: {'max': 9999, 'current': 0}, this_model: undefined, + model_menu: undefined, this_action: undefined, this_recipe_param: undefined, this_item: {}, this_target: {}, - show_modal: false + show_modal: false, + show_split: false, + paginated: false, } }, mounted() { @@ -78,6 +125,13 @@ export default { 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.$nextTick(() => { + if (!this.paginated) { + this.getItems() + } + }) + }, methods: { // this.genericAPI inherited from ApiMixin @@ -165,13 +219,14 @@ export default { } this.clearState() }, - getItems: function (params) { - let column = params?.column ?? 'left' + getItems: function (params, col) { + let column = col || 'left' this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => { - if (result.data.results.length) { - this['items_' + column] = this['items_' + column].concat(result.data?.results) - this[column + '_counts']['max'] = result.data.count - this[column + '_counts']['current'] = this['items_' + column].length + let results = result.data?.results ?? result.data + if (results?.length) { + this['items_' + column] = this['items_' + column].concat(results) + this[column + '_counts']['max'] = result.data?.count ?? 0 + this[column + '_counts']['current'] = this['items_' + column]?.length } else { this[column + '_counts']['max'] = 0 this[column + '_counts']['current'] = 0 diff --git a/vue/src/apps/RecipeView/RecipeView.vue b/vue/src/apps/RecipeView/RecipeView.vue index 74dc0c11..fcd063b1 100644 --- a/vue/src/apps/RecipeView/RecipeView.vue +++ b/vue/src/apps/RecipeView/RecipeView.vue @@ -155,7 +155,6 @@ + + diff --git a/vue/src/components/GenericPill.vue b/vue/src/components/GenericPill.vue new file mode 100644 index 00000000..c443652a --- /dev/null +++ b/vue/src/components/GenericPill.vue @@ -0,0 +1,44 @@ + + + diff --git a/vue/src/components/Ingredient.vue b/vue/src/components/Ingredient.vue index 0ee0c471..8a81e595 100644 --- a/vue/src/components/Ingredient.vue +++ b/vue/src/components/Ingredient.vue @@ -26,8 +26,11 @@
- + + + + {{ ingredient.note }}
@@ -72,3 +75,13 @@ export default { } } + + diff --git a/vue/src/components/ModelMenu.vue b/vue/src/components/ModelMenu.vue new file mode 100644 index 00000000..d22779d3 --- /dev/null +++ b/vue/src/components/ModelMenu.vue @@ -0,0 +1,61 @@ + + + \ No newline at end of file diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index 896baa24..41f114b7 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -12,7 +12,7 @@ "all_fields_optional": "All fields are optional and can be left empty.", "convert_internal": "Convert to internal recipe", "show_only_internal": "Show only internal recipes", - "show_split_screen": "Show split view", + "show_split_screen": "Split View", "Log_Recipe_Cooking": "Log Recipe Cooking", "External_Recipe_Image": "External Recipe Image", diff --git a/vue/src/utils/models.js b/vue/src/utils/models.js index f5fbbdc0..9acaee25 100644 --- a/vue/src/utils/models.js +++ b/vue/src/utils/models.js @@ -62,9 +62,13 @@ export class Models { 'name': i18n.t('Food'), // *OPTIONAL* : parameters will be built model -> model_type -> default 'apiName': 'Food', // *REQUIRED* : the name that is used in api.ts for this model 'model_type': this.TREE, // *OPTIONAL* : model specific params for api, if not present will attempt modeltype_create then default_create + 'paginated': true, + 'move': true, + 'merge': true, 'badges': { - 'linked_recipe': true + 'linked_recipe': true, }, + 'tags': [{'field': 'supermarket_category', 'label': 'name', 'color': 'info'}], // REQUIRED: unordered array of fields that can be set during create 'create': { // if not defined partialUpdate will use the same parameters, prepending 'id' @@ -113,6 +117,9 @@ export class Models { 'name': i18n.t('Keyword'), // *OPTIONAL: parameters will be built model -> model_type -> default 'apiName': 'Keyword', 'model_type': this.TREE, + 'paginated': true, + 'move': true, + 'merge': true, 'badges': { 'icon': true }, @@ -146,6 +153,7 @@ export class Models { static UNIT = { 'name': i18n.t('Unit'), 'apiName': 'Unit', + 'paginated': true, 'create': { 'params': [['name', 'description']], 'form': { @@ -165,7 +173,7 @@ export class Models { } } }, - 'move': false + 'merge': true } static SHOPPING_LIST = {} static RECIPE_BOOK = { @@ -220,6 +228,53 @@ export class Models { } }, } + static SHOPPING_CATEGORY_RELATION = { + 'name': i18n.t('Shopping_Category'), + 'apiName': 'SupermarketCategory', + 'create': { + 'params': [['category', 'supermarket', 'order']], + 'form': { + 'name': { + 'form_field': true, + 'type': 'text', + 'field': 'name', + 'label': i18n.t('Name'), + 'placeholder': '' + }, + 'description': { + 'form_field': true, + 'type': 'text', + 'field': 'description', + 'label': i18n.t('Description'), + 'placeholder': '' + } + } + }, + } + static SUPERMARKET = { + 'name': i18n.t('Supermarket'), + 'apiName': 'Supermarket', + 'tags': [{'field': 'category_to_supermarket', 'label': 'category::name', 'color': 'info'}], + 'create': { + 'params': [['name', 'description', 'category_to_supermarket']], + 'form': { + 'name': { + 'form_field': true, + 'type': 'text', + 'field': 'name', + 'label': i18n.t('Name'), + 'placeholder': '' + }, + 'description': { + 'form_field': true, + 'type': 'text', + 'field': 'description', + 'label': i18n.t('Description'), + 'placeholder': '' + }, + } + }, + } static RECIPE = { 'name': i18n.t('Recipe'),