Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .docker/templates/default.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ server {

port_in_redirect off;

client_max_body_size 10m;

proxy_connect_timeout 3600;
proxy_send_timeout 3600;
proxy_read_timeout 3600;
Expand Down
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ registration_worker_sleep_time=10
# Shipment worker settings
shipment_worker_sleep_time=10
sender_label=Something Corp
physical_mail_forsendelse_type=12345 # Forsendelsestype id agreed with the print provider (Fjernprint)

# Message broker settings
message_broker_queue_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Expand Down
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.2.0]

### Added

- Shipments can now be sent as physical mail (Fysisk Post) or as Digital Post with physical fallback.

### Fixed

- Added error handling on syntax errors in templates.
- Bumped nginx max file size to 10mb.

## [0.1.0]

### Added
Expand All @@ -30,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Initial release

[Unreleased]: https://github.com/itk-dev-rpa/OpenPostbud/compare/0.1.0...HEAD
[Unreleased]: https://github.com/itk-dev-rpa/OpenPostbud/compare/0.2.0...HEAD
[0.2.0]: https://github.com/itk-dev-rpa/OpenPostbud/releases/tag/0.2.0
[0.1.0]: https://github.com/itk-dev-rpa/OpenPostbud/releases/tag/0.1.0
[0.0.1]: https://github.com/itk-dev-rpa/OpenPostbud/releases/tag/0.0.1
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@

## Introduction

OpenPostbud is a web application that makes it possible to do mail merge and mass shipment of Digital Post
using Kombit's Serviceplatformen.
OpenPostbud is a web application that makes it possible to do mail merge and mass shipment of
Digital Post and Fjernpost using Kombit's Serviceplatformen.

A shipment can be sent in one of three ways, chosen by the user when the shipment is created:

- **Digital Post**: Sent as Digital Post. Recipients not registered for Digital Post are marked as failed.
- **Fysisk Post**: Sent as physical mail (Fjernprint). The recipient's address must be printed in the
letter itself so it is visible through the window of the envelope.
- **Digital med fysisk fallback**: Sent as Digital Post if the recipient is registered for it, otherwise
sent as physical mail. The channel each individual letter was sent through is recorded and shown in the UI.

