Merge pull request 'billing_module' (#2) from billing_module into develop

Reviewed-on: #2
This commit is contained in:
anghenfil 2022-01-24 07:27:37 +00:00
commit 9c2c4e3eac
44 changed files with 1226 additions and 190 deletions

77
Cargo.lock generated
View File

@ -256,6 +256,28 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "chrono-tz"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552"
dependencies = [
"chrono",
"chrono-tz-build",
"phf",
]
[[package]]
name = "chrono-tz-build"
version = "0.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069"
dependencies = [
"parse-zoneinfo",
"phf",
"phf_codegen",
]
[[package]]
name = "cipher"
version = "0.2.5"
@ -523,6 +545,7 @@ version = "0.2.0"
dependencies = [
"base64",
"chrono",
"chrono-tz",
"config",
"diesel",
"diesel_geometry",
@ -1289,6 +1312,15 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "parse-zoneinfo"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
dependencies = [
"regex",
]
[[package]]
name = "pear"
version = "0.2.3"
@ -1361,6 +1393,45 @@ dependencies = [
"sha-1",
]
[[package]]
name = "phf"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
dependencies = [
"siphasher",
"uncased",
]
[[package]]
name = "pin-project-lite"
version = "0.2.6"
@ -1816,6 +1887,12 @@ dependencies = [
"libc",
]
[[package]]
name = "siphasher"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a86232ab60fa71287d7f2ddae4a7073f6b7aac33631c3015abb556f08c6d0a3e"
[[package]]
name = "slab"
version = "0.4.3"

View File

@ -17,7 +17,8 @@ diesel = { version = "1.4.6", features = ["postgres", "uuidv07", "chrono", "serd
diesel_geometry = "1.4.0"
uuid = { version = "0.8.2", features = ["serde", "v4"] }
rust-argon2 = "0.8.3"
chrono = { version = "0.4.19", features = ["serde"] }
chrono = { version = "0.4", features = ["serde"] }
chrono-tz = "0.6"
rand = "0.8.3"
iban_validate = "4.0.0"
base64 = "0.13.0"

View File

@ -18,6 +18,7 @@ login_lock_duration = 1800
reset_password_token_valid_duration = 3600
user_support_email = "support@einsatz.online"
new_member_default_role = "member"
default_timezone = "Etc/UTC"
[mail]
from = "No Reply <noreply@localhost>"
@ -26,4 +27,8 @@ reply_to = "support@localhost"
communication_email_type_id = "a0a93b6e-f200-11ea-ad50-e86a6451da25"
[api]
default_pagination_limit = 20
default_pagination_limit = 20
[billing]
#If set true members don't need billing related permissions if they are member_responsible for specific event
member_responsible_overwrites_permissions = true

View File

@ -18,6 +18,7 @@ login_lock_duration = 1800
reset_password_token_valid_duration = 3600
user_support_email = "support@einsatz.online"
new_member_default_role = "member"
default_timezone = "Europe/Berlin"
[mail]
from = "No Reply <noreply@localhost>"
@ -26,4 +27,8 @@ reply_to = "support@localhost"
communication_email_type_id = "a0a93b6e-f200-11ea-ad50-e86a6451da25"
[api]
default_pagination_limit = 20
default_pagination_limit = 20
[billing]
#If set true members don't need billing related permissions if they are member_responsible for specific event
member_responsible_overwrites_permissions = true

View File

@ -0,0 +1,66 @@
-- Delete permissions
DELETE
FROM permissions
WHERE permission LIKE 'modules.event#_billing.personnel.view' ESCAPE '#';
DELETE
FROM permissions
WHERE permission LIKE 'modules.event#_billing.start#_end#_times.edit' ESCAPE '#';
DELETE
FROM permissions
WHERE permission LIKE 'modules.event#_billing.view' ESCAPE '#';
DELETE
FROM permissions
WHERE permission LIKE 'modules.event#_billing.start#_end#_times.view' ESCAPE '#';
DELETE
FROM permissions
WHERE permission LIKE 'modules.event#_billing.approve' ESCAPE '#';
DELETE
FROM permissions
WHERE permission LIKE 'modules.event#_billing.personnel.edit' ESCAPE '#';
-- Drop Table personnel_billing
DROP TABLE IF EXISTS personnel_billing;
-- Remove real_start_time and real_end_time from eu_position_instances
alter table eu_position_instances
drop column real_start_time;
alter table eu_position_instances
drop column real_end_time;
-- Remove fields planned_start_time, planned_end_time, real_start_time, real_end_time, billing_rate_id and billing_state_id from eu_instances
alter table eu_instances
drop constraint eu_instances_billing_states_state_id_fk;
alter table eu_instances
drop constraint eu_instances_personnel_billing_rates_billing_rate_id_fk;
alter table eu_instances
drop column planned_start_time;
alter table eu_instances
drop column planned_end_time;
alter table eu_instances
drop column real_start_time;
alter table eu_instances
drop column real_end_time;
alter table eu_instances
drop column billing_rate_id;
alter table eu_instances
drop column billing_state_id;
-- Drop table personnel_billing_rates
DROP TABLE IF EXISTS personnel_billing_rates;
-- Drop table billing_states
DROP TABLE IF EXISTS billing_states;

View File

@ -0,0 +1,101 @@
-- Create new table billing states to define billing states for eu_instances
create table billing_states
(
state_id text
constraint billing_states_pk
primary key,
description text,
final_approve boolean default false not null
);
-- Create new table personnel_billing_rates to define billing rates for personnel billing
create table personnel_billing_rates
(
billing_rate_id uuid default uuid_generate_v1() not null
constraint billing_rates_pk
primary key,
name text not null,
description text,
active boolean default true not null,
payment_per_hour numeric(18, 8) default 0 not null,
lump_sum numeric(18, 8) default 0 not null
);
-- Add fields planned_start_time, planned_end_time, real_start_time, real_end_time, billing_rate_id and billing_state_id to eu_instances
alter table eu_instances
add planned_start_time timestamptz;
alter table eu_instances
add planned_end_time timestamptz;
alter table eu_instances
add real_start_time timestamptz;
alter table eu_instances
add real_end_time timestamptz;
alter table eu_instances
add billing_rate_id uuid;
alter table eu_instances
add billing_state_id text;
alter table eu_instances
add constraint eu_instances_billing_states_state_id_fk
foreign key (billing_state_id) references billing_states;
alter table eu_instances
add constraint eu_instances_personnel_billing_rates_billing_rate_id_fk
foreign key (billing_rate_id) references personnel_billing_rates;
-- Add real_start_time and real_end_time to eu_position_instances
alter table eu_position_instances
add real_start_time timestamptz;
alter table eu_position_instances
add real_end_time timestamptz;
-- Add table personnel_billing to store money calculated for each position_instance
create table personnel_billing
(
position_instance_id uuid
constraint personnel_billing_pk
primary key
constraint personnel_billing_eu_position_instances_position_instance_id_fk
references eu_position_instances,
member_id uuid not null
constraint personnel_billing_members_entity_id_fk
references members,
fulfilled_time int default 0 not null,
money_for_time decimal(18, 8) default 0 not null,
money_from_lump_sum decimal(18, 8) default 0 not null,
total_money decimal(18, 8) default 0 not null
);
-- Add permissions for billing module
INSERT INTO permissions (permission, description, context, context_type)
VALUES ('modules.event_billing.view',
'Permission to see event billing UI. Note: This and all other billing related permissions are overwritten if member is responsible for event and member_responsible_overwrites_permissions is set to true in config!',
false, null);
INSERT INTO permissions (permission, description, context, context_type)
VALUES ('modules.event_billing.start_end_times.view', 'Permission to see start/end times for event units in billing UI',
false, null);
INSERT INTO permissions (permission, description, context, context_type)
VALUES ('modules.event_billing.start_end_times.edit',
'Permission to edit start/end times for event units in billing UI', false, null);
INSERT INTO permissions (permission, description, context, context_type)
VALUES ('modules.event_billing.personnel.view', 'Permission to see personnel billing in billing UI', false, null);
INSERT INTO permissions (permission, description, context, context_type)
VALUES ('modules.event_billing.personnel.edit', 'Permission to edit personnel billing in billing UI', false, null);
INSERT INTO permissions (permission, description, context, context_type)
VALUES ('modules.event_billing.approve', 'Permission to approve billing for specified (context) billing_states', true,
'billing_state');

View File

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
alter table users
drop column timezone;

View File

@ -0,0 +1,3 @@
-- Your SQL goes here
alter table users
add timezone text;

View File

@ -21,7 +21,24 @@
<input type="text" class="form-control" id="edit_event_cast_instance_name">
</div>
</div>
<button class="btn btn-success edit_event_cast_add_template" style="float: right">Hinzufügen</button><br><br>
<div class="form-group row">
<label class="col-sm-3 col-form-label"
for="edit_event_cast_instance_planned_start_time">Beginn: </label>
<div class="col-sm-9">
<input class="form-control" id="edit_event_cast_instance_planned_start_time"
type="datetime-local">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label"
for="edit_event_cast_instance_planned_end_time">Ende: </label>
<div class="col-sm-9">
<input class="form-control" id="edit_event_cast_instance_planned_end_time"
type="datetime-local">
</div>
</div>
<button class="btn btn-success edit_event_cast_add_template" style="float: right">Hinzufügen</button>
<br><br>
</div>
</div>

View File

@ -1,15 +1,25 @@
<div class="instance col-lg-4" style="padding: 2px;" data-instance-id="{{instance_id}}" data-template-id="{{template_id}}">
<div class="card">
<div class="card-header {{#if complete}}acs-green{{else}}acs-red{{/if}}">
{{name}}<button class="iconbutton remove_instance_button" style="float: right;"><svg color="red" width="20" height="20" fill="currentColor">
<use xlink:href="/img/bootstrap-icons.svg#trash"></use>
</svg></button>
<div class="card-header d-flex justify-content-between align-items-center {{#if complete}}acs-green{{else}}acs-red{{/if}}">
{{name}}
<div class="card-header-buttons">
<button class="iconbutton remove_instance_button" style="float: right;">
<svg color="red" fill="currentColor" height="20" width="20">
<use xlink:href="/img/bootstrap-icons.svg#trash"></use>
</svg>
</button>
<button class="btn btn-success btn-sm save-button" style="display: none;">Änderungen
Speichern
</button>
</div>
</div>
<div class="card-body">
<h5>Personal:</h5>
<div class="eu_cast_instance_personal">
{{#each positions}}
<div class="form-group row eu_cast_instance_personal_position" data-instance-id="{{../instance_id}}" data-position-id="{{position_id}}" data-member-name="{{member_name}}" data-member-id="{{taken_by}}">
<div class="form-group row eu_cast_instance_personal_position" data-instance-id="{{../instance_id}}"
data-position-id="{{position_id}}" data-member-name="{{member_name}}"
data-member-id="{{taken_by}}">
<label class="col-4 col-form-label">{{name}}</label>
<div class="input-group mb-3 col-8">
{{> search base=this.base type="member"}}
@ -21,7 +31,8 @@
<h5>Fahrzeuge:</h5>
<div class="eu_cast_instance_vehicles">
{{#each vehicle_positions}}
<div class="form-group row eu_cast_instance_vehicle_position" data-instance-id="{{../instance_id}}" data-position-id="{{position_id}}" data-identifier="{{identifier}}" data-entity-id="{{taken_by}}">
<div class="form-group row eu_cast_instance_vehicle_position" data-instance-id="{{../instance_id}}"
data-position-id="{{position_id}}" data-identifier="{{identifier}}" data-entity-id="{{taken_by}}">
<label class="col-4 col-form-label">{{name}}</label>
<div class="input-group mb-3 col-8">
{{> search base=this.base type="vehicle"}}
@ -30,7 +41,27 @@
{{/each}}
</div>
{{/if}}
<hr>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Name: </label>
<div class="col-sm-9">
<input class="form-control eu_cast_instance_name qsf" type="text" value="{{name}}">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Beginn: </label>
<div class="col-sm-9">
<input class="form-control eu_cast_instance_planned_start_time qsf" type="datetime-local"
value="{{planned_start_time}}">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Ende: </label>
<div class="col-sm-9">
<input class="form-control eu_cast_instance_planned_end_time qsf" type="datetime-local"
value="{{planned_end_time}}">
</div>
</div>
</div>
</div>
</div>

View File

@ -4,16 +4,25 @@
{{name}}
</div>
<div class="card-body">
<div class="row">
<p class="col-4">Einsatzzeit:</p>
<p class="col-8">
{{planned_time}}
</p>
</div>
<h5>Personal:</h5>
<div class="eu_cast_instance_personal">
{{#each positions}}
<div class="form-group row eu_cast_instance_personal_position" data-instance-id="{{../instance_id}}" data-position-id="{{position_id}}" data-member-name="{{member_name}}" data-member-id="{{taken_by}}">
<label class="col-4 col-form-label">{{name}}</label>
<div class="input-group mb-3 col-8">
{{#if taken_by}}{{member_name}}{{else}}
<button class="btn btn-block btn-sm btn-secondary eu_cast_instance_self_register">Eintragen</button>
{{/if}}
</div>
<div class="form-group row eu_cast_instance_personal_position" data-instance-id="{{../instance_id}}"
data-position-id="{{position_id}}" data-member-name="{{member_name}}"
data-member-id="{{taken_by}}">
<label class="col-4 col-form-label">{{name}}</label>
<div class="input-group mb-3 col-8">
{{#if taken_by}}{{member_name}}{{else}}
<button class="btn btn-block btn-sm btn-secondary eu_cast_instance_self_register">Eintragen
</button>
{{/if}}
</div>
</div>
{{/each}}
</div>

View File

@ -14,6 +14,7 @@ $(document).ready(function () {
EventEditModule = (function () {
let templates = {};
let event = {};
let instance_modified = false;
let load_templates = function () {
$.get("/templates/em_edit_event_core_data.hbs", function (res) {
@ -56,6 +57,9 @@ EventEditModule = (function () {
var template_search = new MiniSearchbar("edit_event_cast_template_search", null, null, null);
template_search.setup();
$("#edit_event_cast_instance_planned_start_time").val(event.start);
$("#edit_event_cast_instance_planned_end_time").val(event.end);
$(".edit_event_cast_add_template").on("click", add_template_instance);
load_instances();
}
@ -169,7 +173,8 @@ EventEditModule = (function () {
instance.template_id = template;
instance.name = instance_name;
instance.event_id = event_id;
instance.planned_start_time = $("#edit_event_cast_instance_planned_start_time").val() || undefined;
instance.planned_end_time = $("#edit_event_cast_instance_planned_end_time").val() || undefined;
$.ajax({
url: '/api/events/' + event_id + '/instances',
type: 'POST',
@ -215,7 +220,7 @@ EventEditModule = (function () {
if(this.vehicle_positions[i].taken_by){
let vehicle = await get_vehicle(this.vehicle_positions[i].taken_by);
this.vehicle_positions[i].identifier = vehicle.identifier;
}else{
} else {
vehicles_taken = false;
}
}
@ -223,15 +228,20 @@ EventEditModule = (function () {
$("#instances_container").append(templates.cast_instance(this));
$(".remove_instance_button").off("click").on("click", remove_instance);
$(".qsf").off("focusin focusout").on("focusin focusout", activate_modified).keyup(function (e) {
if (event.which === 13) {
activate_modified(e.target);
}
});
$(".eu_cast_instance_personal_position").each(function(){
$(".eu_cast_instance_personal_position").each(function () {
let pos = $(this).data("position-id");
let instance = $(this).data("instance-id");
let callback = async function(caller){
let callback = async function (caller) {
let ms = this;
let requirements_fulfilled = await check_position_requirements(pos, $(caller).data("entity-id"));
if(!requirements_fulfilled){
if (!requirements_fulfilled) {
$("#overwrite_position_requirements_modal").modal();
$(".overwrite_position_requirements_modal_submit").off("click").on("click", function(){
add_entity_to_position(instance, pos, $(caller).data("entity-id"));
@ -283,8 +293,45 @@ EventEditModule = (function () {
}
});
};
let activate_modified = function () {
if (!$(this).hasClass("modified")) { //Only execute on first modification
$(this).closest(".instance").find(".save-button").off("click").on("click", save).show(); //Show save button
$(this).addClass("modified");
}
};
let deactivate_modified = function () {
var delete_event = function(){
};
let save = async function () {
let data = {};
let instance = $(this).closest(".instance");
data.instance_id = instance.data("instance-id") || undefined;
data.name = instance.find(".eu_cast_instance_name").val() || undefined;
data.planned_start_time = instance.find(".eu_cast_instance_planned_start_time").val() || undefined;
data.planned_end_time = instance.find(".eu_cast_instance_planned_end_time").val() || undefined;
//TODO: error if no name
$.ajax({
type: "PATCH",
url: "/api/events/" + event_id + "/instances/" + data.instance_id,
contentType: 'application/json',
timeout: 3000,
data: JSON.stringify(data),
error: function () {
alert("Es ist ein Fehler aufgetreten.");
},
success: async function (data) {
if (is_ok(data)) {
location.reload(); //TODO: soft reload
}
}
});
console.log("saving:" + data);
};
var delete_event = function () {
$.ajax({
url: '/api/events/' + event_id,
type: 'DELETE',

View File

@ -163,13 +163,29 @@ EventListModule = ( function() {
this.positions = await load_positions_for_instance(this.instance_id);
this.vehicle_positions = await load_vehicle_positions_for_instance(this.instance_id);
if (this.planned_start_time && this.planned_end_time) {
let date = new Date(this.planned_start_time);
let start_date = ('0' + date.getDate()).slice(-2) + '.' + ('0' + (date.getMonth() + 1)).slice(-2) + '.' + date.getFullYear();
let start_time = ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ' - ';
date = new Date(this.planned_end_time);
let end_date = ('0' + date.getDate()).slice(-2) + '.' + ('0' + (date.getMonth() + 1)).slice(-2) + '.' + date.getFullYear();
let end_time = ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2);
if (start_date === end_date) {
this.planned_time = start_date + " " + start_time + end_time;
} else {
this.planned_time = start_date + " " + start_time + end_date + " " + end_time;
}
}
let positions_taken = true;
for(let i=0;i<this.positions.length;i++){
this.positions[i].base = "search_"+this.positions[i].instance_id+"_"+this.positions[i].position_id;
if(this.positions[i].taken_by){
for (let i = 0; i < this.positions.length; i++) {
this.positions[i].base = "search_" + this.positions[i].instance_id + "_" + this.positions[i].position_id;
if (this.positions[i].taken_by) {
let member = await get_member(this.positions[i].taken_by);
this.positions[i].member_name = member.firstname + " "+member.lastname;
}else{
this.positions[i].member_name = member.firstname + " " + member.lastname;
} else {
positions_taken = false;
}
}

View File

@ -1 +1 @@
v0.2-71-g4b9ea4a
v0.2-80-gf8eca4a

View File

@ -1,18 +1,19 @@
use chrono::NaiveDateTime;
use diesel::{BoolExpressionMethods, ExpressionMethods, JoinOnDsl, PgTextExpressionMethods, RunQueryDsl, sql_query};
use diesel::{BoolExpressionMethods, ExpressionMethods, PgTextExpressionMethods, RunQueryDsl, sql_query};
use diesel::dsl::any;
use diesel::pg::types::sql_types::Uuid;
use rocket::State;
use crate::database::controller::connector::establish_connection;
use crate::database::controller::events::instances::instances::RawEventUnitInstance;
use crate::database::controller::events::templates::vehicle_positions::get_eu_vehicle_positions_for_template;
use crate::database::model::events::{Event, EventType, EventUnitInstance, EventUnitInstancePosition, EventUnitInstanceVehiclePosition, EventUnitPosition, EventUnitTemplate, EventUnitVehiclePosition};
use crate::database::model::events::{Event, EventType, EventUnitInstancePosition, EventUnitInstanceVehiclePosition, EventUnitPosition, EventUnitTemplate, EventUnitVehiclePosition};
use crate::diesel::QueryDsl;
use crate::helper::settings::Settings;
use crate::schema::eu_positions_templates;
pub mod templates;
pub mod instances;
//TODO: migrate to multiple files to improve readability
pub fn add_event(settings: &State<Settings>, data: Event) -> Result<Event, diesel::result::Error> {
@ -376,11 +377,11 @@ pub fn update_eu_template(settings: &State<Settings>, template: EventUnitTemplat
}
}
pub fn add_instance(settings: &State<Settings>, instance: EventUnitInstance) -> Result<EventUnitInstance, diesel::result::Error>{
pub fn add_instance(settings: &State<Settings>, instance: RawEventUnitInstance) -> Result<RawEventUnitInstance, diesel::result::Error> {
use crate::schema::eu_instances::dsl::*;
let connection = establish_connection(settings);
match diesel::insert_into(eu_instances).values(instance).get_result(&connection){
match diesel::insert_into(eu_instances).values(instance).get_result(&connection) {
Ok(res) => Ok(res),
Err(e) => {
error!("Couldn't create event unit instance: {}", e);
@ -423,19 +424,6 @@ pub fn add_position_instances_for_instance(settings: &State<Settings>, instance_
Ok(())
}
pub fn get_instances(settings: &State<Settings>, event: uuid::Uuid) -> Result<Vec<EventUnitInstance>, diesel::result::Error>{
use crate::schema::eu_instances::dsl::*;
let connection = establish_connection(settings);
match eu_instances.filter(event_id.eq(event)).get_results(&connection){
Ok(instances) => Ok(instances),
Err(e) => {
error!("Couldn't get event unit instances: {}", e);
Err(e)
}
}
}
pub fn get_instance_positions(settings: &State<Settings>, instance_id2: uuid::Uuid) -> Result<Vec<EventUnitInstancePosition>, diesel::result::Error>{
let connection = establish_connection(settings);

View File

@ -0,0 +1,68 @@
use std::convert::TryInto;
use chrono::Utc;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use rocket::State;
use crate::database::controller::connector::establish_connection;
use crate::modules::api::events::instances::update::PatchInstanceData;
use crate::schema::eu_instances;
use crate::Settings;
#[derive(Queryable, Clone, Deserialize, Serialize, AsChangeset, Insertable)]
#[table_name = "eu_instances"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(entity_id)]
pub struct RawEventUnitInstance {
pub(crate) instance_id: uuid::Uuid,
pub(crate) template_id: uuid::Uuid,
pub(crate) name: String,
pub(crate) event_id: uuid::Uuid,
pub(crate) planned_start_time: Option<chrono::DateTime<Utc>>,
pub(crate) planned_end_time: Option<chrono::DateTime<Utc>>,
pub(crate) real_start_time: Option<chrono::DateTime<Utc>>,
pub(crate) real_end_time: Option<chrono::DateTime<Utc>>,
pub(crate) billing_rate_id: Option<uuid::Uuid>,
pub(crate) billing_state_id: Option<String>,
}
pub fn get_instances(settings: &State<Settings>, event: uuid::Uuid) -> Result<Vec<RawEventUnitInstance>, diesel::result::Error> {
use crate::schema::eu_instances::dsl::*;
let connection = establish_connection(settings);
match eu_instances.filter(event_id.eq(event)).get_results(&connection) {
Ok(instances) => Ok(instances),
Err(e) => {
error!("Couldn't get event unit instances: {}", e);
Err(e)
}
}
}
#[derive(Queryable, Clone, Deserialize, Serialize, AsChangeset, Insertable)]
#[table_name = "eu_instances"]
#[primary_key(entity_id)]
pub struct RawEventUnitInstanceChangeset {
pub(crate) instance_id: uuid::Uuid,
pub(crate) event_id: uuid::Uuid,
pub(crate) name: Option<String>,
pub(crate) planned_start_time: Option<Option<chrono::DateTime<Utc>>>,
pub(crate) planned_end_time: Option<Option<chrono::DateTime<Utc>>>,
pub(crate) real_start_time: Option<Option<chrono::DateTime<Utc>>>,
pub(crate) real_end_time: Option<Option<chrono::DateTime<Utc>>>,
pub(crate) billing_rate_id: Option<Option<uuid::Uuid>>,
pub(crate) billing_state_id: Option<Option<String>>,
}
pub fn update_instance(settings: &State<Settings>, patch: RawEventUnitInstanceChangeset) -> Result<RawEventUnitInstance, diesel::result::Error> {
use crate::schema::eu_instances::dsl::*;
let connection = establish_connection(settings);
match diesel::update(eu_instances.filter(event_id.eq(patch.event_id)).filter(instance_id.eq(patch.instance_id))).set(patch).get_result(&connection) {
Ok(update) => Ok(update),
Err(e) => {
error!("Couldn't patch event unit instance: {}", e);
Err(e)
}
}
}

View File

@ -0,0 +1,2 @@
pub mod instance_positions;
pub mod instances;

View File

@ -1,14 +1,15 @@
use diesel::ExpressionMethods;
use diesel::result::Error;
use rocket::State;
use crate::database::controller::connector::establish_connection;
use crate::database::model::users::User;
use crate::diesel::prelude::*;
use crate::diesel::QueryDsl;
use crate::helper::settings::Settings;
use diesel::ExpressionMethods;
use rocket::State;
use crate::schema::members::columns::entity_id;
use diesel::result::Error;
use crate::schema::users::dsl::users;
use crate::schema::members::dsl::members;
use crate::schema::users::dsl::users;
pub fn get_user_by_email(email: String, settings: &State<Settings>) -> Option<User> {
use crate::schema::users::dsl::email as email1;
@ -42,7 +43,7 @@ pub fn get_user_by_member(settings: &State<Settings>, member_uuid: uuid::Uuid) -
let connection = establish_connection(settings);
let user : Result<User, diesel::result::Error> = members.inner_join(crate::schema::users::table).filter(entity_id.eq(member_uuid)).select((id, password, email, username)).first(&connection);
let user: Result<User, diesel::result::Error> = members.inner_join(crate::schema::users::table).filter(entity_id.eq(member_uuid)).select((id, password, email, username, timezone)).first(&connection);
match user{
Ok(user) => Ok(Some(user)),
Err(e) => {

View File

@ -1,17 +1,18 @@
use chrono::NaiveDateTime;
use crate::schema::events;
use crate::schema::event_types;
use diesel::sql_types::{Jsonb, Nullable, SmallInt, Text, Timestamp, Uuid};
use crate::schema::eu_instances;
use crate::schema::eu_positions;
use crate::schema::eu_templates;
use crate::schema::eu_instances;
use crate::schema::eu_vehicle_positions;
use diesel::sql_types::{Uuid, Text, Nullable, Jsonb, Timestamp, SmallInt};
use crate::schema::event_types;
use crate::schema::events;
#[derive(Queryable, Clone, Deserialize, Serialize, AsChangeset, Insertable, QueryableByName)]
#[table_name = "events"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(entity_id)]
pub struct Event{
pub struct Event {
#[sql_type = "Uuid"]
pub(crate) entity_id: uuid::Uuid,
#[sql_type = "Text"]
@ -80,7 +81,8 @@ pub struct EventUnitTemplate{
#[table_name = "eu_instances"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(entity_id)]
pub struct EventUnitInstance{
#[deprecated]
pub struct EventUnitInstanceDeprecated {
pub(crate) instance_id: uuid::Uuid,
pub(crate) template_id: uuid::Uuid,
pub(crate) name: String,

View File

@ -4,4 +4,5 @@ pub struct User {
pub(crate) password: Option<String>,
pub(crate) email: String,
pub(crate) username: Option<String>,
pub(crate) timezone: Option<String>,
}

View File

@ -14,4 +14,6 @@ pub mod user_request_guard;
pub mod mail_queue;
pub mod mail_templates;
pub mod permission;
pub mod order_enum;
pub mod order_enum;
pub mod time;
pub mod serde_patch;

59
src/helper/serde_patch.rs Normal file
View File

@ -0,0 +1,59 @@
use std::convert::TryInto;
use serde::{Deserialize, Deserializer};
#[derive(Debug, PartialEq)]
pub enum Patch<T> {
Missing,
Null,
Value(T),
}
impl<T> Default for Patch<T> {
fn default() -> Self {
Patch::Missing
}
}
impl<T> From<Option<T>> for Patch<T> {
fn from(opt: Option<T>) -> Patch<T> {
match opt {
Some(v) => Patch::Value(v),
None => Patch::Null,
}
}
}
impl<T> Into<Option<Option<T>>> for Patch<T> {
fn into(self) -> Option<Option<T>> {
match self {
Patch::Missing => None,
Patch::Null => Some(None),
Patch::Value(v) => Some(Some(v))
}
}
}
impl<T> TryInto<Option<T>> for Patch<T> {
type Error = ();
fn try_into(self) -> Result<Option<T>, Self::Error> {
match self {
Patch::Missing => Ok(None),
Patch::Null => Err(()),
Patch::Value(v) => Ok(Some(v))
}
}
}
impl<'de, T> Deserialize<'de> for Patch<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Option::deserialize(deserializer).map(Into::into)
}
}

View File

@ -1,15 +1,17 @@
use core::fmt;
use std::{error, iter};
use std::collections::HashMap;
use std::sync::RwLock;
use chrono::{DateTime, Utc};
use rand::{Rng, thread_rng};
use rand::distributions::Alphanumeric;
use crate::database::model::users::User;
use crate::helper::session_cookies::model::{
SessionCookie, SessionCookieError, SessionCookieStorage,
};
use crate::modules::member_management::model::member::Member;
use chrono::{DateTime, Utc};
use core::fmt;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use std::collections::HashMap;
use std::{error, iter};
use std::sync::RwLock;
impl SessionCookieStorage {
/// Generates random alphanumerical id
@ -122,10 +124,11 @@ impl error::Error for SessionCookieError {
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
use uuid::Uuid;
use super::*;
#[test]
pub fn test_add() {
let storage = SessionCookieStorage::new();
@ -136,7 +139,8 @@ mod tests {
id: uuid,
password: None,
email: "test@test.de".to_string(),
username: None
username: None,
timezone: None,
};
let inserted_cookie = storage.add(
@ -162,7 +166,8 @@ mod tests {
id: uuid,
password: None,
email: "test@test.de".to_string(),
username: None
username: None,
timezone: None,
};
let inserted_cookie = storage.add(
@ -189,7 +194,8 @@ mod tests {
id: uuid,
password: None,
email: "test@test.de".to_string(),
username: None
username: None,
timezone: None,
};
let inserted_cookie = storage.add(
@ -213,7 +219,8 @@ mod tests {
id: uuid,
password: None,
email: "test@test.de".to_string(),
username: None
username: None,
timezone: None,
};
storage.add(

View File

@ -1,12 +1,13 @@
use config::{Config, ConfigError, Environment, File};
use std::env;
#[derive(Debug, Deserialize, Default)]
use config::{Config, ConfigError, Environment, File};
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Database {
pub connection_string: String,
}
#[derive(Debug, Deserialize, Default)]
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Application {
pub url: String,
pub default_language: String,
@ -20,26 +21,33 @@ pub struct Application {
pub reset_password_token_valid_duration: i64,
pub user_support_email: String,
pub new_member_default_role: String,
pub default_timezone: String,
}
#[derive(Debug, Deserialize, Default)]
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Mail{
pub from : String,
pub reply_to : String,
pub from: String,
pub reply_to: String,
pub communication_email_type_id: String,
}
#[derive(Debug, Deserialize, Default)]
pub struct Api{
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Api {
pub default_pagination_limit: i64,
}
#[derive(Debug, Deserialize, Default)]
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Billing {
pub member_responsible_overwrites_permissions: bool,
}
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Settings {
pub database: Database,
pub application: Application,
pub mail: Mail,
pub api: Api,
pub billing: Billing,
}
impl Settings {

160
src/helper/time.rs Normal file
View File

@ -0,0 +1,160 @@
use chrono::{DateTime, NaiveDateTime, ParseError, TimeZone, Utc};
use chrono_tz::Tz;
use rocket::State;
use crate::helper::session_cookies::model::SessionCookie;
use crate::Settings;
pub fn get_timezone(settings: &State<Settings>, cookie: &SessionCookie) -> Tz {
let default_tz: Tz = match settings.application.default_timezone.parse() {
Ok(tz) => tz,
Err(e) => {
panic!("Invalid configuration! Couldn't parse default_timezone: {}", e);
}
};
match cookie.user.timezone.clone() {
Some(tz) => match tz.parse() {
Ok(tz) => tz,
Err(e) => {
warn!("Unparsable timezone for user: {}, falling back to default timezone. Error is {}", cookie.user.id, e);
default_tz
}
},
None => default_tz
}
}
pub fn datetime_str_to_utc(settings: &State<Settings>, cookie: &SessionCookie, user_datetime: &str) -> Result<DateTime<Utc>, ParseError> {
let tz = get_timezone(settings, cookie);
let naive = NaiveDateTime::parse_from_str(user_datetime, "%Y-%m-%dT%H:%M")?;
let local_datetime = tz.from_local_datetime(&naive).unwrap();
Ok(local_datetime.with_timezone(&Utc))
}
pub fn utc_to_local_user_time(settings: &State<Settings>, cookie: &SessionCookie, utc: DateTime<Utc>) -> String {
let tz = get_timezone(settings, cookie);
let local_time = utc.with_timezone(&tz);
local_time.format("%Y-%m-%dT%H:%M").to_string()
}
#[cfg(test)]
mod tests {
use chrono::{DateTime, Local, TimeZone, Utc};
use rocket::{Build, Rocket};
use crate::database::model::users::User;
use crate::helper::settings::Application;
use super::*;
fn get_test_settings() -> Settings {
Settings {
database: Default::default(),
application: Application {
url: "".to_string(),
default_language: "".to_string(),
fallback_language: "".to_string(),
loglevel: "".to_string(),
session_timeout: 0,
upload_path: "".to_string(),
max_login_attempts: 0,
login_lock_duration: 0,
name: "".to_string(),
reset_password_token_valid_duration: 0,
user_support_email: "".to_string(),
new_member_default_role: "".to_string(),
default_timezone: "Europe/Berlin".to_string(),
},
mail: Default::default(),
api: Default::default(),
billing: Default::default(),
}
}
fn get_test_cookies() -> SessionCookie {
SessionCookie {
id: "".to_string(),
expires: Utc::now(),
user: User {
id: Default::default(),
password: None,
email: "".to_string(),
username: None,
timezone: None,
},
member: None,
}
}
#[test]
pub fn test_utc_to_local_user_time() {
let utcdate: DateTime<Utc> = Utc.ymd(2022, 1, 23).and_hms(12, 0, 0);
let rocket = rocket::build().manage(get_test_settings());
let res = utc_to_local_user_time(State::get(&rocket).unwrap(), &get_test_cookies(), utcdate);
assert_eq!(res, "2022-01-23T13:00");
}
#[test]
pub fn test_datetime_str_to_utc_error() {
let rocket = rocket::build().manage(get_test_settings());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23").is_err());
}
#[test]
pub fn test_datetime_str_to_utc_error2() {
let rocket = rocket::build().manage(get_test_settings());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23T").is_err());
}
#[test]
pub fn test_datetime_str_to_utc_error3() {
let rocket = rocket::build().manage(get_test_settings());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23T15:00:00").is_err());
}
#[test]
pub fn test_datetime_str_to_utc_error4() {
let rocket = rocket::build().manage(get_test_settings());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23T15").is_err());
}
#[test]
pub fn test_datetime_str_to_utc_error5() {
let rocket = rocket::build().manage(get_test_settings());
assert!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "Black Mamba").is_err());
}
#[test]
pub fn test_datetime_str_to_utc_correct() {
let rocket = rocket::build().manage(get_test_settings());
let utcdate: DateTime<Utc> = Utc.ymd(2022, 1, 23).and_hms(12, 0, 0);
assert_eq!(datetime_str_to_utc(State::get(&rocket).unwrap(), &get_test_cookies(), "2022-01-23T13:00").unwrap(), utcdate);
}
#[test]
pub fn test_timezone() {
let utcdate: DateTime<Utc> = Utc.ymd(2022, 1, 23).and_hms(12, 0, 0);
let tz: Tz = "Europe/Berlin".parse().unwrap();
let localdate1 = utcdate.with_timezone(&tz);
let localdate2: DateTime<Local> = Local.ymd(2022, 1, 23).and_hms(13, 0, 0);
print!("{}:{}", localdate1.to_string(), localdate2.to_string());
assert_eq!(localdate1, localdate2);
}
#[test]
pub fn test_timezone_to_utc() {
let tz: Tz = "Europe/Berlin".parse().unwrap();
let naive = NaiveDateTime::parse_from_str("2022-01-23T13:00", "%Y-%m-%dT%H:%M").unwrap();
let local_datetime = tz.from_local_datetime(&naive).unwrap();
let converted_to_utc: DateTime<Utc> = local_datetime.with_timezone(&Utc);
let native_utc: DateTime<Utc> = Utc.ymd(2022, 1, 23).and_hms(12, 0, 0);
print!("{}:{}", converted_to_utc.to_string(), native_utc.to_string());
assert_eq!(converted_to_utc, native_utc);
}
}

View File

@ -1,6 +1,7 @@
extern crate argon2;
extern crate base64;
extern crate chrono;
extern crate chrono_tz;
extern crate config;
#[macro_use]
extern crate diesel;
@ -211,6 +212,7 @@ fn rocket() -> _ {
modules::event_management::edit_event::edit_event,
modules::api::events::instances::create::create_instance,
modules::api::events::instances::read::read_instances,
modules::api::events::instances::update::patch_instance,
modules::api::events::event_units::position::create::create_event_unit_vehicle_position,
modules::api::events::event_units::templates::read::read_event_unit_template_vehicle_positions,
modules::api::events::event_units::position::delete::delete_event_unit_vehicle_positions,

View File

@ -1,13 +1,12 @@
use crate::helper::session_cookies::model::SessionCookie;
use rocket::State;
use crate::helper::settings::Settings;
use rocket::http::Status;
use crate::helper::sitebuilder::model::sidebar::Sidebar;
use crate::helper::sitebuilder::model::general::{Header, Stylesheet, Footer, Script};
use rocket::State;
use rocket_dyn_templates::Template;
use crate::database::model::roles::Role;
use crate::database::controller::roles::get_roles;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
use crate::helper::sitebuilder::model::general::{Footer, Header, Script, Stylesheet};
use crate::helper::sitebuilder::model::sidebar::Sidebar;
use crate::modules::admin_settings::permissions::SettingsModule;
#[get("/portal/settings/roles")]

View File

@ -1,4 +1,3 @@
use diesel::result::Error;
use rocket::serde::json::Json;
use rocket::State;

View File

@ -74,7 +74,7 @@ pub fn put_position_in_template(
let data = put_position_in_template_data.into_inner();
if (data.template_id != template_id || data.position_entity_id != position_id) {
if data.template_id != template_id || data.position_entity_id != position_id {
return Err(Json(
ApiError::new(400, "template_id or position_entity_id in URI doesn't match POST data!".to_string()).to_wrapper(),
));

View File

@ -3,7 +3,7 @@ use rocket::State;
use crate::database::controller::events::{EventUnitTemplatePosition, get_event_unit_positions_for_template, get_event_unit_templates, get_event_unit_templates_count};
use crate::database::controller::events::templates::vehicle_positions::get_eu_vehicle_positions_for_template;
use crate::database::model::events::{EventUnitPosition, EventUnitTemplate, EventUnitVehiclePosition};
use crate::database::model::events::{EventUnitTemplate, EventUnitVehiclePosition};
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
use crate::helper::translate_diesel_error::translate_diesel;

View File

@ -1,20 +1,27 @@
use rocket::State;
use crate::helper::settings::Settings;
use crate::helper::session_cookies::model::SessionCookie;
use chrono::{DateTime, FixedOffset, NaiveDateTime};
use chrono_tz::Tz;
use diesel::result::Error;
use rocket::serde::json::Json;
use crate::database::model::events::EventUnitInstance;
use crate::modules::api::model::api_outcome::{ApiErrorWrapper, ApiError};
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use rocket::State;
use crate::database::controller::entities::generate_entity;
use crate::database::controller::events::{add_instance, add_position_instances_for_instance};
use crate::database::controller::events::instances::instances::RawEventUnitInstance;
use crate::database::model::events::EventUnitInstanceDeprecated;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
use crate::helper::time::datetime_str_to_utc;
use crate::helper::translate_diesel_error::translate_diesel;
use diesel::result::Error;
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
#[derive(Queryable, Clone, Deserialize, Serialize)]
pub struct CreateInstanceData{
pub struct CreateInstanceData {
pub template_id: String,
pub name: String,
pub event_id: String,
pub event_id: uuid::Uuid,
pub planned_start_time: Option<String>,
pub planned_end_time: Option<String>,
}
#[post("/api/events/<event_id>/instances", format = "json", data = "<create_instance_data>")]
@ -23,8 +30,8 @@ pub fn create_instance(
cookie: SessionCookie,
create_instance_data: Json<CreateInstanceData>,
event_id: String,
) -> Result<Json<EventUnitInstance>, Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member)?;
) -> Result<Json<RawEventUnitInstance>, Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member.clone())?;
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
return Err(Json(
ApiError::new(403, "Keine Berechtigung Einsätze zu bearbeiten!".to_string()).to_wrapper(),
@ -32,21 +39,55 @@ pub fn create_instance(
}
let cid = create_instance_data.into_inner();
if event_id != cid.event_id{
if parse_uuid_string(event_id.clone())? != cid.event_id {
return Err(Json(
ApiError::new(400, "Two different event_ids in body and parameter!".to_string()).to_wrapper(),
))
}
let entity_id = match generate_entity(settings){
let entity_id = match generate_entity(settings) {
Ok(entity) => entity,
Err(_e) => return Err(Json(ApiError::new(500, "Konnte keine neue Entität anlegen.".to_string()).to_wrapper())),
};
let instance = EventUnitInstance{
let planned_start_time = match cid.planned_start_time {
Some(dt) => match datetime_str_to_utc(settings, &cookie, &dt) {
Ok(dt) => Some(dt),
Err(e) => {
warn!("Couldn't parse planned start time as DateTime: {}",e);
return Err(Json(
ApiError::new(400, "Couldn't parse planned start time!".to_string()).to_wrapper(),
))
}
},
None => None
};
let planned_end_time = match cid.planned_end_time {
Some(dt) => match datetime_str_to_utc(settings, &cookie, &dt) {
Ok(dt) => Some(dt),
Err(e) => {
warn!("Couldn't parse planned end time as DateTime: {}",e);
return Err(Json(
ApiError::new(400, "Couldn't parse planned end time!".to_string()).to_wrapper(),
))
}
},
None => None
};
let instance = RawEventUnitInstance {
instance_id: entity_id,
template_id: parse_uuid_string(cid.template_id)?,
name: cid.name,
event_id: parse_uuid_string(event_id)?
event_id: parse_uuid_string(event_id)?,
planned_start_time,
planned_end_time,
real_start_time: None,
real_end_time: None,
billing_rate_id: None,
billing_state_id: None,
};
match add_position_instances_for_instance(settings, instance.instance_id, instance.template_id){

View File

@ -1,12 +1,65 @@
use crate::helper::settings::Settings;
use rocket::State;
use crate::helper::session_cookies::model::SessionCookie;
use crate::database::model::events::{EventUnitInstance, EventUnitInstancePosition, EventUnitInstanceVehiclePosition};
use rocket::serde::json::Json;
use crate::modules::api::model::api_outcome::{ApiErrorWrapper, ApiError};
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use crate::database::controller::events::{get_instances, get_instance_positions, get_instance_vehicle_positions};
use rocket::State;
use crate::database::controller::events::{get_instance_positions, get_instance_vehicle_positions};
use crate::database::controller::events::instances::instances::{get_instances, RawEventUnitInstance};
use crate::database::model::events::{EventUnitInstanceDeprecated, EventUnitInstancePosition, EventUnitInstanceVehiclePosition};
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
use crate::helper::time::utc_to_local_user_time;
use crate::helper::translate_diesel_error::translate_diesel;
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
/// This struct contains RawEventUnitInstance data but converts all UTC DateTimes to local time
#[derive(Queryable, Clone, Deserialize, Serialize, )]
pub struct EventUnitInstance {
pub instance_id: uuid::Uuid,
pub template_id: uuid::Uuid,
pub name: String,
pub event_id: uuid::Uuid,
pub planned_start_time: Option<String>,
pub planned_end_time: Option<String>,
pub real_start_time: Option<String>,
pub real_end_time: Option<String>,
pub billing_rate_id: Option<uuid::Uuid>,
pub billing_state_id: Option<String>,
}
impl EventUnitInstance {
/// Convert RawEventUnitInstance to EventUnitInstance by converting Utc DateTimes to timestamps with user specified timezone
pub fn from_raw(raw: RawEventUnitInstance, settings: &State<Settings>, cookie: &SessionCookie) -> EventUnitInstance {
let planned_start_time = match raw.planned_start_time {
Some(dt) => Some(utc_to_local_user_time(settings, cookie, dt)),
None => None
};
let planned_end_time = match raw.planned_end_time {
Some(dt) => Some(utc_to_local_user_time(settings, cookie, dt)),
None => None
};
let real_start_time = match raw.real_start_time {
Some(dt) => Some(utc_to_local_user_time(settings, cookie, dt)),
None => None
};
let real_end_time = match raw.real_end_time {
Some(dt) => Some(utc_to_local_user_time(settings, cookie, dt)),
None => None
};
EventUnitInstance {
instance_id: raw.instance_id,
template_id: raw.template_id,
name: raw.name,
event_id: raw.event_id,
planned_start_time,
planned_end_time,
real_start_time,
real_end_time,
billing_rate_id: raw.billing_rate_id,
billing_state_id: raw.billing_state_id,
}
}
}
#[get("/api/events/<event_id>/instances", format = "json", rank = 1)]
pub fn read_instances(
@ -14,17 +67,24 @@ pub fn read_instances(
cookie: SessionCookie,
event_id: String,
) -> Result<Json<Vec<EventUnitInstance>>, Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member)?;
let caller = parse_member_cookie(cookie.member.clone())?;
if !caller.has_permission(crate::permissions::modules::event_management::events::VIEW.to_string()) {
return Err(Json(
ApiError::new(403, "Keine Berechtigung Einsätze abzurufen!".to_string()).to_wrapper(),
));
}
match get_instances(settings, parse_uuid_string(event_id)?){
Ok(pos) => Ok(Json(pos)),
let pos = match get_instances(settings, parse_uuid_string(event_id)?) {
Ok(pos) => pos,
Err(e) => return Err(translate_diesel(e))
};
let mut res: Vec<EventUnitInstance> = vec![];
for instance in pos {
res.push(EventUnitInstance::from_raw(instance, &settings, &cookie));
}
Ok(Json(res))
}
#[get("/api/events/instances/<instance_id>/positions", format = "json", rank = 1)]

View File

@ -1,11 +1,20 @@
use crate::helper::settings::Settings;
use rocket::State;
use crate::helper::session_cookies::model::SessionCookie;
use std::convert::TryInto;
use chrono::{DateTime, ParseError, Utc};
use rocket::serde::json::Json;
use crate::modules::api::model::api_outcome::{ApiErrorWrapper, ApiError};
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use crate::database::controller::events::change_position_instances;
use rocket::State;
use uuid::Uuid;
use crate::database::controller::events::{change_position_instances, get_event};
use crate::database::controller::events::instances::instances::{RawEventUnitInstanceChangeset, update_instance};
use crate::helper::serde_patch::Patch;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
use crate::helper::time::datetime_str_to_utc;
use crate::helper::translate_diesel_error::translate_diesel;
use crate::modules::api::events::instances::read::EventUnitInstance;
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
use crate::modules::event_management::check_position_requirements::{check_position_requirements, RequirementParserError};
#[put("/api/events/instances/<instance_id>/positions/<position_id>/entities/<entity_id>", format = "json", rank = 1)]
@ -72,8 +81,179 @@ pub fn remove_entity_from_position(
));
}
match change_position_instances(settings, parse_uuid_string(instance_id)?, parse_uuid_string(position_id)?, None){
match change_position_instances(settings, parse_uuid_string(instance_id)?, parse_uuid_string(position_id)?, None) {
Ok(pos) => Ok(Json(pos)),
Err(e) => return Err(translate_diesel(e))
}
}
#[derive(Queryable, Deserialize)]
pub struct PatchInstanceData {
pub instance_id: uuid::Uuid,
#[serde(default)]
pub name: Patch<String>,
#[serde(default)]
pub planned_start_time: Patch<String>,
#[serde(default)]
pub planned_end_time: Patch<String>,
#[serde(default)]
pub real_start_time: Patch<String>,
#[serde(default)]
pub real_end_time: Patch<String>,
#[serde(default)]
pub billing_rate_id: Patch<uuid::Uuid>,
#[serde(default)]
pub billing_state_id: Patch<String>,
}
#[patch("/api/events/<event_id>/instances/<instance_id>", format = "json", data = "<patch_instance_data>")]
pub fn patch_instance(
settings: &State<Settings>,
cookie: SessionCookie,
patch_instance_data: Json<PatchInstanceData>,
event_id: String,
instance_id: String,
) -> Result<Json<EventUnitInstance>, Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member.clone())?;
if !caller.has_permission(crate::permissions::modules::event_management::events::EDIT.to_string()) {
return Err(Json(
ApiError::new(403, "Keine Berechtigung Einsätze zu bearbeiten!".to_string()).to_wrapper(),
));
}
let pid = patch_instance_data.into_inner();
let event_id = parse_uuid_string(event_id)?;
let instance_id = parse_uuid_string(instance_id)?;
if instance_id != pid.instance_id {
return Err(Json(ApiError::new(400, "instance_id in URI doesn't match instance_id in body data.".to_string()).to_wrapper()))
}
let name = match pid.name.try_into() {
Ok(name) => name,
Err(_) => return Err(Json(ApiError::new(400, "Cannot set name to null!".to_string()).to_wrapper()))
};
let event = match get_event(settings, event_id) {
Ok(event) => event,
Err(e) => return Err(translate_diesel(e))
};
//Check if real_start_time or real_end_time is changed
if matches!(pid.real_start_time, Patch::Value(_)) || matches!(pid.real_start_time, Patch::Null) || matches!(pid.real_end_time, Patch::Value(_)) || matches!(pid.real_end_time, Patch::Null) {
//If so, check for additional permissions
if !caller.has_permission(crate::permissions::modules::event_billing::start_end_times::EDIT.to_string()) {
match event.member_responsible {
Some(resp) => {
if caller.entity_id != resp {
return Err(Json(
ApiError::new(403, "Keine Berechtigung Einsatzzeiten zu verändern!".to_string()).to_wrapper(),
));
}
}
None => {
return Err(Json(
ApiError::new(403, "Keine Berechtigung Einsatzzeiten zu verändern!".to_string()).to_wrapper(),
));
}
}
}
}
if matches!(pid.billing_rate_id, Patch::Value(_)) || matches!(pid.billing_rate_id, Patch::Null) || matches!(pid.billing_state_id, Patch::Value(_)) || matches!(pid.billing_state_id, Patch::Null) {
if !caller.has_permission(crate::permissions::modules::event_billing::personnel::EDIT.to_string()) {
match event.member_responsible {
Some(resp) => {
if caller.entity_id != resp {
return Err(Json(
ApiError::new(403, "Keine Berechtigung Abrechnung zu verändern!".to_string()).to_wrapper(),
));
}
}
None => {
return Err(Json(
ApiError::new(403, "Keine Berechtigung Abrechnung zu verändern!".to_string()).to_wrapper(),
));
}
}
}
}
let planned_start_time = match pid.planned_start_time {
Patch::Missing => None,
Patch::Null => Some(None),
Patch::Value(v) => {
match datetime_str_to_utc(settings, &cookie, &v) {
Ok(v) => Some(Some(v)),
Err(e) => {
warn!("Couldn't parse planned start time as DateTime: {}",e);
return Err(Json(
ApiError::new(400, "Couldn't parse planned start time!".to_string()).to_wrapper(),
))
}
}
}
};
let planned_end_time = match pid.planned_end_time {
Patch::Missing => None,
Patch::Null => Some(None),
Patch::Value(v) => {
match datetime_str_to_utc(settings, &cookie, &v) {
Ok(v) => Some(Some(v)),
Err(e) => {
warn!("Couldn't parse planned end time as DateTime: {}",e);
return Err(Json(
ApiError::new(400, "Couldn't parse planned end time!".to_string()).to_wrapper(),
))
}
}
}
};
let real_start_time = match pid.real_start_time {
Patch::Missing => None,
Patch::Null => Some(None),
Patch::Value(v) => {
match datetime_str_to_utc(settings, &cookie, &v) {
Ok(v) => Some(Some(v)),
Err(e) => {
warn!("Couldn't parse real start time as DateTime: {}",e);
return Err(Json(
ApiError::new(400, "Couldn't parse real start time!".to_string()).to_wrapper(),
))
}
}
}
};
let real_end_time = match pid.real_end_time {
Patch::Missing => None,
Patch::Null => Some(None),
Patch::Value(v) => {
match datetime_str_to_utc(settings, &cookie, &v) {
Ok(v) => Some(Some(v)),
Err(e) => {
warn!("Couldn't parse real end time as DateTime: {}",e);
return Err(Json(
ApiError::new(400, "Couldn't parse real end time!".to_string()).to_wrapper(),
))
}
}
}
};
let data = RawEventUnitInstanceChangeset {
instance_id,
event_id,
name,
planned_start_time,
planned_end_time,
real_start_time,
real_end_time,
billing_rate_id: pid.billing_rate_id.into(),
billing_state_id: pid.billing_state_id.into(),
};
match update_instance(settings, data) {
Ok(res) => Ok(Json(EventUnitInstance::from_raw(res, settings, &cookie))),
Err(e) => return Err(translate_diesel(e))
}
}

View File

@ -1,18 +1,18 @@
use rocket::State;
use crate::helper::settings::Settings;
use crate::helper::session_cookies::model::SessionCookie;
use chrono::{Duration, Local, NaiveDateTime};
use rocket::serde::json::Json;
use crate::modules::api::model::api_outcome::{ApiErrorWrapper, ApiError};
use crate::database::model::events::Event;
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string, parse_uuid};
use chrono::{NaiveDateTime, Local, Duration};
use crate::database::controller::events::{get_events, get_event_count, get_event, get_events_for_member_in_future, get_instances, get_instance_positions};
use crate::helper::translate_diesel_error::translate_diesel;
use uuid::Uuid;
use rocket::State;
use crate::database::controller::events::{get_event, get_event_count, get_events, get_events_for_member_in_future, get_instance_positions};
use crate::database::controller::events::instances::instances::get_instances;
use crate::database::model::events::Event;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
use crate::helper::translate_diesel_error::translate_diesel;
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid, parse_uuid_string};
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
#[derive(Queryable, Clone, Deserialize, Serialize)]
pub struct EventList{
pub struct EventList {
pub(crate) events: Vec<Event>,
pub(crate) total_event_count: i64,
}

View File

@ -1,3 +1,6 @@
use rocket::serde::json::Json;
use rocket::State;
use crate::database::controller::groups::{get_group, get_group_entity_states, get_raw_groups, GroupEntityState};
use crate::database::controller::groups_permissions::get_group_role_permissions;
use crate::database::controller::members::check_access_to_resource;
@ -8,12 +11,8 @@ use crate::helper::settings::Settings;
use crate::helper::translate_diesel_error::translate_diesel;
use crate::modules::api::groups::create::GroupRolePermission;
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use crate::modules::api::members::get_member::MemberSearchResult;
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
use rocket::State;
use rocket::serde::json::Json;
#[derive(Serialize, Deserialize, Queryable, Clone)]
pub struct CallerGroupPermissions {
pub(crate) permission_groups_core_edit: bool,

View File

@ -1,22 +1,23 @@
use rocket::State;
use crate::helper::settings::Settings;
use crate::helper::session_cookies::model::SessionCookie;
use crate::modules::api::model::api_outcome::{ApiErrorWrapper, ApiError};
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use rocket::serde::json::Json;
use crate::database::controller::members_groups::{add_member_to_group, add_member_to_state};
use crate::helper::translate_diesel_error::translate_diesel;
use crate::database::controller::members::check_access_to_resource;
use crate::modules::member_management::model::groups::{GroupUpdateData};
use rocket::State;
use crate::database::controller::groups::update_group_core_data;
use crate::database::controller::members::check_access_to_resource;
use crate::database::controller::members_groups::{add_member_to_group, add_member_to_state};
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
use crate::helper::translate_diesel_error::translate_diesel;
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
use crate::modules::member_management::model::groups::GroupUpdateData;
#[put("/api/groups/<group_id>/members/<member_id>", format = "json")]
pub fn put_member_in_group(settings: &State<Settings>, cookie: SessionCookie, group_id: String, member_id: String) -> Result<(), Json<ApiErrorWrapper>>{
pub fn put_member_in_group(settings: &State<Settings>, cookie: SessionCookie, group_id: String, member_id: String) -> Result<(), Json<ApiErrorWrapper>> {
let caller = parse_member_cookie(cookie.member)?;
let member_id = parse_uuid_string(member_id)?;
let group_id = parse_uuid_string(group_id)?;
if !check_access_to_resource(settings, caller.entity_id, group_id, crate::permissions::modules::member_management::groups::members::EDIT){
if !check_access_to_resource(settings, caller.entity_id, group_id, crate::permissions::modules::member_management::groups::members::EDIT) {
return Err(Json(ApiError::new(403, "Keine Berechtigung Gruppenmitglieder zu ändern!".to_string()).to_wrapper()))
}
@ -51,9 +52,9 @@ pub fn put_member_in_state(settings: &State<Settings>, cookie: SessionCookie, gr
let caller = parse_member_cookie(cookie.member)?;
let member_id = parse_uuid_string(member_id)?;
let group_id = parse_uuid_string(group_id)?;
let state_id = if(state_id == "null"){
let state_id = if state_id == "null" {
None
}else{
} else {
Some(parse_uuid_string(state_id)?)
};

View File

@ -1,14 +1,14 @@
use rocket::State;
use crate::helper::settings::Settings;
use crate::helper::session_cookies::model::SessionCookie;
use rocket::serde::json::Json;
use crate::modules::api::model::api_outcome::{ApiErrorWrapper, ApiError};
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use rocket::State;
use crate::database::controller::members::check_access_to_resource;
use crate::database::controller::permissions::{get_permission, get_role_permission_id, get_role_permission_context};
use crate::helper::translate_diesel_error::translate_diesel;
use crate::database::controller::permissions::{get_role_permission_context, get_role_permission_id};
use crate::database::controller::roles::get_roles_for_member;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
use crate::helper::translate_diesel_error::translate_diesel;
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use crate::modules::api::model::api_outcome::ApiErrorWrapper;
/// Check if caller has permission
///

View File

@ -1,14 +1,16 @@
use crate::database::controller::api_members::{get_member_search_result, get_member_search_result_by_name, get_member_list, get_member_list_count, MemberListEntry, MemberListEntryWithPermissions};
use std::convert::TryFrom;
use rocket::serde::json::Json;
use rocket::State;
use crate::database::controller::api_members::{get_member_list, get_member_list_count, get_member_search_result, get_member_search_result_by_name, MemberListEntryWithPermissions};
use crate::database::controller::api_members::MemberListOrder;
use crate::database::controller::groups::GroupMembership;
use crate::helper::session_cookies::model::SessionCookie;
use crate::helper::settings::Settings;
use crate::helper::translate_diesel_error::translate_diesel;
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_uuid_string};
use crate::modules::api::model::api_outcome::{ApiError, ApiErrorWrapper};
use rocket::State;
use rocket::serde::json::Json;
use crate::helper::translate_diesel_error::translate_diesel;
use crate::database::controller::api_members::MemberListOrder;
use std::convert::TryFrom;
use crate::database::controller::groups::GroupMembership;
#[derive(Serialize, Deserialize, Queryable, Clone)]
pub struct MemberSearchResult {
@ -132,7 +134,7 @@ pub fn api_member_list(
let mut groups_res: Vec<uuid::Uuid> = vec![];
for group in groups{
if(group != "") {
if group != "" {
match uuid::Uuid::parse_str(group) {
Ok(uuidres) => {
groups_res.push(uuidres);

View File

@ -1,16 +1,14 @@
use rocket::serde::json::Json;
use rocket::{State, request, Request, Response, response};
use crate::helper::settings::Settings;
use crate::database::controller::users::get_user_by_username;
use crate::database::controller::login_protection::{login_attempts_exceeded, login_attempts_usernames_exceeded, add_login_attempt, add_login_attempt_username};
use crate::database::controller::members::get_members_by_user_uuid;
use base64::decode;
use rocket::{request, Request, Response, response, State};
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome};
use rocket::http::{Status, ContentType, Header};
use std::str::{FromStr, Utf8Error};
use rocket::http::uncased::Uncased;
use std::borrow::Cow;
use rocket::response::Responder;
use base64::{decode, DecodeError};
use rocket::serde::json::Json;
use crate::database::controller::login_protection::{add_login_attempt_username, login_attempts_usernames_exceeded};
use crate::database::controller::members::get_members_by_user_uuid;
use crate::database::controller::users::get_user_by_username;
use crate::helper::settings::Settings;
#[derive(Queryable, Clone, Deserialize, Serialize)]
pub struct MatrixAuthRequest {
@ -107,22 +105,25 @@ pub fn matrix_check_credentials(
Some(pw) => pw
};
if argon2::verify_encoded(&password_hash, auth.user.password.as_ref()).unwrap() {
let member = match get_members_by_user_uuid(user.id, &settings).first(){
match get_members_by_user_uuid(user.id, &settings).first() {
Some(member) => {
return Json(MatrixAuthResponse {auth: Auth{
success: true,
mxid: Some(format!("@{}:drk.digital", id)),
profile: Some(Profile{
display_name: format!("{} {}", member.firstname, member.lastname)
})
}})
return Json(MatrixAuthResponse {
auth: Auth {
success: true,
mxid: Some(format!("@{}:drk.digital", id)),
profile: Some(Profile {
display_name: format!("{} {}", member.firstname, member.lastname)
}),
}
})
},
None => return Json(MatrixAuthResponse {auth: Auth {
None => return Json(MatrixAuthResponse {
auth: Auth {
success: false,
mxid: None,
profile: None
}})
};
}
} else {
add_login_attempt_username(settings, id);
return Json(MatrixAuthResponse {auth: Auth{

View File

View File

@ -7,4 +7,5 @@ pub mod resource_management;
pub mod welcome;
pub mod communicator;
pub mod event_management;
pub mod print;
pub mod print;
pub mod event_billing;

View File

@ -142,11 +142,28 @@ pub mod modules {
pub const EDIT: &'static str = "modules.scheduler.appointments.edit";
}
}
pub mod settings{
pub mod settings {
pub const VIEW: &'static str = "modules.settings.view";
pub mod role_permissions{
pub mod role_permissions {
pub const VIEW: &'static str = "modules.settings.role_permissions.view";
pub const EDIT: &'static str = "modules.settings.role_permissions.edit";
}
}
pub mod event_billing {
pub const VIEW: &'static str = "modules.event_billing.view";
pub const APPROVE: &'static str = "modules.event_billing.approve";
pub mod personnel {
pub const VIEW: &'static str = "modules.event_billing.personnel.view";
pub const EDIT: &'static str = "modules.event_billing.personnel.edit";
}
pub mod start_end_times {
pub const VIEW: &'static str = "modules.event_billing.start_end_times.view";
pub const EDIT: &'static str = "modules.event_billing.start_end_times.edit";
}
}
}

View File

@ -47,6 +47,17 @@ table! {
}
}
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
billing_states (state_id) {
state_id -> Text,
description -> Nullable<Text>,
final_approve -> Bool,
}
}
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
@ -119,6 +130,12 @@ table! {
template_id -> Uuid,
name -> Text,
event_id -> Uuid,
planned_start_time -> Nullable<Timestamptz>,
planned_end_time -> Nullable<Timestamptz>,
real_start_time -> Nullable<Timestamptz>,
real_end_time -> Nullable<Timestamptz>,
billing_rate_id -> Nullable<Uuid>,
billing_state_id -> Nullable<Text>,
}
}
@ -131,6 +148,8 @@ table! {
position_id -> Uuid,
taken_by -> Nullable<Uuid>,
position_instance_id -> Uuid,
real_start_time -> Nullable<Timestamptz>,
real_end_time -> Nullable<Timestamptz>,
}
}
@ -400,6 +419,34 @@ table! {
}
}
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
personnel_billing (position_instance_id) {
position_instance_id -> Uuid,
member_id -> Uuid,
fulfilled_time -> Int4,
money_for_time -> Numeric,
money_from_lump_sum -> Numeric,
total_money -> Numeric,
}
}
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
personnel_billing_rates (billing_rate_id) {
billing_rate_id -> Uuid,
name -> Text,
description -> Nullable<Text>,
active -> Bool,
payment_per_hour -> Numeric,
lump_sum -> Numeric,
}
}
table! {
use diesel::sql_types::*;
use diesel_geometry::sql_types::*;
@ -494,6 +541,7 @@ table! {
password -> Nullable<Text>,
email -> Text,
username -> Nullable<Text>,
timezone -> Nullable<Text>,
}
}
@ -533,6 +581,8 @@ joinable!(communication_targets -> communication_types (com_type));
joinable!(communication_targets -> entities (entity_id));
joinable!(cost_centres_members -> cost_centres (cost_centre_shortid));
joinable!(cost_centres_members -> members (member_entity_id));
joinable!(eu_instances -> billing_states (billing_state_id));
joinable!(eu_instances -> personnel_billing_rates (billing_rate_id));
joinable!(eu_positions -> entities (entity_id));
joinable!(eu_templates -> entities (entity_id));
joinable!(eu_vehicle_positions -> entities (entity_id));
@ -555,6 +605,8 @@ joinable!(members -> users (users_id));
joinable!(members_roles -> entities (member_id));
joinable!(members_roles -> roles (role_id));
joinable!(password_resets -> users (user_id));
joinable!(personnel_billing -> eu_position_instances (position_instance_id));
joinable!(personnel_billing -> members (member_id));
joinable!(qualifications -> qualification_categories (category));
joinable!(qualifications_members -> members (member_id));
joinable!(qualifications_members -> qualifications (qualification_id));
@ -574,6 +626,7 @@ allow_tables_to_appear_in_same_query!(
addresses_entities,
appointment_types,
appointments,
billing_states,
buildings,
communication_targets,
communication_types,
@ -602,6 +655,8 @@ allow_tables_to_appear_in_same_query!(
notification_types,
password_resets,
permissions,
personnel_billing,
personnel_billing_rates,
qualification_categories,
qualifications,
qualifications_members,