Added web interface for organizations.
It looks like crap, but it allows you to edit the details and the members.
This commit is contained in:
parent
64eab850c5
commit
e9cb235640
@ -313,6 +313,20 @@ class OrgManager:
|
||||
'$pull': {'unknown_members': member_email}
|
||||
})
|
||||
|
||||
def org_members(self, member_sting_ids: typing.Iterable[str]) -> typing.List[dict]:
|
||||
"""Returns the user documents of the organization members.
|
||||
|
||||
This is a workaround to provide membership information for
|
||||
organizations without giving 'mortal' users access to /api/users.
|
||||
"""
|
||||
from pillar.api.utils import str2id
|
||||
|
||||
member_ids = [str2id(uid) for uid in member_sting_ids]
|
||||
users_coll = current_app.db('users')
|
||||
users = users_coll.find({'_id': {'$in': member_ids}},
|
||||
projection={'_id': 1, 'full_name': 1, 'email': 1})
|
||||
return list(users)
|
||||
|
||||
|
||||
def setup_app(app):
|
||||
from . import patch, hooks
|
||||
|
@ -1,5 +1,5 @@
|
||||
def setup_app(app):
|
||||
from . import main, users, projects, nodes, notifications, redirects, subquery
|
||||
from . import main, users, projects, nodes, notifications, organizations, redirects, subquery
|
||||
main.setup_app(app, url_prefix=None)
|
||||
users.setup_app(app, url_prefix=None)
|
||||
redirects.setup_app(app, url_prefix='/r')
|
||||
@ -7,3 +7,4 @@ def setup_app(app):
|
||||
nodes.setup_app(app, url_prefix='/nodes')
|
||||
notifications.setup_app(app, url_prefix='/notifications')
|
||||
subquery.setup_app(app)
|
||||
organizations.setup_app(app, url_prefix='/orgs')
|
||||
|
5
pillar/web/organizations/__init__.py
Normal file
5
pillar/web/organizations/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from .routes import blueprint
|
||||
|
||||
|
||||
def setup_app(app, url_prefix=None):
|
||||
app.register_blueprint(blueprint, url_prefix=url_prefix)
|
83
pillar/web/organizations/routes.py
Normal file
83
pillar/web/organizations/routes.py
Normal file
@ -0,0 +1,83 @@
|
||||
import logging
|
||||
|
||||
import attr
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
import flask_wtf.csrf
|
||||
import werkzeug.exceptions as wz_exceptions
|
||||
|
||||
from pillarsdk import User
|
||||
|
||||
import pillar.flask_extra
|
||||
from pillar import current_app
|
||||
from pillar.api.utils import authorization, str2id, gravatar
|
||||
from pillar.web.system_util import pillar_api
|
||||
from pillar.api.utils.authentication import current_user
|
||||
|
||||
|
||||
from pillarsdk import Organization
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
blueprint = Blueprint('pillar.web.organizations', __name__, url_prefix='/organizations')
|
||||
|
||||
|
||||
@blueprint.route('/', endpoint='index')
|
||||
def index(organization_id: str = None):
|
||||
api = pillar_api()
|
||||
|
||||
organizations = Organization.all(api=api)
|
||||
|
||||
if not organization_id and organizations['_items']:
|
||||
organization_id = organizations['_items'][0]._id
|
||||
|
||||
can_create_organization = current_user().has_cap('create-organization')
|
||||
|
||||
return render_template('organizations/index.html',
|
||||
can_create_organization=can_create_organization,
|
||||
organizations=organizations,
|
||||
open_organization_id=organization_id)
|
||||
|
||||
|
||||
@blueprint.route('/<organization_id>')
|
||||
@pillar.flask_extra.vary_xhr()
|
||||
def view_embed(organization_id: str):
|
||||
if not request.is_xhr:
|
||||
return index(organization_id)
|
||||
|
||||
api = pillar_api()
|
||||
|
||||
organization: Organization = Organization.find(organization_id, api=api)
|
||||
|
||||
om = current_app.org_manager
|
||||
organization_oid = str2id(organization_id)
|
||||
|
||||
members = om.org_members(organization.members)
|
||||
for member in members:
|
||||
member['avatar'] = gravatar(member.get('email'))
|
||||
member['_id'] = str(member['_id'])
|
||||
|
||||
can_edit = om.user_is_admin(organization_oid)
|
||||
|
||||
csrf = flask_wtf.csrf.generate_csrf()
|
||||
|
||||
return render_template('organizations/view_embed.html',
|
||||
organization=organization,
|
||||
members=members,
|
||||
can_edit=can_edit,
|
||||
seats_used=len(members) + len(organization.unknown_members),
|
||||
csrf=csrf)
|
||||
|
||||
|
||||
@blueprint.route('/create-new', methods=['POST'])
|
||||
@authorization.require_login(require_cap='create-organization')
|
||||
def create_new():
|
||||
"""Creates a new Organization, owned by the currently logged-in user."""
|
||||
|
||||
user_id = current_user().user_id
|
||||
log.info('Creating new organization for user %s', user_id)
|
||||
|
||||
name = request.form['name']
|
||||
seat_count = int(request.form['seat_count'], 10)
|
||||
|
||||
org_doc = current_app.org_manager.create_new_org(name, user_id, seat_count)
|
||||
|
||||
return jsonify({'_id': org_doc['_id']}), 201
|
182
src/templates/organizations/index.jade
Normal file
182
src/templates/organizations/index.jade
Normal file
@ -0,0 +1,182 @@
|
||||
| {% extends 'layout.html' %}
|
||||
| {% block bodyattrs %}{{ super() }} data-context='organizations'{% endblock %}
|
||||
| {% block page_title %}Organizations{% endblock %}
|
||||
|
||||
| {% block body %}
|
||||
#col_main.organization-index
|
||||
#col_main-content
|
||||
.col_header.item-list-header
|
||||
i.pi-cloud
|
||||
| Your organizations
|
||||
|
||||
.item-list.col-scrollable
|
||||
.table
|
||||
.table-head
|
||||
.table-row
|
||||
.table-cell.item-name
|
||||
span.collapser(title="Collapse name column") Name
|
||||
.table-cell.item-priority
|
||||
span.collapser(title="Collapse priority column") Members
|
||||
.table-cell.item-priority
|
||||
span.collapser(title="Collapse priority column") Unknown Members
|
||||
|
||||
.table-body
|
||||
| {% for organization in organizations['_items'] %}
|
||||
| {% set link_url = url_for('pillar.web.organizations.view_embed', organization_id=organization._id) %}
|
||||
.table-row(id="organization-{{ organization._id }}")
|
||||
.table-cell.item-name
|
||||
a(data-organization-id="{{ organization._id }}",
|
||||
href="{{ link_url }}",
|
||||
class="organization-link")
|
||||
span(class="organization-name-{{ organization._id }}") {{ organization.name }}
|
||||
.table-cell.item-members
|
||||
a(data-organization-id="{{ organization._id }}",
|
||||
href="{{ link_url }}",
|
||||
class="organization-link")
|
||||
span(class="organization-projects-{{ organization._id }}") {{ organization.members|hide_none|count }}
|
||||
.table-cell.item-unknown-members
|
||||
a(data-organization-id="{{ organization._id }}",
|
||||
href="{{ link_url }}",
|
||||
class="organization-link")
|
||||
span(class="organization-projects-{{ organization._id }}") {{ organization.unknown_members|hide_none|count }}
|
||||
| {% endfor %}
|
||||
|
||||
#item-action-panel
|
||||
| {% if can_create_organization %}
|
||||
button.btn(onclick='createNewOrganization(this)') Create new organization (max {{max_organizations}})
|
||||
#create_organization_result_panel
|
||||
| {% endif %}
|
||||
|
||||
#col_right
|
||||
.col_header
|
||||
span.header_text
|
||||
#item-details.col-scrollable
|
||||
.item-details-empty
|
||||
| Select an organization
|
||||
| {% endblock %}
|
||||
|
||||
|
||||
| {% block footer_scripts %}
|
||||
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.typeahead-0.11.1.min.js')}}")
|
||||
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/algoliasearch-3.19.0.min.js')}}")
|
||||
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.autocomplete-0.22.0.min.js') }}", async=true)
|
||||
|
||||
script.
|
||||
|
||||
/* Returns a more-or-less reasonable message given an error response object. */
|
||||
function xhrErrorResponseMessage(err) {
|
||||
if (typeof err.responseJSON == 'undefined')
|
||||
return err.statusText;
|
||||
|
||||
if (typeof err.responseJSON._error != 'undefined' && typeof err.responseJSON._error.message != 'undefined')
|
||||
return err.responseJSON._error.message;
|
||||
|
||||
if (typeof err.responseJSON._message != 'undefined')
|
||||
return err.responseJSON._message
|
||||
|
||||
return err.statusText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an organization in the #item-details div.
|
||||
*/
|
||||
function item_open(item_id, pushState)
|
||||
{
|
||||
if (item_id === undefined ) {
|
||||
throw new ReferenceError("item_open(" + item_id + ") called.");
|
||||
}
|
||||
|
||||
// Style elements starting with item_type and dash, e.g. "#job-uuid"
|
||||
var clean_classes = 'active processing';
|
||||
var current_item = $('#organization-' + item_id);
|
||||
|
||||
$('[id^="organization-"]').removeClass(clean_classes);
|
||||
current_item
|
||||
.removeClass(clean_classes)
|
||||
.addClass('processing');
|
||||
|
||||
var item_url = '/orgs/' + item_id;
|
||||
statusBarSet('default', 'Loading organization…');
|
||||
|
||||
$.get(item_url, function(item_data) {
|
||||
statusBarClear();
|
||||
$('#item-details').html(item_data);
|
||||
$('#col_right .col_header span.header_text').text('Organization details');
|
||||
|
||||
current_item
|
||||
.removeClass(clean_classes)
|
||||
.addClass('active');
|
||||
|
||||
}).fail(function(xhr) {
|
||||
if (console) {
|
||||
console.log('Error fetching organization', item_id, 'from', item_url);
|
||||
console.log('XHR:', xhr);
|
||||
}
|
||||
|
||||
current_item.removeClass(clean_classes);
|
||||
statusBarSet('error', 'Failed to open organization', 'pi-warning');
|
||||
|
||||
if (xhr.status) {
|
||||
$('#item-details').html(xhr.responseText);
|
||||
} else {
|
||||
$('#item-details').html('<p class="text-danger">Opening ' + item_type + ' failed. There possibly was ' +
|
||||
'an error connecting to the server. Please check your network connection and ' +
|
||||
'try again.</p>');
|
||||
}
|
||||
});
|
||||
|
||||
// Determine whether we should push the new state or not.
|
||||
pushState = (typeof pushState !== 'undefined') ? pushState : true;
|
||||
if (!pushState) return;
|
||||
|
||||
// Push the correct URL onto the history.
|
||||
var push_state = {itemId: item_id};
|
||||
|
||||
window.history.pushState(
|
||||
push_state,
|
||||
'Organization: ' + item_id,
|
||||
item_url
|
||||
);
|
||||
}
|
||||
|
||||
{% if open_organization_id %}
|
||||
$(function() { item_open('{{ open_organization_id }}', false); });
|
||||
{% endif %}
|
||||
{% if can_create_organization %}
|
||||
function createNewOrganization(button) {
|
||||
$(button)
|
||||
.attr('disabled', 'disabled')
|
||||
.fadeTo(200, 0.1);
|
||||
$('#create_organization_result_panel').html('');
|
||||
|
||||
// TODO: create a form to get the initial info from the user.
|
||||
$.post(
|
||||
'{{ url_for('pillar.organizations.create_new') }}',
|
||||
{
|
||||
name: 'New Organization',
|
||||
seat_count: 1,
|
||||
}
|
||||
)
|
||||
.done(function() {
|
||||
var $p = $('<p>').text('organization created, reloading list.')
|
||||
$('#create_organization_result_panel').html($p);
|
||||
|
||||
window.location.reload();
|
||||
})
|
||||
.fail(function(err) {
|
||||
var msg = xhrErrorResponseMessage(err);
|
||||
$('#create_organization_result_panel').html('Error creating organization: ' + msg);
|
||||
|
||||
$(button)
|
||||
.fadeTo(1000, 1.0)
|
||||
.queue(function() {
|
||||
$(this)
|
||||
.removeAttr('disabled')
|
||||
.dequeue()
|
||||
;
|
||||
})
|
||||
})
|
||||
;
|
||||
}
|
||||
{% endif %}
|
||||
| {% endblock %}
|
325
src/templates/organizations/view_embed.jade
Normal file
325
src/templates/organizations/view_embed.jade
Normal file
@ -0,0 +1,325 @@
|
||||
.flamenco-box.organization
|
||||
form#item_form(onsubmit="return editOrganization()")
|
||||
| {% if can_edit %}
|
||||
.input-group
|
||||
input.item-name.input-transparent(
|
||||
name="name",
|
||||
type="text",
|
||||
placeholder="Organization's name",
|
||||
value="{{ organization.name | hide_none }}")
|
||||
.input-group
|
||||
textarea.item-description.input-transparent(
|
||||
name="description",
|
||||
type="text",
|
||||
rows=1,
|
||||
placeholder="Organization's description") {{ organization.description | hide_none }}
|
||||
.input-group
|
||||
input.item-website.input-transparent(
|
||||
name="website",
|
||||
type="text",
|
||||
placeholder="Organization's website",
|
||||
value="{{ organization.website | hide_none }}")
|
||||
.input-group
|
||||
input.item-location.input-transparent(
|
||||
name="location",
|
||||
type="text",
|
||||
placeholder="Organization's location",
|
||||
value="{{ organization.location | hide_none }}")
|
||||
.input-group
|
||||
button#item-save.btn.btn-default.btn-block(type='submit')
|
||||
i.pi-check
|
||||
| Save Organization
|
||||
| {% else %}
|
||||
p.item-name {{ organization.name | hide_none }}
|
||||
| {% if organization.description %}
|
||||
p.item-description {{ organization.description | hide_none }}
|
||||
p.item-website {{ organization.website | hide_none }}
|
||||
p.item-location {{ organization.location | hide_none }}
|
||||
| {% endif %}
|
||||
| {% endif %}
|
||||
|
||||
h4 Properties
|
||||
.table.item-properties
|
||||
.table-body
|
||||
.table-row.properties-last-updated
|
||||
.table-cell Last Updated
|
||||
.table-cell(title='Unable to edit, set by Organization')
|
||||
| {{ organization._updated | hide_none | pretty_date_time }}
|
||||
.table-row.properties-seat-count
|
||||
.table-cell Seat Count
|
||||
.table-cell(title='Unable to edit, determined by subscription')
|
||||
| {{ organization.seat_count }} ({{ seats_used }} used)
|
||||
.table-row.properties-org-roles
|
||||
.table-cell User roles
|
||||
.table-cell(title='Unable to edit, determined by subscription')
|
||||
| {{ organization.org_roles | sort | join(', ') }}
|
||||
|
||||
|
||||
.flamenco-box.manager
|
||||
h4 Organization members
|
||||
| {% if can_edit %}
|
||||
.row
|
||||
.sharing-users-search
|
||||
.form-group
|
||||
input#user-select.form-control(
|
||||
name='contacts',
|
||||
type='text',
|
||||
placeholder='Add member by name')
|
||||
| {% endif %}
|
||||
.row
|
||||
ul.sharing-users-list
|
||||
| {% for member in members %}
|
||||
li.sharing-users-item(
|
||||
data-user-id="{{ member['_id'] }}",
|
||||
class="{% if current_user.objectid == member['_id'] %}self{% endif %}")
|
||||
.sharing-users-avatar
|
||||
img(src="{{ member['avatar'] }}")
|
||||
.sharing-users-details
|
||||
span.sharing-users-name
|
||||
| {{ member['full_name'] }}
|
||||
| {% if current_user.objectid == member['_id'] %}
|
||||
small (You)
|
||||
| {% endif %}
|
||||
| {% if organization['admin_uid'] == member['_id'] %}
|
||||
small (admin)
|
||||
| {% endif %}
|
||||
span.sharing-users-extra {{ member['username'] }}
|
||||
.sharing-users-action
|
||||
| {% if can_edit %}
|
||||
| {% if current_user.objectid == member['_id'] %}
|
||||
button.user-remove(title="Leave as member of this organization")
|
||||
i.pi-trash
|
||||
| {% else %}
|
||||
button.user-remove(title="Remove this user from this organization")
|
||||
i.pi-trash
|
||||
| {% endif %}
|
||||
| {% endif %}
|
||||
| {% endfor %}
|
||||
.row
|
||||
h5 Users without Blender Cloud account
|
||||
ul.sharing-users-list.unknown-members
|
||||
| {% for email in organization.unknown_members %}
|
||||
li.sharing-users-item.unknown-member(data-user-email='{{ email }}')
|
||||
.sharing-users-avatar
|
||||
img(src="{{ email | gravatar }}")
|
||||
.sharing-users-details
|
||||
span.sharing-users-email {{ email }}
|
||||
.sharing-users-action
|
||||
| {% if can_edit %}
|
||||
button.user-remove(title="Remove this user from this organization")
|
||||
i.pi-trash
|
||||
| {% endif %}
|
||||
| {% endfor %}
|
||||
|
||||
| {% if can_edit %}
|
||||
h5 Batch-add members by email address
|
||||
form#batch_add_form(onsubmit="return batchAddUsers()")
|
||||
.input-group
|
||||
textarea.item-description.input-transparent(
|
||||
name="emails",
|
||||
type="text",
|
||||
rows=1,
|
||||
placeholder="Email addresses, separated by space/enter")
|
||||
.input-group
|
||||
button.btn.btn-default.btn-block(type='submit')
|
||||
i.pi-check
|
||||
| Add members
|
||||
| {% endif %}
|
||||
|
||||
.action-result-panel
|
||||
|
||||
#item-view-feed
|
||||
| {% if config.DEBUG %}
|
||||
.debug-info
|
||||
a.debug-info-toggle(role='button',
|
||||
data-toggle='collapse',
|
||||
href='#debug-content-organization',
|
||||
aria-expanded='false',
|
||||
aria-controls='debug-content-organization')
|
||||
i.pi-info
|
||||
| Debug Info
|
||||
#debug-content-organization.collapse
|
||||
pre.
|
||||
{{ organization.to_dict() | pprint }}
|
||||
| {% endif %}
|
||||
|
||||
| {% block footer_scripts %}
|
||||
|
||||
| {% if can_edit %}
|
||||
script.
|
||||
|
||||
function patchOrganization(patch) {
|
||||
if (typeof patch == 'undefined') {
|
||||
throw 'patchOrganization(undefined) called';
|
||||
}
|
||||
|
||||
if (console) console.log('patchOrganization', patch);
|
||||
|
||||
var promise = $.ajax({
|
||||
url: '/api/organizations/{{ organization._id }}',
|
||||
method: 'PATCH',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(patch),
|
||||
})
|
||||
.fail(function(err) {
|
||||
if (console) console.log('Error patching: ', err);
|
||||
})
|
||||
;
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var APPLICATION_ID = '{{config.ALGOLIA_USER}}'
|
||||
var SEARCH_ONLY_API_KEY = '{{config.ALGOLIA_PUBLIC_KEY}}';
|
||||
var INDEX_NAME = '{{config.ALGOLIA_INDEX_USERS}}';
|
||||
var client = algoliasearch(APPLICATION_ID, SEARCH_ONLY_API_KEY);
|
||||
var index = client.initIndex(INDEX_NAME);
|
||||
|
||||
$('#user-select').autocomplete({hint: false}, [
|
||||
{
|
||||
source: function (q, cb) {
|
||||
index.search(q, {hitsPerPage: 5}, function (error, content) {
|
||||
if (error) {
|
||||
cb([]);
|
||||
return;
|
||||
}
|
||||
cb(content.hits, content);
|
||||
});
|
||||
},
|
||||
displayKey: 'full_name',
|
||||
minLength: 2,
|
||||
limit: 10,
|
||||
templates: {
|
||||
suggestion: function (hit) {
|
||||
var suggestion = hit.full_name + ' (' + hit.username + ')';
|
||||
var $p = $('<p>').text(suggestion);
|
||||
return $p.html();
|
||||
}
|
||||
}
|
||||
}
|
||||
]).on('autocomplete:selected', function (event, hit, dataset) {
|
||||
var $existing = $('li.sharing-users-item[data-user-id="' + hit.objectID + '"]');
|
||||
if ($existing.length) {
|
||||
$existing
|
||||
.addClass('active')
|
||||
.delay(1000)
|
||||
.queue(function() {
|
||||
console.log('no');
|
||||
$existing.removeClass('active');
|
||||
$existing.dequeue();
|
||||
});
|
||||
toastr.info('User is already member of this organization');
|
||||
}
|
||||
else {
|
||||
addUser('{{ organization["_id"] }}', hit.objectID);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function addUser(organizationId, userId){
|
||||
if (!userId || userId.length == 0) {
|
||||
toastr.error('Please select a user from the list');
|
||||
return;
|
||||
}
|
||||
|
||||
patchOrganization({
|
||||
op: 'assign-user',
|
||||
user_id: userId
|
||||
})
|
||||
.done(function (data) {
|
||||
setTimeout(function(){ $('.sharing-users-item').removeClass('added');}, 350);
|
||||
statusBarSet('success', 'Member added to this organization!', 'pi-grin');
|
||||
|
||||
// TODO fsiddi: avoid the reloading of the entire page?
|
||||
window.location.reload();
|
||||
})
|
||||
.fail(function (err) {
|
||||
var msg = xhrErrorResponseMessage(err);
|
||||
toastr.error('Could not add member: ' + msg);
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
| {% endif %}
|
||||
script.
|
||||
$(document).ready(function() {
|
||||
$('body').off('click', '.user-remove'); // remove previous handlers.
|
||||
$('body').on('click', '.user-remove', function(e) {
|
||||
var user_id = $(this).closest('*[data-user-id]').data('user-id');
|
||||
var user_email = $(this).closest('*[data-user-email]').data('user-email');
|
||||
removeUser(user_id, user_email);
|
||||
});
|
||||
|
||||
function removeUser(user_id, email) {
|
||||
if (typeof user_id == 'undefined' && typeof email == 'undefined') {
|
||||
throw "removeUser(undefined, undefined) called";
|
||||
}
|
||||
var organization_id = '{{ organization._id }}';
|
||||
var patch = {op: 'remove-user'};
|
||||
|
||||
if (typeof user_id !== 'undefined') {
|
||||
patch.user_id = user_id;
|
||||
}
|
||||
if (typeof email !== 'undefined') {
|
||||
patch.email = String(email);
|
||||
}
|
||||
|
||||
patchOrganization(patch)
|
||||
.done(function() {
|
||||
$("ul.sharing-users-list").find("[data-user-id='" + user_id + "']").remove();
|
||||
item_open('{{ organization._id }}', false);
|
||||
toastr.success('User removed from this organization');
|
||||
}).fail(function (data) {
|
||||
var msg = xhrErrorResponseMessage(data);
|
||||
toastr.error('Error removing user: ' + msg);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function editOrganization() {
|
||||
var $form = $('#item_form');
|
||||
var new_name = $form.find('*[name="name"]').val();
|
||||
|
||||
patchOrganization({
|
||||
op: 'edit-from-web',
|
||||
name: new_name,
|
||||
description: $form.find('*[name="description"]').val(),
|
||||
website: $form.find('*[name="website"]').val(),
|
||||
location: $form.find('*[name="location"]').val(),
|
||||
})
|
||||
.done(function() {
|
||||
$('span.organization-name-{{ organization._id }}').text(new_name);
|
||||
})
|
||||
.fail(function(err) {
|
||||
var msg = xhrErrorResponseMessage(err);
|
||||
toastr.error('Error editing organization: ' + msg);
|
||||
})
|
||||
;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function batchAddUsers() {
|
||||
var $form = $('#batch_add_form');
|
||||
var emails = $form.find('*[name="emails"]').val().split(/\s/);
|
||||
console.log(emails);
|
||||
|
||||
patchOrganization({
|
||||
op: 'assign-users',
|
||||
emails: emails,
|
||||
})
|
||||
.done(function() {
|
||||
item_open('{{ organization._id }}', false);
|
||||
})
|
||||
.fail(function(err) {
|
||||
var msg = xhrErrorResponseMessage(err);
|
||||
toastr.error('Error adding members: ' + msg);
|
||||
})
|
||||
;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
| {% endblock %}
|
Loading…
x
Reference in New Issue
Block a user