playing with FDC search integration

This commit is contained in:
vabene1111
2025-03-20 22:09:44 +01:00
parent c36eaf934f
commit dea4fa000e
30 changed files with 344 additions and 38 deletions

View File

@@ -1502,6 +1502,18 @@ class ServerSettingsSerializer(serializers.Serializer):
read_only_fields = '__ALL__'
class FdcQueryFoodsSerializer(serializers.Serializer):
fdcId = serializers.IntegerField()
description = serializers.CharField()
dataType = serializers.CharField()
class FdcQuerySerializer(serializers.Serializer):
totalHits = serializers.IntegerField()
currentPage = serializers.IntegerField()
totalPages = serializers.IntegerField()
foods = FdcQueryFoodsSerializer(many=True)
# Export/Import Serializers
class KeywordExportSerializer(KeywordSerializer):

View File

@@ -118,6 +118,7 @@ urlpatterns = [
path('api/recipe-from-source/', api.RecipeUrlImportView.as_view(), name='api_recipe_from_source'),
path('api/image-to-recipe/', api.ImageToRecipeView.as_view(), name='api_image_to_recipe'),
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),
path('api/fdc-search/', api.FdcSearchView.as_view(), name='api_fdc_search'),
path('api/share-link/<int:pk>', api.share_link, name='api_share_link'),
path('api/reset-food-inheritance/', api.reset_food_inheritance, name='api_reset_food_inheritance'),
path('api/switch-active-space/<int:space_id>/', api.switch_active_space, name='api_switch_active_space'),

View File

