Merge branch 'develop' into feature/shopping-ui
# Conflicts: # cookbook/models.py # vue/src/locales/en.json
186
.env.template
@ -1,191 +1,15 @@
|
|||||||
# only set this to true when testing/debugging
|
# ---------------------------------------------------------------------------
|
||||||
# when unset: 1 (true) - dont unset this, just for development
|
# This template contains only required options.
|
||||||
DEBUG=0
|
# Visit the docs to find more https://docs.tandoor.dev/system/configuration/
|
||||||
SQL_DEBUG=0
|
# ---------------------------------------------------------------------------
|
||||||
DEBUG_TOOLBAR=0
|
|
||||||
# Gunicorn log level for debugging (default value is "info" when unset)
|
|
||||||
# (see https://docs.gunicorn.org/en/stable/settings.html#loglevel for available settings)
|
|
||||||
# GUNICORN_LOG_LEVEL="debug"
|
|
||||||
|
|
||||||
# HTTP port to bind to
|
|
||||||
# TANDOOR_PORT=8080
|
|
||||||
|
|
||||||
# hosts the application can run under e.g. recipes.mydomain.com,cooking.mydomain.com,...
|
|
||||||
ALLOWED_HOSTS=*
|
|
||||||
|
|
||||||
# Cross Site Request Forgery protection
|
|
||||||
# (https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-CSRF_TRUSTED_ORIGINS)
|
|
||||||
# CSRF_TRUSTED_ORIGINS = []
|
|
||||||
|
|
||||||
# Cross Origin Resource Sharing
|
|
||||||
# (https://github.com/adamchainz/django-cors-header)
|
|
||||||
# CORS_ALLOW_ALL_ORIGINS = True
|
|
||||||
|
|
||||||
# random secret key, use for example `base64 /dev/urandom | head -c50` to generate one
|
# random secret key, use for example `base64 /dev/urandom | head -c50` to generate one
|
||||||
# ---------------------------- AT LEAST ONE REQUIRED -------------------------
|
|
||||||
SECRET_KEY=
|
SECRET_KEY=
|
||||||
SECRET_KEY_FILE=
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
|
|
||||||
# your default timezone See https://timezonedb.com/time-zones for a list of timezones
|
|
||||||
TZ=Europe/Berlin
|
|
||||||
|
|
||||||
# add only a database password if you want to run with the default postgres, otherwise change settings accordingly
|
# add only a database password if you want to run with the default postgres, otherwise change settings accordingly
|
||||||
DB_ENGINE=django.db.backends.postgresql
|
DB_ENGINE=django.db.backends.postgresql
|
||||||
# DB_OPTIONS= {} # e.g. {"sslmode":"require"} to enable ssl
|
|
||||||
POSTGRES_HOST=db_recipes
|
POSTGRES_HOST=db_recipes
|
||||||
|
POSTGRES_DB=djangodb
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_USER=djangouser
|
POSTGRES_USER=djangouser
|
||||||
# ---------------------------- AT LEAST ONE REQUIRED -------------------------
|
|
||||||
POSTGRES_PASSWORD=
|
POSTGRES_PASSWORD=
|
||||||
POSTGRES_PASSWORD_FILE=
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
POSTGRES_DB=djangodb
|
|
||||||
|
|
||||||
# database connection string, when used overrides other database settings.
|
|
||||||
# format might vary depending on backend
|
|
||||||
# DATABASE_URL = engine://username:password@host:port/dbname
|
|
||||||
|
|
||||||
# the default value for the user preference 'fractions' (enable/disable fraction support)
|
|
||||||
# default: disabled=0
|
|
||||||
FRACTION_PREF_DEFAULT=0
|
|
||||||
|
|
||||||
# the default value for the user preference 'comments' (enable/disable commenting system)
|
|
||||||
# default comments enabled=1
|
|
||||||
COMMENT_PREF_DEFAULT=1
|
|
||||||
|
|
||||||
# Users can set a amount of time after which the shopping list is refreshed when they are in viewing mode
|
|
||||||
# This is the minimum interval users can set. Setting this to low will allow users to refresh very frequently which
|
|
||||||
# might cause high load on the server. (Technically they can obviously refresh as often as they want with their own scripts)
|
|
||||||
SHOPPING_MIN_AUTOSYNC_INTERVAL=5
|
|
||||||
|
|
||||||
# Default for user setting sticky navbar
|
|
||||||
# STICKY_NAV_PREF_DEFAULT=1
|
|
||||||
|
|
||||||
# If base URL is something other than just / (you are serving a subfolder in your proxy for instance http://recipe_app/recipes/)
|
|
||||||
# Be sure to not have a trailing slash: e.g. '/recipes' instead of '/recipes/'
|
|
||||||
# SCRIPT_NAME=/recipes
|
|
||||||
|
|
||||||
# If staticfiles are stored at a different location uncomment and change accordingly, MUST END IN /
|
|
||||||
# this is not required if you are just using a subfolder
|
|
||||||
# This can either be a relative path from the applications base path or the url of an external host
|
|
||||||
# STATIC_URL=/static/
|
|
||||||
|
|
||||||
# If mediafiles are stored at a different location uncomment and change accordingly, MUST END IN /
|
|
||||||
# this is not required if you are just using a subfolder
|
|
||||||
# This can either be a relative path from the applications base path or the url of an external host
|
|
||||||
# MEDIA_URL=/media/
|
|
||||||
|
|
||||||
# Serve mediafiles directly using gunicorn. Basically everyone recommends not doing this. Please use any of the examples
|
|
||||||
# provided that include an additional nxginx container to handle media file serving.
|
|
||||||
# If you know what you are doing turn this back on (1) to serve media files using djangos serve() method.
|
|
||||||
# when unset: 1 (true) - this is temporary until an appropriate amount of time has passed for everyone to migrate
|
|
||||||
GUNICORN_MEDIA=0
|
|
||||||
|
|
||||||
# GUNICORN SERVER RELATED SETTINGS (see https://docs.gunicorn.org/en/stable/design.html#how-many-workers for recommended settings)
|
|
||||||
# GUNICORN_WORKERS=1
|
|
||||||
# GUNICORN_THREADS=1
|
|
||||||
|
|
||||||
# S3 Media settings: store mediafiles in s3 or any compatible storage backend (e.g. minio)
|
|
||||||
# as long as S3_ACCESS_KEY is not set S3 features are disabled
|
|
||||||
# S3_ACCESS_KEY=
|
|
||||||
# S3_SECRET_ACCESS_KEY=
|
|
||||||
# S3_BUCKET_NAME=
|
|
||||||
# S3_REGION_NAME= # default none, set your region might be required
|
|
||||||
# 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)
|
|
||||||
# EMAIL_HOST=
|
|
||||||
# EMAIL_PORT=
|
|
||||||
# EMAIL_HOST_USER=
|
|
||||||
# EMAIL_HOST_PASSWORD=
|
|
||||||
# EMAIL_USE_TLS=0
|
|
||||||
# EMAIL_USE_SSL=0
|
|
||||||
# email sender address (default 'webmaster@localhost')
|
|
||||||
# DEFAULT_FROM_EMAIL=
|
|
||||||
# prefix used for account related emails (default "[Tandoor Recipes] ")
|
|
||||||
# ACCOUNT_EMAIL_SUBJECT_PREFIX=
|
|
||||||
|
|
||||||
# allow authentication via the REMOTE-USER header (can be used for e.g. authelia).
|
|
||||||
# ATTENTION: Leave off if you don't know what you are doing! Enabling this without proper configuration will enable anybody
|
|
||||||
# to login with any username!
|
|
||||||
# See docs for additional information: https://docs.tandoor.dev/features/authentication/#reverse-proxy-authentication
|
|
||||||
# when unset: 0 (false)
|
|
||||||
REMOTE_USER_AUTH=0
|
|
||||||
|
|
||||||
# Default settings for spaces, apply per space and can be changed in the admin view
|
|
||||||
# SPACE_DEFAULT_MAX_RECIPES=0 # 0=unlimited recipes
|
|
||||||
# SPACE_DEFAULT_MAX_USERS=0 # 0=unlimited users per space
|
|
||||||
# SPACE_DEFAULT_MAX_FILES=0 # Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.
|
|
||||||
# SPACE_DEFAULT_ALLOW_SHARING=1 # Allow users to share recipes with public links
|
|
||||||
|
|
||||||
# allow people to create local accounts on your application instance (without an invite link)
|
|
||||||
# social accounts will always be able to sign up
|
|
||||||
# when unset: 0 (false)
|
|
||||||
# ENABLE_SIGNUP=0
|
|
||||||
|
|
||||||
# If signup is enabled you might want to add a captcha to it to prevent spam
|
|
||||||
# HCAPTCHA_SITEKEY=
|
|
||||||
# HCAPTCHA_SECRET=
|
|
||||||
|
|
||||||
# if signup is enabled you might want to provide urls to data protection policies or terms and conditions
|
|
||||||
# TERMS_URL=
|
|
||||||
# PRIVACY_URL=
|
|
||||||
# IMPRINT_URL=
|
|
||||||
|
|
||||||
# enable serving of prometheus metrics under the /metrics path
|
|
||||||
# ATTENTION: view is not secured (as per the prometheus default way) so make sure to secure it
|
|
||||||
# trough your web server (or leave it open of you dont care if the stats are exposed)
|
|
||||||
# ENABLE_METRICS=0
|
|
||||||
|
|
||||||
# allows you to setup OAuth providers
|
|
||||||
# see docs for more information https://docs.tandoor.dev/features/authentication/
|
|
||||||
# SOCIAL_PROVIDERS = allauth.socialaccount.providers.github, allauth.socialaccount.providers.nextcloud,
|
|
||||||
|
|
||||||
# Should a newly created user from a social provider get assigned to the default space and given permission by default ?
|
|
||||||
# ATTENTION: This feature might be deprecated in favor of a space join and public viewing system in the future
|
|
||||||
# default 0 (false), when 1 (true) users will be assigned space and group
|
|
||||||
# SOCIAL_DEFAULT_ACCESS = 1
|
|
||||||
|
|
||||||
# if SOCIAL_DEFAULT_ACCESS is used, which group should be added
|
|
||||||
# SOCIAL_DEFAULT_GROUP=guest
|
|
||||||
|
|
||||||
# Django session cookie settings. Can be changed to allow a single django application to authenticate several applications
|
|
||||||
# when running under the same database
|
|
||||||
# SESSION_COOKIE_DOMAIN=.example.com
|
|
||||||
# SESSION_COOKIE_NAME=sessionid # use this only to not interfere with non unified django applications under the same top level domain
|
|
||||||
|
|
||||||
# by default SORT_TREE_BY_NAME is disabled this will store all Keywords and Food in the order they are created
|
|
||||||
# enabling this setting makes saving new keywords and foods very slow, which doesn't matter in most usecases.
|
|
||||||
# however, when doing large imports of recipes that will create new objects, can increase total run time by 10-15x
|
|
||||||
# Keywords and Food can be manually sorted by name in Admin
|
|
||||||
# This value can also be temporarily changed in Admin, it will revert the next time the application is started
|
|
||||||
# This will be fixed/changed in the future by changing the implementation or finding a better workaround for sorting
|
|
||||||
# SORT_TREE_BY_NAME=0
|
|
||||||
# LDAP authentication
|
|
||||||
# default 0 (false), when 1 (true) list of allowed users will be fetched from LDAP server
|
|
||||||
#LDAP_AUTH=
|
|
||||||
#AUTH_LDAP_SERVER_URI=
|
|
||||||
#AUTH_LDAP_BIND_DN=
|
|
||||||
#AUTH_LDAP_BIND_PASSWORD=
|
|
||||||
#AUTH_LDAP_USER_SEARCH_BASE_DN=
|
|
||||||
#AUTH_LDAP_TLS_CACERTFILE=
|
|
||||||
#AUTH_LDAP_START_TLS=
|
|
||||||
|
|
||||||
# Enables exporting PDF (see export docs)
|
|
||||||
# Disabled by default, uncomment to enable
|
|
||||||
# ENABLE_PDF_EXPORT=1
|
|
||||||
|
|
||||||
# Recipe exports are cached for a certain time by default, adjust time if needed
|
|
||||||
# EXPORT_FILE_CACHE_DURATION=600
|
|
||||||
|
|
||||||
# if you want to do many requests to the FDC API you need to get a (free) API key. Demo key is limited to 30 requests / hour or 50 requests / day
|
|
||||||
#FDC_API_KEY=DEMO_KEY
|
|
||||||
|
|
||||||
# API throttle limits
|
|
||||||
# you may use X per second, minute, hour or day
|
|
||||||
# DRF_THROTTLE_RECIPE_URL_IMPORT=60/hour
|
|
@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2024-01-06 15:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cookbook', '0206_rename_sticky_navbar_userpreference_nav_sticky_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='space',
|
||||||
|
name='logo_color_128',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_128', to='cookbook.userfile'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='space',
|
||||||
|
name='logo_color_144',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_144', to='cookbook.userfile'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='space',
|
||||||
|
name='logo_color_180',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_180', to='cookbook.userfile'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='space',
|
||||||
|
name='logo_color_192',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_192', to='cookbook.userfile'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='space',
|
||||||
|
name='logo_color_32',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_32', to='cookbook.userfile'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='space',
|
||||||
|
name='logo_color_512',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_512', to='cookbook.userfile'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='space',
|
||||||
|
name='logo_color_svg',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='space_logo_color_svg', to='cookbook.userfile'),
|
||||||
|
),
|
||||||
|
]
|
@ -289,6 +289,14 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
|
|||||||
nav_bg_color = models.CharField(max_length=8, default='', blank=True, )
|
nav_bg_color = models.CharField(max_length=8, default='', blank=True, )
|
||||||
nav_text_color = models.CharField(max_length=16, choices=NAV_TEXT_COLORS, default=BLANK)
|
nav_text_color = models.CharField(max_length=16, choices=NAV_TEXT_COLORS, default=BLANK)
|
||||||
|
|
||||||
|
logo_color_32 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_32')
|
||||||
|
logo_color_128 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_128')
|
||||||
|
logo_color_144 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_144')
|
||||||
|
logo_color_180 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_180')
|
||||||
|
logo_color_192 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_192')
|
||||||
|
logo_color_512 = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_512')
|
||||||
|
logo_color_svg = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_logo_color_svg')
|
||||||
|
|
||||||
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)
|
||||||
@ -1346,6 +1354,9 @@ class UserFile(ExportModelOperationsMixin('user_files'), models.Model, Permissio
|
|||||||
self.file_size_kb = round(self.file.size / 1000)
|
self.file_size_kb = round(self.file.size / 1000)
|
||||||
super(UserFile, self).save(*args, **kwargs)
|
super(UserFile, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.name} (#{self.id})'
|
||||||
|
|
||||||
|
|
||||||
class Automation(ExportModelOperationsMixin('automations'), models.Model, PermissionModelMixin):
|
class Automation(ExportModelOperationsMixin('automations'), models.Model, PermissionModelMixin):
|
||||||
FOOD_ALIAS = 'FOOD_ALIAS'
|
FOOD_ALIAS = 'FOOD_ALIAS'
|
||||||
|
@ -283,6 +283,13 @@ class SpaceSerializer(WritableNestedModelSerializer):
|
|||||||
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)
|
nav_logo = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||||
custom_space_theme = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
custom_space_theme = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||||
|
logo_color_32 = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||||
|
logo_color_128 = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||||
|
logo_color_144 = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||||
|
logo_color_180 = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||||
|
logo_color_192 = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||||
|
logo_color_512 = UserFileViewSerializer(required=False, many=False, allow_null=True)
|
||||||
|
logo_color_svg = 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()
|
||||||
@ -304,7 +311,8 @@ 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', 'nav_logo', 'space_theme', 'custom_space_theme', 'nav_bg_color', 'nav_text_color', 'use_plural',)
|
'image', 'nav_logo', 'space_theme', 'custom_space_theme', 'nav_bg_color', 'nav_text_color', 'use_plural',
|
||||||
|
'logo_color_32', 'logo_color_128', 'logo_color_144', 'logo_color_180', 'logo_color_192', 'logo_color_512', 'logo_color_svg',)
|
||||||
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',)
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,41 +0,0 @@
|
|||||||
<svg width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g id="Logo" transform="matrix(0.637323,0,0,0.637323,-243.095,-716.725)">
|
|
||||||
<g id="Kreis" transform="matrix(1.44936,0,0,1.50279,387.258,1039.34)">
|
|
||||||
<ellipse cx="273.123" cy="324.015" rx="259.822" ry="250.584" style="fill:url(#_Linear1);"/>
|
|
||||||
<clipPath id="_clip2">
|
|
||||||
<ellipse cx="273.123" cy="324.015" rx="259.822" ry="250.584"/>
|
|
||||||
</clipPath>
|
|
||||||
<g clip-path="url(#_clip2)">
|
|
||||||
<g id="Shadow" transform="matrix(1.10322,0,0,1.064,-5.58287,50.5786)">
|
|
||||||
<path d="M156.285,427.208L389.554,660.477L668.803,495.551L374.012,200.761L156.285,427.208Z" style="fill:rgb(22,22,22);"/>
|
|
||||||
<g transform="matrix(1,0,0,1,-4.22105,0.775864)">
|
|
||||||
<path d="M208.628,178.613L485.935,455.919L590.027,364.63L296.923,71.526L294.175,138.989L208.628,178.613Z" style="fill:rgb(22,22,22);"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(1,0,0,1,-85.3876,27.8512)">
|
|
||||||
<path d="M310.385,145.641L587.692,422.948L590.392,361.357L297.288,68.253L294.175,138.989L310.385,145.641Z" style="fill:rgb(22,22,22);"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(1.471,0,0,1.471,406.537,1149.69)">
|
|
||||||
<path d="M256.049,220C286.222,219.994 312.656,207.31 329.388,194.134C346.35,180.754 370.899,183.406 384.611,200.1C407.129,227.376 420.598,261.944 420.598,299.53C420.598,361.08 382.604,437.101 329.764,463.706C307.035,475.15 283.466,480.586 256.098,480.599L256.098,480.599L256.049,480.599L256,480.599L256,480.599C228.632,480.586 205.063,475.15 182.334,463.706C129.494,437.101 91.5,361.08 91.5,299.53C91.5,261.944 104.969,227.376 127.487,200.1C141.199,183.406 165.748,180.754 182.71,194.134C199.442,207.31 225.876,219.994 256.049,220Z" style="fill:rgb(255,203,118);"/>
|
|
||||||
</g>
|
|
||||||
<g id="Flame-2" transform="matrix(0.965725,0,0,0.89175,164.497,436.391)">
|
|
||||||
<path d="M604.408,844.314C601.981,840.845 601.962,836.056 604.362,832.565C606.763,829.074 611.005,827.721 614.769,829.246C633.87,836.869 658.833,848.629 678.207,864.452C718.526,897.381 729.55,919.407 738.552,942.091C749.208,968.943 750.785,996.68 748.515,1016.08C742.018,1071.61 700.355,1117.5 641.034,1117.5C581.713,1117.5 534.493,1072.05 533.553,1016.08C532.986,982.372 543.985,955.443 555.988,936.22C558.982,931.437 564.594,929.469 569.609,931.444C574.623,933.419 577.757,938.831 577.215,944.58C575.493,956.716 574.362,969.372 574.932,979.484C576.863,1013.7 597.171,1022.5 618.083,1022.29C640.371,1022.08 662.925,1003.17 654.797,954.895C647.69,912.681 622.362,870.194 604.408,844.314Z" style="fill:rgb(255,111,0);"/>
|
|
||||||
<clipPath id="_clip3">
|
|
||||||
<path d="M604.408,844.314C601.981,840.845 601.962,836.056 604.362,832.565C606.763,829.074 611.005,827.721 614.769,829.246C633.87,836.869 658.833,848.629 678.207,864.452C718.526,897.381 729.55,919.407 738.552,942.091C749.208,968.943 750.785,996.68 748.515,1016.08C742.018,1071.61 700.355,1117.5 641.034,1117.5C581.713,1117.5 534.493,1072.05 533.553,1016.08C532.986,982.372 543.985,955.443 555.988,936.22C558.982,931.437 564.594,929.469 569.609,931.444C574.623,933.419 577.757,938.831 577.215,944.58C575.493,956.716 574.362,969.372 574.932,979.484C576.863,1013.7 597.171,1022.5 618.083,1022.29C640.371,1022.08 662.925,1003.17 654.797,954.895C647.69,912.681 622.362,870.194 604.408,844.314Z"/>
|
|
||||||
</clipPath>
|
|
||||||
<g clip-path="url(#_clip3)">
|
|
||||||
<g transform="matrix(1.28784,-0.270602,0.285942,1.59598,247.349,825.209)">
|
|
||||||
<path d="M255.004,46.957C279.547,58.545 306,85.447 313.307,120.161C325.437,177.791 291.571,193.789 262.496,192.403C215.889,190.181 200.194,153.246 231.326,108.9C250.631,81.401 232.663,36.408 255.004,46.957Z" style="fill:rgb(255,209,0);"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g id="Hut" transform="matrix(1.521,0,0,1.521,393.566,1149.06)">
|
|
||||||
<path d="M228.197,408.524C222.698,408.524 217.813,406.688 214.024,403.619C211.776,401.794 210.92,398.752 211.888,396.024C212.856,393.295 215.437,391.472 218.332,391.472C232.214,391.4 256.112,391.396 256.112,391.396C256.112,391.396 280.009,391.4 293.891,391.472C296.786,391.472 299.367,393.295 300.335,396.024C301.303,398.752 300.447,401.794 298.199,403.619C294.41,406.688 289.526,408.524 284.027,408.524L228.197,408.524ZM217.24,378.877C214.208,378.877 211.3,377.671 209.158,375.525C207.015,373.379 205.814,370.469 205.82,367.436C205.831,361.119 205.842,354.539 205.842,354.539C205.842,350.423 203.097,346.814 199.131,345.714C185.313,341.841 175.2,329.468 175.2,314.823C175.2,297.07 190.059,282.657 208.362,282.657C208.362,282.657 208.362,282.657 208.362,282.657C215.401,282.657 221.675,278.218 224.017,271.581C227.243,262.39 236.411,252.015 256,251.998L256,251.998L256.223,251.998L256.223,251.998C275.812,252.015 284.98,262.39 288.206,271.581C290.549,278.218 296.822,282.657 303.861,282.657C303.861,282.657 303.861,282.657 303.861,282.657C322.164,282.657 337.023,297.07 337.023,314.823C337.023,329.468 326.911,341.841 313.093,345.714C309.127,346.814 306.382,350.423 306.381,354.539C306.381,354.539 306.386,361.127 306.391,367.447C306.394,370.478 305.191,373.385 303.049,375.529C300.907,377.672 298.001,378.877 294.971,378.877C275.615,378.877 236.604,378.877 217.24,378.877Z" style="fill:rgb(22,22,22);"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2e-06,0,0,2e-06,3755.77,81.7179)"><stop offset="0" style="stop-color:rgb(39,39,39);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(108,108,108);stop-opacity:1"/></linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
BIN
cookbook/static/assets/logo_color_128.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
BIN
cookbook/static/assets/logo_color_192.png
Normal file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
@ -3,6 +3,8 @@
|
|||||||
{% load theming_tags %}
|
{% load theming_tags %}
|
||||||
{% load custom_tags %}
|
{% load custom_tags %}
|
||||||
|
|
||||||
|
{% theme_values request as theme_values %}
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>{% block title %}
|
<title>{% block title %}
|
||||||
@ -11,30 +13,25 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<meta name="robots" content="noindex,nofollow"/>
|
<meta name="robots" content="noindex,nofollow"/>
|
||||||
|
|
||||||
|
<link rel="icon" href="{{ theme_values.logo_color_svg }}">
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="{% static 'assets/favicon.svg' %}">
|
<link rel="icon" href="{{ theme_values.logo_color_32 }}" sizes="32x32">
|
||||||
<link rel="shortcut icon" href="{% static 'assets/favicon.svg' %}">
|
<link rel="icon" href="{{ theme_values.logo_color_128 }}" sizes="128x128">
|
||||||
<link rel="icon" type="image/png" href="{% static 'assets/favicon-32x32.png' %}" sizes="32x32">
|
<link rel="icon" href="{{ theme_values.logo_color_192 }}" sizes="192x192">
|
||||||
<link rel="icon" type="image/png" href="{% static 'assets/favicon-16x16.png' %}" sizes="16x16">
|
<link rel="apple-touch-icon" href="{{ theme_values.logo_color_180 }}" sizes="180x180">
|
||||||
|
|
||||||
<link rel="mask-icon" href="{% static 'assets/safari-pinned-tab.svg' %}" color="#161616">
|
|
||||||
<link rel="apple-touch-icon" href="{% static 'assets/apple-touch-icon.png' %}" sizes="180x180">
|
|
||||||
|
|
||||||
<link rel="manifest" crossorigin="use-credentials" href="{% url 'web_manifest' %}">
|
<link rel="manifest" crossorigin="use-credentials" href="{% url 'web_manifest' %}">
|
||||||
<meta name="msapplication-TileColor" content="#ffffff">
|
|
||||||
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
|
|
||||||
|
|
||||||
|
<meta name="msapplication-TileColor" content="{{ theme_values.nav_bg_color }}">
|
||||||
|
<meta name="msapplication-TileImage" content="{{ theme_values.logo_color_144 }}">
|
||||||
|
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161616">
|
<meta name="theme-color" content="{{ theme_values.nav_bg_color }}">
|
||||||
<meta name="msapplication-TileColor" content="#161616">
|
|
||||||
<meta name="theme-color" content="#ffffff">
|
|
||||||
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
||||||
|
|
||||||
<!-- Bootstrap 4 -->
|
<!-- Bootstrap 4 -->
|
||||||
<link id="id_main_css" href="{% theme_url request %}" rel="stylesheet">
|
<link id="id_main_css" href="{{ theme_values.theme }}" rel="stylesheet">
|
||||||
{% if request.user.is_authenticated and request.space.custom_space_theme %}
|
{% if theme_values.custom_theme %}
|
||||||
<link id="id_custom_css" href="{% custom_theme request %}" rel="stylesheet">
|
<link id="id_custom_css" href="{{ theme_values.custom_theme }}" rel="stylesheet">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
@ -79,15 +76,15 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-lg {% nav_text_color request %}"
|
<nav class="navbar navbar-expand-lg {{ theme_values.nav_text_class }}"
|
||||||
id="id_main_nav"
|
id="id_main_nav"
|
||||||
style="{% sticky_nav request %}; background-color: {% nav_bg_color request %}">
|
style="{{ theme_values.sticky_nav }}; background-color: {{ theme_values.nav_bg_color }}">
|
||||||
|
|
||||||
{% if not request.user.userpreference.left_handed %}
|
{% if not request.user.userpreference.left_handed %}
|
||||||
{% if not request.user.is_authenticated or request.user.userpreference.nav_show_logo %}
|
{% 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="{{ theme_values.nav_logo }}" alt="Logo">
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -101,7 +98,7 @@
|
|||||||
{% if not request.user.is_authenticated or request.user.userpreference.nav_show_logo %}
|
{% 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="{{ theme_values.nav_logo }}" alt="Logo">
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -129,7 +129,7 @@
|
|||||||
[](https://github.com/vabene1111/recipes)
|
[](https://github.com/vabene1111/recipes)
|
||||||
[GitHub](https://github.com/vabene1111/recipes)
|
[GitHub](https://github.com/vabene1111/recipes)
|
||||||
|
|
||||||

|

|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
<div style="text-align: center">
|
<div style="text-align: center">
|
||||||
@ -142,7 +142,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<a href="https://github.com/vabene1111/recipes">https://github.com/vabene1111/recipes</a> <br/>
|
<a href="https://github.com/vabene1111/recipes">https://github.com/vabene1111/recipes</a> <br/>
|
||||||
<a href="https://github.com/vabene1111/recipes">GitHub</a> <br/>
|
<a href="https://github.com/vabene1111/recipes">GitHub</a> <br/>
|
||||||
<img src="{% static 'assets/favicon.svg' %}" class="img-fluid" alt="{% trans 'This will become an image' %}"
|
<img src="{% static 'assets/logo_color_svg.svg' %}" class="img-fluid" alt="{% trans 'This will become an image' %}"
|
||||||
style="height: 3vw">
|
style="height: 3vw">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -3,15 +3,24 @@ from django.templatetags.static import static
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from cookbook.models import UserPreference, UserFile, Space
|
from cookbook.models import UserPreference, UserFile, Space
|
||||||
from recipes.settings import STICKY_NAV_PREF_DEFAULT
|
from recipes.settings import STICKY_NAV_PREF_DEFAULT, UNAUTHENTICATED_THEME_FROM_SPACE
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def theme_url(request):
|
def theme_values(request):
|
||||||
if not request.user.is_authenticated:
|
space = None
|
||||||
return static('themes/tandoor.min.css')
|
if request.space:
|
||||||
|
space = request.space
|
||||||
|
if not request.user.is_authenticated and UNAUTHENTICATED_THEME_FROM_SPACE > 0:
|
||||||
|
with scopes_disabled():
|
||||||
|
space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first()
|
||||||
|
|
||||||
|
return get_theming_values(space, request.user)
|
||||||
|
|
||||||
|
|
||||||
|
def get_theming_values(space, user):
|
||||||
themes = {
|
themes = {
|
||||||
UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css',
|
UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css',
|
||||||
UserPreference.FLATLY: 'themes/flatly.min.css',
|
UserPreference.FLATLY: 'themes/flatly.min.css',
|
||||||
@ -20,58 +29,48 @@ 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.space.custom_space_theme:
|
nav_text_type_mapping = {Space.DARK: 'navbar-light',
|
||||||
# return request.space.custom_space_theme.file.url
|
Space.LIGHT: 'navbar-dark'} # inverted since navbar-dark means the background
|
||||||
|
|
||||||
if request.space.space_theme in themes:
|
tv = {
|
||||||
return static(themes[request.space.space_theme])
|
'logo_color_32': static('assets/logo_color_32.png'),
|
||||||
else:
|
'logo_color_128': static('assets/logo_color_128.png'),
|
||||||
if request.user.userpreference.theme in themes:
|
'logo_color_144': static('assets/logo_color_144.png'),
|
||||||
return static(themes[request.user.userpreference.theme])
|
'logo_color_180': static('assets/logo_color_180.png'),
|
||||||
else:
|
'logo_color_192': static('assets/logo_color_192.png'),
|
||||||
raise AttributeError
|
'logo_color_512': static('assets/logo_color_512.png'),
|
||||||
|
'logo_color_svg': static('assets/logo_color_svg.svg'),
|
||||||
|
'custom_theme': None,
|
||||||
|
'theme': static(themes[UserPreference.TANDOOR]),
|
||||||
|
'nav_logo': static('assets/brand_logo.png'),
|
||||||
|
'nav_bg_color': '#ddbf86',
|
||||||
|
'nav_text_class': 'navbar-light',
|
||||||
|
'sticky_nav': 'position: sticky; top: 0; left: 0; z-index: 1000;',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
if user.is_authenticated:
|
||||||
def custom_theme(request):
|
if user.userpreference.theme in themes:
|
||||||
if request.user.is_authenticated and request.space.custom_space_theme:
|
tv['theme'] = static(themes[user.userpreference.theme])
|
||||||
return request.space.custom_space_theme.file.url
|
if user.userpreference.nav_bg_color:
|
||||||
|
tv['nav_bg_color'] = user.userpreference.nav_bg_color
|
||||||
|
if user.userpreference.nav_text_color and user.userpreference.nav_text_color in nav_text_type_mapping:
|
||||||
|
tv['nav_text_class'] = nav_text_type_mapping[user.userpreference.nav_text_color]
|
||||||
|
if not user.userpreference.nav_sticky:
|
||||||
|
tv['sticky_nav'] = ''
|
||||||
|
|
||||||
|
if space:
|
||||||
@register.simple_tag
|
for logo in list(tv.keys()):
|
||||||
def logo_url(request):
|
if logo.startswith('logo_color_') and getattr(space, logo, None):
|
||||||
if request.user.is_authenticated and getattr(getattr(request, "space", {}), 'nav_logo', None):
|
tv[logo] = getattr(space, logo).file.url
|
||||||
return request.space.nav_logo.file.url
|
if space.custom_space_theme:
|
||||||
else:
|
tv['custom_theme'] = space.custom_space_theme.file.url
|
||||||
return static('assets/brand_logo.png')
|
if space.space_theme in themes:
|
||||||
|
tv['theme'] = static(themes[space.space_theme])
|
||||||
|
if space.nav_logo:
|
||||||
@register.simple_tag
|
tv['nav_logo'] = space.nav_logo.file.url
|
||||||
def nav_bg_color(request):
|
if space.nav_bg_color:
|
||||||
if not request.user.is_authenticated:
|
tv['nav_bg_color'] = space.nav_bg_color
|
||||||
return '#ddbf86'
|
if space.nav_text_color and space.nav_text_color in nav_text_type_mapping:
|
||||||
else:
|
tv['nav_text_class'] = nav_text_type_mapping[space.nav_text_color]
|
||||||
if request.space.nav_bg_color:
|
return tv
|
||||||
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
|
|
||||||
def sticky_nav(request):
|
|
||||||
if (not request.user.is_authenticated and STICKY_NAV_PREF_DEFAULT) or (request.user.is_authenticated and request.user.userpreference.nav_sticky):
|
|
||||||
return 'position: sticky; top: 0; left: 0; z-index: 1000;'
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
36
cookbook/tests/other/test_theming.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from django.contrib import auth
|
||||||
|
from django.templatetags.static import static
|
||||||
|
|
||||||
|
from cookbook.models import Space, UserPreference, UserFile
|
||||||
|
from cookbook.templatetags.theming_tags import theme_values, get_theming_values
|
||||||
|
|
||||||
|
|
||||||
|
def test_theming_function(space_1, u1_s1):
|
||||||
|
user = auth.get_user(u1_s1)
|
||||||
|
# uf = UserFile.objects.create(name='test', space=space_1, created_by=user) #TODO add file tests
|
||||||
|
|
||||||
|
assert get_theming_values(space_1, user)['theme'] == static('themes/tandoor.min.css')
|
||||||
|
assert get_theming_values(space_1, user)['nav_bg_color'] == '#ddbf86'
|
||||||
|
assert get_theming_values(space_1, user)['nav_text_class'] == 'navbar-light'
|
||||||
|
assert get_theming_values(space_1, user)['nav_logo'] == static('assets/brand_logo.png')
|
||||||
|
assert get_theming_values(space_1, user)['sticky_nav'] == 'position: sticky; top: 0; left: 0; z-index: 1000;'
|
||||||
|
|
||||||
|
user.userpreference.theme = UserPreference.TANDOOR_DARK
|
||||||
|
user.userpreference.nav_bg_color = '#ffffff'
|
||||||
|
user.userpreference.nav_text_color = UserPreference.LIGHT
|
||||||
|
user.userpreference.nav_sticky = False
|
||||||
|
user.userpreference.save()
|
||||||
|
|
||||||
|
assert get_theming_values(space_1, user)['theme'] == static('themes/tandoor_dark.min.css')
|
||||||
|
assert get_theming_values(space_1, user)['nav_bg_color'] == '#ffffff'
|
||||||
|
assert get_theming_values(space_1, user)['nav_text_class'] == 'navbar-dark'
|
||||||
|
assert get_theming_values(space_1, user)['sticky_nav'] == ''
|
||||||
|
|
||||||
|
space_1.space_theme = Space.BOOTSTRAP
|
||||||
|
space_1.nav_bg_color = '#000000'
|
||||||
|
space_1.nav_text_color = UserPreference.DARK
|
||||||
|
space_1.save()
|
||||||
|
|
||||||
|
assert get_theming_values(space_1, user)['theme'] == static('themes/bootstrap.min.css')
|
||||||
|
assert get_theming_values(space_1, user)['nav_bg_color'] == '#000000'
|
||||||
|
assert get_theming_values(space_1, user)['nav_text_class'] == 'navbar-light'
|
@ -162,8 +162,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('service-worker.js', (TemplateView.as_view(template_name="sw.js", content_type='application/javascript', )),
|
path('service-worker.js', (TemplateView.as_view(template_name="sw.js", content_type='application/javascript', )),
|
||||||
name='service_worker'),
|
name='service_worker'),
|
||||||
path('manifest.json', (TemplateView.as_view(template_name="manifest.json", content_type='application/json', )),
|
path('manifest.json', views.web_manifest, name='web_manifest'),
|
||||||
name='web_manifest'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
generic_models = (
|
generic_models = (
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -14,8 +15,9 @@ from django.contrib.auth.password_validation import validate_password
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect, JsonResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
from django.templatetags.static import static
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@ -335,13 +337,16 @@ def system(request):
|
|||||||
database_message = _('Everything is fine!')
|
database_message = _('Everything is fine!')
|
||||||
elif postgres_ver < postgres_current - 2:
|
elif postgres_ver < postgres_current - 2:
|
||||||
database_status = 'danger'
|
database_status = 'danger'
|
||||||
database_message = _('PostgreSQL %(v)s is deprecated. Upgrade to a fully supported version!') % {'v': postgres_ver}
|
database_message = _('PostgreSQL %(v)s is deprecated. Upgrade to a fully supported version!') % {
|
||||||
|
'v': postgres_ver}
|
||||||
else:
|
else:
|
||||||
database_status = 'info'
|
database_status = 'info'
|
||||||
database_message = _('You are running PostgreSQL %(v1)s. PostgreSQL %(v2)s is recommended') % {'v1': postgres_ver, 'v2': postgres_current}
|
database_message = _('You are running PostgreSQL %(v1)s. PostgreSQL %(v2)s is recommended') % {
|
||||||
|
'v1': postgres_ver, 'v2': postgres_current}
|
||||||
else:
|
else:
|
||||||
database_status = 'info'
|
database_status = 'info'
|
||||||
database_message = _('This application is not running with a Postgres database backend. This is ok but not recommended as some features only work with postgres databases.')
|
database_message = _(
|
||||||
|
'This application is not running with a Postgres database backend. This is ok but not recommended as some features only work with postgres databases.')
|
||||||
|
|
||||||
secret_key = False if os.getenv('SECRET_KEY') else True
|
secret_key = False if os.getenv('SECRET_KEY') else True
|
||||||
|
|
||||||
@ -366,10 +371,12 @@ def system(request):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
current_app = row
|
current_app = row
|
||||||
migration_info[current_app] = {'app': current_app, 'unapplied_migrations': [], 'applied_migrations': [], 'total': 0}
|
migration_info[current_app] = {'app': current_app, 'unapplied_migrations': [], 'applied_migrations': [],
|
||||||
|
'total': 0}
|
||||||
|
|
||||||
for key in migration_info.keys():
|
for key in migration_info.keys():
|
||||||
migration_info[key]['total'] = len(migration_info[key]['unapplied_migrations']) + len(migration_info[key]['applied_migrations'])
|
migration_info[key]['total'] = len(migration_info[key]['unapplied_migrations']) + len(
|
||||||
|
migration_info[key]['applied_migrations'])
|
||||||
|
|
||||||
return render(request, 'system.html', {
|
return render(request, 'system.html', {
|
||||||
'gunicorn_media': settings.GUNICORN_MEDIA,
|
'gunicorn_media': settings.GUNICORN_MEDIA,
|
||||||
@ -431,7 +438,8 @@ def invite_link(request, token):
|
|||||||
link.used_by = request.user
|
link.used_by = request.user
|
||||||
link.save()
|
link.save()
|
||||||
|
|
||||||
user_space = UserSpace.objects.create(user=request.user, space=link.space, internal_note=link.internal_note, invite_link=link, active=False)
|
user_space = UserSpace.objects.create(user=request.user, space=link.space,
|
||||||
|
internal_note=link.internal_note, invite_link=link, active=False)
|
||||||
|
|
||||||
if request.user.userspace_set.count() == 1:
|
if request.user.userspace_set.count() == 1:
|
||||||
user_space.active = True
|
user_space.active = True
|
||||||
@ -472,6 +480,65 @@ def report_share_abuse(request, token):
|
|||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
|
||||||
|
|
||||||
|
def web_manifest(request):
|
||||||
|
icons = [
|
||||||
|
{"src": static("/assets/logo_color.svg"), "sizes": "any"},
|
||||||
|
{"src": static("/assets/logo_color144.png"), "type": "image/png", "sizes": "144x144"},
|
||||||
|
{"src": static("/assets/logo_color512.png"), "type": "image/png", "sizes": "512x512"}
|
||||||
|
]
|
||||||
|
|
||||||
|
if request.user.is_authenticated and getattr(request.space, 'logo_color_svg') and getattr(request.space, 'logo_color_144') and getattr(request.space, 'logo_color_512'):
|
||||||
|
icons = [
|
||||||
|
{"src": request.space.logo_color_svg.file.url, "sizes": "any"},
|
||||||
|
{"src": request.space.logo_color_144.file.url, "type": "image/png", "sizes": "144x144"},
|
||||||
|
{"src": request.space.logo_color_512.file.url, "type": "image/png", "sizes": "512x512"}
|
||||||
|
]
|
||||||
|
|
||||||
|
manifest_info = {
|
||||||
|
"name": "Tandoor Recipes",
|
||||||
|
"short_name": "Tandoor",
|
||||||
|
"description": _("Manage recipes, shopping list, meal plans and more."),
|
||||||
|
"icons": icons,
|
||||||
|
"start_url": "./search",
|
||||||
|
"background_color": "#ffcb76",
|
||||||
|
"display": "standalone",
|
||||||
|
"scope": ".",
|
||||||
|
"theme_color": "#ffcb76",
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": _("Plan"),
|
||||||
|
"short_name": _("Plan"),
|
||||||
|
"description": _("View your meal Plan"),
|
||||||
|
"url": "./plan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": _("Books"),
|
||||||
|
"short_name": _("Books"),
|
||||||
|
"description": _("View your cookbooks"),
|
||||||
|
"url": "./books"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": _("Shopping"),
|
||||||
|
"short_name": _("Shopping"),
|
||||||
|
"description": _("View your shopping lists"),
|
||||||
|
"url": "./list/shopping-list/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"share_target": {
|
||||||
|
"action": "/data/import/url",
|
||||||
|
"method": "GET",
|
||||||
|
"params": {
|
||||||
|
"title": "title",
|
||||||
|
"url": "url",
|
||||||
|
"text": "text"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonResponse(manifest_info, json_dumps_params={'indent': 4})
|
||||||
|
|
||||||
|
|
||||||
def markdown_info(request):
|
def markdown_info(request):
|
||||||
return render(request, 'markdown_info.html', {})
|
return render(request, 'markdown_info.html', {})
|
||||||
|
|
||||||
|
599
docs/system/configuration.md
Normal file
@ -0,0 +1,599 @@
|
|||||||
|
This page describes all configuration options for the application
|
||||||
|
server. All settings must be configured in the environment of the
|
||||||
|
application server, usually by adding them to the `.env` file.
|
||||||
|
|
||||||
|
## Required Settings
|
||||||
|
|
||||||
|
The following settings need to be set appropriately for your installation.
|
||||||
|
They are included in the default `env.template`.
|
||||||
|
|
||||||
|
### Secret Key
|
||||||
|
|
||||||
|
Random secret key (at least 50 characters), use for example `base64 /dev/urandom | head -c50` to generate one.
|
||||||
|
It is used internally by django for various signing/cryptographic operations and **should be kept secret**.
|
||||||
|
See [Django Docs](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-SECRET_KEY)
|
||||||
|
|
||||||
|
```
|
||||||
|
SECRET_KEY=#$tp%v6*(*ba01wcz(ip(i5vfz8z$f%qdio&q@anr1#$=%(m4c
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively you can point to a file containing just the secret key value. If using containers make sure the file is
|
||||||
|
persistent and available inside the container.
|
||||||
|
|
||||||
|
```
|
||||||
|
SECRET_KEY_FILE=/path/to/file.txt
|
||||||
|
|
||||||
|
// contents of file
|
||||||
|
#$tp%v6*(*ba01wcz(ip(i5vfz8z$f%qdio&q@anr1#$=%(m4c
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
Multiple parameters are required to configure the database.
|
||||||
|
|
||||||
|
| Var | Options | Description |
|
||||||
|
|-------------------|--------------------------------------------------------------------|-------------------------------------------------------------------------|
|
||||||
|
| DB_ENGINE | django.db.backends.postgresql (default) django.db.backends.sqlite3 | Type of database connection. Production should always use postgresql. |
|
||||||
|
| POSTGRES_HOST | any | Used to connect to database server. Use container name in docker setup. |
|
||||||
|
| POSTGRES_DB | any | Name of database. |
|
||||||
|
| POSTGRES_PORT | 1-65535 | Port of database, Postgresql default `5432` |
|
||||||
|
| POSTGRES_USER | any | Username for database connection. |
|
||||||
|
| POSTGRES_PASSWORD | any | Password for database connection. |
|
||||||
|
|
||||||
|
#### Password file
|
||||||
|
|
||||||
|
> default `None` - options: file path
|
||||||
|
|
||||||
|
Path to file containing the database password. Overrides `POSTGRES_PASSWORD`. Only applied when using Docker (or other
|
||||||
|
setups running `boot.sh`)
|
||||||
|
|
||||||
|
```
|
||||||
|
POSTGRES_PASSWORD_FILE=
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Connection String
|
||||||
|
|
||||||
|
> default `None` - options: according to database specifications
|
||||||
|
|
||||||
|
Instead of configuring the connection using multiple individual environment parameters, you can use a connection string.
|
||||||
|
The connection string will override all other database settings.
|
||||||
|
|
||||||
|
```
|
||||||
|
DATABASE_URL = engine://username:password@host:port/dbname
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Connection Options
|
||||||
|
|
||||||
|
> default `{}` - options: according to database specifications
|
||||||
|
|
||||||
|
Additional connection options can be set as shown in the example below.
|
||||||
|
|
||||||
|
```
|
||||||
|
DB_OPTIONS={"sslmode":"require"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional Settings
|
||||||
|
|
||||||
|
All optional settings are, as their name says, optional and can be ignored safely. If you want to know more about what
|
||||||
|
you can do with them take a look through this page. I recommend using the categories to guide yourself.
|
||||||
|
|
||||||
|
### Server configuration
|
||||||
|
|
||||||
|
Configuration options for serving related services.
|
||||||
|
|
||||||
|
#### Port
|
||||||
|
|
||||||
|
> default `8080` - options: `1-65535`
|
||||||
|
|
||||||
|
Port for gunicorn to bind to. Should not be changed if using docker stack with reverse proxy.
|
||||||
|
|
||||||
|
```
|
||||||
|
TANDOOR_PORT=8080
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Allowed Hosts
|
||||||
|
|
||||||
|
> default `*` - options: `recipes.mydomain.com,cooking.mydomain.com,...` (comma seperated domain/ip list)
|
||||||
|
|
||||||
|
Security setting to prevent HTTP Host Header Attacks,
|
||||||
|
see [Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#allowed-hosts).
|
||||||
|
Many reverse proxies handle this and require the setting to be `*` (default).
|
||||||
|
|
||||||
|
```
|
||||||
|
ALLOWED_HOSTS=recipes.mydomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
#### URL Path
|
||||||
|
|
||||||
|
> default `None` - options: `/custom/url/base/path`
|
||||||
|
|
||||||
|
If base URL is something other than just / (you are serving a subfolder in your proxy for
|
||||||
|
instance http://recipe_app/recipes/)
|
||||||
|
Be sure to not have a trailing slash: e.g. '/recipes' instead of '/recipes/'
|
||||||
|
|
||||||
|
```
|
||||||
|
SCRIPT_NAME=/recipes
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Static URL
|
||||||
|
|
||||||
|
> default `/static/` - options: `/any/url/path/`, `https://any.domain.name/and/url/path`
|
||||||
|
|
||||||
|
If staticfiles are stored or served from a different location uncomment and change accordingly.
|
||||||
|
This can either be a relative path from the applications base path or the url of an external host.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
- MUST END IN `/`
|
||||||
|
- This is not required if you are just using a subfolder
|
||||||
|
|
||||||
|
```
|
||||||
|
STATIC_URL=/static/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Media URL
|
||||||
|
|
||||||
|
> default `/static/` - options: `/any/url/path/`, `https://any.domain.name/and/url/path`
|
||||||
|
|
||||||
|
If mediafiles are stored at a different location uncomment and change accordingly.
|
||||||
|
This can either be a relative path from the applications base path or the url of an external host
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
- MUST END IN `/`
|
||||||
|
- This is **not required** if you are just using a subfolder
|
||||||
|
- This is **not required** if using S3/object storage
|
||||||
|
|
||||||
|
```
|
||||||
|
MEDIA_URL=/media/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Gunicorn Workers
|
||||||
|
|
||||||
|
> default `3` - options `1-X`
|
||||||
|
|
||||||
|
Set the number of gunicorn workers to start when starting using `boot.sh` (all container installations).
|
||||||
|
The default is likely appropriate for most installations.
|
||||||
|
See [Gunicorn docs](https://docs.gunicorn.org/en/stable/design.html#how-many-workers) for recommended settings.
|
||||||
|
|
||||||
|
```
|
||||||
|
GUNICORN_WORKERS=3
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Gunicorn Threads
|
||||||
|
|
||||||
|
> default `2` - options `1-X`
|
||||||
|
|
||||||
|
Set the number of gunicorn threads to start when starting using `boot.sh` (all container installations).
|
||||||
|
The default is likely appropriate for most installations.
|
||||||
|
See [Gunicorn docs](https://docs.gunicorn.org/en/stable/design.html#how-many-workers) for recommended settings.
|
||||||
|
|
||||||
|
```
|
||||||
|
GUNICORN_THREADS=2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Gunicorn Media
|
||||||
|
|
||||||
|
> default `0` - options `0`, `1`
|
||||||
|
|
||||||
|
Serve media files directly using gunicorn. Basically everyone recommends not doing this. Please use any of the examples
|
||||||
|
provided that include an additional nxginx container to handle media file serving.
|
||||||
|
If you know what you are doing turn this on (`1`) to serve media files using djangos serve() method.
|
||||||
|
|
||||||
|
```
|
||||||
|
GUNICORN_MEDIA=0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CSRF Trusted Origins
|
||||||
|
|
||||||
|
> default `[]` - options: [list,of,trusted,origins]
|
||||||
|
|
||||||
|
Allows setting origins to allow for unsafe requests.
|
||||||
|
See [Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#csrf-trusted-origins)
|
||||||
|
|
||||||
|
```
|
||||||
|
CSRF_TRUSTED_ORIGINS = []
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Cors origins
|
||||||
|
|
||||||
|
> default `False` - options: `False`, `True`
|
||||||
|
|
||||||
|
By default, cross-origin resource sharing is disabled. Enabling this will allow access to your resources from other
|
||||||
|
domains.
|
||||||
|
Please read [the docs](https://github.com/adamchainz/django-cors-headers) carefully before enabling this.
|
||||||
|
|
||||||
|
```
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = True
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Session Cookies
|
||||||
|
|
||||||
|
Django session cookie settings. Can be changed to allow a single django application to authenticate several applications
|
||||||
|
when running under the same database.
|
||||||
|
|
||||||
|
```
|
||||||
|
SESSION_COOKIE_DOMAIN=.example.com
|
||||||
|
SESSION_COOKIE_NAME=sessionid # use this only to not interfere with non unified django applications under the same top level domain
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
Some features can be enabled/disabled on a server level because they might change the user experience significantly,
|
||||||
|
they might be unstable/beta or they have performance/security implications.
|
||||||
|
|
||||||
|
#### Captcha
|
||||||
|
|
||||||
|
If you allow signing up to your instance you might want to use a captcha to prevent spam.
|
||||||
|
Tandoor supports HCAPTCHA which is supposed to be a privacy-friendly captcha provider.
|
||||||
|
See [HCAPTCHA website](https://www.hcaptcha.com/) for more information and to acquire your sitekey and secret.
|
||||||
|
|
||||||
|
```
|
||||||
|
HCAPTCHA_SITEKEY=
|
||||||
|
HCAPTCHA_SECRET=
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Metrics
|
||||||
|
|
||||||
|
Enable serving of prometheus metrics under the `/metrics` path
|
||||||
|
|
||||||
|
!!! danger
|
||||||
|
The view is not secured (as per the prometheus default way) so make sure to secure it
|
||||||
|
through your web server.
|
||||||
|
|
||||||
|
```
|
||||||
|
ENABLE_METRICS=0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tree Sorting
|
||||||
|
|
||||||
|
> default `0` - options `0`, `1`
|
||||||
|
|
||||||
|
By default SORT_TREE_BY_NAME is disabled this will store all Keywords and Food in the order they are created.
|
||||||
|
Enabling this setting makes saving new keywords and foods very slow, which doesn't matter in most usecases.
|
||||||
|
However, when doing large imports of recipes that will create new objects, can increase total run time by 10-15x
|
||||||
|
Keywords and Food can be manually sorted by name in Admin
|
||||||
|
This value can also be temporarily changed in Admin, it will revert the next time the application is started
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Disabling tree sorting is a temporary fix, in the future we might find a better implementation to allow tree sorting
|
||||||
|
without the large performance impacts.
|
||||||
|
|
||||||
|
```
|
||||||
|
SORT_TREE_BY_NAME=0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PDF Export
|
||||||
|
|
||||||
|
> default `0` - options `0`, `1`
|
||||||
|
|
||||||
|
Exporting PDF's is a community contributed feature to export recipes as PDF files. This requires the server to download
|
||||||
|
a chromium binary and is generally implemented only rudimentary and somewhat slow depending on your server device.
|
||||||
|
|
||||||
|
See [Export feature docs](https://docs.tandoor.dev/features/import_export/#pdf) for additional information.
|
||||||
|
|
||||||
|
```
|
||||||
|
ENABLE_PDF_EXPORT=1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Legal URLS
|
||||||
|
|
||||||
|
Depending on your jurisdiction you might need to provide any of the following URLs for your instance.
|
||||||
|
|
||||||
|
```
|
||||||
|
TERMS_URL=
|
||||||
|
PRIVACY_URL=
|
||||||
|
IMPRINT_URL=
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
All configurable variables regarding authentication.
|
||||||
|
Please also visit the [dedicated docs page](https://docs.tandoor.dev/features/authentication/) for more information.
|
||||||
|
|
||||||
|
#### Default Permissions
|
||||||
|
|
||||||
|
Configures if a newly created user (from social auth or public signup) should automatically join into the given space and
|
||||||
|
default group.
|
||||||
|
|
||||||
|
This setting is targeted at private, single space instances that typically have a custom authentication system managing
|
||||||
|
access to the data.
|
||||||
|
|
||||||
|
!!! danger
|
||||||
|
With public signup enabled this will give everyone access to the data in the given space
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
This feature might be deprecated in favor of a space join and public viewing system in the future
|
||||||
|
|
||||||
|
> default `0` (disabled) - options `0`, `1-X` (space id)
|
||||||
|
|
||||||
|
When enabled will join user into space and apply group configured in `SOCIAL_DEFAULT_GROUP`.
|
||||||
|
|
||||||
|
```
|
||||||
|
SOCIAL_DEFAULT_ACCESS = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
> default `guest` - options `guest`, `user`, `admin`
|
||||||
|
|
||||||
|
```
|
||||||
|
SOCIAL_DEFAULT_GROUP=guest
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Enable Signup
|
||||||
|
|
||||||
|
> default `0` - options `0`, `1`
|
||||||
|
|
||||||
|
Allow everyone to create local accounts on your application instance (without an invite link)
|
||||||
|
You might want to setup HCAPTCHA to prevent bots from creating accounts/spam.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Social accounts will always be able to sign up, if providers are configured
|
||||||
|
|
||||||
|
```
|
||||||
|
ENABLE_SIGNUP=0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Social Auth
|
||||||
|
|
||||||
|
Allows you to set up external OAuth providers.
|
||||||
|
|
||||||
|
```
|
||||||
|
SOCIAL_PROVIDERS = allauth.socialaccount.providers.github, allauth.socialaccount.providers.nextcloud,
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Remote User Auth
|
||||||
|
> default `0` - options `0`, `1`
|
||||||
|
|
||||||
|
Allow authentication via the REMOTE-USER header (can be used for e.g. authelia).
|
||||||
|
|
||||||
|
!!! danger
|
||||||
|
Leave off if you don't know what you are doing! Enabling this without proper configuration will enable anybody
|
||||||
|
to login with any username!
|
||||||
|
|
||||||
|
```
|
||||||
|
REMOTE_USER_AUTH=0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LDAP
|
||||||
|
|
||||||
|
LDAP based authentication is disabled by default. You can enable it by setting `LDAP_AUTH` to `1` and configuring the
|
||||||
|
other
|
||||||
|
settings accordingly. Please remove/comment settings you do not need for your setup.
|
||||||
|
|
||||||
|
```
|
||||||
|
LDAP_AUTH=
|
||||||
|
AUTH_LDAP_SERVER_URI=
|
||||||
|
AUTH_LDAP_BIND_DN=
|
||||||
|
AUTH_LDAP_BIND_PASSWORD=
|
||||||
|
AUTH_LDAP_USER_SEARCH_BASE_DN=
|
||||||
|
AUTH_LDAP_TLS_CACERTFILE=
|
||||||
|
AUTH_LDAP_START_TLS=
|
||||||
|
```
|
||||||
|
|
||||||
|
### External Services
|
||||||
|
|
||||||
|
#### Email
|
||||||
|
|
||||||
|
Email Settings, see [Django docs](https://docs.djangoproject.com/en/3.2/ref/settings/#email-host) for additional
|
||||||
|
information.
|
||||||
|
Required for email confirmation and password reset (automatically activates if host is set).
|
||||||
|
|
||||||
|
```
|
||||||
|
EMAIL_HOST=
|
||||||
|
EMAIL_PORT=
|
||||||
|
EMAIL_HOST_USER=
|
||||||
|
EMAIL_HOST_PASSWORD=
|
||||||
|
EMAIL_USE_TLS=0
|
||||||
|
EMAIL_USE_SSL=0
|
||||||
|
# email sender address (default 'webmaster@localhost')
|
||||||
|
DEFAULT_FROM_EMAIL=
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional settings (only copy the ones you need)
|
||||||
|
|
||||||
|
```
|
||||||
|
# prefix used for account related emails (default "[Tandoor Recipes] ")
|
||||||
|
ACCOUNT_EMAIL_SUBJECT_PREFIX=
|
||||||
|
```
|
||||||
|
|
||||||
|
#### S3 Object storage
|
||||||
|
|
||||||
|
If you want to store your users media files using an external storage provider supporting the S3 API's (Like S3,
|
||||||
|
MinIO, ...)
|
||||||
|
configure the following settings accordingly.
|
||||||
|
As long as `S3_ACCESS_KEY` is not set, all object storage related settings are disabled.
|
||||||
|
|
||||||
|
See also [Django Storages Docs](https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html) for additional
|
||||||
|
information.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Settings are only named S3 but apply to all compatible object storage providers.
|
||||||
|
|
||||||
|
Required settings
|
||||||
|
|
||||||
|
```
|
||||||
|
S3_ACCESS_KEY=
|
||||||
|
S3_SECRET_ACCESS_KEY=
|
||||||
|
S3_BUCKET_NAME=
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional settings (only copy the ones you need)
|
||||||
|
|
||||||
|
```
|
||||||
|
S3_REGION_NAME= # default none, set your region might be required
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### FDC Api
|
||||||
|
|
||||||
|
The FDC Api is used to automatically load nutrition information from
|
||||||
|
the [FDC Nutrition Database](https://fdc.nal.usda.gov/fdc-app.html#/).
|
||||||
|
The default `DEMO_KEY` is limited to 30 requests / hour or 50 requests / day.
|
||||||
|
If you want to do many requests to the FDC API you need to get a (free) API
|
||||||
|
key [here](https://fdc.nal.usda.gov/api-key-signup.html).
|
||||||
|
|
||||||
|
```
|
||||||
|
FDC_API_KEY=DEMO_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging/Development settings
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
These settings should not be left on in production as they might provide additional attack surfaces and
|
||||||
|
information to adversaries.
|
||||||
|
|
||||||
|
#### Debug
|
||||||
|
|
||||||
|
> default `0` - options: `0`, `1`
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Please enable this before posting logs anywhere to ask for help.
|
||||||
|
|
||||||
|
Setting to `1` enables several django debug features and additional
|
||||||
|
logs ([see docs](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-DEBUG)).
|
||||||
|
|
||||||
|
```
|
||||||
|
DEBUG=0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Debug Toolbar
|
||||||
|
|
||||||
|
> default `0` - options: `0`, `1`
|
||||||
|
|
||||||
|
Set to `1` to enable django debug toolbar middleware. Toolbar only shows if `DEBUG=1` is set and the requesting IP
|
||||||
|
is in `INTERNAL_IPS`.
|
||||||
|
See [Django Debug Toolbar Docs](https://django-debug-toolbar.readthedocs.io/en/latest/).
|
||||||
|
|
||||||
|
```
|
||||||
|
DEBUG_TOOLBAR=0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SQL Debug
|
||||||
|
|
||||||
|
> default `0` - options: `0`, `1`
|
||||||
|
|
||||||
|
Set to `1` to enable additional query output on the search page.
|
||||||
|
|
||||||
|
```
|
||||||
|
SQL_DEBUG=0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Gunicorn Log Level
|
||||||
|
|
||||||
|
> default `info` - options: [see Gunicorn Docs](https://docs.gunicorn.org/en/stable/settings.html#loglevel)
|
||||||
|
|
||||||
|
Increase or decrease the logging done by gunicorn (the python wsgi application).
|
||||||
|
|
||||||
|
```
|
||||||
|
GUNICORN_LOG_LEVEL="debug"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Default User Preferences
|
||||||
|
|
||||||
|
Having default user preferences is nice so that users signing up to your instance already have the settings you deem
|
||||||
|
appropriate.
|
||||||
|
|
||||||
|
#### Fractions
|
||||||
|
|
||||||
|
> default `0` - options: `0`,`1`
|
||||||
|
|
||||||
|
The default value for the user preference 'fractions' (showing amounts as decimals or fractions).
|
||||||
|
|
||||||
|
```
|
||||||
|
FRACTION_PREF_DEFAULT=0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Comments
|
||||||
|
|
||||||
|
> default `1` - options: `0`,`1`
|
||||||
|
|
||||||
|
The default value for the user preference 'comments' (enable/disable commenting system)
|
||||||
|
|
||||||
|
```
|
||||||
|
COMMENT_PREF_DEFAULT=1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sticky Navigation
|
||||||
|
|
||||||
|
> default `1` - options: `0`,`1`
|
||||||
|
|
||||||
|
The default value for the user preference 'sticky navigation' (always show navbar on top or hide when scrolling)
|
||||||
|
|
||||||
|
```
|
||||||
|
STICKY_NAV_PREF_DEFAULT=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cosmetic / Preferences
|
||||||
|
|
||||||
|
#### Timezone
|
||||||
|
|
||||||
|
> default `Europe/Berlin` - options: [see timezone DB](https://timezonedb.com/time-zones)
|
||||||
|
|
||||||
|
Default timezone to use for database
|
||||||
|
connections ([see Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#time-zone)).
|
||||||
|
Usually everything is converted to the users timezone so this setting doesn't really need to be correct.
|
||||||
|
|
||||||
|
```
|
||||||
|
TZ=Europe/Berlin
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Default Theme
|
||||||
|
> default `0` - options `1-X` (space ID)
|
||||||
|
|
||||||
|
Tandoors appearance can be changed on a user and space level but unauthenticated users always see the tandoor default style.
|
||||||
|
With this setting you can specify the ID of a space of which the appearance settings should be applied if a user is not logged in.
|
||||||
|
|
||||||
|
```
|
||||||
|
UNAUTHENTICATED_THEME_FROM_SPACE=
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rate Limiting / Performance
|
||||||
|
|
||||||
|
#### Shopping auto sync
|
||||||
|
|
||||||
|
> default `5` - options: `1-XXX`
|
||||||
|
|
||||||
|
Users can set an amount of time after which the shopping list is automatically refreshed.
|
||||||
|
This is the minimum interval users can set. Setting this to a low value will allow users to automatically refresh very
|
||||||
|
frequently which
|
||||||
|
might cause high load on the server. (Technically they can obviously refresh as often as they want with their own
|
||||||
|
scripts)
|
||||||
|
|
||||||
|
```
|
||||||
|
SHOPPING_MIN_AUTOSYNC_INTERVAL=5
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API Url Import throttle
|
||||||
|
|
||||||
|
> default `60/hour` - options: `x/hour`, `x/day`, `x/minute`, `x/second`
|
||||||
|
|
||||||
|
Limits how many recipes a user can import per hour.
|
||||||
|
A rate limit is recommended to prevent users from abusing your server for (DDoS) relay attacks and to prevent external
|
||||||
|
service
|
||||||
|
providers from blocking your server for too many request.
|
||||||
|
|
||||||
|
```
|
||||||
|
DRF_THROTTLE_RECIPE_URL_IMPORT=60/hour
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Default Space Limits
|
||||||
|
You might want to limit how many resources a user might create. The following settings apply automatically to newly
|
||||||
|
created spaces. These defaults can be changed in the admin view after a space has been created.
|
||||||
|
|
||||||
|
If unset, all settings default to unlimited/enabled
|
||||||
|
|
||||||
|
```
|
||||||
|
SPACE_DEFAULT_MAX_RECIPES=0 # 0=unlimited recipes
|
||||||
|
SPACE_DEFAULT_MAX_USERS=0 # 0=unlimited users per space
|
||||||
|
SPACE_DEFAULT_MAX_FILES=0 # Maximum file storage for space in MB. 0 for unlimited, -1 to disable file upload.
|
||||||
|
SPACE_DEFAULT_ALLOW_SHARING=1 # Allow users to share recipes with public links
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Export file caching
|
||||||
|
> default `600` - options `1-X`
|
||||||
|
|
||||||
|
Recipe exports are cached for a certain time (in seconds) by default, adjust time if needed
|
||||||
|
```
|
||||||
|
EXPORT_FILE_CACHE_DURATION=600
|
||||||
|
```
|
@ -45,6 +45,7 @@ nav:
|
|||||||
- Storages and Sync: features/external_recipes.md
|
- Storages and Sync: features/external_recipes.md
|
||||||
- Import/Export: features/import_export.md
|
- Import/Export: features/import_export.md
|
||||||
- System:
|
- System:
|
||||||
|
- Configuration: system/configuration.md
|
||||||
- Updating: system/updating.md
|
- Updating: system/updating.md
|
||||||
- Permission System: system/permissions.md
|
- Permission System: system/permissions.md
|
||||||
- Backup: system/backup.md
|
- Backup: system/backup.md
|
||||||
|
@ -44,7 +44,7 @@ INTERNAL_IPS = os.getenv('INTERNAL_IPS').split(
|
|||||||
',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1']
|
',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1']
|
||||||
|
|
||||||
# allow djangos wsgi server to server mediafiles
|
# allow djangos wsgi server to server mediafiles
|
||||||
GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', True)))
|
GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', False)))
|
||||||
|
|
||||||
if os.getenv('REVERSE_PROXY_AUTH') is not None:
|
if os.getenv('REVERSE_PROXY_AUTH') is not None:
|
||||||
print('DEPRECATION WARNING: Environment var "REVERSE_PROXY_AUTH" is deprecated. Please use "REMOTE_USER_AUTH".')
|
print('DEPRECATION WARNING: Environment var "REVERSE_PROXY_AUTH" is deprecated. Please use "REMOTE_USER_AUTH".')
|
||||||
@ -57,6 +57,7 @@ COMMENT_PREF_DEFAULT = bool(int(os.getenv('COMMENT_PREF_DEFAULT', True)))
|
|||||||
FRACTION_PREF_DEFAULT = bool(int(os.getenv('FRACTION_PREF_DEFAULT', False)))
|
FRACTION_PREF_DEFAULT = bool(int(os.getenv('FRACTION_PREF_DEFAULT', False)))
|
||||||
KJ_PREF_DEFAULT = bool(int(os.getenv('KJ_PREF_DEFAULT', False)))
|
KJ_PREF_DEFAULT = bool(int(os.getenv('KJ_PREF_DEFAULT', False)))
|
||||||
STICKY_NAV_PREF_DEFAULT = bool(int(os.getenv('STICKY_NAV_PREF_DEFAULT', True)))
|
STICKY_NAV_PREF_DEFAULT = bool(int(os.getenv('STICKY_NAV_PREF_DEFAULT', True)))
|
||||||
|
UNAUTHENTICATED_THEME_FROM_SPACE = 2 #int(os.getenv('UNAUTHENTICATED_THEME_FROM_SPACE', 0))
|
||||||
|
|
||||||
# minimum interval that users can set for automatic sync of shopping lists
|
# minimum interval that users can set for automatic sync of shopping lists
|
||||||
SHOPPING_MIN_AUTOSYNC_INTERVAL = int(
|
SHOPPING_MIN_AUTOSYNC_INTERVAL = int(
|
||||||
@ -69,7 +70,8 @@ if os.getenv('CSRF_TRUSTED_ORIGINS'):
|
|||||||
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',')
|
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',')
|
||||||
|
|
||||||
if CORS_ORIGIN_ALLOW_ALL := os.getenv('CORS_ORIGIN_ALLOW_ALL') is not None:
|
if CORS_ORIGIN_ALLOW_ALL := os.getenv('CORS_ORIGIN_ALLOW_ALL') is not None:
|
||||||
print('DEPRECATION WARNING: Environment var "CORS_ORIGIN_ALLOW_ALL" is deprecated. Please use "CORS_ALLOW_ALL_ORIGINS."')
|
print(
|
||||||
|
'DEPRECATION WARNING: Environment var "CORS_ORIGIN_ALLOW_ALL" is deprecated. Please use "CORS_ALLOW_ALL_ORIGINS."')
|
||||||
CORS_ALLOW_ALL_ORIGINS = CORS_ORIGIN_ALLOW_ALL
|
CORS_ALLOW_ALL_ORIGINS = CORS_ORIGIN_ALLOW_ALL
|
||||||
else:
|
else:
|
||||||
CORS_ALLOW_ALL_ORIGINS = bool(int(os.getenv("CORS_ALLOW_ALL_ORIGINS", True)))
|
CORS_ALLOW_ALL_ORIGINS = bool(int(os.getenv("CORS_ALLOW_ALL_ORIGINS", True)))
|
||||||
@ -158,7 +160,8 @@ try:
|
|||||||
INSTALLED_APPS.append(plugin_module)
|
INSTALLED_APPS.append(plugin_module)
|
||||||
|
|
||||||
plugin_config = {
|
plugin_config = {
|
||||||
'name': plugin_class.verbose_name if hasattr(plugin_class, 'verbose_name') else plugin_class.name,
|
'name': plugin_class.verbose_name if hasattr(plugin_class,
|
||||||
|
'verbose_name') else plugin_class.name,
|
||||||
'version': plugin_class.VERSION if hasattr(plugin_class, 'VERSION') else 'unknown',
|
'version': plugin_class.VERSION if hasattr(plugin_class, 'VERSION') else 'unknown',
|
||||||
'website': plugin_class.website if hasattr(plugin_class, 'website') else '',
|
'website': plugin_class.website if hasattr(plugin_class, 'website') else '',
|
||||||
'github': plugin_class.github if hasattr(plugin_class, 'github') else '',
|
'github': plugin_class.github if hasattr(plugin_class, 'github') else '',
|
||||||
@ -166,7 +169,8 @@ try:
|
|||||||
'base_path': os.path.join(BASE_DIR, 'recipes', 'plugins', d),
|
'base_path': os.path.join(BASE_DIR, 'recipes', 'plugins', d),
|
||||||
'base_url': plugin_class.base_url,
|
'base_url': plugin_class.base_url,
|
||||||
'bundle_name': plugin_class.bundle_name if hasattr(plugin_class, 'bundle_name') else '',
|
'bundle_name': plugin_class.bundle_name if hasattr(plugin_class, 'bundle_name') else '',
|
||||||
'api_router_name': plugin_class.api_router_name if hasattr(plugin_class, 'api_router_name') else '',
|
'api_router_name': plugin_class.api_router_name if hasattr(plugin_class,
|
||||||
|
'api_router_name') else '',
|
||||||
'nav_main': plugin_class.nav_main if hasattr(plugin_class, 'nav_main') else '',
|
'nav_main': plugin_class.nav_main if hasattr(plugin_class, 'nav_main') else '',
|
||||||
'nav_dropdown': plugin_class.nav_dropdown if hasattr(plugin_class, 'nav_dropdown') else '',
|
'nav_dropdown': plugin_class.nav_dropdown if hasattr(plugin_class, 'nav_dropdown') else '',
|
||||||
}
|
}
|
||||||
@ -256,7 +260,8 @@ if LDAP_AUTH:
|
|||||||
ldap.SCOPE_SUBTREE,
|
ldap.SCOPE_SUBTREE,
|
||||||
os.getenv('AUTH_LDAP_USER_SEARCH_FILTER_STR', '(uid=%(user)s)'),
|
os.getenv('AUTH_LDAP_USER_SEARCH_FILTER_STR', '(uid=%(user)s)'),
|
||||||
)
|
)
|
||||||
AUTH_LDAP_USER_ATTR_MAP = ast.literal_eval(os.getenv('AUTH_LDAP_USER_ATTR_MAP')) if os.getenv('AUTH_LDAP_USER_ATTR_MAP') else {
|
AUTH_LDAP_USER_ATTR_MAP = ast.literal_eval(os.getenv('AUTH_LDAP_USER_ATTR_MAP')) if os.getenv(
|
||||||
|
'AUTH_LDAP_USER_ATTR_MAP') else {
|
||||||
'first_name': 'givenName',
|
'first_name': 'givenName',
|
||||||
'last_name': 'sn',
|
'last_name': 'sn',
|
||||||
'email': 'mail',
|
'email': 'mail',
|
||||||
|
@ -191,6 +191,37 @@
|
|||||||
</b-form-select>
|
</b-form-select>
|
||||||
</b-form-group>
|
</b-form-group>
|
||||||
|
|
||||||
|
<h5>{{ $t('CustomLogos') }}</h5>
|
||||||
|
<p>{{$t('CustomLogoHelp')}} </p>
|
||||||
|
<b-form-group :label="$t('Logo')+' 32x32px'">
|
||||||
|
<generic-multiselect :initial_single_selection="space.logo_color_32"
|
||||||
|
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_32 = $event.val;"></generic-multiselect>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group :label="$t('Logo')+' 128x128px'">
|
||||||
|
<generic-multiselect :initial_single_selection="space.logo_color_128"
|
||||||
|
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_128 = $event.val;"></generic-multiselect>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group :label="$t('Logo')+' 144x144px'">
|
||||||
|
<generic-multiselect :initial_single_selection="space.logo_color_144"
|
||||||
|
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_144 = $event.val;"></generic-multiselect>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group :label="$t('Logo')+' 180x180px'">
|
||||||
|
<generic-multiselect :initial_single_selection="space.logo_color_180"
|
||||||
|
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_180 = $event.val;"></generic-multiselect>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group :label="$t('Logo')+' 192x192px'">
|
||||||
|
<generic-multiselect :initial_single_selection="space.logo_color_192"
|
||||||
|
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_192 = $event.val;"></generic-multiselect>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group :label="$t('Logo')+' 512x512px'">
|
||||||
|
<generic-multiselect :initial_single_selection="space.logo_color_512"
|
||||||
|
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_512 = $event.val;"></generic-multiselect>
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group :label="$t('Logo')+' SVG'">
|
||||||
|
<generic-multiselect :initial_single_selection="space.logo_color_svg"
|
||||||
|
:model="Models.USERFILE" :multiple="false" @change="space.logo_color_svg = $event.val;"></generic-multiselect>
|
||||||
|
</b-form-group>
|
||||||
|
|
||||||
<b-button variant="success" @click="updateSpace()">{{ $t('Update') }}</b-button>
|
<b-button variant="success" @click="updateSpace()">{{ $t('Update') }}</b-button>
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
|
@ -284,7 +284,9 @@
|
|||||||
"CustomTheme": "Custom Theme",
|
"CustomTheme": "Custom Theme",
|
||||||
"CustomThemeHelp": "Override styles of the selected theme by uploading a custom CSS file.",
|
"CustomThemeHelp": "Override styles of the selected theme by uploading a custom CSS file.",
|
||||||
"CustomImageHelp": "Upload an image to show in the space overview.",
|
"CustomImageHelp": "Upload an image to show in the space overview.",
|
||||||
"CustomNavLogoHelp": "Upload an image to use as the space logo.",
|
"CustomNavLogoHelp": "Upload an image to use as the navigation bar logo.",
|
||||||
|
"CustomLogoHelp": "Upload square images in different sizes to change to logo in the browser tab and installed web app.",
|
||||||
|
"CustomLogos": "Custom Logos",
|
||||||
"SupermarketCategoriesOnly": "Supermarket Categories Only",
|
"SupermarketCategoriesOnly": "Supermarket Categories Only",
|
||||||
"MoveCategory": "Move To: ",
|
"MoveCategory": "Move To: ",
|
||||||
"CountMore": "...+{count} more",
|
"CountMore": "...+{count} more",
|
||||||
@ -293,7 +295,7 @@
|
|||||||
"Warning": "Warning",
|
"Warning": "Warning",
|
||||||
"NoCategory": "No category selected.",
|
"NoCategory": "No category selected.",
|
||||||
"InheritWarning": "{food} is set to inherit, changes may not persist.",
|
"InheritWarning": "{food} is set to inherit, changes may not persist.",
|
||||||
"ShowDelayed": "Show postponed items",
|
"ShowDelayed": "Show Delayed Items",
|
||||||
"Completed": "Completed",
|
"Completed": "Completed",
|
||||||
"OfflineAlert": "You are offline, shopping list may not syncronize.",
|
"OfflineAlert": "You are offline, shopping list may not syncronize.",
|
||||||
"shopping_share": "Share Shopping List",
|
"shopping_share": "Share Shopping List",
|
||||||
@ -454,8 +456,8 @@
|
|||||||
"show_ingredient_overview": "Display a list of all ingredients at the start of the recipe.",
|
"show_ingredient_overview": "Display a list of all ingredients at the start of the recipe.",
|
||||||
"Ingredient Overview": "Ingredient Overview",
|
"Ingredient Overview": "Ingredient Overview",
|
||||||
"last_viewed": "Last Viewed",
|
"last_viewed": "Last Viewed",
|
||||||
"created_on": "Created on",
|
"created_on": "Created On",
|
||||||
"updatedon": "Updated on",
|
"updatedon": "Updated On",
|
||||||
"Imported_From": "Imported from",
|
"Imported_From": "Imported from",
|
||||||
"advanced_search_settings": "Advanced Search Settings",
|
"advanced_search_settings": "Advanced Search Settings",
|
||||||
"nothing_planned_today": "You have nothing planned for today!",
|
"nothing_planned_today": "You have nothing planned for today!",
|
||||||
@ -550,12 +552,5 @@
|
|||||||
"Transpose_Words": "Transpose Words",
|
"Transpose_Words": "Transpose Words",
|
||||||
"Name_Replace": "Name Replace",
|
"Name_Replace": "Name Replace",
|
||||||
"Food_Replace": "Food Replace",
|
"Food_Replace": "Food Replace",
|
||||||
"Unit_Replace": "Unit Replace",
|
"Unit_Replace": "Unit Replace"
|
||||||
"Delete_All": "Delete All",
|
|
||||||
"PostponedUntil": "Postponed until",
|
|
||||||
"Postpone": "Postponed until",
|
|
||||||
"ShowRecentlyCompleted": "Show recently completed items",
|
|
||||||
"Delay": "Delay",
|
|
||||||
"Entries": "Entries",
|
|
||||||
"created_by": "Created by"
|
|
||||||
}
|
}
|