mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-25 19:29:30 -05:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ef21241bf | ||
|
|
5e77adf7e6 | ||
|
|
4df0a46701 | ||
|
|
f186404628 | ||
|
|
8e3ec91f3c | ||
|
|
2605addf34 | ||
|
|
1ab3e57b83 | ||
|
|
2f36ae5112 | ||
|
|
acc19ca65e | ||
|
|
ea213e2dfd | ||
|
|
02cf3264a3 | ||
|
|
a0b1186558 | ||
|
|
27e47718bb | ||
|
|
f78dd209bd | ||
|
|
b4e0b51f5b | ||
|
|
eedce4dcfd | ||
|
|
006be92180 | ||
|
|
1fae004785 | ||
|
|
239a88cd24 | ||
|
|
22b432a6ae | ||
|
|
c88566a4ae | ||
|
|
5f8e371793 | ||
|
|
94d9ac03ea | ||
|
|
897ac97423 | ||
|
|
24aeae6de9 | ||
|
|
ce941db3be | ||
|
|
5ff91ee47f | ||
|
|
ce1f55ffd1 | ||
|
|
8700e2df69 | ||
|
|
f4df84b609 | ||
|
|
ba473123ba | ||
|
|
98a54ef38f | ||
|
|
7fdc9c7cb8 | ||
|
|
dc3b1566d7 | ||
|
|
5429c4d557 | ||
|
|
dabcea6ba7 | ||
|
|
e91790f5ac | ||
|
|
51076d4ced | ||
|
|
1cb37fe2d2 | ||
|
|
61a9f0647b | ||
|
|
ac2ab62050 | ||
|
|
c50efac00e | ||
|
|
bf16e61a1f | ||
|
|
d464633c70 | ||
|
|
b78d0ec30b | ||
|
|
da09602834 | ||
|
|
5ead4967a5 | ||
|
|
8bb7ce2062 |
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']
|
||||
@@ -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.'),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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, ..)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
25
cookbook/migrations/0179_recipe_private_recipe_shared.py
Normal file
25
cookbook/migrations/0179_recipe_private_recipe_shared.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0180_invitelink_reusable.py
Normal file
18
cookbook/migrations/0180_invitelink_reusable.py
Normal 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),
|
||||
),
|
||||
]
|
||||
19
cookbook/migrations/0181_space_image.py
Normal file
19
cookbook/migrations/0181_space_image.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
19
cookbook/migrations/0182_userpreference_image.py
Normal file
19
cookbook/migrations/0182_userpreference_image.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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
1
cookbook/static/css/vue-multiselect.min.css
vendored
1
cookbook/static/css/vue-multiselect.min.css
vendored
File diff suppressed because one or more lines are too long
2
cookbook/static/js/Sortable.min.js
vendored
2
cookbook/static/js/Sortable.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
6
cookbook/static/js/vue.min.js
vendored
6
cookbook/static/js/vue.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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' />"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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">×</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>
|
||||
@@ -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>
|
||||
@@ -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/"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
32
cookbook/templates/profile.html
Normal file
32
cookbook/templates/profile.html
Normal 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 %}
|
||||
@@ -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 %},
|
||||
|
||||
@@ -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">«</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">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endblock pagination.next %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock pagination %}
|
||||
{% endblock content %}
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
45
cookbook/templates/user_settings.html
Normal file
45
cookbook/templates/user_settings.html
Normal 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 %}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
@@ -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":
|
||||
|
||||
@@ -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', {})
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
45
vue/src/apps/ProfileView/ProfileView.vue
Normal file
45
vue/src/apps/ProfileView/ProfileView.vue
Normal 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>
|
||||
18
vue/src/apps/ProfileView/main.js
Normal file
18
vue/src/apps/ProfileView/main.js
Normal 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')
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
82
vue/src/apps/SettingsView/SettingsView.vue
Normal file
82
vue/src/apps/SettingsView/SettingsView.vue
Normal 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>
|
||||
17
vue/src/apps/SettingsView/main.js
Normal file
17
vue/src/apps/SettingsView/main.js
Normal 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')
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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/>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{{ book_copy.icon }} {{ 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')"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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: {
|
||||
|
||||
59
vue/src/components/Settings/APISettingsComponent.vue
Normal file
59
vue/src/components/Settings/APISettingsComponent.vue
Normal 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>
|
||||
86
vue/src/components/Settings/AccountSettingsComponent.vue
Normal file
86
vue/src/components/Settings/AccountSettingsComponent.vue
Normal 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>
|
||||
110
vue/src/components/Settings/CosmeticSettingsComponent.vue
Normal file
110
vue/src/components/Settings/CosmeticSettingsComponent.vue
Normal 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>
|
||||
72
vue/src/components/Settings/MealPlanSettingsComponent.vue
Normal file
72
vue/src/components/Settings/MealPlanSettingsComponent.vue
Normal 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>
|
||||
59
vue/src/components/Settings/SearchSettingsComponent.vue
Normal file
59
vue/src/components/Settings/SearchSettingsComponent.vue
Normal 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>
|
||||
87
vue/src/components/Settings/ShoppingSettingsComponent.vue
Normal file
87
vue/src/components/Settings/ShoppingSettingsComponent.vue
Normal 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>
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user