OpenPostbud is split into two logical parts: The web app and the task workers.
The web app is the frontend presented to the user. The workers run in separate processes
Expand Down Expand Up @@ -54,6 +62,7 @@ The shipment, registration and message broker workers need the following environ
| shipment_worker_sleep_time | The number of seconds for the shipment worker to idle | Integer | |
| shipment_worker_delay | The number of seconds to wait before a new shipment is processed | Integer | 300 |
| sender_label | The label to set on the sender of Digital Post | String | |
| physical_mail_forsendelse_type | The forsendelsestype id agreed with the print provider (Fjernprint) | Integer | |
| message_broker_queue_id | The UUID of the message broker queue. Get this from the Kombit admin page | UUID | |
| message_broker_worker_sleep_time | The number of seconds for the message broker worker to idle | Integer | |

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "OpenPostbud"
version = "0.1.0"
version = "0.2.0"
authors = [
{ name="Mathias Gammelgaard", email="ghbm@aarhus.dk" },
]
Expand All @@ -20,7 +20,7 @@ dependencies = [
"SQLAlchemy>=2",
"nicegui>=3.9",
"python-dotenv>=1",
"python_serviceplatformen>=3.1,<4",
"python_serviceplatformen>=3.2,<4",
"passlib>=1.7",
"PyJWT>=2.10",
"docxtpl>=0.20.2",
Expand Down
1 change: 1 addition & 0 deletions src/OpenPostbud/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def format(self, record: logging.LogRecord):
SHIPMENT_WORKER_SLEEP_TIME = float(os.environ['shipment_worker_sleep_time'])
SENDER_LABEL = os.environ['sender_label']
SHIPMENT_WORKER_DELAY = int(os.getenv("shipment_worker_delay", "300"))
PHYSICAL_MAIL_FORSENDELSE_TYPE = int(os.environ['physical_mail_forsendelse_type'])

# Message broker worker
MESSAGE_BROKER_QUEUE_ID = os.environ['message_broker_queue_id']
Expand Down
13 changes: 13 additions & 0 deletions src/OpenPostbud/database/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,16 @@ class ShipmentStatus(Enum):
DELIVERED = "Leveret"
FAILED = "Fejlet"
ABORTED = "Afbrudt"


class PostType(Enum):
"""An enum representing how a shipment should be sent.

DIGITAL: Send as Digital Post only.
PHYSICAL: Send as physical mail (Fysisk Post) only.
AUTO: Send as Digital Post, falling back to physical mail if the
recipient is not registered for Digital Post.
"""
DIGITAL = "Digital Post"
PHYSICAL = "Fysisk Post"
AUTO = "Digital med fysisk fallback"
13 changes: 9 additions & 4 deletions src/OpenPostbud/database/digital_post/letters.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from OpenPostbud.database import connection
from OpenPostbud.database.data_types.encrypted_string import EncryptedString
from OpenPostbud.database.data_types.id_generator import create_id
from OpenPostbud.database.common import ShipmentStatus
from OpenPostbud.database.common import ShipmentStatus, PostType
from OpenPostbud.database.digital_post import templates
from OpenPostbud.database import document_storage
from OpenPostbud.utils import docx_util
Expand Down Expand Up @@ -49,6 +49,7 @@ class Letter(Base):
message: Mapped[str] = mapped_column(String(100), nullable=True)
field_data: Mapped[str] = mapped_column(EncryptedString())
transaction_id: Mapped[str] = mapped_column(nullable=True)
sent_as: Mapped[PostType] = mapped_column(nullable=True)

def to_row_dict(self) -> dict[str, str]:
"""Convert to a dictionary to be shown in a table."""
Expand All @@ -57,7 +58,8 @@ def to_row_dict(self) -> dict[str, str]:
"recipient": self.recipient_id,
"updated_at": self.updated_at.strftime("%d/%m/%Y %H:%M:%S"),
"status": self.status.value,
"message": self.message
"message": self.message,
"sent_as": self.sent_as.value if self.sent_as else ""
}

def merge_letter(self) -> bytes:
Expand All @@ -82,21 +84,24 @@ def merge_letter(self) -> bytes:

return template.file_data

def set_status(self, status: ShipmentStatus, transaction_id: str | None = None, message: str | None = None):
def set_status(self, status: ShipmentStatus, transaction_id: str | None = None, message: str | None = None, sent_as: PostType | None = None):
"""Set the status of the letter in the database.
The transaction id is not overwritten if the given value is None.
The transaction id and sent_as are not overwritten if the given value is None.

Args:
status: The status to set on the letter.
transaction_id: The transaction id from Digital Post. Defaults to None.
message: The message to set on the letter. Defaults to None.
sent_as: The post type the letter was actually sent as. Defaults to None.
"""
values = {}
values["status"] = status
values["updated_at"] = datetime.now()
values["message"] = message
if transaction_id:
values["transaction_id"] = transaction_id
if sent_as:
values["sent_as"] = sent_as

