Merge branch 'rbac' of github.com:ansible/ansible-tower into rbac

This commit is contained in:
Wayne Witzel III
2016-03-15 09:08:40 -04:00
51 changed files with 1416 additions and 317 deletions

View File

@@ -924,7 +924,6 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
def get_related(self, obj): def get_related(self, obj):
res = super(ProjectSerializer, self).get_related(obj) res = super(ProjectSerializer, self).get_related(obj)
res.update(dict( res.update(dict(
organizations = reverse('api:project_organizations_list', args=(obj.pk,)),
teams = reverse('api:project_teams_list', args=(obj.pk,)), teams = reverse('api:project_teams_list', args=(obj.pk,)),
playbooks = reverse('api:project_playbooks', args=(obj.pk,)), playbooks = reverse('api:project_playbooks', args=(obj.pk,)),
update = reverse('api:project_update_view', args=(obj.pk,)), update = reverse('api:project_update_view', args=(obj.pk,)),
@@ -936,6 +935,9 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer):
notifiers_error = reverse('api:project_notifiers_error_list', args=(obj.pk,)), notifiers_error = reverse('api:project_notifiers_error_list', args=(obj.pk,)),
access_list = reverse('api:project_access_list', args=(obj.pk,)), access_list = reverse('api:project_access_list', args=(obj.pk,)),
)) ))
if obj.organization:
res['organization'] = reverse('api:organization_detail',
args=(obj.organization.pk,))
# Backwards compatibility. # Backwards compatibility.
if obj.current_update: if obj.current_update:
res['current_update'] = reverse('api:project_update_detail', res['current_update'] = reverse('api:project_update_detail',

View File

@@ -44,7 +44,6 @@ project_urls = patterns('awx.api.views',
url(r'^$', 'project_list'), url(r'^$', 'project_list'),
url(r'^(?P<pk>[0-9]+)/$', 'project_detail'), url(r'^(?P<pk>[0-9]+)/$', 'project_detail'),
url(r'^(?P<pk>[0-9]+)/playbooks/$', 'project_playbooks'), url(r'^(?P<pk>[0-9]+)/playbooks/$', 'project_playbooks'),
url(r'^(?P<pk>[0-9]+)/organizations/$', 'project_organizations_list'),
url(r'^(?P<pk>[0-9]+)/teams/$', 'project_teams_list'), url(r'^(?P<pk>[0-9]+)/teams/$', 'project_teams_list'),
url(r'^(?P<pk>[0-9]+)/update/$', 'project_update_view'), url(r'^(?P<pk>[0-9]+)/update/$', 'project_update_view'),
url(r'^(?P<pk>[0-9]+)/project_updates/$', 'project_updates_list'), url(r'^(?P<pk>[0-9]+)/project_updates/$', 'project_updates_list'),

View File

@@ -829,13 +829,6 @@ class ProjectPlaybooks(RetrieveAPIView):
model = Project model = Project
serializer_class = ProjectPlaybooksSerializer serializer_class = ProjectPlaybooksSerializer
class ProjectOrganizationsList(SubListCreateAttachDetachAPIView):
model = Organization
serializer_class = OrganizationSerializer
parent_model = Project
relationship = 'organizations'
class ProjectTeamsList(SubListCreateAttachDetachAPIView): class ProjectTeamsList(SubListCreateAttachDetachAPIView):
model = Team model = Team

View File

@@ -1048,7 +1048,7 @@ class JobHostSummaryAccess(BaseAccess):
model = JobHostSummary model = JobHostSummary
def get_queryset(self): def get_queryset(self):
qs = self.model.accessible_objects(self.user, {'read':True}) qs = self.model.objects
qs = qs.select_related('job', 'job__job_template', 'host') qs = qs.select_related('job', 'job__job_template', 'host')
if self.user.is_superuser: if self.user.is_superuser:
return qs return qs
@@ -1073,7 +1073,7 @@ class JobEventAccess(BaseAccess):
model = JobEvent model = JobEvent
def get_queryset(self): def get_queryset(self):
qs = self.model.accessible_objects(self.user, {'read':True}) qs = self.model.objects
qs = qs.select_related('job', 'job__job_template', 'host', 'parent') qs = qs.select_related('job', 'job__job_template', 'host', 'parent')
qs = qs.prefetch_related('hosts', 'children') qs = qs.prefetch_related('hosts', 'children')
@@ -1110,7 +1110,7 @@ class UnifiedJobTemplateAccess(BaseAccess):
model = UnifiedJobTemplate model = UnifiedJobTemplate
def get_queryset(self): def get_queryset(self):
qs = self.model.accessible_objects(self.user, {'read':True}) qs = self.model.objects
project_qs = self.user.get_queryset(Project).filter(scm_type__in=[s[0] for s in Project.SCM_TYPE_CHOICES]) project_qs = self.user.get_queryset(Project).filter(scm_type__in=[s[0] for s in Project.SCM_TYPE_CHOICES])
inventory_source_qs = self.user.get_queryset(InventorySource).filter(source__in=CLOUD_INVENTORY_SOURCES) inventory_source_qs = self.user.get_queryset(InventorySource).filter(source__in=CLOUD_INVENTORY_SOURCES)
job_template_qs = self.user.get_queryset(JobTemplate) job_template_qs = self.user.get_queryset(JobTemplate)
@@ -1142,7 +1142,7 @@ class UnifiedJobAccess(BaseAccess):
model = UnifiedJob model = UnifiedJob
def get_queryset(self): def get_queryset(self):
qs = self.model.accessible_objects(self.user, {'read':True}) qs = self.model.objects
project_update_qs = self.user.get_queryset(ProjectUpdate) project_update_qs = self.user.get_queryset(ProjectUpdate)
inventory_update_qs = self.user.get_queryset(InventoryUpdate).filter(source__in=CLOUD_INVENTORY_SOURCES) inventory_update_qs = self.user.get_queryset(InventoryUpdate).filter(source__in=CLOUD_INVENTORY_SOURCES)
job_qs = self.user.get_queryset(Job) job_qs = self.user.get_queryset(Job)
@@ -1263,7 +1263,7 @@ class ActivityStreamAccess(BaseAccess):
model = ActivityStream model = ActivityStream
def get_queryset(self): def get_queryset(self):
qs = self.model.accessible_objects(self.user, {'read':True}) qs = self.model.objects
qs = qs.select_related('actor') qs = qs.select_related('actor')
qs = qs.prefetch_related('organization', 'user', 'inventory', 'host', 'group', 'inventory_source', qs = qs.prefetch_related('organization', 'user', 'inventory', 'host', 'group', 'inventory_source',
'inventory_update', 'credential', 'team', 'project', 'project_update', 'inventory_update', 'credential', 'team', 'project', 'project_update',

View File

@@ -200,4 +200,19 @@ class Migration(migrations.Migration):
name='rolepermission', name='rolepermission',
index_together=set([('content_type', 'object_id')]), index_together=set([('content_type', 'object_id')]),
), ),
migrations.RenameField(
model_name='organization',
old_name='projects',
new_name='deprecated_projects',
),
migrations.AlterField(
model_name='organization',
name='deprecated_projects',
field=models.ManyToManyField(related_name='deprecated_organizations', to='main.Project', blank=True),
),
migrations.AddField(
model_name='project',
name='organization',
field=models.ForeignKey(related_name='projects', to='main.Organization', blank=True, null=True),
),
] ]

View File

@@ -694,9 +694,9 @@ class ProjectAccess(BaseAccess):
if self.user.is_superuser: if self.user.is_superuser:
return qs return qs
team_ids = set(Team.objects.filter(deprecated_users__in=[self.user]).values_list('id', flat=True)) team_ids = set(Team.objects.filter(deprecated_users__in=[self.user]).values_list('id', flat=True))
qs = qs.filter(Q(created_by=self.user, organizations__isnull=True) | qs = qs.filter(Q(created_by=self.user, deprecated_organizations__isnull=True) |
Q(organizations__deprecated_admins__in=[self.user], organizations__active=True) | Q(deprecated_organizations__deprecated_admins__in=[self.user], deprecated_organizations__active=True) |
Q(organizations__deprecated_users__in=[self.user], organizations__active=True) | Q(deprecated_organizations__deprecated_users__in=[self.user], deprecated_organizations__active=True) |
Q(teams__in=team_ids)) Q(teams__in=team_ids))
allowed_deploy = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY] allowed_deploy = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY]
allowed_check = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY, PERM_INVENTORY_CHECK] allowed_check = [PERM_JOBTEMPLATE_CREATE, PERM_INVENTORY_DEPLOY, PERM_INVENTORY_CHECK]
@@ -726,9 +726,9 @@ class ProjectAccess(BaseAccess):
def can_change(self, obj, data): def can_change(self, obj, data):
if self.user.is_superuser: if self.user.is_superuser:
return True return True
if obj.created_by == self.user and not obj.organizations.filter(active=True).count(): if obj.created_by == self.user and not obj.deprecated_organizations.filter(active=True).count():
return True return True
if obj.organizations.filter(active=True, deprecated_admins__in=[self.user]).exists(): if obj.deprecated_organizations.filter(active=True, deprecated_admins__in=[self.user]).exists():
return True return True
return False return False
@@ -880,7 +880,7 @@ class JobTemplateAccess(BaseAccess):
Q(cloud_credential_id__in=credential_ids) | Q(cloud_credential__isnull=True), Q(cloud_credential_id__in=credential_ids) | Q(cloud_credential__isnull=True),
) )
org_admin_ids = base_qs.filter( org_admin_ids = base_qs.filter(
Q(project__organizations__deprecated_admins__in=[self.user]) | Q(project__deprecated_organizations__deprecated_admins__in=[self.user]) |
(Q(project__isnull=True) & Q(job_type=PERM_INVENTORY_SCAN) & Q(inventory__organization__deprecated_admins__in=[self.user])) (Q(project__isnull=True) & Q(job_type=PERM_INVENTORY_SCAN) & Q(inventory__organization__deprecated_admins__in=[self.user]))
) )
@@ -1097,7 +1097,7 @@ class JobAccess(BaseAccess):
credential_id__in=credential_ids, credential_id__in=credential_ids,
) )
org_admin_ids = base_qs.filter( org_admin_ids = base_qs.filter(
Q(project__organizations__deprecated_admins__in=[self.user]) | Q(project__deprecated_organizations__deprecated_admins__in=[self.user]) |
(Q(project__isnull=True) & Q(job_type=PERM_INVENTORY_SCAN) & Q(inventory__organization__deprecated_admins__in=[self.user])) (Q(project__isnull=True) & Q(job_type=PERM_INVENTORY_SCAN) & Q(inventory__organization__deprecated_admins__in=[self.user]))
) )

