Compare commits

..

1 Commits

Author SHA1 Message Date
55254b20f7 Telemetry / logging work in progress 2025-04-28 14:54:49 +02:00
61 changed files with 333 additions and 1596 deletions

3
.vscode/launch.json vendored
View File

@ -12,8 +12,7 @@
"env": { "env": {
"FLASK_APP": "src/server/BreCal", "FLASK_APP": "src/server/BreCal",
"FLASK_DEBUG": "1", "FLASK_DEBUG": "1",
"SECRET_KEY" : "zdiTz8P3jXOc7jztIQAoelK4zztyuCpJ", // https://randomkeygen.com/ "SECRET_KEY" : "zdiTz8P3jXOc7jztIQAoelK4zztyuCpJ" // https://randomkeygen.com/
"FLASK_RUN_PORT": "5000"
}, },
"args": [ "args": [
"run", "run",

View File

@ -1,537 +0,0 @@
# Bremen calling API
Version: _1.7.0_
Last change: _Nov 14, 2025_
## Introduction
This API allows users to interact with "Bremen calling" without an UI. Apart vom querying data via _GET_ endpoints users may create and update shipcalls, assign participants and update participant times for shipcalls.
Creating and updating times and shipcalls depend on the participant roles a user is assigned to. For example, if a participant has the role "AGENCY" they may change assignments _and_ create and update agency times. A participant with the role "PILOT" on the other hand may not change the assigments and only create/update times for the pilot.
### Authentication
- **ApiKey**: API key in `header` header named `Authorization`. This is a JWT Token that the caller receives upon login.
### Notes on this version
This version refers to _1.7_ whereas the public client currently has version _1.6_. This means that there is some functionality available in the API that cannot be accessed through the UI yet, specifically notifications.
There is no documentation for the structures returned by _GET_ requests but these can easily be determined via a single query.
## Ship Endpoints
### `DELETE /ships`
**Summary:** Delete a ship (logically).
A ship can only be logically deleted, since it is possible to have been used in previous shipcalls. On logical delete, the ship can no longer be selected in a new ship call.
#### Parameters
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| id | query | integer | Yes | **Id of ship**. *Example: 42*. Id of ship to be deleted. |
#### Responses
- **200**
- **400**
- **401**
- **500**
- **503**
---
### `GET /ships`
**Summary:** gets a list of ships
Gets a list of ships including logically deleted ships to be used with shipcalls
#### Responses
- **200**: list of ships
- **400**
- **401**
- **500**
- **503**
---
### `POST /ships`
**Summary:** create a new ship entry
adds a new non-existing ship to the database. The ships IMO number is the unique identifier.
#### Request Body
Ship details. **Do not** provide id parameter.
**JSON Schema**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | integer | No | |
| name | string | No | |
| imo | integer | No | |
| callsign | string | No | |
| participant_id | integer | No | Optional reference to participant (tug role) |
| length | number | No | |
| width | number | No | |
| is_tug | boolean | No | |
| bollard_pull | integer | No | |
| eni | integer | No | BSMD internal use |
| created | string | No | Readonly field set by the database when ship was created |
| modified | string | No | Readonly field set by the database when ship was last modified |
| deleted | boolean | No | marks the ship as logically deleted |
#### Responses
- **201**
- **400**
- **401**
- **500**
- **503**
---
### `PUT /ships`
**Summary:** Update a ship entry
Updating a ship entry. Please do not modify the IMO number. In that case please add a new entry.
#### Request Body
Updated ship entry. The id parameter is **required**.
**JSON Schema**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | integer | No | |
| name | string | No | |
| imo | integer | No | |
| callsign | string | No | |
| participant_id | integer | No | Optional reference to participant (tug role) |
| length | number | No | |
| width | number | No | |
| is_tug | boolean | No | |
| bollard_pull | integer | No | |
| eni | integer | No | BSMD internal use |
| created | string | No | Readonly field set by the database when ship was created |
| modified | string | No | Readonly field set by the database when ship was last modified |
| deleted | boolean | No | marks the ship as logically deleted |
#### Responses
- **200**
- **400**
- **401**
- **500**
- **503**
---
## Shipcall Endpoints
### `GET /shipcalls`
**Summary:** Gets a list of ship calls
Get current ship calls
#### Parameters
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| past_days | query | integer | No | number of days in the past to include in the result. *Example: 7*. |
#### Responses
- **200**: ship call list
- **400**
- **401**
- **500**
- **503**
---
### `POST /shipcalls`
**Summary:** Create a new ship call
A new shipcall is created without times at this point. This is ususally done by the BSMD or a participant with that particular role.
#### Request Body
Creates a new ship call. **Do not** provide id parameter.
**JSON Schema**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | integer | No | |
| ship_id | integer | Yes | |
| port_id | integer | No | |
| type | string | Yes | Type of ship call |
| eta | string | No | |
| voyage | string | No | |
| etd | string | No | |
| arrival_berth_id | integer | No | |
| departure_berth_id | integer | No | |
| tug_required | boolean | No | |
| pilot_required | boolean | No | |
| flags | integer | No | |
| pier_side | boolean | No | |
| bunkering | boolean | No | |
| replenishing_terminal | boolean | No | |
| replenishing_lock | boolean | No | |
| draft | number | No | |
| tidal_window_from | string | No | |
| tidal_window_to | string | No | |
| rain_sensitive_cargo | boolean | No | |
| recommended_tugs | integer | No | |
| anchored | boolean | No | |
| moored_lock | boolean | No | |
| canceled | boolean | No | |
| evaluation | string | No | Evaluation of the ship call |
| evaluation_message | string | No | |
| time_ref_point | integer | No | Physical reference point for all times given in shipcall and depending times entries |
| participants | array<object> | No | |
| created | string | No | Readonly field set by the database when shipcall was created |
| modified | string | No | Readonly field set by the database when shipcall was last modified |
#### Responses
- **201**
- **400**
- **401**
- **500**
- **503**
---
### `PUT /shipcalls`
**Summary:** Updates a ship call
Updates a shipcall. Usually done if the participant assignments change.
#### Request Body
Creates a new ship call. The id parameter is **required**.
**JSON Schema**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | integer | No | |
| ship_id | integer | Yes | |
| port_id | integer | No | |
| type | string | Yes | Type of ship call |
| eta | string | No | |
| voyage | string | No | |
| etd | string | No | |
| arrival_berth_id | integer | No | |
| departure_berth_id | integer | No | |
| tug_required | boolean | No | |
| pilot_required | boolean | No | |
| flags | integer | No | |
| pier_side | boolean | No | |
| bunkering | boolean | No | |
| replenishing_terminal | boolean | No | |
| replenishing_lock | boolean | No | |
| draft | number | No | |
| tidal_window_from | string | No | |
| tidal_window_to | string | No | |
| rain_sensitive_cargo | boolean | No | |
| recommended_tugs | integer | No | |
| anchored | boolean | No | |
| moored_lock | boolean | No | |
| canceled | boolean | No | |
| evaluation | string | No | Evaluation of the ship call |
| evaluation_message | string | No | |
| time_ref_point | integer | No | Physical reference point for all times given in shipcall and depending times entries |
| participants | array<object> | No | |
| created | string | No | Readonly field set by the database when shipcall was created |
| modified | string | No | Readonly field set by the database when shipcall was last modified |
#### Responses
- **200**
- **400**
- **401**
- **500**
- **503**
---
## Static Endpoints
### `GET /berths`
**Summary:** Gets a list of all berths registered
Returns a list of berths, including berths that are (logically) deleted
#### Responses
- **200**: list of berths
- **400**
- **401**
- **500**
- **503**
---
### `GET /history`
**Summary:** History data
This endpoint returns a list of changes made to the specific shipcall
#### Parameters
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| shipcall_id | query | integer | Yes | **Id of ship call**. *Example: 3*. Id given in ship call list |
#### Responses
- **200**: list of history entries
- **400**
- **401**
- **500**
- **503**
---
### `GET /notifications`
**Summary:** Gets a list of notifications pursuant to a specified participant and ship call
List of notifications (tbd)
#### Parameters
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| shipcall_id | query | integer | No | **Id of ship call**. *Example: 52*. Id given in ship call list |
#### Responses
- **200**: notification list
- **400**
- **401**
- **500**
- **503**
---
### `GET /participants`
**Summary:** gets one or all participants
If no parameter is given, all participants are returned. The list can be used to display participant information in the context of ship calls.
#### Parameters
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| user_id | query | integer | No | **Id of user**. *Example: 2*. User id returned by verify call. |
#### Responses
- **200**: one or all participants as list
- **400**
- **401**
- **404**
- **500**
- **503**
---
### `GET /ports`
**Summary:** Your GET endpoint
Returns a list of ports
#### Responses
- **200**: list of ports
- **401**
- **403**
- **500**
- **503**
---
## Times Endpoints
### `DELETE /times`
**Summary:** Delete a times entry for a ship call.
A times entry is typically deleted if the agent for example changes or removes the participant assignment for a particular role.
#### Parameters
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| id | query | integer | Yes | **Id of times**. *Example: 42*. Id of times entry to be deleted. |
#### Responses
- **200**
- **400**
- **401**
- **500**
- **503**
---
### `GET /times`
**Summary:** Gets list of times
Get all times assigned to a shipcall. These might not be complete.
#### Parameters
| Name | In | Type | Required | Description |
|------|----|------|----------|-------------|
| shipcall_id | query | integer | No | **Id**. *Example: 42*. Id of referenced ship call. |
#### Responses
- **200**: list of recorded times
- **400**
- **401**
- **500**
- **503**
---
### `POST /times`
**Summary:** Create a new times entry for a ship call
The times entry for a shipcall is created with reference to a participant. For each participant type there should be only one times data record.
#### Request Body
Times entry that will be added to the ship call. **Do not** provide id parameter.
**JSON Schema**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | integer | No | |
| eta_berth | string | No | Arrival time at berth |
| eta_berth_fixed | boolean | No | If true, the eta is fixed and cannot be changed |
| etd_berth | string | No | departure time from berth |
| etd_berth_fixed | boolean | No | If true, the etd is fixed and cannot be changed |
| lock_time | string | No | arrival time at lock |
| lock_time_fixed | boolean | No | If true, the lock time is fixed and cannot be changed |
| zone_entry | string | No | Expected time of entry into the zone |
| zone_entry_fixed | boolean | No | If true, the zone entry time is fixed and cannot be changed |
| operations_start | string | No | Start time for terminal operations |
| operations_end | string | No | End time for terminal operations |
| remarks | string | No | Additional remarks |
| shipcall_id | integer | Yes | Reference to a shipcall id |
| participant_id | integer | Yes | Reference to a participant id |
| berth_id | integer | No | Reference to a berth id |
| berth_info | string | No | Additional info text for berth |
| pier_side | boolean | No | true if ship is rotated, false otherwise |
| participant_type | integer | No | |
| ata | string | No | ata can be set by mooring if actual times are different from planned |
| atd | string | No | atd can be set by mooring if actual times are different from planned |
| eta_interval_end | string | No | Optional end of the interval for the times eta entry |
| etd_interval_end | string | No | Optional end of the interval for the times etd entry |
| created | string | No | Readonly field set by the database when times record was created |
| modified | string | No | Readonly field set by the database when times record was last modified |
#### Responses
- **201**
- **400**
- **401**
- **500**
- **503**
---
### `PUT /times`
**Summary:** Update a times entry for a ship call
Updating a times entry for a ship for a particular participant. The times entries are required for a shipcall to pass the validation rules.
#### Request Body
Times entry that will be added to the ship call. The id parameter is **required**.
**JSON Schema**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | integer | No | |
| eta_berth | string | No | Arrival time at berth |
| eta_berth_fixed | boolean | No | If true, the eta is fixed and cannot be changed |
| etd_berth | string | No | departure time from berth |
| etd_berth_fixed | boolean | No | If true, the etd is fixed and cannot be changed |
| lock_time | string | No | arrival time at lock |
| lock_time_fixed | boolean | No | If true, the lock time is fixed and cannot be changed |
| zone_entry | string | No | Expected time of entry into the zone |
| zone_entry_fixed | boolean | No | If true, the zone entry time is fixed and cannot be changed |
| operations_start | string | No | Start time for terminal operations |
| operations_end | string | No | End time for terminal operations |
| remarks | string | No | Additional remarks |
| shipcall_id | integer | Yes | Reference to a shipcall id |
| participant_id | integer | Yes | Reference to a participant id |
| berth_id | integer | No | Reference to a berth id |
| berth_info | string | No | Additional info text for berth |
| pier_side | boolean | No | true if ship is rotated, false otherwise |
| participant_type | integer | No | |
| ata | string | No | ata can be set by mooring if actual times are different from planned |
| atd | string | No | atd can be set by mooring if actual times are different from planned |
| eta_interval_end | string | No | Optional end of the interval for the times eta entry |
| etd_interval_end | string | No | Optional end of the interval for the times etd entry |
| created | string | No | Readonly field set by the database when times record was created |
| modified | string | No | Readonly field set by the database when times record was last modified |
#### Responses
- **200**
- **400**
- **401**
- **500**
- **503**
---
## User Endpoints
### `POST /login`
**Summary:** Returns a JWT session token and user data if successful
Perform login
#### Request Body
Login credentials
**JSON Schema**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| username | string | Yes | |
| password | string | Yes | |
#### Responses
- **200**: Successful response
- **400**
- **403**
- **500**
- **503**
**Response 200 JSON Schema**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | integer | No | |
| participant_id | integer | No | |
| first_name | string | No | |
| last_name | string | No | |
| user_name | string | No | |
| user_phone | string | No | |
| user_email | string | No | |
| notify_email | boolean | No | |
| notify_whatsapp | boolean | No | |
| notify_signal | boolean | No | |
| notify_popup | boolean | No | |
| exp | number | No | |
| token | string | No | |
| notify_on | array<string> | No | |
---
### `PUT /user`
**Summary:** Update user details (first/last name, phone, password)
Update user information
#### Request Body
User details
**JSON Schema**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | integer | No | |
| old_password | string | No | |
| new_password | string | No | |
| first_name | string | No | |
| last_name | string | No | |
| user_phone | string | No | |
| user_email | string | No | |
| notify_email | boolean | No | |
| notify_popup | boolean | No | |
| notify_whatsapp | boolean | No | |
| notify_signal | boolean | No | |
| notify_on | array<string> | No | |
#### Responses
- **200**
- **400**
- **401**
- **500**
- **503**
---

View File

@ -22,8 +22,6 @@ tags:
- name: times - name: times
- name: static - name: static
- name: ship - name: ship
- name: notification
- name: history
paths: paths:
/login: /login:
post: post:
@ -32,7 +30,6 @@ paths:
tags: tags:
- user - user
operationId: login operationId: login
security: []
requestBody: requestBody:
description: Login credentials description: Login credentials
required: true required: true
@ -259,7 +256,7 @@ paths:
- shipcall - shipcall
operationId: shipcallUpdate operationId: shipcallUpdate
requestBody: requestBody:
description: Updates a ship call. The id parameter is **required**. description: Creates a new ship call. The id parameter is **required**.
required: true required: true
content: content:
application/json: application/json:
@ -446,7 +443,7 @@ paths:
- name: user_id - name: user_id
in: query in: query
required: false required: false
description: '**Id of user**. *Example: 2*. User id returned by login call.' description: '**Id of user**. *Example: 2*. User id returned by verify call.'
schema: schema:
type: integer type: integer
example: 2 example: 2
@ -584,7 +581,7 @@ paths:
- times - times
operationId: timesUpdate operationId: timesUpdate
requestBody: requestBody:
description: Times entry that will be updated for the ship call. The id parameter is **required**. description: Times entry that will be added to the ship call. The id parameter is **required**.
required: true required: true
content: content:
application/json: application/json:
@ -652,18 +649,18 @@ paths:
$ref: '#/components/responses/503' $ref: '#/components/responses/503'
/notifications: /notifications:
get: get:
summary: Gets a list of notifications pursuant to a specified ship call summary: Gets a list of notifications pursuant to a specified participant and ship call
description: List of notifications (tbd) description: List of notifications (tbd)
tags: tags:
- notification - static
operationId: notificationsGet operationId: notificationsGet
parameters: parameters:
- name: participant_id - name: shipcall_id
in: query in: query
required: false required: false
description: '**Id of participant**. *Example: 7*. Id of logged in participant.' description: '**Id of ship call**. *Example: 52*. Id given in ship call list'
schema: schema:
$ref: '#/components/schemas/participant_id' $ref: '#/components/schemas/shipcallId'
responses: responses:
'200': '200':
description: notification list description: notification list
@ -726,7 +723,7 @@ paths:
description: This endpoint returns a list of changes made to the specific shipcall description: This endpoint returns a list of changes made to the specific shipcall
summary: History data summary: History data
tags: tags:
- history - static
operationId: historyGet operationId: historyGet
parameters: parameters:
- name: shipcall_id - name: shipcall_id
@ -1759,11 +1756,10 @@ components:
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
user_details: user_details:
type: object type: object
description: user metadata and editable fields description: fields that a user may change
properties: properties:
id: id:
type: integer type: integer
readOnly: true
example: 42 example: 42
old_password: old_password:
type: string type: string

View File

@ -48,156 +48,3 @@ DROP TABLE IF EXISTS `shipcall`;
SET FOREIGN_KEY_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;
``` ```
## Schema
```mermaid
erDiagram
participant {
INT id PK
VARCHAR name
VARCHAR street
VARCHAR postal_code
VARCHAR city
INT type
INT flags
}
port {
INT id PK
VARCHAR name
CHAR locode
}
berth {
INT id PK
VARCHAR name
BIT lock
INT owner_id FK
INT authority_id FK
INT port_id FK
BIT deleted
}
ship {
INT id PK
VARCHAR name
INT imo
VARCHAR callsign
INT participant_id FK
FLOAT length
FLOAT width
BIT is_tug
INT bollard_pull
INT eni
BIT deleted
}
shipcall {
INT id PK
INT ship_id FK
TINYINT type
DATETIME eta
DATETIME etd
INT arrival_berth_id FK
INT departure_berth_id FK
INT port_id FK
INT flags
BIT tug_required
BIT pilot_required
}
times {
INT id PK
INT shipcall_id FK
INT participant_id FK
INT berth_id FK
INT participant_type
DATETIME eta_berth
DATETIME etd_berth
DATETIME lock_time
DATETIME zone_entry
}
notification {
INT id PK
INT shipcall_id FK
INT participant_id FK
TINYINT level
TINYINT type
}
history {
INT id PK
INT participant_id FK
INT user_id FK
INT shipcall_id FK
DATETIME timestamp
DATETIME eta
INT type
INT operation
}
shipcall_participant_map {
INT id PK
INT shipcall_id FK
INT participant_id FK
INT type
}
shipcall_tug_map {
INT id PK
INT shipcall_id FK
INT ship_id FK
}
participant_port_map {
INT id PK
INT participant_id FK
INT port_id FK
}
user {
INT id PK
INT participant_id FK
VARCHAR first_name
VARCHAR last_name
VARCHAR user_name
VARCHAR user_email
}
role {
INT id PK
VARCHAR name
VARCHAR description
}
securable {
INT id PK
VARCHAR name
}
role_securable_map {
INT id PK
INT role_id FK
INT securable_id FK
}
user_role_map {
INT id PK
INT user_id FK
INT role_id FK
}
participant ||--o{ berth : owner_id
participant ||--o{ berth : authority_id
port ||--o{ berth : port_id
participant ||--o{ ship : participant_id
ship ||--o{ shipcall : ship_id
berth ||--o{ shipcall : arrival_berth_id
berth ||--o{ shipcall : departure_berth_id
port ||--o{ shipcall : port_id
shipcall ||--|| times : shipcall_id
participant ||--|| times : participant_id
berth ||--o{ times : berth_id
shipcall ||--o{ notification : shipcall_id
participant ||--o{ notification : participant_id
participant ||--o{ history : participant_id
user ||--o{ history : user_id
shipcall ||--o{ history : shipcall_id
shipcall ||--o{ shipcall_participant_map : shipcall_id
participant ||--o{ shipcall_participant_map : participant_id
shipcall ||--o{ shipcall_tug_map : shipcall_id
ship ||--o{ shipcall_tug_map : ship_id
participant ||--o{ participant_port_map : participant_id
port ||--o{ participant_port_map : port_id
participant ||--o{ user : participant_id
user ||--o{ user_role_map : user_id
role ||--o{ user_role_map : role_id
role ||--o{ role_securable_map : role_id
securable ||--o{ role_securable_map : securable_id
```

View File

@ -1,46 +0,0 @@
-- Inspect duplicates first
WITH duplicate_participants AS (
SELECT
shipcall_id,
participant_type,
COUNT(*) AS cnt
FROM times
GROUP BY shipcall_id, participant_type
HAVING COUNT(*) > 1
)
SELECT
t.*
FROM times AS t
JOIN duplicate_participants AS d
ON d.shipcall_id = t.shipcall_id
AND (d.participant_type <=> t.participant_type)
ORDER BY t.shipcall_id, t.participant_type, t.id;
-- Delete all but the highest-id entry per (shipcall_id, participant_type)
WITH ordered_times AS (
SELECT
id,
ROW_NUMBER() OVER (
PARTITION BY shipcall_id, participant_type
ORDER BY id DESC
) AS rn
FROM times
)
DELETE FROM times
WHERE id IN (
SELECT id
FROM ordered_times
WHERE rn > 1
);
-- Optional: re-check that no duplicates remain
WITH duplicate_participants AS (
SELECT
shipcall_id,
participant_type,
COUNT(*) AS cnt
FROM times
GROUP BY shipcall_id, participant_type
HAVING COUNT(*) > 1
)
SELECT COUNT(*) AS remaining_duplicates FROM duplicate_participants;

View File

@ -1,37 +1,11 @@
-- This script clears all data from the database tables related to the port management system. use bremen_calling_test;
DELETE FROM notification WHERE id > 0; DELETE FROM notification WHERE id > 0;
DELETE FROM history WHERE id > 0; DELETE FROM history WHERE id > 0;
DELETE FROM notification WHERE id > 0;
DELETE FROM shipcall_participant_map WHERE id > 0; DELETE FROM shipcall_participant_map WHERE id > 0;
DELETE FROM participant_port_map WHERE id > 0;
DELETE FROM shipcall_tug_map WHERE id > 0;
DELETE FROM times WHERE id > 0; DELETE FROM times WHERE id > 0;
DELETE FROM shipcall WHERE id > 0; DELETE FROM shipcall WHERE id > 0;
DELETE FROM user_role_map WHERE id > 0;
DELETE FROM role_securable_map WHERE id > 0;
DELETE FROM user_role_map WHERE id > 0;
DELETE FROM securable WHERE id > 0;
DELETE FROM role WHERE id > 0;
DELETE FROM user WHERE id > 0;
delete FROM ship WHERE id > 0;
DELETE FROM berth WHERE id > 0;
DELETE FROM participant WHERE id > 0;
DELETE FROM port WHERE id > 0;

View File

@ -1,8 +1,8 @@
-- MySQL dump 10.13 Distrib 8.0.43, for Win64 (x86_64) -- MySQL dump 10.13 Distrib 8.0.33, for Win64 (x86_64)
-- --
-- Host: localhost Database: bremen_calling_test -- Host: localhost Database: bremen_calling_test
-- ------------------------------------------------------ -- ------------------------------------------------------
-- Server version 8.0.42-0ubuntu0.24.10.1 -- Server version 8.0.34-0ubuntu0.22.04.1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
@ -28,65 +28,17 @@ CREATE TABLE `berth` (
`lock` bit(1) DEFAULT NULL COMMENT 'The lock must be used', `lock` bit(1) DEFAULT NULL COMMENT 'The lock must be used',
`owner_id` int unsigned DEFAULT NULL, `owner_id` int unsigned DEFAULT NULL,
`authority_id` int unsigned DEFAULT NULL, `authority_id` int unsigned DEFAULT NULL,
`port_id` int unsigned DEFAULT NULL,
`created` datetime DEFAULT CURRENT_TIMESTAMP, `created` datetime DEFAULT CURRENT_TIMESTAMP,
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, `modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) DEFAULT b'0', `deleted` bit(1) DEFAULT b'0',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `FK_OWNER_PART_idx` (`owner_id`), KEY `FK_OWNER_PART_idx` (`owner_id`),
KEY `FK_AUTHORITY_PART_idx` (`authority_id`) /*!80000 INVISIBLE */, KEY `FK_AUTHORITY_PART_idx` (`authority_id`),
KEY `FK_PORT_PART_idx` (`port_id`),
CONSTRAINT `FK_AUTHORITY_PART` FOREIGN KEY (`authority_id`) REFERENCES `participant` (`id`), CONSTRAINT `FK_AUTHORITY_PART` FOREIGN KEY (`authority_id`) REFERENCES `participant` (`id`),
CONSTRAINT `FK_OWNER_PART` FOREIGN KEY (`owner_id`) REFERENCES `participant` (`id`), CONSTRAINT `FK_OWNER_PART` FOREIGN KEY (`owner_id`) REFERENCES `participant` (`id`)
CONSTRAINT `FK_PORT` FOREIGN KEY (`port_id`) REFERENCES `port` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT ) ENGINE=InnoDB AUTO_INCREMENT=195 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Berth of ship for a ship call';
) ENGINE=InnoDB AUTO_INCREMENT=205 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Berth of ship for a ship call';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `history`
--
DROP TABLE IF EXISTS `history`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `history` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`participant_id` int unsigned NOT NULL,
`user_id` int unsigned DEFAULT NULL,
`shipcall_id` int unsigned NOT NULL,
`timestamp` datetime NOT NULL COMMENT 'Time of saving',
`eta` datetime DEFAULT NULL COMMENT 'Current ETA / ETD value (depends if shipcall or times were saved)',
`type` int NOT NULL COMMENT 'shipcall or times',
`operation` int NOT NULL COMMENT 'insert, update or delete',
PRIMARY KEY (`id`),
KEY `FK_HISTORY_PARTICIPANT_idx` (`participant_id`),
KEY `FK_HISTORY_SHIPCALL_idx` (`shipcall_id`),
KEY `FK_HISTORY_USER` (`user_id`),
CONSTRAINT `FK_HISTORY_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`),
CONSTRAINT `FK_HISTORY_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`),
CONSTRAINT `FK_HISTORY_USER` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=23537 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='This table stores a history of changes made to shipcalls so that everyone can see who changed what and when';
/*!40101 SET character_set_client = @saved_cs_client */;
CREATE TABLE `history` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`participant_id` int unsigned NOT NULL,
`user_id` int unsigned DEFAULT NULL,
`shipcall_id` int unsigned NOT NULL,
`timestamp` datetime NOT NULL COMMENT 'Time of saving',
`eta` datetime DEFAULT NULL COMMENT 'Current ETA / ETD value (depends if shipcall or times were saved)',
`type` int NOT NULL COMMENT 'shipcall or times',
`operation` int NOT NULL COMMENT 'insert, update or delete',
PRIMARY KEY (`id`),
KEY `FK_HISTORY_PARTICIPANT_idx` (`participant_id`),
KEY `FK_HISTORY_SHIPCALL_idx` (`shipcall_id`),
KEY `FK_HISTORY_USER` (`user_id`),
CONSTRAINT `FK_HISTORY_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`),
CONSTRAINT `FK_HISTORY_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`),
CONSTRAINT `FK_HISTORY_USER` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=29292 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='This table stores a history of changes made to shipcalls so that everyone can see who changed what and when';
-- --
-- Table structure for table `notification` -- Table structure for table `notification`
-- --
@ -96,19 +48,20 @@ DROP TABLE IF EXISTS `notification`;
/*!50503 SET character_set_client = utf8mb4 */; /*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `notification` ( CREATE TABLE `notification` (
`id` int unsigned NOT NULL AUTO_INCREMENT, `id` int unsigned NOT NULL AUTO_INCREMENT,
`shipcall_id` int unsigned DEFAULT NULL, `times_id` int unsigned NOT NULL COMMENT 'times record that caused the notification',
`participant_id` int unsigned DEFAULT NULL, `participant_id` int unsigned NOT NULL COMMENT 'participant ref',
`acknowledged` bit(1) DEFAULT b'0' COMMENT 'true if UI acknowledged',
`level` tinyint DEFAULT NULL COMMENT 'severity of the notification', `level` tinyint DEFAULT NULL COMMENT 'severity of the notification',
`type` tinyint DEFAULT NULL COMMENT 'Email/UI/Other', `type` tinyint DEFAULT NULL COMMENT 'Email/UI/Other',
`message` varchar(512) DEFAULT NULL COMMENT 'individual message', `message` varchar(256) DEFAULT NULL COMMENT 'individual message',
`created` datetime DEFAULT CURRENT_TIMESTAMP, `created` datetime DEFAULT CURRENT_TIMESTAMP,
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, `modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `FK_NOTIFICATION_SHIPCALL_idx` (`shipcall_id`), KEY `FK_NOT_TIMES` (`times_id`),
KEY `FK_NOTIFICATION_PARTICIPANT_idx` (`participant_id`), KEY `FK_NOT_PART` (`participant_id`),
CONSTRAINT `FK_NOTIFICATION_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, CONSTRAINT `FK_NOT_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`),
CONSTRAINT `FK_NOTIFICATION_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`) CONSTRAINT `FK_NOT_TIMES` FOREIGN KEY (`times_id`) REFERENCES `times` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10398 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='An entry corresponds to an alarm given by a violated rule during times update'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='An entry corresponds to an alarm given by a violated rule during times update';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
@ -130,46 +83,7 @@ CREATE TABLE `participant` (
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, `modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) DEFAULT b'0', `deleted` bit(1) DEFAULT b'0',
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=160 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='An organization taking part'; ) ENGINE=InnoDB AUTO_INCREMENT=137 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='An organization taking part';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `participant_port_map`
--
DROP TABLE IF EXISTS `participant_port_map`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `participant_port_map` (
`id` int NOT NULL AUTO_INCREMENT,
`participant_id` int unsigned NOT NULL COMMENT 'Ref to participant',
`port_id` int unsigned NOT NULL COMMENT 'Ref to port',
`created` datetime DEFAULT CURRENT_TIMESTAMP,
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `FK_PP_PARTICIPANT` (`participant_id`),
KEY `FK_PP_PORT` (`port_id`),
CONSTRAINT `FK_PP_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`),
CONSTRAINT `FK_PP_PORT` FOREIGN KEY (`port_id`) REFERENCES `port` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=86 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Mapping table that assigns participants to a port';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `port`
--
DROP TABLE IF EXISTS `port`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `port` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(128) NOT NULL COMMENT 'Name of port',
`locode` char(5) DEFAULT NULL COMMENT 'UNECE locode',
`created` datetime DEFAULT CURRENT_TIMESTAMP,
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) DEFAULT b'0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Port as reference for shipcalls and berths';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
@ -252,7 +166,7 @@ CREATE TABLE `ship` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `FK_SHIP_PARTICIPANT` (`participant_id`), KEY `FK_SHIP_PARTICIPANT` (`participant_id`),
CONSTRAINT `FK_SHIP_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`) CONSTRAINT `FK_SHIP_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=485 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; ) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
@ -288,25 +202,16 @@ CREATE TABLE `shipcall` (
`canceled` bit(1) DEFAULT NULL, `canceled` bit(1) DEFAULT NULL,
`evaluation` int unsigned DEFAULT NULL, `evaluation` int unsigned DEFAULT NULL,
`evaluation_message` varchar(512) DEFAULT NULL, `evaluation_message` varchar(512) DEFAULT NULL,
`evaluation_time` datetime DEFAULT NULL,
`evaluation_notifications_sent` bit(1) DEFAULT NULL,
`port_id` int unsigned NOT NULL DEFAULT '1' COMMENT 'Selected port for this shipcall',
`time_ref_point` int DEFAULT '0' COMMENT 'Index of a location which is the reference point for all time value entries, e.g. berth or Geeste',
`created` datetime DEFAULT CURRENT_TIMESTAMP, `created` datetime DEFAULT CURRENT_TIMESTAMP,
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, `modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `FK_SHIPCALL_SHIP` (`ship_id`), KEY `FK_SHIPCALL_SHIP` (`ship_id`),
KEY `FK_SHIPCALL_BERTH_ARRIVAL` (`arrival_berth_id`), KEY `FK_SHIPCALL_BERTH_ARRIVAL` (`arrival_berth_id`),
KEY `FK_SHIPCALL_BERTH_DEPARTURE` (`departure_berth_id`), KEY `FK_SHIPCALL_BERTH_DEPARTURE` (`departure_berth_id`),
KEY `idx_shipcall_type` (`type`),
KEY `idx_shipcall_eta` (`eta`),
KEY `idx_shipcall_etd` (`etd`),
KEY `FK_SHIPCALL_PORT_idx` (`port_id`),
CONSTRAINT `FK_SHIPCALL_BERTH_ARRIVAL` FOREIGN KEY (`arrival_berth_id`) REFERENCES `berth` (`id`), CONSTRAINT `FK_SHIPCALL_BERTH_ARRIVAL` FOREIGN KEY (`arrival_berth_id`) REFERENCES `berth` (`id`),
CONSTRAINT `FK_SHIPCALL_BERTH_DEPARTURE` FOREIGN KEY (`departure_berth_id`) REFERENCES `berth` (`id`), CONSTRAINT `FK_SHIPCALL_BERTH_DEPARTURE` FOREIGN KEY (`departure_berth_id`) REFERENCES `berth` (`id`),
CONSTRAINT `FK_SHIPCALL_PORT` FOREIGN KEY (`port_id`) REFERENCES `port` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_SHIPCALL_SHIP` FOREIGN KEY (`ship_id`) REFERENCES `ship` (`id`) CONSTRAINT `FK_SHIPCALL_SHIP` FOREIGN KEY (`ship_id`) REFERENCES `ship` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2789 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Incoming, outgoing or moving to another berth'; ) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Incoming, outgoing or moving to another berth';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
@ -320,15 +225,15 @@ CREATE TABLE `shipcall_participant_map` (
`id` int NOT NULL AUTO_INCREMENT, `id` int NOT NULL AUTO_INCREMENT,
`shipcall_id` int unsigned DEFAULT NULL, `shipcall_id` int unsigned DEFAULT NULL,
`participant_id` int unsigned DEFAULT NULL, `participant_id` int unsigned DEFAULT NULL,
`type` int unsigned DEFAULT NULL, `type` int unsigned DEFAULT NULL COMMENT 'Type of participant role',
`created` datetime DEFAULT CURRENT_TIMESTAMP, `created` datetime DEFAULT CURRENT_TIMESTAMP,
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, `modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `FK_MAP_PARTICIPANT_SHIPCALL` (`shipcall_id`), KEY `FK_MAP_PARTICIPANT_SHIPCALL` (`shipcall_id`),
KEY `FK_MAP_SHIPCALL_PARTICIPANT` (`participant_id`), KEY `FK_MAP_SHIPCALL_PARTICIPANT` (`participant_id`),
CONSTRAINT `FK_MAP_PARTICIPANT_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`) ON DELETE SET NULL, CONSTRAINT `FK_MAP_PARTICIPANT_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`),
CONSTRAINT `FK_MAP_SHIPCALL_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`) CONSTRAINT `FK_MAP_SHIPCALL_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8933 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Associates a participant with a shipcall'; ) ENGINE=InnoDB AUTO_INCREMENT=128 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Associates a participant with a shipcall';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
@ -380,20 +285,13 @@ CREATE TABLE `times` (
`berth_info` varchar(512) DEFAULT NULL, `berth_info` varchar(512) DEFAULT NULL,
`pier_side` bit(1) DEFAULT NULL, `pier_side` bit(1) DEFAULT NULL,
`participant_type` int unsigned DEFAULT NULL, `participant_type` int unsigned DEFAULT NULL,
`ata` datetime DEFAULT NULL COMMENT 'Relevant only for mooring, this field can be used to record actual ATA',
`atd` datetime DEFAULT NULL COMMENT 'Relevant only for mooring, this field can be used to record actual ATD',
`eta_interval_end` datetime DEFAULT NULL COMMENT 'If this value is set the times are given as interval instead of a single point in time. The start time value depends on the participant type.',
`etd_interval_end` datetime DEFAULT NULL COMMENT 'If this value is set the times are given as interval instead of a single point in time. The start time value depends on the participant type.',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uniq_shipcall_participant` (`shipcall_id`,`participant_type`),
KEY `FK_TIME_SHIPCALL` (`shipcall_id`), KEY `FK_TIME_SHIPCALL` (`shipcall_id`),
KEY `FK_TIME_PART` (`participant_id`) /*!80000 INVISIBLE */, KEY `FK_TIME_PART` (`participant_id`) /*!80000 INVISIBLE */,
KEY `FK_TIME_BERTH` (`berth_id`) /*!80000 INVISIBLE */, KEY `FK_TIME_BERTH` (`berth_id`) /*!80000 INVISIBLE */,
KEY `idx_times_eta_berth` (`eta_berth`),
KEY `idx_times_etd_berth` (`etd_berth`),
CONSTRAINT `FK_TIME_BERTH` FOREIGN KEY (`berth_id`) REFERENCES `berth` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, CONSTRAINT `FK_TIME_BERTH` FOREIGN KEY (`berth_id`) REFERENCES `berth` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_TIME_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`) CONSTRAINT `FK_TIME_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7863 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='the planned time for the participants work'; ) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='the planned time for the participants work';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
@ -405,7 +303,7 @@ DROP TABLE IF EXISTS `user`;
/*!50503 SET character_set_client = utf8mb4 */; /*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user` ( CREATE TABLE `user` (
`id` int unsigned NOT NULL AUTO_INCREMENT, `id` int unsigned NOT NULL AUTO_INCREMENT,
`participant_id` int unsigned NOT NULL, `participant_id` int unsigned DEFAULT NULL,
`first_name` varchar(45) DEFAULT NULL, `first_name` varchar(45) DEFAULT NULL,
`last_name` varchar(45) DEFAULT NULL, `last_name` varchar(45) DEFAULT NULL,
`user_name` varchar(45) DEFAULT NULL, `user_name` varchar(45) DEFAULT NULL,
@ -413,17 +311,12 @@ CREATE TABLE `user` (
`user_phone` varchar(128) DEFAULT NULL, `user_phone` varchar(128) DEFAULT NULL,
`password_hash` varchar(128) DEFAULT NULL, `password_hash` varchar(128) DEFAULT NULL,
`api_key` varchar(256) DEFAULT NULL, `api_key` varchar(256) DEFAULT NULL,
`notify_email` bit(1) DEFAULT NULL,
`notify_whatsapp` bit(1) DEFAULT NULL,
`notify_signal` bit(1) DEFAULT NULL,
`notify_popup` bit(1) DEFAULT NULL,
`notify_event` int DEFAULT NULL COMMENT 'Bitflag of selected notification event types that the user wants to be notified of',
`created` datetime DEFAULT CURRENT_TIMESTAMP, `created` datetime DEFAULT CURRENT_TIMESTAMP,
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, `modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `FK_USER_PART` (`participant_id`), KEY `FK_USER_PART` (`participant_id`),
CONSTRAINT `FK_USER_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`) CONSTRAINT `FK_USER_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='member of a participant'; ) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='member of a participant';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
@ -446,57 +339,6 @@ CREATE TABLE `user_role_map` (
CONSTRAINT `FK_USER_ROLE` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) CONSTRAINT `FK_USER_ROLE` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Assigns a user to a role'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Assigns a user to a role';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping routines for database 'bremen_calling_test'
--
/*!50003 DROP PROCEDURE IF EXISTS `delete_data` */;
/*!50003 SET @saved_cs_client = @@character_set_client */ ;
/*!50003 SET @saved_cs_results = @@character_set_results */ ;
/*!50003 SET @saved_col_connection = @@collation_connection */ ;
/*!50003 SET character_set_client = utf8mb4 */ ;
/*!50003 SET character_set_results = utf8mb4 */ ;
/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ;
/*!50003 SET @saved_sql_mode = @@sql_mode */ ;
/*!50003 SET sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION' */ ;
DELIMITER ;;
CREATE DEFINER=`ds`@`localhost` PROCEDURE `delete_data`()
BEGIN
DECLARE shipcall_id_var int;
DECLARE done INT DEFAULT FALSE;
DECLARE shipcall_iter CURSOR FOR
SELECT shipcall.id FROM shipcall
LEFT JOIN times ON
times.shipcall_id = shipcall.id AND times.participant_type = 8
WHERE
-- ARRIVAL
(type = 1 AND GREATEST(shipcall.eta, COALESCE(times.eta_berth, 0)) <= CURRENT_DATE() - INTERVAL 1 MONTH) OR
-- DEPARTURE / SHIFTING
(type != 1 AND GREATEST(shipcall.etd, COALESCE(times.etd_berth, 0)) <= CURRENT_DATE() - INTERVAL 1 MONTH);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN shipcall_iter;
delete_loop: LOOP
FETCH shipcall_iter INTO shipcall_id_var;
IF done THEN
LEAVE delete_loop;
END IF;
DELETE FROM shipcall_participant_map WHERE shipcall_id = shipcall_id_var;
DELETE FROM shipcall_tug_map WHERE shipcall_id = shipcall_id_var;
DELETE FROM times WHERE shipcall_id = shipcall_id_var;
DELETE FROM history WHERE shipcall_id = shipcall_id_var;
DELETE FROM shipcall WHERE id = shipcall_id_var;
END LOOP;
CLOSE shipcall_iter;
END ;;
DELIMITER ;
/*!50003 SET sql_mode = @saved_sql_mode */ ;
/*!50003 SET character_set_client = @saved_cs_client */ ;
/*!50003 SET character_set_results = @saved_cs_results */ ;
/*!50003 SET collation_connection = @saved_col_connection */ ;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
@ -507,4 +349,4 @@ DELIMITER ;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2025-11-17 8:26:36 -- Dump completed on 2023-10-06 14:52:04

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,11 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Bremen Calling</title>
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1 +1 @@
1.8.0.0 1.7.0.7

