Compare commits

...

34 Commits

Author SHA1 Message Date
f68e9ee218 Notification endpoint now filters for participant_id and fixed parsing of event type enumeration array 2025-12-09 12:21:11 +01:00
18f6d53998 Added logging to PutUser failures 2025-12-08 15:40:44 +01:00
4b8e878735 Added ChatGPT-created Mermaid database diagram to the Readme file 2025-12-08 15:40:32 +01:00
3f7da82ea6 Updated create schema script and added script to clear times doublettes for same participant type 2025-12-08 15:40:18 +01:00
c1e3e8939a Externalize all configuration parameters Pt.I 2025-12-05 18:08:15 +01:00
dc98b1d500 Merge branch 'release/1.7.0' into develop 2025-12-05 16:05:42 +01:00
a50cd9cc9a Merge branch 'bugfix/deactivating_users_roleeditor' into develop 2025-12-05 16:00:45 +01:00
d06669e943 Merge branch 'release/1.7.0' into develop 2025-12-05 15:58:38 +01:00
6362f47d43 Updated project settings, removed participant 'active' and fixed user delete 2025-10-13 17:37:59 +02:00
c6954fb222 Fixed some validation issues that have cropped up over the last months 2025-10-07 12:01:57 +02:00
6d8b86280c changed e-mail formatting to direct url at actual notified shipcall 2025-09-30 14:46:34 +02:00
2a1570d9f5 bugfix enum format
fixed required case

fixed more default occurrances

changed validates signature
2025-09-08 15:02:06 +02:00
d180dac600 improved next 24hr schedule check query that takes precedence for times eta value 2025-03-14 15:03:38 +01:00
8b4131332b release pooled SQL connection when sending an email 2025-03-07 10:05:14 +01:00
27b9f46f30 Avoid adding the same notification twice to a sender 2025-03-06 09:51:12 +01:00
c8550431e0 Bugfix for last update 2025-03-05 17:39:39 +01:00
a1b807824e fix for checking notification types when e-mail notifications are evaluated 2025-03-04 17:55:18 +01:00
189626d61c Reduced log-verbosity on the server 2025-02-27 13:48:13 +01:00
3d76acb2f0 Version bump to 1.7.0.7 2025-02-10 08:14:57 +01:00
98c05aed3b Fixed error where rows where not collapsed when user is not of type pilot 2025-02-09 13:57:03 +01:00
98696aee93 Fixed flag evaluation for notification selection type 2025-02-08 13:41:44 +01:00
7f706dfc51 Fixed typo 2025-02-08 11:16:21 +01:00
ab12e28d3d Fix bug when checking for assigned pilot in case tidal times are changed 2025-02-06 06:54:19 +01:00
e9a7e03ebf Version bump to 1.7.0.6 2025-02-05 20:06:07 +01:00
f1c5bd3cd8 Added event type evaluation and storage of selection bitflag. Fixed some details in the UI 2025-02-05 19:24:07 +01:00
bb13d74849 Extended API and added event type selection to about dialog 2025-02-05 09:27:59 +01:00
55cf17d169 Changed Win Target to .NET8, updated YAML for user notification event selection 2025-02-04 10:12:29 +01:00
ea634a3af2 Allow pilots to enter tidal times 2025-02-03 12:02:24 +01:00
21471d4d41 Allow shipcall PUT also by PILOT 2025-02-03 11:56:44 +01:00
fce897fae4 Fix filtering of notifications depending on participant assignment to shipcall in case the notification has no participant id 2025-02-03 11:14:51 +01:00
64c6607076 Fix E-Mail validation error reporting 2025-02-03 10:35:46 +01:00
6dedc04957 changed bg color for missing data 2025-01-21 15:19:18 +01:00
213f7cf58c fixed path 2025-01-21 14:52:50 +01:00
49a8498bbe Changed settings for test version 2025-01-21 13:47:52 +01:00
44 changed files with 767 additions and 221 deletions

View File

