Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
"program": "${workspaceFolder}/submit-api/wsgi.py",
"console": "integratedTerminal",
"justMyCode": true
},
{
"name": "Submit CRON - CENTRE",
"type": "python",
"cwd": "${workspaceFolder}/submit-cron/",
"request": "launch",
"program": "${workspaceFolder}/submit-cron/invoke_jobs.py",
"args": ["EMAIL", "CENTRE"],
"console": "integratedTerminal",
"justMyCode": true
}
]
}
2 changes: 2 additions & 0 deletions submit-cron/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ class _Config(): # pylint: disable=too-few-public-methods

CONDITION_API_BASE_URL = os.getenv('CONDITION_API_BASE_URL')

ENVIRONMENT = os.getenv('ENVIRONMENT', os.getenv('ENV_NAME', ''))


class DevConfig(_Config): # pylint: disable=too-few-public-methods
"""Dev Config."""
Expand Down
2 changes: 0 additions & 2 deletions submit-cron/invoke_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
from utils.logger import setup_logging
from datetime import datetime

from submit_api.models.project import Project

import config

setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'logging.conf')) # important to do this first
Expand Down
4 changes: 2 additions & 2 deletions submit-cron/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ certifi==2025.4.26
cffi==1.17.1
charset-normalizer==3.4.2
click==8.1.8
cryptography==44.0.3
cryptography==41.0.7
flake8-import-order==0.18.2
flask-cors==5.0.1
flask-jwt-oidc==0.8.0
Expand Down Expand Up @@ -55,4 +55,4 @@ typing_extensions==4.13.2
urllib3==2.4.0
zimports==0.6.1
zipp==3.21.0
-e git+https://github.com/bcgov/EPIC.submit.git@develop#egg=submit-api&subdirectory=submit-api
-e git+https://github.com/bcgov/EPIC.submit.git@release/v1.0.8_cron_changes#egg=submit-api&subdirectory=submit-api
3 changes: 2 additions & 1 deletion submit-cron/requirements/prod.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ python-dotenv
requests
flask_cors
pyhumps
importlib-resources
importlib-resources
pytz
2 changes: 2 additions & 0 deletions submit-cron/src/submit_cron/models/db.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Initilizations for db, migration and marshmallow."""

from contextlib import contextmanager

from flask import current_app
from flask_marshmallow import Marshmallow
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker


# DB initialize in __init__ file
# db variable use for create models from here
db = SQLAlchemy()
Expand Down
11 changes: 9 additions & 2 deletions submit-cron/src/submit_cron/processors/centre/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
# processors/centre/__init__.py
from typing import Dict, Callable
from typing import Callable, Dict

from submit_api.data_classes.email_details import EmailDetails

from ...models.email_job import EmailJob
from .access_denied import process_access_denied
from .access_granted import process_access_granted
from .access_request_received_dst import process_access_request_received_dst
from .access_request_submitted import process_access_request_submitted
from ...models.email_job import EmailJob


# Template names (export as constants, so they’re used consistently)
TEMPLATE_ACCESS_REQUEST_SUBMITTED = "access_request_submitted_confirmation.html"
ACCESS_REQUEST_RECEIVED_NOTIFICATION = 'access_request_received_notification.html'
ACCESS_GRANTED_NOTIFICATION = "access_granted_notification.html"
ACCESS_DENIED_NOTIFICATION = "access_denied_notification.html"

# Map: template_name -> processor function
# Each processor takes (job: EmailJob) and returns EmailDetails
PROCESSORS: Dict[str, Callable[[EmailJob], EmailDetails]] = {
TEMPLATE_ACCESS_REQUEST_SUBMITTED: process_access_request_submitted,
ACCESS_REQUEST_RECEIVED_NOTIFICATION: process_access_request_received_dst,
ACCESS_GRANTED_NOTIFICATION: process_access_granted,
ACCESS_DENIED_NOTIFICATION: process_access_denied
}

__all__ = [
Expand Down
46 changes: 46 additions & 0 deletions submit-cron/src/submit_cron/processors/centre/access_denied.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import Any, Dict, List

from submit_api.data_classes.email_details import EmailDetails
from submit_api.exceptions import BadRequestError

from submit_cron.models.email_job import EmailJob


def _require(payload: Dict[str, Any], fields: List[str]) -> None:
missing = [f for f in fields if not payload.get(f)]
if missing:
raise BadRequestError(f"Missing required payload fields: {', '.join(missing)}")


def process_access_denied(job: EmailJob) -> EmailDetails:
"""
Processor for the 'access request submitted' template.