View File

@ -9,10 +9,11 @@
<section name="BreCalClient.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" /> <section name="BreCalClient.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
</sectionGroup> </sectionGroup>
</configSections> </configSections>
<log4net> <log4net debug="true">
<root> <root>
<level value="DEBUG"/> <level value="DEBUG"/>
<appender-ref ref="LogFileAppender"/> <appender-ref ref="LogFileAppender"/>
<appender-ref ref="loki" />
</root> </root>
<appender name="LogFileAppender" type="log4net.Appender.RollingFileAppender"> <appender name="LogFileAppender" type="log4net.Appender.RollingFileAppender">
<param name="File" value="log-BreCalClient.txt"/> <param name="File" value="log-BreCalClient.txt"/>
@ -25,6 +26,28 @@
<param name="ConversionPattern" value="%date [%thread] %-5level [%logger] - %message%newline"/> <param name="ConversionPattern" value="%date [%thread] %-5level [%logger] - %message%newline"/>
</layout> </layout>
</appender> </appender>
<appender name="loki" type="Log4Net.Appender.Loki.LokiAppender, Log4Net.Appender.Grafana.Loki">
<Environment value="Development" />
<!-- Global label to be added to the log stream -->
<Application value="BreCal" />
<!-- Global label to be added to the log stream -->
<BufferSize value="512" />
<!-- To configure the buffer size, default: 512 -->
<ServiceUrl value="http://loki:3100" />
<!-- Loki URL -->
<!--BasicAuthUserName value="" /-->
<!-- To be added if basic authent enabled -->
<!--BasicAuthPassword value="" /-->
<!-- To be added if basic authent enabled -->
<GZipCompression value="true" />
<!-- To compress the post request using GZip compression -->
<TrustSelfSignedCerts value="false" />
<!-- To trust self signed certificates. Default: false -->
<Labels>
<Label name="host" value="%property{log4net:HostName}" />
<Label name="logger" value="%logger" />
</Labels>
</appender>
</log4net> </log4net>
<applicationSettings> <applicationSettings>
<BreCalClient.Properties.Settings> <BreCalClient.Properties.Settings>
@ -32,7 +55,7 @@
<value>#751D1F</value> <value>#751D1F</value>
</setting> </setting>
<setting name="APP_TITLE" serializeAs="String"> <setting name="APP_TITLE" serializeAs="String">
<value>!!Bremen calling Entwicklungsversion!!</value> <value>!!Bremen calling Testversion!!</value>
</setting> </setting>
<setting name="LOGO_IMAGE_URL" serializeAs="String"> <setting name="LOGO_IMAGE_URL" serializeAs="String">
<value>https://www.textbausteine.net/</value> <value>https://www.textbausteine.net/</value>

