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 * backend logs with hal logo
* set_password orchestration method?
* LAST version of this shit http://wkhtmltopdf.org/downloads.html * LAST version of this shit http://wkhtmltopdf.org/downloads.html
@ -39,8 +37,6 @@
* mail backend related_models = ('resources__content_type') ?? * mail backend related_models = ('resources__content_type') ??
* Domain backend PowerDNS Bind validation support?
* Maildir billing tests/ webdisk billing tests (avg metric) * Maildir billing tests/ webdisk billing tests (avg metric)
@ -50,10 +46,6 @@
* rename accounts register to "account", and reated api and admin references * 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 * prevent deletion of main user by the user itself
* AccountAdminMixin auto adds 'account__name' on searchfields * 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 * 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) * 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 * resource min max allocation with validation
* mailman needs both aliases when address_name is provided (default messages and bounces and all) * 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) * Create an admin service_view with icons (like SaaS app)
* Resource graph for each related object * 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 * Service.account change and orders consistency
* Mix webapps type with backends (two for the price of one) * SaaS model splitted into SaaSUser and SaaSSite? inherit from SaaS
* 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
* prevent @pangea.org email addresses on contacts, enforce at least one email without @pangea.org * 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 * <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 * 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 * contact.alternative_phone on a phone.tooltip, email:to
* better validate options and directives (url locations, filesystem paths, etc..) * better validate options and directives (url locations, filesystem paths, etc..)
* make sure that you understand the risks * make sure that you understand the risks
* full support for deactivation of services/accounts * full support for deactivation of services/accounts
* Display admin.is_active (disabled account special icon and order by support) * 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 '/' * 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 * 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 * 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 * 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? * automaitcally set passwords and email users?
* website directives uniquenes validation on serializers * 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): def get_readonly_fields(self, request, obj=None):
fields = super(ChangeAddFieldsMixin, self).get_readonly_fields(request, obj) fields = super(ChangeAddFieldsMixin, self).get_readonly_fields(request, obj)
if obj: if obj:
return fields + self.get_change_readonly_fields(request, obj=obj) return fields + self.get_change_readonly_fields(request, obj)
return fields return fields
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):

View File

