Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
# Conflicts:
#	docs/faq.md
  • Loading branch information
vabene1111 committed Dec 3, 2023
2 parents 4723a7e + fd02804 commit abf8f79
Show file tree
Hide file tree
Showing 38 changed files with 5,115 additions and 495 deletions.
4 changes: 3 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ SECRET_KEY_FILE=
# ---------------------------------------------------------------

# your default timezone See https://timezonedb.com/time-zones for a list of timezones
TIMEZONE=Europe/Berlin
TZ=Europe/Berlin

# add only a database password if you want to run with the default postgres, otherwise change settings accordingly
DB_ENGINE=django.db.backends.postgresql
Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion cookbook/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,9 @@ class ShareLinkAdmin(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)
Expand Down
19 changes: 19 additions & 0 deletions cookbook/helper/fdc_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import json


def get_all_nutrient_types():
f = open('') # <--- download the foundation food or any other dataset and retrieve all nutrition ID's from it https://fdc.nal.usda.gov/download-datasets.html
json_data = json.loads(f.read())

nutrients = {}
for food in json_data['FoundationFoods']:
for entry in food['foodNutrients']:
nutrients[entry['nutrient']['id']] = {'name': entry['nutrient']['name'], 'unit': entry['nutrient']['unitName']}

nutrient_ids = list(nutrients.keys())
nutrient_ids.sort()
for nid in nutrient_ids:
print('{', f'value: {nid}, text: "{nutrients[nid]["name"]} [{nutrients[nid]["unit"]}] ({nid})"', '},')


get_all_nutrient_types()
10 changes: 5 additions & 5 deletions cookbook/helper/recipe_url_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,9 @@ def get_from_scraper(scrape, request):
if len(recipe_json['steps']) == 0:
recipe_json['steps'].append({'instruction': '', 'ingredients': [], })

recipe_json['description'] = recipe_json['description'][:512]
if len(recipe_json['description']) > 256: # split at 256 as long descriptions don't look good on recipe cards
recipe_json['steps'][0]['instruction'] = f"*{recipe_json['description']}* \n\n" + recipe_json['steps'][0]['instruction']
else:
recipe_json['description'] = recipe_json['description'][:512]

try:
for x in scrape.ingredients():
Expand Down Expand Up @@ -259,13 +258,14 @@ def get_from_youtube_scraper(url, request):
]
}

# TODO add automation here
try:
automation_engine = AutomationEngine(request, source=url)
video = YouTube(url=url)
video = YouTube(url)
video.streams.first() # this is required to execute some kind of generator/web request that fetches the description
default_recipe_json['name'] = automation_engine.apply_regex_replace_automation(video.title, Automation.NAME_REPLACE)
default_recipe_json['image'] = video.thumbnail_url
default_recipe_json['steps'][0]['instruction'] = automation_engine.apply_regex_replace_automation(video.description, Automation.INSTRUCTION_REPLACE)
if video.description:
default_recipe_json['steps'][0]['instruction'] = automation_engine.apply_regex_replace_automation(video.description, Automation.INSTRUCTION_REPLACE)

except Exception:
pass
Expand Down
Original file line number Diff line number Diff line change
@@ -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),
),
]
6 changes: 3 additions & 3 deletions cookbook/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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?

Expand Down Expand Up @@ -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'),
]


Expand Down
8 changes: 8 additions & 0 deletions cookbook/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from PIL import Image
from rest_framework import serializers
from rest_framework.exceptions import NotFound, ValidationError
from rest_framework.fields import IntegerField

from cookbook.helper.CustomStorageClass import CachedS3Boto3Storage
from cookbook.helper.HelperFunctions import str2bool
Expand Down Expand Up @@ -524,6 +525,7 @@ class Meta:

class PropertyTypeSerializer(OpenDataModelMixin, WritableNestedModelSerializer, UniqueFieldsMixin):
id = serializers.IntegerField(required=False)
order = IntegerField(default=0, required=False)

def create(self, validated_data):
validated_data['name'] = validated_data['name'].strip()
Expand Down Expand Up @@ -985,6 +987,8 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
shared = UserSerializer(many=True, required=False, allow_null=True)
shopping = serializers.SerializerMethodField('in_shopping')

to_date = serializers.DateField(required=False)

def get_note_markdown(self, obj):
return markdown(obj.note)

Expand All @@ -993,6 +997,10 @@ def in_shopping(self, obj):

def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user

