diff --git a/.gitignore b/.gitignore
index 13f37e3f1c..2358d2e2a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-lib/settings/local_settings.py
+lib/settings/local_settings.py*
lib/acom.sqlite3
lib/projects
lib/public/media
diff --git a/lib/main/admin.py b/lib/main/admin.py
index d02f9135e8..357f56f5d4 100644
--- a/lib/main/admin.py
+++ b/lib/main/admin.py
@@ -217,19 +217,20 @@ class ProjectAdmin(BaseModelAdmin):
list_display = ('name', 'description', 'active')
fieldsets = (
(None, {'fields': (('name', 'active'), 'description', 'local_path',
- 'get_available_playbooks_display')}),
+ 'get_playbooks_display')}),
(_('Tags'), {'fields': ('tags',)}),
(_('Audit Trail'), {'fields': ('created', 'created_by', 'audit_trail',)}),
)
readonly_fields = ('created', 'created_by', 'audit_trail',
- 'get_available_playbooks_display')
+ 'get_playbooks_display')
filter_horizontal = ('tags',)
+ form = ProjectAdminForm
- def get_available_playbooks_display(self, obj):
+ def get_playbooks_display(self, obj):
return '
'.join([format_html('{0}', x) for x in
- obj.available_playbooks])
- get_available_playbooks_display.short_description = _('Available playbooks')
- get_available_playbooks_display.allow_tags = True
+ obj.playbooks])
+ get_playbooks_display.short_description = _('Playbooks')
+ get_playbooks_display.allow_tags = True
class PermissionAdmin(BaseModelAdmin):
diff --git a/lib/main/forms.py b/lib/main/forms.py
index c90c14c462..a94ef1b0ff 100644
--- a/lib/main/forms.py
+++ b/lib/main/forms.py
@@ -68,6 +68,18 @@ class GroupForm(forms.ModelForm):
variable_data = JSONFormField(required=False, widget=forms.Textarea(attrs={'class': 'vLargeTextField'}))
+class ProjectAdminForm(forms.ModelForm):
+ '''Custom admin form for Projects.'''
+
+ local_path = forms.ChoiceField(choices=[])
+
+ class Meta:
+ model = Project
+
+ def __init__(self, *args, **kwargs):
+ super(ProjectAdminForm, self).__init__(*args, **kwargs)
+ self.fields['local_path'].choices = [(x, x) for x in Project.get_local_path_choices()]
+
class JobTemplateAdminForm(forms.ModelForm):
'''Custom admin form for creating/editing JobTemplates.'''
@@ -80,7 +92,7 @@ class JobTemplateAdminForm(forms.ModelForm):
super(JobTemplateAdminForm, self).__init__(*args, **kwargs)
playbook_choices = []
for project in Project.objects.all():
- for playbook in project.available_playbooks:
+ for playbook in project.playbooks:
playbook_choices.append((playbook,
PlaybookOption(project, playbook)))
self.fields['playbook'].choices = [EMPTY_CHOICE] + playbook_choices
diff --git a/lib/main/migrations/0017_changes.py b/lib/main/migrations/0017_changes.py
new file mode 100644
index 0000000000..7bbcfe2a96
--- /dev/null
+++ b/lib/main/migrations/0017_changes.py
@@ -0,0 +1,294 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Removing unique constraint on 'Project', fields ['local_path']
+ db.delete_unique(u'main_project', ['local_path'])
+
+
+ # Changing field 'Project.local_path'
+ db.alter_column(u'main_project', 'local_path', self.gf('django.db.models.fields.CharField')(max_length=1024))
+
+ # Change absolute paths to only the directory name instead.
+ if not db.dry_run:
+ import os
+ for project in orm.Project.objects.all():
+ project.local_path = os.path.basename(project.local_path)
+ project.save()
+
+ def backwards(self, orm):
+
+ # Change directory names back to aboslute paths.
+ if not db.dry_run:
+ import os
+ from django.conf import settings
+ for project in orm.Project.objects.all():
+ project.local_path = os.path.join(settings.PROJECTS_ROOT, os.path.basename(project.local_path))
+ project.save()
+
+ # Changing field 'Project.local_path'
+ db.alter_column(u'main_project', 'local_path', self.gf('django.db.models.fields.FilePathField')(max_length=1024, path='/Users/chris/Sandbox/ansible-commander/lib/projects', unique=True))
+ # Adding unique constraint on 'Project', fields ['local_path']
+ db.create_unique(u'main_project', ['local_path'])
+
+
+ models = {
+ u'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ u'auth.permission': {
+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ u'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ u'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'main.audittrail': {
+ 'Meta': {'object_name': 'AuditTrail'},
+ 'comment': ('django.db.models.fields.TextField', [], {}),
+ 'delta': ('django.db.models.fields.TextField', [], {}),
+ 'detail': ('django.db.models.fields.TextField', [], {}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+ 'resource_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Tag']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'})
+ },
+ 'main.credential': {
+ 'Meta': {'object_name': 'Credential'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'credential_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
+ 'ssh_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'ssh_key_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+ 'ssh_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+ 'ssh_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+ 'sudo_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+ 'sudo_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'credential_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
+ 'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Team']", 'blank': 'True', 'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credentials'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['auth.User']", 'blank': 'True', 'null': 'True'})
+ },
+ 'main.group': {
+ 'Meta': {'unique_together': "(('name', 'inventory'),)", 'object_name': 'Group'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'group_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'groups'", 'blank': 'True', 'to': "orm['main.Host']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Inventory']"}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
+ 'parents': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'children'", 'blank': 'True', 'to': "orm['main.Group']"}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'group_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
+ 'variable_data': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'group'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.VariableData']", 'blank': 'True', 'null': 'True'})
+ },
+ 'main.host': {
+ 'Meta': {'unique_together': "(('name', 'inventory'),)", 'object_name': 'Host'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'host_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': "orm['main.Inventory']"}),
+ 'last_job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Job']", 'blank': 'True', 'null': 'True'}),
+ 'last_job_host_summary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job_summary+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': u"orm['main.JobHostSummary']", 'blank': 'True', 'null': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'host_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
+ 'variable_data': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'host'", 'unique': 'True', 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.VariableData']", 'blank': 'True', 'null': 'True'})
+ },
+ 'main.inventory': {
+ 'Meta': {'unique_together': "(('name', 'organization'),)", 'object_name': 'Inventory'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'inventory_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
+ 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventories'", 'to': "orm['main.Organization']"}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'inventory_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"})
+ },
+ 'main.job': {
+ 'Meta': {'object_name': 'Job'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'job_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
+ 'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'job\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
+ 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Credential']"}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}),
+ 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'jobs'", 'blank': 'True', 'through': u"orm['main.JobHostSummary']", 'to': "orm['main.Host']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}),
+ 'job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobTemplate']", 'blank': 'True', 'null': 'True'}),
+ 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
+ 'playbook': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['main.Project']"}),
+ 'result_stdout': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'job_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
+ 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'})
+ },
+ 'main.jobevent': {
+ 'Meta': {'ordering': "('pk',)", 'object_name': 'JobEvent'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'event_data': ('jsonfield.fields.JSONField', [], {'default': "''", 'blank': 'True'}),
+ 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'host': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Host']", 'blank': 'True', 'null': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events'", 'to': "orm['main.Job']"})
+ },
+ u'main.jobhostsummary': {
+ 'Meta': {'ordering': "('-pk',)", 'unique_together': "[('job', 'host')]", 'object_name': 'JobHostSummary'},
+ 'changed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'dark': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'host': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Host']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Job']"}),
+ 'ok': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'processed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'skipped': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'main.jobtemplate': {
+ 'Meta': {'object_name': 'JobTemplate'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'jobtemplate_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'jobtemplate\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
+ 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}),
+ 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
+ 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_templates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['main.Project']"}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'jobtemplate_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
+ 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'})
+ },
+ 'main.organization': {
+ 'Meta': {'object_name': 'Organization'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'admins': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'admin_of_organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}),
+ 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organization_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
+ 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['main.Project']"}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organization_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
+ 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['auth.User']"})
+ },
+ 'main.permission': {
+ 'Meta': {'object_name': 'Permission'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'permission_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
+ 'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['main.Project']"}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'permission_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
+ 'team': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Team']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"})
+ },
+ u'main.project': {
+ 'Meta': {'object_name': 'Project'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'project_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'project\', \'app_label\': u\'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'project_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"})
+ },
+ 'main.tag': {
+ 'Meta': {'object_name': 'Tag'},
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'})
+ },
+ 'main.team': {
+ 'Meta': {'object_name': 'Team'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'team_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
+ 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'teams'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Organization']"}),
+ 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['main.Project']"}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'team_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"}),
+ 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['auth.User']"})
+ },
+ 'main.variabledata': {
+ 'Meta': {'object_name': 'VariableData'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'audit_trail': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'variabledata_by_audit_trail'", 'blank': 'True', 'to': "orm['main.AuditTrail']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': '"{\'class\': \'variabledata\', \'app_label\': \'main\'}(class)s_created"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}),
+ 'data': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'variabledata_by_tag'", 'blank': 'True', 'to': "orm['main.Tag']"})
+ }
+ }
+
+ complete_apps = ['main']
\ No newline at end of file
diff --git a/lib/main/models/__init__.py b/lib/main/models/__init__.py
index 948e92864a..1af2ed7ab4 100644
--- a/lib/main/models/__init__.py
+++ b/lib/main/models/__init__.py
@@ -370,13 +370,25 @@ class Project(CommonModel):
# this is not part of the project, but managed with perms
# inventories = models.ManyToManyField('Inventory', blank=True, related_name='projects')
- local_path = models.FilePathField(
- path=settings.PROJECTS_ROOT,
- recursive=False,
- allow_files=False,
- allow_folders=True,
+ # Project files must be available on the server in folders directly
+ # beneath the path specified by settings.PROJECTS_ROOT. There is no way
+ # via the API to upload/update a project or its playbooks; this must be
+ # done by other means for now.
+
+ @classmethod
+ def get_local_path_choices(cls):
+ if os.path.exists(settings.PROJECTS_ROOT):
+ return [x for x in os.listdir(settings.PROJECTS_ROOT)
+ if os.path.isdir(os.path.join(settings.PROJECTS_ROOT, x))
+ and not x.startswith('.')]
+ else:
+ return []
+
+ local_path = models.CharField(
max_length=1024,
- unique=True,
+ # Not unique for now, otherwise "deletes" won't allow reusing the
+ # same path for another active project.
+ #unique=True,
help_text=_('Local path (relative to PROJECTS_ROOT) containing '
'playbooks and related files for this project.')
)
@@ -386,11 +398,19 @@ class Project(CommonModel):
def get_absolute_url(self):
return reverse('main:projects_detail', args=(self.pk,))
+ def get_project_path(self):
+ local_path = os.path.basename(self.local_path)
+ if local_path and not local_path.startswith('.'):
+ proj_path = os.path.join(settings.PROJECTS_ROOT, local_path)
+ if os.path.exists(proj_path):
+ return proj_path
+
@property
- def available_playbooks(self):
- playbooks = []
- if self.local_path and os.path.exists(self.local_path):
- for dirpath, dirnames, filenames in os.walk(self.local_path):
+ def playbooks(self):
+ results = []
+ project_path = self.get_project_path()
+ if project_path:
+ for dirpath, dirnames, filenames in os.walk(project_path):
for filename in filenames:
if os.path.splitext(filename)[-1] != '.yml':
continue
@@ -407,15 +427,15 @@ class Project(CommonModel):
continue
except (TypeError, IndexError, KeyError):
continue
- playbook = os.path.relpath(playbook, self.local_path)
+ playbook = os.path.relpath(playbook, project_path)
# Filter files in a roles subdirectory.
if 'roles' in playbook.split(os.sep):
continue
# Filter files in a tasks subdirectory.
if 'tasks' in playbook.split(os.sep):
continue
- playbooks.append(playbook)
- return playbooks
+ results.append(playbook)
+ return results
class Permission(CommonModelNameNotUnique):
'''
diff --git a/lib/main/serializers.py b/lib/main/serializers.py
index af5f2930b6..b1d1914a9e 100644
--- a/lib/main/serializers.py
+++ b/lib/main/serializers.py
@@ -163,20 +163,35 @@ class AuditTrailSerializer(BaseSerializer):
class ProjectSerializer(BaseSerializer):
- available_playbooks = serializers.Field(source='available_playbooks')
+ playbooks = serializers.Field(source='playbooks')
+ local_path_choices = serializers.SerializerMethodField('get_local_path_choices')
class Meta:
model = Project
- fields = BASE_FIELDS + ('local_path', 'available_playbooks')
+ fields = BASE_FIELDS + ('local_path', 'local_path_choices')
# 'default_playbook', 'scm_type')
def get_related(self, obj):
res = super(ProjectSerializer, self).get_related(obj)
res.update(dict(
organizations = reverse('main:projects_organizations_list', args=(obj.pk,)),
+ playbooks = reverse('main:projects_detail_playbooks', args=(obj.pk,)),
))
return res
+ def get_local_path_choices(self, obj):
+ return Project.get_local_path_choices()
+
+class ProjectPlaybooksSerializer(ProjectSerializer):
+
+ class Meta:
+ model = Project
+ fields = ('playbooks',)
+
+ def to_native(self, obj):
+ ret = super(ProjectPlaybooksSerializer, self).to_native(obj)
+ return ret.get('playbooks', [])
+
class InventorySerializer(BaseSerializer):
class Meta:
diff --git a/lib/main/tasks.py b/lib/main/tasks.py
index a2c5db1ac1..2ea3aeb2f5 100644
--- a/lib/main/tasks.py
+++ b/lib/main/tasks.py
@@ -201,7 +201,10 @@ class RunJob(Task):
kwargs['ssh_key_path'] = self.build_ssh_key_path(job, **kwargs)
kwargs['passwords'] = self.build_passwords(job, **kwargs)
args = self.build_args(job, **kwargs)
- cwd = job.project.local_path
+ cwd = job.project.get_project_path()
+ if not cwd:
+ raise RuntimeError('project local_path %s cannot be found' %
+ project.local_path)
env = self.build_env(job, **kwargs)
status, stdout = self.run_pexpect(job_pk, args, cwd, env,
kwargs['passwords'])
diff --git a/lib/main/tests/base.py b/lib/main/tests/base.py
index 2c8fd094b1..a783a490f6 100644
--- a/lib/main/tests/base.py
+++ b/lib/main/tests/base.py
@@ -95,8 +95,8 @@ class BaseTestMixin(object):
test_playbook_file.write(playbook_content)
test_playbook_file.close()
return Project.objects.create(
- name=name, description=description, local_path=project_dir,
- created_by=created_by,
+ name=name, description=description,
+ local_path=os.path.basename(project_dir), created_by=created_by,
#scm_type='git', default_playbook='foo.yml',
)
diff --git a/lib/main/tests/jobs.py b/lib/main/tests/jobs.py
index 5844d03b4c..3f84459484 100644
--- a/lib/main/tests/jobs.py
+++ b/lib/main/tests/jobs.py
@@ -315,7 +315,7 @@ class BaseJobTest(BaseTest):
job_type='check',
inventory= self.inv_eng,
project=self.proj_dev,
- playbook=self.proj_dev.available_playbooks[0],
+ playbook=self.proj_dev.playbooks[0],
created_by=self.user_sue,
)
self.jt_eng_run = JobTemplate.objects.create(
@@ -323,7 +323,7 @@ class BaseJobTest(BaseTest):
job_type='run',
inventory= self.inv_eng,
project=self.proj_dev,
- playbook=self.proj_dev.available_playbooks[0],
+ playbook=self.proj_dev.playbooks[0],
created_by=self.user_sue,
)
@@ -334,7 +334,7 @@ class BaseJobTest(BaseTest):
job_type='check',
inventory= self.inv_sup,
project=self.proj_test,
- playbook=self.proj_test.available_playbooks[0],
+ playbook=self.proj_test.playbooks[0],
created_by=self.user_sue,
)
self.jt_sup_run = JobTemplate.objects.create(
@@ -342,7 +342,7 @@ class BaseJobTest(BaseTest):
job_type='run',
inventory= self.inv_sup,
project=self.proj_test,
- playbook=self.proj_test.available_playbooks[0],
+ playbook=self.proj_test.playbooks[0],
created_by=self.user_sue,
)
@@ -353,7 +353,7 @@ class BaseJobTest(BaseTest):
job_type='check',
inventory= self.inv_ops_east,
project=self.proj_prod,
- playbook=self.proj_prod.available_playbooks[0],
+ playbook=self.proj_prod.playbooks[0],
credential=self.cred_ops_east,
created_by=self.user_sue,
)
@@ -362,7 +362,7 @@ class BaseJobTest(BaseTest):
job_type='run',
inventory= self.inv_ops_east,
project=self.proj_prod,
- playbook=self.proj_prod.available_playbooks[0],
+ playbook=self.proj_prod.playbooks[0],
credential=self.cred_ops_east,
created_by=self.user_sue,
)
@@ -371,7 +371,7 @@ class BaseJobTest(BaseTest):
job_type='check',
inventory= self.inv_ops_west,
project=self.proj_prod,
- playbook=self.proj_prod.available_playbooks[0],
+ playbook=self.proj_prod.playbooks[0],
credential=self.cred_ops_west,
created_by=self.user_sue,
)
@@ -380,7 +380,7 @@ class BaseJobTest(BaseTest):
job_type='run',
inventory= self.inv_ops_west,
project=self.proj_prod,
- playbook=self.proj_prod.available_playbooks[0],
+ playbook=self.proj_prod.playbooks[0],
credential=self.cred_ops_west,
created_by=self.user_sue,
)
@@ -454,7 +454,7 @@ class JobTemplateTest(BaseJobTest):
inventory = self.inventory.pk,
project = self.project.pk,
job_type = PERM_INVENTORY_DEPLOY,
- playbook = self.project.available_playbooks[0],
+ playbook = self.project.playbooks[0],
)
with self.current_user(self.normal_django_user):
response = self.post(url, data, expect=201)
diff --git a/lib/main/tests/projects.py b/lib/main/tests/projects.py
index b5c3fe96ac..5ee9eeb394 100644
--- a/lib/main/tests/projects.py
+++ b/lib/main/tests/projects.py
@@ -42,7 +42,7 @@ class ProjectsTest(BaseTest):
self.setup_users()
self.organizations = self.make_organizations(self.super_django_user, 10)
- self.projects = self.make_projects(self.normal_django_user, 10)
+ self.projects = self.make_projects(self.normal_django_user, 10, TEST_PLAYBOOK)
# add projects to organizations in a more or less arbitrary way
for project in self.projects[0:2]:
@@ -100,50 +100,56 @@ class ProjectsTest(BaseTest):
# here is a user without any permissions...
return ('nobody', 'nobody')
- def test_available_playbooks(self):
+ def test_playbooks(self):
def write_test_file(project, name, content):
- full_path = os.path.join(project.local_path, name)
+ full_path = os.path.join(project.get_project_path(), name)
if not os.path.exists(os.path.dirname(full_path)):
os.makedirs(os.path.dirname(full_path))
- f = file(os.path.join(project.local_path, name), 'wb')
+ f = file(full_path, 'wb')
f.write(content)
f.close()
# Invalid local_path
project = self.projects[0]
- project.local_path = os.path.join(project.local_path,
- 'does_not_exist')
+ project.local_path = 'path_does_not_exist'
project.save()
- self.assertEqual(len(project.available_playbooks), 0)
+ self.assertFalse(project.get_project_path())
+ self.assertEqual(len(project.playbooks), 0)
# Simple playbook
project = self.projects[1]
+ self.assertEqual(len(project.playbooks), 1)
write_test_file(project, 'foo.yml', TEST_PLAYBOOK)
- self.assertEqual(len(project.available_playbooks), 1)
+ self.assertEqual(len(project.playbooks), 2)
# Other files
project = self.projects[2]
+ self.assertEqual(len(project.playbooks), 1)
write_test_file(project, 'foo.txt', 'not a playbook')
- self.assertEqual(len(project.available_playbooks), 0)
+ self.assertEqual(len(project.playbooks), 1)
# Empty playbook
project = self.projects[3]
+ self.assertEqual(len(project.playbooks), 1)
write_test_file(project, 'blah.yml', '')
- self.assertEqual(len(project.available_playbooks), 0)
+ self.assertEqual(len(project.playbooks), 1)
# Invalid YAML
project = self.projects[4]
+ self.assertEqual(len(project.playbooks), 1)
write_test_file(project, 'blah.yml', TEST_PLAYBOOK + '----')
- self.assertEqual(len(project.available_playbooks), 0)
+ self.assertEqual(len(project.playbooks), 1)
# No hosts or includes
project = self.projects[5]
+ self.assertEqual(len(project.playbooks), 1)
playbook_content = TEST_PLAYBOOK.replace('hosts', 'hoists')
write_test_file(project, 'blah.yml', playbook_content)
- self.assertEqual(len(project.available_playbooks), 0)
+ self.assertEqual(len(project.playbooks), 1)
# Playbook in roles folder
project = self.projects[6]
+ self.assertEqual(len(project.playbooks), 1)
write_test_file(project, 'roles/blah.yml', TEST_PLAYBOOK)
- self.assertEqual(len(project.available_playbooks), 0)
+ self.assertEqual(len(project.playbooks), 1)
# Playbook in tasks folder
project = self.projects[7]
+ self.assertEqual(len(project.playbooks), 1)
write_test_file(project, 'tasks/blah.yml', TEST_PLAYBOOK)
- self.assertEqual(len(project.available_playbooks), 0)
-
+ self.assertEqual(len(project.playbooks), 1)
def test_mainline(self):
@@ -180,6 +186,11 @@ class ProjectsTest(BaseTest):
self.delete(project, expect=204, auth=self.get_normal_credentials())
self.get(project, expect=404, auth=self.get_normal_credentials())
+ # can list playbooks for projects
+ proj_playbooks = '/api/v1/projects/%d/playbooks/' % self.projects[2].pk
+ got = self.get(proj_playbooks, expect=200, auth=self.get_super_credentials())
+ self.assertEqual(got, self.projects[2].playbooks)
+
# can list member organizations for projects
proj_orgs = '/api/v1/projects/1/organizations/'
# only usable as superuser
diff --git a/lib/main/tests/tasks.py b/lib/main/tests/tasks.py
index 37b092f8db..fe74215354 100644
--- a/lib/main/tests/tasks.py
+++ b/lib/main/tests/tasks.py
@@ -177,7 +177,7 @@ class RunJobTest(BaseCeleryTest):
'credential': self.credential,
}
try:
- opts['playbook'] = self.project.available_playbooks[0]
+ opts['playbook'] = self.project.playbooks[0]
except (AttributeError, IndexError):
pass
opts.update(kwargs)
@@ -196,7 +196,7 @@ class RunJobTest(BaseCeleryTest):
'credential': self.credential,
}
try:
- opts['playbook'] = self.project.available_playbooks[0]
+ opts['playbook'] = self.project.playbooks[0]
except (AttributeError, IndexError):
pass
opts.update(kwargs)
diff --git a/lib/main/urls.py b/lib/main/urls.py
index af6e72f77b..0008d979f7 100644
--- a/lib/main/urls.py
+++ b/lib/main/urls.py
@@ -50,6 +50,7 @@ users_urls = patterns('lib.main.views',
projects_urls = patterns('lib.main.views',
url(r'^$', 'projects_list'),
url(r'^(?P[0-9]+)/$', 'projects_detail'),
+ url(r'^(?P[0-9]+)/playbooks/$', 'projects_detail_playbooks'),
url(r'^(?P[0-9]+)/organizations/$', 'projects_organizations_list'),
)
diff --git a/lib/main/views.py b/lib/main/views.py
index bab9c4c9f2..c56467da80 100644
--- a/lib/main/views.py
+++ b/lib/main/views.py
@@ -403,6 +403,12 @@ class ProjectsDetail(BaseDetail):
serializer_class = ProjectSerializer
permission_classes = (CustomRbac,)
+class ProjectsDetailPlaybooks(generics.RetrieveAPIView):
+
+ model = Project
+ serializer_class = ProjectPlaybooksSerializer
+ permission_classes = (CustomRbac,)
+
class ProjectsOrganizationsList(BaseSubList):
model = Organization
diff --git a/lib/settings/local_settings.py.25142.1368115546.78.tmp b/lib/settings/local_settings.py.25142.1368115546.78.tmp
deleted file mode 100644
index 0eb3e522ba..0000000000
--- a/lib/settings/local_settings.py.25142.1368115546.78.tmp
+++ /dev/null
@@ -1,97 +0,0 @@
-# Local Django settings for Ansible Commander project.
-
-# Copyright (c) 2013 AnsibleWorks, Inc.
-#
-# This file is part of Ansible Commander.
-#
-# Ansible Commander is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, version 3 of the License.
-#
-# Ansible Commander is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Ansible Commander. If not, see .
-
-
-from defaults import *
-
-ADMINS = (
- # ('Your Name', 'your_email@domain.com'),
-)
-
-MANAGERS = ADMINS
-
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.postgresql_psycopg2',
- 'NAME': 'acom',
- 'USER': 'ansible_commander',
- 'PASSWORD': 'gateIsDown',
- 'HOST': '',
- 'PORT': '',
- }
-}
-
-if 'test' in sys.argv or 'ACOM_TEST_DATABASE_NAME' in os.environ:
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'acom.sqlite3'),
- # Test database cannot be :memory: for celery/inventory tests to work.
- 'TEST_NAME': os.path.join(BASE_DIR, 'acom_test.sqlite3'),
- }
- }
-
-# Absolute filesystem path to the directory to host projects (with playbooks).
-# This directory should not be web-accessible.
-PROJECTS_ROOT = os.path.join(BASE_DIR, 'projects')
-
-# Local time zone for this installation. Choices can be found here:
-# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
-# although not all choices may be available on all operating systems.
-# On Unix systems, a value of None will cause Django to use the same
-# timezone as the operating system.
-# If running in a Windows environment this must be set to the same as your
-# system time zone.
-TIME_ZONE = 'America/New_York'
-
-# Language code for this installation. All choices can be found here:
-# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'en-us'
-
-# SECURITY WARNING: keep the secret key used in production secret!
-# Hardcoded values can leak through source control. Consider loading
-# the secret key from an environment variable or a file instead.
-SECRET_KEY = 'p7z7g1ql4%6+(6nlebb6hdk7sd^&fnjpal308%n%+p^_e6vo1y'
-
-# Email address that error messages come from.
-SERVER_EMAIL = 'root@localhost'
-
-# The email backend to use. For possible shortcuts see django.core.mail.
-# The default is to use the SMTP backend.
-# Third-party backends can be specified by providing a Python path
-# to a module that defines an EmailBackend class.
-EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
-
-# Host for sending email.
-EMAIL_HOST = 'localhost'
-
-# Port for sending email.
-EMAIL_PORT = 25
-
-# Optional SMTP authentication information for EMAIL_HOST.
-EMAIL_HOST_USER = ''
-EMAIL_HOST_PASSWORD = ''
-EMAIL_USE_TLS = False
-
-# Default email address to use for various automated correspondence from
-# the site managers.
-DEFAULT_FROM_EMAIL = 'webmaster@localhost'
-
-# Subject-line prefix for email messages send with django.core.mail.mail_admins
-# or ...mail_managers. Make sure to include the trailing space.
-EMAIL_SUBJECT_PREFIX = '[ACOM] '
diff --git a/lib/settings/local_settings.py.26911.1368115716.57.tmp b/lib/settings/local_settings.py.26911.1368115716.57.tmp
deleted file mode 100644
index 0eb3e522ba..0000000000
--- a/lib/settings/local_settings.py.26911.1368115716.57.tmp
+++ /dev/null
@@ -1,97 +0,0 @@
-# Local Django settings for Ansible Commander project.
-
-# Copyright (c) 2013 AnsibleWorks, Inc.
-#
-# This file is part of Ansible Commander.
-#
-# Ansible Commander is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, version 3 of the License.
-#
-# Ansible Commander is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Ansible Commander. If not, see .
-
-
-from defaults import *
-
-ADMINS = (
- # ('Your Name', 'your_email@domain.com'),
-)
-
-MANAGERS = ADMINS
-
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.postgresql_psycopg2',
- 'NAME': 'acom',
- 'USER': 'ansible_commander',
- 'PASSWORD': 'gateIsDown',
- 'HOST': '',
- 'PORT': '',
- }
-}
-
-if 'test' in sys.argv or 'ACOM_TEST_DATABASE_NAME' in os.environ:
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'acom.sqlite3'),
- # Test database cannot be :memory: for celery/inventory tests to work.
- 'TEST_NAME': os.path.join(BASE_DIR, 'acom_test.sqlite3'),
- }
- }
-
-# Absolute filesystem path to the directory to host projects (with playbooks).
-# This directory should not be web-accessible.
-PROJECTS_ROOT = os.path.join(BASE_DIR, 'projects')
-
-# Local time zone for this installation. Choices can be found here:
-# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
-# although not all choices may be available on all operating systems.
-# On Unix systems, a value of None will cause Django to use the same
-# timezone as the operating system.
-# If running in a Windows environment this must be set to the same as your
-# system time zone.
-TIME_ZONE = 'America/New_York'
-
-# Language code for this installation. All choices can be found here:
-# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'en-us'
-
-# SECURITY WARNING: keep the secret key used in production secret!
-# Hardcoded values can leak through source control. Consider loading
-# the secret key from an environment variable or a file instead.
-SECRET_KEY = 'p7z7g1ql4%6+(6nlebb6hdk7sd^&fnjpal308%n%+p^_e6vo1y'
-
-# Email address that error messages come from.
-SERVER_EMAIL = 'root@localhost'
-
-# The email backend to use. For possible shortcuts see django.core.mail.
-# The default is to use the SMTP backend.
-# Third-party backends can be specified by providing a Python path
-# to a module that defines an EmailBackend class.
-EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
-
-# Host for sending email.
-EMAIL_HOST = 'localhost'
-
-# Port for sending email.
-EMAIL_PORT = 25
-
-# Optional SMTP authentication information for EMAIL_HOST.
-EMAIL_HOST_USER = ''
-EMAIL_HOST_PASSWORD = ''
-EMAIL_USE_TLS = False
-
-# Default email address to use for various automated correspondence from
-# the site managers.
-DEFAULT_FROM_EMAIL = 'webmaster@localhost'
-
-# Subject-line prefix for email messages send with django.core.mail.mail_admins
-# or ...mail_managers. Make sure to include the trailing space.
-EMAIL_SUBJECT_PREFIX = '[ACOM] '
diff --git a/lib/settings/local_settings.py.28656.1368116066.38.tmp b/lib/settings/local_settings.py.28656.1368116066.38.tmp
deleted file mode 100644
index 0eb3e522ba..0000000000
--- a/lib/settings/local_settings.py.28656.1368116066.38.tmp
+++ /dev/null
@@ -1,97 +0,0 @@
-# Local Django settings for Ansible Commander project.
-
-# Copyright (c) 2013 AnsibleWorks, Inc.
-#
-# This file is part of Ansible Commander.
-#
-# Ansible Commander is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, version 3 of the License.
-#
-# Ansible Commander is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Ansible Commander. If not, see .
-
-
-from defaults import *
-
-ADMINS = (
- # ('Your Name', 'your_email@domain.com'),
-)
-
-MANAGERS = ADMINS
-
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.postgresql_psycopg2',
- 'NAME': 'acom',
- 'USER': 'ansible_commander',
- 'PASSWORD': 'gateIsDown',
- 'HOST': '',
- 'PORT': '',
- }
-}
-
-if 'test' in sys.argv or 'ACOM_TEST_DATABASE_NAME' in os.environ:
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'acom.sqlite3'),
- # Test database cannot be :memory: for celery/inventory tests to work.
- 'TEST_NAME': os.path.join(BASE_DIR, 'acom_test.sqlite3'),
- }
- }
-
-# Absolute filesystem path to the directory to host projects (with playbooks).
-# This directory should not be web-accessible.
-PROJECTS_ROOT = os.path.join(BASE_DIR, 'projects')
-
-# Local time zone for this installation. Choices can be found here:
-# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
-# although not all choices may be available on all operating systems.
-# On Unix systems, a value of None will cause Django to use the same
-# timezone as the operating system.
-# If running in a Windows environment this must be set to the same as your
-# system time zone.
-TIME_ZONE = 'America/New_York'
-
-# Language code for this installation. All choices can be found here:
-# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'en-us'
-
-# SECURITY WARNING: keep the secret key used in production secret!
-# Hardcoded values can leak through source control. Consider loading
-# the secret key from an environment variable or a file instead.
-SECRET_KEY = 'p7z7g1ql4%6+(6nlebb6hdk7sd^&fnjpal308%n%+p^_e6vo1y'
-
-# Email address that error messages come from.
-SERVER_EMAIL = 'root@localhost'
-
-# The email backend to use. For possible shortcuts see django.core.mail.
-# The default is to use the SMTP backend.
-# Third-party backends can be specified by providing a Python path
-# to a module that defines an EmailBackend class.
-EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
-
-# Host for sending email.
-EMAIL_HOST = 'localhost'
-
-# Port for sending email.
-EMAIL_PORT = 25
-
-# Optional SMTP authentication information for EMAIL_HOST.
-EMAIL_HOST_USER = ''
-EMAIL_HOST_PASSWORD = ''
-EMAIL_USE_TLS = False
-
-# Default email address to use for various automated correspondence from
-# the site managers.
-DEFAULT_FROM_EMAIL = 'webmaster@localhost'
-
-# Subject-line prefix for email messages send with django.core.mail.mail_admins
-# or ...mail_managers. Make sure to include the trailing space.
-EMAIL_SUBJECT_PREFIX = '[ACOM] '
diff --git a/lib/settings/local_settings.py.520.1367863259.57.tmp b/lib/settings/local_settings.py.520.1367863259.57.tmp
deleted file mode 100644
index 0eb3e522ba..0000000000
--- a/lib/settings/local_settings.py.520.1367863259.57.tmp
+++ /dev/null
@@ -1,97 +0,0 @@
-# Local Django settings for Ansible Commander project.
-
-# Copyright (c) 2013 AnsibleWorks, Inc.
-#
-# This file is part of Ansible Commander.
-#
-# Ansible Commander is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, version 3 of the License.
-#
-# Ansible Commander is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Ansible Commander. If not, see .
-
-
-from defaults import *
-
-ADMINS = (
- # ('Your Name', 'your_email@domain.com'),
-)
-
-MANAGERS = ADMINS
-
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.postgresql_psycopg2',
- 'NAME': 'acom',
- 'USER': 'ansible_commander',
- 'PASSWORD': 'gateIsDown',
- 'HOST': '',
- 'PORT': '',
- }
-}
-
-if 'test' in sys.argv or 'ACOM_TEST_DATABASE_NAME' in os.environ:
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'acom.sqlite3'),
- # Test database cannot be :memory: for celery/inventory tests to work.
- 'TEST_NAME': os.path.join(BASE_DIR, 'acom_test.sqlite3'),
- }
- }
-
-# Absolute filesystem path to the directory to host projects (with playbooks).
-# This directory should not be web-accessible.
-PROJECTS_ROOT = os.path.join(BASE_DIR, 'projects')
-
-# Local time zone for this installation. Choices can be found here:
-# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
-# although not all choices may be available on all operating systems.
-# On Unix systems, a value of None will cause Django to use the same
-# timezone as the operating system.
-# If running in a Windows environment this must be set to the same as your
-# system time zone.
-TIME_ZONE = 'America/New_York'
-
-# Language code for this installation. All choices can be found here:
-# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'en-us'
-
-# SECURITY WARNING: keep the secret key used in production secret!
-# Hardcoded values can leak through source control. Consider loading
-# the secret key from an environment variable or a file instead.
-SECRET_KEY = 'p7z7g1ql4%6+(6nlebb6hdk7sd^&fnjpal308%n%+p^_e6vo1y'
-
-# Email address that error messages come from.
-SERVER_EMAIL = 'root@localhost'
-
-# The email backend to use. For possible shortcuts see django.core.mail.
-# The default is to use the SMTP backend.
-# Third-party backends can be specified by providing a Python path
-# to a module that defines an EmailBackend class.
-EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
-
-# Host for sending email.
-EMAIL_HOST = 'localhost'
-
-# Port for sending email.
-EMAIL_PORT = 25
-
-# Optional SMTP authentication information for EMAIL_HOST.
-EMAIL_HOST_USER = ''
-EMAIL_HOST_PASSWORD = ''
-EMAIL_USE_TLS = False
-
-# Default email address to use for various automated correspondence from
-# the site managers.
-DEFAULT_FROM_EMAIL = 'webmaster@localhost'
-
-# Subject-line prefix for email messages send with django.core.mail.mail_admins
-# or ...mail_managers. Make sure to include the trailing space.
-EMAIL_SUBJECT_PREFIX = '[ACOM] '
diff --git a/lib/settings/local_settings.py.773.1367863511.14.tmp b/lib/settings/local_settings.py.773.1367863511.14.tmp
deleted file mode 100644
index 0eb3e522ba..0000000000
--- a/lib/settings/local_settings.py.773.1367863511.14.tmp
+++ /dev/null
@@ -1,97 +0,0 @@
-# Local Django settings for Ansible Commander project.
-
-# Copyright (c) 2013 AnsibleWorks, Inc.
-#
-# This file is part of Ansible Commander.
-#
-# Ansible Commander is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, version 3 of the License.
-#
-# Ansible Commander is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Ansible Commander. If not, see .
-
-
-from defaults import *
-
-ADMINS = (
- # ('Your Name', 'your_email@domain.com'),
-)
-
-MANAGERS = ADMINS
-
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.postgresql_psycopg2',
- 'NAME': 'acom',
- 'USER': 'ansible_commander',
- 'PASSWORD': 'gateIsDown',
- 'HOST': '',
- 'PORT': '',
- }
-}
-
-if 'test' in sys.argv or 'ACOM_TEST_DATABASE_NAME' in os.environ:
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'acom.sqlite3'),
- # Test database cannot be :memory: for celery/inventory tests to work.
- 'TEST_NAME': os.path.join(BASE_DIR, 'acom_test.sqlite3'),
- }
- }
-
-# Absolute filesystem path to the directory to host projects (with playbooks).
-# This directory should not be web-accessible.
-PROJECTS_ROOT = os.path.join(BASE_DIR, 'projects')
-
-# Local time zone for this installation. Choices can be found here:
-# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
-# although not all choices may be available on all operating systems.
-# On Unix systems, a value of None will cause Django to use the same
-# timezone as the operating system.
-# If running in a Windows environment this must be set to the same as your
-# system time zone.
-TIME_ZONE = 'America/New_York'
-
-# Language code for this installation. All choices can be found here:
-# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'en-us'
-
-# SECURITY WARNING: keep the secret key used in production secret!
-# Hardcoded values can leak through source control. Consider loading
-# the secret key from an environment variable or a file instead.
-SECRET_KEY = 'p7z7g1ql4%6+(6nlebb6hdk7sd^&fnjpal308%n%+p^_e6vo1y'
-
-# Email address that error messages come from.
-SERVER_EMAIL = 'root@localhost'
-
-# The email backend to use. For possible shortcuts see django.core.mail.
-# The default is to use the SMTP backend.
-# Third-party backends can be specified by providing a Python path
-# to a module that defines an EmailBackend class.
-EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
-
-# Host for sending email.
-EMAIL_HOST = 'localhost'
-
-# Port for sending email.
-EMAIL_PORT = 25
-
-# Optional SMTP authentication information for EMAIL_HOST.
-EMAIL_HOST_USER = ''
-EMAIL_HOST_PASSWORD = ''
-EMAIL_USE_TLS = False
-
-# Default email address to use for various automated correspondence from
-# the site managers.
-DEFAULT_FROM_EMAIL = 'webmaster@localhost'
-
-# Subject-line prefix for email messages send with django.core.mail.mail_admins
-# or ...mail_managers. Make sure to include the trailing space.
-EMAIL_SUBJECT_PREFIX = '[ACOM] '
diff --git a/lib/settings/local_settings.py.987.1367863546.31.tmp b/lib/settings/local_settings.py.987.1367863546.31.tmp
deleted file mode 100644
index 0eb3e522ba..0000000000
--- a/lib/settings/local_settings.py.987.1367863546.31.tmp
+++ /dev/null
@@ -1,97 +0,0 @@
-# Local Django settings for Ansible Commander project.
-
-# Copyright (c) 2013 AnsibleWorks, Inc.
-#
-# This file is part of Ansible Commander.
-#
-# Ansible Commander is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, version 3 of the License.
-#
-# Ansible Commander is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Ansible Commander. If not, see .
-
-
-from defaults import *
-
-ADMINS = (
- # ('Your Name', 'your_email@domain.com'),
-)
-
-MANAGERS = ADMINS
-
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.postgresql_psycopg2',
- 'NAME': 'acom',
- 'USER': 'ansible_commander',
- 'PASSWORD': 'gateIsDown',
- 'HOST': '',
- 'PORT': '',
- }
-}
-
-if 'test' in sys.argv or 'ACOM_TEST_DATABASE_NAME' in os.environ:
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'acom.sqlite3'),
- # Test database cannot be :memory: for celery/inventory tests to work.
- 'TEST_NAME': os.path.join(BASE_DIR, 'acom_test.sqlite3'),
- }
- }
-
-# Absolute filesystem path to the directory to host projects (with playbooks).
-# This directory should not be web-accessible.
-PROJECTS_ROOT = os.path.join(BASE_DIR, 'projects')
-
-# Local time zone for this installation. Choices can be found here:
-# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
-# although not all choices may be available on all operating systems.
-# On Unix systems, a value of None will cause Django to use the same
-# timezone as the operating system.
-# If running in a Windows environment this must be set to the same as your
-# system time zone.
-TIME_ZONE = 'America/New_York'
-
-# Language code for this installation. All choices can be found here:
-# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'en-us'
-
-# SECURITY WARNING: keep the secret key used in production secret!
-# Hardcoded values can leak through source control. Consider loading
-# the secret key from an environment variable or a file instead.
-SECRET_KEY = 'p7z7g1ql4%6+(6nlebb6hdk7sd^&fnjpal308%n%+p^_e6vo1y'
-
-# Email address that error messages come from.
-SERVER_EMAIL = 'root@localhost'
-
-# The email backend to use. For possible shortcuts see django.core.mail.
-# The default is to use the SMTP backend.
-# Third-party backends can be specified by providing a Python path
-# to a module that defines an EmailBackend class.
-EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
-
-# Host for sending email.
-EMAIL_HOST = 'localhost'
-
-# Port for sending email.
-EMAIL_PORT = 25
-
-# Optional SMTP authentication information for EMAIL_HOST.
-EMAIL_HOST_USER = ''
-EMAIL_HOST_PASSWORD = ''
-EMAIL_USE_TLS = False
-
-# Default email address to use for various automated correspondence from
-# the site managers.
-DEFAULT_FROM_EMAIL = 'webmaster@localhost'
-
-# Subject-line prefix for email messages send with django.core.mail.mail_admins
-# or ...mail_managers. Make sure to include the trailing space.
-EMAIL_SUBJECT_PREFIX = '[ACOM] '