Expected job.payload:
{
"recipients": ["user@example.com"], # required
"user_name": "Jane Doe", # required
"application_name": "EPIC.centre", # required
"sender": "staff@email.com", # required (email address)
}
"""
payload = job.payload or {}
_require(payload, ["recipients", "user_name", "application_name", "sender"])

recipients = payload["recipients"]
if not isinstance(recipients, list) or not recipients:
raise BadRequestError("payload.recipients must be a non-empty list of email addresses")

subject = f"Your EPIC Access Request for {payload['application_name']} Has Been Denied"

email_details = EmailDetails(
template_name=job.template_name,
body_args={
'user_name': payload['user_name'],
'application_name': payload['application_name']
},
subject=subject,
sender=payload['sender'],
recipients=recipients,
)
return email_details
50 changes: 50 additions & 0 deletions submit-cron/src/submit_cron/processors/centre/access_granted.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from typing import Any, Dict, List

from submit_api.data_classes.email_details import EmailDetails
from submit_api.exceptions import BadRequestError

from submit_cron.models.email_job import EmailJob


def _require(payload: Dict[str, Any], fields: List[str]) -> None:
missing = [f for f in fields if not payload.get(f)]
if missing:
raise BadRequestError(f"Missing required payload fields: {', '.join(missing)}")


def process_access_granted(job: EmailJob) -> EmailDetails:
"""
Processor for the 'access request submitted' template.