View File

@@ -131,9 +131,49 @@ def migrate_projects(apps, schema_editor):
Project = apps.get_model('main', 'Project') Project = apps.get_model('main', 'Project')
Permission = apps.get_model('main', 'Permission') Permission = apps.get_model('main', 'Permission')
JobTemplate = apps.get_model('main', 'JobTemplate')
for project in Project.objects.all(): # Migrate projects to single organizations, duplicating as necessary
if project.organizations.count() == 0 and project.created_by is not None: for project in [p for p in Project.objects.all()]:
original_project_name = project.name
project_orgs = project.deprecated_organizations.distinct().all()
if project_orgs.count() > 1:
first_org = None
for org in project_orgs:
if first_org is None:
# For the first org, re-use our existing Project object, so don't do the below duplication effort
first_org = org
project.name = first_org.name + ' - ' + original_project_name
project.organization = first_org
project.save()
else:
new_prj = Project.objects.create(
created = project.created,
description = project.description,
name = org.name + ' - ' + original_project_name,
old_pk = project.old_pk,
created_by_id = project.created_by_id,
scm_type = project.scm_type,
scm_url = project.scm_url,
scm_branch = project.scm_branch,
scm_clean = project.scm_clean,
scm_delete_on_update = project.scm_delete_on_update,
scm_delete_on_next_update = project.scm_delete_on_next_update,
scm_update_on_launch = project.scm_update_on_launch,
scm_update_cache_timeout = project.scm_update_cache_timeout,
credential = project.credential,
organization = org
)
migrations[original_project_name]['projects'].add(new_prj)
job_templates = JobTemplate.objects.filter(inventory__organization=org).all()
for jt in job_templates:
jt.project = new_prj
jt.save()
# Migrate permissions
for project in [p for p in Project.objects.all()]:
if project.organization is None and project.created_by is not None:
project.admin_role.members.add(project.created_by) project.admin_role.members.add(project.created_by)
migrations[project.name]['users'].add(project.created_by) migrations[project.name]['users'].add(project.created_by)
@@ -141,11 +181,10 @@ def migrate_projects(apps, schema_editor):
team.member_role.children.add(project.member_role) team.member_role.children.add(project.member_role)
migrations[project.name]['teams'].add(team) migrations[project.name]['teams'].add(team)
if project.organizations.count() > 0: if project.organization is not None:
for org in project.organizations.all(): for user in project.organization.deprecated_users.all():
for user in org.deprecated_users.all(): project.member_role.members.add(user)
project.member_role.members.add(user) migrations[project.name]['users'].add(user)
migrations[project.name]['users'].add(user)
for perm in Permission.objects.filter(project=project, active=True): for perm in Permission.objects.filter(project=project, active=True):
# All perms at this level just imply a user or team can read # All perms at this level just imply a user or team can read

View File

@@ -362,9 +362,9 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
success_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_success__in=[self, self.project])) success_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_success__in=[self, self.project]))
any_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_any__in=[self, self.project])) any_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_any__in=[self, self.project]))
# Get Organization Notifiers # Get Organization Notifiers
error_notifiers = set(error_notifiers + list(base_notifiers.filter(organization_notifiers_for_errors__in=self.project.organizations.all()))) error_notifiers = set(error_notifiers + list(base_notifiers.filter(organization_notifiers_for_errors=self.project.organization)))
success_notifiers = set(success_notifiers + list(base_notifiers.filter(organization_notifiers_for_success__in=self.project.organizations.all()))) success_notifiers = set(success_notifiers + list(base_notifiers.filter(organization_notifiers_for_success=self.project.organization)))
any_notifiers = set(any_notifiers + list(base_notifiers.filter(organization_notifiers_for_any__in=self.project.organizations.all()))) any_notifiers = set(any_notifiers + list(base_notifiers.filter(organization_notifiers_for_any=self.project.organization)))
return dict(error=list(error_notifiers), success=list(success_notifiers), any=list(any_notifiers)) return dict(error=list(error_notifiers), success=list(success_notifiers), any=list(any_notifiers))
class Job(UnifiedJob, JobOptions): class Job(UnifiedJob, JobOptions):

View File

@@ -48,10 +48,10 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin):
blank=True, blank=True,
related_name='admin_of_organizations', related_name='admin_of_organizations',
) )
projects = models.ManyToManyField( deprecated_projects = models.ManyToManyField(
'Project', 'Project',
blank=True, blank=True,
related_name='organizations', related_name='deprecated_organizations',
) )
admin_role = ImplicitRoleField( admin_role = ImplicitRoleField(
role_name='Organization Administrator', role_name='Organization Administrator',

View File

@@ -198,6 +198,13 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
app_label = 'main' app_label = 'main'
ordering = ('id',) ordering = ('id',)
organization = models.ForeignKey(
'Organization',
blank=True,
null=True,
on_delete=models.CASCADE,
related_name='projects',
)
scm_delete_on_next_update = models.BooleanField( scm_delete_on_next_update = models.BooleanField(
default=False, default=False,
editable=False, editable=False,
@@ -212,13 +219,13 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
admin_role = ImplicitRoleField( admin_role = ImplicitRoleField(
role_name='Project Administrator', role_name='Project Administrator',
role_description='May manage this project', role_description='May manage this project',
parent_role='organizations.admin_role', parent_role='organization.admin_role',
permissions = {'all': True} permissions = {'all': True}
) )
auditor_role = ImplicitRoleField( auditor_role = ImplicitRoleField(
role_name='Project Auditor', role_name='Project Auditor',
role_description='May read all settings associated with this project', role_description='May read all settings associated with this project',
parent_role='organizations.auditor_role', parent_role='organization.auditor_role',
permissions = {'read': True} permissions = {'read': True}
) )
member_role = ImplicitRoleField( member_role = ImplicitRoleField(
@@ -343,9 +350,9 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
success_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_success=self)) success_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_success=self))
any_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_any=self)) any_notifiers = list(base_notifiers.filter(unifiedjobtemplate_notifiers_for_any=self))
# Get Organization Notifiers # Get Organization Notifiers
error_notifiers = set(error_notifiers + list(base_notifiers.filter(organization_notifiers_for_errors__in=self.organizations.all()))) error_notifiers = set(error_notifiers + list(base_notifiers.filter(organization_notifiers_for_errors=self.organization)))
success_notifiers = set(success_notifiers + list(base_notifiers.filter(organization_notifiers_for_success__in=self.organizations.all()))) success_notifiers = set(success_notifiers + list(base_notifiers.filter(organization_notifiers_for_success=self.organization)))
any_notifiers = set(any_notifiers + list(base_notifiers.filter(organization_notifiers_for_any__in=self.organizations.all()))) any_notifiers = set(any_notifiers + list(base_notifiers.filter(organization_notifiers_for_any=self.organization)))
return dict(error=list(error_notifiers), success=list(success_notifiers), any=list(any_notifiers)) return dict(error=list(error_notifiers), success=list(success_notifiers), any=list(any_notifiers))
def get_absolute_url(self): def get_absolute_url(self):

View File

@@ -205,7 +205,7 @@ def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj):
hosts = hosts(host_count=1) hosts = hosts(host_count=1)
fact_scans(fact_scans=1) fact_scans(fact_scans=1)
team_obj.users.add(user_obj) team_obj.member_role.members.add(user_obj)
url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,)) url = reverse('api:host_fact_versions_list', args=(hosts[0].pk,))
response = get(url, user_obj) response = get(url, user_obj)
@@ -235,7 +235,7 @@ def test_super_user_ok(hosts, fact_scans, get, user, team):
@pytest.mark.django_db @pytest.mark.django_db
def test_user_admin_ok(organization, hosts, fact_scans, get, user, team): def test_user_admin_ok(organization, hosts, fact_scans, get, user, team):
user_admin = user('johnson', False) user_admin = user('johnson', False)
organization.admins.add(user_admin) organization.admin_role.members.add(user_admin)
response = _test_user_access_control(hosts, fact_scans, get, user_admin, team) response = _test_user_access_control(hosts, fact_scans, get, user_admin, team)
@@ -247,7 +247,7 @@ def test_user_admin_ok(organization, hosts, fact_scans, get, user, team):
def test_user_admin_403(organization, organizations, hosts, fact_scans, get, user, team): def test_user_admin_403(organization, organizations, hosts, fact_scans, get, user, team):
user_admin = user('johnson', False) user_admin = user('johnson', False)
org2 = organizations(1) org2 = organizations(1)
org2[0].admins.add(user_admin) org2[0].admin_role.members.add(user_admin)
response = _test_user_access_control(hosts, fact_scans, get, user_admin, team) response = _test_user_access_control(hosts, fact_scans, get, user_admin, team)

View File

@@ -132,7 +132,7 @@ def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj):
hosts = hosts(host_count=1) hosts = hosts(host_count=1)
fact_scans(fact_scans=1) fact_scans(fact_scans=1)
team_obj.users.add(user_obj) team_obj.member_role.members.add(user_obj)
url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,)) url = reverse('api:host_fact_compare_view', args=(hosts[0].pk,))
response = get(url, user_obj) response = get(url, user_obj)
@@ -162,7 +162,7 @@ def test_super_user_ok(hosts, fact_scans, get, user, team):
@pytest.mark.django_db @pytest.mark.django_db
def test_user_admin_ok(organization, hosts, fact_scans, get, user, team): def test_user_admin_ok(organization, hosts, fact_scans, get, user, team):
user_admin = user('johnson', False) user_admin = user('johnson', False)
organization.admins.add(user_admin) organization.admin_role.members.add(user_admin)
response = _test_user_access_control(hosts, fact_scans, get, user_admin, team) response = _test_user_access_control(hosts, fact_scans, get, user_admin, team)
@@ -174,7 +174,7 @@ def test_user_admin_ok(organization, hosts, fact_scans, get, user, team):
def test_user_admin_403(organization, organizations, hosts, fact_scans, get, user, team): def test_user_admin_403(organization, organizations, hosts, fact_scans, get, user, team):
user_admin = user('johnson', False) user_admin = user('johnson', False)
org2 = organizations(1) org2 = organizations(1)
org2[0].admins.add(user_admin) org2[0].admin_role.members.add(user_admin)
response = _test_user_access_control(hosts, fact_scans, get, user_admin, team) response = _test_user_access_control(hosts, fact_scans, get, user_admin, team)

