Commit acad05e7 authored by Giorgos Korfiatis's avatar Giorgos Korfiatis

Merge branch 'feature-admin-change-email' into develop

parents a29b9540 80dcedd2
......@@ -45,6 +45,10 @@ Admin
* Improve the displayed data in the tables
* Display more information regarding the enabled authentication providers
* Display pending modifications of projects
* Add the action 'modify user e-mail' in the Admin interface
* Display data related with the modification of users' e-mails like 'e-mail
pending verification', 'e-mail change requested at', 'initially accepted
e-mail'
.. _Changelog-0.17:
......
# Copyright (C) 2010-2014 GRNET S.A.
# Copyright (C) 2010-2016 GRNET S.A.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -34,6 +34,8 @@ class AdminAction(object):
caution_level: Indication of how much careful the user should be:
Accepted values: none, warning, dangerous.
description: A short text that describes an action
data_keys: A list with the extra data dict keys required for the
action
Methods:
f: The function that will trigger once an action is
......@@ -43,7 +45,8 @@ class AdminAction(object):
"""
def __init__(self, name, target, f, c=None, allowed_groups='admin',
karma='neutral', caution_level='none', description=''):
karma='neutral', caution_level='none', description='',
data_keys=[]):
"""Initialize the AdminAction class."""
self.name = name
self.description = description
......@@ -52,6 +55,7 @@ class AdminAction(object):
self.caution_level = caution_level
self.allowed_groups = allowed_groups
self.f = f
self.data_keys = data_keys
if c:
self.check = c
......
# Copyright (C) 2010-2014 GRNET S.A.
# Copyright (C) 2010-2016 GRNET S.A.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -54,7 +54,7 @@ class GroupJSONView(AdminJSONView):
JSON_CLASS = GroupJSONView
def do_action(request, op, id):
def do_action(request, op, id, data):
raise AdminHttp404("There are no actions for Groups")
......
# Copyright (C) 2010-2014 GRNET S.A.
# Copyright (C) 2010-2016 GRNET S.A.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -60,7 +60,7 @@ def generate_actions():
actions['reassign'] = IPAction(name='Reassign to project', f=noop,
karma='neutral', caution_level='dangerous',)
actions['contact'] = IPAction(name='Send e-mail', f=send_admin_email,)
actions['contact'] = IPAction(name='Send e‑mail', f=send_admin_email,)
update_actions_rbac(actions)
......
# Copyright (C) 2010-2014 GRNET S.A.
# Copyright (C) 2010-2016 GRNET S.A.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -155,7 +155,7 @@ JSON_CLASS = IPJSONView
@transaction.commit_on_success
@has_permission_or_403(cached_actions)
def do_action(request, op, id):
def do_action(request, op, id, data):
"""Apply the requested action on the specified ip."""
if op == "contact":
user = get_user_or_404(id)
......
# Copyright (C) 2010-2014 GRNET S.A.
# Copyright (C) 2010-2016 GRNET S.A.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -85,7 +85,7 @@ def generate_actions():
karma='neutral',
caution_level='dangerous',)
actions['contact'] = NetworkAction(name='Send e-mail', f=send_admin_email,
actions['contact'] = NetworkAction(name='Send e‑mail', f=send_admin_email,
c=check_network_action("CONTACT"),)
update_actions_rbac(actions)
......
......@@ -146,7 +146,7 @@ JSON_CLASS = NetworkJSONView
@transaction.commit_on_success
@has_permission_or_403(cached_actions)
def do_action(request, op, id):
def do_action(request, op, id, data):
"""Apply the requested action on the specified network."""
if op == "contact":
user = get_user_or_404(id)
......
# Copyright (C) 2010-2014 GRNET S.A.
# Copyright (C) 2010-2016 GRNET S.A.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -107,7 +107,7 @@ def generate_actions():
karma='good',
caution_level='warning',)
actions['contact'] = ProjectAction(name='Send e-mail', f=send_admin_email,)
actions['contact'] = ProjectAction(name='Send e‑mail', f=send_admin_email,)
update_actions_rbac(actions)
......
......@@ -199,7 +199,7 @@ JSON_CLASS = ProjectJSONView
@has_permission_or_403(cached_actions)
@transaction.commit_on_success
def do_action(request, op, id):
def do_action(request, op, id, data):
"""Apply the requested action on the specified user."""
if op == "contact":
user = get_user_or_404(id)
......
# Copyright (C) 2010-2014 GRNET S.A.
# Copyright (C) 2010-2016 GRNET S.A.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -21,6 +21,7 @@ from astakos.im import user_logic as users
from synnefo_admin.admin.actions import AdminAction
from synnefo_admin.admin.utils import update_actions_rbac, send_admin_email
from astakos.im.user_utils import change_user_email
class UserAction(AdminAction):
......@@ -80,7 +81,12 @@ def generate_actions():
name='Resend verification', f=users.send_verification_mail,
karma='good', c=check_user_action("SEND_VERIFICATION_MAIL"),)
actions['contact'] = UserAction(name='Send e-mail', f=send_admin_email,)
actions['contact'] = UserAction(name='Send e‑mail', f=send_admin_email,)
actions['modify_email'] = UserAction(name='Change e‑mail',
f=change_user_email, karma='bad',
caution_level='warning',
data_keys=['new_email'],)
update_actions_rbac(actions)
......
......@@ -116,6 +116,14 @@ class UserJSONView(AdminJSONView):
def add_verbose_data(self, inst):
extra_dict = OrderedDict()
if inst.email_change_is_pending():
extra_dict['pending_email'] = {
'display_name': "E-mail pending verification",
'value': inst.emailchanges.all()[0].new_email_address,
'visible': True,
}
extra_dict['status'] = {
'display_name': "Status",
'value': inst.status_display,
......@@ -163,7 +171,7 @@ JSON_CLASS = UserJSONView
@has_permission_or_403(cached_actions)
@transaction.commit_on_success
def do_action(request, op, id):
def do_action(request, op, id, data):
"""Apply the requested action on the specified user."""
user = get_user_or_404(id, for_update=True)
actions = get_permitted_actions(cached_actions, request.user)
......@@ -172,6 +180,9 @@ def do_action(request, op, id):
actions[op].apply(user, 'Rejected by the admin')
elif op == 'contact':
actions[op].apply(user, request)
elif op == 'modify_email':
if isinstance(data, dict):
actions[op].apply(user, data.get('new_email'))
else:
actions[op].apply(user)
......
# Copyright (C) 2010-2014 GRNET S.A.
# Copyright (C) 2010-2016 GRNET S.A.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -114,7 +114,7 @@ def generate_actions():
karma='neutral',
caution_level='dangerous',)
actions['contact'] = VMAction(name='Send e-mail', f=send_admin_email,)
actions['contact'] = VMAction(name='Send e‑mail', f=send_admin_email,)
update_actions_rbac(actions)
......
......@@ -147,7 +147,7 @@ JSON_CLASS = VMJSONView
@transaction.commit_on_success
@has_permission_or_403(cached_actions)
def do_action(request, op, id):
def do_action(request, op, id, data):
"""Apply the requested action on the specified user."""
if op == "contact":
user = get_user_or_404(id)
......
# Copyright (C) 2010-2014 GRNET S.A.
# Copyright (C) 2010-2016 GRNET S.A.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -38,7 +38,7 @@ def generate_actions():
"""Create a list of actions on volumes."""
actions = OrderedDict()
actions['contact'] = VolumeAction(name='Send e-mail', f=send_admin_email,)
actions['contact'] = VolumeAction(name='Send e‑mail', f=send_admin_email,)
update_actions_rbac(actions)
......
......@@ -169,7 +169,7 @@ JSON_CLASS = VolumeJSONView
@transaction.commit_on_success
@has_permission_or_403(cached_actions)
def do_action(request, op, id):
def do_action(request, op, id, data):
"""Apply the requested action on the specified volume."""
if op == "contact":
user = get_user_or_404(id)
......
......@@ -127,19 +127,55 @@ $(document).ready(function() {
function resetItemInfo(modal) {
var $modal = $(modal);
$modal.find('.summary .info-list').remove();
$modal.find('.summary dl').remove();
}
function drawModalSingleItem(modalID, itemName, itemID) {
var tpl;
var html;
var $summary = $(modalID).find('.modal-body .summary');
var $actionBtn = $(modalID).find('.apply-action');
var html = _.template(snf.modals.html.singleItemInfo);
// cannot use JSON.parse because data-keys inlude single quotes
var inputsNames = $actionBtn.attr('data-keys').slice(1, -1).replace(/ /g,'').split(',');
inputsNames = inputsNames.map(function(item) {
return item.slice(1, -1); // remove extra quotes
});
if (modalID == '#user-modify_email') {
tpl = snf.modals.html.singleItemInfoWithEmailInput;
html = _.template(tpl,
{
name: itemName,
id: itemID,
inputName: inputsNames[0] // modify_email has only 1 input
}
);
}
else{
tpl = snf.modals.html.singleItemInfo;
html = _.template(tpl, {name: itemName, id: itemID});
}
$actionBtn.attr('data-ids','['+itemID+']');
$summary.append(html({name: itemName, id: itemID}));
$summary.append(html);
};
function collectActionData(modal) {
var $list = $(modal).find('.info-list');
var actionData = [];
var hasInputs = $list.find('dd input').length > 0;
var itemData = {};
itemData['id'] = $list.attr('data-itemid');
if(hasInputs) {
itemData['data'] = {};
var key = $list.find('dd input').attr('name');
var value = $list.find('dd input').val();
itemData['data'][key] = value;
}
actionData.push(itemData)
return actionData;
};
$('.modal').find('.cancel').click(function() {
$modal =$(this).closest('.modal');
snf.modals.resetInputs($modal);
......@@ -157,12 +193,16 @@ $(document).ready(function() {
if($modal.attr('data-type') === 'contact') {
noError = snf.modals.validateContactForm($modal);
}
if($modal.attr('data-type') === 'modify_email') {
var validForm = snf.modals.validateModifyEmailForm($modal);
noError = noError && validForm;
}
if(!noError) {
e.preventDefault();
e.stopPropagation();
}
else {
snf.modals.performAction($modal, $notificationArea, snf.modals.html.notifyRefreshPage, 0, countAction);
snf.modals.performAction($modal, $notificationArea, snf.modals.html.notifyRefreshPage, collectActionData($modal), 0, countAction);
snf.modals.resetInputs($modal);
snf.modals.resetErrors($modal);
resetItemInfo($modal);
......
$(document).ready(function() {
var $actionbar = $('.actionbar');
var $actionbar = $('.actionbar');
if($actionbar.length > 0) {
sticker();
......@@ -635,7 +635,7 @@ $(document).ready(function() {
};
/* Checks how many rows are selected and adjusts the classes and
the text of the select-qll btn */
the text of the select-all btn */
function updateToggleAllSelect() {
var $togglePageItems = $('#select-page');
var $label = $togglePageItems.find('span')
......@@ -682,6 +682,26 @@ $(document).ready(function() {
$modal.find('.toggle-more').find('span').text('Show all');
};
function collectActionData(modal) {
var $table = $(modal).find('.table-selected');
var actionData = [];
var hasInputs = $table.find('tr:first input').length > 0;
$table.find('tr').each(function() {
var itemData = {};
itemData['id'] = $(this).attr('data-itemid');
if(hasInputs) {
itemData['data'] = {};
$(this).find('input').each(function() {
var key = $(this).attr('name');
itemData['data'][key] = $(this).val();
});
}
actionData.push(itemData);
});
return actionData;
};
$('.modal .cancel').click(function(e) {
$('[data-toggle="popover"]').popover('hide');
var $modal = $(this).closest('.modal');
......@@ -705,6 +725,7 @@ $(document).ready(function() {
var $modal = $(this).closest('.modal');
var noError = true;
var itemsNum = $modal.find('tbody tr').length;
var itemsData;
if(selected.items.length === 0) {
snf.modals.showError($modal, 'no-selected');
noError = false;
......@@ -713,13 +734,18 @@ $(document).ready(function() {
var validForm = snf.modals.validateContactForm($modal);
noError = noError && validForm;
}
if($modal.attr('data-type') === 'modify_email') {
var validForm = snf.modals.validateModifyEmailForm($modal);
noError = noError && validForm;
}
if(!noError) {
e.preventDefault();
e.stopPropagation();
}
else {
$('[data-toggle="popover"]').popover('hide');
snf.modals.performAction($modal, $notificationArea, snf.modals.html.notifyReloadTable, itemsNum, countAction);
itemsData = collectActionData($modal);
snf.modals.performAction($modal, $notificationArea, snf.modals.html.notifyReloadTable, itemsData, itemsNum, countAction);
snf.modals.resetErrors($modal);
snf.modals.resetInputs($modal);
removeWarningDupl($modal);
......@@ -775,6 +801,14 @@ $(document).ready(function() {
var idsArray = [];
var warningMsg = snf.modals.html.warningDuplicates;
var warningInserted = false;
// cannot use JSON.parse because data-keys inlude single quotes
var inputsNames = $actionBtn.attr('data-keys').slice(1, -1).replace(/ /g,'').split(',');
inputsNames = inputsNames.map(function(item) {
return item.slice(1, -1); // remove extra quotes
});
// association tracks for each user the related resource
// use to contact by selecting the resource of the user, not the user himself
var associations = {};
var $btn = $(modalID).find('.toggle-more');
$tableBody.empty();
......@@ -782,11 +816,15 @@ $(document).ready(function() {
uniqueProp = 'contact_id';
for(var i=0; i<rowsNum; i++) {
var currContactID = selected.items[i][uniqueProp];
// if there is no record of the current user, keep it and keep and the corresponding resource
if(associations[currContactID] === undefined) {
associations[currContactID] = [selected.items[i]['item_name']];
}
// if the user is already kept (selected other resource that he owns)
// keep and the other resource
else {
selected.items[i]['notFirst'] = true; // not the first item with the current contact_id
selected.items[i]['notFirst'] = true;
associations[currContactID].push(selected.items[i]['item_name']);
}
if(!warningInserted && selected.items[i]['notFirst']) {
......@@ -797,17 +835,54 @@ $(document).ready(function() {
for(var i=0; i<rowsNum; i++) {
if (!selected.items[i]['notFirst']) {
idsArray.push(selected.items[i][uniqueProp]);
currentRow = _.template(snf.modals.html.contactRow, {itemID: selected.items[i].contact_id, showAssociations: (itemType !== 'user'), associations: associations[selected.items[i][uniqueProp]].toString().replace(/\,/gi, ', '), fullName: selected.items[i].contact_name, email: selected.items[i].contact_email, hidden: (i >maxVisible)})
currentRow = _.template(
snf.modals.html.contactRow,
{
itemID: selected.items[i].contact_id,
showAssociations: (itemType !== 'user'),
associations: associations[selected.items[i][uniqueProp]].toString().replace(/\,/gi, ', '),
fullName: selected.items[i].contact_name,
email: selected.items[i].contact_email,
hidden: (i > maxVisible)
}
);
htmlRows += currentRow;
}
}
}
else if(modalType === "modify_email") {
uniqueProp = 'id';
for(var i=0; i<rowsNum; i++) {
idsArray.push(selected.items[i][uniqueProp]);
currentRow = _.template(
snf.modals.html.modifyEmailRow,
{
itemID: selected.items[i].contact_id,
fullName: selected.items[i].contact_name,
email: selected.items[i].contact_email,
hidden: (i > maxVisible),
inputName: inputsNames[0] // modify_email has only 1 input
}
);
htmlRows += currentRow;
}
}
else {
uniqueProp = 'id';
for(var i=0; i<rowsNum; i++) {
idsArray.push(selected.items[i][uniqueProp]);
currentRow = _.template(snf.modals.html.commonRow, {itemID: selected.items[i].id, itemName: selected.items[i].item_name, ownerEmail: selected.items[i].contact_email, ownerName: selected.items[i].contact_name, hidden: (i >=maxVisible)})
currentRow = _.template(
snf.modals.html.commonRow,
{
itemID: selected.items[i].id,
itemName: selected.items[i].item_name,
ownerEmail: selected.items[i].contact_email,
ownerName: selected.items[i].contact_name,
hidden: (i >= maxVisible)
}
);
htmlRows += currentRow;
}
}
......
......@@ -13,7 +13,7 @@ p.progress-area {
.modal {
&[data-item="user"] {
&:not([data-type="contact"]) .table-selected td:nth-child(3){
&:not([data-type="contact"]) .table-selected .owner-name {
display: none;
}
}
......@@ -73,7 +73,6 @@ p.progress-area {
border-color: $gray-dark;
}
}
}
.modal {
......@@ -188,13 +187,11 @@ p.progress-area {
font-size: 20px;
margin-left: 10px;
position: absolute;
// right: 8px;
top: 6px;
display: none;
&:hover,
&:focus{
color: red;
// color:#A80A0A;
text-decoration: none;
}
}
......@@ -214,13 +211,28 @@ p.progress-area {
.modal {
.table-selected {
th,td {
word-break: break-word;
word-break: break-word;
vertical-align: middle;
}
td:last-child {
.wrap {
padding-right: 36px;
}
}
td.td-with-input {
width: 250px;
.remove {
top: 14px;
}
.error-sign {
margin-left: 6px;
right: -25px;
top: 13px;
}
input {
vertical-align: middle;
}
}
tr:nth-child(2n){
background: darken(#fff, 5%);
}
......@@ -236,7 +248,6 @@ p.progress-area {
color: red;
}
}
.remove {
position: absolute;
right: 14px;
......@@ -246,5 +257,20 @@ p.progress-area {
text-decoration: none;
}
}
}
.with-inputs {
margin-top: 10px;
dt, dd {
position: relative;
height: 34px;
line-height: 34px;
input {
max-width: 272px;
line-height: normal;
vertical-align: middle;
}
}
}
}
......@@ -19,7 +19,7 @@ $total-black: #222;
// Dimensions
$nav-side-padding: 20px;
$sidebar-width: 110px;
$sidebar-width: 116px;
$datatabled-actions-height: 35px;
$details-title-height: 35px;
......
{% load admin_tags %}
<!--<div class="modal fade" id="{{type}}-{{ op }}" data-type="{{ op }}" data-backdrop="static" data-keyboard="false">-->
<div class="modal fade" data-item ={{ action.target }} id="{{action.target}}-{{ op }}" data-backdrop="static" data-keyboard="true" tabindex="-1" data-karma={{ action.karma }} data-caution={{ action.caution_level }} data-type={{ op }}>
<div class="modal-dialog modal-md">
<div class="modal-dialog {% if op == 'modify_email' and view_type == 'list' %} modal-lg {% else %} modal-md {% endif %}">
<div class="modal-content area">
<div class="modal-header">
<a class="close cancel" data-dismiss="modal">×</a>
<h3 class="elem">{{ action.name }}</h3>
<h3 class="elem">{{ action.name|safe }}</h3>
</div>
<div class="modal-body">
{% if op == "contact" %}
<div class="form-sender form-area">
<label>From:</label>
<input type="text" name="sender" class="sender"
form="contactForm" value="{{mail.sender}}" />
<a data-error="empty-sender" data-toggle="popover" data-trigger="hover" class="error-sign snf-exclamation-sign" href="#" rel="tooltip" data-content="Missing the sender address of the e&#8209mail."></a>
<a data-error="invalid-email" data-toggle="popover" data-trigger="hover" class="error-sign snf-exclamation-sign" href="#" rel="tooltip" data-content="Invalid e&#8209mail address."></a>
</div>
</br>
<div class="form-subject form-area">
<label>Subject:</label>