with connection.get_session() as session:
q = (
Expand Down
9 changes: 7 additions & 2 deletions src/OpenPostbud/database/digital_post/shipments.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from OpenPostbud import config
from OpenPostbud.database.base import Base
from OpenPostbud.database import connection
from OpenPostbud.database.common import PostType
from OpenPostbud.database.data_types.id_generator import create_id
from OpenPostbud.database import document_storage

Expand All @@ -23,12 +24,14 @@ class Shipment(Base):
template_id: Mapped[int] = mapped_column(ForeignKey("Templates.id"))
created_at: Mapped[datetime] = mapped_column(default=datetime.now)
created_by: Mapped[str] = mapped_column(String(50))
post_type: Mapped[PostType] = mapped_column(default=PostType.DIGITAL)

def to_row_dict(self):
"""Convert to a dictionary to be shown in a table."""
return {
"id": str(self.id),
"name": self.name,
"post_type": self.post_type.value,
"created_at": self.created_at.strftime("%d/%m/%Y %H:%M:%S"),
"created_by": self.created_by,
}
Expand All @@ -38,14 +41,15 @@ def get_deletion_date(self) -> datetime:
return self.created_at + timedelta(days=config.SHIPMENT_LIFETIME_DAYS)


def add_shipment(name: str, description: str, created_by: str, template_id: int) -> int:
def add_shipment(name: str, description: str, created_by: str, template_id: int, post_type: PostType = PostType.DIGITAL) -> int:
"""Add a new Shipment to the database.

Args:
name: The name of the shipment.
description: The description of the shipment.
created_by: The name of the user who created the shipment.
template_id: The id of the template connected to the shipment.
post_type: How the shipment should be sent. Defaults to Digital Post.

Returns:
The id of the new shipment.
Expand All @@ -54,7 +58,8 @@ def add_shipment(name: str, description: str, created_by: str, template_id: int)
name=name,
description=description,
template_id=template_id,
created_by=created_by
created_by=created_by,
post_type=post_type
)

with connection.get_session() as session:
Expand Down
7 changes: 7 additions & 0 deletions src/OpenPostbud/database/migrations/sql/002_add_post_type.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ALTER TABLE "Shipments" ADD COLUMN post_type VARCHAR(8) NOT NULL DEFAULT 'DIGITAL'


ALTER TABLE "Letters" ADD COLUMN sent_as VARCHAR(8)


UPDATE "Letters" SET sent_as = 'DIGITAL' WHERE transaction_id IS NOT NULL
5 changes: 5 additions & 0 deletions src/OpenPostbud/routes/user/forsendelser.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
SHIPMENTS_COLUMNS = [
{'name': "id", 'label': "ID", 'field': "id"},
{'name': "name", 'label': "Navn", 'field': "name"},
{'name': "post_type", 'label': "Metode", 'field': "post_type"},
{'name': "created_at", 'label': "Oprettet", 'field': "created_at"},
{'name': "created_by", 'label': "Oprettet af", 'field': "created_by"}
]
Expand All @@ -19,6 +20,7 @@
{'name': "id", 'label': "ID", 'field': "id"},
{'name': "recipient", 'label': "Modtager", 'field': "recipient"},
{'name': "status", 'label': "Status", 'field': "status"},
{'name': "sent_as", 'label': "Sendt som", 'field': "sent_as"},
{'name': "updated_at", 'label': "Status Opdateret", 'field': "updated_at"},
{'name': "Message", 'label': "Besked", 'field': "message"}
]
Expand Down Expand Up @@ -81,6 +83,9 @@ def __init__(self, shipment_id: str) -> None:
ui.label("Beskrivelse:").classes("text-bold")
ui_components.MultilineLabel(self.shipment.description)

ui.label("Metode:").classes("text-bold")
ui.label(self.shipment.post_type.value)

ui.label("Skabelon:").classes("text-bold")
ui.link(template_name).on("click", self._download_template)

Expand Down
21 changes: 18 additions & 3 deletions src/OpenPostbud/routes/user/send_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
from nicegui import ui, APIRouter, app
from nicegui import run as nicegui_run
from nicegui.events import UploadEventArguments
from jinja2.exceptions import TemplateSyntaxError

from OpenPostbud import ui_components
from OpenPostbud.middleware import authentication
from OpenPostbud.database.digital_post import letters, shipments, templates
from OpenPostbud.database.digital_post.letters import MemoFields
from OpenPostbud.database.common import PostType
from OpenPostbud.utils import docx_util


Expand All @@ -31,7 +33,7 @@ def page():
"""Show the 'send_post page."""
ui_components.header()
ui.label("Ny Forsendelse").classes("text-4xl")
ui.label("På denne side kan du oprette en ny forsendelse af Digital Post.")
ui.label("På denne side kan du oprette en ny forsendelse af Digital Post eller Fysisk Post.")

SendPostPage()

Expand Down Expand Up @@ -71,7 +73,8 @@ def _send_post(self):
self.step1.shipment_name.value,
self.step1.shipment_desc.value,
authentication.get_current_user(),
template_id)
template_id,
self.step1.post_type.value)
letters.add_letters(shipment_id, self.step2.csv_data)
ui.navigate.to(app.url_path_for("Shipment Detail", shipment_id=shipment_id))
finally:
Expand All @@ -94,6 +97,13 @@ def __init__(self):
validation={"Maks 200 tegn": lambda v: len(v) <= 200, "Skal udfyldes": lambda v: len(v) != 0},
).classes("w-full")