View File

@@ -97,8 +97,9 @@ def project(instance, organization):
prj = Project.objects.create(name="test-proj", prj = Project.objects.create(name="test-proj",
description="test-proj-desc", description="test-proj-desc",
scm_type="git", scm_type="git",
scm_url="https://github.com/jlaska/ansible-playbooks") scm_url="https://github.com/jlaska/ansible-playbooks",
prj.organizations.add(organization) organization=organization
)
return prj return prj
@pytest.fixture @pytest.fixture

View File

@@ -86,7 +86,6 @@ def test_inherited_notifiers(get, post, user, organization, project):
u) u)
assert response.status_code == 201 assert response.status_code == 201
notifiers.append(response.data['id']) notifiers.append(response.data['id'])
organization.projects.add(project)
i = Inventory.objects.create(name='test', organization=organization) i = Inventory.objects.create(name='test', organization=organization)
i.save() i.save()
g = Group.objects.create(name='test', inventory=i) g = Group.objects.create(name='test', inventory=i)
@@ -109,7 +108,6 @@ def test_inherited_notifiers(get, post, user, organization, project):
@pytest.mark.django_db @pytest.mark.django_db
def test_notifier_merging(get, post, user, organization, project, notifier): def test_notifier_merging(get, post, user, organization, project, notifier):
user('admin-poster', True) user('admin-poster', True)
organization.projects.add(project)
organization.notifiers_any.add(notifier) organization.notifiers_any.add(notifier)
project.notifiers_any.add(notifier) project.notifiers_any.add(notifier)
assert len(project.notifiers['any']) == 1 assert len(project.notifiers['any']) == 1

View File

@@ -4,6 +4,7 @@ from awx.main.models import (
Role, Role,
RolePermission, RolePermission,
Organization, Organization,
Group,
) )
@@ -97,20 +98,24 @@ def test_team_symantics(organization, team, alice):
assert organization.accessible_by(alice, {'read': True}) is False assert organization.accessible_by(alice, {'read': True}) is False
@pytest.mark.django_db @pytest.mark.django_db
def test_auto_m2m_adjuments(organization, project, alice): def test_auto_m2m_adjuments(organization, inventory, group, alice):
'Ensures the auto role reparenting is working correctly through m2m maps' 'Ensures the auto role reparenting is working correctly through m2m maps'
organization.admin_role.members.add(alice) g1 = group(name='g1')
assert project.accessible_by(alice, {'read': True}) is True g1.admin_role.members.add(alice)
assert g1.accessible_by(alice, {'read': True}) is True
g2 = group(name='g2')
assert g2.accessible_by(alice, {'read': True}) is False
project.organizations.remove(organization) g2.parents.add(g1)
assert project.accessible_by(alice, {'read': True}) is False assert g2.accessible_by(alice, {'read': True}) is True
project.organizations.add(organization) g2.parents.remove(g1)
assert project.accessible_by(alice, {'read': True}) is True assert g2.accessible_by(alice, {'read': True}) is False
g1.children.add(g2)
assert g2.accessible_by(alice, {'read': True}) is True
g1.children.remove(g2)
assert g2.accessible_by(alice, {'read': True}) is False
organization.projects.remove(project)
assert project.accessible_by(alice, {'read': True}) is False
organization.projects.add(project)
assert project.accessible_by(alice, {'read': True}) is True
@pytest.mark.django_db @pytest.mark.django_db
def test_auto_field_adjuments(organization, inventory, team, alice): def test_auto_field_adjuments(organization, inventory, team, alice):

View File