@ -31,7 +31,7 @@ disable.verbose_name = _("Disable")
def list_contacts(modeladmin, request, queryset): def list_contacts(modeladmin, request, queryset):
ids = queryset.values_list('id', flat=True) ids = queryset.values_list('id', flat=True)
if not ids: if not ids:
message.warning(request, "Select at least one account.") messages.warning(request, "Select at least one account.")
return return
url = reverse('admin:contacts_contact_changelist') url = reverse('admin:contacts_contact_changelist')
url += '?account__in=%s' % ','.join(map(str, ids)) 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) form_url=form_url, extra_context=context)
def get_fieldsets(self, request, obj=None): 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: if not obj:
fields = AccountCreationForm.create_related_fields fields = AccountCreationForm.create_related_fields
if fields: if fields:
@ -153,6 +153,17 @@ class AccountAdminMixin(object):
account = None account = None
list_select_related = ('account',) 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): def account_link(self, instance):
account = instance.account if instance.pk else self.account account = instance.account if instance.pk else self.account
url = change_url(account) url = change_url(account)
@ -161,6 +172,23 @@ class AccountAdminMixin(object):
account_link.allow_tags = True account_link.allow_tags = True
account_link.admin_order_field = 'account__username' 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): def get_fields(self, request, obj=None):
""" remove account or account_link depending on the case """ """ remove account or account_link depending on the case """
fields = super(AccountAdminMixin, self).get_fields(request, obj) fields = super(AccountAdminMixin, self).get_fields(request, obj)
@ -181,7 +209,7 @@ class AccountAdminMixin(object):
""" provide account for filter_by_account_fields """ """ provide account for filter_by_account_fields """
if obj: if obj:
self.account = obj.account 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): def formfield_for_dbfield(self, db_field, **kwargs):
""" Filter by account """ """ Filter by account """
@ -207,7 +235,7 @@ class AccountAdminMixin(object):
def get_formset(self, request, obj=None, **kwargs): def get_formset(self, request, obj=None, **kwargs):
""" provides form.account for convinience """ """ 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.form.account = self.account
formset.account = self.account formset.account = self.account
return formset return formset
@ -263,7 +291,7 @@ class AccountAdminMixin(object):
class SelectAccountAdminMixin(AccountAdminMixin): class SelectAccountAdminMixin(AccountAdminMixin):
""" Provides support for accounts on ModelAdmin """ """ Provides support for accounts on ModelAdmin """
def get_inline_instances(self, request, obj=None): 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: if self.account:
account = self.account account = self.account
else: else:

View File

@ -1,4 +1,5 @@
from django.contrib.admin import SimpleListFilter from django.contrib.admin import SimpleListFilter
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -18,3 +19,23 @@ class HasMainUserListFilter(SimpleListFilter):
return queryset.filter(users__isnull=False).distinct() return queryset.filter(users__isnull=False).distinct()
if self.value() == 'False': if self.value() == 'False':
return queryset.filter(users__isnull=True).distinct() 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 import timezone
from django.utils.translation import ugettext_lazy as _ 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.core import services, accounts
from orchestra.utils import send_email_template from orchestra.utils import send_email_template
@ -63,11 +65,15 @@ class Account(auth.AbstractBaseUser):
def save(self, active_systemuser=False, *args, **kwargs): def save(self, active_systemuser=False, *args, **kwargs):
created = not self.pk 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) super(Account, self).save(*args, **kwargs)
if created: if created:
self.main_systemuser = self.systemusers.create(account=self, username=self.username, self.main_systemuser = self.systemusers.create(account=self, username=self.username,
password=self.password, is_active=active_systemuser) password=self.password, is_active=active_systemuser)
self.save(update_fields=['main_systemuser']) self.save(update_fields=['main_systemuser'])
elif was_active != self.is_active:
self.notify_related()
def clean(self): def clean(self):
self.short_name = self.short_name.strip() self.short_name = self.short_name.strip()
@ -76,12 +82,15 @@ class Account(auth.AbstractBaseUser):
def disable(self): def disable(self):
self.is_active = False self.is_active = False
self.save(update_fields=['is_active']) self.save(update_fields=['is_active'])
self.notify_related()
def notify_related(self):
# Trigger save() on related objects that depend on this account # Trigger save() on related objects that depend on this account
for rel in self._meta.get_all_related_objects(): for rel in self._meta.get_all_related_objects():
source = getattr(rel, 'related_model', rel.model) source = getattr(rel, 'related_model', rel.model)
if source in services and hasattr(source, 'active'): if source in services and hasattr(source, 'active'):
for obj in getattr(self, rel.get_accessor_name()).all(): 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): def send_email(self, template, context, contacts=[], attachments=[], html=None):
contacts = self.contacts.filter(email_usages=contacts) contacts = self.contacts.filter(email_usages=contacts)

View File

@ -145,19 +145,19 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
display_payment_state.short_description = _("Payment") display_payment_state.short_description = _("Payment")
def get_readonly_fields(self, request, obj=None): 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: if obj and not obj.is_open:
fields += self.add_fields fields += self.add_fields
return fields return fields
def get_fieldsets(self, request, obj=None): 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: if obj and obj.is_open:
fieldsets = (fieldsets[0],) fieldsets = (fieldsets[0],)
return fieldsets return fieldsets
def get_change_view_actions(self, obj=None): 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 = [] exclude = []
if obj: if obj:
if not obj.is_open: 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] return [action for action in actions if action.__name__ not in exclude]
def get_inline_instances(self, request, obj=None): 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: 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) == BillLineInline]
return [inline for inline in inlines if not type(inline) == ClosedBillLineInline] 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] zone_name = zone.split()[0][:-1]
checkzone = settings.DOMAINS_CHECKZONE_BIN_PATH checkzone = settings.DOMAINS_CHECKZONE_BIN_PATH
cmd = ' '.join(["echo -e '%s'" % zone, '|', checkzone, zone_name, '/dev/stdin']) cmd = ' '.join(["echo -e '%s'" % zone, '|', checkzone, zone_name, '/dev/stdin'])
print cmd
check = run(cmd, error_codes=[0, 1], display=False) check = run(cmd, error_codes=[0, 1], display=False)
if check.return_code == 1: if check.return_code == 1:
errors = re.compile(r'zone.*: (.*)').findall(check.stdout)[:-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 import ExtendedModelAdmin, ChangePasswordAdminMixin
from orchestra.admin.utils import admin_link, change_url from orchestra.admin.utils import admin_link, change_url
from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
from orchestra.apps.accounts.filters import IsActiveListFilter
from . import settings from . import settings
from .actions import SendMailboxEmail from .actions import SendMailboxEmail
@ -30,9 +31,9 @@ class AutoresponseInline(admin.StackedInline):
class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin): class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = ( 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') search_fields = ('account__username', 'account__short_name', 'account__full_name', 'name')
add_fieldsets = ( add_fieldsets = (
(None, { (None, {
@ -81,7 +82,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
return super(MailboxAdmin, self).get_actions(request) return super(MailboxAdmin, self).get_actions(request)
def get_fieldsets(self, request, obj=None): 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: if obj and obj.filtering == obj.CUSTOM:
# not collapsed filtering when exists # not collapsed filtering when exists
fieldsets = copy.deepcopy(fieldsets) fieldsets = copy.deepcopy(fieldsets)
@ -147,7 +148,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
def get_fields(self, request, obj=None): def get_fields(self, request, obj=None):
""" Remove mailboxes field when creating address from a popup i.e. from mailbox add form """ """ 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']): if '_to_field' in parse_qs(request.META['QUERY_STRING']):
# Add address popup # Add address popup
fields = list(fields) fields = list(fields)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -93,3 +93,6 @@ class SoftwareService(plugins.Plugin):
def delete(self): def delete(self):
pass 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.wordpress.WordPressService',
'orchestra.apps.saas.services.dokuwiki.DokuWikiService', 'orchestra.apps.saas.services.dokuwiki.DokuWikiService',
'orchestra.apps.saas.services.drupal.DrupalService', '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', SAAS_BSCW_DOMAIN = getattr(settings, 'SAAS_BSCW_DOMAIN',
'bscw.orchestra.lan' 'bscw.orchestra.lan'
) )
SAAS_BSCW_DEFAULT_QUOTA = getattr(settings, 'SAAS_BSCW_DEFAULT_QUOTA', SAAS_BSCW_DEFAULT_QUOTA = getattr(settings, 'SAAS_BSCW_DEFAULT_QUOTA',
50 50
) )
SAAS_GITLAB_ROOT_PASSWORD = getattr(settings, 'SAAS_GITLAB_ROOT_PASSWORD', SAAS_GITLAB_ROOT_PASSWORD = getattr(settings, 'SAAS_GITLAB_ROOT_PASSWORD',
'secret' 'secret'
) )

View File

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

View File

@ -4,6 +4,7 @@ import decimal
from dateutil import relativedelta from dateutil import relativedelta
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -44,7 +45,7 @@ class ServiceHandler(plugins.Plugin):
def validate_match(self, service): def validate_match(self, service):
if not service.match: if not service.match:
raise ValidationError(_("Match should be provided.")) service.match = 'True'
try: try:
obj = service.content_type.model_class().objects.all()[0] obj = service.content_type.model_class().objects.all()[0]
except IndexError: except IndexError:
@ -125,7 +126,7 @@ class ServiceHandler(plugins.Plugin):
instance._meta.model_name: instance, instance._meta.model_name: instance,
} }
if not self.order_description: 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) return eval(self.order_description, safe_locals)
def get_billing_point(self, order, bp=None, **options): 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 " "Related instance can be instantiated with <tt>instance</tt> keyword or "
"<tt>content_type.model_name</tt>.</br>" "<tt>content_type.model_name</tt>.</br>"
"<tt>&nbsp;databaseuser.type == 'MYSQL'</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;contractedplan.plan.name == 'association_fee''</tt><br>"
"<tt>&nbsp;instance.active</tt>")) "<tt>&nbsp;instance.active</tt>"))
handler_type = models.CharField(_("handler"), max_length=256, blank=True, 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. " help_text=_("Period in which orders will be ignored if cancelled. "
"Useful for designating <i>trial periods</i>"), "Useful for designating <i>trial periods</i>"),
choices=( choices=(
(NEVER, _("No ignore")), (NEVER, _("Never")),
(ONE_DAY, _("One day")), (ONE_DAY, _("One day")),
(TWO_DAYS, _("Two days")), (TWO_DAYS, _("Two days")),
(TEN_DAYS, _("Ten days")), (TEN_DAYS, _("Ten days")),
@ -112,7 +112,7 @@ class Service(models.Model):
"<tt>&nbsp;miscellaneous.amount</tt><br>" "<tt>&nbsp;miscellaneous.amount</tt><br>"
"<tt>&nbsp;max((account.resources.traffic.used or 0) -" "<tt>&nbsp;max((account.resources.traffic.used or 0) -"
" getattr(account.miscellaneous.filter(is_active=True," " 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, nominal_price = models.DecimalField(_("nominal price"), max_digits=12,
decimal_places=2) decimal_places=2)
tax = models.PositiveIntegerField(_("tax"), choices=settings.SERVICES_SERVICE_TAXES, tax = models.PositiveIntegerField(_("tax"), choices=settings.SERVICES_SERVICE_TAXES,
@ -174,11 +174,12 @@ class Service(models.Model):
def clean(self): def clean(self):
self.description = self.description.strip() self.description = self.description.strip()
validators.all_valid({ if hasattr(self, 'content_type'):
'content_type': (self.handler.validate_content_type, self), validators.all_valid({
'match': (self.handlers.validate_match, self), 'content_type': (self.handler.validate_content_type, self),
'metric': (self.handlers.validate_metric, self), 'match': (self.handler.validate_match, self),
}) 'metric': (self.handler.validate_metric, self),
})
def get_pricing_period(self): def get_pricing_period(self):
if self.pricing_period == self.BILLING_PERIOD: 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_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 functools import partial
from django import forms 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.db import transaction
from django.shortcuts import render from django.shortcuts import render
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -28,3 +29,30 @@ def grant_permission(modeladmin, request, queryset):
# TODO # TODO
grant_permission.url_name = 'grant-permission' grant_permission.url_name = 'grant-permission'
grant_permission.verbose_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 import ExtendedModelAdmin, ChangePasswordAdminMixin
from orchestra.admin.utils import wrap_admin_view from orchestra.admin.utils import wrap_admin_view
from orchestra.apps.accounts.admin import SelectAccountAdminMixin from orchestra.apps.accounts.admin import SelectAccountAdminMixin
from orchestra.apps.accounts.filters import IsActiveListFilter
from orchestra.forms import UserCreationForm, UserChangeForm from orchestra.forms import UserCreationForm, UserChangeForm
from . import settings from . import settings
from .actions import grant_permission from .actions import grant_permission, delete_selected
from .filters import IsMainListFilter from .filters import IsMainListFilter
from .forms import SystemUserCreationForm, SystemUserChangeForm from .forms import SystemUserCreationForm, SystemUserChangeForm
from .models import SystemUser from .models import SystemUser
@ -25,7 +26,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
list_display = ( list_display = (
'username', 'account_link', 'shell', 'display_home', 'display_active', 'display_main' 'username', 'account_link', 'shell', 'display_home', 'display_active', 'display_main'
) )
list_filter = ('is_active', 'shell', IsMainListFilter) list_filter = (IsActiveListFilter, 'shell', IsMainListFilter)
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('username', 'password', 'account_link', 'is_active') 'fields': ('username', 'password', 'account_link', 'is_active')
@ -50,15 +51,9 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
add_form = SystemUserCreationForm add_form = SystemUserCreationForm
form = SystemUserChangeForm form = SystemUserChangeForm
ordering = ('-id',) ordering = ('-id',)
actions = (grant_permission,) actions = (delete_selected, grant_permission,)
change_view_actions = actions 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): def display_main(self, user):
return user.is_main return user.is_main
display_main.short_description = _("Main") display_main.short_description = _("Main")
@ -70,7 +65,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
display_home.admin_order_field = 'home' display_home.admin_order_field = 'home'
def get_form(self, request, obj=None, **kwargs): 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 form.account = self.account
if obj: if obj:
# Has to be done here and not in the form because of strange phenomenon # 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): def has_delete_permission(self, request, obj=None):
if obj and obj.is_main: if obj and obj.is_main:
return False 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) admin.site.register(SystemUser, SystemUserAdmin)

View File

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

View File

@ -19,11 +19,11 @@ class SelectPluginAdminMixin(object):
else: else:
plugin = self.plugin.get_plugin(self.plugin_value)() plugin = self.plugin.get_plugin(self.plugin_value)()
self.form = plugin.get_form() 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): def get_fields(self, request, obj=None):
""" Try to maintain original field ordering """ """ 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_fields = list(self.get_readonly_fields(request, obj))
head, tail = [], [] head, tail = [], []
for field in fields: for field in fields: