diff --git a/Makefile b/Makefile index 3f98e79566..debab9d61e 100644 --- a/Makefile +++ b/Makefile @@ -48,19 +48,19 @@ smoke-test: .PHONY: run run: ## Run the web app - flask run -p 6011 --host=0.0.0.0 + poetry run flask run -p 6011 --host=0.0.0.0 .PHONY: run-celery-local run-celery-local: ## Run the celery workers with all the queues - ./scripts/run_celery_local.sh + poetry run ./scripts/run_celery_local.sh .PHONY: run-celery-local-filtered run-celery-local-filtered: ## Run the celery workers with all queues but filter out common scheduled tasks - ./scripts/run_celery_local.sh 2>&1 >/dev/null | grep -iEv 'beat|in-flight-to-inbox|run-scheduled-jobs|check-job-status' + poetry run ./scripts/run_celery_local.sh 2>&1 >/dev/null | grep -iEv 'beat|in-flight-to-inbox|run-scheduled-jobs|check-job-status' .PHONY: run-celery-purge run-celery-purge: ## Purge the celery queues - ./scripts/run_celery_purge.sh + poetry run ./scripts/run_celery_purge.sh .PHONY: run-db run-db: ## psql to access dev database diff --git a/README.md b/README.md index d2c8a6bf01..e36ade9a6e 100644 --- a/README.md +++ b/README.md @@ -17,78 +17,7 @@ Contains: For any issues during the following instructions, make sure to review the **Frequent problems** section toward the end of the document. -### Local installation instruction - -#### On OS X: - -1. Install PyEnv with Homebrew. This will preserve your sanity. - -`brew install pyenv` - -2. Install Python 3.10.8 or whatever is the latest - -`pyenv install 3.10.8` - -3. If you expect no conflicts, set `3.10.8` as you default - -`pyenv global 3.10.8` - -4. Ensure it installed by running - -`python --version` - -if it did not, take a look here: https://github.com/pyenv/pyenv/issues/660 - -5. Install `poetry`: - -`pip install poetry==1.3.2` - -6. Restart your terminal and make your virtual environtment: - -`poetry env use $(which python)` - -8. Verify that the environment was created and activated by poetry - -`poetry env list` - -9. Install [Postgres.app](http://postgresapp.com/). - -10. Create the database for the application - -`createdb --user=postgres notification_api` - -11. Install the required environment variables via our LastPast Vault - -Within the team's *LastPass Vault*, you should find corresponding folders for this -project containing the `.env` content that you should copy in your project root folder. This -will grant the application necessary access to our internal infrastructure. - -If you don't have access to our *LastPass Vault* (as you evaluate our notification -platform for example), you will find a sane set of defaults exists in the `.env.example` -file. Copy that file to `.env` and customize it to your needs. - -12. Install all dependencies - -`poetry install` - -1. Generate the version file ?!? - -`make generate-version-file` - -14. Run all DB migrations - -`flask db upgrade` - -15. Run the service - -`make run` - -15a. To test - -`poetry install --with test` - -`make test` - +### Local installation instruction (Use Dev Containers) #### In a [VS Code devcontainer](https://code.visualstudio.com/docs/remote/containers-tutorial) 1. Install VS Code diff --git a/app/celery/service_callback_tasks.py b/app/celery/service_callback_tasks.py index c3a560b175..af3d51e3b2 100644 --- a/app/celery/service_callback_tasks.py +++ b/app/celery/service_callback_tasks.py @@ -65,31 +65,23 @@ def _send_data_to_service_callback_api(self, data, service_callback_url, token, data=json.dumps(data), headers={ "Content-Type": "application/json", - "Authorization": "Bearer {}".format(token), + "Authorization": f"Bearer {token}", }, timeout=5, ) current_app.logger.info( - "{} sending {} to {}, response {}".format( - function_name, - notification_id, - service_callback_url, - response.status_code, - ) + f"{function_name} sending {notification_id} to {service_callback_url}, response {response.status_code}" ) response.raise_for_status() except RequestException as e: current_app.logger.warning( - "{} request failed for notification_id: {} and url: {}. exc: {}".format( - function_name, notification_id, service_callback_url, e - ) + f"{function_name} request failed for notification_id: {notification_id} and url: {service_callback_url}. exc: {e}" ) - if not isinstance(e, HTTPError) or e.response.status_code >= 500: + # Retry if the response status code is server-side or 429 (too many requests). + if not isinstance(e, HTTPError) or e.response.status_code >= 500 or e.response.status_code == 429: try: self.retry(queue=QueueNames.CALLBACKS_RETRY) except self.MaxRetriesExceededError: current_app.logger.warning( - "Retry: {} has retried the max num of times for callback url {} and notification_id: {}".format( - function_name, service_callback_url, notification_id - ) + "Retry: {function_name} has retried the max num of times for callback url {service_callback_url} and notification_id: {notification_id}" ) diff --git a/app/models.py b/app/models.py index 81767406a1..215ad47372 100644 --- a/app/models.py +++ b/app/models.py @@ -280,6 +280,8 @@ class EmailBranding(BaseModel): UUID(as_uuid=True), db.ForeignKey("organisation.id", ondelete="SET NULL"), index=True, nullable=True ) organisation = db.relationship("Organisation", back_populates="email_branding", foreign_keys=[organisation_id]) + alt_text_en = db.Column(db.String(), nullable=True) + alt_text_fr = db.Column(db.String(), nullable=True) def serialize(self) -> dict: serialized = { @@ -290,6 +292,8 @@ def serialize(self) -> dict: "text": self.text, "brand_type": self.brand_type, "organisation_id": str(self.organisation_id) if self.organisation_id else "", + "alt_text_en": self.alt_text_en, + "alt_text_fr": self.alt_text_fr, } return serialized diff --git a/migrations/versions/0446_add_alt_text.py b/migrations/versions/0446_add_alt_text.py new file mode 100644 index 0000000000..868ce33db7 --- /dev/null +++ b/migrations/versions/0446_add_alt_text.py @@ -0,0 +1,34 @@ +""" +Revision ID: 0446_add_alt_text.py +Revises: 0445_add_org_id_branding.py +Create Date: 2024-04-23 +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy import text + +revision = "0446_add_alt_text" +down_revision = "0445_add_org_id_branding" + + +def upgrade(): + table_description = op.get_bind().execute( + text("SELECT * FROM information_schema.columns WHERE table_name = 'email_branding'") + ) + + # Check if the column exists + if "alt_text_en" not in [column["column_name"] for column in table_description]: + op.add_column( + "email_branding", + sa.Column("alt_text_en", sa.String(), nullable=True), + ) + if "alt_text_fr" not in [column["column_name"] for column in table_description]: + op.add_column( + "email_branding", + sa.Column("alt_text_fr", sa.String(), nullable=True), + ) + + +def downgrade(): + op.drop_column("email_branding", "alt_text_fr") + op.drop_column("email_branding", "alt_text_en") diff --git a/migrations/versions/0447_update_verify_code_template.py b/migrations/versions/0447_update_verify_code_template.py new file mode 100644 index 0000000000..9db7e8f1c8 --- /dev/null +++ b/migrations/versions/0447_update_verify_code_template.py @@ -0,0 +1,97 @@ +""" + +Revision ID: 0447_update_verify_code_template +Revises: 0446_add_alt_text +Create Date: 2023-10-05 00:00:00 + +""" +from datetime import datetime + +from alembic import op +from flask import current_app + +revision = "0447_update_verify_code_template" +down_revision = "0446_add_alt_text" + +near_content = "\n".join( + [ + "[[en]]", + "Hi ((name)),", + "", + "Here is your security code to log in to GC Notify:", + "", + "^ **((verify_code))**", + "[[/en]]", + "", + "---", + "", + "[[fr]]", + "Bonjour ((name)),", + "", + "Voici votre code de sécurité pour vous connecter à Notification GC:", + "", + "^ **((verify_code))**", + "[[/fr]]", + ] +) + + +templates = [ + { + "id": current_app.config["EMAIL_2FA_TEMPLATE_ID"], + "template_type": "email", + "subject": "Sign in | Connectez-vous", + "content": near_content, + "process_type": "priority", + }, +] + + +def upgrade(): + conn = op.get_bind() + + for template in templates: + current_version = conn.execute("select version from templates where id='{}'".format(template["id"])).fetchone() + name = conn.execute("select name from templates where id='{}'".format(template["id"])).fetchone() + template["version"] = current_version[0] + 1 + template["name"] = name[0] + + template_update = """ + UPDATE templates SET content = '{}', subject = '{}', version = '{}', updated_at = '{}' + WHERE id = '{}' + """ + template_history_insert = """ + INSERT INTO templates_history (id, name, template_type, created_at, content, archived, service_id, subject, + created_by_id, version, process_type, hidden) + VALUES ('{}', '{}', '{}', '{}', '{}', False, '{}', '{}', '{}', {}, '{}', false) + """ + + for template in templates: + op.execute( + template_update.format( + template["content"], + template["subject"], + template["version"], + datetime.utcnow(), + template["id"], + ) + ) + + op.execute( + template_history_insert.format( + template["id"], + template["name"], + template["template_type"], + datetime.utcnow(), + template["content"], + current_app.config["NOTIFY_SERVICE_ID"], + template["subject"], + current_app.config["NOTIFY_USER_ID"], + template["version"], + template["process_type"], + ) + ) + + +def downgrade(): + pass diff --git a/poetry.lock b/poetry.lock index 1ab0346e40..5002212cb4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2444,7 +2444,7 @@ requests = ">=2.0.0" [[package]] name = "notifications-utils" -version = "52.1.5" +version = "52.2.0" description = "Shared python code for Notification - Provides logging utils etc." optional = false python-versions = "~3.10.9" @@ -2479,8 +2479,8 @@ werkzeug = "2.3.7" [package.source] type = "git" url = "https://github.com/cds-snc/notifier-utils.git" -reference = "52.1.5" -resolved_reference = "9d9e8c7c32e3608f4dd8f320eaba4bb67edfcbf5" +reference = "52.2.0" +resolved_reference = "0146718a423b0144566392ab3082d15779f4a73f" [[package]] name = "ordered-set" @@ -4255,4 +4255,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "~3.10.9" -content-hash = "f00992b7f47d8434a76d0be08135eace31315c696e20a222a10e2bf926e8a561" +content-hash = "18ed30bed79c84db2cc559fecdba223a88c8d8546f4fb951f85ef1d76142a714" diff --git a/pyproject.toml b/pyproject.toml index db3ae6fff3..973ced94f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ Werkzeug = "2.3.7" MarkupSafe = "2.1.4" # REVIEW: v2 is using sha512 instead of sha1 by default (in v1) itsdangerous = "2.1.2" -notifications-utils = { git = "https://github.com/cds-snc/notifier-utils.git", tag = "52.1.5" } +notifications-utils = { git = "https://github.com/cds-snc/notifier-utils.git", tag = "52.2.0" } # rsa = "4.9 # awscli 1.22.38 depends on rsa<4.8 typing-extensions = "4.7.1" greenlet = "2.0.2" diff --git a/tests-perf/locust/locust.conf b/tests-perf/locust/locust.conf index c1eba3b220..76aa3d2273 100644 --- a/tests-perf/locust/locust.conf +++ b/tests-perf/locust/locust.conf @@ -3,7 +3,7 @@ locustfile = tests-perf/locust/locust-notifications.py host = https://api.staging.notification.cdssandbox.xyz users = 3000 spawn-rate = 20 -run-time = 5m +run-time = 10m # headless = true # master = true diff --git a/tests/app/celery/test_service_callback_tasks.py b/tests/app/celery/test_service_callback_tasks.py index a8259ccd29..eda0e212d8 100644 --- a/tests/app/celery/test_service_callback_tasks.py +++ b/tests/app/celery/test_service_callback_tasks.py @@ -90,8 +90,9 @@ def test_send_complaint_to_service_posts_https_request_to_service_with_signed_da @pytest.mark.parametrize("notification_type", ["email", "letter", "sms"]) -def test__send_data_to_service_callback_api_retries_if_request_returns_500_with_signed_data( - notify_db_session, mocker, notification_type +@pytest.mark.parametrize("status_code", [429, 500, 503]) +def test__send_data_to_service_callback_api_retries_if_request_returns_error_code_with_signed_data( + notify_db_session, mocker, notification_type, status_code ): callback_api, template = _set_up_test_data(notification_type, "delivery_status") datestr = datetime(2017, 6, 20) @@ -107,7 +108,7 @@ def test__send_data_to_service_callback_api_retries_if_request_returns_500_with_ signed_data = _set_up_data_for_status_update(callback_api, notification) mocked = mocker.patch("app.celery.service_callback_tasks.send_delivery_status_to_service.retry") with requests_mock.Mocker() as request_mock: - request_mock.post(callback_api.url, json={}, status_code=500) + request_mock.post(callback_api.url, json={}, status_code=status_code) send_delivery_status_to_service(notification.id, signed_status_update=signed_data) assert mocked.call_count == 1 diff --git a/tests/app/email_branding/test_rest.py b/tests/app/email_branding/test_rest.py index 05e0b5a48e..5cee670554 100644 --- a/tests/app/email_branding/test_rest.py +++ b/tests/app/email_branding/test_rest.py @@ -39,7 +39,9 @@ def test_get_email_branding_options_filter_org(admin_request, notify_db, notify_ def test_get_email_branding_by_id(admin_request, notify_db, notify_db_session): - email_branding = EmailBranding(colour="#FFFFFF", logo="/path/image.png", name="Some Org", text="My Org") + email_branding = EmailBranding( + colour="#FFFFFF", logo="/path/image.png", name="Some Org", text="My Org", alt_text_en="hello world" + ) notify_db.session.add(email_branding) notify_db.session.commit() @@ -57,6 +59,8 @@ def test_get_email_branding_by_id(admin_request, notify_db, notify_db_session): "text", "brand_type", "organisation_id", + "alt_text_en", + "alt_text_fr", } assert response["email_branding"]["colour"] == "#FFFFFF" assert response["email_branding"]["logo"] == "/path/image.png" @@ -64,6 +68,8 @@ def test_get_email_branding_by_id(admin_request, notify_db, notify_db_session): assert response["email_branding"]["text"] == "My Org" assert response["email_branding"]["id"] == str(email_branding.id) assert response["email_branding"]["brand_type"] == str(email_branding.brand_type) + assert response["email_branding"]["alt_text_en"] == "hello world" + assert response["email_branding"]["alt_text_fr"] is None def test_post_create_email_branding(admin_request, notify_db_session):