if 'to_date' not in validated_data or validated_data['to_date'] is None:
validated_data['to_date'] = validated_data['from_date']

mealplan = super().create(validated_data)
if self.context['request'].data.get('addshopping', False) and self.context['request'].data.get('recipe', None):
SLR = RecipeShoppingEditor(user=validated_data['created_by'], space=validated_data['space'])
Expand Down
31 changes: 31 additions & 0 deletions cookbook/templates/property_editor.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}

{% block title %}{% trans 'Property Editor' %}{% endblock %}

{% block content_fluid %}

<div id="app">
<property-editor-view></property-editor-view>
</div>


{% endblock %}


{% block script %}
{% if debug %}
<script src="{% url 'js_reverse' %}"></script>
{% else %}
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
{% endif %}

<script type="application/javascript">
window.RECIPE_ID = {{ recipe_id }}
</script>

{% render_bundle 'property_editor_view' %}
{% endblock %}
3 changes: 2 additions & 1 deletion cookbook/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def extend(self, r):
router.register(r'recipe-book', api.RecipeBookViewSet)
router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet)
router.register(r'unit-conversion', api.UnitConversionViewSet)
router.register(r'food-property-type', api.PropertyTypeViewSet)
router.register(r'food-property-type', api.PropertyTypeViewSet) # TODO rename + regenerate
router.register(r'food-property', api.PropertyViewSet)
router.register(r'shopping-list', api.ShoppingListViewSet)
router.register(r'shopping-list-entry', api.ShoppingListEntryViewSet)
Expand Down Expand Up @@ -91,6 +91,7 @@ def extend(self, r):
path('history/', views.history, name='view_history'),
path('supermarket/', views.supermarket, name='view_supermarket'),
path('ingredient-editor/', views.ingredient_editor, name='view_ingredient_editor'),
path('property-editor/<int:pk>', views.property_editor, name='view_property_editor'),
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),

path('api/import/', api.import_files, name='view_import'),
Expand Down
55 changes: 52 additions & 3 deletions cookbook/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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
Expand Down Expand Up @@ -75,7 +75,7 @@
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
Expand Down Expand Up @@ -104,6 +104,7 @@
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):
Expand Down Expand Up @@ -595,6 +596,54 @@ def shopping(self, request, pk):
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
if properties with a fdc_id already exist they will be overridden, if existing properties don't have a fdc_id they won't be changed
"""
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 = []

# delete all properties where the property type has a fdc_id as these should be overridden
for fp in food.properties.all():
if fp.property_type.fdc_id:
fp.delete()

for pt in PropertyType.objects.filter(space=request.space, fdc_id__gte=0).all():
if pt.fdc_id:
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({'msg': f'there was an error parsing the FDC data, please check the server logs'}, status=500, json_dumps_params={'indent': 4})

def destroy(self, *args, **kwargs):
try:
return (super().destroy(self, *args, **kwargs))
Expand Down Expand Up @@ -1454,7 +1503,7 @@ def import_files(request):
"""
limit, msg = above_space_limit(request.space)
if limit:
return Response({'error': msg}, status=status.HTTP_400_BAD_REQUEST)
return Response({'error': True, 'msg': _('File is above space limit')}, status=status.HTTP_400_BAD_REQUEST)

form = ImportForm(request.POST, request.FILES)
if form.is_valid() and request.FILES != {}:
Expand Down
13 changes: 9 additions & 4 deletions cookbook/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ def ingredient_editor(request):
return render(request, 'ingredient_editor.html', template_vars)


@group_required('user')
def property_editor(request, pk):
return render(request, 'property_editor.html', {'recipe_id': pk})


@group_required('guest')
def shopping_settings(request):
if request.space.demo:
Expand All @@ -220,10 +225,10 @@ def shopping_settings(request):
if not sp:
sp = SearchPreferenceForm(user=request.user)
fields_searched = (
len(search_form.cleaned_data['icontains'])
+ len(search_form.cleaned_data['istartswith'])
+ len(search_form.cleaned_data['trigram'])
+ len(search_form.cleaned_data['fulltext'])
len(search_form.cleaned_data['icontains'])
+ len(search_form.cleaned_data['istartswith'])
+ len(search_form.cleaned_data['trigram'])
+ len(search_form.cleaned_data['fulltext'])
)
if search_form.cleaned_data['preset'] == 'fuzzy':
sp.search = SearchPreference.SIMPLE
Expand Down
Loading

0 comments on commit abf8f79

Please sign in to comment.