View File

@ -18,8 +18,11 @@ namespace BreCalClient
private void Application_Startup(object sender, StartupEventArgs e) private void Application_Startup(object sender, StartupEventArgs e)
{ {
// Configure the logger
log4net.Config.XmlConfigurator.Configure();
// Window size sanity check // Window size sanity check
if(Settings.Default.Width == 0) if (Settings.Default.Width == 0)
{ {
Settings.Default.Width = 800; Settings.Default.Width = 800;
Settings.Default.Save(); Settings.Default.Save();

View File

@ -120,7 +120,7 @@ namespace BreCalClient
if (!_notificationsCollection.Where(x => x.Id == notification.Id).Any()) if (!_notificationsCollection.Where(x => x.Id == notification.Id).Any())
{ {
List<AppNotification> newList = new(_notificationsCollection); List<AppNotification> newList = [.. _notificationsCollection];
AppNotification ap = new(notification.Id) AppNotification ap = new(notification.Id)
{ {

View File

@ -8,8 +8,8 @@
<SignAssembly>True</SignAssembly> <SignAssembly>True</SignAssembly>
<StartupObject>BreCalClient.App</StartupObject> <StartupObject>BreCalClient.App</StartupObject>
<AssemblyOriginatorKeyFile>..\..\misc\brecal.snk</AssemblyOriginatorKeyFile> <AssemblyOriginatorKeyFile>..\..\misc\brecal.snk</AssemblyOriginatorKeyFile>
<AssemblyVersion>1.8.0.0</AssemblyVersion> <AssemblyVersion>1.7.0.7</AssemblyVersion>
<FileVersion>1.8.0.0</FileVersion> <FileVersion>1.7.0.7</FileVersion>
<Title>Bremen calling client</Title> <Title>Bremen calling client</Title>
<Description>A Windows WPF client for the Bremen calling API.</Description> <Description>A Windows WPF client for the Bremen calling API.</Description>
<ApplicationIcon>containership.ico</ApplicationIcon> <ApplicationIcon>containership.ico</ApplicationIcon>
@ -118,12 +118,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.6.1" /> <PackageReference Include="Extended.Wpf.Toolkit" Version="4.7.25104.5739" />
<PackageReference Include="JsonSubTypes" Version="2.0.1" /> <PackageReference Include="JsonSubTypes" Version="2.0.1" />
<PackageReference Include="log4net" Version="3.0.3" /> <PackageReference Include="log4net" Version="3.0.4" />
<PackageReference Include="Log4Net.Appender.Grafana.Loki" Version="1.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Polly" Version="8.5.1" /> <PackageReference Include="Polly" Version="8.5.2" />
<PackageReference Include="RestSharp" Version="112.0.0" /> <PackageReference Include="RestSharp" Version="112.1.0" />
<PackageReference Include="ToastNotifications" Version="2.5.1" /> <PackageReference Include="ToastNotifications" Version="2.5.1" />
<PackageReference Include="ToastNotifications.Messages" Version="2.5.1" /> <PackageReference Include="ToastNotifications.Messages" Version="2.5.1" />
</ItemGroup> </ItemGroup>

View File

@ -141,6 +141,7 @@ namespace BreCalClient
private void Window_Loaded(object sender, RoutedEventArgs e) private void Window_Loaded(object sender, RoutedEventArgs e)
{ {
_log.Info("Client startup");
labelGeneralStatus.Text = $"Connection {ConnectionStatus.UNDEFINED}"; labelGeneralStatus.Text = $"Connection {ConnectionStatus.UNDEFINED}";
labelVersion.Text = "V. " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; labelVersion.Text = "V. " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
if (!string.IsNullOrEmpty(Properties.Settings.Default.APP_TITLE)) if (!string.IsNullOrEmpty(Properties.Settings.Default.APP_TITLE))
@ -157,6 +158,7 @@ namespace BreCalClient
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{ {
_log.Info("Client closing");
// serialize filter settings // serialize filter settings
Properties.Settings.Default.FilterCriteriaMap = SearchFilterModel.Serialize(); Properties.Settings.Default.FilterCriteriaMap = SearchFilterModel.Serialize();
Properties.Settings.Default.Save(); Properties.Settings.Default.Save();

View File

@ -4,8 +4,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
--> -->
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<ApplicationRevision>0</ApplicationRevision> <ApplicationRevision>1</ApplicationRevision>
<ApplicationVersion>1.8.0.0</ApplicationVersion> <ApplicationVersion>1.7.0.7</ApplicationVersion>
<BootstrapperEnabled>True</BootstrapperEnabled> <BootstrapperEnabled>True</BootstrapperEnabled>
<Configuration>Debug</Configuration> <Configuration>Debug</Configuration>
<CreateDesktopShortcut>True</CreateDesktopShortcut> <CreateDesktopShortcut>True</CreateDesktopShortcut>
@ -21,7 +21,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<OpenBrowserOnPublish>False</OpenBrowserOnPublish> <OpenBrowserOnPublish>False</OpenBrowserOnPublish>
<Platform>Any CPU</Platform> <Platform>Any CPU</Platform>
<ProductName>Bremen calling development client</ProductName> <ProductName>Bremen calling development client</ProductName>
<PublishDir>bin\Debug\net8.0-windows7.0\win-x64\app.publish\</PublishDir> <PublishDir>bin\Debug\net6.0-windows\win-x64\app.publish\</PublishDir>
<PublishUrl>bin\publish.devel\</PublishUrl> <PublishUrl>bin\publish.devel\</PublishUrl>
<PublisherName>Informatikbüro Daniel Schick</PublisherName> <PublisherName>Informatikbüro Daniel Schick</PublisherName>
<PublishProtocol>ClickOnce</PublishProtocol> <PublishProtocol>ClickOnce</PublishProtocol>
@ -33,12 +33,12 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<SignManifests>False</SignManifests> <SignManifests>False</SignManifests>
<SuiteName>Bremen calling</SuiteName> <SuiteName>Bremen calling</SuiteName>
<SupportUrl>https://www.textbausteine.net/</SupportUrl> <SupportUrl>https://www.textbausteine.net/</SupportUrl>
<TargetFramework>net8.0-windows7.0</TargetFramework> <TargetFramework>net6.0-windows</TargetFramework>
<UpdateEnabled>True</UpdateEnabled> <UpdateEnabled>True</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode> <UpdateMode>Foreground</UpdateMode>
<UpdateRequired>True</UpdateRequired> <UpdateRequired>True</UpdateRequired>
<WebPageFileName>Publish.html</WebPageFileName> <WebPageFileName>Publish.html</WebPageFileName>
<MinimumRequiredVersion>1.8.0.0</MinimumRequiredVersion> <MinimumRequiredVersion>1.7.0.7</MinimumRequiredVersion>
<SkipPublishVerification>false</SkipPublishVerification> <SkipPublishVerification>false</SkipPublishVerification>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -12,7 +12,7 @@ namespace BreCalClient.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.12.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.10.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
@ -34,7 +34,7 @@ namespace BreCalClient.Properties {
[global::System.Configuration.ApplicationScopedSettingAttribute()] [global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("!!Bremen calling Entwicklungsversion!!")] [global::System.Configuration.DefaultSettingValueAttribute("!!Bremen calling Testversion!!")]
public string APP_TITLE { public string APP_TITLE {
get { get {
return ((string)(this["APP_TITLE"])); return ((string)(this["APP_TITLE"]));

View File

@ -6,7 +6,7 @@
<Value Profile="(Default)">#751D1F</Value> <Value Profile="(Default)">#751D1F</Value>
</Setting> </Setting>
<Setting Name="APP_TITLE" Type="System.String" Scope="Application"> <Setting Name="APP_TITLE" Type="System.String" Scope="Application">
<Value Profile="(Default)">!!Bremen calling Entwicklungsversion!!</Value> <Value Profile="(Default)">!!Bremen calling Testversion!!</Value>
</Setting> </Setting>
<Setting Name="LOGO_IMAGE_URL" Type="System.String" Scope="Application"> <Setting Name="LOGO_IMAGE_URL" Type="System.String" Scope="Application">
<Value Profile="(Default)">https://www.textbausteine.net/</Value> <Value Profile="(Default)">https://www.textbausteine.net/</Value>

View File

@ -624,15 +624,6 @@ namespace BreCalClient.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Draft.
/// </summary>
public static string textDraftNoUnit {
get {
return ResourceManager.GetString("textDraftNoUnit", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Edit. /// Looks up a localized string similar to Edit.
/// </summary> /// </summary>
@ -1155,15 +1146,6 @@ namespace BreCalClient.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Position.
/// </summary>
public static string textPosition {
get {
return ResourceManager.GetString("textPosition", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Rain sensitive cargo. /// Looks up a localized string similar to Rain sensitive cargo.
/// </summary> /// </summary>

View File

@ -235,9 +235,6 @@
<data name="textDraft" xml:space="preserve"> <data name="textDraft" xml:space="preserve">
<value>Tiefgang (m)</value> <value>Tiefgang (m)</value>
</data> </data>
<data name="textDraftNoUnit" xml:space="preserve">
<value>Tiefgang</value>
</data>
<data name="textEdit" xml:space="preserve"> <data name="textEdit" xml:space="preserve">
<value>Bearbeiten</value> <value>Bearbeiten</value>
</data> </data>

View File

@ -649,10 +649,4 @@
<data name="textNotifyOn" xml:space="preserve"> <data name="textNotifyOn" xml:space="preserve">
<value>Notify on</value> <value>Notify on</value>
</data> </data>
<data name="textPosition" xml:space="preserve">
<value>Position</value>
</data>
<data name="textDraftNoUnit" xml:space="preserve">
<value>Draft</value>
</data>
</root> </root>

View File

@ -29,6 +29,7 @@
<RowDefinition Height=".125*"/> <RowDefinition Height=".125*"/>
<RowDefinition Height=".125*"/> <RowDefinition Height=".125*"/>
<RowDefinition Height=".125*"/> <RowDefinition Height=".125*"/>
<RowDefinition Height=".125*"/> <RowDefinition Height=".125*"/>
<RowDefinition Height=".125*"/> <RowDefinition Height=".125*"/>
<RowDefinition Height=".125*"/> <RowDefinition Height=".125*"/>
@ -63,16 +64,16 @@
<TextBlock x:Name="textBlockIMO" Padding="0" FontWeight="DemiBold" /> <TextBlock x:Name="textBlockIMO" Padding="0" FontWeight="DemiBold" />
</Viewbox> </Viewbox>
<Viewbox Grid.Row="2" Grid.Column="0" HorizontalAlignment="Left"> <Viewbox Grid.Row="2" Grid.Column="0" HorizontalAlignment="Left">
<TextBlock Text="{x:Static p:Resources.textLengthWidth}" Padding="0" /> <TextBlock Text="{x:Static p:Resources.textCallsign}" />
</Viewbox> </Viewbox>
<Viewbox Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left"> <Viewbox Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left">
<TextBlock x:Name="textBlockLengthWidth" Padding="0"/> <TextBlock x:Name="textBlockCallsign" Padding="0"/>
</Viewbox> </Viewbox>
<Viewbox Grid.Row="3" Grid.Column="0" HorizontalAlignment="Left"> <Viewbox Grid.Row="3" Grid.Column="0" HorizontalAlignment="Left">
<TextBlock Text="{x:Static p:Resources.textDraftNoUnit}" /> <TextBlock Text="{x:Static p:Resources.textLengthWidth}" Padding="0" />
</Viewbox> </Viewbox>
<Viewbox Grid.Row="3" Grid.Column="1" HorizontalAlignment="Left"> <Viewbox Grid.Row="3" Grid.Column="1" HorizontalAlignment="Left">
<TextBlock x:Name="textBlockDraft" Padding="0"/> <TextBlock x:Name="textBlockLengthWidth" Padding="0"/>
</Viewbox> </Viewbox>
<Viewbox Grid.Row="4" Grid.Column="0" HorizontalAlignment="Left"> <Viewbox Grid.Row="4" Grid.Column="0" HorizontalAlignment="Left">
<TextBlock Text="ETA" x:Name="labelETA"/> <TextBlock Text="ETA" x:Name="labelETA"/>
@ -89,6 +90,7 @@
<Viewbox Grid.Row="6" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Left"> <Viewbox Grid.Row="6" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Left">
<TextBlock x:Name="textBlockHarbour" Padding="0" FontWeight="DemiBold" /> <TextBlock x:Name="textBlockHarbour" Padding="0" FontWeight="DemiBold" />
</Viewbox> </Viewbox>
</Grid> </Grid>
<Grid Grid.Row="0" Grid.Column="1"> <Grid Grid.Row="0" Grid.Column="1">
<Grid.RowDefinitions> <Grid.RowDefinitions>

View File

@ -269,7 +269,7 @@ namespace BreCalClient
this.imageEvaluation.ToolTip = null; this.imageEvaluation.ToolTip = null;
this.textBlockBerth.Text = this.ShipcallControlModel?.GetBerthText(null); this.textBlockBerth.Text = this.ShipcallControlModel?.GetBerthText(null);
this.textBlockDraft.Text = (this.ShipcallControlModel?.Shipcall?.Draft != null) ? $"{this.ShipcallControlModel?.Shipcall?.Draft.Value.ToString("N2")} m" : "-"; this.textBlockCallsign.Text = this.ShipcallControlModel?.Ship?.Callsign;
this.textBlockETA.Text = this.ShipcallControlModel?.GetETAETD(true); this.textBlockETA.Text = this.ShipcallControlModel?.GetETAETD(true);
this.textBlockIMO.Text = this.ShipcallControlModel?.Ship?.Imo.ToString(); this.textBlockIMO.Text = this.ShipcallControlModel?.Ship?.Imo.ToString();
@ -292,6 +292,7 @@ namespace BreCalClient
if((this.ShipcallControlModel?.Shipcall?.Type == ShipcallType.Arrival) && (this.ShipcallControlModel?.Shipcall.TimeRefPoint != null)) if((this.ShipcallControlModel?.Shipcall?.Type == ShipcallType.Arrival) && (this.ShipcallControlModel?.Shipcall.TimeRefPoint != null))
{ {
int timeRefPointIndex = this.ShipcallControlModel?.Shipcall?.TimeRefPoint ?? 0; int timeRefPointIndex = this.ShipcallControlModel?.Shipcall?.TimeRefPoint ?? 0;
if (timeRefPointIndex < 0) timeRefPointIndex = 0;
this.labelETAETDAgent.Content = BreCalLists.TimeRefs[timeRefPointIndex]; this.labelETAETDAgent.Content = BreCalLists.TimeRefs[timeRefPointIndex];
this.labelETAETDMooring.Content = BreCalLists.TimeRefs[timeRefPointIndex]; this.labelETAETDMooring.Content = BreCalLists.TimeRefs[timeRefPointIndex];

View File

@ -8,7 +8,7 @@
<applicationSettings> <applicationSettings>
<RoleEditor.Properties.Settings> <RoleEditor.Properties.Settings>
<setting name="ConnectionString" serializeAs="String"> <setting name="ConnectionString" serializeAs="String">
<value>Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_test;Port=33307</value> <value>Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_test;Port=33306</value>
</setting> </setting>
</RoleEditor.Properties.Settings> </RoleEditor.Properties.Settings>
</applicationSettings> </applicationSettings>

View File

@ -59,7 +59,7 @@
<Label Content="Street" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right"/> <Label Content="Street" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="Postal code" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Right"/> <Label Content="Postal code" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="City" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Right"/> <Label Content="City" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="Deleted" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Right"/> <Label Content="Active" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="Type" Grid.Row="5" Grid.Column="1" HorizontalAlignment="Right"/> <Label Content="Type" Grid.Row="5" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="Created" Grid.Row="6" Grid.Column="1" HorizontalAlignment="Right"/> <Label Content="Created" Grid.Row="6" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="Modified" Grid.Row="7" Grid.Column="1" HorizontalAlignment="Right"/> <Label Content="Modified" Grid.Row="7" Grid.Column="1" HorizontalAlignment="Right"/>
@ -67,7 +67,7 @@
<TextBox x:Name="textBoxParticipantStreet" Grid.Row="1" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" /> <TextBox x:Name="textBoxParticipantStreet" Grid.Row="1" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<TextBox x:Name="textBoxParticipantPostalCode" Grid.Row="2" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" /> <TextBox x:Name="textBoxParticipantPostalCode" Grid.Row="2" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<TextBox x:Name="textBoxParticipantCity" Grid.Row="3" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" /> <TextBox x:Name="textBoxParticipantCity" Grid.Row="3" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<CheckBox x:Name="checkboxParticipantDeleted" Grid.Row="4" Grid.Column="2" VerticalAlignment="Center" IsEnabled="False" /> <CheckBox x:Name="checkboxParticipantActive" Grid.Row="4" Grid.Column="2" VerticalAlignment="Center" />
<xctk:CheckComboBox x:Name="comboBoxParticipantType" Grid.Row="5" Grid.Column="2" Margin="2" SelectedValue="Key" DisplayMemberPath="Value" /> <xctk:CheckComboBox x:Name="comboBoxParticipantType" Grid.Row="5" Grid.Column="2" Margin="2" SelectedValue="Key" DisplayMemberPath="Value" />
<TextBox x:Name="textBoxParticipantCreated" Grid.Row="6" IsReadOnly="True" IsEnabled="False" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" /> <TextBox x:Name="textBoxParticipantCreated" Grid.Row="6" IsReadOnly="True" IsEnabled="False" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
<StackPanel Orientation="Horizontal" Grid.Row="7" Grid.Column="0"> <StackPanel Orientation="Horizontal" Grid.Row="7" Grid.Column="0">
@ -167,7 +167,7 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ListBox x:Name="listBoxUser" Margin="2" Grid.RowSpan="9" SelectionChanged="listBoxUser_SelectionChanged"> <ListBox x:Name="listBoxUser" Margin="2" Grid.RowSpan="9" SelectionChanged="listBoxUser_SelectionChanged">
<ListBox.ContextMenu> <ListBox.ContextMenu>
<ContextMenu Name="contextMenuUser"> <ContextMenu>
<MenuItem x:Name="menuItemNewUser" Header="New.." Click="menuItemNewUser_Click"> <MenuItem x:Name="menuItemNewUser" Header="New.." Click="menuItemNewUser_Click">
<MenuItem.Icon> <MenuItem.Icon>
<Image Source="Resources/add.png" /> <Image Source="Resources/add.png" />

View File

@ -61,8 +61,6 @@ namespace RoleEditor
// load all participants // load all participants
List<Participant> participants = await Participant.LoadAll(_dbManager); List<Participant> participants = await Participant.LoadAll(_dbManager);
participants.Sort((x, y) => string.Compare(x.Name, y.Name));
foreach (Participant p in participants) foreach (Participant p in participants)
{ {
_participants.Add(p); _participants.Add(p);
@ -467,8 +465,8 @@ namespace RoleEditor
this.textBoxParticipantStreet.Text = (p != null) ? p.Street : string.Empty; this.textBoxParticipantStreet.Text = (p != null) ? p.Street : string.Empty;
this.textBoxParticipantPostalCode.Text = (p != null) ? p.PostalCode : string.Empty; this.textBoxParticipantPostalCode.Text = (p != null) ? p.PostalCode : string.Empty;
this.textBoxParticipantCity.Text = (p != null) ? p.City : string.Empty; this.textBoxParticipantCity.Text = (p != null) ? p.City : string.Empty;
// this.checkboxParticipantActive.Checked = (p != null) ? p.
this.textBoxParticipantCreated.Text = (p != null) ? p.Created.ToString() : string.Empty; this.textBoxParticipantCreated.Text = (p != null) ? p.Created.ToString() : string.Empty;
this.checkboxParticipantDeleted.IsChecked = (p != null) ? p.Deleted : null;
this.textBoxParticipantModified.Text = (p != null) ? p.Modified.ToString() : string.Empty; this.textBoxParticipantModified.Text = (p != null) ? p.Modified.ToString() : string.Empty;
this.checkBoxParticipantAllowBSMD.IsChecked = (p != null) ? p.IsFlagSet(Participant.ParticipantFlags.ALLOW_BSMD) : null; this.checkBoxParticipantAllowBSMD.IsChecked = (p != null) ? p.IsFlagSet(Participant.ParticipantFlags.ALLOW_BSMD) : null;
this.comboBoxParticipantType.SelectedItems.Clear(); this.comboBoxParticipantType.SelectedItems.Clear();
@ -512,11 +510,6 @@ namespace RoleEditor
_assignedPorts.Add(pa); _assignedPorts.Add(pa);
} }
} }
this.contextMenuUser.IsEnabled = !p.Deleted;
this.buttonAddPortAssignment.IsEnabled = !p.Deleted;
this.buttonRemovePortAssignment.IsEnabled = !p.Deleted;
} }
private async void listBoxRoles_SelectionChanged(object sender, SelectionChangedEventArgs e) private async void listBoxRoles_SelectionChanged(object sender, SelectionChangedEventArgs e)
@ -601,7 +594,7 @@ namespace RoleEditor
if(this.listBoxParticipant.SelectedItem is Participant p) if(this.listBoxParticipant.SelectedItem is Participant p)
{ {
await p.Delete(_dbManager); await p.Delete(_dbManager);
p.Deleted = true; this._participants.Remove(p);
} }
} }
catch (Exception ex) catch (Exception ex)
@ -635,7 +628,6 @@ namespace RoleEditor
{ {
if (this.listBoxUser.SelectedItem is User u) if (this.listBoxUser.SelectedItem is User u)
{ {
await u.ExecuteNonQuery(_dbManager); // extra history delete happens here
await u.Delete(_dbManager); await u.Delete(_dbManager);
this._users.Remove(u); this._users.Remove(u);
} }

View File

@ -2,12 +2,12 @@
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows7.0</TargetFramework> <TargetFramework>net6.0-windows</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<ApplicationIcon>Resources\lock_preferences.ico</ApplicationIcon> <ApplicationIcon>Resources\lock_preferences.ico</ApplicationIcon>
<FileVersion>1.8.0.0</FileVersion> <FileVersion>1.7.0.7</FileVersion>
<AssemblyVersion>1.8.0.0</AssemblyVersion> <AssemblyVersion>1.7.0.7</AssemblyVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -30,8 +30,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" /> <PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="ExcelDataReader" Version="3.8.0" /> <PackageReference Include="ExcelDataReader" Version="3.7.0-develop00385" />
<PackageReference Include="Extended.Wpf.Toolkit" Version="5.0.0" /> <PackageReference Include="Extended.Wpf.Toolkit" Version="4.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -13,12 +13,6 @@
"SccProvider" = "8:" "SccProvider" = "8:"
"Hierarchy" "Hierarchy"
{ {
"Entry"
{
"MsmKey" = "8:_1E7663DCE02A4D848349229A724E961A"
"OwnerKey" = "8:_UNDEFINED"
"MsmSig" = "8:_UNDEFINED"
}
"Entry" "Entry"
{ {
"MsmKey" = "8:_3E48B6E716164CC1826E094025517B3F" "MsmKey" = "8:_3E48B6E716164CC1826E094025517B3F"
@ -31,24 +25,6 @@
"OwnerKey" = "8:_UNDEFINED" "OwnerKey" = "8:_UNDEFINED"
"MsmSig" = "8:_UNDEFINED" "MsmSig" = "8:_UNDEFINED"
} }
"Entry"
{
"MsmKey" = "8:_CD20A468610C42B89F66B4D3367A5A6A"
"OwnerKey" = "8:_UNDEFINED"
"MsmSig" = "8:_UNDEFINED"
}
"Entry"
{
"MsmKey" = "8:_UNDEFINED"
"OwnerKey" = "8:_CD20A468610C42B89F66B4D3367A5A6A"
"MsmSig" = "8:_UNDEFINED"
}
"Entry"
{
"MsmKey" = "8:_UNDEFINED"
"OwnerKey" = "8:_1E7663DCE02A4D848349229A724E961A"
"MsmSig" = "8:_UNDEFINED"
}
} }
"Configurations" "Configurations"
{ {
@ -100,14 +76,6 @@
{ {
"LaunchCondition" "LaunchCondition"
{ {
"{A06ECF26-33A3-4562-8140-9B0E340D4F24}:_3415D375792A4611BF998D78F56CD22C"
{
"Name" = "8:.NET Framework"
"Message" = "8:[VSDNETMSG]"
"FrameworkVersion" = "8:.NETFramework,Version=v4.7.2"
"AllowLaterVersions" = "11:FALSE"
"InstallUrl" = "8:http://go.microsoft.com/fwlink/?LinkId=863262"
}
"{A06ECF26-33A3-4562-8140-9B0E340D4F24}:_7C5ED856EDF94532A041DBACD5D5C09E" "{A06ECF26-33A3-4562-8140-9B0E340D4F24}:_7C5ED856EDF94532A041DBACD5D5C09E"
{ {
"Name" = "8:.NET Core" "Name" = "8:.NET Core"
@ -122,37 +90,6 @@
} }
"File" "File"
{ {
"{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_1E7663DCE02A4D848349229A724E961A"
{
"AssemblyRegister" = "3:1"
"AssemblyIsInGAC" = "11:FALSE"
"AssemblyAsmDisplayName" = "8:Xceed.Wpf.AvalonDock.resources, Version=4.6.0.0, Culture=de, PublicKeyToken=3e4669d2f30244f4, processorArchitecture=MSIL"
"ScatterAssemblies"
{
"_1E7663DCE02A4D848349229A724E961A"
{
"Name" = "8:Xceed.Wpf.AvalonDock.resources.dll"
"Attributes" = "3:512"
}
}
"SourcePath" = "8:..\\BreCalClient\\bin\\Debug\\net6.0-windows\\de\\Xceed.Wpf.AvalonDock.resources.dll"
"TargetName" = "8:"
"Tag" = "8:"
"Folder" = "8:_F64284776BC0480CBF6C33B1FE00C374"
"Condition" = "8:"
"Transitive" = "11:FALSE"
"Vital" = "11:TRUE"
"ReadOnly" = "11:FALSE"
"Hidden" = "11:FALSE"
"System" = "11:FALSE"
"Permanent" = "11:FALSE"
"SharedLegacy" = "11:FALSE"
"PackageAs" = "3:1"
"Register" = "3:1"
"Exclude" = "11:FALSE"
"IsDependency" = "11:FALSE"
"IsolateTo" = "8:"
}
"{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_4EE484EAA4A246CBBB283030A6054BC0" "{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_4EE484EAA4A246CBBB283030A6054BC0"
{ {
"SourcePath" = "8:..\\BreCalClient\\Resources\\containership.ico" "SourcePath" = "8:..\\BreCalClient\\Resources\\containership.ico"
@ -173,37 +110,6 @@
"IsDependency" = "11:FALSE" "IsDependency" = "11:FALSE"
"IsolateTo" = "8:" "IsolateTo" = "8:"
} }
"{9F6F8455-1EF1-4B85-886A-4223BCC8E7F7}:_CD20A468610C42B89F66B4D3367A5A6A"
{
"AssemblyRegister" = "3:1"
"AssemblyIsInGAC" = "11:FALSE"
"AssemblyAsmDisplayName" = "8:BreCalClient.resources, Version=1.6.2.0, Culture=de, PublicKeyToken=9ce7b6b354e08ac9, processorArchitecture=MSIL"
"ScatterAssemblies"
{
"_CD20A468610C42B89F66B4D3367A5A6A"
{
"Name" = "8:BreCalClient.resources.dll"
"Attributes" = "3:512"
}
}
"SourcePath" = "8:..\\BreCalClient\\bin\\Debug\\net6.0-windows\\de\\BreCalClient.resources.dll"
"TargetName" = "8:"
"Tag" = "8:"
"Folder" = "8:_F64284776BC0480CBF6C33B1FE00C374"
"Condition" = "8:"
"Transitive" = "11:FALSE"
"Vital" = "11:TRUE"
"ReadOnly" = "11:FALSE"
"Hidden" = "11:FALSE"
"System" = "11:FALSE"
"Permanent" = "11:FALSE"
"SharedLegacy" = "11:FALSE"
"PackageAs" = "3:1"
"Register" = "3:1"
"Exclude" = "11:FALSE"
"IsDependency" = "11:FALSE"
"IsolateTo" = "8:"
}
} }
"FileType" "FileType"
{ {
@ -231,17 +137,6 @@
"Property" = "8:TARGETDIR" "Property" = "8:TARGETDIR"
"Folders" "Folders"
{ {
"{9EF0B969-E518-4E46-987F-47570745A589}:_F64284776BC0480CBF6C33B1FE00C374"
{
"Name" = "8:de"
"AlwaysCreate" = "11:FALSE"
"Condition" = "8:"
"Transitive" = "11:FALSE"
"Property" = "8:_319F0FD8E72443BFA3AE5E1F3F42523B"
"Folders"
{
}
}
} }
} }
"{1525181F-901A-416C-8A58-119130FE478E}:_8BBC7FE2F38E4B41A71D26CCED7D0BCB" "{1525181F-901A-416C-8A58-119130FE478E}:_8BBC7FE2F38E4B41A71D26CCED7D0BCB"

View File

@ -1,4 +1,8 @@
using System.Data; using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace brecal.model namespace brecal.model
@ -38,11 +42,6 @@ namespace brecal.model
/// <param name="cmd">CMD created by DB manager</param> /// <param name="cmd">CMD created by DB manager</param>
public abstract void SetDelete(IDbCommand cmd); public abstract void SetDelete(IDbCommand cmd);
public virtual void SetNonQuery(IDbCommand cmd)
{
// default: do nothing
}
/// <summary> /// <summary>
/// Each database entity must be able to save itself to the database /// Each database entity must be able to save itself to the database
/// </summary> /// </summary>
@ -66,10 +65,5 @@ namespace brecal.model
await manager.ExecuteNonQuery(this.SetDelete); await manager.ExecuteNonQuery(this.SetDelete);
} }
public async Task ExecuteNonQuery(IDBManager manager)
{
await manager.ExecuteNonQuery(this.SetNonQuery);
}
} }
} }

View File

@ -55,8 +55,6 @@ namespace brecal.model
public uint Flags { get; set; } public uint Flags { get; set; }
public bool Deleted { get; set; } = false;
#endregion #endregion
#region public static methods #region public static methods
@ -85,7 +83,6 @@ namespace brecal.model
if (!reader.IsDBNull(6)) p.Flags = (uint)reader.GetInt32(6); if (!reader.IsDBNull(6)) p.Flags = (uint)reader.GetInt32(6);
if (!reader.IsDBNull(7)) p.Created = reader.GetDateTime(7); if (!reader.IsDBNull(7)) p.Created = reader.GetDateTime(7);
if (!reader.IsDBNull(8)) p.Modified = reader.GetDateTime(8); if (!reader.IsDBNull(8)) p.Modified = reader.GetDateTime(8);
if (!reader.IsDBNull(9)) p.Deleted = reader.GetBoolean(9);
result.Add(p); result.Add(p);
} }
return result; return result;
@ -93,7 +90,7 @@ namespace brecal.model
public static void SetLoadQuery(IDbCommand cmd, params object?[] list) public static void SetLoadQuery(IDbCommand cmd, params object?[] list)
{ {
cmd.CommandText = "SELECT id, name, street, postal_code, city, type, flags, created, modified, deleted FROM participant"; cmd.CommandText = "SELECT id, name, street, postal_code, city, type, flags, created, modified FROM participant";
} }
#endregion #endregion
@ -114,7 +111,7 @@ namespace brecal.model
public override void SetDelete(IDbCommand cmd) public override void SetDelete(IDbCommand cmd)
{ {
cmd.CommandText = "UPDATE participant SET deleted = 1 WHERE id = @ID"; cmd.CommandText = "DELETE FROM participant WHERE id = @ID";
IDataParameter idParam = cmd.CreateParameter(); IDataParameter idParam = cmd.CreateParameter();
idParam.ParameterName = "ID"; idParam.ParameterName = "ID";

View File

@ -101,16 +101,6 @@ namespace brecal.model
return this.Username ?? $"{base.Id} - {this.GetType().Name}"; return this.Username ?? $"{base.Id} - {this.GetType().Name}";
} }
public override void SetNonQuery(IDbCommand cmd)
{
cmd.CommandText = "UPDATE history set user_id = NULL WHERE user_id = @ID";
IDataParameter idParam = cmd.CreateParameter();
idParam.ParameterName = "ID";
idParam.Value = this.Id;
cmd.Parameters.Add(idParam);
}
#endregion #endregion
#region private methods #region private methods

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MySqlConnector" Version="2.4.0" /> <PackageReference Include="MySqlConnector" Version="2.3.0-beta.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,9 +1,11 @@
from flask import Flask from flask import Flask
import os import os
import sys
import logging import logging
from . import local_db from . import local_db
import logging_loki
import logging.handlers
from multiprocessing import Queue
from .api import shipcalls from .api import shipcalls
from .api import participant from .api import participant
@ -37,6 +39,7 @@ from BreCal.stubs.df_times import get_df_times
from BreCal.services.schedule_routines import setup_schedule, run_schedule_permanently_in_background from BreCal.services.schedule_routines import setup_schedule, run_schedule_permanently_in_background
def create_app(test_config=None, instance_path=None): def create_app(test_config=None, instance_path=None):
app = Flask(__name__, instance_relative_config=True) app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping( app.config.from_mapping(
SECRET_KEY='dev' SECRET_KEY='dev'
@ -48,8 +51,6 @@ def create_app(test_config=None, instance_path=None):
if instance_path is not None: if instance_path is not None:
app.instance_path = instance_path app.instance_path = instance_path
elif app.config.get("INSTANCE_PATH"):
app.instance_path = app.config["INSTANCE_PATH"]
try: try:
import os import os
@ -71,23 +72,23 @@ def create_app(test_config=None, instance_path=None):
app.register_blueprint(history.bp) app.register_blueprint(history.bp)
app.register_blueprint(ports.bp) app.register_blueprint(ports.bp)
log_level = getattr(logging, app.config.get("LOG_LEVEL", "DEBUG")) logging.basicConfig(filename='brecaltest.log', level=logging.DEBUG, format='%(asctime)s | %(name)s | %(levelname)s | %(message)s')
log_kwargs = {"format": "%(asctime)s | %(name)s | %(levelname)s | %(message)s"} handler = logging_loki.LokiQueueHandler(
if app.config.get("LOG_TO_STDERR"): Queue(-1),
log_kwargs["stream"] = sys.stderr url="http://loki.fritz.box:3100/loki/api/v1/push",
else: tags={"application": "brecal"},
log_kwargs["filename"] = app.config.get("LOG_FILE", "brecaltest.log") version="1",
logging.basicConfig(level=log_level, **log_kwargs) )
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logger.addHandler(handler)
if app.config.get("SECRET_KEY"): local_db.initPool(os.path.dirname(app.instance_path))
os.environ["SECRET_KEY"] = app.config["SECRET_KEY"]
local_db.initPool(os.path.dirname(app.instance_path), config=app.config)
logging.info('App started') logging.info('App started')
# Setup Routine jobs (e.g., reevaluation of shipcalls) # Setup Routine jobs (e.g., reevaluation of shipcalls)
setup_schedule(update_shipcalls_interval_in_minutes=app.config.get("SCHEDULE_UPDATE_SHIPCALLS_MINUTES", 60)) setup_schedule(update_shipcalls_interval_in_minutes=60)
run_schedule_permanently_in_background(latency=app.config.get("SCHEDULE_BACKGROUND_LATENCY_SECONDS", 30)) run_schedule_permanently_in_background(latency=30)
logging.info('Routine Jobs are defined.') logging.info('Routine Jobs are defined.')
return app return app

View File

@ -14,14 +14,7 @@ def GetNotifications():
try: try:
if 'Authorization' in request.headers: if 'Authorization' in request.headers:
token = request.headers.get('Authorization') token = request.headers.get('Authorization')
participant_id = None return impl.notifications.GetNotifications(token)
if 'participant_id' in request.args:
try:
participant_id = int(request.args.get('participant_id'))
except (TypeError, ValueError):
return create_dynamic_exception_response(ex=None, status_code=400, message="participant_id must be an integer")
return impl.notifications.GetNotifications(token, participant_id=participant_id)
else: else:
return create_dynamic_exception_response(ex=None, status_code=403, message="not authenticated") return create_dynamic_exception_response(ex=None, status_code=403, message="not authenticated")

View File

@ -89,7 +89,7 @@ def PutShipcalls():
# validate the PUT shipcall data and the user's authority # validate the PUT shipcall data and the user's authority
InputValidationShipcall.evaluate_put_data(user_data, loadedModel, content) InputValidationShipcall.evaluate_put_data(user_data, loadedModel, content)
return impl.shipcalls.PutShipcalls(loadedModel, content) return impl.shipcalls.PutShipcalls(loadedModel)
except ValidationError as ex: except ValidationError as ex:
return create_validation_error_response(ex=ex, status_code=400) return create_validation_error_response(ex=ex, status_code=400)

View File

@ -72,7 +72,7 @@ def PutTimes():
# validate the request # validate the request
InputValidationTimes.evaluate_put_data(user_data, loadedModel, content) InputValidationTimes.evaluate_put_data(user_data, loadedModel, content)
return impl.times.PutTimes(loadedModel, content) return impl.times.PutTimes(loadedModel)
except ValidationError as ex: except ValidationError as ex:
return create_validation_error_response(ex=ex, status_code=400) return create_validation_error_response(ex=ex, status_code=400)

View File

@ -7,17 +7,12 @@ from marshmallow import ValidationError
from . import verify_if_request_is_json from . import verify_if_request_is_json
from BreCal.validators.validation_error import create_dynamic_exception_response, create_validation_error_response from BreCal.validators.validation_error import create_dynamic_exception_response, create_validation_error_response
import json
import logging
import traceback
bp = Blueprint('user', __name__) bp = Blueprint('user', __name__)
@bp.route('/user', methods=['put']) @bp.route('/user', methods=['put'])
@auth_guard() # no restriction by role @auth_guard() # no restriction by role
def PutUser(): def PutUser():
content = None
try: try:
verify_if_request_is_json(request) verify_if_request_is_json(request)
@ -26,11 +21,9 @@ def PutUser():
return impl.user.PutUser(loadedModel) return impl.user.PutUser(loadedModel)
except ValidationError as ex: except ValidationError as ex:
logging.warning("UserSchema validation failed. Payload=%s", json.dumps(content, default=str))
return create_validation_error_response(ex=ex, status_code=400) return create_validation_error_response(ex=ex, status_code=400)
except Exception as ex: except Exception as ex:
logging.error("UserSchema load failed. Payload=%s\n%s", json.dumps(content, default=str), traceback.format_exc())
return create_dynamic_exception_response(ex=None, status_code=400, message="bad format") return create_dynamic_exception_response(ex=None, status_code=400, message="bad format")

View File

@ -10,7 +10,6 @@ def GetBerths(options):
No parameters, gets all entries No parameters, gets all entries
""" """
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)

View File

@ -16,8 +16,6 @@ def GetHistory(options):
options["shipcall_id"]: **Id of shipcall**. options["shipcall_id"]: **Id of shipcall**.
""" """
pooledConnection = None
data = []
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
@ -28,6 +26,10 @@ def GetHistory(options):
data = commands.query("SELECT id, participant_id, shipcall_id, timestamp, eta, type, operation FROM history WHERE shipcall_id = ?shipcallid?", data = commands.query("SELECT id, participant_id, shipcall_id, timestamp, eta, type, operation FROM history WHERE shipcall_id = ?shipcallid?",
model=History.from_query_row, model=History.from_query_row,
param={"shipcallid" : options["shipcall_id"]}) param={"shipcallid" : options["shipcall_id"]})
pooledConnection.close()
except Exception as ex: except Exception as ex:
pdb.pm() pdb.pm()
logging.error(ex) logging.error(ex)
@ -35,9 +37,6 @@ def GetHistory(options):
result = {} result = {}
result["error_field"] = "call failed" result["error_field"] = "call failed"
return json.dumps("call failed"), 500 return json.dumps("call failed"), 500
finally:
if pooledConnection is not None:
pooledConnection.close()
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}

View File

@ -6,16 +6,14 @@ import bcrypt
from ..schemas import model from ..schemas import model
from .. import local_db from .. import local_db
from ..services import jwt_handler from ..services import jwt_handler
from BreCal.database.sql_queries import SQLQuery
def GetUser(options): def GetUser(options):
pooledConnection = None
try: try:
if "password" in options and "username" in options: if "password" in options and "username" in options:
hash = bcrypt.hashpw(options["password"].encode('utf-8'), bcrypt.gensalt( 12 )).decode('utf8')
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
# query = SQLQuery.get_user() # query = SQLQuery.get_user()
@ -39,7 +37,7 @@ def GetUser(options):
"notify_whatsapp": data[0].notify_whatsapp, "notify_whatsapp": data[0].notify_whatsapp,
"notify_signal": data[0].notify_signal, "notify_signal": data[0].notify_signal,
"notify_popup": data[0].notify_popup, "notify_popup": data[0].notify_popup,
"notify_on": model.notification_types_to_names(model.bitflag_to_list(data[0].notify_event)) "notify_on": model.bitflag_to_list(data[0].notify_event)
} }
token = jwt_handler.generate_jwt(payload=result, lifetime=120) # generate token valid 60 mins token = jwt_handler.generate_jwt(payload=result, lifetime=120) # generate token valid 60 mins
result["token"] = token # add token to user data result["token"] = token # add token to user data
@ -65,3 +63,7 @@ def GetUser(options):
finally: finally:
if pooledConnection is not None: if pooledConnection is not None:
pooledConnection.close() pooledConnection.close()
# $2b$12$uWLE0r32IrtCV30WkMbVwOdltgeibymZyYAf4ZnQb2Bip8hrkGGwG
# $2b$12$.vEapj9xU8z0RK0IpIGeYuRIl0ktdMt4XdJQBhVn.3K2hmvm7qD3y
# $2b$12$yL3PiseU70ciwEuMVM4OtuMwR6tNuIT9vvBiBG/uyMrPxa16E2Zqu

View File

@ -6,22 +6,18 @@ from ..schemas import model
from .. import local_db from .. import local_db
from BreCal.database.sql_queries import SQLQuery from BreCal.database.sql_queries import SQLQuery
def GetNotifications(token, participant_id=None): def GetNotifications(token):
""" """
Optional filtering by participant_id. Returns delivered (level=2) notifications. No parameters, gets all entries
""" """
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
query = "SELECT id, shipcall_id, participant_id, level, type, message, created, modified FROM notification WHERE level = 2" data = commands.query("SELECT id, shipcall_id, participant_id, level, type, message, created, modified FROM notification " +
params = {} "WHERE level = 2", model=model.Notification.from_query_row)
if participant_id is not None: pooledConnection.close()
query += " AND participant_id = ?participant_id?"
params["participant_id"] = participant_id
data = commands.query(query, model=model.Notification.from_query_row, param=params if params else None)
except Exception as ex: except Exception as ex:
logging.error(ex) logging.error(ex)
@ -29,9 +25,6 @@ def GetNotifications(token, participant_id=None):
result = {} result = {}
result["error_field"] = "call failed" result["error_field"] = "call failed"
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}

View File

@ -12,7 +12,6 @@ def GetParticipant(options):
options["user_id"]: **Id of user**. *Example: 2*. User id returned by login call. options["user_id"]: **Id of user**. *Example: 2*. User id returned by login call.
""" """
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)

View File

@ -11,7 +11,6 @@ def GetPorts(token):
No parameters, gets all entries No parameters, gets all entries
""" """
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)

View File

@ -18,8 +18,8 @@ def GetShipcalls(options):
No parameters, gets all entries No parameters, gets all entries
""" """
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
# query = SQLQuery.get_shipcalls(options) # query = SQLQuery.get_shipcalls(options)
@ -70,8 +70,8 @@ def PostShipcalls(schemaModel):
""" """
# This creates a *new* entry # This creates a *new* entry
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
@ -185,15 +185,15 @@ def PostShipcalls(schemaModel):
pooledConnection.close() pooledConnection.close()
def PutShipcalls(schemaModel, original_payload=None): def PutShipcalls(schemaModel):
""" """
:param schemaModel: The deserialized dict of the request :param schemaModel: The deserialized dict of the request
""" """
# This updates an *existing* entry # This updates an *existing* entry
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
@ -205,16 +205,15 @@ def PutShipcalls(schemaModel, original_payload=None):
theshipcall = commands.query_single_or_default("SELECT * FROM shipcall where id = ?id?", sentinel, param={"id" : schemaModel["id"]}) theshipcall = commands.query_single_or_default("SELECT * FROM shipcall where id = ?id?", sentinel, param={"id" : schemaModel["id"]})
if theshipcall is sentinel: if theshipcall is sentinel:
pooledConnection.close()
return json.dumps("no such record"), 404, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps("no such record"), 404, {'Content-Type': 'application/json; charset=utf-8'}
was_canceled = theshipcall["canceled"] was_canceled = theshipcall["canceled"]
provided_keys = set(original_payload.keys()) if isinstance(original_payload, dict) else None # query = SQLQuery.get_shipcall_put(schemaModel)
query = "UPDATE shipcall SET "
update_clauses = [] isNotFirst = False
for key in schemaModel.keys(): for key in schemaModel.keys():
if provided_keys is not None and key not in provided_keys:
continue
param_key = key param_key = key
if key == "id": if key == "id":
continue continue
@ -236,19 +235,19 @@ def PutShipcalls(schemaModel, original_payload=None):
param_key = "evaluation_value" param_key = "evaluation_value"
if key == "evaluation_value": if key == "evaluation_value":
continue continue
update_clauses.append(f"{key} = ?{param_key}?") if isNotFirst:
query += ", "
isNotFirst = True
query += key + " = ?" + param_key + "? "
if update_clauses: query += "WHERE id = ?id?"
query = "UPDATE shipcall SET " + ", ".join(update_clauses) + " WHERE id = ?id?"
commands.execute(query, param=schemaModel)
ship_id_value = schemaModel.get("ship_id") if (provided_keys is None or "ship_id" in provided_keys) else theshipcall["ship_id"] affected_rows = commands.execute(query, param=schemaModel)
shipdata = get_ship_data_for_id(ship_id_value) shipdata = get_ship_data_for_id(schemaModel["ship_id"])
message = shipdata['name'] message = shipdata['name']
type_value = schemaModel.get("type_value") if (provided_keys is None or "type" in provided_keys) else theshipcall["type"] if "type_value" in schemaModel:
if type_value is not None: match schemaModel["type_value"]:
match type_value:
case 1: case 1:
message += " [ARRIVAL]" message += " [ARRIVAL]"
case 2: case 2:
@ -260,9 +259,6 @@ def PutShipcalls(schemaModel, original_payload=None):
pquery = "SELECT id, participant_id, type FROM shipcall_participant_map where shipcall_id = ?id?" pquery = "SELECT id, participant_id, type FROM shipcall_participant_map where shipcall_id = ?id?"
pdata = commands.query(pquery,param={"id" : schemaModel["id"]}) # existing list of assignments pdata = commands.query(pquery,param={"id" : schemaModel["id"]}) # existing list of assignments
if schemaModel.get("participants") is None:
schemaModel["participants"] = []
# loop across passed participant ids, creating entries for those not present in pdata # loop across passed participant ids, creating entries for those not present in pdata
existing_notifications = get_notification_for_shipcall_and_type(schemaModel["id"], 1) # type = 1 is assignment existing_notifications = get_notification_for_shipcall_and_type(schemaModel["id"], 1) # type = 1 is assignment
@ -311,9 +307,8 @@ def PutShipcalls(schemaModel, original_payload=None):
commands.execute(nquery, param={"shipcall_id" : schemaModel["id"], "participant_id" : elem["participant_id"], "message" : message}) commands.execute(nquery, param={"shipcall_id" : schemaModel["id"], "participant_id" : elem["participant_id"], "message" : message})
break break
canceled_value = schemaModel.get("canceled") if schemaModel["canceled"] is not None:
if canceled_value is not None: if schemaModel["canceled"] and not was_canceled:
if canceled_value and not was_canceled:
# create a canceled notification for all currently assigned participants # create a canceled notification for all currently assigned participants
stornoNotificationQuery = "INSERT INTO notification (shipcall_id, participant_id, level, type, message) VALUES (?shipcall_id?, ?participant_id?, 0, 7, ?message?)" stornoNotificationQuery = "INSERT INTO notification (shipcall_id, participant_id, level, type, message) VALUES (?shipcall_id?, ?participant_id?, 0, 7, ?message?)"
for participant_assignment in schemaModel["participants"]: for participant_assignment in schemaModel["participants"]:

View File

@ -11,8 +11,8 @@ def GetShips(token):
No parameters, gets all entries No parameters, gets all entries
""" """
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
# query = SQLQuery.get_ships() # query = SQLQuery.get_ships()
@ -44,8 +44,8 @@ def PostShip(schemaModel):
# TODO: Validate the incoming data # TODO: Validate the incoming data
# This creates a *new* entry # This creates a *new* entry
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
@ -83,6 +83,8 @@ def PostShip(schemaModel):
# new_id = commands.execute_scalar(nquery) # new_id = commands.execute_scalar(nquery)
new_id = commands.execute_scalar("select last_insert_id()") new_id = commands.execute_scalar("select last_insert_id()")
pooledConnection.close()
return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'}
except Exception as ex: except Exception as ex:
@ -91,9 +93,6 @@ def PostShip(schemaModel):
result = {} result = {}
result["error_field"] = "call failed" result["error_field"] = "call failed"
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()
def PutShip(schemaModel): def PutShip(schemaModel):
@ -102,8 +101,8 @@ def PutShip(schemaModel):
""" """
# This updates an *existing* entry # This updates an *existing* entry
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
@ -126,6 +125,8 @@ def PutShip(schemaModel):
affected_rows = commands.execute(query, param=schemaModel) affected_rows = commands.execute(query, param=schemaModel)
pooledConnection.close()
return json.dumps({"id" : schemaModel["id"]}), 200, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps({"id" : schemaModel["id"]}), 200, {'Content-Type': 'application/json; charset=utf-8'}
except Exception as ex: except Exception as ex:
@ -134,9 +135,6 @@ def PutShip(schemaModel):
result = {} result = {}
result["error_field"] = "call failed" result["error_field"] = "call failed"
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()
def DeleteShip(options): def DeleteShip(options):
@ -145,14 +143,16 @@ def DeleteShip(options):
options["id"] options["id"]
""" """
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
# query = SQLQuery.get_ship_delete_by_id() # query = SQLQuery.get_ship_delete_by_id()
# affected_rows = commands.execute(query, param={"id" : options["id"]}) # affected_rows = commands.execute(query, param={"id" : options["id"]})
affected_rows = commands.execute("UPDATE ship SET deleted = 1 WHERE id = ?id?", param={"id" : options["id"]}) affected_rows = commands.execute("UPDATE ship SET deleted = 1 WHERE id = ?id?", param={"id" : options["id"]})
pooledConnection.close()
if affected_rows == 1: if affected_rows == 1:
return json.dumps({"id" : options["id"]}), 200, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps({"id" : options["id"]}), 200, {'Content-Type': 'application/json; charset=utf-8'}
@ -166,6 +166,3 @@ def DeleteShip(options):
result = {} result = {}
result["error_field"] = "call failed" result["error_field"] = "call failed"
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()

View File

@ -18,8 +18,8 @@ def GetTimes(options):
""" """
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
# query = SQLQuery.get_times() # query = SQLQuery.get_times()
@ -28,6 +28,7 @@ def GetTimes(options):
"zone_entry, zone_entry_fixed, operations_start, operations_end, remarks, shipcall_id, participant_id, " + "zone_entry, zone_entry_fixed, operations_start, operations_end, remarks, shipcall_id, participant_id, " +
"berth_id, berth_info, pier_side, participant_type, created, modified, ata, atd, eta_interval_end, etd_interval_end FROM times " + "berth_id, berth_info, pier_side, participant_type, created, modified, ata, atd, eta_interval_end, etd_interval_end FROM times " +
"WHERE times.shipcall_id = ?scid?", model=model.Times, param={"scid" : options["shipcall_id"]}) "WHERE times.shipcall_id = ?scid?", model=model.Times, param={"scid" : options["shipcall_id"]})
pooledConnection.close()
except Exception as ex: except Exception as ex:
logging.error(traceback.format_exc()) logging.error(traceback.format_exc())
@ -37,10 +38,6 @@ def GetTimes(options):
result["error_field"] = "call failed" result["error_field"] = "call failed"
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
finally:
if pooledConnection is not None:
pooledConnection.close()
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
@ -54,8 +51,8 @@ def PostTimes(schemaModel):
# TODO: Validate the upload data # TODO: Validate the upload data
# This creates a *new* entry # This creates a *new* entry
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
@ -115,45 +112,36 @@ def PostTimes(schemaModel):
pooledConnection.close() pooledConnection.close()
def PutTimes(schemaModel, original_payload=None): def PutTimes(schemaModel):
""" """
:param schemaModel: The deserialized model of the record to be inserted :param schemaModel: The deserialized model of the record to be inserted
""" """
# This updates an *existing* entry # This updates an *existing* entry
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
sentinel = object() query = "UPDATE times SET "
existing_times = commands.query_single_or_default("SELECT * FROM times WHERE id = ?id?", sentinel, param={"id": schemaModel["id"]}) isNotFirst = False
if existing_times is sentinel:
return json.dumps("no such record"), 404, {'Content-Type': 'application/json; charset=utf-8'}
provided_keys = set(original_payload.keys()) if isinstance(original_payload, dict) else None
if "shipcall_id" not in schemaModel or (provided_keys is not None and "shipcall_id" not in provided_keys):
schemaModel["shipcall_id"] = existing_times["shipcall_id"]
schemaModel = {k:v.value if isinstance(v, (Enum, Flag)) else v for k,v in schemaModel.items()}
update_clauses = []
for key in schemaModel.keys(): for key in schemaModel.keys():
if provided_keys is not None and key not in provided_keys:
continue
if key == "id": if key == "id":
continue continue
if key == "created": if key == "created":
continue continue
if key == "modified": if key == "modified":
continue continue
update_clauses.append(f"{key} = ?{key}?") if isNotFirst:
query += ", "
isNotFirst = True
query += key + " = ?" + key + "? "
if update_clauses: query += "WHERE id = ?id?"
query = "UPDATE times SET " + ", ".join(update_clauses) + " WHERE id = ?id?"
commands.execute(query, param=schemaModel) schemaModel = {k:v.value if isinstance(v, (Enum, Flag)) else v for k,v in schemaModel.items()}
affected_rows = commands.execute(query, param=schemaModel)
# apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database 'shipcall' # apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database 'shipcall'
evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=schemaModel["shipcall_id"]) # every times data object refers to the 'shipcall_id' evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=schemaModel["shipcall_id"]) # every times data object refers to the 'shipcall_id'
@ -189,8 +177,8 @@ def DeleteTimes(options):
options["id"] options["id"]
""" """
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
shipcall_id = commands.execute_scalar("SELECT shipcall_id FROM times WHERE id = ?id?", param={"id" : options["id"]}) shipcall_id = commands.execute_scalar("SELECT shipcall_id FROM times WHERE id = ?id?", param={"id" : options["id"]})

View File

@ -14,8 +14,8 @@ def PutUser(schemaModel):
""" """
# This updates an *existing* entry # This updates an *existing* entry
pooledConnection = None
try: try:
pooledConnection = local_db.getPoolConnection() pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
@ -26,6 +26,7 @@ def PutUser(schemaModel):
# theuser = commands.query_single_or_default(query, sentinel, param={"id" : schemaModel["id"]}, model=model.User) # theuser = commands.query_single_or_default(query, sentinel, param={"id" : schemaModel["id"]}, model=model.User)
theuser = commands.query_single_or_default("SELECT * FROM user where id = ?id?", sentinel, param={"id" : schemaModel["id"]}, model=model.User) theuser = commands.query_single_or_default("SELECT * FROM user where id = ?id?", sentinel, param={"id" : schemaModel["id"]}, model=model.User)
if theuser is sentinel: if theuser is sentinel:
pooledConnection.close()
# #TODO: result = {"message":"no such record"} -> json.dumps # #TODO: result = {"message":"no such record"} -> json.dumps
return json.dumps("no such record"), 404, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps("no such record"), 404, {'Content-Type': 'application/json; charset=utf-8'}

View File

@ -1,90 +1,58 @@
import mysql.connector import mysql.connector
from mysql.connector import pooling
import pydapper import pydapper
import logging import logging
import json import json
import os import os
import sys import sys
from BreCal.schemas import defs from BreCal.schemas import defs
config_path = None config_path = None
secure_dir = None
_connection_pool = None
def initPool(instancePath, connection_filename="connection_data_test.json"):
def _load_json(path):
with open(path, encoding="utf-8") as fh:
return json.load(fh)
def _build_pool_config(connection_data, pool_name, pool_size):
pool_config = dict(connection_data)
pool_config.setdefault("pool_name", pool_name)
pool_config.setdefault("pool_size", pool_size)
return pool_config
def initPool(instancePath, config=None, connection_filename="connection_data_prod.json",
credentials_file="email_credentials_test.json", pool_name="brecal_pool", pool_size=10,
secure_directory=None):
"""
Initialize the MySQL connection pool and load email credentials.
"""
global config_path, secure_dir, _connection_pool
try: try:
if config: global config_path
connection_filename = config.get("DB_CONNECTION_FILE", connection_filename) if(config_path == None):
credentials_file = config.get("EMAIL_CREDENTIALS_FILE", credentials_file) config_path = os.path.join(instancePath,f'../../../secure/{connection_filename}') #connection_data_test.json');
pool_name = config.get("DB_POOL_NAME", pool_name)
pool_size = config.get("DB_POOL_SIZE", pool_size)
secure_directory = config.get("SECURE_DIR", secure_directory)
if secure_dir is None: config_path = "E:/temp/connection_data.json"
secure_dir = secure_directory if secure_directory else os.path.join(instancePath, '../../../secure') print (config_path)
if config_path is None:
config_path = os.path.join(secure_dir, connection_filename)
print(config_path)
if not os.path.exists(config_path): if not os.path.exists(config_path):
print('cannot find ' + os.path.abspath(config_path)) print ('cannot find ' + os.path.abspath(config_path))
print("instance path", instancePath) print("instance path", instancePath)
sys.exit(1) sys.exit(1)
connection_data = _load_json(config_path) f = open(config_path);
if _connection_pool is None: connection_data = json.load(f)
pool_config = _build_pool_config(connection_data, pool_name, pool_size) f.close()
_connection_pool = pooling.MySQLConnectionPool(**pool_config)
conn_from_pool = _connection_pool.get_connection() conn_from_pool = mysql.connector.connect(**connection_data)
try:
commands = pydapper.using(conn_from_pool)
commands.query("SELECT id from `user` LIMIT 1")
print("DB connection successful")
finally:
conn_from_pool.close()
credentials_path = os.path.join(secure_dir, credentials_file) commands = pydapper.using(conn_from_pool)
data = commands.query("SELECT id from `user`")
print("DB connection successful")
conn_from_pool.close()
credentials_file = "email_credentials_test.json"
credentials_path = os.path.join(instancePath,f'../../../secure/{credentials_file}')
credentials_path = "E:/temp/email_credentials_devel.json"
if not os.path.exists(credentials_path): if not os.path.exists(credentials_path):
print('cannot find ' + os.path.abspath(credentials_path)) print ('cannot find ' + os.path.abspath(credentials_path))
sys.exit(1) sys.exit(1)
defs.email_credentials = _load_json(credentials_path) f = open(credentials_path);
defs.email_credentials = json.load(f)
f.close()
except mysql.connector.PoolError as e: except mysql.connector.PoolError as e:
logging.error(f"Failed to create connection pool: {e}") logging.error(f"Failed to create connection pool: {e}")
print(e) print(e)
except Exception as e: except Exception as e:
logging.error("Failed to initialize DB pool: %s", e)
print(e) print(e)
def getPoolConnection(): def getPoolConnection():
if _connection_pool is None: global config_path
raise RuntimeError("Connection pool not initialized. Call initPool first.") f = open(config_path);
try: connection_data = json.load(f)
return _connection_pool.get_connection() return mysql.connector.connect(**connection_data)
except mysql.connector.PoolError as exc:
logging.error("Connection pool exhausted: %s", exc)
raise

View File

@ -3,42 +3,49 @@
"type" : 1, "type" : 1,
"color" : "#0867ec", "color" : "#0867ec",
"name" : "assignment", "name" : "assignment",
"link" : "https://www.bremen-calling.de/",
"msg_text" : "Nominierung" "msg_text" : "Nominierung"
}, },
{ {
"type" : 2, "type" : 2,
"color" : "#ea5c00", "color" : "#ea5c00",
"name" : "next24h", "name" : "next24h",
"link" : "https://www.bremen-calling.de/",
"msg_text" : "Morgenrunde relevant" "msg_text" : "Morgenrunde relevant"
}, },
{ {
"type" : 3, "type" : 3,
"color" : "#f34336", "color" : "#f34336",
"name" : "time_conflict", "name" : "time_conflict",
"link" : "https://www.bremen-calling.de/",
"msg_text" : "Zeitlicher Konflikt" "msg_text" : "Zeitlicher Konflikt"
}, },
{ {
"type" : 4, "type" : 4,
"color" : "#28b532", "color" : "#28b532",
"name" : "time_conflict_resolved", "name" : "time_conflict_resolved",
"link" : "https://www.bremen-calling.de/",
"msg_text" : "Zeitlicher Konflikt gelöst" "msg_text" : "Zeitlicher Konflikt gelöst"
}, },
{ {
"type" : 5, "type" : 5,
"color" : "#a8a8a8", "color" : "#a8a8a8",
"name" : "unassigned", "name" : "unassigned",
"link" : "https://www.bremen-calling.de/",
"msg_text" : "Nominierung abgewählt" "msg_text" : "Nominierung abgewählt"
}, },
{ {
"type" : 6, "type" : 6,
"color" : "#a8a800", "color" : "#a8a800",
"name" : "missing_data", "name" : "missing_data",
"link" : "https://www.bremen-calling.de/",
"msg_text" : "Fehlende Daten" "msg_text" : "Fehlende Daten"
}, },
{ {
"type" : 7, "type" : 7,
"color" : "#808070", "color" : "#808070",
"name" : "cancelled", "name" : "cancelled",
"link" : "https://www.bremen-calling.de/",
"msg_text" : "Storno" "msg_text" : "Storno"
} }
] ]

View File

@ -5,7 +5,7 @@ from marshmallow_enum import EnumField
from enum import IntEnum from enum import IntEnum
from marshmallow_dataclass import dataclass from marshmallow_dataclass import dataclass
from typing import Iterable, List from typing import List
import json import json
import re import re
@ -85,31 +85,19 @@ class NotificationType(IntEnum):
def _missing_(cls, value): def _missing_(cls, value):
return cls.undefined return cls.undefined
def bitflag_to_list(bitflag: int | None) -> list[NotificationType]: def bitflag_to_list(bitflag: int) -> list[NotificationType]:
"""Converts an integer bitflag to a list of NotificationType enums."""
if bitflag is None: if bitflag is None:
return [] return []
"""Converts an integer bitflag to a list of NotificationType enums."""
return [nt for nt in NotificationType if bitflag & (1 << (nt.value - 1))] return [nt for nt in NotificationType if bitflag & (1 << (nt.value - 1))]
def list_to_bitflag(notifications: Iterable[NotificationType | str | int] | None) -> int: def list_to_bitflag(notifications: fields.List) -> int:
"""Converts a list of NotificationType enums (or their names/values) to an integer bitflag.""" """Converts a list of NotificationType enums to an integer bitflag."""
if not notifications: try:
iter(notifications)
return sum(1 << (nt.value - 1) for nt in notifications)
except TypeError as te:
return 0 return 0
bitflag = 0
for nt in notifications:
enum_val = None
if isinstance(nt, NotificationType):
enum_val = nt
elif isinstance(nt, str):
enum_val = NotificationType[nt]
else:
enum_val = NotificationType(nt)
bitflag |= 1 << (enum_val.value - 1)
return bitflag
def notification_types_to_names(notifications: Iterable[NotificationType]) -> list[str]:
"""Render NotificationType values as their names for API responses."""
return [nt.name for nt in notifications]
class ShipcallType(IntEnum): class ShipcallType(IntEnum):
@ -213,7 +201,7 @@ class Participant(Schema):
ports: List[int] = field(default_factory=list) ports: List[int] = field(default_factory=list)
@validates("type") @validates("type")
def validate_type(self, value, **kwargs): def validate_type(self, value):
# e.g., when an IntFlag has the values 1,2,4; the maximum valid value is 7 # e.g., when an IntFlag has the values 1,2,4; the maximum valid value is 7
max_int = sum([int(val) for val in list(ParticipantType._value2member_map_.values())]) max_int = sum([int(val) for val in list(ParticipantType._value2member_map_.values())])
min_int = 0 min_int = 0
@ -224,7 +212,7 @@ class Participant(Schema):
@validates("flags") @validates("flags")
def validate_flags(self, value, **kwargs): def validate_flags(self, value):
# e.g., when an IntFlag has the values 1,2,4; the maximum valid value is 7 # e.g., when an IntFlag has the values 1,2,4; the maximum valid value is 7
max_int = sum([int(val) for val in list(ParticipantFlag._value2member_map_.values())]) max_int = sum([int(val) for val in list(ParticipantFlag._value2member_map_.values())])
min_int = 0 min_int = 0
@ -249,7 +237,7 @@ class ShipcallSchema(Schema):
id = fields.Integer(required=True) id = fields.Integer(required=True)
ship_id = fields.Integer(required=True) ship_id = fields.Integer(required=True)
port_id = fields.Integer(required=True) port_id = fields.Integer(required=True)
type = fields.Enum(ShipcallType, load_default=ShipcallType.undefined, dump_default=ShipcallType.undefined) type = fields.Enum(ShipcallType, default=ShipcallType.undefined)
eta = fields.DateTime(required=False, allow_none=True) eta = fields.DateTime(required=False, allow_none=True)
voyage = fields.String(allow_none=True, required=False, validate=[validate.Length(max=16)]) voyage = fields.String(allow_none=True, required=False, validate=[validate.Length(max=16)])
etd = fields.DateTime(required=False, allow_none=True) etd = fields.DateTime(required=False, allow_none=True)
@ -270,7 +258,7 @@ class ShipcallSchema(Schema):
anchored = fields.Bool(required=False, allow_none=True) anchored = fields.Bool(required=False, allow_none=True)
moored_lock = fields.Bool(required=False, allow_none=True) moored_lock = fields.Bool(required=False, allow_none=True)
canceled = fields.Bool(required=False, allow_none=True) canceled = fields.Bool(required=False, allow_none=True)
evaluation = fields.Enum(EvaluationType, required=False, allow_none=True, load_default=EvaluationType.undefined, dump_default=ShipcallType.undefined) evaluation = fields.Enum(EvaluationType, required=False, allow_none=True, default=EvaluationType.undefined)
evaluation_message = fields.Str(allow_none=True, required=False) evaluation_message = fields.Str(allow_none=True, required=False)
evaluation_time = fields.DateTime(required=False, allow_none=True) evaluation_time = fields.DateTime(required=False, allow_none=True)
evaluation_notifications_sent = fields.Bool(required=False, allow_none=True) evaluation_notifications_sent = fields.Bool(required=False, allow_none=True)
@ -293,7 +281,7 @@ class ShipcallSchema(Schema):
return data return data
@validates("type") @validates("type")
def validate_type(self, value, **kwargs): def validate_type(self, value):
valid_shipcall_type = int(value) in [item.value for item in ShipcallType] valid_shipcall_type = int(value) in [item.value for item in ShipcallType]
if not valid_shipcall_type: if not valid_shipcall_type:
@ -430,7 +418,7 @@ class TimesSchema(Schema):
berth_info = fields.String(required=False, allow_none=True, validate=[validate.Length(max=512)]) berth_info = fields.String(required=False, allow_none=True, validate=[validate.Length(max=512)])
pier_side = fields.Bool(required=False, allow_none = True) pier_side = fields.Bool(required=False, allow_none = True)
shipcall_id = fields.Integer(required=True) shipcall_id = fields.Integer(required=True)
participant_type = fields.Integer(required = False, allow_none=True) # TODO: could become Enum participant_type = fields.Integer(Required = False, allow_none=True)# TODO: could become Enum. # participant_type = fields.Enum(ParticipantType, required=False, allow_none=True, default=ParticipantType.undefined) #fields.Integer(required=False, allow_none=True)
ata = fields.DateTime(required=False, allow_none=True) ata = fields.DateTime(required=False, allow_none=True)
atd = fields.DateTime(required=False, allow_none=True) atd = fields.DateTime(required=False, allow_none=True)
eta_interval_end = fields.DateTime(required=False, allow_none=True) eta_interval_end = fields.DateTime(required=False, allow_none=True)
@ -439,7 +427,7 @@ class TimesSchema(Schema):
modified = fields.DateTime(required=False, allow_none=True) modified = fields.DateTime(required=False, allow_none=True)
@validates("participant_type") @validates("participant_type")
def validate_participant_type(self, value, **kwargs): def validate_participant_type(self, value):
# #TODO: it may also make sense to block multi-assignments, whereas a value could be BSMD+AGENCY # #TODO: it may also make sense to block multi-assignments, whereas a value could be BSMD+AGENCY
# while the validation fails when one of those multi-assignments is BSMD, it passes in cases, # while the validation fails when one of those multi-assignments is BSMD, it passes in cases,
# such as AGENCY+PILOT # such as AGENCY+PILOT
@ -452,56 +440,56 @@ class TimesSchema(Schema):
raise ValidationError({"participant_type":f"the participant_type must not be .BSMD"}) raise ValidationError({"participant_type":f"the participant_type must not be .BSMD"})
@validates("eta_berth") @validates("eta_berth")
def validate_eta_berth(self, value, **kwargs): def validate_eta_berth(self, value):
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future # violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
# when 'value' is 'None', a ValidationError is not issued. # when 'value' is 'None', a ValidationError is not issued.
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12) valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
return return
@validates("etd_berth") @validates("etd_berth")
def validate_etd_berth(self, value, **kwargs): def validate_etd_berth(self, value):
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future # violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
# when 'value' is 'None', a ValidationError is not issued. # when 'value' is 'None', a ValidationError is not issued.
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12) valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
return return
@validates("lock_time") @validates("lock_time")
def validate_lock_time(self, value, **kwargs): def validate_lock_time(self, value):
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future # violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
# when 'value' is 'None', a ValidationError is not issued. # when 'value' is 'None', a ValidationError is not issued.
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12) valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
return return
@validates("zone_entry") @validates("zone_entry")
def validate_zone_entry(self, value, **kwargs): def validate_zone_entry(self, value):
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future # violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
# when 'value' is 'None', a ValidationError is not issued. # when 'value' is 'None', a ValidationError is not issued.
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12) valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
return return
@validates("operations_start") @validates("operations_start")
def validate_operations_start(self, value, **kwargs): def validate_operations_start(self, value):
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future # violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
# when 'value' is 'None', a ValidationError is not issued. # when 'value' is 'None', a ValidationError is not issued.
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12) valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
return return
@validates("operations_end") @validates("operations_end")
def validate_operations_end(self, value, **kwargs): def validate_operations_end(self, value):
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future # violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
# when 'value' is 'None', a ValidationError is not issued. # when 'value' is 'None', a ValidationError is not issued.
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12) valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
return return
@validates("eta_interval_end") @validates("eta_interval_end")
def validate_eta_interval_end(self, value, **kwargs): def validate_eta_interval_end(self, value):
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future # violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
# when 'value' is 'None', a ValidationError is not issued. # when 'value' is 'None', a ValidationError is not issued.
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12) valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
return return
@validates("etd_interval_end") @validates("etd_interval_end")
def validate_etd_interval_end(self, value, **kwargs): def validate_etd_interval_end(self, value):
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future # violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
# when 'value' is 'None', a ValidationError is not issued. # when 'value' is 'None', a ValidationError is not issued.
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12) valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
@ -528,14 +516,14 @@ class UserSchema(Schema):
notify_on = fields.List(fields.Enum(NotificationType), required=False, allow_none=True) notify_on = fields.List(fields.Enum(NotificationType), required=False, allow_none=True)
@validates("user_phone") @validates("user_phone")
def validate_user_phone(self, value, **kwargs): def validate_user_phone(self, value):
if value is not None: if value is not None:
valid_characters = list(map(str,range(0,10)))+["+", " "] valid_characters = list(map(str,range(0,10)))+["+", " "]
if not all([v in valid_characters for v in value]): if not all([v in valid_characters for v in value]):
raise ValidationError({"user_phone":f"one of the phone number values is not valid."}) raise ValidationError({"user_phone":f"one of the phone number values is not valid."})
@validates("user_email") @validates("user_email")
def validate_user_email(self, value, **kwargs): def validate_user_email(self, value):
if value and not re.match(r"[^@]+@[^@]+\.[^@]+", value): if value and not re.match(r"[^@]+@[^@]+\.[^@]+", value):
raise ValidationError({"user_email":f"invalid email address"}) raise ValidationError({"user_email":f"invalid email address"})
@ -585,7 +573,8 @@ class User:
notify_popup: bool notify_popup: bool
created: datetime created: datetime
modified: datetime modified: datetime
notify_event: int | None = 0 ports: List[NotificationType] = field(default_factory=list)
notify_event: List[NotificationType] = field(default_factory=list)
def __hash__(self): def __hash__(self):
return hash(id) return hash(id)
@ -623,15 +612,15 @@ class ShipSchema(Schema):
participant_id = fields.Int(allow_none=True, required=False) participant_id = fields.Int(allow_none=True, required=False)
length = fields.Float(allow_none=True, required=False, validate=[validate.Range(min=0, max=1000, min_inclusive=False, max_inclusive=False)]) length = fields.Float(allow_none=True, required=False, validate=[validate.Range(min=0, max=1000, min_inclusive=False, max_inclusive=False)])
width = fields.Float(allow_none=True, required=False, validate=[validate.Range(min=0, max=100, min_inclusive=False, max_inclusive=False)]) width = fields.Float(allow_none=True, required=False, validate=[validate.Range(min=0, max=100, min_inclusive=False, max_inclusive=False)])
is_tug = fields.Bool(allow_none=True, required=False, load_default=False, dump_default=False) is_tug = fields.Bool(allow_none=True, required=False, default=False)
bollard_pull = fields.Int(allow_none=True, required=False) bollard_pull = fields.Int(allow_none=True, required=False)
eni = fields.Int(allow_none=True, required=False) eni = fields.Int(allow_none=True, required=False)
created = fields.DateTime(allow_none=True, required=False) created = fields.DateTime(allow_none=True, required=False)
modified = fields.DateTime(allow_none=True, required=False) modified = fields.DateTime(allow_none=True, required=False)
deleted = fields.Bool(allow_none=True, required=False, load_default=False, dump_default=False) deleted = fields.Bool(allow_none=True, required=False, default=False)
@validates("name") @validates("name")
def validate_name(self, value, **kwargs): def validate_name(self, value):
character_length = len(str(value)) character_length = len(str(value))
if character_length<1: if character_length<1:
raise ValidationError({"name":f"'name' argument should have at least one character"}) raise ValidationError({"name":f"'name' argument should have at least one character"})
@ -643,7 +632,7 @@ class ShipSchema(Schema):
return return
@validates("imo") @validates("imo")
def validate_imo(self, value, **kwargs): def validate_imo(self, value):
value = str(value).zfill(7) # 1 becomes '0000001' (7 characters). 12345678 becomes '12345678' (8 characters) value = str(value).zfill(7) # 1 becomes '0000001' (7 characters). 12345678 becomes '12345678' (8 characters)
imo_length = len(value) imo_length = len(value)
if imo_length != 7: if imo_length != 7:
@ -651,7 +640,7 @@ class ShipSchema(Schema):
return return
@validates("callsign") @validates("callsign")
def validate_callsign(self, value, **kwargs): def validate_callsign(self, value):
if value is not None: if value is not None:
callsign_length = len(str(value)) callsign_length = len(str(value))
if callsign_length>8: if callsign_length>8:

