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 %} + + {% include pn %} + {% endfor %} + {% trans 'Markdown Guide' %} @@ -375,6 +382,7 @@ + {% message_of_the_day request as message_of_the_day %} {% if message_of_the_day %}
@@ -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) {