@@ -107,7 +107,7 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer, Au
SupermarketSerializer, SyncLogSerializer, SyncSerializer,
UnitConversionSerializer, UnitSerializer, UserFileSerializer, UserPreferenceSerializer,
UserSerializer, UserSpaceSerializer, ViewLogSerializer, ImportImageSerializer,
LocalizationSerializer, ServerSettingsSerializer, RecipeFromSourceResponseSerializer, ShoppingListEntryBulkCreateSerializer
LocalizationSerializer, ServerSettingsSerializer, RecipeFromSourceResponseSerializer, ShoppingListEntryBulkCreateSerializer, FdcQuerySerializer
)
from cookbook.version_info import TANDOOR_VERSION
from cookbook.views.import_export import get_integration
@@ -1936,6 +1936,38 @@ class AppImportView(APIView):
else:
return Response({'error': True, 'msg': form.errors}, status=status.HTTP_400_BAD_REQUEST)
class FdcSearchView(APIView):
permission_classes = [CustomIsUser & CustomTokenHasReadWriteScope]
@extend_schema(responses=FdcQuerySerializer(many=False),
parameters=[OpenApiParameter(name='query', type=str), OpenApiParameter(name='dataType', description='options: Branded,Foundation,Survey (FNDDS),SR Legacy', type=str, many=True)])
def get(self, request, format=None):
query = self.request.query_params.get('query', None)
if query is not None:
data_types = self.request.query_params.getlist('dataType', ['Foundation'])
response = requests.get(f'https://api.nal.usda.gov/fdc/v1/foods/search?api_key={FDC_API_KEY}&query={query}&dataType={",".join(data_types)}')
if response.status_code == 429:
return JsonResponse(
{
'msg':
'API Key Rate Limit reached/exceeded, see https://api.data.gov/docs/rate-limits/ for more information. \
Configure your key in Tandoor using environment FDC_API_KEY variable.'
},
status=429,
json_dumps_params={'indent': 4})
if response.status_code != 200:
return JsonResponse({
'msg': f'Error while requesting FDC data using url https://api.nal.usda.gov/fdc/v1/foods/search?api_key=*****&query={query}'},
status=response.status_code,
json_dumps_params={'indent': 4})
return Response(FdcQuerySerializer(context={'request': request}).to_representation(json.loads(response.content)), status=status.HTTP_200_OK)
@extend_schema(
request=None,
responses=None,

View File

@@ -0,0 +1,43 @@
<template>
<v-dialog max-width="900" v-model="dialog">
<v-card>
<v-closable-card-title :title="$t('Search')" icon="$search" v-model="dialog"></v-closable-card-title>
<v-card-text>
<v-text-field v-model="query">
<template #append>
<v-btn icon="$search" @click="fdcSearch()"></v-btn>
</template>
</v-text-field>
<v-list>
<v-list-item v-for="f in fdcQueryResults?.foods">{{ f}}</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
import {ref} from "vue";
import {ApiApi, FdcQuery} from "@/openapi";
const dialog = defineModel<boolean>({required: true})
const query = ref("")
const fdcQueryResults = ref<undefined|FdcQuery>(undefined)
function fdcSearch(){
let api = new ApiApi()
api.apiFdcSearchRetrieve({query: query.value}).then(r => {
fdcQueryResults.value = r
})
}
</script>
<style scoped>
</style>

View File

@@ -262,6 +262,7 @@
"Print": "",
"Profile": "",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"PropertyType": "",
"Property_Editor": "",
"Protected": "",

View File

@@ -255,6 +255,7 @@
"Print": "Печат",
"Profile": "",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"PropertyType": "",
"Property_Editor": "",
"Protected": "Защитен",

View File

@@ -311,6 +311,7 @@
"Profile": "",
"Properties": "Egenskaber",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"Property": "Egenskab",
"PropertyType": "",
"Property_Editor": "",

View File

@@ -333,7 +333,7 @@
"Properties": "Eigenschaften",
"PropertiesFoodHelp": "Eigenschaften können für Rezepte und Lebensmittel erfasst werden. Eigenschaften von Lebensmitteln werden entsprechend der Menge für das Rezept ausgerechnet und überschreiben die Rezepteigenschaften. ",
"Properties_Food_Amount": "Eigenschaften: Lebensmittelmenge",
"Properties_Food_Unit": "Nährwert Einheit",
"Properties_Food_Unit": "Eigenschaft Einheit",
"Property": "Eigenschaft",
"PropertyType": "Eigenschafts Typ",
"Property_Editor": "Eigenschaften bearbeiten",

View File

@@ -303,6 +303,7 @@
"Profile": "",
"Properties": "Ιδιότητες",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"Property": "Ιδιότητα",
"PropertyType": "",
"Property_Editor": "",

View File

@@ -329,6 +329,7 @@
"Profile": "",
"Properties": "Propiedades",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"Property": "Propiedad",
"PropertyType": "",
"Property_Editor": "Editor de Propiedades",

View File

@@ -197,6 +197,7 @@
"Print": "Tulosta",
"Profile": "",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"PropertyType": "",
"Property_Editor": "",
"Proteins": "Proteiinit",

View File

@@ -305,6 +305,7 @@
"Profile": "",
"Properties": "Tulajdonságok",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"Property": "Tulajdonság",
"PropertyType": "",
"Property_Editor": "",

View File

@@ -143,6 +143,7 @@
"Print": "Տպել",
"Profile": "",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"PropertyType": "",
"Property_Editor": "",
"Proteins": "",

View File

@@ -282,6 +282,7 @@
"Private_Recipe_Help": "Resep hanya diperlihatkan kepada Anda dan orang-orang yang dibagikan resep tersebut.",
"Profile": "",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"PropertyType": "",
"Property_Editor": "",
"Protected": "Terlindung",

View File

@@ -290,6 +290,7 @@
"Private_Recipe_Help": "La ricetta viene mostrata solo a te e a chi l'hai condivisa.",
"Profile": "",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"PropertyType": "",
"Property_Editor": "",
"Protected": "Protetto",

View File

@@ -309,6 +309,7 @@
"Profile": "",
"Properties": "",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"Property": "",
"PropertyType": "",
"Property_Editor": "",

View File

@@ -301,6 +301,7 @@
"Profile": "",
"Properties": "Egenskaper",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"Property": "Egenskap",
"PropertyType": "",
"Property_Editor": "",

View File

@@ -305,6 +305,7 @@
"Profile": "",
"Properties": "Eigenschappen",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"Property": "Eigenschap",
"PropertyType": "",
"Property_Editor": "",

View File

@@ -252,6 +252,7 @@
"Private_Recipe_Help": "A receita só é mostrada ás pessoas com que foi partilhada.",
"Profile": "",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"PropertyType": "",
"Property_Editor": "",
"Protected": "Protegido",

View File

@@ -294,6 +294,7 @@
"Private_Recipe_Help": "Rețeta este arătată doar ție și oamenilor cu care este împărtășită.",
"Profile": "",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"PropertyType": "",
"Property_Editor": "",
"Protected": "Protejat",

View File

@@ -238,6 +238,7 @@
"Print": "Распечатать",
"Profile": "",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"PropertyType": "",
"Property_Editor": "",
"Protected": "Защищено",

View File

@@ -233,6 +233,7 @@
"Private_Recipe_Help": "Recept je prikazan samo vam in osebam, s katerimi ga delite.",
"Profile": "",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"PropertyType": "",
"Property_Editor": "",
"Proteins": "Beljakovine",

View File

@@ -270,6 +270,7 @@
"Private_Recipe_Help": "Рецепт показаний тільки Вам і тими з ким ви поділилися їм.",
"Profile": "",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"PropertyType": "",
"Property_Editor": "",
"Protected": "Захищено",

View File

@@ -118,6 +118,7 @@
"Print": "",
"Profile": "",
"PropertiesFoodHelp": "",
"Properties_Food_Unit": "",
"PropertyType": "",
"Property_Editor": "",
"Proteins": "",

View File

@@ -17,6 +17,8 @@ models/CustomFilter.ts
models/DefaultPageEnum.ts
models/DeleteEnum.ts
models/ExportLog.ts
models/FdcQuery.ts
models/FdcQueryFoods.ts
models/Food.ts
models/FoodInheritField.ts
models/FoodShoppingUpdate.ts

View File

@@ -23,6 +23,7 @@ import type {
CookLog,
CustomFilter,
ExportLog,
FdcQuery,
Food,
FoodInheritField,
FoodShoppingUpdate,
@@ -165,6 +166,8 @@ import {
CustomFilterToJSON,
ExportLogFromJSON,
ExportLogToJSON,
FdcQueryFromJSON,
FdcQueryToJSON,
FoodFromJSON,
FoodToJSON,
FoodInheritFieldFromJSON,
@@ -609,6 +612,11 @@ export interface ApiExportLogUpdateRequest {
exportLog: Omit<ExportLog, 'createdBy'|'createdAt'>;
}
export interface ApiFdcSearchRetrieveRequest {
dataType?: Array<string>;
query?: string;
}
export interface ApiFoodCreateRequest {
food: Omit<Food, 'shopping'|'parent'|'numchild'|'fullName'|'substituteOnhand'>;
}
@@ -684,7 +692,7 @@ export interface ApiImageToRecipeCreateRequest {
image: string;
}
export interface ApiImageToRecipeCreate2Request {
export interface ApiImportCreateRequest {
image: string;
}
@@ -3462,6 +3470,42 @@ export class ApiApi extends runtime.BaseAPI {
return await response.value();
}
/**
*/
async apiFdcSearchRetrieveRaw(requestParameters: ApiFdcSearchRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<FdcQuery>> {
const queryParameters: any = {};
if (requestParameters['dataType'] != null) {
queryParameters['dataType'] = requestParameters['dataType'];
}
if (requestParameters['query'] != null) {
queryParameters['query'] = requestParameters['query'];
}
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/fdc-search/`,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.JSONApiResponse(response, (jsonValue) => FdcQueryFromJSON(jsonValue));
}
/**
*/
async apiFdcSearchRetrieve(requestParameters: ApiFdcSearchRetrieveRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<FdcQuery> {
const response = await this.apiFdcSearchRetrieveRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* logs request counts to redis cache total/per user/
*/
@@ -4165,7 +4209,7 @@ export class ApiApi extends runtime.BaseAPI {
}
const response = await this.request({
path: `/api/image-to-recipe`,
path: `/api/image-to-recipe/`,
method: 'POST',
headers: headerParameters,
query: queryParameters,
@@ -4184,11 +4228,11 @@ export class ApiApi extends runtime.BaseAPI {
/**
*/
async apiImageToRecipeCreate2Raw(requestParameters: ApiImageToRecipeCreate2Request, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<RecipeFromSourceResponse>> {
async apiImportCreateRaw(requestParameters: ApiImportCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<RecipeFromSourceResponse>> {
if (requestParameters['image'] == null) {
throw new runtime.RequiredError(
'image',
'Required parameter "image" was null or undefined when calling apiImageToRecipeCreate2().'
'Required parameter "image" was null or undefined when calling apiImportCreate().'
);
}
@@ -4219,7 +4263,7 @@ export class ApiApi extends runtime.BaseAPI {
}
const response = await this.request({
path: `/api/image-to-recipe/`,
path: `/api/import/`,
method: 'POST',
headers: headerParameters,
query: queryParameters,
@@ -4231,40 +4275,11 @@ export class ApiApi extends runtime.BaseAPI {
/**
*/
async apiImageToRecipeCreate2(requestParameters: ApiImageToRecipeCreate2Request, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<RecipeFromSourceResponse> {
const response = await this.apiImageToRecipeCreate2Raw(requestParameters, initOverrides);
async apiImportCreate(requestParameters: ApiImportCreateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<RecipeFromSourceResponse> {
const response = await this.apiImportCreateRaw(requestParameters, initOverrides);
return await response.value();
}
/**
* function to handle files passed by application importer
*/
async apiImportCreateRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> {
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.apiKey) {
headerParameters["Authorization"] = await this.configuration.apiKey("Authorization"); // ApiKeyAuth authentication
}
const response = await this.request({
path: `/api/import/`,
method: 'POST',
headers: headerParameters,
query: queryParameters,
}, initOverrides);
return new runtime.VoidApiResponse(response);
}
/**
* function to handle files passed by application importer
*/
async apiImportCreate(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> {
await this.apiImportCreateRaw(initOverrides);
}
/**
* logs request counts to redis cache total/per user/
*/

View File

@@ -0,0 +1,95 @@
/* 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.
*/
import { mapValues } from '../runtime';
import type { FdcQueryFoods } from './FdcQueryFoods';
import {
FdcQueryFoodsFromJSON,
FdcQueryFoodsFromJSONTyped,
FdcQueryFoodsToJSON,
} from './FdcQueryFoods';
/**
*
* @export
* @interface FdcQuery
*/
export interface FdcQuery {
/**
*
* @type {number}
* @memberof FdcQuery
*/
totalHits: number;
/**
*
* @type {number}
* @memberof FdcQuery
*/
currentPage: number;
/**
*
* @type {number}
* @memberof FdcQuery
*/
totalPages: number;
/**
*
* @type {Array<FdcQueryFoods>}
* @memberof FdcQuery
*/
foods: Array<FdcQueryFoods>;
}
/**
* Check if a given object implements the FdcQuery interface.
*/
export function instanceOfFdcQuery(value: object): value is FdcQuery {
if (!('totalHits' in value) || value['totalHits'] === undefined) return false;
if (!('currentPage' in value) || value['currentPage'] === undefined) return false;
if (!('totalPages' in value) || value['totalPages'] === undefined) return false;
if (!('foods' in value) || value['foods'] === undefined) return false;
return true;
}
export function FdcQueryFromJSON(json: any): FdcQuery {
return FdcQueryFromJSONTyped(json, false);
}
export function FdcQueryFromJSONTyped(json: any, ignoreDiscriminator: boolean): FdcQuery {
if (json == null) {
return json;
}
return {
'totalHits': json['totalHits'],
'currentPage': json['currentPage'],
'totalPages': json['totalPages'],
'foods': ((json['foods'] as Array<any>).map(FdcQueryFoodsFromJSON)),
};
}
export function FdcQueryToJSON(value?: FdcQuery | null): any {
if (value == null) {
return value;
}
return {
'totalHits': value['totalHits'],
'currentPage': value['currentPage'],
'totalPages': value['totalPages'],
'foods': ((value['foods'] as Array<any>).map(FdcQueryFoodsToJSON)),
};
}

View File

@@ -0,0 +1,79 @@
/* 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.
*/
import { mapValues } from '../runtime';
/**
*
* @export
* @interface FdcQueryFoods
*/
export interface FdcQueryFoods {
/**
*
* @type {number}
* @memberof FdcQueryFoods
*/
fdcId: number;
/**
*
* @type {string}
* @memberof FdcQueryFoods
*/
description: string;
/**
*
* @type {string}
* @memberof FdcQueryFoods
*/
dataType: string;
}
/**
* Check if a given object implements the FdcQueryFoods interface.
*/
export function instanceOfFdcQueryFoods(value: object): value is FdcQueryFoods {
if (!('fdcId' in value) || value['fdcId'] === undefined) return false;
if (!('description' in value) || value['description'] === undefined) return false;
if (!('dataType' in value) || value['dataType'] === undefined) return false;
return true;
}
export function FdcQueryFoodsFromJSON(json: any): FdcQueryFoods {
return FdcQueryFoodsFromJSONTyped(json, false);
}
export function FdcQueryFoodsFromJSONTyped(json: any, ignoreDiscriminator: boolean): FdcQueryFoods {
if (json == null) {
return json;
}
return {
'fdcId': json['fdcId'],
'description': json['description'],
'dataType': json['dataType'],
};
}
export function FdcQueryFoodsToJSON(value?: FdcQueryFoods | null): any {
if (value == null) {
return value;
}
return {
'fdcId': value['fdcId'],
'description': value['description'],
'dataType': value['dataType'],
};
}

View File

@@ -14,6 +14,8 @@ export * from './CustomFilter';
export * from './DefaultPageEnum';
export * from './DeleteEnum';
export * from './ExportLog';
export * from './FdcQuery';
export * from './FdcQueryFoods';
export * from './Food';
export * from './FoodInheritField';
export * from './FoodShoppingUpdate';

View File

@@ -117,6 +117,9 @@
</v-col>
</v-row>
<v-btn @click="fdcDialog = true">OpenSearch</v-btn>
<fdc-search-dialog v-model="fdcDialog"></fdc-search-dialog>
</v-container>
<v-dialog v-model="dialog" max-width="600">
@@ -153,6 +156,7 @@ import ModelEditDialog from "@/components/dialogs/ModelEditDialog.vue";
import VClosableCardTitle from "@/components/dialogs/VClosableCardTitle.vue";
import {useUrlSearchParams} from "@vueuse/core";
import BtnCopy from "@/components/buttons/BtnCopy.vue";
import FdcSearchDialog from "@/components/dialogs/FdcSearchDialog.vue";
const params = useUrlSearchParams('history', {})
@@ -168,6 +172,8 @@ const calculatorFromNumerator = ref(250)
const calculatorFromDenominator = ref(500)
const calculatorToDenominator = ref(100)
const fdcDialog = ref(false)
const recipe = ref<undefined | Recipe>()
const propertyTypes = ref([] as PropertyType[])
const foods = ref(new Map<number, Food & { loading?: boolean }>())