View File

@ -31,7 +31,6 @@ def UpdateShipcalls(options:dict = {'past_days':2}):
options: options:
key: 'past_days'. Is used to execute a filtered query of all available shipcalls. Defaults to 2 (days) key: 'past_days'. Is used to execute a filtered query of all available shipcalls. Defaults to 2 (days)
""" """
pooledConnection = None
try: try:
pooledConnection = getPoolConnection() pooledConnection = getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
@ -50,11 +49,10 @@ def UpdateShipcalls(options:dict = {'past_days':2}):
# apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database # apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database
evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=shipcall_id) # new_id (last insert id) refers to the shipcall id evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=shipcall_id) # new_id (last insert id) refers to the shipcall id
pooledConnection.close()
except Exception as ex: except Exception as ex:
logging.error(ex) logging.error(ex)
finally:
if pooledConnection is not None:
pooledConnection.close()
return return
def UpdateNotifications(cooldown_in_mins:int=10): def UpdateNotifications(cooldown_in_mins:int=10):
@ -63,7 +61,6 @@ def UpdateNotifications(cooldown_in_mins:int=10):
notification is updated to state 1 and a notification is received by the user notification is updated to state 1 and a notification is received by the user
""" """
pooledConnection = None
try: try:
pooledConnection = getPoolConnection() pooledConnection = getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
@ -73,39 +70,32 @@ def UpdateNotifications(cooldown_in_mins:int=10):
for notification in data: for notification in data:
commands.execute("UPDATE notification SET level = 1 WHERE id = ?id?", param={"id":notification.id}) commands.execute("UPDATE notification SET level = 1 WHERE id = ?id?", param={"id":notification.id})
pooledConnection.close()
except Exception as ex: except Exception as ex:
logging.error(ex) logging.error(ex)
finally:
if pooledConnection is not None:
pooledConnection.close()
def ClearNotifications(max_age_in_days:int=3): def ClearNotifications(max_age_in_days:int=3):
""" """
This function clears all notifications in state ("level") 2 that are older than x days This function clears all notifications in state ("level") 2 that are older than x days
""" """
pooledConnection = None
try: try:
pooledConnection = getPoolConnection() pooledConnection = getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
query = f"DELETE FROM notification WHERE level = 2 and created < TIMESTAMP(NOW() - INTERVAL {max_age_in_days} DAY)" query = f"DELETE FROM notification WHERE level = 2 and created < TIMESTAMP(NOW() - INTERVAL {max_age_in_days} DAY)"
result = commands.execute(query) result = commands.execute(query)
pooledConnection.close()
if(result > 0): if(result > 0):
logging.info(f"Deleted {result} notifications") logging.info(f"Deleted {result} notifications")
except Exception as ex: except Exception as ex:
logging.error(ex) logging.error(ex)
finally:
if pooledConnection is not None:
pooledConnection.close()
def SendEmails(email_dict): def SendEmails(email_dict):
""" """
This function sends emails to all users in the emaildict This function sends emails to all users in the emaildict
""" """
pooledConnection = None
conn = None
try: try:
pooledConnection = getPoolConnection() pooledConnection = getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)
@ -145,8 +135,7 @@ def SendEmails(email_dict):
with open(os.path.join(current_path,'../msg/notification_element.html'), mode="r", encoding="utf-8") as file: with open(os.path.join(current_path,'../msg/notification_element.html'), mode="r", encoding="utf-8") as file:
element = file.read() element = file.read()
element = element.replace("[[color]]", message_type["color"]) element = element.replace("[[color]]", message_type["color"])
linktext = defs.email_credentials["url_template"] + str(notification.shipcall_id) element = element.replace("[[link]]", message_type["link"])
element = element.replace("[[link]]", linktext)
# We want to show the following information for each notification: # We want to show the following information for each notification:
# Ship-name, Arr/Dep/Shift, ETA/ETD, berth # Ship-name, Arr/Dep/Shift, ETA/ETD, berth
@ -194,7 +183,6 @@ def SendEmails(email_dict):
def SendNotifications(): def SendNotifications():
# perhaps this will be moved somewhere else later # perhaps this will be moved somewhere else later
pooledConnection = None
try: try:
# find all notifications in level 1 # find all notifications in level 1
pooledConnection = getPoolConnection() pooledConnection = getPoolConnection()
@ -291,7 +279,6 @@ def add_function_to_schedule_send_notifications(interval_in_minutes:int=1):
return return
def eval_next_24_hrs(): def eval_next_24_hrs():
pooledConnection = None
try: try:
pooledConnection = getPoolConnection() pooledConnection = getPoolConnection()
commands = pydapper.using(pooledConnection) commands = pydapper.using(pooledConnection)