ui.label("Vælg hvordan forsendelsen skal sendes.")
self.post_type = ui.radio({pt: pt.value for pt in PostType}, value=PostType.DIGITAL)
physical_hint = ui.label(
"Bemærk: Ved Fysisk Post skal modtagerens adresse fremgå af brevet, så den kan ses i kuvertens rude."
).classes("text-secondary")
physical_hint.bind_visibility_from(self.post_type, "value", backward=lambda v: v != PostType.DIGITAL)

def validate(self) -> bool:
"""Validator function for step 1."""
name_ok = self.shipment_name.validate()
Expand Down Expand Up @@ -161,7 +171,12 @@ async def _on_template_upload(self, e: UploadEventArguments):
self.template_bytes = await e.file.read()

if self.template_name.endswith(".docx"):
self.template_fields = docx_util.get_merge_fields(self.template_bytes)
try:
self.template_fields = docx_util.get_merge_fields(self.template_bytes)
except TemplateSyntaxError as error:
ui.notify(f"Syntaksfejl i skabelon: {error}", type="negative", timeout=0, actions=[{"label": "Luk", "color": "white"}])
self._remove_template()
raise
else:
self.template_fields = []

Expand Down
32 changes: 25 additions & 7 deletions src/OpenPostbud/workers/message_broker_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,18 @@ def handle_message(message: str):
message_data = envelope_tree.find("kuvert:Beskeddata/besked:Base64", ENVELOPE_NAMESPACES).text
message_data = base64.b64decode(message_data).decode()

if event_uuid in EVENTS_DIGITAL:
if sender_name == "Digital Post":
handle_digital_post_message(
message_time=message_time,
sender_name=sender_name,
event_name=event_name,
message_data=message_data)
else:
handle_physical_mail_message()
handle_physical_mail_message(
message_time=message_time,
sender_name=sender_name,
event_name=event_name,
message_data=message_data)


def handle_digital_post_message(message_time: str, sender_name: str, event_name: str, message_data: str):
Expand All @@ -167,11 +171,25 @@ def handle_digital_post_message(message_time: str, sender_name: str, event_name:
update_letter_status(message_uuid, event_name, error_message)


def handle_physical_mail_message():
"""Handle a message from the a physical mail sender."""
# We currently don't support physical mail.
# We don't know how to handle them properly.
logging.error("Physical mail messages are not currently supported.")
def handle_physical_mail_message(message_time: str, sender_name: str, event_name: str, message_data: str):
"""Handle a status message from a physical mail (Fjernprint) provider.

Args:
message_time: The message time from the message.
sender_name: The sender name from the message.
event_name: The event name from the message.
message_data: The decoded base64 message data.
"""
# Decode message
message_tree = ElementTree.fromstring(message_data)
afsendelse_id = message_tree.find("default:AfsendelseIdentifikator", MESSAGE_NAMESPACES).text
error_message = message_tree.find("default:FejlDetaljer/default:FejlTekst", MESSAGE_NAMESPACES)
if error_message is not None:
error_message = error_message.text

logging.info(f"Message received: {message_time} - {sender_name=} - {event_name=} - {afsendelse_id=} - {error_message=}")

update_letter_status(afsendelse_id, event_name, error_message)


def update_letter_status(transaction_id: str, event_name: str, error: str | None):
Expand Down
Loading
Loading