diff --git a/.env.template b/.env.template
index cb62dc52..a54e0fe0 100644
--- a/.env.template
+++ b/.env.template
@@ -3,6 +3,9 @@
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
diff --git a/.github/workflows/build-docker-open-data.yml b/.github/workflows/build-docker-open-data.yml
new file mode 100644
index 00000000..cdf0a023
--- /dev/null
+++ b/.github/workflows/build-docker-open-data.yml
@@ -0,0 +1,120 @@
+name: Build Docker Container with open data plugin installed
+
+on: push
+
+jobs:
+ build-container:
+ name: Build ${{ matrix.name }} Container
+ runs-on: ubuntu-latest
+ if: github.repository_owner == 'TandoorRecipes'
+ continue-on-error: ${{ matrix.continue-on-error }}
+ permissions:
+ contents: read
+ packages: write
+ strategy:
+ matrix:
+ include:
+ # Standard build config
+ - name: Standard
+ dockerfile: Dockerfile
+ platforms: linux/amd64,linux/arm64
+ suffix: ""
+ continue-on-error: false
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Get version number
+ id: get_version
+ run: |
+ if [[ "$GITHUB_REF" = refs/tags/* ]]; then
+ echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
+ elif [[ "$GITHUB_REF" = refs/heads/beta ]]; then
+ echo VERSION=beta >> $GITHUB_OUTPUT
+ else
+ echo VERSION=develop >> $GITHUB_OUTPUT
+ fi
+
+ # Update Version number
+ - name: Update version file
+ uses: DamianReeves/write-file-action@v1.2
+ with:
+ path: recipes/version.py
+ contents: |
+ VERSION_NUMBER = '${{ steps.get_version.outputs.VERSION }}-open-data'
+ BUILD_REF = '${{ github.sha }}'
+ write-mode: overwrite
+
+ # clone open data plugin
+ - name: clone open data plugin repo
+ uses: actions/checkout@master
+ with:
+ repository: TandoorRecipes/open_data_plugin
+ ref: master
+ path: ./recipes/plugins/open_data_plugin
+
+ # Build Vue frontend
+ - uses: actions/setup-node@v3
+ with:
+ node-version: '14'
+ cache: yarn
+ cache-dependency-path: vue/yarn.lock
+ - name: Install dependencies
+ working-directory: ./vue
+ run: yarn install --frozen-lockfile
+ - name: Build dependencies
+ working-directory: ./vue
+ run: yarn build
+
+ - name: Setup Open Data Plugin Links
+ working-directory: ./recipes/plugins/open_data_plugin
+ run: python setup_repo.py
+
+ - name: Build Open Data Frontend
+ working-directory: ./recipes/plugins/open_data_plugin/vue
+ run: yarn build
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+ - name: Set up Buildx
+ uses: docker/setup-buildx-action@v2
+ - name: Login to Docker Hub
+ uses: docker/login-action@v2
+ if: github.secret_source == 'Actions'
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v2
+ if: github.secret_source == 'Actions'
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ github.token }}
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v4
+ with:
+ images: |
+ vabene1111/recipes
+ ghcr.io/TandoorRecipes/recipes
+ flavor: |
+ latest=false
+ suffix=${{ matrix.suffix }}
+ tags: |
+ type=raw,value=latest,suffix=-open-data-plugin,enable=${{ startsWith(github.ref, 'refs/tags/') }}
+ type=semver,suffix=-open-data-plugin,pattern={{version}}
+ type=semver,suffix=-open-data-plugin,pattern={{major}}.{{minor}}
+ type=semver,suffix=-open-data-plugin,pattern={{major}}
+ type=ref,suffix=-open-data-plugin,event=branch
+ - name: Build and Push
+ uses: docker/build-push-action@v4
+ with:
+ context: .
+ file: ${{ matrix.dockerfile }}
+ pull: true
+ push: ${{ github.secret_source == 'Actions' }}
+ platforms: ${{ matrix.platforms }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
diff --git a/boot.sh b/boot.sh
index b6321741..0ff1fba1 100644
--- a/boot.sh
+++ b/boot.sh
@@ -4,6 +4,7 @@ source venv/bin/activate
TANDOOR_PORT="${TANDOOR_PORT:-8080}"
GUNICORN_WORKERS="${GUNICORN_WORKERS:-3}"
GUNICORN_THREADS="${GUNICORN_THREADS:-2}"
+GUNICORN_LOG_LEVEL="${GUNICORN_LOG_LEVEL:-'info'}"
NGINX_CONF_FILE=/opt/recipes/nginx/conf.d/Recipes.conf
display_warning() {
@@ -65,4 +66,4 @@ echo "Done"
chmod -R 755 /opt/recipes/mediafiles
-exec gunicorn -b :$TANDOOR_PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level INFO recipes.wsgi
+exec gunicorn -b :$TANDOOR_PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --access-logfile - --error-logfile - --log-level $GUNICORN_LOG_LEVEL recipes.wsgi
diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html
index a70e4294..30756ef4 100644
--- a/cookbook/templates/base.html
+++ b/cookbook/templates/base.html
@@ -9,6 +9,7 @@
{% endblock %}
+
@@ -49,7 +50,7 @@
@@ -349,6 +350,12 @@
{% trans 'Overview' %}
{% endif %}
+ {% plugin_dropdown_nav_templates as plugin_dropdown_nav_templates %}
+ {% for pn in plugin_dropdown_nav_templates %}
+
@@ -439,7 +447,7 @@
localStorage.setItem('STATIC_URL', "{% base_path request 'static_base' %}")
localStorage.setItem('DEBUG', "{% is_debug %}")
localStorage.setItem('USER_ID', "{{request.user.pk}}")
-
+
window.addEventListener("load", () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("{% url 'service_worker' %}", {scope: "{% base_path request 'base' %}" + '/'}).then(function (reg) {
diff --git a/cookbook/templatetags/custom_tags.py b/cookbook/templatetags/custom_tags.py
index b958999a..5ade2985 100644
--- a/cookbook/templatetags/custom_tags.py
+++ b/cookbook/templatetags/custom_tags.py
@@ -16,7 +16,7 @@ from cookbook.helper.mdx_attributes import MarkdownFormatExtension
from cookbook.helper.mdx_urlize import UrlizeExtension
from cookbook.models import Space, get_model_name
from recipes import settings
-from recipes.settings import STATIC_URL
+from recipes.settings import STATIC_URL, PLUGINS
register = template.Library()
@@ -132,6 +132,14 @@ def is_debug():
def markdown_link():
return f"{_('You can use markdown to format this field. See the ')}
{_('docs here')}"
+@register.simple_tag
+def plugin_dropdown_nav_templates():
+ templates = []
+ for p in PLUGINS:
+ if p['nav_dropdown']:
+ templates.append(p['nav_dropdown'])
+ return templates
+
@register.simple_tag
def bookmarklet(request):
diff --git a/cookbook/urls.py b/cookbook/urls.py
index 07d21005..356714d2 100644
--- a/cookbook/urls.py
+++ b/cookbook/urls.py
@@ -6,7 +6,7 @@ from rest_framework import permissions, routers
from rest_framework.schemas import get_schema_view
from cookbook.helper import dal
-from recipes.settings import DEBUG
+from recipes.settings import DEBUG, PLUGINS
from recipes.version import VERSION_NUMBER
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, MealPlan, Recipe,
@@ -16,7 +16,13 @@ from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keywor
from .views import api, data, delete, edit, import_export, lists, new, telegram, views
from .views.api import CustomAuthToken, ImportOpenData
-router = routers.DefaultRouter()
+# extend DRF default router class to allow including additional routers
+class DefaultRouter(routers.DefaultRouter):
+ def extend(self, r):
+ self.registry.extend(r.registry)
+
+
+router = DefaultRouter()
router.register(r'automation', api.AutomationViewSet)
router.register(r'bookmarklet-import', api.BookmarkletImportViewSet)
router.register(r'cook-log', api.CookLogViewSet)
@@ -56,6 +62,13 @@ router.register(r'user-space', api.UserSpaceViewSet)
router.register(r'view-log', api.ViewLogViewSet)
router.register(r'access-token', api.AccessTokenViewSet)
+for p in PLUGINS:
+ if c := locate(f'{p["module"]}.urls.{p["api_router_name"]}'):
+ try:
+ router.extend(c)
+ except AttributeError:
+ pass
+
urlpatterns = [
path('', views.index, name='index'),
path('setup/', views.setup, name='view_setup'),
@@ -122,7 +135,6 @@ urlpatterns = [
path('api/switch-active-space/
/', api.switch_active_space, name='api_switch_active_space'),
path('api/download-file//', api.download_file, name='api_download_file'),
-
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
# TODO is this deprecated? not yet, some old forms remain, could likely be changed to generic API endpoints
path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), # TODO is this deprecated?
diff --git a/recipes/settings.py b/recipes/settings.py
index e89dbc6d..d05d0373 100644
--- a/recipes/settings.py
+++ b/recipes/settings.py
@@ -80,6 +80,8 @@ DJANGO_TABLES2_PAGE_RANGE = 8
HCAPTCHA_SITEKEY = os.getenv('HCAPTCHA_SITEKEY', '')
HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '')
+FDA_API_KEY = os.getenv('FDA_API_KEY', 'DEMO_KEY')
+
SHARING_ABUSE = bool(int(os.getenv('SHARING_ABUSE', False)))
SHARING_LIMIT = int(os.getenv('SHARING_LIMIT', 0))
@@ -144,6 +146,9 @@ try:
'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 '',
+ '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 '',
}
PLUGINS.append(plugin_config)
except Exception:
@@ -412,7 +417,7 @@ for p in PLUGINS:
if p['bundle_name'] != '':
WEBPACK_LOADER[p['bundle_name']] = {
'CACHE': not DEBUG,
- 'BUNDLE_DIR_NAME': f'{p["base_path"]}/vue/', # must end with slash
+ 'BUNDLE_DIR_NAME': f'vue/', # must end with slash
'STATS_FILE': os.path.join(p["base_path"], 'vue', 'webpack-stats.json'),
'POLL_INTERVAL': 0.1,
'TIMEOUT': None,
diff --git a/requirements.txt b/requirements.txt
index 7c90b700..04679001 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,7 +17,7 @@ Markdown==3.4.3
Pillow==9.4.0
psycopg2-binary==2.9.5
python-dotenv==0.21.0
-requests==2.28.2
+requests==2.31.0
six==1.16.0
webdavclient3==3.14.6
whitenoise==6.2.0
diff --git a/vue/src/components/Modals/GenericModalForm.vue b/vue/src/components/Modals/GenericModalForm.vue
index 5012a2c7..b8b7da98 100644
--- a/vue/src/components/Modals/GenericModalForm.vue
+++ b/vue/src/components/Modals/GenericModalForm.vue
@@ -109,6 +109,10 @@ export default {
mounted() {
this.id = Math.random()
this.$root.$on("change", this.storeValue) // bootstrap modal placed at document so have to listen at root of component
+
+ if (this.models !== null){
+ this.Models = this.models // override models definition file with prop
+ }
},
computed: {
advancedForm() {
@@ -179,6 +183,7 @@ export default {
if (this.dirty) {
this.dirty = false
this.$emit("finish-action", "cancel")
+ this.$emit("hidden")
}
},
storeValue: function (field, value) {
diff --git a/vue/src/stores/GenericApiStore.js b/vue/src/stores/GenericApiStore.js
new file mode 100644
index 00000000..e69de29b
diff --git a/vue/src/utils/utils.js b/vue/src/utils/utils.js
index fda87dbb..8cb011b2 100644
--- a/vue/src/utils/utils.js
+++ b/vue/src/utils/utils.js
@@ -50,7 +50,7 @@ export class StandardToasts {
static FAIL_MOVE = "FAIL_MOVE"
static FAIL_MERGE = "FAIL_MERGE"
- static makeStandardToast(context, toast, err) {
+ static makeStandardToast(context, toast, err = undefined, always_show_errors = false) {
let title = ''
let msg = ''
let variant = ''
@@ -124,7 +124,7 @@ export class StandardToasts {
}
- let DEBUG = localStorage.getItem("DEBUG") === "True" || false
+ let DEBUG = localStorage.getItem("DEBUG") === "True" || always_show_errors
if (err !== undefined && 'response' in err && 'headers' in err.response) {
if (DEBUG && err.response.headers['content-type'] === 'application/json' && err.response.status < 500) {
@@ -311,7 +311,7 @@ export function calculateHourMinuteSplit(amount) {
let minutes = amount - hours * 60
let output_text = hours + " h"
- if (minutes > 0){
+ if (minutes > 0) {
output_text += " " + minutes + " min"
}
@@ -368,6 +368,9 @@ export const ApiMixin = {
let func = setup.function
let parameters = buildParams(options, setup)
let apiClient = new ApiApiFactory()
+ if (model.apiClient !== undefined) {
+ apiClient = model.apiClient
+ }
return apiClient[func](...parameters)
},
genericGetAPI: function (url, options) {