diff --git a/orchestra/apps/accounts/forms.py b/orchestra/apps/accounts/forms.py
index ccd8eb16..0088a867 100644
--- a/orchestra/apps/accounts/forms.py
+++ b/orchestra/apps/accounts/forms.py
@@ -5,14 +5,8 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.core.validators import validate_password
from orchestra.forms.widgets import ReadOnlyWidget
-from .models import Account
-
class AccountCreationForm(auth.forms.UserCreationForm):
-# class Meta:
-# model = Account
-# fields = ("username",)
-
def __init__(self, *args, **kwargs):
super(AccountCreationForm, self).__init__(*args, **kwargs)
self.fields['password1'].validators.append(validate_password)
@@ -21,8 +15,9 @@ class AccountCreationForm(auth.forms.UserCreationForm):
# Since model.clean() will check this, this is redundant,
# but it sets a nicer error message than the ORM and avoids conflicts with contrib.auth
username = self.cleaned_data["username"]
- if hasattr(Account, 'systemusers'):
- systemuser_model = Account.systemusers.related.model
+ account_model = self._meta.model
+ if hasattr(account_model, 'systemusers'):
+ systemuser_model = account_model.systemusers.related.model
if systemuser_model.objects.filter(username=username).exists():
raise forms.ValidationError(self.error_messages['duplicate_username'])
return username
diff --git a/orchestra/apps/bills/models.py b/orchestra/apps/bills/models.py
index 0044d82b..86ecf6d1 100644
--- a/orchestra/apps/bills/models.py
+++ b/orchestra/apps/bills/models.py
@@ -55,8 +55,8 @@ class Bill(models.Model):
type = models.CharField(_("type"), max_length=16, choices=TYPES)
created_on = models.DateField(_("created on"), auto_now_add=True)
closed_on = models.DateField(_("closed on"), blank=True, null=True)
- is_open = models.BooleanField(_("is open"), default=True)
- is_sent = models.BooleanField(_("is sent"), default=False)
+ is_open = models.BooleanField(_("open"), default=True)
+ is_sent = models.BooleanField(_("sent"), default=False)
due_on = models.DateField(_("due on"), null=True, blank=True)
updated_on = models.DateField(_("updated on"), auto_now=True)
total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
diff --git a/orchestra/apps/databases/models.py b/orchestra/apps/databases/models.py
index 13f9c9bc..079ef0c2 100644
--- a/orchestra/apps/databases/models.py
+++ b/orchestra/apps/databases/models.py
@@ -40,7 +40,7 @@ class Role(models.Model):
related_name='roles')
user = models.ForeignKey('databases.DatabaseUser', verbose_name=_("user"),
related_name='roles')
- is_owner = models.BooleanField(_("is owener"), default=False)
+ is_owner = models.BooleanField(_("owner"), default=False)
class Meta:
unique_together = ('database', 'user')
diff --git a/orchestra/apps/mails/models.py b/orchestra/apps/mails/models.py
index 58aa2561..c761e6b0 100644
--- a/orchestra/apps/mails/models.py
+++ b/orchestra/apps/mails/models.py
@@ -20,7 +20,7 @@ class Mailbox(models.Model):
validators=[validators.validate_sieve],
help_text=_("Arbitrary email filtering in sieve language. "
"This overrides any automatic junk email filtering"))
- is_active = models.BooleanField(_("is active"), default=True)
+ is_active = models.BooleanField(_("active"), default=True)
# addresses = models.ManyToManyField('mails.Address',
# verbose_name=_("addresses"),
# related_name='mailboxes', blank=True)
diff --git a/orchestra/apps/miscellaneous/models.py b/orchestra/apps/miscellaneous/models.py
index 2d76ab78..6de14273 100644
--- a/orchestra/apps/miscellaneous/models.py
+++ b/orchestra/apps/miscellaneous/models.py
@@ -6,11 +6,11 @@ from orchestra.core import services
class MiscService(models.Model):
name = models.CharField(_("name"), max_length=256)
- description = models.TextField(blank=True)
- has_amount = models.BooleanField(default=False,
+ description = models.TextField(_("description"), blank=True)
+ has_amount = models.BooleanField(_("has amount"), default=False,
help_text=_("Designates whether this service has amount "
"property or not."))
- is_active = models.BooleanField(default=True,
+ is_active = models.BooleanField(_("active"), default=True,
help_text=_("Whether new instances of this service can be created "
"or not. Unselect this instead of deleting services."))
@@ -25,7 +25,7 @@ class Miscellaneous(models.Model):
related_name='miscellaneous')
description = models.TextField(_("description"), blank=True)
amount = models.PositiveIntegerField(_("amount"), default=1)
- is_active = models.BooleanField(default=True,
+ is_active = models.BooleanField(_("active"), default=True,
help_text=_("Designates whether this service should be treated as "
"active. Unselect this instead of deleting services."))
diff --git a/orchestra/apps/orchestration/models.py b/orchestra/apps/orchestration/models.py
index 91bf1f93..3bd6aeb4 100644
--- a/orchestra/apps/orchestration/models.py
+++ b/orchestra/apps/orchestration/models.py
@@ -136,7 +136,7 @@ class Route(models.Model):
# async = models.BooleanField(default=False)
# method = models.CharField(_("method"), max_lenght=32, choices=method_choices,
# default=MethodBackend.get_default())
- is_active = models.BooleanField(_("is active"), default=True)
+ is_active = models.BooleanField(_("active"), default=True)
class Meta:
unique_together = ('backend', 'host')
diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py
index 5eb55601..76046b84 100644
--- a/orchestra/apps/payments/models.py
+++ b/orchestra/apps/payments/models.py
@@ -22,7 +22,7 @@ class PaymentSource(models.Model):
method = models.CharField(_("method"), max_length=32,
choices=PaymentMethod.get_plugin_choices())
data = JSONField(_("data"))
- is_active = models.BooleanField(_("is active"), default=True)
+ is_active = models.BooleanField(_("active"), default=True)
objects = PaymentSourcesQueryset.as_manager()
diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py
index af2de256..9cffc3ae 100644
--- a/orchestra/apps/resources/models.py
+++ b/orchestra/apps/resources/models.py
@@ -65,7 +65,7 @@ class Resource(models.Model):
monitors = fields.MultiSelectField(_("monitors"), max_length=256, blank=True,
choices=ServiceMonitor.get_plugin_choices(),
help_text=_("Monitor backends used for monitoring this resource."))
- is_active = models.BooleanField(_("is active"), default=True)
+ is_active = models.BooleanField(_("active"), default=True)
objects = ResourceQuerySet.as_manager()
diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py
index a53bcc78..5cfbbff5 100644
--- a/orchestra/apps/services/models.py
+++ b/orchestra/apps/services/models.py
@@ -18,9 +18,9 @@ from .handlers import ServiceHandler
class Plan(models.Model):
name = models.CharField(_("plan"), max_length=128)
- is_default = models.BooleanField(_("is default"), default=False)
- is_combinable = models.BooleanField(_("is combinable"), default=True)
- allow_multiple = models.BooleanField(_("allow multipls"), default=False)
+ is_default = models.BooleanField(_("default"), default=False)
+ is_combinable = models.BooleanField(_("combinable"), default=True)
+ allow_multiple = models.BooleanField(_("allow multiple"), default=False)
def __unicode__(self):
return self.name
@@ -107,7 +107,7 @@ class Service(models.Model):
"enables customized behaviour far beyond what options "
"here allow to."),
choices=ServiceHandler.get_plugin_choices())
- is_active = models.BooleanField(_("is active"), default=True)
+ is_active = models.BooleanField(_("active"), default=True)
# Billing
billing_period = models.CharField(_("billing period"), max_length=16,
help_text=_("Renewal period for recurring invoicing"),
@@ -133,7 +133,7 @@ class Service(models.Model):
# (ONE_MONTH, _("One month")),
# ),
# default=ONE_MONTH, blank=True)
- is_fee = models.BooleanField(_("is fee"), default=False,
+ is_fee = models.BooleanField(_("fee"), default=False,
help_text=_("Designates whether this service should be billed as "
" membership fee or not"))
# Pricing
diff --git a/orchestra/apps/systemusers/admin.py b/orchestra/apps/systemusers/admin.py
index 9bfd1221..93b3d54b 100644
--- a/orchestra/apps/systemusers/admin.py
+++ b/orchestra/apps/systemusers/admin.py
@@ -17,7 +17,7 @@ class SystemUserAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_filter = ('is_active',)
fieldsets = (
(None, {
- 'fields': ('username', 'password', 'is_active', 'account_link')
+ 'fields': ('username', 'password', 'account_link', 'is_active')
}),
(_("System"), {
'fields': ('home', 'shell', 'groups'),
diff --git a/orchestra/apps/systemusers/forms.py b/orchestra/apps/systemusers/forms.py
index 7eb468c8..a18bacf2 100644
--- a/orchestra/apps/systemusers/forms.py
+++ b/orchestra/apps/systemusers/forms.py
@@ -5,14 +5,9 @@ from django.utils.translation import ugettext, ugettext_lazy as _
from orchestra.apps.accounts.models import Account
from orchestra.core.validators import validate_password
-from .models import SystemUser
-
+# TODO orchestra.UserCretionForm
class UserCreationForm(auth.forms.UserCreationForm):
-# class Meta:
-# model = SystemUser
-# fields = ('username',)
-
def __init__(self, *args, **kwargs):
super(UserCreationForm, self).__init__(*args, **kwargs)
self.fields['password1'].validators.append(validate_password)
@@ -21,12 +16,13 @@ class UserCreationForm(auth.forms.UserCreationForm):
# Since model.clean() will check this, this is redundant,
# but it sets a nicer error message than the ORM and avoids conflicts with contrib.auth
username = self.cleaned_data["username"]
- account_model = SystemUser.account.field.rel.to
+ account_model = self._meta.model.account.field.rel.to
if account_model.objects.filter(username=username).exists():
raise forms.ValidationError(self.error_messages['duplicate_username'])
return username
+# TODO orchestra.UserCretionForm
class UserChangeForm(forms.ModelForm):
password = auth.forms.ReadOnlyPasswordHashField(label=_("Password"),
help_text=_("Raw passwords are not stored, so there is no way to see "
diff --git a/orchestra/apps/systemusers/models.py b/orchestra/apps/systemusers/models.py
index 6011e55d..e37745f8 100644
--- a/orchestra/apps/systemusers/models.py
+++ b/orchestra/apps/systemusers/models.py
@@ -26,7 +26,7 @@ class SystemUser(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='systemusers')
home = models.CharField(_("home"), max_length=256, blank=True,
- help_text=_("Home directory relative to account's ~primary_user"))
+ help_text=_("Home directory relative to account's ~main_user"))
shell = models.CharField(_("shell"), max_length=32,
choices=settings.USERS_SHELLS, default=settings.USERS_DEFAULT_SHELL)
groups = models.ManyToManyField('systemusers.Group', blank=True,
diff --git a/orchestra/apps/systemusers/settings.py b/orchestra/apps/systemusers/settings.py
index e9d8a7a7..bfe29b1d 100644
--- a/orchestra/apps/systemusers/settings.py
+++ b/orchestra/apps/systemusers/settings.py
@@ -8,9 +8,10 @@ USERS_SYSTEMUSER_HOME = getattr(settings, 'USERES_SYSTEMUSER_HOME', '/home/%(use
USERS_FTP_LOG_PATH = getattr(settings, 'USERS_FTP_LOG_PATH', '/var/log/vsftpd.log')
USERS_SHELLS = getattr(settings, 'USERS_SHELLS', (
- ('/bin/false', _("FTP/sFTP only")),
- ('/bin/rsync', _("rsync shell")),
- ('/bin/bash', "Bash"),
+ ('/bin/false', _("No shell, FTP only")),
+ ('/bin/rsync', _("No shell, SFTP/RSYNC only")),
+ ('/bin/bash', "/bin/bash"),
+ ('/bin/sh', "/bin/sh"),
))
USERS_DEFAULT_SHELL = getattr(settings, 'USERS_DEFAULT_SHELL', '/bin/false')
diff --git a/orchestra/apps/users/admin.py b/orchestra/apps/users/admin.py
index 02405915..6451d0af 100644
--- a/orchestra/apps/users/admin.py
+++ b/orchestra/apps/users/admin.py
@@ -11,23 +11,21 @@ from orchestra.apps.accounts.admin import AccountAdminMixin
from .forms import UserCreationForm, UserChangeForm
from .models import User
+from .roles.filters import role_list_filter_factory
class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
- list_display = ('username', 'account_link', 'is_main', 'is_superuser', 'is_active')
- list_filter = ('is_main', 'is_superuser', 'is_active')
+ list_display = ('username', 'display_is_main')
+ list_filter = ('is_staff', 'is_superuser', 'is_active')
fieldsets = (
(None, {
- 'fields': ('username', 'password', 'account_link')
- }),
- (_("System"), {
- 'fields': ('home', 'shell', 'groups'),
+ 'fields': ('account', 'username', 'password')
}),
(_("Personal info"), {
'fields': ('first_name', 'last_name', 'email')
}),
(_("Permissions"), {
- 'fields': ('is_main', 'is_active', 'is_superuser')
+ 'fields': ('is_active', 'is_staff', 'is_superuser', 'display_is_main')
}),
(_("Important dates"), {
'fields': ('last_login', 'date_joined')
@@ -39,25 +37,74 @@ class UserAdmin(AccountAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
'fields': ('username', 'password1', 'password2', 'account'),
}),
)
- search_fields = ['username']
- readonly_fields = ('is_main', 'account_link',)
+ search_fields = ['username', 'account__user__username']
+ readonly_fields = ('display_is_main', 'account_link')
change_readonly_fields = ('username',)
filter_horizontal = ()
- filter_by_account_fields = ('groups',)
add_form = UserCreationForm
form = UserChangeForm
+ roles = []
ordering = ('-id',)
+ change_form_template = 'admin/users/user/change_form.html'
- def get_form(self, request, obj=None, **kwargs):
- """ exclude self reference on groups """
- form = super(AccountAdminMixin, self).get_form(request, obj=obj, **kwargs)
- if obj:
- # Has to be done here and not in the form because of strange phenomenon
- # derived from monkeypatching formfield.widget.render on AccountAdminMinxin,
- # don't ask.
- formfield = form.base_fields['groups']
- formfield.queryset = formfield.queryset.exclude(id=obj.id)
- return form
+ def display_is_main(self, instance):
+ return instance.is_main
+ display_is_main.short_description = _("is main")
+ display_is_main.boolean = True
+
+ def get_urls(self):
+ """ Returns the additional urls for the change view links """
+ urls = super(UserAdmin, self).get_urls()
+ opts = self.model._meta
+ new_urls = patterns("")
+ for role in self.roles:
+ new_urls += patterns("",
+ url('^(\d+)/%s/$' % role.url_name,
+ wrap_admin_view(self, role().change_view),
+ name='%s_%s_%s_change' % (opts.app_label, opts.model_name, role.name)),
+ url('^(\d+)/%s/delete/$' % role.url_name,
+ wrap_admin_view(self, role().delete_view),
+ name='%s_%s_%s_delete' % (opts.app_label, opts.model_name, role.name))
+ )
+ return new_urls + urls
+
+ def get_fieldsets(self, request, obj=None):
+ fieldsets = super(UserAdmin, self).get_fieldsets(request, obj=obj)
+ if obj and obj.account:
+ fieldsets[0][1]['fields'] = ('account_link',) + fieldsets[0][1]['fields'][1:]
+ return fieldsets
+
+ def get_list_display(self, request):
+ roles = []
+ for role in self.roles:
+ def has_role(user, role_class=role):
+ role = role_class(user=user)
+ if role.exists:
+ return ''
+ url = reverse('admin:users_user_%s_change' % role.name, args=(user.pk,))
+ false = ''
+ return '%s' % (url, false)
+ has_role.short_description = _("Has %s") % role.name
+ has_role.admin_order_field = role.name
+ has_role.allow_tags = True
+ roles.append(has_role)
+ return list(self.list_display) + roles + ['account_link']
+
+ def get_list_filter(self, request):
+ roles = [ role_list_filter_factory(role) for role in self.roles ]
+ return list(self.list_filter) + roles
+
+ def change_view(self, request, object_id, **kwargs):
+ user = self.get_object(User, unquote(object_id))
+ extra_context = kwargs.get('extra_context', {})
+ extra_context['roles'] = [ role(user=user) for role in self.roles ]
+ kwargs['extra_context'] = extra_context
+ return super(UserAdmin, self).change_view(request, object_id, **kwargs)
+
+ def get_queryset(self, request):
+ """ Select related for performance """
+ related = ['account__user'] + [ role.name for role in self.roles ]
+ return super(UserAdmin, self).get_queryset(request).select_related(*related)
admin.site.register(User, UserAdmin)
diff --git a/orchestra/apps/users/api.py b/orchestra/apps/users/api.py
index c62c7e18..dda373e5 100644
--- a/orchestra/apps/users/api.py
+++ b/orchestra/apps/users/api.py
@@ -10,6 +10,11 @@ from .serializers import UserSerializer
class UserViewSet(AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
model = get_user_model()
serializer_class = UserSerializer
+
+ def get_queryset(self):
+ """ select related roles """
+ qs = super(UserViewSet, self).get_queryset()
+ return qs.select_related(*self.inserted)
router.register(r'users', UserViewSet)
diff --git a/orchestra/apps/users/forms.py b/orchestra/apps/users/forms.py
index a4d3c9c2..42c52ac5 100644
--- a/orchestra/apps/users/forms.py
+++ b/orchestra/apps/users/forms.py
@@ -47,4 +47,3 @@ class UserChangeForm(forms.ModelForm):
# This is done here, rather than on the field, because the
# field does not have access to the initial value
return self.initial["password"]
-
diff --git a/orchestra/apps/users/models.py b/orchestra/apps/users/models.py
index 25f87373..b91b72aa 100644
--- a/orchestra/apps/users/models.py
+++ b/orchestra/apps/users/models.py
@@ -7,8 +7,6 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.core import services
-from . import settings
-
class User(auth.AbstractBaseUser):
username = models.CharField(_("username"), max_length=64, unique=True,
@@ -16,22 +14,19 @@ class User(auth.AbstractBaseUser):
"./-/_ only."),
validators=[validators.RegexValidator(r'^[\w.-]+$',
_("Enter a valid username."), 'invalid')])
- account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), related_name='users')
+ account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
+ related_name='users')
first_name = models.CharField(_("first name"), max_length=30, blank=True)
last_name = models.CharField(_("last name"), max_length=30, blank=True)
- email = models.EmailField(_("email address"), blank=True)
+ email = models.EmailField(_('email address'), blank=True)
is_superuser = models.BooleanField(_("superuser status"), default=False,
- help_text=_("Designates that this user has all permissions without "
- "explicitly assigning them."))
- is_main = models.BooleanField(_("is main"), default=False)
-# system_password = models.CharField(_("system password"), max_length=128)
- home = models.CharField(_("home"), max_length=256, blank=True,
- help_text=_("Home directory relative to account's ~primary_user"))
- shell = models.CharField(_("shell"), max_length=32,
- choices=settings.USERS_SHELLS, default=settings.USERS_DEFAULT_SHELL)
- groups = models.ManyToManyField('self', blank=True,
- help_text=_("A new group will be created for the user. "
- "Which additional groups would you like them to be a member of?"))
+ help_text=_("Designates that this user has all permissions without "
+ "explicitly assigning them."))
+ is_staff = models.BooleanField(_("staff status"), default=False,
+ help_text=_("Designates whether the user can log into this admin "
+ "site."))
+ is_admin = models.BooleanField(_("admin status"), default=False,
+ help_text=_("Designates whether the user can administrate its account."))
is_active = models.BooleanField(_("active"), default=True,
help_text=_("Designates whether this user should be treated as "
"active. Unselect this instead of deleting accounts."))
@@ -40,11 +35,11 @@ class User(auth.AbstractBaseUser):
objects = auth.UserManager()
USERNAME_FIELD = 'username'
- REQUIRED_FIELDS = []
+ REQUIRED_FIELDS = ['email']
@property
- def is_staff(self):
- return self.is_superuser or self.is_main
+ def is_main(self):
+ return self.account.user == self
def get_full_name(self):
full_name = '%s %s' % (self.first_name, self.last_name)
@@ -92,9 +87,6 @@ class User(auth.AbstractBaseUser):
if self.is_active and self.is_superuser:
return True
return auth._user_has_module_perms(self, app_label)
-#
-# def set_system_password(self, raw_password):
-# self.system_password = make_password(raw_password)
-services.register(User)
+services.register(User, menu=False)
diff --git a/orchestra/apps/users/serializers.py b/orchestra/apps/users/serializers.py
index 6323097b..321e8b02 100644
--- a/orchestra/apps/users/serializers.py
+++ b/orchestra/apps/users/serializers.py
@@ -33,4 +33,3 @@ class UserSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeriali
if not obj.pk:
obj.set_password(obj.password)
super(UserSerializer, self).save_object(obj, **kwargs)
-
diff --git a/orchestra/apps/users/settings.py b/orchestra/apps/users/settings.py
index e9d8a7a7..012099a4 100644
--- a/orchestra/apps/users/settings.py
+++ b/orchestra/apps/users/settings.py
@@ -1,16 +1,6 @@
from django.conf import settings
-from django.utils.translation import ugettext, ugettext_lazy as _
-
USERS_SYSTEMUSER_HOME = getattr(settings, 'USERES_SYSTEMUSER_HOME', '/home/%(username)s')
USERS_FTP_LOG_PATH = getattr(settings, 'USERS_FTP_LOG_PATH', '/var/log/vsftpd.log')
-
-USERS_SHELLS = getattr(settings, 'USERS_SHELLS', (
- ('/bin/false', _("FTP/sFTP only")),
- ('/bin/rsync', _("rsync shell")),
- ('/bin/bash', "Bash"),
-))
-
-USERS_DEFAULT_SHELL = getattr(settings, 'USERS_DEFAULT_SHELL', '/bin/false')
diff --git a/orchestra/apps/websites/models.py b/orchestra/apps/websites/models.py
index c64fcc6d..69f70292 100644
--- a/orchestra/apps/websites/models.py
+++ b/orchestra/apps/websites/models.py
@@ -28,7 +28,7 @@ class Website(models.Model):
domains = models.ManyToManyField(settings.WEBSITES_DOMAIN_MODEL,
related_name='websites', verbose_name=_("domains"))
contents = models.ManyToManyField('webapps.WebApp', through='websites.Content')
- is_active = models.BooleanField(_("is active"), default=True)
+ is_active = models.BooleanField(_("active"), default=True)
def __unicode__(self):
return self.name