From 42ced25e10e6193361345836e13fa39084fb80da Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Wed, 3 Jan 2024 15:05:44 +0100 Subject: [PATCH 01/16] improved settings override message --- vue/src/apps/SpaceManageView/SpaceManageView.vue | 2 +- vue/src/components/Settings/CosmeticSettingsComponent.vue | 3 +++ vue/src/locales/en.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/vue/src/apps/SpaceManageView/SpaceManageView.vue b/vue/src/apps/SpaceManageView/SpaceManageView.vue index f6af304079..3dab190af2 100644 --- a/vue/src/apps/SpaceManageView/SpaceManageView.vue +++ b/vue/src/apps/SpaceManageView/SpaceManageView.vue @@ -137,7 +137,7 @@

{{ $t('Cosmetic') }}

- {{ $t('Space_Cosmetic_Settings') }} + {{ $t('Space_Cosmetic_Settings') }} + + {{ $t('Space_Cosmetic_Settings') }} + Tandoor diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index 79e697cc86..5dc720492a 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -425,7 +425,7 @@ "Nav_Text_Mode": "Navigation Text Mode", "Nav_Text_Mode_Help": "Behaves differently for every theme.", "Nav_Color_Help": "Change navigation color.", - "Space_Cosmetic_Settings": "All cosmetic space settings override users individual settings.", + "Space_Cosmetic_Settings": "Some cosmetic settings can be changed by space administrators and will override client settings for that space.", "Use_Kj": "Use kJ instead of kcal", "Comments_setting": "Show Comments", "click_image_import": "Click the image you want to import for this recipe", From 30f3a697f09b36e6cd50758a2cde9f692ff571df Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Wed, 3 Jan 2024 15:13:24 +0100 Subject: [PATCH 02/16] fixed space theme defaults in model --- cookbook/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cookbook/models.py b/cookbook/models.py index a1162cffa1..f091b8a446 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -283,11 +283,11 @@ class Space(ExportModelOperationsMixin('space'), models.Model): name = models.CharField(max_length=128, default='Default') image = models.ForeignKey("UserFile", on_delete=models.SET_NULL, null=True, blank=True, related_name='space_image') - space_theme = models.CharField(choices=THEMES, max_length=128, default=TANDOOR) + space_theme = models.CharField(choices=THEMES, max_length=128, default=BLANK) 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) + nav_text_color = models.CharField(max_length=16, choices=NAV_TEXT_COLORS, default=BLANK) created_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True) created_at = models.DateTimeField(auto_now_add=True) From ecf985f5e30de5523d6e87906e1dda7203a62b06 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Sat, 6 Jan 2024 14:38:27 +0800 Subject: [PATCH 03/16] change gunicorn media default --- recipes/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/settings.py b/recipes/settings.py index 2f5b95e7e6..0a68e63ced 100644 --- a/recipes/settings.py +++ b/recipes/settings.py @@ -44,7 +44,7 @@ ',') if os.getenv('INTERNAL_IPS') else ['127.0.0.1'] # 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: print('DEPRECATION WARNING: Environment var "REVERSE_PROXY_AUTH" is deprecated. Please use "REMOTE_USER_AUTH".') From ac5333d0e741b1af29ab7d7786fcd2261bf99618 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Sat, 6 Jan 2024 15:34:55 +0800 Subject: [PATCH 04/16] cleaned .env template and created dedicated docs page for environment configuration --- .env.template | 186 +---------- docs/system/configuration.md | 589 +++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 595 insertions(+), 181 deletions(-) create mode 100644 docs/system/configuration.md diff --git a/.env.template b/.env.template index 3c91b3195e..a7adf092ff 100644 --- a/.env.template +++ b/.env.template @@ -1,191 +1,15 @@ -# only set this to true when testing/debugging -# when unset: 1 (true) - dont unset this, just for development -DEBUG=0 -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 +# --------------------------------------------------------------------------- +# This template contains only required options. +# Visit the docs to find more https://docs.tandoor.dev/system/configuration/ +# --------------------------------------------------------------------------- # random secret key, use for example `base64 /dev/urandom | head -c50` to generate one -# ---------------------------- AT LEAST ONE REQUIRED ------------------------- 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 DB_ENGINE=django.db.backends.postgresql -# DB_OPTIONS= {} # e.g. {"sslmode":"require"} to enable ssl POSTGRES_HOST=db_recipes +POSTGRES_DB=djangodb POSTGRES_PORT=5432 POSTGRES_USER=djangouser -# ---------------------------- AT LEAST ONE REQUIRED ------------------------- 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 \ No newline at end of file diff --git a/docs/system/configuration.md b/docs/system/configuration.md new file mode 100644 index 0000000000..ad0c81077b --- /dev/null +++ b/docs/system/configuration.md @@ -0,0 +1,589 @@ +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 +``` + +### 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 +``` diff --git a/mkdocs.yml b/mkdocs.yml index 5561ba7368..b7f50de22f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -45,6 +45,7 @@ nav: - Storages and Sync: features/external_recipes.md - Import/Export: features/import_export.md - System: + - Configuration: system/configuration.md - Updating: system/updating.md - Permission System: system/permissions.md - Backup: system/backup.md From 953dc75a8d7fa75055628bfcad4f75fd64646e5d Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Sat, 6 Jan 2024 21:43:40 +0800 Subject: [PATCH 05/16] added ability to change unauthenticated theme --- cookbook/templatetags/theming_tags.py | 38 ++++++++++++++++++++++----- docs/system/configuration.md | 10 +++++++ recipes/settings.py | 13 ++++++--- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/cookbook/templatetags/theming_tags.py b/cookbook/templatetags/theming_tags.py index ec35b2f095..e110f6a64c 100644 --- a/cookbook/templatetags/theming_tags.py +++ b/cookbook/templatetags/theming_tags.py @@ -3,15 +3,13 @@ from django_scopes import scopes_disabled 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.simple_tag def theme_url(request): - if not request.user.is_authenticated: - return static('themes/tandoor.min.css') themes = { UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css', UserPreference.FLATLY: 'themes/flatly.min.css', @@ -20,8 +18,13 @@ def theme_url(request): UserPreference.TANDOOR: 'themes/tandoor.min.css', UserPreference.TANDOOR_DARK: 'themes/tandoor_dark.min.css', } - # if request.space.custom_space_theme: - # return request.space.custom_space_theme.file.url + + if not request.user.is_authenticated: + if UNAUTHENTICATED_THEME_FROM_SPACE > 0: # TODO load unauth space setting on boot in settings.py and use them here + with scopes_disabled(): + return static(themes[Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first().space_theme]) + else: + return static('themes/tandoor.min.css') if request.space.space_theme in themes: return static(themes[request.space.space_theme]) @@ -36,10 +39,19 @@ def theme_url(request): def custom_theme(request): if request.user.is_authenticated and request.space.custom_space_theme: return request.space.custom_space_theme.file.url + elif UNAUTHENTICATED_THEME_FROM_SPACE > 0: + with scopes_disabled(): + return Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first().custom_space_theme.file.url @register.simple_tag def logo_url(request): + if not request.user.is_authenticated: + if UNAUTHENTICATED_THEME_FROM_SPACE > 0: + with scopes_disabled(): + space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first() + if getattr(space, 'nav_logo', None): + return space.nav_logo.file.url if request.user.is_authenticated and getattr(getattr(request, "space", {}), 'nav_logo', None): return request.space.nav_logo.file.url else: @@ -49,6 +61,11 @@ def logo_url(request): @register.simple_tag def nav_bg_color(request): if not request.user.is_authenticated: + if UNAUTHENTICATED_THEME_FROM_SPACE > 0: + with scopes_disabled(): + space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first() + if space.nav_bg_color: + return space.nav_bg_color return '#ddbf86' else: if request.space.nav_bg_color: @@ -59,8 +76,14 @@ def nav_bg_color(request): @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 + type_mapping = {Space.DARK: 'navbar-light', + Space.LIGHT: 'navbar-dark'} # inverted since navbar-dark means the background if not request.user.is_authenticated: + if UNAUTHENTICATED_THEME_FROM_SPACE > 0: + with scopes_disabled(): + space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first() + if space.nav_text_color: + return type_mapping[space.nav_text_color] return 'navbar-dark' else: if request.space.nav_text_color != Space.BLANK: @@ -71,7 +94,8 @@ def nav_text_color(request): @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): + 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 '' diff --git a/docs/system/configuration.md b/docs/system/configuration.md index ad0c81077b..c235113423 100644 --- a/docs/system/configuration.md +++ b/docs/system/configuration.md @@ -538,6 +538,16 @@ Usually everything is converted to the users timezone so this setting doesn't re 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 diff --git a/recipes/settings.py b/recipes/settings.py index 0a68e63ced..644bf8a446 100644 --- a/recipes/settings.py +++ b/recipes/settings.py @@ -57,6 +57,7 @@ FRACTION_PREF_DEFAULT = bool(int(os.getenv('FRACTION_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))) +UNAUTHENTICATED_THEME_FROM_SPACE = int(os.getenv('UNAUTHENTICATED_THEME_FROM_SPACE', 0)) # minimum interval that users can set for automatic sync of shopping lists SHOPPING_MIN_AUTOSYNC_INTERVAL = int( @@ -69,7 +70,8 @@ CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS').split(',') 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 else: CORS_ALLOW_ALL_ORIGINS = bool(int(os.getenv("CORS_ALLOW_ALL_ORIGINS", True))) @@ -158,7 +160,8 @@ INSTALLED_APPS.append(plugin_module) 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', 'website': plugin_class.website if hasattr(plugin_class, 'website') else '', 'github': plugin_class.github if hasattr(plugin_class, 'github') else '', @@ -166,7 +169,8 @@ 'base_path': os.path.join(BASE_DIR, 'recipes', 'plugins', d), 'base_url': plugin_class.base_url, '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_dropdown': plugin_class.nav_dropdown if hasattr(plugin_class, 'nav_dropdown') else '', } @@ -256,7 +260,8 @@ ldap.SCOPE_SUBTREE, 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', 'last_name': 'sn', 'email': 'mail', From 50e1eaf6452b8bdd1530006fdc2fe6990307cd15 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Sat, 6 Jan 2024 22:35:22 +0800 Subject: [PATCH 06/16] fixed bg color for unauthenticated nav --- cookbook/templatetags/theming_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/templatetags/theming_tags.py b/cookbook/templatetags/theming_tags.py index e110f6a64c..81543e79d5 100644 --- a/cookbook/templatetags/theming_tags.py +++ b/cookbook/templatetags/theming_tags.py @@ -84,7 +84,7 @@ def nav_text_color(request): space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first() if space.nav_text_color: return type_mapping[space.nav_text_color] - return 'navbar-dark' + return 'navbar-light' else: if request.space.nav_text_color != Space.BLANK: return type_mapping[request.space.nav_text_color] From c6fa635af287711baa22a8d526345d7ebdd26c00 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Sat, 6 Jan 2024 23:23:17 +0800 Subject: [PATCH 07/16] basics of custom icons --- ...color_128_space_logo_color_144_and_more.py | 49 +++++++++++ cookbook/models.py | 11 +++ cookbook/static/assets/favicon-16x16.png | Bin 1164 -> 0 bytes cookbook/static/assets/favicon.svg | 41 --------- .../{safari-pinned-tab.svg => logo_black.svg} | 0 cookbook/static/assets/logo_color_128.png | Bin 0 -> 7425 bytes .../{logo_color144.png => logo_color_144.png} | Bin ...pple-touch-icon.png => logo_color_180.png} | Bin cookbook/static/assets/logo_color_192.png | Bin 0 -> 11462 bytes .../{favicon-32x32.png => logo_color_32.png} | Bin .../{logo_color512.png => logo_color_512.png} | Bin .../{logo_color.svg => logo_color_svg.svg} | 0 cookbook/templates/base.html | 23 +++-- cookbook/templatetags/theming_tags.py | 29 ++++++- cookbook/urls.py | 3 +- cookbook/views/views.py | 81 ++++++++++++++++-- 16 files changed, 173 insertions(+), 64 deletions(-) create mode 100644 cookbook/migrations/0207_space_logo_color_128_space_logo_color_144_and_more.py delete mode 100644 cookbook/static/assets/favicon-16x16.png delete mode 100644 cookbook/static/assets/favicon.svg rename cookbook/static/assets/{safari-pinned-tab.svg => logo_black.svg} (100%) create mode 100644 cookbook/static/assets/logo_color_128.png rename cookbook/static/assets/{logo_color144.png => logo_color_144.png} (100%) rename cookbook/static/assets/{apple-touch-icon.png => logo_color_180.png} (100%) create mode 100644 cookbook/static/assets/logo_color_192.png rename cookbook/static/assets/{favicon-32x32.png => logo_color_32.png} (100%) rename cookbook/static/assets/{logo_color512.png => logo_color_512.png} (100%) rename cookbook/static/assets/{logo_color.svg => logo_color_svg.svg} (100%) diff --git a/cookbook/migrations/0207_space_logo_color_128_space_logo_color_144_and_more.py b/cookbook/migrations/0207_space_logo_color_128_space_logo_color_144_and_more.py new file mode 100644 index 0000000000..6c3adbf5e6 --- /dev/null +++ b/cookbook/migrations/0207_space_logo_color_128_space_logo_color_144_and_more.py @@ -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'), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index f091b8a446..0614c97493 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -289,6 +289,14 @@ class Space(ExportModelOperationsMixin('space'), models.Model): 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) + 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_at = models.DateTimeField(auto_now_add=True) message = models.CharField(max_length=512, default='', blank=True) @@ -1344,6 +1352,9 @@ def save(self, *args, **kwargs): self.file_size_kb = round(self.file.size / 1000) super(UserFile, self).save(*args, **kwargs) + def __str__(self): + return f'{self.name} (#{self.id})' + class Automation(ExportModelOperationsMixin('automations'), models.Model, PermissionModelMixin): FOOD_ALIAS = 'FOOD_ALIAS' diff --git a/cookbook/static/assets/favicon-16x16.png b/cookbook/static/assets/favicon-16x16.png deleted file mode 100644 index 2ed29e2cad737ff5c3799ae8f2f8577d281ad50d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1164 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>2SRW&s|9ZfkINfA*|4GncwRW&7L z6%AEI6I~?}JtZZ1X(3?|VG&WFAwc6*R5gmDB+u1wMq9|Ls;e7lsfJj{ni#2EoXL4P zlg(U95oj#XJ;7%3E0Va345Sav=Kk5p5U49Guc)k~s_ttf|G0+b${d#OD;ZxEFxjaH z3X6&ZT?n+-%gMyc!KlqmDQvNl8hdlM;e#p6=N|pbw6Q%DD6r5d}^y@@PPm#N%quJPQVD(DGBleB^Gv%<&S^=ek?KZ-3GR|%O3su_3P2Hw;mJOHf#V2y_*P? zUhcvEF7h`MP(5Rkx4X-{`I{DVgEW_T`ns||WftaFw|;+kpAS%4(9^{+MB;LCLPA1H zT4HMQ^9L!ZiD@YeMUp#rRF;%{{ld#rxnrkw$&Q~A5kE>xWaiF^jk$Zr)~57Fqy$Sz$(}v6HGlswGc`Bama<5&lsY*b zKBTO)cu{iF<43|Qk}WzylP3iSUA`0?JlWW2bBkn))28B}qR*c`e=Z8<;_7yhVCnYu z3O?O*>U8s|AZ@MHE|M&(va=??es%iv>(`T{rDh*#xMsFpW&5_R+qZ8mD=F*rW{?fL zb9eHc+t;tpzGJ`8D~Rhw$;-~kYW_2=N>6QhX`GTW(_!A^$@49swJ)wB`Jv|saDBFsX&Us$iT=@*T7iU&@#l( z)XLb>%G6TZz|hLTAW5gF3PnS1eoAIqC2kGEg{O`IHH24%M3e+2mMat#<)>xlq$-qD z7Nja<7L+72FjUNW{E3I7Fib<^l>g~7o=<}qn3cKplDUPIg}o<>FbgZVG?*MtVOHK8 sqHy}gl@mwK9FaM~e!9V9ftMb`D{;Y+Pfn&&fmSehy85}Sb4q9e0NlNo2><{9 diff --git a/cookbook/static/assets/favicon.svg b/cookbook/static/assets/favicon.svg deleted file mode 100644 index bcce5ce8b3..0000000000 --- a/cookbook/static/assets/favicon.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - diff --git a/cookbook/static/assets/safari-pinned-tab.svg b/cookbook/static/assets/logo_black.svg similarity index 100% rename from cookbook/static/assets/safari-pinned-tab.svg rename to cookbook/static/assets/logo_black.svg diff --git a/cookbook/static/assets/logo_color_128.png b/cookbook/static/assets/logo_color_128.png new file mode 100644 index 0000000000000000000000000000000000000000..544e05c8f39af01a2c7dd11ad64333e093c7b654 GIT binary patch literal 7425 zcmV+c9sc5pP)Py6yGcYrRCr$PT?u%TMf(5E_a$iyrIec@cS{l!5K&MNweaWgfK`x1SiC_+!~?Gd zR?!6&1Qygq5meZ97X(BUK|uilIg~?14x5D1(lad}_tlnW{(Vcjq-l=tm@h5J`#gF2 zKxW=~XXckV-gys(%k+mH0rrRP{pHT%G64SIAo!yLK=#gm@gEfjmm~mMw{G2vbKcVX zY04P8f^(k07^}-UufrIN1wbDY=Nyc&a{x|q&QAjP9l#;Rn42+{!#U4njHS^hB_(C= zr3RFX9so&6N%bU2x=xa$_5j*>Jf60UF*|^=U$>|^D`t#20PF&=)8p~%@OV7iQ&Uq9 zifW)T_*|3#Xw|A!d&XE-09_bkT>vzzjKKqt!8zXyU^C}@Wb&Rp^JRZ*~m&-+~MU|(j4ghAi+wb6;3a^_kd%76Tp@WC9}_%F)t$?bNAO4*<1x)oby!x z7I{3LMX9N&#|>>v1@kHcfYz;B58|8;;hgtTA-}4k>n5b3p{)qWr`;OBsSDLN|6U#M z@2icihpVaLskSiZe7P*k^HNe$7O1UjMEObwfYoa4${2fybN(P>OfsUAWjxzAJc~~z zXMja}cjYwzGRwC(JFPZmrPaZybB2oI0q`%*`COOFwb95vhvQvo0I*uEbpZ^MBxxw; zJSm)@qQ0N_CpY@t>$3v?48ZU5mU27_Q=Rp&>G$fQnhnT@F_!A_cs>H~q08kuWN2eS zm{$q_tX69e0FN`q`d3D$H%;7!I*FwG^U}8^P|U6<$z(@8%rW?EK&^7lmjIaQa=BJV zdORWufaK(4liTfnf-yFNb8d}vXUp14ZgvXqPw|`auL0y$lxR_QJjU&A5D8IT0NzVT zNO*VMx^+J2G`8u80U#+UDN&N7XBcB6jqQ>$?jH}&LZ|MhFBGO-d71Ymz>me|L)Gx` zcFigXWdeCian5IWJf62wQ&ab;s;IGWL;zs7+mkux&jRSHQBL92szrOSZFVZ;XdfNT zF#s#e-ww(e{@1=(Oxk;ea5;i7T*eri=5RRZu3{(|4gj0YHUPkj06H4l6AjE~+?RtM zedz(vhkh-A=}&=~P5&(pvoaGjw4|xZHUN{IPUm7xwG~z}6aY4xZ76^-01_3-6VYO| zVv4b8b}FPOpJofZTR{+2&G?=1n4cSwGXV)j+rUJp(@8f4L&z`ySglst>tAAwMQpUR zjL`h2GSQ*ynetV41K1jv+u34HJ8gZLv>{Z6$}A@&1QR= zG4=}QJTjxCprgTxT-V|>{`+QD`LD@7=t}@WH1`|xEf~D9KB`wYR!>kS6l3fN=lt(Z zr}J$cT@6@OH~{FEU!u_)46$35c96jL~Tnk_8ha| z-YrdV#$!@6F>{7xp-V5HD9rz};%|pK{=pF~a0*u~U6HE5gha`p9>Gy4S98+F$SDa?j(vx~72;KNq z9i$$YBMJh}`98+jXi>KVApr2+^CwrOfFLCSxf zVE#5D^iUK+IXhuTEM}yfe?5%kN&sLRW9&r{dIH@5APt`^%hM}U$@5nGT-@5wYw1C= z(F5o*st)!Ymm$ltA`tvIH5Ktq{j9_3Snx3bzvDp}(zYHnqwneqM;!tI0WLzcthl&% zlwOUYE&z~jKQAwDI_JF7)VuBD&tSm~es8+>0__IWMgE!d%fAv3eDhK|nzf^i-;aVk z6qS{_?#nvJI(6Z|8A%Y7n7Pkr*s$R!J#PoP06^Eh664IbB82#W&5W@hzyr;F?rU)+ z7C*NEiGQw#lSLQ07Y+oAMrEK~=d%^tAnTz$!FxDleLPm@mft%f2?7Am=(!$h1^_+j zN|N+$Wxndsw0zbj16RfQOkCrVeY-2r;LiFLW-Sy5R*p$W^J@aE05thNCS?9h$M1;6 zyWVqv?{^UdflHF4CsI;UG(9D20)W?;V^(Fo=a&t1ElV+e=$=}BPObuQJ#f?Awc$Em z@fsKm1V2tnMSRNu9%A%rOxr*Zn!2kxrtA)^5Q-ECy!Hf-X)!9%1OU6;KALkru`-u_ zYP(C_)0lIepBAq*fX=`huf^i+^?~jC{6TOP#7;?_X zxLhu}hN~jg03aK_5>K4DjIoHBaRuwq?FlC^?dsg}UpE49E3oqi6MBvdZt3d_g4u(! zadST(3sGN=hgHGMpu&Tj9>ptH(p!lS z(Q16p`F~HJULBLZ4z8~!5RB}64r9k24E*aSCgcf753?G!0N*MR(i6;aI-T?atC&;) zfK0cGi;L$OZM~~h`r(Pc~GgnQMli;hW>|c?Q{;DfwFE!rqq>j1z6H$f5pb?3DQdjFd6r6tjEgQ*mXqNqf=Sk2!P-b zm&^5`l37p$0P@uZ6a5$ESnz*n4u-R-g zOMGUASJ~MwS*^pa0=~kNZpioTh75sJI%|_*C{6zMLxEI>+D6JC5WMenIv>{z01C@O zns7alIkXLj`dT5+0_xd?tTFEASj=*YcOy3d1X(39jCzF%R}*ptu-olVa?aloJbs3; ztMD>tKuasKqwwH{ng(z_&r&o97-LU49FBhm6F4LQyoWyu)pF6$FFjp|k&Ow-bF7fT z`WY!1j3vP!_`-V_3@CI^08l74Ns`u9!Dw7zNB^C6`_ZdW5OdzJwF4JekQR-nw`jle zscn`(Kqlu(j9SC+KI-v!x&;ei7Zd#LGEb@Q+HR# zynQi|?gC6EQ>n?hUJ!83pA6gfGML2e{ z5GPI-;m6;U-P0%pZ2Y<3aaXgm=+~?O_L}Nk?)MxqVPSeS7NtkyNMU%>^0IO2dUXP7 z$3>xLbqnHaL?I*Z1lIqOTPnX6A`p;0fgZmD#@HtghvP4S+7Ac-3co7LGChJuJObyf z*Ed4{o12zSejr-8B^UEnrDK~TJP-&3M~~8r4$!Gy5jxc^D*a1fXck*ueVlPphU1tC zWa+#0kh%1)8tUApAs+5y$DeL$9=U0V&Crl^`V)`9! z0Gc(dfu{$y!;s#s!WAw3{=Fr;@%N9mD=O<-NG}LD=eN6Dt`+_b2Lr$u#@HLGrPNVj zV7F!%Ka}*g$|>*g8O;2`g@xY*coFnt69@##TMAtA{@ZLAHK-jL)m7&t`puRs+&`W| zl&K>%=iKVz;3sH!Z|0iiipsFZp`}9g+1x#m-}EO z3ItcTs*9)o+!lRq)V@Rh`)em&eD?>{xosX5)hWG&%lntl>Tkm$T$U%)~$g0p;lwa7`xTsaFCVb|EK~0Y&ILc9=&JG zlw9C?Z_gHZ>4A1eH&c#3{n8E|J&OZJ&VRDgaAWV~3P5@C3=53=_ER0v zyAwI^T_*n#ShjILhE3ET9X1#UD3_7TSPZt%W;2bI0^0YO**z-F@1Lv4CvMp)ng3V0ToZ%lhZb0(Hg(d zH;QWqF6ezbIOlS?%5^T#8NiT3;2V!#Q)=l`Su|zd4lG`~5BuFd@dN`1(>$RD26Rir z3quUM35Okm&)278_~i9r%c(6O%knw5+Z`4FbP*MP4gB%_+YlccSsi=X0MX=6ov$vh z^YedcUuhsXda@9~H+Nh;3E%PZ&0USS2WTCKk@#)QJZMm00G&AvUXfJPM9 z@T(gG#+G{kQ{2qrH+q-82v!EQ8aN-vPiZqG4JNN7NoVu&^2$BH1bPl$Rgwx_s4BQg z{pwgXO(;vnb294fO_={JMS4zKZp7f8$_zdg38k>mt)rIX7Xb<(-{wpvQ+jT0 zZcE?)3$y}EQ^0GkC*;kwJjUJ?hWrZ zphgUPvv~Bmv3cp0v01}dkU2Oxei%g~|FJ=n4?!q-qNAhdWoBmH8*T*V>SVtf?UZ zKt`d(#lx_9u!|Ery=^ z=VttOWr&1DDw{ExOxd})xfK+A!B&7VoO5BWVSTzZ#?%pF_DZ9)WGcrqVDXes2A6sU zi=fA+e2*pT(nPQ-bF^8mg7VX6A|E6e;RgcEp-vQ6u zBgWaK?Y#?e;D{KT0LIvv{QUfY{&x`z5wmh?4>WJ6&W$?I6fat{4^K|3nk&Gxr@CNB zZ~cy=^aen00isBj<*jbFJK!E1u^>?sAM1dAH|c+6{_dw73?3IIinFMZQ^{xEt3A-8 zqkdnF~$n>^YepVO(GU3@>*LR%zK&A4e6)ln=&x;4T@k_b?Bq9 z-E;+v@a+j1g_3co2%=a|ZnrzAEnY|fP^ehO7$r^9y>?fNHet>5o*?5;5j1=0Z+Ln3 zRuQap^LS}E*ePPj@Vl-=|KxH{>`JQ#EJwyGWW*wZ7-L8C^YcSGVTJ{O$Z+u@ z6+*rP^q-ZkQqS7o(kn&;5iyYdJleFZjR)_r3y&B=?TAzWna$?+a&mH>2sH*_0YFY7 zA`?cenDT#Uksv<9HmQh5O!*#bwu?2Q@E^Qhotj|Ab2p);0S2!0xJ8d#g2;2=zyUG< z4H-CD2>{4fKv>F{=LcPdVSS9rIk{kUDxRJmbh{HWt{;p}4jK$nZv!(H@5Y3=>SG87 zYL_v#BR@aCoF{O=a!Q1zq)`P01q&Htx|2qAu-3->aW^SxR&|+8JKg9%Mw(>#MG>RG!nKtgjzjj8~wqhPld^>+J17PjX zIT-Y^7zLLs%e&ofx1ta%inah6WJ(+vlseYq@yr9DJbZSLSzbKh7PM(0Ch1aH6Lg1eJ@^yRxGKjl|EmXu z^Cw@$-p`16;+AE3z1!_hRw|_86`+ivsN~T`g^APS2oi44jB*$I+xuJa;WF(3qI~2& zci_MQRi3S?0YK^FB}tmgIoEZRkBu?oi#L*mM+x1Ra}3{Z$;P)^#Tir306%i)mAJjL zI3-ruq-ExxME8+j;>2nFZeSLR<(2I0?D64%?EAfH0C*3CqZwlrB2TMnn97dqXT!+5 z#aj!jm7_AtPo1{|Q&c*x29j5jr2M?RJj#%$inIcN++=1k#`>#j&988@MaKMbU2$b| zv1b82GDWiPu6@6w?+Yt%ra*nHU_UuM@$vBuTrTy#WSRirT?KkD#@_Yj$#@NVk z4TbuiwgB1+XiE@5MtBT~bpD_vpO${w0`!n1Nr&xrdxLfB)@j~DbOC^bPfAKklx2B3 zfWCSiKxrASTRRi?9xdrI$Uj%)$ECplV z*X_)DE;>5;xy;N=-D!P=0Kj`57yw`rfJ8k5MPcM^O^r*?phq^nbwwGpMn^}#l9`#Q z>b@ULc3}YUf?z1;{B_2d=4g-76^>^$UuMVY}xO-B-S-sHmA)Sy>}= z3*==We3b2WI~@qebIvu#gSR$`!5(uV^>nDoiEJz0Y5trY8ICwdPGG(JkigFNAIlK`3A^gg~HNmO&3pND9iHUn3$N6X=!Q3TDKUL z7a$N2NMmR+nV#XCYgQW<6cpf=c6IUmkoHw>HK1QTY54T!{4tx&yJKQv?$WE_Q@ZpR zz6JOSLT?9dx0@Ul=pL)dkD5SWmd=45XUI9jV$yA$rgW?VmMc8x!N0z8=>FA{C9lO| zS(KQVc&{GazS_=26aZzywQk+IM{#lS<67+r2n453p9Y0!qtNbmcWa5J4a7T}gc?k; z%#yHWsIpdFDnB9@gnmw zO7zg9Qxn{BO(Vlr1=8lN*`9;-Kj)ObGw5~Tq+h++Z2r1hwQ3J)G3L-a`BcBs08l18 z*%&g$$n%KIZB?!3z91;$SEHH*-L7qfZXK>b$JX`HN~a;m-mGKTygLu8wq%$7rK}?& zNS5V&(b3Uk($mvF6~Rv{k4guC4-iPpHAJo7K_C$L4u(mV(7Jgov~C`cmW^s6wwf8S zF{aX0gWes8PMj_({WYIFa}Fm@7a=Y87*h5hMaupoI9Ke^I<^7p(FxyVGR?}$${H(_ zu|{T91^^_#*ZE|iBuN7~=Zf9X!ht~J*a)pA%kpuv*}TAFvAm$vX+x+0L6}u30F=o{ zzKDf|g?9nC)B6cK7F87lvMe7mnM{k~Ze0H9CE zNF`MT1a!EQW!Y&qo9AR?WKeYOkS^MS=TsHDst$nvb%w&#NRrgkcTmv5Twm~zmF6M7-Jq; zmUCoT-pCl6lbM;hTGh|53KYI50pL4sNl8idBuTo?oARE*bdp=Ny|A!QCS{RGq6ZpT zmh&Y^+Fb&`)lpGV9|sD*DzZ(Zd0q4X2sA*H^n!EVvY?=#Yf({A50A&wfO8(tIj59q zX3jb38yM&3w`OmqTu#4e0F*lS3|0M~4NDSPi z(#B-~Tq-lEu^*QKpmE?Xm1>s(aH-6s#(w-iS?ZDqYUaO(00000NkvXXu0mjfW3UxY literal 0 HcmV?d00001 diff --git a/cookbook/static/assets/logo_color144.png b/cookbook/static/assets/logo_color_144.png similarity index 100% rename from cookbook/static/assets/logo_color144.png rename to cookbook/static/assets/logo_color_144.png diff --git a/cookbook/static/assets/apple-touch-icon.png b/cookbook/static/assets/logo_color_180.png similarity index 100% rename from cookbook/static/assets/apple-touch-icon.png rename to cookbook/static/assets/logo_color_180.png diff --git a/cookbook/static/assets/logo_color_192.png b/cookbook/static/assets/logo_color_192.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed65212a06d4197bd1de89d76497e65ba076596 GIT binary patch literal 11462 zcmZvCWmH>T*X;=ecWtoZ1&Uj7w<5(gI23npaVu^Gin}|-DPAb6002YwqogYA+4bKS84>oYx?+n2dw^V2WyFE1G17ei zpa5hg#nitT9P1;ws?W~7jAahe1kePp71E>%m||nU%?acpV5DCF#C%D#&vYi}!rCt+ zv$w;k8tLe@vGK11sNjTgq=EwbMig)<+CS1I#*JiTli^}GnV+H}n(XL=gH3}KN@NCyvTwK)2<(f-8lmt<%J`xsB z^K~6w-&jOl!%w^sbpSN?%UY635PuSMjb~hr5=PQeGcq0`0S5YoVQl6qz}^1OnVA{< z$X;iD;6VTbz<3Iy?prV)yY~P-c($)a^YQWh!YK~!gOdR%+Zd_fO99lpyFCz736Sg5 z!i^`7;Rxh0&86dQ7qn^lcup7Kys()99QiZJ+6_+mqH5|u8MDYpJ~`}K=tda3Px6ty zwfu~X<$t1r)UZ<1Xn?7ysSu97v0@Rxr=IJINro>&_QygC52ie7yoxKYw7h%7@=k^W z4%oPG{!l+PGxNFOFCtZ&cmhcD_2oI8jNjEVYW|R{2hxfy8b9a5Q|`xCRKMy1mp*G2 zt_@v)JM?p?;`S;$KDCJQaktIy%+skeXXN1Ez+|waz#ZoC1PY3Z8XKUMTfDnlK-5%S z!kp0eNKB;i07HOO=zHCxD3c~Wow0^h-o7~kV93^&q@kt7Iy^XdudkH8KUzKsM?p(l zsIQ`|BtM8pT(3dq}~eg zOiV*#_VL%RU)x2vinPYWWWdz)G+tR***lKDcL1a+K2zL&XWGkyqQPPN{6WB4>A>Tq z;a9sx5;69~I;BHqsN{g{Y*llNO9#!ncjiv!=H}m)qDdAF^5OjHBd&ToQM(}2(a>y< z;XCUJKVuSG=DYLPBAZfXSN(0n?xP6pwwiX@q&E$0k*L?(+Mo@xrsd}jqHkzYMbaQZ z1s};}teCB@3K5$mg0jAX(`@7INSaCvo`K2XB>i}=B7lg8{4-DuuOF4SiT() zzmwT(0>#lO0iI11L|A5xT59;*%Mai#8~dj8BpDnT93jU`&(tWOvj-5Q5zo+ zwL0z|`?_}_fhO2coiARlPe<9g^CKCMAf^@QU5pu*S}XQEs~j@uQuqKVW#DB{`Ck$-WzJqWzZ$&=Qd!crql(7e8q0XYvD_DtxThSA;q*=T85ZTU$ckb=IG+v5Am0 z1|yRE<;SpMsMm+Cy3D(a85Hh&N4#KXct?5*uh+eewTQ0&(3i-@3>>*Xe(qa9uWaXz zW*2aL?KNvYcz^NzOyX+EgBBq2s4_1~%Gs#=*?JmLgp;*Prg(i{J1Q{aH22BP5R=9a|{_KJ~` zNzr-3E@tSmH+&pESqS0zYG_onPVTZjoR~D&J|5BOaAIVQt^5xSaDg3F#KqY;FvCsu zu-qJF8pyfv_9NVFo3wQ=~-r7!QdL-;@6?d@Lck@W{sujk`g6v2oIV5+7KHJoPyiR8< zrDHQ41azfmT=omT#bUlfr|ki4IPxB4Tt0#nIA(AW*-vL5kdr!6khBAl&v5)#r{L^G zjDDwZGdn4Ni~?h~!5n;M+xh-0EoEgnlolNU!0^o;zg4b0?SS%2=?NH)t%b1ol)(*u zn)LNeES|GD`7*MW5wG$Rx2eQN7_81Pmg>&En zYmv>xh2w)(@NU&ggARdaXA1^0AzNVH{Q$a613k}M54$|Jf-rpSwy)@Gw?sAUb7X+) zj8y}aJa%R}YyL$ujM7eVr9S4dM#y(LmB}F*H)qBpcAH}HElmU^A3i-YRFwx!5*2yR zwWOuRvvEzPpas6mzDZ;Z(#lgYX`;M6+;Y@o+D_c|Y>Cv?#EJQO_mk}Jmc;Go*RS3< z(oty6*YZ_Ur$bs#Zc2hDqrAfZy^D8O>5E|2;za{`XF(N^Lt-#?uLdX_j*|=&plFh> z2{DtGqBLNw9HQQU4AvpBULuN;dU|>T6ujjY2s@(;sB8KEKI_p}PorT%9UqD-mA4=3 zBB_l`6^#GB=I%z<*aH;nZ^f{8q|3Z|t`aj-vZNIsG4=$F`q09^2yETWi;!Y}W;yG^ z9pDZmF5cKB*#`N`>EC`r^M*S9#j~ml@^_|;BZ)HTkZ1w?+xUr0{~_gBLB#JRBycAYGXGdFYbbKfY9h z&PFf;p-3%6o?pIOWBNn=UUsc}aE4BW@P4%A9SRG^pX{!B%$rs>hk)p!?IVXg<)f7? z0(*IO^4A9tv35z80%9bTq8cF+s$Mauke_+nnCj0B>7$r%WA_#Y)wk}I>v)!^fajnF z6#8IWkaaf`T_FX3$)58gJAe<^5DLIduD=vO{xJr2sb>1UCr|tI2cI-FUU_St*3>TC z@09}fhZRHkUH2SAlURqj5MWiO3G`)5lH8>?58d7cyZV*6Ys`g4`3Knb*OUm5I0kOY zY{KKwKeWhJF+3Oz2(rFcJJ~l(Gd-yeARkR30|WLRt*hgVC%mZyT=0O9kw_9}q~NOp zS+&fNB2VE_x1E(Q6`yi%WB|2rh&|piqMh`Kg5&F+vmUpOiBJBSuz$x37aEv4A;|wP z)~ES{-1c`sN&CJJLwr8Z3`R=wBZ0AGE3kW+;GZu8X)z!s1)MDc-@GF*oq>e^i*=R0 zNX55(eaf7BEtN0a=Hx>bMs%zrBMx<=gvo{c(`tyX8 z1X(tf?tV7MCiSAY4jbO~R6hQm z#}Ck(knDPcwg)tHwcD2u=Z@%w$%!}I_i%W6626unKmcah8IkvV{n(?=QjObh6=Q<3P-Zv z1iP|IONTSI9Q9JS2e!!+XwPWo)MjiJlo!k|UN_hy2R7N$cjdTz{!E*9820+UE<2v6 z>xg0_WBu^_OP0EMKv$s#Jn)Ua*z)&x-vhOEbzi z-|t!jZu-O~fIx&%UP+YV&dLA>nn}Z%+d4AfWDRsxc)|58l(Qt;lo6Xs5&$#i<~o*r z|L`{<``a&4kjW2WksQ(7 zQs*yK{+G?7(vRZmQ19Dgm0%d>7(Z$eO$3UYfDRNyKvtWx-d{;Db7nXbOtM9wS$iGn7204_>?8&uPPd}{w!Bt zhCH4hAqUS;vL~R@I%2j1;yKK(RC?ebt-lp@fy&3k;sAxc9#Ef-Ahee2O z6s0c3$KFyQk1fSk$uvCye@dw1?Q~Hf61!UigE&|!LOD_aG0<&xc2+ASp_`it;3bXO z&$#p}H`A+Y;4GT<6`XCQY1XdaEu5xs6Rr*T`i!_Cuua%TePL_+_tJq@%kR(LVGb_74c~ zoZ3?ctcdUM-_I51DEj9mbSU;6P|+r%Z=Q1zgYhk9w$5kxONQsA*#AeCPyQZ)+PEpmW!X{fZ4V1_1?Zjoi$(m5a=3*to}2*=EvRZl$7Xncg{N3e!BF{i z>u-t$R^;xz|IT!NDV_p?q1}Xx%T#M3X4R$CDIY9zKFh?kbmv=L$HBBDdJz+aBX(-B zE=c;8^DlFTeM%`#|LOgsy~}%gA6?15TNM+CpP2*{_1mzz@$)`O!r5=kudy!pADCO4 z11J`#kh|}7baZf&dHl-qkYQ2%Y~7dAefzQY*!&^T3gt)2eDz&5s8)gM*Ia*=+zb~g zIOCGxZEPqhr`dO^|L{MCQqrt5HM3-~I2zKwpzeb7aN)7Y-Wi8|trDbK^exW-OwwrV&!7+%)_^)a*I5av$Kw$cceG?Na z!Xof<`B)xex<%VyCyoG12~MU>sw-}CaZItqOIv*o2|oqL7ci>zuCKr_Kh}@!=M;;v ztp(!J($Y+7wD-Il23h@xiGPSX%C;KYgIYslN)RnZg108dQ5& zDk{eO({Ew=&jbt#Z=~XDt`mQVPOP>dZF!OQ1t(Hl+RvIhD0^ymE%Tjgr;x-oELPkYRrt=%R}FTZEVGC(msYvmTng^j0h8C;+Ee4yklG zu>sG}j-|J@`m*NLD2|3eOuZZ)L||pS1(|xGM{E*^pgbBf>6tLnrV!8kq9clgZR_5;3J(GcRVUS# zP45ei?p`u{9LeuOB_=ptDIzK|Jm^~BgcqE{=;mQ`Am3qj*&0S)Ob(~3*CYpD`|Yia zL?$GZPg1UFUCwu;VjbI9AVbFz$#6vtJs%$JstK-A^gj{1ThpyLTd_`b%I&YRx}04J zh~($!rvD=1+XA2zBqSrwYz*=TEwZ$-gSs z%2W9z!?K^EUD_=AlkBeQ0O~bEps47h{|hBN1vB#~19JD=7Nyas{Qj!$M&+VJ9CmeP z3c@zh`2pSH7r)9V6YPmEB3C;jW6PMCC$w zY<*?z1`el3N49cohsl8~0k9OrJ9Zz6dOgv8_e2!L_B4apayGis@#fU0K>`Q+3IJhv zKO82^Nxu=OSX1|x#i~}xOkScmyZ25ux3FMELxEXGb?KpBeO|Q}vAUBU!y{Tmr zyAU548X$TaV7Sg7+mRaZCi#VG+z3^!6g}{9#r}3H7p`mOWZXwh_iyE55DrQH$@kpA z4mgl;Z;1HQ#!~pY`w<#+_VC)|)i<20gd;_uwdqgX8kyt$0RC#n-OHb{iVKsK5skaa z!YSP`PX%?bnDVV+|iz`&Rr@$7I>C(yAD*d~iWao;CdPvvk~|U3{-&Tjv>7Ul6gSzMu3n zkOc`x%6+?O-N(gg(wCkr8Id(0@9!H~7vmWbxYhi7+b(nV*dh1SvCB4eOL`-KSN|zS zqE&NrqQgvZsmVITz49-6*JPvlgl^@|^>b?79-ZLh3E?MxaRhATfq*Rgrse7;lP)3{ z+|B6bE5@Uko&;nL(+Xp|XsX#2=mb#yU7i!OC7uZS)_92Ax=4Z9#}ao;t4df^**Ph$dj%l%}YA<}%ZP@`Fo`;-fMA;?Tf z_kZPHJhp#}rw;m)s4LOwRY}PqlE=adCs-{##zwBv!2NXhZarzjyX4;jgE}^Nm>Sy_ z@kkix-Y^eTv=j;gEM)S5MOML9i(H8-S1$ zXA%$T`ra)GH#>j!*e1y5cCG8`{$aoJoOiQ!DF?eA!=3XlEE-^#+%%uYaIIpUOvB`7 z3o%jfCM{W}=VZ?>vnH2bYk|x14qvf;zn24m4`FVHTnOX@>0f(C_Nd=wg||D7A~eW; z?XhKq>g%GlwK3sgwZ8;4Dp>0#qXbsM?!E#bA-cI&O4dSYh1c#l>;&RITk(rvVQZ-@Vo6cmyQ-HtA$@LdOUqg8WIDd)= zDui~f6~65@vCOlT%0vfxx$ouy%UREe_$uNK7w~W=Ff`zahY0pxwJ#>>as*a9t{GCe zhE89k99pIC8R}vB2wVWp3;>P*`7aT5ozYIb8PAALSYjOHvIzdQeO|$<_pNp{RJr94 z8|Lk$_sN2y0*&?|?%XUB=iIL!VjW0}_3`Dl9p;&SSde1K&B6!FAGZnxUl;%W>KW!%8TRu-oRf5?sfqaKOD=erEFx6_RRe3ge~}Jt6<$9KoF%Nns05 zh_#sVrhDrTyFjU$cLMdf{W#CRJdWHxuxxR*Mt|Wiq#^?!vp&V< zK2sxie}^gQU;MUaO^*|nI7fjZuH3k%Chg)1W4m6(Ev7bBDJhC;qAA;)?-xlYh{ zCoA$V-=7h?mW_HPEs4UO~XPRuX+V`9>a zOf((q7D>h|a(gcigv~ zo9cWIsYOv~B1~NpL1Nl99dVVVJPl!)F<~T;; z0wB}LuMJAI-NF}%<&c1lhsm+9RFtKOVxbb8@bFPAwI3*DCr2xNV` zL}nUt)x`%tiX$BIzl9OdM%&Z^UM?yWo2F$Rjy`mh7E*YOEX=H#aNdy}OQyWnUFF>K5qNlA!TMd$}7Oc8m;o0HKoPfr@sIPV01 zE`%EKp5JA0LFdyNb5<5G4d1OR{G{Rq|ALLy> z@nvIIC*CAu92T|qS}wY5>Sw3=R#wljUG;6$6|(}g^AqWW)nk2R<3x2xVbRe(Z8Hy8vl6DTt|t znVDH*j#&Ydv3lOo;O@5gEH2cyG+I4Bw?p2;!lJ2TOA1LXJtc!4clxM zQDpRtjPfwD4NGQ(eG8G!kTRW}cuIZtUFl~7iQ^S7@V-`ic@b)noy}n$qczLeRbJ=o zAxT@4^y?lrVWgRwtgNhZD90de-2dTLEAo>jF$ZNt$PhUH4bfN4%H$X~?}7Y8LHTQ{ zAyNsV&sRk$DJcRG5fNEvHi&?c6EA5x@9u+73<%w|v9Ehk!nJNOJ^L5iW#OsomIX~d zA5V6O4a*Gc%)fX0Ksh+#Yh=y4)EJwOjAd*X+<-1ad;69b*k% z794q758qJNv3my2MR(BIlY%1z%HI6CxG`B6CEthbicNg=lQ8}tDlvq05#*JkuU3%f z_h%twf1@0Obz9s?>?e7h+>iO1q_8p$BEW$3f_oDWu zu!0(t-Pe7j^a-_-rPY!S<3f7HrhF6XOs7u)W zyGnj5J-aPo4F7EZ(rK?xhb-=7WmUS4>=+QNjP74wU!NOD;SEPolAM_aB_X2qTx_w^ zZuAwc*$>W{!;b%w)HZylU1zv!z)V?!%ux6Hx5es|%l5PBrmTR$-NoyJq#W-ngBNY4 zq#v}jv~TD&glT}Q)AddrDXCs(07Op0NFZ6(_I)@fq;_&fQLQa?V4%TQLICpfX;Z(| z{nNm}0B=FPk~o~`F^47bu6KuONNy5O4@ZgO4c5F}&!~&pYF%(y)nCso$8{(UGN6xhO-3U1bH29Z8FTR zd$Rh)$Hy;i|NEz`DA0?~0fZ&2+GmeJ9-B<6C^b_=wz$wmX7468Q~>aaC@MAIzVWl7 zAH;NdXlO`PoK$twEKo^S*22Ld&*>|F(-I@$x;k}=$8=(2Uz_yt(s^tX+5493 ztr8Mpja?kf9+;LoBBjy>2Ovz%Zl_LmF9xwsSm|Lp3}H-oT|90s=wG*pu(rXG=?;6s z=kYsOpDi28vAY-fD#^i$P%;cH>;{)dy$2{LVVn`x$H3S`r73<%`K^C8^Qo^0J@aP# z`fkt0r^8+K!>`A(j%FJZ!%~y97e#~Yf6t|O8b6Yg%P&gAxc}2tl$zGSIr1y*v{+f; zYST32&o)csx2y}n2#At`<<&Dk)dV)5`(0aOuWZK2YloaB^_L1luKU@vho}q5s)zw= zivXPrm(E`)iKy>1)|h0kURv47VM%V&(QM+3Q}uxUMTk-u@v3_mUB~E3ArKZaIxfNY#y6MJt)b`MwmBb`X1s)nlQFhe1~ihAJM;$LHCKFlSNJArFJX4ji4sskraI?J_o<$?bE! zf2dUFKliyt+n1h!VUkI)qX>Wqdp9>UH{a>>Ex1-3Dd4Ifbv(KwEk#nrD@2 z!h8T@SK&!kM^{rIw~}GXJh~muS%q+Y0*NBZD=8^md$x}|jhXdZqqs@#w^Pnt%I}T8 zW5%_;yV^%HJ}oocU?bz{n6+DObc4-TSf@dyT>$0&#p9bgKrr7;kn+5MrtKkidXj~Y zMID`?%A&w@CCx8ZtUBvqHRMf_meEv?OJ;_%6UpYB&-GW4-B>{U5h z@+)`;IhuU$rcZ5Rr&+HUA$3UT)PmSlS(jsQg83oc?yIr!CU3i^q82bS29D+IJDG)w zr6E{;2vfZy%!xb4=Qps42oGOWR#&HCtoQ}3-NW>U4GhtUVIzanveA8>{vc0uP{`%# zZj?t+DSR<<+rt>BAi~4DwDIK|X%`2st_b;_CHD%}}%l5EbVo#e~gddBNlYtBgql zq4=7sN#h9qqS7~W)J@f}4KUV2yzl%=Pkk*@Zczd_uaxrSNmnXc@Tl@$Q9(Nen&|`L2$Xm zEY>PX^co>Intj(hX|7me)V8Zl0CDq5P2Ef5jqH{{PDLVx<50fM84Qr69Y_=lp`f7b z64z{A7pE|o`}6;{j{SIQ9HoLtJjyc6G!p6eiQL}>X+I%YcN(Wqd6GX)*$Pnkm)0x!lnmH3}k66b{R=CfRmJatOh${(eZN3Y=QjaAL_H8q>N|-=x!Ybj;-kn|u*|eVI$M zw1>v`nkF;8byt}QIX8?*`>vg=9KEC-8w?I+Hy@}kkD{&9Sy&y$9;$4Qx zljk2_PTHBjY*QbrB~O|2@bP`En0tBJME5PT@dV=Hu4*e_$&C=WDjp=+;>UJ9O$j5y zd)SDmQ|)P}EIm+L5GQ_GM0n&%X8iEsradt+Q5M?qPgOoV3=nM}Wf!XN)G4>bbEBZ} z#dkid#SwB^48unjmrUJ{t=YVEV$x67#jA69Rw_eMzZ-_sLn{UBJ}mnPxF1b0>-jc| zhBapuKEgVTT-oD1pCZ-r9w;cb_*-b_{r$Tp*u|5iX$7$6&^?Kts@mE&V{mk?VVfUd z;~Pp`!}lFbxzuJT|CCafkX`3p#x$^c7O=s8I?YUMF_8N(Ps3mGYEby$-0VXRJ(vuY z;QdH$NJ|n05WC-7)M&de+cxsSEFXcZ9@?wkJA}gnKQo_c@?BFyL%#vVlAx z6@Vy&arZa#K|1S_M-x{3t6;<@8OEJm!yn6#42TJU0i(pak;7}ne;Lyx@EWyRA*gUd z{K{Bw11Z%1{Q1+CoS5k9)y((nDozU}RvO8lf1BZ3d9O+CidiyeU$^4UD*k`MJtd5b zsKC?#vdsc24GxNHmSs!OS%1j&7wJbQSZ!;-(PYUjf`fxmpWZ)zm?ER2+v#S|k36yd zDc^H@0y9KKYQ>79(0WVuzQb!(IIIh1R0cgsQ^WvRcEJa4X5Jhxi^PBb?nFN$Mj(5g zh7KHfL`?6K4)KX(zlfiIuv4Hgiqe3+`7?4L8>@)O>+wh8HtT^U8j!LDZ}IfvGhB*c}H8i@g}E|8(O!5fPeGLy!l=HFoyyyJDVswm%b@K0{0sioM* zY9{ABb*hc6KLaL0_62Wt^j!vHN#Lk{oY?q!2O z5;b$G%UXtu;#(KZ>~R^fXZXcTv_MhABkMkGTGOZ}@|!u#D{sJF4BIy%GV-zS*!}R} zz9>FbVO{|rC_0QU5%q*ppaMdbWZP!^u$f+?7M5_-f?|X`U}Abn#h|E8^#8N$N#xy~ WSHd$<2M9Kp49H3;N>+&*1^z$Ps{!@^ literal 0 HcmV?d00001 diff --git a/cookbook/static/assets/favicon-32x32.png b/cookbook/static/assets/logo_color_32.png similarity index 100% rename from cookbook/static/assets/favicon-32x32.png rename to cookbook/static/assets/logo_color_32.png diff --git a/cookbook/static/assets/logo_color512.png b/cookbook/static/assets/logo_color_512.png similarity index 100% rename from cookbook/static/assets/logo_color512.png rename to cookbook/static/assets/logo_color_512.png diff --git a/cookbook/static/assets/logo_color.svg b/cookbook/static/assets/logo_color_svg.svg similarity index 100% rename from cookbook/static/assets/logo_color.svg rename to cookbook/static/assets/logo_color_svg.svg diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html index db70ed89f2..6c5f425de9 100644 --- a/cookbook/templates/base.html +++ b/cookbook/templates/base.html @@ -3,6 +3,8 @@ {% load theming_tags %} {% load custom_tags %} +{% theme_values request as theme_values %} + {% block title %} @@ -11,23 +13,18 @@ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="robots" content="noindex,nofollow"/> - - <link rel="shortcut icon" type="image/x-icon" href="{% static 'assets/favicon.svg' %}"> - <link rel="shortcut icon" href="{% static 'assets/favicon.svg' %}"> - <link rel="icon" type="image/png" href="{% static 'assets/favicon-32x32.png' %}" sizes="32x32"> - <link rel="icon" type="image/png" href="{% static 'assets/favicon-16x16.png' %}" sizes="16x16"> - - <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="icon" href="{{ theme_values.logo_color_svg }}"> + <link rel="icon" href="{{ theme_values.logo_color_32 }}" sizes="32x32"> + <link rel="icon" href="{{ theme_values.logo_color_128 }}" sizes="128x128"> + <link rel="icon" href="{{ theme_values.logo_color_192 }}" sizes="192x192"> + <link rel="apple-touch-icon" href="{{ theme_values.logo_color_180 }}" sizes="180x180"> <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="{% nav_bg_color request %}"> + <meta name="msapplication-TileImage" content="{{ theme_values.logo_color_144 }}"> - <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161616"> - <meta name="msapplication-TileColor" content="#161616"> - <meta name="theme-color" content="#ffffff"> + <meta name="theme-color" content="{% nav_bg_color request %}"> <meta name="apple-mobile-web-app-capable" content="yes"/> diff --git a/cookbook/templatetags/theming_tags.py b/cookbook/templatetags/theming_tags.py index 81543e79d5..6358f0167f 100644 --- a/cookbook/templatetags/theming_tags.py +++ b/cookbook/templatetags/theming_tags.py @@ -8,6 +8,33 @@ register = template.Library() +@register.simple_tag +def theme_values(request): + # TODO move all theming values to this tag to prevent double queries + tv = { + 'logo_color_32': static('assets/logo_color_32.png'), + 'logo_color_128': static('assets/logo_color_128.png'), + 'logo_color_144': static('assets/logo_color_144.png'), + 'logo_color_180': static('assets/logo_color_180.png'), + 'logo_color_192': static('assets/logo_color_192.png'), + 'logo_color_512': static('assets/logo_color_512.png'), + 'logo_color_svg': static('assets/logo_color_svg.svg'), + } + space = None + if request.space: + space = request.space + if UNAUTHENTICATED_THEME_FROM_SPACE > 0: # TODO load unauth space setting on boot in settings.py and use them here + with scopes_disabled(): + space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first() + + for logo in list(tv.keys()): + print(f'looking for {logo} in {space} has logo {getattr(space, logo, None)}') + if logo.startswith('logo_color_') and getattr(space, logo, None): + tv[logo] = getattr(space, logo).file.url + + return tv + + @register.simple_tag def theme_url(request): themes = { @@ -20,7 +47,7 @@ def theme_url(request): } if not request.user.is_authenticated: - if UNAUTHENTICATED_THEME_FROM_SPACE > 0: # TODO load unauth space setting on boot in settings.py and use them here + if UNAUTHENTICATED_THEME_FROM_SPACE > 0: # TODO load unauth space setting on boot in settings.py and use them here with scopes_disabled(): return static(themes[Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first().space_theme]) else: diff --git a/cookbook/urls.py b/cookbook/urls.py index 1b1e6548f0..9566541a28 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -162,8 +162,7 @@ def extend(self, r): path('service-worker.js', (TemplateView.as_view(template_name="sw.js", content_type='application/javascript', )), name='service_worker'), - path('manifest.json', (TemplateView.as_view(template_name="manifest.json", content_type='application/json', )), - name='web_manifest'), + path('manifest.json', views.web_manifest, name='web_manifest'), ] generic_models = ( diff --git a/cookbook/views/views.py b/cookbook/views/views.py index dc8d9f102e..328ea65ecd 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -1,3 +1,4 @@ +import json import os import re from datetime import datetime @@ -14,8 +15,9 @@ from django.core.exceptions import ValidationError from django.core.management import call_command 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.templatetags.static import static from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.translation import gettext as _ @@ -335,13 +337,16 @@ def system(request): database_message = _('Everything is fine!') elif postgres_ver < postgres_current - 2: 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: 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: 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 @@ -366,10 +371,12 @@ def system(request): pass else: 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(): - 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', { 'gunicorn_media': settings.GUNICORN_MEDIA, @@ -431,7 +438,8 @@ def invite_link(request, token): link.used_by = request.user 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: user_space.active = True @@ -472,6 +480,65 @@ def report_share_abuse(request, token): 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): return render(request, 'markdown_info.html', {}) From f9bfb8e258217e3fc8808cb94e0a4e9eb8365788 Mon Sep 17 00:00:00 2001 From: vabene1111 <vabene1234@googlemail.com> Date: Sun, 7 Jan 2024 08:12:20 +0800 Subject: [PATCH 08/16] added custom logo to space manage view --- cookbook/serializer.py | 10 +++++- .../apps/SpaceManageView/SpaceManageView.vue | 31 +++++++++++++++++++ vue/src/locales/en.json | 4 ++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index bb7bbf62de..d4f7bd4710 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -283,6 +283,13 @@ class SpaceSerializer(WritableNestedModelSerializer): 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) + 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): return UserSpace.objects.filter(space=obj).count() @@ -304,7 +311,8 @@ class Meta: fields = ( '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', - '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 = ( 'id', 'created_by', 'created_at', 'max_recipes', 'max_file_storage_mb', 'max_users', 'allow_sharing', 'demo',) diff --git a/vue/src/apps/SpaceManageView/SpaceManageView.vue b/vue/src/apps/SpaceManageView/SpaceManageView.vue index 3dab190af2..01bf7e2395 100644 --- a/vue/src/apps/SpaceManageView/SpaceManageView.vue +++ b/vue/src/apps/SpaceManageView/SpaceManageView.vue @@ -191,6 +191,37 @@ </b-form-select> </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-col> </b-row> diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index 5dc720492a..e085bd9257 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -284,7 +284,9 @@ "CustomTheme": "Custom Theme", "CustomThemeHelp": "Override styles of the selected theme by uploading a custom CSS file.", "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", "MoveCategory": "Move To: ", "CountMore": "...+{count} more", From 1dda4126c1dc67d307310ee17732910e15ebe688 Mon Sep 17 00:00:00 2001 From: vabene1111 <vabene1234@googlemail.com> Date: Sun, 7 Jan 2024 16:55:12 +0800 Subject: [PATCH 09/16] fxied theming tags --- cookbook/templatetags/theming_tags.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cookbook/templatetags/theming_tags.py b/cookbook/templatetags/theming_tags.py index 6358f0167f..97bf48d7b7 100644 --- a/cookbook/templatetags/theming_tags.py +++ b/cookbook/templatetags/theming_tags.py @@ -49,7 +49,9 @@ def theme_url(request): if not request.user.is_authenticated: if UNAUTHENTICATED_THEME_FROM_SPACE > 0: # TODO load unauth space setting on boot in settings.py and use them here with scopes_disabled(): - return static(themes[Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first().space_theme]) + theme = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first().space_theme + if theme in themes: + return static(themes[theme]) else: return static('themes/tandoor.min.css') @@ -68,7 +70,9 @@ def custom_theme(request): return request.space.custom_space_theme.file.url elif UNAUTHENTICATED_THEME_FROM_SPACE > 0: with scopes_disabled(): - return Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first().custom_space_theme.file.url + space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first() + if space.custom_space_theme: + return space.custom_space_theme.file.url @register.simple_tag @@ -109,7 +113,7 @@ def nav_text_color(request): if UNAUTHENTICATED_THEME_FROM_SPACE > 0: with scopes_disabled(): space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first() - if space.nav_text_color: + if space.nav_text_color and space.nav_text_color in type_mapping: return type_mapping[space.nav_text_color] return 'navbar-light' else: From 5a5ce4d736bf7cd3030767de19872948b755dfc3 Mon Sep 17 00:00:00 2001 From: vabene1111 <vabene1234@googlemail.com> Date: Sun, 7 Jan 2024 17:17:14 +0800 Subject: [PATCH 10/16] moved theming functions to main tag --- cookbook/templates/base.html | 6 ++-- cookbook/templatetags/theming_tags.py | 40 +++++++++++++++++++++++---- recipes/settings.py | 2 +- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html index 6c5f425de9..3c1a92e0ba 100644 --- a/cookbook/templates/base.html +++ b/cookbook/templates/base.html @@ -29,9 +29,9 @@ <meta name="apple-mobile-web-app-capable" content="yes"/> <!-- Bootstrap 4 --> - <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"> + <link id="id_main_css" href="{{ theme_values.theme }}" rel="stylesheet"> + {% if theme_values.custom_theme %} + <link id="id_custom_css" href="{{ theme_values.custom_theme }}" rel="stylesheet"> {% endif %} diff --git a/cookbook/templatetags/theming_tags.py b/cookbook/templatetags/theming_tags.py index 97bf48d7b7..1bf569538a 100644 --- a/cookbook/templatetags/theming_tags.py +++ b/cookbook/templatetags/theming_tags.py @@ -10,6 +10,14 @@ @register.simple_tag def theme_values(request): + themes = { + UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css', + UserPreference.FLATLY: 'themes/flatly.min.css', + UserPreference.DARKLY: 'themes/darkly.min.css', + UserPreference.SUPERHERO: 'themes/superhero.min.css', + UserPreference.TANDOOR: 'themes/tandoor.min.css', + UserPreference.TANDOOR_DARK: 'themes/tandoor_dark.min.css', + } # TODO move all theming values to this tag to prevent double queries tv = { 'logo_color_32': static('assets/logo_color_32.png'), @@ -19,6 +27,8 @@ def theme_values(request): 'logo_color_192': static('assets/logo_color_192.png'), '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]) } space = None if request.space: @@ -32,6 +42,24 @@ def theme_values(request): if logo.startswith('logo_color_') and getattr(space, logo, None): tv[logo] = getattr(space, logo).file.url + with scopes_disabled(): + 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() + if space: + if space.custom_space_theme: + tv['custom_theme'] = space.custom_space_theme.file.url + if space.space_theme in themes: + return static(themes[space.space_theme]) + + if request.user.is_authenticated: + if request.space.custom_space_theme: + tv['custom_theme'] = request.space.custom_space_theme.file.url + if request.space.space_theme in themes: + tv['theme'] = themes[request.space.space_theme] + else: + tv['theme'] = themes[request.user.userpreference.theme] + return tv @@ -54,14 +82,14 @@ def theme_url(request): return static(themes[theme]) else: return static('themes/tandoor.min.css') - - if request.space.space_theme in themes: - return static(themes[request.space.space_theme]) else: - if request.user.userpreference.theme in themes: - return static(themes[request.user.userpreference.theme]) + if request.space.space_theme in themes: + return static(themes[request.space.space_theme]) else: - raise AttributeError + if request.user.userpreference.theme in themes: + return static(themes[request.user.userpreference.theme]) + else: + return static('themes/tandoor.min.css') @register.simple_tag diff --git a/recipes/settings.py b/recipes/settings.py index 644bf8a446..a22cd39429 100644 --- a/recipes/settings.py +++ b/recipes/settings.py @@ -57,7 +57,7 @@ FRACTION_PREF_DEFAULT = bool(int(os.getenv('FRACTION_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))) -UNAUTHENTICATED_THEME_FROM_SPACE = int(os.getenv('UNAUTHENTICATED_THEME_FROM_SPACE', 0)) +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 SHOPPING_MIN_AUTOSYNC_INTERVAL = int( From 54e2615c86e1e898033a7d91f1ee66f514d7e342 Mon Sep 17 00:00:00 2001 From: vabene1111 <vabene1234@googlemail.com> Date: Sun, 7 Jan 2024 17:24:20 +0800 Subject: [PATCH 11/16] cleaned up into single flow --- cookbook/templatetags/theming_tags.py | 79 ++++++--------------------- 1 file changed, 16 insertions(+), 63 deletions(-) diff --git a/cookbook/templatetags/theming_tags.py b/cookbook/templatetags/theming_tags.py index 1bf569538a..7f0f0e9c84 100644 --- a/cookbook/templatetags/theming_tags.py +++ b/cookbook/templatetags/theming_tags.py @@ -28,79 +28,32 @@ def theme_values(request): '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]) + 'theme': static(themes[UserPreference.TANDOOR]), + 'nav_logo_url': 'assets/brand_logo.png', } space = None if request.space: space = request.space - if UNAUTHENTICATED_THEME_FROM_SPACE > 0: # TODO load unauth space setting on boot in settings.py and use them here + 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() - for logo in list(tv.keys()): - print(f'looking for {logo} in {space} has logo {getattr(space, logo, None)}') - if logo.startswith('logo_color_') and getattr(space, logo, None): - tv[logo] = getattr(space, logo).file.url + if request.user.is_authenticated: + if request.user.userpreference.theme in themes: + tv['theme'] = static(themes[request.user.userpreference.theme]) - with scopes_disabled(): - 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() - if space: - if space.custom_space_theme: - tv['custom_theme'] = space.custom_space_theme.file.url - if space.space_theme in themes: - return static(themes[space.space_theme]) - - if request.user.is_authenticated: - if request.space.custom_space_theme: - tv['custom_theme'] = request.space.custom_space_theme.file.url - if request.space.space_theme in themes: - tv['theme'] = themes[request.space.space_theme] - else: - tv['theme'] = themes[request.user.userpreference.theme] - - return tv + if space: + for logo in list(tv.keys()): + print(f'looking for {logo} in {space} has logo {getattr(space, logo, None)}') + if logo.startswith('logo_color_') and getattr(space, logo, None): + tv[logo] = getattr(space, logo).file.url - -@register.simple_tag -def theme_url(request): - themes = { - UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css', - UserPreference.FLATLY: 'themes/flatly.min.css', - UserPreference.DARKLY: 'themes/darkly.min.css', - UserPreference.SUPERHERO: 'themes/superhero.min.css', - UserPreference.TANDOOR: 'themes/tandoor.min.css', - UserPreference.TANDOOR_DARK: 'themes/tandoor_dark.min.css', - } - - if not request.user.is_authenticated: - if UNAUTHENTICATED_THEME_FROM_SPACE > 0: # TODO load unauth space setting on boot in settings.py and use them here - with scopes_disabled(): - theme = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first().space_theme - if theme in themes: - return static(themes[theme]) - else: - return static('themes/tandoor.min.css') - else: - if request.space.space_theme in themes: - return static(themes[request.space.space_theme]) - else: - if request.user.userpreference.theme in themes: - return static(themes[request.user.userpreference.theme]) - else: - return static('themes/tandoor.min.css') - - -@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 - elif UNAUTHENTICATED_THEME_FROM_SPACE > 0: - with scopes_disabled(): - space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first() if space.custom_space_theme: - return space.custom_space_theme.file.url + tv['custom_theme'] = space.custom_space_theme.file.url + if space.space_theme in themes: + tv['theme'] = static(themes[space.space_theme]) + + return tv @register.simple_tag From 6f3d4491ed1120b3e7ef86072352a2a10629dbc3 Mon Sep 17 00:00:00 2001 From: vabene1111 <vabene1234@googlemail.com> Date: Sun, 7 Jan 2024 17:51:33 +0800 Subject: [PATCH 12/16] implemented user settings --- cookbook/templates/base.html | 4 +-- cookbook/templatetags/theming_tags.py | 35 ++++++++++++--------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html index 3c1a92e0ba..6193be1fe7 100644 --- a/cookbook/templates/base.html +++ b/cookbook/templates/base.html @@ -84,7 +84,7 @@ {% 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' %}" 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> {% endif %} {% endif %} @@ -98,7 +98,7 @@ {% 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' %}" 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> {% endif %} {% endif %} diff --git a/cookbook/templatetags/theming_tags.py b/cookbook/templatetags/theming_tags.py index 7f0f0e9c84..13ee1cae06 100644 --- a/cookbook/templatetags/theming_tags.py +++ b/cookbook/templatetags/theming_tags.py @@ -18,7 +18,9 @@ def theme_values(request): UserPreference.TANDOOR: 'themes/tandoor.min.css', UserPreference.TANDOOR_DARK: 'themes/tandoor_dark.min.css', } - # TODO move all theming values to this tag to prevent double queries + nav_text_type_mapping = {Space.DARK: 'navbar-light', + Space.LIGHT: 'navbar-dark'} # inverted since navbar-dark means the background + tv = { 'logo_color_32': static('assets/logo_color_32.png'), 'logo_color_128': static('assets/logo_color_128.png'), @@ -29,7 +31,9 @@ def theme_values(request): 'logo_color_svg': static('assets/logo_color_svg.svg'), 'custom_theme': None, 'theme': static(themes[UserPreference.TANDOOR]), - 'nav_logo_url': 'assets/brand_logo.png', + 'nav_logo': static('assets/brand_logo.png'), + 'nav_bg_color': '#ddbf86', + 'nav_text_class': 'navbar-light', } space = None if request.space: @@ -41,35 +45,28 @@ def theme_values(request): if request.user.is_authenticated: if request.user.userpreference.theme in themes: tv['theme'] = static(themes[request.user.userpreference.theme]) + if request.user.userpreference.nav_bg_color: + tv['nav_bg_color'] = request.user.userpreference.nav_bg_color + if request.user.userpreference.nav_text_color and request.user.userpreference.nav_text_color in nav_text_type_mapping: + tv['nav_text_class'] = request.user.userpreference.nav_text_color if space: for logo in list(tv.keys()): - print(f'looking for {logo} in {space} has logo {getattr(space, logo, None)}') if logo.startswith('logo_color_') and getattr(space, logo, None): tv[logo] = getattr(space, logo).file.url - if space.custom_space_theme: tv['custom_theme'] = space.custom_space_theme.file.url if space.space_theme in themes: tv['theme'] = static(themes[space.space_theme]) - + if space.nav_logo: + tv['nav_logo'] = space.nav_logo.file.url + if space.nav_bg_color: + tv['nav_bg_color'] = space.nav_bg_color + if space.nav_text_color and space.nav_text_color in nav_text_type_mapping: + tv['nav_text_class'] = nav_text_type_mapping[space.nav_text_color] return tv -@register.simple_tag -def logo_url(request): - if not request.user.is_authenticated: - if UNAUTHENTICATED_THEME_FROM_SPACE > 0: - with scopes_disabled(): - space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first() - if getattr(space, 'nav_logo', None): - return space.nav_logo.file.url - if request.user.is_authenticated and getattr(getattr(request, "space", {}), 'nav_logo', None): - return request.space.nav_logo.file.url - else: - return static('assets/brand_logo.png') - - @register.simple_tag def nav_bg_color(request): if not request.user.is_authenticated: From c8e674da16ac9bcae830d613f2be20451686487b Mon Sep 17 00:00:00 2001 From: vabene1111 <vabene1234@googlemail.com> Date: Sun, 7 Jan 2024 18:07:47 +0800 Subject: [PATCH 13/16] all themes in one function --- cookbook/templates/base.html | 8 +++---- cookbook/templatetags/theming_tags.py | 34 --------------------------- 2 files changed, 4 insertions(+), 38 deletions(-) diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html index 6193be1fe7..8eb26d97d9 100644 --- a/cookbook/templates/base.html +++ b/cookbook/templates/base.html @@ -21,10 +21,10 @@ <link rel="manifest" crossorigin="use-credentials" href="{% url 'web_manifest' %}"> - <meta name="msapplication-TileColor" content="{% nav_bg_color request %}"> + <meta name="msapplication-TileColor" content="{{ theme_values.nav_bg_color }}"> <meta name="msapplication-TileImage" content="{{ theme_values.logo_color_144 }}"> - <meta name="theme-color" content="{% nav_bg_color request %}"> + <meta name="theme-color" content="{{ theme_values.nav_bg_color }}"> <meta name="apple-mobile-web-app-capable" content="yes"/> @@ -76,9 +76,9 @@ </head> <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" - style="{% sticky_nav request %}; background-color: {% nav_bg_color request %}"> + style="{% sticky_nav request %}; background-color: {{ theme_values.nav_bg_color }}"> {% if not request.user.userpreference.left_handed %} {% if not request.user.is_authenticated or request.user.userpreference.nav_show_logo %} diff --git a/cookbook/templatetags/theming_tags.py b/cookbook/templatetags/theming_tags.py index 13ee1cae06..eb3d5ca63f 100644 --- a/cookbook/templatetags/theming_tags.py +++ b/cookbook/templatetags/theming_tags.py @@ -67,40 +67,6 @@ def theme_values(request): return tv -@register.simple_tag -def nav_bg_color(request): - if not request.user.is_authenticated: - if UNAUTHENTICATED_THEME_FROM_SPACE > 0: - with scopes_disabled(): - space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first() - if space.nav_bg_color: - return space.nav_bg_color - return '#ddbf86' - else: - 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: - if UNAUTHENTICATED_THEME_FROM_SPACE > 0: - with scopes_disabled(): - space = Space.objects.filter(id=UNAUTHENTICATED_THEME_FROM_SPACE).first() - if space.nav_text_color and space.nav_text_color in type_mapping: - return type_mapping[space.nav_text_color] - return 'navbar-light' - 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 ( From 761e423bdeccb75125d2a7d732aa3566b6131b51 Mon Sep 17 00:00:00 2001 From: vabene1111 <vabene1234@googlemail.com> Date: Sun, 7 Jan 2024 18:17:31 +0800 Subject: [PATCH 14/16] fixed markdown info --- cookbook/templates/markdown_info.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cookbook/templates/markdown_info.html b/cookbook/templates/markdown_info.html index 853fe14fa0..b885043f06 100644 --- a/cookbook/templates/markdown_info.html +++ b/cookbook/templates/markdown_info.html @@ -129,7 +129,7 @@ <h2>{% trans 'Images & Links' %}</h2> [](https://github.com/vabene1111/recipes) [GitHub](https://github.com/vabene1111/recipes) - ![{% trans 'This will become an image' %}]({% static 'assets/favicon.svg' %}) + ![{% trans 'This will become an image' %}]({% static 'assets/logo_color_svg.svg' %}) </code></pre> <div style="text-align: center"> @@ -142,7 +142,7 @@ <h2>{% trans 'Images & Links' %}</h2> <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">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"> </div> From 71e5484f0ce946c9e54db326e962cf63e3ea3aa2 Mon Sep 17 00:00:00 2001 From: vabene1111 <vabene1234@googlemail.com> Date: Sun, 7 Jan 2024 22:34:59 +0800 Subject: [PATCH 15/16] test for theming function + sticky nav --- cookbook/templates/base.html | 2 +- cookbook/templatetags/theming_tags.py | 44 +++++++++++++-------------- cookbook/tests/other/test_theming.py | 36 ++++++++++++++++++++++ 3 files changed, 59 insertions(+), 23 deletions(-) create mode 100644 cookbook/tests/other/test_theming.py diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html index 8eb26d97d9..d84ccbd46c 100644 --- a/cookbook/templates/base.html +++ b/cookbook/templates/base.html @@ -78,7 +78,7 @@ <nav class="navbar navbar-expand-lg {{ theme_values.nav_text_class }}" id="id_main_nav" - style="{% sticky_nav request %}; background-color: {{ theme_values.nav_bg_color }}"> + style="{{ theme_values.sticky_nav }}; background-color: {{ theme_values.nav_bg_color }}"> {% if not request.user.userpreference.left_handed %} {% if not request.user.is_authenticated or request.user.userpreference.nav_show_logo %} diff --git a/cookbook/templatetags/theming_tags.py b/cookbook/templatetags/theming_tags.py index eb3d5ca63f..f35096fd57 100644 --- a/cookbook/templatetags/theming_tags.py +++ b/cookbook/templatetags/theming_tags.py @@ -10,6 +10,17 @@ @register.simple_tag def theme_values(request): + space = None + 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 = { UserPreference.BOOTSTRAP: 'themes/bootstrap.min.css', UserPreference.FLATLY: 'themes/flatly.min.css', @@ -34,21 +45,19 @@ def theme_values(request): '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;', } - space = None - 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() - if request.user.is_authenticated: - if request.user.userpreference.theme in themes: - tv['theme'] = static(themes[request.user.userpreference.theme]) - if request.user.userpreference.nav_bg_color: - tv['nav_bg_color'] = request.user.userpreference.nav_bg_color - if request.user.userpreference.nav_text_color and request.user.userpreference.nav_text_color in nav_text_type_mapping: - tv['nav_text_class'] = request.user.userpreference.nav_text_color + + if user.is_authenticated: + if user.userpreference.theme in themes: + tv['theme'] = static(themes[user.userpreference.theme]) + 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: for logo in list(tv.keys()): @@ -65,12 +74,3 @@ def theme_values(request): if space.nav_text_color and space.nav_text_color in nav_text_type_mapping: tv['nav_text_class'] = nav_text_type_mapping[space.nav_text_color] return tv - - -@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 '' diff --git a/cookbook/tests/other/test_theming.py b/cookbook/tests/other/test_theming.py new file mode 100644 index 0000000000..d9aa0189bd --- /dev/null +++ b/cookbook/tests/other/test_theming.py @@ -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' From d493ba72a1546fd6568c177580778cd905f07211 Mon Sep 17 00:00:00 2001 From: vabene1111 <vabene1234@googlemail.com> Date: Mon, 8 Jan 2024 20:24:23 +0800 Subject: [PATCH 16/16] fixed mealplan to date set wrongly when open multiple times --- vue/src/components/MealPlanEditModal.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vue/src/components/MealPlanEditModal.vue b/vue/src/components/MealPlanEditModal.vue index 6be3505965..ddaae2027c 100644 --- a/vue/src/components/MealPlanEditModal.vue +++ b/vue/src/components/MealPlanEditModal.vue @@ -212,7 +212,7 @@ export default { 'entryEditing.from_date': { handler(newVal, oldVal) { if (newVal !== undefined && oldVal !== undefined) { - if (newVal !== oldVal) { + if (newVal !== oldVal && newVal !== this.entryEditing.to_date) { let change = Math.abs(moment(oldVal).diff(moment(this.entryEditing.to_date), 'days')) // even though negative numbers might be correct, they would be illogical as to needs to always be larger than from this.entryEditing.to_date = moment(newVal).add(change, 'd').format("YYYY-MM-DD") }