first working property editor prototype
This commit is contained in:
parent
cce2407bc0
commit
0a0e3a48c3
@ -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
|
@ -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)
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
]
|
||||
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user