diff --git a/TODO.md b/TODO.md index 74c25d43..178b3ecf 100644 --- a/TODO.md +++ b/TODO.md @@ -163,3 +163,6 @@ APPS app? * pip upgrade or install + + +* disable account triggers save on cascade to execute backends save(update_field=[]) diff --git a/orchestra/admin/forms.py b/orchestra/admin/forms.py index 0eb2f397..8b9dab9c 100644 --- a/orchestra/admin/forms.py +++ b/orchestra/admin/forms.py @@ -111,14 +111,15 @@ class AdminPasswordChangeForm(forms.Form): if password: self.user.set_password(password) if commit: - self.user.save() + self.user.save(update_fields=['password']) for ix, rel in enumerate(self.related): password = self.cleaned_data['password1_%s' % ix] if password: + print password set_password = getattr(rel, 'set_password') set_password(password) if commit: - rel.save() + rel.save(update_fields=['password']) return self.user def _get_changed_data(self): diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py index 9be768dd..f67be137 100644 --- a/orchestra/admin/utils.py +++ b/orchestra/admin/utils.py @@ -33,28 +33,28 @@ def get_modeladmin(model, import_module=True): def insertattr(model, name, value, weight=0): """ Inserts attribute to a modeladmin """ - modeladmin = model + modeladmin_class = model if models.Model in model.__mro__: - modeladmin = type(get_modeladmin(model)) + modeladmin_class = type(get_modeladmin(model)) # Avoid inlines defined on parent class be shared between subclasses # Seems that if we use tuples they are lost in some conditions like changing # the tuple in modeladmin.__init__ - if not getattr(modeladmin, name): - setattr(type(modeladmin), name, []) + if not getattr(modeladmin_class, name): + setattr(modeladmin_class, name, []) - inserted_attrs = getattr(modeladmin, '__inserted_attrs__', {}) + inserted_attrs = getattr(modeladmin_class, '__inserted_attrs__', {}) if not name in inserted_attrs: weights = {} - if hasattr(modeladmin, 'weights') and name in modeladmin.weights: - weights = modeladmin.weights.get(name) + if hasattr(modeladmin_class, 'weights') and name in modeladmin_class.weights: + weights = modeladmin_class.weights.get(name) inserted_attrs[name] = [ - (attr, weights.get(attr, 0)) for attr in getattr(modeladmin, name) + (attr, weights.get(attr, 0)) for attr in getattr(modeladmin_class, name) ] inserted_attrs[name].append((value, weight)) inserted_attrs[name].sort(key=lambda a: a[1]) - setattr(modeladmin, name, [ attr[0] for attr in inserted_attrs[name] ]) - setattr(modeladmin, '__inserted_attrs__', inserted_attrs) + setattr(modeladmin_class, name, [ attr[0] for attr in inserted_attrs[name] ]) + setattr(modeladmin_class, '__inserted_attrs__', inserted_attrs) def wrap_admin_view(modeladmin, view): diff --git a/orchestra/api/actions.py b/orchestra/api/actions.py index 0a02be68..bfc3f04b 100644 --- a/orchestra/api/actions.py +++ b/orchestra/api/actions.py @@ -9,10 +9,13 @@ class SetPasswordApiMixin(object): @action(serializer_class=SetPasswordSerializer) def set_password(self, request, pk): obj = self.get_object() - serializer = SetPasswordSerializer(data=request.DATA) + data = request.DATA + if isinstance(data, basestring): + data = {'password': data} + serializer = SetPasswordSerializer(data=data) if serializer.is_valid(): obj.set_password(serializer.data['password']) - obj.save() + obj.save(update_fields=['password']) return Response({'status': 'password changed'}) else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/orchestra/api/options.py b/orchestra/api/options.py index 19b72a2f..d94e0bd7 100644 --- a/orchestra/api/options.py +++ b/orchestra/api/options.py @@ -1,8 +1,9 @@ from django.core.exceptions import ImproperlyConfigured +from django.utils.module_loading import autodiscover_modules from rest_framework.routers import DefaultRouter, Route, flatten, replace_methodname from orchestra import settings -from orchestra.utils.apps import autodiscover as module_autodiscover +#from orchestra.utils.apps import autodiscover as module_autodiscover from orchestra.utils.python import import_class from .helpers import insert_links, replace_collectionmethodname @@ -99,16 +100,16 @@ class LinkHeaderRouter(DefaultRouter): def insert(self, prefix_or_model, name, field, **kwargs): """ Dynamically add new fields to an existing serializer """ viewset = self.get_viewset(prefix_or_model) - setattr(viewset, 'inserted', getattr(viewset, 'inserted', [])) +# setattr(viewset, 'inserted', getattr(viewset, 'inserted', [])) if viewset.serializer_class is None: viewset.serializer_class = viewset().get_serializer_class() viewset.serializer_class.base_fields.update({name: field(**kwargs)}) - if not name in viewset.inserted: - viewset.serializer_class.Meta.fields += (name,) - viewset.inserted.append(name) +# if not name in viewset.inserted: + viewset.serializer_class.Meta.fields += (name,) +# viewset.inserted.append(name) # Create a router and register our viewsets with it. router = LinkHeaderRouter() -autodiscover = lambda: (module_autodiscover('api'), module_autodiscover('serializers')) +autodiscover = lambda: (autodiscover_modules('api'), autodiscover_modules('serializers')) diff --git a/orchestra/apps/accounts/api.py b/orchestra/apps/accounts/api.py index d9479b0b..6ee24660 100644 --- a/orchestra/apps/accounts/api.py +++ b/orchestra/apps/accounts/api.py @@ -1,6 +1,6 @@ from rest_framework import viewsets -from orchestra.api import router +from orchestra.api import router, SetPasswordApiMixin from .models import Account from .serializers import AccountSerializer @@ -12,7 +12,7 @@ class AccountApiMixin(object): return qs.filter(account=self.request.user.pk) -class AccountViewSet(viewsets.ModelViewSet): +class AccountViewSet(SetPasswordApiMixin, viewsets.ModelViewSet): model = Account serializer_class = AccountSerializer singleton_pk = lambda _,request: request.user.pk diff --git a/orchestra/apps/databases/admin.py b/orchestra/apps/databases/admin.py index db84e83f..293612ae 100644 --- a/orchestra/apps/databases/admin.py +++ b/orchestra/apps/databases/admin.py @@ -42,7 +42,7 @@ class PermissionInline(AccountAdminMixin, admin.TabularInline): """ Make value input widget bigger """ formfield = super(PermissionInline, self).formfield_for_dbfield(db_field, **kwargs) if db_field.name == 'database': - # Hack widget render in order to append ?account=id to the add url + # Hack widget render in order to append ?type='db_type' to the add url db_type = self.parent_object.type old_render = formfield.widget.render def render(*args, **kwargs): diff --git a/orchestra/apps/databases/backends.py b/orchestra/apps/databases/backends.py index 89cfc80e..1985e965 100644 --- a/orchestra/apps/databases/backends.py +++ b/orchestra/apps/databases/backends.py @@ -6,7 +6,7 @@ from orchestra.apps.resources import ServiceMonitor from . import settings -class MySQLDBBackend(ServiceController): +class MySQLBackend(ServiceController): verbose_name = "MySQL database" model = 'databases.Database' diff --git a/orchestra/apps/databases/models.py b/orchestra/apps/databases/models.py index 079ef0c2..33081a14 100644 --- a/orchestra/apps/databases/models.py +++ b/orchestra/apps/databases/models.py @@ -32,7 +32,7 @@ class Database(models.Model): @property def owner(self): - self.users.get(is_owner=True) + return self.roles.get(is_owner=True).user class Role(models.Model): @@ -52,6 +52,11 @@ class Role(models.Model): if self.user.type != self.database.type: msg = _("Database and user type doesn't match") raise validators.ValidationError(msg) + roles = self.database.roles.values('id') + print roles + if not roles or (len(roles) == 1 and roles[0].id == self.id): + print 'seld' + self.is_owner = True class DatabaseUser(models.Model): diff --git a/orchestra/apps/databases/serializers.py b/orchestra/apps/databases/serializers.py index 5dbad818..740aa357 100644 --- a/orchestra/apps/databases/serializers.py +++ b/orchestra/apps/databases/serializers.py @@ -14,7 +14,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): fields = ('user', 'is_owner',) -class PermissionSerializer(serializers.HyperlinkedModelSerializer): +class RoleSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Role fields = ('database', 'is_owner',) @@ -32,9 +32,9 @@ class DatabaseUserSerializer(AccountSerializerMixin, serializers.HyperlinkedMode password = serializers.CharField(max_length=128, label=_('Password'), validators=[validate_password], write_only=True, widget=widgets.PasswordInput) - permission = PermissionSerializer(source='roles', many=True) + roles = RoleSerializer(many=True, read_only=True) class Meta: model = DatabaseUser - fields = ('url', 'username', 'password', 'type', 'permission') + fields = ('url', 'username', 'password', 'type', 'roles') write_only_fields = ('username',) diff --git a/orchestra/apps/databases/tests/functional_tests/tests.py b/orchestra/apps/databases/tests/functional_tests/tests.py index f142ccbc..51326cdd 100644 --- a/orchestra/apps/databases/tests/functional_tests/tests.py +++ b/orchestra/apps/databases/tests/functional_tests/tests.py @@ -1,4 +1,5 @@ -#import MySQLdb +import MySQLdb +import os from functools import partial from django.conf import settings as djsettings @@ -9,21 +10,22 @@ from selenium.webdriver.support.select import Select from orchestra.apps.accounts.models import Account from orchestra.apps.orchestration.models import Server, Route from orchestra.utils.system import run -from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii +from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii, save_response_on_error, + snapshot_on_error) from ... import backends, settings from ...models import Database class DatabaseTestMixin(object): - MASTER_ADDR = 'localhost' + MASTER_SERVER = os.environ.get('ORCHESTRA_SECOND_SERVER', 'localhost') DEPENDENCIES = ( 'orchestra.apps.orchestration', 'orcgestra.apps.databases', ) def setUp(self): - super(SystemUserMixin, self).setUp() + super(DatabaseTestMixin, self).setUp() self.add_route() djsettings.DEBUG = True @@ -49,28 +51,79 @@ class DatabaseTestMixin(object): raise NotImplementedError def test_add(self): - self.add() + dbname = '%s_database' % random_ascii(5) + username = '%s_dbuser' % random_ascii(10) + password = '@!?%spppP001' % random_ascii(5) + self.add(dbname, username, password) + self.validate_create_table(dbname, username, password) - - -class MysqlBackendMixin(object): +class MySQLBackendMixin(object): + db_type = 'mysql' + def add_route(self): - server = Server.objects.create(name=self.MASTER_ADDR) - backend = backends.MysqlBackend.get_name() - Route.objects.create(backend=backend, match="database.type == 'mysql'", host=server) + server = Server.objects.create(name=self.MASTER_SERVER) + backend = backends.MySQLBackend.get_name() + match = "database.type == '%s'" % self.db_type + Route.objects.create(backend=backend, match=match, host=server) + match = "databaseuser.type == '%s'" % self.db_type + backend = backends.MySQLUserBackend.get_name() + Route.objects.create(backend=backend, match=match, host=server) def validate_create_table(self, name, username, password): - db = MySQLdb.connect(host=self.MASTER_ADDR, user=username, passwd=password, db=name) + db = MySQLdb.connect(host=self.MASTER_SERVER, port=3306, user=username, passwd=password, db=name) cur = db.cursor() cur.execute('CREATE TABLE test;') def validate_delete(self, name, username, password): self.asseRaises(MySQLdb.ConnectionError, - MySQLdb.connect(host=self.MASTER_ADDR, user=username, passwd=password, db=name)) + self.validate_create_table, name, username, password) -class RESTDatabaseTest(DatabaseTestMixin): - def add(self, dbname): - self.api.databases.create(name=dbname) +class RESTDatabaseMixin(DatabaseTestMixin): + def setUp(self): + super(RESTDatabaseMixin, self).setUp() + self.rest_login() + + @save_response_on_error + def add(self, dbname, username, password): + user = self.rest.databaseusers.create(username=username, password=password) + self.rest.databases.create(name=dbname, user=user, type=self.db_type) + + +class AdminDatabaseMixin(DatabaseTestMixin): + def setUp(self): + super(AdminDatabaseMixin, self).setUp() + self.admin_login() + + @snapshot_on_error + def add(self, dbname, username, password): + url = self.live_server_url + reverse('admin:databases_database_add') + self.selenium.get(url) + + type_input = self.selenium.find_element_by_id('id_type') + type_select = Select(type_input) + type_select.select_by_value(self.db_type) + + name_field = self.selenium.find_element_by_id('id_name') + name_field.send_keys(dbname) + + username_field = self.selenium.find_element_by_id('id_username') + username_field.send_keys(username) + + password_field = self.selenium.find_element_by_id('id_password1') + password_field.send_keys(password) + password_field = self.selenium.find_element_by_id('id_password2') + password_field.send_keys(password) + + name_field.submit() + self.assertNotEqual(url, self.selenium.current_url) + + +class RESTMysqlDatabaseTest(MySQLBackendMixin, RESTDatabaseMixin, BaseLiveServerTestCase): + pass + + +class AdminMysqlDatabaseTest(MySQLBackendMixin, AdminDatabaseMixin, BaseLiveServerTestCase): + pass diff --git a/orchestra/apps/domains/tests/functional_tests/tests.py b/orchestra/apps/domains/tests/functional_tests/tests.py index 2f159866..4060f41b 100644 --- a/orchestra/apps/domains/tests/functional_tests/tests.py +++ b/orchestra/apps/domains/tests/functional_tests/tests.py @@ -257,12 +257,7 @@ class AdminDomainMixin(DomainTestMixin): @snapshot_on_error def delete(self, domain_name): domain = Domain.objects.get(name=domain_name) - delete = reverse('admin:domains_domain_delete', args=(domain.pk,)) - url = self.live_server_url + delete - self.selenium.get(url) - form = self.selenium.find_element_by_name('post') - form.submit() - self.assertNotEqual(url, self.selenium.current_url) + self.admin_delete(domain) @snapshot_on_error def update(self, domain_name, records): diff --git a/orchestra/apps/mails/admin.py b/orchestra/apps/mails/admin.py index 9237aa7c..7f008a9c 100644 --- a/orchestra/apps/mails/admin.py +++ b/orchestra/apps/mails/admin.py @@ -43,7 +43,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdm fieldsets = ( (None, { 'classes': ('wide',), - 'fields': ('account_link', 'name', 'password'), + 'fields': ('name', 'password', 'is_active', 'account_link'), }), (_("Filtering"), { 'classes': ('collapse',), diff --git a/orchestra/apps/mails/api.py b/orchestra/apps/mails/api.py index 24baeeca..9e452c68 100644 --- a/orchestra/apps/mails/api.py +++ b/orchestra/apps/mails/api.py @@ -1,6 +1,6 @@ from rest_framework import viewsets -from orchestra.api import router +from orchestra.api import router, SetPasswordApiMixin from orchestra.apps.accounts.api import AccountApiMixin from .models import Address, Mailbox @@ -13,7 +13,7 @@ class AddressViewSet(AccountApiMixin, viewsets.ModelViewSet): -class MailboxViewSet(AccountApiMixin, viewsets.ModelViewSet): +class MailboxViewSet(SetPasswordApiMixin, AccountApiMixin, viewsets.ModelViewSet): model = Mailbox serializer_class = MailboxSerializer diff --git a/orchestra/apps/mails/backends.py b/orchestra/apps/mails/backends.py index b6418d3a..021968b9 100644 --- a/orchestra/apps/mails/backends.py +++ b/orchestra/apps/mails/backends.py @@ -28,7 +28,7 @@ class PasswdVirtualUserBackend(ServiceController): def set_user(self, context): self.append(textwrap.dedent(""" if [[ $( grep "^%(username)s:" %(passwd_path)s ) ]]; then - sed -i "s/^%(username)s:.*/%(passwd)s/" %(passwd_path)s + sed -i 's#^%(username)s:.*#%(passwd)s#' %(passwd_path)s else echo '%(passwd)s' >> %(passwd_path)s fi""" % context @@ -49,22 +49,6 @@ class PasswdVirtualUserBackend(ServiceController): context['filter_path'] = os.path.join(context['home'], '.orchestra.sieve') self.append("echo '%(filtering)s' > %(filter_path)s" % context) - def set_quota(self, mailbox, context): - if not hasattr(mailbox, 'resources'): - return - context.update({ - 'maildir_path': '~%(username)s/Maildir' % context, - 'maildirsize_path': '~%(username)s/Maildir/maildirsize' % context, - 'quota': mailbox.resources.disk.allocated*1000*1000, - }) - self.append("mkdir -p %(maildir_path)s" % context) - self.append(textwrap.dedent(""" - sed -i '1s/.*/%(quota)s,S/' %(maildirsize_path)s || { - echo '%(quota)s,S' > %(maildirsize_path)s && - chown %(username)s %(maildirsize_path)s; - }""" % context - )) - def save(self, mailbox): context = self.get_context(mailbox) self.set_user(context) @@ -73,9 +57,11 @@ class PasswdVirtualUserBackend(ServiceController): def delete(self, mailbox): context = self.get_context(mailbox) self.append("{ sleep 2 && killall -u %(uid)s -s KILL; } &" % context) - self.append("killall -u %(uid)s" % context) + self.append("killall -u %(uid)s || true" % context) self.append("sed -i '/^%(username)s:.*/d' %(passwd_path)s" % context) - self.append("rm -fr %(home)s" % context) + # TODO delete + context['deleted'] = context['home'].rstrip('/') + '.deleted' + self.append("mv %(home)s %(deleted)s" % context) def get_extra_fields(self, mailbox, context): context['quota'] = self.get_quota(mailbox) diff --git a/orchestra/apps/mails/models.py b/orchestra/apps/mails/models.py index 8e2e1bb5..1f8e4c25 100644 --- a/orchestra/apps/mails/models.py +++ b/orchestra/apps/mails/models.py @@ -36,7 +36,10 @@ class Mailbox(models.Model): @cached_property def active(self): - return self.is_active and self.account.is_active + try: + return self.is_active and self.account.is_active + except type(self).account.field.rel.to.DoesNotExist: + return self.is_active def set_password(self, raw_password): self.password = make_password(raw_password) diff --git a/orchestra/apps/mails/serializers.py b/orchestra/apps/mails/serializers.py index e0435f9f..d48121e6 100644 --- a/orchestra/apps/mails/serializers.py +++ b/orchestra/apps/mails/serializers.py @@ -9,7 +9,7 @@ class MailboxSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri class Meta: model = Mailbox # TODO 'use_custom_filtering', - fields = ('url', 'name', 'password', 'custom_filtering', 'addresses') + fields = ('url', 'name', 'password', 'custom_filtering', 'addresses', 'is_active') def validate_password(self, attrs, source): """ POST only password """ @@ -44,6 +44,6 @@ class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri def validate(self, attrs): if not attrs['mailboxes'] and not attrs['forward']: - raise serializers.ValidationError("mailboxes or forward should be provided") + raise serializers.ValidationError("mailboxes or forward addresses should be provided") return attrs diff --git a/orchestra/apps/mails/tests/functional_tests/tests.py b/orchestra/apps/mails/tests/functional_tests/tests.py index 89cac724..2d519d4e 100644 --- a/orchestra/apps/mails/tests/functional_tests/tests.py +++ b/orchestra/apps/mails/tests/functional_tests/tests.py @@ -20,31 +20,8 @@ from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii, snapshot from ... import backends, settings from ...models import Mailbox -#>>> mail.list() -#('OK', ['(\\HasNoChildren) "." INBOX']) -#>>> mail.select('INBOX') -#('OK', ['18']) - -#>>> mail.getquota('INBOX') -#imaplib.error: GETQUOTA command error: BAD ['Error in IMAP command GETQUOTA: Unknown command.'] - -#mail.fetch(10, '(RFC822)') -#('OK', [('10 (FLAGS (\\Seen) RFC822 {550}', 'Return-Path: \r\nDelivered-To: \r\nReceived: from test3.orchestra.lan\r\n\tby test3.orchestra.lan (Dovecot) with LMTP id hvDUEAIKL1QlOQAAL4hJug\r\n\tfor ; Fri, 03 Oct 2014 16:41:38 -0400\r\nReceived: by test3.orchestra.lan (Postfix, from userid 0)\r\n\tid 43BB1F94633; Fri, 3 Oct 2014 16:41:38 -0400 (EDT)\r\nTo: rata@orchestra.lan\r\nSubject: hola\r\nMessage-Id: <20141003204138.43BB1F94633@test3.orchestra.lan>\r\nDate: Fri, 3 Oct 2014 16:41:38 -0400 (EDT)\r\nFrom: root@test3.orchestra.lan (root)\r\n\r\n\r\n\r\n'), ')']) -#>>> mail.close() -#('OK', ['Close completed.']) - -#pop = poplib.POP3('localhost') -#pop.user('rata') -#pop.pass_('3') -#>>> pop.list() -#('+OK 18 messages:', ['1 552', '2 550', '3 550', '4 548', '5 546', '6 546', '7 554', '8 548', '9 550', '10 550', '11 546', '12 546', '13 546', '14 544', '15 548', '16 577', '17 546', '18 546'], 135) -#>>> pop.quit() -#'+OK Logging out.' - - -# FIXME django load production database at the begining of tests class MailboxMixin(object): MASTER_SERVER = os.environ.get('ORCHESTRA_SLAVE_SERVER', 'localhost') DEPENDENCIES = ( @@ -56,7 +33,10 @@ class MailboxMixin(object): def setUp(self): super(MailboxMixin, self).setUp() self.add_route() -# apps.get_app_config('resources').reload_relations() doesn't work + # TODO fix this + from django.apps import apps + # clean resource relation from other tests + apps.get_app_config('resources').reload_relations() djsettings.DEBUG = True def add_route(self): @@ -96,27 +76,6 @@ class MailboxMixin(object): def add_group(self, username, groupname): raise NotImplementedError - def validate_user(self, username): - idcmd = sshr(self.MASTER_SERVER, "id %s" % username) - self.assertEqual(0, idcmd.return_code) - user = SystemUser.objects.get(username=username) - groups = list(user.groups.values_list('username', flat=True)) - groups.append(user.username) - idgroups = idcmd.stdout.strip().split(' ')[2] - idgroups = re.findall(r'\d+\((\w+)\)', idgroups) - self.assertEqual(set(groups), set(idgroups)) - - def validate_delete(self, username): - self.assertRaises(SystemUser.DoesNotExist, SystemUser.objects.get, username=username) - self.assertRaises(CommandError, - sshrun, self.MASTER_SERVER,'id %s' % username, display=False) - self.assertRaises(CommandError, - sshrun, self.MASTER_SERVER, 'grep "^%s:" /etc/groups' % username, display=False) - self.assertRaises(CommandError, - sshrun, self.MASTER_SERVER, 'grep "^%s:" /etc/passwd' % username, display=False) - self.assertRaises(CommandError, - sshrun, self.MASTER_SERVER, 'grep "^%s:" /etc/shadow' % username, display=False) - def login_imap(self, username, password): mail = imaplib.IMAP4_SSL(self.MASTER_SERVER) status, msg = mail.login(username, password) @@ -144,6 +103,9 @@ class MailboxMixin(object): finally: server.quit() + def validate_mailbox(self, username): + sshrun(self.MASTER_SERVER, "doveadm search -u %s ALL" % username, display=False) + def validate_email(self, username, token): home = Mailbox.objects.get(name=username).get_home() sshrun(self.MASTER_SERVER, "grep '%s' %s/Maildir/new/*" % (token, home), display=False) @@ -152,14 +114,15 @@ class MailboxMixin(object): username = '%s_mailbox' % random_ascii(10) password = '@!?%spppP001' % random_ascii(5) self.add(username, password) - self.addCleanup(partial(self.delete, username)) + self.addCleanup(self.delete, username) imap = self.login_imap(username, password) + self.validate_mailbox(username) def test_change_password(self): username = '%s_systemuser' % random_ascii(10) password = '@!?%spppP001' % random_ascii(5) self.add(username, password) - self.addCleanup(partial(self.delete, username)) + self.addCleanup(self.delete, username) imap = self.login_imap(username, password) new_password = '@!?%spppP001' % random_ascii(5) self.change_password(username, new_password) @@ -171,7 +134,7 @@ class MailboxMixin(object): self.add_quota_resource() quota = 100 self.add(username, password, quota=quota) - self.addCleanup(partial(self.delete, username)) + self.addCleanup(self.delete, username) get_quota = "doveadm quota get -u %s 2>&1|grep STORAGE|awk {'print $5'}" % username stdout = sshrun(self.MASTER_SERVER, get_quota, display=False).stdout self.assertEqual(quota*1024, int(stdout)) @@ -183,7 +146,7 @@ class MailboxMixin(object): username = '%s_mailbox' % random_ascii(10) password = '@!?%spppP001' % random_ascii(5) self.add(username, password) - self.addCleanup(partial(self.delete, username)) + self.addCleanup(self.delete, username) msg = MIMEText("Hola bishuns") msg['To'] = 'noexists@example.com' msg['From'] = '%s@%s' % (username, self.MASTER_SERVER) @@ -199,7 +162,7 @@ class MailboxMixin(object): username = '%s_mailbox' % random_ascii(10) password = '@!?%spppP001' % random_ascii(5) self.add(username, password) - self.addCleanup(partial(self.delete, username)) + self.addCleanup(self.delete, username) domain = '%s_domain.lan' % random_ascii(5) name = '%s_name' % random_ascii(5) domain = self.account.domains.create(name=domain) @@ -207,6 +170,47 @@ class MailboxMixin(object): token = random_ascii(100) self.send_email("%s@%s" % (name, domain), token) self.validate_email(username, token) + + def test_disable(self): + username = '%s_systemuser' % random_ascii(10) + password = '@!?%spppP001' % random_ascii(5) + self.add(username, password) + self.validate_mailbox(username) + self.addCleanup(self.delete, username) + imap = self.login_imap(username, password) + self.disable(username) + self.assertRaises(imap.error, self.login_imap, username, password) + + def test_delete(self): + username = '%s_systemuser' % random_ascii(10) + password = '@!?%sppppP001' % random_ascii(5) + self.add(username, password) + imap = self.login_imap(username, password) + self.validate_mailbox(username) + mailbox = Mailbox.objects.get(name=username) + home = mailbox.get_home() + self.delete(username) + self.assertRaises(Mailbox.DoesNotExist, Mailbox.objects.get, name=username) + self.assertRaises(CommandError, self.validate_mailbox, username) + self.assertRaises(imap.error, self.login_imap, username, password) + self.assertRaises(CommandError, + sshrun, self.MASTER_SERVER, 'ls %s' % home, display=False) + + def test_delete_address(self): + username = '%s_mailbox' % random_ascii(10) + password = '@!?%spppP001' % random_ascii(5) + self.add(username, password) + self.addCleanup(self.delete, username) + domain = '%s_domain.lan' % random_ascii(5) + name = '%s_name' % random_ascii(5) + domain = self.account.domains.create(name=domain) + self.add_address(username, name, domain) + token = random_ascii(100) + self.send_email("%s@%s" % (name, domain), token) + self.validate_email(username, token) + self.delete_address(username) + self.send_email("%s@%s" % (name, domain), token) + self.validate_email(username, token) class RESTMailboxMixin(MailboxMixin): @@ -243,7 +247,21 @@ class RESTMailboxMixin(MailboxMixin): mailbox = self.rest.mailboxes.retrieve(name=username).get() domain = self.rest.domains.retrieve(name=domain.name).get() self.rest.addresses.create(name=name, domain=domain, mailboxes=[mailbox]) - + + @save_response_on_error + def delete_address(self, username): + mailbox = self.rest.mailboxes.retrieve(name=username).get() + self.rest.addresses.delete() + + @save_response_on_error + def change_password(self, username, password): + mailbox = self.rest.mailboxes.retrieve(name=username).get() + mailbox.set_password(password=password) + + @save_response_on_error + def disable(self, username): + mailbox = self.rest.mailboxes.retrieve(name=username).get() + mailbox.update(is_active=False) class AdminMailboxMixin(MailboxMixin): @@ -267,8 +285,12 @@ class AdminMailboxMixin(MailboxMixin): password_field.send_keys(password) password_field = self.selenium.find_element_by_id('id_password2') password_field.send_keys(password) - if quota is not None: + from orchestra.admin.utils import get_modeladmin + m = get_modeladmin(Mailbox) + print 't', type(m).inlines + print 'm', m.inlines + self.take_screenshot() quota_field = self.selenium.find_element_by_id( 'id_resources-resourcedata-content_type-object_id-0-allocated') quota_field.clear() @@ -280,27 +302,12 @@ class AdminMailboxMixin(MailboxMixin): @snapshot_on_error def delete(self, username): mailbox = Mailbox.objects.get(name=username) - delete = reverse('admin:mails_mailbox_delete', args=(mailbox.pk,)) - url = self.live_server_url + delete - self.selenium.get(url) - confirmation = self.selenium.find_element_by_name('post') - confirmation.submit() - self.assertNotEqual(url, self.selenium.current_url) + self.admin_delete(mailbox) @snapshot_on_error def change_password(self, username, password): mailbox = Mailbox.objects.get(name=username) - change_password = reverse('admin:mails_mailbox_change_password', args=(mailbox.pk,)) - url = self.live_server_url + change_password - self.selenium.get(url) - - password_field = self.selenium.find_element_by_id('id_password1') - password_field.send_keys(password) - password_field = self.selenium.find_element_by_id('id_password2') - password_field.send_keys(password) - password_field.submit() - - self.assertNotEqual(url, self.selenium.current_url) + self.admin_change_password(mailbox, password) @snapshot_on_error def add_address(self, username, name, domain): @@ -320,6 +327,18 @@ class AdminMailboxMixin(MailboxMixin): name_field.submit() self.assertNotEqual(url, self.selenium.current_url) + + @snapshot_on_error + def delete_address(self, username): + mailbox = Mailbox.objects.get(name=username) + address = mailbox.addresses.get() + self.admin_delete(address) + + @snapshot_on_error + def disable(self, username): + mailbox = Mailbox.objects.get(name=username) + self.admin_disable(mailbox) + class RESTMailboxTest(RESTMailboxMixin, BaseLiveServerTestCase): pass diff --git a/orchestra/apps/orchestration/models.py b/orchestra/apps/orchestration/models.py index 2e13a65f..826b795f 100644 --- a/orchestra/apps/orchestration/models.py +++ b/orchestra/apps/orchestration/models.py @@ -3,11 +3,12 @@ import socket from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.db import models +from django.utils.module_loading import autodiscover_modules from django.utils.translation import ugettext_lazy as _ from orchestra.core.validators import validate_ip_address, ValidationError from orchestra.models.fields import NullableCharField -from orchestra.utils.apps import autodiscover +#from orchestra.utils.apps import autodiscover from . import settings, manager from .backends import ServiceBackend @@ -133,7 +134,7 @@ class BackendOperation(models.Model): return ServiceBackend.get_backend(self.backend) -autodiscover('backends') +autodiscover_modules('backends') class Route(models.Model): diff --git a/orchestra/apps/resources/admin.py b/orchestra/apps/resources/admin.py index 7ff83d40..8d5cb35d 100644 --- a/orchestra/apps/resources/admin.py +++ b/orchestra/apps/resources/admin.py @@ -132,23 +132,26 @@ def resource_inline_factory(resources): def has_add_permission(self, *args, **kwargs): """ Hidde add another """ return False - return ResourceInline +from orchestra.utils import database_ready def insert_resource_inlines(): + # Clean previous state + for related in Resource._related: + modeladmin = get_modeladmin(related) + modeladmin_class = type(modeladmin) + for inline in getattr(modeladmin_class, 'inlines', []): + if inline.__name__ == 'ResourceInline': + modeladmin_class.inlines.remove(inline) + modeladmin.inlines = modeladmin_class.inlines + for ct, resources in Resource.objects.group_by('content_type').iteritems(): inline = resource_inline_factory(resources) model = ct.model_class() modeladmin = get_modeladmin(model) - inserted = False - inlines = [] - for existing in getattr(modeladmin, 'inlines', []): - if type(inline) == type(existing): - existing = inline - inserted = True - inlines.append(existing) - if inserted: - modeladmin.inlines = inlines - else: - insertattr(model, 'inlines', inline) + insertattr(model, 'inlines', inline) + modeladmin.inlines = type(modeladmin).inlines + +if database_ready(): + insert_resource_inlines() diff --git a/orchestra/apps/resources/apps.py b/orchestra/apps/resources/apps.py index c2408b54..d547c4a1 100644 --- a/orchestra/apps/resources/apps.py +++ b/orchestra/apps/resources/apps.py @@ -9,15 +9,13 @@ class ResourcesConfig(AppConfig): def ready(self): if database_ready(): - from .admin import insert_resource_inlines from .models import create_resource_relation create_resource_relation() - insert_resource_inlines() def reload_relations(self): from .admin import insert_resource_inlines from .models import create_resource_relation from .serializers import insert_resource_serializers - create_resource_relation() insert_resource_inlines() insert_resource_serializers() + create_resource_relation() diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py index 9cd01f40..9e09bf42 100644 --- a/orchestra/apps/resources/models.py +++ b/orchestra/apps/resources/models.py @@ -31,6 +31,7 @@ class Resource(models.Model): (MONTHLY_SUM, _("Monthly Sum")), (MONTHLY_AVG, _("Monthly Average")), ) + _related = set() # keeps track of related models for resource cleanup name = models.CharField(_("name"), max_length=32, help_text=_('Required. 32 characters or fewer. Lowercase letters, ' @@ -102,6 +103,7 @@ class Resource(models.Model): task.save(update_fields=['crontab']) if created: # This only work on tests because of multiprocessing used on real deployments + print 'saved' apps.get_app_config('resources').reload_relations() def delete(self, *args, **kwargs): @@ -192,8 +194,21 @@ def create_resource_relation(): self.obj = obj return self + # Clean previous state + for related in Resource._related: + try: + delattr(related, 'resource_set') + delattr(related, 'resources') + except AttributeError: + pass + else: + related._meta.virtual_fields = [ + field for field in related._meta.virtual_fields if field.rel.to != ResourceData + ] + relation = GenericRelation('resources.ResourceData') for ct, resources in Resource.objects.group_by('content_type').iteritems(): model = ct.model_class() model.add_to_class('resource_set', relation) model.resources = ResourceHandler() + Resource._related.add(model) diff --git a/orchestra/apps/resources/serializers.py b/orchestra/apps/resources/serializers.py index 1205fcb5..8ee14239 100644 --- a/orchestra/apps/resources/serializers.py +++ b/orchestra/apps/resources/serializers.py @@ -30,6 +30,15 @@ class ResourceSerializer(serializers.ModelSerializer): # Monkey-patching section def insert_resource_serializers(): + # clean previous state + for related in Resource._related: + viewset = router.get_viewset(related) + fields = list(viewset.serializer_class.Meta.fields) + try: + fields.remove('resources') + except ValueError: + pass + viewset.serializer_class.Meta.fields = fields # Create nested serializers on target models for ct, resources in Resource.objects.group_by('content_type').iteritems(): model = ct.model_class() diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py index 5cfbbff5..53c45fd8 100644 --- a/orchestra/apps/services/models.py +++ b/orchestra/apps/services/models.py @@ -6,11 +6,12 @@ from django.db.models.loading import get_model from django.contrib.contenttypes.models import ContentType from django.core.validators import ValidationError from django.utils.functional import cached_property +from django.utils.module_loading import autodiscover_modules from django.utils.translation import ugettext_lazy as _ from orchestra.core import caches, services, accounts from orchestra.models import queryset -from orchestra.utils.apps import autodiscover +#from orchestra.utils.apps import autodiscover from . import settings, rating from .handlers import ServiceHandler @@ -70,7 +71,7 @@ class Rate(models.Model): return "{}-{}".format(str(self.price), self.quantity) -autodiscover('handlers') +autodiscover_modules('handlers') class Service(models.Model): diff --git a/orchestra/apps/systemusers/backends.py b/orchestra/apps/systemusers/backends.py index 54708235..4589a942 100644 --- a/orchestra/apps/systemusers/backends.py +++ b/orchestra/apps/systemusers/backends.py @@ -37,7 +37,7 @@ class SystemUserBackend(ServiceController): self.append("groupdel %(username)s || true" % context) if user.is_main: # TODO delete instead of this shit - context['deleted'] = context['home'][:-1]+'.deleted' + context['deleted'] = context['home'].rstrip('/') + '.deleted' self.append("mv %(home)s %(deleted)s" % context) def get_groups(self, user): diff --git a/orchestra/apps/systemusers/models.py b/orchestra/apps/systemusers/models.py index 5c68e1d2..d07a1800 100644 --- a/orchestra/apps/systemusers/models.py +++ b/orchestra/apps/systemusers/models.py @@ -48,7 +48,6 @@ class SystemUser(models.Model): @cached_property def active(self): - a = type(self).account.field.model try: return self.is_active and self.account.is_active except type(self).account.field.rel.to.DoesNotExist: diff --git a/orchestra/apps/systemusers/serializers.py b/orchestra/apps/systemusers/serializers.py index 49af6929..3d2c923d 100644 --- a/orchestra/apps/systemusers/serializers.py +++ b/orchestra/apps/systemusers/serializers.py @@ -9,11 +9,20 @@ from orchestra.core.validators import validate_password from .models import SystemUser +class GroupSerializer(serializers.ModelSerializer): + class Meta: + model = SystemUser + fields = ('username',) + + def from_native(self, data, files=None): + return SystemUser.objects.get(username=data['username']) + + class SystemUserSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): password = serializers.CharField(max_length=128, label=_('Password'), validators=[validate_password], write_only=True, required=False, widget=widgets.PasswordInput) - groups = serializers.RelatedField(many=True) + groups = GroupSerializer(many=True, allow_add_remove=True, required=False) class Meta: model = SystemUser @@ -30,6 +39,8 @@ class SystemUserSerializer(AccountSerializerMixin, serializers.HyperlinkedModelS raise serializers.ValidationError(_("Password required")) return attrs + # TODO validate gruops != self + def save_object(self, obj, **kwargs): # FIXME this method will be called when saving nested serializers :( if not obj.pk: diff --git a/orchestra/apps/systemusers/tests/functional_tests/tests.py b/orchestra/apps/systemusers/tests/functional_tests/tests.py index f5016baf..1c54292d 100644 --- a/orchestra/apps/systemusers/tests/functional_tests/tests.py +++ b/orchestra/apps/systemusers/tests/functional_tests/tests.py @@ -13,7 +13,8 @@ from selenium.webdriver.support.select import Select from orchestra.apps.accounts.models import Account from orchestra.apps.orchestration.models import Server, Route from orchestra.utils.system import run, sshrun -from orchestra.utils.tests import BaseLiveServerTestCase, random_ascii, snapshot_on_error +from orchestra.utils.tests import (BaseLiveServerTestCase, random_ascii, snapshot_on_error, + save_response_on_error) from ... import backends, settings from ...models import SystemUser @@ -71,13 +72,14 @@ class SystemUserMixin(object): def validate_delete(self, username): self.assertRaises(SystemUser.DoesNotExist, SystemUser.objects.get, username=username) self.assertRaises(CommandError, - sshrun, self.MASTER_SERVER,'id %s' % username, display=False) + sshrun, self.MASTER_SERVER, 'id %s' % username, display=False) self.assertRaises(CommandError, sshrun, self.MASTER_SERVER, 'grep "^%s:" /etc/groups' % username, display=False) self.assertRaises(CommandError, sshrun, self.MASTER_SERVER, 'grep "^%s:" /etc/passwd' % username, display=False) self.assertRaises(CommandError, sshrun, self.MASTER_SERVER, 'grep "^%s:" /etc/shadow' % username, display=False) + # Home will be deleted on account delete, see test_delete_account def validate_ftp(self, username, password): connection = ftplib.FTP(self.MASTER_SERVER) @@ -105,22 +107,24 @@ class SystemUserMixin(object): username = '%s_systemuser' % random_ascii(10) password = '@!?%spppP001' % random_ascii(5) self.add(username, password) - self.addCleanup(partial(self.delete, username)) + self.addCleanup(self.delete, username) self.validate_user(username) def test_ftp(self): username = '%s_systemuser' % random_ascii(10) password = '@!?%spppP001' % random_ascii(5) self.add(username, password, shell='/dev/null') - self.addCleanup(partial(self.delete, username)) - self.assertRaises(paramiko.AuthenticationException, self.validate_sftp, username, password) - self.assertRaises(paramiko.AuthenticationException, self.validate_ssh, username, password) + self.addCleanup(self.delete, username) + self.assertRaises(paramiko.AuthenticationException, + self.validate_sftp, username, password) + self.assertRaises(paramiko.AuthenticationException, + self.validate_ssh, username, password) def test_sftp(self): username = '%s_systemuser' % random_ascii(10) password = '@!?%spppP001' % random_ascii(5) self.add(username, password, shell='/bin/rssh') - self.addCleanup(partial(self.delete, username)) + self.addCleanup(self.delete, username) self.validate_sftp(username, password) self.assertRaises(AssertionError, self.validate_ssh, username, password) @@ -128,7 +132,7 @@ class SystemUserMixin(object): username = '%s_systemuser' % random_ascii(10) password = '@!?%spppP001' % random_ascii(5) self.add(username, password, shell='/bin/bash') - self.addCleanup(partial(self.delete, username)) + self.addCleanup(self.delete, username) self.validate_ssh(username, password) def test_delete(self): @@ -143,12 +147,12 @@ class SystemUserMixin(object): username = '%s_systemuser' % random_ascii(10) password = '@!?%spppP001' % random_ascii(5) self.add(username, password) - self.addCleanup(partial(self.delete, username)) + self.addCleanup(self.delete, username) self.validate_user(username) username2 = '%s_systemuser' % random_ascii(10) password2 = '@!?%spppP001' % random_ascii(5) self.add(username2, password2) - self.addCleanup(partial(self.delete, username2)) + self.addCleanup(self.delete, username2) self.validate_user(username2) self.add_group(username, username2) user = SystemUser.objects.get(username=username) @@ -160,7 +164,7 @@ class SystemUserMixin(object): username = '%s_systemuser' % random_ascii(10) password = '@!?%spppP001' % random_ascii(5) self.add(username, password, shell='/dev/null') - self.addCleanup(partial(self.delete, username)) + self.addCleanup(self.delete, username) self.validate_ftp(username, password) self.disable(username) self.validate_user(username) @@ -170,7 +174,7 @@ class SystemUserMixin(object): username = '%s_systemuser' % random_ascii(10) password = '@!?%spppP001' % random_ascii(5) self.add(username, password) - self.addCleanup(partial(self.delete, username)) + self.addCleanup(self.delete, username) self.validate_ftp(username, password) new_password = '@!?%spppP001' % random_ascii(5) self.change_password(username, new_password) @@ -185,33 +189,38 @@ class RESTSystemUserMixin(SystemUserMixin): self.rest_login() # create main user self.save(self.account.username) - self.addCleanup(partial(self.delete, self.account.username)) + self.addCleanup(self.delete, self.account.username) + @save_response_on_error def add(self, username, password, shell='/dev/null'): self.rest.systemusers.create(username=username, password=password, shell=shell) + @save_response_on_error def delete(self, username): user = self.rest.systemusers.retrieve(username=username).get() user.delete() + @save_response_on_error def add_group(self, username, groupname): user = self.rest.systemusers.retrieve(username=username).get() - group = self.rest.systemusers.retrieve(username=groupname).get() - user.groups.append(group) # TODO + user.groups.append({'username': groupname}) user.save() + @save_response_on_error def disable(self, username): user = self.rest.systemusers.retrieve(username=username).get() user.is_active = False user.save() + @save_response_on_error def save(self, username): user = self.rest.systemusers.retrieve(username=username).get() user.save() + @save_response_on_error def change_password(self, username, password): user = self.rest.systemusers.retrieve(username=username).get() - user.change_password(password) + user.set_password(password) class AdminSystemUserMixin(SystemUserMixin): @@ -220,7 +229,7 @@ class AdminSystemUserMixin(SystemUserMixin): self.admin_login() # create main user self.save(self.account.username) - self.addCleanup(partial(self.delete, self.account.username)) + self.addCleanup(self.delete, self.account.username) @snapshot_on_error def add(self, username, password, shell='/dev/null'): @@ -249,24 +258,12 @@ class AdminSystemUserMixin(SystemUserMixin): @snapshot_on_error def delete(self, username): user = SystemUser.objects.get(username=username) - delete = reverse('admin:systemusers_systemuser_delete', args=(user.pk,)) - url = self.live_server_url + delete - self.selenium.get(url) - confirmation = self.selenium.find_element_by_name('post') - confirmation.submit() - self.assertNotEqual(url, self.selenium.current_url) + self.admin_delete(user) @snapshot_on_error def disable(self, username): user = SystemUser.objects.get(username=username) - change = reverse('admin:systemusers_systemuser_change', args=(user.pk,)) - url = self.live_server_url + change - self.selenium.get(url) - is_active = self.selenium.find_element_by_id('id_is_active') - is_active.click() - save = self.selenium.find_element_by_name('_save') - save.submit() - self.assertNotEqual(url, self.selenium.current_url) + self.admin_disable(user) @snapshot_on_error def add_group(self, username, groupname): @@ -294,17 +291,8 @@ class AdminSystemUserMixin(SystemUserMixin): @snapshot_on_error def change_password(self, username, password): user = SystemUser.objects.get(username=username) - change_password = reverse('admin:systemusers_systemuser_change_password', args=(user.pk,)) - url = self.live_server_url + change_password - self.selenium.get(url) - - password_field = self.selenium.find_element_by_id('id_password1') - password_field.send_keys(password) - password_field = self.selenium.find_element_by_id('id_password2') - password_field.send_keys(password) - password_field.submit() - - self.assertNotEqual(url, self.selenium.current_url) + self.admin_change_password(user, password) + class RESTSystemUserTest(RESTSystemUserMixin, BaseLiveServerTestCase): pass @@ -337,10 +325,10 @@ class AdminSystemUserTest(AdminSystemUserMixin, BaseLiveServerTestCase): email = self.selenium.find_element_by_id('id_contacts-0-email') email.send_keys(account_email) email.submit() + self.assertNotEqual(url, self.selenium.current_url) account = Account.objects.get(username=account_username) - self.addCleanup(account.delete) - self.assertNotEqual(url, self.selenium.current_url) + self.addCleanup(self.delete, account_username) self.assertEqual(0, sshr(self.MASTER_SERVER, "id %s" % account.username).return_code) @snapshot_on_error diff --git a/orchestra/bin/orchestra-admin b/orchestra/bin/orchestra-admin index ca0a250f..c6dd6a20 100755 --- a/orchestra/bin/orchestra-admin +++ b/orchestra/bin/orchestra-admin @@ -130,7 +130,8 @@ function install_requirements () { libxml2-dev \ libxslt1-dev \ wkhtmltopdf \ - xvfb" + xvfb \ + python-mysqldb" PIP="django==1.7 \ django-celery-email==1.0.4 \ diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index b47bcc4b..ecbecc28 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -107,7 +107,7 @@ INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'django.contrib.admin', + 'django.contrib.admin.apps.SimpleAdminConfig', 'orchestra.apps.accounts', 'orchestra.apps.contacts', diff --git a/orchestra/urls.py b/orchestra/urls.py index b8848188..62360a44 100644 --- a/orchestra/urls.py +++ b/orchestra/urls.py @@ -25,9 +25,9 @@ urlpatterns = patterns('', url(r'^media/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT, 'show_indexes': True} ) - ) + if settings.DEBUG: import debug_toolbar urlpatterns += patterns('', diff --git a/orchestra/utils/apps.py b/orchestra/utils/apps.py index 69ebfa29..c6ecaa69 100644 --- a/orchestra/utils/apps.py +++ b/orchestra/utils/apps.py @@ -1,20 +1,21 @@ -from django.utils.importlib import import_module -from django.utils.module_loading import module_has_submodule +#from django.utils.importlib import import_module +#from django.utils.module_loading import module_has_submodule -def autodiscover(module): - """ Auto-discover INSTALLED_APPS module.py """ - from django.conf import settings - for app in settings.INSTALLED_APPS: - mod = import_module(app) - try: - import_module('%s.%s' % (app, module)) - except ImportError: - # Decide whether to bubble up this error. If the app just - # doesn't have the module, we can ignore the error - # attempting to import it, otherwise we want it to bubble up. - if module_has_submodule(mod, module): - print '%s module caused this error:' % module - raise + +#def autodiscover(module): +# """ Auto-discover INSTALLED_APPS module.py """ +# from django.conf import settings +# for app in settings.INSTALLED_APPS: +# mod = import_module(app) +# try: +# import_module('%s.%s' % (app, module)) +# except ImportError: +# # Decide whether to bubble up this error. If the app just +# # doesn't have the module, we can ignore the error +# # attempting to import it, otherwise we want it to bubble up. +# if module_has_submodule(mod, module): +# print '%s module caused this error:' % module +# raise def isinstalled(app): """ returns True if app is installed """ diff --git a/orchestra/utils/tests.py b/orchestra/utils/tests.py index 51de04ba..bfc646d6 100644 --- a/orchestra/utils/tests.py +++ b/orchestra/utils/tests.py @@ -7,6 +7,7 @@ from functools import wraps from django.conf import settings from django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model from django.contrib.sessions.backends.db import SessionStore +from django.core.urlresolvers import reverse from django.test import LiveServerTestCase, TestCase from orm.api import Api from selenium.webdriver.firefox.webdriver import WebDriver @@ -115,7 +116,43 @@ class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase): filename = 'screenshot_%s_%s.png' % (self.id(), timestamp) path = '/home/orchestra/snapshots' self.selenium.save_screenshot(os.path.join(path, filename)) - + + def admin_delete(self, obj): + opts = obj._meta + app_label, model_name = opts.app_label, opts.model_name + delete = reverse('admin:%s_%s_delete' % (app_label, model_name), args=(obj.pk,)) + url = self.live_server_url + delete + self.selenium.get(url) + confirmation = self.selenium.find_element_by_name('post') + confirmation.submit() + self.assertNotEqual(url, self.selenium.current_url) + + def admin_disable(self, obj): + opts = obj._meta + app_label, model_name = opts.app_label, opts.model_name + change = reverse('admin:%s_%s_change' % (app_label, model_name), args=(obj.pk,)) + url = self.live_server_url + change + self.selenium.get(url) + is_active = self.selenium.find_element_by_id('id_is_active') + is_active.click() + save = self.selenium.find_element_by_name('_save') + save.submit() + self.assertNotEqual(url, self.selenium.current_url) + + def admin_change_password(self, obj, password): + opts = obj._meta + app_label, model_name = opts.app_label, opts.model_name + change_password = reverse('admin:%s_%s_change_password' % (app_label, model_name), args=(obj.pk,)) + url = self.live_server_url + change_password + self.selenium.get(url) + + password_field = self.selenium.find_element_by_id('id_password1') + password_field.send_keys(password) + password_field = self.selenium.find_element_by_id('id_password2') + password_field.send_keys(password) + password_field.submit() + + self.assertNotEqual(url, self.selenium.current_url) def snapshot_on_error(test): @wraps(test) diff --git a/scripts/services/mysql.md b/scripts/services/mysql.md index efc74d0a..f99b5d43 100644 --- a/scripts/services/mysql.md +++ b/scripts/services/mysql.md @@ -2,3 +2,5 @@ MySQL ===== apt-get install mysql-server + +sed -i "s/bind-address = 127.0.0.1/bind-address = 0.0.0.0/" /etc/mysql/my.cnf