Merge branch 'develop' into feature/shopping-ui

# Conflicts:
#	vue/src/locales/en.json
This commit is contained in:
vabene1111 2024-01-03 15:09:30 +01:00
commit 146e97c8ec
30 changed files with 1098 additions and 803 deletions

View File

@ -60,9 +60,9 @@ admin.site.register(UserSpace, UserSpaceAdmin)
class UserPreferenceAdmin(admin.ModelAdmin): class UserPreferenceAdmin(admin.ModelAdmin):
list_display = ('name', 'theme', 'nav_color', 'default_page',) list_display = ('name', 'theme', 'default_page')
search_fields = ('user__username',) search_fields = ('user__username',)
list_filter = ('theme', 'nav_color', 'default_page',) list_filter = ('theme', 'default_page',)
date_hierarchy = 'created_at' date_hierarchy = 'created_at'
filter_horizontal = ('plan_share', 'shopping_share',) filter_horizontal = ('plan_share', 'shopping_share',)
@ -108,11 +108,16 @@ class SupermarketCategoryInline(admin.TabularInline):
class SupermarketAdmin(admin.ModelAdmin): class SupermarketAdmin(admin.ModelAdmin):
list_display = ('name', 'space',)
inlines = (SupermarketCategoryInline,) inlines = (SupermarketCategoryInline,)
class SupermarketCategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'space',)
admin.site.register(Supermarket, SupermarketAdmin) admin.site.register(Supermarket, SupermarketAdmin)
admin.site.register(SupermarketCategory) admin.site.register(SupermarketCategory, SupermarketCategoryAdmin)
class SyncLogAdmin(admin.ModelAdmin): class SyncLogAdmin(admin.ModelAdmin):
@ -163,10 +168,18 @@ def delete_unattached_steps(modeladmin, request, queryset):
class StepAdmin(admin.ModelAdmin): class StepAdmin(admin.ModelAdmin):
list_display = ('name', 'order',) list_display = ('recipe_and_name', 'order', 'space')
search_fields = ('name',) ordering = ('recipe__name', 'name', 'space',)
search_fields = ('name', 'recipe__name')
actions = [delete_unattached_steps] actions = [delete_unattached_steps]
@staticmethod
@admin.display(description="Name")
def recipe_and_name(obj):
if not obj.recipe_set.exists():
return f"Orphaned Step{'':s if not obj.name else f': {obj.name}'}"
return f"{obj.recipe_set.first().name}: {obj.name}" if obj.name else obj.recipe_set.first().name
admin.site.register(Step, StepAdmin) admin.site.register(Step, StepAdmin)
@ -183,8 +196,9 @@ def rebuild_index(modeladmin, request, queryset):
class RecipeAdmin(admin.ModelAdmin): class RecipeAdmin(admin.ModelAdmin):
list_display = ('name', 'internal', 'created_by', 'storage') list_display = ('name', 'internal', 'created_by', 'storage', 'space')
search_fields = ('name', 'created_by__username') search_fields = ('name', 'created_by__username')
ordering = ('name', 'created_by__username',)
list_filter = ('internal',) list_filter = ('internal',)
date_hierarchy = 'created_at' date_hierarchy = 'created_at'
@ -198,7 +212,14 @@ class RecipeAdmin(admin.ModelAdmin):
admin.site.register(Recipe, RecipeAdmin) admin.site.register(Recipe, RecipeAdmin)
admin.site.register(Unit)
class UnitAdmin(admin.ModelAdmin):
list_display = ('name', 'space')
ordering = ('name', 'space',)
search_fields = ('name',)
admin.site.register(Unit, UnitAdmin)
# admin.site.register(FoodInheritField) # admin.site.register(FoodInheritField)
@ -229,10 +250,16 @@ def delete_unattached_ingredients(modeladmin, request, queryset):
class IngredientAdmin(admin.ModelAdmin): class IngredientAdmin(admin.ModelAdmin):
list_display = ('food', 'amount', 'unit') list_display = ('recipe_name', 'amount', 'unit', 'food', 'space')
search_fields = ('food__name', 'unit__name') search_fields = ('food__name', 'unit__name', 'step__recipe__name')
actions = [delete_unattached_ingredients] actions = [delete_unattached_ingredients]
@staticmethod
@admin.display(description="Recipe")
def recipe_name(obj):
recipes = obj.step_set.first().recipe_set.all() if obj.step_set.exists() else None
return recipes.first().name if recipes else 'Orphaned Ingredient'
admin.site.register(Ingredient, IngredientAdmin) admin.site.register(Ingredient, IngredientAdmin)
@ -258,7 +285,7 @@ admin.site.register(RecipeImport, RecipeImportAdmin)
class RecipeBookAdmin(admin.ModelAdmin): class RecipeBookAdmin(admin.ModelAdmin):
list_display = ('name', 'user_name') list_display = ('name', 'user_name', 'space')
search_fields = ('name', 'created_by__username') search_fields = ('name', 'created_by__username')
@staticmethod @staticmethod
@ -334,11 +361,11 @@ class ShoppingListEntryAdmin(admin.ModelAdmin):
admin.site.register(ShoppingListEntry, ShoppingListEntryAdmin) admin.site.register(ShoppingListEntry, ShoppingListEntryAdmin)
class ShoppingListAdmin(admin.ModelAdmin): # class ShoppingListAdmin(admin.ModelAdmin):
list_display = ('id', 'created_by', 'created_at') # list_display = ('id', 'created_by', 'created_at')
admin.site.register(ShoppingList, ShoppingListAdmin) # admin.site.register(ShoppingList, ShoppingListAdmin)
class ShareLinkAdmin(admin.ModelAdmin): class ShareLinkAdmin(admin.ModelAdmin):

View File

@ -33,64 +33,6 @@ class DateWidget(forms.DateInput):
super().__init__(**kwargs) super().__init__(**kwargs)
class UserPreferenceForm(forms.ModelForm):
prefix = 'preference'
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')
super().__init__(*args, **kwargs)
self.fields['plan_share'].queryset = User.objects.filter(userspace__space=space).all()
class Meta:
model = UserPreference
fields = (
'default_unit', 'use_fractions', 'use_kj', 'theme', 'nav_color',
'sticky_navbar', 'default_page', 'plan_share', 'ingredient_decimals', 'comments', 'left_handed', 'show_step_ingredients',
)
labels = {
'default_unit': _('Default unit'),
'use_fractions': _('Use fractions'),
'use_kj': _('Use KJ'),
'theme': _('Theme'),
'nav_color': _('Navbar color'),
'sticky_navbar': _('Sticky navbar'),
'default_page': _('Default page'),
'plan_share': _('Plan sharing'),
'ingredient_decimals': _('Ingredient decimal places'),
'shopping_auto_sync': _('Shopping list auto sync period'),
'comments': _('Comments'),
'left_handed': _('Left-handed mode'),
'show_step_ingredients': _('Show step ingredients table')
}
help_texts = {
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
'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)'),
'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.'),
'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 '
'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.'),
'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.'),
'show_step_ingredients': _('Add ingredients table next to recipe steps. Applies at creation time for manually created and URL imported recipes. Individual steps can be overridden in the edit recipe view.')
}
widgets = {
'plan_share': MultiSelectWidget,
'shopping_share': MultiSelectWidget,
}
class UserNameForm(forms.ModelForm): class UserNameForm(forms.ModelForm):
prefix = 'name' prefix = 'name'

Binary file not shown.

View File