View File

@ -1,31 +0,0 @@
import typing
from marshmallow import ValidationError
class InputValidationBase:
"""
Base class for input validators. Common validation methods are grouped here.
"""
@staticmethod
def check_required_fields(content:dict, required_fields:list[str]):
missing_fields = [field for field in required_fields if content.get(field) is None]
if missing_fields:
raise ValidationError({"required_fields": f"Missing required fields: {missing_fields}"})
@staticmethod
def check_if_entry_is_already_deleted(entry:dict):
"""
Checks if the entry has 'deleted' set to True/1.
"""
if entry.get("deleted") in [True, 1, "1"]:
raise ValidationError({"deleted": "The selected entry is already deleted."})
@staticmethod
def check_deleted_flag_on_post(content:dict):
"""
Checks if 'deleted' is set to 1/True in a POST (Create) request.
"""
if content.get("deleted") in [True, 1, "1"]:
raise ValidationError({"deleted": "Cannot create an entry with 'deleted' set to 1."})

View File

@ -18,11 +18,10 @@ from BreCal.validators.input_validation_utils import check_if_user_is_bsmd_type,
from BreCal.database.sql_handler import execute_sql_query_standalone from BreCal.database.sql_handler import execute_sql_query_standalone
from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
from BreCal.validators.input_validation_base import InputValidationBase
import werkzeug import werkzeug
class InputValidationShip(InputValidationBase): class InputValidationShip():
""" """
This class combines a complex set of individual input validation functions into a joint object. This class combines a complex set of individual input validation functions into a joint object.
It uses static methods, so the object does not need to be instantiated, but functions can be called immediately. It uses static methods, so the object does not need to be instantiated, but functions can be called immediately.
@ -56,13 +55,6 @@ class InputValidationShip(InputValidationBase):
# 3.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema) # 3.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
InputValidationShip.optionally_evaluate_bollard_pull_value(content) InputValidationShip.optionally_evaluate_bollard_pull_value(content)
# 4.) No deleted flag may be set on POST
InputValidationShip.check_deleted_flag_on_post(content)
# 5.) Check if is_tug is null
InputValidationShip.check_is_tug_null(content)
return return
@staticmethod @staticmethod
@ -78,10 +70,6 @@ class InputValidationShip(InputValidationBase):
# 4.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema) # 4.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
InputValidationShip.optionally_evaluate_bollard_pull_value(content) InputValidationShip.optionally_evaluate_bollard_pull_value(content)
# 5.) Check if tug is null
InputValidationShip.check_is_tug_null(content)
return return
@staticmethod @staticmethod
@ -171,11 +159,5 @@ class InputValidationShip(InputValidationBase):
raise ValidationError({"deleted":f"The selected ship entry is already deleted."}) raise ValidationError({"deleted":f"The selected ship entry is already deleted."})
return return
@staticmethod
def check_is_tug_null(content:dict):
is_tug = content.get("is_tug", None)
if is_tug is None:
raise ValidationError({"is_tug":f"The 'is_tug' property must be set to either True or False."})
return

