first working property editor prototype

This commit is contained in:
vabene1111 2023-11-29 21:20:10 +01:00
parent cce2407bc0
commit 0a0e3a48c3
8 changed files with 3969 additions and 24 deletions

View File

@ -183,3 +183,5 @@ REMOTE_USER_AUTH=0
# Recipe exports are cached for a certain time by default, adjust time if needed
# EXPORT_FILE_CACHE_DURATION=600
# if you want to do many requests to the FDC API you need to get a (free) API key. Demo key is limited to 30 requests / hour or 50 requests / day
#FDC_API_KEY=DEMO_KEY

View File

@ -349,7 +349,9 @@ admin.site.register(ShareLink, ShareLinkAdmin)
class PropertyTypeAdmin(admin.ModelAdmin):
list_display = ('id', 'name')
search_fields = ('space',)
list_display = ('id', 'space', 'name', 'fdc_id')
admin.site.register(PropertyType, PropertyTypeAdmin)

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.7 on 2023-11-29 19:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0204_propertytype_fdc_id'),
]
operations = [
migrations.AlterField(
model_name='food',
name='fdc_id',
field=models.IntegerField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='propertytype',
name='fdc_id',
field=models.IntegerField(blank=True, default=None, null=True),
),
]

View File

@ -591,7 +591,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
preferred_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_unit')
preferred_shopping_unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, default=None, related_name='preferred_shopping_unit')
fdc_id = models.CharField(max_length=128, null=True, blank=True, default=None)
fdc_id = models.IntegerField(null=True, default=None, blank=True)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
@ -767,7 +767,7 @@ class PropertyType(models.Model, PermissionModelMixin):
(PRICE, _('Price')), (GOAL, _('Goal')), (OTHER, _('Other'))), null=True, blank=True)
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
fdc_id = models.CharField(max_length=128, null=True, blank=True, default=None)
fdc_id = models.IntegerField(null=True, default=None, blank=True)
# TODO show if empty property?
# TODO formatting property?
@ -809,7 +809,7 @@ class FoodProperty(models.Model):
class Meta:
constraints = [
models.UniqueConstraint(fields=['food', 'property'], name='property_unique_food')
models.UniqueConstraint(fields=['food', 'property'], name='property_unique_food'),
]

View File

@ -26,7 +26,7 @@ from django.db.models import Case, Count, Exists, OuterRef, ProtectedError, Q, S
from django.db.models.fields.related import ForeignObjectRel
from django.db.models.functions import Coalesce, Lower
from django.db.models.signals import post_save
from django.http import FileResponse, HttpResponse, JsonResponse
from django.http import FileResponse, HttpResponse, JsonResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils import timezone
@ -75,7 +75,7 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilte
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog)
UserFile, UserPreference, UserSpace, ViewLog, FoodProperty)
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud
@ -104,6 +104,7 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
UserSerializer, UserSpaceSerializer, ViewLogSerializer)
from cookbook.views.import_export import get_integration
from recipes import settings
from recipes.settings import FDC_API_KEY
class StandardFilterMixin(ViewSetMixin):
@ -595,6 +596,49 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
created_by=request.user)
return Response(content, status=status.HTTP_204_NO_CONTENT)
@decorators.action(detail=True, methods=['POST'], )
def fdc(self, request, pk):
"""
updates the food with all possible data from the FDC Api (only adds new, does not change existing properties)
"""
food = self.get_object()
response = requests.get(f'https://api.nal.usda.gov/fdc/v1/food/{food.fdc_id}?api_key={FDC_API_KEY}')
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})
try:
data = json.loads(response.content)
food_property_list = []
food_property_types = food.foodproperty_set.values_list('property__property_type_id', flat=True)
for pt in PropertyType.objects.filter(space=request.space).all():
if pt.fdc_id and pt.id not in food_property_types:
for fn in data['foodNutrients']:
if fn['nutrient']['id'] == pt.fdc_id:
food_property_list.append(Property(
property_type_id=pt.id,
property_amount=round(fn['amount'], 2),
import_food_id=food.id,
space=self.request.space,
))
Property.objects.bulk_create(food_property_list, ignore_conflicts=True, unique_fields=('space', 'import_food_id', 'property_type',))
property_food_relation_list = []
for p in Property.objects.filter(space=self.request.space, import_food_id=food.id).values_list('import_food_id', 'id', ):
property_food_relation_list.append(Food.properties.through(food_id=p[0], property_id=p[1]))
FoodProperty.objects.bulk_create(property_food_relation_list, ignore_conflicts=True, unique_fields=('food_id', 'property_id',))
Property.objects.filter(space=self.request.space, import_food_id=food.id).update(import_food_id=None)
return self.retrieve(request, pk)
except Exception as e:
traceback.print_exc()
return JsonResponse({'error': f'{e} - check server log'}, status=500, json_dumps_params={'indent': 4})
def destroy(self, *args, **kwargs):
try:
return (super().destroy(self, *args, **kwargs))