Expected job.payload:
{
"recipients": ["user@example.com"], # required
"user_name": "Jane Doe", # required
"application_name": "EPIC.centre", # required
"sender": "staff@email.com", # required (email address),
"access_level": "VIEWER",
"auth_link: "https://centre.example.com/request-access" # required
}
"""
payload = job.payload or {}
_require(payload, ["recipients", "user_name", "application_name", "sender", "auth_link", "access_level"])

recipients = payload["recipients"]
if not isinstance(recipients, list) or not recipients:
raise BadRequestError("payload.recipients must be a non-empty list of email addresses")

subject = f"Your EPIC Access Request for {payload['application_name']} Has Been Granted"

email_details = EmailDetails(
template_name=job.template_name,
body_args={
'user_name': payload['user_name'],
'application_name': payload['application_name'],
'auth_link': payload['auth_link'],
'access_level': payload['access_level']
},
subject=subject,
sender=payload['sender'],
recipients=recipients,
)
return email_details
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, Any, List
from typing import Any, Dict, List

from submit_api.data_classes.email_details import EmailDetails
from submit_api.exceptions import BadRequestError
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, Any, List
from typing import Any, Dict, List

from submit_api.data_classes.email_details import EmailDetails
from submit_api.exceptions import BadRequestError
Expand Down
4 changes: 3 additions & 1 deletion submit-cron/src/submit_cron/repositories/email_repository.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from typing import List
from sqlalchemy import Table, Column, Integer, String, Text, MetaData, select, DateTime, func

from sqlalchemy import Column, DateTime, Integer, MetaData, String, Table, Text, func, select
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Session

from submit_cron.models.email_job import EmailJob


metadata = MetaData()

# Local definition of the table (decoupled from submit_api.models)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import requests
from flask import current_app
from submit_api.models.project import Project

from submit_cron.models import db
from submit_api.models.project import Project


class ApprovedConditionService:
Expand Down
8 changes: 5 additions & 3 deletions submit-cron/src/submit_cron/services/centre_email_service.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import logging
from typing import Callable, Dict

from submit_cron.services.ches_service import ChesApiService
from submit_cron.repositories.email_repository import EmailRepository
from submit_cron.models.email_job import EmailJob
from submit_api.data_classes.email_details import EmailDetails
from submit_api.exceptions import BadRequestError

from submit_cron.models.email_job import EmailJob
from submit_cron.repositories.email_repository import EmailRepository
from submit_cron.services.ches_service import ChesApiService


logger = logging.getLogger(__name__)


Expand Down
29 changes: 27 additions & 2 deletions submit-cron/src/submit_cron/services/ches_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

import requests
from flask import current_app

from submit_api.data_classes.email_details import EmailDetails

from utils.template import Template


Expand Down Expand Up @@ -63,7 +63,7 @@ def _ensure_valid_token(self):

@staticmethod
def _get_email_body_from_template(template_name: str, body_args: dict, template_sub_directory: str = None):
"""Get email body from a template."""
"""Get email body from a template with optional environment message for centre templates."""
if not template_name:
raise ValueError('Template name is required')

Expand All @@ -77,8 +77,33 @@ def _get_email_body_from_template(template_name: str, body_args: dict, template_
body_args['logo_url'] = f'{current_app.config.get("WEB_URL")}/assets/EAO_Logo-BZOR9oRj.png'
rendered_body = template.render(body_args)

# Add environment notification for centre templates in non-production
if template_sub_directory == 'centre':
env_name = current_app.config.get('ENVIRONMENT', '')
if env_name and env_name.lower() != 'production':
env_message = ChesApiService._create_environment_banner(env_name)
rendered_body = ChesApiService._inject_environment_banner(rendered_body, env_message)

return rendered_body

@staticmethod
def _create_environment_banner(env_name: str) -> str:
"""Create HTML banner showing the current environment."""
return f'''
<div style="background-color: #fff3cd; border: 1px solid #ffc107; padding: 10px;
margin: 20px 0; text-align: center; font-size: 14px; color: #856404;">
<strong>You are using {env_name} environment</strong>
</div>
'''

@staticmethod
def _inject_environment_banner(rendered_body: str, env_message: str) -> str:
"""Inject environment banner into the email body."""
if '</body>' in rendered_body:
return rendered_body.replace('</body>', f'{env_message}</body>')
else:
return rendered_body + env_message

def _get_email_body(self, email_details: EmailDetails, template_sub_directory: str = None):
"""Get email body based on details or template."""
if email_details.body:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from urllib.parse import urljoin

from flask import current_app
from submit_api.data_classes.email_details import EmailDetails
from submit_api.enums.role import RoleEnum
from submit_api.exceptions import BadRequestError
from submit_api.models.account_project import AccountProject as AccountProjectModel
from submit_api.models.invitations import Invitations as InvitationsModel
from submit_api.models.package import Package as PackageModel
from submit_api.models.project import Project as ProjectModel
from submit_api.enums.role import RoleEnum
from submit_api.models.account_project import AccountProject as AccountProjectModel
from submit_api.utils.constants import NEW_USER_INVITATION_EMAIL_TEMPLATE

from submit_cron.models import db
from submit_api.utils.constants import NEW_USER_INVITATION_EMAIL_TEMPLATE
from urllib.parse import urljoin


class InvitationEmailService: # pylint: disable=too-few-public-methods
Expand Down
17 changes: 8 additions & 9 deletions submit-cron/src/submit_cron/services/mail_service.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
from datetime import datetime
from functools import partial
from typing import List

from submit_api.data_classes.email_details import EmailDetails
from submit_api.exceptions import BadRequestError
from submit_api.models.email_queue import EmailQueue, EmailStatus
from submit_api.models.invitations import Invitations as InvitationsModel
from submit_api.models.package import Package as PackageModel
from submit_api.models.email_queue import EmailQueue, EmailStatus
from functools import partial
from submit_api.utils.constants import (
MANAGEMENT_PLAN_RESUBMISSION_REQUEST_EMAIL_TEMPLATE, MANAGEMENT_PLAN_SUBMISSION_CONFIRMATION_EMAIL_TEMPLATE,
MANAGEMENT_PLAN_SUBMISSION_NOTIFY_STAFF_EMAIL_TEMPLATE, MANAGEMENT_PLAN_UPDATE_REQUEST_CREATED_EMAIL_TEMPLATE,
NEW_USER_INVITATION_EMAIL_TEMPLATE)

from submit_cron.services.package_submission_email_service import PackageSubmissionEmailService
from submit_cron.services.ches_service import ChesApiService
from submit_cron.models import db
from submit_cron.services.ches_service import ChesApiService
from submit_cron.services.invitation_email_service import InvitationEmailService
from submit_cron.services.package_submission_email_service import PackageSubmissionEmailService
from submit_cron.services.request_update_email_service import RequestUpdateEmailService
from submit_api.utils.constants import MANAGEMENT_PLAN_SUBMISSION_CONFIRMATION_EMAIL_TEMPLATE, \
MANAGEMENT_PLAN_UPDATE_REQUEST_CREATED_EMAIL_TEMPLATE, \
NEW_USER_INVITATION_EMAIL_TEMPLATE, \
MANAGEMENT_PLAN_SUBMISSION_NOTIFY_STAFF_EMAIL_TEMPLATE, \
MANAGEMENT_PLAN_RESUBMISSION_REQUEST_EMAIL_TEMPLATE
from submit_cron.services.resubmission_email_service import ResubmissionEmailService


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
from submit_api.data_classes.email_details import EmailDetails
from submit_api.exceptions import BadRequestError
from submit_api.models import AccountProject
from submit_api.models.project import Project as ProjectModel
from submit_api.models.package import Package as PackageModel
from submit_api.models.user import User as UserModel
from submit_api.models.account_user import AccountUser as AccountUserModel
from submit_api.models.package import Package as PackageModel
from submit_api.models.project import Project as ProjectModel
from submit_api.models.submission import SubmissionType
from submit_api.models.user import User as UserModel
from submit_api.utils.constants import (
MANAGEMENT_PLAN_SUBMISSION_CONFIRMATION_EMAIL_TEMPLATE, MANAGEMENT_PLAN_SUBMISSION_NOTIFY_STAFF_EMAIL_TEMPLATE)

from submit_cron.utils import constants
from submit_cron.models import db
from submit_api.utils.constants import MANAGEMENT_PLAN_SUBMISSION_CONFIRMATION_EMAIL_TEMPLATE, MANAGEMENT_PLAN_SUBMISSION_NOTIFY_STAFF_EMAIL_TEMPLATE
from submit_cron.utils import constants
from submit_cron.utils.datetime import convert_utc_to_local_str


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from submit_api.data_classes.email_details import EmailDetails
from submit_api.exceptions import BadRequestError
from submit_api.models.package import Package as PackageModel
from submit_api.utils.constants import MANAGEMENT_PLAN_UPDATE_REQUEST_CREATED_EMAIL_TEMPLATE

from submit_cron.utils import constants
from submit_api.utils.constants import MANAGEMENT_PLAN_UPDATE_REQUEST_CREATED_EMAIL_TEMPLATE


class RequestUpdateEmailService: # pylint: disable=too-few-public-methods
Expand Down
Loading
Loading