Compare commits
25 Commits
develop
...
v1.6.1.0_p
| Author | SHA1 | Date | |
|---|---|---|---|
| 33dd7518ef | |||
| 5459e1dd78 | |||
| f7fed255d5 | |||
| c3d8e901c4 | |||
| be08ad6a3a | |||
| 4f96ff066c | |||
| 2600b6ebc3 | |||
| 27e20e15f7 | |||
| 864e023495 | |||
| 70977e9abd | |||
| d536d9479e | |||
| c253de0ba8 | |||
| e28a5c1560 | |||
| 2df14d957b | |||
| c322564996 | |||
| f035f2427f | |||
| 2ff4b45a52 | |||
| 7f011d7dc9 | |||
| f0c33868d2 | |||
| 86408adb7c | |||
| 17ca43646f | |||
| b5c100294c | |||
| ddd2efa86d | |||
| 05d02773fd | |||
| 8e2adb3daa |
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@ -12,8 +12,7 @@
|
||||
"env": {
|
||||
"FLASK_APP": "src/server/BreCal",
|
||||
"FLASK_DEBUG": "1",
|
||||
"SECRET_KEY" : "zdiTz8P3jXOc7jztIQAoelK4zztyuCpJ", // https://randomkeygen.com/
|
||||
"FLASK_RUN_PORT": "5000"
|
||||
"SECRET_KEY" : "zdiTz8P3jXOc7jztIQAoelK4zztyuCpJ" // https://randomkeygen.com/
|
||||
},
|
||||
"args": [
|
||||
"run",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,537 +0,0 @@
|
||||
# Bremen calling API
|
||||
Version: _1.7.0_
|
||||
Last change: _Nov 14, 2025_
|
||||
|
||||
## Introduction
|
||||
|
||||
This API allows users to interact with "Bremen calling" without an UI. Apart vom querying data via _GET_ endpoints users may create and update shipcalls, assign participants and update participant times for shipcalls.
|
||||
|
||||
Creating and updating times and shipcalls depend on the participant roles a user is assigned to. For example, if a participant has the role "AGENCY" they may change assignments _and_ create and update agency times. A participant with the role "PILOT" on the other hand may not change the assigments and only create/update times for the pilot.
|
||||
|
||||
### Authentication
|
||||
- **ApiKey**: API key in `header` header named `Authorization`. This is a JWT Token that the caller receives upon login.
|
||||
|
||||
### Notes on this version
|
||||
|
||||
This version refers to _1.7_ whereas the public client currently has version _1.6_. This means that there is some functionality available in the API that cannot be accessed through the UI yet, specifically notifications.
|
||||
|
||||
There is no documentation for the structures returned by _GET_ requests but these can easily be determined via a single query.
|
||||
|
||||
## Ship Endpoints
|
||||
|
||||
### `DELETE /ships`
|
||||
**Summary:** Delete a ship (logically).
|
||||
A ship can only be logically deleted, since it is possible to have been used in previous shipcalls. On logical delete, the ship can no longer be selected in a new ship call.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| id | query | integer | Yes | **Id of ship**. *Example: 42*. Id of ship to be deleted. |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
### `GET /ships`
|
||||
**Summary:** gets a list of ships
|
||||
Gets a list of ships including logically deleted ships to be used with shipcalls
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**: list of ships
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
### `POST /ships`
|
||||
**Summary:** create a new ship entry
|
||||
adds a new non-existing ship to the database. The ships IMO number is the unique identifier.
|
||||
|
||||
#### Request Body
|
||||
Ship details. **Do not** provide id parameter.
|
||||
|
||||
**JSON Schema**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| id | integer | No | |
|
||||
| name | string | No | |
|
||||
| imo | integer | No | |
|
||||
| callsign | string | No | |
|
||||
| participant_id | integer | No | Optional reference to participant (tug role) |
|
||||
| length | number | No | |
|
||||
| width | number | No | |
|
||||
| is_tug | boolean | No | |
|
||||
| bollard_pull | integer | No | |
|
||||
| eni | integer | No | BSMD internal use |
|
||||
| created | string | No | Readonly field set by the database when ship was created |
|
||||
| modified | string | No | Readonly field set by the database when ship was last modified |
|
||||
| deleted | boolean | No | marks the ship as logically deleted |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **201**
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
### `PUT /ships`
|
||||
**Summary:** Update a ship entry
|
||||
Updating a ship entry. Please do not modify the IMO number. In that case please add a new entry.
|
||||
|
||||
#### Request Body
|
||||
Updated ship entry. The id parameter is **required**.
|
||||
|
||||
**JSON Schema**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| id | integer | No | |
|
||||
| name | string | No | |
|
||||
| imo | integer | No | |
|
||||
| callsign | string | No | |
|
||||
| participant_id | integer | No | Optional reference to participant (tug role) |
|
||||
| length | number | No | |
|
||||
| width | number | No | |
|
||||
| is_tug | boolean | No | |
|
||||
| bollard_pull | integer | No | |
|
||||
| eni | integer | No | BSMD internal use |
|
||||
| created | string | No | Readonly field set by the database when ship was created |
|
||||
| modified | string | No | Readonly field set by the database when ship was last modified |
|
||||
| deleted | boolean | No | marks the ship as logically deleted |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
## Shipcall Endpoints
|
||||
|
||||
### `GET /shipcalls`
|
||||
**Summary:** Gets a list of ship calls
|
||||
Get current ship calls
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| past_days | query | integer | No | number of days in the past to include in the result. *Example: 7*. |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**: ship call list
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
### `POST /shipcalls`
|
||||
**Summary:** Create a new ship call
|
||||
A new shipcall is created without times at this point. This is ususally done by the BSMD or a participant with that particular role.
|
||||
|
||||
#### Request Body
|
||||
Creates a new ship call. **Do not** provide id parameter.
|
||||
|
||||
**JSON Schema**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| id | integer | No | |
|
||||
| ship_id | integer | Yes | |
|
||||
| port_id | integer | No | |
|
||||
| type | string | Yes | Type of ship call |
|
||||
| eta | string | No | |
|
||||
| voyage | string | No | |
|
||||
| etd | string | No | |
|
||||
| arrival_berth_id | integer | No | |
|
||||
| departure_berth_id | integer | No | |
|
||||
| tug_required | boolean | No | |
|
||||
| pilot_required | boolean | No | |
|
||||
| flags | integer | No | |
|
||||
| pier_side | boolean | No | |
|
||||
| bunkering | boolean | No | |
|
||||
| replenishing_terminal | boolean | No | |
|
||||
| replenishing_lock | boolean | No | |
|
||||
| draft | number | No | |
|
||||
| tidal_window_from | string | No | |
|
||||
| tidal_window_to | string | No | |
|
||||
| rain_sensitive_cargo | boolean | No | |
|
||||
| recommended_tugs | integer | No | |
|
||||
| anchored | boolean | No | |
|
||||
| moored_lock | boolean | No | |
|
||||
| canceled | boolean | No | |
|
||||
| evaluation | string | No | Evaluation of the ship call |
|
||||
| evaluation_message | string | No | |
|
||||
| time_ref_point | integer | No | Physical reference point for all times given in shipcall and depending times entries |
|
||||
| participants | array<object> | No | |
|
||||
| created | string | No | Readonly field set by the database when shipcall was created |
|
||||
| modified | string | No | Readonly field set by the database when shipcall was last modified |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **201**
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
### `PUT /shipcalls`
|
||||
**Summary:** Updates a ship call
|
||||
Updates a shipcall. Usually done if the participant assignments change.
|
||||
|
||||
#### Request Body
|
||||
Creates a new ship call. The id parameter is **required**.
|
||||
|
||||
**JSON Schema**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| id | integer | No | |
|
||||
| ship_id | integer | Yes | |
|
||||
| port_id | integer | No | |
|
||||
| type | string | Yes | Type of ship call |
|
||||
| eta | string | No | |
|
||||
| voyage | string | No | |
|
||||
| etd | string | No | |
|
||||
| arrival_berth_id | integer | No | |
|
||||
| departure_berth_id | integer | No | |
|
||||
| tug_required | boolean | No | |
|
||||
| pilot_required | boolean | No | |
|
||||
| flags | integer | No | |
|
||||
| pier_side | boolean | No | |
|
||||
| bunkering | boolean | No | |
|
||||
| replenishing_terminal | boolean | No | |
|
||||
| replenishing_lock | boolean | No | |
|
||||
| draft | number | No | |
|
||||
| tidal_window_from | string | No | |
|
||||
| tidal_window_to | string | No | |
|
||||
| rain_sensitive_cargo | boolean | No | |
|
||||
| recommended_tugs | integer | No | |
|
||||
| anchored | boolean | No | |
|
||||
| moored_lock | boolean | No | |
|
||||
| canceled | boolean | No | |
|
||||
| evaluation | string | No | Evaluation of the ship call |
|
||||
| evaluation_message | string | No | |
|
||||
| time_ref_point | integer | No | Physical reference point for all times given in shipcall and depending times entries |
|
||||
| participants | array<object> | No | |
|
||||
| created | string | No | Readonly field set by the database when shipcall was created |
|
||||
| modified | string | No | Readonly field set by the database when shipcall was last modified |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
## Static Endpoints
|
||||
|
||||
### `GET /berths`
|
||||
**Summary:** Gets a list of all berths registered
|
||||
Returns a list of berths, including berths that are (logically) deleted
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**: list of berths
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
### `GET /history`
|
||||
**Summary:** History data
|
||||
This endpoint returns a list of changes made to the specific shipcall
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| shipcall_id | query | integer | Yes | **Id of ship call**. *Example: 3*. Id given in ship call list |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**: list of history entries
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
### `GET /notifications`
|
||||
**Summary:** Gets a list of notifications pursuant to a specified participant and ship call
|
||||
List of notifications (tbd)
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| shipcall_id | query | integer | No | **Id of ship call**. *Example: 52*. Id given in ship call list |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**: notification list
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
### `GET /participants`
|
||||
**Summary:** gets one or all participants
|
||||
If no parameter is given, all participants are returned. The list can be used to display participant information in the context of ship calls.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| user_id | query | integer | No | **Id of user**. *Example: 2*. User id returned by verify call. |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**: one or all participants as list
|
||||
- **400**
|
||||
- **401**
|
||||
- **404**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
### `GET /ports`
|
||||
**Summary:** Your GET endpoint
|
||||
Returns a list of ports
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**: list of ports
|
||||
- **401**
|
||||
- **403**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
## Times Endpoints
|
||||
|
||||
### `DELETE /times`
|
||||
**Summary:** Delete a times entry for a ship call.
|
||||
A times entry is typically deleted if the agent for example changes or removes the participant assignment for a particular role.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| id | query | integer | Yes | **Id of times**. *Example: 42*. Id of times entry to be deleted. |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
### `GET /times`
|
||||
**Summary:** Gets list of times
|
||||
Get all times assigned to a shipcall. These might not be complete.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------|----|------|----------|-------------|
|
||||
| shipcall_id | query | integer | No | **Id**. *Example: 42*. Id of referenced ship call. |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**: list of recorded times
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
### `POST /times`
|
||||
**Summary:** Create a new times entry for a ship call
|
||||
The times entry for a shipcall is created with reference to a participant. For each participant type there should be only one times data record.
|
||||
|
||||
#### Request Body
|
||||
Times entry that will be added to the ship call. **Do not** provide id parameter.
|
||||
|
||||
**JSON Schema**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| id | integer | No | |
|
||||
| eta_berth | string | No | Arrival time at berth |
|
||||
| eta_berth_fixed | boolean | No | If true, the eta is fixed and cannot be changed |
|
||||
| etd_berth | string | No | departure time from berth |
|
||||
| etd_berth_fixed | boolean | No | If true, the etd is fixed and cannot be changed |
|
||||
| lock_time | string | No | arrival time at lock |
|
||||
| lock_time_fixed | boolean | No | If true, the lock time is fixed and cannot be changed |
|
||||
| zone_entry | string | No | Expected time of entry into the zone |
|
||||
| zone_entry_fixed | boolean | No | If true, the zone entry time is fixed and cannot be changed |
|
||||
| operations_start | string | No | Start time for terminal operations |
|
||||
| operations_end | string | No | End time for terminal operations |
|
||||
| remarks | string | No | Additional remarks |
|
||||
| shipcall_id | integer | Yes | Reference to a shipcall id |
|
||||
| participant_id | integer | Yes | Reference to a participant id |
|
||||
| berth_id | integer | No | Reference to a berth id |
|
||||
| berth_info | string | No | Additional info text for berth |
|
||||
| pier_side | boolean | No | true if ship is rotated, false otherwise |
|
||||
| participant_type | integer | No | |
|
||||
| ata | string | No | ata can be set by mooring if actual times are different from planned |
|
||||
| atd | string | No | atd can be set by mooring if actual times are different from planned |
|
||||
| eta_interval_end | string | No | Optional end of the interval for the times eta entry |
|
||||
| etd_interval_end | string | No | Optional end of the interval for the times etd entry |
|
||||
| created | string | No | Readonly field set by the database when times record was created |
|
||||
| modified | string | No | Readonly field set by the database when times record was last modified |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **201**
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
### `PUT /times`
|
||||
**Summary:** Update a times entry for a ship call
|
||||
Updating a times entry for a ship for a particular participant. The times entries are required for a shipcall to pass the validation rules.
|
||||
|
||||
#### Request Body
|
||||
Times entry that will be added to the ship call. The id parameter is **required**.
|
||||
|
||||
**JSON Schema**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| id | integer | No | |
|
||||
| eta_berth | string | No | Arrival time at berth |
|
||||
| eta_berth_fixed | boolean | No | If true, the eta is fixed and cannot be changed |
|
||||
| etd_berth | string | No | departure time from berth |
|
||||
| etd_berth_fixed | boolean | No | If true, the etd is fixed and cannot be changed |
|
||||
| lock_time | string | No | arrival time at lock |
|
||||
| lock_time_fixed | boolean | No | If true, the lock time is fixed and cannot be changed |
|
||||
| zone_entry | string | No | Expected time of entry into the zone |
|
||||
| zone_entry_fixed | boolean | No | If true, the zone entry time is fixed and cannot be changed |
|
||||
| operations_start | string | No | Start time for terminal operations |
|
||||
| operations_end | string | No | End time for terminal operations |
|
||||
| remarks | string | No | Additional remarks |
|
||||
| shipcall_id | integer | Yes | Reference to a shipcall id |
|
||||
| participant_id | integer | Yes | Reference to a participant id |
|
||||
| berth_id | integer | No | Reference to a berth id |
|
||||
| berth_info | string | No | Additional info text for berth |
|
||||
| pier_side | boolean | No | true if ship is rotated, false otherwise |
|
||||
| participant_type | integer | No | |
|
||||
| ata | string | No | ata can be set by mooring if actual times are different from planned |
|
||||
| atd | string | No | atd can be set by mooring if actual times are different from planned |
|
||||
| eta_interval_end | string | No | Optional end of the interval for the times eta entry |
|
||||
| etd_interval_end | string | No | Optional end of the interval for the times etd entry |
|
||||
| created | string | No | Readonly field set by the database when times record was created |
|
||||
| modified | string | No | Readonly field set by the database when times record was last modified |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
|
||||
## User Endpoints
|
||||
|
||||
### `POST /login`
|
||||
**Summary:** Returns a JWT session token and user data if successful
|
||||
Perform login
|
||||
|
||||
#### Request Body
|
||||
Login credentials
|
||||
|
||||
**JSON Schema**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| username | string | Yes | |
|
||||
| password | string | Yes | |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**: Successful response
|
||||
- **400**
|
||||
- **403**
|
||||
- **500**
|
||||
- **503**
|
||||
|
||||
**Response 200 JSON Schema**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| id | integer | No | |
|
||||
| participant_id | integer | No | |
|
||||
| first_name | string | No | |
|
||||
| last_name | string | No | |
|
||||
| user_name | string | No | |
|
||||
| user_phone | string | No | |
|
||||
| user_email | string | No | |
|
||||
| notify_email | boolean | No | |
|
||||
| notify_whatsapp | boolean | No | |
|
||||
| notify_signal | boolean | No | |
|
||||
| notify_popup | boolean | No | |
|
||||
| exp | number | No | |
|
||||
| token | string | No | |
|
||||
| notify_on | array<string> | No | |
|
||||
|
||||
---
|
||||
|
||||
### `PUT /user`
|
||||
**Summary:** Update user details (first/last name, phone, password)
|
||||
Update user information
|
||||
|
||||
#### Request Body
|
||||
User details
|
||||
|
||||
**JSON Schema**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| id | integer | No | |
|
||||
| old_password | string | No | |
|
||||
| new_password | string | No | |
|
||||
| first_name | string | No | |
|
||||
| last_name | string | No | |
|
||||
| user_phone | string | No | |
|
||||
| user_email | string | No | |
|
||||
| notify_email | boolean | No | |
|
||||
| notify_popup | boolean | No | |
|
||||
| notify_whatsapp | boolean | No | |
|
||||
| notify_signal | boolean | No | |
|
||||
| notify_on | array<string> | No | |
|
||||
|
||||
#### Responses
|
||||
|
||||
- **200**
|
||||
- **400**
|
||||
- **401**
|
||||
- **500**
|
||||
- **503**
|
||||
---
|
||||
@ -2,7 +2,7 @@ openapi: 3.0.0
|
||||
x-stoplight:
|
||||
id: mwv4y8vcnopwr
|
||||
info:
|
||||
version: 1.7.0
|
||||
version: 1.6.0
|
||||
title: Bremen calling API
|
||||
description: 'Administer DEBRE ship calls, times and notifications'
|
||||
termsOfService: 'https://www.bsmd.de/'
|
||||
@ -14,16 +14,14 @@ info:
|
||||
name: Use at your own risk
|
||||
url: 'https://www.bsmd.de/license'
|
||||
servers:
|
||||
- url: 'https://brecaltest.bsmd-emswe.eu'
|
||||
description: Development server hosted on vcup
|
||||
- url: 'https://brecal.bsmd-emswe.eu'
|
||||
description: Test server hosted on vcup
|
||||
tags:
|
||||
- name: user
|
||||
- name: shipcall
|
||||
- name: times
|
||||
- name: static
|
||||
- name: ship
|
||||
- name: notification
|
||||
- name: history
|
||||
paths:
|
||||
/login:
|
||||
post:
|
||||
@ -32,7 +30,6 @@ paths:
|
||||
tags:
|
||||
- user
|
||||
operationId: login
|
||||
security: []
|
||||
requestBody:
|
||||
description: Login credentials
|
||||
required: true
|
||||
@ -259,7 +256,7 @@ paths:
|
||||
- shipcall
|
||||
operationId: shipcallUpdate
|
||||
requestBody:
|
||||
description: Updates a ship call. The id parameter is **required**.
|
||||
description: Creates a new ship call. The id parameter is **required**.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
@ -446,7 +443,7 @@ paths:
|
||||
- name: user_id
|
||||
in: query
|
||||
required: false
|
||||
description: '**Id of user**. *Example: 2*. User id returned by login call.'
|
||||
description: '**Id of user**. *Example: 2*. User id returned by verify call.'
|
||||
schema:
|
||||
type: integer
|
||||
example: 2
|
||||
@ -584,7 +581,7 @@ paths:
|
||||
- times
|
||||
operationId: timesUpdate
|
||||
requestBody:
|
||||
description: Times entry that will be updated for the ship call. The id parameter is **required**.
|
||||
description: Times entry that will be added to the ship call. The id parameter is **required**.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
@ -652,18 +649,18 @@ paths:
|
||||
$ref: '#/components/responses/503'
|
||||
/notifications:
|
||||
get:
|
||||
summary: Gets a list of notifications pursuant to a specified ship call
|
||||
summary: Gets a list of notifications pursuant to a specified participant and ship call
|
||||
description: List of notifications (tbd)
|
||||
tags:
|
||||
- notification
|
||||
- static
|
||||
operationId: notificationsGet
|
||||
parameters:
|
||||
- name: participant_id
|
||||
- name: shipcall_id
|
||||
in: query
|
||||
required: false
|
||||
description: '**Id of participant**. *Example: 7*. Id of logged in participant.'
|
||||
required: true
|
||||
description: '**Id of ship call**. *Example: 52*. Id given in ship call list'
|
||||
schema:
|
||||
$ref: '#/components/schemas/participant_id'
|
||||
$ref: '#/components/schemas/shipcallId'
|
||||
responses:
|
||||
'200':
|
||||
description: notification list
|
||||
@ -726,7 +723,7 @@ paths:
|
||||
description: This endpoint returns a list of changes made to the specific shipcall
|
||||
summary: History data
|
||||
tags:
|
||||
- history
|
||||
- static
|
||||
operationId: historyGet
|
||||
parameters:
|
||||
- name: shipcall_id
|
||||
@ -1541,7 +1538,6 @@ components:
|
||||
eta: '2023-08-21T08:23:35Z'
|
||||
operation: update
|
||||
type: shipcall
|
||||
|
||||
notification:
|
||||
type: object
|
||||
description: a notification created by the engine if a times entry violates a rule
|
||||
@ -1549,16 +1545,10 @@ components:
|
||||
id:
|
||||
type: integer
|
||||
example: 42
|
||||
nullable: false
|
||||
shipcall_id:
|
||||
type: integer
|
||||
example: 5
|
||||
nullable: false
|
||||
participant_id:
|
||||
type: integer
|
||||
example: 9
|
||||
nullable: true
|
||||
type:
|
||||
notification_type:
|
||||
$ref: '#/components/schemas/NotificationType'
|
||||
message:
|
||||
type: string
|
||||
@ -1577,9 +1567,8 @@ components:
|
||||
example:
|
||||
id: 42
|
||||
shipcall_id: 5
|
||||
participant_id: 9
|
||||
type: next24h
|
||||
message: Shipcall may be relevant to you in the next 24 hours
|
||||
notification_type: email
|
||||
message: Entry XY violates rule Z
|
||||
created: '2023-08-21T08:23:35Z'
|
||||
modified: '2023-08-21T08:23:35Z'
|
||||
notification_list:
|
||||
@ -1590,14 +1579,13 @@ components:
|
||||
example:
|
||||
- id: 42
|
||||
shipcall_id: 5
|
||||
participant_id: 9
|
||||
type: time_conflict
|
||||
notification_type: email
|
||||
message: Entry XY violates rule Z
|
||||
created: '2023-08-21T08:23:35Z'
|
||||
modified: '2023-08-21T08:23:35Z'
|
||||
- id: 43
|
||||
shipcall_id: 7
|
||||
type: time_conflict
|
||||
notification_type: email
|
||||
message: Entry AB violates rule C
|
||||
created: '2023-08-21T08:23:35Z'
|
||||
modified: '2023-08-21T08:23:35Z'
|
||||
@ -1710,28 +1698,10 @@ components:
|
||||
example: johndoe
|
||||
user_phone:
|
||||
type: string
|
||||
nullable: true
|
||||
example: '1234567890'
|
||||
user_email:
|
||||
type: string
|
||||
nullable: true
|
||||
example: no@where.com
|
||||
notify_email:
|
||||
type: boolean
|
||||
nullable: true
|
||||
example: true
|
||||
notify_whatsapp:
|
||||
type: boolean
|
||||
nullable: true
|
||||
example: false
|
||||
notify_signal:
|
||||
type: boolean
|
||||
nullable: true
|
||||
example: false
|
||||
notify_popup:
|
||||
type: boolean
|
||||
nullable: true
|
||||
example: false
|
||||
exp:
|
||||
type: number
|
||||
format: float
|
||||
@ -1739,14 +1709,6 @@ components:
|
||||
token:
|
||||
type: string
|
||||
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|
||||
notify_on:
|
||||
type: array
|
||||
nullable: true
|
||||
items:
|
||||
$ref: '#/components/schemas/NotificationType'
|
||||
example:
|
||||
- assignment
|
||||
- next24h
|
||||
example:
|
||||
id: 42
|
||||
participant_id: 5
|
||||
@ -1759,11 +1721,10 @@ components:
|
||||
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|
||||
user_details:
|
||||
type: object
|
||||
description: user metadata and editable fields
|
||||
description: fields that a user may change
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
readOnly: true
|
||||
example: 42
|
||||
old_password:
|
||||
type: string
|
||||
@ -1795,30 +1756,6 @@ components:
|
||||
type: string
|
||||
nullable: true
|
||||
example: no@where.com
|
||||
notify_email:
|
||||
type: boolean
|
||||
nullable: true
|
||||
example: true
|
||||
notify_popup:
|
||||
type: boolean
|
||||
nullable: true
|
||||
example: false
|
||||
notify_whatsapp:
|
||||
type: boolean
|
||||
nullable: true
|
||||
example: false
|
||||
notify_signal:
|
||||
type: boolean
|
||||
nullable: true
|
||||
example: false
|
||||
notify_on:
|
||||
type: array
|
||||
nullable: true
|
||||
items:
|
||||
$ref: '#/components/schemas/NotificationType'
|
||||
example:
|
||||
- assignment
|
||||
- next24h
|
||||
example:
|
||||
id: 42
|
||||
old_password: oldpassword
|
||||
@ -1867,14 +1804,10 @@ components:
|
||||
type: string
|
||||
description: Type of notification
|
||||
enum:
|
||||
- assignment
|
||||
- next24h
|
||||
- time_conflict
|
||||
- time_conflict_resolved
|
||||
- unassigned
|
||||
- missing_data
|
||||
- cancelled
|
||||
example: time_conflict
|
||||
- undefined
|
||||
- email
|
||||
- push
|
||||
example: email
|
||||
EvaluationType:
|
||||
description: Evaluation of the ship call
|
||||
readOnly: true
|
||||
|
||||
153
misc/Readme.md
153
misc/Readme.md
@ -48,156 +48,3 @@ DROP TABLE IF EXISTS `shipcall`;
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
```
|
||||
|
||||
## Schema
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
participant {
|
||||
INT id PK
|
||||
VARCHAR name
|
||||
VARCHAR street
|
||||
VARCHAR postal_code
|
||||
VARCHAR city
|
||||
INT type
|
||||
INT flags
|
||||
}
|
||||
port {
|
||||
INT id PK
|
||||
VARCHAR name
|
||||
CHAR locode
|
||||
}
|
||||
berth {
|
||||
INT id PK
|
||||
VARCHAR name
|
||||
BIT lock
|
||||
INT owner_id FK
|
||||
INT authority_id FK
|
||||
INT port_id FK
|
||||
BIT deleted
|
||||
}
|
||||
ship {
|
||||
INT id PK
|
||||
VARCHAR name
|
||||
INT imo
|
||||
VARCHAR callsign
|
||||
INT participant_id FK
|
||||
FLOAT length
|
||||
FLOAT width
|
||||
BIT is_tug
|
||||
INT bollard_pull
|
||||
INT eni
|
||||
BIT deleted
|
||||
}
|
||||
shipcall {
|
||||
INT id PK
|
||||
INT ship_id FK
|
||||
TINYINT type
|
||||
DATETIME eta
|
||||
DATETIME etd
|
||||
INT arrival_berth_id FK
|
||||
INT departure_berth_id FK
|
||||
INT port_id FK
|
||||
INT flags
|
||||
BIT tug_required
|
||||
BIT pilot_required
|
||||
}
|
||||
times {
|
||||
INT id PK
|
||||
INT shipcall_id FK
|
||||
INT participant_id FK
|
||||
INT berth_id FK
|
||||
INT participant_type
|
||||
DATETIME eta_berth
|
||||
DATETIME etd_berth
|
||||
DATETIME lock_time
|
||||
DATETIME zone_entry
|
||||
}
|
||||
notification {
|
||||
INT id PK
|
||||
INT shipcall_id FK
|
||||
INT participant_id FK
|
||||
TINYINT level
|
||||
TINYINT type
|
||||
}
|
||||
history {
|
||||
INT id PK
|
||||
INT participant_id FK
|
||||
INT user_id FK
|
||||
INT shipcall_id FK
|
||||
DATETIME timestamp
|
||||
DATETIME eta
|
||||
INT type
|
||||
INT operation
|
||||
}
|
||||
shipcall_participant_map {
|
||||
INT id PK
|
||||
INT shipcall_id FK
|
||||
INT participant_id FK
|
||||
INT type
|
||||
}
|
||||
shipcall_tug_map {
|
||||
INT id PK
|
||||
INT shipcall_id FK
|
||||
INT ship_id FK
|
||||
}
|
||||
participant_port_map {
|
||||
INT id PK
|
||||
INT participant_id FK
|
||||
INT port_id FK
|
||||
}
|
||||
user {
|
||||
INT id PK
|
||||
INT participant_id FK
|
||||
VARCHAR first_name
|
||||
VARCHAR last_name
|
||||
VARCHAR user_name
|
||||
VARCHAR user_email
|
||||
}
|
||||
role {
|
||||
INT id PK
|
||||
VARCHAR name
|
||||
VARCHAR description
|
||||
}
|
||||
securable {
|
||||
INT id PK
|
||||
VARCHAR name
|
||||
}
|
||||
role_securable_map {
|
||||
INT id PK
|
||||
INT role_id FK
|
||||
INT securable_id FK
|
||||
}
|
||||
user_role_map {
|
||||
INT id PK
|
||||
INT user_id FK
|
||||
INT role_id FK
|
||||
}
|
||||
|
||||
participant ||--o{ berth : owner_id
|
||||
participant ||--o{ berth : authority_id
|
||||
port ||--o{ berth : port_id
|
||||
participant ||--o{ ship : participant_id
|
||||
ship ||--o{ shipcall : ship_id
|
||||
berth ||--o{ shipcall : arrival_berth_id
|
||||
berth ||--o{ shipcall : departure_berth_id
|
||||
port ||--o{ shipcall : port_id
|
||||
shipcall ||--|| times : shipcall_id
|
||||
participant ||--|| times : participant_id
|
||||
berth ||--o{ times : berth_id
|
||||
shipcall ||--o{ notification : shipcall_id
|
||||
participant ||--o{ notification : participant_id
|
||||
participant ||--o{ history : participant_id
|
||||
user ||--o{ history : user_id
|
||||
shipcall ||--o{ history : shipcall_id
|
||||
shipcall ||--o{ shipcall_participant_map : shipcall_id
|
||||
participant ||--o{ shipcall_participant_map : participant_id
|
||||
shipcall ||--o{ shipcall_tug_map : shipcall_id
|
||||
ship ||--o{ shipcall_tug_map : ship_id
|
||||
participant ||--o{ participant_port_map : participant_id
|
||||
port ||--o{ participant_port_map : port_id
|
||||
participant ||--o{ user : participant_id
|
||||
user ||--o{ user_role_map : user_id
|
||||
role ||--o{ user_role_map : role_id
|
||||
role ||--o{ role_securable_map : role_id
|
||||
securable ||--o{ role_securable_map : securable_id
|
||||
```
|
||||
|
||||
@ -17,11 +17,3 @@ public bool ShouldSerializeEvaluation()
|
||||
```
|
||||
|
||||
Witziger(!)weise funktioniert es für das Property EvaluationMessage korrekt.
|
||||
|
||||
### Vacuum Yaml Linter
|
||||
|
||||
Example Usage:
|
||||
|
||||
```bash
|
||||
vacuum lint -d .\misc\BreCalApi.yaml --fail-severity warn
|
||||
```
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
# Versionshistorie
|
||||
|
||||
## 1.7
|
||||
|
||||
### YAML / API
|
||||
|
||||
1. Notifications GET: Der Parameter "shipcall_id" ist jetzt optional für den Abruf von Benachrichtigungen.
|
||||
|
||||
2. Notification: Enthält jetzt ein neues Feld "participant_id". Ist dieses gesetzt, richtet sich die Benachrichtigung an diesen Teilnehmer. Ist das Feld nicht vorhanden, richtet sich die Benachrichtigung an alle Beteiligten des shipcall
|
||||
|
||||
3. Die Benutzerdaten (login_result) enthalten jetzt die Felder (Flags) der Zuordnung für die verschiedenen Benachrichtigungs-Wege, aktuell implementiert ist notify_email und notify_popup. Diese können auch über user_details analog zu Telefonnummer, Name etc. gesetzt werden.
|
||||
|
||||
4. Die Enumeration NotificationType enthält jetzt nicht mehr den Benachrichtigungsweg, sondern den Typ des Ereignisses, das die Benachrichtigung ausgelöst hat. Aktuell werden 7 Ereignisse unterschieden.
|
||||
|
||||
5. Die Benutzerdaten enthalten eine Liste NotifyOn vom Typ NotificationType. In dieser Aufzählung sind die Ereignisse enthalten, über die der Benutzer benachrichtigt werden will. Wenn diese Liste leer oder nicht vorhanden ist erhält der Benutzer keine Nachrichten, auch wenn er einen Benachrichtigungsweg ausgewählt hat.
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
-- Inspect duplicates first
|
||||
WITH duplicate_participants AS (
|
||||
SELECT
|
||||
shipcall_id,
|
||||
participant_type,
|
||||
COUNT(*) AS cnt
|
||||
FROM times
|
||||
GROUP BY shipcall_id, participant_type
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
SELECT
|
||||
t.*
|
||||
FROM times AS t
|
||||
JOIN duplicate_participants AS d
|
||||
ON d.shipcall_id = t.shipcall_id
|
||||
AND (d.participant_type <=> t.participant_type)
|
||||
ORDER BY t.shipcall_id, t.participant_type, t.id;
|
||||
|
||||
-- Delete all but the highest-id entry per (shipcall_id, participant_type)
|
||||
WITH ordered_times AS (
|
||||
SELECT
|
||||
id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY shipcall_id, participant_type
|
||||
ORDER BY id DESC
|
||||
) AS rn
|
||||
FROM times
|
||||
)
|
||||
DELETE FROM times
|
||||
WHERE id IN (
|
||||
SELECT id
|
||||
FROM ordered_times
|
||||
WHERE rn > 1
|
||||
);
|
||||
|
||||
-- Optional: re-check that no duplicates remain
|
||||
WITH duplicate_participants AS (
|
||||
SELECT
|
||||
shipcall_id,
|
||||
participant_type,
|
||||
COUNT(*) AS cnt
|
||||
FROM times
|
||||
GROUP BY shipcall_id, participant_type
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
SELECT COUNT(*) AS remaining_duplicates FROM duplicate_participants;
|
||||
@ -1,37 +0,0 @@
|
||||
-- This script clears all data from the database tables related to the port management system.
|
||||
|
||||
DELETE FROM notification WHERE id > 0;
|
||||
|
||||
DELETE FROM history WHERE id > 0;
|
||||
|
||||
DELETE FROM notification WHERE id > 0;
|
||||
|
||||
DELETE FROM shipcall_participant_map WHERE id > 0;
|
||||
|
||||
DELETE FROM participant_port_map WHERE id > 0;
|
||||
|
||||
DELETE FROM shipcall_tug_map WHERE id > 0;
|
||||
|
||||
DELETE FROM times WHERE id > 0;
|
||||
|
||||
DELETE FROM shipcall WHERE id > 0;
|
||||
|
||||
DELETE FROM user_role_map WHERE id > 0;
|
||||
|
||||
DELETE FROM role_securable_map WHERE id > 0;
|
||||
|
||||
DELETE FROM user_role_map WHERE id > 0;
|
||||
|
||||
DELETE FROM securable WHERE id > 0;
|
||||
|
||||
DELETE FROM role WHERE id > 0;
|
||||
|
||||
DELETE FROM user WHERE id > 0;
|
||||
|
||||
delete FROM ship WHERE id > 0;
|
||||
|
||||
DELETE FROM berth WHERE id > 0;
|
||||
|
||||
DELETE FROM participant WHERE id > 0;
|
||||
|
||||
DELETE FROM port WHERE id > 0;
|
||||
@ -1,8 +1,8 @@
|
||||
-- MySQL dump 10.13 Distrib 8.0.43, for Win64 (x86_64)
|
||||
-- MySQL dump 10.13 Distrib 8.0.33, for Win64 (x86_64)
|
||||
--
|
||||
-- Host: localhost Database: bremen_calling_test
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 8.0.42-0ubuntu0.24.10.1
|
||||
-- Server version 8.0.34-0ubuntu0.22.04.1
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
@ -28,65 +28,17 @@ CREATE TABLE `berth` (
|
||||
`lock` bit(1) DEFAULT NULL COMMENT 'The lock must be used',
|
||||
`owner_id` int unsigned DEFAULT NULL,
|
||||
`authority_id` int unsigned DEFAULT NULL,
|
||||
`port_id` int unsigned DEFAULT NULL,
|
||||
`created` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`deleted` bit(1) DEFAULT b'0',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `FK_OWNER_PART_idx` (`owner_id`),
|
||||
KEY `FK_AUTHORITY_PART_idx` (`authority_id`) /*!80000 INVISIBLE */,
|
||||
KEY `FK_PORT_PART_idx` (`port_id`),
|
||||
KEY `FK_AUTHORITY_PART_idx` (`authority_id`),
|
||||
CONSTRAINT `FK_AUTHORITY_PART` FOREIGN KEY (`authority_id`) REFERENCES `participant` (`id`),
|
||||
CONSTRAINT `FK_OWNER_PART` FOREIGN KEY (`owner_id`) REFERENCES `participant` (`id`),
|
||||
CONSTRAINT `FK_PORT` FOREIGN KEY (`port_id`) REFERENCES `port` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=205 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Berth of ship for a ship call';
|
||||
CONSTRAINT `FK_OWNER_PART` FOREIGN KEY (`owner_id`) REFERENCES `participant` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=195 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Berth of ship for a ship call';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `history`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `history`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `history` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`participant_id` int unsigned NOT NULL,
|
||||
`user_id` int unsigned DEFAULT NULL,
|
||||
`shipcall_id` int unsigned NOT NULL,
|
||||
`timestamp` datetime NOT NULL COMMENT 'Time of saving',
|
||||
`eta` datetime DEFAULT NULL COMMENT 'Current ETA / ETD value (depends if shipcall or times were saved)',
|
||||
`type` int NOT NULL COMMENT 'shipcall or times',
|
||||
`operation` int NOT NULL COMMENT 'insert, update or delete',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `FK_HISTORY_PARTICIPANT_idx` (`participant_id`),
|
||||
KEY `FK_HISTORY_SHIPCALL_idx` (`shipcall_id`),
|
||||
KEY `FK_HISTORY_USER` (`user_id`),
|
||||
CONSTRAINT `FK_HISTORY_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`),
|
||||
CONSTRAINT `FK_HISTORY_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`),
|
||||
CONSTRAINT `FK_HISTORY_USER` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=23537 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='This table stores a history of changes made to shipcalls so that everyone can see who changed what and when';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
CREATE TABLE `history` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`participant_id` int unsigned NOT NULL,
|
||||
`user_id` int unsigned DEFAULT NULL,
|
||||
`shipcall_id` int unsigned NOT NULL,
|
||||
`timestamp` datetime NOT NULL COMMENT 'Time of saving',
|
||||
`eta` datetime DEFAULT NULL COMMENT 'Current ETA / ETD value (depends if shipcall or times were saved)',
|
||||
`type` int NOT NULL COMMENT 'shipcall or times',
|
||||
`operation` int NOT NULL COMMENT 'insert, update or delete',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `FK_HISTORY_PARTICIPANT_idx` (`participant_id`),
|
||||
KEY `FK_HISTORY_SHIPCALL_idx` (`shipcall_id`),
|
||||
KEY `FK_HISTORY_USER` (`user_id`),
|
||||
CONSTRAINT `FK_HISTORY_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`),
|
||||
CONSTRAINT `FK_HISTORY_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`),
|
||||
CONSTRAINT `FK_HISTORY_USER` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=29292 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='This table stores a history of changes made to shipcalls so that everyone can see who changed what and when';
|
||||
|
||||
|
||||
--
|
||||
-- Table structure for table `notification`
|
||||
--
|
||||
@ -96,19 +48,20 @@ DROP TABLE IF EXISTS `notification`;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `notification` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`shipcall_id` int unsigned DEFAULT NULL,
|
||||
`participant_id` int unsigned DEFAULT NULL,
|
||||
`times_id` int unsigned NOT NULL COMMENT 'times record that caused the notification',
|
||||
`participant_id` int unsigned NOT NULL COMMENT 'participant ref',
|
||||
`acknowledged` bit(1) DEFAULT b'0' COMMENT 'true if UI acknowledged',
|
||||
`level` tinyint DEFAULT NULL COMMENT 'severity of the notification',
|
||||
`type` tinyint DEFAULT NULL COMMENT 'Email/UI/Other',
|
||||
`message` varchar(512) DEFAULT NULL COMMENT 'individual message',
|
||||
`message` varchar(256) DEFAULT NULL COMMENT 'individual message',
|
||||
`created` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `FK_NOTIFICATION_SHIPCALL_idx` (`shipcall_id`),
|
||||
KEY `FK_NOTIFICATION_PARTICIPANT_idx` (`participant_id`),
|
||||
CONSTRAINT `FK_NOTIFICATION_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT `FK_NOTIFICATION_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=10398 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='An entry corresponds to an alarm given by a violated rule during times update';
|
||||
KEY `FK_NOT_TIMES` (`times_id`),
|
||||
KEY `FK_NOT_PART` (`participant_id`),
|
||||
CONSTRAINT `FK_NOT_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`),
|
||||
CONSTRAINT `FK_NOT_TIMES` FOREIGN KEY (`times_id`) REFERENCES `times` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='An entry corresponds to an alarm given by a violated rule during times update';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
@ -130,46 +83,7 @@ CREATE TABLE `participant` (
|
||||
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`deleted` bit(1) DEFAULT b'0',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=160 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='An organization taking part';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `participant_port_map`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `participant_port_map`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `participant_port_map` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`participant_id` int unsigned NOT NULL COMMENT 'Ref to participant',
|
||||
`port_id` int unsigned NOT NULL COMMENT 'Ref to port',
|
||||
`created` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `FK_PP_PARTICIPANT` (`participant_id`),
|
||||
KEY `FK_PP_PORT` (`port_id`),
|
||||
CONSTRAINT `FK_PP_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`),
|
||||
CONSTRAINT `FK_PP_PORT` FOREIGN KEY (`port_id`) REFERENCES `port` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=86 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Mapping table that assigns participants to a port';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `port`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `port`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `port` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(128) NOT NULL COMMENT 'Name of port',
|
||||
`locode` char(5) DEFAULT NULL COMMENT 'UNECE locode',
|
||||
`created` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`deleted` bit(1) DEFAULT b'0',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Port as reference for shipcalls and berths';
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=137 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='An organization taking part';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
@ -252,7 +166,7 @@ CREATE TABLE `ship` (
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `FK_SHIP_PARTICIPANT` (`participant_id`),
|
||||
CONSTRAINT `FK_SHIP_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=485 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
@ -288,25 +202,16 @@ CREATE TABLE `shipcall` (
|
||||
`canceled` bit(1) DEFAULT NULL,
|
||||
`evaluation` int unsigned DEFAULT NULL,
|
||||
`evaluation_message` varchar(512) DEFAULT NULL,
|
||||
`evaluation_time` datetime DEFAULT NULL,
|
||||
`evaluation_notifications_sent` bit(1) DEFAULT NULL,
|
||||
`port_id` int unsigned NOT NULL DEFAULT '1' COMMENT 'Selected port for this shipcall',
|
||||
`time_ref_point` int DEFAULT '0' COMMENT 'Index of a location which is the reference point for all time value entries, e.g. berth or Geeste',
|
||||
`created` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `FK_SHIPCALL_SHIP` (`ship_id`),
|
||||
KEY `FK_SHIPCALL_BERTH_ARRIVAL` (`arrival_berth_id`),
|
||||
KEY `FK_SHIPCALL_BERTH_DEPARTURE` (`departure_berth_id`),
|
||||
KEY `idx_shipcall_type` (`type`),
|
||||
KEY `idx_shipcall_eta` (`eta`),
|
||||
KEY `idx_shipcall_etd` (`etd`),
|
||||
KEY `FK_SHIPCALL_PORT_idx` (`port_id`),
|
||||
CONSTRAINT `FK_SHIPCALL_BERTH_ARRIVAL` FOREIGN KEY (`arrival_berth_id`) REFERENCES `berth` (`id`),
|
||||
CONSTRAINT `FK_SHIPCALL_BERTH_DEPARTURE` FOREIGN KEY (`departure_berth_id`) REFERENCES `berth` (`id`),
|
||||
CONSTRAINT `FK_SHIPCALL_PORT` FOREIGN KEY (`port_id`) REFERENCES `port` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT `FK_SHIPCALL_SHIP` FOREIGN KEY (`ship_id`) REFERENCES `ship` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2789 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Incoming, outgoing or moving to another berth';
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Incoming, outgoing or moving to another berth';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
@ -320,15 +225,15 @@ CREATE TABLE `shipcall_participant_map` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`shipcall_id` int unsigned DEFAULT NULL,
|
||||
`participant_id` int unsigned DEFAULT NULL,
|
||||
`type` int unsigned DEFAULT NULL,
|
||||
`type` int unsigned DEFAULT NULL COMMENT 'Type of participant role',
|
||||
`created` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `FK_MAP_PARTICIPANT_SHIPCALL` (`shipcall_id`),
|
||||
KEY `FK_MAP_SHIPCALL_PARTICIPANT` (`participant_id`),
|
||||
CONSTRAINT `FK_MAP_PARTICIPANT_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`) ON DELETE SET NULL,
|
||||
CONSTRAINT `FK_MAP_PARTICIPANT_SHIPCALL` FOREIGN KEY (`shipcall_id`) REFERENCES `shipcall` (`id`),
|
||||
CONSTRAINT `FK_MAP_SHIPCALL_PARTICIPANT` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=8933 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Associates a participant with a shipcall';
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=128 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Associates a participant with a shipcall';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
@ -380,20 +285,13 @@ CREATE TABLE `times` (
|
||||
`berth_info` varchar(512) DEFAULT NULL,
|
||||
`pier_side` bit(1) DEFAULT NULL,
|
||||
`participant_type` int unsigned DEFAULT NULL,
|
||||
`ata` datetime DEFAULT NULL COMMENT 'Relevant only for mooring, this field can be used to record actual ATA',
|
||||
`atd` datetime DEFAULT NULL COMMENT 'Relevant only for mooring, this field can be used to record actual ATD',
|
||||
`eta_interval_end` datetime DEFAULT NULL COMMENT 'If this value is set the times are given as interval instead of a single point in time. The start time value depends on the participant type.',
|
||||
`etd_interval_end` datetime DEFAULT NULL COMMENT 'If this value is set the times are given as interval instead of a single point in time. The start time value depends on the participant type.',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_shipcall_participant` (`shipcall_id`,`participant_type`),
|
||||
KEY `FK_TIME_SHIPCALL` (`shipcall_id`),
|
||||
KEY `FK_TIME_PART` (`participant_id`) /*!80000 INVISIBLE */,
|
||||
KEY `FK_TIME_BERTH` (`berth_id`) /*!80000 INVISIBLE */,
|
||||
KEY `idx_times_eta_berth` (`eta_berth`),
|
||||
KEY `idx_times_etd_berth` (`etd_berth`),
|
||||
CONSTRAINT `FK_TIME_BERTH` FOREIGN KEY (`berth_id`) REFERENCES `berth` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT `FK_TIME_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=7863 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='the planned time for the participants work';
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='the planned time for the participants work';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
@ -405,7 +303,7 @@ DROP TABLE IF EXISTS `user`;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `user` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`participant_id` int unsigned NOT NULL,
|
||||
`participant_id` int unsigned DEFAULT NULL,
|
||||
`first_name` varchar(45) DEFAULT NULL,
|
||||
`last_name` varchar(45) DEFAULT NULL,
|
||||
`user_name` varchar(45) DEFAULT NULL,
|
||||
@ -413,17 +311,12 @@ CREATE TABLE `user` (
|
||||
`user_phone` varchar(128) DEFAULT NULL,
|
||||
`password_hash` varchar(128) DEFAULT NULL,
|
||||
`api_key` varchar(256) DEFAULT NULL,
|
||||
`notify_email` bit(1) DEFAULT NULL,
|
||||
`notify_whatsapp` bit(1) DEFAULT NULL,
|
||||
`notify_signal` bit(1) DEFAULT NULL,
|
||||
`notify_popup` bit(1) DEFAULT NULL,
|
||||
`notify_event` int DEFAULT NULL COMMENT 'Bitflag of selected notification event types that the user wants to be notified of',
|
||||
`created` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
`modified` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `FK_USER_PART` (`participant_id`),
|
||||
CONSTRAINT `FK_USER_PART` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='member of a participant';
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='member of a participant';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
@ -446,57 +339,6 @@ CREATE TABLE `user_role_map` (
|
||||
CONSTRAINT `FK_USER_ROLE` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Assigns a user to a role';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping routines for database 'bremen_calling_test'
|
||||
--
|
||||
/*!50003 DROP PROCEDURE IF EXISTS `delete_data` */;
|
||||
/*!50003 SET @saved_cs_client = @@character_set_client */ ;
|
||||
/*!50003 SET @saved_cs_results = @@character_set_results */ ;
|
||||
/*!50003 SET @saved_col_connection = @@collation_connection */ ;
|
||||
/*!50003 SET character_set_client = utf8mb4 */ ;
|
||||
/*!50003 SET character_set_results = utf8mb4 */ ;
|
||||
/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ;
|
||||
/*!50003 SET @saved_sql_mode = @@sql_mode */ ;
|
||||
/*!50003 SET sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION' */ ;
|
||||
DELIMITER ;;
|
||||
CREATE DEFINER=`ds`@`localhost` PROCEDURE `delete_data`()
|
||||
BEGIN
|
||||
DECLARE shipcall_id_var int;
|
||||
DECLARE done INT DEFAULT FALSE;
|
||||
|
||||
DECLARE shipcall_iter CURSOR FOR
|
||||
SELECT shipcall.id FROM shipcall
|
||||
LEFT JOIN times ON
|
||||
times.shipcall_id = shipcall.id AND times.participant_type = 8
|
||||
WHERE
|
||||
-- ARRIVAL
|
||||
(type = 1 AND GREATEST(shipcall.eta, COALESCE(times.eta_berth, 0)) <= CURRENT_DATE() - INTERVAL 1 MONTH) OR
|
||||
-- DEPARTURE / SHIFTING
|
||||
(type != 1 AND GREATEST(shipcall.etd, COALESCE(times.etd_berth, 0)) <= CURRENT_DATE() - INTERVAL 1 MONTH);
|
||||
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
|
||||
|
||||
OPEN shipcall_iter;
|
||||
|
||||
delete_loop: LOOP
|
||||
FETCH shipcall_iter INTO shipcall_id_var;
|
||||
IF done THEN
|
||||
LEAVE delete_loop;
|
||||
END IF;
|
||||
DELETE FROM shipcall_participant_map WHERE shipcall_id = shipcall_id_var;
|
||||
DELETE FROM shipcall_tug_map WHERE shipcall_id = shipcall_id_var;
|
||||
DELETE FROM times WHERE shipcall_id = shipcall_id_var;
|
||||
DELETE FROM history WHERE shipcall_id = shipcall_id_var;
|
||||
DELETE FROM shipcall WHERE id = shipcall_id_var;
|
||||
END LOOP;
|
||||
CLOSE shipcall_iter;
|
||||
END ;;
|
||||
DELIMITER ;
|
||||
/*!50003 SET sql_mode = @saved_sql_mode */ ;
|
||||
/*!50003 SET character_set_client = @saved_cs_client */ ;
|
||||
/*!50003 SET character_set_results = @saved_cs_results */ ;
|
||||
/*!50003 SET collation_connection = @saved_col_connection */ ;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
@ -507,4 +349,4 @@ DELIMITER ;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2025-11-17 8:26:36
|
||||
-- Dump completed on 2023-10-06 14:52:04
|
||||
|
||||
BIN
misc/favicon.ico
BIN
misc/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB |
@ -1,11 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Bremen Calling</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,113 +0,0 @@
|
||||
# Benachrichtigungen
|
||||
|
||||
___
|
||||
|
||||
## Benachrichtigungs-Typen (Auslöser)
|
||||
|
||||
### 1. Teilnehmer wird zugeordnet
|
||||
|
||||
Ein Teilnehmer wird über die Anwendung einem Anlauf zugeordnet. Dies ist entweder die Agentur (durch BSMD zugeordnet) oder ein weiterer Teilnehmer außer dem Hafenamt, der durch die Agentur zugeordnet wird. Die Zuordnung des Hafenamts erfolgt automatisch bei Anlage des Anlaufs.
|
||||
|
||||
### 2. Morgenrunde ist relevant
|
||||
|
||||
Ein Teilnehmer ist einem Anlauf zugeordnet. Dieser Anlauf findet in den nächsten 24 Stunden statt und ist daher für die "Morgenrunde" relevant. Der Teilnehmer erhält dazu eine Benachrichtigung.
|
||||
|
||||
### 3. Zeitlicher Konflikt ("Ampel")
|
||||
|
||||
Durch unterschiedliche Zeitangaben der Teilnehmer wird die Ampel ausgelöst und stellt eine entsprechende Fehlermeldung dar. Die Benachrichtigung wird ausgelöst bei folgenden Ampel-Wechseln:
|
||||
|
||||
* grün -> gelb
|
||||
* grün -> rot
|
||||
* gelb -> rot
|
||||
|
||||
### 4. Auflösung zeitl. Konflikt
|
||||
|
||||
* rot -> gelb
|
||||
* rot -> grün
|
||||
* gelb -> grün
|
||||
|
||||
### 5. Abwählen eines Teilnehmer
|
||||
|
||||
Der Teilnehmer ist nicht mehr länger dem Anlauf zugeordnet.
|
||||
|
||||
### 6. Fehlende Daten
|
||||
|
||||
Dienstleister, die 16 Stunden vor ETA/ETD und Agenturen, die 20 Stunden vor ETA/ETD keine Angaben gemacht haben.
|
||||
|
||||
### 7. Storno
|
||||
|
||||
Wird ein Anlauf storniert erhalten alle bis dahin zugeordneten Teilnehmer eine Benachrichtigung.
|
||||
|
||||
## API
|
||||
|
||||
```yaml
|
||||
NotificationType:
|
||||
type: string
|
||||
description: Type of notification
|
||||
enum:
|
||||
- assignment
|
||||
- next24h
|
||||
- time_conflict
|
||||
- time_conflict_resolved
|
||||
- unassigned
|
||||
- missing_data
|
||||
- cancelled
|
||||
```
|
||||
|
||||
## Entfernen von Benachrichtigungen
|
||||
|
||||
Unter den folgenden Voraussetzungen werden Benachrichtigungen wieder aus dem System entfernt:
|
||||
|
||||
* Die Benachrichtigung ist älter als 3 Tage (n.B.: Zeitraum definieren)
|
||||
* Ein Teilnehmer wird wieder abgewählt
|
||||
* Ein zeitlicher Konflikt wird aufgelöst
|
||||
|
||||
## Ablauf der Benachrichtigungen
|
||||
|
||||
Eine Benachrichtung enthält folgende Informationen:
|
||||
|
||||
* Verweis auf den Anlauf (shipcall)
|
||||
* ein Erstell- und Änderungsdatum
|
||||
* einen Benachrichtigungs-Typ
|
||||
* einen Zustand ("level")
|
||||
|
||||
Der Zustand steuert den Ablauf, wenn die Prüfungsfunktion die Anläufe durchsucht oder ein Anlauf gespeichert wird.
|
||||
|
||||
Wird einer der Zustände 1-3 erkannt wird geprüft, ob bereits eine Benachrichtigung vorhanden ist. Ist dies nicht der Fall, wird eine Benachrichtigung neu erstellt im Zustand "0".
|
||||
|
||||
Die Prüfungsfunktion durchläuft alle Benachrichtigungen. Abhängig vom Zustand (0-2) werden folgende Aktionen ausgeführt:
|
||||
|
||||
* Ist die Benachrichtigung um Zustand "0" und sind mind. 10 Minuten vergangen, wird die Benachrichtigung in den Zustand "1" versetzt.
|
||||
|
||||
* Ist die Benachrichtigung im Zustand "1" wird versucht, allen dafür eingetragenenen Benutzern eine E-Mail zu senden. Ist dies erfolgreich, wechselt die Benachrichtigung in den Zustand "2".
|
||||
|
||||
* Ist die Benachrichtigung im Zustand "2" und sind mind. 3 Tage vergangen wird die Benachrichtigung gelöscht.
|
||||
|
||||
```mermaid
|
||||
---
|
||||
title: Ablauf
|
||||
---
|
||||
stateDiagram-v2
|
||||
state "Level 0" as lvl0
|
||||
state "Level 1" as lvl1
|
||||
state "Level 2" as lvl2
|
||||
[*] --> lvl0: Zustand 1-3 erkannt
|
||||
lvl0 --> lvl1: +10 min.
|
||||
lvl1 --> lvl2: E-Mail Versand erfolgt
|
||||
lvl2 --> [*]: +3 Tage ODER Zustand 1-3 nicht mehr relevant
|
||||
|
||||
|
||||
lvl0 --> [*]: Zustand 1-3 nicht mehr relevant
|
||||
|
||||
```
|
||||
|
||||
## Bemerkungen
|
||||
|
||||
Für die Zukunft sind ggf. auch Benachrichtigungen via Whatsapp/Signal geplant. Diese verhalten sich analog zu den E-Mail Benachrichtigungen und werden nur einmal versendet. Die Anzahl der Zustände wird dabei erhöht bzw. der Wechsel in den Endzustand ensprechend angepasst.
|
||||
|
||||
## Benachrichtigungstext
|
||||
|
||||
... TBD
|
||||
|
||||
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
ALTER TABLE `notification`
|
||||
ADD COLUMN `participant_id` INT UNSIGNED NULL DEFAULT NULL AFTER `shipcall_id`,
|
||||
ADD INDEX `FK_NOTIFICATION_PARTICIPANT_idx` (`participant_id` ASC) VISIBLE;
|
||||
;
|
||||
ALTER TABLE `notification`
|
||||
ADD CONSTRAINT `FK_NOTIFICATION_PARTICIPANT`
|
||||
FOREIGN KEY (`participant_id`)
|
||||
REFERENCES `participant` (`id`)
|
||||
ON DELETE RESTRICT
|
||||
ON UPDATE RESTRICT;
|
||||
|
||||
ALTER TABLE `user`
|
||||
ADD COLUMN `notify_event` INT NULL COMMENT 'Bitflag of selected notification event types that the user wants to be notified of' AFTER `notify_popup`;
|
||||
@ -1 +1 @@
|
||||
1.8.0.0
|
||||
1.6.0.8
|
||||
@ -1,10 +0,0 @@
|
||||
# Schnittstelle Weserport Anforderungen
|
||||
|
||||
##
|
||||
|
||||
Automatische Zuordnung:
|
||||
|
||||
* Hafenamt
|
||||
* Festmacher
|
||||
* Lotsen (>120m l 13m br)
|
||||
|
||||
@ -7,12 +7,11 @@
|
||||
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
|
||||
xmlns:p = "clr-namespace:BreCalClient.Resources"
|
||||
mc:Ignorable="d" Left="{local:SettingBinding W1Left}" Top="{local:SettingBinding W1Top}"
|
||||
Title="Help" Height="512" Width="800" Loaded="Window_Loaded">
|
||||
Title="Help" Height="374" Width="500" Loaded="Window_Loaded">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="180" />
|
||||
<ColumnDefinition Width=".5*" />
|
||||
<ColumnDefinition Width=".5*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="28" />
|
||||
@ -26,13 +25,6 @@
|
||||
<RowDefinition Height="28" />
|
||||
<RowDefinition Height="28" />
|
||||
<RowDefinition Height="28" />
|
||||
<RowDefinition Height="28" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="28" />
|
||||
<RowDefinition Height="28" />
|
||||
<RowDefinition Height="28" />
|
||||
<RowDefinition Height="28" />
|
||||
<RowDefinition Height="10" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="28" />
|
||||
|
||||
@ -50,33 +42,21 @@
|
||||
Informatikbüro Daniel Schick
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
<Border BorderThickness="0 0 0 2" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" BorderBrush="Gray" />
|
||||
|
||||
<Label FontWeight="DemiBold" Grid.Row="3" Grid.Column="0" Content="{x:Static p:Resources.textChangeContactInfo}" HorizontalContentAlignment="Right"/>
|
||||
<Label Grid.Row="4" Grid.Column="0" Content="{x:Static p:Resources.textEmail}" HorizontalContentAlignment="Right" />
|
||||
<Label Grid.Row="5" Grid.Column="0" Content="{x:Static p:Resources.textPhone}" HorizontalContentAlignment="Right" />
|
||||
<TextBox Name="textBoxUserEmail" Grid.Column="1" Grid.Row="4" Margin="2" VerticalContentAlignment="Center" />
|
||||
<TextBox Name="textBoxUserPhone" Grid.Column="1" Grid.Row="5" Margin="2" VerticalContentAlignment="Center" />
|
||||
|
||||
<Label FontWeight="DemiBold" Grid.Row="7" Grid.Column="0" Content="{x:Static p:Resources.textNotifications}" HorizontalContentAlignment="Right"/>
|
||||
<CheckBox HorizontalAlignment="Right" Margin="2" Grid.Column="0" Grid.Row="8" VerticalAlignment="Center" x:Name="checkboxEMailNotify" />
|
||||
<Label Grid.Row="8" Grid.Column="1" Content="{x:Static p:Resources.textNotifyEmail}" />
|
||||
<CheckBox HorizontalAlignment="Right" Margin="2" Grid.Column="0" Grid.Row="9" VerticalAlignment="Center" x:Name="checkboxPushNotify" />
|
||||
<Label Grid.Row="9" Grid.Column="1" Content="{x:Static p:Resources.textNotifyPush}" />
|
||||
<Label Grid.Row="7" Grid.Column="2" Content="{x:Static p:Resources.textNotifyOn}" />
|
||||
<xctk:CheckListBox Grid.Column="2" Grid.Row="8" Grid.RowSpan="3" x:Name="checkListBoxEventSelection" Margin="2" />
|
||||
<Button x:Name="buttonChangeUserFields" Click="buttonChangeUserFields_Click" Grid.Column="2" Grid.Row="11" Margin="2" Content="{x:Static p:Resources.textChange}" Width="80" HorizontalAlignment="Right" IsEnabled="True" />
|
||||
|
||||
<Border BorderThickness="0 0 0 2" Grid.Row="12" Grid.Column="0" Grid.ColumnSpan="3" BorderBrush="Gray" />
|
||||
<Label FontWeight="DemiBold" Grid.Row="13" Grid.Column="0" Content="{x:Static p:Resources.textChangePassword}" HorizontalContentAlignment="Right"/>
|
||||
|
||||
<xctk:WatermarkPasswordBox Watermark="{x:Static p:Resources.textOldPassword}" Grid.Column="1" Grid.Row="13" Margin="2" x:Name="wpBoxOldPassword" TextChanged="wpBoxOldPassword_TextChanged"/>
|
||||
<xctk:WatermarkPasswordBox Watermark="{x:Static p:Resources.textNewPassword}" Grid.Column="1" Grid.Row="14" Margin="2" x:Name="wpBoxNewPassword" TextChanged="wpBoxOldPassword_TextChanged"/>
|
||||
<xctk:WatermarkPasswordBox Watermark="{x:Static p:Resources.textRepeatNewPassword}" Grid.Column="1" Grid.Row="15" Margin="2" x:Name="wpBoxNewPasswordRepeat" TextChanged="wpBoxOldPassword_TextChanged"/>
|
||||
<Button x:Name="buttonChangePassword" Click="buttonChangePassword_Click" Grid.Column="1" Grid.Row="16" Margin="2" Content="{x:Static p:Resources.textChangePassword}" Width="120" HorizontalAlignment="Right" IsEnabled="False" />
|
||||
<Border BorderThickness="0 0 0 2" Grid.Row="17" Grid.Column="0" Grid.ColumnSpan="3" BorderBrush="Gray" />
|
||||
<Label FontWeight="DemiBold" Grid.Row="7" Grid.Column="0" Content="{x:Static p:Resources.textChangePassword}" HorizontalContentAlignment="Right"/>
|
||||
|
||||
<xctk:WatermarkPasswordBox Watermark="{x:Static p:Resources.textOldPassword}" Grid.Column="1" Grid.Row="7" Margin="2" x:Name="wpBoxOldPassword" TextChanged="wpBoxOldPassword_TextChanged"/>
|
||||
<xctk:WatermarkPasswordBox Watermark="{x:Static p:Resources.textNewPassword}" Grid.Column="1" Grid.Row="8" Margin="2" x:Name="wpBoxNewPassword" TextChanged="wpBoxOldPassword_TextChanged"/>
|
||||
<xctk:WatermarkPasswordBox Watermark="{x:Static p:Resources.textRepeatNewPassword}" Grid.Column="1" Grid.Row="9" Margin="2" x:Name="wpBoxNewPasswordRepeat" TextChanged="wpBoxOldPassword_TextChanged"/>
|
||||
<Button x:Name="buttonChangePassword" Click="buttonChangePassword_Click" Grid.Column="1" Grid.Row="10" Margin="2" Content="{x:Static p:Resources.textChange}" Width="80" HorizontalAlignment="Left" IsEnabled="True" />
|
||||
|
||||
<Button x:Name="buttonClose" Click="buttonClose_Click" Content="{x:Static p:Resources.textClose}" Width="80" Margin="2" Grid.Column="2" Grid.Row="19" HorizontalAlignment="Right" />
|
||||
<Button x:Name="buttonClose" Click="buttonClose_Click" Content="{x:Static p:Resources.textClose}" Width="80" Margin="2" Grid.Column="1" Grid.Row="12" HorizontalAlignment="Right" />
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@ -34,7 +34,6 @@ namespace BreCalClient
|
||||
#region events
|
||||
|
||||
public event Action<string, string>? ChangePasswordRequested;
|
||||
public event Action? ChangeUserSettingsRequested;
|
||||
|
||||
#endregion
|
||||
|
||||
@ -46,25 +45,14 @@ namespace BreCalClient
|
||||
}
|
||||
|
||||
private void buttonChangePassword_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.ChangePasswordRequested?.Invoke(this.wpBoxOldPassword.Password, this.wpBoxNewPassword.Password);
|
||||
}
|
||||
|
||||
private void buttonChangeUserFields_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (this.LoginResult != null)
|
||||
{
|
||||
this.LoginResult.UserPhone = this.textBoxUserPhone.Text.Trim();
|
||||
this.LoginResult.UserEmail = this.textBoxUserEmail.Text.Trim();
|
||||
this.LoginResult.NotifyEmail = this.checkboxEMailNotify.IsChecked ?? false;
|
||||
this.LoginResult.NotifyPopup = this.checkboxPushNotify.IsChecked ?? false;
|
||||
if ((this.checkListBoxEventSelection.SelectedItems.Count > 0) && (this.LoginResult.NotifyOn == null))
|
||||
this.LoginResult.NotifyOn = new();
|
||||
this.LoginResult.NotifyOn.Clear();
|
||||
foreach (NotificationType nt in this.checkListBoxEventSelection.SelectedItems)
|
||||
this.LoginResult.NotifyOn.Add(nt);
|
||||
this.ChangeUserSettingsRequested?.Invoke();
|
||||
}
|
||||
|
||||
this.ChangePasswordRequested?.Invoke(this.wpBoxOldPassword.Password, this.wpBoxNewPassword.Password);
|
||||
}
|
||||
|
||||
private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
|
||||
@ -85,23 +73,13 @@ namespace BreCalClient
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.checkListBoxEventSelection.ItemsSource = Enum.GetValues(typeof(BreCalClient.misc.Model.NotificationType));
|
||||
|
||||
if(LoginResult != null)
|
||||
{
|
||||
this.textBoxUserEmail.Text = LoginResult.UserEmail;
|
||||
this.textBoxUserPhone.Text = LoginResult.UserPhone;
|
||||
this.checkboxEMailNotify.IsChecked = LoginResult.NotifyEmail;
|
||||
this.checkboxPushNotify.IsChecked = LoginResult.NotifyPopup;
|
||||
if (LoginResult.NotifyOn != null)
|
||||
{
|
||||
foreach (NotificationType nt in LoginResult.NotifyOn)
|
||||
this.checkListBoxEventSelection.SelectedItems.Add(nt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,16 +29,16 @@
|
||||
<applicationSettings>
|
||||
<BreCalClient.Properties.Settings>
|
||||
<setting name="BG_COLOR" serializeAs="String">
|
||||
<value>#751D1F</value>
|
||||
<value>#203864</value>
|
||||
</setting>
|
||||
<setting name="APP_TITLE" serializeAs="String">
|
||||
<value>!!Bremen calling Entwicklungsversion!!</value>
|
||||
<value>Bremen calling</value>
|
||||
</setting>
|
||||
<setting name="LOGO_IMAGE_URL" serializeAs="String">
|
||||
<value>https://www.textbausteine.net/</value>
|
||||
</setting>
|
||||
<setting name="API_URL" serializeAs="String">
|
||||
<value>https://brecaltest.bsmd-emswe.eu</value>
|
||||
<value>https://brecal.bsmd-emswe.eu</value>
|
||||
</setting>
|
||||
</BreCalClient.Properties.Settings>
|
||||
</applicationSettings>
|
||||
@ -86,12 +86,6 @@
|
||||
<setting name="FilterCriteriaMap" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="W5Top" serializeAs="String">
|
||||
<value>0</value>
|
||||
</setting>
|
||||
<setting name="W5Left" serializeAs="String">
|
||||
<value>0</value>
|
||||
</setting>
|
||||
</BreCalClient.Properties.Settings>
|
||||
</userSettings>
|
||||
</configuration>
|
||||
@ -3,7 +3,6 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:BreCalClient"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
xmlns:options="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
|
||||
StartupUri="MainWindow.xaml" Exit="Application_Exit" Startup="Application_Startup" >
|
||||
<Application.Resources>
|
||||
|
||||
@ -15,95 +14,6 @@
|
||||
<sys:Double x:Key="{x:Static SystemParameters.VerticalScrollBarWidthKey}">10</sys:Double>
|
||||
<sys:Double x:Key="{x:Static SystemParameters.HorizontalScrollBarHeightKey}">10</sys:Double>
|
||||
|
||||
<Color x:Key="InformationColor">#147ec9</Color>
|
||||
<SolidColorBrush x:Key="InformationColorBrush" Color="{StaticResource InformationColor}" options:Freeze="True" />
|
||||
|
||||
<Color x:Key="SuccessColor">#11ad45</Color>
|
||||
<SolidColorBrush x:Key="SuccessColorBrush" Color="{StaticResource SuccessColor}" options:Freeze="True" />
|
||||
|
||||
<Color x:Key="ErrorColor">#e60914</Color>
|
||||
<SolidColorBrush x:Key="ErrorColorBrush" Color="{StaticResource ErrorColor}" options:Freeze="True" />
|
||||
|
||||
<Color x:Key="WarningColor">#f5a300</Color>
|
||||
<SolidColorBrush x:Key="WarningColorBrush" Color="{StaticResource WarningColor}" options:Freeze="True" />
|
||||
|
||||
<Canvas x:Key="InformationIcon" Width="24" Height="24">
|
||||
<Path Data="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" Fill="White" />
|
||||
</Canvas>
|
||||
|
||||
<Canvas x:Key="SuccessIcon" Width="24" Height="24">
|
||||
<Path Data="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" Fill="White" />
|
||||
</Canvas>
|
||||
|
||||
<Canvas x:Key="ErrorIcon" Width="24" Height="24">
|
||||
<Path Data="M11,15H13V17H11V15M11,7H13V13H11V7M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20Z" Fill="White" />
|
||||
</Canvas>
|
||||
|
||||
<Canvas x:Key="WarningIcon" Width="24" Height="24">
|
||||
<Path Data="M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16" Fill="White" />
|
||||
</Canvas>
|
||||
|
||||
<Canvas x:Key="CloseIcon" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
|
||||
<Path Width="31.6666" Height="31.6667" Canvas.Left="22.1666" Canvas.Top="22.1667" Stretch="Fill" Fill="#FF000000" Data="F1 M 26.9166,22.1667L 37.9999,33.25L 49.0832,22.1668L 53.8332,26.9168L 42.7499,38L 53.8332,49.0834L 49.0833,53.8334L 37.9999,42.75L 26.9166,53.8334L 22.1666,49.0833L 33.25,38L 22.1667,26.9167L 26.9166,22.1667 Z "/>
|
||||
</Canvas>
|
||||
|
||||
<Style TargetType="Border" x:Key="NotificationBorder">
|
||||
<Setter Property="Padding" Value="5" />
|
||||
<Setter Property="Effect">
|
||||
<Setter.Value>
|
||||
<DropShadowEffect Opacity="0.5" ShadowDepth="1" BlurRadius="2" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Rectangle" x:Key="NotificationIcon">
|
||||
<Setter Property="Width" Value="24"/>
|
||||
<Setter Property="Height" Value="24"/>
|
||||
<Setter Property="Margin" Value="0,5,5,5" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Fill" Value="White"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TextBlock" x:Key="NotificationText">
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="Margin" Value="5,0,0,0" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="{x:Type Button}" x:Key="NotificationCloseButton">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="#FFF" />
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
<Setter Property="VerticalAlignment" Value="Top" />
|
||||
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type Button}">
|
||||
<Border Background="{TemplateBinding Background}">
|
||||
<ContentPresenter Content="{TemplateBinding Content}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,0,0,0" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#33000000" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Background" Value="#77000000" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Rectangle" x:Key="CloseButtonIcon">
|
||||
<Setter Property="Width" Value="10"/>
|
||||
<Setter Property="Height" Value="10"/>
|
||||
<Setter Property="Fill" Value="{Binding Path=Foreground, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}" />
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
</Application.Resources>
|
||||
|
||||
@ -1,207 +0,0 @@
|
||||
// Copyright (c) 2024- schick Informatik
|
||||
// Description: Helper (static) class to handle polled API notifications
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using ToastNotifications.Core;
|
||||
using BreCalClient.misc.Model;
|
||||
|
||||
namespace BreCalClient
|
||||
{
|
||||
internal class AppNotification(int id)
|
||||
{
|
||||
private static readonly Dictionary<int, AppNotification> _notifications = [];
|
||||
private static readonly ObservableCollection<AppNotification> _notificationsCollection = [];
|
||||
|
||||
#region Properties
|
||||
|
||||
public int Id { get { return id; } }
|
||||
|
||||
public string? NotificationType
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public string? NotificationDisplay
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public string? NotificationDate
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public string? Ship
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public string? ShipcallType
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public string? Berth
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public string? ETA
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public string? Message
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
|
||||
public static ObservableCollection<AppNotification> AppNotifications { get { return _notificationsCollection; } }
|
||||
|
||||
#endregion
|
||||
|
||||
#region internal statics
|
||||
|
||||
internal static void LoadFromSettings()
|
||||
{
|
||||
_notifications.Clear();
|
||||
|
||||
if (Properties.Settings.Default.Notifications != null)
|
||||
{
|
||||
// load notification ids that have been processed
|
||||
foreach (string? notification_id in Properties.Settings.Default.Notifications)
|
||||
{
|
||||
if (Int32.TryParse(notification_id, out int result))
|
||||
_notifications.Add(result, new AppNotification(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Clear()
|
||||
{
|
||||
_notifications.Clear();
|
||||
SaveNotifications();
|
||||
}
|
||||
|
||||
internal static bool UpdateNotifications(List<Notification> notifications, System.Collections.Concurrent.ConcurrentDictionary<int, ShipcallControlModel> currentShipcalls, ToastViewModel vm, LoginResult loginResult)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
foreach (Notification notification in notifications)
|
||||
{
|
||||
if (notification.ParticipantId.HasValue && notification.ParticipantId.Value != App.Participant.Id) // not meant for us
|
||||
continue;
|
||||
|
||||
if (!currentShipcalls.ContainsKey(notification.ShipcallId)) // not one of our shipcalls (maybe for another port or filtered)
|
||||
continue;
|
||||
|
||||
// filter out notifications for shipcalls where we are not nomiated/assigned
|
||||
if (!notification.ParticipantId.HasValue)
|
||||
{
|
||||
bool iAmAssigned = false;
|
||||
foreach (ParticipantAssignment p in currentShipcalls[notification.ShipcallId].AssignedParticipants.Values)
|
||||
{
|
||||
if (p.ParticipantId.Equals(App.Participant.Id))
|
||||
{
|
||||
iAmAssigned = true; break;
|
||||
}
|
||||
}
|
||||
if (!iAmAssigned) continue;
|
||||
}
|
||||
|
||||
// filter out notifications the user is not interested in
|
||||
if((notification.Type != null) && !loginResult.NotifyOn.Contains(notification.Type.Value))
|
||||
continue;
|
||||
|
||||
if (!_notificationsCollection.Where(x => x.Id == notification.Id).Any())
|
||||
{
|
||||
List<AppNotification> newList = new(_notificationsCollection);
|
||||
|
||||
AppNotification ap = new(notification.Id)
|
||||
{
|
||||
NotificationType = notification.Type.ToString(),
|
||||
NotificationDate = notification.Created.ToString(),
|
||||
Ship = currentShipcalls[notification.ShipcallId]?.Ship?.Name,
|
||||
ShipcallType = currentShipcalls[notification.ShipcallId]?.Shipcall?.Type.ToString(),
|
||||
ETA = currentShipcalls[notification.ShipcallId]?.GetETAETD(true)
|
||||
};
|
||||
Times? agencyTimes = currentShipcalls[notification.ShipcallId]?.GetTimesForParticipantType(Extensions.ParticipantType.AGENCY);
|
||||
ap.Berth = currentShipcalls[notification.ShipcallId]?.GetBerthText(agencyTimes);
|
||||
ap.Message = notification.Message;
|
||||
|
||||
System.Diagnostics.Trace.WriteLine($"Notification {notification.Id} Type {notification.Type}");
|
||||
MessageOptions options = new()
|
||||
{
|
||||
FontSize = 14,
|
||||
ShowCloseButton = true,
|
||||
Tag = ap
|
||||
};
|
||||
|
||||
newList.Add(ap);
|
||||
newList.Sort((a, b) => (a.NotificationDate ?? "").CompareTo(b.NotificationDate));
|
||||
_notificationsCollection.Clear();
|
||||
foreach(AppNotification newAp in newList)
|
||||
_notificationsCollection.Add(newAp);
|
||||
|
||||
ap.NotificationDisplay = string.Empty;
|
||||
switch(notification.Type)
|
||||
{
|
||||
case misc.Model.NotificationType.Assignment:
|
||||
ap.NotificationDisplay = Resources.Resources.textAssignment; break;
|
||||
case misc.Model.NotificationType.Next24h:
|
||||
ap.NotificationDisplay = Resources.Resources.textNext24h; break;
|
||||
case misc.Model.NotificationType.TimeConflict:
|
||||
ap.NotificationDisplay = Resources.Resources.textTimeConflict; break;
|
||||
case misc.Model.NotificationType.TimeConflictResolved:
|
||||
ap.NotificationDisplay = Resources.Resources.textTimeConflictResolved; break;
|
||||
case misc.Model.NotificationType.MissingData:
|
||||
ap.NotificationDisplay = Resources.Resources.textMissingData; break;
|
||||
case misc.Model.NotificationType.Cancelled:
|
||||
ap.NotificationDisplay = Resources.Resources.textCancelled; break;
|
||||
case misc.Model.NotificationType.Unassigned:
|
||||
ap.NotificationDisplay = Resources.Resources.textUnassigned; break;
|
||||
}
|
||||
|
||||
string toastText = ap.NotificationDisplay + "\n";
|
||||
|
||||
toastText += $"{ap.Ship} ({ap.ShipcallType}) - {ap.ETA} - {ap.Berth}";
|
||||
if (!string.IsNullOrEmpty(ap.Message))
|
||||
toastText += $" \n{ap.Message}";
|
||||
|
||||
if (_notifications.TryAdd(notification.Id, ap))
|
||||
{
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
vm.ShowAppNotification(toastText, options);
|
||||
});
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result)
|
||||
SaveNotifications(); // store notification ids in config array on change
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static void SaveNotifications()
|
||||
{
|
||||
if (Properties.Settings.Default.Notifications == null)
|
||||
Properties.Settings.Default.Notifications = [];
|
||||
else
|
||||
Properties.Settings.Default.Notifications.Clear();
|
||||
foreach (int notification_id in _notifications.Keys)
|
||||
{
|
||||
Properties.Settings.Default.Notifications.Add(notification_id.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
// Copyright (c) 2024- schick Informatik
|
||||
// Description:
|
||||
//
|
||||
|
||||
using ToastNotifications;
|
||||
using ToastNotifications.Core;
|
||||
|
||||
namespace BreCalClient
|
||||
{
|
||||
public static class AppNotificationExtension
|
||||
{
|
||||
public static void ShowAppNotification(this Notifier notifier, string message)
|
||||
{
|
||||
notifier.Notify(() => new AppNotificationMessage(message));
|
||||
}
|
||||
|
||||
public static void ShowAppNotification(this Notifier notifier, string message, MessageOptions displayOptions)
|
||||
{
|
||||
notifier.Notify(() => new AppNotificationMessage(message, displayOptions));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
using System.Windows;
|
||||
using ToastNotifications.Core;
|
||||
using ToastNotifications.Messages.Core;
|
||||
|
||||
namespace BreCalClient
|
||||
{
|
||||
public class AppNotificationMessage : MessageBase<AppNotificationPart>
|
||||
{
|
||||
public AppNotificationMessage(string message) : this(message, new MessageOptions())
|
||||
{
|
||||
}
|
||||
|
||||
public AppNotificationMessage(string message, MessageOptions options) : base(message, options)
|
||||
{
|
||||
}
|
||||
|
||||
protected override AppNotificationPart CreateDisplayPart()
|
||||
{
|
||||
return new AppNotificationPart(this);
|
||||
}
|
||||
|
||||
protected override void UpdateDisplayOptions(AppNotificationPart displayPart, MessageOptions options)
|
||||
{
|
||||
// if (options.FontSize != null)
|
||||
// displayPart.Text.FontSize = options.FontSize.Value;
|
||||
|
||||
// displayPart.CloseButton.Visibility = options.ShowCloseButton ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
<core:NotificationDisplayPart x:Class="BreCalClient.AppNotificationPart"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:core="clr-namespace:ToastNotifications.Core;assembly=ToastNotifications"
|
||||
mc:Ignorable="d" d:DesignWidth="250" >
|
||||
<Border x:Name="ContentWrapper" Style="{DynamicResource NotificationBorder}" Background="{DynamicResource ErrorColorBrush}">
|
||||
<Grid x:Name="ContentContainer">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="25" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Rectangle x:Name="Icon" Width="24" Height="24">
|
||||
<Rectangle.Fill>
|
||||
<VisualBrush Visual="{StaticResource ErrorIcon}" />
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
|
||||
<TextBlock x:Name="Text" Text="{Binding Message, Mode=OneTime}" Style="{StaticResource NotificationText}" Grid.Column="1" />
|
||||
<Button x:Name="CloseButton" Style="{StaticResource NotificationCloseButton}" Padding="1" Grid.Column="2" Click="OnClose" Visibility="Hidden">
|
||||
<Rectangle Style="{StaticResource CloseButtonIcon}" Margin="2">
|
||||
<Rectangle.OpacityMask>
|
||||
<VisualBrush Stretch="Fill" Visual="{StaticResource CloseIcon}" />
|
||||
</Rectangle.OpacityMask>
|
||||
</Rectangle>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</core:NotificationDisplayPart>
|
||||
@ -1,57 +0,0 @@
|
||||
using BreCalClient.misc.Model;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using ToastNotifications.Core;
|
||||
|
||||
namespace BreCalClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for NotificationPart.xaml
|
||||
/// </summary>
|
||||
public partial class AppNotificationPart : NotificationDisplayPart
|
||||
{
|
||||
public AppNotificationPart(AppNotificationMessage appNotification)
|
||||
{
|
||||
InitializeComponent();
|
||||
Bind(appNotification);
|
||||
|
||||
if (appNotification.Options.Tag is AppNotification ap)
|
||||
{
|
||||
switch (ap.NotificationType)
|
||||
{
|
||||
case "TimeConflict":
|
||||
this.ContentWrapper.Background = Brushes.Red;
|
||||
break;
|
||||
case "TimeConflictResolved":
|
||||
this.ContentWrapper.Background = Brushes.Green;
|
||||
break;
|
||||
case "Assignment":
|
||||
this.ContentWrapper.Background = Brushes.Blue;
|
||||
break;
|
||||
case "Next24h":
|
||||
this.ContentWrapper.Background = Brushes.DarkOrange;
|
||||
break;
|
||||
case "Unassigned":
|
||||
this.ContentWrapper.Background = Brushes.Gray;
|
||||
break;
|
||||
case "MissingData":
|
||||
this.ContentWrapper.Background = Brushes.DarkKhaki;
|
||||
break;
|
||||
case "Cancelled":
|
||||
this.ContentWrapper.Background = Brushes.DarkGray;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void OnClose(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Notification.Close();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -2,18 +2,18 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWPF>true</UseWPF>
|
||||
<SignAssembly>True</SignAssembly>
|
||||
<StartupObject>BreCalClient.App</StartupObject>
|
||||
<AssemblyOriginatorKeyFile>..\..\misc\brecal.snk</AssemblyOriginatorKeyFile>
|
||||
<AssemblyVersion>1.8.0.0</AssemblyVersion>
|
||||
<FileVersion>1.8.0.0</FileVersion>
|
||||
<AssemblyVersion>1.6.1.0</AssemblyVersion>
|
||||
<FileVersion>1.6.1.0</FileVersion>
|
||||
<Title>Bremen calling client</Title>
|
||||
<Description>A Windows WPF client for the Bremen calling API.</Description>
|
||||
<ApplicationIcon>containership.ico</ApplicationIcon>
|
||||
<AssemblyName>BreCalTestClient</AssemblyName>
|
||||
<AssemblyName>BreCalClient</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -26,7 +26,6 @@
|
||||
<None Remove="Resources\arrow_up_blue.png" />
|
||||
<None Remove="Resources\arrow_up_green.png" />
|
||||
<None Remove="Resources\arrow_up_red.png" />
|
||||
<None Remove="Resources\bell3.png" />
|
||||
<None Remove="Resources\check.png" />
|
||||
<None Remove="Resources\clipboard.png" />
|
||||
<None Remove="Resources\clock.png" />
|
||||
@ -85,7 +84,6 @@
|
||||
<Resource Include="Resources\arrow_up_blue.png" />
|
||||
<Resource Include="Resources\arrow_up_green.png" />
|
||||
<Resource Include="Resources\arrow_up_red.png" />
|
||||
<Resource Include="Resources\bell3.png" />
|
||||
<Resource Include="Resources\check.png" />
|
||||
<Resource Include="Resources\clipboard.png" />
|
||||
<Resource Include="Resources\clock.png" />
|
||||
@ -120,12 +118,10 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.6.1" />
|
||||
<PackageReference Include="JsonSubTypes" Version="2.0.1" />
|
||||
<PackageReference Include="log4net" Version="3.0.3" />
|
||||
<PackageReference Include="log4net" Version="2.0.17" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Polly" Version="8.5.1" />
|
||||
<PackageReference Include="Polly" Version="8.4.1" />
|
||||
<PackageReference Include="RestSharp" Version="112.0.0" />
|
||||
<PackageReference Include="ToastNotifications" Version="2.5.1" />
|
||||
<PackageReference Include="ToastNotifications.Messages" Version="2.5.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
xmlns:p = "clr-namespace:BreCalClient.Resources"
|
||||
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
|
||||
mc:Ignorable="d" Left="{local:SettingBinding W1Left}" Top="{local:SettingBinding W1Top}"
|
||||
Title="{x:Static p:Resources.textEditTimes}" Height="415" Width="500" Loaded="Window_Loaded" ResizeMode="CanResizeWithGrip" Icon="Resources/containership.ico">
|
||||
Title="{x:Static p:Resources.textEditTimes}" Height="331" Width="500" Loaded="Window_Loaded" ResizeMode="CanResizeWithGrip" Icon="Resources/containership.ico">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width=".20*" />
|
||||
@ -22,9 +22,6 @@
|
||||
<RowDefinition Height="28" />
|
||||
<RowDefinition Height="28" />
|
||||
<RowDefinition Height="28" />
|
||||
<RowDefinition Height="28" x:Name="rowt1" />
|
||||
<RowDefinition Height="28" x:Name="rowt2" />
|
||||
<RowDefinition Height="28" x:Name="rowt3" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="28" />
|
||||
</Grid.RowDefinitions>
|
||||
@ -38,10 +35,8 @@
|
||||
<Label Grid.Row="4" Grid.Column="0" Content="ATD" HorizontalContentAlignment="Right" x:Name="labelATD" />
|
||||
<Label Grid.Row="5" Grid.Column="0" Content="{x:Static p:Resources.textLockTime}" HorizontalContentAlignment="Right" />
|
||||
<Label Grid.Row="6" Grid.Column="0" Content="{x:Static p:Resources.textZoneEntryTime}" HorizontalContentAlignment="Right" />
|
||||
<Label Grid.Row="7" Grid.Column="0" Content="{x:Static p:Resources.textTidalWindow}" HorizontalContentAlignment="Right" />
|
||||
<Label Grid.Row="8" Grid.Column="0" Content="{x:Static p:Resources.textFrom}" HorizontalContentAlignment="Right" />
|
||||
<Label Grid.Row="9" Grid.Column="0" Content="{x:Static p:Resources.textTo}" HorizontalContentAlignment="Right" />
|
||||
<Label Grid.Row="10" Grid.Column="0" Content="{x:Static p:Resources.textRemarks}" HorizontalContentAlignment="Right" />
|
||||
|
||||
<Label Grid.Row="7" Grid.Column="0" Content="{x:Static p:Resources.textRemarks}" HorizontalContentAlignment="Right" />
|
||||
|
||||
<Grid Grid.Row="1" Grid.Column="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
@ -142,7 +137,7 @@
|
||||
</ContextMenu>
|
||||
</xctk:DateTimePicker.ContextMenu>
|
||||
</local:DateTimePickerExt>
|
||||
|
||||
<!--CheckBox IsEnabled="False" Grid.Row="3" Grid.Column="2" Margin="4,0,0,0" Name="checkBoxLockTimeFixed" VerticalAlignment="Center" /-->
|
||||
<local:DateTimePickerExt IsEnabled="False" Grid.Row="6" Grid.Column="1" Margin="2" x:Name="datePickerZoneEntry" Format="Custom" FormatString="dd.MM. yyyy HH:mm">
|
||||
<xctk:DateTimePicker.ContextMenu>
|
||||
<ContextMenu>
|
||||
@ -154,12 +149,10 @@
|
||||
</ContextMenu>
|
||||
</xctk:DateTimePicker.ContextMenu>
|
||||
</local:DateTimePickerExt>
|
||||
|
||||
<xctk:DateTimePicker Name="datePickerTidalWindowFrom" Grid.Column="1" Grid.Row="8" Margin="2" IsEnabled="False" Format="Custom" FormatString="dd.MM. yyyy HH:mm"/>
|
||||
<xctk:DateTimePicker Name="datePickerTidalWindowTo" Grid.Column="1" Grid.Row="9" Margin="2" IsEnabled="False" Format="Custom" FormatString="dd.MM. yyyy HH:mm"/>
|
||||
<!--CheckBox IsEnabled="False" Grid.Row="4" Grid.Column="2" Margin="4,0,0,0" Name="checkBoxZoneEntryFixed" VerticalAlignment="Center" /-->
|
||||
|
||||
<TextBox Grid.Row="10" Grid.Column="1" Margin="2" Name="textBoxRemarks" TextWrapping="Wrap" AcceptsReturn="True" SpellCheck.IsEnabled="True" AcceptsTab="False" IsReadOnly="True" MaxLength="512"/>
|
||||
<StackPanel Grid.Row="11" Grid.Column="1" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<TextBox Grid.Row="7" Grid.Column="1" Margin="2" Name="textBoxRemarks" TextWrapping="Wrap" AcceptsReturn="True" SpellCheck.IsEnabled="True" AcceptsTab="False" IsReadOnly="True" MaxLength="512"/>
|
||||
<StackPanel Grid.Row="8" Grid.Column="1" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Width= "80" Margin="2" Content="{x:Static p:Resources.textOK}" x:Name="buttonOK" Click="buttonOK_Click" />
|
||||
<Button Width="80" Margin="2" Content="{x:Static p:Resources.textCancel}" x:Name="buttonCancel" Click="buttonCancel_Click"/>
|
||||
<Button Width="28" x:Name="buttonClearAll" Click="buttonClearAll_Click" Margin="2" IsEnabled="False">
|
||||
|
||||
@ -86,8 +86,6 @@ namespace BreCalClient
|
||||
this.datePickerLockTime.Value = null;
|
||||
this.datePickerZoneEntry.Value = null;
|
||||
this.textBoxRemarks.Text = null;
|
||||
this.datePickerTidalWindowFrom.Value = null;
|
||||
this.datePickerTidalWindowTo.Value = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,30 +153,6 @@ namespace BreCalClient
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((Extensions.ParticipantType)this.Times.ParticipantType == Extensions.ParticipantType.PILOT)
|
||||
{
|
||||
if ((this.datePickerTidalWindowFrom.Value != this.ShipcallModel.Shipcall?.TidalWindowFrom) || (this.datePickerTidalWindowTo.Value != this.ShipcallModel.Shipcall?.TidalWindowTo)) // something has changed
|
||||
{
|
||||
if (datePickerTidalWindowTo.Value.IsTooOld() || this.datePickerTidalWindowFrom.Value.IsTooOld())
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textTideTimesInThePast;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.datePickerTidalWindowFrom.Value.HasValue && this.datePickerTidalWindowTo.Value.HasValue && this.datePickerTidalWindowFrom.Value > this.datePickerTidalWindowTo.Value)
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textEndValueBeforeStartValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((this.datePickerTidalWindowFrom.Value.HasValue && !this.datePickerTidalWindowTo.Value.HasValue) || (!this.datePickerTidalWindowFrom.Value.HasValue && this.datePickerTidalWindowTo.Value.HasValue))
|
||||
{
|
||||
message = BreCalClient.Resources.Resources.textTidalBothValues;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -193,26 +167,19 @@ namespace BreCalClient
|
||||
this.Times.ZoneEntry = this.datePickerZoneEntry.Value;
|
||||
this.Times.Ata = this.datePickerATA.Value;
|
||||
this.Times.Atd = this.datePickerATD.Value;
|
||||
|
||||
Extensions.ParticipantType pType = (Extensions.ParticipantType)this.Times.ParticipantType;
|
||||
if ((pType == Extensions.ParticipantType.PILOT) && this.ShipcallModel.Shipcall != null)
|
||||
{
|
||||
this.ShipcallModel.Shipcall.TidalWindowFrom = this.datePickerTidalWindowFrom.Value;
|
||||
this.ShipcallModel.Shipcall.TidalWindowTo = this.datePickerTidalWindowTo.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyToControls()
|
||||
{
|
||||
this.textBoxRemarks.Text = this.Times.Remarks;
|
||||
this.datePickerETABerth.Value = this.Times.EtaBerth;
|
||||
if(this.datePickerETABerth.IsEnabled && (this.Times.EtaBerth == null) && (this.AgencyTimes?.EtaBerth != null) && (ShipcallModel.Shipcall?.Type == ShipcallType.Arrival) && (this.AgencyTimes?.EtaBerth > DateTime.Now))
|
||||
if(this.datePickerETABerth.IsEnabled && (this.Times.EtaBerth == null) && (this.AgencyTimes?.EtaBerth != null) && (ShipcallModel.Shipcall?.Type == ShipcallType.Arrival) && (this.AgencyTimes?.EtaBerth > DateTime.Now.AddDays(-1)))
|
||||
{
|
||||
this.datePickerETABerth.Value = this.AgencyTimes.EtaBerth;
|
||||
if (this.datePickerETABerth.Template.FindName("PART_TextBox", this.datePickerETABerth) is WatermarkTextBox tb) { tb.Focus(); tb.SelectAll(); }
|
||||
}
|
||||
this.datePickerETDBerth.Value = this.Times.EtdBerth;
|
||||
if(this.datePickerETDBerth.IsEnabled && (this.Times.EtdBerth == null) && (this.AgencyTimes?.EtdBerth != null) && ((ShipcallModel.Shipcall?.Type == ShipcallType.Departure) || (ShipcallModel.Shipcall?.Type == ShipcallType.Shifting)) && (this.AgencyTimes?.EtdBerth > DateTime.Now))
|
||||
if(this.datePickerETDBerth.IsEnabled && (this.Times.EtdBerth == null) && (this.AgencyTimes?.EtdBerth != null) && ((ShipcallModel.Shipcall?.Type == ShipcallType.Departure) || (ShipcallModel.Shipcall?.Type == ShipcallType.Shifting)) && (this.AgencyTimes?.EtdBerth > DateTime.Now.AddDays(-1)))
|
||||
{
|
||||
this.datePickerETDBerth.Value = this.AgencyTimes.EtdBerth;
|
||||
if (this.datePickerETDBerth.Template.FindName("PART_TextBox", this.datePickerETDBerth) is WatermarkTextBox tb) tb.SelectAll();
|
||||
@ -223,14 +190,14 @@ namespace BreCalClient
|
||||
this.datePickerATA.Value = this.Times.Ata;
|
||||
this.datePickerATD.Value = this.Times.Atd;
|
||||
this.datePickerETABerth_End.Value = this.Times.EtaIntervalEnd;
|
||||
if (this.datePickerETABerth_End.IsEnabled && (this.Times.EtaIntervalEnd == null) && (this.Times.EtaBerth == null) && (this.AgencyTimes?.EtaIntervalEnd != null) && (ShipcallModel.Shipcall?.Type == ShipcallType.Arrival))
|
||||
if (this.datePickerETABerth_End.IsEnabled && (this.Times.EtaIntervalEnd == null) && (this.Times.EtaBerth == null) && (this.AgencyTimes?.EtaIntervalEnd != null) && (ShipcallModel.Shipcall?.Type == ShipcallType.Arrival) && (this.AgencyTimes?.EtaBerth > DateTime.Now.AddDays(-1)))
|
||||
{
|
||||
this.datePickerETABerth_End.Value = this.AgencyTimes.EtaIntervalEnd;
|
||||
//if (this.datePickerETABerth_End.Template.FindName("PART_TextBox", this.datePickerETABerth_End) is WatermarkTextBox tb) { tb.Focus(); tb.SelectAll(); }
|
||||
}
|
||||
|
||||
this.datePickerETDBerth_End.Value = this.Times.EtdIntervalEnd;
|
||||
if (this.datePickerETDBerth_End.IsEnabled && (this.Times.EtdIntervalEnd == null) && (this.Times.EtdBerth == null) && (this.AgencyTimes?.EtdIntervalEnd != null) && ((ShipcallModel.Shipcall?.Type == ShipcallType.Departure) || (ShipcallModel.Shipcall?.Type == ShipcallType.Shifting)))
|
||||
if (this.datePickerETDBerth_End.IsEnabled && (this.Times.EtdIntervalEnd == null) && (this.Times.EtdBerth == null) && (this.AgencyTimes?.EtdIntervalEnd != null) && ((ShipcallModel.Shipcall?.Type == ShipcallType.Departure) || (ShipcallModel.Shipcall?.Type == ShipcallType.Shifting)) && (this.AgencyTimes?.EtdBerth > DateTime.Now.AddDays(-1)))
|
||||
{
|
||||
this.datePickerETDBerth_End.Value = this.AgencyTimes.EtdIntervalEnd;
|
||||
//if (this.datePickerETDBerth_End.Template.FindName("PART_TextBox", this.datePickerETDBerth_End) is WatermarkTextBox tb) { tb.Focus(); tb.SelectAll(); }
|
||||
@ -251,19 +218,6 @@ namespace BreCalClient
|
||||
}
|
||||
}
|
||||
|
||||
Extensions.ParticipantType pType = (Extensions.ParticipantType)this.Times.ParticipantType;
|
||||
if ((pType == Extensions.ParticipantType.PILOT) && this.ShipcallModel.Shipcall != null)
|
||||
{
|
||||
this.datePickerTidalWindowFrom.Value = this.ShipcallModel.Shipcall.TidalWindowFrom;
|
||||
this.datePickerTidalWindowTo.Value = this.ShipcallModel.Shipcall.TidalWindowTo;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.rowt1.Height = new(0);
|
||||
this.rowt2.Height = new(0);
|
||||
this.rowt3.Height = new(0);
|
||||
}
|
||||
|
||||
this.SetLockButton(this.Times.EtaBerthFixed ?? false);
|
||||
}
|
||||
|
||||
@ -329,12 +283,8 @@ namespace BreCalClient
|
||||
this.datePickerLockTime.IsEnabled = true;
|
||||
break;
|
||||
case Extensions.ParticipantType.TUG:
|
||||
this.datePickerZoneEntry.IsEnabled = (ShipcallModel.Shipcall?.Type == ShipcallType.Arrival);
|
||||
break;
|
||||
case Extensions.ParticipantType.PILOT:
|
||||
this.datePickerZoneEntry.IsEnabled = (ShipcallModel.Shipcall?.Type == ShipcallType.Arrival);
|
||||
this.datePickerTidalWindowFrom.IsEnabled = true;
|
||||
this.datePickerTidalWindowTo.IsEnabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2024- schick Informatik
|
||||
// Description:
|
||||
// Description: Window to show (complete) list of current shipcall histories
|
||||
//
|
||||
|
||||
using BreCalClient.misc.Api;
|
||||
|
||||
@ -124,8 +124,6 @@
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="26" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="26" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="26" />
|
||||
@ -159,25 +157,19 @@
|
||||
</StatusBarItem>
|
||||
<Separator Grid.Column="9"/>
|
||||
<StatusBarItem Grid.Column="10">
|
||||
<Button x:Name="buttonNotifications" Click="buttonNotifications_Click" Width="20" ToolTip="{x:Static p:Resources.textShowNotifications}">
|
||||
<Image Source="./Resources/bell3.png"/>
|
||||
</Button>
|
||||
</StatusBarItem>
|
||||
<Separator Grid.Column="9"/>
|
||||
<StatusBarItem Grid.Column="12">
|
||||
<TextBlock Name="labelStatusBar"></TextBlock>
|
||||
</StatusBarItem>
|
||||
<Separator Grid.Column="13"/>
|
||||
<StatusBarItem Grid.Column="14">
|
||||
<Separator Grid.Column="11"/>
|
||||
<StatusBarItem Grid.Column="12">
|
||||
<Button x:Name="buttonManualRefresh" Width="20" Click="buttonManualRefresh_Click" ToolTip="{x:Static p:Resources.textTriggerManualRefresh}">
|
||||
<Image Source="./Resources/nav_refresh_green.png"/>
|
||||
</Button>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem Grid.Column="15">
|
||||
<StatusBarItem Grid.Column="13">
|
||||
<TextBlock x:Name="labelLatestUpdate" />
|
||||
</StatusBarItem>
|
||||
<Separator Grid.Column="16"/>
|
||||
<StatusBarItem Grid.Column="17">
|
||||
<Separator Grid.Column="14"/>
|
||||
<StatusBarItem Grid.Column="15">
|
||||
<ProgressBar Name="generalProgressStatus" Width="90" Height="16" Foreground="LightGray"/>
|
||||
</StatusBarItem>
|
||||
</StatusBar>
|
||||
|
||||
@ -36,11 +36,8 @@ namespace BreCalClient
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly ILog _log = LogManager.GetLogger(typeof(MainWindow));
|
||||
private readonly ToastViewModel _vm;
|
||||
|
||||
private const int SHIPCALL_UPDATE_INTERVAL_SECONDS = 30;
|
||||
private const int SHIPS_UPDATE_INTERVAL_SECONDS = 120;
|
||||
private const int CHECK_NOTIFICATIONS_INTERVAL_SECONDS = 5;
|
||||
private const int PROGRESS_STEPS = 50;
|
||||
|
||||
#region Fields
|
||||
@ -52,7 +49,7 @@ namespace BreCalClient
|
||||
|
||||
private readonly ConcurrentDictionary<int, ShipcallControlModel> _allShipcallsDict = new();
|
||||
private readonly ConcurrentDictionary<int, ShipcallControl> _allShipCallsControlDict = new();
|
||||
private readonly List<ShipcallControlModel> _visibleControlModels = [];
|
||||
private readonly List<ShipcallControlModel> _visibleControlModels = new();
|
||||
|
||||
private readonly ShipcallApi _shipcallApi;
|
||||
private readonly UserApi _userApi;
|
||||
@ -71,7 +68,6 @@ namespace BreCalClient
|
||||
// private bool _filterChanged = false;
|
||||
// private bool _sequenceChanged = false;
|
||||
private HistoryDialog? _historyDialog;
|
||||
private NotificationDialog? _notificationDialog;
|
||||
|
||||
#endregion
|
||||
|
||||
@ -126,13 +122,6 @@ namespace BreCalClient
|
||||
RetryConfiguration.AsyncRetryPolicy = retryPolicy;
|
||||
|
||||
this.generalProgressStatus.Maximum = PROGRESS_STEPS;
|
||||
_vm = new ToastViewModel();
|
||||
this.Unloaded += MainWindow_Unloaded;
|
||||
}
|
||||
|
||||
private void MainWindow_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_vm.OnUnloaded();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -152,7 +141,6 @@ namespace BreCalClient
|
||||
};
|
||||
this.comboBoxSortOrder.ItemsSource = Enum.GetValues(typeof(Extensions.SortOrder));
|
||||
this.comboBoxSortOrder.SelectedIndex = (int)_sortOrder;
|
||||
AppNotification.LoadFromSettings();
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
@ -180,22 +168,32 @@ namespace BreCalClient
|
||||
{
|
||||
if (_loginResult.Id > 0)
|
||||
{
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
this.busyIndicator.IsBusy = false;
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
this._userApi.Configuration.ApiKey["Authorization"] = _loginResult.Token;
|
||||
this._shipcallApi.Configuration.ApiKey["Authorization"] = _loginResult.Token;
|
||||
this._timesApi.Configuration.ApiKey["Authorization"] = _loginResult.Token;
|
||||
this._staticApi.Configuration.ApiKey["Authorization"] = _loginResult.Token;
|
||||
this._shipApi.Configuration.ApiKey["Authorization"] = _loginResult.Token;
|
||||
this.LoadStaticLists();
|
||||
this.labelUsername.Text = $"{_loginResult.FirstName} {_loginResult.LastName}";
|
||||
bool loadingSuccessful = await this.LoadStaticLists();
|
||||
if (loadingSuccessful)
|
||||
{
|
||||
this.labelUsername.Text = $"{_loginResult.FirstName} {_loginResult.LastName}";
|
||||
this.busyIndicator.IsBusy = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Mouse.OverrideCursor = null;
|
||||
textUsername.Text = "";
|
||||
textPassword.Password = "";
|
||||
textUsername.Focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
labelGeneralStatus.Text = $"Connection {ConnectionStatus.SUCCESSFUL}";
|
||||
}
|
||||
catch (ApiException ex)
|
||||
{
|
||||
if ((ex.ErrorContent != null && ((string)ex.ErrorContent).StartsWith('{'))) {
|
||||
if ((ex.ErrorContent != null && ((string)ex.ErrorContent).StartsWith("{"))) {
|
||||
Error? anError = JsonConvert.DeserializeObject<Error>((string)ex.ErrorContent);
|
||||
if ((anError != null) && anError.ErrorField.Equals("invalid credentials"))
|
||||
this.labelLoginResult.Content = BreCalClient.Resources.Resources.textWrongCredentials;
|
||||
@ -309,7 +307,7 @@ namespace BreCalClient
|
||||
scmOut.Shipcall.DepartureBerthId = esc.ShipcallModel.Shipcall?.ArrivalBerthId;
|
||||
if (esc.ShipcallModel.Shipcall != null)
|
||||
{
|
||||
scmOut.Shipcall.Participants = [];
|
||||
scmOut.Shipcall.Participants = new();
|
||||
scmOut.Shipcall.Participants.AddRange(esc.ShipcallModel.Shipcall.Participants);
|
||||
foreach(ParticipantType pType in esc.ShipcallModel.AssignedParticipants.Keys)
|
||||
scmOut.AssignedParticipants[pType] = esc.ShipcallModel.AssignedParticipants[pType];
|
||||
@ -336,7 +334,11 @@ namespace BreCalClient
|
||||
{
|
||||
UserDetails ud = new()
|
||||
{
|
||||
Id = _loginResult.Id,
|
||||
Id = _loginResult.Id,
|
||||
FirstName = _loginResult.FirstName,
|
||||
LastName = _loginResult.LastName,
|
||||
UserPhone = _loginResult.UserPhone,
|
||||
UserEmail = _loginResult.UserEmail,
|
||||
OldPassword = oldPw,
|
||||
NewPassword = newPw
|
||||
};
|
||||
@ -346,41 +348,6 @@ namespace BreCalClient
|
||||
MessageBox.Show(BreCalClient.Resources.Resources.textPasswordChanged, BreCalClient.Resources.Resources.textConfirmation, MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Dispatcher.Invoke(new Action(() =>
|
||||
{
|
||||
ShowErrorDialog(ex.Message, "Error saving user information");
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
ad.ChangeUserSettingsRequested += async () =>
|
||||
{
|
||||
if (_loginResult != null)
|
||||
{
|
||||
UserDetails ud = new()
|
||||
{
|
||||
Id = _loginResult.Id,
|
||||
FirstName = _loginResult.FirstName,
|
||||
LastName = _loginResult.LastName,
|
||||
UserPhone = _loginResult.UserPhone,
|
||||
UserEmail = _loginResult.UserEmail,
|
||||
NotifyEmail = _loginResult.NotifyEmail,
|
||||
NotifyPopup = _loginResult.NotifyPopup,
|
||||
NotifySignal = _loginResult.NotifySignal,
|
||||
NotifyWhatsapp = _loginResult.NotifyWhatsapp,
|
||||
};
|
||||
if (_loginResult.NotifyOn != null)
|
||||
{
|
||||
ud.NotifyOn = new(_loginResult.NotifyOn);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _userApi.UserUpdateAsync(ud);
|
||||
MessageBox.Show(BreCalClient.Resources.Resources.textInformationUpdated, BreCalClient.Resources.Resources.textConfirmation, MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Dispatcher.Invoke(new Action(() =>
|
||||
{
|
||||
@ -418,7 +385,7 @@ namespace BreCalClient
|
||||
{
|
||||
this.searchFilterControl.SearchFilter.Ports.Clear();
|
||||
|
||||
List<Berth> berths = [];
|
||||
List<Berth> berths = new List<Berth>();
|
||||
foreach (Port port in comboBoxPorts.SelectedItems)
|
||||
{
|
||||
this.searchFilterControl.SearchFilter.Ports.Add(port.Id);
|
||||
@ -430,7 +397,7 @@ namespace BreCalClient
|
||||
this.searchFilterControl.SetBerths(berths);
|
||||
|
||||
this.SearchFilterControl_SearchFilterChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async void comboBoxSortOrder_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
|
||||
{
|
||||
@ -449,8 +416,8 @@ namespace BreCalClient
|
||||
_historyDialog.Closed += (sender, e) => { this._historyDialog = null; };
|
||||
_historyDialog.HistoryItemSelected += (x) =>
|
||||
{
|
||||
if(_allShipCallsControlDict.TryGetValue(x, out ShipcallControl? value))
|
||||
value.BringIntoView();
|
||||
if(_allShipCallsControlDict.ContainsKey(x))
|
||||
_allShipCallsControlDict[x].BringIntoView();
|
||||
};
|
||||
_historyDialog.Show();
|
||||
}
|
||||
@ -460,21 +427,6 @@ namespace BreCalClient
|
||||
}
|
||||
}
|
||||
|
||||
private void buttonNotifications_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_notificationDialog == null)
|
||||
{
|
||||
_notificationDialog = new NotificationDialog();
|
||||
_notificationDialog.AppNotifications = AppNotification.AppNotifications;
|
||||
_notificationDialog.Closed += (sender, e) => { this._notificationDialog = null; };
|
||||
_notificationDialog.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
_notificationDialog.Activate();
|
||||
}
|
||||
}
|
||||
|
||||
private void buttonManualRefresh_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_refreshImmediately = true; // set flag to avoid timer loop termination
|
||||
@ -486,16 +438,23 @@ namespace BreCalClient
|
||||
|
||||
#region network operations
|
||||
|
||||
private async void LoadStaticLists()
|
||||
private async Task<bool> LoadStaticLists()
|
||||
{
|
||||
if (_loginResult == null) return;
|
||||
if (_loginResult == null) return false;
|
||||
|
||||
BreCalLists.InitializePorts(await _staticApi.GetPortsAsync());
|
||||
BreCalLists.InitializeBerths(await _staticApi.BerthsGetAsync());
|
||||
BreCalLists.InitializeShips(await _shipApi.ShipsGetAsync());
|
||||
BreCalLists.InitializeParticipants(await _staticApi.ParticipantsGetAsync());
|
||||
|
||||
this.searchFilterControl.SetBerths(BreCalLists.Berths);
|
||||
if(BreCalLists.Participants.Count == 0)
|
||||
{
|
||||
MessageBox.Show(BreCalClient.Resources.Resources.textNoPortAssigned, BreCalClient.Resources.Resources.textError, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
Mouse.OverrideCursor = Cursors.Wait;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.searchFilterControl.SetBerths(BreCalLists.Berths);
|
||||
|
||||
foreach (Participant participant in BreCalLists.Participants)
|
||||
{
|
||||
@ -514,14 +473,14 @@ namespace BreCalClient
|
||||
SearchFilterModel? currentFilter = null;
|
||||
if (SearchFilterModel.filterMap != null)
|
||||
{
|
||||
if((_loginResult != null) && SearchFilterModel.filterMap.TryGetValue(_loginResult.Id, out SearchFilterModel? value))
|
||||
if((_loginResult != null) && SearchFilterModel.filterMap.ContainsKey(_loginResult.Id))
|
||||
{
|
||||
currentFilter = value;
|
||||
currentFilter = SearchFilterModel.filterMap[_loginResult.Id];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SearchFilterModel.filterMap = [];
|
||||
SearchFilterModel.filterMap = new();
|
||||
}
|
||||
if (currentFilter == null)
|
||||
{
|
||||
@ -530,8 +489,8 @@ namespace BreCalClient
|
||||
SearchFilterModel.filterMap[_loginResult.Id] = currentFilter;
|
||||
}
|
||||
this.searchFilterControl.SetFilterFromModel(currentFilter);
|
||||
|
||||
if (currentFilter.Ports != null)
|
||||
|
||||
if ((currentFilter.Ports != null) && (this.comboBoxPorts.ItemsSource != null))
|
||||
{
|
||||
foreach (Port p in this.comboBoxPorts.ItemsSource)
|
||||
{
|
||||
@ -542,15 +501,15 @@ namespace BreCalClient
|
||||
|
||||
_ = Task.Run(() => RefreshShipcalls());
|
||||
_ = Task.Run(() => RefreshShips());
|
||||
_ = Task.Run(() => CheckNotifications());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public async Task RefreshShips()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
{
|
||||
Thread.Sleep(SHIPS_UPDATE_INTERVAL_SECONDS * 1000);
|
||||
BreCalLists.InitializeShips(await _shipApi.ShipsGetAsync());
|
||||
}
|
||||
@ -607,7 +566,7 @@ namespace BreCalClient
|
||||
// load times for each shipcall
|
||||
List<Times> currentTimes = await _timesApi.TimesGetAsync(shipcall.Id);
|
||||
|
||||
if (!_allShipcallsDict.TryGetValue(shipcall.Id, out ShipcallControlModel? value))
|
||||
if (!_allShipcallsDict.ContainsKey(shipcall.Id))
|
||||
{
|
||||
// add entry
|
||||
ShipcallControlModel scm = new()
|
||||
@ -619,9 +578,10 @@ namespace BreCalClient
|
||||
}
|
||||
else
|
||||
{
|
||||
value.Shipcall = shipcall;
|
||||
value.Times = currentTimes;
|
||||
UpdateShipcall(value);
|
||||
// update entry
|
||||
_allShipcallsDict[shipcall.Id].Shipcall = shipcall;
|
||||
_allShipcallsDict[shipcall.Id].Times = currentTimes;
|
||||
UpdateShipcall(_allShipcallsDict[shipcall.Id]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -665,19 +625,6 @@ namespace BreCalClient
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CheckNotifications()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Thread.Sleep(CHECK_NOTIFICATIONS_INTERVAL_SECONDS * 1000);
|
||||
if (_loginResult?.NotifyPopup ?? false)
|
||||
{
|
||||
List<Notification> notifications = await _staticApi.NotificationsGetAsync();
|
||||
AppNotification.UpdateNotifications(notifications, _allShipcallsDict, _vm, _loginResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region basic operations
|
||||
@ -688,8 +635,8 @@ namespace BreCalClient
|
||||
_allShipcallsDict[scm.Shipcall.Id] = scm;
|
||||
|
||||
Shipcall shipcall = scm.Shipcall;
|
||||
if (BreCalLists.ShipLookupDict.TryGetValue(shipcall.ShipId, out ShipModel? value))
|
||||
scm.Ship = value.Ship;
|
||||
if (BreCalLists.ShipLookupDict.ContainsKey(shipcall.ShipId))
|
||||
scm.Ship = BreCalLists.ShipLookupDict[shipcall.ShipId].Ship;
|
||||
|
||||
if (shipcall.Type == ShipcallType.Arrival)
|
||||
{
|
||||
@ -722,8 +669,8 @@ namespace BreCalClient
|
||||
{
|
||||
if(scm.Shipcall == null) return;
|
||||
Shipcall shipcall = scm.Shipcall;
|
||||
if (BreCalLists.ShipLookupDict.TryGetValue(shipcall.ShipId, out ShipModel? value))
|
||||
scm.Ship = value.Ship;
|
||||
if (BreCalLists.ShipLookupDict.ContainsKey(shipcall.ShipId))
|
||||
scm.Ship = BreCalLists.ShipLookupDict[shipcall.ShipId].Ship;
|
||||
|
||||
if (shipcall.Type == ShipcallType.Arrival)
|
||||
{
|
||||
@ -777,7 +724,7 @@ namespace BreCalClient
|
||||
// first add everything
|
||||
this._visibleControlModels.AddRange(_allShipcallsDict.Values);
|
||||
|
||||
// now remove elements whose filter criteria are met
|
||||
// now remove elements whose filter criteria are met
|
||||
|
||||
if(sfm.Berths.Count > 0 )
|
||||
{
|
||||
@ -968,10 +915,10 @@ namespace BreCalClient
|
||||
foreach (ShipcallControlModel visibleModel in this._visibleControlModels)
|
||||
{
|
||||
if (visibleModel.Shipcall == null) continue; // should not happen
|
||||
if (this._allShipCallsControlDict.TryGetValue(visibleModel.Shipcall.Id, out ShipcallControl? value))
|
||||
if (this._allShipCallsControlDict.ContainsKey(visibleModel.Shipcall.Id))
|
||||
{
|
||||
value.RefreshData();
|
||||
this.stackPanel.Children.Add(value);
|
||||
this._allShipCallsControlDict[visibleModel.Shipcall.Id].RefreshData();
|
||||
this.stackPanel.Children.Add(this._allShipCallsControlDict[visibleModel.Shipcall.Id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1077,13 +1024,6 @@ namespace BreCalClient
|
||||
etc.Times.Id = apiResultId.VarId;
|
||||
obj.ShipcallControlModel?.Times.Add(etc.Times);
|
||||
}
|
||||
|
||||
// a pilot may have changed the tidal window so we update the shipcall too in this case
|
||||
if(((Extensions.ParticipantType)etc.Times.ParticipantType) == ParticipantType.PILOT)
|
||||
{
|
||||
await _shipcallApi.ShipcallUpdateAsync(obj.ShipcallControlModel?.Shipcall);
|
||||
}
|
||||
|
||||
_refreshImmediately = true;
|
||||
_tokenSource.Cancel();
|
||||
}
|
||||
@ -1121,8 +1061,8 @@ namespace BreCalClient
|
||||
}
|
||||
else
|
||||
{
|
||||
if(editControl.ShipcallModel.AssignedParticipants.TryGetValue(ParticipantType.AGENCY, out ParticipantAssignment? value))
|
||||
editControl.Times.ParticipantId = value.ParticipantId;
|
||||
if(editControl.ShipcallModel.AssignedParticipants.ContainsKey(ParticipantType.AGENCY))
|
||||
editControl.Times.ParticipantId = editControl.ShipcallModel.AssignedParticipants[ParticipantType.AGENCY].ParticipantId;
|
||||
}
|
||||
editControl.Times.ParticipantType = (int)ParticipantType.AGENCY;
|
||||
if(editControl.ShowDialog() ?? false)
|
||||
@ -1138,9 +1078,9 @@ namespace BreCalClient
|
||||
}
|
||||
|
||||
// always try to be the agent, even if we are BSMD
|
||||
if (editControl.ShipcallModel.AssignedParticipants.TryGetValue(ParticipantType.AGENCY, out ParticipantAssignment? value))
|
||||
if (editControl.ShipcallModel.AssignedParticipants.ContainsKey(ParticipantType.AGENCY))
|
||||
{
|
||||
editControl.Times.ParticipantId = value.ParticipantId;
|
||||
editControl.Times.ParticipantId = editControl.ShipcallModel.AssignedParticipants[ParticipantType.AGENCY].ParticipantId;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -1189,7 +1129,7 @@ namespace BreCalClient
|
||||
// (if the special-flag is enabled). Assigned Agency: ShipcallParticipantMap(id=628, shipcall_id=115, participant_id=10,
|
||||
// type=8, created=datetime.datetime(2024, 8, 28, 15, 13, 14), modified=None) with Flags: 42\"}
|
||||
|
||||
Match m = ErrorRegex().Match(message);
|
||||
Match m = Regex.Match(message, "\\{(.*)\\}");
|
||||
if ((m != null) && m.Success)
|
||||
{
|
||||
try
|
||||
@ -1200,24 +1140,24 @@ namespace BreCalClient
|
||||
if (msg.error_field != null)
|
||||
{
|
||||
caption = $"{caption}: {msg.error_field}";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(msg.error_description != null)
|
||||
{
|
||||
message = msg.error_description;
|
||||
}
|
||||
message = msg.error_description;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
_log.ErrorFormat("{0} - {1}", caption, message);
|
||||
|
||||
|
||||
Dispatcher.Invoke(new Action(() =>
|
||||
{
|
||||
MessageBox.Show(message, caption, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}));
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void EnableControlsForParticipant()
|
||||
@ -1234,9 +1174,6 @@ namespace BreCalClient
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
[GeneratedRegex("\\{(.*)\\}")]
|
||||
private static partial Regex ErrorRegex();
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
<Window x:Class="BreCalClient.NotificationDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:BreCalClient"
|
||||
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
|
||||
xmlns:p = "clr-namespace:BreCalClient.Resources"
|
||||
mc:Ignorable="d" Left="{local:SettingBinding W5Left}" Top="{local:SettingBinding W5Top}"
|
||||
Title="{x:Static p:Resources.textNotifications}" Height="450" Width="800" Loaded="Window_Loaded">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="28" />
|
||||
</Grid.RowDefinitions>
|
||||
<local:ENIDataGrid x:Name="dataGridNotifications" Grid.Row="0" SelectionMode="Single" IsReadOnly="True" AutoGenerateColumns="False"
|
||||
CanUserAddRows="False" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
|
||||
<local:ENIDataGrid.RowStyle>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding NotificationType}" Value="Assignment">
|
||||
<Setter Property="Foreground" Value="Blue"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding NotificationType}" Value="Next24h">
|
||||
<Setter Property="Foreground" Value="DarkOrange"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding NotificationType}" Value="TimeConflict">
|
||||
<Setter Property="Background" Value="Red"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding NotificationType}" Value="TimeConflictResolved">
|
||||
<Setter Property="Foreground" Value="Green"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding NotificationType}" Value="Unassigned">
|
||||
<Setter Property="Foreground" Value="DarkGray"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding NotificationType}" Value="MissingData">
|
||||
<Setter Property="Foreground" Value="Yellow" />
|
||||
<Setter Property="Background" Value="DarkGray" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding NotificationType}" Value="Cancelled">
|
||||
<Setter Property="Background" Value="LightGray" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</local:ENIDataGrid.RowStyle>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Id" Binding="{Binding Path=Id}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="{x:Static p:Resources.textType}" Binding="{Binding Path=NotificationDisplay}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="{x:Static p:Resources.textDate}" Binding="{Binding Path=NotificationDate}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="{x:Static p:Resources.textShip}" Binding="{Binding Path=Ship}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="{x:Static p:Resources.textShipcall}" Binding="{Binding Path=ShipcallType}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="ETA/ETD" Binding="{Binding Path=ETA}" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="{x:Static p:Resources.textBerth}" Binding="{Binding Path=Berth}" IsReadOnly="True"/>
|
||||
</DataGrid.Columns>
|
||||
</local:ENIDataGrid>
|
||||
<Grid Grid.Row="1" Grid.Column="0" >
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="22" />
|
||||
<ColumnDefinition Width="80" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width=".2*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button x:Name="buttonClose" Click="buttonClose_Click" Content="{x:Static p:Resources.textClose}" Width="80" Margin="2" Grid.Row="0" Grid.Column="3" HorizontalAlignment="Right" />
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2024- schick Informatik
|
||||
// Description:
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace BreCalClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for NotificationDialog.xaml
|
||||
/// </summary>
|
||||
public partial class NotificationDialog : Window
|
||||
{
|
||||
public NotificationDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
internal ObservableCollection<AppNotification>? AppNotifications { get; set; }
|
||||
|
||||
private void buttonClose_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.Close();
|
||||
}
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.dataGridNotifications.ItemsSource = AppNotifications;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,8 +4,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ApplicationRevision>0</ApplicationRevision>
|
||||
<ApplicationVersion>1.8.0.0</ApplicationVersion>
|
||||
<ApplicationRevision>2</ApplicationRevision>
|
||||
<ApplicationVersion>1.6.0.4</ApplicationVersion>
|
||||
<BootstrapperEnabled>True</BootstrapperEnabled>
|
||||
<Configuration>Debug</Configuration>
|
||||
<CreateDesktopShortcut>True</CreateDesktopShortcut>
|
||||
@ -21,7 +21,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<OpenBrowserOnPublish>False</OpenBrowserOnPublish>
|
||||
<Platform>Any CPU</Platform>
|
||||
<ProductName>Bremen calling development client</ProductName>
|
||||
<PublishDir>bin\Debug\net8.0-windows7.0\win-x64\app.publish\</PublishDir>
|
||||
<PublishDir>bin\Debug\net6.0-windows\win-x64\app.publish\</PublishDir>
|
||||
<PublishUrl>bin\publish.devel\</PublishUrl>
|
||||
<PublisherName>Informatikbüro Daniel Schick</PublisherName>
|
||||
<PublishProtocol>ClickOnce</PublishProtocol>
|
||||
@ -33,12 +33,12 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<SignManifests>False</SignManifests>
|
||||
<SuiteName>Bremen calling</SuiteName>
|
||||
<SupportUrl>https://www.textbausteine.net/</SupportUrl>
|
||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<UpdateEnabled>True</UpdateEnabled>
|
||||
<UpdateMode>Foreground</UpdateMode>
|
||||
<UpdateRequired>True</UpdateRequired>
|
||||
<WebPageFileName>Publish.html</WebPageFileName>
|
||||
<MinimumRequiredVersion>1.8.0.0</MinimumRequiredVersion>
|
||||
<MinimumRequiredVersion>1.6.0.4</MinimumRequiredVersion>
|
||||
<SkipPublishVerification>false</SkipPublishVerification>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@ -4,8 +4,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ApplicationRevision>0</ApplicationRevision>
|
||||
<ApplicationVersion>6.0</ApplicationVersion>
|
||||
<ApplicationRevision>8</ApplicationRevision>
|
||||
<ApplicationVersion>1.6.1.0</ApplicationVersion>
|
||||
<BootstrapperEnabled>False</BootstrapperEnabled>
|
||||
<Configuration>Release</Configuration>
|
||||
<CreateWebPageOnPublish>True</CreateWebPageOnPublish>
|
||||
@ -30,7 +30,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<UpdateEnabled>True</UpdateEnabled>
|
||||
<UpdateMode>Foreground</UpdateMode>
|
||||
<UpdateRequired>False</UpdateRequired>
|
||||
<UpdateRequired>True</UpdateRequired>
|
||||
<WebPageFileName>Publish.html</WebPageFileName>
|
||||
<CreateDesktopShortcut>True</CreateDesktopShortcut>
|
||||
<ErrorReportUrl>https://www.bsmd-emswe.eu/</ErrorReportUrl>
|
||||
@ -40,6 +40,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<SupportUrl>https://www.bsmd-emswe.eu/</SupportUrl>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SkipPublishVerification>false</SkipPublishVerification>
|
||||
<MinimumRequiredVersion>1.6.1.0</MinimumRequiredVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PublishFile Include="containership.ico">
|
||||
|
||||
@ -4,8 +4,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ApplicationRevision>5</ApplicationRevision>
|
||||
<ApplicationVersion>1.7.0.7</ApplicationVersion>
|
||||
<ApplicationRevision>6</ApplicationRevision>
|
||||
<ApplicationVersion>1.6.0.8</ApplicationVersion>
|
||||
<BootstrapperEnabled>True</BootstrapperEnabled>
|
||||
<Configuration>Debug</Configuration>
|
||||
<CreateDesktopShortcut>True</CreateDesktopShortcut>
|
||||
@ -27,7 +27,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<SelfContained>True</SelfContained>
|
||||
<SignatureAlgorithm>(none)</SignatureAlgorithm>
|
||||
<SignManifests>False</SignManifests>
|
||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<UpdateEnabled>True</UpdateEnabled>
|
||||
<UpdateMode>Foreground</UpdateMode>
|
||||
<UpdateRequired>True</UpdateRequired>
|
||||
@ -38,10 +38,10 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<PublisherName>Informatikbüro Daniel Schick</PublisherName>
|
||||
<SuiteName>Bremen Calling</SuiteName>
|
||||
<SupportUrl>http://www.textbausteine.net/</SupportUrl>
|
||||
<PublishDir>bin\Debug\net8.0-windows7.0\win-x64\app.publish\</PublishDir>
|
||||
<PublishDir>bin\Debug\net6.0-windows\win-x64\app.publish\</PublishDir>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SkipPublishVerification>false</SkipPublishVerification>
|
||||
<MinimumRequiredVersion>1.7.0.7</MinimumRequiredVersion>
|
||||
<MinimumRequiredVersion>1.6.0.8</MinimumRequiredVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PublishFile Include="containership.ico">
|
||||
|
||||
43
src/BreCalClient/Properties/Settings.Designer.cs
generated
43
src/BreCalClient/Properties/Settings.Designer.cs
generated
@ -12,7 +12,7 @@ namespace BreCalClient.Properties {
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.12.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.10.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
@ -25,7 +25,7 @@ namespace BreCalClient.Properties {
|
||||
|
||||
[global::System.Configuration.ApplicationScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("#751D1F")]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("#203864")]
|
||||
public string BG_COLOR {
|
||||
get {
|
||||
return ((string)(this["BG_COLOR"]));
|
||||
@ -34,7 +34,7 @@ namespace BreCalClient.Properties {
|
||||
|
||||
[global::System.Configuration.ApplicationScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("!!Bremen calling Entwicklungsversion!!")]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("Bremen calling")]
|
||||
public string APP_TITLE {
|
||||
get {
|
||||
return ((string)(this["APP_TITLE"]));
|
||||
@ -64,7 +64,7 @@ namespace BreCalClient.Properties {
|
||||
|
||||
[global::System.Configuration.ApplicationScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("https://brecaltest.bsmd-emswe.eu")]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("https://brecal.bsmd-emswe.eu")]
|
||||
public string API_URL {
|
||||
get {
|
||||
return ((string)(this["API_URL"]));
|
||||
@ -226,40 +226,5 @@ namespace BreCalClient.Properties {
|
||||
this["FilterCriteriaMap"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
public global::System.Collections.Specialized.StringCollection Notifications {
|
||||
get {
|
||||
return ((global::System.Collections.Specialized.StringCollection)(this["Notifications"]));
|
||||
}
|
||||
set {
|
||||
this["Notifications"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("0")]
|
||||
public double W5Top {
|
||||
get {
|
||||
return ((double)(this["W5Top"]));
|
||||
}
|
||||
set {
|
||||
this["W5Top"] = value;
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("0")]
|
||||
public double W5Left {
|
||||
get {
|
||||
return ((double)(this["W5Left"]));
|
||||
}
|
||||
set {
|
||||
this["W5Left"] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
<Profiles />
|
||||
<Settings>
|
||||
<Setting Name="BG_COLOR" Type="System.String" Scope="Application">
|
||||
<Value Profile="(Default)">#751D1F</Value>
|
||||
<Value Profile="(Default)">#203864</Value>
|
||||
</Setting>
|
||||
<Setting Name="APP_TITLE" Type="System.String" Scope="Application">
|
||||
<Value Profile="(Default)">!!Bremen calling Entwicklungsversion!!</Value>
|
||||
<Value Profile="(Default)">Bremen calling</Value>
|
||||
</Setting>
|
||||
<Setting Name="LOGO_IMAGE_URL" Type="System.String" Scope="Application">
|
||||
<Value Profile="(Default)">https://www.textbausteine.net/</Value>
|
||||
@ -15,7 +15,7 @@
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
<Setting Name="API_URL" Type="System.String" Scope="Application">
|
||||
<Value Profile="(Default)">https://brecaltest.bsmd-emswe.eu</Value>
|
||||
<Value Profile="(Default)">https://brecal.bsmd-emswe.eu</Value>
|
||||
</Setting>
|
||||
<Setting Name="Width" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">800</Value>
|
||||
@ -56,14 +56,5 @@
|
||||
<Setting Name="FilterCriteriaMap" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
<Setting Name="Notifications" Type="System.Collections.Specialized.StringCollection" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
<Setting Name="W5Top" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">0</Value>
|
||||
</Setting>
|
||||
<Setting Name="W5Left" Type="System.Double" Scope="User">
|
||||
<Value Profile="(Default)">0</Value>
|
||||
</Setting>
|
||||
</Settings>
|
||||
</SettingsFile>
|
||||
142
src/BreCalClient/Resources/Resources.Designer.cs
generated
142
src/BreCalClient/Resources/Resources.Designer.cs
generated
@ -167,16 +167,6 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Byte[].
|
||||
/// </summary>
|
||||
public static byte[] bell3 {
|
||||
get {
|
||||
object obj = ResourceManager.GetObject("bell3", resourceCulture);
|
||||
return ((byte[])(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized resource of type System.Byte[].
|
||||
/// </summary>
|
||||
@ -390,15 +380,6 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Participant assigned to shipcall.
|
||||
/// </summary>
|
||||
public static string textAssignment {
|
||||
get {
|
||||
return ResourceManager.GetString("textAssignment", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Berth.
|
||||
/// </summary>
|
||||
@ -579,15 +560,6 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Date.
|
||||
/// </summary>
|
||||
public static string textDate {
|
||||
get {
|
||||
return ResourceManager.GetString("textDate", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Delete.
|
||||
/// </summary>
|
||||
@ -813,15 +785,6 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Information successfully updated.
|
||||
/// </summary>
|
||||
public static string textInformationUpdated {
|
||||
get {
|
||||
return ResourceManager.GetString("textInformationUpdated", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Interval.
|
||||
/// </summary>
|
||||
@ -885,15 +848,6 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The participant has not provided any info.
|
||||
/// </summary>
|
||||
public static string textMissingData {
|
||||
get {
|
||||
return ResourceManager.GetString("textMissingData", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Moored in lock.
|
||||
/// </summary>
|
||||
@ -931,47 +885,11 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Relevant next 24hrs.
|
||||
/// Looks up a localized string similar to No port assigned to this participant.
|
||||
/// </summary>
|
||||
public static string textNext24h {
|
||||
public static string textNoPortAssigned {
|
||||
get {
|
||||
return ResourceManager.GetString("textNext24h", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Notifications.
|
||||
/// </summary>
|
||||
public static string textNotifications {
|
||||
get {
|
||||
return ResourceManager.GetString("textNotifications", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Notify by e-mail.
|
||||
/// </summary>
|
||||
public static string textNotifyEmail {
|
||||
get {
|
||||
return ResourceManager.GetString("textNotifyEmail", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Notify on.
|
||||
/// </summary>
|
||||
public static string textNotifyOn {
|
||||
get {
|
||||
return ResourceManager.GetString("textNotifyOn", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Notify by push notification in app.
|
||||
/// </summary>
|
||||
public static string textNotifyPush {
|
||||
get {
|
||||
return ResourceManager.GetString("textNotifyPush", resourceCulture);
|
||||
return ResourceManager.GetString("textNoPortAssigned", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1281,24 +1199,6 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Shipcall.
|
||||
/// </summary>
|
||||
public static string textShipcall {
|
||||
get {
|
||||
return ResourceManager.GetString("textShipcall", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The shipcall was cancelled.
|
||||
/// </summary>
|
||||
public static string textShipcallCancelled {
|
||||
get {
|
||||
return ResourceManager.GetString("textShipcallCancelled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Ship length.
|
||||
/// </summary>
|
||||
@ -1335,15 +1235,6 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show notificiations.
|
||||
/// </summary>
|
||||
public static string textShowNotifications {
|
||||
get {
|
||||
return ResourceManager.GetString("textShowNotifications", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Sort order.
|
||||
/// </summary>
|
||||
@ -1407,24 +1298,6 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Participants disagree on times.
|
||||
/// </summary>
|
||||
public static string textTimeConflict {
|
||||
get {
|
||||
return ResourceManager.GetString("textTimeConflict", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Times conflict resolved.
|
||||
/// </summary>
|
||||
public static string textTimeConflictResolved {
|
||||
get {
|
||||
return ResourceManager.GetString("textTimeConflictResolved", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Timestamp.
|
||||
/// </summary>
|
||||
@ -1515,15 +1388,6 @@ namespace BreCalClient.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Participant unassigned from shipcall.
|
||||
/// </summary>
|
||||
public static string textUnassigned {
|
||||
get {
|
||||
return ResourceManager.GetString("textUnassigned", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to User login.
|
||||
/// </summary>
|
||||
|
||||
@ -556,49 +556,7 @@
|
||||
<data name="textStartTimeMissing" xml:space="preserve">
|
||||
<value>Wenn eine Ende-Zeit angegeben wird, muss auch eine Start-Zeit angegeben werden</value>
|
||||
</data>
|
||||
<data name="textInformationUpdated" xml:space="preserve">
|
||||
<value>Einstellungen erfolgreich aktualisiert</value>
|
||||
</data>
|
||||
<data name="textNotifications" xml:space="preserve">
|
||||
<value>Benachrichtigungen</value>
|
||||
</data>
|
||||
<data name="textNotifyEmail" xml:space="preserve">
|
||||
<value>E-Mail Benachrichtigung</value>
|
||||
</data>
|
||||
<data name="textNotifyPush" xml:space="preserve">
|
||||
<value>Banner / Push Benachrichtigung in App</value>
|
||||
</data>
|
||||
<data name="textAssignment" xml:space="preserve">
|
||||
<value>Teilnehmer wurde nominiert</value>
|
||||
</data>
|
||||
<data name="textDate" xml:space="preserve">
|
||||
<value>Datum</value>
|
||||
</data>
|
||||
<data name="textNext24h" xml:space="preserve">
|
||||
<value>Relevant für Morgenrunde (24hrs)</value>
|
||||
</data>
|
||||
<data name="textShipcall" xml:space="preserve">
|
||||
<value>Anlauf</value>
|
||||
</data>
|
||||
<data name="textShowNotifications" xml:space="preserve">
|
||||
<value>Benachrichtigungen anzeigen</value>
|
||||
</data>
|
||||
<data name="textTimeConflict" xml:space="preserve">
|
||||
<value>"Ampel"-Regel(n) wurde verletzt</value>
|
||||
</data>
|
||||
<data name="textTimeConflictResolved" xml:space="preserve">
|
||||
<value>Zeitliche Konflikte aufgelöst</value>
|
||||
</data>
|
||||
<data name="textUnassigned" xml:space="preserve">
|
||||
<value>Nominierung des Teilnehmer entfernt</value>
|
||||
</data>
|
||||
<data name="textMissingData" xml:space="preserve">
|
||||
<value>Der Teilnehmer hat keine Daten eingetragen</value>
|
||||
</data>
|
||||
<data name="textShipcallCancelled" xml:space="preserve">
|
||||
<value>Der Anlauf wurde storniert</value>
|
||||
</data>
|
||||
<data name="textNotifyOn" xml:space="preserve">
|
||||
<value>Benachrichtigung bei</value>
|
||||
<data name="textNoPortAssigned" xml:space="preserve">
|
||||
<value>Es ist keine Hafenzuordnung vorhanden</value>
|
||||
</data>
|
||||
</root>
|
||||
@ -601,53 +601,8 @@
|
||||
<data name="textStartTimeMissing" xml:space="preserve">
|
||||
<value>If an end time is set, a start time is also required</value>
|
||||
</data>
|
||||
<data name="textInformationUpdated" xml:space="preserve">
|
||||
<value>Information successfully updated</value>
|
||||
</data>
|
||||
<data name="textNotifications" xml:space="preserve">
|
||||
<value>Notifications</value>
|
||||
</data>
|
||||
<data name="textNotifyEmail" xml:space="preserve">
|
||||
<value>Notify by e-mail</value>
|
||||
</data>
|
||||
<data name="textNotifyPush" xml:space="preserve">
|
||||
<value>Notify by push notification in app</value>
|
||||
</data>
|
||||
<data name="bell3" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||
<value>bell3.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</data>
|
||||
<data name="textAssignment" xml:space="preserve">
|
||||
<value>Participant assigned to shipcall</value>
|
||||
</data>
|
||||
<data name="textDate" xml:space="preserve">
|
||||
<value>Date</value>
|
||||
</data>
|
||||
<data name="textNext24h" xml:space="preserve">
|
||||
<value>Relevant next 24hrs</value>
|
||||
</data>
|
||||
<data name="textShipcall" xml:space="preserve">
|
||||
<value>Shipcall</value>
|
||||
</data>
|
||||
<data name="textShowNotifications" xml:space="preserve">
|
||||
<value>Show notificiations</value>
|
||||
</data>
|
||||
<data name="textTimeConflict" xml:space="preserve">
|
||||
<value>Participants disagree on times</value>
|
||||
</data>
|
||||
<data name="textTimeConflictResolved" xml:space="preserve">
|
||||
<value>Times conflict resolved</value>
|
||||
</data>
|
||||
<data name="textUnassigned" xml:space="preserve">
|
||||
<value>Participant unassigned from shipcall</value>
|
||||
</data>
|
||||
<data name="textMissingData" xml:space="preserve">
|
||||
<value>The participant has not provided any info</value>
|
||||
</data>
|
||||
<data name="textShipcallCancelled" xml:space="preserve">
|
||||
<value>The shipcall was cancelled</value>
|
||||
</data>
|
||||
<data name="textNotifyOn" xml:space="preserve">
|
||||
<value>Notify on</value>
|
||||
<data name="textNoPortAssigned" xml:space="preserve">
|
||||
<value>No port assigned to this participant</value>
|
||||
</data>
|
||||
<data name="textPosition" xml:space="preserve">
|
||||
<value>Position</value>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
@ -98,7 +98,7 @@ namespace BreCalClient
|
||||
private async void DataGridShips_CreateRequested()
|
||||
{
|
||||
|
||||
ShipModel shipModel = new(ShipModel.LastEditShip ?? new Ship());
|
||||
ShipModel shipModel = new((ShipModel.LastEditShip != null) ? ShipModel.LastEditShip : new Ship());
|
||||
|
||||
EditShipDialog esd = new()
|
||||
{
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<UserControl x:Class="BreCalClient.ShipcallControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:p = "clr-namespace:BreCalClient.Resources"
|
||||
xmlns:sets="clr-namespace:BreCalClient.Properties"
|
||||
xmlns:db="clr-namespace:BreCalClient;assembly=BreCalTestClient"
|
||||
mc:Ignorable="d"
|
||||
xmlns:db="clr-namespace:BreCalClient;assembly=BreCalClient"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="135" d:DesignWidth="800">
|
||||
<Border BorderBrush="LightGray" Margin="1" BorderThickness="1">
|
||||
<Grid>
|
||||
@ -48,7 +48,7 @@
|
||||
<Viewbox Grid.Column="1">
|
||||
<TextBlock x:Name="textBlockShiftingSequence"></TextBlock>
|
||||
</Viewbox>
|
||||
<Label Grid.Column="2" FontSize="12" x:Name="labelShipName" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
<Label Grid.Column="2" FontSize="12" x:Name="labelShipName" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" PreviewMouseUp="Image_PreviewMouseUp">
|
||||
<TextBlock Name="textBlockShipName" />
|
||||
</Label>
|
||||
@ -95,7 +95,7 @@
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="14" />
|
||||
</Grid.RowDefinitions>
|
||||
<Label Padding="0" Grid.Row="0" Grid.Column="0" Grid.RowSpan="1" FontSize="13" Content="- / -" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
<Label Padding="0" Grid.Row="0" Grid.Column="0" Grid.RowSpan="1" FontSize="13" Content="- / -" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Name="labelAgent" PreviewMouseUp="labelAgent_PreviewMouseUp"/>
|
||||
<Label Name="labelLastChangeAgency" FontSize="9" Padding="0" VerticalContentAlignment="Center" Grid.Row="1" Foreground="WhiteSmoke" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" HorizontalContentAlignment="Center" />
|
||||
</Grid>
|
||||
@ -104,7 +104,7 @@
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="14" />
|
||||
</Grid.RowDefinitions>
|
||||
<Label Padding="0" Grid.Row="0" Grid.Column="0" Grid.RowSpan="1" FontSize="13" Content="- / -" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
<Label Padding="0" Grid.Row="0" Grid.Column="0" Grid.RowSpan="1" FontSize="13" Content="- / -" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Name="labelMooring" PreviewMouseUp="labelMooring_PreviewMouseUp"/>
|
||||
<Label Name="labelLastChangeMooring" FontSize="9" Padding="0" VerticalContentAlignment="Center" Grid.Row="1" Foreground="WhiteSmoke" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" HorizontalContentAlignment="Center" />
|
||||
</Grid>
|
||||
@ -113,7 +113,7 @@
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="14" />
|
||||
</Grid.RowDefinitions>
|
||||
<Label Padding="0" Grid.Row="0" Grid.Column="0" Grid.RowSpan="1" FontSize="13" Content="- / -" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
<Label Padding="0" Grid.Row="0" Grid.Column="0" Grid.RowSpan="1" FontSize="13" Content="- / -" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Name="labelPortAuthority" PreviewMouseUp="labelPortAuthority_PreviewMouseUp"/>
|
||||
<Label Name="labelLastChangePortAuthority" FontSize="9" Padding="0" VerticalContentAlignment="Center" Grid.Row="1" Foreground="WhiteSmoke" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" HorizontalContentAlignment="Center" />
|
||||
</Grid>
|
||||
@ -122,7 +122,7 @@
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="14" />
|
||||
</Grid.RowDefinitions>
|
||||
<Label Padding="0" Grid.Row="0" Grid.Column="0" Grid.RowSpan="1" FontSize="13" Content="- / -" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
<Label Padding="0" Grid.Row="0" Grid.Column="0" Grid.RowSpan="1" FontSize="13" Content="- / -" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Name="labelPilot" PreviewMouseUp="labelPilot_PreviewMouseUp"/>
|
||||
<Label Name="labelLastChangePilot" FontSize="9" Padding="0" VerticalContentAlignment="Center" Grid.Row="1" Foreground="WhiteSmoke" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" HorizontalContentAlignment="Center" />
|
||||
</Grid>
|
||||
@ -131,7 +131,7 @@
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="14" />
|
||||
</Grid.RowDefinitions>
|
||||
<Label Padding="0" Grid.Row="0" Grid.Column="0" Grid.RowSpan="1" FontSize="13" Content="- / -" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
<Label Padding="0" Grid.Row="0" Grid.Column="0" Grid.RowSpan="1" FontSize="13" Content="- / -" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Name="labelTug" PreviewMouseUp="labelTug_PreviewMouseUp"/>
|
||||
<Label Name="labelLastChangeTug" FontSize="9" Padding="0" VerticalContentAlignment="Center" Grid.Row="1" Foreground="WhiteSmoke" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" HorizontalContentAlignment="Center" />
|
||||
</Grid>
|
||||
@ -140,11 +140,11 @@
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="14" />
|
||||
</Grid.RowDefinitions>
|
||||
<Label Padding="0" Grid.Row="0" Grid.Column="0" Grid.RowSpan="1" FontSize="13" Content="- / -" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
<Label Padding="0" Grid.Row="0" Grid.Column="0" Grid.RowSpan="1" FontSize="13" Content="- / -" Foreground="White" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Name="labelTerminal" PreviewMouseUp="labelTerminal_PreviewMouseUp"/>
|
||||
<Label Name="labelLastChangeTerminal" FontSize="9" Padding="0" VerticalContentAlignment="Center" Grid.Row="1" Foreground="WhiteSmoke" Background="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" HorizontalContentAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
|
||||
<!-- AGENCY -->
|
||||
<Border Grid.Row="2" Grid.Column="1" BorderThickness="1, 0, 0, 0" BorderBrush="{Binding Source={x:Static sets:Settings.Default}, Path=BG_COLOR}" Padding="3,0,0,0">
|
||||
<Grid Grid.Row="2" Grid.Column="1">
|
||||
@ -158,16 +158,16 @@
|
||||
<RowDefinition Height="14" />
|
||||
<RowDefinition Height=".5*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
|
||||
<Label Grid.Row="0" Grid.Column="0" Content = "ETA" x:Name="labelETAETDAgent" Padding="0" VerticalContentAlignment="Center" />
|
||||
<Label Grid.Row="1" Grid.Column="0" Content="{x:Static p:Resources.textRemarks}" Padding="0" VerticalContentAlignment="Center" FontSize="9"/>
|
||||
<Border Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" BorderThickness="0,1,0,0" BorderBrush="Gray" >
|
||||
<Label Content="{x:Static p:Resources.textBerth}" Padding="0" VerticalContentAlignment="Center" FontSize="9"/>
|
||||
<Label Content="{x:Static p:Resources.textBerth}" Padding="0" VerticalContentAlignment="Center" FontSize="9"/>
|
||||
</Border>
|
||||
|
||||
|
||||
<Label Grid.Row="0" Grid.Column="1" Padding="0" VerticalContentAlignment="Center" x:Name="labelAgencyETAETDValue" FontWeight="DemiBold"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Grid.RowSpan="1" Padding="0" TextWrapping="Wrap" VerticalAlignment="Top" x:Name="textBlockAgencyRemarks" FontSize="10"/>
|
||||
|
||||
|
||||
<Label Grid.Row="2" Grid.Column="1" HorizontalContentAlignment="Left" x:Name="labelAgencyBerth" Padding="0" VerticalContentAlignment="Center" FontSize="11" FontWeight="SemiBold" />
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="{x:Static p:Resources.textRemarks}" Padding="0" VerticalAlignment="Top" TextWrapping="Wrap" FontSize="9"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" Grid.RowSpan="1" Padding="0" TextWrapping="Wrap" VerticalAlignment="Top" x:Name="textBlockAgencyBerthRemarks" FontSize="10"/>
|
||||
|
||||
@ -25,7 +25,7 @@ namespace BreCalClient
|
||||
Participant? _terminal;
|
||||
Participant? _tug;
|
||||
Participant? _port_administration;
|
||||
private static readonly ILog _log = LogManager.GetLogger(typeof(ShipcallControl));
|
||||
private static readonly ILog _log = LogManager.GetLogger(typeof(ShipcallControl));
|
||||
|
||||
#endregion
|
||||
|
||||
@ -82,7 +82,7 @@ namespace BreCalClient
|
||||
this.labelAgencyBerth.Content = "";
|
||||
this.labelAgencyETAETDValue.Content = "";
|
||||
this.textBlockAgencyRemarks.Text = "";
|
||||
this.textBlockAgencyBerthRemarks.Text = "";
|
||||
this.textBlockAgencyBerthRemarks.Text = "";
|
||||
}
|
||||
|
||||
_mooring = this.ShipcallControlModel?.GetParticipantForType(Extensions.ParticipantType.MOORING);
|
||||
@ -215,13 +215,13 @@ namespace BreCalClient
|
||||
switch (this.ShipcallControlModel?.Shipcall?.Type)
|
||||
{
|
||||
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:,,,/BreCalClient;component/Resources/arrow_down_red.png"));
|
||||
break;
|
||||
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:,,,/BreCalClient;component/Resources/arrow_up_blue.png"));
|
||||
break;
|
||||
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:,,,/BreCalClient;component/Resources/arrow_right_green.png"));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -230,13 +230,13 @@ namespace BreCalClient
|
||||
switch(this.ShipcallControlModel?.LightMode)
|
||||
{
|
||||
case EvaluationType.Green:
|
||||
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalTestClient;component/Resources/check.png"));
|
||||
this.imageEvaluation.Source = new BitmapImage(new Uri("pack://application:,,,/BreCalClient;component/Resources/check.png"));
|
||||
break;
|
||||
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:,,,/BreCalClient;component/Resources/sign_warning.png"));
|
||||
break;
|
||||
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:,,,/BreCalClient;component/Resources/delete2.png"));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -320,7 +320,7 @@ namespace BreCalClient
|
||||
{
|
||||
this.labelLastChangeAgency.Foreground = Brushes.White;
|
||||
this.labelLastChangeAgency.FontWeight = FontWeights.DemiBold;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.labelLastChangeAgency.Foreground = Brushes.LightGray;
|
||||
@ -349,13 +349,13 @@ namespace BreCalClient
|
||||
atdRowDefinition.Height = mooringTimes.Atd.HasValue ? new(15) : new(0);
|
||||
|
||||
if(mooringTimes.Ata.HasValue)
|
||||
{
|
||||
labelTimesMooringATA.Content = mooringTimes.Ata.Value.ToString("dd.MM.yyyy HH:mm");
|
||||
{
|
||||
labelTimesMooringATA.Content = mooringTimes.Ata.Value.ToString("dd.MM.yyyy HH:mm");
|
||||
}
|
||||
|
||||
if (mooringTimes.Atd.HasValue)
|
||||
{
|
||||
labelTimesMooringATD.Content = mooringTimes.Atd.Value.ToString("dd.MM.yyyy HH:mm");
|
||||
{
|
||||
labelTimesMooringATD.Content = mooringTimes.Atd.Value.ToString("dd.MM.yyyy HH:mm");
|
||||
}
|
||||
|
||||
DateTime? lc = this.ShipcallControlModel?.GetLastChangeForType(Extensions.ParticipantType.MOORING);
|
||||
@ -388,7 +388,7 @@ namespace BreCalClient
|
||||
this.imagePortAuthorityLocked.Visibility = (portAuthorityTimes.EtaBerthFixed ?? false) ? Visibility.Visible : Visibility.Hidden;
|
||||
lockTimeRowDefinition.Height = portAuthorityTimes.LockTime.HasValue ? new(15) : new(0);
|
||||
if(portAuthorityTimes.LockTime.HasValue)
|
||||
{
|
||||
{
|
||||
labelPortAuthorityLockTime.Content = portAuthorityTimes.LockTime.Value.ToString("dd.MM.yyyy HH:mm");
|
||||
}
|
||||
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
// Copyright (c) 2024- schick Informatik
|
||||
// Description:
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
|
||||
using ToastNotifications;
|
||||
using ToastNotifications.Core;
|
||||
using ToastNotifications.Lifetime;
|
||||
using ToastNotifications.Lifetime.Clear;
|
||||
using ToastNotifications.Messages;
|
||||
using ToastNotifications.Position;
|
||||
|
||||
namespace BreCalClient
|
||||
{
|
||||
internal class ToastViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly Notifier _notifier;
|
||||
|
||||
public ToastViewModel()
|
||||
{
|
||||
_notifier = new Notifier(cfg =>
|
||||
{
|
||||
cfg.PositionProvider = new WindowPositionProvider(
|
||||
parentWindow: Application.Current.MainWindow,
|
||||
corner: Corner.BottomRight,
|
||||
offsetX: 25,
|
||||
offsetY: 100);
|
||||
|
||||
cfg.LifetimeSupervisor = new TimeAndCountBasedLifetimeSupervisor(
|
||||
notificationLifetime: TimeSpan.FromSeconds(30),
|
||||
maximumNotificationCount: MaximumNotificationCount.FromCount(6));
|
||||
|
||||
cfg.Dispatcher = Application.Current.Dispatcher;
|
||||
|
||||
cfg.DisplayOptions.TopMost = false;
|
||||
cfg.DisplayOptions.Width = 250;
|
||||
});
|
||||
|
||||
_notifier.ClearMessages(new ClearAll());
|
||||
}
|
||||
|
||||
public void OnUnloaded()
|
||||
{
|
||||
_notifier.Dispose();
|
||||
}
|
||||
|
||||
public void ShowAppNotification(string message, MessageOptions options)
|
||||
{
|
||||
_notifier?.ShowAppNotification(message, options);
|
||||
}
|
||||
|
||||
public void ShowAppNotification(string message)
|
||||
{
|
||||
_notifier?.ShowAppNotification(message);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged(string? propertyName = null)
|
||||
{
|
||||
var handler = PropertyChanged;
|
||||
handler?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
public void ClearAll()
|
||||
{
|
||||
_notifier.ClearMessages(new ClearAll());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@
|
||||
<applicationSettings>
|
||||
<RoleEditor.Properties.Settings>
|
||||
<setting name="ConnectionString" serializeAs="String">
|
||||
<value>Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_test;Port=33307</value>
|
||||
<value>Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_test;Port=33306</value>
|
||||
</setting>
|
||||
</RoleEditor.Properties.Settings>
|
||||
</applicationSettings>
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
<Label Content="Street" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right"/>
|
||||
<Label Content="Postal code" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Right"/>
|
||||
<Label Content="City" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Right"/>
|
||||
<Label Content="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="Created" Grid.Row="6" Grid.Column="1" HorizontalAlignment="Right"/>
|
||||
<Label Content="Modified" Grid.Row="7" Grid.Column="1" HorizontalAlignment="Right"/>
|
||||
@ -67,7 +67,7 @@
|
||||
<TextBox x:Name="textBoxParticipantStreet" Grid.Row="1" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
|
||||
<TextBox x:Name="textBoxParticipantPostalCode" Grid.Row="2" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
|
||||
<TextBox x:Name="textBoxParticipantCity" Grid.Row="3" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
|
||||
<CheckBox x:Name="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" />
|
||||
<TextBox x:Name="textBoxParticipantCreated" Grid.Row="6" IsReadOnly="True" IsEnabled="False" Grid.Column="2" Margin="2" VerticalContentAlignment="Center" />
|
||||
<StackPanel Orientation="Horizontal" Grid.Row="7" Grid.Column="0">
|
||||
@ -167,7 +167,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<ListBox x:Name="listBoxUser" Margin="2" Grid.RowSpan="9" SelectionChanged="listBoxUser_SelectionChanged">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu Name="contextMenuUser">
|
||||
<ContextMenu>
|
||||
<MenuItem x:Name="menuItemNewUser" Header="New.." Click="menuItemNewUser_Click">
|
||||
<MenuItem.Icon>
|
||||
<Image Source="Resources/add.png" />
|
||||
|
||||
@ -61,8 +61,6 @@ namespace RoleEditor
|
||||
|
||||
// load all participants
|
||||
List<Participant> participants = await Participant.LoadAll(_dbManager);
|
||||
participants.Sort((x, y) => string.Compare(x.Name, y.Name));
|
||||
|
||||
foreach (Participant p in participants)
|
||||
{
|
||||
_participants.Add(p);
|
||||
@ -466,9 +464,9 @@ namespace RoleEditor
|
||||
this.textBoxParticipantName.Text = (p != null) ? p.Name : string.Empty;
|
||||
this.textBoxParticipantStreet.Text = (p != null) ? p.Street : string.Empty;
|
||||
this.textBoxParticipantPostalCode.Text = (p != null) ? p.PostalCode : string.Empty;
|
||||
this.textBoxParticipantCity.Text = (p != null) ? p.City : string.Empty;
|
||||
this.textBoxParticipantCity.Text = (p != null) ? p.City : string.Empty;
|
||||
// this.checkboxParticipantActive.Checked = (p != null) ? p.
|
||||
this.textBoxParticipantCreated.Text = (p != null) ? p.Created.ToString() : string.Empty;
|
||||
this.checkboxParticipantDeleted.IsChecked = (p != null) ? p.Deleted : null;
|
||||
this.textBoxParticipantModified.Text = (p != null) ? p.Modified.ToString() : string.Empty;
|
||||
this.checkBoxParticipantAllowBSMD.IsChecked = (p != null) ? p.IsFlagSet(Participant.ParticipantFlags.ALLOW_BSMD) : null;
|
||||
this.comboBoxParticipantType.SelectedItems.Clear();
|
||||
@ -512,11 +510,6 @@ namespace RoleEditor
|
||||
_assignedPorts.Add(pa);
|
||||
}
|
||||
}
|
||||
|
||||
this.contextMenuUser.IsEnabled = !p.Deleted;
|
||||
this.buttonAddPortAssignment.IsEnabled = !p.Deleted;
|
||||
this.buttonRemovePortAssignment.IsEnabled = !p.Deleted;
|
||||
|
||||
}
|
||||
|
||||
private async void listBoxRoles_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
@ -601,7 +594,7 @@ namespace RoleEditor
|
||||
if(this.listBoxParticipant.SelectedItem is Participant p)
|
||||
{
|
||||
await p.Delete(_dbManager);
|
||||
p.Deleted = true;
|
||||
this._participants.Remove(p);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -635,7 +628,6 @@ namespace RoleEditor
|
||||
{
|
||||
if (this.listBoxUser.SelectedItem is User u)
|
||||
{
|
||||
await u.ExecuteNonQuery(_dbManager); // extra history delete happens here
|
||||
await u.Delete(_dbManager);
|
||||
this._users.Remove(u);
|
||||
}
|
||||
|
||||
10
src/RoleEditor/Properties/Settings.Designer.cs
generated
10
src/RoleEditor/Properties/Settings.Designer.cs
generated
@ -9,20 +9,20 @@
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace RoleEditor.Properties {
|
||||
|
||||
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.10.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
|
||||
public static Settings Default {
|
||||
get {
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[global::System.Configuration.ApplicationScopedSettingAttribute()]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Configuration.DefaultSettingValueAttribute("Server=localhost;User ID=ds;Password=HalloWach_2323XXL!!;Database=bremen_calling_" +
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWPF>true</UseWPF>
|
||||
<ApplicationIcon>Resources\lock_preferences.ico</ApplicationIcon>
|
||||
<FileVersion>1.8.0.0</FileVersion>
|
||||
<AssemblyVersion>1.8.0.0</AssemblyVersion>
|
||||
<FileVersion>1.6.0.4</FileVersion>
|
||||
<AssemblyVersion>1.6.0.4</AssemblyVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -30,8 +30,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="ExcelDataReader" Version="3.8.0" />
|
||||
<PackageReference Include="Extended.Wpf.Toolkit" Version="5.0.0" />
|
||||
<PackageReference Include="ExcelDataReader" Version="3.7.0-develop00385" />
|
||||
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -67,6 +67,14 @@
|
||||
"PrivateKeyFile" = "8:"
|
||||
"TimeStampServer" = "8:"
|
||||
"InstallerBootstrapper" = "3:2"
|
||||
"BootstrapperCfg:{63ACBE69-63AA-4F98-B2B6-99F9E24495F2}"
|
||||
{
|
||||
"Enabled" = "11:TRUE"
|
||||
"PromptEnabled" = "11:TRUE"
|
||||
"PrerequisitesLocation" = "2:1"
|
||||
"Url" = "8:"
|
||||
"ComponentsUrl" = "8:"
|
||||
}
|
||||
}
|
||||
"Release"
|
||||
{
|
||||
@ -83,6 +91,14 @@
|
||||
"PrivateKeyFile" = "8:"
|
||||
"TimeStampServer" = "8:"
|
||||
"InstallerBootstrapper" = "3:2"
|
||||
"BootstrapperCfg:{63ACBE69-63AA-4F98-B2B6-99F9E24495F2}"
|
||||
{
|
||||
"Enabled" = "11:TRUE"
|
||||
"PromptEnabled" = "11:TRUE"
|
||||
"PrerequisitesLocation" = "2:1"
|
||||
"Url" = "8:"
|
||||
"ComponentsUrl" = "8:"
|
||||
}
|
||||
}
|
||||
}
|
||||
"Deployable"
|
||||
@ -271,15 +287,15 @@
|
||||
{
|
||||
"Name" = "8:Microsoft Visual Studio"
|
||||
"ProductName" = "8:Bremen calling"
|
||||
"ProductCode" = "8:{6EF71F76-3E55-483B-A032-4B29FDAFE6A4}"
|
||||
"PackageCode" = "8:{8E55F30F-A9C8-41E9-BCE8-431770A951ED}"
|
||||
"ProductCode" = "8:{0AD2A791-5208-4D6F-8601-598EE8FE9BF7}"
|
||||
"PackageCode" = "8:{FE992D07-0C79-4C5A-9D71-66C8F3EAFB55}"
|
||||
"UpgradeCode" = "8:{1C7FA3E4-BAB9-4911-9348-73094357FC7C}"
|
||||
"AspNetVersion" = "8:"
|
||||
"RestartWWWService" = "11:FALSE"
|
||||
"RemovePreviousVersions" = "11:TRUE"
|
||||
"DetectNewerInstalledVersion" = "11:TRUE"
|
||||
"InstallAllUsers" = "11:FALSE"
|
||||
"ProductVersion" = "8:1.2.2"
|
||||
"ProductVersion" = "8:1.6.1"
|
||||
"Manufacturer" = "8:Informatikbüro Daniel Schick"
|
||||
"ARPHELPTELEPHONE" = "8:+49 (0) 421 - 38 48 27"
|
||||
"ARPHELPLINK" = "8:https://www.bsmd.de/"
|
||||
@ -821,7 +837,7 @@
|
||||
{
|
||||
"{5259A561-127C-4D43-A0A1-72F10C7B3BF8}:_3E48B6E716164CC1826E094025517B3F"
|
||||
{
|
||||
"SourcePath" = "8:..\\BreCalClient\\obj\\Release\\net6.0-windows\\apphost.exe"
|
||||
"SourcePath" = "8:..\\BreCalClient\\obj\\Debug\\net6.0-windows\\apphost.exe"
|
||||
"TargetName" = "8:"
|
||||
"Tag" = "8:"
|
||||
"Folder" = "8:_7C8F7547CED64B3CAE7D0296A8BCE23F"
|
||||
|
||||
@ -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;
|
||||
|
||||
namespace brecal.model
|
||||
@ -38,11 +42,6 @@ namespace brecal.model
|
||||
/// <param name="cmd">CMD created by DB manager</param>
|
||||
public abstract void SetDelete(IDbCommand cmd);
|
||||
|
||||
public virtual void SetNonQuery(IDbCommand cmd)
|
||||
{
|
||||
// default: do nothing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Each database entity must be able to save itself to the database
|
||||
/// </summary>
|
||||
@ -62,13 +61,8 @@ namespace brecal.model
|
||||
/// Each entity must be able to delete itself
|
||||
/// </summary>
|
||||
public async Task Delete(IDBManager manager)
|
||||
{
|
||||
await manager.ExecuteNonQuery(this.SetDelete);
|
||||
}
|
||||
|
||||
public async Task ExecuteNonQuery(IDBManager manager)
|
||||
{
|
||||
await manager.ExecuteNonQuery(this.SetNonQuery);
|
||||
await manager.ExecuteNonQuery(this.SetDelete);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -55,8 +55,6 @@ namespace brecal.model
|
||||
|
||||
public uint Flags { get; set; }
|
||||
|
||||
public bool Deleted { get; set; } = false;
|
||||
|
||||
#endregion
|
||||
|
||||
#region public static methods
|
||||
@ -85,7 +83,6 @@ namespace brecal.model
|
||||
if (!reader.IsDBNull(6)) p.Flags = (uint)reader.GetInt32(6);
|
||||
if (!reader.IsDBNull(7)) p.Created = reader.GetDateTime(7);
|
||||
if (!reader.IsDBNull(8)) p.Modified = reader.GetDateTime(8);
|
||||
if (!reader.IsDBNull(9)) p.Deleted = reader.GetBoolean(9);
|
||||
result.Add(p);
|
||||
}
|
||||
return result;
|
||||
@ -93,7 +90,7 @@ namespace brecal.model
|
||||
|
||||
public static void SetLoadQuery(IDbCommand cmd, params object?[] list)
|
||||
{
|
||||
cmd.CommandText = "SELECT id, name, street, postal_code, city, type, flags, created, modified, deleted FROM participant";
|
||||
cmd.CommandText = "SELECT id, name, street, postal_code, city, type, flags, created, modified FROM participant";
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -114,13 +111,13 @@ namespace brecal.model
|
||||
|
||||
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();
|
||||
idParam.ParameterName = "ID";
|
||||
idParam.Value = this.Id;
|
||||
cmd.Parameters.Add(idParam);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@ -101,16 +101,6 @@ namespace brecal.model
|
||||
return this.Username ?? $"{base.Id} - {this.GetType().Name}";
|
||||
}
|
||||
|
||||
public override void SetNonQuery(IDbCommand cmd)
|
||||
{
|
||||
cmd.CommandText = "UPDATE history set user_id = NULL WHERE user_id = @ID";
|
||||
|
||||
IDataParameter idParam = cmd.CreateParameter();
|
||||
idParam.ParameterName = "ID";
|
||||
idParam.Value = this.Id;
|
||||
cmd.Parameters.Add(idParam);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region private methods
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MySqlConnector" Version="2.4.0" />
|
||||
<PackageReference Include="MySqlConnector" Version="2.3.0-beta.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from flask import Flask
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
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
|
||||
|
||||
def create_app(test_config=None, instance_path=None):
|
||||
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
app.config.from_mapping(
|
||||
SECRET_KEY='dev'
|
||||
@ -48,8 +48,6 @@ def create_app(test_config=None, instance_path=None):
|
||||
|
||||
if instance_path is not None:
|
||||
app.instance_path = instance_path
|
||||
elif app.config.get("INSTANCE_PATH"):
|
||||
app.instance_path = app.config["INSTANCE_PATH"]
|
||||
|
||||
try:
|
||||
import os
|
||||
@ -71,23 +69,13 @@ def create_app(test_config=None, instance_path=None):
|
||||
app.register_blueprint(history.bp)
|
||||
app.register_blueprint(ports.bp)
|
||||
|
||||
log_level = getattr(logging, app.config.get("LOG_LEVEL", "DEBUG"))
|
||||
log_kwargs = {"format": "%(asctime)s | %(name)s | %(levelname)s | %(message)s"}
|
||||
if app.config.get("LOG_TO_STDERR"):
|
||||
log_kwargs["stream"] = sys.stderr
|
||||
else:
|
||||
log_kwargs["filename"] = app.config.get("LOG_FILE", "brecaltest.log")
|
||||
logging.basicConfig(level=log_level, **log_kwargs)
|
||||
|
||||
if app.config.get("SECRET_KEY"):
|
||||
os.environ["SECRET_KEY"] = app.config["SECRET_KEY"]
|
||||
|
||||
local_db.initPool(os.path.dirname(app.instance_path), config=app.config)
|
||||
logging.basicConfig(filename='brecal.log', level=logging.DEBUG, format='%(asctime)s | %(name)s | %(levelname)s | %(message)s')
|
||||
local_db.initPool(os.path.dirname(app.instance_path))
|
||||
logging.info('App started')
|
||||
|
||||
# Setup Routine jobs (e.g., reevaluation of shipcalls)
|
||||
setup_schedule(update_shipcalls_interval_in_minutes=app.config.get("SCHEDULE_UPDATE_SHIPCALLS_MINUTES", 60))
|
||||
run_schedule_permanently_in_background(latency=app.config.get("SCHEDULE_BACKGROUND_LATENCY_SECONDS", 30))
|
||||
setup_schedule(update_shipcalls_interval_in_minutes=60)
|
||||
run_schedule_permanently_in_background(latency=30)
|
||||
logging.info('Routine Jobs are defined.')
|
||||
|
||||
return app
|
||||
|
||||
@ -12,18 +12,12 @@ bp = Blueprint('notifications', __name__)
|
||||
@auth_guard() # no restriction by role
|
||||
def GetNotifications():
|
||||
try:
|
||||
if 'Authorization' in request.headers:
|
||||
token = request.headers.get('Authorization')
|
||||
participant_id = None
|
||||
if 'participant_id' in request.args:
|
||||
try:
|
||||
participant_id = int(request.args.get('participant_id'))
|
||||
except (TypeError, ValueError):
|
||||
return create_dynamic_exception_response(ex=None, status_code=400, message="participant_id must be an integer")
|
||||
|
||||
return impl.notifications.GetNotifications(token, participant_id=participant_id)
|
||||
if 'shipcall_id' in request.args:
|
||||
options = {}
|
||||
options["shipcall_id"] = request.args.get("shipcall_id")
|
||||
return impl.notifications.GetNotifications(options)
|
||||
else:
|
||||
return create_dynamic_exception_response(ex=None, status_code=403, message="not authenticated")
|
||||
|
||||
return create_dynamic_exception_response(ex=None, status_code=400, message="missing argument: shipcall_id")
|
||||
|
||||
except Exception as ex:
|
||||
return create_dynamic_exception_response(ex=ex, status_code=400)
|
||||
|
||||
@ -89,7 +89,7 @@ def PutShipcalls():
|
||||
|
||||
# validate the PUT shipcall data and the user's authority
|
||||
InputValidationShipcall.evaluate_put_data(user_data, loadedModel, content)
|
||||
return impl.shipcalls.PutShipcalls(loadedModel, content)
|
||||
return impl.shipcalls.PutShipcalls(loadedModel)
|
||||
|
||||
except ValidationError as ex:
|
||||
return create_validation_error_response(ex=ex, status_code=400)
|
||||
|
||||
@ -72,7 +72,7 @@ def PutTimes():
|
||||
|
||||
# validate the request
|
||||
InputValidationTimes.evaluate_put_data(user_data, loadedModel, content)
|
||||
return impl.times.PutTimes(loadedModel, content)
|
||||
return impl.times.PutTimes(loadedModel)
|
||||
|
||||
except ValidationError as ex:
|
||||
return create_validation_error_response(ex=ex, status_code=400)
|
||||
|
||||
@ -2,35 +2,29 @@ from flask import Blueprint, request
|
||||
from ..schemas import model
|
||||
from .. import impl
|
||||
from ..services.auth_guard import auth_guard
|
||||
|
||||
import json
|
||||
import logging
|
||||
from marshmallow import ValidationError
|
||||
from . import verify_if_request_is_json
|
||||
from BreCal.validators.validation_error import create_dynamic_exception_response, create_validation_error_response
|
||||
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
bp = Blueprint('user', __name__)
|
||||
|
||||
@bp.route('/user', methods=['put'])
|
||||
@auth_guard() # no restriction by role
|
||||
def PutUser():
|
||||
|
||||
content = None
|
||||
try:
|
||||
verify_if_request_is_json(request)
|
||||
|
||||
|
||||
content = request.get_json(force=True)
|
||||
loadedModel = model.UserSchema().load(data=content, many=False, partial=True)
|
||||
return impl.user.PutUser(loadedModel)
|
||||
|
||||
|
||||
except ValidationError as ex:
|
||||
logging.warning("UserSchema validation failed. Payload=%s", json.dumps(content, default=str))
|
||||
return create_validation_error_response(ex=ex, status_code=400)
|
||||
|
||||
except Exception as ex:
|
||||
logging.error("UserSchema load failed. Payload=%s\n%s", json.dumps(content, default=str), traceback.format_exc())
|
||||
return create_dynamic_exception_response(ex=None, status_code=400, message="bad format")
|
||||
|
||||
|
||||
|
||||
@ -47,10 +47,17 @@ class PierSide(IntEnum):
|
||||
"""These enumerators determine the pier side of a shipcall."""
|
||||
PORTSIDE = 0 # Port/Backbord
|
||||
STARBOARD_SIDE = 1 # Starboard / Steuerbord
|
||||
|
||||
class NotificationType(IntFlag):
|
||||
"""determines the method by which a notification is distributed to users. Flagging allows selecting multiple notification types."""
|
||||
UNDEFINED = 0
|
||||
EMAIL = 1
|
||||
POPUP = 2
|
||||
MESSENGER = 4
|
||||
|
||||
class ParticipantFlag(IntFlag):
|
||||
"""
|
||||
| 1 | If this flag is set on a shipcall record with participant type Agency (8),
|
||||
| 1 | If this flag is set on a shipcall record with participant type Agency (8),
|
||||
all participants of type BSMD (1) may edit the record.
|
||||
"""
|
||||
undefined = 0
|
||||
|
||||
@ -8,7 +8,7 @@ def create_sql_query_shipcall_get(options:dict)->str:
|
||||
|
||||
args:
|
||||
options : dict. A dictionary, which must contains the 'past_days' key (int). Determines the range
|
||||
by which shipcalls are filtered.
|
||||
by which shipcalls are filtered.
|
||||
"""
|
||||
if "participant_id" not in options: # if no participant_id is given, all shipcalls are selected
|
||||
query = ("SELECT s.id as id, ship_id, port_id, type, eta, voyage, etd, arrival_berth_id, departure_berth_id, tug_required, pilot_required, " +
|
||||
@ -17,7 +17,7 @@ def create_sql_query_shipcall_get(options:dict)->str:
|
||||
"evaluation_message, evaluation_time, evaluation_notifications_sent, s.created as created, s.modified as modified, time_ref_point " +
|
||||
"FROM shipcall s " +
|
||||
"LEFT JOIN times t ON t.shipcall_id = s.id AND t.participant_type = 8 " +
|
||||
"WHERE " +
|
||||
"WHERE " +
|
||||
"(type = 1 AND " +
|
||||
"((t.id IS NOT NULL AND t.eta_berth >= DATE(NOW() - INTERVAL %d DAY)) OR " +
|
||||
"(eta >= DATE(NOW() - INTERVAL %d DAY)))) OR " +
|
||||
@ -61,9 +61,9 @@ def create_sql_query_shipcall_post(schemaModel:dict)->str:
|
||||
if key == "evaluation":
|
||||
continue
|
||||
if key == "evaluation_message":
|
||||
continue
|
||||
continue
|
||||
if key == "type_value":
|
||||
continue
|
||||
continue
|
||||
if key == "evaluation_value":
|
||||
continue
|
||||
if isNotFirst:
|
||||
@ -214,12 +214,12 @@ def create_sql_query_ship_put(schemaModel:dict):
|
||||
class SQLQuery():
|
||||
"""
|
||||
This class provides quick access to different SQL query functions, which creates default queries for the BreCal package.
|
||||
Each method is callable without initializing the SQLQuery object.
|
||||
Each method is callable without initializing the SQLQuery object.
|
||||
|
||||
Example:
|
||||
SQLQuery.get_berths()
|
||||
|
||||
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
|
||||
When the data violates one of the rules, a marshmallow.ValidationError is raised, which details the issues.
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
@ -233,30 +233,30 @@ class SQLQuery():
|
||||
def get_history()->str:
|
||||
query = "SELECT id, participant_id, shipcall_id, timestamp, eta, type, operation FROM history WHERE shipcall_id = ?shipcallid?"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_user()->str:
|
||||
query = "SELECT id, participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash, " +\
|
||||
"api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, notify_event, created, modified FROM user " +\
|
||||
"api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, created, modified FROM user " +\
|
||||
"WHERE user_name = ?username? OR user_email = ?username?"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_notifications()->str:
|
||||
query = "SELECT id, shipcall_id, level, type, message, created, modified FROM notification " + \
|
||||
"WHERE shipcall_id = ?scid?"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_participant_by_user_id()->str:
|
||||
query = "SELECT p.id as id, p.name as name, p.street as street, p.postal_code as postal_code, p.city as city, p.type as type, p.flags as flags, p.created as created, p.modified as modified, p.deleted as deleted FROM participant p INNER JOIN user u WHERE u.participant_id = p.id and u.id = ?userid?"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_participants()->str:
|
||||
query = "SELECT id, name, street, postal_code, city, type, flags, created, modified, deleted FROM participant p ORDER BY p.name"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_shipcalls(options:dict={'past_days':3})->str:
|
||||
# a pytest proves this method to be identical to create_sql_query_shipcall_get(options)
|
||||
@ -278,24 +278,17 @@ class SQLQuery():
|
||||
f"(etd >= DATE(NOW() - INTERVAL {past_days} DAY)))) " + \
|
||||
"ORDER BY eta")
|
||||
return query
|
||||
|
||||
def get_next24hrs_shipcalls()->str:
|
||||
query = ("SELECT s.id as id, ship.name as name FROM shipcall s INNER JOIN ship ON s.ship_id = ship.id LEFT JOIN times t on t.shipcall_id = s.id AND t.participant_type = 8 " + \
|
||||
"WHERE (type = 1 AND (COALESCE(t.eta_berth, eta) >= NOW() AND COALESCE(t.eta_berth, eta) < (NOW() + INTERVAL 1 DAY)))" + \
|
||||
"OR ((type = 2 OR type = 3) AND (COALESCE(t.etd_berth, etd) >= NOW() AND COALESCE(t.etd_berth, etd) < (NOW() + INTERVAL 1 DAY)))"
|
||||
"AND s.canceled = 0")
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_ships()->str:
|
||||
query = "SELECT id, name, imo, callsign, participant_id, length, width, is_tug, bollard_pull, eni, created, modified, deleted FROM ship ORDER BY name"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_ship_by_id()->str:
|
||||
query = "SELECT * FROM ship where id = ?id?"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_times()->str:
|
||||
query = "SELECT id, eta_berth, eta_berth_fixed, etd_berth, etd_berth_fixed, lock_time, lock_time_fixed, " + \
|
||||
@ -303,12 +296,12 @@ class SQLQuery():
|
||||
"berth_id, berth_info, pier_side, participant_type, created, modified, ata, atd, eta_interval_end, etd_interval_end FROM times " + \
|
||||
"WHERE times.shipcall_id = ?scid?"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_user_by_id():
|
||||
query = "SELECT * FROM user where id = ?id?"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_user_put(schemaModel:dict):
|
||||
# a pytest proves this method to be identical to create_sql_query_user_put(schemaModel)
|
||||
@ -333,7 +326,7 @@ class SQLQuery():
|
||||
def get_participant_from_id()->str:
|
||||
query = "SELECT id, type, flags FROM participant WHERE id=?participant_id?"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_post(schemaModel:dict)->str:
|
||||
# a pytest proves this method to be identical to create_sql_query_shipcall_post(schemaModel)
|
||||
@ -356,28 +349,28 @@ class SQLQuery():
|
||||
def get_last_insert_id()->str:
|
||||
query = "select last_insert_id()"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_post_last_insert_id()->str:
|
||||
"""alias function. May be deleted soon"""
|
||||
query = SQLQuery.get_last_insert_id()
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_post_update_shipcall_participant_map()->str:
|
||||
query = "INSERT INTO shipcall_participant_map (shipcall_id, participant_id, type) VALUES (?shipcall_id?, ?participant_id?, ?type?)"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def create_sql_query_history_post()->str:
|
||||
query = create_sql_query_history_post()
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_by_id()->str:
|
||||
query = "SELECT * FROM shipcall where id = ?id?"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_put(schemaModel:dict)->str:
|
||||
# a pytest proves this method to be identical to create_sql_query_shipcall_put(schemaModel)
|
||||
@ -391,12 +384,12 @@ class SQLQuery():
|
||||
|
||||
query = prefix + body + suffix
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_participant_map_by_shipcall_id()->str:
|
||||
query = "SELECT id, participant_id, type FROM shipcall_participant_map where shipcall_id = ?id?"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_participant_map_by_shipcall_id_and_type()->str:
|
||||
query = "SELECT id, participant_id FROM shipcall_participant_map where (shipcall_id = ?id? AND type=?type?)"
|
||||
@ -439,14 +432,14 @@ class SQLQuery():
|
||||
def get_ship_delete_by_id()->str:
|
||||
query = "UPDATE ship SET deleted = 1 WHERE id = ?id?"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_notification_post()->str:
|
||||
raise NotImplementedError()
|
||||
# #TODO: this query is wrong and just a proxy for a POST request
|
||||
query = "INSERT INTO shipcall_participant_map (shipcall_id, participant_id, type) VALUES (?shipcall_id?, ?participant_id?, ?type?)"
|
||||
return query
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_shipcall_put_notification_state()->str:
|
||||
raise NotImplementedError()
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
from BreCal.database.sql_handler import execute_sql_query_standalone
|
||||
|
||||
import datetime
|
||||
|
||||
def get_user_data_for_id(user_id:int, expiration_time:int=90):
|
||||
@ -34,10 +33,4 @@ def get_port_ids_for_participant_id(participant_id:int):
|
||||
"""helper function to load all port ids for a participant"""
|
||||
query = "SELECT port_id FROM participant_port_map where participant_id = ?participant_id?"
|
||||
pdata = execute_sql_query_standalone(query=query, param={"participant_id":participant_id})
|
||||
return pdata
|
||||
|
||||
def get_notification_for_shipcall_and_type(shipcall_id:int, notification_type:int):
|
||||
"""helper function to load a notification for a shipcall and a specific type"""
|
||||
query = "SELECT * FROM notification where shipcall_id = ?shipcall_id? and type = ?type?"
|
||||
pdata = execute_sql_query_standalone(query=query, param={"shipcall_id":shipcall_id, "type":notification_type}, command_type="query")
|
||||
return pdata
|
||||
@ -10,7 +10,6 @@ def GetBerths(options):
|
||||
No parameters, gets all entries
|
||||
"""
|
||||
|
||||
pooledConnection = None
|
||||
try:
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
@ -16,8 +16,6 @@ def GetHistory(options):
|
||||
options["shipcall_id"]: **Id of shipcall**.
|
||||
"""
|
||||
|
||||
pooledConnection = None
|
||||
data = []
|
||||
try:
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
@ -28,6 +26,10 @@ def GetHistory(options):
|
||||
data = commands.query("SELECT id, participant_id, shipcall_id, timestamp, eta, type, operation FROM history WHERE shipcall_id = ?shipcallid?",
|
||||
model=History.from_query_row,
|
||||
param={"shipcallid" : options["shipcall_id"]})
|
||||
|
||||
|
||||
pooledConnection.close()
|
||||
|
||||
except Exception as ex:
|
||||
pdb.pm()
|
||||
logging.error(ex)
|
||||
@ -35,9 +37,6 @@ def GetHistory(options):
|
||||
result = {}
|
||||
result["error_field"] = "call failed"
|
||||
return json.dumps("call failed"), 500
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
|
||||
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
|
||||
@ -6,22 +6,19 @@ import bcrypt
|
||||
from ..schemas import model
|
||||
from .. import local_db
|
||||
from ..services import jwt_handler
|
||||
|
||||
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
|
||||
def GetUser(options):
|
||||
|
||||
pooledConnection = None
|
||||
|
||||
try:
|
||||
if "password" in options and "username" in options:
|
||||
|
||||
hash = bcrypt.hashpw(options["password"].encode('utf-8'), bcrypt.gensalt( 12 )).decode('utf8')
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
# query = SQLQuery.get_user()
|
||||
# data = commands.query(query, model=model.User, param={"username" : options["username"]})
|
||||
data = commands.query("SELECT id, participant_id, first_name, last_name, user_name, user_email, user_phone, password_hash, " +
|
||||
"api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, notify_event, created, modified FROM user " +
|
||||
"api_key, notify_email, notify_whatsapp, notify_signal, notify_popup, created, modified FROM user " +
|
||||
"WHERE user_name = ?username? OR user_email = ?username?",
|
||||
model=model.User, param={"username" : options["username"]})
|
||||
|
||||
@ -34,12 +31,7 @@ def GetUser(options):
|
||||
"last_name": data[0].last_name,
|
||||
"user_name": data[0].user_name,
|
||||
"user_phone": data[0].user_phone,
|
||||
"user_email": data[0].user_email,
|
||||
"notify_email": data[0].notify_email,
|
||||
"notify_whatsapp": data[0].notify_whatsapp,
|
||||
"notify_signal": data[0].notify_signal,
|
||||
"notify_popup": data[0].notify_popup,
|
||||
"notify_on": model.notification_types_to_names(model.bitflag_to_list(data[0].notify_event))
|
||||
"user_email": data[0].user_email
|
||||
}
|
||||
token = jwt_handler.generate_jwt(payload=result, lifetime=120) # generate token valid 60 mins
|
||||
result["token"] = token # add token to user data
|
||||
@ -65,3 +57,7 @@ def GetUser(options):
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
|
||||
# $2b$12$uWLE0r32IrtCV30WkMbVwOdltgeibymZyYAf4ZnQb2Bip8hrkGGwG
|
||||
# $2b$12$.vEapj9xU8z0RK0IpIGeYuRIl0ktdMt4XdJQBhVn.3K2hmvm7qD3y
|
||||
# $2b$12$yL3PiseU70ciwEuMVM4OtuMwR6tNuIT9vvBiBG/uyMrPxa16E2Zqu
|
||||
@ -6,22 +6,22 @@ from ..schemas import model
|
||||
from .. import local_db
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
|
||||
def GetNotifications(token, participant_id=None):
|
||||
def GetNotifications(options):
|
||||
"""
|
||||
Optional filtering by participant_id. Returns delivered (level=2) notifications.
|
||||
:param options: A dictionary containing all the paramters for the Operations
|
||||
options["shipcall_id"]: **Id**. *Example: 42*. Id of referenced ship call.
|
||||
|
||||
"""
|
||||
|
||||
pooledConnection = None
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
query = "SELECT id, shipcall_id, participant_id, level, type, message, created, modified FROM notification WHERE level = 2"
|
||||
params = {}
|
||||
if participant_id is not None:
|
||||
query += " AND participant_id = ?participant_id?"
|
||||
params["participant_id"] = participant_id
|
||||
|
||||
data = commands.query(query, model=model.Notification.from_query_row, param=params if params else None)
|
||||
# query = SQLQuery.get_notifications()
|
||||
# data = commands.query(query, model=model.Notification.from_query_row, param={"scid" : options["shipcall_id"]})
|
||||
data = commands.query("SELECT id, shipcall_id, level, type, message, created, modified FROM notification " +
|
||||
"WHERE shipcall_id = ?scid?", model=model.Notification.from_query_row, param={"scid" : options["shipcall_id"]})
|
||||
pooledConnection.close()
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
@ -29,9 +29,6 @@ def GetNotifications(token, participant_id=None):
|
||||
result = {}
|
||||
result["error_field"] = "call failed"
|
||||
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
|
||||
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@ def GetParticipant(options):
|
||||
options["user_id"]: **Id of user**. *Example: 2*. User id returned by login call.
|
||||
"""
|
||||
|
||||
pooledConnection = None
|
||||
try:
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
@ -11,7 +11,6 @@ def GetPorts(token):
|
||||
No parameters, gets all entries
|
||||
"""
|
||||
|
||||
pooledConnection = None
|
||||
try:
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
@ -8,8 +8,7 @@ from .. import local_db
|
||||
from ..services.auth_guard import check_jwt
|
||||
|
||||
from BreCal.database.update_database import evaluate_shipcall_state
|
||||
from BreCal.database.sql_utils import get_notification_for_shipcall_and_type, get_ship_data_for_id
|
||||
from BreCal.database.sql_queries import create_sql_query_shipcall_get
|
||||
from BreCal.database.sql_queries import create_sql_query_shipcall_get, create_sql_query_shipcall_post, create_sql_query_shipcall_put, create_sql_query_history_post, create_sql_query_history_put, SQLQuery
|
||||
from marshmallow import Schema, fields, ValidationError
|
||||
from BreCal.validators.validation_error import create_validation_error_response
|
||||
|
||||
@ -18,8 +17,8 @@ def GetShipcalls(options):
|
||||
No parameters, gets all entries
|
||||
"""
|
||||
|
||||
pooledConnection = None
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
# query = SQLQuery.get_shipcalls(options)
|
||||
@ -70,13 +69,12 @@ def PostShipcalls(schemaModel):
|
||||
"""
|
||||
|
||||
# This creates a *new* entry
|
||||
pooledConnection = None
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
# query = SQLQuery.get_shipcall_post(schemaModel) # create_sql_query_shipcall_post(schemaModel)
|
||||
|
||||
query = "INSERT INTO shipcall ("
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
@ -135,27 +133,12 @@ def PostShipcalls(schemaModel):
|
||||
# new_id = commands.execute_scalar(lquery)
|
||||
new_id = commands.execute_scalar("select last_insert_id()")
|
||||
|
||||
shipdata = get_ship_data_for_id(schemaModel["ship_id"])
|
||||
message = shipdata['name']
|
||||
if "type_value" in schemaModel:
|
||||
match schemaModel["type_value"]:
|
||||
case 1:
|
||||
message += " [ARRIVAL]"
|
||||
case 2:
|
||||
message += " [DEPARTURE]"
|
||||
case 3:
|
||||
message += " [SHIFTING]"
|
||||
|
||||
|
||||
# add participant assignments if we have a list of participants
|
||||
if 'participants' in schemaModel:
|
||||
# pquery = SQLQuery.get_shipcall_post_update_shipcall_participant_map()
|
||||
pquery = "INSERT INTO shipcall_participant_map (shipcall_id, participant_id, type) VALUES (?shipcall_id?, ?participant_id?, ?type?)"
|
||||
nquery = "INSERT INTO notification (shipcall_id, participant_id, level, type, message) VALUES (?shipcall_id?, ?participant_id?, 0, 1, ?message?)" # type = 1 is assignment
|
||||
|
||||
for participant_assignment in schemaModel["participants"]:
|
||||
commands.execute(pquery, param={"shipcall_id" : new_id, "participant_id" : participant_assignment["participant_id"], "type" : participant_assignment["type"]})
|
||||
commands.execute(nquery, param={"shipcall_id" : new_id, "participant_id" : participant_assignment["participant_id"], "message" : message})
|
||||
|
||||
# apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database
|
||||
# evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=new_id) # new_id (last insert id) refers to the shipcall id
|
||||
@ -185,15 +168,15 @@ def PostShipcalls(schemaModel):
|
||||
pooledConnection.close()
|
||||
|
||||
|
||||
def PutShipcalls(schemaModel, original_payload=None):
|
||||
def PutShipcalls(schemaModel):
|
||||
"""
|
||||
|
||||
:param schemaModel: The deserialized dict of the request
|
||||
"""
|
||||
|
||||
# This updates an *existing* entry
|
||||
pooledConnection = None
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
@ -202,19 +185,17 @@ def PutShipcalls(schemaModel, original_payload=None):
|
||||
# test if object to update is found
|
||||
|
||||
sentinel = object()
|
||||
|
||||
# query = SQLQuery.get_shipcall_by_id()
|
||||
# theshipcall = commands.query_single_or_default(query, sentinel, param={"id" : schemaModel["id"]})
|
||||
theshipcall = commands.query_single_or_default("SELECT * FROM shipcall where id = ?id?", sentinel, param={"id" : schemaModel["id"]})
|
||||
if theshipcall is sentinel:
|
||||
pooledConnection.close()
|
||||
return json.dumps("no such record"), 404, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
was_canceled = theshipcall["canceled"]
|
||||
|
||||
provided_keys = set(original_payload.keys()) if isinstance(original_payload, dict) else None
|
||||
|
||||
update_clauses = []
|
||||
# query = SQLQuery.get_shipcall_put(schemaModel)
|
||||
query = "UPDATE shipcall SET "
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
if provided_keys is not None and key not in provided_keys:
|
||||
continue
|
||||
param_key = key
|
||||
if key == "id":
|
||||
continue
|
||||
@ -236,36 +217,20 @@ def PutShipcalls(schemaModel, original_payload=None):
|
||||
param_key = "evaluation_value"
|
||||
if key == "evaluation_value":
|
||||
continue
|
||||
update_clauses.append(f"{key} = ?{param_key}?")
|
||||
if isNotFirst:
|
||||
query += ", "
|
||||
isNotFirst = True
|
||||
query += key + " = ?" + param_key + "? "
|
||||
|
||||
if update_clauses:
|
||||
query = "UPDATE shipcall SET " + ", ".join(update_clauses) + " WHERE id = ?id?"
|
||||
commands.execute(query, param=schemaModel)
|
||||
query += "WHERE id = ?id?"
|
||||
|
||||
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]"
|
||||
affected_rows = commands.execute(query, param=schemaModel)
|
||||
|
||||
# pquery = SQLQuery.get_shipcall_participant_map_by_shipcall_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
|
||||
|
||||
if schemaModel.get("participants") is None:
|
||||
schemaModel["participants"] = []
|
||||
|
||||
# loop across passed participant ids, creating entries for those not present in pdata
|
||||
|
||||
existing_notifications = get_notification_for_shipcall_and_type(schemaModel["id"], 1) # type = 1 is assignment
|
||||
# loop across passed participant ids, creating entries for those not present in pdata
|
||||
|
||||
for participant_assignment in schemaModel["participants"]:
|
||||
found_participant = False
|
||||
@ -275,18 +240,8 @@ def PutShipcalls(schemaModel, original_payload=None):
|
||||
break
|
||||
if not found_participant:
|
||||
# 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?)"
|
||||
commands.execute(spquery, param={"shipcall_id" : schemaModel["id"], "participant_id" : participant_assignment["participant_id"], "type" : participant_assignment["type"]})
|
||||
|
||||
# create a notification but only if there is no existing notification in level 0
|
||||
found_notification = False
|
||||
for existing_notification in existing_notifications:
|
||||
if existing_notification["participant_id"] == participant_assignment["participant_id"] and existing_notification["level"] == 1:
|
||||
found_notification = True
|
||||
break
|
||||
if not found_notification:
|
||||
nquery = "INSERT INTO notification (shipcall_id, participant_id, level, type, message) VALUES (?shipcall_id?, ?participant_id?, 0, 1, ?message?)" # type = 1 is assignment
|
||||
commands.execute(nquery, param={"shipcall_id" : schemaModel["id"], "participant_id" : participant_assignment["participant_id"], "message" : message})
|
||||
nquery = "INSERT INTO shipcall_participant_map (shipcall_id, participant_id, type) VALUES (?shipcall_id?, ?participant_id?, ?type?)"
|
||||
commands.execute(nquery, param={"shipcall_id" : schemaModel["id"], "participant_id" : participant_assignment["participant_id"], "type" : participant_assignment["type"]})
|
||||
|
||||
# loop across existing pdata entries, deleting those not present in participant list
|
||||
for elem in pdata:
|
||||
@ -299,26 +254,7 @@ def PutShipcalls(schemaModel, original_payload=None):
|
||||
# dquery = SQLQuery.get_shipcall_participant_map_delete_by_id()
|
||||
dquery = "DELETE FROM shipcall_participant_map WHERE id = ?existing_id?"
|
||||
commands.execute(dquery, param={"existing_id" : elem["id"]})
|
||||
# TODO: Create un-assignment notification but only if level > 0 else delete existing notification
|
||||
for existing_notification in existing_notifications:
|
||||
if existing_notification["participant_id"] == elem["participant_id"]:
|
||||
if existing_notification["level"] == 0:
|
||||
nquery = "DELETE FROM notification WHERE id = ?nid?"
|
||||
commands.execute(nquery, param={"nid" : existing_notification["id"]})
|
||||
else:
|
||||
# create un-assignment notification
|
||||
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})
|
||||
break
|
||||
|
||||
canceled_value = schemaModel.get("canceled")
|
||||
if canceled_value is not None:
|
||||
if canceled_value and not was_canceled:
|
||||
# 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?)"
|
||||
for participant_assignment in schemaModel["participants"]:
|
||||
commands.execute(stornoNotificationQuery, param={"shipcall_id" : schemaModel["id"], "participant_id" : participant_assignment["participant_id"], "message" : message})
|
||||
|
||||
|
||||
# save history data
|
||||
# TODO: set ETA properly
|
||||
# query = SQLQuery.create_sql_query_history_put()
|
||||
|
||||
@ -11,8 +11,8 @@ def GetShips(token):
|
||||
No parameters, gets all entries
|
||||
"""
|
||||
|
||||
pooledConnection = None
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
# query = SQLQuery.get_ships()
|
||||
@ -44,8 +44,8 @@ def PostShip(schemaModel):
|
||||
# TODO: Validate the incoming data
|
||||
|
||||
# This creates a *new* entry
|
||||
pooledConnection = None
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
@ -83,6 +83,8 @@ def PostShip(schemaModel):
|
||||
# new_id = commands.execute_scalar(nquery)
|
||||
new_id = commands.execute_scalar("select last_insert_id()")
|
||||
|
||||
pooledConnection.close()
|
||||
|
||||
return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
except Exception as ex:
|
||||
@ -91,9 +93,6 @@ def PostShip(schemaModel):
|
||||
result = {}
|
||||
result["error_field"] = "call failed"
|
||||
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
|
||||
|
||||
def PutShip(schemaModel):
|
||||
@ -102,8 +101,8 @@ def PutShip(schemaModel):
|
||||
"""
|
||||
|
||||
# This updates an *existing* entry
|
||||
pooledConnection = None
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
@ -126,6 +125,8 @@ def PutShip(schemaModel):
|
||||
|
||||
affected_rows = commands.execute(query, param=schemaModel)
|
||||
|
||||
pooledConnection.close()
|
||||
|
||||
return json.dumps({"id" : schemaModel["id"]}), 200, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
except Exception as ex:
|
||||
@ -134,9 +135,6 @@ def PutShip(schemaModel):
|
||||
result = {}
|
||||
result["error_field"] = "call failed"
|
||||
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
|
||||
|
||||
def DeleteShip(options):
|
||||
@ -145,14 +143,16 @@ def DeleteShip(options):
|
||||
options["id"]
|
||||
|
||||
"""
|
||||
pooledConnection = None
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
# query = SQLQuery.get_ship_delete_by_id()
|
||||
# affected_rows = commands.execute(query, param={"id" : options["id"]})
|
||||
affected_rows = commands.execute("UPDATE ship SET deleted = 1 WHERE id = ?id?", param={"id" : options["id"]})
|
||||
|
||||
pooledConnection.close()
|
||||
|
||||
if affected_rows == 1:
|
||||
return json.dumps({"id" : options["id"]}), 200, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
@ -165,7 +165,4 @@ def DeleteShip(options):
|
||||
print(ex)
|
||||
result = {}
|
||||
result["error_field"] = "call failed"
|
||||
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
@ -18,8 +18,8 @@ def GetTimes(options):
|
||||
|
||||
"""
|
||||
|
||||
pooledConnection = None
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
# query = SQLQuery.get_times()
|
||||
@ -28,6 +28,7 @@ def GetTimes(options):
|
||||
"zone_entry, zone_entry_fixed, operations_start, operations_end, remarks, shipcall_id, participant_id, " +
|
||||
"berth_id, berth_info, pier_side, participant_type, created, modified, ata, atd, eta_interval_end, etd_interval_end FROM times " +
|
||||
"WHERE times.shipcall_id = ?scid?", model=model.Times, param={"scid" : options["shipcall_id"]})
|
||||
pooledConnection.close()
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(traceback.format_exc())
|
||||
@ -37,10 +38,6 @@ def GetTimes(options):
|
||||
result["error_field"] = "call failed"
|
||||
return json.dumps(result), 500, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
|
||||
return json.dumps(data, default=model.obj_dict), 200, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
|
||||
@ -54,8 +51,8 @@ def PostTimes(schemaModel):
|
||||
# TODO: Validate the upload data
|
||||
|
||||
# This creates a *new* entry
|
||||
pooledConnection = None
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
@ -115,45 +112,36 @@ def PostTimes(schemaModel):
|
||||
pooledConnection.close()
|
||||
|
||||
|
||||
def PutTimes(schemaModel, original_payload=None):
|
||||
def PutTimes(schemaModel):
|
||||
"""
|
||||
|
||||
:param schemaModel: The deserialized model of the record to be inserted
|
||||
"""
|
||||
|
||||
# This updates an *existing* entry
|
||||
pooledConnection = None
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
sentinel = object()
|
||||
existing_times = commands.query_single_or_default("SELECT * FROM times WHERE id = ?id?", sentinel, param={"id": schemaModel["id"]})
|
||||
if existing_times is sentinel:
|
||||
return json.dumps("no such record"), 404, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
provided_keys = set(original_payload.keys()) if isinstance(original_payload, dict) else None
|
||||
|
||||
if "shipcall_id" not in schemaModel or (provided_keys is not None and "shipcall_id" not in provided_keys):
|
||||
schemaModel["shipcall_id"] = existing_times["shipcall_id"]
|
||||
|
||||
schemaModel = {k:v.value if isinstance(v, (Enum, Flag)) else v for k,v in schemaModel.items()}
|
||||
|
||||
update_clauses = []
|
||||
query = "UPDATE times SET "
|
||||
isNotFirst = False
|
||||
for key in schemaModel.keys():
|
||||
if provided_keys is not None and key not in provided_keys:
|
||||
continue
|
||||
if key == "id":
|
||||
continue
|
||||
if key == "created":
|
||||
continue
|
||||
if key == "modified":
|
||||
continue
|
||||
update_clauses.append(f"{key} = ?{key}?")
|
||||
if isNotFirst:
|
||||
query += ", "
|
||||
isNotFirst = True
|
||||
query += key + " = ?" + key + "? "
|
||||
|
||||
if update_clauses:
|
||||
query = "UPDATE times SET " + ", ".join(update_clauses) + " WHERE id = ?id?"
|
||||
commands.execute(query, param=schemaModel)
|
||||
query += "WHERE id = ?id?"
|
||||
|
||||
schemaModel = {k:v.value if isinstance(v, (Enum, Flag)) else v for k,v in schemaModel.items()}
|
||||
affected_rows = commands.execute(query, param=schemaModel)
|
||||
|
||||
# apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database 'shipcall'
|
||||
evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=schemaModel["shipcall_id"]) # every times data object refers to the 'shipcall_id'
|
||||
@ -189,8 +177,8 @@ def DeleteTimes(options):
|
||||
options["id"]
|
||||
|
||||
"""
|
||||
pooledConnection = None
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
shipcall_id = commands.execute_scalar("SELECT shipcall_id FROM times WHERE id = ?id?", param={"id" : options["id"]})
|
||||
@ -219,4 +207,4 @@ def DeleteTimes(options):
|
||||
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
pooledConnection.close()
|
||||
@ -14,8 +14,8 @@ def PutUser(schemaModel):
|
||||
"""
|
||||
|
||||
# This updates an *existing* entry
|
||||
pooledConnection = None
|
||||
try:
|
||||
|
||||
pooledConnection = local_db.getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
@ -26,6 +26,7 @@ def PutUser(schemaModel):
|
||||
# theuser = commands.query_single_or_default(query, sentinel, param={"id" : schemaModel["id"]}, model=model.User)
|
||||
theuser = commands.query_single_or_default("SELECT * FROM user where id = ?id?", sentinel, param={"id" : schemaModel["id"]}, model=model.User)
|
||||
if theuser is sentinel:
|
||||
pooledConnection.close()
|
||||
# #TODO: result = {"message":"no such record"} -> json.dumps
|
||||
return json.dumps("no such record"), 404, {'Content-Type': 'application/json; charset=utf-8'}
|
||||
|
||||
@ -34,7 +35,7 @@ def PutUser(schemaModel):
|
||||
# should this be refactored?
|
||||
# Also, what about the 'user_name'?
|
||||
# 'participant_id' would also not trigger an update in isolation
|
||||
if "first_name" in schemaModel or "last_name" in schemaModel or "user_phone" in schemaModel or "user_email" in schemaModel or "notify_email" in schemaModel or "notify_whatsapp" in schemaModel or "notify_signal" in schemaModel or "notify_popup" in schemaModel or "notify_on" in schemaModel:
|
||||
if "first_name" in schemaModel or "last_name" in schemaModel or "user_phone" in schemaModel or "user_email" in schemaModel:
|
||||
# query = SQLQuery.get_user_put(schemaModel)
|
||||
query = "UPDATE user SET "
|
||||
isNotFirst = False
|
||||
@ -48,14 +49,7 @@ def PutUser(schemaModel):
|
||||
if isNotFirst:
|
||||
query += ", "
|
||||
isNotFirst = True
|
||||
|
||||
if key != "notify_on":
|
||||
query += key + " = ?" + key + "? "
|
||||
else:
|
||||
flag_value = model.list_to_bitflag(schemaModel["notify_on"])
|
||||
query += "notify_event = " + str(flag_value) + " "
|
||||
|
||||
|
||||
query += key + " = ?" + key + "? "
|
||||
|
||||
query += "WHERE id = ?id?"
|
||||
affected_rows = commands.execute(query, param=schemaModel)
|
||||
|
||||
@ -1,90 +1,42 @@
|
||||
import mysql.connector
|
||||
from mysql.connector import pooling
|
||||
import pydapper
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from BreCal.schemas import defs
|
||||
|
||||
config_path = None
|
||||
secure_dir = None
|
||||
_connection_pool = None
|
||||
|
||||
|
||||
def _load_json(path):
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def _build_pool_config(connection_data, pool_name, pool_size):
|
||||
pool_config = dict(connection_data)
|
||||
pool_config.setdefault("pool_name", pool_name)
|
||||
pool_config.setdefault("pool_size", pool_size)
|
||||
return pool_config
|
||||
|
||||
|
||||
def initPool(instancePath, config=None, connection_filename="connection_data_prod.json",
|
||||
credentials_file="email_credentials_test.json", pool_name="brecal_pool", pool_size=10,
|
||||
secure_directory=None):
|
||||
"""
|
||||
Initialize the MySQL connection pool and load email credentials.
|
||||
"""
|
||||
global config_path, secure_dir, _connection_pool
|
||||
def initPool(instancePath, connection_filename="connection_data_prod.json"):
|
||||
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)
|
||||
global config_path
|
||||
if(config_path == None):
|
||||
config_path = os.path.join(instancePath,f'../../../secure/{connection_filename}') #connection_data_test.json');
|
||||
|
||||
if secure_dir is None:
|
||||
secure_dir = secure_directory if secure_directory else os.path.join(instancePath, '../../../secure')
|
||||
print (config_path)
|
||||
|
||||
if config_path is None:
|
||||
config_path = os.path.join(secure_dir, connection_filename)
|
||||
|
||||
print(config_path)
|
||||
if not os.path.exists(config_path):
|
||||
print('cannot find ' + os.path.abspath(config_path))
|
||||
print ('cannot find ' + os.path.abspath(config_path))
|
||||
print("instance path", instancePath)
|
||||
sys.exit(1)
|
||||
|
||||
connection_data = _load_json(config_path)
|
||||
if _connection_pool is None:
|
||||
pool_config = _build_pool_config(connection_data, pool_name, pool_size)
|
||||
_connection_pool = pooling.MySQLConnectionPool(**pool_config)
|
||||
f = open(config_path);
|
||||
connection_data = json.load(f)
|
||||
|
||||
conn_from_pool = _connection_pool.get_connection()
|
||||
try:
|
||||
commands = pydapper.using(conn_from_pool)
|
||||
commands.query("SELECT id from `user` LIMIT 1")
|
||||
print("DB connection successful")
|
||||
finally:
|
||||
conn_from_pool.close()
|
||||
|
||||
credentials_path = os.path.join(secure_dir, credentials_file)
|
||||
|
||||
if not os.path.exists(credentials_path):
|
||||
print('cannot find ' + os.path.abspath(credentials_path))
|
||||
sys.exit(1)
|
||||
|
||||
defs.email_credentials = _load_json(credentials_path)
|
||||
conn_from_pool = mysql.connector.connect(**connection_data)
|
||||
|
||||
commands = pydapper.using(conn_from_pool)
|
||||
data = commands.query("SELECT id from `user`")
|
||||
print("DB connection successful")
|
||||
conn_from_pool.close()
|
||||
except mysql.connector.PoolError as e:
|
||||
logging.error(f"Failed to create connection pool: {e}")
|
||||
print(e)
|
||||
except Exception as e:
|
||||
logging.error("Failed to initialize DB pool: %s", e)
|
||||
print(e)
|
||||
|
||||
|
||||
def getPoolConnection():
|
||||
if _connection_pool is None:
|
||||
raise RuntimeError("Connection pool not initialized. Call initPool first.")
|
||||
try:
|
||||
return _connection_pool.get_connection()
|
||||
except mysql.connector.PoolError as exc:
|
||||
logging.error("Connection pool exhausted: %s", exc)
|
||||
raise
|
||||
global config_path
|
||||
f = open(config_path);
|
||||
connection_data = json.load(f)
|
||||
return mysql.connector.connect(**connection_data)
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
[
|
||||
{
|
||||
"type" : 1,
|
||||
"color" : "#0867ec",
|
||||
"name" : "assignment",
|
||||
"msg_text" : "Nominierung"
|
||||
},
|
||||
{
|
||||
"type" : 2,
|
||||
"color" : "#ea5c00",
|
||||
"name" : "next24h",
|
||||
"msg_text" : "Morgenrunde relevant"
|
||||
},
|
||||
{
|
||||
"type" : 3,
|
||||
"color" : "#f34336",
|
||||
"name" : "time_conflict",
|
||||
"msg_text" : "Zeitlicher Konflikt"
|
||||
},
|
||||
{
|
||||
"type" : 4,
|
||||
"color" : "#28b532",
|
||||
"name" : "time_conflict_resolved",
|
||||
"msg_text" : "Zeitlicher Konflikt gelöst"
|
||||
},
|
||||
{
|
||||
"type" : 5,
|
||||
"color" : "#a8a8a8",
|
||||
"name" : "unassigned",
|
||||
"msg_text" : "Nominierung abgewählt"
|
||||
},
|
||||
{
|
||||
"type" : 6,
|
||||
"color" : "#a8a800",
|
||||
"name" : "missing_data",
|
||||
"msg_text" : "Fehlende Daten"
|
||||
},
|
||||
{
|
||||
"type" : 7,
|
||||
"color" : "#808070",
|
||||
"name" : "cancelled",
|
||||
"msg_text" : "Storno"
|
||||
}
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%; min-width: 100%;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top; padding-bottom: 16px;" valign="top">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top; border-radius: 4px; text-align: center; background-color: [[color]];" valign="top" align="center" bgcolor="[[color]]">
|
||||
<span style="font-size:14px; color: #ffffff;">[[notification_text]]</span><br/>
|
||||
<a href="[[link]]" target="_blank" style="border: solid 2px [[color]]; border-radius: 4px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 16px; font-weight: bold; margin: 0; padding: 12px 24px; text-decoration: none; text-transform: capitalize; background-color: [[color]]; border-color: [[color]]; color: #ffffff;">[[text]]</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -1,148 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Bremen calling Benachrichtigung</title>
|
||||
<style media="all" type="text/css">
|
||||
@media all {
|
||||
.btn-primary table td:hover {
|
||||
background-color: #ec0867 !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #ec0867 !important;
|
||||
border-color: #ec0867 !important;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 640px) {
|
||||
.main p,
|
||||
.main td,
|
||||
.main span {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
padding-top: 8px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
font-size: 16px !important;
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="font-family: Helvetica, sans-serif; -webkit-font-smoothing: antialiased; font-size: 16px; line-height: 1.3; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; background-color: #f4f5f6; margin: 0; padding: 0;">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f4f5f6; width: 100%;" width="100%" bgcolor="#f4f5f6">
|
||||
<tr>
|
||||
<td style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top;" valign="top"> </td>
|
||||
<td class="container" style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top; max-width: 600px; padding: 0; padding-top: 24px; width: 600px; margin: 0 auto;" width="600" valign="top">
|
||||
<div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 600px; padding: 0;">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Benachrichtung von Bremen calling!</span>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border: 1px solid #eaebed; border-radius: 16px; width: 100%;" width="100%">
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper" style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top; box-sizing: border-box; padding: 24px;" valign="top">
|
||||
<p style="font-family: Helvetica, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">Hallo,</p>
|
||||
<p style="font-family: Helvetica, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">Sie erhalten eine oder mehrere Benachrichtigungen von Bremen calling:</p>
|
||||
|
||||
<!-- notifications begin -->
|
||||
|
||||
[[NOTIFICATION_ELEMENTS]]
|
||||
|
||||
<!-- notifications end -->
|
||||
|
||||
<p style="font-family: Helvetica, sans-serif; font-size: 16px; font-weight: normal; margin: 0; margin-bottom: 16px;">Wenn Sie diese E-Mails nicht länger erhalten wollen, loggen Sie sich bitte in der Bremen Calling App ein. Im Bereich "Passwort ändern"
|
||||
können Sie auch die Benachrichtungen anpassen.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer" style="clear: both; padding-top: 24px; text-align: center; width: 100%;">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;" width="100%">
|
||||
<tr>
|
||||
<td class="content-block" style="font-family: Helvetica, sans-serif; vertical-align: top; color: #9a9ea6; font-size: 16px; text-align: center;" valign="top" align="center">
|
||||
<span class="apple-link" style="color: #9a9ea6; font-size: 16px; text-align: center;">Bremer Schiffsmeldedienst GbR - Hafenkopf II / Überseetor 20 - 28217 Bremen / Germany</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block powered-by" style="font-family: Helvetica, sans-serif; vertical-align: top; color: #9a9ea6; font-size: 16px; text-align: center;" valign="top" align="center">
|
||||
Kontaktieren Sie uns unter <a href="mailto:bremencalling@bsmd.de" style="color: #9a9ea6; font-size: 16px; text-align: center;">bremencalling@bsmd.de</a>.<br />
|
||||
<a href="https://www.bsmd.de" style="color: #9a9ea6; font-size: 16px; text-align: center;">www.bsmd.de</a><br />
|
||||
Tel.: +49 421 38 48 27
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- END FOOTER -->
|
||||
|
||||
<!-- END CENTERED WHITE CONTAINER --></div>
|
||||
</td>
|
||||
<td style="font-family: Helvetica, sans-serif; font-size: 16px; vertical-align: top;" valign="top"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,20 +0,0 @@
|
||||
# Version: 1.7.0
|
||||
|
||||
# Constants for the notification system
|
||||
NOTIFICATION_COOLDOWN_MINS = 10 # until a notification gets real and cannot be deleted anymore
|
||||
NOTIFICATION_MAX_AGE_DAYS = 3 # 3 days until a notification gets deleted
|
||||
|
||||
# Placeholder for the email credentials filled by startup logic
|
||||
email_credentials = dict()
|
||||
|
||||
# Holding var for global message notification type info
|
||||
message_types = dict()
|
||||
|
||||
# Constants for the email display
|
||||
|
||||
shipcall_types = {
|
||||
1: "Arrival",
|
||||
2: "Departure",
|
||||
3: "Shifting"
|
||||
}
|
||||
|
||||
@ -5,17 +5,15 @@ from marshmallow_enum import EnumField
|
||||
from enum import IntEnum
|
||||
|
||||
from marshmallow_dataclass import dataclass
|
||||
from typing import Iterable, List
|
||||
from typing import List
|
||||
|
||||
import json
|
||||
import re
|
||||
import datetime
|
||||
|
||||
from BreCal.validators.time_logic import validate_time_is_in_not_too_distant_future
|
||||
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
|
||||
from BreCal.database.enums import ParticipantType, ParticipantFlag
|
||||
|
||||
|
||||
# from BreCal. ... import check_if_user_is_bsmd_type
|
||||
|
||||
def obj_dict(obj):
|
||||
if isinstance(obj, datetime.datetime):
|
||||
@ -67,51 +65,23 @@ class EvaluationType(IntEnum):
|
||||
return cls.undefined
|
||||
|
||||
class NotificationType(IntEnum):
|
||||
|
||||
"""
|
||||
This type is not the way the user is informed but the type of the notification, e.g. time conflict, time conflict resolved, etc.
|
||||
It can be understood as an event type
|
||||
Any user has the attributes
|
||||
'notify_email' -> NotificationType.email
|
||||
'notify_popup' -> NotificationType.push
|
||||
'notify_whatsapp' -> undeclared
|
||||
'notify_signal' -> undeclared
|
||||
"""
|
||||
|
||||
assignment = 1
|
||||
next24h = 2
|
||||
time_conflict = 3
|
||||
time_conflict_resolved = 4
|
||||
unassigned = 5
|
||||
missing_data = 6
|
||||
cancelled = 7
|
||||
undefined = 0
|
||||
email = 1
|
||||
push = 2
|
||||
# whatsapp = 3
|
||||
# signal = 4
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.undefined
|
||||
|
||||
def bitflag_to_list(bitflag: int | None) -> list[NotificationType]:
|
||||
"""Converts an integer bitflag to a list of NotificationType enums."""
|
||||
if bitflag is None:
|
||||
return []
|
||||
return [nt for nt in NotificationType if bitflag & (1 << (nt.value - 1))]
|
||||
|
||||
def list_to_bitflag(notifications: Iterable[NotificationType | str | int] | None) -> int:
|
||||
"""Converts a list of NotificationType enums (or their names/values) to an integer bitflag."""
|
||||
if not notifications:
|
||||
return 0
|
||||
bitflag = 0
|
||||
for nt in notifications:
|
||||
enum_val = None
|
||||
if isinstance(nt, NotificationType):
|
||||
enum_val = nt
|
||||
elif isinstance(nt, str):
|
||||
enum_val = NotificationType[nt]
|
||||
else:
|
||||
enum_val = NotificationType(nt)
|
||||
bitflag |= 1 << (enum_val.value - 1)
|
||||
return bitflag
|
||||
|
||||
def notification_types_to_names(notifications: Iterable[NotificationType]) -> list[str]:
|
||||
"""Render NotificationType values as their names for API responses."""
|
||||
return [nt.name for nt in notifications]
|
||||
|
||||
|
||||
class ShipcallType(IntEnum):
|
||||
undefined = 0
|
||||
arrival = 1
|
||||
@ -175,7 +145,6 @@ class Notification:
|
||||
"""
|
||||
id: int
|
||||
shipcall_id: int # 'shipcall record that caused the notification'
|
||||
participant_id: int # 'optional participant reference that needs to be specifically notified, if null all participants are notified'
|
||||
level: int # 'severity of the notification'
|
||||
type: NotificationType # 'type of the notification'
|
||||
message: str # 'individual message'
|
||||
@ -186,7 +155,6 @@ class Notification:
|
||||
return {
|
||||
"id": self.id,
|
||||
"shipcall_id": self.shipcall_id,
|
||||
"participant_id": self.participant_id,
|
||||
"level": self.level,
|
||||
"type": self.type.name if isinstance(self.type, IntEnum) else NotificationType(self.type).name,
|
||||
"message": self.message,
|
||||
@ -195,8 +163,8 @@ class Notification:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_query_row(self, id, shipcall_id, participant_id, level, type, message, created, modified):
|
||||
return self(id, shipcall_id, participant_id, level, NotificationType(type), message, created, modified)
|
||||
def from_query_row(self, id, shipcall_id, level, type, message, created, modified):
|
||||
return self(id, shipcall_id, level, NotificationType(type), message, created, modified)
|
||||
|
||||
@dataclass
|
||||
class Participant(Schema):
|
||||
@ -213,7 +181,7 @@ class Participant(Schema):
|
||||
ports: List[int] = field(default_factory=list)
|
||||
|
||||
@validates("type")
|
||||
def validate_type(self, value, **kwargs):
|
||||
def validate_type(self, value):
|
||||
# e.g., when an IntFlag has the values 1,2,4; the maximum valid value is 7
|
||||
max_int = sum([int(val) for val in list(ParticipantType._value2member_map_.values())])
|
||||
min_int = 0
|
||||
@ -224,7 +192,7 @@ class Participant(Schema):
|
||||
|
||||
|
||||
@validates("flags")
|
||||
def validate_flags(self, value, **kwargs):
|
||||
def validate_flags(self, value):
|
||||
# e.g., when an IntFlag has the values 1,2,4; the maximum valid value is 7
|
||||
max_int = sum([int(val) for val in list(ParticipantFlag._value2member_map_.values())])
|
||||
min_int = 0
|
||||
@ -249,7 +217,7 @@ class ShipcallSchema(Schema):
|
||||
id = fields.Integer(required=True)
|
||||
ship_id = fields.Integer(required=True)
|
||||
port_id = fields.Integer(required=True)
|
||||
type = fields.Enum(ShipcallType, load_default=ShipcallType.undefined, dump_default=ShipcallType.undefined)
|
||||
type = fields.Enum(ShipcallType, default=ShipcallType.undefined)
|
||||
eta = fields.DateTime(required=False, allow_none=True)
|
||||
voyage = fields.String(allow_none=True, required=False, validate=[validate.Length(max=16)])
|
||||
etd = fields.DateTime(required=False, allow_none=True)
|
||||
@ -270,7 +238,7 @@ class ShipcallSchema(Schema):
|
||||
anchored = fields.Bool(required=False, allow_none=True)
|
||||
moored_lock = fields.Bool(required=False, allow_none=True)
|
||||
canceled = fields.Bool(required=False, allow_none=True)
|
||||
evaluation = fields.Enum(EvaluationType, required=False, allow_none=True, load_default=EvaluationType.undefined, dump_default=ShipcallType.undefined)
|
||||
evaluation = fields.Enum(EvaluationType, required=False, allow_none=True, default=EvaluationType.undefined)
|
||||
evaluation_message = fields.Str(allow_none=True, required=False)
|
||||
evaluation_time = fields.DateTime(required=False, allow_none=True)
|
||||
evaluation_notifications_sent = fields.Bool(required=False, allow_none=True)
|
||||
@ -293,7 +261,7 @@ class ShipcallSchema(Schema):
|
||||
return data
|
||||
|
||||
@validates("type")
|
||||
def validate_type(self, value, **kwargs):
|
||||
def validate_type(self, value):
|
||||
valid_shipcall_type = int(value) in [item.value for item in ShipcallType]
|
||||
|
||||
if not valid_shipcall_type:
|
||||
@ -430,7 +398,7 @@ class TimesSchema(Schema):
|
||||
berth_info = fields.String(required=False, allow_none=True, validate=[validate.Length(max=512)])
|
||||
pier_side = fields.Bool(required=False, allow_none = True)
|
||||
shipcall_id = fields.Integer(required=True)
|
||||
participant_type = fields.Integer(required = False, allow_none=True) # TODO: could become Enum
|
||||
participant_type = fields.Integer(Required = False, allow_none=True)# TODO: could become Enum. # participant_type = fields.Enum(ParticipantType, required=False, allow_none=True, default=ParticipantType.undefined) #fields.Integer(required=False, allow_none=True)
|
||||
ata = fields.DateTime(required=False, allow_none=True)
|
||||
atd = fields.DateTime(required=False, allow_none=True)
|
||||
eta_interval_end = fields.DateTime(required=False, allow_none=True)
|
||||
@ -439,7 +407,7 @@ class TimesSchema(Schema):
|
||||
modified = fields.DateTime(required=False, allow_none=True)
|
||||
|
||||
@validates("participant_type")
|
||||
def validate_participant_type(self, value, **kwargs):
|
||||
def validate_participant_type(self, value):
|
||||
# #TODO: it may also make sense to block multi-assignments, whereas a value could be BSMD+AGENCY
|
||||
# while the validation fails when one of those multi-assignments is BSMD, it passes in cases,
|
||||
# such as AGENCY+PILOT
|
||||
@ -452,56 +420,56 @@ class TimesSchema(Schema):
|
||||
raise ValidationError({"participant_type":f"the participant_type must not be .BSMD"})
|
||||
|
||||
@validates("eta_berth")
|
||||
def validate_eta_berth(self, value, **kwargs):
|
||||
def validate_eta_berth(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("etd_berth")
|
||||
def validate_etd_berth(self, value, **kwargs):
|
||||
def validate_etd_berth(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("lock_time")
|
||||
def validate_lock_time(self, value, **kwargs):
|
||||
def validate_lock_time(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("zone_entry")
|
||||
def validate_zone_entry(self, value, **kwargs):
|
||||
def validate_zone_entry(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("operations_start")
|
||||
def validate_operations_start(self, value, **kwargs):
|
||||
def validate_operations_start(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("operations_end")
|
||||
def validate_operations_end(self, value, **kwargs):
|
||||
def validate_operations_end(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("eta_interval_end")
|
||||
def validate_eta_interval_end(self, value, **kwargs):
|
||||
def validate_eta_interval_end(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
return
|
||||
|
||||
@validates("etd_interval_end")
|
||||
def validate_etd_interval_end(self, value, **kwargs):
|
||||
def validate_etd_interval_end(self, value):
|
||||
# violation when time is not in the future, but also does not exceed a threshold for the 'reasonable' future
|
||||
# when 'value' is 'None', a ValidationError is not issued.
|
||||
valid_time = validate_time_is_in_not_too_distant_future(raise_validation_error=True, value=value, months=12)
|
||||
@ -521,22 +489,17 @@ class UserSchema(Schema):
|
||||
user_email = fields.String(allow_none=True, required=False, validate=[validate.Length(max=64)])
|
||||
old_password = fields.String(allow_none=True, required=False, validate=[validate.Length(max=128)])
|
||||
new_password = fields.String(allow_none=True, required=False, validate=[validate.Length(min=6, max=128)])
|
||||
notify_email = fields.Bool(allow_none=True, required=False)
|
||||
notify_whatsapp = fields.Bool(allow_none=True, required=False)
|
||||
notify_signal = fields.Bool(allow_none=True, required=False)
|
||||
notify_popup = fields.Bool(allow_none=True, required=False)
|
||||
notify_on = fields.List(fields.Enum(NotificationType), required=False, allow_none=True)
|
||||
# #TODO: the user schema does not (yet) include the 'notify_' fields
|
||||
|
||||
@validates("user_phone")
|
||||
def validate_user_phone(self, value, **kwargs):
|
||||
if value is not None:
|
||||
valid_characters = list(map(str,range(0,10)))+["+", " "]
|
||||
if not all([v in valid_characters for v in value]):
|
||||
raise ValidationError({"user_phone":f"one of the phone number values is not valid."})
|
||||
def validate_user_phone(self, value):
|
||||
valid_characters = list(map(str,range(0,10)))+["+", " "]
|
||||
if not all([v in valid_characters for v in value]):
|
||||
raise ValidationError({"user_phone":f"one of the phone number values is not valid."})
|
||||
|
||||
@validates("user_email")
|
||||
def validate_user_email(self, value, **kwargs):
|
||||
if value and not re.match(r"[^@]+@[^@]+\.[^@]+", value):
|
||||
def validate_user_email(self, value):
|
||||
if not "@" in value:
|
||||
raise ValidationError({"user_email":f"invalid email address"})
|
||||
|
||||
|
||||
@ -579,20 +542,12 @@ class User:
|
||||
user_phone: str
|
||||
password_hash: str
|
||||
api_key: str
|
||||
notify_email: bool
|
||||
notify_whatsapp: bool
|
||||
notify_signal: bool
|
||||
notify_popup: bool
|
||||
notify_email: bool # #TODO_clarify: should we use an IntFlag for multi-assignment?
|
||||
notify_whatsapp: bool # #TODO_clarify: should we use an IntFlag for multi-assignment?
|
||||
notify_signal: bool # #TODO_clarify: should we use an IntFlag for multi-assignment?
|
||||
notify_popup: bool # #TODO_clarify: should we use an IntFlag for multi-assignment?
|
||||
created: datetime
|
||||
modified: datetime
|
||||
notify_event: int | None = 0
|
||||
|
||||
def __hash__(self):
|
||||
return hash(id)
|
||||
|
||||
def wants_notifications(self, notification_type: NotificationType):
|
||||
events = bitflag_to_list(self.notify_event)
|
||||
return notification_type in events
|
||||
|
||||
@dataclass
|
||||
class Ship:
|
||||
@ -623,15 +578,15 @@ class ShipSchema(Schema):
|
||||
participant_id = fields.Int(allow_none=True, required=False)
|
||||
length = fields.Float(allow_none=True, required=False, validate=[validate.Range(min=0, max=1000, min_inclusive=False, max_inclusive=False)])
|
||||
width = fields.Float(allow_none=True, required=False, validate=[validate.Range(min=0, max=100, min_inclusive=False, max_inclusive=False)])
|
||||
is_tug = fields.Bool(allow_none=True, required=False, load_default=False, dump_default=False)
|
||||
is_tug = fields.Bool(allow_none=True, required=False, default=False)
|
||||
bollard_pull = fields.Int(allow_none=True, required=False)
|
||||
eni = fields.Int(allow_none=True, required=False)
|
||||
created = fields.DateTime(allow_none=True, required=False)
|
||||
modified = fields.DateTime(allow_none=True, required=False)
|
||||
deleted = fields.Bool(allow_none=True, required=False, load_default=False, dump_default=False)
|
||||
deleted = fields.Bool(allow_none=True, required=False, default=False)
|
||||
|
||||
@validates("name")
|
||||
def validate_name(self, value, **kwargs):
|
||||
def validate_name(self, value):
|
||||
character_length = len(str(value))
|
||||
if character_length<1:
|
||||
raise ValidationError({"name":f"'name' argument should have at least one character"})
|
||||
@ -643,7 +598,7 @@ class ShipSchema(Schema):
|
||||
return
|
||||
|
||||
@validates("imo")
|
||||
def validate_imo(self, value, **kwargs):
|
||||
def validate_imo(self, value):
|
||||
value = str(value).zfill(7) # 1 becomes '0000001' (7 characters). 12345678 becomes '12345678' (8 characters)
|
||||
imo_length = len(value)
|
||||
if imo_length != 7:
|
||||
@ -651,7 +606,7 @@ class ShipSchema(Schema):
|
||||
return
|
||||
|
||||
@validates("callsign")
|
||||
def validate_callsign(self, value, **kwargs):
|
||||
def validate_callsign(self, value):
|
||||
if value is not None:
|
||||
callsign_length = len(str(value))
|
||||
if callsign_length>8:
|
||||
|
||||
@ -15,13 +15,13 @@ from email.mime.application import MIMEApplication
|
||||
|
||||
class EmailHandler():
|
||||
"""
|
||||
Creates an EmailHandler, which is capable of connecting to a mail server at a respective port,
|
||||
as well as logging into a specific user's mail address.
|
||||
Upon creating messages, these can be sent via this handler.
|
||||
Creates an EmailHandler, which is capable of connecting to a mail server at a respective port,
|
||||
as well as logging into a specific user's mail address.
|
||||
Upon creating messages, these can be sent via this handler.
|
||||
|
||||
Options:
|
||||
mail_server: address of the server, such as 'smtp.gmail.com' or 'w01d5503.kasserver.com
|
||||
mail_port:
|
||||
mail_port:
|
||||
25 - SMTP Port, to send emails
|
||||
110 - POP3 Port, to receive emails
|
||||
143 - IMAP Port, to receive from IMAP
|
||||
@ -38,9 +38,6 @@ class EmailHandler():
|
||||
|
||||
self.server = smtplib.SMTP_SSL(self.mail_server, self.mail_port) # alternatively, SMTP
|
||||
|
||||
# set the following to 0 to avoid log spamming
|
||||
self.server.set_debuglevel(1) # 0: no debug, 1: debug
|
||||
|
||||
def check_state(self):
|
||||
"""check, whether the server login took place and is open."""
|
||||
try:
|
||||
@ -48,7 +45,7 @@ class EmailHandler():
|
||||
return status_code==250 # 250: b'2.0.0 Ok'
|
||||
except smtplib.SMTPServerDisconnected:
|
||||
return False
|
||||
|
||||
|
||||
def check_connection(self):
|
||||
"""check, whether the server object is connected to the server. If not, connect it. """
|
||||
try:
|
||||
@ -56,7 +53,7 @@ class EmailHandler():
|
||||
except smtplib.SMTPServerDisconnected:
|
||||
self.server.connect(self.mail_server, self.mail_port)
|
||||
return
|
||||
|
||||
|
||||
def check_login(self)->bool:
|
||||
"""check, whether the server object is logged in as a user"""
|
||||
user = self.server.__dict__.get("user",None)
|
||||
@ -64,8 +61,8 @@ class EmailHandler():
|
||||
|
||||
def login(self, interactive:bool=True):
|
||||
"""
|
||||
login on the determined mail server's mail address. By default, this function opens an interactive window to
|
||||
type the password without echoing (printing '*******' instead of readable characters).
|
||||
login on the determined mail server's mail address. By default, this function opens an interactive window to
|
||||
type the password without echoing (printing '*******' instead of readable characters).
|
||||
|
||||
returns (status_code, status_msg)
|
||||
"""
|
||||
@ -80,7 +77,7 @@ class EmailHandler():
|
||||
def create_email(self, subject:str, message_body:str)->EmailMessage:
|
||||
"""
|
||||
Create an EmailMessage object, which contains the Email's header ("Subject"), content ("Message Body") and the sender's address ("From").
|
||||
The EmailMessage object does not contain the recipients yet, as these will be defined upon sending the Email.
|
||||
The EmailMessage object does not contain the recipients yet, as these will be defined upon sending the Email.
|
||||
"""
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = subject
|
||||
@ -88,16 +85,16 @@ class EmailHandler():
|
||||
#msg["To"] = email_tgts # will be defined in self.send_email
|
||||
msg.set_content(message_body)
|
||||
return msg
|
||||
|
||||
|
||||
def build_recipients(self, email_tgts:list[str]):
|
||||
"""
|
||||
email formatting does not support lists. Instead, items are joined into a comma-space-separated string.
|
||||
Example:
|
||||
[mail1@mail.com, mail2@mail.com] becomes
|
||||
email formatting does not support lists. Instead, items are joined into a comma-space-separated string.
|
||||
Example:
|
||||
[mail1@mail.com, mail2@mail.com] becomes
|
||||
'mail1@mail.com, mail2@mail.com'
|
||||
"""
|
||||
return ', '.join(email_tgts)
|
||||
|
||||
|
||||
def open_mime_application(self, path:str)->MIMEApplication:
|
||||
"""open a local file, read the bytes into a MIMEApplication object, which is built with the proper subtype (based on the file extension)"""
|
||||
with open(path, 'rb') as file:
|
||||
@ -105,24 +102,24 @@ class EmailHandler():
|
||||
|
||||
attachment.add_header('Content-Disposition','attachment',filename=str(os.path.basename(path)))
|
||||
return attachment
|
||||
|
||||
|
||||
def attach_file(self, path:str, msg:email.mime.multipart.MIMEMultipart)->None:
|
||||
"""
|
||||
attach a file to the message. This function opens the file, reads its bytes, defines the mime type by the
|
||||
path extension. The filename is appended as the header.
|
||||
attach a file to the message. This function opens the file, reads its bytes, defines the mime type by the
|
||||
path extension. The filename is appended as the header.
|
||||
|
||||
mimetypes: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||
"""
|
||||
attachment = self.open_mime_application(path)
|
||||
msg.attach(attachment)
|
||||
return
|
||||
|
||||
|
||||
def send_email(self, msg:EmailMessage, email_tgts:list[str], cc_tgts:typing.Optional[list[str]]=None, bcc_tgts:typing.Optional[list[str]]=None, debug:bool=False)->typing.Union[dict,EmailMessage]:
|
||||
"""
|
||||
send a prepared email message to recipients (email_tgts), copy (cc_tgts) and blind copy (bcc_tgts).
|
||||
Returns a dictionary of feedback, which is commonly empty and the EmailMessage.
|
||||
|
||||
When failing, this function returns an SMTP error instead of returning the default outputs.
|
||||
send a prepared email message to recipients (email_tgts), copy (cc_tgts) and blind copy (bcc_tgts).
|
||||
Returns a dictionary of feedback, which is commonly empty and the EmailMessage.
|
||||
|
||||
When failing, this function returns an SMTP error instead of returning the default outputs.
|
||||
"""
|
||||
# Set the Recipients
|
||||
msg["To"] = self.build_recipients(email_tgts)
|
||||
@ -133,15 +130,15 @@ class EmailHandler():
|
||||
if bcc_tgts is not None:
|
||||
msg["Bcc"] = self.build_recipients(bcc_tgts)
|
||||
|
||||
# when debugging, do not send the Email, but return the EmailMessage.
|
||||
# when debugging, do not send the Email, but return the EmailMessage.
|
||||
if debug:
|
||||
return {}, msg
|
||||
|
||||
|
||||
assert self.check_login(), f"currently not logged in. Cannot send an Email. Make sure to properly use self.login first. "
|
||||
# send the prepared EmailMessage via the server.
|
||||
feedback = self.server.send_message(msg)
|
||||
return feedback, msg
|
||||
|
||||
|
||||
def translate_mail_to_multipart(self, msg:EmailMessage):
|
||||
"""EmailMessage does not support HTML and attachments. Hence, one can convert an EmailMessage object."""
|
||||
if msg.is_multipart():
|
||||
@ -162,11 +159,11 @@ class EmailHandler():
|
||||
# attach the remainder of the msg, such as the body, to the MIMEMultipart
|
||||
msg_new.attach(msg)
|
||||
return msg_new
|
||||
|
||||
|
||||
def print_email_attachments(self, msg:MIMEMultipart)->list[str]:
|
||||
"""return a list of lines of an Email, which contain 'filename=' as a list. """
|
||||
return [line_ for line_ in msg.as_string().split("\n") if "filename=" in line_]
|
||||
|
||||
|
||||
def close(self):
|
||||
self.server.__dict__.pop("user",None)
|
||||
self.server.__dict__.pop("password",None)
|
||||
|
||||
@ -1,17 +1,9 @@
|
||||
import logging
|
||||
import pydapper
|
||||
import smtplib
|
||||
import json
|
||||
import os
|
||||
from email.message import EmailMessage
|
||||
|
||||
from BreCal.schemas import model, defs
|
||||
from BreCal.schemas import model
|
||||
from BreCal.local_db import getPoolConnection
|
||||
from BreCal.database.update_database import evaluate_shipcall_state
|
||||
from BreCal.database.sql_queries import create_sql_query_shipcall_get
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
from BreCal.database.sql_utils import get_notification_for_shipcall_and_type
|
||||
from BreCal.services.email_handling import EmailHandler
|
||||
|
||||
import threading
|
||||
import schedule
|
||||
@ -31,7 +23,6 @@ def UpdateShipcalls(options:dict = {'past_days':2}):
|
||||
options:
|
||||
key: 'past_days'. Is used to execute a filtered query of all available shipcalls. Defaults to 2 (days)
|
||||
"""
|
||||
pooledConnection = None
|
||||
try:
|
||||
pooledConnection = getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
@ -40,8 +31,6 @@ def UpdateShipcalls(options:dict = {'past_days':2}):
|
||||
query = create_sql_query_shipcall_get(options)
|
||||
data = commands.query(query, model=model.Shipcall)
|
||||
|
||||
data = [s for s in data if not s.canceled] # filter out canceled shipcalls
|
||||
|
||||
# get the shipcall ids, which are of interest
|
||||
shipcall_ids = [dat.id for dat in data]
|
||||
|
||||
@ -50,280 +39,21 @@ def UpdateShipcalls(options:dict = {'past_days':2}):
|
||||
# apply 'Traffic Light' evaluation to obtain 'GREEN', 'YELLOW' or 'RED' evaluation state. The function internally updates the mysql database
|
||||
evaluate_shipcall_state(mysql_connector_instance=pooledConnection, shipcall_id=shipcall_id) # new_id (last insert id) refers to the shipcall id
|
||||
|
||||
pooledConnection.close()
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
return
|
||||
|
||||
def UpdateNotifications(cooldown_in_mins:int=10):
|
||||
"""
|
||||
This function evaluates all notifications in state ("level") 0 which have been recently created. If a specified amount of time has passed the
|
||||
notification is updated to state 1 and a notification is received by the user
|
||||
"""
|
||||
|
||||
pooledConnection = None
|
||||
try:
|
||||
pooledConnection = getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
query = f"SELECT * FROM notification WHERE level = 0 AND created < TIMESTAMP(NOW() - INTERVAL {cooldown_in_mins} MINUTE)"
|
||||
data = commands.query(query, model=model.Notification)
|
||||
for notification in data:
|
||||
commands.execute("UPDATE notification SET level = 1 WHERE id = ?id?", param={"id":notification.id})
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
|
||||
def ClearNotifications(max_age_in_days:int=3):
|
||||
"""
|
||||
This function clears all notifications in state ("level") 2 that are older than x days
|
||||
"""
|
||||
|
||||
pooledConnection = None
|
||||
try:
|
||||
pooledConnection = getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
query = f"DELETE FROM notification WHERE level = 2 and created < TIMESTAMP(NOW() - INTERVAL {max_age_in_days} DAY)"
|
||||
result = commands.execute(query)
|
||||
if(result > 0):
|
||||
logging.info(f"Deleted {result} notifications")
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
|
||||
def SendEmails(email_dict):
|
||||
"""
|
||||
This function sends emails to all users in the emaildict
|
||||
"""
|
||||
pooledConnection = None
|
||||
conn = None
|
||||
try:
|
||||
pooledConnection = getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
|
||||
conn = smtplib.SMTP(defs.email_credentials["server"], defs.email_credentials["port"])
|
||||
conn.set_debuglevel(1) # set this to 0 to disable debug output to log
|
||||
conn.ehlo()
|
||||
conn.starttls()
|
||||
conn.ehlo()
|
||||
conn.login(defs.email_credentials["sender"], defs.email_credentials["password_send"])
|
||||
|
||||
current_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
if not defs.message_types:
|
||||
f = open(os.path.join(current_path,"../msg/msg_types.json"), encoding='utf-8');
|
||||
defs.message_types = json.load(f)
|
||||
f.close()
|
||||
|
||||
for user_email, notifications in email_dict.items():
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = '[Bremen calling] Notification'
|
||||
msg["From"] = defs.email_credentials["sender"]
|
||||
msg["To"] = user_email
|
||||
|
||||
with open(os.path.join(current_path,'../msg/notification_template.html'), mode="r", encoding="utf-8") as file:
|
||||
body = file.read()
|
||||
|
||||
replacement = ""
|
||||
|
||||
for notification in notifications:
|
||||
|
||||
message_type = next((x for x in defs.message_types if x["type"] == notification.type), None)
|
||||
if message_type is None:
|
||||
logging.error(f"Message type {notification.type} not found")
|
||||
continue
|
||||
|
||||
with open(os.path.join(current_path,'../msg/notification_element.html'), mode="r", encoding="utf-8") as file:
|
||||
element = file.read()
|
||||
element = element.replace("[[color]]", message_type["color"])
|
||||
linktext = defs.email_credentials["url_template"] + str(notification.shipcall_id)
|
||||
element = element.replace("[[link]]", linktext)
|
||||
|
||||
# We want to show the following information for each notification:
|
||||
# Ship-name, Arr/Dep/Shift, ETA/ETD, berth
|
||||
sentinel = object()
|
||||
shipcall = commands.query_single_or_default("SELECT * FROM shipcall WHERE id = ?id?", sentinel, model=model.Shipcall, param={"id":notification.shipcall_id})
|
||||
if shipcall is sentinel:
|
||||
logging.error(f"Shipcall with id {notification.shipcall_id} not found")
|
||||
continue
|
||||
shipcall_type = defs.shipcall_types[shipcall.type]
|
||||
eta_text = shipcall.eta.strftime("%d.%m.%Y %H:%M") if shipcall.type == 1 else shipcall.etd.strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
ship = commands.query_single_or_default("SELECT * FROM ship WHERE id = ?id?", sentinel, model=model.Ship, param={"id":shipcall.ship_id})
|
||||
if ship is sentinel:
|
||||
logging.error(f"Ship with id {shipcall.ship_id} not found")
|
||||
continue
|
||||
berth_id = shipcall.arrival_berth_id if shipcall.type == 1 else shipcall.departure_berth_id
|
||||
berth = commands.query_single_or_default("SELECT * FROM berth WHERE id = ?id?", sentinel, model=model.Berth, param={"id":berth_id})
|
||||
berth_text = ""
|
||||
if berth is not sentinel:
|
||||
berth_text = berth.name
|
||||
times = commands.query_single_or_default("SELECT * FROM times WHERE shipcall_id = ?id? and participant_type = 8", sentinel, model=model.Times, param={"id":notification.shipcall_id})
|
||||
if times is not sentinel:
|
||||
eta_text = times.eta_berth.strftime("%d.%m.%Y %H:%M") if shipcall.type == 1 else times.etd_berth.strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
text = f"{ship.name} ({shipcall_type}) - {eta_text} - {berth_text}"
|
||||
|
||||
element = element.replace("[[text]]", text)
|
||||
element = element.replace("[[notification_text]]", message_type["msg_text"])
|
||||
|
||||
replacement += element
|
||||
|
||||
body = body.replace("[[NOTIFICATION_ELEMENTS]]", replacement)
|
||||
msg.set_content(body, subtype='html', charset='utf-8', cte='8bit')
|
||||
|
||||
conn.sendmail(defs.email_credentials["sender"], user_email, msg.as_string())
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
finally:
|
||||
if conn is not None:
|
||||
conn.quit()
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
|
||||
|
||||
def SendNotifications():
|
||||
# perhaps this will be moved somewhere else later
|
||||
pooledConnection = None
|
||||
try:
|
||||
# find all notifications in level 1
|
||||
pooledConnection = getPoolConnection()
|
||||
query = "SELECT * from notification WHERE level = 1"
|
||||
commands = pydapper.using(pooledConnection)
|
||||
data = commands.query(query, model=model.Notification)
|
||||
if len(data) == 0:
|
||||
return
|
||||
|
||||
# cache participants and users for performance beforehand
|
||||
query = "SELECT * from participant";
|
||||
participants = commands.query(query, model=model.Participant)
|
||||
|
||||
email_dict = dict()
|
||||
users_dict = dict()
|
||||
user_query = "SELECT * from user"
|
||||
users = commands.query(user_query)
|
||||
for participant in participants:
|
||||
for user in users:
|
||||
if user["participant_id"] == participant.id:
|
||||
if not participant.id in users_dict:
|
||||
users_dict[participant.id] = []
|
||||
users_dict[participant.id].append(user)
|
||||
# break
|
||||
|
||||
|
||||
for notification in data:
|
||||
if not notification.participant_id: # no participant defined, this update goes to all participants of this shipcall
|
||||
p_query = "SELECT * from shipcall_participant_map where shipcall_id = ?id?"
|
||||
assigned_participants = commands.query(p_query, model=model.ShipcallParticipantMap, param={"id":notification.shipcall_id})
|
||||
for assigned_participant in assigned_participants:
|
||||
if not assigned_participant.participant_id in users_dict:
|
||||
continue
|
||||
users = users_dict[assigned_participant.participant_id]
|
||||
for user in users:
|
||||
# send notification to user
|
||||
if user["notify_email"]:
|
||||
if user["user_email"] not in email_dict:
|
||||
email_dict[user["user_email"]] = []
|
||||
if notification not in email_dict[user["user_email"]]:
|
||||
email_dict[user["user_email"]].append(notification)
|
||||
if user["notify_whatsapp"]:
|
||||
# TBD
|
||||
pass
|
||||
if user["notify_signal"]:
|
||||
# TBD
|
||||
pass
|
||||
else:
|
||||
if notification.participant_id in users_dict:
|
||||
users = users_dict[notification.participant_id]
|
||||
for user in users:
|
||||
user_notifications = model.bitflag_to_list(user["notify_event"])
|
||||
# send notification to user
|
||||
if user["notify_email"] and notification.type in user_notifications:
|
||||
if user["user_email"] not in email_dict:
|
||||
email_dict[user["user_email"]] = []
|
||||
if notification not in email_dict[user["user_email"]]:
|
||||
email_dict[user["user_email"]].append(notification)
|
||||
if user["notify_whatsapp"] and notification.type in user_notifications:
|
||||
# TBD
|
||||
pass
|
||||
if user["notify_signal"] and notification.type in user_notifications:
|
||||
# TBD
|
||||
pass
|
||||
|
||||
# mark as sent
|
||||
commands.execute("UPDATE notification SET level = 2 WHERE id = ?id?", param={"id":notification.id})
|
||||
|
||||
# send emails (if any)
|
||||
if len(email_dict) > 0:
|
||||
SendEmails(email_dict)
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
|
||||
def add_function_to_schedule__update_shipcalls(interval_in_minutes:int, options:dict={'past_days':2}):
|
||||
kwargs_ = {"options":options}
|
||||
schedule.every(interval_in_minutes).minutes.do(UpdateShipcalls, **kwargs_)
|
||||
return
|
||||
|
||||
def add_function_to_evaluate_notifications(interval_in_minutes:int=1):
|
||||
schedule.every(1).minutes.do(UpdateNotifications, interval_in_minutes)
|
||||
def add_function_to_schedule__send_notifications(vr, interval_in_minutes:int=10):
|
||||
schedule.every(interval_in_minutes).minutes.do(vr.notifier.send_notifications)
|
||||
return
|
||||
|
||||
def add_function_to_clear_notifications(interval_in_days:int=3):
|
||||
schedule.every(30).minutes.do(ClearNotifications, interval_in_days)
|
||||
return
|
||||
|
||||
def add_function_to_schedule_send_notifications(interval_in_minutes:int=1):
|
||||
schedule.every(interval_in_minutes).minutes.do(SendNotifications)
|
||||
return
|
||||
|
||||
def eval_next_24_hrs():
|
||||
pooledConnection = None
|
||||
try:
|
||||
pooledConnection = getPoolConnection()
|
||||
commands = pydapper.using(pooledConnection)
|
||||
query = SQLQuery.get_next24hrs_shipcalls()
|
||||
data = commands.query(query)
|
||||
nquery = "INSERT INTO notification (shipcall_id, participant_id, level, type, message) VALUES (?shipcall_id?, ?participant_id?, 0, 2, ?message?)"
|
||||
for shipcall in data:
|
||||
existing_notifications = get_notification_for_shipcall_and_type(shipcall["id"], 2)
|
||||
query = SQLQuery.get_shipcall_participant_map_by_shipcall_id()
|
||||
participants = commands.query(query, model=dict, param={"id":shipcall["id"]})
|
||||
for participant in participants:
|
||||
if participant["type"] == 1: # BSMD
|
||||
continue
|
||||
# if participant["type"] == 32: # PORT AUTHORITY # Christin: Brake möchte sie vielleicht doch haben
|
||||
# continue
|
||||
# check if "open" notification already exists
|
||||
found_notification = False
|
||||
for existing_notification in existing_notifications:
|
||||
if existing_notification["participant_id"] == participant["id"] and existing_notification["level"] == 0:
|
||||
found_notification = True
|
||||
break
|
||||
if not found_notification:
|
||||
commands.execute(nquery, param={"shipcall_id":shipcall["id"], "participant_id": participant["participant_id"], "message":shipcall["name"]})
|
||||
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
|
||||
return
|
||||
|
||||
def setup_schedule(update_shipcalls_interval_in_minutes:int=60):
|
||||
|
||||
@ -334,14 +64,8 @@ def setup_schedule(update_shipcalls_interval_in_minutes:int=60):
|
||||
# update the evaluation state in every recent shipcall
|
||||
add_function_to_schedule__update_shipcalls(update_shipcalls_interval_in_minutes)
|
||||
|
||||
add_function_to_evaluate_notifications(defs.NOTIFICATION_COOLDOWN_MINS)
|
||||
|
||||
add_function_to_clear_notifications(defs.NOTIFICATION_MAX_AGE_DAYS)
|
||||
|
||||
schedule.every().day.at("09:00").do(eval_next_24_hrs)
|
||||
|
||||
add_function_to_schedule_send_notifications(1)
|
||||
|
||||
# placeholder: create/send notifications
|
||||
# add_function_to_schedule__send_notifications(...)
|
||||
return
|
||||
|
||||
|
||||
|
||||
@ -18,19 +18,18 @@ def get_user_simple():
|
||||
|
||||
created = datetime.datetime.now()
|
||||
modified = created+datetime.timedelta(seconds=10)
|
||||
|
||||
|
||||
notify_email = True
|
||||
notify_whatsapp = True
|
||||
notify_signal = True
|
||||
notify_popup = True
|
||||
|
||||
|
||||
user = User(
|
||||
user_id,
|
||||
participant_id,
|
||||
first_name,
|
||||
last_name,
|
||||
user_name,
|
||||
user_id,
|
||||
participant_id,
|
||||
first_name,
|
||||
last_name,
|
||||
user_name,
|
||||
user_email,
|
||||
user_phone,
|
||||
password_hash,
|
||||
|
||||
@ -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.validators.validation_base_utils import check_if_int_is_valid_flag
|
||||
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
|
||||
from BreCal.validators.input_validation_base import InputValidationBase
|
||||
|
||||
import werkzeug
|
||||
|
||||
class InputValidationShip(InputValidationBase):
|
||||
class InputValidationShip():
|
||||
"""
|
||||
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.
|
||||
@ -56,13 +55,6 @@ class InputValidationShip(InputValidationBase):
|
||||
|
||||
# 3.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
|
||||
InputValidationShip.optionally_evaluate_bollard_pull_value(content)
|
||||
|
||||
# 4.) No deleted flag may be set on POST
|
||||
InputValidationShip.check_deleted_flag_on_post(content)
|
||||
|
||||
# 5.) Check if is_tug is null
|
||||
InputValidationShip.check_is_tug_null(content)
|
||||
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
@ -78,10 +70,6 @@ class InputValidationShip(InputValidationBase):
|
||||
|
||||
# 4.) Check for reasonable Values (see BreCal.schemas.model.ShipSchema)
|
||||
InputValidationShip.optionally_evaluate_bollard_pull_value(content)
|
||||
|
||||
# 5.) Check if tug is null
|
||||
InputValidationShip.check_is_tug_null(content)
|
||||
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
@ -171,11 +159,5 @@ class InputValidationShip(InputValidationBase):
|
||||
raise ValidationError({"deleted":f"The selected ship entry is already deleted."})
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def check_is_tug_null(content:dict):
|
||||
is_tug = content.get("is_tug", None)
|
||||
if is_tug is None:
|
||||
raise ValidationError({"is_tug":f"The 'is_tug' property must be set to either True or False."})
|
||||
return
|
||||
|
||||
|
||||
|
||||
@ -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.validators.validation_base_utils import check_if_int_is_valid_flag
|
||||
from BreCal.validators.validation_base_utils import check_if_string_has_special_characters
|
||||
from BreCal.validators.input_validation_base import InputValidationBase
|
||||
from BreCal.database.sql_queries import SQLQuery
|
||||
import werkzeug
|
||||
|
||||
|
||||
|
||||
class InputValidationShipcall(InputValidationBase):
|
||||
class InputValidationShipcall():
|
||||
"""
|
||||
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.
|
||||
@ -61,11 +60,7 @@ class InputValidationShipcall(InputValidationBase):
|
||||
InputValidationShipcall.check_participant_list_not_empty_when_user_is_agency(loadedModel)
|
||||
|
||||
# check for reasonable values in the shipcall fields
|
||||
InputValidationShipcall.check_shipcall_values(loadedModel, content, forbidden_keys=["evaluation", "evaluation_message", "canceled"]) # "canceled"
|
||||
|
||||
# check for deleted flag on POST
|
||||
InputValidationShipcall.check_deleted_flag_on_post(content)
|
||||
|
||||
InputValidationShipcall.check_shipcall_values(loadedModel, content, forbidden_keys=["evaluation", "evaluation_message"]) # "canceled"
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
@ -551,18 +546,12 @@ class InputValidationShipcall(InputValidationBase):
|
||||
# query = 'SELECT * FROM shipcall_participant_map where (shipcall_id = ?shipcall_id? AND type=?participant_type?)'
|
||||
# assigned_agency = execute_sql_query_standalone(query=query, model=ShipcallParticipantMap, param={"shipcall_id" : shipcall_id, "participant_type":int(ParticipantType.AGENCY)})
|
||||
assigned_agency = get_assigned_participant_of_type(shipcall_id, participant_type=ParticipantType.AGENCY)
|
||||
assigned_pilot = get_assigned_participant_of_type(shipcall_id, participant_type=ParticipantType.PILOT)
|
||||
an_agency_is_assigned = True if assigned_agency is not None else False
|
||||
a_pilot_is_assigned = True if assigned_pilot is not None else False
|
||||
|
||||
else:
|
||||
# Agency assigned? User must belong to the assigned agency or be a BSMD user, in case the flag is set
|
||||
assigned_agency = [spm for spm in shipcall_participant_map if int(spm.type) == int(ParticipantType.AGENCY)]
|
||||
assigned_pilot = [spm for spm in shipcall_participant_map if int(spm.type) == int(ParticipantType.PILOT)]
|
||||
an_agency_is_assigned = len(assigned_agency)==1
|
||||
a_pilot_is_assigned = len(assigned_pilot)==1
|
||||
if a_pilot_is_assigned:
|
||||
assigned_pilot = assigned_pilot[0]
|
||||
|
||||
if len(assigned_agency)>1:
|
||||
raise ValidationError({"internal_error":f"Internal error? Found more than one assigned agency for the shipcall with ID {shipcall_id}. Found: {assigned_agency}"})
|
||||
@ -578,19 +567,18 @@ class InputValidationShipcall(InputValidationBase):
|
||||
### USER authority ###
|
||||
# determine, whether the user is a) the assigned agency or b) a BSMD participant
|
||||
user_is_assigned_agency = (user_participant_id == assigned_agency.id)
|
||||
user_is_assigned_pilot = a_pilot_is_assigned and (user_participant_id == assigned_pilot.id)
|
||||
|
||||
# when the BSMD flag is set: the user must be either BSMD or the assigned agency
|
||||
# when the BSMD flag is not set: the user must be the assigned agency
|
||||
user_is_authorized = (user_is_bsmd or user_is_assigned_agency or user_is_assigned_pilot) #if agency_has_bsmd_flag else user_is_assigned_agency
|
||||
user_is_authorized = (user_is_bsmd or user_is_assigned_agency) #if agency_has_bsmd_flag else user_is_assigned_agency
|
||||
|
||||
if not user_is_authorized:
|
||||
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY / BSMD / PILOT (if the special-flag is enabled). Assigned Agency: {assigned_agency} with Flags: {assigned_agency.flags}") # Forbidden: 403
|
||||
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). Assigned Agency: {assigned_agency} with Flags: {assigned_agency.flags}") # Forbidden: 403
|
||||
|
||||
else:
|
||||
# when there is no assigned agency, only BSMD users can update the shipcall
|
||||
if not user_is_bsmd:
|
||||
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY / BSMD / PILOT users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.") # part of a pytest.raises. Forbidden: 403
|
||||
raise werkzeug.exceptions.Forbidden(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.") # part of a pytest.raises. Forbidden: 403
|
||||
|
||||
return
|
||||
|
||||
|
||||
@ -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 get_assigned_participant_of_type
|
||||
from BreCal.database.sql_utils import get_times_data_for_id
|
||||
from BreCal.validators.input_validation_base import InputValidationBase
|
||||
from BreCal.validators.validation_base_utils import check_if_int_is_valid_flag, check_if_string_has_special_characters
|
||||
import werkzeug
|
||||
|
||||
@ -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.
|
||||
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
|
||||
InputValidationTimes.check_dataset_values(user_data, loadedModel, content)
|
||||
|
||||
# 5.) Deleted flag may not be set on POST
|
||||
InputValidationTimes.check_deleted_flag_on_post(content)
|
||||
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
|
||||
@ -1,25 +1,26 @@
|
||||
import typing
|
||||
import re
|
||||
from string import ascii_letters, digits
|
||||
|
||||
|
||||
_VALID = re.compile(r'^[\w &-]+$') # \w == Unicode letters+digits+underscore
|
||||
|
||||
def check_if_string_has_special_characters(text:typing.Optional[str]):
|
||||
"""
|
||||
check, whether there are any characters within the provided string, which are not found in the ascii letters or digits
|
||||
ascii_letters: abcd (...) and ABCD (...)
|
||||
digits: 0123 (...)
|
||||
|
||||
Formerly, this solution was used but was found to be too restrictive:
|
||||
Source: https://stackoverflow.com/questions/57062794/is-there-a-way-to-check-if-a-string-contains-special-characters
|
||||
User: https://stackoverflow.com/users/10035985/andrej-kesely
|
||||
returns bool
|
||||
"""
|
||||
return not _VALID.fullmatch(text) if text else False
|
||||
if text is None:
|
||||
return False
|
||||
return bool(set(text).difference(ascii_letters + digits + ' '))
|
||||
|
||||
|
||||
def check_if_int_is_valid_flag(value, enum_object):
|
||||
if value is None: # when no value is provided, the value is considered valid.
|
||||
return False
|
||||
|
||||
|
||||
# e.g., when an IntFlag has the values 1,2,4; the maximum valid value is 7
|
||||
max_int = sum([int(val) for val in list(enum_object._value2member_map_.values())])
|
||||
return 0 < value <= max_int
|
||||
|
||||
@ -59,7 +59,7 @@ def create_validation_error_response(ex:ValidationError, status_code:int=400, cr
|
||||
|
||||
if create_log:
|
||||
logging.warning(ex) if ex is not None else logging.warning(message)
|
||||
# print(ex) if ex is not None else print(message)
|
||||
print(ex) if ex is not None else print(message)
|
||||
return (serialized_response, status_code)
|
||||
|
||||
def create_werkzeug_error_response(ex:Forbidden, status_code:int=403, create_log:bool=True)->typing.Tuple[str,int]:
|
||||
@ -71,7 +71,7 @@ def create_werkzeug_error_response(ex:Forbidden, status_code:int=403, create_log
|
||||
|
||||
if create_log:
|
||||
logging.warning(ex) if ex is not None else logging.warning(message)
|
||||
# print(ex) if ex is not None else print(message)
|
||||
print(ex) if ex is not None else print(message)
|
||||
return serialized_response, status_code
|
||||
|
||||
def create_dynamic_exception_response(ex, status_code:int=400, message:typing.Optional[str]=None, create_log:bool=True):
|
||||
@ -83,5 +83,5 @@ def create_dynamic_exception_response(ex, status_code:int=400, message:typing.Op
|
||||
|
||||
if create_log:
|
||||
logging.warning(ex) if ex is not None else logging.warning(message)
|
||||
# print(ex) if ex is not None else print(message)
|
||||
print(ex) if ex is not None else print(message)
|
||||
return (serialized_response, status_code)
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import copy
|
||||
import logging
|
||||
import pydapper
|
||||
import re
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@ -8,7 +7,6 @@ import datetime
|
||||
from BreCal.database.enums import StatusFlags
|
||||
from BreCal.validators.validation_rule_functions import ValidationRuleFunctions
|
||||
from BreCal.schemas.model import Shipcall
|
||||
from BreCal.local_db import getPoolConnection
|
||||
|
||||
|
||||
class ValidationRules(ValidationRuleFunctions):
|
||||
@ -52,8 +50,7 @@ class ValidationRules(ValidationRuleFunctions):
|
||||
# 'translate' all error codes into readable, human-understandable format.
|
||||
evaluation_results = [(state, self.describe_error_message(msg)) for (state, msg) in evaluation_results]
|
||||
|
||||
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
|
||||
evaluation_state = np.max(np.array([result[0].value for result in evaluation_results])) if len(evaluation_results)>0 else StatusFlags.GREEN.value
|
||||
@ -79,7 +76,7 @@ class ValidationRules(ValidationRuleFunctions):
|
||||
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
|
||||
# unbundle individual results. evaluation_states becomes an integer, violation
|
||||
evaluation_states_new = [StatusFlags(res[0]).value for res in results]
|
||||
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]
|
||||
@ -87,54 +84,9 @@ class ValidationRules(ValidationRuleFunctions):
|
||||
# build the list of evaluation times ('now', as isoformat)
|
||||
#evaluation_time = self.get_notification_times(evaluation_states_new)
|
||||
|
||||
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)
|
||||
if evaluation_states_new[0] == 2:
|
||||
match evaluation_states_old[0]:
|
||||
case 0:
|
||||
send_notification = True
|
||||
case 1:
|
||||
send_notification = True
|
||||
notification_type = 6 # YELLOW (mapped to missing_data)
|
||||
if evaluation_states_new[0] == 3:
|
||||
match evaluation_states_old[0]:
|
||||
case 0:
|
||||
send_notification = True
|
||||
case 1:
|
||||
send_notification = True
|
||||
case 2:
|
||||
send_notification = True
|
||||
|
||||
if send_notification:
|
||||
query = f"INSERT INTO notification (shipcall_id, type, level, message) VALUES (?shipcall_id?, {notification_type}, 0, ?message?)"
|
||||
commands.execute(query, param={"shipcall_id" : int(shipcall_df.index[0]), "message" : violations[0]})
|
||||
|
||||
if evaluation_states_new[0] == 1 and evaluation_states_old[0] != 0: # this resolves the conflict
|
||||
query = f"SELECT * from notification where shipcall_id = ?shipcall_id? and type = {notification_type} and level = 0"
|
||||
existing_notification = commands.query(query, param={"shipcall_id" : int(shipcall_df.index[0])})
|
||||
if len(existing_notification) > 0:
|
||||
query = "DELETE from notification where id = ?id?"
|
||||
commands.execute(query, param={"id" : existing_notification[0]["id"]})
|
||||
else:
|
||||
query = "INSERT INTO notification (shipcall_id, type, level) VALUES (?shipcall_id?, 4, 0)"
|
||||
commands.execute(query, param={"shipcall_id" : int(shipcall_df.index[0])})
|
||||
finally:
|
||||
if pooledConnection is not None:
|
||||
pooledConnection.close()
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
# TODO: detect evaluation state changes and create notifications
|
||||
|
||||
shipcall_df.loc[:,"evaluation"] = evaluation_states_new
|
||||
shipcall_df.loc[:,"evaluation_message"] = violations
|
||||
#shipcall_df.loc[:,"evaluation_time"] = evaluation_time
|
||||
@ -155,14 +107,14 @@ class ValidationRules(ValidationRuleFunctions):
|
||||
# e.g.: Evaluation message too long. Violated Rules: ['Rule #0001C', 'Rule #0001H', 'Rule #0001F', 'Rule #0001G', 'Rule #0001L', 'Rule #0001M', 'Rule #0001J', 'Rule #0001K']
|
||||
violation = f"Evaluation message too long. Violated Rules: {concise}"
|
||||
return violation
|
||||
|
||||
|
||||
def undefined_method(self) -> str:
|
||||
"""this function should apply the ValidationRules to the respective .shipcall, in regards to .times"""
|
||||
return (StatusFlags.GREEN, False) # (state:str, should_notify:bool)
|
||||
|
||||
|
||||
def determine_notification_state(self, state_old, state_new):
|
||||
"""
|
||||
this method determines state changes in the notification state. When the state increases, a user is notified about it.
|
||||
this method determines state changes in the notification state. When the state increases, a user is notified about it.
|
||||
state order: (NONE = GREEN < YELLOW < RED)
|
||||
"""
|
||||
# identify a state increase
|
||||
@ -171,10 +123,10 @@ class ValidationRules(ValidationRuleFunctions):
|
||||
# when a state increases, a notification must be sent. Thereby, the field should be set to False ({evaluation_notifications_sent})
|
||||
evaluation_notifications_sent = False if bool(should_notify) else None
|
||||
return evaluation_notifications_sent
|
||||
|
||||
|
||||
def identify_notification_state_change(self, state_old, state_new) -> bool:
|
||||
"""
|
||||
determines, whether the observed state change should trigger a notification.
|
||||
determines, whether the observed state change should trigger a notification.
|
||||
internally, this function maps StatusFlags to an integer and determines, if the successor state is more severe than the predecessor.
|
||||
|
||||
state changes trigger a notification in the following cases:
|
||||
@ -184,7 +136,7 @@ class ValidationRules(ValidationRuleFunctions):
|
||||
|
||||
(none -> yellow) or (none -> red)
|
||||
due to the values in the enumeration objects, the states are mapped to provide this function.
|
||||
green=1, yellow=2, red=3, none=1. Hence, critical changes can be observed by simply checking with "greater than".
|
||||
green=1, yellow=2, red=3, none=1. Hence, critical changes can be observed by simply checking with "greater than".
|
||||
|
||||
returns bool, whether a notification should be triggered
|
||||
"""
|
||||
@ -193,12 +145,12 @@ class ValidationRules(ValidationRuleFunctions):
|
||||
state_old = StatusFlags.NONE.value
|
||||
state_old = max(int(state_old), StatusFlags.GREEN.value)
|
||||
return int(state_new) > int(state_old)
|
||||
|
||||
|
||||
def get_notification_times(self, evaluation_states_new)->list[datetime.datetime]:
|
||||
"""# build the list of evaluation times ('now', as isoformat)"""
|
||||
evaluation_times = [datetime.datetime.now().isoformat() for _i in range(len(evaluation_states_new))]
|
||||
return evaluation_times
|
||||
|
||||
|
||||
def get_notification_states(self, evaluation_states_old, evaluation_states_new)->list[bool]:
|
||||
"""# build the list of 'evaluation_notifications_sent'. The value is 'False', when a notification should be created"""
|
||||
evaluation_notifications_sent = [self.determine_notification_state(state_old=int(state_old), state_new=int(state_new)) for state_old, state_new in zip(evaluation_states_old, evaluation_states_new)]
|
||||
@ -208,7 +160,7 @@ class ValidationRules(ValidationRuleFunctions):
|
||||
def inspect_shipcall_evaluation(vr, sql_handler, shipcall_id):
|
||||
"""
|
||||
# debug only!
|
||||
|
||||
|
||||
a simple debugging function, which serves in inspecting an evaluation function for a single shipcall id. It returns the result and all related data.
|
||||
returns: result, shipcall_df (filtered by shipcall id), shipcall, spm (shipcall participant map, filtered by shipcall id), times_df (filtered by shipcall id)
|
||||
"""
|
||||
@ -218,7 +170,7 @@ def inspect_shipcall_evaluation(vr, sql_handler, shipcall_id):
|
||||
result = vr.evaluate(shipcall=shipcall)
|
||||
notification_state = vr.identify_notification_state_change(state_old=int(shipcall.evaluation), state_new=int(result[0]))
|
||||
print(f"Previous state: {int(shipcall.evaluation)}, New State: {result[0]}, Notification State: {notification_state}")
|
||||
|
||||
|
||||
times_df = sql_handler.df_dict.get("times")
|
||||
times_df = times_df.loc[times_df["shipcall_id"]==shipcall_id]
|
||||
|
||||
|
||||
@ -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 runpy
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
INSTANCE_DIR = BASE_DIR / "instance"
|
||||
CONFIG_PATH = INSTANCE_DIR / "config.py"
|
||||
sys.path.insert(0, '/var/www/brecal/src/server')
|
||||
sys.path.insert(0, '/var/www/venv/lib/python3.12/site-packages/')
|
||||
|
||||
config = {}
|
||||
if CONFIG_PATH.exists():
|
||||
config = runpy.run_path(str(CONFIG_PATH))
|
||||
import schedule
|
||||
|
||||
app_root = config.get("APP_ROOT", str(BASE_DIR))
|
||||
site_packages = config.get("SITE_PACKAGES")
|
||||
# set the key
|
||||
os.environ['SECRET_KEY'] = 'zdiTz8P3jXOc7jztIQAoelK4zztyuCpJ'
|
||||
|
||||
sys.path.insert(0, app_root)
|
||||
if site_packages:
|
||||
sys.path.insert(0, site_packages)
|
||||
# Set up logging
|
||||
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
|
||||
|
||||
if config.get("SECRET_KEY"):
|
||||
os.environ["SECRET_KEY"] = config["SECRET_KEY"]
|
||||
|
||||
log_kwargs = {"level": getattr(logging, config.get("LOG_LEVEL", "DEBUG")), "stream": sys.stderr}
|
||||
logging.basicConfig(**log_kwargs)
|
||||
# Set up Scheduled Jobs
|
||||
|
||||
# Import and run the Flask app
|
||||
from BreCal import create_app
|
||||
application = create_app(instance_path=config.get("INSTANCE_PATH"))
|
||||
application = create_app()
|
||||
|
||||
@ -2,7 +2,7 @@ from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name='BreCal',
|
||||
version='1.7.0',
|
||||
version='1.6.0',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
import os
|
||||
import smtplib
|
||||
import json
|
||||
import sys
|
||||
|
||||
from email.message import EmailMessage
|
||||
|
||||
path_to_email_credentials = os.path.join(os.path.dirname(__file__), "email_credentials.json")
|
||||
|
||||
def send_email_example(credentials, recipient):
|
||||
"""
|
||||
sends an email to the given email address
|
||||
:param email: the email address to send the email to
|
||||
:param subject: the subject of the email
|
||||
:param message: the message of the email
|
||||
:return: None
|
||||
"""
|
||||
# create the email message
|
||||
msg = EmailMessage()
|
||||
|
||||
|
||||
msg["Subject"] = '[Bremen calling] Test Email'
|
||||
msg["From"] = credentials["sender"]
|
||||
|
||||
conn = smtplib.SMTP(credentials["server"], credentials["port"])
|
||||
conn.set_debuglevel(1)
|
||||
conn.ehlo()
|
||||
conn.starttls()
|
||||
conn.ehlo()
|
||||
conn.login(credentials["sender"], credentials["password_send"])
|
||||
try:
|
||||
conn.sendmail(credentials["sender"], recipient, msg.as_string())
|
||||
except smtplib.SMTPException as ex:
|
||||
print(f"Error: {ex}")
|
||||
finally:
|
||||
conn.quit()
|
||||
|
||||
|
||||
config_path = "E:/temp/email_credentials.json"
|
||||
print (config_path)
|
||||
|
||||
if not os.path.exists(config_path):
|
||||
print ('cannot find ' + os.path.abspath(config_path))
|
||||
sys.exit(1)
|
||||
|
||||
f = open(config_path);
|
||||
credentials = json.load(f)
|
||||
|
||||
send_email_example(credentials, "bald@puls200.de")
|
||||
@ -637,7 +637,7 @@ def test_shipcall_put_request_fails_when_different_participant_id_is_assigned(ge
|
||||
|
||||
# agency with different participant id is assigned
|
||||
ivs = InputValidationShipcall()
|
||||
with pytest.raises(werkzeug.exceptions.Forbidden, match=f"PUT Requests for shipcalls can only be issued by an assigned AGENCY BSMD PILOT users"):
|
||||
with pytest.raises(werkzeug.exceptions.Forbidden, match=f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users"):
|
||||
ivs.check_user_is_authorized_for_put_request(user_data, loadedModel, content, spm_shipcall_data)
|
||||
return
|
||||
|
||||
@ -706,7 +706,7 @@ def test_shipcall_put_request_fails_when_no_agency_is_assigned(get_shipcall_id_a
|
||||
# no agency assigned
|
||||
ivs = InputValidationShipcall()
|
||||
|
||||
with pytest.raises(werkzeug.exceptions.Forbidden, match=re.escape(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY BSMD PILOT users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.")):
|
||||
with pytest.raises(werkzeug.exceptions.Forbidden, match=re.escape(f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.")):
|
||||
ivs.check_user_is_authorized_for_put_request(user_data, loadedModel, content, spm_shipcall_data)
|
||||
return
|
||||
|
||||
@ -738,7 +738,7 @@ def test_shipcall_put_request_fails_when_user_is_not_authorized(get_shipcall_id_
|
||||
|
||||
# current user is not authorized
|
||||
ivs = InputValidationShipcall()
|
||||
with pytest.raises(werkzeug.exceptions.Forbidden, match=f"PUT Requests for shipcalls can only be issued by an assigned AGENCY BSMD PILOT users"):
|
||||
with pytest.raises(werkzeug.exceptions.Forbidden, match=f"PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users"):
|
||||
ivs.check_user_is_authorized_for_put_request(user_data, loadedModel, content, spm_shipcall_data)
|
||||
return
|
||||
|
||||
@ -769,7 +769,7 @@ def test_shipcall_put_request_fails_when_user_tries_self_assignment(get_shipcall
|
||||
|
||||
# self-assignment. User is participant 6, and wants to assign participant 6.
|
||||
ivs = InputValidationShipcall()
|
||||
with pytest.raises(werkzeug.exceptions.Forbidden, match=re.escape("PUT Requests for shipcalls can only be issued by an assigned AGENCY BSMD PILOT users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.")):
|
||||
with pytest.raises(werkzeug.exceptions.Forbidden, match=re.escape("PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD users (if the special-flag is enabled). There is no assigned agency yet, so only BSMD users can change datasets.")):
|
||||
# previous error message: An agency cannot self-register for a shipcall. The request is issued by an agency-user and tries to assign an AGENCY as the participant of the shipcall.""
|
||||
# however, self-assignment is no longer possible, because the SPM is verified beforehand.
|
||||
ivs.check_user_is_authorized_for_put_request(user_data, loadedModel, content, spm_shipcall_data)
|
||||
@ -846,7 +846,7 @@ def test_shipcall_put_request_fails_input_validation_shipcall_when_shipcall_is_c
|
||||
assert user.participant_id == 4
|
||||
|
||||
#### verification should fail, because participant_id 4 is ParticipantType.PILOT (neither an assigned agency, nor bsmd)
|
||||
with pytest.raises(werkzeug.exceptions.Forbidden, match="PUT Requests for shipcalls can only be issued by an assigned AGENCY BSMD PILOT user"):
|
||||
with pytest.raises(werkzeug.exceptions.Forbidden, match="PUT Requests for shipcalls can only be issued by an assigned AGENCY or BSMD user"):
|
||||
InputValidationShipcall.evaluate_put_data(user_data, loadedModel, content)
|
||||
|
||||
### PASSES:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user