Compare commits

..

48 Commits
1.3.1 ... 1.3.3

Author SHA1 Message Date
vabene1111
9ef21241bf markdown editor adjustments 2022-08-01 17:13:44 +02:00
vabene1111
5e77adf7e6 fixed mail send 2022-08-01 16:56:18 +02:00
vabene1111
4df0a46701 Merge pull request #1975 from TandoorRecipes/dependabot/pip/icalendar-4.1.0
Bump icalendar from 4.0.9 to 4.1.0
2022-08-01 16:28:48 +02:00
dependabot[bot]
f186404628 Bump icalendar from 4.0.9 to 4.1.0
Bumps [icalendar](https://github.com/collective/icalendar) from 4.0.9 to 4.1.0.
- [Release notes](https://github.com/collective/icalendar/releases)
- [Changelog](https://github.com/collective/icalendar/blob/4.1.0/CHANGES.rst)
- [Commits](https://github.com/collective/icalendar/compare/4.0.9...4.1.0)

---
updated-dependencies:
- dependency-name: icalendar
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 14:28:24 +00:00
vabene1111
8e3ec91f3c Merge pull request #1961 from AquaticLava/develop
Fixes  #1234
2022-08-01 16:28:18 +02:00
vabene1111
2605addf34 Merge pull request #1974 from TandoorRecipes/dependabot/pip/drf-writable-nested-0.6.4
Bump drf-writable-nested from 0.6.3 to 0.6.4
2022-08-01 16:27:48 +02:00
vabene1111
1ab3e57b83 Merge pull request #1947 from dmaes/develop
Add S3_CUSTOM_DOMAIN settings, closes #1943
2022-08-01 16:27:16 +02:00
dependabot[bot]
2f36ae5112 Bump drf-writable-nested from 0.6.3 to 0.6.4
Bumps [drf-writable-nested](https://github.com/beda-software/drf-writable-nested) from 0.6.3 to 0.6.4.
- [Release notes](https://github.com/beda-software/drf-writable-nested/releases)
- [Changelog](https://github.com/beda-software/drf-writable-nested/blob/master/CHANGELOG.md)
- [Commits](https://github.com/beda-software/drf-writable-nested/compare/v0.6.3...v0.6.4)

---
updated-dependencies:
- dependency-name: drf-writable-nested
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 14:27:15 +00:00
vabene1111
acc19ca65e Merge pull request #1973 from TandoorRecipes/dependabot/pip/python-ldap-3.4.2
Bump python-ldap from 3.4.0 to 3.4.2
2022-08-01 16:26:46 +02:00
vabene1111
ea213e2dfd Merge pull request #1948 from iamkarlson/feature/fix_csrf_django_40
fixed csrf
2022-08-01 16:25:56 +02:00
vabene1111
02cf3264a3 Merge branch 'develop' of https://github.com/vabene1111/recipes into develop 2022-08-01 16:18:46 +02:00
vabene1111
a0b1186558 settings wip 2022-08-01 16:18:43 +02:00
dependabot[bot]
27e47718bb Bump python-ldap from 3.4.0 to 3.4.2
Bumps [python-ldap](https://github.com/python-ldap/python-ldap) from 3.4.0 to 3.4.2.
- [Release notes](https://github.com/python-ldap/python-ldap/releases)
- [Commits](https://github.com/python-ldap/python-ldap/compare/python-ldap-3.4.0...python-ldap-3.4.2)

---
updated-dependencies:
- dependency-name: python-ldap
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 05:52:23 +00:00
vabene1111
f78dd209bd Merge pull request #1967 from TandoorRecipes/dependabot/pip/recipe-scrapers-14.11.0
Bump recipe-scrapers from 14.6.0 to 14.11.0
2022-08-01 07:51:58 +02:00
dependabot[bot]
b4e0b51f5b Bump recipe-scrapers from 14.6.0 to 14.11.0
Bumps [recipe-scrapers](https://github.com/hhursev/recipe-scrapers) from 14.6.0 to 14.11.0.
- [Release notes](https://github.com/hhursev/recipe-scrapers/releases)
- [Commits](https://github.com/hhursev/recipe-scrapers/compare/14.6.0...14.11.0)

---
updated-dependencies:
- dependency-name: recipe-scrapers
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-01 00:13:53 +00:00
AquaticLava
eedce4dcfd fixes shopping list number showing completed items 2022-07-27 21:03:21 -06:00
AquaticLava
006be92180 Fixes #1234 2022-07-26 17:49:43 -06:00
Tomasz Klimczak
1fae004785 Translated using Weblate (Polish)
Currently translated at 100.0% (427 of 427 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pl/
2022-07-26 21:32:41 +00:00
Huth Jimmy
239a88cd24 Translated using Weblate (French)
Currently translated at 88.5% (378 of 427 strings)

Translation: Tandoor/Recipes Frontend
Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/
2022-07-26 21:32:41 +00:00
vabene1111
22b432a6ae Merge pull request #1952 from tomtjes/patch-1
fix minor issues in CopyMeThat importer
2022-07-25 14:48:26 +02:00
tomtjes
c88566a4ae fix minor issues in CopyMeThat importer
improve handling of empty fields and fields that exceed character limits
2022-07-22 12:14:23 -04:00
vabene1111
5f8e371793 Merge pull request #1949 from TandoorRecipes/dependabot/npm_and_yarn/vue/terser-4.8.1
Bump terser from 4.8.0 to 4.8.1 in /vue
2022-07-21 07:46:56 +02:00
dependabot[bot]
94d9ac03ea Bump terser from 4.8.0 to 4.8.1 in /vue
Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-21 05:21:46 +00:00
George Green
897ac97423 fixed csrf 2022-07-20 21:17:29 +02:00
dmaes
24aeae6de9 update .env.template 2022-07-20 10:45:52 +02:00
dmaes
ce941db3be add S3_CUSTOM_DOMAIN setting 2022-07-20 08:54:34 +02:00
vabene1111
5ff91ee47f more settings in 2022-07-15 17:39:44 +02:00
vabene1111
ce1f55ffd1 settings wip 2022-07-15 17:12:01 +02:00
vabene1111
8700e2df69 basics of new settings page working 2022-07-14 17:50:20 +02:00
vabene1111
f4df84b609 added ability to set space images 2022-07-14 15:23:59 +02:00
vabene1111
ba473123ba added ability to use invite link more than once 2022-07-14 11:28:13 +02:00
vabene1111
98a54ef38f removed lots of unused stuff 2022-07-14 10:37:15 +02:00
vabene1111
7fdc9c7cb8 added sharing permission test 2022-07-14 10:30:45 +02:00
vabene1111
dc3b1566d7 made shared field for recipe api optional 2022-07-14 10:21:35 +02:00
vabene1111
5429c4d557 Merge pull request #1937 from tomtjes/patch-1
Update copymethat.py
2022-07-14 09:53:43 +02:00
tomtjes
dabcea6ba7 Update copymethat.py
- make use of field for source URL
- preserve "I made this" flag as keyword
- preserve long descriptions in full at bottom of steps
- preserve ingredient and step headers
2022-07-13 14:37:16 -04:00
vabene1111
e91790f5ac added ability to mark recipes as private 2022-07-13 15:46:39 +02:00
vabene1111
51076d4ced Merge branch 'master' into develop 2022-07-13 10:25:39 +02:00
vabene1111
1cb37fe2d2 dont allow space manage page in demo 2022-07-13 10:25:22 +02:00
vabene1111
61a9f0647b added userspace admin 2022-07-13 10:24:12 +02:00
vabene1111
ac2ab62050 moved import functions to proper api function 2022-07-12 21:14:51 +02:00
vabene1111
c50efac00e basics for profile page 2022-07-12 20:57:13 +02:00
vabene1111
bf16e61a1f removed unused stuff and fixed manifest 2022-07-12 20:52:32 +02:00
vabene1111
d464633c70 removed django filters 2022-07-12 20:38:18 +02:00
vabene1111
b78d0ec30b added userspace admin 2022-07-12 20:37:38 +02:00
vabene1111
da09602834 removed old search pages 2022-07-12 20:05:59 +02:00
vabene1111
5ead4967a5 removed ingredient list shopping 2022-07-12 19:54:18 +02:00
vabene1111
8bb7ce2062 removed user servings feature 2022-07-12 19:43:11 +02:00
74 changed files with 1934 additions and 1368 deletions

View File

@@ -77,6 +77,7 @@ GUNICORN_MEDIA=0
# S3_QUERYSTRING_AUTH=1 # default true, set to 0 to serve media from a public bucket without signed urls
# S3_QUERYSTRING_EXPIRE=3600 # number of seconds querystring are valid for
# S3_ENDPOINT_URL= # when using a custom endpoint like minio
# S3_CUSTOM_DOMAIN= # when using a CDN/proxy to S3 (see https://github.com/TandoorRecipes/recipes/issues/1943)
# Email Settings, see https://docs.djangoproject.com/en/3.2/ref/settings/#email-host
# Required for email confirmation and password reset (automatically activates if host is set)

View File

@@ -15,7 +15,7 @@ from .models import (BookmarkletImport, Comment, CookLog, Food, FoodInheritField
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, SearchPreference, ShareLink,
ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
TelegramBot, Unit, UserFile, UserPreference, ViewLog, Automation)
TelegramBot, Unit, UserFile, UserPreference, ViewLog, Automation, UserSpace)
class CustomUserAdmin(UserAdmin):
@@ -46,15 +46,23 @@ class SpaceAdmin(admin.ModelAdmin):
admin.site.register(Space, SpaceAdmin)
class UserSpaceAdmin(admin.ModelAdmin):
list_display = ('user', 'space',)
search_fields = ('user', 'space',)
admin.site.register(UserSpace, UserSpaceAdmin)
class UserPreferenceAdmin(admin.ModelAdmin):
list_display = ('name', 'theme', 'nav_color', 'default_page', 'search_style',) # TODO add new fields
list_display = ('name', 'theme', 'nav_color', 'default_page',)
search_fields = ('user__username',)
list_filter = ('theme', 'nav_color', 'default_page', 'search_style')
list_filter = ('theme', 'nav_color', 'default_page',)
date_hierarchy = 'created_at'
@staticmethod
def name(obj):
return obj.user.get_user_name()
return obj.user.get_user_display_name()
admin.site.register(UserPreference, UserPreferenceAdmin)
@@ -67,7 +75,7 @@ class SearchPreferenceAdmin(admin.ModelAdmin):
@staticmethod
def name(obj):
return obj.user.get_user_name()
return obj.user.get_user_display_name()
admin.site.register(SearchPreference, SearchPreferenceAdmin)
@@ -169,7 +177,7 @@ class RecipeAdmin(admin.ModelAdmin):
@staticmethod
def created_by(obj):
return obj.created_by.get_user_name()
return obj.created_by.get_user_display_name()
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
actions = [rebuild_index]
@@ -208,7 +216,7 @@ class CommentAdmin(admin.ModelAdmin):
@staticmethod
def name(obj):
return obj.created_by.get_user_name()
return obj.created_by.get_user_display_name()
admin.site.register(Comment, CommentAdmin)
@@ -227,7 +235,7 @@ class RecipeBookAdmin(admin.ModelAdmin):
@staticmethod
def user_name(obj):
return obj.created_by.get_user_name()
return obj.created_by.get_user_display_name()
admin.site.register(RecipeBook, RecipeBookAdmin)
@@ -245,7 +253,7 @@ class MealPlanAdmin(admin.ModelAdmin):
@staticmethod
def user(obj):
return obj.created_by.get_user_name()
return obj.created_by.get_user_display_name()
admin.site.register(MealPlan, MealPlanAdmin)

View File

@@ -1,62 +0,0 @@
import django_filters
from django.conf import settings
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Q
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from cookbook.forms import MultiSelectWidget
from cookbook.models import Food, Keyword, Recipe
with scopes_disabled():
class RecipeFilter(django_filters.FilterSet):
name = django_filters.CharFilter(method='filter_name')
keywords = django_filters.ModelMultipleChoiceFilter(
queryset=Keyword.objects.none(),
widget=MultiSelectWidget,
method='filter_keywords'
)
foods = django_filters.ModelMultipleChoiceFilter(
queryset=Food.objects.none(),
widget=MultiSelectWidget,
method='filter_foods',
label=_('Ingredients')
)
def __init__(self, data=None, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(data, *args, **kwargs)
self.filters['foods'].queryset = Food.objects.filter(space=space).all()
self.filters['keywords'].queryset = Keyword.objects.filter(space=space).all()
@staticmethod
def filter_keywords(queryset, name, value):
if not name == 'keywords':
return queryset
for x in value:
queryset = queryset.filter(keywords=x)
return queryset
@staticmethod
def filter_foods(queryset, name, value):
if not name == 'foods':
return queryset
for x in value:
queryset = queryset.filter(steps__ingredients__food__name=x).distinct()
return queryset
@staticmethod
def filter_name(queryset, name, value):
if not name == 'name':
return queryset
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
'django.db.backends.postgresql']:
queryset = queryset.annotate(similarity=TrigramSimilarity('name', value), ).filter(
Q(similarity__gt=0.1) | Q(name__unaccent__icontains=value)).order_by('-similarity')
else:
queryset = queryset.filter(name__icontains=value)
return queryset
class Meta:
model = Recipe
fields = ['name', 'keywords', 'foods', 'internal']

View File

@@ -45,8 +45,7 @@ class UserPreferenceForm(forms.ModelForm):
model = UserPreference
fields = (
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
'sticky_navbar', 'default_page', 'show_recent', 'search_style',
'plan_share', 'ingredient_decimals', 'comments', 'left_handed',
'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed',
)
labels = {
@@ -57,8 +56,6 @@ class UserPreferenceForm(forms.ModelForm):
'nav_color': _('Navbar color'),
'sticky_navbar': _('Sticky navbar'),
'default_page': _('Default page'),
'show_recent': _('Show recent recipes'),
'search_style': _('Search style'),
'plan_share': _('Plan sharing'),
'ingredient_decimals': _('Ingredient decimal places'),
'shopping_auto_sync': _('Shopping list auto sync period'),
@@ -68,23 +65,21 @@ class UserPreferenceForm(forms.ModelForm):
help_texts = {
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
# noqa: E501
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'), # noqa: E501
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
'use_fractions': _(
'Enables support for fractions in ingredient amounts (e.g. convert decimals to fractions automatically)'),
# noqa: E501
'use_kj': _('Display nutritional energy amounts in joules instead of calories'), # noqa: E501
'use_kj': _('Display nutritional energy amounts in joules instead of calories'),
'plan_share': _('Users with whom newly created meal plans should be shared by default.'),
'shopping_share': _('Users with whom to share shopping lists.'),
# noqa: E501
'show_recent': _('Show recently viewed recipes on search page.'), # noqa: E501
'ingredient_decimals': _('Number of decimals to round ingredients.'), # noqa: E501
'comments': _('If you want to be able to create and see comments underneath recipes.'), # noqa: E501
'ingredient_decimals': _('Number of decimals to round ingredients.'),
'comments': _('If you want to be able to create and see comments underneath recipes.'),
'shopping_auto_sync': _(
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit ' # noqa: E501
'of mobile data. If lower than instance limit it is reset when saving.' # noqa: E501
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
'of mobile data. If lower than instance limit it is reset when saving.'
),
'sticky_navbar': _('Makes the navbar stick to the top of the page.'), # noqa: E501
'sticky_navbar': _('Makes the navbar stick to the top of the page.'),
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
'mealplan_autoexclude_onhand': _('Exclude ingredients that are on hand.'),
'left_handed': _('Will optimize the UI for use with your left hand.')
@@ -336,9 +331,9 @@ class MealPlanForm(forms.ModelForm):
)
help_texts = {
'shared': _('You can list default users to share recipes with in the settings.'), # noqa: E501
'shared': _('You can list default users to share recipes with in the settings.'),
'note': _('You can use markdown to format this field. See the <a href="/docs/markdown/">docs here</a>')
# noqa: E501
}
widgets = {
@@ -493,8 +488,8 @@ class ShoppingPreferenceForm(forms.ModelForm):
help_texts = {
'shopping_share': _('Users will see all items you add to your shopping list. They must add you to see items on their list.'),
'shopping_auto_sync': _(
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit ' # noqa: E501
'of mobile data. If lower than instance limit it is reset when saving.' # noqa: E501
'Setting to 0 will disable auto sync. When viewing a shopping list the list is updated every set seconds to sync changes someone else might have made. Useful when shopping with multiple people but might use a little bit '
'of mobile data. If lower than instance limit it is reset when saving.'
),
'mealplan_autoadd_shopping': _('Automatically add meal plan ingredients to shopping list.'),
'mealplan_autoinclude_related': _('When adding a meal plan to the shopping list (manually or automatically), include all related recipes.'),

View File

@@ -14,7 +14,7 @@ class AllAuthCustomAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
"""
Whether to allow sign ups.
Whether to allow sign-ups.
"""
signup_token = False
if 'signup_token' in request.session and InviteLink.objects.filter(valid_until__gte=datetime.datetime.today(), used_by=None, uuid=request.session['signup_token']).exists():
@@ -31,7 +31,10 @@ class AllAuthCustomAdapter(DefaultAccountAdapter):
default = datetime.datetime.now()
c = caches['default'].get_or_set(email, default, timeout=360)
if c == default:
super(AllAuthCustomAdapter, self).send_mail(template_prefix, email, context)
try:
super(AllAuthCustomAdapter, self).send_mail(template_prefix, email, context)
except Exception: # dont fail signup just because confirmation mail could not be send
pass
else:
messages.add_message(self.request, messages.ERROR, _('In order to prevent spam, the requested email was not send. Please wait a few minutes and try again.'))
else:

View File

@@ -299,6 +299,45 @@ class CustomIsShare(permissions.BasePermission):
return False
class CustomRecipePermission(permissions.BasePermission):
"""
Custom permission class for recipe api endpoint
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view): # user is either at least a guest or a share link is given and the request is safe
share = request.query_params.get('share', None)
return has_group_permission(request.user, ['guest']) or (share and request.method in SAFE_METHODS and 'pk' in view.kwargs)
def has_object_permission(self, request, view, obj):
share = request.query_params.get('share', None)
if share:
return share_link_valid(obj, share)
else:
if obj.private:
return ((obj.created_by == request.user) or (request.user in obj.shared.all())) and obj.space == request.space
else:
return has_group_permission(request.user, ['guest']) and obj.space == request.space
class CustomUserPermission(permissions.BasePermission):
"""
Custom permission class for user api endpoint
"""
message = _('You do not have the required permissions to view this page!')
def has_permission(self, request, view): # a space filtered user list is visible for everyone
return has_group_permission(request.user, ['guest'])
def has_object_permission(self, request, view, obj): # object write permissions are only available for user
if request.method in SAFE_METHODS and 'pk' in view.kwargs and has_group_permission(request.user, ['guest']) and request.space in obj.userspace_set.all():
return True
elif request.user == obj:
return True
else:
return False
def above_space_limit(space): # TODO add file storage limit
"""
Test if the space has reached any limit (e.g. max recipes, users, ..)

View File

@@ -4,17 +4,14 @@ from datetime import date, timedelta
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector, TrigramSimilarity
from django.core.cache import caches
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Sum,
Value, When)
from django.db.models import (Avg, Case, Count, Exists, F, Func, Max, OuterRef, Q, Subquery, Value, When)
from django.db.models.functions import Coalesce, Lower, Substr
from django.utils import timezone, translation
from django.utils.translation import gettext as _
from cookbook.filters import RecipeFilter
from cookbook.helper.HelperFunctions import Round, str2bool
from cookbook.helper.permission_helper import has_group_permission
from cookbook.managers import DICTIONARY
from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, RecipeBook, SearchFields,
from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, SearchFields,
SearchPreference, ViewLog)
from recipes import settings
@@ -759,12 +756,3 @@ class RecipeFacet():
else:
return queryset.filter(depth__lte=depth).values('id', 'name', 'numchild').order_by(Lower('name').asc())
def old_search(request):
if has_group_permission(request.user, ('guest',)):
params = dict(request.GET)
params['internal'] = None
f = RecipeFilter(params,
queryset=Recipe.objects.filter(space=request.space).all().order_by(Lower('name').asc()),
space=request.space)
return f.qs

View File

@@ -2,7 +2,7 @@ import re
from io import BytesIO
from zipfile import ZipFile
from bs4 import BeautifulSoup
from bs4 import BeautifulSoup, Tag
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import IngredientParser
@@ -21,18 +21,21 @@ class CopyMeThat(Integration):
def get_recipe_from_file(self, file):
# 'file' comes is as a beautifulsoup object
recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip(), created_by=self.request.user, internal=True, space=self.request.space, )
try:
source = file.find("a", {"id": "original_link"}).text
except AttributeError:
source = None
recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip()[:128], source_url=source, created_by=self.request.user, internal=True, space=self.request.space, )
for category in file.find_all("span", {"class": "recipeCategory"}):
keyword, created = Keyword.objects.get_or_create(name=category.text, space=self.request.space)
recipe.keywords.add(keyword)
try:
recipe.servings = parse_servings(file.find("a", {"id": "recipeYield"}).text.strip())
recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip())
recipe.waiting_time = iso_duration_to_minutes(file.find("span", {"meta": "cookTime"}).text.strip())
recipe.description = (file.find("div ", {"id": "description"}).text.strip())[:512]
except AttributeError:
pass
@@ -42,36 +45,65 @@ class CopyMeThat(Integration):
except AttributeError:
pass
step = Step.objects.create(instruction='', space=self.request.space, )
ingredient_parser = IngredientParser(self.request, True)
for ingredient in file.find_all("li", {"class": "recipeIngredient"}):
if ingredient.text == "":
continue
amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip())
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, original_text=ingredient.text.strip(), space=self.request.space,
))
for s in file.find_all("li", {"class": "instruction"}):
if s.text == "":
continue
step.instruction += s.text.strip() + ' \n\n'
for s in file.find_all("li", {"class": "recipeNote"}):
if s.text == "":
continue
step.instruction += s.text.strip() + ' \n\n'
try:
if file.find("a", {"id": "original_link"}).text != '':
step.instruction += "\n\n" + _("Imported from") + ": " + file.find("a", {"id": "original_link"}).text
step.save()
if len(file.find("span", {"id": "made_this"}).text.strip()) > 0:
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=_('I made this'))[0])
except AttributeError:
pass
step = Step.objects.create(instruction='', space=self.request.space, )
ingredient_parser = IngredientParser(self.request, True)
ingredients = file.find("ul", {"id": "recipeIngredients"})
if isinstance(ingredients, Tag):
for ingredient in ingredients.children:
if not isinstance(ingredient, Tag) or not ingredient.text.strip() or "recipeIngredient_spacer" in ingredient['class']:
continue
if any(x in ingredient['class'] for x in ["recipeIngredient_subheader", "recipeIngredient_note"]):
step.ingredients.add(Ingredient.objects.create(is_header=True, note=ingredient.text.strip()[:256], original_text=ingredient.text.strip(), space=self.request.space, ))
else:
amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip())
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(food=f, unit=u, amount=amount, note=note, original_text=ingredient.text.strip(), space=self.request.space, ))
instructions = file.find("ol", {"id": "recipeInstructions"})
if isinstance(instructions, Tag):
for instruction in instructions.children:
if not isinstance(instruction, Tag) or instruction.text == "":
continue
if "instruction_subheader" in instruction['class']:
if step.instruction:
step.save()
recipe.steps.add(step)
step = Step.objects.create(instruction='', space=self.request.space, )
step.name = instruction.text.strip()[:128]
else:
step.instruction += instruction.text.strip() + ' \n\n'
notes = file.find_all("li", {"class": "recipeNote"})
if notes:
step.instruction += '*Notes:* \n\n'
for n in notes:
if n.text == "":
continue
step.instruction += '*' + n.text.strip() + '* \n\n'
description = ''
try:
description = file.find("div", {"id": "description"}).text.strip()
except AttributeError:
pass
if len(description) <= 512:
recipe.description = description
else:
recipe.description = description[:480] + ' ... (full description below)'
step.instruction += '*Description:* \n\n*' + description + '* \n\n'
step.save()
recipe.steps.add(step)
# import the Primary recipe image that is stored in the Zip

View File

@@ -43,7 +43,7 @@ class Integration:
self.export_type = export_type
self.ignored_recipes = []
description = f'Imported by {request.user.get_user_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}. Type: {export_type}'
description = f'Imported by {request.user.get_user_display_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}. Type: {export_type}'
icon = '📥'
try:

View File

@@ -0,0 +1,21 @@
# Generated by Django 4.0.6 on 2022-07-12 18:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0177_recipe_show_ingredient_overview'),
]
operations = [
migrations.RemoveField(
model_name='userpreference',
name='search_style',
),
migrations.RemoveField(
model_name='userpreference',
name='show_recent',
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 4.0.6 on 2022-07-13 10:53
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0178_remove_userpreference_search_style_and_more'),
]
operations = [
migrations.AddField(
model_name='recipe',
name='private',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='recipe',
name='shared',
field=models.ManyToManyField(blank=True, related_name='recipe_shared_with', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.0.6 on 2022-07-14 09:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0179_recipe_private_recipe_shared'),
]
operations = [
migrations.AddField(
model_name='invitelink',
name='reusable',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.0.6 on 2022-07-14 11:14
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0180_invitelink_reusable'),
]
operations = [
migrations.AddField(
model_name='space',
name='image',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_image', to='cookbook.userfile'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.0.6 on 2022-07-14 13:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0181_space_image'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='image',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_image', to='cookbook.userfile'),
),
]

View File

@@ -4,6 +4,7 @@ import re
import uuid
from datetime import date, timedelta
from PIL import Image
from annoying.fields import AutoOneToOneField
from django.contrib import auth
from django.contrib.auth.models import Group, User
@@ -25,7 +26,7 @@ from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT, KJ_PR
SORT_TREE_BY_NAME, STICKY_NAV_PREF_DEFAULT)
def get_user_name(self):
def get_user_display_name(self):
if not (name := f"{self.first_name} {self.last_name}") == " ":
return name
else:
@@ -57,7 +58,7 @@ def get_shopping_share(self):
]))
auth.models.User.add_to_class('get_user_name', get_user_name)
auth.models.User.add_to_class('get_user_display_name', get_user_display_name)
auth.models.User.add_to_class('get_shopping_share', get_shopping_share)
auth.models.User.add_to_class('get_active_space', get_active_space)
@@ -244,6 +245,7 @@ class FoodInheritField(models.Model, PermissionModelMixin):
class Space(ExportModelOperationsMixin('space'), models.Model):
name = models.CharField(max_length=128, default='Default')
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, related_name='space_image')
created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
created_at = models.DateTimeField(auto_now_add=True)
message = models.CharField(max_length=512, default='', blank=True)
@@ -355,34 +357,16 @@ class UserPreference(models.Model, PermissionModelMixin):
(BOOKS, _('Books')),
)
# Search Style
SMALL = 'SMALL'
LARGE = 'LARGE'
NEW = 'NEW'
SEARCH_STYLE = ((SMALL, _('Small')), (LARGE, _('Large')), (NEW, _('New')))
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, related_name='user_image')
theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR)
nav_color = models.CharField(
choices=COLORS, max_length=128, default=PRIMARY
)
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
default_unit = models.CharField(max_length=32, default='g')
use_fractions = models.BooleanField(default=FRACTION_PREF_DEFAULT)
use_kj = models.BooleanField(default=KJ_PREF_DEFAULT)
default_page = models.CharField(
choices=PAGES, max_length=64, default=SEARCH
)
search_style = models.CharField(
choices=SEARCH_STYLE, max_length=64, default=NEW
)
show_recent = models.BooleanField(default=True)
plan_share = models.ManyToManyField(
User, blank=True, related_name='plan_share_default'
)
shopping_share = models.ManyToManyField(
User, blank=True, related_name='shopping_share'
)
default_page = models.CharField(choices=PAGES, max_length=64, default=SEARCH)
plan_share = models.ManyToManyField(User, blank=True, related_name='plan_share_default')
shopping_share = models.ManyToManyField(User, blank=True, related_name='shopping_share')
ingredient_decimals = models.IntegerField(default=2)
comments = models.BooleanField(default=COMMENT_PREF_DEFAULT)
shopping_auto_sync = models.IntegerField(default=5)
@@ -612,7 +596,7 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
# remove all inherited fields from food
trough = Food.inherit_fields.through
trough.objects.all().delete()
# food is going to inherit attributes
if len(inherit) > 0:
# ManyToMany cannot be updated through an UPDATE operation
@@ -749,6 +733,8 @@ class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModel
internal = models.BooleanField(default=False)
nutrition = models.ForeignKey(NutritionInformation, blank=True, null=True, on_delete=models.CASCADE)
show_ingredient_overview = models.BooleanField(default=True)
private = models.BooleanField(default=False)
shared = models.ManyToManyField(User, blank=True, related_name='recipe_shared_with')
source_url = models.CharField(max_length=1024, default=None, blank=True, null=True)
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
@@ -1017,9 +1003,8 @@ class InviteLink(ExportModelOperationsMixin('invite_link'), models.Model, Permis
email = models.EmailField(blank=True)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
valid_until = models.DateField(default=default_valid_until)
used_by = models.ForeignKey(
User, null=True, on_delete=models.CASCADE, related_name='used_by'
)
used_by = models.ForeignKey(User, null=True, on_delete=models.CASCADE, related_name='used_by')
reusable = models.BooleanField(default=False)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
@@ -1187,6 +1172,13 @@ class UserFile(ExportModelOperationsMixin('user_files'), models.Model, Permissio
objects = ScopedManager(space='space')
space = models.ForeignKey(Space, on_delete=models.CASCADE)
def is_image(self):
try:
img = Image.open(self.file.file.file)
return True
except Exception:
return False
def save(self, *args, **kwargs):
if hasattr(self.file, 'file') and isinstance(self.file.file, UploadedFile) or isinstance(self.file.file, InMemoryUploadedFile):
self.file.name = f'{uuid.uuid4()}' + pathlib.Path(self.file.name).suffix

View File

@@ -5,7 +5,7 @@ from gettext import gettext as _
from html import escape
from smtplib import SMTPException
from django.contrib.auth.models import Group, User
from django.contrib.auth.models import Group, User, AnonymousUser
from django.core.mail import send_mail
from django.db.models import Avg, Q, QuerySet, Sum
from django.http import BadHeaderError
@@ -124,22 +124,26 @@ class SpaceFilterSerializer(serializers.ListSerializer):
# if query is sliced it came from api request not nested serializer
return super().to_representation(data)
if self.child.Meta.model == User:
data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all()
if type(self.context['request'].user) == AnonymousUser:
data = []
else:
data = data.filter(userspace__space=self.context['request'].user.get_active_space()).all()
else:
data = data.filter(**{'__'.join(data.model.get_space_key()): self.context['request'].space})
return super().to_representation(data)
class UserNameSerializer(WritableNestedModelSerializer):
username = serializers.SerializerMethodField('get_user_label')
class UserSerializer(WritableNestedModelSerializer):
display_name = serializers.SerializerMethodField('get_user_label')
def get_user_label(self, obj):
return obj.get_user_name()
return obj.get_user_display_name()
class Meta:
list_serializer_class = SpaceFilterSerializer
model = User
fields = ('id', 'username')
fields = ('id', 'username', 'first_name', 'last_name', 'display_name')
read_only_fields = ('username', )
class GroupSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
@@ -170,104 +174,6 @@ class FoodInheritFieldSerializer(UniqueFieldsMixin, WritableNestedModelSerialize
read_only_fields = ['id']
class SpaceSerializer(WritableNestedModelSerializer):
user_count = serializers.SerializerMethodField('get_user_count')
recipe_count = serializers.SerializerMethodField('get_recipe_count')
file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
food_inherit = FoodInheritFieldSerializer(many=True)
def get_user_count(self, obj):
return UserSpace.objects.filter(space=obj).count()
def get_recipe_count(self, obj):
return Recipe.objects.filter(space=obj).count()
def get_file_size_mb(self, obj):
try:
return UserFile.objects.filter(space=obj).aggregate(Sum('file_size_kb'))['file_size_kb__sum'] / 1000
except TypeError:
return 0
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
class Meta:
model = Space
fields = ('id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb',)
read_only_fields = ('id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo',)
class UserSpaceSerializer(WritableNestedModelSerializer):
user = UserNameSerializer(read_only=True)
groups = GroupSerializer(many=True)
def validate(self, data):
if self.instance.user == self.context['request'].space.created_by: # can't change space owner permission
raise serializers.ValidationError(_('Cannot modify Space owner permission.'))
return super().validate(data)
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
class Meta:
model = UserSpace
fields = ('id', 'user', 'space', 'groups', 'active', 'created_at', 'updated_at',)
read_only_fields = ('id', 'created_at', 'updated_at', 'space')
class SpacedModelSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
class Meta:
list_serializer_class = SpaceFilterSerializer
model = MealType
fields = ('id', 'name', 'order', 'icon', 'color', 'default', 'created_by')
read_only_fields = ('created_by',)
class UserPreferenceSerializer(WritableNestedModelSerializer):
food_inherit_default = serializers.SerializerMethodField('get_food_inherit_defaults')
plan_share = UserNameSerializer(many=True, allow_null=True, required=False)
shopping_share = UserNameSerializer(many=True, allow_null=True, required=False)
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
def get_food_inherit_defaults(self, obj):
return FoodInheritFieldSerializer(obj.user.get_active_space().food_inherit.all(), many=True).data
def get_food_children_exist(self, obj):
space = getattr(self.context.get('request', None), 'space', None)
return Food.objects.filter(depth__gt=0, space=space).exists()
def update(self, instance, validated_data):
with scopes_disabled():
return super().update(instance, validated_data)
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
class Meta:
model = UserPreference
fields = (
'user', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj', 'search_style',
'show_recent', 'plan_share',
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping',
'food_inherit_default', 'default_delay',
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days',
'csv_delim', 'csv_prefix',
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'food_children_exist'
)
class UserFileSerializer(serializers.ModelSerializer):
file = serializers.FileField(write_only=True)
file_download = serializers.SerializerMethodField('get_download_link')
@@ -344,6 +250,106 @@ class UserFileViewSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'file')
class SpaceSerializer(WritableNestedModelSerializer):
user_count = serializers.SerializerMethodField('get_user_count')
recipe_count = serializers.SerializerMethodField('get_recipe_count')
file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
food_inherit = FoodInheritFieldSerializer(many=True)
image = UserFileViewSerializer(required=False, many=False)
def get_user_count(self, obj):
return UserSpace.objects.filter(space=obj).count()
def get_recipe_count(self, obj):
return Recipe.objects.filter(space=obj).count()
def get_file_size_mb(self, obj):
try:
return UserFile.objects.filter(space=obj).aggregate(Sum('file_size_kb'))['file_size_kb__sum'] / 1000
except TypeError:
return 0
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
class Meta:
model = Space
fields = ('id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
'allow_sharing', 'demo', 'food_inherit', 'show_facet_count', 'user_count', 'recipe_count', 'file_size_mb', 'image',)
read_only_fields = ('id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo',)
class UserSpaceSerializer(WritableNestedModelSerializer):
user = UserSerializer(read_only=True)
groups = GroupSerializer(many=True)
def validate(self, data):
if self.instance.user == self.context['request'].space.created_by: # can't change space owner permission
raise serializers.ValidationError(_('Cannot modify Space owner permission.'))
return super().validate(data)
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
class Meta:
model = UserSpace
fields = ('id', 'user', 'space', 'groups', 'active', 'created_at', 'updated_at',)
read_only_fields = ('id', 'created_at', 'updated_at', 'space')
class SpacedModelSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
class MealTypeSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
class Meta:
list_serializer_class = SpaceFilterSerializer
model = MealType
fields = ('id', 'name', 'order', 'icon', 'color', 'default', 'created_by')
read_only_fields = ('created_by',)
class UserPreferenceSerializer(WritableNestedModelSerializer):
food_inherit_default = serializers.SerializerMethodField('get_food_inherit_defaults')
plan_share = UserSerializer(many=True, allow_null=True, required=False)
shopping_share = UserSerializer(many=True, allow_null=True, required=False)
food_children_exist = serializers.SerializerMethodField('get_food_children_exist')
image = UserFileViewSerializer(required=False, allow_null=True, many=False)
def get_food_inherit_defaults(self, obj):
return FoodInheritFieldSerializer(obj.user.get_active_space().food_inherit.all(), many=True).data
def get_food_children_exist(self, obj):
space = getattr(self.context.get('request', None), 'space', None)
return Food.objects.filter(depth__gt=0, space=space).exists()
def update(self, instance, validated_data):
with scopes_disabled():
return super().update(instance, validated_data)
def create(self, validated_data):
raise ValidationError('Cannot create using this endpoint')
class Meta:
model = UserPreference
fields = (
'user', 'image', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj',
'plan_share', 'sticky_navbar',
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping',
'food_inherit_default', 'default_delay',
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days',
'csv_delim', 'csv_prefix',
'filter_to_supermarket', 'shopping_add_onhand', 'left_handed', 'food_children_exist'
)
class StorageSerializer(SpacedModelSerializer):
def create(self, validated_data):
@@ -732,6 +738,7 @@ class RecipeSerializer(RecipeBaseSerializer):
keywords = KeywordSerializer(many=True)
rating = serializers.SerializerMethodField('get_recipe_rating')
last_cooked = serializers.SerializerMethodField('get_recipe_last_cooked')
shared = UserSerializer(many=True, required=False)
class Meta:
model = Recipe
@@ -739,6 +746,7 @@ class RecipeSerializer(RecipeBaseSerializer):
'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time',
'waiting_time', 'created_by', 'created_at', 'updated_at', 'source_url',
'internal', 'show_ingredient_overview', 'nutrition', 'servings', 'file_path', 'servings_text', 'rating', 'last_cooked',
'private', 'shared',
)
read_only_fields = ['image', 'created_by', 'created_at']
@@ -776,7 +784,7 @@ class CommentSerializer(serializers.ModelSerializer):
class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
shared = UserNameSerializer(many=True, required=False)
shared = UserSerializer(many=True, required=False)
def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user
@@ -789,7 +797,7 @@ class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerialize
class RecipeBookSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
shared = UserNameSerializer(many=True)
shared = UserSerializer(many=True)
filter = CustomFilterSerializer(allow_null=True, required=False)
def create(self, validated_data):
@@ -816,7 +824,7 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
book = validated_data['book']
recipe = validated_data['recipe']
if not book.get_owner() == self.context['request'].user and not self.context[
'request'].user in book.get_shared():
'request'].user in book.get_shared():
raise NotFound(detail=None, code=None)
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
return obj
@@ -833,7 +841,7 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
meal_type_name = serializers.ReadOnlyField(source='meal_type.name') # TODO deprecate once old meal plan was removed
note_markdown = serializers.SerializerMethodField('get_note_markdown')
servings = CustomDecimalField()
shared = UserNameSerializer(many=True, required=False, allow_null=True)
shared = UserSerializer(many=True, required=False, allow_null=True)
shopping = serializers.SerializerMethodField('in_shopping')
def get_note_markdown(self, obj):
@@ -872,11 +880,11 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
value = value.quantize(
Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
return (
obj.name
or getattr(obj.mealplan, 'title', None)
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
or obj.recipe.name
) + f' ({value:.2g})'
obj.name
or getattr(obj.mealplan, 'title', None)
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
or obj.recipe.name
) + f' ({value:.2g})'
def update(self, instance, validated_data):
# TODO remove once old shopping list
@@ -897,7 +905,7 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
ingredient_note = serializers.ReadOnlyField(source='ingredient.note')
recipe_mealplan = ShoppingListRecipeSerializer(source='list_recipe', read_only=True)
amount = CustomDecimalField()
created_by = UserNameSerializer(read_only=True)
created_by = UserSerializer(read_only=True)
completed_at = serializers.DateTimeField(allow_null=True, required=False)
def get_fields(self, *args, **kwargs):
@@ -965,7 +973,7 @@ class ShoppingListEntryCheckedSerializer(serializers.ModelSerializer):
class ShoppingListSerializer(WritableNestedModelSerializer):
recipes = ShoppingListRecipeSerializer(many=True, allow_null=True)
entries = ShoppingListEntrySerializer(many=True, allow_null=True)
shared = UserNameSerializer(many=True)
shared = UserSerializer(many=True)
supermarket = SupermarketSerializer(allow_null=True)
def create(self, validated_data):
@@ -1078,7 +1086,7 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
if obj.email:
try:
if InviteLink.objects.filter(space=self.context['request'].space, created_at__gte=datetime.now() - timedelta(hours=4)).count() < 20:
message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(self.context['request'].user.username)
message = _('Hello') + '!\n\n' + _('You have been invited by ') + escape(self.context['request'].user.get_user_display_name())
message += _(' to join their Tandoor Recipes space ') + escape(self.context['request'].space.name) + '.\n\n'
message += _('Click the following link to activate your account: ') + self.context['request'].build_absolute_uri(reverse('view_invite', args=[str(obj.uuid)])) + '\n\n'
message += _('If the link does not work use the following code to manually join the space: ') + str(obj.uuid) + '\n\n'
@@ -1100,7 +1108,7 @@ class InviteLinkSerializer(WritableNestedModelSerializer):
class Meta:
model = InviteLink
fields = (
'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'created_by', 'created_at',)
'id', 'uuid', 'email', 'group', 'valid_until', 'used_by', 'reusable', 'created_by', 'created_at',)
read_only_fields = ('id', 'uuid', 'created_by', 'created_at',)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,43 +0,0 @@
/* frac.js (C) 2012-present SheetJS -- http://sheetjs.com */
/*https://developer.aliyun.com/mirror/npm/package/frac/v/0.3.0 Apache license*/
var frac = function frac(x, D, mixed) {
var n1 = Math.floor(x), d1 = 1;
var n2 = n1+1, d2 = 1;
if(x !== n1) while(d1 <= D && d2 <= D) {
var m = (n1 + n2) / (d1 + d2);
if(x === m) {
if(d1 + d2 <= D) { d1+=d2; n1+=n2; d2=D+1; }
else if(d1 > d2) d2=D+1;
else d1=D+1;
break;
}
else if(x < m) { n2 = n1+n2; d2 = d1+d2; }
else { n1 = n1+n2; d1 = d1+d2; }
}
if(d1 > D) { d1 = d2; n1 = n2; }
if(!mixed) return [0, n1, d1];
var q = Math.floor(n1/d1);
return [q, n1 - q*d1, d1];
};
frac.cont = function cont(x, D, mixed) {
var sgn = x < 0 ? -1 : 1;
var B = x * sgn;
var P_2 = 0, P_1 = 1, P = 0;
var Q_2 = 1, Q_1 = 0, Q = 0;
var A = Math.floor(B);
while(Q_1 < D) {
A = Math.floor(B);
P = A * P_1 + P_2;
Q = A * Q_1 + Q_2;
if((B - A) < 0.00000005) break;
B = 1 / (B - A);
P_2 = P_1; P_1 = P;
Q_2 = Q_1; Q_1 = Q;
}
if(Q > D) { if(Q_1 > D) { Q = Q_2; P = P_2; } else { Q = Q_1; P = P_1; } }
if(!mixed) return [0, sgn * P, Q];
var q = Math.floor(sgn * P/Q);
return [q, sgn*P - q*Q, Q];
};
// eslint-disable-next-line no-undef
if(typeof module !== 'undefined' && typeof DO_NOT_EXPORT_FRAC === 'undefined') module.exports = frac;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,3 @@
import django_tables2 as tables
from django.utils.html import format_html
from django.utils.translation import gettext as _
@@ -8,60 +7,6 @@ from .models import (CookLog, InviteLink, Recipe, RecipeImport,
Storage, Sync, SyncLog, ViewLog)
class ImageUrlColumn(tables.Column):
def render(self, value):
if value.url:
return value.url
return None
class RecipeTableSmall(tables.Table):
id = tables.LinkColumn('edit_recipe', args=[A('id')])
name = tables.LinkColumn('view_recipe', args=[A('id')])
all_tags = tables.Column(
attrs={
'td': {'class': 'd-none d-lg-table-cell'},
'th': {'class': 'd-none d-lg-table-cell'}
}
)
class Meta:
model = Recipe
template_name = 'generic/table_template.html'
fields = ('id', 'name', 'all_tags')
class RecipeTable(tables.Table):
edit = tables.TemplateColumn(
"<a style='color: inherit' href='{% url 'edit_recipe' record.id %}' >" + _('Edit') + "</a>" # noqa: E501
)
name = tables.LinkColumn('view_recipe', args=[A('id')])
all_tags = tables.Column(
attrs={
'td': {'class': 'd-none d-lg-table-cell'},
'th': {'class': 'd-none d-lg-table-cell'}
}
)
image = ImageUrlColumn()
class Meta:
model = Recipe
template_name = 'recipes_table.html'
fields = (
'id', 'name', 'all_tags', 'description', 'image', 'instructions',
'working_time', 'waiting_time', 'internal'
)
# class IngredientTable(tables.Table):
# id = tables.LinkColumn('edit_food', args=[A('id')])
# class Meta:
# model = Keyword
# template_name = 'generic/table_template.html'
# fields = ('id', 'name')
class StorageTable(tables.Table):
id = tables.LinkColumn('edit_storage', args=[A('id')])
@@ -122,7 +67,6 @@ class RecipeImportTable(tables.Table):
fields = ('id', 'name', 'file_path')
class InviteLinkTable(tables.Table):
link = tables.TemplateColumn(
"<input value='{{ request.scheme }}://{{ request.get_host }}{% url 'view_invite' record.uuid %}' class='form-control' />"

View File

@@ -285,7 +285,7 @@
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_space,view_settings,view_history,view_system,docs_markdown' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"><i
class="fas fa-fw fa-user-alt"></i> {{ user.get_user_name }}
class="fas fa-fw fa-user-alt"></i> {{ user.get_user_display_name }}
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">

View File

@@ -1,43 +0,0 @@
{% extends "base.html" %}
{% load django_tables2 %}
{% load crispy_forms_tags %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "Cookbook" %}{% endblock %}
{% block extra_head %}
{{ units_form.media }}
{% endblock %}
{% block content %}
<h2><i class="fas fa-shopping-cart"></i> {% trans 'Edit Ingredients' %}</h2>
{% blocktrans %}
The following form can be used if, accidentally, two (or more) units or ingredients where created that should be
the same.
It merges two units or ingredients and updates all recipes using them.
{% endblocktrans %}
<br/>
<br/>
<h4>{% trans 'Units' %}</h4>
<form action="{% url 'edit_food' %}" method="post"
onsubmit="return confirm('{% trans 'Are you sure that you want to merge these two units?' %}')">
{% csrf_token %}
{{ units_form|crispy }}
<button class="btn btn-danger" type="submit"
><i
class="fas fa-sync-alt"></i> {% trans 'Merge' %}</button>
</form>
<h4>{% trans 'Ingredients' %}</h4>
<form action="{% url 'edit_food' %}" method="post"
onsubmit="return confirm('{% trans 'Are you sure that you want to merge these two ingredients?' %}')">
{% csrf_token %}
{{ food_form|crispy }}
<button class="btn btn-danger" type="submit">
<i class="fas fa-sync-alt"></i> {% trans 'Merge' %}</button>
</form>
{% endblock %}

View File

@@ -1,26 +0,0 @@
{% extends "base.html" %}
{% load crispy_forms_filters %}
{% load i18n %}
{% load static %}
{% block title %}{% trans 'Import Recipes' %}{% endblock %}
{% block extra_head %}
{{ form.media }}
{% endblock %}
{% block content %}
<h2>{% trans 'Import' %}</h2>
<div class="row">
<div class="col col-md-12">
<form action="." method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-success" type="submit"><i class="fas fa-file-import"></i> {% trans 'Import' %}
</button>
</form>
</div>
</div>
{% endblock %}

View File

@@ -1,58 +0,0 @@
{% load i18n %}
<div class="modal" tabindex="-1" role="dialog" id="modal_recipe">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans 'Recipe' %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" style="text-align: center">
<i class="fas fa-spinner fa-spin fa-8x" id="id_spinner"></i>
<a href="" id="a_recipe_open" target="_blank" onclick="afterClick()" style="font-size: 250%"></a>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>
</div>
<script type="application/javascript">
function openRecipe(id) {
var link = $('#a_recipe_open');
link.hide();
$('#id_spinner').show();
var url = "{% url 'api_get_external_file_link' recipe_id=12345 %}".replace(/12345/, id);
link.text("{% trans 'Open Recipe' %}");
$('#modal_recipe').modal('show');
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
link.attr("href", this.responseText);
link.show();
} else {
window.open(this.responseText);
$('#modal_recipe').modal('hide');
}
$('#id_spinner').hide();
}
};
xhttp.open("GET", url, true);
xhttp.send();
}
function afterClick() {
$('#modal_recipe').modal('hide');
return true;
}
</script>

