Feature: Send emails
This commit is contained in:
parent
4f906cb73b
commit
dcd5a4e052
|
@ -20,4 +20,6 @@ user_support_email = "support@einsatz.online"
|
|||
|
||||
[mail]
|
||||
from = "No Reply <noreply@localhost>"
|
||||
reply_to = "support@localhost"
|
||||
reply_to = "support@localhost"
|
||||
#type_id for emails in communication_types table
|
||||
communication_email_type_id = "a0a93b6e-f200-11ea-ad50-e86a6451da25"
|
|
@ -11,3 +11,15 @@ DELETE
|
|||
FROM permissions
|
||||
WHERE permission LIKE 'modules.communicator.email.send' ESCAPE '#';
|
||||
|
||||
DELETE
|
||||
FROM roles_permissions
|
||||
WHERE role_permission_id = '7d433bac-4f89-11eb-86be-e86a64d41d89';
|
||||
|
||||
DELETE
|
||||
FROM roles_permissions
|
||||
WHERE role_permission_id = '7d447c92-4f89-11eb-86be-e86a64d41d89';
|
||||
|
||||
DELETE
|
||||
FROM roles_permissions
|
||||
WHERE role_permission_id = '7d4202c8-4f89-11eb-86be-e86a64d41d89';
|
||||
|
||||
|
|
|
@ -8,3 +8,12 @@ VALUES ('modules.communicator.edit', null);
|
|||
INSERT INTO permissions (permission, description)
|
||||
VALUES ('modules.communicator.email.send', 'Permission to send email via communicator');
|
||||
|
||||
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
|
||||
VALUES ('admin', 'modules.communicator.view', DEFAULT);
|
||||
|
||||
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
|
||||
VALUES ('admin', 'modules.communicator.edit', DEFAULT);
|
||||
|
||||
INSERT INTO roles_permissions (role_id, permission_id, role_permission_id)
|
||||
VALUES ('admin', 'modules.communicator.email.send', DEFAULT);
|
||||
|
||||
|
|
|
@ -135,12 +135,6 @@ ul ul a {
|
|||
color: black;
|
||||
}
|
||||
|
||||
/*[readonly]:focus{
|
||||
outline: none !important;
|
||||
-webkit-appearance:none;
|
||||
box-shadow: none !important;
|
||||
}*/
|
||||
|
||||
label{
|
||||
color: dimgrey;
|
||||
}
|
||||
|
@ -188,4 +182,11 @@ th.rotate > div > span {
|
|||
.versiontag{
|
||||
padding: 10px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.quicksearch-overlay{
|
||||
position:absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
.quicksearch-overlay-li{
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
$( document ).ready(function() {
|
||||
Communicator.load_groups();
|
||||
Communicator.load_units();
|
||||
$("#send-email-submit").on("click", Communicator.send_email);
|
||||
$(".email-to-searchbar").on('keyup', function(event){
|
||||
if(event.key === "Enter"){
|
||||
$(".email-to-searchbar-overlay").children("li").first().trigger("click");
|
||||
$(".email-to-searchbar").val("");
|
||||
}
|
||||
QuickSearch.handle_typing(QuickSearch.SEARCHTYPE.MEMBER, ".email-to-searchbar", ".email-to-searchbar-overlay", "email-to-searchbar-overlay-result", "#email-to");
|
||||
});
|
||||
$(".email-to-searchbar-overlay").on("focusout", function (){
|
||||
$(".email-to-searchbar-overlay").hide();
|
||||
})
|
||||
$(".email-to-searchbar-overlay").on("mouseleave", function (){
|
||||
$(".email-to-searchbar-overlay").hide();
|
||||
});
|
||||
$(".email-to-search").on("mouseenter", function (){
|
||||
$(".email-to-searchbar-overlay").show();
|
||||
});
|
||||
$(".email-to-search").on("focusin", function (){
|
||||
$(".email-to-searchbar-overlay").html("");
|
||||
});
|
||||
$(".email-to-searchbar").on("click", function (){
|
||||
$(".email-to-searchbar").val("");
|
||||
});
|
||||
|
||||
$(".email-cc-searchbar").on('keyup', function(event){
|
||||
if(event.key === "Enter"){
|
||||
$(".email-cc-searchbar-overlay").children("li").first().trigger("click");
|
||||
$(".email-cc-searchbar").val("");
|
||||
}
|
||||
QuickSearch.handle_typing(QuickSearch.SEARCHTYPE.MEMBER, ".email-cc-searchbar", ".email-cc-searchbar-overlay", "email-cc-searchbar-overlay-result", "#email-cc");
|
||||
});
|
||||
$(".email-cc-searchbar-overlay").on("focusout", function (){
|
||||
$(".email-cc-searchbar-overlay").hide();
|
||||
})
|
||||
$(".email-cc-searchbar-overlay").on("mouseleave", function (){
|
||||
$(".email-cc-searchbar-overlay").hide();
|
||||
});
|
||||
$(".email-cc-search").on("mouseenter", function (){
|
||||
$(".email-cc-searchbar-overlay").show();
|
||||
});
|
||||
$(".email-cc-search").on("focusin", function (){
|
||||
$(".email-cc-searchbar-overlay").html("");
|
||||
});
|
||||
$(".email-cc-searchbar").on("click", function (){
|
||||
$(".email-cc-searchbar").val("");
|
||||
});
|
||||
|
||||
$(".email-bcc-searchbar").on('keyup', function(event){
|
||||
if(event.key === "Enter"){
|
||||
$(".email-bcc-searchbar-overlay").children("li").first().trigger("click");
|
||||
$(".email-bcc-searchbar").val("");
|
||||
}
|
||||
QuickSearch.handle_typing(QuickSearch.SEARCHTYPE.MEMBER, ".email-bcc-searchbar", ".email-bcc-searchbar-overlay", "email-bcc-searchbar-overlay-result", "#email-bcc");
|
||||
});
|
||||
$(".email-bcc-searchbar-overlay").on("focusout", function (){
|
||||
$(".email-bcc-searchbar-overlay").hide();
|
||||
})
|
||||
$(".email-bcc-searchbar-overlay").on("mouseleave", function (){
|
||||
$(".email-bcc-searchbar-overlay").hide();
|
||||
});
|
||||
$(".email-bcc-search").on("mouseenter", function (){
|
||||
$(".email-bcc-searchbar-overlay").show();
|
||||
});
|
||||
$(".email-bcc-search").on("focusin", function (){
|
||||
$(".email-bcc-searchbar-overlay").html("");
|
||||
});
|
||||
$(".email-bcc-searchbar").on("click", function (){
|
||||
$(".email-bcc-searchbar").val("");
|
||||
});
|
||||
});
|
||||
|
||||
QuickSearch = (function(){
|
||||
const SEARCHTYPE = {
|
||||
MEMBER: 1,
|
||||
};
|
||||
var remove_email = function(){
|
||||
$(this).parent().remove();
|
||||
};
|
||||
var handle_typing = function(type, input, overlay, overlayresult, rec){
|
||||
if(type === 1){
|
||||
$(overlay).html("");
|
||||
$.ajax({
|
||||
url: '/api/members/',
|
||||
type: 'GET',
|
||||
data: {
|
||||
"name": $(input).val()
|
||||
},
|
||||
contentType: 'application/json',
|
||||
success: function(data) {
|
||||
if(is_ok(data)) {
|
||||
$.each(data.members, function(index, value){
|
||||
$(overlay).append("<li data-entity-id=\""+value.entity_id+"\" data-firstname=\""+value.firstname+"\" data-lastname=\""+value.lastname+"\" class=\"quicksearch-overlay-li list-group-item "+overlayresult+"\"><span>"+value.firstname+" "+value.lastname+"</span></li>")
|
||||
});
|
||||
$("."+overlayresult).off("click").on("click", function(){
|
||||
$(rec).append("<span data-entity-id=\""+$(this).data("entity-id")+"\" class=\"badge badge-secondary\">"+$(this).data("firstname")+" "+$(this).data("lastname")+" <svg width=\"1.5em\" height=\"1.5em\" fill=\"currentColor\" class=\"remove-email\"><use xlink:href=\"/img/bootstrap-icons.svg#trash\"/></svg></span>");
|
||||
$(".remove-email").unbind("click").on("click", QuickSearch.remove_email);
|
||||
});
|
||||
}
|
||||
},
|
||||
timeout: 3000,
|
||||
error: function() {
|
||||
alert("Verbindung zum Server unterbrochen!");
|
||||
}
|
||||
});
|
||||
$(overlay).show();
|
||||
}else{
|
||||
console.log("Unkown searchtype: "+type);
|
||||
}
|
||||
};
|
||||
return{
|
||||
handle_typing: handle_typing,
|
||||
remove_email: remove_email,
|
||||
SEARCHTYPE: SEARCHTYPE,
|
||||
}
|
||||
})();
|
||||
|
||||
Communicator = (function(){
|
||||
var load_groups = function(){
|
||||
$.ajax({
|
||||
url: '/api/groups?with_caller_permission=modules.communicator.email.send',
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
success: function(data) {
|
||||
if(is_ok(data)) {
|
||||
$(data.groups).each(function(){
|
||||
$(".group_selection_list").append("<span class=\"form-check group_selection_group\">\n" +
|
||||
" <input type=\"checkbox\" class=\"form-check-input selected_group\" id=\""+this.group_id+"\" data-group-id=\""+this.group_id+"\">\n" +
|
||||
" <label class=\"form-check-label\" for=\""+this.group_id+"\">"+this.name+"</label>\n" +
|
||||
" </span>")
|
||||
})
|
||||
}
|
||||
},
|
||||
timeout: 3000,
|
||||
error: function() {
|
||||
alert("Verbindung zum Server unterbrochen!");
|
||||
}
|
||||
});
|
||||
};
|
||||
var load_units = function(){
|
||||
$.ajax({
|
||||
url: '/api/units?with_caller_permission=modules.communicator.email.send',
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
success: function(data) {
|
||||
if(is_ok(data)) {
|
||||
$(data).each(function(){
|
||||
$(".unit_selection_list").append("<span class=\"form-check unit_selection_group\">\n" +
|
||||
" <input type=\"checkbox\" class=\"form-check-input selected_unit\" id=\""+this.unit_id+"\" data-unit-id=\""+this.unit_id+"\" name=\"selected_groups\">" +
|
||||
" <label class=\"form-check-label\" for=\""+this.unit_id+"\">"+this.name+"</label>\n" +
|
||||
" </span>")
|
||||
})
|
||||
}
|
||||
},
|
||||
timeout: 3000,
|
||||
error: function() {
|
||||
alert("Verbindung zum Server unterbrochen!");
|
||||
}
|
||||
});
|
||||
};
|
||||
var send_email = function(){
|
||||
var email = $();
|
||||
|
||||
if($("#email-to > span").length > 0){
|
||||
email.to_members = [];
|
||||
$("#email-to > span").each(function(){email.to_members.push($(this).data("entity-id"))});
|
||||
}
|
||||
if($("#email-cc > span").length > 0){
|
||||
email.cc_members = [];
|
||||
$("#email-cc > span").each(function(){email.cc_members.push($(this).data("entity-id"))});
|
||||
}
|
||||
if($("#email-bcc > span").length > 0){
|
||||
email.bcc_members = [];
|
||||
$("#email-bcc > span").each(function(){email.bcc_members.push($(this).data("entity-id"))});
|
||||
}
|
||||
if($("#email-subject").val()){
|
||||
email.subject = $("#email-subject").val();
|
||||
}else{
|
||||
alert("Email konnte nicht gesendet werden: Es muss ein Betreff angegeben werden!");
|
||||
return;
|
||||
}
|
||||
if($("#email-body").val()){
|
||||
email.body = $("#email-body").val();
|
||||
}else{
|
||||
alert("Email konnte nicht gesendet werden: Die Nachricht darf nicht leer sein!");
|
||||
return;
|
||||
}
|
||||
|
||||
email.selected_groups = [];
|
||||
email.selected_units = [];
|
||||
|
||||
$(".selected_group").each(function(){
|
||||
if($(this).prop('checked')){
|
||||
email.selected_groups.push($(this).data("group-id"));
|
||||
}
|
||||
});
|
||||
|
||||
$(".selected_units").each(function(){
|
||||
if($(this).prop('checked')){
|
||||
email.selected_units.push($(this).data("unit-id"));
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: '/api/communicator/email',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(email),
|
||||
success: function(data) {
|
||||
if(is_ok(data)) {
|
||||
$("#email-to").html("");
|
||||
$("#email-cc").html("");
|
||||
$("#email-bcc").html("");
|
||||
$(".selected_group").prop("checked", false);
|
||||
$(".selected_unit").prop("checked", false);
|
||||
$("#email-subject").val("");
|
||||
$("#email-body").val("");
|
||||
$(".alert").append("<div class=\"alert alert-success\" role=\"alert\">Die Email wird nun versendet!</div>");
|
||||
}
|
||||
},
|
||||
timeout: 3000,
|
||||
error: function() {
|
||||
alert("Verbindung zum Server unterbrochen!");
|
||||
}
|
||||
});
|
||||
};
|
||||
var to_searchbar = function(){
|
||||
var searchterm = $("#email-to-searchbar").val();
|
||||
console.log(searchterm);
|
||||
};
|
||||
return{
|
||||
load_groups: load_groups,
|
||||
load_units: load_units,
|
||||
send_email: send_email,
|
||||
to_searchbar: to_searchbar,
|
||||
};
|
||||
})();
|
|
@ -12,7 +12,7 @@ $( document ).ready(function() {
|
|||
$("#searchbar").on("click", Searchbar.searchbar_onclick);
|
||||
$("#searchbar").on("keyup", Searchbar.searchbar_typing);
|
||||
$("#searchbar").bind("paste", Searchbar.searchbar_typing);
|
||||
$("#searchbar").bind("cut", Searchbar.sWearchbar_typing);
|
||||
$("#searchbar").bind("cut", Searchbar.searchbar_typing);
|
||||
$("#searchbar").on("mouseenter", function (){
|
||||
$(".search-result-overlay").show();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
{{> header }}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="wrapper">
|
||||
{{> sidebar }}
|
||||
<div id="content">
|
||||
{{> searchbar}}
|
||||
<hr>
|
||||
<span class="alert"></span>
|
||||
<div class="col">
|
||||
<div class="card bg-light">
|
||||
<div class="card-header">E-Mail versenden</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="form-group row">
|
||||
<label for="email-to" class="col-sm-1 col-form-label">An: </label>
|
||||
<div class="col-sm-11">
|
||||
<span id="email-to"></span>
|
||||
<div class="email-to-search">
|
||||
<input type="text" class="form-control email-to-searchbar" value="{{to}}">
|
||||
<ul class="list-group"><span class="quicksearch-overlay email-to-searchbar-overlay" style="display: none"></span></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="email-cc" class="col-sm-1 col-form-label">Cc: </label>
|
||||
<div class="col-sm-11">
|
||||
<span id="email-cc"></span>
|
||||
<div class="email-cc-search">
|
||||
<input type="text" class="form-control email-cc-searchbar" value="{{cc}}">
|
||||
<ul class="list-group"><span class="quicksearch-overlay email-cc-searchbar-overlay" style="display: none"></span></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="email-bcc" class="col-sm-1 col-form-label">Bcc: </label>
|
||||
<div class="col-sm-11">
|
||||
<span id="email-bcc"></span>
|
||||
<div class="email-bcc-search">
|
||||
<input type="text" class="form-control email-bcc-searchbar" value="{{bcc}}">
|
||||
<ul class="list-group"><span class="quicksearch-overlay email-bcc-searchbar-overlay" style="display: none"></span></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>Hinweis: E-Mails an Gruppen werden aus Gründen des Datenschutzes immer nur im BCC gesendet.</p>
|
||||
<div class="filter">
|
||||
<ul class="nav nav-tabs" id="filterTab" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="group-tab" data-toggle="tab" href="#group" role="tab" aria-controls="group" aria-selected="true">Gruppen</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="units-tab" data-toggle="tab" href="#units" role="tab" aria-controls="units" aria-selected="false">Einsatzeinheiten</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="filterTabContent">
|
||||
<div class="tab-pane fade show active" id="group" role="tabpanel" aria-labelledby="group-tab">
|
||||
<div class="group_selection_list">
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="units" role="tabpanel" aria-labelledby="units-tab">
|
||||
<div class="unit_selection_list">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="email-subject" class="col-sm-1 col-form-label">Betreff: </label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" id="email-subject" name="email-subject">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email-body" class="col-form-label">Nachricht: </label>
|
||||
<textarea class="form-control" id="email-body" name="email-body" rows="10"></textarea>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="send-email-submit" style="float: right;">E-Mail senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{> footer }}
|
|
@ -38,6 +38,16 @@
|
|||
{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if sidebar.communicator.visible}}
|
||||
<li>
|
||||
<a {{#if sidebar.communicator.active}}class="sidebar_entry_active"{{/if}} href="/portal/communicator/email">Kommunikator</a>
|
||||
{{#if sidebar.communicator.active}}
|
||||
<ul>
|
||||
<li><a href="/portal/communicator/email">Email</a></li>
|
||||
</ul>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if sidebar.resource_management.visible}}
|
||||
<li>
|
||||
<a {{#if sidebar.resource_management.active}}class="sidebar_entry_active"{{/if}} href="/portal/rm">Ressourcen</a>
|
||||
|
|
|
@ -1 +1 @@
|
|||
unknown
|
||||
v0.1-16-g4f906cb
|
||||
|
|
|
@ -2,10 +2,10 @@ use crate::database::controller::connector::establish_connection;
|
|||
use crate::helper::settings::Settings;
|
||||
use crate::modules::api::members::member_communication::{CommunicationTarget, CommunicationType};
|
||||
use crate::schema::communication_targets::dsl::{communication_targets, target_id};
|
||||
use diesel::query_dsl::filter_dsl::FilterDsl;
|
||||
use diesel::{ExpressionMethods, RunQueryDsl};
|
||||
use rocket::State;
|
||||
use diesel::query_dsl::select_dsl::SelectDsl;
|
||||
use crate::schema::groups_entities::dsl::groups_entities;
|
||||
use diesel::{ExpressionMethods, RunQueryDsl, QueryDsl, JoinOnDsl};
|
||||
use crate::schema::units_members::dsl::units_members;
|
||||
|
||||
/// Updates communication target via API.
|
||||
/// Will only change field if field is Some, will ignore field if it's None.
|
||||
|
@ -49,4 +49,56 @@ pub fn get_member_communication_types(settings: &State<Settings>) -> Result<Vec<
|
|||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns list of all email addresses belonging to the specified member
|
||||
pub fn get_member_email_addresses(settings: &State<Settings>, member_id: uuid::Uuid) -> Result<Vec<String>, diesel::result::Error>{
|
||||
use crate::schema::communication_targets::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
let email_type_id = uuid::Uuid::parse_str(&settings.mail.communication_email_type_id).expect("No valid communication_email_type_id set in config! Cant send emails!");
|
||||
|
||||
let data: Result<Vec<String>, diesel::result::Error> = communication_targets.select((value)).filter(com_type.eq(email_type_id)).filter(entity_id.eq(member_id)).load(&connection);
|
||||
match data{
|
||||
Ok(data) => Ok(data),
|
||||
Err(e) => {
|
||||
error!("Couldn't get email addresses for member: {}", e);
|
||||
Err(e)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_group_email_addresses(settings: &State<Settings>, group_id2: uuid::Uuid) -> Result<Vec<String>, diesel::result::Error>{
|
||||
use crate::schema::communication_targets::dsl::*;
|
||||
use crate::schema::groups_entities::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
let email_type_id = uuid::Uuid::parse_str(&settings.mail.communication_email_type_id).expect("No valid communication_email_type_id set in config! Cant send emails!");
|
||||
|
||||
let data: Result<Vec<String>, diesel::result::Error> = communication_targets.distinct().left_join(groups_entities.on(crate::schema::groups_entities::entity_id.eq(crate::schema::communication_targets::entity_id))).filter(group_id.eq(group_id2)).select((value)).filter(com_type.eq(email_type_id)).load(&connection);
|
||||
match data{
|
||||
Ok(data) => Ok(data),
|
||||
Err(e) => {
|
||||
error!("Couldn't get email addresses for group: {}", e);
|
||||
Err(e)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_unit_email_addresses(settings: &State<Settings>,unit_id2: uuid::Uuid) -> Result<Vec<String>, diesel::result::Error>{
|
||||
use crate::schema::communication_targets::dsl::*;
|
||||
use crate::schema::units_members::dsl::*;
|
||||
|
||||
let connection = establish_connection(settings);
|
||||
let email_type_id = uuid::Uuid::parse_str(&settings.mail.communication_email_type_id).expect("No valid communication_email_type_id set in config! Cant send emails!");
|
||||
|
||||
let data: Result<Vec<String>, diesel::result::Error> = communication_targets.distinct().left_join(units_members.on(crate::schema::units_members::member_id.eq(crate::schema::communication_targets::entity_id))).filter(unit_id.eq(unit_id2)).select((value)).filter(com_type.eq(email_type_id)).load(&connection);
|
||||
match data{
|
||||
Ok(data) => Ok(data),
|
||||
Err(e) => {
|
||||
error!("Couldn't get email addresses for unit: {}", e);
|
||||
Err(e)
|
||||
},
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ pub struct Application {
|
|||
pub struct Mail{
|
||||
pub from : String,
|
||||
pub reply_to : String,
|
||||
pub communication_email_type_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use crate::helper::sitebuilder::model::alerts::{Alert, AlertClass};
|
||||
use crate::helper::sitebuilder::model::sidebar::{
|
||||
EventManagement, MemberManagement, ResourceManagement, Sidebar, Summary,
|
||||
};
|
||||
use crate::helper::sitebuilder::model::sidebar::{EventManagement, MemberManagement, ResourceManagement, Sidebar, Summary, Communicator};
|
||||
use crate::modules::member_management::model::member::Member;
|
||||
|
||||
impl Alert {
|
||||
|
@ -16,7 +14,7 @@ impl Sidebar {
|
|||
firstname: member.firstname.clone(),
|
||||
lastname: member.lastname.clone(),
|
||||
summary: Summary {
|
||||
visible: member.has_permission("modules.dashboard.view".to_string()),
|
||||
visible: member.has_permission(crate::permissions::modules::dashboard::VIEW.to_string()),
|
||||
active: false,
|
||||
},
|
||||
resource_management: ResourceManagement {
|
||||
|
@ -31,6 +29,10 @@ impl Sidebar {
|
|||
visible: member.has_permission("modules.event_management.view".to_string()),
|
||||
active: false,
|
||||
},
|
||||
communicator: Communicator {
|
||||
visible: member.has_permission("modules.communicator.view".to_string()),
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ pub struct Sidebar {
|
|||
pub resource_management: ResourceManagement,
|
||||
pub member_management: MemberManagement,
|
||||
pub event_management: EventManagement,
|
||||
pub communicator: Communicator,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
@ -31,3 +32,9 @@ pub struct EventManagement {
|
|||
pub visible: bool,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Communicator {
|
||||
pub visible: bool,
|
||||
pub active: bool,
|
||||
}
|
|
@ -133,6 +133,7 @@ fn main() {
|
|||
modules::api::members::member_qualification::api_member_qualifications_read,
|
||||
modules::api::groups::create::create_group,
|
||||
modules::api::groups::delete::delete_groups,
|
||||
modules::api::groups::read::get_groups,
|
||||
modules::api::groups::read::read_group,
|
||||
modules::api::groups::update::put_member_in_group,
|
||||
modules::api::groups::delete::delete_member_from_group,
|
||||
|
@ -143,10 +144,12 @@ fn main() {
|
|||
modules::api::users::create::create_user,
|
||||
modules::api::users::delete::delete_user,
|
||||
modules::api::users::update::update_user,
|
||||
modules::api::communicator::create::create_email,
|
||||
modules::member_management::view::personal_profile::member_management_personal_profile_get,
|
||||
modules::member_management::view::personal_profile::member_management_personal_profile_post,
|
||||
modules::member_management::view::create_member::member_management_add_member,
|
||||
modules::member_management::view::create_member::member_management_add_member_post,
|
||||
modules::communicator::send_email::communicator_email_get,
|
||||
],
|
||||
)
|
||||
.mount("/css", StaticFiles::from("resources/css"))
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
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 rocket_contrib::json::Json;
|
||||
use crate::modules::api::member_management::controller::parser::parse_member_cookie;
|
||||
use crate::helper::mail_queue::queue::{Mail, MailQueue};
|
||||
use crate::helper::mail_queue::worker::send_mail;
|
||||
use std::sync::{Arc, PoisonError, RwLockWriteGuard};
|
||||
use std::collections::VecDeque;
|
||||
use crate::database::controller::api_communication_targets::{get_member_email_addresses, get_group_email_addresses, get_unit_email_addresses};
|
||||
use diesel::result::Error;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize)]
|
||||
pub struct ApiEmail{
|
||||
pub(crate) to: Option<Vec<String>>,
|
||||
pub(crate) to_members: Option<Vec<uuid::Uuid>>,
|
||||
pub(crate) cc: Option<Vec<String>>,
|
||||
pub(crate) cc_members: Option<Vec<uuid::Uuid>>,
|
||||
pub(crate) bcc: Option<Vec<String>>,
|
||||
pub(crate) bcc_members: Option<Vec<uuid::Uuid>>,
|
||||
pub(crate) reply_to: Option<String>,
|
||||
pub(crate) selected_groups: Option<Vec<uuid::Uuid>>,
|
||||
pub(crate) selected_units: Option<Vec<uuid::Uuid>>,
|
||||
pub(crate) subject: String,
|
||||
pub(crate) body: String,
|
||||
}
|
||||
|
||||
#[post("/api/communicator/email", format = "json", data = "<mail>")]
|
||||
pub fn create_email(mq: State<Arc<MailQueue>>, settings: State<Settings>, cookie: SessionCookie, mail: Json<ApiEmail>) -> Result<(), Json<ApiErrorWrapper>>{
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
if !caller.has_permission(crate::permissions::modules::communicator::email::SEND.to_string()){
|
||||
return Err(Json(ApiError::new(403, "Keine Berechtigung Email zu versenden!".to_string()).to_wrapper()));
|
||||
}
|
||||
|
||||
//TODO: Check for each receiver if caller has permission to send email
|
||||
|
||||
let mail = mail.into_inner();
|
||||
|
||||
let mut to: Vec<String> = vec![];
|
||||
let mut cc: Vec<String> = vec![];
|
||||
let mut bcc: Vec<String> = vec![];
|
||||
|
||||
match mail.to{
|
||||
Some(mut mail) => {
|
||||
to.append(mail.as_mut())
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
match mail.to_members{
|
||||
Some(members) => {
|
||||
for member_id in members{
|
||||
match get_member_email_addresses(&settings, member_id){
|
||||
Ok(mut addresses) => {
|
||||
to.append(addresses.as_mut());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
match mail.cc{
|
||||
Some(mut mail) => {
|
||||
cc.append(mail.as_mut())
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
match mail.cc_members{
|
||||
Some(members) => {
|
||||
for member_id in members{
|
||||
match get_member_email_addresses(&settings, member_id){
|
||||
Ok(mut addresses) => {
|
||||
cc.append(addresses.as_mut());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
match mail.bcc{
|
||||
Some(mut mail) => {
|
||||
bcc.append(mail.as_mut())
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
match mail.bcc_members{
|
||||
Some(members) => {
|
||||
for member_id in members{
|
||||
match get_member_email_addresses(&settings, member_id){
|
||||
Ok(mut addresses) => {
|
||||
bcc.append(addresses.as_mut());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
match mail.selected_groups{
|
||||
None => {}
|
||||
Some(groups) => {
|
||||
for group in groups{
|
||||
match get_group_email_addresses(&settings, group){
|
||||
Ok(mut emails) => {
|
||||
bcc.append(emails.as_mut());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match mail.selected_units{
|
||||
None => {}
|
||||
Some(units) => {
|
||||
for unit in units{
|
||||
match get_unit_email_addresses(&settings, unit){
|
||||
Ok(mut emails) => {
|
||||
bcc.append(emails.as_mut());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if to.is_empty() && cc.is_empty() && bcc.is_empty(){
|
||||
return Err(Json(ApiError::new(422, "Es muss mindestens ein Empfänger angegeben werden!".to_string()).to_wrapper()))
|
||||
}
|
||||
|
||||
let composed_mail = Mail::new(settings.mail.from.clone(), to,mail.subject,cc,bcc,mail.reply_to,mail.body,None);
|
||||
|
||||
match mq.add_mail(composed_mail){
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(Json(ApiError::new(500, "Couldn't add mail to mail queue!".to_string()).to_wrapper()))
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
pub mod create;
|
|
@ -60,10 +60,7 @@ pub fn create_group(
|
|||
) {
|
||||
Ok(()) => Ok(Json(group)),
|
||||
Err(e) => {
|
||||
return Err(Json(
|
||||
ApiError::new(500, "Es ist ein Datenbankfehler aufgetreten.".to_string())
|
||||
.to_wrapper(),
|
||||
))
|
||||
return Err(translate_diesel(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use crate::database::controller::groups::get_group;
|
||||
use crate::database::controller::groups_permissions::{
|
||||
get_group_permission_for_role, get_group_role_permissions,
|
||||
};
|
||||
use crate::database::controller::groups::{get_group, get_raw_groups};
|
||||
use crate::database::controller::groups_permissions::{get_group_permission_for_role, get_group_role_permissions, check_role_has_permission_on_entity};
|
||||
use crate::database::controller::members::check_member_has_access;
|
||||
use crate::database::controller::members_groups::get_member_search_results_in_group;
|
||||
use crate::database::model::groups::RawGroup;
|
||||
|
@ -34,6 +32,49 @@ pub struct DetailedGroup {
|
|||
pub(crate) caller_permissions: CallerGroupPermissions,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetGroupsResult{
|
||||
pub(crate) groups: Vec<RawGroup>
|
||||
}
|
||||
|
||||
/// Api method to get list of groups
|
||||
/// Filters:
|
||||
/// * with_caller_permission (String): Only show groups where caller has permission x
|
||||
#[get("/api/groups?<with_caller_permission>", format = "json")]
|
||||
pub fn get_groups(settings: State<Settings>, cookie: SessionCookie, with_caller_permission: Option<String>) -> Result<Json<GetGroupsResult>, Json<ApiErrorWrapper>>{
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
if !caller.has_permission(crate::permissions::modules::member_management::groups::VIEW.to_string()) {
|
||||
return Err(Json(ApiError::new(403, "Keine Berechtigung, Gruppen abzurufen!".to_string()).to_wrapper()));
|
||||
}
|
||||
|
||||
let groups = match get_raw_groups(&settings){
|
||||
Ok(groups) => groups,
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
};
|
||||
|
||||
let mut groups_with_permission : Vec<RawGroup> = vec![];
|
||||
|
||||
for group in groups{
|
||||
match &with_caller_permission {
|
||||
Some(caller_permission) => {
|
||||
if check_member_has_access(&settings, caller.entity_id, group.group_id, &caller_permission){
|
||||
groups_with_permission.push(group);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
groups_with_permission.push(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let res = GetGroupsResult{
|
||||
groups: groups_with_permission
|
||||
};
|
||||
|
||||
Ok(Json(res))
|
||||
|
||||
}
|
||||
|
||||
#[get("/api/groups/<entity_id>", format = "json")]
|
||||
pub fn read_group(
|
||||
settings: State<Settings>,
|
||||
|
|
|
@ -3,4 +3,5 @@ pub mod member_management;
|
|||
pub mod members;
|
||||
pub mod model;
|
||||
pub mod units;
|
||||
pub mod users;
|
||||
pub mod users;
|
||||
pub mod communicator;
|
|
@ -9,15 +9,30 @@ use crate::database::controller::members::check_member_has_access;
|
|||
use crate::database::controller::units::get_units;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
|
||||
#[get("/api/units", format = "json")]
|
||||
#[get("/api/units?<with_caller_permission>", format = "json")]
|
||||
pub fn read_unit_list(
|
||||
settings: State<Settings>,
|
||||
cookie: SessionCookie,
|
||||
with_caller_permission: Option<String>
|
||||
) -> Result<Json<Vec<RawUnit>>, Json<ApiErrorWrapper>> {
|
||||
let _caller = parse_member_cookie(cookie.member)?;
|
||||
let caller = parse_member_cookie(cookie.member)?;
|
||||
|
||||
match get_units(&settings){
|
||||
Ok(units) => Ok(Json(units)),
|
||||
Err(e) => Err(translate_diesel(e))
|
||||
let units = match get_units(&settings){
|
||||
Ok(units) => units,
|
||||
Err(e) => return Err(translate_diesel(e))
|
||||
};
|
||||
|
||||
|
||||
match with_caller_permission{
|
||||
None => Ok(Json(units)),
|
||||
Some(permission) => {
|
||||
let mut unit_list : Vec<RawUnit> = vec![];
|
||||
for unit in units{
|
||||
if check_member_has_access(&settings, caller.entity_id, unit.unit_id, &permission){
|
||||
unit_list.push(unit);
|
||||
}
|
||||
}
|
||||
Ok(Json(unit_list))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
pub mod send_email;
|
|
@ -0,0 +1,57 @@
|
|||
use rocket_contrib::templates::Template;
|
||||
use rocket::http::Status;
|
||||
use rocket::State;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::sitebuilder::model::general::{Header, Footer, Stylesheet, Script};
|
||||
use crate::helper::sitebuilder::model::sidebar::Sidebar;
|
||||
use crate::helper::sitebuilder::model::alerts::Alert;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CommunicatorEmailTemplate{
|
||||
pub header: Header,
|
||||
pub footer: Footer,
|
||||
pub sidebar: Sidebar,
|
||||
pub alert: Option<Alert>,
|
||||
pub to: Option<String>,
|
||||
pub cc: Option<String>,
|
||||
pub bcc: Option<String>
|
||||
}
|
||||
|
||||
#[get("/portal/communicator/email?<to>&<cc>&<bcc>")]
|
||||
pub fn communicator_email_get(cookie: SessionCookie, to: Option<String>, cc: Option<String>, bcc: Option<String>) -> Result<Template, Status> {
|
||||
let caller = match cookie.member {
|
||||
//Unwraps member from cookie or send user to login if no member specified (user skipped member selection)
|
||||
Some(member) => member,
|
||||
None => return Err(Status::Unauthorized),
|
||||
};
|
||||
|
||||
if !caller.has_permission(crate::permissions::modules::communicator::VIEW.to_string()){
|
||||
return Err(Status::Forbidden)
|
||||
}
|
||||
|
||||
let header = Header{
|
||||
html_language: "de".to_string(),
|
||||
site_title: "Email versenden".to_string(),
|
||||
stylesheets: vec![Stylesheet {
|
||||
path: "/css/errms.css".to_string(),
|
||||
}]
|
||||
};
|
||||
|
||||
let footer = Footer { scripts: vec![Script{ path: "/js/communicator.js".to_string() }] };
|
||||
|
||||
let mut sidebar = Sidebar::new(caller);
|
||||
sidebar.communicator.active=true;
|
||||
|
||||
let templ = CommunicatorEmailTemplate{
|
||||
header,
|
||||
footer,
|
||||
sidebar,
|
||||
alert: None,
|
||||
to,
|
||||
cc,
|
||||
bcc
|
||||
};
|
||||
|
||||
Ok(Template::render("module_communicator_email", templ))
|
||||
}
|
|
@ -16,7 +16,7 @@ pub fn render(settings: &State<Settings>, caller: Member) -> Result<Template, St
|
|||
|
||||
let header = Header {
|
||||
html_language: "de".to_string(),
|
||||
site_title: "Mitgliedprofil".to_string(),
|
||||
site_title: "Gruppen".to_string(),
|
||||
stylesheets: vec![Stylesheet {
|
||||
path: "/css/errms.css".to_string(),
|
||||
}],
|
||||
|
|
|
@ -12,7 +12,6 @@ use crate::database::controller::create_member::create_member;
|
|||
#[get("/portal/mm/add_member")]
|
||||
pub fn member_management_add_member(
|
||||
cookie: SessionCookie,
|
||||
settings: State<Settings>,
|
||||
) -> Result<Template, Status> {
|
||||
let caller = match cookie.member {
|
||||
//Unwraps member from cookie or send user to login if no member specified (user skipped member selection)
|
||||
|
|
|
@ -5,3 +5,4 @@ pub mod member_management;
|
|||
pub mod operation_management;
|
||||
pub mod resource_management;
|
||||
pub mod welcome;
|
||||
pub mod communicator;
|
|
@ -15,6 +15,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
addresses_entities (address_id, entitiy_id) {
|
||||
address_id -> Uuid,
|
||||
|
@ -24,6 +25,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
buildings (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -47,6 +49,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
communication_types (type_id) {
|
||||
type_id -> Uuid,
|
||||
|
@ -56,6 +59,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
cost_centres (short_id) {
|
||||
short_id -> Int4,
|
||||
|
@ -75,6 +79,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
entities (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -83,6 +88,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
groups (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -93,6 +99,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
groups_entities (group_id, entity_id) {
|
||||
group_id -> Uuid,
|
||||
|
@ -102,6 +109,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
license_categories (name) {
|
||||
name -> Text,
|
||||
|
@ -111,6 +119,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
licenses_members (member_id, license_name) {
|
||||
member_id -> Uuid,
|
||||
|
@ -121,6 +130,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
login_attempts (id) {
|
||||
id -> Uuid,
|
||||
|
@ -154,6 +164,9 @@ table! {
|
|||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
members_roles (member_id, role_id) {
|
||||
member_id -> Uuid,
|
||||
role_id -> Text,
|
||||
|
@ -162,6 +175,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
password_resets (token) {
|
||||
token -> Text,
|
||||
|
@ -172,6 +186,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
permissions (permission) {
|
||||
permission -> Text,
|
||||
|
@ -181,6 +196,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
qualification_categories (id) {
|
||||
id -> Uuid,
|
||||
|
@ -191,6 +207,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
qualifications (id) {
|
||||
id -> Uuid,
|
||||
|
@ -202,6 +219,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
qualifications_members (member_id, qualification_id) {
|
||||
member_id -> Uuid,
|
||||
|
@ -211,6 +229,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
roles (id) {
|
||||
id -> Text,
|
||||
|
@ -220,6 +239,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
roles_permissions (role_permission_id) {
|
||||
role_id -> Text,
|
||||
|
@ -230,6 +250,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
roles_permissions_context (role_permission_id, entity) {
|
||||
role_permission_id -> Uuid,
|
||||
|
@ -239,6 +260,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
units (unit_id) {
|
||||
unit_id -> Uuid,
|
||||
|
@ -248,6 +270,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
units_members (unit_id, member_id) {
|
||||
unit_id -> Uuid,
|
||||
|
@ -258,6 +281,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
users (id) {
|
||||
id -> Uuid,
|
||||
|
@ -268,6 +292,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
vehicles (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
|
Loading…
Reference in New Issue