View File

@ -89,7 +89,7 @@ DJANGO_TABLES2_PAGE_RANGE = 8
HCAPTCHA_SITEKEY = os.getenv('HCAPTCHA_SITEKEY', '')
HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '')
FDA_API_KEY = os.getenv('FDA_API_KEY', 'DEMO_KEY')
FDC_API_KEY = os.getenv('FDC_API_KEY', 'DEMO_KEY')
SHARING_ABUSE = bool(int(os.getenv('SHARING_ABUSE', False)))
SHARING_LIMIT = int(os.getenv('SHARING_LIMIT', 0))

View File

@ -2,31 +2,33 @@
<div id="app">
<div>
<h2 v-if="recipe">{{ recipe.name}}</h2>
<h2 v-if="recipe">{{ recipe.name }}</h2>
<table class="table table-sm table-bordered">
<thead>
<tr>
<td>{{ $t('Name') }}</td>
<td v-for="pt in property_types" v-bind:key="pt.id">{{ pt.name }}
<input type="text" v-model="pt.unit" @change="updatePropertyType(pt)">
<input v-model="pt.fdc_id" type="number" placeholder="FDC ID" @change="updatePropertyType(pt)"></td>
<input class="form-control form-control-sm" type="text" v-model="pt.unit" @change="updatePropertyType(pt)">
<input class="form-control form-control-sm" v-model="pt.fdc_id" type="number" placeholder="FDC ID" @change="updatePropertyType(pt)"></td>
</tr>
</thead>
<tbody>
<tr v-for="f in this.foods" v-bind:key="f.food.id">
<td>
{{ f.food.name }}
{{ $t('Property') }} / <input type="number" v-model="f.food.properties_food_amount" @change="updateFood(f.food)">
{{ f.food.name }} #{{ f.food.id }}
{{ $t('Property') }} / <input class="form-control form-control-sm" type="number" v-model="f.food.properties_food_amount" @change="updateFood(f.food)">
<generic-multiselect
@change="f.food.properties_food_unit = $event.val; updateFood(f.food)"
:initial_selection="f.food.properties_food_unit"
label="name" :model="Models.UNIT"
:multiple="false"/>
<input v-model="f.food.fdc_id" placeholder="FDC ID">
<button>Load FDC</button>
<input class="form-control form-control-sm" v-model="f.food.fdc_id" placeholder="FDC ID" @change="updateFood(f.food)">
<button @click="updateFoodFromFDC(f.food)">Load FDC</button>
</td>
<td v-for="p in f.properties" v-bind:key="`${f.id}_${p.property_type.id}`">
<input class="form-control form-control-sm" type="number" v-model="p.property_amount"> {{ p.property_type.unit }} ({{ p.property_type.name }})
</td>
<td v-for="p in f.properties" v-bind:key="`${f.id}_${p.property_type.id}`"><input type="number" v-model="p.property_amount"> {{ p.property_type.unit }}</td>
</tr>
</tbody>
</table>
@ -84,17 +86,20 @@ export default {
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
let apiClient = new ApiApiFactory()
apiClient.retrieveRecipe("112").then(result => {
this.recipe = result.data
})
apiClient.listPropertyTypes().then(result => {
this.property_types = result.data
})
this.loadData();
},
methods: {
loadData: function () {
let apiClient = new ApiApiFactory()
apiClient.retrieveRecipe("112").then(result => {
this.recipe = result.data
})
apiClient.listPropertyTypes().then(result => {
this.property_types = result.data
})
},
updateFood: function (food) {
let apiClient = new ApiApiFactory()
apiClient.partialUpdateFood(food.id, food).then(result => {
@ -112,6 +117,16 @@ export default {
}).catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
updateFoodFromFDC: function (food) {
let apiClient = new ApiApiFactory()
apiClient.fdcFood(food.id).then(result => {
this.loadData()
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
}).catch((err) => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
}
},
}

File diff suppressed because it is too large Load Diff