Commit 482c59f5 by Kálmán Viktor

Merge branch 'master' into feature-store

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/urls.py
parents 9fabdf4b 61b90140
# register a signal do update permissions every migration.
# This is based on app django_extensions update_permissions command
from south.signals import post_migrate
def update_permissions_after_migration(app, **kwargs):
"""
Update app permission just after every migration.
This is based on app django_extensions update_permissions
management command.
"""
from django.conf import settings
from django.db.models import get_app, get_models
from django.contrib.auth.management import create_permissions
create_permissions(get_app(app), get_models(), 2 if settings.DEBUG else 0)
post_migrate.connect(update_permissions_after_migration)
...@@ -271,6 +271,7 @@ LOCAL_APPS = ( ...@@ -271,6 +271,7 @@ LOCAL_APPS = (
'dashboard', 'dashboard',
'manager', 'manager',
'acl', 'acl',
'monitor',
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
......
...@@ -20,7 +20,7 @@ from logging import getLogger ...@@ -20,7 +20,7 @@ from logging import getLogger
from .models import activity_context, has_suffix from .models import activity_context, has_suffix
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied, ImproperlyConfigured
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -30,7 +30,7 @@ class Operation(object): ...@@ -30,7 +30,7 @@ class Operation(object):
"""Base class for VM operations. """Base class for VM operations.
""" """
async_queue = 'localhost.man' async_queue = 'localhost.man'
required_perms = () required_perms = None
do_not_call_in_templates = True do_not_call_in_templates = True
abortable = False abortable = False
has_percentage = False has_percentage = False
...@@ -141,6 +141,9 @@ class Operation(object): ...@@ -141,6 +141,9 @@ class Operation(object):
pass pass
def check_auth(self, user): def check_auth(self, user):
if self.required_perms is None:
raise ImproperlyConfigured(
"Set required_perms to () if none needed.")
if not user.has_perms(self.required_perms): if not user.has_perms(self.required_perms):
raise PermissionDenied("%s doesn't have the required permissions." raise PermissionDenied("%s doesn't have the required permissions."
% user) % user)
......
...@@ -1240,6 +1240,24 @@ ...@@ -1240,6 +1240,24 @@
} }
}, },
{ {
"pk": 1367,
"model": "auth.permission",
"fields": {
"codename": "create_vm",
"name": "Can create a new VM.",
"content_type": 28
}
},
{
"pk": 1368,
"model": "auth.permission",
"fields": {
"codename": "access_console",
"name": "Can access the graphical console of a VM.",
"content_type": 28
}
},
{
"pk": 1, "pk": 1,
"model": "auth.group", "model": "auth.group",
"fields": { "fields": {
......
...@@ -25,6 +25,7 @@ from django.contrib.auth.forms import ( ...@@ -25,6 +25,7 @@ from django.contrib.auth.forms import (
) )
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.core.exceptions import PermissionDenied, ValidationError
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import ( from crispy_forms.layout import (
...@@ -39,13 +40,16 @@ from django.template import Context ...@@ -39,13 +40,16 @@ from django.template import Context
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from sizefield.widgets import FileSizeWidget from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
from firewall.models import Vlan, Host from firewall.models import Vlan, Host
from storage.models import Disk from storage.models import Disk
from vm.models import ( from vm.models import (
InstanceTemplate, Lease, InterfaceTemplate, Node, Trait InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance
) )
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.models import Permission
from .models import Profile, GroupProfile from .models import Profile, GroupProfile
from circle.settings.base import LANGUAGES from circle.settings.base import LANGUAGES
from django.utils.translation import string_concat from django.utils.translation import string_concat
...@@ -592,6 +596,17 @@ class TemplateForm(forms.ModelForm): ...@@ -592,6 +596,17 @@ class TemplateForm(forms.ModelForm):
n = self.instance.interface_set.values_list("vlan", flat=True) n = self.instance.interface_set.values_list("vlan", flat=True)
self.initial['networks'] = n self.initial['networks'] = n
self.allowed_fields = (
'name', 'access_method', 'description', 'system', 'tags')
if self.user.has_perm('vm.change_template_resources'):
self.allowed_fields += tuple(set(self.fields.keys()) -
set(['raw_data']))
if self.user.is_superuser:
self.allowed_fields += ('raw_data', )
for name, field in self.fields.items():
if name not in self.allowed_fields:
field.widget.attrs['disabled'] = 'disabled'
if not self.instance.pk and len(self.errors) < 1: if not self.instance.pk and len(self.errors) < 1:
self.instance.priority = 20 self.instance.priority = 20
self.instance.ram_size = 512 self.instance.ram_size = 512
...@@ -602,14 +617,35 @@ class TemplateForm(forms.ModelForm): ...@@ -602,14 +617,35 @@ class TemplateForm(forms.ModelForm):
return User.objects.get(pk=self.instance.owner.pk) return User.objects.get(pk=self.instance.owner.pk)
return self.user return self.user
def clean_raw_data(self): def _clean_fields(self):
# if raw_data has changed and the user is not superuser try:
if "raw_data" in self.changed_data and not self.user.is_superuser: old = InstanceTemplate.objects.get(pk=self.instance.pk)
old_raw_data = InstanceTemplate.objects.get( except InstanceTemplate.DoesNotExist:
pk=self.instance.pk).raw_data old = None
return old_raw_data for name, field in self.fields.items():
else: if name in self.allowed_fields:
return self.cleaned_data['raw_data'] value = field.widget.value_from_datadict(
self.data, self.files, self.add_prefix(name))
try:
if isinstance(field, forms.FileField):
initial = self.initial.get(name, field.initial)
value = field.clean(value, initial)
else:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self._errors[name] = self.error_class(e.messages)
if name in self.cleaned_data:
del self.cleaned_data[name]
elif old:
if name == 'networks':
self.cleaned_data[name] = [
i.vlan for i in self.instance.interface_set.all()]
else:
self.cleaned_data[name] = getattr(old, name)
def save(self, commit=True): def save(self, commit=True):
data = self.cleaned_data data = self.cleaned_data
...@@ -623,6 +659,8 @@ class TemplateForm(forms.ModelForm): ...@@ -623,6 +659,8 @@ class TemplateForm(forms.ModelForm):
networks = InterfaceTemplate.objects.filter( networks = InterfaceTemplate.objects.filter(
template=self.instance).values_list("vlan", flat=True) template=self.instance).values_list("vlan", flat=True)
for m in data['networks']: for m in data['networks']:
if not m.has_level(self.user, "user"):
raise PermissionDenied()
if m.pk not in networks: if m.pk not in networks:
InterfaceTemplate(vlan=m, managed=m.managed, InterfaceTemplate(vlan=m, managed=m.managed,
template=self.instance).save() template=self.instance).save()
...@@ -634,10 +672,6 @@ class TemplateForm(forms.ModelForm): ...@@ -634,10 +672,6 @@ class TemplateForm(forms.ModelForm):
@property @property
def helper(self): def helper(self):
kwargs_raw_data = {}
if not self.user.is_superuser:
kwargs_raw_data['readonly'] = None
helper = FormHelper() helper = FormHelper()
helper.layout = Layout( helper.layout = Layout(
Field("name"), Field("name"),
...@@ -689,7 +723,7 @@ class TemplateForm(forms.ModelForm): ...@@ -689,7 +723,7 @@ class TemplateForm(forms.ModelForm):
_("Virtual machine settings"), _("Virtual machine settings"),
Field('access_method'), Field('access_method'),
Field('boot_menu'), Field('boot_menu'),
Field('raw_data', **kwargs_raw_data), Field('raw_data'),
Field('req_traits'), Field('req_traits'),
Field('description'), Field('description'),
Field("parent", type="hidden"), Field("parent", type="hidden"),
...@@ -882,8 +916,6 @@ class VmDownloadDiskForm(forms.Form): ...@@ -882,8 +916,6 @@ class VmDownloadDiskForm(forms.Form):
@property @property
def helper(self): def helper(self):
helper = FormHelper(self) helper = FormHelper(self)
helper.add_input(Submit("submit", _("Create"),
css_class="btn btn-success"))
helper.form_tag = False helper.form_tag = False
return helper return helper
...@@ -1147,3 +1179,66 @@ class UserKeyForm(forms.ModelForm): ...@@ -1147,3 +1179,66 @@ class UserKeyForm(forms.ModelForm):
if self.user: if self.user:
self.instance.user = self.user self.instance.user = self.user
return super(UserKeyForm, self).clean() return super(UserKeyForm, self).clean()
class TraitsForm(forms.ModelForm):
class Meta:
model = Instance
fields = ('req_traits', )
@property
def helper(self):
helper = FormHelper()
helper.form_show_labels = False
helper.form_action = reverse_lazy("dashboard.views.vm-traits",
kwargs={'pk': self.instance.pk})
helper.add_input(Submit("submit", _("Save"),
css_class="btn btn-success", ))
return helper
class RawDataForm(forms.ModelForm):
class Meta:
model = Instance
fields = ('raw_data', )
@property
def helper(self):
helper = FormHelper()
helper.form_show_labels = False
helper.form_action = reverse_lazy("dashboard.views.vm-raw-data",
kwargs={'pk': self.instance.pk})
helper.add_input(Submit("submit", _("Save"),
css_class="btn btn-success",
css_id="submit-password-button"))
return helper
permissions_filtered = Permission.objects.exclude(
codename__startswith="add_").exclude(
codename__startswith="delete_").exclude(
codename__startswith="change_")
class GroupPermissionForm(forms.ModelForm):
permissions = forms.ModelMultipleChoiceField(
queryset=permissions_filtered,
widget=FilteredSelectMultiple(_("permissions"), is_stacked=False)
)
class Meta:
model = Group
fields = ('permissions', )
@property
def helper(self):
helper = FormHelper()
helper.form_show_labels = False
helper.form_action = reverse_lazy(
"dashboard.views.group-permissions",
kwargs={'group_pk': self.instance.pk})
helper.add_input(Submit("submit", _("Save"),
css_class="btn btn-success", ))
return helper
...@@ -140,6 +140,18 @@ class Profile(Model): ...@@ -140,6 +140,18 @@ class Profile(Model):
return self.get_display_name() return self.get_display_name()
class FutureMember(Model):
org_id = CharField(max_length=64, help_text=_(
'Unique identifier of the person, e.g. a student number.'))
group = ForeignKey(Group)
class Meta:
unique_together = ('org_id', 'group')
def __unicode__(self):
return u"%s (%s)" % (self.org_id, self.group)
class GroupProfile(AclBase): class GroupProfile(AclBase):
ACL_LEVELS = ( ACL_LEVELS = (
('operator', _('operator')), ('operator', _('operator')),
...@@ -224,6 +236,10 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'): ...@@ -224,6 +236,10 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
group, unicode(g)) group, unicode(g))
g.user_set.add(sender) g.user_set.add(sender)
for i in FutureMember.objects.filter(org_id=value):
i.group.user_set.add(sender)
i.delete()
owneratrs = getattr(settings, 'SAML_GROUP_OWNER_ATTRIBUTES', []) owneratrs = getattr(settings, 'SAML_GROUP_OWNER_ATTRIBUTES', [])
for group in chain(*[attributes[i] for group in chain(*[attributes[i]
for i in owneratrs if i in attributes]): for i in owneratrs if i in attributes]):
......
...@@ -192,6 +192,9 @@ ...@@ -192,6 +192,9 @@
}, },
mousedown: function(ev) { mousedown: function(ev) {
if (this.element[0].disabled) {
return false;
}
// Touch: Get the original event: // Touch: Get the original event:
if (this.touchCapable && ev.type === 'touchstart') { if (this.touchCapable && ev.type === 'touchstart') {
......
...@@ -723,6 +723,7 @@ textarea[name="list-new-namelist"] { ...@@ -723,6 +723,7 @@ textarea[name="list-new-namelist"] {
} }
<<<<<<< HEAD
#store-list-list { #store-list-list {
list-style: none; list-style: none;
} }
...@@ -792,3 +793,32 @@ textarea[name="list-new-namelist"] { ...@@ -792,3 +793,32 @@ textarea[name="list-new-namelist"] {
.no-hover:hover { .no-hover:hover {
background: none !important; background: none !important;
} }
#group-detail-permissions .filtered {
margin: 2px 0;
padding: 2px 3px;
vertical-align: middle;
font-family: "Lucida Grande", Verdana, Arial, sans-serif;
font-weight: normal;
font-size: 11px;
border: 1px solid #ccc;
}
#group-detail-permissions .selector-available h2,
#group-detail-permissions .selector-chosen h2 {
margin: 0;
padding: 5px 8px 5px 8px;
font-size: 12px;
text-align: left;
font-weight: bold;
background: #7CA0C7;
color: white;
}
#group-detail-user-table {
margin-top: 20px;
}
#group-detail-permissions input[type="submit"]{
margin-top: -6px;
}
...@@ -512,7 +512,10 @@ function addMessage(text, type) { ...@@ -512,7 +512,10 @@ function addMessage(text, type) {
$('body').animate({scrollTop: 0}); $('body').animate({scrollTop: 0});
div = '<div style="display: none;" class="alert alert-' + type + '">' + text + '</div>'; div = '<div style="display: none;" class="alert alert-' + type + '">' + text + '</div>';
$('.messagelist').html('').append(div); $('.messagelist').html('').append(div);
$('.messagelist div').fadeIn(); var div = $('.messagelist div').fadeIn();
setTimeout(function() {
$(div).fadeOut();
}, 9000);
} }
......
...@@ -30,4 +30,56 @@ $(function() { ...@@ -30,4 +30,56 @@ $(function() {
}); });
return false; return false;
}); });
/* if the operation fails show the modal again */
$("body").on("click", "#op-form-send", function() {
var url = $(this).closest("form").prop("action");
$.ajax({
url: url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
type: 'POST',
data: $(this).closest('form').serialize(),
success: function(data, textStatus, xhr) {
/* hide the modal we just submitted */
$('#confirmation-modal').modal("hide");
/* if it was successful trigger a click event on activity, this will
* - go to that tab
* - starts refreshing the activity
*/
if(data.success) {
$('a[href="#activity"]').trigger("click");
/* if there are messages display them */
if(data.messages && data.messages.length > 0) {
addMessage(data.messages.join("<br />"), "danger");
}
}
else {
/* if the post was not successful wait for the modal to disappear
* then append the new modal
*/
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('body').append(data);
$('#confirmation-modal').modal('show');
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
});
}
},
error: function(xhr, textStatus, error) {
$('#confirmation-modal').modal("hide");
if (xhr.status == 500) {
addMessage("500 Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
}
});
return false;
});
}); });
...@@ -5,19 +5,20 @@ $(function() { ...@@ -5,19 +5,20 @@ $(function() {
} }
$('a[href="#activity"]').click(function(){ $('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('icon-spin'); $('a[href="#activity"] i').addClass('icon-spin');
checkNewActivity(false,0); checkNewActivity(false, 1);
}); });
/* save resources */ /* save resources */
$('#vm-details-resources-save').click(function() { $('#vm-details-resources-save').click(function() {
$('i.icon-save', this).removeClass("icon-save").addClass("icon-refresh icon-spin"); $('i.icon-save', this).removeClass("icon-save").addClass("icon-refresh icon-spin");
var vm = $(this).data("vm");
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: location.href, url: "/dashboard/vm/" + vm + "/op/resources_change/",
data: $('#vm-details-resources-form').serialize(), data: $('#vm-details-resources-form').serialize(),
success: function(data, textStatus, xhr) { success: function(data, textStatus, xhr) {
addMessage(data['message'], 'success');
$("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save"); $("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save");
$('a[href="#activity"]').trigger("click");
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
$("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save"); $("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save");
...@@ -328,6 +329,17 @@ function decideActivityRefresh() { ...@@ -328,6 +329,17 @@ function decideActivityRefresh() {
return check; return check;
} }
/* unescapes html got via the request, also removes whitespaces and replaces all ' with " */
function unescapeHTML(html) {
return html.replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&').replace(/&ndash;/g, "–").replace(/\//g, "").replace(/'/g, '"').replace(/&#39;/g, "'").replace(/ /g, '');
}
/* the html page contains some tags that were modified via js (titles for example), we delete these
also some html tags are closed with / */
function changeHTML(html) {
return html.replace(/data-original-title/g, "title").replace(/title=""/g, "").replace(/\//g, '').replace(/ /g, '');
}
function checkNewActivity(only_status, runs) { function checkNewActivity(only_status, runs) {
// set default only_status to false // set default only_status to false
only_status = typeof only_status !== 'undefined' ? only_status : false; only_status = typeof only_status !== 'undefined' ? only_status : false;
...@@ -339,8 +351,12 @@ function checkNewActivity(only_status, runs) { ...@@ -339,8 +351,12 @@ function checkNewActivity(only_status, runs) {
data: {'only_status': only_status}, data: {'only_status': only_status},
success: function(data) { success: function(data) {
if(!only_status) { if(!only_status) {
$("#activity-timeline").html(data['activities']); a = unescapeHTML(data['activities']);
b = changeHTML($("#activity-timeline").html());
if(a != b)
$("#activity-timeline").html(data['activities']);
$("#ops").html(data['ops']); $("#ops").html(data['ops']);
$("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip(); $("[title]").tooltip();
} }
...@@ -352,6 +368,14 @@ function checkNewActivity(only_status, runs) { ...@@ -352,6 +368,14 @@ function checkNewActivity(only_status, runs) {
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled"); $("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
} }
if(data['status'] == "STOPPED") {
$(".enabled-when-stopped").prop("disabled", false);
$(".hide-when-stopped").hide();
} else {
$(".enabled-when-stopped").prop("disabled", true);
$(".hide-when-stopped").show();
}
if(runs > 0 && decideActivityRefresh()) { if(runs > 0 && decideActivityRefresh()) {
setTimeout( setTimeout(
function() {checkNewActivity(only_status, runs + 1)}, function() {checkNewActivity(only_status, runs + 1)},
......
...@@ -9,8 +9,7 @@ ...@@ -9,8 +9,7 @@
<title>{% block title %}{% block title-page %}{% endblock %} | {% block title-site %}CIRCLE{% endblock %}{% endblock %}</title> <title>{% block title %}{% block title-page %}{% endblock %} | {% block title-site %}CIRCLE{% endblock %}{% endblock %}</title>
<script src="//code.jquery.com/jquery-1.10.2.min.js"></script> <script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="//code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
......
...@@ -14,7 +14,12 @@ ...@@ -14,7 +14,12 @@
</h3> </h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{{ body|safe|default:"(body missing from context.)" }} {% if template %}
{% include template %}
{% else %}
{{ body|safe|default:"(body missing from context.)" }}
{% endif %}
</div> </div>
</div>
</div> </div>
{% endblock %} {% endblock %}
...@@ -3,7 +3,11 @@ ...@@ -3,7 +3,11 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-body"> <div class="modal-body">
{{ body|safe|default:"(body missing from context.)" }} {% if template %}
{% include template %}
{% else %}
{{ body|safe|default:"(body missing from context.)" }}
{% endif %}
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
......
...@@ -16,10 +16,12 @@ ...@@ -16,10 +16,12 @@
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
{% endfor %} {% endfor %}
{% if perms.vm.create_base_template %}
<div class="panel panel-default template-choose-list-element"> <div class="panel panel-default template-choose-list-element">
<input type="radio" name="parent" value="base_vm"/> <input type="radio" name="parent" value="base_vm"/>
{% trans "Create a new base VM without disk" %} {% trans "Create a new base VM without disk" %}
</div> </div>
{% endif %}
<button type="submit" id="template-choose-next-button" class="btn btn-success pull-right">{% trans "Next" %}</button> <button type="submit" id="template-choose-next-button" class="btn btn-success pull-right">{% trans "Next" %}</button>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
......
{% extends "base.html" %}
{% load i18n %}
{% block title-site %}Dashboard | CIRCLE{% endblock %}
{% block content %}
{% blocktrans with group=object member=member %}
Do you really want to remove {{member}} from {{group}}?
{% endblocktrans %}
<form action="" method="POST">{% csrf_token %}
<input type="submit" value="{% trans "Remove" %}" />
</form>
{% endblock %}
...@@ -48,6 +48,8 @@ ...@@ -48,6 +48,8 @@
{% crispy group_profile_form %} {% crispy group_profile_form %}
</form> </form>
<hr />
<h3>{% trans "User list"|capfirst %} <h3>{% trans "User list"|capfirst %}
{% if perms.auth.add_user %} {% if perms.auth.add_user %}
<a href="{% url "dashboard.views.create-user" group.pk %}" class="btn btn-success pull-right">{% trans "Create user" %}</a> <a href="{% url "dashboard.views.create-user" group.pk %}" class="btn btn-success pull-right">{% trans "Create user" %}</a>
...@@ -71,23 +73,37 @@ ...@@ -71,23 +73,37 @@
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
{% for i in future_users %}
<tr>
<td>
<i class="icon-user text-muted"></i>
</td>
<td> {{ i.org_id }} </td>
<td>
<a href="{% url "dashboard.views.remove-future-user" member_org_id=i.org_id group_pk=group.pk %}"
class="real-link btn-link btn-xs">