View File

@ -17,13 +17,12 @@ from BreCal.database.sql_handler import get_assigned_participant_of_type
from BreCal.database.sql_handler import execute_sql_query_standalone from BreCal.database.sql_handler import execute_sql_query_standalone
from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
from BreCal.validators.input_validation_base import InputValidationBase
from BreCal.database.sql_queries import SQLQuery from BreCal.database.sql_queries import SQLQuery
import werkzeug import werkzeug
class InputValidationShipcall(InputValidationBase): class InputValidationShipcall():
""" """
This class combines a complex set of individual input validation functions into a joint object. This class combines a complex set of individual input validation functions into a joint object.
It uses static methods, so the object does not need to be instantiated, but functions can be called immediately. It uses static methods, so the object does not need to be instantiated, but functions can be called immediately.
@ -61,11 +60,7 @@ class InputValidationShipcall(InputValidationBase):
InputValidationShipcall.check_participant_list_not_empty_when_user_is_agency(loadedModel) InputValidationShipcall.check_participant_list_not_empty_when_user_is_agency(loadedModel)
# check for reasonable values in the shipcall fields # check for reasonable values in the shipcall fields
InputValidationShipcall.check_shipcall_values(loadedModel, content, forbidden_keys=["evaluation", "evaluation_message", "canceled"]) # "canceled" InputValidationShipcall.check_shipcall_values(loadedModel, content, forbidden_keys=["evaluation", "evaluation_message"]) # "canceled"
# check for deleted flag on POST
InputValidationShipcall.check_deleted_flag_on_post(content)
return return
@staticmethod @staticmethod