View File

@@ -1,18 +0,0 @@
<!--
As there is apparently no good way to pass django named URLs to Vue/Webpack we will pack the urls we need into
this object and load it in all the templates where we load Vue apps
Reason for not using other alternatives
## django-js-reverse
bad performance because the 25kb or so path file needs to be loaded before any other request can be made
or all paths need to be printed in template which is apparently not recommended for CSP reasons (although this here
might do the same)
-->
<script type="application/javascript">
window.DJANGO_URLS = {
'edit_storage'
}
</script>

View File

@@ -37,12 +37,6 @@
"short_name": "Shopping",
"description": "View your shopping lists",
"url": "./list/shopping-list/"
},
{
"name": "Latest Shopping List",
"short_name": "Shopping List",
"description": "View the latest shopping list",
"url": "./shopping/latest/"
}
]
}

View File

@@ -1,85 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% load custom_tags %}
{% load i18n %}
{% block title %}{% trans 'Meal Plan View' %}{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{% static 'custom/css/markdown_blockquote.css' %}">
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12">
<h3>{{ plan.meal_type }} {{ plan.date }} <a href="{% url 'edit_meal_plan' plan.pk %}"
class="d-print-none"><i class="fas fa-pencil-alt"></i></a>
</h3>
<small class="text-muted">{% trans 'Created by' %} {{ plan.created_by.get_user_name }}</small>
{% if plan.shared.all %}
<br/><small class="text-muted">{% trans 'Shared with' %}
{% for x in plan.shared.all %}{{ x.get_user_name }}{% if not forloop.last %}, {% endif %} {% endfor %}</small>
{% endif %}
</div>
</div>
<br/>
<br/>
{% if plan.title %}
<div class="row">
<div class="col col-12">
<h4>{{ plan.title }}</h4>
</div>
</div>
{% endif %}
{% if plan.recipe %}
<div class="row">
<div class="col col-12">
<div class="card">
<div class="card-body">
{% recipe_rating plan.recipe request.user as rating %}
<h5 class="card-title"><a
href="{% url 'view_recipe' plan.recipe.pk %}">{{ plan.recipe }}</a> {{ rating|safe }}
</h5>
{% recipe_last plan.recipe request.user as last_cooked %}
{% if last_cooked %}
{% trans 'Last cooked' %} {{ last_cooked|date }}
{% else %}
{% trans 'Never cooked before.' %}
{% endif %}
{% if plan.recipe.keywords %}
<br/>
<br/>
{% for x in plan.recipe.keywords.all %}
<span class="badge badge-pill badge-light">{{ x }}</span>
{% endfor %}
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
{% if plan.note %}
<br/>
<div class="row">
<div class="col col-12">
{{ plan.note | markdown | safe }}
</div>
</div>
{% endif %}
{% if same_day_plan %}
<br/>
<h4>{% trans 'Other meals on this day' %}</h4>
<ul class="list-group list-group-flush">
{% for x in same_day_plan %}
<li class="list-group-item"><a href="{% url 'view_plan_entry' x.pk %}">{{ x.get_label }}
({{ x.meal_type }})</a></li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,32 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% block title %}{% trans 'Profile' %}{% endblock %}
{% block content %}
<div id="app" >
<profile-view></profile-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.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
</script>
{% render_bundle 'profile_view' %}
{% endblock %}

View File

@@ -64,7 +64,6 @@
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
window.RECIPE_ID = {{recipe.pk}};
window.USER_SERVINGS = {{ user_servings }};
window.SHARE_UID = '{{ share }}';
window.USER_PREF = {
'use_fractions': {% if request.user.userpreference.use_fractions %} true {% else %} false {% endif %},

View File

@@ -1,153 +0,0 @@
{% load crispy_forms_tags %}
{% load i18n %}
{% load django_tables2 %}
{% load static %}
{% load custom_tags %}
{% block content %}
<div class="row">
<div class="col">
<div class="table-container">
{% block table %}
<table {% render_attrs table.attrs class="table" %}>
{% for row in table.paginated_rows %}
<div class="card" style="margin-top: 1px;">
<div class="row no-gutters">
<div class="col-md-4">
<a href="{% url 'view_recipe' row.cells.id %}">
{% if row.cells.image|length > 1 %}
<img src=" {{ row.cells.image }}" alt="{% trans 'Recipe Image' %}"
class="card-img" style="object-fit:cover;height: 160px">
{% else %}
<img src="{% static 'assets/recipe_no_image.svg' %}"
alt="{% trans 'Recipe Image' %}"
class="card-img d-none d-md-block"
style="object-fit: cover; height: 130px">
{% endif %}
</a>
</div>
<div class="col-md-8">
<div class="card-body" style="padding: 16px">
<div class="d-flex">
<div class="flex-fill">
<h5 class="card-title p-0 m-0">{{ row.cells.name }}
{% recipe_rating row.record request.user as rating %}
{{ rating|safe }}
</h5>
{%if row.record.description|length > 0 %}
<p class="card-subtitle p-0 m-0 text-muted" style="height:3em; overflow:hidden;">
{{ row.cells.description }}
</p>
{% endif %}
<p class="card-text{% if not row.record.keywords %} d-none d-lg-block{% endif %}">
{% for x in row.record.keywords.all %}
<span class="badge badge-pill badge-light">{{ x }}</span>
{% endfor %}
</p>
<p class="card-text">
{% if row.cells.working_time != 0 %}
<span class="badge badge-secondary"><i
class="fas fa-user-clock"></i> {% trans 'Preparation time ca.' %} {{ row.cells.working_time }} min </span>
{% endif %}
{% if row.cells.waiting_time != 0 %}
<span
class="badge badge-secondary"><i
class="far fa-clock"></i> {% trans 'Waiting time ca.' %} {{ row.cells.waiting_time }} min </span>
{% endif %}
{% if not row.record.internal %}
<span class="badge badge-info">{% trans 'External' %} </span>
{% endif %}
{% recipe_last row.record request.user as last_cooked %}
{% if last_cooked %}
<span class="badge badge-primary">{% trans 'Last cooked' %} {{ last_cooked|date }}</span>
{% endif %}
</p>
</div>
<div>
<div class="dropdown">
<a class="btn shadow-none" href="#" role="button"
id="dropdownMenuLink"
data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<i class="fas fa-ellipsis-v text-muted"></i>
</a>
<div class="dropdown-menu dropdown-menu-right"
aria-labelledby="dropdownMenuLink">
<a class="dropdown-item"
href="{% url 'edit_recipe' row.record.pk %}"><i
class="fas fa-pencil-alt fa-fw"></i> {% trans 'Edit' %}
</a>
<button class="dropdown-item"
onclick="openCookLogModal({{ row.record.pk }})"><i
class="fas fa-clipboard-list fa-fw"></i> {% trans 'Log Cooking' %}
</button>
<a class="dropdown-item"
href="{% url 'delete_recipe' row.record.pk %}"><i
class="fas fa-trash fa-fw"></i> {% trans 'Delete' %}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</table>
{% endblock table %}
</div>
</div>
</div>
{% block pagination %}
{% if table.page and table.paginator.num_pages > 1 %}
<nav aria-label="Table navigation">
<ul class="pagination justify-content-center flex-wrap">
{% if table.page.has_previous %}
{% block pagination.previous %}
<li class="previous page-item">
<a href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}"
class="page-link">
<span aria-hidden="true">&laquo;</span>
{% trans 'previous' %}
</a>
</li>
{% endblock pagination.previous %}
{% endif %}
{% if table.page.has_previous or table.page.has_next %}
{% block pagination.range %}
{% for p in table.page|table_page_range:table.paginator %}
<li class="page-item{% if table.page.number == p %} active{% endif %}">
<a class="page-link"
{% if p != '...' %}href="{% querystring table.prefixed_page_field=p %}"{% endif %}>
{{ p }}
</a>
</li>
{% endfor %}
{% endblock pagination.range %}
{% endif %}
{% if table.page.has_next %}
{% block pagination.next %}
<li class="next page-item">
<a href="{% querystring table.prefixed_page_field=table.page.next_page_number %}"
class="page-link">
{% trans 'next' %}
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% endblock pagination.next %}
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock pagination %}
{% endblock content %}