@ -1,7 +1,7 @@
# Generated by Django 4.1.10 on 2023-08-29 11:59 # Generated by Django 4.1.10 on 2023-08-29 11:59
from django.db import migrations from django.db import migrations
from django.db.models import F, Value from django.db.models import F, Value, Count
from django.db.models.functions import Concat from django.db.models.functions import Concat
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
@ -13,9 +13,24 @@ def migrate_icons(apps, schema_editor):
PropertyType = apps.get_model('cookbook', 'PropertyType') PropertyType = apps.get_model('cookbook', 'PropertyType')
RecipeBook = apps.get_model('cookbook', 'RecipeBook') RecipeBook = apps.get_model('cookbook', 'RecipeBook')
duplicate_meal_types = MealType.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1).all()
if len(duplicate_meal_types) > 0:
raise RuntimeError(f'Duplicate MealTypes found, please remove/rename them and run migrations again/restart the container. {duplicate_meal_types}')
MealType.objects.update(name=Concat(F('icon'), Value(' '), F('name'))) MealType.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
duplicate_meal_types = Keyword.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1).all()
if len(duplicate_meal_types) > 0:
raise RuntimeError(f'Duplicate Keyword found, please remove/rename them and run migrations again/restart the container. {duplicate_meal_types}')
Keyword.objects.update(name=Concat(F('icon'), Value(' '), F('name'))) Keyword.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
duplicate_meal_types = PropertyType.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1).all()
if len(duplicate_meal_types) > 0:
raise RuntimeError(f'Duplicate PropertyType found, please remove/rename them and run migrations again/restart the container. {duplicate_meal_types}')
PropertyType.objects.update(name=Concat(F('icon'), Value(' '), F('name'))) PropertyType.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
duplicate_meal_types = RecipeBook.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1).all()
if len(duplicate_meal_types) > 0:
raise RuntimeError(f'Duplicate RecipeBook found, please remove/rename them and run migrations again/restart the container. {duplicate_meal_types}')
RecipeBook.objects.update(name=Concat(F('icon'), Value(' '), F('name'))) RecipeBook.objects.update(name=Concat(F('icon'), Value(' '), F('name')))
@ -25,10 +40,7 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.RunPython( migrate_icons),
migrations.RunPython(
migrate_icons
),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='propertytype', name='propertytype',
options={'ordering': ('order',)}, options={'ordering': ('order',)},

View File