@ -5008,7 +5008,7 @@ namespace BreCalClient.misc.Client
{
Proxy = null;
UserAgent = WebUtility.UrlEncode("OpenAPI-Generator/1.0.0/csharp");
BasePath = "https://brecaldevel.bsmd-emswe.eu";
BasePath = "https://brecaltest.bsmd-emswe.eu";
DefaultHeaders = new ConcurrentDictionary<string, string>();
ApiKey = new ConcurrentDictionary<string, string>();
ApiKeyPrefix = new ConcurrentDictionary<string, string>();
@ -5016,7 +5016,7 @@ namespace BreCalClient.misc.Client
{
{
new Dictionary<string, object> {
{"url", "https://brecaldevel.bsmd-emswe.eu"},
{"url", "https://brecaltest.bsmd-emswe.eu"},
{"description", "Development server hosted on vcup"},
}
}
@ -5035,7 +5035,7 @@ namespace BreCalClient.misc.Client
IDictionary<string, string> defaultHeaders,
IDictionary<string, string> apiKey,
IDictionary<string, string> apiKeyPrefix,
string basePath = "https://brecaldevel.bsmd-emswe.eu") : this()
string basePath = "https://brecaltest.bsmd-emswe.eu") : this()
{
if (string.IsNullOrWhiteSpace(basePath))
throw new ArgumentException("The provided basePath is invalid.", "basePath");

View File

@ -14,7 +14,7 @@ info:
name: Use at your own risk
url: 'https://www.bsmd.de/license'
servers:
- url: 'https://brecaldevel.bsmd-emswe.eu'
- url: 'https://brecaltest.bsmd-emswe.eu'
description: Development server hosted on vcup
tags:
- name: user
@ -658,12 +658,12 @@ paths:
- notification
operationId: notificationsGet
parameters:
- name: shipcall_id
- name: participant_id
in: query
required: false
description: '**Id of ship call**. *Example: 52*. Id given in ship call list'
description: '**Id of participant**. *Example: 7*. Id of logged in participant.'
schema:
$ref: '#/components/schemas/shipcallId'
$ref: '#/components/schemas/participant_id'
responses:
'200':
description: notification list

View File

@ -48,3 +48,156 @@ DROP TABLE IF EXISTS `shipcall`;
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

@ -0,0 +1,46 @@
-- 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,8 +1,8 @@
-- MySQL dump 10.13 Distrib 8.0.33, for Win64 (x86_64)
-- MySQL dump 10.13 Distrib 8.0.43, for Win64 (x86_64)
--
-- Host: localhost Database: bremen_calling_test
-- ------------------------------------------------------
-- Server version 8.0.34-0ubuntu0.22.04.1
-- Server version 8.0.42-0ubuntu0.24.10.1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
@ -28,17 +28,65 @@ CREATE TABLE `berth` (
`lock` bit(1) DEFAULT NULL COMMENT 'The lock must be used',
`owner_id` int unsigned DEFAULT NULL,
`authority_id` int unsigned DEFAULT NULL,
`port_id` int unsigned DEFAULT NULL,
`created` datetime DEFAULT CURRENT_TIMESTAMP,
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) DEFAULT b'0',
PRIMARY KEY (`id`),
KEY `FK_OWNER_PART_idx` (`owner_id`),
KEY `FK_AUTHORITY_PART_idx` (`authority_id`),
KEY `FK_AUTHORITY_PART_idx` (`authority_id`) /*!80000 INVISIBLE */,
KEY `FK_PORT_PART_idx` (`port_id`),
CONSTRAINT `FK_AUTHORITY_PART` FOREIGN KEY (`authority_id`) REFERENCES `participant` (`id`),
CONSTRAINT `FK_OWNER_PART` FOREIGN KEY (`owner_id`) REFERENCES `participant` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=195 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Berth of ship for a ship call';
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=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 */;
--
-- 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`
--
@ -48,20 +96,19 @@ DROP TABLE IF EXISTS `notification`;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `notification` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`times_id` int unsigned NOT NULL COMMENT 'times record that caused the notification',
`participant_id` int unsigned NOT NULL COMMENT 'participant ref',
`acknowledged` bit(1) DEFAULT b'0' COMMENT 'true if UI acknowledged',
`shipcall_id` int unsigned DEFAULT NULL,
`participant_id` int unsigned DEFAULT NULL,
`level` tinyint DEFAULT NULL COMMENT 'severity of the notification',
`type` tinyint DEFAULT NULL COMMENT 'Email/UI/Other',
`message` varchar(256) DEFAULT NULL COMMENT 'individual message',
`message` varchar(512) DEFAULT NULL COMMENT 'individual message',
`created` datetime DEFAULT CURRENT_TIMESTAMP,
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `FK_NOT_TIMES` (`times_id`),
KEY `FK_NOT_PART` (`participant_id`),
CONSTRAINT `FK_NOT_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`),
CONSTRAINT `FK_NOT_TIMES` FOREIGN KEY (`times_id`) REFERENCES `times` (`id`)
) 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';
KEY `FK_NOTIFICATION_SHIPCALL_idx` (`shipcall_id`),
KEY `FK_NOTIFICATION_PARTICIPANT_idx` (`participant_id`),
CONSTRAINT `FK_NOTIFICATION_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `FK_NOTIFICATION_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`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';
/*!40101 SET character_set_client = @saved_cs_client */;
--
@ -83,7 +130,46 @@ CREATE TABLE `participant` (
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
`deleted` bit(1) DEFAULT b'0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=137 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='An organization taking part';
) ENGINE=InnoDB AUTO_INCREMENT=160 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 */;
--
@ -166,7 +252,7 @@ CREATE TABLE `ship` (
PRIMARY KEY (`id`),
KEY `FK_SHIP_PARTICIPANT` (`participant_id`),
CONSTRAINT `FK_SHIP_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ENGINE=InnoDB AUTO_INCREMENT=485 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@ -202,16 +288,25 @@ CREATE TABLE `shipcall` (
`canceled` bit(1) DEFAULT NULL,
`evaluation` int unsigned 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,
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `FK_SHIPCALL_SHIP` (`ship_id`),
KEY `FK_SHIPCALL_BERTH_ARRIVAL` (`arrival_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_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`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Incoming, outgoing or moving to another berth';
) ENGINE=InnoDB AUTO_INCREMENT=2789 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Incoming, outgoing or moving to another berth';
/*!40101 SET character_set_client = @saved_cs_client */;
--
@ -225,15 +320,15 @@ CREATE TABLE `shipcall_participant_map` (
`id` int NOT NULL AUTO_INCREMENT,
`shipcall_id` int unsigned DEFAULT NULL,
`participant_id` int unsigned DEFAULT NULL,
`type` int unsigned DEFAULT NULL COMMENT 'Type of participant role',
`type` int unsigned DEFAULT NULL,
`created` datetime DEFAULT CURRENT_TIMESTAMP,
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `FK_MAP_PARTICIPANT_SHIPCALL` (`shipcall_id`),
KEY `FK_MAP_SHIPCALL_PARTICIPANT` (`participant_id`),
CONSTRAINT `FK_MAP_PARTICIPANT_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`),
CONSTRAINT `FK_MAP_PARTICIPANT_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`) ON DELETE SET NULL,
CONSTRAINT `FK_MAP_SHIPCALL_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=128 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Associates a participant with a shipcall';
) ENGINE=InnoDB AUTO_INCREMENT=8933 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Associates a participant with a shipcall';
/*!40101 SET character_set_client = @saved_cs_client */;
--
@ -285,13 +380,20 @@ CREATE TABLE `times` (
`berth_info` varchar(512) DEFAULT NULL,
`pier_side` bit(1) 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`),
UNIQUE KEY `uniq_shipcall_participant` (`shipcall_id`,`participant_type`),
KEY `FK_TIME_SHIPCALL` (`shipcall_id`),
KEY `FK_TIME_PART` (`participant_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_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='the planned time for the participants work';
) ENGINE=InnoDB AUTO_INCREMENT=7863 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='the planned time for the participants work';
/*!40101 SET character_set_client = @saved_cs_client */;
--
@ -303,7 +405,7 @@ DROP TABLE IF EXISTS `user`;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`participant_id` int unsigned DEFAULT NULL,
`participant_id` int unsigned NOT NULL,
`first_name` varchar(45) DEFAULT NULL,
`last_name` varchar(45) DEFAULT NULL,
`user_name` varchar(45) DEFAULT NULL,
@ -311,12 +413,17 @@ CREATE TABLE `user` (
`user_phone` varchar(128) DEFAULT NULL,
`password_hash` varchar(128) 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,
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `FK_USER_PART` (`participant_id`),
CONSTRAINT `FK_USER_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='member of a participant';
) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='member of a participant';
/*!40101 SET character_set_client = @saved_cs_client */;
--
@ -339,6 +446,57 @@ CREATE TABLE `user_role_map` (
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';
/*!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 */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
@ -349,4 +507,4 @@ CREATE TABLE `user_role_map` (
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2023-10-06 14:52:04
-- Dump completed on 2025-11-17 8:26:36

View File

@ -1 +1 @@
1.8.0.0
1.8.0.0

View File

@ -29,7 +29,7 @@
<applicationSettings>
<BreCalClient.Properties.Settings>
<setting name="BG_COLOR" serializeAs="String">
<value>#1D751F</value>
<value>#751D1F</value>
</setting>
<setting name="APP_TITLE" serializeAs="String">
<value>!!Bremen calling Entwicklungsversion!!</value>
@ -38,7 +38,7 @@
<value>https://www.textbausteine.net/</value>
</setting>
<setting name="API_URL" serializeAs="String">
<value>https://brecaldevel.bsmd-emswe.eu</value>
<value>https://brecaltest.bsmd-emswe.eu</value>
</setting>
</BreCalClient.Properties.Settings>
</applicationSettings>

View File

@ -35,7 +35,7 @@ namespace BreCalClient
this.ContentWrapper.Background = Brushes.Gray;
break;
case "MissingData":
this.ContentWrapper.Background= Brushes.Yellow;
this.ContentWrapper.Background = Brushes.DarkKhaki;
break;
case "Cancelled":
this.ContentWrapper.Background = Brushes.DarkGray;

View File

@ -13,7 +13,7 @@
<Title>Bremen calling client</Title>
<Description>A Windows WPF client for the Bremen calling API.</Description>
<ApplicationIcon>containership.ico</ApplicationIcon>
<AssemblyName>BreCalDevelClient</AssemblyName>
<AssemblyName>BreCalTestClient</AssemblyName>
</PropertyGroup>
<ItemGroup>

View File

@ -4,8 +4,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.6.0.3</ApplicationVersion>
<ApplicationRevision>5</ApplicationRevision>
<ApplicationVersion>1.7.0.7</ApplicationVersion>
<BootstrapperEnabled>True</BootstrapperEnabled>
<Configuration>Debug</Configuration>
<CreateDesktopShortcut>True</CreateDesktopShortcut>
@ -27,10 +27,10 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<SelfContained>True</SelfContained>
<SignatureAlgorithm>(none)</SignatureAlgorithm>
<SignManifests>False</SignManifests>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<UpdateEnabled>True</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateRequired>False</UpdateRequired>
<UpdateRequired>True</UpdateRequired>
<WebPageFileName>Publish.html</WebPageFileName>
<CreateDesktopShortcut>True</CreateDesktopShortcut>
<ErrorReportUrl>https://www.bsmd-emswe.eu/</ErrorReportUrl>
@ -38,8 +38,10 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PublisherName>Informatikbüro Daniel Schick</PublisherName>
<SuiteName>Bremen Calling</SuiteName>
<SupportUrl>http://www.textbausteine.net/</SupportUrl>
<PublishDir>bin\Debug\net6.0-windows\win-x64\app.publish\</PublishDir>
<PublishDir>bin\Debug\net8.0-windows7.0\win-x64\app.publish\</PublishDir>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SkipPublishVerification>false</SkipPublishVerification>
<MinimumRequiredVersion>1.7.0.7</MinimumRequiredVersion>
</PropertyGroup>
<ItemGroup>
<PublishFile Include="containership.ico">

View File

@ -9,29 +9,29 @@
//------------------------------------------------------------------------------
namespace BreCalClient.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.12.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("#1D751F")]
[global::System.Configuration.DefaultSettingValueAttribute("#751D1F")]
public string BG_COLOR {
get {
return ((string)(this["BG_COLOR"]));
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("!!Bremen calling Entwicklungsversion!!")]
@ -40,7 +40,7 @@ namespace BreCalClient.Properties {
return ((string)(this["APP_TITLE"]));
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("https://www.textbausteine.net/")]
@ -49,7 +49,7 @@ namespace BreCalClient.Properties {
return ((string)(this["LOGO_IMAGE_URL"]));
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]
@ -61,16 +61,16 @@ namespace BreCalClient.Properties {
this["FilterCriteria"] = value;
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("https://brecaldevel.bsmd-emswe.eu")]
[global::System.Configuration.DefaultSettingValueAttribute("https://brecaltest.bsmd-emswe.eu")]
public string API_URL {
get {
return ((string)(this["API_URL"]));
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("800")]
@ -82,7 +82,7 @@ namespace BreCalClient.Properties {
this["Width"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("450")]
@ -94,7 +94,7 @@ namespace BreCalClient.Properties {
this["Height"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
@ -106,7 +106,7 @@ namespace BreCalClient.Properties {
this["Left"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
@ -118,7 +118,7 @@ namespace BreCalClient.Properties {
this["Top"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
@ -130,7 +130,7 @@ namespace BreCalClient.Properties {
this["W1Left"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
@ -142,7 +142,7 @@ namespace BreCalClient.Properties {
this["W1Top"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
@ -154,7 +154,7 @@ namespace BreCalClient.Properties {
this["W2Left"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
@ -166,7 +166,7 @@ namespace BreCalClient.Properties {
this["W2Top"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
@ -178,7 +178,7 @@ namespace BreCalClient.Properties {
this["W3Left"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
@ -190,7 +190,7 @@ namespace BreCalClient.Properties {
this["W3Top"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
@ -202,7 +202,7 @@ namespace BreCalClient.Properties {
this["W4Left"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
@ -214,7 +214,7 @@ namespace BreCalClient.Properties {
this["W4Top"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]
@ -226,7 +226,7 @@ namespace BreCalClient.Properties {
this["FilterCriteriaMap"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public global::System.Collections.Specialized.StringCollection Notifications {
@ -237,7 +237,7 @@ namespace BreCalClient.Properties {
this["Notifications"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
@ -249,7 +249,7 @@ namespace BreCalClient.Properties {
this["W5Top"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]

View File

@ -3,7 +3,7 @@
<Profiles />
<Settings>
<Setting Name="BG_COLOR" Type="System.String" Scope="Application">
<Value Profile="(Default)">#1D751F</Value>
<Value Profile="(Default)">#751D1F</Value>
</Setting>
<Setting Name="APP_TITLE" Type="System.String" Scope="Application">
<Value Profile="(Default)">!!Bremen calling Entwicklungsversion!!</Value>
@ -15,7 +15,7 @@
<Value Profile="(Default)" />
</Setting>
<Setting Name="API_URL" Type="System.String" Scope="Application">
<Value Profile="(Default)">https://brecaldevel.bsmd-emswe.eu</Value>
<Value Profile="(Default)">https://brecaltest.bsmd-emswe.eu</Value>
</Setting>
<Setting Name="Width" Type="System.Double" Scope="User">
<Value Profile="(Default)">800</Value>

View File

@ -5,7 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:p = "clr-namespace:BreCalClient.Resources"
xmlns:sets="clr-namespace:BreCalClient.Properties"
xmlns:db="clr-namespace:BreCalClient;assembly=BreCalDevelClient"
xmlns:db="clr-namespace:BreCalClient;assembly=BreCalTestClient"
mc:Ignorable="d"
d:DesignHeight="135" d:DesignWidth="800">
<Border BorderBrush="LightGray" Margin="1" BorderThickness="1">

View File

@ -215,13 +215,13 @@ namespace BreCalClient
switch (this.ShipcallControlModel?.Shipcall?.Type)
{
case ShipcallType.Arrival: // incoming
this.imageShipcallType.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalDevelClient;component/Resources/arrow_down_red.png"));
this.imageShipcallType.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalTestClient;component/Resources/arrow_down_red.png"));
break;
case ShipcallType.Departure: // outgoing
this.imageShipcallType.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalDevelClient;component/Resources/arrow_up_blue.png"));
this.imageShipcallType.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalTestClient;component/Resources/arrow_up_blue.png"));
break;
case ShipcallType.Shifting: // shifting
this.imageShipcallType.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalDevelClient;component/Resources/arrow_right_green.png"));
this.imageShipcallType.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalTestClient;component/Resources/arrow_right_green.png"));
break;
default:
break;
@ -230,13 +230,13 @@ namespace BreCalClient
switch(this.ShipcallControlModel?.LightMode)
{
case EvaluationType.Green:
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalDevelClient;component/Resources/check.png"));
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalTestClient;component/Resources/check.png"));
break;
case EvaluationType.Yellow:
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalDevelClient;component/Resources/sign_warning.png"));
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalTestClient;component/Resources/sign_warning.png"));
break;
case EvaluationType.Red:
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalDevelClient;component/Resources/delete2.png"));
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalTestClient;component/Resources/delete2.png"));
break;
default:
break;

View File

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

View File

@ -59,7 +59,7 @@
<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="City" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="Active" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Right"/>
<Label Content="Deleted" Grid.Row="4" 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="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="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" />
<CheckBox x:Name="checkboxParticipantActive" Grid.Row="4" Grid.Column="2" VerticalAlignment="Center" />
<CheckBox x:Name="checkboxParticipantDeleted" Grid.Row="4" Grid.Column="2" VerticalAlignment="Center" IsEnabled="False" />
<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" />
<StackPanel Orientation="Horizontal" Grid.Row="7" Grid.Column="0">
@ -167,7 +167,7 @@
</Grid.ColumnDefinitions>
<ListBox x:Name="listBoxUser" Margin="2" Grid.RowSpan="9" SelectionChanged="listBoxUser_SelectionChanged">
<ListBox.ContextMenu>
<ContextMenu>
<ContextMenu Name="contextMenuUser">
<MenuItem x:Name="menuItemNewUser" Header="New.." Click="menuItemNewUser_Click">
<MenuItem.Icon>
<Image Source="Resources/add.png" />

View File

@ -61,6 +61,8 @@ namespace RoleEditor
// load all participants
List<Participant> participants = await Participant.LoadAll(_dbManager);
participants.Sort((x, y) => string.Compare(x.Name, y.Name));
foreach (Participant p in participants)
{
_participants.Add(p);
@ -464,9 +466,9 @@ namespace RoleEditor
this.textBoxParticipantName.Text = (p != null) ? p.Name : string.Empty;
this.textBoxParticipantStreet.Text = (p != null) ? p.Street : string.Empty;
this.textBoxParticipantPostalCode.Text = (p != null) ? p.PostalCode : string.Empty;
this.textBoxParticipantCity.Text = (p != null) ? p.City : string.Empty;
// this.checkboxParticipantActive.Checked = (p != null) ? p.
this.textBoxParticipantCity.Text = (p != null) ? p.City : 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.checkBoxParticipantAllowBSMD.IsChecked = (p != null) ? p.IsFlagSet(Participant.ParticipantFlags.ALLOW_BSMD) : null;
this.comboBoxParticipantType.SelectedItems.Clear();
@ -510,6 +512,11 @@ namespace RoleEditor
_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)
@ -594,7 +601,7 @@ namespace RoleEditor
if(this.listBoxParticipant.SelectedItem is Participant p)
{
await p.Delete(_dbManager);
this._participants.Remove(p);
p.Deleted = true;
}
}
catch (Exception ex)
@ -628,6 +635,7 @@ namespace RoleEditor
{
if (this.listBoxUser.SelectedItem is User u)
{
await u.ExecuteNonQuery(_dbManager); // extra history delete happens here
await u.Delete(_dbManager);
this._users.Remove(u);
}

View File

@ -9,24 +9,24 @@
//------------------------------------------------------------------------------
namespace RoleEditor.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.10.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_" +
"devel;Port=33306")]
"test;Port=33306")]
public string ConnectionString {
get {
return ((string)(this["ConnectionString"]));

View File

@ -3,7 +3,7 @@
<Profiles />
<Settings>
<Setting Name="ConnectionString" Type="System.String" Scope="Application">
<Value Profile="(Default)">Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_devel;Port=33306</Value>
<Value Profile="(Default)">Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_test;Port=33306</Value>
</Setting>
</Settings>
</SettingsFile>

View File

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

View File

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

View File

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

View File

@ -101,6 +101,16 @@ namespace brecal.model
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
#region private methods

View File

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

View File

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

View File

@ -1,6 +1,7 @@
from flask import Flask
import os
import sys
import logging
from . import local_db
@ -36,7 +37,6 @@ from BreCal.stubs.df_times import get_df_times
from BreCal.services.schedule_routines import setup_schedule, run_schedule_permanently_in_background
def create_app(test_config=None, instance_path=None):
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY='dev'
@ -48,6 +48,8 @@ def create_app(test_config=None, instance_path=None):
if instance_path is not None:
app.instance_path = instance_path
elif app.config.get("INSTANCE_PATH"):
app.instance_path = app.config["INSTANCE_PATH"]
try:
import os
@ -69,13 +71,23 @@ def create_app(test_config=None, instance_path=None):
app.register_blueprint(history.bp)
app.register_blueprint(ports.bp)
logging.basicConfig(filename='brecal.log', level=logging.WARNING, format='%(asctime)s | %(name)s | %(levelname)s | %(message)s')
local_db.initPool(os.path.dirname(app.instance_path))
log_level = getattr(logging, app.config.get("LOG_LEVEL", "DEBUG"))
log_kwargs = {"format": "%(asctime)s | %(name)s | %(levelname)s | %(message)s"}
if app.config.get("LOG_TO_STDERR"):
log_kwargs["stream"] = sys.stderr
else:
log_kwargs["filename"] = app.config.get("LOG_FILE", "brecaltest.log")
logging.basicConfig(level=log_level, **log_kwargs)
if app.config.get("SECRET_KEY"):
os.environ["SECRET_KEY"] = app.config["SECRET_KEY"]
local_db.initPool(os.path.dirname(app.instance_path), config=app.config)
logging.info('App started')
# Setup Routine jobs (e.g., reevaluation of shipcalls)
setup_schedule(update_shipcalls_interval_in_minutes=60)
run_schedule_permanently_in_background(latency=30)
setup_schedule(update_shipcalls_interval_in_minutes=app.config.get("SCHEDULE_UPDATE_SHIPCALLS_MINUTES", 60))
run_schedule_permanently_in_background(latency=app.config.get("SCHEDULE_BACKGROUND_LATENCY_SECONDS", 30))
logging.info('Routine Jobs are defined.')
return app

View File

@ -14,7 +14,14 @@ def GetNotifications():
try:
if 'Authorization' in request.headers:
token = request.headers.get('Authorization')
return impl.notifications.GetNotifications(token)
participant_id = None
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:
return create_dynamic_exception_response(ex=None, status_code=403, message="not authenticated")

View File

@ -7,12 +7,17 @@ from marshmallow import ValidationError
from . import verify_if_request_is_json
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.route('/user', methods=['put'])
@auth_guard() # no restriction by role
def PutUser():
content = None
try:
verify_if_request_is_json(request)
@ -21,9 +26,11 @@ def PutUser():
return impl.user.PutUser(loadedModel)
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)
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")

View File

@ -281,10 +281,8 @@ class SQLQuery():
def get_next24hrs_shipcalls()->str:
query = ("SELECT s.id as id, ship.name as name FROM shipcall s INNER JOIN ship ON s.ship_id = ship.id LEFT JOIN times t on t.shipcall_id = s.id AND t.participant_type = 8 " + \
"WHERE (type = 1 AND ((t.id IS NOT NULL AND t.eta_berth >= NOW() AND t.eta_berth < (NOW() + INTERVAL 1 DAY))" + \
"OR (eta >= NOW() AND eta < (NOW() + INTERVAL 1 DAY)))) OR " + \
"((type = 2 OR type = 3) AND ((t.id IS NOT NULL AND t.etd_berth >= NOW() AND " + \
"t.etd_berth < (NOW() + INTERVAL 1 DAY)) OR (etd >= NOW() AND etd < (NOW() + INTERVAL 1 DAY))))" + \
"WHERE (type = 1 AND (COALESCE(t.eta_berth, eta) >= NOW() AND COALESCE(t.eta_berth, eta) < (NOW() + INTERVAL 1 DAY)))" + \
"OR ((type = 2 OR type = 3) AND (COALESCE(t.etd_berth, etd) >= NOW() AND COALESCE(t.etd_berth, etd) < (NOW() + INTERVAL 1 DAY)))"
"AND s.canceled = 0")
return query

View File

@ -8,6 +8,7 @@ from .. import local_db
from ..services import jwt_handler
def GetUser(options):
pooledConnection = None
@ -38,7 +39,7 @@ def GetUser(options):
"notify_whatsapp": data[0].notify_whatsapp,
"notify_signal": data[0].notify_signal,
"notify_popup": data[0].notify_popup,
"notify_on": model.bitflag_to_list(data[0].notify_event)
"notify_on": model.notification_types_to_names(model.bitflag_to_list(data[0].notify_event))
}
token = jwt_handler.generate_jwt(payload=result, lifetime=120) # generate token valid 60 mins
result["token"] = token # add token to user data

View File

@ -6,17 +6,22 @@ from ..schemas import model
from .. import local_db
from BreCal.database.sql_queries import SQLQuery
def GetNotifications(token):
def GetNotifications(token, participant_id=None):
"""
No parameters, gets all entries
Optional filtering by participant_id. Returns delivered (level=2) notifications.
"""
pooledConnection = None
try:
pooledConnection = local_db.getPoolConnection()
commands = pydapper.using(pooledConnection)
data = commands.query("SELECT id, shipcall_id, participant_id, level, type, message, created, modified FROM notification " +
"WHERE level = 2", model=model.Notification.from_query_row)
query = "SELECT id, shipcall_id, participant_id, level, type, message, created, modified FROM notification WHERE level = 2"
params = {}
if participant_id is not None:
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:
logging.error(ex)

View File

@ -8,6 +8,7 @@ import sys
from BreCal.schemas import defs
config_path = None
secure_dir = None
_connection_pool = None
@ -23,17 +24,26 @@ def _build_pool_config(connection_data, pool_name, pool_size):
return pool_config
def initPool(instancePath, connection_filename="connection_data_prod.json",
pool_name="brecal_pool", pool_size=10):
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, _connection_pool
global config_path, secure_dir, _connection_pool
try:
if config:
connection_filename = config.get("DB_CONNECTION_FILE", connection_filename)
credentials_file = config.get("EMAIL_CREDENTIALS_FILE", credentials_file)
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:
secure_dir = secure_directory if secure_directory else os.path.join(instancePath, '../../../secure')
if config_path is None:
config_path = os.path.join(instancePath, f'../../../secure/{connection_filename}')
# config_path = 'C:\\temp\\connection_data_test.json'
config_path = os.path.join(secure_dir, connection_filename)
print(config_path)
if not os.path.exists(config_path):
@ -54,10 +64,7 @@ def initPool(instancePath, connection_filename="connection_data_prod.json",
finally:
conn_from_pool.close()
credentials_file = "email_credentials_test.json"
credentials_path = os.path.join(instancePath, f'../../../secure/{credentials_file}')
# credentials_path = 'C:\\temp\\email_credentials_test.json'
credentials_path = os.path.join(secure_dir, credentials_file)
if not os.path.exists(credentials_path):
print('cannot find ' + os.path.abspath(credentials_path))

View File

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

View File

@ -5,7 +5,7 @@ from marshmallow_enum import EnumField
from enum import IntEnum
from marshmallow_dataclass import dataclass
from typing import List
from typing import Iterable, List
import json
import re
@ -85,19 +85,31 @@ class NotificationType(IntEnum):
def _missing_(cls, value):
return cls.undefined
def bitflag_to_list(bitflag: int) -> list[NotificationType]:
def bitflag_to_list(bitflag: int | None) -> list[NotificationType]:
"""Converts an integer bitflag to a list of NotificationType enums."""
if bitflag is None:
return []
"""Converts an integer bitflag to a list of NotificationType enums."""
return [nt for nt in NotificationType if bitflag & (1 << (nt.value - 1))]
def list_to_bitflag(notifications: fields.List) -> int:
"""Converts a list of NotificationType enums to an integer bitflag."""
try:
iter(notifications)
return sum(1 << (nt.value - 1) for nt in notifications)
except TypeError as te:
def list_to_bitflag(notifications: Iterable[NotificationType | str | int] | None) -> int:
"""Converts a list of NotificationType enums (or their names/values) to an integer bitflag."""
if not notifications:
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):
@ -573,8 +585,7 @@ class User:
notify_popup: bool
created: datetime
modified: datetime
ports: List[NotificationType] = field(default_factory=list)
notify_event: List[NotificationType] = field(default_factory=list)
notify_event: int | None = 0
def __hash__(self):
return hash(id)

View File

@ -15,13 +15,13 @@ from email.mime.application import MIMEApplication
class EmailHandler():
"""
Creates an EmailHandler, which is capable of connecting to a mail server at a respective port,
as well as logging into a specific user's mail address.
Upon creating messages, these can be sent via this handler.
Creates an EmailHandler, which is capable of connecting to a mail server at a respective port,
as well as logging into a specific user's mail address.
Upon creating messages, these can be sent via this handler.
Options:
mail_server: address of the server, such as 'smtp.gmail.com' or 'w01d5503.kasserver.com
mail_port:
mail_port:
25 - SMTP Port, to send emails
110 - POP3 Port, to receive emails
143 - IMAP Port, to receive from IMAP
@ -38,6 +38,9 @@ class EmailHandler():
self.server = smtplib.SMTP_SSL(self.mail_server, self.mail_port) # alternatively, SMTP
# set the following to 0 to avoid log spamming
self.server.set_debuglevel(1) # 0: no debug, 1: debug
def check_state(self):
"""check, whether the server login took place and is open."""
try:
@ -45,7 +48,7 @@ class EmailHandler():
return status_code==250 # 250: b'2.0.0 Ok'
except smtplib.SMTPServerDisconnected:
return False
def check_connection(self):
"""check, whether the server object is connected to the server. If not, connect it. """
try:
@ -53,7 +56,7 @@ class EmailHandler():
except smtplib.SMTPServerDisconnected:
self.server.connect(self.mail_server, self.mail_port)
return
def check_login(self)->bool:
"""check, whether the server object is logged in as a user"""
user = self.server.__dict__.get("user",None)
@ -61,8 +64,8 @@ class EmailHandler():
def login(self, interactive:bool=True):
"""
login on the determined mail server's mail address. By default, this function opens an interactive window to
type the password without echoing (printing '*******' instead of readable characters).
login on the determined mail server's mail address. By default, this function opens an interactive window to
type the password without echoing (printing '*******' instead of readable characters).
returns (status_code, status_msg)
"""
@ -77,7 +80,7 @@ class EmailHandler():
def create_email(self, subject:str, message_body:str)->EmailMessage:
"""
Create an EmailMessage object, which contains the Email's header ("Subject"), content ("Message Body") and the sender's address ("From").
The EmailMessage object does not contain the recipients yet, as these will be defined upon sending the Email.
The EmailMessage object does not contain the recipients yet, as these will be defined upon sending the Email.
"""
msg = EmailMessage()
msg["Subject"] = subject
@ -85,16 +88,16 @@ class EmailHandler():
#msg["To"] = email_tgts # will be defined in self.send_email
msg.set_content(message_body)
return msg
def build_recipients(self, email_tgts:list[str]):
"""
email formatting does not support lists. Instead, items are joined into a comma-space-separated string.
Example:
[mail1@mail.com, mail2@mail.com] becomes
email formatting does not support lists. Instead, items are joined into a comma-space-separated string.
Example:
[mail1@mail.com, mail2@mail.com] becomes
'mail1@mail.com, mail2@mail.com'
"""
return ', '.join(email_tgts)
def open_mime_application(self, path:str)->MIMEApplication:
"""open a local file, read the bytes into a MIMEApplication object, which is built with the proper subtype (based on the file extension)"""
with open(path, 'rb') as file:
@ -102,24 +105,24 @@ class EmailHandler():
attachment.add_header('Content-Disposition','attachment',filename=str(os.path.basename(path)))
return attachment
def attach_file(self, path:str, msg:email.mime.multipart.MIMEMultipart)->None:
"""
attach a file to the message. This function opens the file, reads its bytes, defines the mime type by the
path extension. The filename is appended as the header.
attach a file to the message. This function opens the file, reads its bytes, defines the mime type by the
path extension. The filename is appended as the header.
mimetypes: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
"""
attachment = self.open_mime_application(path)
msg.attach(attachment)
return
def send_email(self, msg:EmailMessage, email_tgts:list[str], cc_tgts:typing.Optional[list[str]]=None, bcc_tgts:typing.Optional[list[str]]=None, debug:bool=False)->typing.Union[dict,EmailMessage]:
"""
send a prepared email message to recipients (email_tgts), copy (cc_tgts) and blind copy (bcc_tgts).
Returns a dictionary of feedback, which is commonly empty and the EmailMessage.
When failing, this function returns an SMTP error instead of returning the default outputs.
send a prepared email message to recipients (email_tgts), copy (cc_tgts) and blind copy (bcc_tgts).
Returns a dictionary of feedback, which is commonly empty and the EmailMessage.
When failing, this function returns an SMTP error instead of returning the default outputs.
"""
# Set the Recipients
msg["To"] = self.build_recipients(email_tgts)
@ -130,15 +133,15 @@ class EmailHandler():
if bcc_tgts is not None:
msg["Bcc"] = self.build_recipients(bcc_tgts)
# when debugging, do not send the Email, but return the EmailMessage.
# when debugging, do not send the Email, but return the EmailMessage.
if debug:
return {}, msg
assert self.check_login(), f"currently not logged in. Cannot send an Email. Make sure to properly use self.login first. "
# send the prepared EmailMessage via the server.
feedback = self.server.send_message(msg)
return feedback, msg
def translate_mail_to_multipart(self, msg:EmailMessage):
"""EmailMessage does not support HTML and attachments. Hence, one can convert an EmailMessage object."""
if msg.is_multipart():
@ -159,11 +162,11 @@ class EmailHandler():
# attach the remainder of the msg, such as the body, to the MIMEMultipart
msg_new.attach(msg)
return msg_new
def print_email_attachments(self, msg:MIMEMultipart)->list[str]:
"""return a list of lines of an Email, which contain 'filename=' as a list. """
return [line_ for line_ in msg.as_string().split("\n") if "filename=" in line_]
def close(self):
self.server.__dict__.pop("user",None)
self.server.__dict__.pop("password",None)

View File

@ -124,11 +124,11 @@ def SendEmails(email_dict):
defs.message_types = json.load(f)
f.close()
for user, notifications in email_dict.items():
for user_email, notifications in email_dict.items():
msg = EmailMessage()
msg["Subject"] = '[Bremen calling] Notification'
msg["From"] = defs.email_credentials["sender"]
msg["To"] = user.user_email
msg["To"] = user_email
with open(os.path.join(current_path,'../msg/notification_template.html'), mode="r", encoding="utf-8") as file:
body = file.read()
@ -145,7 +145,8 @@ def SendEmails(email_dict):
with open(os.path.join(current_path,'../msg/notification_element.html'), mode="r", encoding="utf-8") as file:
element = file.read()
element = element.replace("[[color]]", message_type["color"])
element = element.replace("[[link]]", message_type["link"])
linktext = defs.email_credentials["url_template"] + str(notification.shipcall_id)
element = element.replace("[[link]]", linktext)
# We want to show the following information for each notification:
# Ship-name, Arr/Dep/Shift, ETA/ETD, berth
@ -180,7 +181,7 @@ def SendEmails(email_dict):
body = body.replace("[[NOTIFICATION_ELEMENTS]]", replacement)
msg.set_content(body, subtype='html', charset='utf-8', cte='8bit')
conn.sendmail(defs.email_credentials["sender"], user.user_email, msg.as_string())
conn.sendmail(defs.email_credentials["sender"], user_email, msg.as_string())
except Exception as ex:
logging.error(ex)
@ -210,10 +211,10 @@ def SendNotifications():
email_dict = dict()
users_dict = dict()
user_query = "SELECT * from user"
users = commands.query(user_query, model=model.User)
users = commands.query(user_query)
for participant in participants:
for user in users:
if user.participant_id == participant.id:
if user["participant_id"] == participant.id:
if not participant.id in users_dict:
users_dict[participant.id] = []
users_dict[participant.id].append(user)
@ -225,33 +226,39 @@ def SendNotifications():
p_query = "SELECT * from shipcall_participant_map where shipcall_id = ?id?"
assigned_participants = commands.query(p_query, model=model.ShipcallParticipantMap, param={"id":notification.shipcall_id})
for assigned_participant in assigned_participants:
if not assigned_participant.participant_id in users_dict:
continue
users = users_dict[assigned_participant.participant_id]
for user in users:
# send notification to user
if user.notify_email:
if user not in email_dict:
email_dict[user] = []
email_dict[user].append(notification)
if user.notify_whatsapp:
if user["notify_email"]:
if user["user_email"] not in email_dict:
email_dict[user["user_email"]] = []
if notification not in email_dict[user["user_email"]]:
email_dict[user["user_email"]].append(notification)
if user["notify_whatsapp"]:
# TBD
pass
if user.notify_signal:
if user["notify_signal"]:
# TBD
pass
else:
users = users_dict[notification.participant_id]
for user in users:
# send notification to user
if user.notify_email and user.wants_notifications(notification.type):
if user not in email_dict:
email_dict[user] = []
email_dict[user].append(notification)
if user.notify_whatsapp and user.wants_notifications(notification.type):
# TBD
pass
if user.notify_signal and user.wants_notifications(notification.type):
# TBD
pass
if notification.participant_id in users_dict:
users = users_dict[notification.participant_id]
for user in users:
user_notifications = model.bitflag_to_list(user["notify_event"])
# send notification to user
if user["notify_email"] and notification.type in user_notifications:
if user["user_email"] not in email_dict:
email_dict[user["user_email"]] = []
if notification not in email_dict[user["user_email"]]:
email_dict[user["user_email"]].append(notification)
if user["notify_whatsapp"] and notification.type in user_notifications:
# TBD
pass
if user["notify_signal"] and notification.type in user_notifications:
# TBD
pass
# mark as sent
commands.execute("UPDATE notification SET level = 2 WHERE id = ?id?", param={"id":notification.id})

View File

@ -0,0 +1,31 @@
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,10 +18,11 @@ 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.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.input_validation_base import InputValidationBase
import werkzeug
class InputValidationShip():
class InputValidationShip(InputValidationBase):
"""
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.
@ -55,6 +56,13 @@ class InputValidationShip():
# 3.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
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
@staticmethod
@ -70,6 +78,10 @@ class InputValidationShip():
# 4.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
InputValidationShip.optionally_evaluate_bollard_pull_value(content)
# 5.) Check if tug is null
InputValidationShip.check_is_tug_null(content)
return
@staticmethod
@ -159,5 +171,11 @@ class InputValidationShip():
raise ValidationError({"deleted":f"The selected ship entry is already deleted."})
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,12 +17,13 @@ from BreCal.database.sql_handler import get_assigned_participant_of_type
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_string_has_special_characters
from BreCal.validators.input_validation_base import InputValidationBase
from BreCal.database.sql_queries import SQLQuery
import werkzeug
class InputValidationShipcall():
class InputValidationShipcall(InputValidationBase):
"""
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.
@ -60,7 +61,11 @@ class InputValidationShipcall():
InputValidationShipcall.check_participant_list_not_empty_when_user_is_agency(loadedModel)
# check for reasonable values in the shipcall fields
InputValidationShipcall.check_shipcall_values(loadedModel, content, forbidden_keys=["evaluation", "evaluation_message"]) # "canceled"
InputValidationShipcall.check_shipcall_values(loadedModel, content, forbidden_keys=["evaluation", "evaluation_message", "canceled"]) # "canceled"
# check for deleted flag on POST
InputValidationShipcall.check_deleted_flag_on_post(content)
return
@staticmethod

View File

@ -17,6 +17,7 @@ from BreCal.database.sql_queries import SQLQuery
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_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
import werkzeug
@ -63,7 +64,7 @@ def build_post_data_type_dependent_required_fields_dict()->dict[ShipcallType,dic
class InputValidationTimes():
class InputValidationTimes(InputValidationBase):
"""
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.
@ -92,6 +93,10 @@ class InputValidationTimes():
# 4.) Value checking
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
@staticmethod

View File

@ -59,7 +59,7 @@ def create_validation_error_response(ex:ValidationError, status_code:int=400, cr
if create_log:
logging.warning(ex) if ex is not None else logging.warning(message)
print(ex) if ex is not None else print(message)
# print(ex) if ex is not None else print(message)
return (serialized_response, status_code)
def create_werkzeug_error_response(ex:Forbidden, status_code:int=403, create_log:bool=True)->typing.Tuple[str,int]:
@ -71,7 +71,7 @@ def create_werkzeug_error_response(ex:Forbidden, status_code:int=403, create_log
if create_log:
logging.warning(ex) if ex is not None else logging.warning(message)
print(ex) if ex is not None else print(message)
# print(ex) if ex is not None else print(message)
return serialized_response, status_code
def create_dynamic_exception_response(ex, status_code:int=400, message:typing.Optional[str]=None, create_log:bool=True):
@ -83,5 +83,5 @@ def create_dynamic_exception_response(ex, status_code:int=400, message:typing.Op
if create_log:
logging.warning(ex) if ex is not None else logging.warning(message)
print(ex) if ex is not None else print(message)
# print(ex) if ex is not None else print(message)
return (serialized_response, status_code)

View File

@ -52,7 +52,8 @@ class ValidationRules(ValidationRuleFunctions):
# 'translate' all error codes into readable, human-understandable format.
evaluation_results = [(state, self.describe_error_message(msg)) for (state, msg) in evaluation_results]
logging.info(f"Validation results for shipcall {shipcall.id}: {evaluation_results}")
if evaluation_results:
logging.info(f"Validation results for shipcall {shipcall.id}: {evaluation_results}")
# check, what the maximum state flag is and return it
evaluation_state = np.max(np.array([result[0].value for result in evaluation_results])) if len(evaluation_results)>0 else StatusFlags.GREEN.value

View File

@ -0,0 +1,40 @@
"""
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,20 +1,29 @@
import os
import sys
import logging
import os
import runpy
import sys
from pathlib import Path
sys.path.insert(0, '/var/www/brecal/src/server')
sys.path.insert(0, '/var/www/venv/lib/python3.12/site-packages/')
BASE_DIR = Path(__file__).resolve().parent
INSTANCE_DIR = BASE_DIR / "instance"
CONFIG_PATH = INSTANCE_DIR / "config.py"
import schedule
config = {}
if CONFIG_PATH.exists():
config = runpy.run_path(str(CONFIG_PATH))
# set the key
os.environ['SECRET_KEY'] = 'zdiTz8P3jXOc7jztIQAoelK4zztyuCpJ'
app_root = config.get("APP_ROOT", str(BASE_DIR))
site_packages = config.get("SITE_PACKAGES")
# Set up logging
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
sys.path.insert(0, app_root)
if site_packages:
sys.path.insert(0, site_packages)
# Set up Scheduled Jobs
if config.get("SECRET_KEY"):
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
application = create_app()
application = create_app(instance_path=config.get("INSTANCE_PATH"))