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 'True' + url = reverse('admin:users_user_%s_change' % role.name, args=(user.pk,)) + false = '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