Added seafile incon andn fixed random bugs

This commit is contained in:
Marc Aymerich 2015-03-26 16:00:30 +00:00
parent c55cff9a37
commit a6734ea1d1
24 changed files with 176 additions and 85 deletions

61
TODO.md
View File

@ -19,8 +19,6 @@
* backend logs with hal logo
* set_password orchestration method?
* LAST version of this shit http://wkhtmltopdf.org/downloads.html
@ -39,8 +37,6 @@
* mail backend related_models = ('resources__content_type') ??
* Domain backend PowerDNS Bind validation support?
* Maildir billing tests/ webdisk billing tests (avg metric)
@ -50,10 +46,6 @@
* rename accounts register to "account", and reated api and admin references
* Disable services is_active should be computed on the fly in order to distinguish account.is_active from service.is_active when reactivation.
* Perhaps it is time to create a ServiceModel ?
* prevent deletion of main user by the user itself
* AccountAdminMixin auto adds 'account__name' on searchfields
@ -119,8 +111,6 @@
* Make main systemuser able to write/read everything on its home, including stuff created by the CGI user and secondary users
* Prevent users from accessing other users home while at the same time allow access Apache/fcgid/fpm and secondary users (x)
* public_html/webapps directory with root owner and permissions
* resource min max allocation with validation
* mailman needs both aliases when address_name is provided (default messages and bounces and all)
@ -144,28 +134,11 @@
* Create an admin service_view with icons (like SaaS app)
* Resource graph for each related object
* multitenant webapps modeled on WepApp -> name unique for all accounts
* webapp compat webapp-options
* webapps modeled on classes instead of settings?
* Service.account change and orders consistency
* Mix webapps type with backends (two for the price of one)
* Webapp options and type compatibility
* SaaS model splitted into SaaSUser and SaaSSite?
Multi-tenant WebApps
--------------------
* SaaS - Those apps that can't use custom domain
* WebApp - Those apps that can use custom domain
* SaaS model splitted into SaaSUser and SaaSSite? inherit from SaaS
* prevent @pangea.org email addresses on contacts, enforce at least one email without @pangea.org
@ -192,19 +165,17 @@ Php binaries should have this format: /usr/bin/php5.2-cgi
* <IfModule security2_module> and other IfModule on backend SecRule
* Orchestra global search box on the header, based https://github.com/django/django/blob/master/django/contrib/admin/options.py#L866 and iterating over all registered services and inspectin its admin.search_fields
* Orchestra global search box on the page head, based https://github.com/django/django/blob/master/django/contrib/admin/options.py#L866 and iterating over all registered services and inspectin its admin.search_fields
* contain error on plugin missing key (plugin dissabled): NOP, fail hard is better than silently, perhaps fail at starttime? apploading machinary
* contact.alternative_phone on a phone.tooltip, email:to
* better validate options and directives (url locations, filesystem paths, etc..)
* make sure that you understand the risks
* full support for deactivation of services/accounts
* Display admin.is_active (disabled account special icon and order by support)
@ -232,17 +203,39 @@ require_once(/etc/moodles/.$moodle_host.config.php);``` moodle/drupl
* normurlpath '' return '/'
* initial configuration of multisite sas apps with password stored in DATA
* initial configuration of multisite sas apps with password stored in DATA ?? Dsign decission: initial pwds vs eventual consistency vs externa service vs backend raise exception?
* webapps installation complete, passowrd protected
* saas.initial_password autogenerated (ok because its random and not user provided) vs saas.password /change_Form provided + send email with initial_password
* more robust backend error handling, continue executing but exit code > 0 if failure, replace exit_code=0; do_sometging || exit_code=1
* saas require unique emails? connect to backend server to find out because they change
* automaitcally set passwords and email users?
* website directives uniquenes validation on serializers
+ is_Active custom filter with support for instance.account.is_Active
* django virtual field for saas and webapps related objects (db) to show on delete confirmation
if only extra related objects are databases and user databases why not make them first class relations?????
* >>> Account._meta.virtual_fields[0].bulk_related_objects([Account.objects.all()[0]])
[<ResourceData: account-disk: entrep>, <ResourceData: account-traffic: entrep>]
https://github.com/django/django/blob/master/django/db/models/deletion.py#L232
https://github.com/django/django/blob/master/django/contrib/contenttypes/fields.py#L282
from django.contrib.contenttypes.fields import GenericRelation
from django.db import DEFAULT_DB_ALIAS
from orchestra.apps.databases.models import Database
class VirtualRelation(GenericRelation):
def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS):
return []
# return Database.objects.filter(name__in=
# obj.service_instance.get_related() for obj in objs
## return self.remote_field.model._base_manager.db_manager(using).all()
relation = VirtualRelation('databases.Database')
SaaS.add_to_class('databases', relation)
* one to one relation deleteion on both sides??
* webapps/saas delete related db by id not name !! type!=Mysql

View File

@ -124,7 +124,7 @@ class ChangeAddFieldsMixin(object):
def get_readonly_fields(self, request, obj=None):
fields = super(ChangeAddFieldsMixin, self).get_readonly_fields(request, obj)
if obj:
return fields + self.get_change_readonly_fields(request, obj=obj)
return fields + self.get_change_readonly_fields(request, obj)
return fields
def get_fieldsets(self, request, obj=None):

View File

@ -31,7 +31,7 @@ disable.verbose_name = _("Disable")
def list_contacts(modeladmin, request, queryset):
ids = queryset.values_list('id', flat=True)
if not ids:
message.warning(request, "Select at least one account.")
messages.warning(request, "Select at least one account.")
return
url = reverse('admin:contacts_contact_changelist')
url += '?account__in=%s' % ','.join(map(str, ids))

View File

@ -95,7 +95,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
form_url=form_url, extra_context=context)
def get_fieldsets(self, request, obj=None):
fieldsets = super(AccountAdmin, self).get_fieldsets(request, obj=obj)
fieldsets = super(AccountAdmin, self).get_fieldsets(request, obj)
if not obj:
fields = AccountCreationForm.create_related_fields
if fields:
@ -153,6 +153,17 @@ class AccountAdminMixin(object):
account = None
list_select_related = ('account',)
def display_active(self, instance):
if not instance.is_active:
return '<img src="/static/admin/img/icon-no.gif" alt="False">'
elif not instance.account.is_active:
msg = _("Account disabled")
return '<img src="/static/admin/img/icon-unknown.gif" alt="False" title="%s">' % msg
return '<img src="/static/admin/img/icon-yes.gif" alt="True">'
display_active.short_description = _("active")
display_active.allow_tags = True
display_active.admin_order_field = 'is_active'
def account_link(self, instance):
account = instance.account if instance.pk else self.account
url = change_url(account)
@ -161,6 +172,23 @@ class AccountAdminMixin(object):
account_link.allow_tags = True
account_link.admin_order_field = 'account__username'
def render_change_form(self, request, context, *args, **kwargs):
""" Warns user when object's account is disabled """
try:
field = context['adminform'].form.fields['is_active']
except KeyError:
pass
else:
help_text = (
"Designates whether this account should be treated as active. "
"Unselect this instead of deleting accounts."
)
obj = kwargs.get('obj')
if obj and not obj.account.is_active:
help_text += "<br><b style='color:red;'>This user's account is dissabled</b>"
field.help_text = _(help_text)
return super(AccountAdminMixin, self).render_change_form(request, context, *args, **kwargs)
def get_fields(self, request, obj=None):
""" remove account or account_link depending on the case """
fields = super(AccountAdminMixin, self).get_fields(request, obj)
@ -181,7 +209,7 @@ class AccountAdminMixin(object):
""" provide account for filter_by_account_fields """
if obj:
self.account = obj.account
return super(AccountAdminMixin, self).get_readonly_fields(request, obj=obj)
return super(AccountAdminMixin, self).get_readonly_fields(request, obj)
def formfield_for_dbfield(self, db_field, **kwargs):
""" Filter by account """
@ -207,7 +235,7 @@ class AccountAdminMixin(object):
def get_formset(self, request, obj=None, **kwargs):
""" provides form.account for convinience """
formset = super(AccountAdminMixin, self).get_formset(request, obj=obj, **kwargs)
formset = super(AccountAdminMixin, self).get_formset(request, obj, **kwargs)
formset.form.account = self.account
formset.account = self.account
return formset
@ -263,7 +291,7 @@ class AccountAdminMixin(object):
class SelectAccountAdminMixin(AccountAdminMixin):
""" Provides support for accounts on ModelAdmin """
def get_inline_instances(self, request, obj=None):
inlines = super(AccountAdminMixin, self).get_inline_instances(request, obj=obj)
inlines = super(AccountAdminMixin, self).get_inline_instances(request, obj)
if self.account:
account = self.account
else:

View File

@ -1,4 +1,5 @@
from django.contrib.admin import SimpleListFilter
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
@ -18,3 +19,23 @@ class HasMainUserListFilter(SimpleListFilter):
return queryset.filter(users__isnull=False).distinct()
if self.value() == 'False':
return queryset.filter(users__isnull=True).distinct()
class IsActiveListFilter(SimpleListFilter):
title = _("Is active")
parameter_name = 'active'
def lookups(self, request, model_admin):
return (
('True', _("True")),
('False', _("False")),
)
def queryset(self, request, queryset):
if self.value() == 'True':
return queryset.filter(is_active=True, account__is_active=True)
elif self.value() == 'False':
return queryset.filter(Q(is_active=False) | Q(account__is_active=False))
return queryset

View File

@ -6,6 +6,8 @@ from django.db.models.loading import get_model
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration.middlewares import OperationsMiddleware
from orchestra.apps.orchestration.models import BackendOperation as Operation
from orchestra.core import services, accounts
from orchestra.utils import send_email_template
@ -63,11 +65,15 @@ class Account(auth.AbstractBaseUser):
def save(self, active_systemuser=False, *args, **kwargs):
created = not self.pk
if not created:
was_active = Account.objects.filter(pk=self.pk).values_list('is_active', flat=True)[0]
super(Account, self).save(*args, **kwargs)
if created:
self.main_systemuser = self.systemusers.create(account=self, username=self.username,
password=self.password, is_active=active_systemuser)
self.save(update_fields=['main_systemuser'])
elif was_active != self.is_active:
self.notify_related()
def clean(self):
self.short_name = self.short_name.strip()
@ -76,12 +82,15 @@ class Account(auth.AbstractBaseUser):
def disable(self):
self.is_active = False
self.save(update_fields=['is_active'])
self.notify_related()
def notify_related(self):
# Trigger save() on related objects that depend on this account
for rel in self._meta.get_all_related_objects():
source = getattr(rel, 'related_model', rel.model)
if source in services and hasattr(source, 'active'):
for obj in getattr(self, rel.get_accessor_name()).all():
obj.save(update_fields=[])
OperationsMiddleware.collect(Operation.SAVE, instance=obj, update_fields=[])
def send_email(self, template, context, contacts=[], attachments=[], html=None):
contacts = self.contacts.filter(email_usages=contacts)

View File

@ -145,19 +145,19 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
display_payment_state.short_description = _("Payment")
def get_readonly_fields(self, request, obj=None):
fields = super(BillAdmin, self).get_readonly_fields(request, obj=obj)
fields = super(BillAdmin, self).get_readonly_fields(request, obj)
if obj and not obj.is_open:
fields += self.add_fields
return fields
def get_fieldsets(self, request, obj=None):
fieldsets = super(BillAdmin, self).get_fieldsets(request, obj=obj)
fieldsets = super(BillAdmin, self).get_fieldsets(request, obj)
if obj and obj.is_open:
fieldsets = (fieldsets[0],)
return fieldsets
def get_change_view_actions(self, obj=None):
actions = super(BillAdmin, self).get_change_view_actions(obj=obj)
actions = super(BillAdmin, self).get_change_view_actions(obj)
exclude = []
if obj:
if not obj.is_open:
@ -165,7 +165,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
return [action for action in actions if action.__name__ not in exclude]
def get_inline_instances(self, request, obj=None):
inlines = super(BillAdmin, self).get_inline_instances(request, obj=obj)
inlines = super(BillAdmin, self).get_inline_instances(request, obj)
if obj and not obj.is_open:
return [inline for inline in inlines if not type(inline) == BillLineInline]
return [inline for inline in inlines if not type(inline) == ClosedBillLineInline]

View File

@ -110,7 +110,6 @@ def validate_zone(zone):
zone_name = zone.split()[0][:-1]
checkzone = settings.DOMAINS_CHECKZONE_BIN_PATH
cmd = ' '.join(["echo -e '%s'" % zone, '|', checkzone, zone_name, '/dev/stdin'])
print cmd
check = run(cmd, error_codes=[0, 1], display=False)
if check.return_code == 1:
errors = re.compile(r'zone.*: (.*)').findall(check.stdout)[:-1]

View File

@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
from orchestra.admin.utils import admin_link, change_url
from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
from orchestra.apps.accounts.filters import IsActiveListFilter
from . import settings
from .actions import SendMailboxEmail
@ -30,9 +31,9 @@ class AutoresponseInline(admin.StackedInline):
class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = (
'name', 'account_link', 'filtering', 'display_addresses'
'name', 'account_link', 'filtering', 'display_addresses', 'display_active',
)
list_filter = (HasAddressListFilter, 'filtering')
list_filter = (IsActiveListFilter, HasAddressListFilter, 'filtering')
search_fields = ('account__username', 'account__short_name', 'account__full_name', 'name')
add_fieldsets = (
(None, {
@ -81,7 +82,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
return super(MailboxAdmin, self).get_actions(request)
def get_fieldsets(self, request, obj=None):
fieldsets = super(MailboxAdmin, self).get_fieldsets(request, obj=obj)
fieldsets = super(MailboxAdmin, self).get_fieldsets(request, obj)
if obj and obj.filtering == obj.CUSTOM:
# not collapsed filtering when exists
fieldsets = copy.deepcopy(fieldsets)
@ -147,7 +148,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
def get_fields(self, request, obj=None):
""" Remove mailboxes field when creating address from a popup i.e. from mailbox add form """
fields = super(AddressAdmin, self).get_fields(request, obj=obj)
fields = super(AddressAdmin, self).get_fields(request, obj)
if '_to_field' in parse_qs(request.META['QUERY_STRING']):
# Add address popup
fields = list(fields)

View File

@ -88,7 +88,7 @@ class MiscellaneousAdmin(AccountAdminMixin, SelectPluginAdminMixin, admin.ModelA
return fields
def get_form(self, request, obj=None, **kwargs):
form = super(SelectPluginAdminMixin, self).get_form(request, obj=obj, **kwargs)
form = super(SelectPluginAdminMixin, self).get_form(request, obj, **kwargs)
service = self.get_service(obj)
def clean_identifier(self, service=service):
identifier = self.cleaned_data['identifier']

View File

@ -62,7 +62,7 @@ class RouteAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
""" Include dynamic help text for existing objects """
form = super(RouteAdmin, self).get_form(request, obj=obj, **kwargs)
form = super(RouteAdmin, self).get_form(request, obj, **kwargs)
if obj:
form.base_fields['backend'].help_text = self.BACKEND_HELP_TEXT.get(obj.backend, '')
return form

View File

@ -128,7 +128,7 @@ class Order(models.Model):
get_latest_by = 'id'
def __unicode__(self):
return str(self.service)
return unicode(self.service)
@classmethod
def update_orders(cls, instance, service=None, commit=True):
@ -178,8 +178,9 @@ class Order(models.Model):
MetricStorage.store(self, metric)
metric = ', metric:{}'.format(metric)
description = handler.get_order_description(instance)
logger.info("UPDATED order id:{id}, description:{description}{metric}".format(
id=self.id, description=description, metric=metric))
logger.info(u"UPDATED order id:{id}, description:{description}{metric}".format(
id=self.id, description=description, metric=metric).encode('ascii', 'ignore')
)
if self.description != description:
self.description = description
self.save(update_fields=['description'])

View File

@ -34,7 +34,7 @@ class PaymentSource(models.Model):
return PaymentMethod.get_plugin(self.method)
@cached_property
def service_instance(self):
def method_instance(self):
""" Per request lived method_instance """
return self.method_class(self)

View File

@ -27,6 +27,5 @@ class BSCWService(SoftwareService):
form = BSCWForm
serializer = BSCWDataSerializer
icon = 'orchestra/icons/apps/BSCW.png'
# TODO override from settings
site_domain = settings.SAAS_BSCW_DOMAIN
change_readonly_fileds = ('email',)

View File

@ -93,3 +93,6 @@ class SoftwareService(plugins.Plugin):
def delete(self):
pass
def get_related(self):
return []

View File

@ -9,6 +9,7 @@ SAAS_ENABLED_SERVICES = getattr(settings, 'SAAS_ENABLED_SERVICES', (
'orchestra.apps.saas.services.wordpress.WordPressService',
'orchestra.apps.saas.services.dokuwiki.DokuWikiService',
'orchestra.apps.saas.services.drupal.DrupalService',
'orchestra.apps.saas.services.seafile.SeaFileService',
))
@ -42,16 +43,24 @@ SAAS_PHPLIST_BASE_DOMAIN = getattr(settings, 'SAAS_PHPLIST_BASE_DOMAIN',
)
SAAS_SEAFILE_DOMAIN = getattr(settings, 'SAAS_SEAFILE_DOMAIN',
'seafile.orchestra.lan'
)
SAAS_SEAFILE_DEFAULT_QUOTA = getattr(settings, 'SAAS_SEAFILE_DEFAULT_QUOTA',
50
)
SAAS_BSCW_DOMAIN = getattr(settings, 'SAAS_BSCW_DOMAIN',
'bscw.orchestra.lan'
)
SAAS_BSCW_DEFAULT_QUOTA = getattr(settings, 'SAAS_BSCW_DEFAULT_QUOTA',
50
)
SAAS_GITLAB_ROOT_PASSWORD = getattr(settings, 'SAAS_GITLAB_ROOT_PASSWORD',
'secret'
)

View File

@ -67,11 +67,13 @@ view_help.verbose_name = _("Help")
def clone(modeladmin, request, queryset):
service = queryset.get()
fields = modeladmin.get_fields(request)
fk_fields = ('content_type',)
query = []
for field in fields:
if field in fk_fields:
model_field = type(service)._meta.get_field_by_name(field)[0]
if model_field.rel:
value = getattr(service, field + '_id')
elif 'Boolean' in model_field.__class__.__name__:
value = 'True' if getattr(service, field) else ''
else:
value = getattr(service, field)
query.append('%s=%s' % (field, value))

View File

@ -4,6 +4,7 @@ import decimal
from dateutil import relativedelta
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
@ -44,7 +45,7 @@ class ServiceHandler(plugins.Plugin):
def validate_match(self, service):
if not service.match:
raise ValidationError(_("Match should be provided."))
service.match = 'True'
try:
obj = service.content_type.model_class().objects.all()[0]
except IndexError:
@ -125,7 +126,7 @@ class ServiceHandler(plugins.Plugin):
instance._meta.model_name: instance,
}
if not self.order_description:
return '%s: %s' % (self.description, instance)
return u'%s: %s' % (self.description, instance)
return eval(self.order_description, safe_locals)
def get_billing_point(self, order, bp=None, **options):

View File

@ -53,7 +53,7 @@ class Service(models.Model):
"Related instance can be instantiated with <tt>instance</tt> keyword or "
"<tt>content_type.model_name</tt>.</br>"
"<tt>&nbsp;databaseuser.type == 'MYSQL'</tt><br>"
"<tt>&nbsp;miscellaneous.active and miscellaneous.identifier.endswith(('.org', '.net', '.com'))'</tt><br>"
"<tt>&nbsp;miscellaneous.active and miscellaneous.identifier.endswith(('.org', '.net', '.com'))</tt><br>"
"<tt>&nbsp;contractedplan.plan.name == 'association_fee''</tt><br>"
"<tt>&nbsp;instance.active</tt>"))
handler_type = models.CharField(_("handler"), max_length=256, blank=True,
@ -94,7 +94,7 @@ class Service(models.Model):
help_text=_("Period in which orders will be ignored if cancelled. "
"Useful for designating <i>trial periods</i>"),
choices=(
(NEVER, _("No ignore")),
(NEVER, _("Never")),
(ONE_DAY, _("One day")),
(TWO_DAYS, _("Two days")),
(TEN_DAYS, _("Ten days")),
@ -112,7 +112,7 @@ class Service(models.Model):
"<tt>&nbsp;miscellaneous.amount</tt><br>"
"<tt>&nbsp;max((account.resources.traffic.used or 0) -"
" getattr(account.miscellaneous.filter(is_active=True,"
" service__name='traffic prepay').last(), 'amount', 0), 0)</tt>"))
" service__name='traffic-prepay').last(), 'amount', 0), 0)</tt>"))
nominal_price = models.DecimalField(_("nominal price"), max_digits=12,
decimal_places=2)
tax = models.PositiveIntegerField(_("tax"), choices=settings.SERVICES_SERVICE_TAXES,
@ -174,11 +174,12 @@ class Service(models.Model):
def clean(self):
self.description = self.description.strip()
validators.all_valid({
'content_type': (self.handler.validate_content_type, self),
'match': (self.handlers.validate_match, self),
'metric': (self.handlers.validate_metric, self),
})
if hasattr(self, 'content_type'):
validators.all_valid({
'content_type': (self.handler.validate_content_type, self),
'match': (self.handler.validate_match, self),
'metric': (self.handler.validate_metric, self),
})
def get_pricing_period(self):
if self.pricing_period == self.BILLING_PERIOD:

View File

@ -18,4 +18,4 @@ SERVICES_SERVICE_ANUAL_BILLING_MONTH = getattr(settings, 'SERVICES_SERVICE_ANUAL
SERVICES_ORDER_MODEL = getattr(settings, 'SERVICES_ORDER_MODEL', 'orders.Order')
SERVICES_DEFAULT_IGNORE_PERIOD = getattr(settings, 'SERVICES_DEFAULT_IGNORE_PERIOD', 'TWO_DAYS')
SERVICES_DEFAULT_IGNORE_PERIOD = getattr(settings, 'SERVICES_DEFAULT_IGNORE_PERIOD', 'TEN_DAYS')

View File

@ -1,7 +1,8 @@
from functools import partial
from django import forms
from django.contrib import messages
from django.contrib import messages, admin
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.shortcuts import render
from django.utils.safestring import mark_safe
@ -28,3 +29,30 @@ def grant_permission(modeladmin, request, queryset):
# TODO
grant_permission.url_name = 'grant-permission'
grant_permission.verbose_name = _("Grant permission")
def delete_selected(modeladmin, request, queryset):
""" wrapper arround admin.actions.delete_selected to prevent main system users deletion """
opts = modeladmin.model._meta
app_label = opts.app_label
# Check that the user has delete permission for the actual model
if not modeladmin.has_delete_permission(request):
raise PermissionDenied
else:
accounts = []
for user in queryset:
if user.is_main:
accounts.append(user.username)
if accounts:
n = len(accounts)
messages.error(request, ungettext(
"You have selected one main system user (%(accounts)s), which can not be deleted.",
"You have selected some main system users which can not be deleted (%(accounts)s).",
n) % {
'accounts': ', '.join(accounts[:10]+['...'] if n > 10 else accounts)
}
)
return
return admin.actions.delete_selected(modeladmin, request, queryset)
delete_selected.short_description = _("Delete selected %(verbose_name_plural)s")

View File

@ -12,10 +12,11 @@ from django.utils.safestring import mark_safe
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
from orchestra.admin.utils import wrap_admin_view
from orchestra.apps.accounts.admin import SelectAccountAdminMixin
from orchestra.apps.accounts.filters import IsActiveListFilter
from orchestra.forms import UserCreationForm, UserChangeForm
from . import settings
from .actions import grant_permission
from .actions import grant_permission, delete_selected
from .filters import IsMainListFilter
from .forms import SystemUserCreationForm, SystemUserChangeForm
from .models import SystemUser
@ -25,7 +26,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
list_display = (
'username', 'account_link', 'shell', 'display_home', 'display_active', 'display_main'
)
list_filter = ('is_active', 'shell', IsMainListFilter)
list_filter = (IsActiveListFilter, 'shell', IsMainListFilter)
fieldsets = (
(None, {
'fields': ('username', 'password', 'account_link', 'is_active')
@ -50,15 +51,9 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
add_form = SystemUserCreationForm
form = SystemUserChangeForm
ordering = ('-id',)
actions = (grant_permission,)
actions = (delete_selected, grant_permission,)
change_view_actions = actions
def display_active(self, user):
return user.active
display_active.short_description = _("Active")
display_active.admin_order_field = 'is_active'
display_active.boolean = True
def display_main(self, user):
return user.is_main
display_main.short_description = _("Main")
@ -70,7 +65,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
display_home.admin_order_field = 'home'
def get_form(self, request, obj=None, **kwargs):
form = super(SystemUserAdmin, self).get_form(request, obj=obj, **kwargs)
form = super(SystemUserAdmin, self).get_form(request, obj, **kwargs)
form.account = self.account
if obj:
# Has to be done here and not in the form because of strange phenomenon
@ -83,7 +78,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
def has_delete_permission(self, request, obj=None):
if obj and obj.is_main:
return False
return super(SystemUserAdmin, self).has_delete_permission(request, obj=obj)
return super(SystemUserAdmin, self).has_delete_permission(request, obj)
admin.site.register(SystemUser, SystemUserAdmin)

View File

@ -18,6 +18,7 @@ class SystemUserBackend(ServiceController):
context = self.get_context(user)
groups = ','.join(self.get_groups(user))
context['groups_arg'] = '--groups %s' % groups if groups else ''
# TODO userd add will fail if %(user)s group already exists
self.append(textwrap.dedent("""
if [[ $( id %(user)s ) ]]; then
usermod %(user)s --password '%(password)s' --shell %(shell)s %(groups_arg)s

View File

@ -19,11 +19,11 @@ class SelectPluginAdminMixin(object):
else:
plugin = self.plugin.get_plugin(self.plugin_value)()
self.form = plugin.get_form()
return super(SelectPluginAdminMixin, self).get_form(request, obj=obj, **kwargs)
return super(SelectPluginAdminMixin, self).get_form(request, obj, **kwargs)
def get_fields(self, request, obj=None):
""" Try to maintain original field ordering """
fields = super(SelectPluginAdminMixin, self).get_fields(request, obj=obj)
fields = super(SelectPluginAdminMixin, self).get_fields(request, obj)
head_fields = list(self.get_readonly_fields(request, obj))
head, tail = [], []
for field in fields: