importer stuff

This commit is contained in:
vabene1111
2024-12-12 17:26:01 +01:00
parent 37761bf6e7
commit 325ee16d39
12 changed files with 205 additions and 1913 deletions

View File

@@ -106,14 +106,14 @@ def get_from_scraper(scrape, request):
# assign image
try:
recipe_json['image'] = parse_image(scrape.image()) or None
recipe_json['image_url'] = parse_image(scrape.image()) or None
except Exception:
recipe_json['image'] = None
if not recipe_json['image']:
recipe_json['image_url'] = None
if not recipe_json['image_url']:
try:
recipe_json['image'] = parse_image(scrape.schema.data.get('image')) or ''
recipe_json['image_url'] = parse_image(scrape.schema.data.get('image')) or ''
except Exception:
recipe_json['image'] = ''
recipe_json['image_url'] = ''
# assign keywords
try:

View File

@@ -1609,7 +1609,7 @@ class SourceImportRecipeSerializer(serializers.Serializer):
servings_text = serializers.CharField()
working_time = serializers.IntegerField()
waiting_time = serializers.IntegerField()
image = serializers.URLField()
image_url = serializers.URLField()
keywords = SourceImportKeywordSerializer(many=True)
properties = serializers.ListField(child=SourceImportPropertySerializer())

View File

@@ -7,8 +7,6 @@ models/AccessToken.ts
models/AuthToken.ts
models/AutoMealPlan.ts
models/Automation.ts
models/AutomationTypeEnum.ts
models/BaseUnitEnum.ts
models/BookmarkletImport.ts
models/BookmarkletImportList.ts
models/ConnectorConfigConfig.ts
@@ -34,16 +32,6 @@ models/MealPlan.ts
models/MealType.ts
models/MethodEnum.ts
models/NutritionInformation.ts
models/OpenDataCategory.ts
models/OpenDataConversion.ts
models/OpenDataFood.ts
models/OpenDataFoodProperty.ts
models/OpenDataProperty.ts
models/OpenDataStore.ts
models/OpenDataStoreCategory.ts
models/OpenDataUnit.ts
models/OpenDataUnitTypeEnum.ts
models/OpenDataVersion.ts
models/PaginatedAutomationList.ts
models/PaginatedBookmarkletImportListList.ts
models/PaginatedCookLogList.ts
@@ -90,13 +78,6 @@ models/PatchedInviteLink.ts
models/PatchedKeyword.ts
models/PatchedMealPlan.ts
models/PatchedMealType.ts
models/PatchedOpenDataCategory.ts
models/PatchedOpenDataConversion.ts
models/PatchedOpenDataFood.ts
models/PatchedOpenDataProperty.ts
models/PatchedOpenDataStore.ts
models/PatchedOpenDataUnit.ts
models/PatchedOpenDataVersion.ts
models/PatchedProperty.ts
models/PatchedPropertyType.ts
models/PatchedRecipe.ts
@@ -154,6 +135,7 @@ models/SupermarketCategoryRelation.ts
models/Sync.ts
models/SyncLog.ts
models/ThemeEnum.ts
models/TypeEnum.ts
models/Unit.ts
models/UnitConversion.ts
models/User.ts

File diff suppressed because it is too large Load Diff

View File

@@ -13,13 +13,13 @@
*/
import { mapValues } from '../runtime';
import type { AutomationTypeEnum } from './AutomationTypeEnum';
import type { TypeEnum } from './TypeEnum';
import {
AutomationTypeEnumFromJSON,
AutomationTypeEnumFromJSONTyped,
AutomationTypeEnumToJSON,
AutomationTypeEnumToJSONTyped,
} from './AutomationTypeEnum';
TypeEnumFromJSON,
TypeEnumFromJSONTyped,
TypeEnumToJSON,
TypeEnumToJSONTyped,
} from './TypeEnum';
/**
*
@@ -35,10 +35,10 @@ export interface Automation {
id?: number;
/**
*
* @type {AutomationTypeEnum}
* @type {TypeEnum}
* @memberof Automation
*/
type: AutomationTypeEnum;
type: TypeEnum;
/**
*
* @type {string}
@@ -111,7 +111,7 @@ export function AutomationFromJSONTyped(json: any, ignoreDiscriminator: boolean)
return {
'id': json['id'] == null ? undefined : json['id'],
'type': AutomationTypeEnumFromJSON(json['type']),
'type': TypeEnumFromJSON(json['type']),
'name': json['name'] == null ? undefined : json['name'],
'description': json['description'] == null ? undefined : json['description'],
'param1': json['param_1'] == null ? undefined : json['param_1'],
@@ -135,7 +135,7 @@ export function AutomationToJSONTyped(value?: Omit<Automation, 'created_by'> | n
return {
'id': value['id'],
'type': AutomationTypeEnumToJSON(value['type']),
'type': TypeEnumToJSON(value['type']),
'name': value['name'],
'description': value['description'],
'param_1': value['param1'],

View File

@@ -13,13 +13,13 @@
*/
import { mapValues } from '../runtime';
import type { AutomationTypeEnum } from './AutomationTypeEnum';
import type { TypeEnum } from './TypeEnum';
import {
AutomationTypeEnumFromJSON,
AutomationTypeEnumFromJSONTyped,
AutomationTypeEnumToJSON,
AutomationTypeEnumToJSONTyped,
} from './AutomationTypeEnum';
TypeEnumFromJSON,
TypeEnumFromJSONTyped,
TypeEnumToJSON,
TypeEnumToJSONTyped,
} from './TypeEnum';
/**
*
@@ -35,10 +35,10 @@ export interface PatchedAutomation {
id?: number;
/**
*
* @type {AutomationTypeEnum}
* @type {TypeEnum}
* @memberof PatchedAutomation
*/
type?: AutomationTypeEnum;
type?: TypeEnum;
/**
*
* @type {string}
@@ -109,7 +109,7 @@ export function PatchedAutomationFromJSONTyped(json: any, ignoreDiscriminator: b
return {
'id': json['id'] == null ? undefined : json['id'],
'type': json['type'] == null ? undefined : AutomationTypeEnumFromJSON(json['type']),
'type': json['type'] == null ? undefined : TypeEnumFromJSON(json['type']),
'name': json['name'] == null ? undefined : json['name'],
'description': json['description'] == null ? undefined : json['description'],
'param1': json['param_1'] == null ? undefined : json['param_1'],
@@ -133,7 +133,7 @@ export function PatchedAutomationToJSONTyped(value?: Omit<PatchedAutomation, 'cr
return {
'id': value['id'],
'type': AutomationTypeEnumToJSON(value['type']),
'type': TypeEnumToJSON(value['type']),
'name': value['name'],
'description': value['description'],
'param_1': value['param1'],

View File

@@ -100,7 +100,7 @@ export interface SourceImportRecipe {
* @type {string}
* @memberof SourceImportRecipe
*/
image: string;
imageUrl: string;
/**
*
* @type {Array<SourceImportKeyword>}
@@ -128,7 +128,7 @@ export function instanceOfSourceImportRecipe(value: object): value is SourceImpo
if (!('servingsText' in value) || value['servingsText'] === undefined) return false;
if (!('workingTime' in value) || value['workingTime'] === undefined) return false;
if (!('waitingTime' in value) || value['waitingTime'] === undefined) return false;
if (!('image' in value) || value['image'] === undefined) return false;
if (!('imageUrl' in value) || value['imageUrl'] === undefined) return false;
if (!('keywords' in value) || value['keywords'] === undefined) return false;
if (!('properties' in value) || value['properties'] === undefined) return false;
return true;
@@ -153,7 +153,7 @@ export function SourceImportRecipeFromJSONTyped(json: any, ignoreDiscriminator:
'servingsText': json['servings_text'],
'workingTime': json['working_time'],
'waitingTime': json['waiting_time'],
'image': json['image'],
'imageUrl': json['image_url'],
'keywords': ((json['keywords'] as Array<any>).map(SourceImportKeywordFromJSON)),
'properties': ((json['properties'] as Array<any>).map(SourceImportPropertyFromJSON)),
};
@@ -179,7 +179,7 @@ export function SourceImportRecipeToJSONTyped(value?: SourceImportRecipe | null,
'servings_text': value['servingsText'],
'working_time': value['workingTime'],
'waiting_time': value['waitingTime'],
'image': value['image'],
'image_url': value['imageUrl'],
'keywords': ((value['keywords'] as Array<any>).map(SourceImportKeywordToJSON)),
'properties': ((value['properties'] as Array<any>).map(SourceImportPropertyToJSON)),
};

View File

@@ -0,0 +1,70 @@
/* tslint:disable */
/* eslint-disable */
/**
* Tandoor
* Tandoor API Docs
*
* The version of the OpenAPI document: 0.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* * `FOOD_ALIAS` - Food Alias
* * `UNIT_ALIAS` - Unit Alias
* * `KEYWORD_ALIAS` - Keyword Alias
* * `DESCRIPTION_REPLACE` - Description Replace
* * `INSTRUCTION_REPLACE` - Instruction Replace
* * `NEVER_UNIT` - Never Unit
* * `TRANSPOSE_WORDS` - Transpose Words
* * `FOOD_REPLACE` - Food Replace
* * `UNIT_REPLACE` - Unit Replace
* * `NAME_REPLACE` - Name Replace
* @export
*/
export const TypeEnum = {
FoodAlias: 'FOOD_ALIAS',
UnitAlias: 'UNIT_ALIAS',
KeywordAlias: 'KEYWORD_ALIAS',
DescriptionReplace: 'DESCRIPTION_REPLACE',
InstructionReplace: 'INSTRUCTION_REPLACE',
NeverUnit: 'NEVER_UNIT',
TransposeWords: 'TRANSPOSE_WORDS',
FoodReplace: 'FOOD_REPLACE',
UnitReplace: 'UNIT_REPLACE',
NameReplace: 'NAME_REPLACE'
} as const;
export type TypeEnum = typeof TypeEnum[keyof typeof TypeEnum];
export function instanceOfTypeEnum(value: any): boolean {
for (const key in TypeEnum) {
if (Object.prototype.hasOwnProperty.call(TypeEnum, key)) {
if (TypeEnum[key as keyof typeof TypeEnum] === value) {
return true;
}
}
}
return false;
}
export function TypeEnumFromJSON(json: any): TypeEnum {
return TypeEnumFromJSONTyped(json, false);
}
export function TypeEnumFromJSONTyped(json: any, ignoreDiscriminator: boolean): TypeEnum {
return json as TypeEnum;
}
export function TypeEnumToJSON(value?: TypeEnum | null): any {
return value as any;
}
export function TypeEnumToJSONTyped(value: any, ignoreDiscriminator: boolean): TypeEnum {
return value as TypeEnum;
}

View File

@@ -4,8 +4,6 @@ export * from './AccessToken';
export * from './AuthToken';
export * from './AutoMealPlan';
export * from './Automation';
export * from './AutomationTypeEnum';
export * from './BaseUnitEnum';
export * from './BookmarkletImport';
export * from './BookmarkletImportList';
export * from './ConnectorConfigConfig';
@@ -31,16 +29,6 @@ export * from './MealPlan';
export * from './MealType';
export * from './MethodEnum';
export * from './NutritionInformation';
export * from './OpenDataCategory';
export * from './OpenDataConversion';
export * from './OpenDataFood';
export * from './OpenDataFoodProperty';
export * from './OpenDataProperty';
export * from './OpenDataStore';
export * from './OpenDataStoreCategory';
export * from './OpenDataUnit';
export * from './OpenDataUnitTypeEnum';
export * from './OpenDataVersion';
export * from './PaginatedAutomationList';
export * from './PaginatedBookmarkletImportListList';
export * from './PaginatedCookLogList';
@@ -87,13 +75,6 @@ export * from './PatchedInviteLink';
export * from './PatchedKeyword';
export * from './PatchedMealPlan';
export * from './PatchedMealType';
export * from './PatchedOpenDataCategory';
export * from './PatchedOpenDataConversion';
export * from './PatchedOpenDataFood';
export * from './PatchedOpenDataProperty';
export * from './PatchedOpenDataStore';
export * from './PatchedOpenDataUnit';
export * from './PatchedOpenDataVersion';
export * from './PatchedProperty';
export * from './PatchedPropertyType';
export * from './PatchedRecipe';
@@ -151,6 +132,7 @@ export * from './SupermarketCategoryRelation';
export * from './Sync';
export * from './SyncLog';
export * from './ThemeEnum';
export * from './TypeEnum';
export * from './Unit';
export * from './UnitConversion';
export * from './User';

View File

@@ -19,7 +19,7 @@
<v-stepper-window>
<v-stepper-window-item value="1">
<v-card>
<v-card :loading="loading">
<v-card-text>
<v-text-field :label="$t('Website') + ' (https://...)'" @paste="nextTick(loadRecipeFromUrl())" v-model="importUrl">
<template #append>
@@ -44,13 +44,13 @@
<v-row>
<v-col cols="12" md="6">
<h2 class="text-h5">{{ $t('Selected') }}</h2>
<v-img max-height="30vh" :src="importResponse.recipe.image"></v-img>
<v-img max-height="30vh" :src="importResponse.recipe.imageUrl"></v-img>
</v-col>
<v-col cols="12" md="6">
<h2 class="text-h5">{{ $t('Available') }}</h2>
<v-row dense>
<v-col cols="4" v-for="i in importResponse.images">
<v-img max-height="10vh" cover aspect-ratio="1" :src="i" @click="importResponse.recipe.image = i"></v-img>
<v-img max-height="10vh" cover aspect-ratio="1" :src="i" @click="importResponse.recipe.imageUrl = i"></v-img>
</v-col>
</v-row>
</v-col>
@@ -72,9 +72,9 @@
<v-row>
<v-col class="text-center">
<v-btn-group border divided>
<v-btn prepend-icon="fa-solid fa-shuffle" @click="autoSortIngredients()">Auto Sort</v-btn>
<v-btn prepend-icon="fa-solid fa-maximize" @click="splitAllSteps('\n')">Split All</v-btn>
<v-btn prepend-icon="fa-solid fa-minimize" @click="mergeAllSteps()">Merge All</v-btn>
<v-btn prepend-icon="fa-solid fa-shuffle" @click="autoSortIngredients()">{{ $t('Auto_Sort') }}</v-btn>
<v-btn prepend-icon="fa-solid fa-maximize" @click="splitAllSteps('\n')">{{ $t('Split') }}</v-btn>
<v-btn prepend-icon="fa-solid fa-minimize" @click="mergeAllSteps()">{{ $t('Merge') }}</v-btn>
</v-btn-group>
</v-col>
</v-row>
@@ -97,9 +97,12 @@
<v-row>
<v-col>
<v-list>
<v-list-item v-for="i in s.ingredients">
{{ i.amount }} {{ i.unit.name }} {{ i.food.name }}
</v-list-item>
<vue-draggable v-model="s.ingredients" group="ingredients" drag-class="drag-handle">
<v-list-item v-for="i in s.ingredients">
<v-icon size="small" class="drag-handle cursor-grab" icon="$dragHandle"></v-icon>
{{ i.amount }} {{ i.unit.name }} {{ i.food.name }}
</v-list-item>
</vue-draggable>
</v-list>
</v-col>
<v-col>
@@ -114,7 +117,10 @@
</v-stepper-window-item>
<v-stepper-window-item value="5">
<v-btn @click="createRecipeFromImport()">Import</v-btn>
<v-card :loading="loading">
<v-card-title></v-card-title>
<v-btn @click="createRecipeFromImport()">{{ $t('Import') }}</v-btn>
</v-card>
</v-stepper-window-item>
</v-stepper-window>
@@ -139,10 +145,12 @@ import {ApiApi, RecipeFromSourceResponse, SourceImportStep} from "@/openapi";
import {ErrorMessageType, MessageType, useMessageStore} from "@/stores/MessageStore";
import {useRouter} from "vue-router";
import {useUserPreferenceStore} from "@/stores/UserPreferenceStore";
import {VueDraggable} from "vue-draggable-plus";
const router = useRouter()
const stepper = ref("1")
const loading = ref(false)
const importUrl = ref("")
const importResponse = ref({} as RecipeFromSourceResponse)
@@ -152,6 +160,7 @@ const importResponse = ref({} as RecipeFromSourceResponse)
*/
function loadRecipeFromUrl() {
let api = new ApiApi()
loading.value = true
api.apiRecipeFromSourceCreate({recipeFromSource: {url: importUrl.value}}).then(r => {
importResponse.value = r
if (r.duplicates.length == 0) {
@@ -160,6 +169,8 @@ function loadRecipeFromUrl() {
}
}).catch(err => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
}).finally(() => {
loading.value = false
})
}
@@ -168,12 +179,21 @@ function loadRecipeFromUrl() {
*/
function createRecipeFromImport() {
let api = new ApiApi()
console.log(importResponse.value)
api.apiRecipeCreate({recipe: importResponse.value.recipe}).then(r => {
router.push({name: 'view_recipe', params: {id: r.id}})
}).catch(err => {
useMessageStore().addError(ErrorMessageType.CREATE_ERROR, err)
})
if (importResponse.value.recipe) {
loading.value = true
importResponse.value.recipe.keywords = importResponse.value.recipe.keywords.filter(k => k.importKeyword)
api.apiRecipeCreate({recipe: importResponse.value.recipe}).then(r => {
api.apiRecipeImageUpdate({id: r.id, imageUrl: importResponse.value.recipe?.imageUrl}).then(rI => {
router.push({name: 'view_recipe', params: {id: r.id}})
}).finally(() => {
loading.value = false
})
}).catch(err => {
useMessageStore().addError(ErrorMessageType.CREATE_ERROR, err)
})
}
}
/**
@@ -181,8 +201,8 @@ function createRecipeFromImport() {
* @param step step to delete
*/
function deleteStep(step: SourceImportStep) {
if(importResponse.value.recipe){
importResponse.value.recipe.steps.splice(importResponse.value.recipe.steps.findIndex(x => x === step),1)
if (importResponse.value.recipe) {
importResponse.value.recipe.steps.splice(importResponse.value.recipe.steps.findIndex(x => x === step), 1)
}
}
@@ -285,7 +305,7 @@ function autoSortIngredients() {
s.ingredients.push(i)
}
})
if(!found){
if (!found) {
importResponse.value.recipe!.steps[0].ingredients.push(i)
}
// TODO implement a new "second try" algorithm if no exact match was found

View File

@@ -1,43 +1,35 @@
<template>
<v-container class="ps-0 pe-0 pt-0">
<v-container :class="{'ps-0 pe-0 pt-0': mobile}">
<RecipeView :recipe="recipe"></RecipeView>
</v-container>
</template>
<script lang="ts">
import {defineComponent} from 'vue'
<script setup lang="ts">
import {defineComponent, onMounted, ref} from 'vue'
import {ApiApi, Recipe} from "@/openapi";
import RecipeView from "@/components/display/RecipeView.vue";
import {useDisplay} from "vuetify";
export default defineComponent({
name: "RecipeSearchPage",
components: {RecipeView},
watch: {
id: function (newValue) {
this.refreshData(newValue)
},
},
props: {
id: {type: String, required: true}
},
data() {
return {
recipe: {} as Recipe
}
},
mounted() {
this.refreshData(this.id)
},
methods: {
refreshData(recipeId: string) {
const api = new ApiApi()
api.apiRecipeRetrieve({id: Number(recipeId)}).then(r => {
this.recipe = r
})
}
}
const props = defineProps({
id: {type: String, required: true}
})
const {mobile} = useDisplay()
const recipe = ref({} as Recipe)
onMounted(() => {
refreshData(props.id)
})
function refreshData(recipeId: string) {
const api = new ApiApi()
api.apiRecipeRetrieve({id: Number(recipeId)}).then(r => {
recipe.value = r
})
}
</script>
<style scoped>

View File

@@ -1,3 +1,7 @@
import {getCookie} from "@/utils/cookie";
import {Recipe, RecipeFromJSON, RecipeImageFromJSON, UserFileFromJSON} from "@/openapi";
import {ErrorMessageType, PreparedMessage, useMessageStore} from "@/stores/MessageStore";
/**
* Gets a nested property of an object given a dot-notation path.
*
@@ -6,13 +10,42 @@
* @returns The value of the nested property, or `undefined` if not found.
*/
export function getNestedProperty(object: any, path: string): any {
const pathParts = path.split('.');
const pathParts = path.split('.');
return pathParts.reduce((obj, key) => {
if (obj && typeof obj === 'object') {
return obj[key]
} else {
return undefined;
}
}, object);
}
//TODO just some partial code
/**
* I currently don't know how to do this properly through the API client so this
* helper function uploads files for now
*/
export function uploadRecipeImage(recipeId: number, file: File) {
let formData = new FormData()
formData.append('image', file)
//TODO proper URL finding (sub path setups)
// TODO maybe better use existing URL clients response functions for parsing
fetch('/api/recipe/' + recipeId + '/image/', {
method: 'PUT',
headers: {'X-CSRFToken': getCookie('csrftoken')},
body: formData
}).then(r => {
r.json().then(r => {
return RecipeImageFromJSON(r)
})
}).catch(err => {
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
}).finally(() => {
})
return pathParts.reduce((obj, key) => {
if (obj && typeof obj === 'object') {
return obj[key]
} else {
return undefined;
}
}, object);
}