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
|
# Recipe exports are cached for a certain time by default, adjust time if needed
|
||||||
# EXPORT_FILE_CACHE_DURATION=600
|
# 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):
|
class PropertyTypeAdmin(admin.ModelAdmin):
|
||||||
list_display = ('id', 'name')
|
search_fields = ('space',)
|
||||||
|
|
||||||
|
list_display = ('id', 'space', 'name', 'fdc_id')
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(PropertyType, PropertyTypeAdmin)
|
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_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')
|
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)
|
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
|
||||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
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)
|
(PRICE, _('Price')), (GOAL, _('Goal')), (OTHER, _('Other'))), null=True, blank=True)
|
||||||
open_data_slug = models.CharField(max_length=128, null=True, blank=True, default=None)
|
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 show if empty property?
|
||||||
# TODO formatting property?
|
# TODO formatting property?
|
||||||
|
|
||||||
@ -809,7 +809,7 @@ class FoodProperty(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
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.fields.related import ForeignObjectRel
|
||||||
from django.db.models.functions import Coalesce, Lower
|
from django.db.models.functions import Coalesce, Lower
|
||||||
from django.db.models.signals import post_save
|
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.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -75,7 +75,7 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilte
|
|||||||
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
|
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
|
||||||
Step, Storage, Supermarket, SupermarketCategory,
|
Step, Storage, Supermarket, SupermarketCategory,
|
||||||
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
|
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
|
||||||
UserFile, UserPreference, UserSpace, ViewLog)
|
UserFile, UserPreference, UserSpace, ViewLog, FoodProperty)
|
||||||
from cookbook.provider.dropbox import Dropbox
|
from cookbook.provider.dropbox import Dropbox
|
||||||
from cookbook.provider.local import Local
|
from cookbook.provider.local import Local
|
||||||
from cookbook.provider.nextcloud import Nextcloud
|
from cookbook.provider.nextcloud import Nextcloud
|
||||||
@ -104,6 +104,7 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
|
|||||||
UserSerializer, UserSpaceSerializer, ViewLogSerializer)
|
UserSerializer, UserSpaceSerializer, ViewLogSerializer)
|
||||||
from cookbook.views.import_export import get_integration
|
from cookbook.views.import_export import get_integration
|
||||||
from recipes import settings
|
from recipes import settings
|
||||||
|
from recipes.settings import FDC_API_KEY
|
||||||
|
|
||||||
|
|
||||||
class StandardFilterMixin(ViewSetMixin):
|
class StandardFilterMixin(ViewSetMixin):
|
||||||
@ -595,6 +596,49 @@ class FoodViewSet(viewsets.ModelViewSet, TreeMixin):
|
|||||||
created_by=request.user)
|
created_by=request.user)
|
||||||
return Response(content, status=status.HTTP_204_NO_CONTENT)
|
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):
|
def destroy(self, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
return (super().destroy(self, *args, **kwargs))
|
return (super().destroy(self, *args, **kwargs))
|
||||||
|
@ -89,7 +89,7 @@ DJANGO_TABLES2_PAGE_RANGE = 8
|
|||||||
HCAPTCHA_SITEKEY = os.getenv('HCAPTCHA_SITEKEY', '')
|
HCAPTCHA_SITEKEY = os.getenv('HCAPTCHA_SITEKEY', '')
|
||||||
HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '')
|
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_ABUSE = bool(int(os.getenv('SHARING_ABUSE', False)))
|
||||||
SHARING_LIMIT = int(os.getenv('SHARING_LIMIT', 0))
|
SHARING_LIMIT = int(os.getenv('SHARING_LIMIT', 0))
|
||||||
|
@ -9,24 +9,26 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ $t('Name') }}</td>
|
<td>{{ $t('Name') }}</td>
|
||||||
<td v-for="pt in property_types" v-bind:key="pt.id">{{ pt.name }}
|
<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 class="form-control form-control-sm" 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" v-model="pt.fdc_id" type="number" placeholder="FDC ID" @change="updatePropertyType(pt)"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="f in this.foods" v-bind:key="f.food.id">
|
<tr v-for="f in this.foods" v-bind:key="f.food.id">
|
||||||
<td>
|
<td>
|
||||||
{{ f.food.name }}
|
{{ f.food.name }} #{{ f.food.id }}
|
||||||
{{ $t('Property') }} / <input type="number" v-model="f.food.properties_food_amount" @change="updateFood(f.food)">
|
{{ $t('Property') }} / <input class="form-control form-control-sm" type="number" v-model="f.food.properties_food_amount" @change="updateFood(f.food)">
|
||||||
<generic-multiselect
|
<generic-multiselect
|
||||||
@change="f.food.properties_food_unit = $event.val; updateFood(f.food)"
|
@change="f.food.properties_food_unit = $event.val; updateFood(f.food)"
|
||||||
:initial_selection="f.food.properties_food_unit"
|
:initial_selection="f.food.properties_food_unit"
|
||||||
label="name" :model="Models.UNIT"
|
label="name" :model="Models.UNIT"
|
||||||
:multiple="false"/>
|
:multiple="false"/>
|
||||||
<input v-model="f.food.fdc_id" placeholder="FDC ID">
|
<input class="form-control form-control-sm" v-model="f.food.fdc_id" placeholder="FDC ID" @change="updateFood(f.food)">
|
||||||
<button>Load FDC</button>
|
<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>
|
||||||
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -84,6 +86,10 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||||
|
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadData: function () {
|
||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
|
|
||||||
apiClient.retrieveRecipe("112").then(result => {
|
apiClient.retrieveRecipe("112").then(result => {
|
||||||
@ -94,7 +100,6 @@ export default {
|
|||||||
this.property_types = result.data
|
this.property_types = result.data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
updateFood: function (food) {
|
updateFood: function (food) {
|
||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
apiClient.partialUpdateFood(food.id, food).then(result => {
|
apiClient.partialUpdateFood(food.id, food).then(result => {
|
||||||
@ -112,6 +117,16 @@ export default {
|
|||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, 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