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

@ -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

@ -25,7 +25,7 @@ namespace BreCalClient.Properties {
[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"]));
@ -64,7 +64,7 @@ namespace BreCalClient.Properties {
[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"]));

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);
@ -465,8 +467,8 @@ namespace RoleEditor
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.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

@ -26,7 +26,7 @@ namespace RoleEditor.Properties {
[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>
@ -65,5 +66,10 @@ namespace brecal.model
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,7 +114,7 @@ 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";

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_path is None:
config_path = os.path.join(instancePath, f'../../../secure/{connection_filename}')
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)
# config_path = 'C:\\temp\\connection_data_test.json'
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(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

@ -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:

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"))