Compare commits
1 Commits
develop
...
v1.6.40_te
| Author | SHA1 | Date | |
|---|---|---|---|
| a8de64af11 |
@ -5008,7 +5008,7 @@ namespace BreCalClient.misc.Client
|
|||||||
{
|
{
|
||||||
Proxy = null;
|
Proxy = null;
|
||||||
UserAgent = WebUtility.UrlEncode("OpenAPI-Generator/1.0.0/csharp");
|
UserAgent = WebUtility.UrlEncode("OpenAPI-Generator/1.0.0/csharp");
|
||||||
BasePath = "https://brecaltest.bsmd-emswe.eu";
|
BasePath = "https://brecaldevel.bsmd-emswe.eu";
|
||||||
DefaultHeaders = new ConcurrentDictionary<string, string>();
|
DefaultHeaders = new ConcurrentDictionary<string, string>();
|
||||||
ApiKey = new ConcurrentDictionary<string, string>();
|
ApiKey = new ConcurrentDictionary<string, string>();
|
||||||
ApiKeyPrefix = new ConcurrentDictionary<string, string>();
|
ApiKeyPrefix = new ConcurrentDictionary<string, string>();
|
||||||
@ -5016,7 +5016,7 @@ namespace BreCalClient.misc.Client
|
|||||||
{
|
{
|
||||||
{
|
{
|
||||||
new Dictionary<string, object> {
|
new Dictionary<string, object> {
|
||||||
{"url", "https://brecaltest.bsmd-emswe.eu"},
|
{"url", "https://brecaldevel.bsmd-emswe.eu"},
|
||||||
{"description", "Development server hosted on vcup"},
|
{"description", "Development server hosted on vcup"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5035,7 +5035,7 @@ namespace BreCalClient.misc.Client
|
|||||||
IDictionary<string, string> defaultHeaders,
|
IDictionary<string, string> defaultHeaders,
|
||||||
IDictionary<string, string> apiKey,
|
IDictionary<string, string> apiKey,
|
||||||
IDictionary<string, string> apiKeyPrefix,
|
IDictionary<string, string> apiKeyPrefix,
|
||||||
string basePath = "https://brecaltest.bsmd-emswe.eu") : this()
|
string basePath = "https://brecaldevel.bsmd-emswe.eu") : this()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(basePath))
|
if (string.IsNullOrWhiteSpace(basePath))
|
||||||
throw new ArgumentException("The provided basePath is invalid.", "basePath");
|
throw new ArgumentException("The provided basePath is invalid.", "basePath");
|
||||||
|
|||||||
@ -14,7 +14,7 @@ info:
|
|||||||
name: Use at your own risk
|
name: Use at your own risk
|
||||||
url: 'https://www.bsmd.de/license'
|
url: 'https://www.bsmd.de/license'
|
||||||
servers:
|
servers:
|
||||||
- url: 'https://brecaltest.bsmd-emswe.eu'
|
- url: 'https://brecaldevel.bsmd-emswe.eu'
|
||||||
description: Development server hosted on vcup
|
description: Development server hosted on vcup
|
||||||
tags:
|
tags:
|
||||||
- name: user
|
- name: user
|
||||||
@ -658,12 +658,12 @@ paths:
|
|||||||
- notification
|
- notification
|
||||||
operationId: notificationsGet
|
operationId: notificationsGet
|
||||||
parameters:
|
parameters:
|
||||||
- name: participant_id
|
- name: shipcall_id
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
description: '**Id of participant**. *Example: 7*. Id of logged in participant.'
|
description: '**Id of ship call**. *Example: 52*. Id given in ship call list'
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/participant_id'
|
$ref: '#/components/schemas/shipcallId'
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: notification list
|
description: notification list
|
||||||
|
|||||||
153
misc/Readme.md
153
misc/Readme.md
@ -48,156 +48,3 @@ DROP TABLE IF EXISTS `shipcall`;
|
|||||||
SET FOREIGN_KEY_CHECKS = 1;
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Schema
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
erDiagram
|
|
||||||
participant {
|
|
||||||
INT id PK
|
|
||||||
VARCHAR name
|
|
||||||
VARCHAR street
|
|
||||||
VARCHAR postal_code
|
|
||||||
VARCHAR city
|
|
||||||
INT type
|
|
||||||
INT flags
|
|
||||||
}
|
|
||||||
port {
|
|
||||||
INT id PK
|
|
||||||
VARCHAR name
|
|
||||||
CHAR locode
|
|
||||||
}
|
|
||||||
berth {
|
|
||||||
INT id PK
|
|
||||||
VARCHAR name
|
|
||||||
BIT lock
|
|
||||||
INT owner_id FK
|
|
||||||
INT authority_id FK
|
|
||||||
INT port_id FK
|
|
||||||
BIT deleted
|
|
||||||
}
|
|
||||||
ship {
|
|
||||||
INT id PK
|
|
||||||
VARCHAR name
|
|
||||||
INT imo
|
|
||||||
VARCHAR callsign
|
|
||||||
INT participant_id FK
|
|
||||||
FLOAT length
|
|
||||||
FLOAT width
|
|
||||||
BIT is_tug
|
|
||||||
INT bollard_pull
|
|
||||||
INT eni
|
|
||||||
BIT deleted
|
|
||||||
}
|
|
||||||
shipcall {
|
|
||||||
INT id PK
|
|
||||||
INT ship_id FK
|
|
||||||
TINYINT type
|
|
||||||
DATETIME eta
|
|
||||||
DATETIME etd
|
|
||||||
INT arrival_berth_id FK
|
|
||||||
INT departure_berth_id FK
|
|
||||||
INT port_id FK
|
|
||||||
INT flags
|
|
||||||
BIT tug_required
|
|
||||||
BIT pilot_required
|
|
||||||
}
|
|
||||||
times {
|
|
||||||
INT id PK
|
|
||||||
INT shipcall_id FK
|
|
||||||
INT participant_id FK
|
|
||||||
INT berth_id FK
|
|
||||||
INT participant_type
|
|
||||||
DATETIME eta_berth
|
|
||||||
DATETIME etd_berth
|
|
||||||
DATETIME lock_time
|
|
||||||
DATETIME zone_entry
|
|
||||||
}
|
|
||||||
notification {
|
|
||||||
INT id PK
|
|
||||||
INT shipcall_id FK
|
|
||||||
INT participant_id FK
|
|
||||||
TINYINT level
|
|
||||||
TINYINT type
|
|
||||||
}
|
|
||||||
history {
|
|
||||||
INT id PK
|
|
||||||
INT participant_id FK
|
|
||||||
INT user_id FK
|
|
||||||
INT shipcall_id FK
|
|
||||||
DATETIME timestamp
|
|
||||||
DATETIME eta
|
|
||||||
INT type
|
|
||||||
INT operation
|
|
||||||
}
|
|
||||||
shipcall_participant_map {
|
|
||||||
INT id PK
|
|
||||||
INT shipcall_id FK
|
|
||||||
INT participant_id FK
|
|
||||||
INT type
|
|
||||||
}
|
|
||||||
shipcall_tug_map {
|
|
||||||
INT id PK
|
|
||||||
INT shipcall_id FK
|
|
||||||
INT ship_id FK
|
|
||||||
}
|
|
||||||
participant_port_map {
|
|
||||||
INT id PK
|
|
||||||
INT participant_id FK
|
|
||||||
INT port_id FK
|
|
||||||
}
|
|
||||||
user {
|
|
||||||
INT id PK
|
|
||||||
INT participant_id FK
|
|
||||||
VARCHAR first_name
|
|
||||||
VARCHAR last_name
|
|
||||||
VARCHAR user_name
|
|
||||||
VARCHAR user_email
|
|
||||||
}
|
|
||||||
role {
|
|
||||||
INT id PK
|
|
||||||
VARCHAR name
|
|
||||||
VARCHAR description
|
|
||||||
}
|
|
||||||
securable {
|
|
||||||
INT id PK
|
|
||||||
VARCHAR name
|
|
||||||
}
|
|
||||||
role_securable_map {
|
|
||||||
INT id PK
|
|
||||||
INT role_id FK
|
|
||||||
INT securable_id FK
|
|
||||||
}
|
|
||||||
user_role_map {
|
|
||||||
INT id PK
|
|
||||||
INT user_id FK
|
|
||||||
INT role_id FK
|
|
||||||
}
|
|
||||||
|
|
||||||
participant ||--o{ berth : owner_id
|
|
||||||
participant ||--o{ berth : authority_id
|
|
||||||
port ||--o{ berth : port_id
|
|
||||||
participant ||--o{ ship : participant_id
|
|
||||||
ship ||--o{ shipcall : ship_id
|
|
||||||
berth ||--o{ shipcall : arrival_berth_id
|
|
||||||
berth ||--o{ shipcall : departure_berth_id
|
|
||||||
port ||--o{ shipcall : port_id
|
|
||||||
shipcall ||--|| times : shipcall_id
|
|
||||||
participant ||--|| times : participant_id
|
|
||||||
berth ||--o{ times : berth_id
|
|
||||||
shipcall ||--o{ notification : shipcall_id
|
|
||||||
participant ||--o{ notification : participant_id
|
|
||||||
participant ||--o{ history : participant_id
|
|
||||||
user ||--o{ history : user_id
|
|
||||||
shipcall ||--o{ history : shipcall_id
|
|
||||||
shipcall ||--o{ shipcall_participant_map : shipcall_id
|
|
||||||
participant ||--o{ shipcall_participant_map : participant_id
|
|
||||||
shipcall ||--o{ shipcall_tug_map : shipcall_id
|
|
||||||
ship ||--o{ shipcall_tug_map : ship_id
|
|
||||||
participant ||--o{ participant_port_map : participant_id
|
|
||||||
port ||--o{ participant_port_map : port_id
|
|
||||||
participant ||--o{ user : participant_id
|
|
||||||
user ||--o{ user_role_map : user_id
|
|
||||||
role ||--o{ user_role_map : role_id
|
|
||||||
role ||--o{ role_securable_map : role_id
|
|
||||||
securable ||--o{ role_securable_map : securable_id
|
|
||||||
```
|
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
# Tests
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
## schemathesis
|
|
||||||
|
|
||||||
Unter dem Verzeichnis ./src/server/tests befinden sich die Test-Cases, die Max bei der ersten Implementierung der Validierung angelegt hat.
|
|
||||||
Diese sind derzeit nicht "aktiv", bzw. noch nicht bereinigt.
|
|
||||||
|
|
||||||
Ich möchte gern ein automatisches Framework verwenden, das aber nur "manuell" betrieben wird. Änderungen an der API sind selten.
|
|
||||||
|
|
||||||
Aktueller Stand (7.1.26):
|
|
||||||
|
|
||||||
Im Verzeichnis ./src/server/tests kann folgendes ausgeführt werden:
|
|
||||||
|
|
||||||
```pytest -q --maxfail=1 contract/test_openapi_fuzz.py```
|
|
||||||
|
|
||||||
Das Ganze funktioniert nur, wenn auch schemathesis und hypothesis in den passenden(!) Versionen im lokalen _venv_ installiert sind.
|
|
||||||
Aktuell habe ich schemathesis ("latest") und hypothesis 6.120.0:
|
|
||||||
```pip install "hypothesis==6.120.0"```
|
|
||||||
Das muss wegen dependencies so blöd gepinnt werden.
|
|
||||||
@ -68,25 +68,6 @@ CREATE TABLE `history` (
|
|||||||
) 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';
|
) 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 */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
CREATE TABLE `history` (
|
|
||||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
|
||||||
`participant_id` int unsigned NOT NULL,
|
|
||||||
`user_id` int unsigned DEFAULT NULL,
|
|
||||||
`shipcall_id` int unsigned NOT NULL,
|
|
||||||
`timestamp` datetime NOT NULL COMMENT 'Time of saving',
|
|
||||||
`eta` datetime DEFAULT NULL COMMENT 'Current ETA / ETD value (depends if shipcall or times were saved)',
|
|
||||||
`type` int NOT NULL COMMENT 'shipcall or times',
|
|
||||||
`operation` int NOT NULL COMMENT 'insert, update or delete',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `FK_HISTORY_PARTICIPANT_idx` (`participant_id`),
|
|
||||||
KEY `FK_HISTORY_SHIPCALL_idx` (`shipcall_id`),
|
|
||||||
KEY `FK_HISTORY_USER` (`user_id`),
|
|
||||||
CONSTRAINT `FK_HISTORY_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`),
|
|
||||||
CONSTRAINT `FK_HISTORY_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`),
|
|
||||||
CONSTRAINT `FK_HISTORY_USER` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
|
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=29292 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='This table stores a history of changes made to shipcalls so that everyone can see who changed what and when';
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `notification`
|
-- Table structure for table `notification`
|
||||||
--
|
--
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
<applicationSettings>
|
<applicationSettings>
|
||||||
<BreCalClient.Properties.Settings>
|
<BreCalClient.Properties.Settings>
|
||||||
<setting name="BG_COLOR" serializeAs="String">
|
<setting name="BG_COLOR" serializeAs="String">
|
||||||
<value>#751D1F</value>
|
<value>#1D751F</value>
|
||||||
</setting>
|
</setting>
|
||||||
<setting name="APP_TITLE" serializeAs="String">
|
<setting name="APP_TITLE" serializeAs="String">
|
||||||
<value>!!Bremen calling Entwicklungsversion!!</value>
|
<value>!!Bremen calling Entwicklungsversion!!</value>
|
||||||
@ -38,7 +38,7 @@
|
|||||||
<value>https://www.textbausteine.net/</value>
|
<value>https://www.textbausteine.net/</value>
|
||||||
</setting>
|
</setting>
|
||||||
<setting name="API_URL" serializeAs="String">
|
<setting name="API_URL" serializeAs="String">
|
||||||
<value>https://brecaltest.bsmd-emswe.eu</value>
|
<value>https://brecaldevel.bsmd-emswe.eu</value>
|
||||||
</setting>
|
</setting>
|
||||||
</BreCalClient.Properties.Settings>
|
</BreCalClient.Properties.Settings>
|
||||||
</applicationSettings>
|
</applicationSettings>
|
||||||
|
|||||||
@ -35,7 +35,7 @@ namespace BreCalClient
|
|||||||
this.ContentWrapper.Background = Brushes.Gray;
|
this.ContentWrapper.Background = Brushes.Gray;
|
||||||
break;
|
break;
|
||||||
case "MissingData":
|
case "MissingData":
|
||||||
this.ContentWrapper.Background = Brushes.DarkKhaki;
|
this.ContentWrapper.Background= Brushes.Yellow;
|
||||||
break;
|
break;
|
||||||
case "Cancelled":
|
case "Cancelled":
|
||||||
this.ContentWrapper.Background = Brushes.DarkGray;
|
this.ContentWrapper.Background = Brushes.DarkGray;
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
<Title>Bremen calling client</Title>
|
<Title>Bremen calling client</Title>
|
||||||
<Description>A Windows WPF client for the Bremen calling API.</Description>
|
<Description>A Windows WPF client for the Bremen calling API.</Description>
|
||||||
<ApplicationIcon>containership.ico</ApplicationIcon>
|
<ApplicationIcon>containership.ico</ApplicationIcon>
|
||||||
<AssemblyName>BreCalTestClient</AssemblyName>
|
<AssemblyName>BreCalDevelClient</AssemblyName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -4,8 +4,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
-->
|
-->
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ApplicationRevision>5</ApplicationRevision>
|
<ApplicationRevision>0</ApplicationRevision>
|
||||||
<ApplicationVersion>1.7.0.7</ApplicationVersion>
|
<ApplicationVersion>1.6.0.3</ApplicationVersion>
|
||||||
<BootstrapperEnabled>True</BootstrapperEnabled>
|
<BootstrapperEnabled>True</BootstrapperEnabled>
|
||||||
<Configuration>Debug</Configuration>
|
<Configuration>Debug</Configuration>
|
||||||
<CreateDesktopShortcut>True</CreateDesktopShortcut>
|
<CreateDesktopShortcut>True</CreateDesktopShortcut>
|
||||||
@ -27,10 +27,10 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<SelfContained>True</SelfContained>
|
<SelfContained>True</SelfContained>
|
||||||
<SignatureAlgorithm>(none)</SignatureAlgorithm>
|
<SignatureAlgorithm>(none)</SignatureAlgorithm>
|
||||||
<SignManifests>False</SignManifests>
|
<SignManifests>False</SignManifests>
|
||||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
<TargetFramework>net6.0-windows</TargetFramework>
|
||||||
<UpdateEnabled>True</UpdateEnabled>
|
<UpdateEnabled>True</UpdateEnabled>
|
||||||
<UpdateMode>Foreground</UpdateMode>
|
<UpdateMode>Foreground</UpdateMode>
|
||||||
<UpdateRequired>True</UpdateRequired>
|
<UpdateRequired>False</UpdateRequired>
|
||||||
<WebPageFileName>Publish.html</WebPageFileName>
|
<WebPageFileName>Publish.html</WebPageFileName>
|
||||||
<CreateDesktopShortcut>True</CreateDesktopShortcut>
|
<CreateDesktopShortcut>True</CreateDesktopShortcut>
|
||||||
<ErrorReportUrl>https://www.bsmd-emswe.eu/</ErrorReportUrl>
|
<ErrorReportUrl>https://www.bsmd-emswe.eu/</ErrorReportUrl>
|
||||||
@ -38,10 +38,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<PublisherName>Informatikbüro Daniel Schick</PublisherName>
|
<PublisherName>Informatikbüro Daniel Schick</PublisherName>
|
||||||
<SuiteName>Bremen Calling</SuiteName>
|
<SuiteName>Bremen Calling</SuiteName>
|
||||||
<SupportUrl>http://www.textbausteine.net/</SupportUrl>
|
<SupportUrl>http://www.textbausteine.net/</SupportUrl>
|
||||||
<PublishDir>bin\Debug\net8.0-windows7.0\win-x64\app.publish\</PublishDir>
|
<PublishDir>bin\Debug\net6.0-windows\win-x64\app.publish\</PublishDir>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<SkipPublishVerification>false</SkipPublishVerification>
|
|
||||||
<MinimumRequiredVersion>1.7.0.7</MinimumRequiredVersion>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PublishFile Include="containership.ico">
|
<PublishFile Include="containership.ico">
|
||||||
|
|||||||
4
src/BreCalClient/Properties/Settings.Designer.cs
generated
4
src/BreCalClient/Properties/Settings.Designer.cs
generated
@ -25,7 +25,7 @@ namespace BreCalClient.Properties {
|
|||||||
|
|
||||||
[global::System.Configuration.ApplicationScopedSettingAttribute()]
|
[global::System.Configuration.ApplicationScopedSettingAttribute()]
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
[global::System.Configuration.DefaultSettingValueAttribute("#751D1F")]
|
[global::System.Configuration.DefaultSettingValueAttribute("#1D751F")]
|
||||||
public string BG_COLOR {
|
public string BG_COLOR {
|
||||||
get {
|
get {
|
||||||
return ((string)(this["BG_COLOR"]));
|
return ((string)(this["BG_COLOR"]));
|
||||||
@ -64,7 +64,7 @@ namespace BreCalClient.Properties {
|
|||||||
|
|
||||||
[global::System.Configuration.ApplicationScopedSettingAttribute()]
|
[global::System.Configuration.ApplicationScopedSettingAttribute()]
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
[global::System.Configuration.DefaultSettingValueAttribute("https://brecaltest.bsmd-emswe.eu")]
|
[global::System.Configuration.DefaultSettingValueAttribute("https://brecaldevel.bsmd-emswe.eu")]
|
||||||
public string API_URL {
|
public string API_URL {
|
||||||
get {
|
get {
|
||||||
return ((string)(this["API_URL"]));
|
return ((string)(this["API_URL"]));
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<Profiles />
|
<Profiles />
|
||||||
<Settings>
|
<Settings>
|
||||||
<Setting Name="BG_COLOR" Type="System.String" Scope="Application">
|
<Setting Name="BG_COLOR" Type="System.String" Scope="Application">
|
||||||
<Value Profile="(Default)">#751D1F</Value>
|
<Value Profile="(Default)">#1D751F</Value>
|
||||||
</Setting>
|
</Setting>
|
||||||
<Setting Name="APP_TITLE" Type="System.String" Scope="Application">
|
<Setting Name="APP_TITLE" Type="System.String" Scope="Application">
|
||||||
<Value Profile="(Default)">!!Bremen calling Entwicklungsversion!!</Value>
|
<Value Profile="(Default)">!!Bremen calling Entwicklungsversion!!</Value>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<Value Profile="(Default)" />
|
<Value Profile="(Default)" />
|
||||||
</Setting>
|
</Setting>
|
||||||
<Setting Name="API_URL" Type="System.String" Scope="Application">
|
<Setting Name="API_URL" Type="System.String" Scope="Application">
|
||||||
<Value Profile="(Default)">https://brecaltest.bsmd-emswe.eu</Value>
|
<Value Profile="(Default)">https://brecaldevel.bsmd-emswe.eu</Value>
|
||||||
</Setting>
|
</Setting>
|
||||||
<Setting Name="Width" Type="System.Double" Scope="User">
|
<Setting Name="Width" Type="System.Double" Scope="User">
|
||||||
<Value Profile="(Default)">800</Value>
|
<Value Profile="(Default)">800</Value>
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:p = "clr-namespace:BreCalClient.Resources"
|
xmlns:p = "clr-namespace:BreCalClient.Resources"
|
||||||
xmlns:sets="clr-namespace:BreCalClient.Properties"
|
xmlns:sets="clr-namespace:BreCalClient.Properties"
|
||||||
xmlns:db="clr-namespace:BreCalClient;assembly=BreCalTestClient"
|
xmlns:db="clr-namespace:BreCalClient;assembly=BreCalDevelClient"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignHeight="135" d:DesignWidth="800">
|
d:DesignHeight="135" d:DesignWidth="800">
|
||||||
<Border BorderBrush="LightGray" Margin="1" BorderThickness="1">
|
<Border BorderBrush="LightGray" Margin="1" BorderThickness="1">
|
||||||
|
|||||||
@ -215,13 +215,13 @@ namespace BreCalClient
|
|||||||
switch (this.ShipcallControlModel?.Shipcall?.Type)
|
switch (this.ShipcallControlModel?.Shipcall?.Type)
|
||||||
{
|
{
|
||||||
case ShipcallType.Arrival: // incoming
|
case ShipcallType.Arrival: // incoming
|
||||||
this.imageShipcallType.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalTestClient;component/Resources/arrow_down_red.png"));
|
this.imageShipcallType.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalDevelClient;component/Resources/arrow_down_red.png"));
|
||||||
break;
|
break;
|
||||||
case ShipcallType.Departure: // outgoing
|
case ShipcallType.Departure: // outgoing
|
||||||
this.imageShipcallType.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalTestClient;component/Resources/arrow_up_blue.png"));
|
this.imageShipcallType.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalDevelClient;component/Resources/arrow_up_blue.png"));
|
||||||
break;
|
break;
|
||||||
case ShipcallType.Shifting: // shifting
|
case ShipcallType.Shifting: // shifting
|
||||||
this.imageShipcallType.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalTestClient;component/Resources/arrow_right_green.png"));
|
this.imageShipcallType.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalDevelClient;component/Resources/arrow_right_green.png"));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@ -230,13 +230,13 @@ namespace BreCalClient
|
|||||||
switch(this.ShipcallControlModel?.LightMode)
|
switch(this.ShipcallControlModel?.LightMode)
|
||||||
{
|
{
|
||||||
case EvaluationType.Green:
|
case EvaluationType.Green:
|
||||||
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalTestClient;component/Resources/check.png"));
|
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalDevelClient;component/Resources/check.png"));
|
||||||
break;
|
break;
|
||||||
case EvaluationType.Yellow:
|
case EvaluationType.Yellow:
|
||||||
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalTestClient;component/Resources/sign_warning.png"));
|
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalDevelClient;component/Resources/sign_warning.png"));
|
||||||
break;
|
break;
|
||||||
case EvaluationType.Red:
|
case EvaluationType.Red:
|
||||||
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalTestClient;component/Resources/delete2.png"));
|
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalDevelClient;component/Resources/delete2.png"));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<applicationSettings>
|
<applicationSettings>
|
||||||
<RoleEditor.Properties.Settings>
|
<RoleEditor.Properties.Settings>
|
||||||
<setting name="ConnectionString" serializeAs="String">
|
<setting name="ConnectionString" serializeAs="String">
|
||||||
<value>Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_test;Port=33307</value>
|
<value>Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_devel;Port=33306</value>
|
||||||
</setting>
|
</setting>
|
||||||
</RoleEditor.Properties.Settings>
|
</RoleEditor.Properties.Settings>
|
||||||
</applicationSettings>
|
</applicationSettings>
|
||||||
|
|||||||
@ -59,7 +59,7 @@
|
|||||||
<Label Content="Street" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right"/>
|
<Label Content="Street" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right"/>
|
||||||
<Label Content="Postal code" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Right"/>
|
<Label Content="Postal code" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Right"/>
|
||||||
<Label Content="City" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Right"/>
|
<Label Content="City" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Right"/>
|
||||||
<Label Content="Deleted" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Right"/>
|
<Label Content="Active" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Right"/>
|
||||||
<Label Content="Type" Grid.Row="5" Grid.Column="1" HorizontalAlignment="Right"/>
|
<Label Content="Type" Grid.Row="5" Grid.Column="1" HorizontalAlignment="Right"/>
|
||||||
<Label Content="Created" Grid.Row="6" Grid.Column="1" HorizontalAlignment="Right"/>
|
<Label Content="Created" Grid.Row="6" Grid.Column="1" HorizontalAlignment="Right"/>
|
||||||
<Label Content="Modified" Grid.Row="7" Grid.Column="1" HorizontalAlignment="Right"/>
|
<Label Content="Modified" Grid.Row="7" Grid.Column="1" HorizontalAlignment="Right"/>
|
||||||
@ -67,7 +67,7 @@
|
|||||||
<TextBox x:Name="textBoxParticipantStreet" Grid.Row="1" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
|
<TextBox x:Name="textBoxParticipantStreet" Grid.Row="1" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
|
||||||
<TextBox x:Name="textBoxParticipantPostalCode" Grid.Row="2" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
|
<TextBox x:Name="textBoxParticipantPostalCode" Grid.Row="2" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
|
||||||
<TextBox x:Name="textBoxParticipantCity" Grid.Row="3" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
|
<TextBox x:Name="textBoxParticipantCity" Grid.Row="3" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
|
||||||
<CheckBox x:Name="checkboxParticipantDeleted" Grid.Row="4" Grid.Column="2" VerticalAlignment="Center" IsEnabled="False" />
|
<CheckBox x:Name="checkboxParticipantActive" Grid.Row="4" Grid.Column="2" VerticalAlignment="Center" />
|
||||||
<xctk:CheckComboBox x:Name="comboBoxParticipantType" Grid.Row="5" Grid.Column="2" Margin="2" SelectedValue="Key" DisplayMemberPath="Value" />
|
<xctk:CheckComboBox x:Name="comboBoxParticipantType" Grid.Row="5" Grid.Column="2" Margin="2" SelectedValue="Key" DisplayMemberPath="Value" />
|
||||||
<TextBox x:Name="textBoxParticipantCreated" Grid.Row="6" IsReadOnly="True" IsEnabled="False" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
|
<TextBox x:Name="textBoxParticipantCreated" Grid.Row="6" IsReadOnly="True" IsEnabled="False" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
|
||||||
<StackPanel Orientation="Horizontal" Grid.Row="7" Grid.Column="0">
|
<StackPanel Orientation="Horizontal" Grid.Row="7" Grid.Column="0">
|
||||||
@ -167,7 +167,7 @@
|
|||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<ListBox x:Name="listBoxUser" Margin="2" Grid.RowSpan="9" SelectionChanged="listBoxUser_SelectionChanged">
|
<ListBox x:Name="listBoxUser" Margin="2" Grid.RowSpan="9" SelectionChanged="listBoxUser_SelectionChanged">
|
||||||
<ListBox.ContextMenu>
|
<ListBox.ContextMenu>
|
||||||
<ContextMenu Name="contextMenuUser">
|
<ContextMenu>
|
||||||
<MenuItem x:Name="menuItemNewUser" Header="New.." Click="menuItemNewUser_Click">
|
<MenuItem x:Name="menuItemNewUser" Header="New.." Click="menuItemNewUser_Click">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<Image Source="Resources/add.png" />
|
<Image Source="Resources/add.png" />
|
||||||
|
|||||||
@ -61,8 +61,6 @@ namespace RoleEditor
|
|||||||
|
|
||||||
// load all participants
|
// load all participants
|
||||||
List<Participant> participants = await Participant.LoadAll(_dbManager);
|
List<Participant> participants = await Participant.LoadAll(_dbManager);
|
||||||
participants.Sort((x, y) => string.Compare(x.Name, y.Name));
|
|
||||||
|
|
||||||
foreach (Participant p in participants)
|
foreach (Participant p in participants)
|
||||||
{
|
{
|
||||||
_participants.Add(p);
|
_participants.Add(p);
|
||||||
@ -467,8 +465,8 @@ namespace RoleEditor
|
|||||||
this.textBoxParticipantStreet.Text = (p != null) ? p.Street : string.Empty;
|
this.textBoxParticipantStreet.Text = (p != null) ? p.Street : string.Empty;
|
||||||
this.textBoxParticipantPostalCode.Text = (p != null) ? p.PostalCode : string.Empty;
|
this.textBoxParticipantPostalCode.Text = (p != null) ? p.PostalCode : string.Empty;
|
||||||
this.textBoxParticipantCity.Text = (p != null) ? p.City : string.Empty;
|
this.textBoxParticipantCity.Text = (p != null) ? p.City : string.Empty;
|
||||||
|
// this.checkboxParticipantActive.Checked = (p != null) ? p.
|
||||||
this.textBoxParticipantCreated.Text = (p != null) ? p.Created.ToString() : string.Empty;
|
this.textBoxParticipantCreated.Text = (p != null) ? p.Created.ToString() : string.Empty;
|
||||||
this.checkboxParticipantDeleted.IsChecked = (p != null) ? p.Deleted : null;
|
|
||||||
this.textBoxParticipantModified.Text = (p != null) ? p.Modified.ToString() : string.Empty;
|
this.textBoxParticipantModified.Text = (p != null) ? p.Modified.ToString() : string.Empty;
|
||||||
this.checkBoxParticipantAllowBSMD.IsChecked = (p != null) ? p.IsFlagSet(Participant.ParticipantFlags.ALLOW_BSMD) : null;
|
this.checkBoxParticipantAllowBSMD.IsChecked = (p != null) ? p.IsFlagSet(Participant.ParticipantFlags.ALLOW_BSMD) : null;
|
||||||
this.comboBoxParticipantType.SelectedItems.Clear();
|
this.comboBoxParticipantType.SelectedItems.Clear();
|
||||||
@ -512,11 +510,6 @@ namespace RoleEditor
|
|||||||
_assignedPorts.Add(pa);
|
_assignedPorts.Add(pa);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.contextMenuUser.IsEnabled = !p.Deleted;
|
|
||||||
this.buttonAddPortAssignment.IsEnabled = !p.Deleted;
|
|
||||||
this.buttonRemovePortAssignment.IsEnabled = !p.Deleted;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void listBoxRoles_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
private async void listBoxRoles_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
@ -601,7 +594,7 @@ namespace RoleEditor
|
|||||||
if(this.listBoxParticipant.SelectedItem is Participant p)
|
if(this.listBoxParticipant.SelectedItem is Participant p)
|
||||||
{
|
{
|
||||||
await p.Delete(_dbManager);
|
await p.Delete(_dbManager);
|
||||||
p.Deleted = true;
|
this._participants.Remove(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -635,7 +628,6 @@ namespace RoleEditor
|
|||||||
{
|
{
|
||||||
if (this.listBoxUser.SelectedItem is User u)
|
if (this.listBoxUser.SelectedItem is User u)
|
||||||
{
|
{
|
||||||
await u.ExecuteNonQuery(_dbManager); // extra history delete happens here
|
|
||||||
await u.Delete(_dbManager);
|
await u.Delete(_dbManager);
|
||||||
this._users.Remove(u);
|
this._users.Remove(u);
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/RoleEditor/Properties/Settings.Designer.cs
generated
2
src/RoleEditor/Properties/Settings.Designer.cs
generated
@ -26,7 +26,7 @@ namespace RoleEditor.Properties {
|
|||||||
[global::System.Configuration.ApplicationScopedSettingAttribute()]
|
[global::System.Configuration.ApplicationScopedSettingAttribute()]
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
[global::System.Configuration.DefaultSettingValueAttribute("Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_" +
|
[global::System.Configuration.DefaultSettingValueAttribute("Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_" +
|
||||||
"test;Port=33306")]
|
"devel;Port=33306")]
|
||||||
public string ConnectionString {
|
public string ConnectionString {
|
||||||
get {
|
get {
|
||||||
return ((string)(this["ConnectionString"]));
|
return ((string)(this["ConnectionString"]));
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<Profiles />
|
<Profiles />
|
||||||
<Settings>
|
<Settings>
|
||||||
<Setting Name="ConnectionString" Type="System.String" Scope="Application">
|
<Setting Name="ConnectionString" Type="System.String" Scope="Application">
|
||||||
<Value Profile="(Default)">Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_test;Port=33306</Value>
|
<Value Profile="(Default)">Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_devel;Port=33306</Value>
|
||||||
</Setting>
|
</Setting>
|
||||||
</Settings>
|
</Settings>
|
||||||
</SettingsFile>
|
</SettingsFile>
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
<TargetFramework>net6.0-windows</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<ApplicationIcon>Resources\lock_preferences.ico</ApplicationIcon>
|
<ApplicationIcon>Resources\lock_preferences.ico</ApplicationIcon>
|
||||||
@ -30,8 +30,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
<PackageReference Include="ExcelDataReader" Version="3.8.0" />
|
<PackageReference Include="ExcelDataReader" Version="3.7.0-develop00385" />
|
||||||
<PackageReference Include="Extended.Wpf.Toolkit" Version="5.0.0" />
|
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
using System.Data;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace brecal.model
|
namespace brecal.model
|
||||||
@ -38,11 +42,6 @@ namespace brecal.model
|
|||||||
/// <param name="cmd">CMD created by DB manager</param>
|
/// <param name="cmd">CMD created by DB manager</param>
|
||||||
public abstract void SetDelete(IDbCommand cmd);
|
public abstract void SetDelete(IDbCommand cmd);
|
||||||
|
|
||||||
public virtual void SetNonQuery(IDbCommand cmd)
|
|
||||||
{
|
|
||||||
// default: do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Each database entity must be able to save itself to the database
|
/// Each database entity must be able to save itself to the database
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -66,10 +65,5 @@ namespace brecal.model
|
|||||||
await manager.ExecuteNonQuery(this.SetDelete);
|
await manager.ExecuteNonQuery(this.SetDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExecuteNonQuery(IDBManager manager)
|
|
||||||
{
|
|
||||||
await manager.ExecuteNonQuery(this.SetNonQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,8 +55,6 @@ namespace brecal.model
|
|||||||
|
|
||||||
public uint Flags { get; set; }
|
public uint Flags { get; set; }
|
||||||
|
|
||||||
public bool Deleted { get; set; } = false;
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region public static methods
|
#region public static methods
|
||||||
@ -85,7 +83,6 @@ namespace brecal.model
|
|||||||
if (!reader.IsDBNull(6)) p.Flags = (uint)reader.GetInt32(6);
|
if (!reader.IsDBNull(6)) p.Flags = (uint)reader.GetInt32(6);
|
||||||
if (!reader.IsDBNull(7)) p.Created = reader.GetDateTime(7);
|
if (!reader.IsDBNull(7)) p.Created = reader.GetDateTime(7);
|
||||||
if (!reader.IsDBNull(8)) p.Modified = reader.GetDateTime(8);
|
if (!reader.IsDBNull(8)) p.Modified = reader.GetDateTime(8);
|
||||||
if (!reader.IsDBNull(9)) p.Deleted = reader.GetBoolean(9);
|
|
||||||
result.Add(p);
|
result.Add(p);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@ -93,7 +90,7 @@ namespace brecal.model
|
|||||||
|
|
||||||
public static void SetLoadQuery(IDbCommand cmd, params object?[] list)
|
public static void SetLoadQuery(IDbCommand cmd, params object?[] list)
|
||||||
{
|
{
|
||||||
cmd.CommandText = "SELECT id, name, street, postal_code, city, type, flags, created, modified, deleted FROM participant";
|
cmd.CommandText = "SELECT id, name, street, postal_code, city, type, flags, created, modified FROM participant";
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -114,7 +111,7 @@ namespace brecal.model
|
|||||||
|
|
||||||
public override void SetDelete(IDbCommand cmd)
|
public override void SetDelete(IDbCommand cmd)
|
||||||
{
|
{
|
||||||
cmd.CommandText = "UPDATE participant SET deleted = 1 WHERE id = @ID";
|
cmd.CommandText = "DELETE FROM participant WHERE id = @ID";
|
||||||
|
|
||||||
IDataParameter idParam = cmd.CreateParameter();
|
IDataParameter idParam = cmd.CreateParameter();
|
||||||
idParam.ParameterName = "ID";
|
idParam.ParameterName = "ID";
|
||||||
|
|||||||
@ -101,16 +101,6 @@ namespace brecal.model
|
|||||||
return this.Username ?? $"{base.Id} - {this.GetType().Name}";
|
return this.Username ?? $"{base.Id} - {this.GetType().Name}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void SetNonQuery(IDbCommand cmd)
|
|
||||||
{
|
|
||||||
cmd.CommandText = "UPDATE history set user_id = NULL WHERE user_id = @ID";
|
|
||||||
|
|
||||||
IDataParameter idParam = cmd.CreateParameter();
|
|
||||||
idParam.ParameterName = "ID";
|
|
||||||
idParam.Value = this.Id;
|
|
||||||
cmd.Parameters.Add(idParam);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region private methods
|
#region private methods
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MySqlConnector" Version="2.4.0" />
|
<PackageReference Include="MySqlConnector" Version="2.3.0-beta.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import logging
|
import logging
|
||||||
from . import local_db
|
from . import local_db
|
||||||
|
|
||||||
@ -37,6 +36,7 @@ from BreCal.stubs.df_times import get_df_times
|
|||||||
from BreCal.services.schedule_routines import setup_schedule, run_schedule_permanently_in_background
|
from BreCal.services.schedule_routines import setup_schedule, run_schedule_permanently_in_background
|
||||||
|
|
||||||
def create_app(test_config=None, instance_path=None):
|
def create_app(test_config=None, instance_path=None):
|
||||||
|
|
||||||
app = Flask(__name__, instance_relative_config=True)
|
app = Flask(__name__, instance_relative_config=True)
|
||||||
app.config.from_mapping(
|
app.config.from_mapping(
|
||||||
SECRET_KEY='dev'
|
SECRET_KEY='dev'
|
||||||
@ -48,8 +48,6 @@ def create_app(test_config=None, instance_path=None):
|
|||||||
|
|
||||||
if instance_path is not None:
|
if instance_path is not None:
|
||||||
app.instance_path = instance_path
|
app.instance_path = instance_path
|
||||||
elif app.config.get("INSTANCE_PATH"):
|
|
||||||
app.instance_path = app.config["INSTANCE_PATH"]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import os
|
import os
|
||||||
@ -71,23 +69,13 @@ def create_app(test_config=None, instance_path=None):
|
|||||||
app.register_blueprint(history.bp)
|
app.register_blueprint(history.bp)
|
||||||
app.register_blueprint(ports.bp)
|
app.register_blueprint(ports.bp)
|
||||||
|
|
||||||
log_level = getattr(logging, app.config.get("LOG_LEVEL", "DEBUG"))
|
logging.basicConfig(filename='brecal.log', level=logging.WARNING, format='%(asctime)s | %(name)s | %(levelname)s | %(message)s')
|
||||||
log_kwargs = {"format": "%(asctime)s | %(name)s | %(levelname)s | %(message)s"}
|
local_db.initPool(os.path.dirname(app.instance_path))
|
||||||
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')
|
logging.info('App started')
|
||||||
|
|
||||||
# Setup Routine jobs (e.g., reevaluation of shipcalls)
|
# Setup Routine jobs (e.g., reevaluation of shipcalls)
|
||||||
setup_schedule(update_shipcalls_interval_in_minutes=app.config.get("SCHEDULE_UPDATE_SHIPCALLS_MINUTES", 60))
|
setup_schedule(update_shipcalls_interval_in_minutes=60)
|
||||||
run_schedule_permanently_in_background(latency=app.config.get("SCHEDULE_BACKGROUND_LATENCY_SECONDS", 30))
|
run_schedule_permanently_in_background(latency=30)
|
||||||
logging.info('Routine Jobs are defined.')
|
logging.info('Routine Jobs are defined.')
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@ -14,14 +14,7 @@ def GetNotifications():
|
|||||||
try:
|
try:
|
||||||
if 'Authorization' in request.headers:
|
if 'Authorization' in request.headers:
|
||||||
token = request.headers.get('Authorization')
|
token = request.headers.get('Authorization')
|
||||||
participant_id = None
|
return impl.notifications.GetNotifications(token)
|
||||||
if 'participant_id' in request.args:
|
|
||||||
try:
|
|
||||||
participant_id = int(request.args.get('participant_id'))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return create_dynamic_exception_response(ex=None, status_code=400, message="participant_id must be an integer")
|
|
||||||
|
|
||||||
return impl.notifications.GetNotifications(token, participant_id=participant_id)
|
|
||||||
else:
|
else:
|
||||||
return create_dynamic_exception_response(ex=None, status_code=403, message="not authenticated")
|
return create_dynamic_exception_response(ex=None, status_code=403, message="not authenticated")
|
||||||
|
|
||||||
|
|||||||
@ -7,17 +7,12 @@ from marshmallow import ValidationError
|
|||||||
from . import verify_if_request_is_json
|
from . import verify_if_request_is_json
|
||||||
from BreCal.validators.validation_error import create_dynamic_exception_response, create_validation_error_response
|
from BreCal.validators.validation_error import create_dynamic_exception_response, create_validation_error_response
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
bp = Blueprint('user', __name__)
|
bp = Blueprint('user', __name__)
|
||||||
|
|
||||||
@bp.route('/user', methods=['put'])
|
@bp.route('/user', methods=['put'])
|
||||||
@auth_guard() # no restriction by role
|
@auth_guard() # no restriction by role
|
||||||
def PutUser():
|
def PutUser():
|
||||||
|
|
||||||
content = None
|
|
||||||
try:
|
try:
|
||||||
verify_if_request_is_json(request)
|
verify_if_request_is_json(request)
|
||||||
|
|
||||||
@ -26,11 +21,9 @@ def PutUser():
|
|||||||
return impl.user.PutUser(loadedModel)
|
return impl.user.PutUser(loadedModel)
|
||||||
|
|
||||||
except ValidationError as ex:
|
except ValidationError as ex:
|
||||||
logging.warning("UserSchema validation failed. Payload=%s", json.dumps(content, default=str))
|
|
||||||
return create_validation_error_response(ex=ex, status_code=400)
|
return create_validation_error_response(ex=ex, status_code=400)
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.error("UserSchema load failed. Payload=%s\n%s", json.dumps(content, default=str), traceback.format_exc())
|
|
||||||
return create_dynamic_exception_response(ex=None, status_code=400, message="bad format")
|
return create_dynamic_exception_response(ex=None, status_code=400, message="bad format")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -281,8 +281,10 @@ class SQLQuery():
|
|||||||
|
|
||||||
def get_next24hrs_shipcalls()->str:
|
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 " + \
|
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 (COALESCE(t.eta_berth, eta) >= NOW() AND COALESCE(t.eta_berth, eta) < (NOW() + INTERVAL 1 DAY)))" + \
|
"WHERE (type = 1 AND ((t.id IS NOT NULL AND t.eta_berth >= NOW() AND t.eta_berth < (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)))"
|
"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))))" + \
|
||||||
"AND s.canceled = 0")
|
"AND s.canceled = 0")
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,6 @@ from .. import local_db
|
|||||||
from ..services import jwt_handler
|
from ..services import jwt_handler
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def GetUser(options):
|
def GetUser(options):
|
||||||
|
|
||||||
pooledConnection = None
|
pooledConnection = None
|
||||||
@ -39,7 +38,7 @@ def GetUser(options):
|
|||||||
"notify_whatsapp": data[0].notify_whatsapp,
|
"notify_whatsapp": data[0].notify_whatsapp,
|
||||||
"notify_signal": data[0].notify_signal,
|
"notify_signal": data[0].notify_signal,
|
||||||
"notify_popup": data[0].notify_popup,
|
"notify_popup": data[0].notify_popup,
|
||||||
"notify_on": model.notification_types_to_names(model.bitflag_to_list(data[0].notify_event))
|
"notify_on": model.bitflag_to_list(data[0].notify_event)
|
||||||
}
|
}
|
||||||
token = jwt_handler.generate_jwt(payload=result, lifetime=120) # generate token valid 60 mins
|
token = jwt_handler.generate_jwt(payload=result, lifetime=120) # generate token valid 60 mins
|
||||||
result["token"] = token # add token to user data
|
result["token"] = token # add token to user data
|
||||||
|
|||||||
@ -6,22 +6,17 @@ from ..schemas import model
|
|||||||
from .. import local_db
|
from .. import local_db
|
||||||
from BreCal.database.sql_queries import SQLQuery
|
from BreCal.database.sql_queries import SQLQuery
|
||||||
|
|
||||||
def GetNotifications(token, participant_id=None):
|
def GetNotifications(token):
|
||||||
"""
|
"""
|
||||||
Optional filtering by participant_id. Returns delivered (level=2) notifications.
|
No parameters, gets all entries
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pooledConnection = None
|
pooledConnection = None
|
||||||
try:
|
try:
|
||||||
pooledConnection = local_db.getPoolConnection()
|
pooledConnection = local_db.getPoolConnection()
|
||||||
commands = pydapper.using(pooledConnection)
|
commands = pydapper.using(pooledConnection)
|
||||||
query = "SELECT id, shipcall_id, participant_id, level, type, message, created, modified FROM notification WHERE level = 2"
|
data = commands.query("SELECT id, shipcall_id, participant_id, level, type, message, created, modified FROM notification " +
|
||||||
params = {}
|
"WHERE level = 2", model=model.Notification.from_query_row)
|
||||||
if participant_id is not None:
|
|
||||||
query += " AND participant_id = ?participant_id?"
|
|
||||||
params["participant_id"] = participant_id
|
|
||||||
|
|
||||||
data = commands.query(query, model=model.Notification.from_query_row, param=params if params else None)
|
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.error(ex)
|
logging.error(ex)
|
||||||
|
|||||||
@ -136,7 +136,7 @@ def PostShipcalls(schemaModel):
|
|||||||
new_id = commands.execute_scalar("select last_insert_id()")
|
new_id = commands.execute_scalar("select last_insert_id()")
|
||||||
|
|
||||||
shipdata = get_ship_data_for_id(schemaModel["ship_id"])
|
shipdata = get_ship_data_for_id(schemaModel["ship_id"])
|
||||||
message = "The participant has been assigned to the shipcall."
|
message = shipdata['name']
|
||||||
if "type_value" in schemaModel:
|
if "type_value" in schemaModel:
|
||||||
match schemaModel["type_value"]:
|
match schemaModel["type_value"]:
|
||||||
case 1:
|
case 1:
|
||||||
@ -242,6 +242,20 @@ def PutShipcalls(schemaModel, original_payload=None):
|
|||||||
query = "UPDATE shipcall SET " + ", ".join(update_clauses) + " WHERE id = ?id?"
|
query = "UPDATE shipcall SET " + ", ".join(update_clauses) + " WHERE id = ?id?"
|
||||||
commands.execute(query, param=schemaModel)
|
commands.execute(query, param=schemaModel)
|
||||||
|
|
||||||
|
ship_id_value = schemaModel.get("ship_id") if (provided_keys is None or "ship_id" in provided_keys) else theshipcall["ship_id"]
|
||||||
|
|
||||||
|
shipdata = get_ship_data_for_id(ship_id_value)
|
||||||
|
message = shipdata['name']
|
||||||
|
type_value = schemaModel.get("type_value") if (provided_keys is None or "type" in provided_keys) else theshipcall["type"]
|
||||||
|
if type_value is not None:
|
||||||
|
match type_value:
|
||||||
|
case 1:
|
||||||
|
message += " [ARRIVAL]"
|
||||||
|
case 2:
|
||||||
|
message += " [DEPARTURE]"
|
||||||
|
case 3:
|
||||||
|
message += " [SHIFTING]"
|
||||||
|
|
||||||
# pquery = SQLQuery.get_shipcall_participant_map_by_shipcall_id()
|
# pquery = SQLQuery.get_shipcall_participant_map_by_shipcall_id()
|
||||||
pquery = "SELECT id, participant_id, type FROM shipcall_participant_map where shipcall_id = ?id?"
|
pquery = "SELECT id, participant_id, type FROM shipcall_participant_map where shipcall_id = ?id?"
|
||||||
pdata = commands.query(pquery,param={"id" : schemaModel["id"]}) # existing list of assignments
|
pdata = commands.query(pquery,param={"id" : schemaModel["id"]}) # existing list of assignments
|
||||||
@ -260,7 +274,6 @@ def PutShipcalls(schemaModel, original_payload=None):
|
|||||||
found_participant = True
|
found_participant = True
|
||||||
break
|
break
|
||||||
if not found_participant:
|
if not found_participant:
|
||||||
message = "The participant has been assigned to the shipcall."
|
|
||||||
# nquery = SQLQuery.get_shipcall_post_update_shipcall_participant_map()
|
# nquery = SQLQuery.get_shipcall_post_update_shipcall_participant_map()
|
||||||
spquery = "INSERT INTO shipcall_participant_map (shipcall_id, participant_id, type) VALUES (?shipcall_id?, ?participant_id?, ?type?)"
|
spquery = "INSERT INTO shipcall_participant_map (shipcall_id, participant_id, type) VALUES (?shipcall_id?, ?participant_id?, ?type?)"
|
||||||
commands.execute(spquery, param={"shipcall_id" : schemaModel["id"], "participant_id" : participant_assignment["participant_id"], "type" : participant_assignment["type"]})
|
commands.execute(spquery, param={"shipcall_id" : schemaModel["id"], "participant_id" : participant_assignment["participant_id"], "type" : participant_assignment["type"]})
|
||||||
@ -294,7 +307,6 @@ def PutShipcalls(schemaModel, original_payload=None):
|
|||||||
commands.execute(nquery, param={"nid" : existing_notification["id"]})
|
commands.execute(nquery, param={"nid" : existing_notification["id"]})
|
||||||
else:
|
else:
|
||||||
# create un-assignment notification
|
# create un-assignment notification
|
||||||
message = "The participant has been unassigned from the shipcall."
|
|
||||||
nquery = "INSERT INTO notification (shipcall_id, participant_id, level, type, message) VALUES (?shipcall_id?, ?participant_id?, 0, 5, ?message?)"
|
nquery = "INSERT INTO notification (shipcall_id, participant_id, level, type, message) VALUES (?shipcall_id?, ?participant_id?, 0, 5, ?message?)"
|
||||||
commands.execute(nquery, param={"shipcall_id" : schemaModel["id"], "participant_id" : elem["participant_id"], "message" : message})
|
commands.execute(nquery, param={"shipcall_id" : schemaModel["id"], "participant_id" : elem["participant_id"], "message" : message})
|
||||||
break
|
break
|
||||||
@ -302,7 +314,6 @@ def PutShipcalls(schemaModel, original_payload=None):
|
|||||||
canceled_value = schemaModel.get("canceled")
|
canceled_value = schemaModel.get("canceled")
|
||||||
if canceled_value is not None:
|
if canceled_value is not None:
|
||||||
if canceled_value and not was_canceled:
|
if canceled_value and not was_canceled:
|
||||||
message = "The shipcall has been canceled."
|
|
||||||
# create a canceled notification for all currently assigned participants
|
# create a canceled notification for all currently assigned participants
|
||||||
stornoNotificationQuery = "INSERT INTO notification (shipcall_id, participant_id, level, type, message) VALUES (?shipcall_id?, ?participant_id?, 0, 7, ?message?)"
|
stornoNotificationQuery = "INSERT INTO notification (shipcall_id, participant_id, level, type, message) VALUES (?shipcall_id?, ?participant_id?, 0, 7, ?message?)"
|
||||||
for participant_assignment in schemaModel["participants"]:
|
for participant_assignment in schemaModel["participants"]:
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import sys
|
|||||||
from BreCal.schemas import defs
|
from BreCal.schemas import defs
|
||||||
|
|
||||||
config_path = None
|
config_path = None
|
||||||
secure_dir = None
|
|
||||||
_connection_pool = None
|
_connection_pool = None
|
||||||
|
|
||||||
|
|
||||||
@ -24,26 +23,17 @@ def _build_pool_config(connection_data, pool_name, pool_size):
|
|||||||
return pool_config
|
return pool_config
|
||||||
|
|
||||||
|
|
||||||
def initPool(instancePath, config=None, connection_filename="connection_data_prod.json",
|
def initPool(instancePath, connection_filename="connection_data_prod.json",
|
||||||
credentials_file="email_credentials_test.json", pool_name="brecal_pool", pool_size=10,
|
pool_name="brecal_pool", pool_size=10):
|
||||||
secure_directory=None):
|
|
||||||
"""
|
"""
|
||||||
Initialize the MySQL connection pool and load email credentials.
|
Initialize the MySQL connection pool and load email credentials.
|
||||||
"""
|
"""
|
||||||
global config_path, secure_dir, _connection_pool
|
global config_path, _connection_pool
|
||||||
try:
|
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:
|
if config_path is None:
|
||||||
config_path = os.path.join(secure_dir, connection_filename)
|
config_path = os.path.join(instancePath, f'../../../secure/{connection_filename}')
|
||||||
|
|
||||||
|
# config_path = 'C:\\temp\\connection_data_test.json'
|
||||||
|
|
||||||
print(config_path)
|
print(config_path)
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
@ -64,7 +54,10 @@ def initPool(instancePath, config=None, connection_filename="connection_data_pro
|
|||||||
finally:
|
finally:
|
||||||
conn_from_pool.close()
|
conn_from_pool.close()
|
||||||
|
|
||||||
credentials_path = os.path.join(secure_dir, credentials_file)
|
credentials_file = "email_credentials_test.json"
|
||||||
|
credentials_path = os.path.join(instancePath, f'../../../secure/{credentials_file}')
|
||||||
|
|
||||||
|
# credentials_path = 'C:\\temp\\email_credentials_test.json'
|
||||||
|
|
||||||
if not os.path.exists(credentials_path):
|
if not os.path.exists(credentials_path):
|
||||||
print('cannot find ' + os.path.abspath(credentials_path))
|
print('cannot find ' + os.path.abspath(credentials_path))
|
||||||
|
|||||||
@ -3,42 +3,49 @@
|
|||||||
"type" : 1,
|
"type" : 1,
|
||||||
"color" : "#0867ec",
|
"color" : "#0867ec",
|
||||||
"name" : "assignment",
|
"name" : "assignment",
|
||||||
|
"link" : "https://www.bremen-calling.de/",
|
||||||
"msg_text" : "Nominierung"
|
"msg_text" : "Nominierung"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type" : 2,
|
"type" : 2,
|
||||||
"color" : "#ea5c00",
|
"color" : "#ea5c00",
|
||||||
"name" : "next24h",
|
"name" : "next24h",
|
||||||
|
"link" : "https://www.bremen-calling.de/",
|
||||||
"msg_text" : "Morgenrunde relevant"
|
"msg_text" : "Morgenrunde relevant"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type" : 3,
|
"type" : 3,
|
||||||
"color" : "#f34336",
|
"color" : "#f34336",
|
||||||
"name" : "time_conflict",
|
"name" : "time_conflict",
|
||||||
|
"link" : "https://www.bremen-calling.de/",
|
||||||
"msg_text" : "Zeitlicher Konflikt"
|
"msg_text" : "Zeitlicher Konflikt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type" : 4,
|
"type" : 4,
|
||||||
"color" : "#28b532",
|
"color" : "#28b532",
|
||||||
"name" : "time_conflict_resolved",
|
"name" : "time_conflict_resolved",
|
||||||
|
"link" : "https://www.bremen-calling.de/",
|
||||||
"msg_text" : "Zeitlicher Konflikt gelöst"
|
"msg_text" : "Zeitlicher Konflikt gelöst"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type" : 5,
|
"type" : 5,
|
||||||
"color" : "#a8a8a8",
|
"color" : "#a8a8a8",
|
||||||
"name" : "unassigned",
|
"name" : "unassigned",
|
||||||
|
"link" : "https://www.bremen-calling.de/",
|
||||||
"msg_text" : "Nominierung abgewählt"
|
"msg_text" : "Nominierung abgewählt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type" : 6,
|
"type" : 6,
|
||||||
"color" : "#a8a800",
|
"color" : "#a8a800",
|
||||||
"name" : "missing_data",
|
"name" : "missing_data",
|
||||||
|
"link" : "https://www.bremen-calling.de/",
|
||||||
"msg_text" : "Fehlende Daten"
|
"msg_text" : "Fehlende Daten"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type" : 7,
|
"type" : 7,
|
||||||
"color" : "#808070",
|
"color" : "#808070",
|
||||||
"name" : "cancelled",
|
"name" : "cancelled",
|
||||||
|
"link" : "https://www.bremen-calling.de/",
|
||||||
"msg_text" : "Storno"
|
"msg_text" : "Storno"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -5,7 +5,7 @@ from marshmallow_enum import EnumField
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
from marshmallow_dataclass import dataclass
|
from marshmallow_dataclass import dataclass
|
||||||
from typing import Iterable, List
|
from typing import List
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
@ -85,31 +85,19 @@ class NotificationType(IntEnum):
|
|||||||
def _missing_(cls, value):
|
def _missing_(cls, value):
|
||||||
return cls.undefined
|
return cls.undefined
|
||||||
|
|
||||||
def bitflag_to_list(bitflag: int | None) -> list[NotificationType]:
|
def bitflag_to_list(bitflag: int) -> list[NotificationType]:
|
||||||
"""Converts an integer bitflag to a list of NotificationType enums."""
|
|
||||||
if bitflag is None:
|
if bitflag is None:
|
||||||
return []
|
return []
|
||||||
|
"""Converts an integer bitflag to a list of NotificationType enums."""
|
||||||
return [nt for nt in NotificationType if bitflag & (1 << (nt.value - 1))]
|
return [nt for nt in NotificationType if bitflag & (1 << (nt.value - 1))]
|
||||||
|
|
||||||
def list_to_bitflag(notifications: Iterable[NotificationType | str | int] | None) -> int:
|
def list_to_bitflag(notifications: fields.List) -> int:
|
||||||
"""Converts a list of NotificationType enums (or their names/values) to an integer bitflag."""
|
"""Converts a list of NotificationType enums to an integer bitflag."""
|
||||||
if not notifications:
|
try:
|
||||||
|
iter(notifications)
|
||||||
|
return sum(1 << (nt.value - 1) for nt in notifications)
|
||||||
|
except TypeError as te:
|
||||||
return 0
|
return 0
|
||||||
bitflag = 0
|
|
||||||
for nt in notifications:
|
|
||||||
enum_val = None
|
|
||||||
if isinstance(nt, NotificationType):
|
|
||||||
enum_val = nt
|
|
||||||
elif isinstance(nt, str):
|
|
||||||
enum_val = NotificationType[nt]
|
|
||||||
else:
|
|
||||||
enum_val = NotificationType(nt)
|
|
||||||
bitflag |= 1 << (enum_val.value - 1)
|
|
||||||
return bitflag
|
|
||||||
|
|
||||||
def notification_types_to_names(notifications: Iterable[NotificationType]) -> list[str]:
|
|
||||||
"""Render NotificationType values as their names for API responses."""
|
|
||||||
return [nt.name for nt in notifications]
|
|
||||||
|
|
||||||
|
|
||||||
class ShipcallType(IntEnum):
|
class ShipcallType(IntEnum):
|
||||||
@ -585,7 +573,8 @@ class User:
|
|||||||
notify_popup: bool
|
notify_popup: bool
|
||||||
created: datetime
|
created: datetime
|
||||||
modified: datetime
|
modified: datetime
|
||||||
notify_event: int | None = 0
|
ports: List[NotificationType] = field(default_factory=list)
|
||||||
|
notify_event: List[NotificationType] = field(default_factory=list)
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(id)
|
return hash(id)
|
||||||
|
|||||||
@ -38,9 +38,6 @@ class EmailHandler():
|
|||||||
|
|
||||||
self.server = smtplib.SMTP_SSL(self.mail_server, self.mail_port) # alternatively, SMTP
|
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):
|
def check_state(self):
|
||||||
"""check, whether the server login took place and is open."""
|
"""check, whether the server login took place and is open."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -124,11 +124,11 @@ def SendEmails(email_dict):
|
|||||||
defs.message_types = json.load(f)
|
defs.message_types = json.load(f)
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
for user_email, notifications in email_dict.items():
|
for user, notifications in email_dict.items():
|
||||||
msg = EmailMessage()
|
msg = EmailMessage()
|
||||||
msg["Subject"] = '[Bremen calling] Notification'
|
msg["Subject"] = '[Bremen calling] Notification'
|
||||||
msg["From"] = defs.email_credentials["sender"]
|
msg["From"] = defs.email_credentials["sender"]
|
||||||
msg["To"] = user_email
|
msg["To"] = user.user_email
|
||||||
|
|
||||||
with open(os.path.join(current_path,'../msg/notification_template.html'), mode="r", encoding="utf-8") as file:
|
with open(os.path.join(current_path,'../msg/notification_template.html'), mode="r", encoding="utf-8") as file:
|
||||||
body = file.read()
|
body = file.read()
|
||||||
@ -145,8 +145,7 @@ def SendEmails(email_dict):
|
|||||||
with open(os.path.join(current_path,'../msg/notification_element.html'), mode="r", encoding="utf-8") as file:
|
with open(os.path.join(current_path,'../msg/notification_element.html'), mode="r", encoding="utf-8") as file:
|
||||||
element = file.read()
|
element = file.read()
|
||||||
element = element.replace("[[color]]", message_type["color"])
|
element = element.replace("[[color]]", message_type["color"])
|
||||||
linktext = defs.email_credentials["url_template"] + str(notification.shipcall_id)
|
element = element.replace("[[link]]", message_type["link"])
|
||||||
element = element.replace("[[link]]", linktext)
|
|
||||||
|
|
||||||
# We want to show the following information for each notification:
|
# We want to show the following information for each notification:
|
||||||
# Ship-name, Arr/Dep/Shift, ETA/ETD, berth
|
# Ship-name, Arr/Dep/Shift, ETA/ETD, berth
|
||||||
@ -181,7 +180,7 @@ def SendEmails(email_dict):
|
|||||||
body = body.replace("[[NOTIFICATION_ELEMENTS]]", replacement)
|
body = body.replace("[[NOTIFICATION_ELEMENTS]]", replacement)
|
||||||
msg.set_content(body, subtype='html', charset='utf-8', cte='8bit')
|
msg.set_content(body, subtype='html', charset='utf-8', cte='8bit')
|
||||||
|
|
||||||
conn.sendmail(defs.email_credentials["sender"], user_email, msg.as_string())
|
conn.sendmail(defs.email_credentials["sender"], user.user_email, msg.as_string())
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.error(ex)
|
logging.error(ex)
|
||||||
@ -211,10 +210,10 @@ def SendNotifications():
|
|||||||
email_dict = dict()
|
email_dict = dict()
|
||||||
users_dict = dict()
|
users_dict = dict()
|
||||||
user_query = "SELECT * from user"
|
user_query = "SELECT * from user"
|
||||||
users = commands.query(user_query)
|
users = commands.query(user_query, model=model.User)
|
||||||
for participant in participants:
|
for participant in participants:
|
||||||
for user in users:
|
for user in users:
|
||||||
if user["participant_id"] == participant.id:
|
if user.participant_id == participant.id:
|
||||||
if not participant.id in users_dict:
|
if not participant.id in users_dict:
|
||||||
users_dict[participant.id] = []
|
users_dict[participant.id] = []
|
||||||
users_dict[participant.id].append(user)
|
users_dict[participant.id].append(user)
|
||||||
@ -226,39 +225,33 @@ def SendNotifications():
|
|||||||
p_query = "SELECT * from shipcall_participant_map where shipcall_id = ?id?"
|
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})
|
assigned_participants = commands.query(p_query, model=model.ShipcallParticipantMap, param={"id":notification.shipcall_id})
|
||||||
for assigned_participant in assigned_participants:
|
for assigned_participant in assigned_participants:
|
||||||
if not assigned_participant.participant_id in users_dict:
|
|
||||||
continue
|
|
||||||
users = users_dict[assigned_participant.participant_id]
|
users = users_dict[assigned_participant.participant_id]
|
||||||
for user in users:
|
for user in users:
|
||||||
# send notification to user
|
# send notification to user
|
||||||
if user["notify_email"]:
|
if user.notify_email:
|
||||||
if user["user_email"] not in email_dict:
|
if user not in email_dict:
|
||||||
email_dict[user["user_email"]] = []
|
email_dict[user] = []
|
||||||
if notification not in email_dict[user["user_email"]]:
|
email_dict[user].append(notification)
|
||||||
email_dict[user["user_email"]].append(notification)
|
if user.notify_whatsapp:
|
||||||
if user["notify_whatsapp"]:
|
|
||||||
# TBD
|
# TBD
|
||||||
pass
|
pass
|
||||||
if user["notify_signal"]:
|
if user.notify_signal:
|
||||||
# TBD
|
# TBD
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if notification.participant_id in users_dict:
|
users = users_dict[notification.participant_id]
|
||||||
users = users_dict[notification.participant_id]
|
for user in users:
|
||||||
for user in users:
|
# send notification to user
|
||||||
user_notifications = model.bitflag_to_list(user["notify_event"])
|
if user.notify_email and user.wants_notifications(notification.type):
|
||||||
# send notification to user
|
if user not in email_dict:
|
||||||
if user["notify_email"] and notification.type in user_notifications:
|
email_dict[user] = []
|
||||||
if user["user_email"] not in email_dict:
|
email_dict[user].append(notification)
|
||||||
email_dict[user["user_email"]] = []
|
if user.notify_whatsapp and user.wants_notifications(notification.type):
|
||||||
if notification not in email_dict[user["user_email"]]:
|
# TBD
|
||||||
email_dict[user["user_email"]].append(notification)
|
pass
|
||||||
if user["notify_whatsapp"] and notification.type in user_notifications:
|
if user.notify_signal and user.wants_notifications(notification.type):
|
||||||
# TBD
|
# TBD
|
||||||
pass
|
pass
|
||||||
if user["notify_signal"] and notification.type in user_notifications:
|
|
||||||
# TBD
|
|
||||||
pass
|
|
||||||
|
|
||||||
# mark as sent
|
# mark as sent
|
||||||
commands.execute("UPDATE notification SET level = 2 WHERE id = ?id?", param={"id":notification.id})
|
commands.execute("UPDATE notification SET level = 2 WHERE id = ?id?", param={"id":notification.id})
|
||||||
@ -314,8 +307,7 @@ def eval_next_24_hrs():
|
|||||||
found_notification = True
|
found_notification = True
|
||||||
break
|
break
|
||||||
if not found_notification:
|
if not found_notification:
|
||||||
message = "The shipcall is scheduled to arrive/depart within the next 24 hours."
|
commands.execute(nquery, param={"shipcall_id":shipcall["id"], "participant_id": participant["participant_id"], "message":shipcall["name"]})
|
||||||
commands.execute(nquery, param={"shipcall_id":shipcall["id"], "participant_id": participant["participant_id"], "message":message})
|
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logging.error(ex)
|
logging.error(ex)
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
import typing
|
|
||||||
from marshmallow import ValidationError
|
|
||||||
|
|
||||||
class InputValidationBase:
|
|
||||||
"""
|
|
||||||
Base class for input validators. Common validation methods are grouped here.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_required_fields(content:dict, required_fields:list[str]):
|
|
||||||
missing_fields = [field for field in required_fields if content.get(field) is None]
|
|
||||||
if missing_fields:
|
|
||||||
raise ValidationError({"required_fields": f"Missing required fields: {missing_fields}"})
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_if_entry_is_already_deleted(entry:dict):
|
|
||||||
"""
|
|
||||||
Checks if the entry has 'deleted' set to True/1.
|
|
||||||
"""
|
|
||||||
if entry.get("deleted") in [True, 1, "1"]:
|
|
||||||
raise ValidationError({"deleted": "The selected entry is already deleted."})
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_deleted_flag_on_post(content:dict):
|
|
||||||
"""
|
|
||||||
Checks if 'deleted' is set to 1/True in a POST (Create) request.
|
|
||||||
"""
|
|
||||||
if content.get("deleted") in [True, 1, "1"]:
|
|
||||||
raise ValidationError({"deleted": "Cannot create an entry with 'deleted' set to 1."})
|
|
||||||
|
|
||||||
|
|
||||||
@ -18,11 +18,10 @@ from BreCal.validators.input_validation_utils import check_if_user_is_bsmd_type,
|
|||||||
from BreCal.database.sql_handler import execute_sql_query_standalone
|
from BreCal.database.sql_handler import execute_sql_query_standalone
|
||||||
from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag
|
from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag
|
||||||
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
|
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
|
||||||
from BreCal.validators.input_validation_base import InputValidationBase
|
|
||||||
|
|
||||||
import werkzeug
|
import werkzeug
|
||||||
|
|
||||||
class InputValidationShip(InputValidationBase):
|
class InputValidationShip():
|
||||||
"""
|
"""
|
||||||
This class combines a complex set of individual input validation functions into a joint object.
|
This class combines a complex set of individual input validation functions into a joint object.
|
||||||
It uses static methods, so the object does not need to be instantiated, but functions can be called immediately.
|
It uses static methods, so the object does not need to be instantiated, but functions can be called immediately.
|
||||||
@ -56,13 +55,6 @@ class InputValidationShip(InputValidationBase):
|
|||||||
|
|
||||||
# 3.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
|
# 3.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
|
||||||
InputValidationShip.optionally_evaluate_bollard_pull_value(content)
|
InputValidationShip.optionally_evaluate_bollard_pull_value(content)
|
||||||
|
|
||||||
# 4.) No deleted flag may be set on POST
|
|
||||||
InputValidationShip.check_deleted_flag_on_post(content)
|
|
||||||
|
|
||||||
# 5.) Check if is_tug is null
|
|
||||||
InputValidationShip.check_is_tug_null(content)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -78,10 +70,6 @@ class InputValidationShip(InputValidationBase):
|
|||||||
|
|
||||||
# 4.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
|
# 4.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
|
||||||
InputValidationShip.optionally_evaluate_bollard_pull_value(content)
|
InputValidationShip.optionally_evaluate_bollard_pull_value(content)
|
||||||
|
|
||||||
# 5.) Check if tug is null
|
|
||||||
InputValidationShip.check_is_tug_null(content)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -171,11 +159,5 @@ class InputValidationShip(InputValidationBase):
|
|||||||
raise ValidationError({"deleted":f"The selected ship entry is already deleted."})
|
raise ValidationError({"deleted":f"The selected ship entry is already deleted."})
|
||||||
return
|
return
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_is_tug_null(content:dict):
|
|
||||||
is_tug = content.get("is_tug", None)
|
|
||||||
if is_tug is None:
|
|
||||||
raise ValidationError({"is_tug":f"The 'is_tug' property must be set to either True or False."})
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -17,13 +17,12 @@ from BreCal.database.sql_handler import get_assigned_participant_of_type
|
|||||||
from BreCal.database.sql_handler import execute_sql_query_standalone
|
from BreCal.database.sql_handler import execute_sql_query_standalone
|
||||||
from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag
|
from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag
|
||||||
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
|
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
|
||||||
from BreCal.validators.input_validation_base import InputValidationBase
|
|
||||||
from BreCal.database.sql_queries import SQLQuery
|
from BreCal.database.sql_queries import SQLQuery
|
||||||
import werkzeug
|
import werkzeug
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InputValidationShipcall(InputValidationBase):
|
class InputValidationShipcall():
|
||||||
"""
|
"""
|
||||||
This class combines a complex set of individual input validation functions into a joint object.
|
This class combines a complex set of individual input validation functions into a joint object.
|
||||||
It uses static methods, so the object does not need to be instantiated, but functions can be called immediately.
|
It uses static methods, so the object does not need to be instantiated, but functions can be called immediately.
|
||||||
@ -61,11 +60,7 @@ class InputValidationShipcall(InputValidationBase):
|
|||||||
InputValidationShipcall.check_participant_list_not_empty_when_user_is_agency(loadedModel)
|
InputValidationShipcall.check_participant_list_not_empty_when_user_is_agency(loadedModel)
|
||||||
|
|
||||||
# check for reasonable values in the shipcall fields
|
# check for reasonable values in the shipcall fields
|
||||||
InputValidationShipcall.check_shipcall_values(loadedModel, content, forbidden_keys=["evaluation", "evaluation_message", "canceled"]) # "canceled"
|
InputValidationShipcall.check_shipcall_values(loadedModel, content, forbidden_keys=["evaluation", "evaluation_message"]) # "canceled"
|
||||||
|
|
||||||
# check for deleted flag on POST
|
|
||||||
InputValidationShipcall.check_deleted_flag_on_post(content)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -17,7 +17,6 @@ from BreCal.database.sql_queries import SQLQuery
|
|||||||
from BreCal.database.sql_handler import execute_sql_query_standalone
|
from BreCal.database.sql_handler import execute_sql_query_standalone
|
||||||
from BreCal.database.sql_handler import get_assigned_participant_of_type
|
from BreCal.database.sql_handler import get_assigned_participant_of_type
|
||||||
from BreCal.database.sql_utils import get_times_data_for_id
|
from BreCal.database.sql_utils import get_times_data_for_id
|
||||||
from BreCal.validators.input_validation_base import InputValidationBase
|
|
||||||
from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag, check_if_string_has_special_characters
|
from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag, check_if_string_has_special_characters
|
||||||
import werkzeug
|
import werkzeug
|
||||||
|
|
||||||
@ -64,7 +63,7 @@ def build_post_data_type_dependent_required_fields_dict()->dict[ShipcallType,dic
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InputValidationTimes(InputValidationBase):
|
class InputValidationTimes():
|
||||||
"""
|
"""
|
||||||
This class combines a complex set of individual input validation functions into a joint object.
|
This class combines a complex set of individual input validation functions into a joint object.
|
||||||
It uses static methods, so the object does not need to be instantiated, but functions can be called immediately.
|
It uses static methods, so the object does not need to be instantiated, but functions can be called immediately.
|
||||||
@ -93,10 +92,6 @@ class InputValidationTimes(InputValidationBase):
|
|||||||
|
|
||||||
# 4.) Value checking
|
# 4.) Value checking
|
||||||
InputValidationTimes.check_dataset_values(user_data, loadedModel, content)
|
InputValidationTimes.check_dataset_values(user_data, loadedModel, content)
|
||||||
|
|
||||||
# 5.) Deleted flag may not be set on POST
|
|
||||||
InputValidationTimes.check_deleted_flag_on_post(content)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -59,7 +59,7 @@ def create_validation_error_response(ex:ValidationError, status_code:int=400, cr
|
|||||||
|
|
||||||
if create_log:
|
if create_log:
|
||||||
logging.warning(ex) if ex is not None else logging.warning(message)
|
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)
|
return (serialized_response, status_code)
|
||||||
|
|
||||||
def create_werkzeug_error_response(ex:Forbidden, status_code:int=403, create_log:bool=True)->typing.Tuple[str,int]:
|
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:
|
if create_log:
|
||||||
logging.warning(ex) if ex is not None else logging.warning(message)
|
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
|
return serialized_response, status_code
|
||||||
|
|
||||||
def create_dynamic_exception_response(ex, status_code:int=400, message:typing.Optional[str]=None, create_log:bool=True):
|
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:
|
if create_log:
|
||||||
logging.warning(ex) if ex is not None else logging.warning(message)
|
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)
|
return (serialized_response, status_code)
|
||||||
|
|||||||
@ -5,13 +5,11 @@ import re
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import datetime
|
import datetime
|
||||||
import threading
|
|
||||||
from BreCal.database.enums import StatusFlags
|
from BreCal.database.enums import StatusFlags
|
||||||
from BreCal.validators.validation_rule_functions import ValidationRuleFunctions
|
from BreCal.validators.validation_rule_functions import ValidationRuleFunctions
|
||||||
from BreCal.schemas.model import Shipcall
|
from BreCal.schemas.model import Shipcall
|
||||||
from BreCal.local_db import getPoolConnection
|
from BreCal.local_db import getPoolConnection
|
||||||
|
|
||||||
_evaluation_lock = threading.Lock()
|
|
||||||
|
|
||||||
class ValidationRules(ValidationRuleFunctions):
|
class ValidationRules(ValidationRuleFunctions):
|
||||||
"""
|
"""
|
||||||
@ -54,8 +52,7 @@ class ValidationRules(ValidationRuleFunctions):
|
|||||||
# 'translate' all error codes into readable, human-understandable format.
|
# 'translate' all error codes into readable, human-understandable format.
|
||||||
evaluation_results = [(state, self.describe_error_message(msg)) for (state, msg) in evaluation_results]
|
evaluation_results = [(state, self.describe_error_message(msg)) for (state, msg) in evaluation_results]
|
||||||
|
|
||||||
if evaluation_results:
|
logging.info(f"Validation results for shipcall {shipcall.id}: {evaluation_results}")
|
||||||
logging.info(f"Validation results for shipcall {shipcall.id}: {evaluation_results}")
|
|
||||||
|
|
||||||
# check, what the maximum state flag is and return it
|
# 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
|
evaluation_state = np.max(np.array([result[0].value for result in evaluation_results])) if len(evaluation_results)>0 else StatusFlags.GREEN.value
|
||||||
@ -76,52 +73,37 @@ class ValidationRules(ValidationRuleFunctions):
|
|||||||
return evaluation_state, violations
|
return evaluation_state, violations
|
||||||
|
|
||||||
def evaluate_shipcalls(self, shipcall_df:pd.DataFrame)->pd.DataFrame:
|
def evaluate_shipcalls(self, shipcall_df:pd.DataFrame)->pd.DataFrame:
|
||||||
|
"""apply 'evaluate_shipcall_from_df' to each individual shipcall in {shipcall_df}. Returns shipcall_df ('evaluation', 'evaluation_message', 'evaluation_time' and 'evaluation_notifications_sent' are updated)"""
|
||||||
|
evaluation_states_old = [state_old for state_old in shipcall_df.loc[:,"evaluation"]]
|
||||||
|
evaluation_states_old = [state_old if not pd.isna(state_old) else 0 for state_old in evaluation_states_old]
|
||||||
|
results = shipcall_df.apply(lambda x: self.evaluate_shipcall_from_df(x), axis=1).values # returns tuple (state, message)
|
||||||
|
|
||||||
# Acquire lock to prevent race conditions during evaluation and notification creation
|
# unbundle individual results. evaluation_states becomes an integer, violation
|
||||||
with _evaluation_lock:
|
evaluation_states_new = [StatusFlags(res[0]).value for res in results]
|
||||||
"""apply 'evaluate_shipcall_from_df' to each individual shipcall in {shipcall_df}. Returnsshipcall_df ('evaluation', 'evaluation_message', 'evaluation_time' and 'evaluation_notifications_sent' are updated)"""
|
violations = [",\r\n".join(res[1]) if len(res[1])>0 else None for res in results]
|
||||||
evaluation_states_old = [state_old for state_old in shipcall_df.loc[:,"evaluation"]]
|
violations = [self.concise_evaluation_message_if_too_long(violation) for violation in violations]
|
||||||
evaluation_states_old = [state_old if not pd.isna(state_old) else 0 for state_old in evaluation_states_old]
|
|
||||||
results = shipcall_df.apply(lambda x: self.evaluate_shipcall_from_df(x), axis=1).values # returns tuple (state, message)
|
|
||||||
|
|
||||||
# unbundle individual results. evaluation_states becomes an integer, violation
|
# build the list of evaluation times ('now', as isoformat)
|
||||||
evaluation_states_new = [StatusFlags(res[0]).value for res in results]
|
#evaluation_time = self.get_notification_times(evaluation_states_new)
|
||||||
violations = [",\r\n".join(res[1]) if len(res[1])>0 else None for res in results]
|
|
||||||
violations = [self.concise_evaluation_message_if_too_long(violation) for violation in violations]
|
|
||||||
|
|
||||||
# build the list of evaluation times ('now', as isoformat)
|
|
||||||
#evaluation_time = self.get_notification_times(evaluation_states_new)
|
|
||||||
|
|
||||||
if evaluation_states_old is not None and evaluation_states_new is not None:
|
|
||||||
pooledConnection = None
|
|
||||||
participants = None
|
|
||||||
try:
|
|
||||||
pooledConnection = getPoolConnection()
|
|
||||||
commands = pydapper.using(pooledConnection)
|
|
||||||
|
|
||||||
for shipcall_id, state_old_raw, state_new_raw, violation in zip(shipcall_df.index, evaluation_states_old, evaluation_states_new, violations):
|
|
||||||
state_old = int(state_old_raw) if state_old_raw is not None else 0
|
|
||||||
state_new = int(state_new_raw) if state_new_raw is not None else 0
|
|
||||||
logging.info(f"Shipcall {shipcall_id}: state_old={state_old}, state_new={state_new}")
|
|
||||||
if state_old == state_new:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if participants is None:
|
|
||||||
participant_query = "SELECT participant_id, type FROM shipcall_participant_map WHERE shipcall_id = ?shipcall_id?"
|
|
||||||
participants = [participant for participant in commands.query(participant_query, model=dict, param={"shipcall_id" : int(shipcall_id)}) if participant.get("type") != 1]
|
|
||||||
|
|
||||||
|
send_notification = False
|
||||||
|
if evaluation_states_old is not None and evaluation_states_new is not None:
|
||||||
|
if len(evaluation_states_old) == 1 and len(evaluation_states_new) == 1:
|
||||||
|
if evaluation_states_old[0] != evaluation_states_new[0]:
|
||||||
|
pooledConnection = None
|
||||||
|
try:
|
||||||
|
pooledConnection = getPoolConnection()
|
||||||
|
commands = pydapper.using(pooledConnection)
|
||||||
notification_type = 3 # RED (mapped to time_conflict)
|
notification_type = 3 # RED (mapped to time_conflict)
|
||||||
send_notification = False
|
if evaluation_states_new[0] == 2:
|
||||||
|
match evaluation_states_old[0]:
|
||||||
if state_new == 2:
|
|
||||||
match state_old:
|
|
||||||
case 0:
|
case 0:
|
||||||
send_notification = True
|
send_notification = True
|
||||||
case 1:
|
case 1:
|
||||||
send_notification = True
|
send_notification = True
|
||||||
notification_type = 6 # YELLOW (mapped to missing_data)
|
notification_type = 6 # YELLOW (mapped to missing_data)
|
||||||
elif state_new == 3:
|
if evaluation_states_new[0] == 3:
|
||||||
match state_old:
|
match evaluation_states_old[0]:
|
||||||
case 0:
|
case 0:
|
||||||
send_notification = True
|
send_notification = True
|
||||||
case 1:
|
case 1:
|
||||||
@ -130,35 +112,33 @@ class ValidationRules(ValidationRuleFunctions):
|
|||||||
send_notification = True
|
send_notification = True
|
||||||
|
|
||||||
if send_notification:
|
if send_notification:
|
||||||
logging.info(f"Creating notification(s) for shipcall {shipcall_id}, type={notification_type}")
|
query = f"INSERT INTO notification (shipcall_id, type, level, message) VALUES (?shipcall_id?, {notification_type}, 0, ?message?)"
|
||||||
query = f"INSERT INTO notification (shipcall_id, participant_id, type, level, message) VALUES (?shipcall_id?, ?participant_id?, {notification_type}, 0, ?message?)"
|
commands.execute(query, param={"shipcall_id" : int(shipcall_df.index[0]), "message" : violations[0]})
|
||||||
for participant in participants:
|
|
||||||
commands.execute(query, param={"shipcall_id" : int(shipcall_id), "participant_id" : participant["participant_id"], "message" : violation})
|
|
||||||
|
|
||||||
if state_new == 1 and state_old != 0: # this resolves the time conflict
|
if evaluation_states_new[0] == 1 and evaluation_states_old[0] != 0: # this resolves the conflict
|
||||||
logging.info(f"Resolving notifications for shipcall {shipcall_id}, type={notification_type}")
|
query = f"SELECT * from notification where shipcall_id = ?shipcall_id? and type = {notification_type} and level = 0"
|
||||||
query = f"DELETE from notification where shipcall_id = ?shipcall_id? and type = {notification_type} and level = 0"
|
existing_notification = commands.query(query, param={"shipcall_id" : int(shipcall_df.index[0])})
|
||||||
deleted_count = commands.execute(query, param={"shipcall_id" : int(shipcall_id)})
|
if len(existing_notification) > 0:
|
||||||
logging.info(f"Deleted {deleted_count} existing notifications (yet unsent)")
|
query = "DELETE from notification where id = ?id?"
|
||||||
if deleted_count == 0:
|
commands.execute(query, param={"id" : existing_notification[0]["id"]})
|
||||||
query = "INSERT INTO notification (shipcall_id, participant_id, type, level) VALUES (?shipcall_id?, ?participant_id?, 4, 0)"
|
else:
|
||||||
for participant in participants:
|
query = "INSERT INTO notification (shipcall_id, type, level) VALUES (?shipcall_id?, 4, 0)"
|
||||||
commands.execute(query, param={"shipcall_id" : int(shipcall_id), "participant_id" : participant["participant_id"]})
|
commands.execute(query, param={"shipcall_id" : int(shipcall_df.index[0])})
|
||||||
finally:
|
finally:
|
||||||
if pooledConnection is not None:
|
if pooledConnection is not None:
|
||||||
pooledConnection.close()
|
pooledConnection.close()
|
||||||
|
|
||||||
|
|
||||||
# build the list of 'evaluation_notifications_sent'. The value is 'False', when a notification should be created
|
# build the list of 'evaluation_notifications_sent'. The value is 'False', when a notification should be created
|
||||||
#evaluation_notifications_sent = self.get_notification_states(evaluation_states_old, evaluation_states_new)
|
#evaluation_notifications_sent = self.get_notification_states(evaluation_states_old, evaluation_states_new)
|
||||||
|
|
||||||
# TODO: detect evaluation state changes and create notifications
|
# TODO: detect evaluation state changes and create notifications
|
||||||
|
|
||||||
shipcall_df.loc[:,"evaluation"] = evaluation_states_new
|
shipcall_df.loc[:,"evaluation"] = evaluation_states_new
|
||||||
shipcall_df.loc[:,"evaluation_message"] = violations
|
shipcall_df.loc[:,"evaluation_message"] = violations
|
||||||
#shipcall_df.loc[:,"evaluation_time"] = evaluation_time
|
#shipcall_df.loc[:,"evaluation_time"] = evaluation_time
|
||||||
#shipcall_df.loc[:,"evaluation_notifications_sent"] = evaluation_notifications_sent
|
#shipcall_df.loc[:,"evaluation_notifications_sent"] = evaluation_notifications_sent
|
||||||
return shipcall_df
|
return shipcall_df
|
||||||
|
|
||||||
def concise_evaluation_message_if_too_long(self, violation):
|
def concise_evaluation_message_if_too_long(self, violation):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,40 +0,0 @@
|
|||||||
"""
|
|
||||||
Sample configuration for the Flask instance.
|
|
||||||
|
|
||||||
Copy this file to `src/server/instance/config.py` (the instance folder is git-ignored)
|
|
||||||
and adjust the values for each deployment target.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Flask
|
|
||||||
SECRET_KEY = "change-me"
|
|
||||||
|
|
||||||
# Python path adjustments used by the WSGI entrypoint (flaskapp.wsgi)
|
|
||||||
APP_ROOT = "/var/www/brecal/src/server"
|
|
||||||
SITE_PACKAGES = "/var/www/venv/lib/python3.12/site-packages/"
|
|
||||||
|
|
||||||
# Paths to environment-specific secrets and instance data
|
|
||||||
SECURE_DIR = "/var/www/secure" # directory that holds connection/email JSON files
|
|
||||||
INSTANCE_PATH = "/var/www/brecal/src/server/instance"
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_FILE = "brecal.log"
|
|
||||||
LOG_LEVEL = "INFO" # e.g. DEBUG, INFO, WARNING
|
|
||||||
LOG_TO_STDERR = False
|
|
||||||
|
|
||||||
# Database pool setup
|
|
||||||
DB_CONNECTION_FILE = "connection_data_prod.json"
|
|
||||||
DB_POOL_NAME = "brecal_pool"
|
|
||||||
DB_POOL_SIZE = 10
|
|
||||||
|
|
||||||
# Email + notifications
|
|
||||||
EMAIL_CREDENTIALS_FILE = "email_credentials_prod.json"
|
|
||||||
EMAIL_URL_TEMPLATE = "https://brecal.example.com/shipcalls/" # base URL for links in emails
|
|
||||||
SMTP_DEBUG_LEVEL = 0 # 0 = quiet, 1 = verbose
|
|
||||||
|
|
||||||
# Scheduler cadence
|
|
||||||
SCHEDULE_UPDATE_SHIPCALLS_MINUTES = 60
|
|
||||||
SCHEDULE_BACKGROUND_LATENCY_SECONDS = 30
|
|
||||||
|
|
||||||
# Notification cleanup / escalation windows
|
|
||||||
NOTIFICATION_COOLDOWN_MINS = 10
|
|
||||||
NOTIFICATION_MAX_AGE_DAYS = 3
|
|
||||||
@ -1,29 +1,20 @@
|
|||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import runpy
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
import logging
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
sys.path.insert(0, '/var/www/brecal/src/server')
|
||||||
INSTANCE_DIR = BASE_DIR / "instance"
|
sys.path.insert(0, '/var/www/venv/lib/python3.12/site-packages/')
|
||||||
CONFIG_PATH = INSTANCE_DIR / "config.py"
|
|
||||||
|
|
||||||
config = {}
|
import schedule
|
||||||
if CONFIG_PATH.exists():
|
|
||||||
config = runpy.run_path(str(CONFIG_PATH))
|
|
||||||
|
|
||||||
app_root = config.get("APP_ROOT", str(BASE_DIR))
|
# set the key
|
||||||
site_packages = config.get("SITE_PACKAGES")
|
os.environ['SECRET_KEY'] = 'zdiTz8P3jXOc7jztIQAoelK4zztyuCpJ'
|
||||||
|
|
||||||
sys.path.insert(0, app_root)
|
# Set up logging
|
||||||
if site_packages:
|
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
|
||||||
sys.path.insert(0, site_packages)
|
|
||||||
|
|
||||||
if config.get("SECRET_KEY"):
|
# Set up Scheduled Jobs
|
||||||
os.environ["SECRET_KEY"] = config["SECRET_KEY"]
|
|
||||||
|
|
||||||
log_kwargs = {"level": getattr(logging, config.get("LOG_LEVEL", "DEBUG")), "stream": sys.stderr}
|
|
||||||
logging.basicConfig(**log_kwargs)
|
|
||||||
|
|
||||||
|
# Import and run the Flask app
|
||||||
from BreCal import create_app
|
from BreCal import create_app
|
||||||
application = create_app(instance_path=config.get("INSTANCE_PATH"))
|
application = create_app()
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
[project.optional-dependencies]
|
|
||||||
test = [
|
|
||||||
"pytest>=7.0",
|
|
||||||
"schemathesis>=3.0",
|
|
||||||
"requests>=2.31",
|
|
||||||
]
|
|
||||||
@ -20,4 +20,4 @@ pytest
|
|||||||
pytest-cov
|
pytest-cov
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
|
../server/.
|
||||||
|
|||||||
@ -1,45 +0,0 @@
|
|||||||
import os
|
|
||||||
import pytest
|
|
||||||
import requests
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def base_url() -> str:
|
|
||||||
# Example: https://dev.api.mycompany.com
|
|
||||||
url = os.environ.get("API_BASE_URL")
|
|
||||||
if not url:
|
|
||||||
url = "http://neptun.fritz.box"
|
|
||||||
# raise RuntimeError("Set API_BASE_URL")
|
|
||||||
return url.rstrip("/")
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def login_payload() -> dict[str, str]:
|
|
||||||
username = os.environ.get("API_USERNAME")
|
|
||||||
if not username:
|
|
||||||
username = "Londo"
|
|
||||||
password = os.environ.get("API_PASSWORD")
|
|
||||||
if not password:
|
|
||||||
password = "Hallowach"
|
|
||||||
# if not username or not password:
|
|
||||||
# raise RuntimeError("Set API_USERNAME and API_PASSWORD")
|
|
||||||
return {"username": username, "password": password}
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def jwt_token(base_url: str, login_payload: dict[str, str]) -> str:
|
|
||||||
# Adapt these to your auth endpoint + response shape:
|
|
||||||
login_path = os.environ.get("API_LOGIN_PATH", "/login")
|
|
||||||
resp = requests.post(
|
|
||||||
f"{base_url}{login_path}",
|
|
||||||
json=login_payload,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
resp.raise_for_status()
|
|
||||||
|
|
||||||
data = resp.json()
|
|
||||||
token = data.get("access_token") or data.get("token")
|
|
||||||
if not token:
|
|
||||||
raise RuntimeError("Could not find JWT token in login response JSON")
|
|
||||||
return token
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def auth_headers(jwt_token: str) -> dict[str, str]:
|
|
||||||
return {"Authorization": f"Bearer {jwt_token}"}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import schemathesis
|
|
||||||
|
|
||||||
schema = schemathesis.openapi.from_path("../../../misc/BreCalApi.yaml")
|
|
||||||
|
|
||||||
@schema.parametrize()
|
|
||||||
def test_api_conformance(
|
|
||||||
case,
|
|
||||||
base_url: str,
|
|
||||||
auth_headers: dict[str, str],
|
|
||||||
login_payload: dict[str, str],
|
|
||||||
) -> None:
|
|
||||||
# Calls your real service:
|
|
||||||
if case.path == "/login" and case.method.upper() == "POST":
|
|
||||||
response = case.call(base_url=base_url, json=login_payload)
|
|
||||||
else:
|
|
||||||
response = case.call(base_url=base_url, headers=auth_headers)
|
|
||||||
# Validates status code, headers, and body against the OpenAPI schema:
|
|
||||||
case.validate_response(response)
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
[pytest]
|
|
||||||
addopts = -ra
|
|
||||||
testpaths = tests
|
|
||||||
Loading…
Reference in New Issue
Block a user