@@ -16,7 +16,7 @@ def test_job_template_migration_check(deploy_jobtemplate, check_jobtemplate, use
joe = user('joe') joe = user('joe')
check_jobtemplate.project.organizations.all()[0].deprecated_users.add(joe) check_jobtemplate.project.organization.deprecated_users.add(joe)
Permission(user=joe, inventory=check_jobtemplate.inventory, permission_type='read').save() Permission(user=joe, inventory=check_jobtemplate.inventory, permission_type='read').save()
Permission(user=joe, inventory=check_jobtemplate.inventory, Permission(user=joe, inventory=check_jobtemplate.inventory,
@@ -45,7 +45,7 @@ def test_job_template_migration_deploy(deploy_jobtemplate, check_jobtemplate, us
joe = user('joe') joe = user('joe')
deploy_jobtemplate.project.organizations.all()[0].deprecated_users.add(joe) deploy_jobtemplate.project.organization.deprecated_users.add(joe)
Permission(user=joe, inventory=deploy_jobtemplate.inventory, permission_type='read').save() Permission(user=joe, inventory=deploy_jobtemplate.inventory, permission_type='read').save()
Permission(user=joe, inventory=deploy_jobtemplate.inventory, Permission(user=joe, inventory=deploy_jobtemplate.inventory,
@@ -77,7 +77,7 @@ def test_job_template_team_migration_check(deploy_jobtemplate, check_jobtemplate
team.organization = organization team.organization = organization
team.save() team.save()
check_jobtemplate.project.organizations.all()[0].deprecated_users.add(joe) check_jobtemplate.project.organization.deprecated_users.add(joe)
Permission(team=team, inventory=check_jobtemplate.inventory, permission_type='read').save() Permission(team=team, inventory=check_jobtemplate.inventory, permission_type='read').save()
Permission(team=team, inventory=check_jobtemplate.inventory, Permission(team=team, inventory=check_jobtemplate.inventory,
@@ -112,7 +112,7 @@ def test_job_template_team_deploy_migration(deploy_jobtemplate, check_jobtemplat
team.organization = organization team.organization = organization
team.save() team.save()
deploy_jobtemplate.project.organizations.all()[0].deprecated_users.add(joe) deploy_jobtemplate.project.organization.deprecated_users.add(joe)
Permission(team=team, inventory=deploy_jobtemplate.inventory, permission_type='read').save() Permission(team=team, inventory=deploy_jobtemplate.inventory, permission_type='read').save()
Permission(team=team, inventory=deploy_jobtemplate.inventory, Permission(team=team, inventory=deploy_jobtemplate.inventory,

View File

@@ -1,12 +1,95 @@
import pytest import pytest
from awx.main.migrations import _rbac as rbac from awx.main.migrations import _rbac as rbac
from awx.main.models import Role from awx.main.models import Role, Permission, Project, Organization, Credential, JobTemplate, Inventory
from awx.main.models.organization import Permission
from django.apps import apps from django.apps import apps
from awx.main.migrations import _old_access as old_access from awx.main.migrations import _old_access as old_access
@pytest.mark.django_db
def test_project_migration():
'''
o1 o2 o3 with o1 -- i1 o2 -- i2
\ | /
\ | /
c1 ---- p1
/ | \
/ | \
jt1 jt2 jt3
| | |
i1 i2 i1
goes to
o1
|
|
c1 ---- p1
/ |
/ |
jt1 jt3
| |
i1 i1
o2
|
|
c1 ---- p2
|
|
jt2
|
i2
o3
|
|
c1 ---- p3
'''
o1 = Organization.objects.create(name='o1')
o2 = Organization.objects.create(name='o2')
o3 = Organization.objects.create(name='o3')
c1 = Credential.objects.create(name='c1')
p1 = Project.objects.create(name='p1', credential=c1)
p1.deprecated_organizations.add(o1, o2, o3)
i1 = Inventory.objects.create(name='i1', organization=o1)
i2 = Inventory.objects.create(name='i2', organization=o2)
jt1 = JobTemplate.objects.create(name='jt1', project=p1, inventory=i1)
jt2 = JobTemplate.objects.create(name='jt2', project=p1, inventory=i2)
jt3 = JobTemplate.objects.create(name='jt3', project=p1, inventory=i1)
assert o1.projects.count() == 0
assert o2.projects.count() == 0
assert o3.projects.count() == 0
rbac.migrate_projects(apps, None)
jt1 = JobTemplate.objects.get(pk=jt1.pk)
jt2 = JobTemplate.objects.get(pk=jt2.pk)
jt3 = JobTemplate.objects.get(pk=jt3.pk)
assert jt1.project == jt3.project
assert jt1.project != jt2.project
assert o1.projects.count() == 1
assert o2.projects.count() == 1
assert o3.projects.count() == 1
assert o1.projects.all()[0].jobtemplates.count() == 2
assert o2.projects.all()[0].jobtemplates.count() == 1
assert o3.projects.all()[0].jobtemplates.count() == 0
@pytest.mark.django_db @pytest.mark.django_db
def test_project_user_project(user_project, project, user): def test_project_user_project(user_project, project, user):
u = user('owner') u = user('owner')

View File

@@ -39,7 +39,7 @@ class BaseAdHocCommandTest(BaseJobExecutionTest):
self.setup_instances() self.setup_instances()
self.setup_users() self.setup_users()
self.organization = self.make_organizations(self.super_django_user, 1)[0] self.organization = self.make_organizations(self.super_django_user, 1)[0]
self.organization.admins.add(self.normal_django_user) self.organization.admin_role.members.add(self.normal_django_user)
self.inventory = self.organization.inventories.create(name='test-inventory', description='description for test-inventory') self.inventory = self.organization.inventories.create(name='test-inventory', description='description for test-inventory')
self.host = self.inventory.hosts.create(name='host.example.com') self.host = self.inventory.hosts.create(name='host.example.com')
self.host2 = self.inventory.hosts.create(name='host2.example.com') self.host2 = self.inventory.hosts.create(name='host2.example.com')

View File

@@ -634,6 +634,13 @@ dd {
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 5px rgba(255, 88, 80, 0.6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 5px rgba(255, 88, 80, 0.6);
} }
.form-control.ng-dirty.ng-invalid + .select2 .select2-selection,
.form-control.ng-dirty.ng-invalid + .select2 .select2-selection:focus {
border-color: rgba(255, 88, 80, 0.8) !important;
outline: 0 !important;
box-shadow: none !important;
}
.form-control.ng-dirty.ng-pristine { .form-control.ng-dirty.ng-pristine {
border-color: @default-second-border; border-color: @default-second-border;
box-shadow: none; box-shadow: none;
@@ -2008,15 +2015,33 @@ tr td button i {
box-shadow: none; box-shadow: none;
} }
.form-control + .select2 .select2-selection {
border-color: @default-second-border !important;
background-color: #f6f6f6 !important;
color: @default-data-txt !important;
transition: border-color 0.3s !important;
box-shadow: none !important;
}
.form-control:active, .form-control:focus { .form-control:active, .form-control:focus {
box-shadow: none; box-shadow: none;
border-color: #167ec4; border-color: #167ec4;
} }
.form-control:active + .select2 .select2-selection, .form-control:focus + .select2 .select2-selection {
box-shadow: none !important;
border-color: #167ec4 !important;
}
.form-control.ng-dirty.ng-invalid, .form-control.ng-dirty.ng-invalid:focus { .form-control.ng-dirty.ng-invalid, .form-control.ng-dirty.ng-invalid:focus {
box-shadow: none; box-shadow: none;
} }
.form-control.ng-dirty.ng-invalid + .select2 .select2-selection, .form-control.ng-dirty.ng-invalid:focus + .select2 .select2-selection {
box-shadow: none !important;
}
.error { .error {
opacity: 1; opacity: 1;
transition: opacity 0.2s; transition: opacity 0.2s;
@@ -2041,3 +2066,7 @@ tr td button i {
.select2-container--disabled { .select2-container--disabled {
opacity: .35; opacity: .35;
} }
body.is-modalOpen {
overflow: hidden;
}

View File

@@ -41,6 +41,9 @@ table, tbody {
.List-tableHeader:last-of-type { .List-tableHeader:last-of-type {
border-top-right-radius: 5px; border-top-right-radius: 5px;
}
.List-tableHeader--actions {
text-align: right; text-align: right;
} }
@@ -320,6 +323,11 @@ table, tbody {
height: 34px; height: 34px;
} }
.List-searchWidget--compact {
max-width: ~"calc(100% - 91px)";
margin-top: 10px;
}
.List-searchRow { .List-searchRow {
margin-bottom: 20px; margin-bottom: 20px;
} }

View File

@@ -0,0 +1,212 @@
@import "../../shared/branding/colors.default.less";
/** @define AddPermissions */
.AddPermissions-backDrop {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 1041;
opacity: 0.2;
transition: 0.5s opacity;
background: @login-backdrop;
}
.AddPermissions-dialog {
margin: 30px auto;
margin-top: 95px;
}
.AddPermissions-content {
max-width: 750px;
margin: 0 auto;
border: 0;
box-shadow: none;
background-color: @login-bg;
border-radius: 4px;
transition: opacity 0.5s;
z-index: 1042;
position: relative;
opacity: 1;
}
.AddPermissions-header {
padding: 20px;
padding-bottom: 10px;
padding-top: 15px;
}
.AddPermissions-body {
padding: 0px 20px;
}
.AddPermissions-footer {
display: flex;
flex-wrap: wrap-reverse;
align-items: center;
padding: 20px;
padding-bottom: 0px;
padding-top: 20px;
}
.AddPermissions-list .List-searchRow {
height: 0px;
}
.AddPermissions-list .List-searchWidget {
height: 66px;
}
.AddPermissions-list .List-tableHeader:last-child {
border-top-right-radius: 5px;
}
.AddPermissions-list select-all {
display: none;
}
.AddPermissions-title {
margin-top: 5px;
margin-bottom: 20px;
}
.AddPermissions-buttons {
margin-left: auto;
margin-bottom: 20px;
}
.AddPermissions-directions {
margin-top: 10px;
margin-bottom: 20px;
color: #848992;
display: flex;
align-items: center;
}
.AddPermissions-directionNumber {
font-size: 14px;
font-weight: bold;
border-radius: 50%;
background-color: @default-list-header-bg;
padding: 2px 6px;
margin-right: 10px;
}
.AddPermissions-separator {
margin-top: 20px 0px;
width: 100%;
border-bottom: 1px solid @default-second-border;
}
.AddPermissions-roleRow {
display: flex;
margin-bottom: 10px;
align-items: center;
}
.AddPermissions-roleName {
width: 30%;
padding-right: 10px;
display: flex;
align-items: center;
}
.AddPermissions-roleNameVal {
font-size: 14px;
max-width: ~"calc(100% - 46px)";
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.AddPermissions-roleType {
border-radius: 5px;
padding: 0px 6px;
border: 1px solid @default-second-border;
font-size: 10px;
color: @default-interface-txt;
text-transform: uppercase;
background-color: @default-bg;
margin-left: 6px;
}
.AddPermissions-roleSelect {
width: ~"calc(70% - 40px)";
margin-right: 20px;
}
.AddPermissions-roleSelect .Form-dropDown {
height: inherit !important;
}
.AddPermissions-roleRemove {
border-radius: 50%;
padding: 5px 3px;
line-height: 11px;
color: @default-icon;
background-color: @default-tertiary-bg;
border: 0;
}
.AddPermissions-roleRemove:hover {
background-color: @default-err;
color: @default-bg;
}
.AddPermissions-selectHide {
display: none;
}
.AddPermissions .select2-search__field {
text-transform: uppercase;
}
.AddPermissions-keyToggle {
margin-left: auto;
text-transform: uppercase;
padding: 3px 9px;
font-size: 12px;
background-color: @default-bg;
border-radius: 5px;
color: @default-interface-txt;
border: 1px solid @default-second-border;
cursor: pointer;
}
.AddPermissions-keyToggle:hover {
background-color: @default-tertiary-bg;
}
.AddPermissions-keyToggle.is-active {
background-color: @default-link;
border-color: @default-link;
color: @default-bg;
}
.AddPermissions-keyPane {
margin: 20px 0;
border-radius: 5px;
padding: 15px;
padding-bottom: 0px;
border: 1px solid @default-second-border;
color: @default-interface-txt;
}
.AddPermissions-keyRow {
display: flex;
flex-direction: column;
margin-bottom: 15px;
}
.AddPermissions-keyName {
flex: 1 0 auto;
text-transform: uppercase;
font-weight: bold;
padding-bottom: 3px;
}
.AddPermissions-keyDescription {
flex: 1 0 auto;
}

View File

@@ -0,0 +1,177 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name controllers.function:Access
* @description
* Controller for handling permissions adding
*/
export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'ProcessErrors', function (rootScope, scope, GetBasePath, Rest, $q, Wait, ProcessErrors) {
var manuallyUpdateChecklists = function(list, id, isSelected) {
var elemScope = angular
.element("#" +
list + "s_table #" + id + ".List-tableRow input")
.scope();
if (elemScope) {
elemScope.isSelected = !!isSelected;
}
};
scope.allSelected = [];
// the object permissions are being added to
scope.object = scope[scope.$parent.list
.iterator + "_obj"];
// array for all possible roles for the object
scope.roles = Object
.keys(scope.object.summary_fields.roles)
.map(function(key) {
return {
value: scope.object.summary_fields
.roles[key].id,
label: scope.object.summary_fields
.roles[key].name };
});
// TODO: get working with api
// array w roles and descriptions for key
scope.roleKey = Object
.keys(scope.object.summary_fields.roles)
.map(function(key) {
return {
name: scope.object.summary_fields
.roles[key].name,
description: scope.object.summary_fields
.roles[key].description };
});
scope.showKeyPane = false;
scope.toggleKeyPane = function() {
scope.showKeyPane = !scope.showKeyPane;
};
// handle form tab changes
scope.toggleFormTabs = function(list) {
scope.usersSelected = (list === 'users');
scope.teamsSelected = !scope.usersSelected;
};
// manually handle selection/deselection of user/team checkboxes
scope.$on("selectedOrDeselected", function(e, val) {
val = val.value;
if (val.isSelected) {
// deselected, so remove from the allSelected list
scope.allSelected = scope.allSelected.filter(function(i) {
// return all but the object who has the id and type
// of the element to deselect
return (!(val.id === i.id && val.type === i.type));
});
} else {
// selected, so add to the allSelected list
scope.allSelected.push({
name: function() {
if (val.type === "user") {
return (val.first_name &&
val.last_name) ?
val.first_name + " " +
val.last_name :
val.username;
} else {
return val .name;
}
},
type: val.type,
roles: [],
id: val.id
});
}
});
// used to handle changes to the itemsSelected scope var on "next page",
// "sorting etc."
scope.$on("itemsSelected", function(e, inList) {
// compile a list of objects that needed to be checked in the lists
scope.updateLists = scope.allSelected.filter(function(inMemory) {
var notInList = true;
inList.forEach(function(val) {
// if the object is part of the allSelected list and is
// selected,
// you don't need to add it updateLists
if (inMemory.id === val.id &&
inMemory.type === val.type) {
notInList = false;
}
});
return notInList;
});
});
// handle changes to the updatedLists by manually selected those values in
// the UI
scope.$watch("updateLists", function(toUpdate) {
(toUpdate || []).forEach(function(obj) {
manuallyUpdateChecklists(obj.type, obj.id, true);
});
delete scope.updateLists;
});
// remove selected user/team
scope.removeObject = function(obj) {
manuallyUpdateChecklists(obj.type, obj.id, false);
scope.allSelected = scope.allSelected.filter(function(i) {
return (!(obj.id === i.id && obj.type === i.type));
});
};
// update post url list
scope.$watch("allSelected", function(val) {
scope.posts = _
.flatten((val || [])
.map(function (owner) {
var url = GetBasePath(owner.type + "s") + owner.id +
"/roles/";
return (owner.roles || [])
.map(function (role) {
return {url: url,
id: role.value};
});
}));
}, true);
// post roles to api
scope.updatePermissions = function() {
Wait('start');
var requests = scope.posts
.map(function(post) {
Rest.setUrl(post.url);
return Rest.post({"id": post.id});
});
$q.all(requests)
.then(function () {
Wait('stop');
rootScope.$broadcast("refreshList", "permission");
scope.closeModal();
}, function (error) {
Wait('stop');
rootScope.$broadcast("refreshList", "permission");
scope.closeModal();
ProcessErrors(null, error.data, error.status, null, {
hdr: 'Error!',
msg: 'Failed to post role(s): POST returned status' +
error.status
});
});
};
}];

View File

@@ -0,0 +1,58 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import addPermissionsController from './addPermissions.controller';
/* jshint unused: vars */
export default
[ 'templateUrl',
'Wait',
function(templateUrl, Wait) {
return {
restrict: 'E',
scope: true,
controller: addPermissionsController,
templateUrl: templateUrl('access/addPermissions/addPermissions'),
link: function(scope, element, attrs, ctrl) {
scope.toggleFormTabs('users');
$("body").addClass("is-modalOpen");
$("body").append(element);
Wait('start');
scope.$broadcast("linkLists");
setTimeout(function() {
$('#add-permissions-modal').modal("show");
}, 200);
$('.modal[aria-hidden=false]').each(function () {
if ($(this).attr('id') !== 'add-permissions-modal') {
$(this).modal('hide');
}
});
scope.closeModal = function() {
$("body").removeClass("is-modalOpen");
$('#add-permissions-modal').on('hidden.bs.modal',
function () {
$('.AddPermissions').remove();
});
$('#add-permissions-modal').modal('hide');
};
scope.$on('closePermissionsModal', function() {
scope.closeModal();
});
Wait('stop');
window.scrollTo(0,0);
}
};
}
];

View File

@@ -0,0 +1,118 @@
<div id="add-permissions-modal" class="AddPermissions modal fade">
<div class="AddPermissions-backDrop is-loggedOut"></div>
<div class="AddPermissions-dialog">
<div class="AddPermissions-content is-loggedOut">
<div class="AddPermissions-header">
<div class="List-header">
<div class="List-title">
<div class="List-titleText ng-binding">
{{ object.name }}
<div class="List-titleLockup"></div>
Add Permissions
</div>
</div>
<div class="Form-exitHolder">
<button class="Form-exit" ng-click="closeModal()">
<i class="fa fa-times-circle"></i>
</button>
</div>
</div>
</div>
<div class="AddPermissions-body">
<div class="AddPermissions-directions">
<span class="AddPermissions-directionNumber">
1.
</span>
Please select Users / Teams from the lists below.
</div>
<div class="Form-tabHolder">
<div id="users_tab" class="Form-tab"
ng-click="toggleFormTabs('users')"
ng-class="{'is-selected': usersSelected }">
Users
</div>
<div id="teams_tab" class="Form-tab"
ng-click="toggleFormTabs('teams')"
ng-class="{'is-selected': teamsSelected }"
>
Teams
</div>
</div>
<div class="AddPermissions-list" ng-show="usersSelected">
<add-permissions-list type="users">
</add-permissions-list>
</div>
<div class="AddPermissions-list" ng-show="teamsSelected">
<add-permissions-list type="teams">
</add-permissions-list>
</div>
<div class="AddPermissions-separator"
ng-show="allSelected && allSelected.length > 0"></div>
<div class="AddPermissions-directions"
ng-show="allSelected && allSelected.length > 0">
<span class="AddPermissions-directionNumber">
2.
</span>
Please assign roles to the selected users/teams
<div class="AddPermissions-keyToggle"
ng-class="{'is-active': showKeyPane}"
ng-click="toggleKeyPane()">
Key
</div>
</div>
<div class="AddPermissions-keyPane"
ng-show="showKeyPane">
<div class="AddPermissions-keyRow"
ng-repeat="key in roleKey">
<div class="AddPermissions-keyName">
{{ key.name }}
</div>
<div class="AddPermissions-keyDescription">
{{ key.description || "No description provided" }}
</div>
</div>
</div>
<form name="userForm" novalidate>
<ng-form name="userRoleForm">
<div class="AddPermissions-roleRow"
ng-repeat="obj in allSelected">
<div class="AddPermissions-roleName">
<span class="AddPermissions-roleNameVal">
{{ obj.name }}
</span>
<span class="AddPermissions-roleType">
{{ obj.type }}
</span>
</div>
<role-select class="AddPermissions-roleSelect">
</role-select>
<button class="AddPermissions-roleRemove"
ng-click="removeObject(obj)">
<i class="fa fa-times"></i>
</button>
</div>
</ng-form>
</form>
</div>
<div class="AddPermissions-footer">
<div class="buttons Form-buttons AddPermissions-buttons">
<button type="button"
class="btn btn-sm Form-saveButton"
ng-click="updatePermissions()"
ng-disabled="userRoleForm.$invalid || !allSelected || !allSelected.length">
Save
</button>
<button type="button"
class="btn btn-sm Form-cancelButton"
ng-click="closeModal()">
Cancel
</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,58 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/* jshint unused: vars */
export default
['addPermissionsTeamsList', 'addPermissionsUsersList', 'generateList', 'GetBasePath', 'SelectionInit', 'SearchInit',
'PaginateInit', function(addPermissionsTeamsList,
addPermissionsUsersList, generateList,
GetBasePath, SelectionInit, SearchInit, PaginateInit) {
return {
restrict: 'E',
scope: {
},
template: "<div class='addPermissionsList-inner'></div>",
link: function(scope, element, attrs, ctrl) {
scope.$on("linkLists", function(e) {
var generator = generateList,
list = addPermissionsTeamsList,
url = GetBasePath("teams"),
set = "teams",
id = "addPermissionsTeamsList",
mode = "edit";
if (attrs.type === 'users') {
list = addPermissionsUsersList;
url = GetBasePath("users") + "?is_superuser=false";
set = "users";
id = "addPermissionsUsersList";
mode = "edit";
}
scope.id = id;
scope.$watch("selectedItems", function() {
scope.$emit("itemsSelected", scope.selectedItems);
});
element.find(".addPermissionsList-inner")
.attr("id", id);
generator.inject(list, { id: id,
title: false, mode: mode, scope: scope });
SearchInit({ scope: scope, set: set,
list: list, url: url });
PaginateInit({ scope: scope,
list: list, url: url, pageSize: 5 });
scope.search(list.iterator);
});
}
};
}
];

View File

@@ -0,0 +1,15 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import addPermissionsListDirective from './addPermissionsList.directive';
import teamsList from './permissionsTeams.list';
import usersList from './permissionsUsers.list';
export default
angular.module('addPermissionsListModule', [])
.directive('addPermissionsList', addPermissionsListDirective)
.factory('addPermissionsTeamsList', teamsList)
.factory('addPermissionsUsersList', usersList);

View File

@@ -0,0 +1,27 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default function() {
return {
name: 'teams',
iterator: 'team',
listTitleBadge: false,
multiSelect: true,
multiSelectExtended: true,
index: false,
hover: true,
fields: {
name: {
key: true,
label: 'name'
},
},
};
}

View File

@@ -0,0 +1,37 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default function() {
return {
name: 'users',
iterator: 'user',
title: false,
listTitleBadge: false,
multiSelect: true,
multiSelectExtended: true,
index: false,
hover: true,
fields: {
first_name: {
label: 'First Name',
columnClass: 'col-md-3 col-sm-3 hidden-xs'
},
last_name: {
label: 'Last Name',
columnClass: 'col-md-3 col-sm-3 hidden-xs'
},
username: {
key: true,
label: 'Username',
columnClass: 'col-md-3 col-sm-3 col-xs-9'
},
},
};
}

View File

@@ -0,0 +1,14 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import addPermissionsDirective from './addPermissions.directive';
import roleSelect from './roleSelect.directive';
import addPermissionsList from './addPermissionsList/main';
export default
angular.module('AddPermissions', [addPermissionsList.name])
.directive('addPermissions', addPermissionsDirective)
.directive('roleSelect', roleSelect);

View File

@@ -0,0 +1,25 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/* jshint unused: vars */
export default
[
'CreateSelect2',
function(CreateSelect2) {
return {
restrict: 'E',
scope: false,
template: '<select ng-cloak class="AddPermissions-selectHide roleSelect2 form-control" ng-model="obj.roles" ng-options="role.label for role in roles track by role.value" multiple required></select>',
link: function(scope, element, attrs, ctrl) {
CreateSelect2({
element: '.roleSelect2',
multiple: true,
placeholder: 'Select roles'
});
}
};
}
];

View File

@@ -0,0 +1,12 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import roleList from './roleList.directive';
import addPermissions from './addPermissions/main';
export default
angular.module('access', [addPermissions.name])
.directive('roleList', roleList);

View File

@@ -0,0 +1,72 @@
/** @define RoleList */
@import "../shared/branding/colors.default.less";
.RoleList {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
}
.RoleList-tagContainer {
display: flex;
max-width: 100%;
}
.RoleList-tag {
border-radius: 5px;
padding: 2px 10px;
margin: 4px 0px;
border: 1px solid @default-second-border;
font-size: 12px;
color: @default-interface-txt;
text-transform: uppercase;
background-color: @default-bg;
margin-right: 5px;
max-width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.RoleList-tag--deletable {
margin-right: 0px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-right: 0;
max-wdith: ~"calc(100% - 23px)";
}
.RoleList-deleteContainer {
border: 1px solid @default-second-border;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
padding: 0 5px;
margin: 4px 0px;
margin-right: 5px;
align-items: center;
display: flex;
cursor: pointer;
}
.RoleList-tagDelete {
font-size: 13px;
color: @default-icon;
}
.RoleList-name {
flex: initial;
max-width: 100%;
}
.RoleList-tag--deletable > .RoleList-name {
max-width: ~"calc(100% - 23px)";
}
.RoleList-deleteContainer:hover, {
border-color: @default-err;
background-color: @default-err;
}
.RoleList-deleteContainer:hover > .RoleList-tagDelete {
color: @default-bg;
}

View File

@@ -0,0 +1,44 @@
/* jshint unused: vars */
export default
[ 'templateUrl',
function(templateUrl) {
return {
restrict: 'E',
scope: false,
templateUrl: templateUrl('access/roleList'),
link: function(scope, element, attrs) {
// given a list of roles (things like "project
// auditor") which are pulled from two different
// places in summary fields, and creates a
// concatenated/sorted list
scope.roles = []
.concat(scope.permission.summary_fields
.direct_access.map(function(i) {
return {
name: i.role.name,
roleId: i.role.id,
resourceName: i.role.resource_name,
explicit: true
};
}))
.concat(scope.permission.summary_fields
.indirect_access.map(function(i) {
return {
name: i.role.name,
roleId: i.role.id,
explicit: false
};
}))
.sort(function(a, b) {
if (a.name
.toLowerCase() > b.name
.toLowerCase()) {
return 1;
} else {
return -1;
}
});
}
};
}
];

View File

@@ -0,0 +1,13 @@
<div class="RoleList-tagContainer"
ng-repeat="role in roles">
<div class="RoleList-tag"
ng-class="{'RoleList-tag--deletable': role.explicit}">
<span class="RoleList-name">{{ role.name }}</span>
</div>
<div class="RoleList-deleteContainer"
ng-if="role.explicit"
ng-click="deletePermission(permission.id, role.roleId, permission.username, role.name, role.resourceName)">
<i ng-if="role.explicit"
class="fa fa-times RoleList-tagDelete"></i>
</div>
</div>

View File

@@ -31,6 +31,7 @@ import permissions from './permissions/main';
import managementJobs from './management-jobs/main'; import managementJobs from './management-jobs/main';
import jobDetail from './job-detail/main'; import jobDetail from './job-detail/main';
import notifications from './notifications/main'; import notifications from './notifications/main';
import access from './access/main';
// modules // modules
import about from './about/main'; import about from './about/main';
@@ -101,6 +102,7 @@ var tower = angular.module('Tower', [
jobDetail.name, jobDetail.name,
notifications.name, notifications.name,
standardOut.name, standardOut.name,
access.name,
'templates', 'templates',
'Utilities', 'Utilities',
'OrganizationFormDefinition', 'OrganizationFormDefinition',
@@ -884,16 +886,42 @@ var tower = angular.module('Tower', [
}]); }]);
}]) }])
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', '$state', 'CheckLicense', .run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket',
'$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket', 'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService', 'Prompt', 'Rest', 'Wait', 'ProcessErrors', '$state', 'GetBasePath',
'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService', function ($q, $compile, $cookieStore, $rootScope, $log, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
function ( LoadConfig, Store, ShowSocketHelp, pendoService, Prompt, Rest, Wait, ProcessErrors, $state, GetBasePath) {
$q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense,
$location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
LoadConfig, Store, ShowSocketHelp, pendoService)
{
var sock; var sock;
$rootScope.addPermission = function (scope) {
$compile("<add-permissions class='AddPermissions'></add-permissions>")(scope);
}
$rootScope.deletePermission = function (user, role, userName,
roleName, resourceName) {
var action = function () {
$('#prompt-modal').modal('hide');
Wait('start');
var url = GetBasePath("users") + user + "/roles/";
Rest.setUrl(url);
Rest.post({"disassociate": true, "id": role})
.success(function () {
Wait('stop');
$rootScope.$broadcast("refreshList", "permission");
})
.error(function (data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Could not disacssociate user from role. Call to ' + url + ' failed. DELETE returned status: ' + status });
});
};
Prompt({
hdr: 'Remove Role from ' + resourceName,
body: '<div class="Prompt-bodyQuery">Confirm the removal of the <span class="Prompt-emphasis">' + roleName + '</span> role associated with ' + userName + '.</div>',
action: action,
actionText: 'REMOVE'
});
};
function activateTab() { function activateTab() {
// Make the correct tab active // Make the correct tab active
var base = $location.path().replace(/^\//, '').split('/')[0]; var base = $location.path().replace(/^\//, '').split('/')[0];
@@ -1027,6 +1055,7 @@ var tower = angular.module('Tower', [
$rootScope.$on("$stateChangeStart", function (event, next, nextParams, prev) { $rootScope.$on("$stateChangeStart", function (event, next, nextParams, prev) {
$rootScope.$broadcast("closePermissionsModal");
// this line removes the query params attached to a route // this line removes the query params attached to a route
if(prev && prev.$$route && if(prev && prev.$$route &&
prev.$$route.name === 'systemTracking'){ prev.$$route.name === 'systemTracking'){

View File

@@ -649,7 +649,6 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
} }
} }
relatedSets = form.relatedSets(data.related); relatedSets = form.relatedSets(data.related);

View File

@@ -210,36 +210,40 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log,
$scope.$emit("RefreshTeamsList"); $scope.$emit("RefreshTeamsList");
// return a promise from the options request with the permission type choices (including adhoc) as a param // return a promise from the options request with the permission type choices (including adhoc) as a param
var permissionsChoice = fieldChoices({ // var permissionsChoice = fieldChoices({
scope: $scope, // scope: $scope,
url: 'api/v1/' + base + '/' + id + '/permissions/', // url: 'api/v1/' + base + '/' + id + '/permissions/',
field: 'permission_type' // field: 'permission_type'
}); // });
// manipulate the choices from the options request to be set on // // manipulate the choices from the options request to be set on
// scope and be usable by the list form // // scope and be usable by the list form
permissionsChoice.then(function (choices) { // permissionsChoice.then(function (choices) {
choices = // choices =
fieldLabels({ // fieldLabels({
choices: choices // choices: choices
}); // });
_.map(choices, function(n, key) { // _.map(choices, function(n, key) {
$scope.permission_label[key] = n; // $scope.permission_label[key] = n;
}); // });
}); // });
// manipulate the choices from the options request to be usable // manipulate the choices from the options request to be usable
// by the search option for permission_type, you can't inject the // by the search option for permission_type, you can't inject the
// list until this is done! // list until this is done!
permissionsChoice.then(function (choices) { // permissionsChoice.then(function (choices) {
form.related.permissions.fields.permission_type.searchOptions = // form.related.permissions.fields.permission_type.searchOptions =
permissionsSearchSelect({ // permissionsSearchSelect({
choices: choices // choices: choices
}); // });
generator.inject(form, { mode: 'edit', related: true, scope: $scope }); generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset(); generator.reset();
$scope.$emit('loadTeam'); $scope.$emit('loadTeam');
}); // });
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();
$scope.$emit('loadTeam');
$scope.team_id = id; $scope.team_id = id;

View File

@@ -240,37 +240,38 @@ export function UsersEdit($scope, $rootScope, $compile, $location, $log,
$scope.$emit("RefreshUsersList"); $scope.$emit("RefreshUsersList");
// return a promise from the options request with the permission type choices (including adhoc) as a param // // return a promise from the options request with the permission type choices (including adhoc) as a param
var permissionsChoice = fieldChoices({ // var permissionsChoice = fieldChoices({
scope: $scope, // scope: $scope,
url: 'api/v1/' + base + '/' + id + '/permissions/', // url: 'api/v1/' + base + '/' + id + '/permissions/',
field: 'permission_type' // field: 'permission_type'
}); // });
//
// manipulate the choices from the options request to be set on // // manipulate the choices from the options request to be set on
// scope and be usable by the list form // // scope and be usable by the list form
permissionsChoice.then(function (choices) { // permissionsChoice.then(function (choices) {
choices = // choices =
fieldLabels({ // fieldLabels({
choices: choices // choices: choices
}); // });
_.map(choices, function(n, key) { // _.map(choices, function(n, key) {
$scope.permission_label[key] = n; // $scope.permission_label[key] = n;
}); // });
}); // });
// manipulate the choices from the options request to be usable // manipulate the choices from the options request to be usable
// by the search option for permission_type, you can't inject the // by the search option for permission_type, you can't inject the
// list until this is done! // list until this is done!
permissionsChoice.then(function (choices) { // permissionsChoice.then(function (choices) {
form.related.permissions.fields.permission_type.searchOptions = // form.related.permissions.fields.permission_type.searchOptions =
permissionsSearchSelect({ // permissionsSearchSelect({
choices: choices // choices: choices
}); // });
generator.inject(form, { mode: 'edit', related: true, scope: $scope }); // });
generator.reset();
$scope.$emit("loadForm"); generator.inject(form, { mode: 'edit', related: true, scope: $scope });
}); generator.reset();
$scope.$emit("loadForm");
if ($scope.removeFormReady) { if ($scope.removeFormReady) {
$scope.removeFormReady(); $scope.removeFormReady();

View File

@@ -7,7 +7,6 @@
import sanitizeFilter from './shared/xss-sanitizer.filter'; import sanitizeFilter from './shared/xss-sanitizer.filter';
import capitalizeFilter from './shared/capitalize.filter'; import capitalizeFilter from './shared/capitalize.filter';
import longDateFilter from './shared/long-date.filter'; import longDateFilter from './shared/long-date.filter';
export { export {
sanitizeFilter, sanitizeFilter,
capitalizeFilter, capitalizeFilter,

View File

@@ -278,83 +278,38 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
} }
} }
}, },
permissions: {
schedules: {
type: 'collection', type: 'collection',
title: 'Schedules', title: 'Permissions',
iterator: 'schedule', iterator: 'permission',
index: false, index: false,
open: false, open: false,
searchType: 'select',
actions: { actions: {
refresh: {
mode: 'all',
awToolTip: "Refresh the page",
ngClick: "refreshSchedules()",
actionClass: 'btn List-buttonDefault',
buttonContent: 'REFRESH',
ngHide: 'scheduleLoading == false && schedule_active_search == false && schedule_total_rows < 1'
},
add: { add: {
mode: 'all', ngClick: "addPermission",
ngClick: 'addSchedule()', label: 'Add',
awToolTip: 'Add a new schedule', awToolTip: 'Add a permission',
actionClass: 'btn List-buttonSubmit', actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD' buttonContent: '&#43; ADD'
} }
}, },
fields: { fields: {
name: { username: {
key: true, key: true,
label: 'Name', label: 'User',
ngClick: "editSchedule(schedule.id)", linkBase: 'users',
columnClass: "col-md-3 col-sm-3 col-xs-3" class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4'
}, },
dtstart: { role: {
label: 'First Run', label: 'Role',
filter: "longDate", type: 'role',
searchable: false, noSort: true,
columnClass: "col-md-2 col-sm-3 hidden-xs" class: 'col-lg-9 col-md-9 col-sm-9 col-xs-8'
},
next_run: {
label: 'Next Run',
filter: "longDate",
searchable: false,
columnClass: "col-md-2 col-sm-3 col-xs-3"
},
dtend: {
label: 'Final Run',
filter: "longDate",
searchable: false,
columnClass: "col-md-2 col-sm-3 hidden-xs"
}
},
fieldActions: {
"play": {
mode: "all",
ngClick: "toggleSchedule($event, schedule.id)",
awToolTip: "{{ schedule.play_tip }}",
dataTipWatch: "schedule.play_tip",
iconClass: "{{ 'fa icon-schedule-enabled-' + schedule.enabled }}",
dataPlacement: "top"
},
edit: {
label: 'Edit',
ngClick: "editSchedule(schedule.id)",
icon: 'icon-edit',
awToolTip: 'Edit schedule',
dataPlacement: 'top'
},
"delete": {
label: 'Delete',
ngClick: "deleteSchedule(schedule.id)",
icon: 'icon-trash',
awToolTip: 'Delete schedule',
dataPlacement: 'top'
} }
} }
} }
}, },
relatedSets: function(urls) { relatedSets: function(urls) {
@@ -363,9 +318,9 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
iterator: 'organization', iterator: 'organization',
url: urls.organizations url: urls.organizations
}, },
schedules: { permissions: {
iterator: 'schedule', iterator: 'permission',
url: urls.schedules url: urls.resource_access_list
} }
}; };
} }

View File

@@ -158,69 +158,69 @@ export default
} }
}, },
permissions: { // permissions: {
type: 'collection', // type: 'collection',
title: 'Permissions', // title: 'Permissions',
iterator: 'permission', // iterator: 'permission',
open: false, // open: false,
index: false, // index: false,
//
actions: { // actions: {
add: { // add: {
ngClick: "add('permissions')", // ngClick: "add('permissions')",
label: 'Add', // label: 'Add',
awToolTip: 'Add a permission for this user', // awToolTip: 'Add a permission for this user',
ngShow: 'PermissionAddAllowed', // ngShow: 'PermissionAddAllowed',
actionClass: 'btn List-buttonSubmit', // actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD' // buttonContent: '&#43; ADD'
} // }
}, // },
//
fields: { // fields: {
name: { // name: {
key: true, // key: true,
label: 'Name', // label: 'Name',
ngClick: "edit('permissions', permission.id, permission.name)" // ngClick: "edit('permissions', permission.id, permission.name)"
}, // },
inventory: { // inventory: {
label: 'Inventory', // label: 'Inventory',
sourceModel: 'inventory', // sourceModel: 'inventory',
sourceField: 'name', // sourceField: 'name',
ngBind: 'permission.summary_fields.inventory.name' // ngBind: 'permission.summary_fields.inventory.name'
}, // },
project: { // project: {
label: 'Project', // label: 'Project',
sourceModel: 'project', // sourceModel: 'project',
sourceField: 'name', // sourceField: 'name',
ngBind: 'permission.summary_fields.project.name' // ngBind: 'permission.summary_fields.project.name'
}, // },
permission_type: { // permission_type: {
label: 'Permission', // label: 'Permission',
ngBind: 'getPermissionText()', // ngBind: 'getPermissionText()',
searchType: 'select' // searchType: 'select'
} // }
}, // },
//
fieldActions: { // fieldActions: {
edit: { // edit: {
label: 'Edit', // label: 'Edit',
ngClick: "edit('permissions', permission.id, permission.name)", // ngClick: "edit('permissions', permission.id, permission.name)",
icon: 'icon-edit', // icon: 'icon-edit',
awToolTip: 'Edit the permission', // awToolTip: 'Edit the permission',
'class': 'btn btn-default' // 'class': 'btn btn-default'
}, // },
//
"delete": { // "delete": {
label: 'Delete', // label: 'Delete',
ngClick: "delete('permissions', permission.id, permission.name, 'permission')", // ngClick: "delete('permissions', permission.id, permission.name, 'permission')",
icon: 'icon-trash', // icon: 'icon-trash',
"class": 'btn-danger', // "class": 'btn-danger',
awToolTip: 'Delete the permission', // awToolTip: 'Delete the permission',
ngShow: 'PermissionAddAllowed' // ngShow: 'PermissionAddAllowed'
} // }
} // }
//
}, // },
admin_of_organizations: { // Assumes a plural name (e.g. things) admin_of_organizations: { // Assumes a plural name (e.g. things)
type: 'collection', type: 'collection',

View File

@@ -32,7 +32,7 @@ export default
// Which page are we on? // Which page are we on?
if (Empty(next) && previous) { if (Empty(next) && previous) {
// no next page, but there is a previous page // no next page, but there is a previous page
scope[iterator + '_page'] = scope[iterator + '_num_pages']; scope[iterator + '_page'] = /page=\d+/.test(previous) ? parseInt(previous.match(/page=(\d+)/)[1]) + 1 : 2;
} else if (next && Empty(previous)) { } else if (next && Empty(previous)) {
// next page available, but no previous page // next page available, but no previous page
scope[iterator + '_page'] = 1; scope[iterator + '_page'] = 1;

View File

@@ -230,10 +230,15 @@ export default
url += (url.match(/\/$/)) ? '?' : '&'; url += (url.match(/\/$/)) ? '?' : '&';
url += scope[iterator + 'SearchParams']; url += scope[iterator + 'SearchParams'];
url += (scope[iterator + '_page_size']) ? '&page_size=' + scope[iterator + '_page_size'] : ""; url += (scope[iterator + '_page_size']) ? '&page_size=' + scope[iterator + '_page_size'] : "";
scope[iterator + '_active_search'] = true;
RefreshRelated({ scope: scope, set: set, iterator: iterator, url: url }); RefreshRelated({ scope: scope, set: set, iterator: iterator, url: url });
}; };
scope.$on("refreshList", function(e, iterator) {
scope.search(iterator);
});
scope.sort = function (iterator, fld) { scope.sort = function (iterator, fld) {
var sort_order, icon, direction, set; var sort_order, icon, direction, set;

View File

@@ -85,7 +85,9 @@ export default
} }
Store('sessionTime', x); Store('sessionTime', x);
$rootScope.lastUser = $cookieStore.get('current_user').id; if ($cookieStore.get('current_user')) {
$rootScope.lastUser = $cookieStore.get('current_user').id;
}
$cookieStore.remove('token_expires'); $cookieStore.remove('token_expires');
$cookieStore.remove('current_user'); $cookieStore.remove('current_user');
$cookieStore.remove('token'); $cookieStore.remove('token');

View File

@@ -136,6 +136,10 @@
.MainMenu-itemText--username { .MainMenu-itemText--username {
padding-left: 13px; padding-left: 13px;
margin-top: -4px; margin-top: -4px;
max-width: 85px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.MainMenu-itemImage { .MainMenu-itemImage {

View File

@@ -614,7 +614,8 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter'])
var element = params.element, var element = params.element,
options = params.opts, options = params.opts,
multiple = (params.multiple!==undefined) ? params.multiple : true; multiple = (params.multiple!==undefined) ? params.multiple : true,
placeholder = params.placeholder;
$.fn.select2.amd.require([ $.fn.select2.amd.require([
'select2/utils', 'select2/utils',
@@ -632,6 +633,7 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter'])
}, Dropdown); }, Dropdown);
$(element).select2({ $(element).select2({
placeholder: placeholder,
multiple: multiple, multiple: multiple,
containerCssClass: 'Form-dropDown', containerCssClass: 'Form-dropDown',
width: '100%', width: '100%',

View File

@@ -1548,7 +1548,9 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += "<div class=\"buttons Form-buttons\" "; html += "<div class=\"buttons Form-buttons\" ";
html += "id=\"" + this.form.name + "_controls\" "; html += "id=\"" + this.form.name + "_controls\" ";
if (options.mode === 'edit' && this.form.tabs) {
html += "ng-show=\"" + this.form.name + "Selected\"; "
}
html += ">\n"; html += ">\n";
if (this.form.horizontal) { if (this.form.horizontal) {
@@ -1723,32 +1725,52 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += "<tr class=\"List-tableHeaderRow\">\n"; html += "<tr class=\"List-tableHeaderRow\">\n";
html += (collection.index === undefined || collection.index !== false) ? "<th class=\"col-xs-1\">#</th>\n" : ""; html += (collection.index === undefined || collection.index !== false) ? "<th class=\"col-xs-1\">#</th>\n" : "";
for (fld in collection.fields) { for (fld in collection.fields) {
html += "<th class=\"List-tableHeader list-header\" id=\"" + collection.iterator + '-' + fld + "-header\" " + html += "<th class=\"List-tableHeader list-header ";
"ng-click=\"sort('" + collection.iterator + "', '" + fld + "')\">" + html += (collection.fields[fld].class) ? collection.fields[fld].class : "";
collection.fields[fld].label; html += "\" id=\"" + collection.iterator + '-' + fld + "-header\" ";
html += " <i class=\"";
if (collection.fields[fld].key) { if (!collection.fields[fld].noSort) {
if (collection.fields[fld].desc) { html += "ng-click=\"sort('" + collection.iterator + "', '" + fld + "')\">"
html += "fa fa-sort-down";
} else {
html += "fa fa-sort-up";
}
} else { } else {
html += "fa fa-sort"; html += ">";
} }
html += "\"></i></a></th>\n";
html += collection.fields[fld].label;
if (!collection.fields[fld].noSort) {
html += " <i class=\"";
if (collection.fields[fld].key) {
if (collection.fields[fld].desc) {
html += "fa fa-sort-down";
} else {
html += "fa fa-sort-up";
}
} else {
html += "fa fa-sort";
}
html += "\"></i>"
}
html += "</a></th>\n";
}
if (collection.fieldActions) {
html += "<th class=\"List-tableHeader List-tableHeader--actions\">Actions</th>\n";
} }
html += "<th class=\"List-tableHeader\">Actions</th>\n";
html += "</tr>\n"; html += "</tr>\n";
html += "</thead>"; html += "</thead>";
html += "<tbody>\n"; html += "<tbody>\n";
html += "<tr class=\"List-tableHeaderRow\" ng-repeat=\"" + collection.iterator + " in " + itm + "\" "; html += "<tr class=\"List-tableRow\" ng-repeat=\"" + collection.iterator + " in " + itm + "\" ";
html += "ng-class-odd=\"'List-tableRow--oddRow'\" "; html += "ng-class-odd=\"'List-tableRow--oddRow'\" ";
html += "ng-class-even=\"'List-tableRow--evenRow'\" "; html += "ng-class-even=\"'List-tableRow--evenRow'\" ";
html += "id=\"{{ " + collection.iterator + ".id }}\">\n"; html += "id=\"{{ " + collection.iterator + ".id }}\">\n";
if (collection.index === undefined || collection.index !== false) { if (collection.index === undefined || collection.index !== false) {
html += "<td class=\"List-tableCell\">{{ $index + ((" + collection.iterator + "_page - 1) * " + html += "<td class=\"List-tableCell";
html += (collection.fields[fld].class) ? collection.fields[fld].class : "";
html += "\">{{ $index + ((" + collection.iterator + "_page - 1) * " +
collection.iterator + "_page_size) + 1 }}.</td>\n"; collection.iterator + "_page_size) + 1 }}.</td>\n";
} }
cnt = 1; cnt = 1;
@@ -1765,31 +1787,33 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
} }
// Row level actions // Row level actions
html += "<td class=\"List-tableCell List-actionButtonCell actions\">"; if (collection.fieldActions) {
for (act in collection.fieldActions) { html += "<td class=\"List-tableCell List-actionButtonCell actions\">";
fAction = collection.fieldActions[act]; for (act in collection.fieldActions) {
html += "<button id=\"" + ((fAction.id) ? fAction.id : act + "-action") + "\" "; fAction = collection.fieldActions[act];
html += (fAction.href) ? "href=\"" + fAction.href + "\" " : ""; html += "<button id=\"" + ((fAction.id) ? fAction.id : act + "-action") + "\" ";
html += (fAction.ngClick) ? this.attr(fAction, 'ngClick') : ""; html += (fAction.href) ? "href=\"" + fAction.href + "\" " : "";
html += (fAction.ngHref) ? this.attr(fAction, 'ngHref') : ""; html += (fAction.ngClick) ? this.attr(fAction, 'ngClick') : "";
html += (fAction.ngShow) ? this.attr(fAction, 'ngShow') : ""; html += (fAction.ngHref) ? this.attr(fAction, 'ngHref') : "";
html += " class=\"List-actionButton "; html += (fAction.ngShow) ? this.attr(fAction, 'ngShow') : "";
html += (act === 'delete') ? "List-actionButton--delete" : ""; html += " class=\"List-actionButton ";
html += "\""; html += (act === 'delete') ? "List-actionButton--delete" : "";
html += ">"; html += "\"";
if (fAction.iconClass) { html += ">";
html += "<i class=\"" + fAction.iconClass + "\"></i>"; if (fAction.iconClass) {
} else { html += "<i class=\"" + fAction.iconClass + "\"></i>";
html += SelectIcon({ } else {
action: act html += SelectIcon({
}); action: act
});
}
// html += SelectIcon({ action: act });
//html += (fAction.label) ? "<span class=\"list-action-label\"> " + fAction.label + "</span>": "";
html += "</button>";
} }
// html += SelectIcon({ action: act }); html += "</td>";
//html += (fAction.label) ? "<span class=\"list-action-label\"> " + fAction.label + "</span>": ""; html += "</tr>\n";
html += "</button>";
} }
html += "</td>";
html += "</tr>\n";
// Message for loading // Message for loading
html += "<tr ng-show=\"" + collection.iterator + "Loading == true\">\n"; html += "<tr ng-show=\"" + collection.iterator + "Loading == true\">\n";

View File

@@ -449,6 +449,8 @@ angular.module('GeneratorHelpers', [systemStatus.name])
if (field.type !== undefined && field.type === 'DropDown') { if (field.type !== undefined && field.type === 'DropDown') {
html = DropDown(params); html = DropDown(params);
} else if (field.type === 'role') {
html += "<td class=\"List-tableCell\"><role-list class=\"RoleList\"></role-list></td>";
} else if (field.type === 'badgeCount') { } else if (field.type === 'badgeCount') {
html = BadgeCount(params); html = BadgeCount(params);
} else if (field.type === 'badgeOnly') { } else if (field.type === 'badgeOnly') {
@@ -520,7 +522,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
list: list, list: list,
field: field, field: field,
fld: fld, fld: fld,
base: base base: field.linkBase || base
}) + ' '; }) + ' ';
}); });
} }
@@ -532,7 +534,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
list: list, list: list,
field: field, field: field,
fld: fld, fld: fld,
base: base base: field.linkBase || base
}); });
} }
} }
@@ -633,6 +635,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
var iterator = params.iterator, var iterator = params.iterator,
form = params.template, form = params.template,
size = params.size, size = params.size,
mini = params.mini,
includeSize = (params.includeSize === undefined) ? true : params.includeSize, includeSize = (params.includeSize === undefined) ? true : params.includeSize,
ngShow = (params.ngShow) ? params.ngShow : false, ngShow = (params.ngShow) ? params.ngShow : false,
i, html = '', i, html = '',
@@ -666,6 +669,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
if (includeSize) { if (includeSize) {
html += "<div class=\"List-searchWidget "; html += "<div class=\"List-searchWidget ";
html += (mini) ? "List-searchWidget--compact " : "";
html += (size) ? size : "col-lg-4 col-md-8 col-sm-12 col-xs-12"; html += (size) ? size : "col-lg-4 col-md-8 col-sm-12 col-xs-12";
html += "\" id=\"search-widget-container" + modifier + "\">\n"; html += "\" id=\"search-widget-container" + modifier + "\">\n";
} }

