From a246fa1e1e977b4a778c5a21027f2d7818a177a2 Mon Sep 17 00:00:00 2001 From: andreighh <61791875+andreighh@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:46:42 +0300 Subject: [PATCH 1/4] Fix search crash, stale keyword flag and TED/UNGM config issues - Escape user search terms and keywords before building highlight regexes (special characters caused re.error / 500 on search) - Reset Tender.has_keywords when no keywords match on save - Read TED_NOTICE_URL from its own env var instead of TED_URL - Make TEDWorker.archives an instance attribute so archives are not shared between worker instances - Fail TED archive downloads on HTTP errors instead of saving the error page as a .tar.gz - Derive UNGM endpoint URLs and Host header from UNGM_ENDPOINT_URI; drop unused ENDPOINT_URI constants --- app/management/commands/add_award.py | 3 +-- app/management/commands/base/notify.py | 2 -- app/management/commands/update_ungm.py | 2 -- app/models.py | 7 +++---- app/parsers/ted.py | 10 +++++----- app/server_requests.py | 13 ++++++++----- app/views/tenders.py | 11 +++++++---- scratch/settings.py | 2 +- 8 files changed, 25 insertions(+), 25 deletions(-) diff --git a/app/management/commands/add_award.py b/app/management/commands/add_award.py index 813b61f..0a77007 100644 --- a/app/management/commands/add_award.py +++ b/app/management/commands/add_award.py @@ -7,12 +7,11 @@ from time import sleep from random import randint from app.models import Tender, Award, Vendor -from app.server_requests import PAYLOAD +from app.server_requests import PAYLOAD, WINNERS_ENDPOINT_URI import logging from app.management.commands.base.params import BaseParamsUI logger = logging.getLogger(__name__) -WINNERS_ENDPOINT_URI = 'https://www.ungm.org/Public/ContractAward' CSS_TITLE = 'Title' CSS_REFERENCE = 'Reference' diff --git a/app/management/commands/base/notify.py b/app/management/commands/base/notify.py index 8e1e82c..6b2d99c 100644 --- a/app/management/commands/base/notify.py +++ b/app/management/commands/base/notify.py @@ -12,8 +12,6 @@ from app.management.commands.base.params import BaseParamsUI from app.utils import emails_to_notify -ENDPOINT_URI = "https://www.ungm.org" - User = get_user_model() diff --git a/app/management/commands/update_ungm.py b/app/management/commands/update_ungm.py index cbd821c..bd76848 100644 --- a/app/management/commands/update_ungm.py +++ b/app/management/commands/update_ungm.py @@ -7,8 +7,6 @@ from app.notifications import send_error_email from app.utils import log_tenders_update, TenderSource -ENDPOINT_URI = 'https://www.ungm.org' - class Command(BaseCommand, BaseParamsUI): help = 'Gets all UNGM tenders from the past n days' diff --git a/app/models.py b/app/models.py index 94dabfc..61fc79a 100644 --- a/app/models.py +++ b/app/models.py @@ -130,7 +130,7 @@ def marked_keyword_title(self): if not keywords: return title - regex = r"(" + r"|".join(keywords) + r")" + regex = r"(" + r"|".join(map(re.escape, keywords)) + r")" return re.sub(regex, r"\1", title, flags=re.IGNORECASE) @cached_property @@ -140,7 +140,7 @@ def marked_keyword_description(self): if not keywords: return description - regex = r"(" + r"|".join(keywords) + r")" + regex = r"(" + r"|".join(map(re.escape, keywords)) + r")" return re.sub(regex, r"\1", description, flags=re.IGNORECASE) def find_keywords(self, fields): @@ -161,8 +161,7 @@ def is_favorite_of(self, user): def save(self, *args, **kwargs): keywords = list(self.find_keywords(fields)) - if keywords: - self.has_keywords = True + self.has_keywords = bool(keywords) super().save(*args, **kwargs) self.keywords.set(keywords) diff --git a/app/parsers/ted.py b/app/parsers/ted.py index 4d243c7..ac8ec0b 100644 --- a/app/parsers/ted.py +++ b/app/parsers/ted.py @@ -43,9 +43,8 @@ def quit_or_close(ftp): class TEDWorker: - archives = [] - def __init__(self, last_ted_update=None): + self.archives = [] self.path = get_archives_path() if not last_ted_update: last_ted_update = date.today() @@ -132,10 +131,11 @@ def download_archive(self, archive): os.makedirs(self.path) file_path = os.path.join(self.path, archive.full_oj_s) url = urljoin(self.get_archive_url, archive.full_oj_s) + time.sleep(randint(2, 5)) + response = requests.get(url) + print(response.status_code, file_path) + response.raise_for_status() with open(file_path, "wb") as f: - time.sleep(randint(2, 5)) - response = requests.get(url) - print(response.status_code, file_path) f.write(response.content) self.archives.append(file_path) diff --git a/app/server_requests.py b/app/server_requests.py index a77a465..bd5984b 100644 --- a/app/server_requests.py +++ b/app/server_requests.py @@ -7,14 +7,17 @@ from random import randint from time import sleep +from urllib.parse import urlparse from urllib3.exceptions import NewConnectionError +from django.conf import settings + from app.models import UNSPSCCode -LIVE_ENDPOINT_URI = 'https://www.ungm.org' -TENDERS_ENDPOINT_URI = 'https://www.ungm.org/Public/Notice' -WINNERS_ENDPOINT_URI = 'https://www.ungm.org/Public/ContractAward' -SEARCH_UNSPSCS_URI = 'https://www.ungm.org/UNSPSC/Search' +LIVE_ENDPOINT_URI = settings.UNGM_ENDPOINT_URI +TENDERS_ENDPOINT_URI = LIVE_ENDPOINT_URI + '/Public/Notice' +WINNERS_ENDPOINT_URI = LIVE_ENDPOINT_URI + '/Public/ContractAward' +SEARCH_UNSPSCS_URI = LIVE_ENDPOINT_URI + '/UNSPSC/Search' PAYLOAD = { @@ -65,7 +68,7 @@ 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'en-US,en;q=0.5', 'Content-Type': 'application/json; charset=UTF-8', - 'Host': 'www.ungm.org', + 'Host': urlparse(LIVE_ENDPOINT_URI).netloc, 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:31.0)' ' Gecko/20100101 Firefox/31.0', 'X-Requested-With': 'XMLHttpRequest', diff --git a/app/views/tenders.py b/app/views/tenders.py index 3848642..87e1d0f 100644 --- a/app/views/tenders.py +++ b/app/views/tenders.py @@ -387,12 +387,15 @@ def get_context_data(self, **kwargs): } for award in result_awards.to_queryset() ] - regex = re.compile(rf'(\b({pk})\b(\s*({pk})\b)*)', re.I) + terms = '|'.join( + re.escape(term) for term in self.kwargs['pk'].split('+') if term) + if terms: + regex = re.compile(rf'(\b({terms})\b(\s*({terms})\b)*)', re.I) - tender_fields = ['title', 'description'] + tender_fields = ['title', 'description'] - SearchView.update_fields(context['tenders'], tender_fields, regex) - SearchView.update_award_fields(context['awards'], regex) + SearchView.update_fields(context['tenders'], tender_fields, regex) + SearchView.update_award_fields(context['awards'], regex) return context diff --git a/scratch/settings.py b/scratch/settings.py index 3cd50d0..6bd46e9 100644 --- a/scratch/settings.py +++ b/scratch/settings.py @@ -144,7 +144,7 @@ # TED TED_URL = env("TED_URL", "ted.europa.eu") -TED_NOTICE_URL = env("TED_URL", "en/notice/-/detail/") +TED_NOTICE_URL = env("TED_NOTICE_URL", "en/notice/-/detail/") TED_CALENDAR_URL = env("TED_CALENDAR_URL", "en/release-calendar/-/download/file/CSV/") TED_DAILY = env("TED_DAILY", "packages/daily/") TED_FTP_USER = env("TED_FTP_USER", "guest") From cbf2cff040ba603f663b92c8ba5f46821ca8bf18 Mon Sep 17 00:00:00 2001 From: andreighh <61791875+andreighh@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:27:55 +0300 Subject: [PATCH 2/4] Add packages on ghcr and CI --- .github/workflows/docker.yml | 49 +++++++++++++ .github/workflows/tests.yml | 84 ++++++++++++++++++++++ .travis.yml | 38 ---------- CLAUDE.md | 2 +- README.md | 3 +- Release.md | 136 +++++++++++++++++++++++++++++++++++ docker-compose.yml | 6 +- requirements-dev.txt | 2 - scratch/settings.py | 2 +- scratch/test_settings.py | 21 ------ 10 files changed, 276 insertions(+), 67 deletions(-) create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/tests.yml delete mode 100644 .travis.yml create mode 100644 Release.md diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..5f5af81 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,49 @@ +name: Docker image + +# Builds the image and pushes it to GHCR when a version tag (e.g. 1.0.62) is +# pushed. Can also be started by hand from the Actions tab. +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + IMAGE: ghcr.io/${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: docker/setup-buildx-action@v4 + + - uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Version tag -> ghcr.io/eaudeweb/scratch:1.0.62 and :latest + - id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.IMAGE }} + tags: | + type=semver,pattern={{version}} + type=sha + + - uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..aee2cba --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,84 @@ +name: Tests + +on: + pull_request: + branches: [master] + push: + branches: [master] + +permissions: + contents: read + +# A new push to the same branch or pull request cancels the previous run +concurrency: + group: tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + + # Same major versions as docker-compose.yml + services: + postgres: + image: postgres:15-alpine + env: + POSTGRES_DB: scratch + POSTGRES_USER: scratch + POSTGRES_PASSWORD: scratch + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U scratch" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.8 + env: + discovery.type: single-node + xpack.security.enabled: "false" + ES_JAVA_OPTS: -Xms512m -Xmx512m + ports: + - 9200:9200 + options: >- + --health-cmd "curl -fs http://localhost:9200/_cluster/health" + --health-interval 10s + --health-timeout 5s + --health-retries 20 + redis: + image: redis:alpine + ports: + - 6379:6379 + + env: + POSTGRES_HOST: localhost + POSTGRES_DB: scratch + POSTGRES_USER: scratch + POSTGRES_PASSWORD: scratch + ELASTICSEARCH_HOST: localhost:9200 + REDIS_HOST: localhost + + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v7 + with: + python-version: '3.11' + cache: pip + cache-dependency-path: requirements*.txt + + # Headers needed to build python-ldap and psycopg2 + - name: Install system packages + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends libldap-dev libsasl2-dev libpq-dev + + - name: Install dependencies + run: pip install -r requirements-dev.txt + + - name: Check + run: python manage.py check + + - name: Run tests + run: python manage.py test app/tests --settings=scratch.test_settings diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ed6488c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -file: .travis.yml -language: python -python: - - "3.6" -addons: - postgresql: "10" - apt: - packages: - - postgresql-10 - - postgresql-client-10 -env: - global: - - DEBUG=True - - BASE_URL=localhost:8000 - - PGPORT=5432 - - ES_VERSION=6.6.1 - - ES_DOWNLOAD_URL=https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${ES_VERSION}.tar.gz - - ELASTICSEARCH_HOST=127.0.0.1 - - ELASTICSEARCH_AUTH=elastic:changeme -services: - - postgresql -before_script: - - psql -c 'create database travis_ci_test;' -U postgres - - sudo sysctl -w vm.max_map_count=262144 - - sudo service elasticsearch start - - mkdir logging - - chmod -R 777 logging -install: - - wget ${ES_DOWNLOAD_URL} - - tar -xzf elasticsearch-${ES_VERSION}.tar.gz - - ./elasticsearch-${ES_VERSION}/bin/elasticsearch & - - pip install -r requirements-dev.txt -script: - - wget -q --waitretry=1 --retry-connrefused -T 10 -O - http://127.0.0.1:9200 - - ./manage.py check - - coverage run --source='.' ./manage.py test app/tests --settings=scratch.test_settings -after_success: - - coveralls diff --git a/CLAUDE.md b/CLAUDE.md index b7a44f0..10599bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ python manage.py test app.tests.test_ted_parser.TestClassName.test_method --sett - Test fixtures (sample TED XML, UNGM/IUCN HTML, PDFs/DOCX) live in `app/tests/parser_files/`; model factories are in `app/factories.py` (factory_boy). - Tests touching `Keyword` should extend `app.tests.base.BaseTestCase`, which disconnects the Keyword post_save/post_delete signals (otherwise they enqueue a django-q task that re-saves every tender). -- CI (`.travis.yml`) runs `./manage.py check` then `coverage run --source='.' ./manage.py test app/tests --settings=scratch.test_settings`. +- CI (`.github/workflows/tests.yml`, on pull requests to and pushes to `master`) runs `./manage.py check` then `./manage.py test app/tests --settings=scratch.test_settings` against Postgres, Elasticsearch and Redis service containers. `.github/workflows/docker.yml` builds and pushes the image to GHCR on version tags; see `Release.md`. There is no configured linter/formatter. diff --git a/README.md b/README.md index 70498a4..aec0d08 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Scratch ![alt text](https://travis-ci.com/eaudeweb/scratch.svg?branch=master) [![Coverage Status](https://coveralls.io/repos/github/eaudeweb/scratch/badge.svg?branch=master)](https://coveralls.io/github/eaudeweb/scratch?branch=master) [![Docker](https://img.shields.io/docker/cloud/build/eaudeweb/scratch?label=Docker&style=flat)](https://hub.docker.com/r/eaudeweb/scratch/builds) +# Scratch [![Tests](https://github.com/eaudeweb/scratch/actions/workflows/tests.yml/badge.svg)](https://github.com/eaudeweb/scratch/actions/workflows/tests.yml) [![Docker image](https://github.com/eaudeweb/scratch/actions/workflows/docker.yml/badge.svg)](https://github.com/eaudeweb/scratch/actions/workflows/docker.yml) ### Python Django Scratch @@ -46,6 +46,7 @@ python manage.py runserver 0.0.0.0:8000 ``` ### [Cachet Setup Info](Cachet.md) +### [Making a release](Release.md) ## Running with NGINX If you want to use NGINX to serve the project you need to do the following: diff --git a/Release.md b/Release.md new file mode 100644 index 0000000..1504bc9 --- /dev/null +++ b/Release.md @@ -0,0 +1,136 @@ +# Releasing Scratch + +Releases are Docker images built by GitHub Actions and published to +`ghcr.io/eaudeweb/scratch`. Production is a single `docker-compose.yml` on the +server that runs the `web`, `async` and `cron` services from that image (the +crontab is baked into the image too). Deploying is manual. + +``` +git tag 1.0.62 ──> GitHub Actions ──> ghcr.io/eaudeweb/scratch:1.0.62 ──> manual pull on prod +``` + +## 1. Before tagging + +- Make sure the **Tests** workflow (`.github/workflows/tests.yml`) is green on + the pull request. It runs the test suite on every pull request to `master` and + every push to `master`, so you don't need to run it yourself. +- If you changed a scraper, run its update command once locally against a short + range (e.g. `python manage.py update_ungm --days_ago 1`). Keep it to one run so + the sources don't rate limit or block us. +- Check for new migrations (`python manage.py makemigrations --check --dry-run`) + and note whether the release has any, you'll need it when deploying. +- Merge the work into `master`, and check that the Tests run on `master` is green + before tagging. + +## 2. Tag the release + +Versions are plain `MAJOR.MINOR.PATCH`, without a `v` prefix. The last image on +Docker Hub was `1.0.61`, so GHCR releases start at `1.0.62`. To see the latest +tag, fetch them first (clones don't always have all of them): + +```bash +git fetch --tags +git tag --sort=-v:refname | head -3 +``` + +Tag the merge commit on `master` and push the tag: + +```bash +git checkout master && git pull +git tag 1.0.62 +git push origin 1.0.62 +``` + +Pushing the tag starts the **Docker image** workflow +(`.github/workflows/docker.yml`). Follow it in the repository's Actions tab. When +it is green, the image is available as: + +- `ghcr.io/eaudeweb/scratch:1.0.62` +- `ghcr.io/eaudeweb/scratch:latest` +- `ghcr.io/eaudeweb/scratch:sha-` + +The workflow can also be started by hand from the Actions tab (*Run workflow*). +A manual run from a branch only produces the `sha-` tag, which is useful +for trying a build without making a release. + +## 3. Deploy to production + +On the server, in the directory that holds the compose file: + +1. Set the new version in the `.env` file next to the compose file: + + ```bash + SCRATCH_VERSION=1.0.62 + ``` + + The compose file uses it for the `web`, `async` and `cron` images + (`image: ghcr.io/eaudeweb/scratch:${SCRATCH_VERSION}`), so the compose file + itself doesn't change between releases. Docker Compose reads `.env` from the + project directory automatically. Use a version number rather than `latest`, so + it's always clear what is deployed and a rollback is a one-line edit. + +2. Pull and restart: + + ```bash + docker compose pull + docker compose up -d + ``` + + `pull` downloads the new image while the old containers keep running, so a + wrong tag or a registry problem fails before anything is restarted. `up -d` + then recreates only the services whose image changed. + +3. Check that it came up: + + ```bash + docker compose ps + docker compose logs --tail=50 web async cron + ``` + + The Cachet status page should report all components as operational. + +### Migrations, static files and the search index + +`docker-entrypoint.sh` runs these steps when `web` starts, depending on its +environment variables (set in the compose file or `docker/app.env`): + +| Variable | When `yes`, on every start of `web` | +|---|---| +| `DJANGO_MIGRATE` | `manage.py migrate` | +| `DJANGO_COLLECT_STATIC` | `manage.py collectstatic` | +| `DJANGO_INDEX_CONTENT` | `manage.py search_index -f --rebuild` (rebuilds every Elasticsearch index, can take a while) | + +If migrations are not run automatically and the release has any, run them after +`up -d`: + +```bash +docker compose exec web python manage.py migrate +``` + +## Rolling back + +Set `SCRATCH_VERSION` in `.env` back to the previous version and run the same +`docker compose pull` and `docker compose up -d`. Migrations are not reversed by +this. If the release added any, check that the old code still works with the new +schema, or migrate back explicitly +(`python manage.py migrate app `) before switching images. + +## One-time setup (only when we switched from dockerhub to GitHub packages) + +- **Organization settings:** GitHub Actions must be allowed to create packages + for the `eaudeweb` organization. The first tag push creates the `scratch` + package. +- **Access from production:** either make the `scratch` package public (the + repository already is), or log in once on the server with a personal access + token that has the `read:packages` scope: + + ```bash + echo | docker login ghcr.io -u --password-stdin + ``` + +- **Production compose file:** switch `image:` from `eaudeweb/scratch:...` (Docker + Hub) to `ghcr.io/eaudeweb/scratch:${SCRATCH_VERSION}` for `web`, `async` and + `cron`. + +Images up to `1.0.61` are on Docker Hub as `eaudeweb/scratch`. Docker Hub no +longer receives new versions. diff --git a/docker-compose.yml b/docker-compose.yml index 006c0f4..d7ff099 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '2' services: web: - image: eaudeweb/scratch:latest + image: ghcr.io/eaudeweb/scratch:${SCRATCH_VERSION:-latest} container_name: scratch.app environment: - DJANGO_MIGRATE=yes @@ -25,7 +25,7 @@ services: - 8000:8000 async: - image: eaudeweb/scratch:latest + image: ghcr.io/eaudeweb/scratch:${SCRATCH_VERSION:-latest} container_name: scratch.async env_file: - docker/app.env @@ -45,7 +45,7 @@ services: command: ['qcluster'] cron: - image: eaudeweb/scratch:latest + image: ghcr.io/eaudeweb/scratch:${SCRATCH_VERSION:-latest} container_name: scratch.cron environment: - TZ=Europe/Bucharest diff --git a/requirements-dev.txt b/requirements-dev.txt index 44dfc5c..b8dcc81 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,2 @@ -r requirements.txt -coverage==4.5.4 -coveralls==1.8.2 factory_boy==3.2.1 diff --git a/scratch/settings.py b/scratch/settings.py index 6bd46e9..3611989 100644 --- a/scratch/settings.py +++ b/scratch/settings.py @@ -90,7 +90,7 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", - "HOST": "db", + "HOST": env("POSTGRES_HOST", "db"), "PORT": 5432, "NAME": env("POSTGRES_DB", "scratch"), "USER": env("POSTGRES_USER", "scratch"), diff --git a/scratch/test_settings.py b/scratch/test_settings.py index 18414db..7d7765a 100644 --- a/scratch/test_settings.py +++ b/scratch/test_settings.py @@ -1,22 +1 @@ from .settings import * -from getenv import env - - -if 'TRAVIS' in os.environ: - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'travis_ci_test', - 'USER': 'postgres', - 'PASSWORD': '', - 'HOST': 'localhost', - 'PORT': os.getenv('PGPORT'), - }, - } - - ELASTICSEARCH_DSL = { - 'default': { - 'hosts': env('ELASTICSEARCH_TEST_HOST'), - 'http_auth': env('ELASTICSEARCH_TEST_AUTH'), - }, - } From d93e109dd0bf73a7c73f7deaa29ab450d570b378 Mon Sep 17 00:00:00 2001 From: andreighh <61791875+andreighh@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:50:47 +0300 Subject: [PATCH 3/4] Make it work despite outdated packages --- requirements.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index bdd71bd..06c7931 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ python-dateutil>=1.5 beautifulsoup4==4.11.1 Django==4.1.13 django-auth-ldap==3.0.0 -django-elasticsearch-dsl==7.2.2 +django-elasticsearch-dsl==7.3 datefinder==0.7.3 pdfminer==20191125 docx-parser==1.0.1 @@ -10,10 +10,12 @@ docx2txt==0.8 django-getenv==1.3.2 django-q==1.3.9 django-sql-explorer==2.5.0 -django-widget-tweaks==1.4.11 +django-widget-tweaks==1.5.0 elasticsearch-dsl==7.4.0 psycopg2==2.9.5 redis==3.5.3 requests==2.31.0 sentry_sdk==1.14.0 tika==1.24 +# django-q 1.3.9 imports pkg_resources, which setuptools 82 removed +setuptools<82 From 7b572b00a024e8244c00cdbab733429a72615fa9 Mon Sep 17 00:00:00 2001 From: andreighh <61791875+andreighh@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:07:35 +0300 Subject: [PATCH 4/4] Cachet logging issues --- app/tests/test_error.py | 4 ++-- app/utils.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/tests/test_error.py b/app/tests/test_error.py index 586d39d..9d9db6e 100644 --- a/app/tests/test_error.py +++ b/app/tests/test_error.py @@ -10,7 +10,7 @@ class UpdateErrorTestCase(BaseTestCase): @patch.object( TEDWorker, "download_latest_archives", side_effect=Exception("Test error") ) - @patch("app.notifications.send_error_email") + @patch("app.management.commands.update_ted.send_error_email") def test_ted_update_error(self, send_error_email, _): with self.assertRaises(Exception): call_command("update_ted") @@ -19,7 +19,7 @@ def test_ted_update_error(self, send_error_email, _): @patch.object( UNGMWorker, "parse_latest_notices", side_effect=Exception("Test error") ) - @patch("app.notifications.send_error_email") + @patch("app.management.commands.update_ungm.send_error_email") def test_ungm_update_error(self, send_error_email, _): with self.assertRaises(Exception): call_command("update_ungm", days_ago=1) diff --git a/app/utils.py b/app/utils.py index 6a1f556..2e290bf 100644 --- a/app/utils.py +++ b/app/utils.py @@ -21,6 +21,10 @@ def emails_to_notify(): def log_tenders_update(tender_source: TenderSource): def decorator(fn): def wrapper(*args, **kwargs): + if not settings.APP_URL: + # Cachet is not configured + return fn(*args, **kwargs) + headers = { "accept": "application/json", "X-Cachet-Application": "Demo",