View File

@ -17,7 +17,6 @@ from BreCal.database.sql_queries import SQLQuery
from BreCal.database.sql_handler import execute_sql_query_standalone from BreCal.database.sql_handler import execute_sql_query_standalone
from BreCal.database.sql_handler import get_assigned_participant_of_type from BreCal.database.sql_handler import get_assigned_participant_of_type
from BreCal.database.sql_utils import get_times_data_for_id from BreCal.database.sql_utils import get_times_data_for_id
from BreCal.validators.input_validation_base import InputValidationBase
from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag, check_if_string_has_special_characters from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag, check_if_string_has_special_characters
import werkzeug import werkzeug
@ -64,7 +63,7 @@ def build_post_data_type_dependent_required_fields_dict()->dict[ShipcallType,dic
class InputValidationTimes(InputValidationBase): class InputValidationTimes():
""" """
This class combines a complex set of individual input validation functions into a joint object. This class combines a complex set of individual input validation functions into a joint object.
It uses static methods, so the object does not need to be instantiated, but functions can be called immediately. It uses static methods, so the object does not need to be instantiated, but functions can be called immediately.
@ -93,10 +92,6 @@ class InputValidationTimes(InputValidationBase):
# 4.) Value checking # 4.) Value checking
InputValidationTimes.check_dataset_values(user_data, loadedModel, content) InputValidationTimes.check_dataset_values(user_data, loadedModel, content)
# 5.) Deleted flag may not be set on POST
InputValidationTimes.check_deleted_flag_on_post(content)
return return
@staticmethod @staticmethod

View File

@ -1,19 +1,20 @@
import typing import typing
import re from string import ascii_letters, digits
_VALID = re.compile(r'^[\w &-]+$') # \w == Unicode letters+digits+underscore
def check_if_string_has_special_characters(text:typing.Optional[str]): def check_if_string_has_special_characters(text:typing.Optional[str]):
""" """
check, whether there are any characters within the provided string, which are not found in the ascii letters or digits check, whether there are any characters within the provided string, which are not found in the ascii letters or digits
ascii_letters: abcd (...) and ABCD (...)
digits: 0123 (...)
Formerly, this solution was used but was found to be too restrictive:
Source: https://stackoverflow.com/questions/57062794/is-there-a-way-to-check-if-a-string-contains-special-characters Source: https://stackoverflow.com/questions/57062794/is-there-a-way-to-check-if-a-string-contains-special-characters
User: https://stackoverflow.com/users/10035985/andrej-kesely User: https://stackoverflow.com/users/10035985/andrej-kesely
returns bool returns bool
""" """
return not _VALID.fullmatch(text) if text else False if text is None:
return False
return bool(set(text).difference(ascii_letters + digits + ' '))
def check_if_int_is_valid_flag(value, enum_object): def check_if_int_is_valid_flag(value, enum_object):

View File

@ -91,43 +91,40 @@ class ValidationRules(ValidationRuleFunctions):
if evaluation_states_old is not None and evaluation_states_new is not None: if evaluation_states_old is not None and evaluation_states_new is not None:
if len(evaluation_states_old) == 1 and len(evaluation_states_new) == 1: if len(evaluation_states_old) == 1 and len(evaluation_states_new) == 1:
if evaluation_states_old[0] != evaluation_states_new[0]: if evaluation_states_old[0] != evaluation_states_new[0]:
pooledConnection = None pooledConnection = getPoolConnection()
try: commands = pydapper.using(pooledConnection)
pooledConnection = getPoolConnection() notification_type = 3 # RED (mapped to time_conflict)
commands = pydapper.using(pooledConnection) if evaluation_states_new[0] == 2:
notification_type = 3 # RED (mapped to time_conflict) match evaluation_states_old[0]:
if evaluation_states_new[0] == 2: case 0:
match evaluation_states_old[0]: send_notification = True
case 0: case 1:
send_notification = True send_notification = True
case 1: notification_type = 6 # YELLOW (mapped to missing_data)
send_notification = True if evaluation_states_new[0] == 3:
notification_type = 6 # YELLOW (mapped to missing_data) match evaluation_states_old[0]:
if evaluation_states_new[0] == 3: case 0:
match evaluation_states_old[0]: send_notification = True
case 0: case 1:
send_notification = True send_notification = True
case 1: case 2:
send_notification = True send_notification = True
case 2:
send_notification = True
if send_notification: if send_notification:
query = f"INSERT INTO notification (shipcall_id, type, level, message) VALUES (?shipcall_id?, {notification_type}, 0, ?message?)" query = f"INSERT INTO notification (shipcall_id, type, level, message) VALUES (?shipcall_id?, {notification_type}, 0, ?message?)"
commands.execute(query, param={"shipcall_id" : int(shipcall_df.index[0]), "message" : violations[0]}) commands.execute(query, param={"shipcall_id" : int(shipcall_df.index[0]), "message" : violations[0]})
if evaluation_states_new[0] == 1 and evaluation_states_old[0] != 0: # this resolves the conflict if evaluation_states_new[0] == 1 and evaluation_states_old[0] != 0: # this resolves the conflict
query = f"SELECT * from notification where shipcall_id = ?shipcall_id? and type = {notification_type} and level = 0" query = f"SELECT * from notification where shipcall_id = ?shipcall_id? and type = {notification_type} and level = 0"
existing_notification = commands.query(query, param={"shipcall_id" : int(shipcall_df.index[0])}) existing_notification = commands.query(query, param={"shipcall_id" : int(shipcall_df.index[0])})
if len(existing_notification) > 0: if len(existing_notification) > 0:
query = "DELETE from notification where id = ?id?" query = "DELETE from notification where id = ?id?"
commands.execute(query, param={"id" : existing_notification[0]["id"]}) commands.execute(query, param={"id" : existing_notification[0]["id"]})
else: else:
query = "INSERT INTO notification (shipcall_id, type, level) VALUES (?shipcall_id?, 4, 0)" query = "INSERT INTO notification (shipcall_id, type, level) VALUES (?shipcall_id?, 4, 0)"
commands.execute(query, param={"shipcall_id" : int(shipcall_df.index[0])}) commands.execute(query, param={"shipcall_id" : int(shipcall_df.index[0])})
finally:
if pooledConnection is not None: pooledConnection.close()
pooledConnection.close()
# build the list of 'evaluation_notifications_sent'. The value is 'False', when a notification should be created # build the list of 'evaluation_notifications_sent'. The value is 'False', when a notification should be created

View File

@ -1,40 +0,0 @@
"""
Sample configuration for the Flask instance.
Copy this file to `src/server/instance/config.py` (the instance folder is git-ignored)
and adjust the values for each deployment target.
"""
# Flask
SECRET_KEY = "change-me"
# Python path adjustments used by the WSGI entrypoint (flaskapp.wsgi)
APP_ROOT = "/var/www/brecal/src/server"
SITE_PACKAGES = "/var/www/venv/lib/python3.12/site-packages/"
# Paths to environment-specific secrets and instance data
SECURE_DIR = "/var/www/secure" # directory that holds connection/email JSON files
INSTANCE_PATH = "/var/www/brecal/src/server/instance"
# Logging
LOG_FILE = "brecal.log"
LOG_LEVEL = "INFO" # e.g. DEBUG, INFO, WARNING
LOG_TO_STDERR = False
# Database pool setup
DB_CONNECTION_FILE = "connection_data_prod.json"
DB_POOL_NAME = "brecal_pool"
DB_POOL_SIZE = 10
# Email + notifications
EMAIL_CREDENTIALS_FILE = "email_credentials_prod.json"
EMAIL_URL_TEMPLATE = "https://brecal.example.com/shipcalls/" # base URL for links in emails
SMTP_DEBUG_LEVEL = 0 # 0 = quiet, 1 = verbose
# Scheduler cadence
SCHEDULE_UPDATE_SHIPCALLS_MINUTES = 60
SCHEDULE_BACKGROUND_LATENCY_SECONDS = 30
# Notification cleanup / escalation windows
NOTIFICATION_COOLDOWN_MINS = 10
NOTIFICATION_MAX_AGE_DAYS = 3

View File

@ -1,29 +1,20 @@
import logging
import os import os
import runpy
import sys import sys
from pathlib import Path import logging
BASE_DIR = Path(__file__).resolve().parent sys.path.insert(0, '/var/www/brecal_test/src/server')
INSTANCE_DIR = BASE_DIR / "instance" sys.path.insert(0, '/var/www/venv/lib/python3.12/site-packages/')
CONFIG_PATH = INSTANCE_DIR / "config.py"
config = {} import schedule
if CONFIG_PATH.exists():
config = runpy.run_path(str(CONFIG_PATH))
app_root = config.get("APP_ROOT", str(BASE_DIR)) # set the key
site_packages = config.get("SITE_PACKAGES") os.environ['SECRET_KEY'] = 'zdiTz8P3jXOc7jztIQAoelK4zztyuCpJ'
sys.path.insert(0, app_root) # Set up logging
if site_packages: logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
sys.path.insert(0, site_packages)
if config.get("SECRET_KEY"): # Set up Scheduled Jobs
os.environ["SECRET_KEY"] = config["SECRET_KEY"]
log_kwargs = {"level": getattr(logging, config.get("LOG_LEVEL", "DEBUG")), "stream": sys.stderr}
logging.basicConfig(**log_kwargs)
# Import and run the Flask app
from BreCal import create_app from BreCal import create_app
application = create_app(instance_path=config.get("INSTANCE_PATH")) application = create_app()