View File

@@ -416,6 +416,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
function buildTable() { function buildTable() {
var extraClasses = list['class']; var extraClasses = list['class'];
var multiSelect = list.multiSelect ? 'multi-select-list' : null; var multiSelect = list.multiSelect ? 'multi-select-list' : null;
var multiSelectExtended = list.multiSelectExtended ? 'true' : 'false';
if (options.mode === 'summary') { if (options.mode === 'summary') {
extraClasses += ' table-summary'; extraClasses += ' table-summary';
@@ -425,7 +426,8 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
.attr('id', list.name + '_table') .attr('id', list.name + '_table')
.addClass('List-table') .addClass('List-table')
.addClass(extraClasses) .addClass(extraClasses)
.attr('multi-select-list', multiSelect); .attr('multi-select-list', multiSelect)
.attr('is-extended', multiSelectExtended);
} }
@@ -460,7 +462,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
} }
if (list.multiSelect) { if (list.multiSelect) {
innerTable += '<td class="col-xs-1 select-column List-tableCell"><select-list-item item=\"' + list.iterator + '\"></select-list-item></td>'; innerTable += '<td class="col-xs-1 select-column List- List-staticColumn--smallStatus"><select-list-item item=\"' + list.iterator + '\"></select-list-item></td>';
} }
// Change layout if a lookup list, place radio buttons before labels // Change layout if a lookup list, place radio buttons before labels
@@ -609,7 +611,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
function buildSelectAll() { function buildSelectAll() {
return $('<th>') return $('<th>')
.addClass('col-xs-1 select-column List-tableHeader') .addClass('col-xs-1 select-column List-tableHeader List-staticColumn--smallStatus')
.append( .append(
$('<select-all>') $('<select-all>')
.attr('selections-empty', 'selectedItems.length === 0') .attr('selections-empty', 'selectedItems.length === 0')
@@ -665,10 +667,9 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
} }
} }
if (options.mode === 'select') { if (options.mode === 'select') {
html += "<th class=\"List-tableHeader col-lg-1 col-md-1 col-sm-2 col-xs-2\">Select</th>"; html += "<th class=\"List-tableHeader col-lg-1 col-md-1 col-sm-2 col-xs-2\">Select</th>";
} } else if (options.mode === 'edit' && list.fieldActions) {
else if (options.mode === 'edit' && list.fieldActions) { html += "<th class=\"List-tableHeader List-tableHeader--actions actions-column";
html += "<th class=\"List-tableHeader actions-column";
html += (list.fieldActions && list.fieldActions.columnClass) ? " " + list.fieldActions.columnClass : ""; html += (list.fieldActions && list.fieldActions.columnClass) ? " " + list.fieldActions.columnClass : "";
html += "\">"; html += "\">";
html += (list.fieldActions.label === undefined || list.fieldActions.label) ? "Actions" : ""; html += (list.fieldActions.label === undefined || list.fieldActions.label) ? "Actions" : "";

View File

@@ -30,7 +30,7 @@ export default
item: '=item' item: '=item'
}, },
require: '^multiSelectList', require: '^multiSelectList',
template: '<input type="checkbox" data-multi-select-list-item ng-model="isSelected">', template: '<input type="checkbox" data-multi-select-list-item ng-model="isSelected" ng-change="userInteractionSelect()">',
link: function(scope, element, attrs, multiSelectList) { link: function(scope, element, attrs, multiSelectList) {
scope.isSelected = false; scope.isSelected = false;
@@ -52,6 +52,10 @@ export default
multiSelectList.deregisterItem(scope.decoratedItem); multiSelectList.deregisterItem(scope.decoratedItem);
}); });
scope.userInteractionSelect = function() {
scope.$emit("selectedOrDeselected", scope.decoratedItem);
}
} }
}; };
}]; }];

View File

@@ -8,3 +8,8 @@
.Prompt-bodyTarget { .Prompt-bodyTarget {
color: @default-data-txt; color: @default-data-txt;
} }
.Prompt-emphasis {
font-weight: bold;
text-transform: uppercase;
}