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)
......
snf = {
filters: {},
modals: {
performAction: function(modal, notificationArea, warningMsg, itemsCount, countAction) {
performAction: function(modal, notificationArea, warningMsg, itemsData, itemsCount, countAction) {
var $modal = $(modal);
var $notificationArea = $(notificationArea);
var $actionBtn = $modal.find('.apply-action')
var url = $actionBtn.attr('data-url');
var actionName = $actionBtn.find('span').text();
var logID = 'action-'+countAction;
var items = JSON.stringify(itemsData);
var data = {
op: $actionBtn.attr('data-op'),
target: $actionBtn.attr('data-target'),
ids: $actionBtn.attr('data-ids')
items: items
}
var contactAction = (data.op === 'contact' ? true : false);
......@@ -128,7 +130,7 @@ snf = {
var chunks = email.split(" ");
if (chunks.length == 1) {
return (reg.test(email) || lt_gt_reg.test(email))
return (reg.test(email) || lt_gt_reg.test(email));
} else {
chunk = chunks[chunks.length - 1];
return lt_gt_reg.test(chunk);
......@@ -162,6 +164,23 @@ snf = {
}
return noError;
},
toggleEmailErrorSign: function(el){
var val = el.val();
var errorSign = el.siblings('.error-sign');
var isValid = snf.modals.validateEmail(val);
if (!isValid) {
errorSign.show();
} else {
errorSign.hide();
}
},
validateModifyEmailForm: function(modal) {
var $modal = $(modal);
$modal.find('.js-email').each(function(i, el){
snf.modals.toggleEmailErrorSign($(el));
});
return $modal.find('.error-sign:visible').length >0 ? false: true ;
},
resetInputs: function(modal) {
var $modal = $(modal);
$modal.find('input').each(function() {
......@@ -173,40 +192,159 @@ snf = {
});
},
html: {
singleItemInfo: '<dl class="dl-horizontal info-list"><dt>Name:</dt><dd><%= name %></dd><dt>ID:</dt><dd><%= id %></dd><dl>',
removeLogLine: '<a href="" class="remove-icon remove-log" title="Remove this line">X</a>',
notifyPending: '<p class="log" id="<%= logID %>"><span class="pending state-icon snf-font-admin snf-exclamation-sign"></span>Action <b>"<%= actionName %>"</b><% if (itemsCount==1) { %> for <%= itemsCount %> item <% } else if (itemsCount>0) { %> for <%= itemsCount %> items <% } %> is <b class="pending">pending</b>.<%= removeBtn %></p>',
notifySuccess: '<p class="log"><span class="success state-icon snf-font-admin snf-ok"></span>Action <b>"<%= actionName %>"</b><% if (itemsCount==1) { %> for <%= itemsCount %> item <% } else if (itemsCount>0) { %> for <%= itemsCount %> items <% } %> <b class="succeed">succeeded</b>.<%= removeBtn %></p>',
notifyError: '<div class="log"><%= logInfo %></div>',
notifyErrorSum: '<p><span class="error state-icon snf-font-admin snf-remove"></span>Action <b>"<%= actionName %>"</b><% if (itemsCount==1) { %> for <%= itemsCount %> item <% } else if (itemsCount>0) { %> for <%= itemsCount %> items <% } %> <b class="error">failed</b>.<%= removeBtn %></p>',
notifyErrorDetails: '<dl class="dl-horizontal"><%= list %></dl>',
notifyErrorReason: '<dt>Reason:</dt><dd><%= description %></dd>',
notifyErrorIDs: '<dt>IDs:</dt><dd><%= ids %></dd>',
notifyRefreshPage: '<p class="warning">The data of the page maybe out of date. Refresh it, to update them.</p>',
notifyReloadTable: '<p class="warning">You may need to reload the table before making any new selections.<span class="wrap"><a class="clear-reload warning-btn">Clear selected and reload</a></span></p>',
warningDuplicates: '<p class="warning-duplicate">Duplicate accounts have been detected</p>',
commonRow: '<tr data-itemid=<%= itemID %> <% if(hidden) { %> class="hidden-row" <% } %> ><td class="item-name"><%= itemName %></td><td class="item-id"><%= itemID %></td><td class="owner-name"><%= ownerName %></td><td class="owner-email"><div class="wrap"><a class="remove" title="Remove item from selection">X</a><%= ownerEmail %></div></td></tr>',
contactRow: '<tr <% if(showAssociations) { %> title="related with: <%= associations %>" <% } %> data-itemid=<%= itemID %> <% if(hidden) { %> class="hidden-row" <% } %> ><td class="full-name"><%= fullName %></td><td class="email"><div class="wrap"><a class="remove" title="Remove item from selection">X</a><%= email %></div></td></tr>'
singleItemInfo:
'<dl class="dl-horizontal info-list" data-itemid=<%= id %>> \
<dt>Name:</dt><dd><%= name %></dd> \
<dt>ID:</dt><dd><%= id %></dd> \
<dl>',
singleItemInfoWithEmailInput:
'<dl class="dl-horizontal info-list" data-itemid=<%= id %>> \
<dt>Name:</dt><dd><%= name %></dd> \
<dt>ID:</dt><dd><%= id %></dd> \
</dl> \
<dl class="dl-horizontal info-list with-inputs"> \
<dt>New e-mail:</dt> \
<dd> \
<input placeholder="new e-mail" class="new-email js-email" name=<%= inputName %>> \
<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> \
</dd> \
</dl>',
removeLogLine:
'<a href="" class="remove-icon remove-log" title="Remove this line">X</a>',
notifyPending:
'<p class="log" id="<%= logID %>"> \
<span class="pending state-icon snf-font-admin snf-exclamation-sign"></span> \
Action \
<b>"<%= actionName %>"</b> \
<% if (itemsCount==1) { %> for <%= itemsCount %> item <% } else if (itemsCount>0) { %> for <%= itemsCount %> items <% } %> is \
<b class="pending">pending.</b> \
<%= removeBtn %> \
</p>',
notifySuccess:
'<p class="log"> \
<span class="success state-icon snf-font-admin snf-ok"></span> \
Action \
<b>"<%= actionName %>"</b> \
<% if (itemsCount==1) { %> for <%= itemsCount %> item <% } else if (itemsCount>0) { %> for <%= itemsCount %> items <% } %> \
<b class="succeed">succeeded.</b> \
<%= removeBtn %> \
</p>',
notifyError:
'<div class="log"><%= logInfo %></div>',
notifyErrorSum:
'<p> \
<span class="error state-icon snf-font-admin snf-remove"></span> \
Action \
<b>"<%= actionName %>"</b> \
<% if (itemsCount==1) { %> for <%= itemsCount %> item <% } else if (itemsCount>0) { %> for <%= itemsCount %> items <% } %> \
<b class="error">failed.</b> \
<%= removeBtn %></p>',
notifyErrorDetails:
'<dl class="dl-horizontal"><%= list %></dl>',
notifyErrorReason:
'<dt>Reason:</dt><dd><%= description %></dd>',
notifyErrorIDs:
'<dt>IDs:</dt><dd><%= ids %></dd>',
notifyRefreshPage:
'<p class="warning">The data of the page maybe out of date. Refresh it, to update them.</p>',
notifyReloadTable:
'<p class="warning"> \
You may need to reload the table before making any new selections. \
<span class="wrap"> \
<a class="clear-reload warning-btn">Clear selected and reload</a> \
</span> \
</p>',
warningDuplicates:
'<p class="warning-duplicate">Duplicate accounts have been detected.</p>',
commonRow:
'<tr data-itemid=<%= itemID %> <% if(hidden) { %> class="hidden-row" <% } %> > \
<td class="item-name"><%= itemName %></td> \
<td class="item-id"><%= itemID %></td> \
<td class="owner-name"><%= ownerName %></td> \
<td class="owner-email"> \
<div class="wrap"> \
<a class="remove" title="Remove item from selection">X</a> \
<%= ownerEmail %> \
</div> \
</td> \
</tr>',
contactRow:
'<tr <% if(showAssociations) { %> title="related with: <%= associations %>" <% } %> data-itemid=<%= itemID %> <% if(hidden) { %> class="hidden-row" <% } %> > \
<td class="full-name"><%= fullName %></td> \
<td class="email"> \
<div class="wrap"> \
<a class="remove" title="Remove item from selection">X</a> \
<%= email %> \
</div> \
</td> \
</tr>',
modifyEmailRow:
'<tr data-itemid=<%= itemID %> <% if(hidden) { %> class="hidden-row" <% } %> > \
<td class="full-name"><%= fullName %></td> \
<td class="item-id"><%= itemID %></td> \
<td class="email"> \
<div class="wrap"><%= email %></div> \
</td> \
<td class="wrap td-with-input"> \
<input placeholder="new e-mail" class="new-email js-email" name=<%= inputName %>> \
<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> \
<a class="remove" title="Remove item from selection">X</a></td></tr>',
}
},
tables: {
html: {
selectAllBtn: '<a href="" class="select select-all line-btn" data-karma="neutral" data-caution="warning" data-toggle="modal" data-target="#massive-actions-warning"><span>Select All</span></a>',
selectPageBtn: '<a href="" id="select-page" class="select line-btn txt" data-karma="neutral" data-caution="none"><span class="txt-state-a">Select Page</span><span class="txt-state-b">Deselect Page</span></a>',
toggleSelected: '<a href="" class="toggle-selected extra-btn line-btn txt" data-karma="neutral txt"><span class="txt-close">Show selected</span><span class="txt-open">Hide selected</span><span class="badge num selected-num">0</span></a>',
reloadTable: '<a href="" class="line-btn reload-table" data-karma="neutral" data-caution="none" title="Reload table"><span class="snf-font-reload"></span></a>',
clearSelected: '<a href="" id="clear-all" class="disabled deselect line-btn" data-karma="neutral" data-caution="warning" data-toggle="modal" data-target="#clear-all-warning"><span class="snf-font-remove"></span><span>Clear All</span></a>',
toggleNotifications: '',
showTips: '',
trimedCell: '<span title="click to see"><span data-container="body" data-toggle="popover" data-placement="bottom" data-content="<%= data %>"><%= trimmedData %>...</span></span>',
checkboxCell: '<span class="snf-checkbox-unchecked selection-indicator select"></span><span class="snf-checkbox-checked selection-indicator select"></span><%= content %>',
summary: '<a title="Show summary" href="#" class="summary-expand expand-area"><span class="snf-font-admin snf-angle-down"></span></a><dl class="info-summary dl-horizontal"><%= list %></dl>',
summaryLine: '<dt><%= key %></dt><dd><%= value %></dd>',
detailsBtn: '<a title="Details" href="<%= url %>" class="details-link"><span class="snf-font-admin snf-search"></span></a>'
selectAllBtn:
'<a href="" class="select select-all line-btn" data-karma="neutral" data-caution="warning" data-toggle="modal" data-target="#massive-actions-warning"> \
<span>Select All</span> \
</a>',
selectPageBtn:
'<a href="" id="select-page" class="select line-btn txt" data-karma="neutral" data-caution="none"> \
<span class="txt-state-a">Select Page</span> \
<span class="txt-state-b">Deselect Page</span> \
</a>',
toggleSelected:
'<a href="" class="toggle-selected extra-btn line-btn txt" data-karma="neutral txt"> \
<span class="txt-close">Show selected</span> \
<span class="txt-open">Hide selected</span> \
<span class="badge num selected-num">0</span> \
</a>',
reloadTable:
'<a href="" class="line-btn reload-table" data-karma="neutral" data-caution="none" title="Reload table"> \
<span class="snf-font-reload"></span> \
</a>',
clearSelected:
'<a href="" id="clear-all" class="disabled deselect line-btn" data-karma="neutral" data-caution="warning" data-toggle="modal" data-target="#clear-all-warning"> \
<span class="snf-font-remove"></span> \
<span>Clear All</span> \
</a>',
toggleNotifications:
'',
showTips:
'',
trimedCell:
'<span title="click to see"> \
<span data-container="body" data-toggle="popover" data-placement="bottom" data-content="<%= data %>"><%= trimmedData %>...</span> \
</span>',
checkboxCell:
'<span class="snf-checkbox-unchecked selection-indicator select"></span> \
<span class="snf-checkbox-checked selection-indicator select"></span> \
<%= content %>',
summary:
'<a title="Show summary" href="#" class="summary-expand expand-area"> \
<span class="snf-font-admin snf-angle-down"></span> \
</a> \
<dl class="info-summary dl-horizontal"><%= list %></dl>',
summaryLine:
'<dt><%= key %></dt><dd><%= value %></dd>',
detailsBtn:
'<a title="Details" href="<%= url %>" class="details-link"> \
<span class="snf-font-admin snf-search"></span> \
</a>'
}
},
timer: 0,
ajaxdelay: 400
ajaxdelay: 400,
};
function setThemeIcon() {
......@@ -323,3 +461,9 @@ $(document).ready(function(){
}
});
});
$('.modal').on('focusout', '.js-email', function(e){
var el = $(this);
snf.modals.toggleEmailErrorSign(el);
});
......@@ -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);
......