Commit 15033f02 authored by Kostas Papadimitriou's avatar Kostas Papadimitriou
Browse files

Merge branch 'tmp-common-email-recipients' into develop

parents 69b74a54 a78f88cc
......@@ -46,7 +46,8 @@ from astakos.im.models import AstakosUser, Service, Resource
from astakos.im.api.faults import (
Fault, ItemNotFound, InternalServerError, BadRequest)
from astakos.im.settings import (
INVITATIONS_ENABLED, COOKIE_NAME, EMAILCHANGE_ENABLED, QUOTAHOLDER_URL)
INVITATIONS_ENABLED, COOKIE_NAME, EMAILCHANGE_ENABLED, QUOTAHOLDER_URL,
PROJECTS_VISIBLE)
from astakos.im.forms import FeedbackForm
from astakos.im.functions import send_feedback as send_feedback_func
......@@ -156,7 +157,7 @@ def get_menu(request, with_extra_links=False, with_signout=True):
append(item(
url=absolute(request, reverse('resource_usage')),
name="Usage"))
if QUOTAHOLDER_URL:
if QUOTAHOLDER_URL and PROJECTS_VISIBLE:
append(item(
url=absolute(request, reverse('project_list')),
name="Projects"))
......@@ -255,6 +256,7 @@ def __send_feedback(request, email_template_name='im/feedback_mail.txt', user=No
form = FeedbackForm(request.POST)
if not form.is_valid():
logger.error("Invalid feedback request: %r", form.errors)
raise BadRequest('Invalid data')
msg = form.cleaned_data['feedback_msg']
......
......@@ -61,7 +61,7 @@ from astakos.im.models import (
ProjectApplication, Project)
from astakos.im.settings import (
INVITATIONS_PER_LEVEL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY,
RECAPTCHA_ENABLED, DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL,
RECAPTCHA_ENABLED, CONTACT_EMAIL, LOGGING_LEVEL,
PASSWORD_RESET_EMAIL_SUBJECT, NEWPASSWD_INVALIDATE_TOKEN,
MODERATION_ENABLED, PROJECT_MEMBER_JOIN_POLICIES,
PROJECT_MEMBER_LEAVE_POLICIES, EMAILCHANGE_ENABLED,
......@@ -546,7 +546,7 @@ class ExtendedPasswordResetForm(PasswordResetForm):
'site_name': SITENAME,
'user': user,
'baseurl': BASEURL,
'support': DEFAULT_CONTACT_EMAIL
'support': CONTACT_EMAIL
}
from_email = settings.SERVER_EMAIL
send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT),
......@@ -885,7 +885,8 @@ class ProjectApplicationForm(forms.ModelForm):
def save(self, commit=True):
data = dict(self.cleaned_data)
data['precursor_application'] = self.instance.id
data['owner'] = self.user
is_new = self.instance.id is None
data['owner'] = self.user if is_new else self.instance.owner
data['resource_policies'] = self.resource_policies
submit_application(data, request_user=self.user)
......
......@@ -56,7 +56,7 @@ from functools import wraps
import astakos.im.settings as astakos_settings
from astakos.im.settings import (
DEFAULT_CONTACT_EMAIL, SITENAME, BASEURL, LOGGING_LEVEL,
CONTACT_EMAIL, SITENAME, BASEURL, LOGGING_LEVEL,
VERIFICATION_EMAIL_SUBJECT, ACCOUNT_CREATION_SUBJECT,
GROUP_CREATION_SUBJECT, HELPDESK_NOTIFICATION_EMAIL_SUBJECT,
INVITATION_EMAIL_SUBJECT, GREETING_EMAIL_SUBJECT, FEEDBACK_EMAIL_SUBJECT,
......@@ -64,7 +64,8 @@ from astakos.im.settings import (
PROJECT_CREATION_SUBJECT, PROJECT_APPROVED_SUBJECT,
PROJECT_TERMINATION_SUBJECT, PROJECT_SUSPENSION_SUBJECT,
PROJECT_MEMBERSHIP_CHANGE_SUBJECT,
PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES)
PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES, HELPDESK,
ADMINS, MANAGERS)
from astakos.im.notifications import build_notification, NotificationError
from astakos.im.models import (
AstakosUser, Invitation, ProjectMembership, ProjectApplication, Project,
......@@ -115,7 +116,7 @@ def send_verification(user, template_name='im/activation_email.txt'):
'url': url,
'baseurl': BASEURL,
'site_name': SITENAME,
'support': DEFAULT_CONTACT_EMAIL})
'support': CONTACT_EMAIL})
sender = settings.SERVER_EMAIL
try:
send_mail(_(VERIFICATION_EMAIL_SUBJECT), message, sender, [user.email],
......@@ -139,17 +140,16 @@ def _send_admin_notification(template_name,
dictionary=None,
subject='alpha2 testing notification',):
"""
Send notification email to settings.ADMINS.
Send notification email to settings.HELPDESK + settings.MANAGERS.
Raises SendNotificationError
"""
if not settings.ADMINS:
return
dictionary = dictionary or {}
message = render_to_string(template_name, dictionary)
sender = settings.SERVER_EMAIL
recipient_list = [e[1] for e in HELPDESK + MANAGERS]
try:
send_mail(subject, message, sender, [i[1] for i in settings.ADMINS],
send_mail(subject, message, sender, recipient_list,
connection=get_connection())
except (SMTPException, socket.error) as e:
logger.exception(e)
......@@ -168,21 +168,19 @@ def send_account_creation_notification(template_name, dictionary=None):
def send_helpdesk_notification(user, template_name='im/helpdesk_notification.txt'):
"""
Send email to DEFAULT_CONTACT_EMAIL to notify for a new user activation.
Send email to settings.HELPDESK list to notify for a new user activation.
Raises SendNotificationError
"""
if not DEFAULT_CONTACT_EMAIL:
return
message = render_to_string(
template_name,
{'user': user}
)
sender = settings.SERVER_EMAIL
recipient_list = [e[1] for e in HELPDESK + MANAGERS]
try:
send_mail(_(HELPDESK_NOTIFICATION_EMAIL_SUBJECT) % {'user': user.email},
message, sender, [DEFAULT_CONTACT_EMAIL],
connection=get_connection())
message, sender, recipient_list, connection=get_connection())
except (SMTPException, socket.error) as e:
logger.exception(e)
raise SendNotificationError()
......@@ -204,7 +202,7 @@ def send_invitation(invitation, template_name='im/invitation.txt'):
'url': url,
'baseurl': BASEURL,
'site_name': SITENAME,
'support': DEFAULT_CONTACT_EMAIL})
'support': CONTACT_EMAIL})
sender = settings.SERVER_EMAIL
try:
send_mail(subject, message, sender, [invitation.username],
......@@ -232,7 +230,7 @@ def send_greeting(user, email_template_name='im/welcome_email.txt'):
'url': urljoin(BASEURL, reverse('index')),
'baseurl': BASEURL,
'site_name': SITENAME,
'support': DEFAULT_CONTACT_EMAIL})
'support': CONTACT_EMAIL})
sender = settings.SERVER_EMAIL
try:
send_mail(subject, message, sender, [user.email],
......@@ -248,7 +246,7 @@ def send_greeting(user, email_template_name='im/welcome_email.txt'):
def send_feedback(msg, data, user, email_template_name='im/feedback_mail.txt'):
subject = _(FEEDBACK_EMAIL_SUBJECT)
from_email = settings.SERVER_EMAIL
recipient_list = [DEFAULT_CONTACT_EMAIL]
recipient_list = [e[1] for e in HELPDESK]
content = render_to_string(email_template_name, {
'message': msg,
'data': data,
......@@ -271,7 +269,7 @@ def send_change_email(
url = request.build_absolute_uri(url)
t = loader.get_template(email_template_name)
c = {'url': url, 'site_name': SITENAME,
'support': DEFAULT_CONTACT_EMAIL, 'ec': ec}
'support': CONTACT_EMAIL, 'ec': ec}
from_email = settings.SERVER_EMAIL
send_mail(_(EMAIL_CHANGE_EMAIL_SUBJECT), t.render(Context(c)),
from_email, [ec.new_email_address],
......@@ -533,6 +531,8 @@ def accept_membership(project_id, user, request_user=None):
raise PermissionDenied(m)
membership.accept()
logger.info("User %s has been accepted in %s." %
(membership.person.log_display, project))
membership_change_notify(project, membership.person, 'accepted')
......@@ -551,6 +551,8 @@ def reject_membership(project_id, user, request_user=None):
raise PermissionDenied(m)
membership.reject()
logger.info("Request of user %s for %s has been rejected." %
(membership.person.log_display, project))
membership_change_notify(project, membership.person, 'rejected')
......@@ -568,6 +570,8 @@ def cancel_membership(project_id, user_id):
raise PermissionDenied(m)
membership.cancel()
logger.info("Request of user %s for %s has been cancelled." %
(membership.person.log_display, project))
def remove_membership_checks(project, request_user=None):
checkAllowed(project, request_user)
......@@ -586,6 +590,8 @@ def remove_membership(project_id, user, request_user=None):
raise PermissionDenied(m)
membership.remove()
logger.info("User %s has been removed from %s." %
(membership.person.log_display, project))
membership_change_notify(project, membership.person, 'removed')
......@@ -601,6 +607,8 @@ def enroll_member(project_id, user, request_user=None):
raise PermissionDenied(m)
membership.accept()
logger.info("User %s has been enrolled in %s." %
(membership.person.log_display, project))
membership_enroll_notify(project, membership.person)
return membership
......@@ -635,9 +643,13 @@ def leave_project(project_id, user_id):
leave_policy = project.application.member_leave_policy
if leave_policy == AUTO_ACCEPT_POLICY:
membership.remove()
logger.info("User %s has left %s." %
(membership.person.log_display, project))
auto_accepted = True
else:
membership.leave_request()
logger.info("User %s requested to leave %s." %
(membership.person.log_display, project))
membership_leave_request_notify(project, membership.person)
return auto_accepted
......@@ -667,9 +679,13 @@ def join_project(project_id, user_id):
if (join_policy == AUTO_ACCEPT_POLICY and
not project.violates_members_limit(adding=1)):
membership.accept()
logger.info("User %s joined %s." %
(membership.person.log_display, project))
auto_accepted = True
else:
membership_request_notify(project, membership.person)
logger.info("User %s requested to join %s." %
(membership.person.log_display, project))
return auto_accepted
......@@ -692,8 +708,9 @@ def submit_application(kw, request_user=None):
m = _(astakos_messages.NOT_ALLOWED)
raise PermissionDenied(m)
reached, limit = reached_pending_application_limit(request_user.id, precursor)
if reached:
owner = kw['owner']
reached, limit = reached_pending_application_limit(owner.id, precursor)
if not request_user.is_project_admin() and reached:
m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
raise PermissionDenied(m)
......@@ -713,6 +730,8 @@ def submit_application(kw, request_user=None):
application.save()
application.resource_policies = resource_policies
logger.info("User %s submitted %s." %
(request_user.log_display, application.log_display))
application_submit_notify(application)
return application
......@@ -726,6 +745,7 @@ def cancel_application(application_id, request_user=None):
raise PermissionDenied(m)
application.cancel()
logger.info("%s has been cancelled." % (application.log_display))
def dismiss_application(application_id, request_user=None):
application = get_application_for_update(application_id)
......@@ -737,6 +757,7 @@ def dismiss_application(application_id, request_user=None):
raise PermissionDenied(m)
application.dismiss()
logger.info("%s has been dismissed." % (application.log_display))
def deny_application(application_id, reason=None):
application = get_application_for_update(application_id)
......@@ -749,6 +770,8 @@ def deny_application(application_id, reason=None):
if reason is None:
reason = ""
application.deny(reason)
logger.info("%s has been denied with reason \"%s\"." %
(application.log_display, reason))
application_deny_notify(application)
def approve_application(app_id):
......@@ -766,6 +789,7 @@ def approve_application(app_id):
raise PermissionDenied(m)
application.approve()
logger.info("%s has been approved." % (application.log_display))
application_approve_notify(application)
def check_expiration(execute=False):
......@@ -782,7 +806,7 @@ def terminate(project_id):
checkAlive(project)
project.terminate()
logger.info("%s has been terminated." % (project))
project_termination_notify(project)
def suspend(project_id):
......@@ -790,7 +814,7 @@ def suspend(project_id):
checkAlive(project)
project.suspend()
logger.info("%s has been suspended." % (project))
project_suspension_notify(project)
def resume(project_id):
......@@ -801,6 +825,7 @@ def resume(project_id):
raise PermissionDenied(m)
project.resume()
logger.info("%s has been unsuspended." % (project))
def get_by_chain_or_404(chain_id):
try:
......@@ -860,7 +885,7 @@ def _reached_pending_application_limit(user_id):
PENDING = ProjectApplication.PENDING
pending = ProjectApplication.objects.filter(
applicant__id=user_id, state=PENDING).count()
owner__id=user_id, state=PENDING).count()
return pending >= limit, limit
......
......@@ -133,7 +133,7 @@ class Command(NoArgsCommand):
self.show(csv, allow_shorten, chain_dict)
def show(self, csv, allow_shorten, chain_dict):
labels = ('ProjID', 'Name', 'Applicant', 'Email', 'Status', 'AppID')
labels = ('ProjID', 'Name', 'Owner', 'Email', 'Status', 'AppID')
columns = (7, 23, 20, 20, 17, 7)
if not csv:
......@@ -147,7 +147,7 @@ class Command(NoArgsCommand):
fields = [
(info['projectid'], False),
(info['name'], True),
(info['applicant'], True),
(info['owner'], True),
(info['email'], True),
(info['status'], False),
(info['appid'], False),
......@@ -190,8 +190,8 @@ def chain_info(chain_dict):
d = {
'projectid': str(chain),
'name': project.application.name if project else app.name,
'applicant': app.applicant.realname,
'email': app.applicant.email,
'owner': app.owner.realname,
'email': app.owner.email,
'status': status,
'appid': appid,
}
......
......@@ -1667,6 +1667,11 @@ class ProjectApplication(models.Model):
CANCELLED: _('Cancelled')
}
@property
def log_display(self):
return "application %s (%s) for project %s" % (
self.id, self.name, self.chain)
def get_project(self):
try:
project = Project.objects.get(id=self.chain, state=Project.APPROVED)
......
......@@ -16,7 +16,7 @@ MEM_ENROLL_NOTIF = {
}
SENDER = settings.SERVER_EMAIL
ADMINS = settings.ADMINS
NOTIFY_RECIPIENTS = [e[1] for e in settings.MANAGERS + settings.HELPDESK]
def membership_change_notify(project, user, action):
try:
......@@ -69,8 +69,7 @@ def membership_leave_request_notify(project, requested_user):
def application_submit_notify(application):
try:
notification = build_notification(
SENDER,
[i[1] for i in ADMINS],
SENDER, NOTIFY_RECIPIENTS,
_(settings.PROJECT_CREATION_SUBJECT) % application.__dict__,
template='im/projects/project_creation_notification.txt',
dictionary={'object':application})
......
......@@ -14,12 +14,12 @@ INVITATIONS_PER_LEVEL = getattr(settings, 'ASTAKOS_INVITATIONS_PER_LEVEL', {
4: 0
})
# Address to use for outgoing emails
DEFAULT_CONTACT_EMAIL = getattr(
settings, 'ASTAKOS_DEFAULT_CONTACT_EMAIL', 'support@example.synnefo.org')
ADMINS = getattr(settings, 'ADMINS', ())
MANAGERS = getattr(settings, 'MANAGERS', ADMINS)
HELPDESK = getattr(settings, 'HELPDESK', ADMINS)
SERVER_EMAIL = getattr(settings, 'SERVER_EMAIL', None)
ADMINS = getattr(settings, 'ADMINS', None)
CONTACT_EMAIL = settings.CONTACT_EMAIL
SERVER_EMAIL = settings.SERVER_EMAIL
# Identity Management enabled modules
# Supported modules are: 'local', 'twitter' and 'shibboleth'
......@@ -335,6 +335,7 @@ ACTIVATION_REDIRECT_URL = getattr(settings,
'ASTAKOS_ACTIVATION_REDIRECT_URL',
"/im/landing")
# If true, this enables a ui compatibility layer for the introduction of UUIDs
# in identity management. WARNING: Setting to True will break your installation.
TRANSLATE_UUIDS = getattr(settings, 'ASTAKOS_TRANSLATE_UUIDS', False)
......@@ -362,7 +363,9 @@ GOOGLE_SECRET = getattr(settings, 'ASTAKOS_GOOGLE_SECRET', '')
LINKEDIN_TOKEN = getattr(settings, 'ASTAKOS_LINKEDIN_TOKEN', '')
LINKEDIN_SECRET = getattr(settings, 'ASTAKOS_LINKEDIN_SECRET', '')
# Where to redirect the user after successful login when no next parameter is
# set
# URL to redirect the user after successful login when no next parameter is set
LOGIN_SUCCESS_URL = getattr(settings, 'ASTAKOS_LOGIN_SUCCESS_URL',
'/im/landing')
# Whether or not to display projects in astakos menu
PROJECTS_VISIBLE = getattr(settings, 'ASTAKOS_PROJECTS_VISIBLE', False)
......@@ -108,7 +108,7 @@ def login(
if not eppn:
raise KeyError(_(astakos_messages.SHIBBOLETH_MISSING_EPPN) % {
'domain': settings.BASEURL,
'contact_email': settings.DEFAULT_CONTACT_EMAIL
'contact_email': settings.CONTACT_EMAIL
})
if Tokens.SHIB_DISPLAYNAME in tokens:
realname = tokens[Tokens.SHIB_DISPLAYNAME]
......
......@@ -182,7 +182,7 @@ class ShibbolethTests(TestCase):
r = client.get('/im/login/shibboleth?', follow=True)
self.assertContains(r, messages.SHIBBOLETH_MISSING_EPPN % {
'domain': astakos_settings.BASEURL,
'contact_email': astakos_settings.DEFAULT_CONTACT_EMAIL
'contact_email': settings.CONTACT_EMAIL
})
client.set_tokens(eppn="kpapeppn")
......
......@@ -1051,7 +1051,7 @@ def project_add(request):
user = request.user
reached, limit = reached_pending_application_limit(user.id)
if reached:
if not user.is_project_admin() and reached:
m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
messages.error(request, m)
next = reverse('astakos.im.views.project_list')
......@@ -1167,8 +1167,9 @@ def project_modify(request, application_id):
m = _(astakos_messages.NOT_ALLOWED)
raise PermissionDenied(m)
reached, limit = reached_pending_application_limit(user.id, app)
if reached:
owner_id = app.owner_id
reached, limit = reached_pending_application_limit(owner_id, app)
if not user.is_project_admin() and reached:
m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
messages.error(request, m)
next = reverse('astakos.im.views.project_list')
......
......@@ -12,12 +12,6 @@
# 4 : 0
#}
# Address to use for outgoing emails
#ASTAKOS_DEFAULT_CONTACT_EMAIL = 'support@example.synnefo.org'
#SERVER_EMAIL = None
#ADMINS = None
# Identity Management enabled modules
# Supported modules are: 'local', 'twitter' and 'shibboleth'
#ASTAKOS_IM_MODULES = ['local']
......@@ -307,3 +301,9 @@
# OAuth2 LinkedIn credentials.
#ASTAKOS_LINKEDIN_TOKEN = ''
#ASTAKOS_LINKEDIN_SECRET = ''
# URL to redirect the user after successful login when no next parameter is set
# ASTAKOS_LOGIN_SUCCESS_URL = '/im/landing'
# Whether or not to display projects in astakos menu
# ASTAKOS_PROJECTS_VISIBLE = False
......@@ -3,10 +3,17 @@
## Admin names and email addresses
###################################
#
# List of people who receive application notifications, such as code error
# tracebacks. It is recommended to have at least one entry in this list.
#ADMINS = (
# # ('Your Name', 'your_email@domain.com'),
# ('Your Name', 'your_email@domain.com'),
#)
#
# List of people to receive user feedback notifications.
#HELPDESK = ADMINS
#
# A list of people to receive email notifications on some application events
# (e.g. account creation/activation).
#MANAGERS = ADMINS
#
## Email configuration
......@@ -18,3 +25,6 @@
#
## Address to use for outgoing emails
#DEFAULT_FROM_EMAIL = "~okeanos <no-reply@grnet.gr>"
#
# Email where users can contact for support
#CONTACT_EMAIL = "support@synnefo.org"
......@@ -3,10 +3,17 @@
# Admin names and email addresses
##################################
# List of people who receive application notifications, such as code error
# tracebacks. It is recommended to have at least one entry in this list.
ADMINS = (
# ('Your Name', 'your_email@domain.com'),
)
# List of people to receive user feedback notifications.
HELPDESK = ADMINS
# A list of people to receive email notifications on some application events
# (e.g. account creation/activation).
MANAGERS = ADMINS
# Email configuration
......@@ -18,3 +25,6 @@ DEFAULT_CHARSET = 'utf-8'
# Address to use for outgoing emails
DEFAULT_FROM_EMAIL = "~okeanos <no-reply@grnet.gr>"
# Email where users can contact for support
CONTACT_EMAIL = "support@synnefo.org"
......@@ -131,3 +131,6 @@
# endpoints. Set this to False if you deploy cyclades-app/astakos-app on the
# same machine.
#CYCLADES_PROXY_USER_SERVICES = True
#
## Astakos feedback endpoint.
#CYCLADES_USER_FEEDBACK_URL = 'https://accounts.example.synnefo.org/feedback'
......@@ -42,14 +42,6 @@
## How often to check for user usage changes
#UI_QUOTAS_UPDATE_INTERVAL = 10000
#
## List of emails used for sending the feedback messages to (following the ADMINS format)
#FEEDBACK_CONTACTS = (
# # ('Contact Name', 'contact_email@domain.com'),
#)
#
## Email from which the feedback emails will be sent from
#FEEDBACK_EMAIL_FROM = "~okeanos <no-reply@grnet.gr>"
#
## URL to redirect not authenticated users
#UI_LOGIN_URL = "/im/login"
#
......@@ -210,4 +202,3 @@
# 'admin@synnefo.gr': 'system',
# 'images@synnefo.gr': 'system'
#}
#
......@@ -71,6 +71,7 @@ def proxy(request, url, headers={}, body=None):
@csrf_exempt
def delegate_to_feedback_service(request):
logger.debug("Delegate feedback request to %s" % USER_FEEDBACK_URL)
token = request.META.get('HTTP_X_AUTH_TOKEN')
headers = {'X-Auth-Token': token}
return proxy(request, USER_FEEDBACK_URL, headers=headers,
......
......@@ -131,3 +131,6 @@ CYCLADES_USER_CATALOG_URL = 'https://<astakos domain>/user_catalogs'
# endpoints. Set this to False if you deploy cyclades-app/astakos-app on the
# same machine.
CYCLADES_PROXY_USER_SERVICES = True
# Astakos user_catalogs endpoint
CYCLADES_USER_FEEDBACK_URL = 'https://accounts.example.synnefo.org/feedback'
......@@ -42,14 +42,6 @@ UI_CHANGES_SINCE_ALIGNMENT = 0
# How often to check for user usage changes
UI_QUOTAS_UPDATE_INTERVAL = 10000
# List of emails used for sending the feedback messages to (following the ADMINS format)
FEEDBACK_CONTACTS = (
# ('Contact Name', 'contact_email@domain.com'),
)
# Email from which the feedback emails will be sent from
FEEDBACK_EMAIL_FROM = "~okeanos <no-reply@grnet.gr>"
# URL to redirect not authenticated users
UI_LOGIN_URL = "/im/login"
......@@ -211,3 +203,5 @@ UI_SYSTEM_IMAGES_OWNERS = {
'images@synnefo.gr': 'system'
}
# Astakos feedback endpoint. UI uses this setting to post error feedbacks
CYCLADES_USER_FEEDBACK_URL = 'https://accounts.synnefo.org/feedback'
......@@ -57,6 +57,9 @@
<dt>Build backendjobstatus</dt><dd>{{ vm.backendjobstatus }}</dd>
<dt>Build percentage</dt><dd>{{ vm.buildpercentage }}</dd>
</dl>
<dl class="dl-horizontal well">
{{ vm|backend_info|safe }}
</dl>
</div>
<div class="tab-pane" id="network{{ vm.pk }}">
<table class="table well">
......