@ -1,15 +1,23 @@
# Generated by Django 4.2.7 on 2023-11-27 21:09 # Generated by Django 4.2.7 on 2023-11-27 21:09
from django.db import migrations, models from django.db import migrations, models
from django_scopes import scopes_disabled
def fix_fdc_ids(apps, schema_editor):
with scopes_disabled():
# in case any food had a non digit fdc ID before this migration, remove it
Food = apps.get_model('cookbook', 'Food')
Food.objects.exclude(fdc_id__regex=r'^\d+$').exclude(fdc_id=None).update(fdc_id=None)
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('cookbook', '0203_alter_unique_contstraints'), ('cookbook', '0203_alter_unique_contstraints'),
] ]
operations = [ operations = [
migrations.RunPython(fix_fdc_ids),
migrations.AddField( migrations.AddField(
model_name='propertytype', model_name='propertytype',
name='fdc_id', name='fdc_id',

View File

@ -0,0 +1,128 @@
# Generated by Django 4.2.7 on 2024-01-01 18:44
import django
from django.db import migrations, models
from django_scopes import scopes_disabled
TANDOOR = 'TANDOOR'
TANDOOR_DARK = 'TANDOOR_DARK'
BOOTSTRAP = 'BOOTSTRAP'
DARKLY = 'DARKLY'
FLATLY = 'FLATLY'
SUPERHERO = 'SUPERHERO'
PRIMARY = 'PRIMARY'
SECONDARY = 'SECONDARY'
SUCCESS = 'SUCCESS'
INFO = 'INFO'
WARNING = 'WARNING'
DANGER = 'DANGER'
LIGHT = 'LIGHT'
DARK = 'DARK'
# ['light', 'warning', 'info', 'success'] --> light (theming_tags L45)
def get_nav_bg_color(theme, nav_color):
if theme == TANDOOR: # primary not actually primary color but override existed before update, same for dark
return {PRIMARY: '#ddbf86', SECONDARY: '#b55e4f', SUCCESS: '#82aa8b', INFO: '#385f84', WARNING: '#eaaa21', DANGER: '#a7240e', LIGHT: '#cfd5cd', DARK: '#221e1e'}[nav_color]
if theme == TANDOOR_DARK:
return {PRIMARY: '#ddbf86', SECONDARY: '#b55e4f', SUCCESS: '#82aa8b', INFO: '#385f84', WARNING: '#eaaa21', DANGER: '#a7240e', LIGHT: '#cfd5cd', DARK: '#221e1e'}[nav_color]
if theme == BOOTSTRAP:
return {PRIMARY: '#007bff', SECONDARY: '#6c757d', SUCCESS: '#28a745', INFO: '#17a2b8', WARNING: '#ffc107', DANGER: '#dc3545', LIGHT: '#f8f9fa', DARK: '#343a40'}[nav_color]
if theme == DARKLY:
return {PRIMARY: '#375a7f', SECONDARY: '#444', SUCCESS: '#00bc8c', INFO: '#3498DB', WARNING: '#F39C12', DANGER: '#E74C3C', LIGHT: '#999', DARK: '#303030'}[nav_color]
if theme == FLATLY:
return {PRIMARY: '#2C3E50', SECONDARY: '#95a5a6', SUCCESS: '#18BC9C', INFO: '#3498DB', WARNING: '#F39C12', DANGER: '#E74C3C', LIGHT: '#ecf0f1', DARK: '#7b8a8b'}[nav_color]
if theme == SUPERHERO:
return {PRIMARY: '#DF691A', SECONDARY: '#4E5D6C', SUCCESS: '#5cb85c', INFO: '#5bc0de', WARNING: '#f0ad4e', DANGER: '#d9534f', LIGHT: '#abb6c2', DARK: '#4E5D6C'}[nav_color]
def get_nav_text_color(theme, nav_color):
if theme == TANDOOR:
return {PRIMARY: DARK, SECONDARY: DARK, SUCCESS: DARK, INFO: DARK, WARNING: DARK, DANGER: DARK, LIGHT: DARK, DARK: DARK}[nav_color]
if theme == TANDOOR_DARK:
return {PRIMARY: DARK, SECONDARY: DARK, SUCCESS: DARK, INFO: DARK, WARNING: DARK, DANGER: DARK, LIGHT: DARK, DARK: DARK}[nav_color]
if theme == BOOTSTRAP:
return {PRIMARY: DARK, SECONDARY: DARK, SUCCESS: DARK, INFO: DARK, WARNING: DARK, DANGER: DARK, LIGHT: DARK, DARK: DARK}[nav_color]
if theme == DARKLY:
return {PRIMARY: DARK, SECONDARY: DARK, SUCCESS: DARK, INFO: DARK, WARNING: DARK, DANGER: DARK, LIGHT: DARK, DARK: DARK}[nav_color]
if theme == FLATLY:
return {PRIMARY: DARK, SECONDARY: DARK, SUCCESS: LIGHT, INFO: LIGHT, WARNING: LIGHT, DANGER: DARK, LIGHT: LIGHT, DARK: DARK}[nav_color]
if theme == SUPERHERO:
return {PRIMARY: DARK, SECONDARY: DARK, SUCCESS: LIGHT, INFO: LIGHT, WARNING: LIGHT, DANGER: DARK, LIGHT: LIGHT, DARK: DARK}[nav_color]
def get_current_colors(apps, schema_editor):
with scopes_disabled():
# in case any food had a non digit fdc ID before this migration, remove it
UserPreference = apps.get_model('cookbook', 'UserPreference')
update_ups = []
for up in UserPreference.objects.all():
if up.theme != TANDOOR or up.nav_color != PRIMARY:
up.nav_bg_color = get_nav_bg_color(up.theme, up.nav_color)
up.nav_text_color = get_nav_text_color(up.theme, up.nav_color)
up.nav_show_logo = (up.theme == TANDOOR or up.theme == TANDOOR_DARK)
update_ups.append(up)
UserPreference.objects.bulk_update(update_ups, ['nav_bg_color', 'nav_text_color', 'nav_show_logo'])
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0205_alter_food_fdc_id_alter_propertytype_fdc_id'),
]
operations = [
migrations.RenameField(
model_name='userpreference',
old_name='sticky_navbar',
new_name='nav_sticky',
),
migrations.AddField(
model_name='userpreference',
name='nav_bg_color',
field=models.CharField(default='#ddbf86', max_length=8),
),
migrations.AddField(
model_name='userpreference',
name='nav_text_color',
field=models.CharField(choices=[('LIGHT', 'Light'), ('DARK', 'Dark')], default='DARK', max_length=16),
),
migrations.AddField(
model_name='userpreference',
name='nav_show_logo',
field=models.BooleanField(default=True),
),
migrations.RunPython(get_current_colors),
migrations.RemoveField(
model_name='userpreference',
name='nav_color',
),
migrations.AddField(
model_name='space',
name='nav_bg_color',
field=models.CharField(blank=True, default='', max_length=8),
),
migrations.AddField(
model_name='space',
name='nav_logo',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_nav_logo', to='cookbook.userfile'),
),
migrations.AddField(
model_name='space',
name='nav_text_color',
field=models.CharField(choices=[('BLANK', '-------'), ('LIGHT', 'Light'), ('DARK', 'Dark')], default='BLANK', max_length=16),
),
migrations.AddField(
model_name='space',
name='space_theme',
field=models.CharField(choices=[('BLANK', '-------'), ('TANDOOR', 'Tandoor'), ('BOOTSTRAP', 'Bootstrap'), ('DARKLY', 'Darkly'), ('FLATLY', 'Flatly'), ('SUPERHERO', 'Superhero'), ('TANDOOR_DARK', 'Tandoor Dark (INCOMPLETE)')],
default='BLANK',
max_length=128),
),
migrations.AddField(
model_name='space',
name='custom_space_theme',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_theme', to='cookbook.userfile'),
),
]

View File

@ -251,8 +251,44 @@ class FoodInheritField(models.Model, PermissionModelMixin):
class Space(ExportModelOperationsMixin('space'), models.Model): class Space(ExportModelOperationsMixin('space'), models.Model):
# TODO remove redundant theming constants
# Themes
BLANK = 'BLANK'
TANDOOR = 'TANDOOR'
TANDOOR_DARK = 'TANDOOR_DARK'
BOOTSTRAP = 'BOOTSTRAP'
DARKLY = 'DARKLY'
FLATLY = 'FLATLY'
SUPERHERO = 'SUPERHERO'
THEMES = (
(BLANK, '-------'),
(TANDOOR, 'Tandoor'),
(BOOTSTRAP, 'Bootstrap'),
(DARKLY, 'Darkly'),
(FLATLY, 'Flatly'),
(SUPERHERO, 'Superhero'),
(TANDOOR_DARK, 'Tandoor Dark (INCOMPLETE)'),
)
LIGHT = 'LIGHT'
DARK = 'DARK'
NAV_TEXT_COLORS = (
(BLANK, '-------'),
(LIGHT, 'Light'),
(DARK, 'Dark')
)
name = models.CharField(max_length=128, default='Default') name = models.CharField(max_length=128, default='Default')
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_image') image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_image')
space_theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR)
custom_space_theme = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_theme')
nav_logo = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_nav_logo')
nav_bg_color = models.CharField(max_length=8, default='', blank=True, )
nav_text_color = models.CharField(max_length=16, choices=NAV_TEXT_COLORS, default=DARK)
created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True) created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
message = models.CharField(max_length=512, default='', blank=True) message = models.CharField(max_length=512, default='', blank=True)
@ -338,22 +374,10 @@ class UserPreference(models.Model, PermissionModelMixin):
) )
# Nav colors # Nav colors
PRIMARY = 'PRIMARY'
SECONDARY = 'SECONDARY'
SUCCESS = 'SUCCESS'
INFO = 'INFO'
WARNING = 'WARNING'
DANGER = 'DANGER'
LIGHT = 'LIGHT' LIGHT = 'LIGHT'
DARK = 'DARK' DARK = 'DARK'
COLORS = ( NAV_TEXT_COLORS = (
(PRIMARY, 'Primary'),
(SECONDARY, 'Secondary'),
(SUCCESS, 'Success'),
(INFO, 'Info'),
(WARNING, 'Warning'),
(DANGER, 'Danger'),
(LIGHT, 'Light'), (LIGHT, 'Light'),
(DARK, 'Dark') (DARK, 'Dark')
) )
@ -371,8 +395,13 @@ class UserPreference(models.Model, PermissionModelMixin):
user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True) user = AutoOneToOneField(User, on_delete=models.CASCADE, primary_key=True)
image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='user_image') image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='user_image')
theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR) theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR)
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY) nav_bg_color = models.CharField(max_length=8, default='#ddbf86')
nav_text_color = models.CharField(max_length=16, choices=NAV_TEXT_COLORS, default=DARK)
nav_show_logo = models.BooleanField(default=True)
nav_sticky = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT)
default_unit = models.CharField(max_length=32, default='g') default_unit = models.CharField(max_length=32, default='g')
use_fractions = models.BooleanField(default=FRACTION_PREF_DEFAULT) use_fractions = models.BooleanField(default=FRACTION_PREF_DEFAULT)
use_kj = models.BooleanField(default=KJ_PREF_DEFAULT) use_kj = models.BooleanField(default=KJ_PREF_DEFAULT)
@ -382,7 +411,6 @@ class UserPreference(models.Model, PermissionModelMixin):
ingredient_decimals = models.IntegerField(default=2) ingredient_decimals = models.IntegerField(default=2)
comments = models.BooleanField(default=COMMENT_PREF_DEFAULT) comments = models.BooleanField(default=COMMENT_PREF_DEFAULT)
shopping_auto_sync = models.IntegerField(default=5) shopping_auto_sync = models.IntegerField(default=5)
sticky_navbar = models.BooleanField(default=STICKY_NAV_PREF_DEFAULT)
mealplan_autoadd_shopping = models.BooleanField(default=False) mealplan_autoadd_shopping = models.BooleanField(default=False)
mealplan_autoexclude_onhand = models.BooleanField(default=True) mealplan_autoexclude_onhand = models.BooleanField(default=True)
mealplan_autoinclude_related = models.BooleanField(default=True) mealplan_autoinclude_related = models.BooleanField(default=True)
@ -718,6 +746,9 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
space = models.ForeignKey(Space, on_delete=models.CASCADE) space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space') objects = ScopedManager(space='space')
def __str__(self):
return f'{self.pk}: {self.amount} {self.food.name} {self.unit.name}'
class Meta: class Meta:
ordering = ['order', 'pk'] ordering = ['order', 'pk']
indexes = ( indexes = (
@ -745,7 +776,9 @@ class Step(ExportModelOperationsMixin('step'), models.Model, PermissionModelMixi
return render_instructions(self) return render_instructions(self)
def __str__(self): def __str__(self):
return f'{self.pk} {self.name}' if not self.recipe_set.exists():
return f"{self.pk}: {_('Orphaned Step')}"
return f"{self.pk}: {self.name}" if self.name else f"Step: {self.pk}"
class Meta: class Meta:
ordering = ['order', 'pk'] ordering = ['order', 'pk']

View File

@ -212,7 +212,7 @@ class UserFileSerializer(serializers.ModelSerializer):
Image.open(obj.file.file.file) Image.open(obj.file.file.file)
return self.context['request'].build_absolute_uri(obj.file.url) return self.context['request'].build_absolute_uri(obj.file.url)
except Exception: except Exception:
traceback.print_exc() # traceback.print_exc()
return "" return ""
def check_file_limit(self, validated_data): def check_file_limit(self, validated_data):
@ -260,7 +260,7 @@ class UserFileViewSerializer(serializers.ModelSerializer):
Image.open(obj.file.file.file) Image.open(obj.file.file.file)
return self.context['request'].build_absolute_uri(obj.file.url) return self.context['request'].build_absolute_uri(obj.file.url)
except Exception: except Exception:
traceback.print_exc() # traceback.print_exc()
return "" return ""
def create(self, validated_data): def create(self, validated_data):
@ -281,6 +281,8 @@ class SpaceSerializer(WritableNestedModelSerializer):
file_size_mb = serializers.SerializerMethodField('get_file_size_mb') file_size_mb = serializers.SerializerMethodField('get_file_size_mb')
food_inherit = FoodInheritFieldSerializer(many=True) food_inherit = FoodInheritFieldSerializer(many=True)
image = UserFileViewSerializer(required=False, many=False, allow_null=True) image = UserFileViewSerializer(required=False, many=False, allow_null=True)
nav_logo = UserFileViewSerializer(required=False, many=False, allow_null=True)
custom_space_theme = UserFileViewSerializer(required=False, many=False, allow_null=True)
def get_user_count(self, obj): def get_user_count(self, obj):
return UserSpace.objects.filter(space=obj).count() return UserSpace.objects.filter(space=obj).count()
@ -302,7 +304,7 @@ class SpaceSerializer(WritableNestedModelSerializer):
fields = ( fields = (
'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users', 'id', 'name', 'created_by', 'created_at', 'message', 'max_recipes', 'max_file_storage_mb', 'max_users',
'allow_sharing', 'demo', 'food_inherit', 'user_count', 'recipe_count', 'file_size_mb', 'allow_sharing', 'demo', 'food_inherit', 'user_count', 'recipe_count', 'file_size_mb',
'image', 'use_plural',) 'image', 'nav_logo', 'space_theme', 'custom_space_theme', 'nav_bg_color', 'nav_text_color', 'use_plural',)
read_only_fields = ( read_only_fields = (
'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing',
'demo',) 'demo',)
@ -372,8 +374,8 @@ class UserPreferenceSerializer(WritableNestedModelSerializer):
class Meta: class Meta:
model = UserPreference model = UserPreference
fields = ( fields = (
'user', 'image', 'theme', 'nav_color', 'default_unit', 'default_page', 'use_fractions', 'use_kj', 'user', 'image', 'theme', 'nav_bg_color', 'nav_text_color', 'nav_show_logo', 'default_unit', 'default_page', 'use_fractions', 'use_kj',
'plan_share', 'sticky_navbar', 'plan_share', 'nav_sticky',
'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'ingredient_decimals', 'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping',
'food_inherit_default', 'default_delay', 'food_inherit_default', 'default_delay',
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days', 'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days',
@ -1021,7 +1023,7 @@ class AutoMealPlanSerializer(serializers.Serializer):
start_date = serializers.DateField() start_date = serializers.DateField()
end_date = serializers.DateField() end_date = serializers.DateField()
meal_type_id = serializers.IntegerField() meal_type_id = serializers.IntegerField()
keywords = KeywordSerializer(many=True) keyword_ids = serializers.ListField()
servings = CustomDecimalField() servings = CustomDecimalField()
shared = UserSerializer(many=True, required=False, allow_null=True) shared = UserSerializer(many=True, required=False, allow_null=True)
addshopping = serializers.BooleanField() addshopping = serializers.BooleanField()

View File

@ -4457,28 +4457,28 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
} }
.navbar-light .navbar-brand, .navbar-light .navbar-brand:focus, .navbar-light .navbar-brand:hover { .navbar-light .navbar-brand, .navbar-light .navbar-brand:focus, .navbar-light .navbar-brand:hover {
color: rgba(46, 46, 46, .9) color: rgba(0, 0, 0, .9)
} }
.navbar-light .navbar-nav .nav-link { .navbar-light .navbar-nav .nav-link {
color: rgba(46, 46, 46, .5) color: rgba(0, 0, 0, .5)
} }
.navbar-light .navbar-nav .nav-link:focus, .navbar-light .navbar-nav .nav-link:hover { .navbar-light .navbar-nav .nav-link:focus, .navbar-light .navbar-nav .nav-link:hover {
color: rgba(46, 46, 46, .7) color: rgba(0, 0, 0, .7)
} }
.navbar-light .navbar-nav .nav-link.disabled { .navbar-light .navbar-nav .nav-link.disabled {
color: rgba(46, 46, 46, .3) color: rgba(0, 0, 0, .3)
} }
.navbar-light .navbar-nav .active > .nav-link, .navbar-light .navbar-nav .nav-link.active, .navbar-light .navbar-nav .nav-link.show, .navbar-light .navbar-nav .show > .nav-link { .navbar-light .navbar-nav .active > .nav-link, .navbar-light .navbar-nav .nav-link.active, .navbar-light .navbar-nav .nav-link.show, .navbar-light .navbar-nav .show > .nav-link {
color: rgba(46, 46, 46, .9) color: rgba(0, 0, 0, .9)
} }
.navbar-light .navbar-toggler { .navbar-light .navbar-toggler {
color: rgba(46, 46, 46, .5); color: rgba(0, 0, 0, .5);
border-color: rgba(46, 46, 46, .1) border-color: rgba(0, 0, 0, .1)
} }
.navbar-light .navbar-toggler-icon { .navbar-light .navbar-toggler-icon {
@ -4486,11 +4486,7 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
} }
.navbar-light .navbar-text { .navbar-light .navbar-text {
color: rgba(46, 46, 46, .5) color: rgba(0, 0, 0, .5)
}
.navbar-light .navbar-text a, .navbar-light .navbar-text a:focus, .navbar-light .navbar-text a:hover {
color: rgba(46, 46, 46, .9)
} }
.navbar-dark .navbar-brand, .navbar-dark .navbar-brand:focus, .navbar-dark .navbar-brand:hover { .navbar-dark .navbar-brand, .navbar-dark .navbar-brand:focus, .navbar-dark .navbar-brand:hover {
@ -4498,24 +4494,24 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
} }
.navbar-dark .navbar-nav .nav-link { .navbar-dark .navbar-nav .nav-link {
color: hsla(0, 0%, 18%, .5) color: rgba(255, 255, 255, .5)
} }
.navbar-dark .navbar-nav .nav-link:focus, .navbar-dark .navbar-nav .nav-link:hover { .navbar-dark .navbar-nav .nav-link:focus, .navbar-dark .navbar-nav .nav-link:hover {
color: hsla(0, 0%, 18%, .75) color: rgba(255, 255, 255, .75)
} }
.navbar-dark .navbar-nav .nav-link.disabled { .navbar-dark .navbar-nav .nav-link.disabled {
color: hsla(0, 0%, 18%, .25) color: rgba(255, 255, 255, .25)
} }
.navbar-dark .navbar-nav .active > .nav-link, .navbar-dark .navbar-nav .nav-link.active, .navbar-dark .navbar-nav .nav-link.show, .navbar-dark .navbar-nav .show > .nav-link { .navbar-dark .navbar-nav .active > .nav-link, .navbar-dark .navbar-nav .nav-link.active, .navbar-dark .navbar-nav .nav-link.show, .navbar-dark .navbar-nav .show > .nav-link {
color: #2e2e2e color: #FFF
} }
.navbar-dark .navbar-toggler { .navbar-dark .navbar-toggler {
color: rgba(46, 46, 46, 0.5); color: rgba(255, 255, 255, .5);
border-color: rgba(46, 46, 46, 0.5); border-color: rgba(255, 255, 255, .1)
} }
.navbar-dark .navbar-toggler-icon { .navbar-dark .navbar-toggler-icon {
@ -4523,7 +4519,7 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
} }
.navbar-dark .navbar-text { .navbar-dark .navbar-text {
color: hsla(0, 0%, 18%, .5) color: rgba(255, 255, 255, .5)
} }
.navbar-dark .navbar-text a, .navbar-dark .navbar-text a:focus, .navbar-dark .navbar-text a:hover { .navbar-dark .navbar-text a, .navbar-dark .navbar-text a:focus, .navbar-dark .navbar-text a:hover {

View File

@ -4412,28 +4412,28 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
} }
.navbar-light .navbar-brand, .navbar-light .navbar-brand:focus, .navbar-light .navbar-brand:hover { .navbar-light .navbar-brand, .navbar-light .navbar-brand:focus, .navbar-light .navbar-brand:hover {
color: rgba(46, 46, 46, .9) color: rgba(0, 0, 0, .9)
} }
.navbar-light .navbar-nav .nav-link { .navbar-light .navbar-nav .nav-link {
color: rgba(46, 46, 46, .5) color: rgba(0, 0, 0, .5)
} }
.navbar-light .navbar-nav .nav-link:focus, .navbar-light .navbar-nav .nav-link:hover { .navbar-light .navbar-nav .nav-link:focus, .navbar-light .navbar-nav .nav-link:hover {
color: rgba(46, 46, 46, .7) color: rgba(0, 0, 0, .7)
} }
.navbar-light .navbar-nav .nav-link.disabled { .navbar-light .navbar-nav .nav-link.disabled {
color: rgba(46, 46, 46, .3) color: rgba(0, 0, 0, .3)
} }
.navbar-light .navbar-nav .active > .nav-link, .navbar-light .navbar-nav .nav-link.active, .navbar-light .navbar-nav .nav-link.show, .navbar-light .navbar-nav .show > .nav-link { .navbar-light .navbar-nav .active > .nav-link, .navbar-light .navbar-nav .nav-link.active, .navbar-light .navbar-nav .nav-link.show, .navbar-light .navbar-nav .show > .nav-link {
color: rgba(46, 46, 46, .9) color: rgba(0, 0, 0, .9)
} }
.navbar-light .navbar-toggler { .navbar-light .navbar-toggler {
color: rgba(46, 46, 46, .5); color: rgba(0, 0, 0, .5);
border-color: rgba(46, 46, 46, .1) border-color: rgba(0, 0, 0, .1)
} }
.navbar-light .navbar-toggler-icon { .navbar-light .navbar-toggler-icon {
@ -4441,11 +4441,11 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
} }
.navbar-light .navbar-text { .navbar-light .navbar-text {
color: rgba(46, 46, 46, .5) color: rgba(0, 0, 0, .5)
} }
.navbar-light .navbar-text a, .navbar-light .navbar-text a:focus, .navbar-light .navbar-text a:hover { .navbar-light .navbar-text a, .navbar-light .navbar-text a:focus, .navbar-light .navbar-text a:hover {
color: rgba(46, 46, 46, .9) color: rgba(0, 0, 0, .9)
} }
.navbar-dark .navbar-brand, .navbar-dark .navbar-brand:focus, .navbar-dark .navbar-brand:hover { .navbar-dark .navbar-brand, .navbar-dark .navbar-brand:focus, .navbar-dark .navbar-brand:hover {
@ -4453,24 +4453,24 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
} }
.navbar-dark .navbar-nav .nav-link { .navbar-dark .navbar-nav .nav-link {
color: hsla(0, 0%, 5%, .5) color: rgba(255, 255, 255, .5)
} }
.navbar-dark .navbar-nav .nav-link:focus, .navbar-dark .navbar-nav .nav-link:hover { .navbar-dark .navbar-nav .nav-link:focus, .navbar-dark .navbar-nav .nav-link:hover {
color: hsla(0, 0%, 5%, .75) color: rgba(255, 255, 255, .75)
} }
.navbar-dark .navbar-nav .nav-link.disabled { .navbar-dark .navbar-nav .nav-link.disabled {
color: hsla(0, 0%, 5%, .25) color: rgba(255, 255, 255, .25)
} }
.navbar-dark .navbar-nav .active > .nav-link, .navbar-dark .navbar-nav .nav-link.active, .navbar-dark .navbar-nav .nav-link.show, .navbar-dark .navbar-nav .show > .nav-link { .navbar-dark .navbar-nav .active > .nav-link, .navbar-dark .navbar-nav .nav-link.active, .navbar-dark .navbar-nav .nav-link.show, .navbar-dark .navbar-nav .show > .nav-link {
color: #1E1E1E color: #FFF
} }
.navbar-dark .navbar-toggler { .navbar-dark .navbar-toggler {
color: rgba(46, 46, 46, 0.5); color: rgba(255, 255, 255, .5);
border-color: rgba(46, 46, 46, 0.5); border-color: rgba(255, 255, 255, .1)
} }
.navbar-dark .navbar-toggler-icon { .navbar-dark .navbar-toggler-icon {
@ -4478,7 +4478,7 @@ input[type=button].btn-block, input[type=reset].btn-block, input[type=submit].bt
} }
.navbar-dark .navbar-text { .navbar-dark .navbar-text {
color: hsla(0, 0%, 18%, .5) color: rgba(255, 255, 255, .5)
} }
.navbar-dark .navbar-text a, .navbar-dark .navbar-text a:focus, .navbar-dark .navbar-text a:hover { .navbar-dark .navbar-text a, .navbar-dark .navbar-text a:focus, .navbar-dark .navbar-text a:hover {

View File

@ -33,6 +33,11 @@
<!-- Bootstrap 4 --> <!-- Bootstrap 4 -->
<link id="id_main_css" href="{% theme_url request %}" rel="stylesheet"> <link id="id_main_css" href="{% theme_url request %}" rel="stylesheet">
{% if request.user.is_authenticated and request.space.custom_space_theme %}
<link id="id_custom_css" href="{% custom_theme request %}" rel="stylesheet">
{% endif %}
<link href="{% static 'css/app.min.css' %}" rel="stylesheet"> <link href="{% static 'css/app.min.css' %}" rel="stylesheet">
<script src="{% static 'js/jquery-3.5.1.min.js' %}"></script> <script src="{% static 'js/jquery-3.5.1.min.js' %}"></script>
@ -74,12 +79,12 @@
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-lg {% nav_color request %}" <nav class="navbar navbar-expand-lg {% nav_text_color request %}"
id="id_main_nav" id="id_main_nav"
style="{% sticky_nav request %}"> style="{% sticky_nav request %}; background-color: {% nav_bg_color request %}">
{% if not request.user.userpreference.left_handed %} {% if not request.user.userpreference.left_handed %}
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %} {% if not request.user.is_authenticated or request.user.userpreference.nav_show_logo %}
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}" <a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}"
aria-label="Tandoor"> aria-label="Tandoor">
<img class="brand-icon" src="{% logo_url request %}" alt="Logo"> <img class="brand-icon" src="{% logo_url request %}" alt="Logo">
@ -93,7 +98,7 @@
</button> </button>
{% if request.user.userpreference.left_handed %} {% if request.user.userpreference.left_handed %}
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %} {% if not request.user.is_authenticated or request.user.userpreference.nav_show_logo %}
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}" <a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}"
aria-label="Tandoor"> aria-label="Tandoor">
<img class="brand-icon" src="{% logo_url request %}" alt="Logo"> <img class="brand-icon" src="{% logo_url request %}" alt="Logo">

View File

@ -1,7 +1,8 @@
from django import template from django import template
from django.templatetags.static import static from django.templatetags.static import static
from django_scopes import scopes_disabled
from cookbook.models import UserPreference from cookbook.models import UserPreference, UserFile, Space
from recipes.settings import STICKY_NAV_PREF_DEFAULT from recipes.settings import STICKY_NAV_PREF_DEFAULT
register = template.Library() register = template.Library()
@ -19,35 +20,58 @@ def theme_url(request):
UserPreference.TANDOOR: 'themes/tandoor.min.css', UserPreference.TANDOOR: 'themes/tandoor.min.css',
UserPreference.TANDOOR_DARK: 'themes/tandoor_dark.min.css', UserPreference.TANDOOR_DARK: 'themes/tandoor_dark.min.css',
} }
if request.user.userpreference.theme in themes: # if request.space.custom_space_theme:
return static(themes[request.user.userpreference.theme]) # return request.space.custom_space_theme.file.url
if request.space.space_theme in themes:
return static(themes[request.space.space_theme])
else: else:
raise AttributeError if request.user.userpreference.theme in themes:
return static(themes[request.user.userpreference.theme])
else:
raise AttributeError
@register.simple_tag
def custom_theme(request):
if request.user.is_authenticated and request.space.custom_space_theme:
return request.space.custom_space_theme.file.url
@register.simple_tag @register.simple_tag
def logo_url(request): def logo_url(request):
if request.user.is_authenticated and getattr(getattr(request, "space", {}), 'image', None): if request.user.is_authenticated and getattr(getattr(request, "space", {}), 'nav_logo', None):
return request.space.image.file.url return request.space.nav_logo.file.url
else: else:
return static('assets/brand_logo.png') return static('assets/brand_logo.png')
@register.simple_tag @register.simple_tag
def nav_color(request): def nav_bg_color(request):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return 'navbar-light bg-primary' return '#ddbf86'
if request.user.userpreference.nav_color.lower() in ['light', 'warning', 'info', 'success']:
return f'navbar-light bg-{request.user.userpreference.nav_color.lower()}'
else: else:
return f'navbar-dark bg-{request.user.userpreference.nav_color.lower()}' if request.space.nav_bg_color:
return request.space.nav_bg_color
else:
return request.user.userpreference.nav_bg_color
@register.simple_tag
def nav_text_color(request):
type_mapping = {Space.DARK: 'navbar-light', Space.LIGHT: 'navbar-dark'} # inverted since navbar-dark means the background
if not request.user.is_authenticated:
return 'navbar-dark'
else:
if request.space.nav_text_color != Space.BLANK:
return type_mapping[request.space.nav_text_color]
else:
return type_mapping[request.user.userpreference.nav_text_color]
@register.simple_tag @register.simple_tag
def sticky_nav(request): def sticky_nav(request):
if (not request.user.is_authenticated and STICKY_NAV_PREF_DEFAULT) or \ if (not request.user.is_authenticated and STICKY_NAV_PREF_DEFAULT) or (request.user.is_authenticated and request.user.userpreference.nav_sticky):
(request.user.is_authenticated and request.user.userpreference.sticky_navbar): # noqa: E501
return 'position: sticky; top: 0; left: 0; z-index: 1000;' return 'position: sticky; top: 0; left: 0; z-index: 1000;'
else: else:
return '' return ''

