Compare commits

..

1 Commits

43 changed files with 196 additions and 557 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"]));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"]));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ from marshmallow_enum import EnumField
from enum import IntEnum from enum import IntEnum
from marshmallow_dataclass import dataclass from marshmallow_dataclass import dataclass
from typing import Iterable, List from typing import List
import json import json
import re import re
@ -85,31 +85,19 @@ class NotificationType(IntEnum):
def _missing_(cls, value): def _missing_(cls, value):
return cls.undefined return cls.undefined
def bitflag_to_list(bitflag: int | None) -> list[NotificationType]: def bitflag_to_list(bitflag: int) -> list[NotificationType]:
"""Converts an integer bitflag to a list of NotificationType enums."""
if bitflag is None: if bitflag is None:
return [] return []
"""Converts an integer bitflag to a list of NotificationType enums."""
return [nt for nt in NotificationType if bitflag & (1 << (nt.value - 1))] return [nt for nt in NotificationType if bitflag & (1 << (nt.value - 1))]
def list_to_bitflag(notifications: Iterable[NotificationType | str | int] | None) -> int: def list_to_bitflag(notifications: fields.List) -> int:
"""Converts a list of NotificationType enums (or their names/values) to an integer bitflag.""" """Converts a list of NotificationType enums to an integer bitflag."""
if not notifications: try:
iter(notifications)
return sum(1 << (nt.value - 1) for nt in notifications)
except TypeError as te:
return 0 return 0
bitflag = 0
for nt in notifications:
enum_val = None
if isinstance(nt, NotificationType):
enum_val = nt
elif isinstance(nt, str):
enum_val = NotificationType[nt]
else:
enum_val = NotificationType(nt)
bitflag |= 1 << (enum_val.value - 1)
return bitflag
def notification_types_to_names(notifications: Iterable[NotificationType]) -> list[str]:
"""Render NotificationType values as their names for API responses."""
return [nt.name for nt in notifications]
class ShipcallType(IntEnum): class ShipcallType(IntEnum):
@ -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)

View File

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

View File

@ -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,37 +225,31 @@ 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:
user_notifications = model.bitflag_to_list(user["notify_event"])
# send notification to user # send notification to user
if user["notify_email"] and notification.type in user_notifications: if user.notify_email and user.wants_notifications(notification.type):
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 and user.wants_notifications(notification.type):
if user["notify_whatsapp"] and notification.type in user_notifications:
# TBD # TBD
pass pass
if user["notify_signal"] and notification.type in user_notifications: if user.notify_signal and user.wants_notifications(notification.type):
# TBD # TBD
pass pass

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -52,7 +52,6 @@ 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

View File

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

View File

@ -1,29 +1,20 @@
import logging
import os import os
import runpy
import sys import sys
from pathlib import Path import logging
BASE_DIR = Path(__file__).resolve().parent sys.path.insert(0, '/var/www/brecal/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()