diff --git a/cookbook/serializer.py b/cookbook/serializer.py
index 42cc99357..f0090b4ba 100644
--- a/cookbook/serializer.py
+++ b/cookbook/serializer.py
@@ -908,12 +908,12 @@ class CommentSerializer(serializers.ModelSerializer):
class RecipeOverviewSerializer(RecipeBaseSerializer):
- keywords = KeywordLabelSerializer(many=True)
- new = serializers.SerializerMethodField('is_recipe_new')
+ keywords = KeywordLabelSerializer(many=True, read_only=True)
+ new = serializers.SerializerMethodField('is_recipe_new', read_only=True)
recent = serializers.ReadOnlyField()
- rating = CustomDecimalField(required=False, allow_null=True)
- last_cooked = serializers.DateTimeField(required=False, allow_null=True)
+ rating = CustomDecimalField(required=False, allow_null=True, read_only=True)
+ last_cooked = serializers.DateTimeField(required=False, allow_null=True, read_only=True)
def create(self, validated_data):
pass
@@ -928,7 +928,9 @@ class RecipeOverviewSerializer(RecipeBaseSerializer):
'waiting_time', 'created_by', 'created_at', 'updated_at',
'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent'
)
- read_only_fields = ['image', 'created_by', 'created_at']
+ read_only_fields = ['id', 'name', 'description', 'image', 'keywords', 'working_time',
+ 'waiting_time', 'created_by', 'created_at', 'updated_at',
+ 'internal', 'servings', 'servings_text', 'rating', 'last_cooked', 'new', 'recent']
class RecipeSerializer(RecipeBaseSerializer):
diff --git a/cookbook/views/api.py b/cookbook/views/api.py
index b41f76d82..9c0f96e22 100644
--- a/cookbook/views/api.py
+++ b/cookbook/views/api.py
@@ -534,6 +534,17 @@ class SupermarketCategoryRelationViewSet(StandardFilterModelViewSet):
return super().get_queryset()
+# TODO make TreeMixin a view type and move schema to view type
+@extend_schema_view(
+ list=extend_schema(
+ parameters=[
+ OpenApiParameter(name='query', description='lookup if query string is contained within the name, case insensitive', type=str),
+ OpenApiParameter(name='updated_at', description='if model has an updated_at timestamp, filter only models updated at or after datetime', type=str), # TODO format hint
+ OpenApiParameter(name='limit', description='limit number of entries to return', type=str),
+ OpenApiParameter(name='random', description='randomly orders entries (only works together with limit)', type=str),
+ ]
+ )
+)
class KeywordViewSet(viewsets.ModelViewSet, TreeMixin):
queryset = Keyword.objects
model = Keyword
@@ -542,7 +553,7 @@ class KeywordViewSet(viewsets.ModelViewSet, TreeMixin):
pagination_class = DefaultPagination
-class UnitViewSet(viewsets.ModelViewSet, MergeMixin, FuzzyFilterMixin):
+class UnitViewSet(StandardFilterModelViewSet, MergeMixin):
queryset = Unit.objects
model = Unit
serializer_class = UnitSerializer
diff --git a/vue3/package.json b/vue3/package.json
index bb0f8448a..953ba59d2 100644
--- a/vue3/package.json
+++ b/vue3/package.json
@@ -16,6 +16,7 @@
"mavon-editor": "^3.0.1",
"pinia": "^2.1.7",
"vue": "^3.4.15",
+ "vue-multiselect": "^3.0.0-beta.3",
"vue-router": "4",
"vuedraggable": "^4.1.0",
"vuetify": "^3.5.8"
diff --git a/vue3/src/components/inputs/ModelSelect.vue b/vue3/src/components/inputs/ModelSelect.vue
index 1c51909b2..78db3f463 100644
--- a/vue3/src/components/inputs/ModelSelect.vue
+++ b/vue3/src/components/inputs/ModelSelect.vue
@@ -1,87 +1,65 @@
-
-
+
+
+
+
-
-
-
- Press enter to create "{{ search_query }}"
-
-
-
-
-
-
- {{ item.title }}
-
-
-
-
- {{ item.title }}
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/vue3/src/components/inputs/ModelSelectVuetify.vue b/vue3/src/components/inputs/ModelSelectVuetify.vue
new file mode 100644
index 000000000..6b32e4fd3
--- /dev/null
+++ b/vue3/src/components/inputs/ModelSelectVuetify.vue
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+ Press enter to create "{{ search_query }}"
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vue3/src/components/inputs/StepEditor.vue b/vue3/src/components/inputs/StepEditor.vue
index 29acaea22..566ef0acf 100644
--- a/vue3/src/components/inputs/StepEditor.vue
+++ b/vue3/src/components/inputs/StepEditor.vue
@@ -16,7 +16,10 @@
label="Step Name"
>
- Time
+ Time
+ Instructions
+ File
+ Recipe
@@ -34,7 +37,13 @@
+
+
+
@@ -87,10 +96,11 @@ import StepMarkdownEditor from "@/components/inputs/StepMarkdownEditor.vue";
import IngredientsTable from "@/components/display/IngredientsTable.vue";
import IngredientsTableRow from "@/components/display/IngredientsTableRow.vue";
import draggable from "vuedraggable";
+import ModelSelect from "@/components/inputs/ModelSelect.vue";
export default defineComponent({
name: "StepEditor",
- components: {draggable, IngredientsTableRow, IngredientsTable, StepMarkdownEditor},
+ components: {ModelSelect, draggable, IngredientsTableRow, IngredientsTable, StepMarkdownEditor},
emits: ['update:modelValue'],
props: {
modelValue: {
diff --git a/vue3/src/openapi/apis/ApiApi.ts b/vue3/src/openapi/apis/ApiApi.ts
index 9f633233e..8c9bfb70e 100644
--- a/vue3/src/openapi/apis/ApiApi.ts
+++ b/vue3/src/openapi/apis/ApiApi.ts
@@ -715,8 +715,12 @@ export interface ApiKeywordDestroyRequest {
}
export interface ApiKeywordListRequest {
+ limit?: string;
page?: number;
pageSize?: number;
+ query?: string;
+ random?: string;
+ updatedAt?: string;
}
export interface ApiKeywordMergeUpdateRequest {
@@ -953,6 +957,11 @@ export interface ApiOpenDataVersionUpdateRequest {
openDataVersion: OpenDataVersion;
}
+export interface ApiPlanIcalRetrieveRequest {
+ fromDate: string;
+ toDate: string;
+}
+
export interface ApiRecipeBookCreateRequest {
recipeBook: RecipeBook;
}
@@ -1349,8 +1358,12 @@ export interface ApiUnitDestroyRequest {
}
export interface ApiUnitListRequest {
+ limit?: string;
page?: number;
pageSize?: number;
+ query?: string;
+ random?: string;
+ updatedAt?: string;
}
export interface ApiUnitMergeUpdateRequest {
@@ -4795,6 +4808,10 @@ export class ApiApi extends runtime.BaseAPI {
async apiKeywordListRaw(requestParameters: ApiKeywordListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> {
const queryParameters: any = {};
+ if (requestParameters['limit'] != null) {
+ queryParameters['limit'] = requestParameters['limit'];
+ }
+
if (requestParameters['page'] != null) {
queryParameters['page'] = requestParameters['page'];
}
@@ -4803,6 +4820,18 @@ export class ApiApi extends runtime.BaseAPI {
queryParameters['page_size'] = requestParameters['pageSize'];
}
+ if (requestParameters['query'] != null) {
+ queryParameters['query'] = requestParameters['query'];
+ }
+
+ if (requestParameters['random'] != null) {
+ queryParameters['random'] = requestParameters['random'];
+ }
+
+ if (requestParameters['updatedAt'] != null) {
+ queryParameters['updated_at'] = requestParameters['updatedAt'];
+ }
+
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) {
@@ -7047,7 +7076,21 @@ export class ApiApi extends runtime.BaseAPI {
/**
*/
- async apiPlanIcalRetrieveRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> {
+ async apiPlanIcalRetrieveRaw(requestParameters: ApiPlanIcalRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> {
+ if (requestParameters['fromDate'] == null) {
+ throw new runtime.RequiredError(
+ 'fromDate',
+ 'Required parameter "fromDate" was null or undefined when calling apiPlanIcalRetrieve().'
+ );
+ }
+
+ if (requestParameters['toDate'] == null) {
+ throw new runtime.RequiredError(
+ 'toDate',
+ 'Required parameter "toDate" was null or undefined when calling apiPlanIcalRetrieve().'
+ );
+ }
+
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
@@ -7056,7 +7099,7 @@ export class ApiApi extends runtime.BaseAPI {
headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password);
}
const response = await this.request({
- path: `/api/plan-ical/`,
+ path: `/api/plan-ical/{from_date}/{to_date}/`.replace(`{${"from_date"}}`, encodeURIComponent(String(requestParameters['fromDate']))).replace(`{${"to_date"}}`, encodeURIComponent(String(requestParameters['toDate']))),
method: 'GET',
headers: headerParameters,
query: queryParameters,
@@ -7067,8 +7110,8 @@ export class ApiApi extends runtime.BaseAPI {
/**
*/
- async apiPlanIcalRetrieve(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise {
- await this.apiPlanIcalRetrieveRaw(initOverrides);
+ async apiPlanIcalRetrieve(requestParameters: ApiPlanIcalRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise {
+ await this.apiPlanIcalRetrieveRaw(requestParameters, initOverrides);
}
/**
@@ -10440,6 +10483,10 @@ export class ApiApi extends runtime.BaseAPI {
async apiUnitListRaw(requestParameters: ApiUnitListRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> {
const queryParameters: any = {};
+ if (requestParameters['limit'] != null) {
+ queryParameters['limit'] = requestParameters['limit'];
+ }
+
if (requestParameters['page'] != null) {
queryParameters['page'] = requestParameters['page'];
}
@@ -10448,6 +10495,18 @@ export class ApiApi extends runtime.BaseAPI {
queryParameters['page_size'] = requestParameters['pageSize'];
}
+ if (requestParameters['query'] != null) {
+ queryParameters['query'] = requestParameters['query'];
+ }
+
+ if (requestParameters['random'] != null) {
+ queryParameters['random'] = requestParameters['random'];
+ }
+
+ if (requestParameters['updatedAt'] != null) {
+ queryParameters['updated_at'] = requestParameters['updatedAt'];
+ }
+
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) {
diff --git a/vue3/src/openapi/models/RecipeOverview.ts b/vue3/src/openapi/models/RecipeOverview.ts
index 7474562d4..b9cfcd393 100644
--- a/vue3/src/openapi/models/RecipeOverview.ts
+++ b/vue3/src/openapi/models/RecipeOverview.ts
@@ -37,13 +37,13 @@ export interface RecipeOverview {
* @type {string}
* @memberof RecipeOverview
*/
- name: string;
+ readonly name: string;
/**
*
* @type {string}
* @memberof RecipeOverview
*/
- description?: string;
+ readonly description: string | null;
/**
*
* @type {string}
@@ -55,19 +55,19 @@ export interface RecipeOverview {
* @type {Array}
* @memberof RecipeOverview
*/
- keywords: Array;
+ readonly keywords: Array;
/**
*
* @type {number}
* @memberof RecipeOverview
*/
- workingTime?: number;
+ readonly workingTime: number;
/**
*
* @type {number}
* @memberof RecipeOverview
*/
- waitingTime?: number;
+ readonly waitingTime: number;
/**
*
* @type {number}
@@ -91,31 +91,31 @@ export interface RecipeOverview {
* @type {boolean}
* @memberof RecipeOverview
*/
- internal?: boolean;
+ readonly internal: boolean;
/**
*
* @type {number}
* @memberof RecipeOverview
*/
- servings?: number;
+ readonly servings: number;
/**
*
* @type {string}
* @memberof RecipeOverview
*/
- servingsText?: string;
+ readonly servingsText: string;
/**
*
* @type {string}
* @memberof RecipeOverview
*/
- rating?: string;
+ readonly rating: string | null;
/**
*
* @type {Date}
* @memberof RecipeOverview
*/
- lastCooked?: Date;
+ readonly lastCooked: Date | null;
/**
*
* @type {string}
@@ -136,11 +136,19 @@ export interface RecipeOverview {
export function instanceOfRecipeOverview(value: object): boolean {
if (!('id' in value)) return false;
if (!('name' in value)) return false;
+ if (!('description' in value)) return false;
if (!('image' in value)) return false;
if (!('keywords' in value)) return false;
+ if (!('workingTime' in value)) return false;
+ if (!('waitingTime' in value)) return false;
if (!('createdBy' in value)) return false;
if (!('createdAt' in value)) return false;
if (!('updatedAt' in value)) return false;
+ if (!('internal' in value)) return false;
+ if (!('servings' in value)) return false;
+ if (!('servingsText' in value)) return false;
+ if (!('rating' in value)) return false;
+ if (!('lastCooked' in value)) return false;
if (!('_new' in value)) return false;
if (!('recent' in value)) return false;
return true;
@@ -158,19 +166,19 @@ export function RecipeOverviewFromJSONTyped(json: any, ignoreDiscriminator: bool
'id': json['id'],
'name': json['name'],
- 'description': json['description'] == null ? undefined : json['description'],
+ 'description': json['description'],
'image': json['image'],
'keywords': ((json['keywords'] as Array).map(KeywordLabelFromJSON)),
- 'workingTime': json['working_time'] == null ? undefined : json['working_time'],
- 'waitingTime': json['waiting_time'] == null ? undefined : json['waiting_time'],
+ 'workingTime': json['working_time'],
+ 'waitingTime': json['waiting_time'],
'createdBy': json['created_by'],
'createdAt': (new Date(json['created_at'])),
'updatedAt': (new Date(json['updated_at'])),
- 'internal': json['internal'] == null ? undefined : json['internal'],
- 'servings': json['servings'] == null ? undefined : json['servings'],
- 'servingsText': json['servings_text'] == null ? undefined : json['servings_text'],
- 'rating': json['rating'] == null ? undefined : json['rating'],
- 'lastCooked': json['last_cooked'] == null ? undefined : (new Date(json['last_cooked'])),
+ 'internal': json['internal'],
+ 'servings': json['servings'],
+ 'servingsText': json['servings_text'],
+ 'rating': json['rating'],
+ 'lastCooked': (json['last_cooked'] == null ? null : new Date(json['last_cooked'])),
'_new': json['new'],
'recent': json['recent'],
};
@@ -182,16 +190,6 @@ export function RecipeOverviewToJSON(value?: RecipeOverview | null): any {
}
return {
- 'name': value['name'],
- 'description': value['description'],
- 'keywords': ((value['keywords'] as Array).map(KeywordLabelToJSON)),
- 'working_time': value['workingTime'],
- 'waiting_time': value['waitingTime'],
- 'internal': value['internal'],
- 'servings': value['servings'],
- 'servings_text': value['servingsText'],
- 'rating': value['rating'],
- 'last_cooked': value['lastCooked'] == null ? undefined : ((value['lastCooked'] as any).toISOString()),
};
}
diff --git a/vue3/src/pages/MealPlanPage.vue b/vue3/src/pages/MealPlanPage.vue
index b69686ee4..f384aba17 100644
--- a/vue3/src/pages/MealPlanPage.vue
+++ b/vue3/src/pages/MealPlanPage.vue
@@ -1,23 +1,36 @@
+
+
+
+
+
+ multiple food
+
+ single food
+
+ multiple keyowrd
+
-
+
+
+
+
+
+
-
-
diff --git a/vue3/src/pages/RecipeEditPage.vue b/vue3/src/pages/RecipeEditPage.vue
index 77a5c9981..63a1c24cb 100644
--- a/vue3/src/pages/RecipeEditPage.vue
+++ b/vue3/src/pages/RecipeEditPage.vue
@@ -110,7 +110,9 @@ export default defineComponent({
const api = new ApiApi()
api.apiKeywordList({page: 1, pageSize: 100}).then(r => {
- this.keywords = r.results
+ if(r.results){
+ this.keywords = r.results
+ }
})
},
methods: {
diff --git a/vue3/src/types/Models.ts b/vue3/src/types/Models.ts
new file mode 100644
index 000000000..6c54a1f56
--- /dev/null
+++ b/vue3/src/types/Models.ts
@@ -0,0 +1,102 @@
+import {ApiApi, Keyword as IKeyword, Food as IFood, RecipeOverview as IRecipeOverview, Recipe as IRecipe, Unit as IUnit} from "@/openapi";
+
+export function getModelFromStr(model_name: String) {
+ switch (model_name.toLowerCase()) {
+ case 'food': {
+ return new Food
+ }
+ case 'unit': {
+ return new Unit
+ }
+ case 'keyword': {
+ return new Keyword
+ }
+ case 'recipe': {
+ return new Recipe
+ }
+ default: {
+ throw Error(`Invalid Model ${model_name}, did you forget to register it in Models.ts?`)
+ }
+ }
+}
+
+export abstract class GenericModel {
+ abstract list(query: string): Promise>
+
+ abstract create(name: string): Promise
+}
+
+//TODO this can probably be achieved by manipulating the client generation https://openapi-generator.tech/docs/templating/#models
+export class Keyword extends GenericModel {
+ create(name: string) {
+ const api = new ApiApi()
+ return api.apiKeywordCreate({keyword: {name: name} as IKeyword})
+ }
+
+ list(query: string) {
+ const api = new ApiApi()
+ return api.apiKeywordList({query: query}).then(r => {
+ if (r.results) {
+ return r.results
+ } else {
+ return []
+ }
+ })
+ }
+}
+
+export class Food extends GenericModel {
+ create(name: string) {
+ const api = new ApiApi()
+ return api.apiFoodCreate({food: {name: name} as IFood})
+ }
+
+ list(query: string) {
+ const api = new ApiApi()
+ return api.apiFoodList({query: query}).then(r => {
+ if (r.results) {
+ return r.results
+ } else {
+ return []
+ }
+ })
+ }
+}
+
+export class Unit extends GenericModel {
+ create(name: string) {
+ const api = new ApiApi()
+ return api.apiUnitCreate({unit: {name: name} as IUnit})
+ }
+
+ list(query: string) {
+ const api = new ApiApi()
+ return api.apiUnitList({query: query}).then(r => {
+ if (r.results) {
+ return r.results
+ } else {
+ return []
+ }
+ })
+ }
+}
+
+export class Recipe extends GenericModel {
+ create(name: string) {
+ const api = new ApiApi()
+ return api.apiRecipeCreate({recipe: {name: name} as IRecipe}).then(r => {
+ return r as unknown as IRecipeOverview
+ })
+ }
+
+ list(query: string) {
+ const api = new ApiApi()
+ return api.apiRecipeList({query: query}).then(r => {
+ if (r.results) {
+ return r.results
+ } else {
+ return []
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/vue3/src/vuetify.ts b/vue3/src/vuetify.ts
index 23fc01714..916179b29 100644
--- a/vue3/src/vuetify.ts
+++ b/vue3/src/vuetify.ts
@@ -7,6 +7,11 @@ import {createVuetify} from 'vuetify'
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
+ defaults: {
+ VCard: {
+ class: 'overflow-visible' // this is needed so that vue-multiselect options show above a card, vuetify uses overlay container to avoid this
+ }
+ },
theme: {
defaultTheme: 'light',
themes: {
diff --git a/vue3/tsconfig.app.json b/vue3/tsconfig.app.json
index ff68a8e4f..283aac7f2 100644
--- a/vue3/tsconfig.app.json
+++ b/vue3/tsconfig.app.json
@@ -1,6 +1,6 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
- "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+ "include": ["env.d.ts", "src/**/*", "src/**/*.vue","src/**/*.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
diff --git a/vue3/yarn.lock b/vue3/yarn.lock
index fd25408c6..6d4a414db 100644
--- a/vue3/yarn.lock
+++ b/vue3/yarn.lock
@@ -889,6 +889,11 @@ vue-demi@>=0.14.5, vue-demi@>=0.14.7:
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.7.tgz#8317536b3ef74c5b09f268f7782e70194567d8f2"
integrity sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==
+vue-multiselect@^3.0.0-beta.3:
+ version "3.0.0-beta.3"
+ resolved "https://registry.yarnpkg.com/vue-multiselect/-/vue-multiselect-3.0.0-beta.3.tgz#b1348238a84c435582c3f46f2a9c045b29bb976c"
+ integrity sha512-P7Fx+ovVF7WMERSZ0lw6N3p4H4bnQ3NcaY3ORjzFPv0r/6lpIqvFWmK9Xnwze9mgAvmNV1foI1VWrBmjnfBTLQ==
+
vue-router@4:
version "4.2.5"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.5.tgz#b9e3e08f1bd9ea363fdd173032620bc50cf0e98a"