View File

@@ -1,13 +1,19 @@
{% extends "base.html" %} {% load render_bundle from webpack_loader %} {% load static %} {% load i18n %} {% block title %} {{ title }} {% endblock %} {% block content_fluid %}
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% block title %} {{ title }} {% endblock %}
<div id="app">
<shopping-list-view></shopping-list-view>
</div>
{% block content_fluid %}
<div id="app">
<shopping-list-view></shopping-list-view>
</div>
{% endblock %} {% block script %} {% if debug %}
<script src="{% url 'js_reverse' %}"></script>
<script src="{% url 'js_reverse' %}"></script>
{% else %}
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
{% endif %}
<script type="application/javascript">

View File

@@ -32,15 +32,23 @@
{% for us in request.user.userspace_set.all %}
<div class="card">
{% if us.space.image and us.space.image.is_image %}
<img style="height: 15vh; object-fit: cover" src="{{ us.space.image.file.url }}"
class="card-img-top" alt="Image">
{% else %}
<img style="height: 15vh; object-fit: cover" src="{% static 'assets/recipe_no_image.svg' %}"
class="card-img-top" alt="Image">
{% endif %}
<div class="card-body">
<h5 class="card-title"><a
href="{% url 'view_switch_space' us.space.id %}">{{ us.space.name }}</a>
</h5>
{# {% if us.active %}#}
{# <i class="far fa-dot-circle fa-fw"></i>#}
{# {% else %}#}
{# <i class="far fa-circle fa-fw"></i>#}
{# {% endif %}#}
{# {% if us.active %}#}
{# <i class="far fa-dot-circle fa-fw"></i>#}
{# {% else %}#}
{# <i class="far fa-circle fa-fw"></i>#}
{# {% endif %}#}
<p class="card-text"><small
class="text-muted">{% trans 'Owner' %}: {{ us.space.created_by }}</small>
{% if us.space.created_by != us.user %}

View File

@@ -1,53 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans 'Stats' %}{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12">
<h3>{% trans 'Statistics' %} </h3>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
{% trans 'Number of objects' %}
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">{% trans 'Recipes' %} : <span
class="badge badge-pill badge-info">{{ counts.recipes }}</span></li>
<li class="list-group-item">{% trans 'Keywords' %} : <span
class="badge badge-pill badge-info">{{ counts.keywords }}</span></li>
<li class="list-group-item">{% trans 'Units' %} : <span
class="badge badge-pill badge-info">{{ counts.units }}</span></li>
<li class="list-group-item">{% trans 'Ingredients' %} : <span
class="badge badge-pill badge-info">{{ counts.ingredients }}</span></li>
<li class="list-group-item">{% trans 'Recipe Imports' %} : <span
class="badge badge-pill badge-info">{{ counts.recipe_import }}</span></li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
{% trans 'Objects stats' %}
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">{% trans 'Recipes without Keywords' %} : <span
class="badge badge-pill badge-info">{{ counts.recipes_no_keyword }}</span></li>
<li class="list-group-item">{% trans 'External Recipes' %} : <span
class="badge badge-pill badge-info">{{ counts.recipes_external }}</span></li>
<li class="list-group-item">{% trans 'Internal Recipes' %} : <span
class="badge badge-pill badge-info">{{ counts.recipes_internal }}</span></li>
<li class="list-group-item">{% trans 'Comments' %} : <span
class="badge badge-pill badge-info">{{ counts.comments }}</span></li>
</ul>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load static %}
{% load i18n %}
{% load l10n %}
{% load custom_tags %}
{% block title %}{% trans 'Settings' %}{% endblock %}
{% block content %}
<div id="app">
<settings-view></settings-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.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
window.USER_ID = {{ request.user.pk }}
<!--TODO build custom API endpoint for this -->
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
window.AVAILABLE_LANGUAGES = [
{% for language in languages %}
['{{ language.name_local }}', '{{ language.code }}'],
{% endfor %}
]
</script>
{% render_bundle 'settings_view' %}
{% endblock %}

View File

@@ -1,6 +1,7 @@
import json
import pytest
from django.contrib import auth
from django.urls import reverse
from django_scopes import scopes_disabled
@@ -30,6 +31,7 @@ def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)['results']) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 0
# test for space filter
with scopes_disabled():
recipe_1_s1.space = space_2
recipe_1_s1.save()
@@ -37,8 +39,23 @@ def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)['results']) == 0
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 1
# test for private recipe filter
with scopes_disabled():
recipe_1_s1.created_by = auth.get_user(u1_s1)
recipe_1_s1.private = True
recipe_1_s1.save()
def test_share_permission(recipe_1_s1, u1_s1, u1_s2, a_u):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)['results']) == 0
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 0
with scopes_disabled():
recipe_1_s1.created_by = auth.get_user(u1_s2)
recipe_1_s1.save()
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 1
def test_share_permission(recipe_1_s1, u1_s1, u1_s2, u2_s1, a_u):
assert u1_s1.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk])).status_code == 200
assert u1_s2.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk])).status_code == 404
@@ -52,6 +69,15 @@ def test_share_permission(recipe_1_s1, u1_s1, u1_s2, a_u):
assert u1_s1.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk]) + f'?share={share.uuid}').status_code == 200
assert u1_s2.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk]) + f'?share={share.uuid}').status_code == 404 # TODO fix in https://github.com/TandoorRecipes/recipes/issues/1238
recipe_1_s1.created_by = auth.get_user(u1_s1)
recipe_1_s1.private = True
recipe_1_s1.save()
assert a_u.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk]) + f'?share={share.uuid}').status_code == 200
assert u1_s1.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk]) + f'?share={share.uuid}').status_code == 200
assert u2_s1.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk]) + f'?share={share.uuid}').status_code == 200
assert u2_s1.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk])).status_code == 403
@pytest.mark.parametrize("arg", [
['a_u', 403],
@@ -80,6 +106,38 @@ def test_update(arg, request, recipe_1_s1):
validate_recipe(j, json.loads(r.content))
def test_update_share(u1_s1, u2_s1, u1_s2, recipe_1_s1):
with scopes_disabled():
r = u1_s1.patch(
reverse(
DETAIL_URL,
args={recipe_1_s1.id}
),
{'shared': [{'id': auth.get_user(u1_s2).pk, 'username': auth.get_user(u1_s2).username}, {'id': auth.get_user(u2_s1).pk, 'username': auth.get_user(u2_s1).username}]},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == 200
assert len(response['shared']) == 1
assert response['shared'][0]['id'] == auth.get_user(u2_s1).pk
def test_update_private_recipe(u1_s1, u2_s1, recipe_1_s1):
r = u1_s1.patch(reverse(DETAIL_URL, args={recipe_1_s1.id}), {'name': 'test1'}, content_type='application/json')
assert r.status_code == 200
with scopes_disabled():
recipe_1_s1.private = True
recipe_1_s1.created_by = auth.get_user(u1_s1)
recipe_1_s1.save()
r = u1_s1.patch(reverse(DETAIL_URL, args={recipe_1_s1.id}), {'name': 'test2'}, content_type='application/json')
assert r.status_code == 200
r = u2_s1.patch(reverse(DETAIL_URL, args={recipe_1_s1.id}), {'name': 'test3'}, content_type='application/json')
assert r.status_code == 403
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 201],
@@ -107,22 +165,22 @@ def test_add(arg, request, u1_s2):
x += 1
def test_delete(u1_s1, u1_s2, recipe_1_s1):
def test_delete(u1_s1, u1_s2, u2_s1, recipe_1_s1, recipe_2_s1):
with scopes_disabled():
r = u1_s2.delete(
reverse(
DETAIL_URL,
args={recipe_1_s1.id}
)
)
r = u1_s2.delete(reverse(DETAIL_URL, args={recipe_1_s1.id}))
assert r.status_code == 404
r = u1_s1.delete(
reverse(
DETAIL_URL,
args={recipe_1_s1.id}
)
)
r = u1_s1.delete(reverse(DETAIL_URL, args={recipe_1_s1.id}))
assert r.status_code == 204
assert not Recipe.objects.filter(pk=recipe_1_s1.id).exists()
recipe_2_s1.created_by = auth.get_user(u1_s1)
recipe_2_s1.private = True
recipe_2_s1.save()
r = u2_s1.delete(reverse(DETAIL_URL, args={recipe_2_s1.id}))
assert r.status_code == 403
r = u1_s1.delete(reverse(DETAIL_URL, args={recipe_2_s1.id}))
assert r.status_code == 204

View File

@@ -7,22 +7,11 @@ from django.urls import reverse
from cookbook.models import UserSpace
LIST_URL = 'api:username-list'
DETAIL_URL = 'api:username-detail'
LIST_URL = 'api:user-list'
DETAIL_URL = 'api:user-detail'
def test_forbidden_methods(u1_s1):
r = u1_s1.post(
reverse(LIST_URL))
assert r.status_code == 405
r = u1_s1.put(
reverse(
DETAIL_URL,
args=[auth.get_user(u1_s1).pk])
)
assert r.status_code == 405
r = u1_s1.delete(
reverse(
DETAIL_URL,
@@ -69,3 +58,56 @@ def test_list_space(u1_s1, u2_s1, u1_s2, space_2):
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 1
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 2
@pytest.mark.parametrize("arg", [
['a_u', 403],
['g1_s1', 403],
['u1_s1', 200],
['a1_s1', 403],
['g1_s2', 404],
['u1_s2', 404],
['a1_s2', 404],
])
def test_user_retrieve(arg, request, u1_s1):
c = request.getfixturevalue(arg[0])
r = c.get(reverse(DETAIL_URL, args={auth.get_user(u1_s1).id}), )
print(r.content, auth.get_user(u1_s1).username)
assert r.status_code == arg[1]
def test_user_update(u1_s1, u2_s1,u1_s2):
# can update own user
r = u1_s1.patch(
reverse(
DETAIL_URL,
args={auth.get_user(u1_s1).id}
),
{'first_name': 'test'},
content_type='application/json'
)
response = json.loads(r.content)
assert r.status_code == 200
assert response['first_name'] == 'test'
# can't update another user
r = u1_s1.patch(
reverse(
DETAIL_URL,
args={auth.get_user(u2_s1).id}
),
{'first_name': 'test'},
content_type='application/json'
)
assert r.status_code == 403
r = u1_s1.patch(
reverse(
DETAIL_URL,
args={auth.get_user(u1_s2).id}
),
{'first_name': 'test'},
content_type='application/json'
)
assert r.status_code == 404

View File

@@ -47,7 +47,7 @@ router.register(r'sync', api.SyncViewSet)
router.register(r'sync-log', api.SyncLogViewSet)
router.register(r'unit', api.UnitViewSet)
router.register(r'user-file', api.UserFileViewSet)
router.register(r'user-name', api.UserNameViewSet, basename='username')
router.register(r'user', api.UserViewSet)
router.register(r'user-preference', api.UserPreferenceViewSet)
router.register(r'user-space', api.UserSpaceViewSet)
router.register(r'view-log', api.ViewLogViewSet)
@@ -59,23 +59,22 @@ urlpatterns = [
path('space-overview', views.space_overview, name='view_space_overview'),
path('space-manage/<int:space_id>', views.space_manage, name='view_space_manage'),
path('switch-space/<int:space_id>', views.switch_space, name='view_switch_space'),
path('profile/<int:user_id>', views.view_profile, name='view_profile'),
path('no-perm', views.no_perm, name='view_no_perm'),
path('invite/<slug:token>', views.invite_link, name='view_invite'),
path('system/', views.system, name='view_system'),
path('search/', views.search, name='view_search'),
path('search/v2/', views.search_v2, name='view_search_v2'),
path('books/', views.books, name='view_books'),
path('plan/', views.meal_plan, name='view_plan'),
path('plan/entry/<int:pk>', views.meal_plan_entry, name='view_plan_entry'),
path('shopping/latest/', lists.shopping_list, name='view_shopping_latest'),
path('shopping/', lists.shopping_list, name='view_shopping'),
path('settings/', views.user_settings, name='view_settings'),
path('user-settings/', views.user_settings_new, name='view_user_settings'),
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('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
path('import/', import_export.import_recipe, name='view_import'),
path('api/import/', api.import_files, name='view_import'),
path('import-response/<int:pk>/', import_export.import_response, name='view_import_response'),
path('export/', import_export.export_recipe, name='view_export'),
path('export-response/<int:pk>/', import_export.export_response, name='view_export_response'),
@@ -103,7 +102,6 @@ urlpatterns = [
path('data/batch/edit', data.batch_edit, name='data_batch_edit'),
path('data/batch/import', data.batch_import, name='data_batch_import'),
path('data/sync/wait', data.sync_wait, name='data_sync_wait'),
path('data/statistics', data.statistics, name='data_stats'),
path('data/import/url', data.import_url, name='data_import_url'),
path('api/get_external_file_link/<int:recipe_id>/', api.get_external_file_link, name='api_get_external_file_link'),

View File

@@ -2,6 +2,7 @@ import io
import json
import mimetypes
import re
import threading
import traceback
import uuid
from collections import OrderedDict
@@ -44,6 +45,7 @@ from rest_framework.throttling import AnonRateThrottle
from rest_framework.viewsets import ViewSetMixin
from treebeard.exceptions import InvalidMoveToDescendant, InvalidPosition, PathOverflow
from cookbook.forms import ImportForm
from cookbook.helper import recipe_url_import as helper
from cookbook.helper.HelperFunctions import str2bool
from cookbook.helper.image_processing import handle_image
@@ -51,8 +53,8 @@ from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner,
CustomIsOwnerReadOnly, CustomIsShare, CustomIsShared,
CustomIsSpaceOwner, CustomIsUser, group_required,
is_space_owner, switch_user_active_space)
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch, old_search
is_space_owner, switch_user_active_space, above_space_limit, CustomRecipePermission, CustomUserPermission)
from cookbook.helper.recipe_search import RecipeFacet, RecipeSearch
from cookbook.helper.recipe_url_import import get_from_youtube_scraper, get_images_from_soup
from cookbook.helper.scrapers.scrapers import text_scraper
from cookbook.helper.shopping_helper import RecipeShoppingEditor, shopping_helper
@@ -83,8 +85,9 @@ from cookbook.serializer import (AutomationSerializer, BookmarkletImportListSeri
SupermarketCategoryRelationSerializer,
SupermarketCategorySerializer, SupermarketSerializer,
SyncLogSerializer, SyncSerializer, UnitSerializer,
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
UserFileSerializer, UserSerializer, UserPreferenceSerializer,
UserSpaceSerializer, ViewLogSerializer)
from cookbook.views.import_export import get_integration
from recipes import settings
@@ -351,7 +354,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin, ExtendedRecipeMixin):
return Response(content, status=status.HTTP_400_BAD_REQUEST)
class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
class UserViewSet(viewsets.ModelViewSet):
"""
list:
optional parameters
@@ -359,9 +362,9 @@ class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
- **filter_list**: array of user id's to get names for
"""
queryset = User.objects
serializer_class = UserNameSerializer
permission_classes = [CustomIsGuest]
http_method_names = ['get']
serializer_class = UserSerializer
permission_classes = [CustomUserPermission]
http_method_names = ['get', 'patch']
def get_queryset(self):
queryset = self.queryset.filter(userspace__space=self.request.space)
@@ -712,7 +715,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
queryset = Recipe.objects
serializer_class = RecipeSerializer
# TODO split read and write permission for meal plan guest
permission_classes = [CustomIsShare | CustomIsGuest]
permission_classes = [CustomRecipePermission]
pagination_class = RecipePagination
query_params = [
@@ -720,7 +723,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
'Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
QueryParam(name='keywords', description=_(
'ID of keyword a recipe should have. For multiple repeat parameter. Equivalent to keywords_or'),
qtype='int'),
qtype='int'),
QueryParam(name='keywords_or',
description=_('Keyword IDs, repeat for multiple. Return recipes with any of the keywords'),
qtype='int'),
@@ -779,13 +782,14 @@ class RecipeViewSet(viewsets.ModelViewSet):
def get_queryset(self):
share = self.request.query_params.get('share', None)
if self.detail:
if not share:
if self.detail: # if detail request and not list, private condition is verified by permission class
if not share: # filter for space only if not shared
self.queryset = self.queryset.filter(space=self.request.space)
return super().get_queryset()
if not (share and self.detail):
self.queryset = self.queryset.filter(space=self.request.space)
self.queryset = self.queryset.filter(space=self.request.space).filter(
Q(private=False) | (Q(private=True) & (Q(created_by=self.request.user) | Q(shared=self.request.user)))
)
params = {x: self.request.GET.get(x) if len({**self.request.GET}[x]) == 1 else self.request.GET.getlist(x) for x
in list(self.request.GET)}
@@ -797,12 +801,9 @@ class RecipeViewSet(viewsets.ModelViewSet):
if self.request.GET.get('debug', False):
return JsonResponse({
'new': str(self.get_queryset().query),
'old': str(old_search(request).query)
})
return super().list(request, *args, **kwargs)
# TODO write extensive tests for permissions
def get_serializer_class(self):
if self.action == 'list':
return RecipeOverviewSerializer
@@ -1259,6 +1260,35 @@ def download_file(request, file_id):
return Response({}, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
# @schema(AutoSchema()) #TODO add proper schema
@permission_classes([CustomIsUser])
def import_files(request):
"""
function to handle files passed by application importer
"""
limit, msg = above_space_limit(request.space)
if limit:
return Response({'error': msg}, status=status.HTTP_400_BAD_REQUEST)
form = ImportForm(request.POST, request.FILES)
if form.is_valid() and request.FILES != {}:
try:
integration = get_integration(request, form.cleaned_data['type'])
il = ImportLog.objects.create(type=form.cleaned_data['type'], created_by=request.user, space=request.space)
files = []
for f in request.FILES.getlist('files'):
files.append({'file': io.BytesIO(f.read()), 'name': f.name})
t = threading.Thread(target=integration.do_import, args=[files, il, form.cleaned_data['duplicates']])
t.setDaemon(True)
t.start()
return Response({'import_id': il.pk}, status=status.HTTP_200_OK)
except NotImplementedError:
return Response({'error': True, 'msg': _('Importing is not implemented for this provider')}, status=status.HTTP_400_BAD_REQUEST)
def get_recipe_provider(recipe):
if recipe.storage.method == Storage.DROPBOX:
return Dropbox

View File

@@ -123,26 +123,4 @@ def import_url(request):
if bookmarklet_import := BookmarkletImport.objects.filter(id=request.GET['id']).first():
bookmarklet_import_id = bookmarklet_import.pk
return render(request, 'url_import.html', {'api_token': api_token, 'bookmarklet_import_id': bookmarklet_import_id})
class Object(object):
pass
@group_required('user')
def statistics(request):
counts = Object()
counts.recipes = Recipe.objects.filter(space=request.space).count()
counts.keywords = Keyword.objects.filter(space=request.space).count()
counts.recipe_import = RecipeImport.objects.filter(space=request.space).count()
counts.units = Unit.objects.filter(space=request.space).count()
counts.ingredients = Food.objects.filter(space=request.space).count()
counts.comments = Comment.objects.filter(recipe__space=request.space).count()
counts.recipes_internal = Recipe.objects.filter(internal=True, space=request.space).count()
counts.recipes_external = counts.recipes - counts.recipes_internal
counts.recipes_no_keyword = Recipe.objects.filter(keywords=None, space=request.space).count()
return render(request, 'stats.html', {'counts': counts})
return render(request, 'url_import.html', {'api_token': api_token, 'bookmarklet_import_id': bookmarklet_import_id})

View File

@@ -82,42 +82,6 @@ def get_integration(request, export_type):
return Cookmate(request, export_type)
@group_required('user')
def import_recipe(request):
limit, msg = above_space_limit(request.space)
if limit:
messages.add_message(request, messages.WARNING, msg)
return HttpResponseRedirect(reverse('index'))
if request.method == "POST":
form = ImportForm(request.POST, request.FILES)
if form.is_valid() and request.FILES != {}:
try:
integration = get_integration(request, form.cleaned_data['type'])
il = ImportLog.objects.create(type=form.cleaned_data['type'], created_by=request.user, space=request.space)
files = []
for f in request.FILES.getlist('files'):
files.append({'file': BytesIO(f.read()), 'name': f.name})
t = threading.Thread(target=integration.do_import, args=[files, il, form.cleaned_data['duplicates']])
t.setDaemon(True)
t.start()
return JsonResponse({'import_id': il.pk})
except NotImplementedError:
return JsonResponse(
{
'error': True,
'msg': _('Importing is not implemented for this provider')
},
status=400
)
else:
form = ImportForm()
return render(request, 'import.html', {'form': form})
@group_required('user')
def export_recipe(request):
if request.method == "POST":

View File

@@ -11,28 +11,22 @@ from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.models import Group
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.db.models import Avg, Q
from django.db.models.functions import Lower
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext as _
from django_scopes import scopes_disabled
from django_tables2 import RequestConfig
from rest_framework.authtoken.models import Token
from cookbook.filters import RecipeFilter
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, ShoppingPreferenceForm,
SpaceCreateForm, SpaceJoinForm, SpacePreferenceForm, User,
SpaceCreateForm, SpaceJoinForm, User,
UserCreateForm, UserNameForm, UserPreference, UserPreferenceForm)
from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid, switch_user_active_space
from cookbook.models import (Comment, CookLog, Food, InviteLink, Keyword,
MealPlan, RecipeImport, SearchFields, SearchPreference, ShareLink,
Space, Unit, ViewLog, UserSpace)
from cookbook.tables import (CookLogTable, InviteLinkTable, RecipeTable, RecipeTableSmall,
ViewLogTable)
from cookbook.views.data import Object
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan, SearchFields, SearchPreference, ShareLink,
Space, ViewLog, UserSpace)
from cookbook.tables import (CookLogTable, ViewLogTable)
from recipes.version import BUILD_REF, VERSION_NUMBER
@@ -58,34 +52,7 @@ def index(request):
# TODO need to deprecate
def search(request):
if has_group_permission(request.user, ('guest',)):
if request.user.userpreference.search_style == UserPreference.NEW:
return search_v2(request)
f = RecipeFilter(request.GET,
queryset=Recipe.objects.filter(space=request.space).all().order_by(
Lower('name').asc()),
space=request.space)
if request.user.userpreference.search_style == UserPreference.LARGE:
table = RecipeTable(f.qs)
else:
table = RecipeTableSmall(f.qs)
RequestConfig(request, paginate={'per_page': 25}).configure(table)
if request.GET == {} and request.user.userpreference.show_recent:
qs = Recipe.objects.filter(viewlog__created_by=request.user).filter(
space=request.space).order_by('-viewlog__created_at').all()
recent_list = []
for r in qs:
if r not in recent_list:
recent_list.append(r)
if len(recent_list) >= 5:
break
last_viewed = RecipeTable(recent_list)
else:
last_viewed = None
return render(request, 'index.html', {'recipes': table, 'filter': f, 'last_viewed': last_viewed})
return render(request, 'search.html', {})
else:
if request.user.is_authenticated:
return HttpResponseRedirect(reverse('view_no_group'))
@@ -93,11 +60,6 @@ def search(request):
return HttpResponseRedirect(reverse('account_login') + '?next=' + request.path)
@group_required('guest')
def search_v2(request):
return render(request, 'search.html', {})
def no_groups(request):
return render(request, 'no_groups_info.html')
@@ -134,7 +96,7 @@ def space_overview(request):
if 'signup_token' in request.session:
return HttpResponseRedirect(reverse('view_invite', args=[request.session.pop('signup_token', '')]))
create_form = SpaceCreateForm(initial={'name': f'{request.user.username}\'s Space'})
create_form = SpaceCreateForm(initial={'name': f'{request.user.get_user_display_name()}\'s Space'})
join_form = SpaceJoinForm()
return render(request, 'space_overview.html', {'create_form': create_form, 'join_form': join_form})
@@ -190,18 +152,6 @@ def recipe_view(request, pk, share=None):
comment_form = CommentForm()
user_servings = None
if request.user.is_authenticated:
user_servings = CookLog.objects.filter(
recipe=recipe,
created_by=request.user,
servings__gt=0,
space=request.space,
).all().aggregate(Avg('servings'))['servings__avg']
if not user_servings:
user_servings = 0
if request.user.is_authenticated:
if not ViewLog.objects.filter(recipe=recipe, created_by=request.user,
created_at__gt=(timezone.now() - timezone.timedelta(minutes=5)),
@@ -209,8 +159,7 @@ def recipe_view(request, pk, share=None):
ViewLog.objects.create(recipe=recipe, created_by=request.user, space=request.space)
return render(request, 'recipe_view.html',
{'recipe': recipe, 'comments': comments, 'comment_form': comment_form, 'share': share,
'user_servings': user_servings})
{'recipe': recipe, 'comments': comments, 'comment_form': comment_form, 'share': share, })
@group_required('user')
@@ -228,6 +177,16 @@ def supermarket(request):
return render(request, 'supermarket.html', {})
@group_required('user')
def view_profile(request, user_id):
return render(request, 'profile.html', {})
@group_required('guest')
def user_settings_new(request):
return render(request, 'user_settings.html', {})
@group_required('user')
def ingredient_editor(request):
template_vars = {'food_id': -1, 'unit_id': -1}
@@ -241,23 +200,6 @@ def ingredient_editor(request):
return render(request, 'ingredient_editor.html', template_vars)
@group_required('user')
def meal_plan_entry(request, pk):
plan = MealPlan.objects.filter(space=request.space).get(pk=pk)
if plan.created_by != request.user and plan.shared != request.user:
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
same_day_plan = MealPlan.objects \
.filter(date=plan.date, space=request.space) \
.exclude(pk=plan.pk) \
.filter(Q(created_by=request.user) | Q(shared=request.user)) \
.order_by('meal_type').all()
return render(request, 'meal_plan_entry.html', {'plan': plan, 'same_day_plan': same_day_plan})
@group_required('guest')
def user_settings(request):
if request.space.demo:
@@ -283,8 +225,6 @@ def user_settings(request):
up.nav_color = form.cleaned_data['nav_color']
up.default_unit = form.cleaned_data['default_unit']
up.default_page = form.cleaned_data['default_page']
up.show_recent = form.cleaned_data['show_recent']
up.search_style = form.cleaned_data['search_style']
up.plan_share.set(form.cleaned_data['plan_share'])
up.ingredient_decimals = form.cleaned_data['ingredient_decimals'] # noqa: E501
up.comments = form.cleaned_data['comments']
@@ -496,8 +436,9 @@ def invite_link(request, token):
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
if request.user.is_authenticated and not request.user.userspace_set.filter(space=link.space).exists():
link.used_by = request.user
link.save()
if not link.reusable:
link.used_by = request.user
link.save()
user_space = UserSpace.objects.create(user=request.user, space=link.space, active=False)
@@ -519,6 +460,9 @@ def invite_link(request, token):
@group_required('admin')
def space_manage(request, space_id):
if request.space.demo:
messages.add_message(request, messages.ERROR, _('This feature is not available in the demo version!'))
return redirect('index')
space = get_object_or_404(Space, id=space_id)
switch_user_active_space(request.user, space)
return render(request, 'space_manage.html', {})

View File

@@ -52,6 +52,9 @@ SHOPPING_MIN_AUTOSYNC_INTERVAL = int(os.getenv('SHOPPING_MIN_AUTOSYNC_INTERVAL',
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS').split(',') if os.getenv('ALLOWED_HOSTS') else ['*']
if os.getenv('CSRF_TRUSTED_ORIGINS'):
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',')
CORS_ORIGIN_ALLOW_ALL = True
LOGIN_REDIRECT_URL = "index"
@@ -99,7 +102,6 @@ INSTALLED_APPS = [
'django_prometheus',
'django_tables2',
'corsheaders',
'django_filters',
'crispy_forms',
'rest_framework',
'rest_framework.authtoken',
@@ -409,6 +411,8 @@ if os.getenv('S3_ACCESS_KEY', ''):
if os.getenv('S3_ENDPOINT_URL', ''):
AWS_S3_ENDPOINT_URL = os.getenv('S3_ENDPOINT_URL', '')
if os.getenv('S3_CUSTOM_DOMAIN', ''):
AWS_S3_CUSTOM_DOMAIN = os.getenv('S3_CUSTOM_DOMAIN', '')
MEDIA_URL = os.getenv('MEDIA_URL', '/media/')
MEDIA_ROOT = os.path.join(BASE_DIR, "mediafiles")

View File

@@ -4,10 +4,9 @@ django-annoying==0.10.6
django-autocomplete-light==3.9.4
django-cleanup==6.0.0
django-crispy-forms==1.14.0
django-filter==22.1
django-tables2==2.4.1
djangorestframework==3.13.1
drf-writable-nested==0.6.3
drf-writable-nested==0.6.4
bleach==5.0.1
bleach-allowlist==1.0.3
gunicorn==20.1.0
@@ -20,7 +19,7 @@ requests==2.28.1
six==1.16.0
webdavclient3==3.14.6
whitenoise==6.2.0
icalendar==4.0.9
icalendar==4.1.0
pyyaml==6.0
uritemplate==4.1.1
beautifulsoup4==4.11.1
@@ -29,7 +28,7 @@ Jinja2==3.1.2
django-webpack-loader==1.5.0
git+https://github.com/ierror/django-js-reverse@7cab78c4531780ab4b32033d5104ccd5be1a246a
django-allauth==0.51.0
recipe-scrapers==14.6.0
recipe-scrapers==14.11.0
django-scopes==1.2.0.post1
pytest==7.1.2
pytest-django==4.5.2
@@ -39,7 +38,7 @@ django-storages==1.12.3
boto3==1.24.21
django-prometheus==2.2.0
django-hCaptcha==0.2.0
python-ldap==3.4.0
python-ldap==3.4.2
django-auth-ldap==4.1.0
pytest-factoryboy==2.5.0
pyppeteer==1.0.2

View File

@@ -0,0 +1,45 @@
<template>
<div id="app" class="col-12 col-xl-8 col-lg-10 offset-xl-2 offset-lg-1 offset">
<div class="col-12 col-xl-8 col-lg-10 offset-xl-2 offset-lg-1">
</div>
</div>
</template>
<script>
import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import { ApiApiFactory } from "@/utils/openapi/api"
import CookbookSlider from "@/components/CookbookSlider"
import LoadingSpinner from "@/components/LoadingSpinner"
import { StandardToasts, ApiMixin } from "@/utils/utils"
Vue.use(BootstrapVue)
export default {
name: "ProfileView",
mixins: [ApiMixin],
components: { },
data() {
return {
}
},
computed: {
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
},
methods: {
},
}
</script>
<style>
</style>

View File

@@ -0,0 +1,18 @@
import Vue from 'vue'
import App from './ProfileView.vue'
import i18n from '@/i18n'
Vue.config.productionTip = false
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
let publicPath = localStorage.STATIC_URL + 'vue/'
if (process.env.NODE_ENV === 'development') {
publicPath = 'http://localhost:8080/'
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
new Vue({
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -188,10 +188,31 @@
</b-form-checkbox>
<br/>
<label for="id_name"> {{ $t("Imported_From") }}</label>
<label> {{ $t("Imported_From") }}</label>
<b-form-input v-model="recipe.source_url">
</b-form-input>
<br/>
<label> {{ $t("Private_Recipe") }}</label>
<b-form-checkbox v-model="recipe.private">
{{ $t('Private_Recipe_Help') }}
</b-form-checkbox>
<br/>
<label> {{ $t("Share") }}</label>
<generic-multiselect
@change="recipe.shared = $event.val"
parent_variable="recipe.shared"
:initial_selection="recipe.shared"
:label="'display_name'"
:model="Models.USER_NAME"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Share')"
:limit="25"
></generic-multiselect>
</b-collapse>
</div>
</div>
@@ -577,9 +598,9 @@
<div class="col-md-12">
<label :for="'id_instruction_' + step.id">{{ $t("Instructions") }}</label>
<mavon-editor v-model="step.instruction" :autofocus="false"
style="height: 40vh; z-index: auto" :id="'id_instruction_' + step.id"
style="z-index: auto" :id="'id_instruction_' + step.id"
:language="'en'"
:toolbars="md_editor_toolbars"/>
:toolbars="md_editor_toolbars" :defaultOpen="'edit'"/>
<!-- TODO markdown DOCS link and markdown editor -->
</div>
@@ -723,6 +744,7 @@ import GenericModalForm from "@/components/Modals/GenericModalForm"
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
import _debounce from "lodash/debounce";
import GenericMultiselect from "@/components/GenericMultiselect";
// use
Vue.use(mavonEditor)
@@ -731,7 +753,7 @@ Vue.use(BootstrapVue)
export default {
name: "RecipeEditView",
mixins: [ResolveUrlMixin, ApiMixin],
components: {Multiselect, LoadingSpinner, draggable, GenericModalForm},
components: {Multiselect, LoadingSpinner, draggable, GenericModalForm, GenericMultiselect},
data() {
return {
recipe_id: window.RECIPE_ID,
@@ -771,9 +793,9 @@ export default {
imagelink: false,
code: true,
table: false,
fullscreen: true,
readmodel: true,
htmlcode: true,
fullscreen: false,
readmodel: false,
htmlcode: false,
help: true,
undo: true,
redo: true,

View File

@@ -269,10 +269,6 @@ export default {
},
loadRecipe: function (recipe_id) {
apiLoadRecipe(recipe_id).then((recipe) => {
if (window.USER_SERVINGS !== 0) {
recipe.servings = window.USER_SERVINGS
}
let total_time = 0
for (let step of recipe.steps) {
for (let ingredient of step.ingredients) {

View File

@@ -0,0 +1,82 @@
<template>
<div id="app" class="row">
<div class="col-md-3 col-12">
<b-nav vertical>
<b-nav-item :active="visible_settings === 'cosmetic'" @click="visible_settings = 'cosmetic'"><i
class="fas fa-fw fa-eye"></i> Cosmetic
</b-nav-item>
<b-nav-item :active="visible_settings === 'account'" @click="visible_settings = 'account'"><i
class="fas fa-fw fa-user"></i> Account
</b-nav-item>
<b-nav-item :active="visible_settings === 'search'" @click="visible_settings = 'search'"><i
class="fas fa-fw fa-search"></i> Search
</b-nav-item>
<b-nav-item :active="visible_settings === 'shopping'" @click="visible_settings = 'shopping'"><i
class="fas fa-fw fa-shopping-cart"></i> Shopping
</b-nav-item>
<b-nav-item :active="visible_settings === 'meal_plan'" @click="visible_settings = 'meal_plan'"><i
class="fas fa-fw fa-calendar"></i> Meal Plan
</b-nav-item>
<b-nav-item :active="visible_settings === 'api'" @click="visible_settings = 'api'"><i
class="fas fa-fw fa-code"></i> API
</b-nav-item>
</b-nav>
</div>
<div class="col-md-9 col-12">
<cosmetic-settings-component v-if="visible_settings === 'cosmetic'"
:user_id="user_id"></cosmetic-settings-component>
<account-settings-component v-if="visible_settings === 'account'"
:user_id="user_id"></account-settings-component>
<search-settings-component v-if="visible_settings === 'search'"
:user_id="user_id"></search-settings-component>
<shopping-settings-component v-if="visible_settings === 'shopping'"
:user_id="user_id"></shopping-settings-component>
<meal-plan-settings-component v-if="visible_settings === 'meal_plan'"
:user_id="user_id"></meal-plan-settings-component>
<a-p-i-settings-component v-if="visible_settings === 'api'" :user_id="user_id"></a-p-i-settings-component>
</div>
</div>
</template>
<script>
import Vue from "vue"
import {BootstrapVue} from "bootstrap-vue"
import "bootstrap-vue/dist/bootstrap-vue.css"
import CosmeticSettingsComponent from "@/components/Settings/CosmeticSettingsComponent";
import AccountSettingsComponent from "@/components/Settings/AccountSettingsComponent";
import SearchSettingsComponent from "@/components/Settings/SearchSettingsComponent";
import ShoppingSettingsComponent from "@/components/Settings/ShoppingSettingsComponent";
import MealPlanSettingsComponent from "@/components/Settings/MealPlanSettingsComponent";
import APISettingsComponent from "@/components/Settings/APISettingsComponent";
Vue.use(BootstrapVue)
export default {
name: "ProfileView",
mixins: [],
components: {
CosmeticSettingsComponent,
AccountSettingsComponent,
SearchSettingsComponent,
ShoppingSettingsComponent,
MealPlanSettingsComponent,
APISettingsComponent
},
data() {
return {
visible_settings: 'cosmetic',
user_id: window.USER_ID,
}
},
mounted() {
this.$i18n.locale = window.CUSTOM_LOCALE
},
methods: {},
}
</script>
<style>
</style>

View File

@@ -0,0 +1,17 @@
import Vue from 'vue'
import App from './SettingsView.vue'
import i18n from '@/i18n'
Vue.config.productionTip = false
// TODO move this and other default stuff to centralized JS file (verify nothing breaks)
let publicPath = localStorage.STATIC_URL + 'vue/'
if (process.env.NODE_ENV === 'development') {
publicPath = 'http://localhost:8080/'
}
export default __webpack_public_path__ = publicPath // eslint-disable-line
new Vue({
i18n,
render: h => h(App),
}).$mount('#app')

View File

@@ -33,7 +33,7 @@
<template #title>
<b-spinner v-if="loading" type="border" small class="d-inline-block"></b-spinner>
<i v-if="!loading" class="fas fa-shopping-cart fa-fw d-inline-block d-md-none"></i>
<span class="d-none d-md-inline-block">{{ $t('Shopping_list') }}</span>
<span class="d-none d-md-inline-block">{{ $t('Shopping_list') + ` (${items.filter(x => x.checked === false).length})`}}</span>
</template>
<div class="container p-0 p-md-3" id="shoppinglist">
<div class="row">
@@ -177,7 +177,7 @@
<b-tab :title="$t('Recipes')">
<template #title>
<i class="fas fa-book fa-fw d-block d-md-none"></i>
<span class="d-none d-md-block">{{ $t('Recipes') }}</span>
<span class="d-none d-md-block">{{ $t('Recipes') + ` (${Recipes.length})`}}</span>
</template>
<div class="container p-0">
<div class="row">
@@ -258,7 +258,7 @@
<b-tab>
<template #title>
<i class="fas fa-store-alt fa-fw d-block d-md-none"></i>
<span class="d-none d-md-block">{{ $t('Supermarkets') }}</span>
<span class="d-none d-md-block">{{ $t('Supermarkets') + ` (${supermarkets.length})`}}</span>
</template>
<div class="container p-0">
<div class="row">
@@ -514,7 +514,7 @@
"
:model="Models.USER"
:initial_selection="settings.shopping_share"
label="username"
label="display_name"
:multiple="true"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="$t('User')"
@@ -868,7 +868,7 @@ export default {
case "category":
return item?.food?.supermarket_category?.name ?? x
case "created_by":
return item?.created_by?.username ?? x
return item?.created_by?.display_name ?? x
case "recipe":
return item?.recipe_mealplan?.recipe_name ?? x
}

View File

@@ -141,6 +141,13 @@
<label>{{ $t('Message') }}</label>
<b-form-textarea v-model="space.message"></b-form-textarea>
<label>{{ $t('Image') }}</label>
<generic-multiselect :initial_single_selection="space.image"
:model="Models.USERFILE"
:multiple="false"
@change="space.image = $event.val;"></generic-multiselect>
<br/>
<b-form-checkbox v-model="space.show_facet_count"> Facet Count</b-form-checkbox>
<span class="text-muted small">{{ $t('facet_count_info') }}</span><br/>

View File

@@ -5,7 +5,7 @@
{{ book_copy.icon }}&nbsp;{{ book_copy.name }}
<span class="float-right text-primary" @click="editOrSave"><i class="fa" v-bind:class="{ 'fa-pen': !editing, 'fa-save': editing }" aria-hidden="true"></i></span>
</h5>
<b-badge class="font-weight-normal mr-1" v-for="u in book_copy.shared" v-bind:key="u.id" variant="primary" pill>{{ u.username }}</b-badge>
<b-badge class="font-weight-normal mr-1" v-for="u in book_copy.shared" v-bind:key="u.id" variant="primary" pill>{{ u.display_name }}</b-badge>
</b-card-header>
<b-card-body class="p-4">
<div class="form-group" v-if="editing">
@@ -25,7 +25,7 @@
@change="book_copy.shared = $event.val"
parent_variable="book.shared"
:initial_selection="book.shared"
:label="'username'"
:label="'display_name'"
:model="Models.USER_NAME"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Share')"

View File

@@ -7,124 +7,65 @@
</template>
<template v-else>
<td class="d-print-none" v-if="detailed && !show_shopping" @click="done">
<td class="d-print-none" v-if="detailed" @click="done">
<i class="far fa-check-circle text-success" v-if="ingredient.checked"></i>
<i class="far fa-check-circle text-primary" v-if="!ingredient.checked"></i>
</td>
<td class="text-nowrap" @click="done">
<span v-if="ingredient.amount !== 0 && !ingredient.no_amount" v-html="calculateAmount(ingredient.amount)"></span>
<span v-if="ingredient.amount !== 0 && !ingredient.no_amount"
v-html="calculateAmount(ingredient.amount)"></span>
</td>
<td @click="done">
<span v-if="ingredient.unit !== null && !ingredient.no_amount">{{ ingredient.unit.name }}</span>
</td>
<td @click="done">
<template v-if="ingredient.food !== null">
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe.id)" v-if="ingredient.food.recipe !== null" target="_blank" rel="noopener noreferrer">{{ ingredient.food.name }}</a>
<a :href="resolveDjangoUrl('view_recipe', ingredient.food.recipe.id)"
v-if="ingredient.food.recipe !== null" target="_blank"
rel="noopener noreferrer">{{ ingredient.food.name }}</a>
<span v-if="ingredient.food.recipe === null">{{ ingredient.food.name }}</span>
</template>
</td>
<td v-if="detailed && !show_shopping">
<td v-if="detailed">
<div v-if="ingredient.note">
<span v-b-popover.hover="ingredient.note" class="d-print-none touchable p-0 pl-md-2 pr-md-2">
<i class="far fa-comment"></i>
</span>
<div class="d-none d-print-block"><i class="far fa-comment-alt d-print-none"></i> {{ ingredient.note }}</div>
<div class="d-none d-print-block"><i class="far fa-comment-alt d-print-none"></i> {{
ingredient.note
}}
</div>
</div>
</td>
<td v-else-if="show_shopping" class="text-right text-nowrap">
<shopping-badge v-if="ingredient.food.ignore_shopping" :item="shoppingBadgeFood" />
<b-button
v-if="!ingredient.food.ignore_shopping"
class="btn text-decoration-none fas fa-shopping-cart px-2 user-select-none"
variant="link"
v-b-popover.hover.click.blur.html.top="{ title: ShoppingPopover, variant: 'outline-dark' }"
:class="{
'text-success': ingredient.shopping_status === true,
'text-muted': ingredient.shopping_status === false,
'text-warning': ingredient.shopping_status === null,
}"
/>
<span v-if="!ingredient.food.ignore_shopping" class="px-2">
<input type="checkbox" class="align-middle" v-model="shop" @change="changeShopping" />
</span>
<on-hand-badge v-if="!ingredient.food.ignore_shopping" :item="ingredient.food" />
</td>
</template>
</tr>
</template>
<script>
import { calculateAmount, ResolveUrlMixin, ApiMixin } from "@/utils/utils"
import OnHandBadge from "@/components/Badges/OnHand"
import ShoppingBadge from "@/components/Badges/Shopping"
import {calculateAmount, ResolveUrlMixin} from "@/utils/utils"
import Vue from "vue"
import VueSanitize from "vue-sanitize";
Vue.use(VueSanitize);
export default {
name: "IngredientComponent",
components: { OnHandBadge, ShoppingBadge },
props: {
ingredient: Object,
ingredient_factor: { type: Number, default: 1 },
detailed: { type: Boolean, default: true },
show_shopping: { type: Boolean, default: false },
ingredient_factor: {type: Number, default: 1},
detailed: {type: Boolean, default: true},
},
mixins: [ResolveUrlMixin, ApiMixin],
mixins: [ResolveUrlMixin],
data() {
return {
checked: false,
shop: false, // in shopping list for this recipe: boolean
dirty: undefined,
}
},
watch: {
ingredient: {
handler() {},
deep: true,
},
"ingredient.shop": function (newVal) {
this.shop = newVal
},
},
watch: {},
mounted() {
this.shop = this.ingredient?.shop
},
computed: {
shoppingBadgeFood() {
// shopping badge is hidden when ignore_shopping=true.
// force true in this context to allow adding to shopping list from recipe view
return { ...this.ingredient.food, ignore_shopping: false }
},
ShoppingPopover() {
if (this.ingredient?.shopping_status == false) {
return this.$t("NotInShopping", { food: this.ingredient.food.name })
} else {
let category = this.$t("Category") + ": " + this.ingredient?.category ?? this.$t("Undefined")
let popover = []
;(this.ingredient?.shopping_list ?? []).forEach((x) => {
popover.push(
[
"<tr style='border-bottom: 1px solid #ccc'>",
"<td style='padding: 3px;'><em>",
x?.mealplan ?? "",
"</em></td>",
"<td style='padding: 3px;'>",
x?.amount ?? "",
"</td>",
"<td style='padding: 3px;'>",
x?.unit ?? "" + "</td>",
"<td style='padding: 3px;'>",
x?.food ?? "",
"</td></tr>",
].join("")
)
})
return "<table class='table-small'><th colspan='4'>" + category + "</th>" + popover.join("") + "</table>"
}
},
},
methods: {
calculateAmount: function (x) {
@@ -134,10 +75,6 @@ export default {
done: function () {
this.$emit("checked-state-changed", this.ingredient)
},
// sends true/false to parent to save all ingredient shopping updates as a batch
changeShopping: function () {
this.$emit("add-to-shopping", { item: this.ingredient, add: this.shop })
},
},
}
</script>

View File

@@ -6,42 +6,27 @@
<div class="col-6">
<h4 class="card-title"><i class="fas fa-pepper-hot"></i> {{ $t("Ingredients") }}</h4>
</div>
<div class="col-6 text-right" v-if="header">
<h4 class="d-print-none card-title">
<i v-if="show_shopping && ShoppingRecipes.length > 0" class="fas fa-trash text-danger px-2"
@click="saveShopping(true)"></i>
<i v-if="show_shopping" class="fas fa-save text-success px-2" @click="saveShopping()"></i>
<i class="fas fa-shopping-cart px-2" @click="getShopping()"></i>
</h4>
</div>
</div>
</div>
<div class="card-body p-1 p-md-3">
<div class="row text-right" v-if="ShoppingRecipes.length > 1 && !add_shopping_mode">
<div class="col col-md-6 offset-md-6 text-right">
<b-form-select v-model="selected_shoppingrecipe" :options="ShoppingRecipes"
size="sm"></b-form-select>
</div>
</div>
<div class="row no-gutter">
<div class="col-12 m-0" :class="{ 'p-0': !header }">
<table class="table table-sm mb-0">
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
<template v-for="s in steps">
<tr v-bind:key="s.id" v-if="s.show_as_header && s.name !== '' && !add_shopping_mode && steps.length > 1">
<tr v-bind:key="s.id" v-if="s.show_as_header && s.name !== '' && steps.length > 1">
<td colspan="5">
<b>{{ s.name }}</b>
</td>
</tr>
<template v-for="i in s.ingredients">
<ingredient-component
:ingredient="prepareIngredient(i)"
:ingredient="i"
:ingredient_factor="ingredient_factor"
:key="i.id"
:show_shopping="show_shopping"
:detailed="detailed"
@checked-state-changed="$emit('checked-state-changed', $event)"
@add-to-shopping="addShopping($event)"
/>
</template>
</template>
@@ -80,7 +65,6 @@ export default {
servings: {type: Number, default: 1},
detailed: {type: Boolean, default: true},
header: {type: Boolean, default: false},
add_shopping_mode: {type: Boolean, default: false},
recipe_list: {type: Number, default: undefined},
},
data() {
@@ -108,145 +92,13 @@ export default {
},
},
watch: {
ShoppingRecipes: function (newVal, oldVal) {
if (newVal.length === 0 || this.add_shopping_mode) {
this.selected_shoppingrecipe = this.recipe_list
} else if (newVal.length === 1) {
this.selected_shoppingrecipe = newVal[0].value
}
},
selected_shoppingrecipe: function (newVal, oldVal) {
this.update_shopping = this.shopping_list.filter((x) => x.list_recipe === newVal).map((x) => x.ingredient)
this.$emit("change-servings", this.ShoppingRecipes.filter((x) => x.value === this.selected_shoppingrecipe)[0].servings)
},
},
mounted() {
if (this.add_shopping_mode) {
this.show_shopping = true
this.getShopping(false)
}
},
methods: {
getShopping: function (toggle_shopping = true) {
if (toggle_shopping) {
this.show_shopping = !this.show_shopping
}
if (this.show_shopping) {
let ingredient_list = this.steps
.map((x) => x.ingredients)
.flat()
.filter((x) => (x.food !== null && x.food !== undefined))
.map((x) => x.food.id)
let params = {
id: ingredient_list,
checked: "false",
}
this.genericAPI(this.Models.SHOPPING_LIST, this.Actions.LIST, params).then((result) => {
this.shopping_list = result.data
if (this.add_shopping_mode) {
if (this.recipe_list) {
this.$emit(
"starting-cart",
this.shopping_list.filter((x) => x.list_recipe === this.recipe_list).map((x) => x.ingredient)
)
} else {
this.$emit(
"starting-cart",
this.steps
.map((x) => x.ingredients)
.flat()
.filter((x) => x?.food?.food_onhand == false && x?.food?.ignore_shopping == false)
.map((x) => x.id)
)
}
}
})
}
},
saveShopping: function (del_shopping = false) {
let servings = this.servings
if (del_shopping) {
servings = -1
}
let params = {
id: this.recipe,
list_recipe: this.selected_shoppingrecipe,
ingredients: this.update_shopping,
servings: servings,
}
this.genericAPI(this.Models.RECIPE, this.Actions.SHOPPING, params)
.then((result) => {
if (del_shopping) {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_DELETE)
} else if (this.selected_shoppingrecipe) {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
} else {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
}
})
.catch((err) => {
if (del_shopping) {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
} else if (this.selected_shoppingrecipe) {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
} else {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err)
}
this.$emit("shopping-failed")
})
},
addShopping: function (e) {
// ALERT: this will all break if ingredients are re-used between recipes
if (e.add) {
this.update_shopping.push(e.item.id)
this.shopping_list.push({
id: Math.random(),
amount: e.item.amount,
ingredient: e.item.id,
food: e.item.food,
list_recipe: this.selected_shoppingrecipe,
})
} else {
this.update_shopping = [...this.update_shopping.filter((x) => x !== e.item.id)]
this.shopping_list = [...this.shopping_list.filter((x) => !(x.ingredient === e.item.id && x.list_recipe === this.selected_shoppingrecipe))]
}
if (this.add_shopping_mode) {
this.$emit("add-to-shopping", e)
}
},
prepareIngredient: function (i) {
let shopping = this.shopping_list.filter((x) => x.ingredient === i.id)
let selected_list = this.shopping_list.filter((x) => x.list_recipe === this.selected_shoppingrecipe && x.ingredient === i.id)
// checked = in the selected shopping list OR if in shoppping mode without a selected recipe, the default value true unless it is ignored or onhand
let checked = selected_list.length > 0 || (this.add_shopping_mode && !this.selected_shoppingrecipe && !i?.food?.ignore_recipe && !i?.food?.food_onhand)
let shopping_status = false // not in shopping list
if (shopping.length > 0) {
if (selected_list.length > 0) {
shopping_status = true // in shopping list for *this* recipe
} else {
shopping_status = null // in shopping list but not *this* recipe
}
}
return {
...i,
shop: checked,
shopping_status: shopping_status, // possible values: true, false, null
category: i.food?.supermarket_category?.name,
shopping_list: shopping.map((x) => {
return {
mealplan: x?.recipe_mealplan?.name,
amount: x.amount,
food: x.food?.name,
unit: x.unit?.name,
}
}),
}
},
},
}
</script>

View File

@@ -64,7 +64,7 @@
required
@change="entryEditing.shared = $event.val"
parent_variable="entryEditing.shared"
:label="'username'"
:label="'display_name'"
:model="Models.USER_NAME"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Share')"

View File

@@ -1,6 +1,6 @@
<template>
<b-card no-body v-hover v-if="recipe">
<a :href="clickUrl()">
<a :href="this.recipe.id !== undefined ? resolveDjangoUrl('view_recipe', this.recipe.id) : null">
<b-card-img-lazy style="height: 15vh; object-fit: cover" class="" :src="recipe_image" v-bind:alt="$t('Recipe_Image')" top></b-card-img-lazy>
<div class="card-img-overlay h-100 d-flex flex-column justify-content-right float-right text-right pt-2 pr-1" v-if="show_context_menu">
<a>
@@ -15,7 +15,7 @@
<b-card-body class="p-4">
<h6>
<a :href="clickUrl()">
<a :href="this.recipe.id !== undefined ? resolveDjangoUrl('view_recipe', this.recipe.id) : null">
<template v-if="recipe !== null">{{ recipe.name }}</template>
<template v-else>{{ meal_plan.title }}</template>
</a>
@@ -101,14 +101,7 @@ export default {
},
},
methods: {
// TODO: convert this to genericAPI
clickUrl: function () {
if (this.recipe !== null) {
return resolveDjangoUrl("view_recipe", this.recipe.id)
} else {
return resolveDjangoUrl("view_plan_entry", this.meal_plan.id)
}
},
},
directives: {
hover: {

View File

@@ -0,0 +1,59 @@
<template>
<div v-if="user_preferences !== undefined">
</div>
</template>
<script>
import {ApiApiFactory} from "@/utils/openapi/api";
import {StandardToasts} from "@/utils/utils";
import axios from "axios";
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
export default {
name: "APISettingsComponent",
props: {
user_id: Number,
},
data() {
return {
user_preferences: undefined,
languages: [],
}
},
mounted() {
this.user_preferences = this.preferences
this.languages = window.AVAILABLE_LANGUAGES
this.loadSettings()
},
methods: {
loadSettings: function () {
let apiFactory = new ApiApiFactory()
apiFactory.retrieveUserPreference(this.user_id.toString()).then(result => {
this.user_preferences = result.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
updateSettings: function (reload) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUserPreference(this.user_id.toString(), this.user_preferences).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
if (reload !== undefined) {
location.reload()
}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,86 @@
<template>
<div>
<div v-if="user !== undefined">
<b-form-input v-model="user.username" @change="updateUser(false)" disabled></b-form-input>
<b-form-input v-model="user.first_name" @change="updateUser(false)"></b-form-input>
<b-form-input v-model="user.last_name" @change="updateUser(false)"></b-form-input>
</div>
<a :href="resolveDjangoUrl('account_email')" class="btn btn-primary">Emails</a>
<a :href="resolveDjangoUrl('account_change_password')" class="btn btn-primary">Password</a>
<a :href="resolveDjangoUrl('socialaccount_connections')" class="btn btn-primary">Social</a>
</div>
</template>
<script>
import {ApiApiFactory} from "@/utils/openapi/api";
import {ResolveUrlMixin, StandardToasts} from "@/utils/utils";
import axios from "axios";
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
export default {
name: "AccountSettingsComponent",
mixins: [ResolveUrlMixin],
props: {
user_id: Number,
},
data() {
return {
user_preferences: undefined,
user: undefined,
languages: [],
}
},
mounted() {
this.user_preferences = this.preferences
this.languages = window.AVAILABLE_LANGUAGES
this.loadSettings()
},
methods: {
loadSettings: function () {
let apiFactory = new ApiApiFactory()
apiFactory.retrieveUserPreference(this.user_id.toString()).then(result => {
this.user_preferences = result.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
apiFactory.retrieveUser(this.user_id.toString()).then(result => {
this.user = result.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
updateSettings: function (reload) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUserPreference(this.user_id.toString(), this.user_preferences).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
if (reload !== undefined) {
location.reload()
}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
updateUser: function (reload) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUser(this.user_id.toString(), this.user).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
if (reload) {
location.reload()
}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,110 @@
<template>
<div v-if="user_preferences !== undefined">
<b-form-input v-model="user_preferences.default_unit" @change="updateSettings"></b-form-input>
{{ user_preferences.ingredient_decimals }}
<b-form-input type="range" min="0" max="4" step="1" v-model="user_preferences.ingredient_decimals"
@change="updateSettings"></b-form-input>
<b-form-checkbox v-model="user_preferences.use_fractions" @change="updateSettings"></b-form-checkbox>
<hr/>
Language
<b-form-select v-model="$i18n.locale" @change="updateLanguage">
<b-form-select-option v-bind:key="l[0]" v-for="l in languages" :value="l[1]">{{ l[0] }} ({{
l[1]
}})
</b-form-select-option>
</b-form-select>
<b-form-select v-model="user_preferences.theme" @change="updateSettings(true);">
<b-form-select-option value="TANDOOR">Tandoor</b-form-select-option>
<b-form-select-option value="BOOTSTRAP">Bootstrap</b-form-select-option>
<b-form-select-option value="DARKLY">Darkly</b-form-select-option>
<b-form-select-option value="FLATLY">Flatly</b-form-select-option>
<b-form-select-option value="SUPERHERO">Superhero</b-form-select-option>
</b-form-select>
<b-form-checkbox v-model="user_preferences.sticky_navbar" @change="updateSettings(true);"></b-form-checkbox>
<b-form-select v-model="user_preferences.nav_color" @change="updateSettings(true);">
<b-form-select-option value="PRIMARY">Primary</b-form-select-option>
<b-form-select-option value="SECONDARY">Secondary</b-form-select-option>
<b-form-select-option value="SUCCESS">Success</b-form-select-option>
<b-form-select-option value="INFO">Info</b-form-select-option>
<b-form-select-option value="WARNING">Warning</b-form-select-option>
<b-form-select-option value="DANGER">Danger</b-form-select-option>
<b-form-select-option value="LIGHT">Light</b-form-select-option>
<b-form-select-option value="DARK">Dark</b-form-select-option>
</b-form-select>
<hr/>
<b-form-checkbox v-model="user_preferences.use_kj" @change="updateSettings();"></b-form-checkbox>
<b-form-checkbox v-model="user_preferences.comments" @change="updateSettings();"></b-form-checkbox>
<b-form-checkbox v-model="user_preferences.left_handed" @change="updateSettings();"></b-form-checkbox>
</div>
</template>
<script>
import {ApiApiFactory} from "@/utils/openapi/api";
import {resolveDjangoUrl, StandardToasts} from "@/utils/utils";
import axios from "axios";
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
export default {
name: "CosmeticSettingsComponent",
props: {
user_id: Number,
},
data() {
return {
user_preferences: undefined,
languages: [],
}
},
mounted() {
this.user_preferences = this.preferences
this.languages = window.AVAILABLE_LANGUAGES
this.loadSettings()
},
methods: {
loadSettings: function () {
let apiFactory = new ApiApiFactory()
apiFactory.retrieveUserPreference(this.user_id.toString()).then(result => {
this.user_preferences = result.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
updateSettings: function (reload) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUserPreference(this.user_id.toString(), this.user_preferences).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
if (reload !== undefined) {
location.reload()
}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
updateLanguage: function () {
axios.post(resolveDjangoUrl('set_language'), new URLSearchParams({'language': this.$i18n.locale})).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
location.reload()
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,72 @@
<template>
<div v-if="user_preferences !== undefined">
<generic-multiselect
@change="updateSettings(false)"
:model="Models.USER"
:initial_selection="user_preferences.plan_share"
label="display_name"
:multiple="true"
:placeholder="$t('User')"
></generic-multiselect>
</div>
</template>
<script>
import {ApiApiFactory} from "@/utils/openapi/api";
import {ApiMixin, StandardToasts} from "@/utils/utils";
import axios from "axios";
import GenericMultiselect from "@/components/GenericMultiselect";
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
let SETTINGS_COOKIE_NAME = "mealplan_settings"
export default {
name: "MealPlanSettingsComponent",
mixins: [ApiMixin],
components: {GenericMultiselect},
props: {
user_id: Number,
},
data() {
return {
user_preferences: undefined,
languages: [],
}
},
mounted() {
this.user_preferences = this.preferences
this.languages = window.AVAILABLE_LANGUAGES
this.loadSettings()
},
methods: {
loadSettings: function () {
let apiFactory = new ApiApiFactory()
apiFactory.retrieveUserPreference(this.user_id.toString()).then(result => {
this.user_preferences = result.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
updateSettings: function (reload) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUserPreference(this.user_id.toString(), this.user_preferences).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
if (reload !== undefined) {
location.reload()
}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,59 @@
<template>
<div v-if="user_preferences !== undefined">
</div>
</template>
<script>
import {ApiApiFactory} from "@/utils/openapi/api";
import {StandardToasts} from "@/utils/utils";
import axios from "axios";
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
export default {
name: "SearchSettingsComponent",
props: {
user_id: Number,
},
data() {
return {
user_preferences: undefined,
languages: [],
}
},
mounted() {
this.user_preferences = this.preferences
this.languages = window.AVAILABLE_LANGUAGES
this.loadSettings()
},
methods: {
loadSettings: function () {
let apiFactory = new ApiApiFactory()
apiFactory.retrieveUserPreference(this.user_id.toString()).then(result => {
this.user_preferences = result.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
updateSettings: function (reload) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUserPreference(this.user_id.toString(), this.user_preferences).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
if (reload !== undefined) {
location.reload()
}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,87 @@
<template>
<div v-if="user_preferences !== undefined">
<generic-multiselect
@change="updateSettings(false)"
:model="Models.USER"
:initial_selection="user_preferences.shopping_share"
label="display_name"
:multiple="true"
:placeholder="$t('User')"
></generic-multiselect>
<!--TODO load min autosync time from env -->
<b-form-input type="range" min="0" max="60" step="1" v-model="user_preferences.shopping_auto_sync"
@change="updateSettings(false)"></b-form-input>
<b-form-checkbox v-model="user_preferences.mealplan_autoadd_shopping" @change="updateSettings(false)"></b-form-checkbox>
<b-form-checkbox v-model="user_preferences.mealplan_autoexclude_onhand" @change="updateSettings(false)"></b-form-checkbox>
<b-form-checkbox v-model="user_preferences.mealplan_autoinclude_related" @change="updateSettings(false)"></b-form-checkbox>
<b-form-checkbox v-model="user_preferences.shopping_add_onhand" @change="updateSettings(false)"></b-form-checkbox>
<b-form-input type="number" v-model="user_preferences.default_delay" @change="updateSettings(false)"></b-form-input>
<b-form-checkbox v-model="user_preferences.filter_to_supermarket" @change="updateSettings(false)"></b-form-checkbox>
<b-form-input type="range" min="0" max="14" step="1" v-model="user_preferences.shopping_recent_days"
@change="updateSettings(false)"></b-form-input>
<b-form-input v-model="user_preferences.csv_delim" @change="updateSettings(false)"></b-form-input>
<b-form-input v-model="user_preferences.csv_prefix" @change="updateSettings(false)"></b-form-input>
</div>
</template>
<script>
import {ApiApiFactory} from "@/utils/openapi/api";
import {ApiMixin, StandardToasts} from "@/utils/utils";
import axios from "axios";
import GenericMultiselect from "@/components/GenericMultiselect";
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
export default {
name: "ShoppingSettingsComponent",
mixins: [ApiMixin],
components: {GenericMultiselect},
props: {
user_id: Number,
},
data() {
return {
user_preferences: undefined,
languages: [],
}
},
mounted() {
this.user_preferences = this.preferences
this.languages = window.AVAILABLE_LANGUAGES
this.loadSettings()
},
methods: {
loadSettings: function () {
let apiFactory = new ApiApiFactory()
apiFactory.retrieveUserPreference(this.user_id.toString()).then(result => {
this.user_preferences = result.data
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_FETCH, err)
})
},
updateSettings: function (reload) {
let apiFactory = new ApiApiFactory()
apiFactory.partialUpdateUserPreference(this.user_id.toString(), this.user_preferences).then(result => {
StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
if (reload !== undefined) {
location.reload()
}
}).catch(err => {
StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
})
},
}
}
</script>
<style scoped>
</style>

View File

@@ -385,7 +385,7 @@ export default {
return [item?.recipe_mealplan?.mealplan_note, item?.ingredient_note].filter(String)
},
formatOneCreatedBy: function (item) {
return [this.$t("Added_by"), item?.created_by.username, "@", this.formatDate(item.created_at)].join(" ")
return [this.$t("Added_by"), item?.created_by.display_name, "@", this.formatDate(item.created_at)].join(" ")
},
openRecipeCard: function (e, item) {
this.genericAPI(this.Models.RECIPE, this.Actions.FETCH, {id: item.recipe_mealplan.recipe}).then((result) => {

View File

@@ -68,6 +68,10 @@
"Enable_Amount": "Enable Amount",
"Disable_Amount": "Disable Amount",
"Ingredient Editor": "Ingredient Editor",
"Private_Recipe": "Private Recipe",
"Private_Recipe_Help": "Recipe is only shown to you and people its shared with.",
"reusable_help_text": "Should the invite link be usable for more than one user.",
"Add_Step": "Add Step",
"Keywords": "Keywords",
"Books": "Books",

View File

@@ -375,5 +375,11 @@
"Documentation": "Documentation",
"New_Supermarket": "Créer un nouveau supermarché",
"New_Supermarket_Category": "Créer une nouvelle catégorie de supermarché",
"and_down": "& Dessous"
"and_down": "& Dessous",
"recipe_filter": "Filtrer les recettes",
"no_more_images_found": "Pas d'images supplémentaires trouvées sur le site.",
"sql_debug": "Débogage de la base SQL",
"last_cooked": "Dernière recette utilisée",
"times_cooked": "Temps de cuisson",
"show_sortby": "Trier par"
}

View File

@@ -425,5 +425,7 @@
"Message": "Wiadomość",
"reset_food_inheritance": "Zresetuj dziedziczenie",
"reset_food_inheritance_info": "Zresetuj wszystkie produkty spożywcze do domyślnych dziedziczonych pól i ich wartości nadrzędnych.",
"Valid Until": "Ważne do"
"Valid Until": "Ważne do",
"show_ingredient_overview": "Wyświetl listę wszystkich składników na początku przepisu.",
"Ingredient Overview": "Przegląd składników"
}

View File

@@ -8,7 +8,7 @@ axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
export function apiLoadRecipe(recipe_id) {
let url = resolveDjangoUrl('api:recipe-detail', recipe_id)
if (window.SHARE_UID !== undefined) {
if (window.SHARE_UID !== undefined && window.SHARE_UID !== 'None') {
url += '?share=' + window.SHARE_UID
}

View File

@@ -591,7 +591,7 @@ export class Models {
type: "lookup",
field: "shared",
list: "USER",
list_label: "username",
list_label: "display_name",
label: "shared_with",
multiple: true,
},
@@ -669,7 +669,7 @@ export class Models {
apiName: "InviteLink",
paginated: false,
create: {
params: [["email", "group", "valid_until"]],
params: [["email", "group", "valid_until", "reusable"]],
form: {
email: {
form_field: true,
@@ -694,6 +694,14 @@ export class Models {
label: "Valid Until",
placeholder: "",
},
reusable: {
form_field: true,
type: "checkbox",
field: "reusable",
label: "Reusable",
help_text: "reusable_help_text",
placeholder: "",
},
},
},
}

View File

@@ -279,11 +279,29 @@ export interface CustomFilterShared {
*/
id?: number;
/**
*
* Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.
* @type {string}
* @memberof CustomFilterShared
*/
username?: string;
/**
*
* @type {string}
* @memberof CustomFilterShared
*/
first_name?: string;
/**
*
* @type {string}
* @memberof CustomFilterShared
*/
last_name?: string;
/**
*
* @type {string}
* @memberof CustomFilterShared
*/
display_name?: string;
}
/**
*
@@ -1357,6 +1375,12 @@ export interface InviteLink {
* @memberof InviteLink
*/
used_by?: number | null;
/**
*
* @type {boolean}
* @memberof InviteLink
*/
reusable?: boolean;
/**
*
* @type {string}
@@ -1893,6 +1917,18 @@ export interface Recipe {
* @memberof Recipe
*/
last_cooked?: string;
/**
*
* @type {boolean}
* @memberof Recipe
*/
_private?: boolean;
/**
*
* @type {Array<CustomFilterShared>}
* @memberof Recipe
*/
shared?: Array<CustomFilterShared>;
}
/**
*
@@ -2023,6 +2059,12 @@ export interface RecipeBookFilter {
* @interface RecipeFile
*/
export interface RecipeFile {
/**
*
* @type {number}
* @memberof RecipeFile
*/
id?: number;
/**
*
* @type {string}
@@ -2031,16 +2073,16 @@ export interface RecipeFile {
name: string;
/**
*
* @type {any}
* @type {string}
* @memberof RecipeFile
*/
file?: any;
file_download?: string;
/**
*
* @type {number}
* @type {string}
* @memberof RecipeFile
*/
id?: number;
preview?: string;
}
/**
*
@@ -2568,11 +2610,29 @@ export interface ShoppingListCreatedBy {
*/
id?: number;
/**
*
* Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.
* @type {string}
* @memberof ShoppingListCreatedBy
*/
username?: string;
/**
*
* @type {string}
* @memberof ShoppingListCreatedBy
*/
first_name?: string;
/**
*
* @type {string}
* @memberof ShoppingListCreatedBy
*/
last_name?: string;
/**
*
* @type {string}
* @memberof ShoppingListCreatedBy
*/
display_name?: string;
}
/**
*
@@ -3086,6 +3146,43 @@ export interface Space {
* @memberof Space
*/
file_size_mb?: string;
/**
*
* @type {SpaceImage}
* @memberof Space
*/
image?: SpaceImage;
}
/**
*
* @export
* @interface SpaceImage
*/
export interface SpaceImage {
/**
*
* @type {number}
* @memberof SpaceImage
*/
id?: number;
/**
*
* @type {string}
* @memberof SpaceImage
*/
name: string;
/**
*
* @type {string}
* @memberof SpaceImage
*/
file_download?: string;
/**
*
* @type {string}
* @memberof SpaceImage
*/
preview?: string;
}
/**
*
@@ -3430,6 +3527,43 @@ export interface Unit {
*/
description?: string | null;
}
/**
*
* @export
* @interface User
*/
export interface User {
/**
*
* @type {number}
* @memberof User
*/
id?: number;
/**
* Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.
* @type {string}
* @memberof User
*/
username?: string;
/**
*
* @type {string}
* @memberof User
*/
first_name?: string;
/**
*
* @type {string}
* @memberof User
*/
last_name?: string;
/**
*
* @type {string}
* @memberof User
*/
display_name?: string;
}
/**
*
* @export
@@ -3473,25 +3607,6 @@ export interface UserFile {
*/
file_size_kb?: number;
}
/**
*
* @export
* @interface UserName
*/
export interface UserName {
/**
*
* @type {number}
* @memberof UserName
*/
id?: number;
/**
*
* @type {string}
* @memberof UserName
*/
username?: string;
}
/**
*
* @export
@@ -3504,6 +3619,12 @@ export interface UserPreference {
* @memberof UserPreference
*/
user: number;
/**
*
* @type {RecipeFile}
* @memberof UserPreference
*/
image?: RecipeFile | null;
/**
*
* @type {string}
@@ -3542,22 +3663,16 @@ export interface UserPreference {
use_kj?: boolean;
/**
*
* @type {string}
* @type {Array<CustomFilterShared>}
* @memberof UserPreference
*/
search_style?: UserPreferenceSearchStyleEnum;
plan_share?: Array<CustomFilterShared> | null;
/**
*
* @type {boolean}
* @memberof UserPreference
*/
show_recent?: boolean;
/**
*
* @type {Array<CustomFilterShared>}
* @memberof UserPreference
*/
plan_share?: Array<CustomFilterShared> | null;
sticky_navbar?: boolean;
/**
*
* @type {number}
@@ -3690,15 +3805,6 @@ export enum UserPreferenceDefaultPageEnum {
Plan = 'PLAN',
Books = 'BOOKS'
}
/**
* @export
* @enum {string}
*/
export enum UserPreferenceSearchStyleEnum {
Small = 'SMALL',
Large = 'LARGE',
New = 'NEW'
}
/**
*
@@ -4612,6 +4718,39 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions,
};
},
/**
*
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createUser: async (user?: User, options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/user/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(user, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} name
@@ -4713,7 +4852,40 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
};
},
/**
* function to retrieve a recipe from a given url or source string :param request: standard request with additional post parameters - url: url to use for importing recipe - data: if no url is given recipe is imported from provided source data - (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes :return: JsonResponse containing the parsed json, original html,json and images
* function to handle files passed by application importer
* @param {any} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createimportFiles: async (body?: any, options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/import/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* function to retrieve a recipe from a given url or source string :param request: standard request with additional post parameters - url: url to use for importing recipe - data: if no url is given recipe is imported from provided source data - (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes :return: JsonResponse containing the parsed json and images
* @param {any} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@@ -5561,6 +5733,39 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
destroyUser: async (id: string, options: any = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('destroyUser', 'id', id)
const localVarPath = `/api/user/{id}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -6960,7 +7165,7 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
* @throws {RequiredError}
*/
listUsers: async (options: any = {}): Promise<RequestArgs> => {
const localVarPath = `/api/user-name/`;
const localVarPath = `/api/user/`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@@ -8218,6 +8423,43 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
partialUpdateUser: async (id: string, user?: User, options: any = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('partialUpdateUser', 'id', id)
const localVarPath = `/api/user/{id}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(user, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@@ -9399,7 +9641,7 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
retrieveUser: async (id: string, options: any = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('retrieveUser', 'id', id)
const localVarPath = `/api/user-name/{id}/`
const localVarPath = `/api/user/{id}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -10620,6 +10862,43 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateUser: async (id: string, user?: User, options: any = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('updateUser', 'id', id)
const localVarPath = `/api/user/{id}/`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(user, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@@ -10988,6 +11267,16 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.createUnit(unit, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createUser(user?: User, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(user, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} name
@@ -11014,7 +11303,17 @@ export const ApiApiFp = function(configuration?: Configuration) {
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
* function to retrieve a recipe from a given url or source string :param request: standard request with additional post parameters - url: url to use for importing recipe - data: if no url is given recipe is imported from provided source data - (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes :return: JsonResponse containing the parsed json, original html,json and images
* function to handle files passed by application importer
* @param {any} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createimportFiles(body?: any, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<any>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createimportFiles(body, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
* function to retrieve a recipe from a given url or source string :param request: standard request with additional post parameters - url: url to use for importing recipe - data: if no url is given recipe is imported from provided source data - (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes :return: JsonResponse containing the parsed json and images
* @param {any} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@@ -11273,6 +11572,16 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.destroyUnit(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async destroyUser(id: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.destroyUser(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@@ -11670,7 +11979,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listUsers(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UserName>>> {
async listUsers(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<User>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listUsers(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@@ -12040,6 +12349,17 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.partialUpdateUnit(id, unit, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async partialUpdateUser(id: string, user?: User, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.partialUpdateUser(id, user, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@@ -12395,7 +12715,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async retrieveUser(id: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserName>> {
async retrieveUser(id: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveUser(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
@@ -12756,6 +13076,17 @@ export const ApiApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateUnit(id, unit, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateUser(id: string, user?: User, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateUser(id, user, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@@ -13018,6 +13349,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
createUnit(unit?: Unit, options?: any): AxiosPromise<Unit> {
return localVarFp.createUnit(unit, options).then((request) => request(axios, basePath));
},
/**
*
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createUser(user?: User, options?: any): AxiosPromise<User> {
return localVarFp.createUser(user, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} name
@@ -13042,7 +13382,16 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
return localVarFp.createViewLog(viewLog, options).then((request) => request(axios, basePath));
},
/**
* function to retrieve a recipe from a given url or source string :param request: standard request with additional post parameters - url: url to use for importing recipe - data: if no url is given recipe is imported from provided source data - (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes :return: JsonResponse containing the parsed json, original html,json and images
* function to handle files passed by application importer
* @param {any} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createimportFiles(body?: any, options?: any): AxiosPromise<any> {
return localVarFp.createimportFiles(body, options).then((request) => request(axios, basePath));
},
/**
* function to retrieve a recipe from a given url or source string :param request: standard request with additional post parameters - url: url to use for importing recipe - data: if no url is given recipe is imported from provided source data - (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes :return: JsonResponse containing the parsed json and images
* @param {any} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@@ -13275,6 +13624,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
destroyUnit(id: string, options?: any): AxiosPromise<void> {
return localVarFp.destroyUnit(id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
destroyUser(id: string, options?: any): AxiosPromise<void> {
return localVarFp.destroyUser(id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@@ -13636,7 +13994,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listUsers(options?: any): AxiosPromise<Array<UserName>> {
listUsers(options?: any): AxiosPromise<Array<User>> {
return localVarFp.listUsers(options).then((request) => request(axios, basePath));
},
/**
@@ -13972,6 +14330,16 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
partialUpdateUnit(id: string, unit?: Unit, options?: any): AxiosPromise<Unit> {
return localVarFp.partialUpdateUnit(id, unit, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
partialUpdateUser(id: string, user?: User, options?: any): AxiosPromise<User> {
return localVarFp.partialUpdateUser(id, user, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@@ -14293,7 +14661,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
retrieveUser(id: string, options?: any): AxiosPromise<UserName> {
retrieveUser(id: string, options?: any): AxiosPromise<User> {
return localVarFp.retrieveUser(id, options).then((request) => request(axios, basePath));
},
/**
@@ -14620,6 +14988,16 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
updateUnit(id: string, unit?: Unit, options?: any): AxiosPromise<Unit> {
return localVarFp.updateUnit(id, unit, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateUser(id: string, user?: User, options?: any): AxiosPromise<User> {
return localVarFp.updateUser(id, user, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id A unique integer value identifying this user file.
@@ -14930,6 +15308,17 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).createUnit(unit, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public createUser(user?: User, options?: any) {
return ApiApiFp(this.configuration).createUser(user, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} name
@@ -14958,7 +15347,18 @@ export class ApiApi extends BaseAPI {
}
/**
* function to retrieve a recipe from a given url or source string :param request: standard request with additional post parameters - url: url to use for importing recipe - data: if no url is given recipe is imported from provided source data - (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes :return: JsonResponse containing the parsed json, original html,json and images
* function to handle files passed by application importer
* @param {any} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public createimportFiles(body?: any, options?: any) {
return ApiApiFp(this.configuration).createimportFiles(body, options).then((request) => request(this.axios, this.basePath));
}
/**
* function to retrieve a recipe from a given url or source string :param request: standard request with additional post parameters - url: url to use for importing recipe - data: if no url is given recipe is imported from provided source data - (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes :return: JsonResponse containing the parsed json and images
* @param {any} [body]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@@ -15243,6 +15643,17 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).destroyUnit(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public destroyUser(id: string, options?: any) {
return ApiApiFp(this.configuration).destroyUser(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this user file.
@@ -16080,6 +16491,18 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).partialUpdateUnit(id, unit, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public partialUpdateUser(id: string, user?: User, options?: any) {
return ApiApiFp(this.configuration).partialUpdateUser(id, user, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this user file.
@@ -16864,6 +17287,18 @@ export class ApiApi extends BaseAPI {
return ApiApiFp(this.configuration).updateUnit(id, unit, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this user.
* @param {User} [user]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ApiApi
*/
public updateUser(id: string, user?: User, options?: any) {
return ApiApiFp(this.configuration).updateUser(id, user, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id A unique integer value identifying this user file.

View File

@@ -61,6 +61,14 @@ const pages = {
entry: "./src/apps/SpaceManageView/main.js",
chunks: ["chunk-vendors"],
},
profile_view: {
entry: "./src/apps/ProfileView/main.js",
chunks: ["chunk-vendors"],
},
settings_view: {
entry: "./src/apps/SettingsView/main.js",
chunks: ["chunk-vendors"],
}
}
module.exports = {

View File

@@ -10508,9 +10508,9 @@ terser-webpack-plugin@^5.1.1, terser-webpack-plugin@^5.1.3:
terser "^5.7.2"
terser@^4.1.2, terser@^4.6.2:
version "4.8.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17"
integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==
version "4.8.1"
resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f"
integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==
dependencies:
commander "^2.20.0"
source-map "~0.6.1"