View File

@ -738,7 +738,7 @@ class AutoPlanViewSet(viewsets.ViewSet):
serializer = AutoMealPlanSerializer(data=request.data) serializer = AutoMealPlanSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
keywords = serializer.validated_data['keywords'] keyword_ids = serializer.validated_data['keyword_ids']
start_date = serializer.validated_data['start_date'] start_date = serializer.validated_data['start_date']
end_date = serializer.validated_data['end_date'] end_date = serializer.validated_data['end_date']
servings = serializer.validated_data['servings'] servings = serializer.validated_data['servings']
@ -753,8 +753,8 @@ class AutoPlanViewSet(viewsets.ViewSet):
recipes = Recipe.objects.values('id', 'name') recipes = Recipe.objects.values('id', 'name')
meal_plans = list() meal_plans = list()
for keyword in keywords: for keyword_id in keyword_ids:
recipes = recipes.filter(keywords__name=keyword['name']) recipes = recipes.filter(keywords__id=keyword_id)
if len(recipes) == 0: if len(recipes) == 0:
return Response(serializer.data) return Response(serializer.data)

View File

@ -71,12 +71,13 @@ Basic guide to setup Docker and Portainer TrueNAS Core.
-Select "Get Started" to use the Enviroment Portainer is running in -Select "Get Started" to use the Enviroment Portainer is running in
![Screenshot of Enviroment Wizard](https://2914113074-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiZWHJxqQsgWYd9sI88sO%2Fuploads%2Fsig45vFliINvOKGKVStk%2F2.15-install-server-setup-wizard.png?alt=media&token=cd21d9e8-0632-40db-af9a-581365f98209) ![Screenshot of Enviroment Wizard](https://2914113074-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiZWHJxqQsgWYd9sI88sO%2Fuploads%2Fsig45vFliINvOKGKVStk%2F2.15-install-server-setup-wizard.png?alt=media&token=cd21d9e8-0632-40db-af9a-581365f98209)
### 3. Install Tandoor Recipies VIA Portainer Web Editor ### 3. Install Tandoor Recipes VIA Portainer Web Editor
-From the menu select Stacks, click Add stack, give the stack a descriptive name then select Web editor. -From the menu select Stacks, click Add stack, give the stack a descriptive name then select Web editor.
![Screenshot of Stack List](https://2914113074-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiZWHJxqQsgWYd9sI88sO%2Fuploads%2FnBx62EIPhmUy1L0S1iKI%2F2.15-docker_add_stack_web_editor.gif?alt=media&token=c45c0151-9c15-4d79-b229-1a90a7a86b84) ![Screenshot of Stack List](https://2914113074-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiZWHJxqQsgWYd9sI88sO%2Fuploads%2FnBx62EIPhmUy1L0S1iKI%2F2.15-docker_add_stack_web_editor.gif?alt=media&token=c45c0151-9c15-4d79-b229-1a90a7a86b84)
-Use the below code and input it into the Web Editor: -Use the below code and input it into the Web Editor:
`version: "3" ```yaml
version: "3"
services: services:
db_recipes: db_recipes:
restart: always restart: always
@ -87,13 +88,12 @@ services:
- stack.env - stack.env
web_recipes: web_recipes:
# image: vabene1111/recipes:latest image: vabene1111/recipes:latest
image: vabene1111/recipes:beta
env_file: env_file:
- stack.env - stack.env
volumes: volumes:
- staticfiles:/opt/recipes/staticfiles - staticfiles:/opt/recipes/staticfiles
# Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts # Do not make this a bind mount, see https://docs.tandoor.dev/install/docker/#volumes-vs-bind-mounts
- nginx_config:/opt/recipes/nginx/conf.d - nginx_config:/opt/recipes/nginx/conf.d
- ./mediafiles:/opt/recipes/mediafiles - ./mediafiles:/opt/recipes/mediafiles
depends_on: depends_on:
@ -116,7 +116,8 @@ services:
volumes: volumes:
nginx_config: nginx_config:
staticfiles:` staticfiles:
```
-Download the .env template from [HERE](https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template) and load this file by pressing the "Load Variables from .env File" button: -Download the .env template from [HERE](https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template) and load this file by pressing the "Load Variables from .env File" button:
![Screenshot of Add Stack screen](https://www.portainer.io/hubfs/image-png-Feb-21-2022-06-21-15-88-PM.png) ![Screenshot of Add Stack screen](https://www.portainer.io/hubfs/image-png-Feb-21-2022-06-21-15-88-PM.png)

View File

@ -506,7 +506,6 @@
<input <input
class="form-control" class="form-control"
maxlength="256" maxlength="256"
style="height: 100%;"
v-model="ingredient.note" v-model="ingredient.note"
v-bind:placeholder="$t('Note')" v-bind:placeholder="$t('Note')"
v-on:keydown.tab=" v-on:keydown.tab="

View File

@ -1,8 +1,8 @@
<template> <template>
<div id="app"> <div id="app">
<div class="row mt-2"> <b-row class="mt-2">
<div class="col col-12"> <b-col cols="12">
<div v-if="space !== undefined"> <div v-if="space !== undefined">
<h6><i class="fas fa-book"></i> {{ $t('Recipes') }}</h6> <h6><i class="fas fa-book"></i> {{ $t('Recipes') }}</h6>
<b-progress height="1.5rem" :max="space.max_recipes" variant="success" :striped="true"> <b-progress height="1.5rem" :max="space.max_recipes" variant="success" :striped="true">
@ -32,13 +32,13 @@
</b-progress> </b-progress>
</div> </div>
</div> </b-col>
</div> </b-row>
<div class="row mt-4"> <b-row class="mt-4">
<div class="col col-12"> <b-col cols="12">
<div v-if="user_spaces !== undefined"> <div v-if="user_spaces !== undefined">
<h4 class="mt-2"><i class="fas fa-users"></i> {{ $t('Users') }}</h4> <h4><i class="fas fa-users"></i> {{ $t('Users') }}</h4>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
@ -51,14 +51,14 @@
<td>{{ us.user.display_name }}</td> <td>{{ us.user.display_name }}</td>
<td> <td>
<generic-multiselect <generic-multiselect
class="input-group-text m-0 p-0" class="input-group-text m-0 p-0"
@change="us.groups = $event.val; updateUserSpace(us)" @change="us.groups = $event.val; updateUserSpace(us)"
label="name" label="name"
:initial_selection="us.groups" :initial_selection="us.groups"
:model="Models.GROUP" :model="Models.GROUP"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0" style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:limit="10" :limit="10"
:multiple="true" :multiple="true"
/> />
</td> </td>
<td> <td>
@ -67,12 +67,12 @@
</tr> </tr>
</table> </table>
</div> </div>
</div> </b-col>
</div> </b-row>
<div class="row mt-2"> <b-row class="mt-2">
<div class="col col-12"> <b-col cols="12">
<div v-if="invite_links !== undefined"> <div v-if="invite_links !== undefined">
<h4 class="mt-2"><i class="fas fa-users"></i> {{ $t('Invites') }}</h4> <h4 class="mt-2"><i class="fas fa-users"></i> {{ $t('Invites') }}</h4>
<table class="table"> <table class="table">
@ -90,14 +90,14 @@
<td>{{ il.email }}</td> <td>{{ il.email }}</td>
<td> <td>
<generic-multiselect <generic-multiselect
class="input-group-text m-0 p-0" class="input-group-text m-0 p-0"
@change="il.group = $event.val;" @change="il.group = $event.val;"
label="name" label="name"
:initial_single_selection="il.group" :initial_single_selection="il.group"
:model="Models.GROUP" :model="Models.GROUP"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0" style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:limit="10" :limit="10"
:multiple="false" :multiple="false"
/> />
</td> </td>
<td><input type="date" v-model="il.valid_until" class="form-control"></td> <td><input type="date" v-model="il.valid_until" class="form-control"></td>
@ -131,43 +131,102 @@
</table> </table>
<b-button variant="primary" @click="show_invite_create = true">{{ $t('Create') }}</b-button> <b-button variant="primary" @click="show_invite_create = true">{{ $t('Create') }}</b-button>
</div> </div>
</div> </b-col>
</div> </b-row>
<div class="row mt-4" v-if="space !== undefined"> <b-row class="mt-4" v-if="space !== undefined">
<div class="col col-12"> <b-col cols="12">
<h4 class="mt-2"><i class="fas fa-cogs"></i> {{ $t('Settings') }}</h4> <h4>{{ $t('Cosmetic') }}</h4>
<b-alert variant="warning" show><i class="fas fa-exclamation-triangle"></i> {{ $t('Space_Cosmetic_Settings') }}</b-alert>
<label>{{ $t('Message') }}</label> <b-form-group :label="$t('Image')" :description="$t('CustomImageHelp')">
<b-form-textarea v-model="space.message"></b-form-textarea> <generic-multiselect :initial_single_selection="space.image"
:model="Models.USERFILE"
:multiple="false"
@change="space.image = $event.val;"></generic-multiselect>
</b-form-group>
<label>{{ $t('Image') }}</label> <b-form-group :label="$t('Logo')" :description="$t('CustomNavLogoHelp')">
<generic-multiselect :initial_single_selection="space.image" <generic-multiselect :initial_single_selection="space.nav_logo"
:model="Models.USERFILE" :model="Models.USERFILE"
:multiple="false" :multiple="false"
@change="space.image = $event.val;"></generic-multiselect> @change="space.nav_logo = $event.val;"></generic-multiselect>
<br/> </b-form-group>
<label>{{ $t('FoodInherit') }}</label> <b-form-group :label="$t('Theme')">
<generic-multiselect :initial_selection="space.food_inherit" <b-form-select v-model="space.space_theme">
:model="Models.FOOD_INHERIT_FIELDS" <b-form-select-option value="BLANK">----</b-form-select-option>
@change="space.food_inherit = $event.val;"> <b-form-select-option value="TANDOOR">Tandoor</b-form-select-option>
</generic-multiselect> <b-form-select-option value="TANDOOR_DARK">Tandoor Dark (Beta)</b-form-select-option>
<span class="text-muted small">{{ $t('food_inherit_info') }}</span><br/> <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-group>
<a class="btn btn-success" @click="updateSpace()">{{ $t('Update') }}</a><br/>
<a class="btn btn-warning mt-1" @click="resetInheritance()">{{ $t('reset_food_inheritance') }}</a><br/>
<span class="text-muted small">{{ $t('reset_food_inheritance_info') }}</span>
</div>
</div>
<div class="row"> <b-form-group :label="$t('CustomTheme')" :description="$t('CustomThemeHelp')">
<div class="col-md-12"> <generic-multiselect :initial_single_selection="space.custom_space_theme"
:model="Models.USERFILE"
:multiple="false"
@change="space.custom_space_theme = $event.val;"></generic-multiselect>
</b-form-group>
<b-form-group :label="$t('Nav_Color')" :description="$t('Nav_Color_Help')">
<b-input-group>
<b-form-input type="color" v-model="space.nav_bg_color"></b-form-input>
<b-input-group-append>
<b-button @click="space.nav_bg_color = ''">{{ $t('Reset') }}</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
<b-form-group :label="$t('Nav_Text_Mode')" :description="$t('Nav_Text_Mode_Help')">
<b-form-select v-model="space.nav_text_color">
<b-form-select-option value="BLANK">----</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>
</b-form-group>
<b-button variant="success" @click="updateSpace()">{{ $t('Update') }}</b-button>
</b-col>
</b-row>
<b-row class="mt-4" v-if="space !== undefined">
<b-col cols="12">
<h4><i class="fas fa-cogs"></i> {{ $t('Settings') }}</h4>
<b-form-group :label="$t('Message')">
<b-form-textarea v-model="space.message"></b-form-textarea>
</b-form-group>
<b-form-group :label="$t('FoodInherit')" :description="$t('food_inherit_info')">
<generic-multiselect :initial_selection="space.food_inherit"
:model="Models.FOOD_INHERIT_FIELDS"
@change="space.food_inherit = $event.val;">
</generic-multiselect>
</b-form-group>
<b-form-group :description="$t('reset_food_inheritance_info')">
<b-button-group class="mt-2">
<b-button variant="success" @click="updateSpace()">{{ $t('Update') }}</b-button>
<b-button variant="warning" @click="resetInheritance()">{{ $t('reset_food_inheritance') }}</b-button>
</b-button-group>
</b-form-group>
</b-col>
</b-row>
<b-row class="mt-4">
<b-col cols="12">
<h4>{{ $t('Open_Data_Import') }}</h4> <h4>{{ $t('Open_Data_Import') }}</h4>
<open-data-import-component></open-data-import-component> <open-data-import-component></open-data-import-component>
</div> </b-col>
</div> </b-row>
<div class="row mt-4"> <div class="row mt-4">

View File

@ -227,11 +227,18 @@ export default {
async autoPlanThread(autoPlan, mealTypeIndex) { async autoPlanThread(autoPlan, mealTypeIndex) {
let apiClient = new ApiApiFactory() let apiClient = new ApiApiFactory()
let keyword_ids = []
for (const index in autoPlan.keywords[mealTypeIndex]){
let keyword = autoPlan.keywords[mealTypeIndex][index]
keyword_ids.push(keyword.id)
}
let data = { let data = {
"start_date": moment(autoPlan.startDay).format("YYYY-MM-DD"), "start_date": moment(autoPlan.startDay).format("YYYY-MM-DD"),
"end_date": moment(autoPlan.endDay).format("YYYY-MM-DD"), "end_date": moment(autoPlan.endDay).format("YYYY-MM-DD"),
"meal_type_id": autoPlan.meal_types[mealTypeIndex].id, "meal_type_id": autoPlan.meal_types[mealTypeIndex].id,
"keywords": autoPlan.keywords[mealTypeIndex], "keyword_ids": keyword_ids,
"servings": autoPlan.servings, "servings": autoPlan.servings,
"shared": autoPlan.shared, "shared": autoPlan.shared,
"addshopping": autoPlan.addshopping "addshopping": autoPlan.addshopping

View File

@ -49,34 +49,47 @@
</b-form-select> </b-form-select>
</b-form-group> </b-form-group>
<b-alert variant="warning" show><i class="fas fa-exclamation-triangle"></i> {{ $t('Space_Cosmetic_Settings') }}</b-alert>
<b-form-group :label="$t('Theme')"> <b-form-group :label="$t('Theme')">
<b-form-select v-model="user_preferences.theme" @change="updateSettings(true);"> <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="TANDOOR">Tandoor</b-form-select-option>
<b-form-select-option value="TANDOOR_DARK">Tandoor Dark (Beta)</b-form-select-option>
<b-form-select-option value="BOOTSTRAP">Bootstrap</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="DARKLY">Darkly</b-form-select-option>
<b-form-select-option value="FLATLY">Flatly</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-option value="SUPERHERO">Superhero</b-form-select-option>
<b-form-select-option value="TANDOOR_DARK">Tandoor Dark (INCOMPLETE)</b-form-select-option>
</b-form-select> </b-form-select>
</b-form-group> </b-form-group>
<b-form-group :description="$t('Sticky_Nav_Help')">
<b-form-checkbox v-model="user_preferences.sticky_navbar" @change="updateSettings(true);">
{{ $t('Sticky_Nav') }}
</b-form-checkbox>
</b-form-group>
<b-form-group :label="$t('Nav_Color')" :description="$t('Nav_Color_Help')"> <b-form-group :label="$t('Nav_Color')" :description="$t('Nav_Color_Help')">
<b-form-select v-model="user_preferences.nav_color" @change="updateSettings(true);"> <b-input-group>
<b-form-select-option value="PRIMARY">Primary</b-form-select-option> <b-form-input type="color" v-model="user_preferences.nav_bg_color" @change="updateSettings(true);"></b-form-input>
<b-form-select-option value="SECONDARY">Secondary</b-form-select-option> <b-input-group-append>
<b-form-select-option value="SUCCESS">Success</b-form-select-option> <b-button @click="user_preferences.nav_bg_color = '#ddbf86'; updateSettings(true);">{{ $t('Reset') }}</b-button>
<b-form-select-option value="INFO">Info</b-form-select-option> </b-input-group-append>
<b-form-select-option value="WARNING">Warning</b-form-select-option> </b-input-group>
<b-form-select-option value="DANGER">Danger</b-form-select-option> </b-form-group>
<b-form-group :label="$t('Nav_Text_Mode')" :description="$t('Nav_Text_Mode_Help')">
<b-form-select v-model="user_preferences.nav_text_color" @change="updateSettings(true);">
<b-form-select-option value="LIGHT">Light</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-option value="DARK">Dark</b-form-select-option>
</b-form-select> </b-form-select>
</b-form-group>
<b-form-group :description="$t('Show_Logo_Help')">
<b-form-checkbox v-model="user_preferences.nav_show_logo" @change="updateSettings(true);">
{{ $t('Show_Logo') }}
</b-form-checkbox>
</b-form-group>
<b-form-group :description="$t('Sticky_Nav_Help')">
<b-form-checkbox v-model="user_preferences.nav_sticky" @change="updateSettings(true);">
{{ $t('Sticky_Nav') }}
</b-form-checkbox>
</b-form-group> </b-form-group>

View File

@ -535,5 +535,6 @@
"Never_Unit": "Nie Einheit", "Never_Unit": "Nie Einheit",
"Unit_Replace": "Einheit Ersetzen", "Unit_Replace": "Einheit Ersetzen",
"quart": "\"Quart\" [qt] (US, Volumen)", "quart": "\"Quart\" [qt] (US, Volumen)",
"imperial_quart": "Engl. \"Quart\" [imp qt] (UK, Volumen)" "imperial_quart": "Engl. \"Quart\" [imp qt] (UK, Volumen)",
"err_importing_recipe": "Beim Importieren des Rezeptes ist ein Fehler aufgetreten!"
} }

File diff suppressed because it is too large Load Diff

View File

@ -263,9 +263,9 @@
"Current_Period": "תקופה נוכחית", "Current_Period": "תקופה נוכחית",
"Next_Day": "היום הבא", "Next_Day": "היום הבא",
"Previous_Day": "יום קודם", "Previous_Day": "יום קודם",
"Inherit": "", "Inherit": "ירושה",
"InheritFields": "", "InheritFields": "ירושת ערכי שדות",
"FoodInherit": "", "FoodInherit": "ערכי מזון",
"ShowUncategorizedFood": "הצג לא מוגדר", "ShowUncategorizedFood": "הצג לא מוגדר",
"GroupBy": "אסוף לפי", "GroupBy": "אסוף לפי",
"Language": "שפה", "Language": "שפה",
@ -317,14 +317,14 @@
"CategoryName": "שם קטגוריה", "CategoryName": "שם קטגוריה",
"SupermarketName": "שם סופרמרקט", "SupermarketName": "שם סופרמרקט",
"CategoryInstruction": "גרור קטגוריות לשינוי הסדר שבו הן מופיעות ברשימת הקניות.", "CategoryInstruction": "גרור קטגוריות לשינוי הסדר שבו הן מופיעות ברשימת הקניות.",
"shopping_recent_days_desc": "", "shopping_recent_days_desc": "מספר ימי קניות להציג.",
"shopping_recent_days": "", "shopping_recent_days": "מספר ימים",
"download_pdf": "", "download_pdf": "הורד PDF",
"download_csv": "", "download_csv": "הורד CSV",
"csv_delim_help": "", "csv_delim_help": "",
"csv_delim_label": "", "csv_delim_label": "",
"SuccessClipboard": "", "SuccessClipboard": "רשימת קניות הועתקה",
"copy_to_clipboard": "", "copy_to_clipboard": "העתק",
"csv_prefix_help": "תחילית להוספה כאשר מעתיקים את הרשימה ללוח הכתיבה.", "csv_prefix_help": "תחילית להוספה כאשר מעתיקים את הרשימה ללוח הכתיבה.",
"csv_prefix_label": "רשימת תחיליות", "csv_prefix_label": "רשימת תחיליות",
"copy_markdown_table": "העתק כטבלת Markdown", "copy_markdown_table": "העתק כטבלת Markdown",
@ -521,5 +521,15 @@
"Alignment": "יישור", "Alignment": "יישור",
"StartDate": "תאריך התחלה", "StartDate": "תאריך התחלה",
"EndDate": "תאריך סיום", "EndDate": "תאריך סיום",
"OrderInformation": "המוצרים מוצגים מהמספר הקטן לגדול." "OrderInformation": "המוצרים מוצגים מהמספר הקטן לגדול.",
"FDC_ID_help": "מספר FDC",
"FDC_ID": "מספר FDC",
"show_step_ingredients_setting": "הצג חומרי גלם בתוך שלבי המרשם",
"err_importing_recipe": "שגיאה בעת יבוא המרשם!",
"FDC_Search": "חפש FDC",
"property_type_fdc_hint": "רק תכונות עם מספר FDC ימשכו מבסיס נתוני FDC",
"Property_Editor": "עורך ערכים",
"show_step_ingredients_setting_help": "הצג טבלת חומרי גלם לצדי שלבי המרשם. ניתן לשנות בזמן עריכת המרשם.",
"show_step_ingredients": "הראה חומרי גלם בשלבי המרשם",
"hide_step_ingredients": "הסתר חומרי גלם בשלבי המרשם"
} }

View File

@ -345,5 +345,6 @@
"GroupBy": "Сгруппировать по", "GroupBy": "Сгруппировать по",
"food_inherit_info": "Поля для продуктов питания, которые должны наследоваться по умолчанию.", "food_inherit_info": "Поля для продуктов питания, которые должны наследоваться по умолчанию.",
"warning_space_delete": "Вы можете удалить свое пространство, включая все рецепты, списки покупок, планы питания и все остальное, что вы создали. Этого нельзя отменить! Вы уверены, что хотите это сделать?", "warning_space_delete": "Вы можете удалить свое пространство, включая все рецепты, списки покупок, планы питания и все остальное, что вы создали. Этого нельзя отменить! Вы уверены, что хотите это сделать?",
"Description_Replace": "Изменить описание" "Description_Replace": "Изменить описание",
"err_importing_recipe": "Произошла ошибка при импортировании рецепта!"
} }

View File

@ -4537,6 +4537,18 @@ export interface Space {
* @memberof Space * @memberof Space
*/ */
image?: RecipeFile | null; image?: RecipeFile | null;
/**
*
* @type {RecipeFile}
* @memberof Space
*/
nav_logo?: RecipeFile | null;
/**
*
* @type {RecipeFile}
* @memberof Space
*/
space_theme?: RecipeFile | null;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -5124,7 +5136,19 @@ export interface UserPreference {
* @type {string} * @type {string}
* @memberof UserPreference * @memberof UserPreference
*/ */
nav_color?: UserPreferenceNavColorEnum; nav_bg_color?: string;
/**
*
* @type {string}
* @memberof UserPreference
*/
nav_text_color?: UserPreferenceNavTextColorEnum;
/**
*
* @type {boolean}
* @memberof UserPreference
*/
nav_show_logo?: boolean;
/** /**
* *
* @type {string} * @type {string}
@ -5160,7 +5184,7 @@ export interface UserPreference {
* @type {boolean} * @type {boolean}
* @memberof UserPreference * @memberof UserPreference
*/ */
sticky_navbar?: boolean; nav_sticky?: boolean;
/** /**
* *
* @type {number} * @type {number}
@ -5281,13 +5305,7 @@ export enum UserPreferenceThemeEnum {
* @export * @export
* @enum {string} * @enum {string}
*/ */
export enum UserPreferenceNavColorEnum { export enum UserPreferenceNavTextColorEnum {
Primary = 'PRIMARY',
Secondary = 'SECONDARY',
Success = 'SUCCESS',
Info = 'INFO',
Warning = 'WARNING',
Danger = 'DANGER',
Light = 'LIGHT', Light = 'LIGHT',
Dark = 'DARK' Dark = 'DARK'
} }