Random improvements

This commit is contained in:
Marc 2014-10-23 15:38:46 +00:00
parent 5619141514
commit 9534e6e571
44 changed files with 332 additions and 122 deletions

20
TODO.md
View file

@ -12,7 +12,6 @@ TODO
* enforce an emergency email contact and account to contact contacts about problems when mailserver is down * enforce an emergency email contact and account to contact contacts about problems when mailserver is down
* add `BackendLog` retry action * add `BackendLog` retry action
* move invoice contact to invoices app?
* PHPbBckendMiixin with get_php_ini * PHPbBckendMiixin with get_php_ini
* Apache: `IncludeOptional /etc/apache2/extra-vhos[t]/account-site-custom.con[f]` * Apache: `IncludeOptional /etc/apache2/extra-vhos[t]/account-site-custom.con[f]`
* webmail identities and addresses * webmail identities and addresses
@ -143,7 +142,7 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* based on a merge set of save(update_fields) * based on a merge set of save(update_fields)
textwrap.dedent( \\) * textwrap.dedent( \\)
* accounts * accounts
* short name / long name, account name really needed? address? only minimal info.. * short name / long name, account name really needed? address? only minimal info..
@ -159,7 +158,22 @@ textwrap.dedent( \\)
* better modeling of the interdependency between webapps and websites (settings) * better modeling of the interdependency between webapps and websites (settings)
* webapp options cfig agnostic * webapp options cfig agnostic
* Disable menu on tests, fucking overlapping
* service.name / verbose_name instead of .description ? * service.name / verbose_name instead of .description ?
* miscellaneous.name / verbose_name
* service.invoice_name
* Bills can have sublines? * Bills can have sublines?
* proforma without billing contact?
* remove contact addresss, and use invoice contact for it (maybe move to contacts app again)
* env ORCHESTRA_MASTER_SERVER='test1.orchestra.lan' ORCHESTRA_SECOND_SERVER='test2.orchestra.lan' ORCHESTRA_SLAVE_SERVER='test3.orchestra.lan' python manage.py test orchestra.apps.domains.tests.functional_tests.tests:AdminBind9BackendDomainTest
* Pangea modifications: domain registered/non-registered list_display and field with register link: inconsistent, what happen to related objects with a domain that is converted to register-only?
* ForeignKey.swappable
* Field.editable
* ManyToManyField.symmetrical = False (user group)
* REST PERMISSIONS

View file

@ -63,6 +63,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
change_form_template = 'admin/accounts/account/change_form.html' change_form_template = 'admin/accounts/account/change_form.html'
actions = [disable] actions = [disable]
change_view_actions = actions change_view_actions = actions
list_select_related = ('billcontact',)
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """ """ Make value input widget bigger """
@ -99,12 +100,6 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
fieldsets.insert(1, (_("Related services"), {'fields': fields})) fieldsets.insert(1, (_("Related services"), {'fields': fields}))
return fieldsets return fieldsets
def get_queryset(self, request):
""" Select related for performance """
qs = super(AccountAdmin, self).get_queryset(request)
related = ('invoicecontact',)
return qs.select_related(*related)
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
super(AccountAdmin, self).save_model(request, obj, form, change) super(AccountAdmin, self).save_model(request, obj, form, change)
if not change: if not change:
@ -152,6 +147,7 @@ class AccountAdminMixin(object):
change_list_template = 'admin/accounts/account/change_list.html' change_list_template = 'admin/accounts/account/change_list.html'
change_form_template = 'admin/accounts/account/change_form.html' change_form_template = 'admin/accounts/account/change_form.html'
account = None account = None
list_select_related = ('account',)
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
@ -167,11 +163,6 @@ class AccountAdminMixin(object):
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=obj)
def get_queryset(self, request):
""" Select related for performance """
qs = super(AccountAdminMixin, self).get_queryset(request)
return qs.select_related('account')
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
""" Filter by account """ """ Filter by account """
formfield = super(AccountAdminMixin, self).formfield_for_dbfield(db_field, **kwargs) formfield = super(AccountAdminMixin, self).formfield_for_dbfield(db_field, **kwargs)

View file

@ -1,4 +1,5 @@
from rest_framework import viewsets from django.utils.translation import ugettext_lazy as _
from rest_framework import viewsets, exceptions
from orchestra.api import router, SetPasswordApiMixin from orchestra.api import router, SetPasswordApiMixin
@ -21,5 +22,11 @@ class AccountViewSet(SetPasswordApiMixin, viewsets.ModelViewSet):
qs = super(AccountViewSet, self).get_queryset() qs = super(AccountViewSet, self).get_queryset()
return qs.filter(id=self.request.user.pk) return qs.filter(id=self.request.user.pk)
def destroy(self, request, pk=None):
# TODO reimplement in permissions
if not request.user.is_superuser:
raise exceptions.PermissionDenied(_("Accounts can not be deleted."))
super(AccountViewSet, self).destroy(request, pk=pk)
router.register(r'accounts', AccountViewSet) router.register(r'accounts', AccountViewSet)

View file

@ -9,7 +9,12 @@ from .models import Account
def create_account_creation_form(): def create_account_creation_form():
fields = {} fields = {
'create_systemuser': forms.BooleanField(initial=True, required=False,
label=_("Create systemuser"), widget=forms.CheckboxInput(attrs={'disabled': True}),
help_text=_("Designates whether to creates a related system user with the same "
"username and password or not."))
}
for model, key, kwargs, help_text in settings.ACCOUNTS_CREATE_RELATED: for model, key, kwargs, help_text in settings.ACCOUNTS_CREATE_RELATED:
model = get_model(model) model = get_model(model)
field_name = 'create_%s' % model._meta.model_name field_name = 'create_%s' % model._meta.model_name
@ -28,6 +33,9 @@ def create_account_creation_form():
except KeyError: except KeyError:
# Previous validation error # Previous validation error
return return
systemuser_model = Account.main_systemuser.field.rel.to
if systemuser_model.objects.filter(username=account.username).exists():
raise forms.ValidationError(_("A system user with this name already exists"))
for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED: for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED:
model = get_model(model) model = get_model(model)
kwargs = { kwargs = {
@ -44,9 +52,10 @@ def create_account_creation_form():
model = get_model(model) model = get_model(model)
field_name = 'create_%s' % model._meta.model_name field_name = 'create_%s' % model._meta.model_name
if self.cleaned_data[field_name]: if self.cleaned_data[field_name]:
for key, value in related_kwargs.iteritems(): kwargs = {
related_kwargs[key] = eval(value, {'account': account}) key: eval(value, {'account': account}) for key, value in related_kwargs.iteritems()
model.objects.create(account=account, **related_kwargs) }
model.objects.create(account=account, **kwargs)
fields.update({ fields.update({
'create_related_fields': fields.keys(), 'create_related_fields': fields.keys(),

View file

@ -17,6 +17,8 @@ class Account(auth.AbstractBaseUser):
help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."), help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."),
validators=[validators.RegexValidator(r'^[\w.-]+$', validators=[validators.RegexValidator(r'^[\w.-]+$',
_("Enter a valid username."), 'invalid')]) _("Enter a valid username."), 'invalid')])
main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True,
related_name='accounts_main')
first_name = models.CharField(_("first name"), max_length=30, blank=True) first_name = models.CharField(_("first name"), max_length=30, blank=True)
last_name = models.CharField(_("last name"), max_length=30, blank=True) last_name = models.CharField(_("last name"), max_length=30, blank=True)
email = models.EmailField(_('email address'), help_text=_("Used for password recovery")) email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
@ -50,14 +52,22 @@ class Account(auth.AbstractBaseUser):
def is_staff(self): def is_staff(self):
return self.is_superuser return self.is_superuser
@property # @property
def main_systemuser(self): # def main_systemuser(self):
return self.systemusers.get(is_main=True) # return self.systemusers.get(is_main=True)
@classmethod @classmethod
def get_main(cls): def get_main(cls):
return cls.objects.get(pk=settings.ACCOUNTS_MAIN_PK) return cls.objects.get(pk=settings.ACCOUNTS_MAIN_PK)
def save(self, *args, **kwargs):
created = not self.pk
super(Account, self).save(*args, **kwargs)
if created:
self.main_systemuser = self.systemusers.create(account=self, username=self.username,
password=self.password)
self.save(update_fields=['main_systemuser'])
def clean(self): def clean(self):
self.first_name = self.first_name.strip() self.first_name = self.first_name.strip()
self.last_name = self.last_name.strip() self.last_name = self.last_name.strip()
@ -126,13 +136,15 @@ class Account(auth.AbstractBaseUser):
return auth._user_has_module_perms(self, app_label) return auth._user_has_module_perms(self, app_label)
def get_related_passwords(self): def get_related_passwords(self):
related = [] related = [
for model, key, kwargs, __ in settings.ACCOUNTS_CREATE_RELATED: self.main_systemuser,
if 'password' not in kwargs: ]
for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED:
if 'password' not in related_kwargs:
continue continue
model = get_model(model) model = get_model(model)
kwargs = { kwargs = {
key: eval(kwargs[key], {'account': self}) key: eval(related_kwargs[key], {'account': self})
} }
try: try:
rel = model.objects.get(account=self, **kwargs) rel = model.objects.get(account=self, **kwargs)

View file

@ -18,6 +18,10 @@ ACCOUNTS_LANGUAGES = getattr(settings, 'ACCOUNTS_LANGUAGES', (
)) ))
ACCOUNTS_SYSTEMUSER_MODEL = getattr(settings, 'ACCOUNTS_SYSTEMUSER_MODEL',
'systemusers.SystemUser')
ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'en') ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'en')
@ -26,15 +30,6 @@ ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', 1)
ACCOUNTS_CREATE_RELATED = getattr(settings, 'ACCOUNTS_CREATE_RELATED', ( ACCOUNTS_CREATE_RELATED = getattr(settings, 'ACCOUNTS_CREATE_RELATED', (
# <model>, <key field>, <kwargs>, <help_text> # <model>, <key field>, <kwargs>, <help_text>
('systemusers.SystemUser',
'username',
{
'username': 'account.username',
'password': 'account.password',
'is_main': 'True',
},
_("Designates whether to creates a related system user with the same username and password or not."),
),
('mailboxes.Mailbox', ('mailboxes.Mailbox',
'name', 'name',
{ {

View file

@ -36,7 +36,7 @@ class BillLineInline(admin.TabularInline):
if sublines: if sublines:
content = '\n'.join(['%s: %s' % (sub.description, sub.total) for sub in sublines]) content = '\n'.join(['%s: %s' % (sub.description, sub.total) for sub in sublines])
img = static('admin/img/icon_alert.gif') img = static('admin/img/icon_alert.gif')
return '<span title="%s">%s<img src="%s"></img></span>' % (content, str(total), img) return '<span title="%s">%s <img src="%s"></img></span>' % (content, str(total), img)
return total return total
display_total.short_description = _("Total") display_total.short_description = _("Total")
display_total.allow_tags = True display_total.allow_tags = True

View file

@ -3,7 +3,7 @@ from django.contrib import admin
from django.utils.translation import ugettext, ugettext_lazy as _ from django.utils.translation import ugettext, ugettext_lazy as _
from orchestra.admin import AtLeastOneRequiredInlineFormSet from orchestra.admin import AtLeastOneRequiredInlineFormSet
from orchestra.admin.utils import insertattr from orchestra.admin.utils import insertattr, admin_link, change_url
from orchestra.apps.accounts.admin import AccountAdmin, AccountAdminMixin from orchestra.apps.accounts.admin import AccountAdmin, AccountAdminMixin
from orchestra.forms.widgets import paddingCheckboxSelectMultiple from orchestra.forms.widgets import paddingCheckboxSelectMultiple
@ -74,15 +74,20 @@ class ContactInline(admin.StackedInline):
formset = AtLeastOneRequiredInlineFormSet formset = AtLeastOneRequiredInlineFormSet
extra = 0 extra = 0
fields = ( fields = (
'short_name', 'full_name', 'email', 'email_usage', ('phone', 'phone2'), ('short_name', 'full_name'), 'email', 'email_usage', ('phone', 'phone2'),
'address', ('city', 'zipcode'), 'country',
) )
def get_extra(self, request, obj=None, **kwargs): def get_extra(self, request, obj=None, **kwargs):
return 0 if obj and obj.contacts.exists() else 1 return 0 if obj and obj.contacts.exists() else 1
def get_view_on_site_url(self, obj=None):
if obj:
return change_url(obj)
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """ """ Make value input widget bigger """
if db_field.name == 'short_name':
kwargs['widget'] = forms.TextInput(attrs={'size':'15'})
if db_field.name == 'address': if db_field.name == 'address':
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2}) kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
if db_field.name == 'email_usage': if db_field.name == 'email_usage':

View file

@ -296,10 +296,13 @@ class AdminDatabaseMixin(DatabaseTestMixin):
self.selenium.get(url) self.selenium.get(url)
user = DatabaseUser.objects.get(username=username, type=self.db_type) user = DatabaseUser.objects.get(username=username, type=self.db_type)
users_input = self.selenium.find_element_by_id('id_users') users_from = self.selenium.find_element_by_id('id_users_from')
users_select = Select(users_input) users_select = Select(users_from)
users_select.select_by_value(str(user.pk)) users_select.select_by_value(str(user.pk))
add_user = self.selenium.find_element_by_id('id_users_add_link')
add_user.click()
save = self.selenium.find_element_by_name('_save') save = self.selenium.find_element_by_name('_save')
save.submit() save.submit()
self.assertNotEqual(url, self.selenium.current_url) self.assertNotEqual(url, self.selenium.current_url)
@ -310,13 +313,23 @@ class AdminDatabaseMixin(DatabaseTestMixin):
url = self.live_server_url + change_url(database) url = self.live_server_url + change_url(database)
self.selenium.get(url) self.selenium.get(url)
# remove user "username"
user = DatabaseUser.objects.get(username=username, type=self.db_type) user = DatabaseUser.objects.get(username=username, type=self.db_type)
users_input = self.selenium.find_element_by_id('id_users') users_to = self.selenium.find_element_by_id('id_users_to')
users_select = Select(users_input) users_select = Select(users_to)
users_select.deselect_by_value(str(user.pk))
user = DatabaseUser.objects.get(username=username2, type=self.db_type)
users_select.select_by_value(str(user.pk)) users_select.select_by_value(str(user.pk))
remove_user = self.selenium.find_element_by_id('id_users_remove_link')
remove_user.click()
time.sleep(0.2)
# add user "username2"
user = DatabaseUser.objects.get(username=username2, type=self.db_type)
users_from = self.selenium.find_element_by_id('id_users_from')
users_select = Select(users_from)
users_select.select_by_value(str(user.pk))
add_user = self.selenium.find_element_by_id('id_users_add_link')
add_user.click()
time.sleep(0.2)
save = self.selenium.find_element_by_name('_save') save = self.selenium.find_element_by_name('_save')
save.submit() save.submit()

View file

@ -20,19 +20,22 @@ class RecordInline(admin.TabularInline):
formset = RecordInlineFormSet formset = RecordInlineFormSet
verbose_name_plural = _("Extra records") verbose_name_plural = _("Extra records")
class Media: # class Media:
css = { # css = {
'all': ('orchestra/css/hide-inline-id.css',) # 'all': ('orchestra/css/hide-inline-id.css',)
} # }
#
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """ """ Make value input widget bigger """
if db_field.name == 'value': if db_field.name == 'value':
kwargs['widget'] = forms.TextInput(attrs={'size':'100'}) kwargs['widget'] = forms.TextInput(attrs={'size':'100'})
if db_field.name == 'ttl':
kwargs['widget'] = forms.TextInput(attrs={'size':'10'})
return super(RecordInline, self).formfield_for_dbfield(db_field, **kwargs) return super(RecordInline, self).formfield_for_dbfield(db_field, **kwargs)
class DomainInline(admin.TabularInline): class DomainInline(admin.TabularInline):
# TODO account, and record sumary fields
model = Domain model = Domain
fields = ('domain_link',) fields = ('domain_link',)
readonly_fields = ('domain_link',) readonly_fields = ('domain_link',)
@ -47,6 +50,7 @@ class DomainInline(admin.TabularInline):
class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin): class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin):
# TODO name link
fields = ('name', 'account') fields = ('name', 'account')
list_display = ( list_display = (
'structured_name', 'display_is_top', 'websites', 'account_link' 'structured_name', 'display_is_top', 'websites', 'account_link'

View file

@ -91,7 +91,7 @@ class Bind9MasterDomainBackend(ServiceController):
'zone_path': settings.DOMAINS_ZONE_PATH % {'name': domain.name}, 'zone_path': settings.DOMAINS_ZONE_PATH % {'name': domain.name},
'subdomains': domain.subdomains.all(), 'subdomains': domain.subdomains.all(),
'banner': self.get_banner(), 'banner': self.get_banner(),
'slaves': '; '.join(self.get_slaves(domain)) or '"none"', 'slaves': '; '.join(self.get_slaves(domain)) or '',
} }
context.update({ context.update({
'conf_path': settings.DOMAINS_MASTERS_PATH, 'conf_path': settings.DOMAINS_MASTERS_PATH,
@ -133,7 +133,7 @@ class Bind9SlaveDomainBackend(Bind9MasterDomainBackend):
'name': domain.name, 'name': domain.name,
'banner': self.get_banner(), 'banner': self.get_banner(),
'subdomains': domain.subdomains.all(), 'subdomains': domain.subdomains.all(),
'masters': '; '.join(self.get_masters(domain)) or '"none"', 'masters': '; '.join(self.get_masters(domain)) or '',
} }
context.update({ context.update({
'conf_path': settings.DOMAINS_SLAVES_PATH, 'conf_path': settings.DOMAINS_SLAVES_PATH,

View file

@ -42,6 +42,9 @@ class Domain(models.Model):
# don't cache, don't replace by top_id # don't cache, don't replace by top_id
return not bool(self.top) return not bool(self.top)
def get_absolute_url(self):
return 'http://%s' % self.name
def get_records(self): def get_records(self):
""" proxy method, needed for input validation, see helpers.domain_for_validation """ """ proxy method, needed for input validation, see helpers.domain_for_validation """
return self.records.all() return self.records.all()

View file

@ -180,6 +180,7 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin):
'description') 'description')
}), }),
) )
list_select_related = ('queue', 'owner', 'creator')
class Media: class Media:
css = { css = {
@ -286,11 +287,6 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin):
data_formated = markdown(strip_tags(data)) data_formated = markdown(strip_tags(data))
return HttpResponse(data_formated) return HttpResponse(data_formated)
def get_queryset(self, request):
""" Order by structured name and imporve performance """
qs = super(TicketAdmin, self).get_queryset(request)
return qs.select_related('queue', 'owner', 'creator')
class QueueAdmin(admin.ModelAdmin): class QueueAdmin(admin.ModelAdmin):
list_display = ['name', 'default', 'num_tickets'] list_display = ['name', 'default', 'num_tickets']

View file

@ -44,5 +44,11 @@ class List(models.Model):
def set_password(self, password): def set_password(self, password):
self.password = password self.password = password
def get_absolute_url(self):
context = {
'name': self.name
}
return settings.LISTS_LIST_URL % context
services.register(List) services.register(List)

View file

@ -7,10 +7,11 @@ LISTS_DOMAIN_MODEL = getattr(settings, 'LISTS_DOMAIN_MODEL', 'domains.Domain')
LISTS_DEFAULT_DOMAIN = getattr(settings, 'LIST_DEFAULT_DOMAIN', 'lists.orchestra.lan') LISTS_DEFAULT_DOMAIN = getattr(settings, 'LIST_DEFAULT_DOMAIN', 'lists.orchestra.lan')
LISTS_LIST_URL = getattr(settings, 'LISTS_LIST_URL', 'https://lists.orchestra.lan/mailman/listinfo/%(name)s')
LISTS_MAILMAN_POST_LOG_PATH = getattr(settings, 'LISTS_MAILMAN_POST_LOG_PATH', LISTS_MAILMAN_POST_LOG_PATH = getattr(settings, 'LISTS_MAILMAN_POST_LOG_PATH',
'/var/log/mailman/post') '/var/log/mailman/post')
LISTS_MAILMAN_ROOT_PATH = getattr(settings, 'LISTS_MAILMAN_ROOT_PATH', LISTS_MAILMAN_ROOT_PATH = getattr(settings, 'LISTS_MAILMAN_ROOT_PATH',
'/var/lib/mailman/') '/var/lib/mailman/')

View file

@ -70,11 +70,15 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
display_addresses.allow_tags = True display_addresses.allow_tags = True
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):
""" not collapsed filtering when exists """
fieldsets = super(MailboxAdmin, self).get_fieldsets(request, obj=obj) fieldsets = super(MailboxAdmin, self).get_fieldsets(request, obj=obj)
if obj and obj.filtering == obj.CUSTOM: if obj and obj.filtering == obj.CUSTOM:
# not collapsed filtering when exists
fieldsets = copy.deepcopy(fieldsets) fieldsets = copy.deepcopy(fieldsets)
fieldsets[1][1]['classes'] = fieldsets[0][1]['fields'] + ('open',) fieldsets[1][1]['classes'] = fieldsets[0][1]['fields'] + ('open',)
elif '_to_field' in parse_qs(request.META['QUERY_STRING']):
# remove address from popup
fieldsets = list(copy.deepcopy(fieldsets))
fieldsets.pop(-1)
return fieldsets return fieldsets
def get_form(self, *args, **kwargs): def get_form(self, *args, **kwargs):

View file

@ -14,7 +14,6 @@ from django.core.management.base import CommandError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from selenium.webdriver.support.select import Select from selenium.webdriver.support.select import Select
from orchestra.apps.accounts.models import Account
from orchestra.apps.orchestration.models import Server, Route from orchestra.apps.orchestration.models import Server, Route
from orchestra.apps.resources.models import Resource from orchestra.apps.resources.models import Resource
from orchestra.utils.system import run, sshrun from orchestra.utils.system import run, sshrun
@ -303,9 +302,9 @@ class AdminMailboxMixin(MailboxMixin):
url = self.live_server_url + reverse('admin:mailboxes_mailbox_add') url = self.live_server_url + reverse('admin:mailboxes_mailbox_add')
self.selenium.get(url) self.selenium.get(url)
account_input = self.selenium.find_element_by_id('id_account') # account_input = self.selenium.find_element_by_id('id_account')
account_select = Select(account_input) # account_select = Select(account_input)
account_select.select_by_value(str(self.account.pk)) # account_select.select_by_value(str(self.account.pk))
name_field = self.selenium.find_element_by_id('id_name') name_field = self.selenium.find_element_by_id('id_name')
name_field.send_keys(username) name_field.send_keys(username)

View file

@ -10,7 +10,7 @@ from .models import MiscService, Miscellaneous
class MiscServiceAdmin(admin.ModelAdmin): class MiscServiceAdmin(admin.ModelAdmin):
list_display = ('name', 'num_instances') list_display = ('name', 'verbose_name', 'num_instances')
def num_instances(self, misc): def num_instances(self, misc):
""" return num slivers as a link to slivers changelist view """ """ return num slivers as a link to slivers changelist view """

View file

@ -3,11 +3,16 @@ from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.core import services from orchestra.core import services
from orchestra.core.validators import validate_name
class MiscService(models.Model): class MiscService(models.Model):
name = models.CharField(_("name"), max_length=256) name = models.CharField(_("name"), max_length=32, unique=True, validators=[validate_name],
description = models.TextField(_("description"), blank=True) help_text=_("Raw name used for internal referenciation, i.e. service match definition"))
verbose_name = models.CharField(_("verbose name"), max_length=256, blank=True,
help_text=_("Human readable name"))
description = models.TextField(_("description"), blank=True,
help_text=_("Optional description"))
has_amount = models.BooleanField(_("has amount"), default=False, has_amount = models.BooleanField(_("has amount"), default=False,
help_text=_("Designates whether this service has <tt>amount</tt> " help_text=_("Designates whether this service has <tt>amount</tt> "
"property or not.")) "property or not."))
@ -18,6 +23,12 @@ class MiscService(models.Model):
def __unicode__(self): def __unicode__(self):
return self.name return self.name
def clean(self):
self.verbose_name = self.verbose_name.strip()
def get_verbose_name(self):
return self.verbose_name or self.name
class Miscellaneous(models.Model): class Miscellaneous(models.Model):
service = models.ForeignKey(MiscService, verbose_name=_("service"), service = models.ForeignKey(MiscService, verbose_name=_("service"),

View file

@ -181,7 +181,7 @@ class Order(models.Model):
if metric is not None: if metric is not None:
MetricStorage.store(self, metric) MetricStorage.store(self, metric)
metric = ', metric:{}'.format(metric) metric = ', metric:{}'.format(metric)
description = "{}: {}".format(handler.description, str(instance)) description = handler.get_order_description(instance)
logger.info("UPDATED order id:{id}, description:{description}{metric}".format( logger.info("UPDATED order id:{id}, description:{description}{metric}".format(
id=self.id, description=description, metric=metric)) id=self.id, description=description, metric=metric))
if self.description != description: if self.description != description:

View file

@ -31,8 +31,18 @@ def process_transactions(modeladmin, request, queryset):
if not processes: if not processes:
return return
opts = modeladmin.model._meta opts = modeladmin.model._meta
num = len(queryset)
context = { context = {
'title': _("Huston, be advised"), 'title': ungettext(
_("Selected transaction has been processed."),
_("%s Selected transactions have been processed.") % num,
num),
'content_message': ungettext(
_("The following transaction process has been generated, "
"you may want to save it on your computer now."),
_("The following %s transaction processes have been generated, "
"you may want to save it on your computer now.") % len(processes),
len(processes)),
'action_name': _("Process"), 'action_name': _("Process"),
'processes': processes, 'processes': processes,
'opts': opts, 'opts': opts,

View file

@ -92,6 +92,7 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
filter_by_account_fields = ('bill', 'source') filter_by_account_fields = ('bill', 'source')
change_readonly_fields = ('amount', 'currency') change_readonly_fields = ('amount', 'currency')
readonly_fields = ('bill_link', 'display_state', 'process_link', 'account_link', 'source_link') readonly_fields = ('bill_link', 'display_state', 'process_link', 'account_link', 'source_link')
list_select_related = ('account', 'source', 'bill__account')
bill_link = admin_link('bill') bill_link = admin_link('bill')
source_link = admin_link('source') source_link = admin_link('source')
@ -99,10 +100,6 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
account_link = admin_link('bill__account') account_link = admin_link('bill__account')
display_state = admin_colored('state', colors=STATE_COLORS) display_state = admin_colored('state', colors=STATE_COLORS)
def get_queryset(self, request):
qs = super(TransactionAdmin, self).get_queryset(request)
return qs.select_related('source', 'bill__account')
def get_change_view_actions(self, obj=None): def get_change_view_actions(self, obj=None):
actions = super(TransactionAdmin, self).get_change_view_actions() actions = super(TransactionAdmin, self).get_change_view_actions()
exclude = [] exclude = []

View file

@ -1,8 +1,9 @@
{% extends "admin/orchestra/generic_confirmation.html" %} {% extends "admin/orchestra/generic_confirmation.html" %}
{% load i18n admin_urls utils %} {% load i18n admin_urls utils %}
{% block content %} {% block content %}
<p>The following transaction processes have been generated, you may want to save them on your computer now.</p> <p>{{ content_message }}</p>
<ul> <ul>
{% for proc in processes %} {% for proc in processes %}
<li> <a href="{{ proc.id }}">Process #{{ proc.id }}</a> <li> <a href="{{ proc.id }}">Process #{{ proc.id }}</a>

View file

@ -1,6 +1,7 @@
from django.contrib import admin, messages from django.contrib import admin, messages
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin from orchestra.admin import ExtendedModelAdmin
@ -14,6 +15,8 @@ from .models import Resource, ResourceData, MonitorData
class ResourceAdmin(ExtendedModelAdmin): class ResourceAdmin(ExtendedModelAdmin):
# TODO error after saving: u"Key 'name' not found in 'ResourceForm'"
# prepopulated_fields = {'name': ('verbose_name',)}
list_display = ( list_display = (
'id', 'verbose_name', 'content_type', 'period', 'on_demand', 'id', 'verbose_name', 'content_type', 'period', 'on_demand',
'default_allocation', 'unit', 'disable_trigger', 'crontab', 'default_allocation', 'unit', 'disable_trigger', 'crontab',
@ -21,11 +24,11 @@ class ResourceAdmin(ExtendedModelAdmin):
list_filter = (UsedContentTypeFilter, 'period', 'on_demand', 'disable_trigger') list_filter = (UsedContentTypeFilter, 'period', 'on_demand', 'disable_trigger')
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('name', 'content_type', 'period'), 'fields': ('verbose_name', 'name', 'content_type', 'period'),
}), }),
(_("Configuration"), { (_("Configuration"), {
'fields': ('verbose_name', 'unit', 'scale', 'on_demand', 'fields': ('unit', 'scale', 'on_demand', 'default_allocation', 'disable_trigger',
'default_allocation', 'disable_trigger', 'is_active'), 'is_active'),
}), }),
(_("Monitoring"), { (_("Monitoring"), {
'fields': ('monitors', 'crontab'), 'fields': ('monitors', 'crontab'),
@ -36,10 +39,10 @@ class ResourceAdmin(ExtendedModelAdmin):
def add_view(self, request, **kwargs): def add_view(self, request, **kwargs):
""" Warning user if the node is not fully configured """ """ Warning user if the node is not fully configured """
if request.method == 'POST': if request.method == 'POST':
messages.warning(request, _( messages.warning(request, mark_safe(_(
"Restarting orchestra and celerybeat is required to fully apply changes. " "Restarting orchestra and celerybeat is required to fully apply changes.<br> "
"Remember that new allocated values will be applied when objects are saved." "Remember that new allocated values will be applied when objects are saved."
)) )))
return super(ResourceAdmin, self).add_view(request, **kwargs) return super(ResourceAdmin, self).add_view(request, **kwargs)
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):

View file

@ -8,9 +8,12 @@ from djcelery.models import PeriodicTask, CrontabSchedule
from orchestra.core import validators from orchestra.core import validators
from orchestra.models import queryset, fields from orchestra.models import queryset, fields
from orchestra.utils.paths import get_project_root
from orchestra.utils.system import run
from . import helpers from . import helpers
from .backends import ServiceMonitor from .backends import ServiceMonitor
from .validators import validate_scale
class ResourceQuerySet(models.QuerySet): class ResourceQuerySet(models.QuerySet):
@ -34,16 +37,15 @@ class Resource(models.Model):
_related = set() # keeps track of related models for resource cleanup _related = set() # keeps track of related models for resource cleanup
name = models.CharField(_("name"), max_length=32, name = models.CharField(_("name"), max_length=32,
help_text=_('Required. 32 characters or fewer. Lowercase letters, ' help_text=_("Required. 32 characters or fewer. Lowercase letters, "
'digits and hyphen only.'), "digits and hyphen only."),
validators=[validators.validate_name]) validators=[validators.validate_name])
verbose_name = models.CharField(_("verbose name"), max_length=256) verbose_name = models.CharField(_("verbose name"), max_length=256)
content_type = models.ForeignKey(ContentType, content_type = models.ForeignKey(ContentType,
help_text=_("Model where this resource will be hooked.")) help_text=_("Model where this resource will be hooked."))
period = models.CharField(_("period"), max_length=16, choices=PERIODS, period = models.CharField(_("period"), max_length=16, choices=PERIODS,
default=LAST, default=LAST,
help_text=_("Operation used for aggregating this resource monitored" help_text=_("Operation used for aggregating this resource monitored data."))
"data."))
on_demand = models.BooleanField(_("on demand"), default=False, on_demand = models.BooleanField(_("on demand"), default=False,
help_text=_("If enabled the resource will not be pre-allocated, " help_text=_("If enabled the resource will not be pre-allocated, "
"but allocated under the application demand")) "but allocated under the application demand"))
@ -53,8 +55,8 @@ class Resource(models.Model):
"on demand resource")) "on demand resource"))
unit = models.CharField(_("unit"), max_length=16, unit = models.CharField(_("unit"), max_length=16,
help_text=_("The unit in which this resource is measured. " help_text=_("The unit in which this resource is measured. "
"For example GB, KB or subscribers")) "For example GB, KB or subscribers"))
scale = models.PositiveIntegerField(_("scale"), scale = models.CharField(_("scale"), max_length=32, validators=[validate_scale],
help_text=_("Scale in which this resource monitoring resoults should " help_text=_("Scale in which this resource monitoring resoults should "
"be prorcessed to match with unit. e.g. <tt>10**9</tt>")) "be prorcessed to match with unit. e.g. <tt>10**9</tt>"))
disable_trigger = models.BooleanField(_("disable trigger"), default=False, disable_trigger = models.BooleanField(_("disable trigger"), default=False,
@ -79,6 +81,9 @@ class Resource(models.Model):
def __unicode__(self): def __unicode__(self):
return "{}-{}".format(str(self.content_type), self.name) return "{}-{}".format(str(self.content_type), self.name)
def clean(self):
self.verbose_name = self.verbose_name.strip()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
created = not self.pk created = not self.pk
super(Resource, self).save(*args, **kwargs) super(Resource, self).save(*args, **kwargs)
@ -102,7 +107,7 @@ class Resource(models.Model):
task.save(update_fields=['crontab']) task.save(update_fields=['crontab'])
# This only work on tests (multiprocessing used on real deployments) # This only work on tests (multiprocessing used on real deployments)
apps.get_app_config('resources').reload_relations() apps.get_app_config('resources').reload_relations()
# TODO touch wsgi.py for code reloading? run('touch %s/wsgi.py' % get_project_root())
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
super(Resource, self).delete(*args, **kwargs) super(Resource, self).delete(*args, **kwargs)

View file

@ -0,0 +1,8 @@
from django.core.validators import ValidationError
def validate_scale(value):
try:
int(eval(value))
except ValueError:
raise ValidationError(_("%s value is not a valid scale expression"))

View file

@ -1,7 +1,7 @@
from django.contrib.admin import helpers from django.contrib.admin import helpers
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import transaction from django.db import transaction
from django.shortcuts import render from django.shortcuts import render, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -62,3 +62,21 @@ def view_help(modeladmin, request, queryset):
return TemplateResponse(request, 'admin/services/service/help.html', context) return TemplateResponse(request, 'admin/services/service/help.html', context)
view_help.url_name = 'help' view_help.url_name = 'help'
view_help.verbose_name = _("Help") view_help.verbose_name = _("Help")
def clone(modeladmin, request, queryset):
service = queryset.get()
fields = (
'content_type_id', 'match', 'handler_type', 'is_active', 'ignore_superusers', 'billing_period',
'billing_point', 'is_fee', 'metric', 'nominal_price', 'tax', 'pricing_period',
'rate_algorithm', 'on_cancel', 'payment_style',
)
query = []
for field in fields:
value = getattr(service, field)
field = field.replace('_id', '')
query.append('%s=%s' % (field, value))
opts = service._meta
url = reverse('admin:%s_%s_add' % (opts.app_label, opts.model_name))
url += '?%s' % '&'.join(query)
return redirect(url)

View file

@ -9,7 +9,7 @@ from orchestra.admin.filters import UsedContentTypeFilter
from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.core import services from orchestra.core import services
from .actions import update_orders, view_help from .actions import update_orders, view_help, clone
from .models import Plan, ContractedPlan, Rate, Service from .models import Plan, ContractedPlan, Rate, Service
@ -42,7 +42,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
}), }),
(_("Billing options"), { (_("Billing options"), {
'classes': ('wide',), 'classes': ('wide',),
'fields': ('billing_period', 'billing_point', 'is_fee') 'fields': ('billing_period', 'billing_point', 'is_fee', 'order_description')
}), }),
(_("Pricing options"), { (_("Pricing options"), {
'classes': ('wide',), 'classes': ('wide',),
@ -51,7 +51,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
}), }),
) )
inlines = [RateInline] inlines = [RateInline]
actions = [update_orders] actions = [update_orders, clone]
change_view_actions = actions + [view_help] change_view_actions = actions + [view_help]
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
@ -60,7 +60,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
models = [model._meta.model_name for model in services.get()] models = [model._meta.model_name for model in services.get()]
queryset = db_field.rel.to.objects queryset = db_field.rel.to.objects
kwargs['queryset'] = queryset.filter(model__in=models) kwargs['queryset'] = queryset.filter(model__in=models)
if db_field.name in ['match', 'metric']: if db_field.name in ['match', 'metric', 'order_description']:
kwargs['widget'] = forms.TextInput(attrs={'size':'160'}) kwargs['widget'] = forms.TextInput(attrs={'size':'160'})
return super(ServiceAdmin, self).formfield_for_dbfield(db_field, **kwargs) return super(ServiceAdmin, self).formfield_for_dbfield(db_field, **kwargs)

View file

@ -62,6 +62,16 @@ class ServiceHandler(plugins.Plugin):
} }
return eval(self.metric, safe_locals) return eval(self.metric, safe_locals)
def get_order_description(self, instance):
safe_locals = {
'instance': instance,
'obj': instance,
instance._meta.model_name: instance,
}
if not self.order_description:
return '%s: %s' % (self.description, instance)
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):
not_cachable = self.billing_point == self.FIXED_DATE and options.get('fixed_point') not_cachable = self.billing_point == self.FIXED_DATE and options.get('fixed_point')
if not_cachable or bp is None: if not_cachable or bp is None:

View file

@ -10,6 +10,7 @@ from django.utils.module_loading import autodiscover_modules
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.core import caches, services, accounts from orchestra.core import caches, services, accounts
from orchestra.core.validators import validate_name
from orchestra.models import queryset from orchestra.models import queryset
from . import settings, rating from . import settings, rating
@ -17,7 +18,8 @@ from .handlers import ServiceHandler
class Plan(models.Model): class Plan(models.Model):
name = models.CharField(_("plan"), max_length=128) name = models.CharField(_("name"), max_length=32, unique=True, validators=[validate_name])
verbose_name = models.CharField(_("verbose_name"), max_length=128, blank=True)
is_default = models.BooleanField(_("default"), default=False, is_default = models.BooleanField(_("default"), default=False,
help_text=_("Designates whether this plan is used by default or not.")) help_text=_("Designates whether this plan is used by default or not."))
is_combinable = models.BooleanField(_("combinable"), default=True, is_combinable = models.BooleanField(_("combinable"), default=True,
@ -29,7 +31,10 @@ class Plan(models.Model):
return self.name return self.name
def clean(self): def clean(self):
self.name = self.name.strip() self.verbose_name = self.verbose_name.strip()
def get_verbose_name(self):
return self.verbose_name or self.name
class ContractedPlan(models.Model): class ContractedPlan(models.Model):
@ -147,6 +152,12 @@ class Service(models.Model):
is_fee = models.BooleanField(_("fee"), default=False, is_fee = models.BooleanField(_("fee"), default=False,
help_text=_("Designates whether this service should be billed as " help_text=_("Designates whether this service should be billed as "
" membership fee or not")) " membership fee or not"))
order_description = models.CharField(_("Order description"), max_length=128, blank=True,
help_text=_(
"Python <a href='https://docs.python.org/2/library/functions.html#eval'>expression</a> "
"used for generating the description for the bill lines of this services.<br>"
"Defaults to <tt>'%s: %s' % (handler.description, instance)</tt>"
))
# Pricing # Pricing
metric = models.CharField(_("metric"), max_length=256, blank=True, metric = models.CharField(_("metric"), max_length=256, blank=True,
help_text=_( help_text=_(

View file

@ -34,8 +34,7 @@ class JobBillingTest(BaseBillingTest):
if not account: if not account:
account = self.create_account() account = self.create_account()
description = 'Random Job %s' % random_ascii(10) description = 'Random Job %s' % random_ascii(10)
service, __ = MiscService.objects.get_or_create(name='job', description=description, service, __ = MiscService.objects.get_or_create(name='job', has_amount=True)
has_amount=True)
return account.miscellaneous.create(service=service, description=description, amount=amount) return account.miscellaneous.create(service=service, description=description, amount=amount)
def test_job(self): def test_job(self):

View file

@ -43,7 +43,7 @@ class BaseTrafficBillingTest(BaseBillingTest):
period=Resource.MONTHLY_SUM, period=Resource.MONTHLY_SUM,
verbose_name='Account Traffic', verbose_name='Account Traffic',
unit='GB', unit='GB',
scale=10**9, scale='10**9',
on_demand=True, on_demand=True,
monitors='FTPTraffic', monitors='FTPTraffic',
) )

View file

@ -4,18 +4,20 @@ from django.contrib import admin
from django.contrib.admin.util import unquote from django.contrib.admin.util import unquote
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.utils.translation import ugettext, ugettext_lazy as _ from django.utils.translation import ugettext, ugettext_lazy as _
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.forms import UserCreationForm, UserChangeForm from orchestra.forms import UserCreationForm, UserChangeForm
from .filters import IsMainListFilter
from .models import SystemUser from .models import SystemUser
class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin): class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = ('username', 'account_link', 'shell', 'home', 'is_active', 'is_main') list_display = ('username', 'account_link', 'shell', 'home', 'display_active', 'display_main')
list_filter = ('is_active', 'is_main', 'shell') list_filter = ('is_active', 'shell', IsMainListFilter)
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': ('username', 'password', 'account_link', 'is_active') 'fields': ('username', 'password', 'account_link', 'is_active')
@ -26,7 +28,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
) )
add_fieldsets = ( add_fieldsets = (
(None, { (None, {
'fields': ('username', 'password1', 'password2', 'account') 'fields': ('account_link', 'username', 'password1', 'password2')
}), }),
(_("System"), { (_("System"), {
'fields': ('home', 'shell', 'groups'), 'fields': ('home', 'shell', 'groups'),
@ -41,6 +43,17 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
form = UserChangeForm form = UserChangeForm
ordering = ('-id',) ordering = ('-id',)
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")
display_main.boolean = True
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
""" exclude self reference on groups """ """ exclude self reference on groups """
form = super(SystemUserAdmin, self).get_form(request, obj=obj, **kwargs) form = super(SystemUserAdmin, self).get_form(request, obj=obj, **kwargs)
@ -52,5 +65,9 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
formfield.queryset = formfield.queryset.exclude(id=obj.id) formfield.queryset = formfield.queryset.exclude(id=obj.id)
return form return form
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)
admin.site.register(SystemUser, SystemUserAdmin) admin.site.register(SystemUser, SystemUserAdmin)

View file

@ -1,4 +1,5 @@
from rest_framework import viewsets from django.utils.translation import ugettext_lazy as _
from rest_framework import viewsets, exceptions
from orchestra.api import router, SetPasswordApiMixin from orchestra.api import router, SetPasswordApiMixin
from orchestra.apps.accounts.api import AccountApiMixin from orchestra.apps.accounts.api import AccountApiMixin
@ -12,5 +13,11 @@ class SystemUserViewSet(AccountApiMixin, SetPasswordApiMixin, viewsets.ModelView
serializer_class = SystemUserSerializer serializer_class = SystemUserSerializer
filter_fields = ('username',) filter_fields = ('username',)
def destroy(self, request, pk=None):
user = self.get_object()
if user.is_main:
raise exceptions.PermissionDenied(_("Main system user can not be deleted."))
super(SystemUserViewSet, self).destroy(request, pk=pk)
router.register(r'systemusers', SystemUserViewSet) router.register(r'systemusers', SystemUserViewSet)

View file

@ -0,0 +1,23 @@
from django.contrib.admin import SimpleListFilter
from django.db.models import F
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
class IsMainListFilter(SimpleListFilter):
""" Filter Nodes by group according to request.user """
title = _("main")
parameter_name = 'is_main'
def lookups(self, request, model_admin):
return (
('True', _("Yes")),
('False', _("No")),
)
def queryset(self, request, queryset):
if self.value() == 'True':
return queryset.filter(account__main_systemuser_id=F('id'))
if self.value() == 'False':
return queryset.exclude(account__main_systemuser_id=F('id'))

View file

@ -36,7 +36,7 @@ class SystemUser(models.Model):
groups = models.ManyToManyField('self', blank=True, groups = models.ManyToManyField('self', blank=True,
help_text=_("A new group will be created for the user. " help_text=_("A new group will be created for the user. "
"Which additional groups would you like them to be a member of?")) "Which additional groups would you like them to be a member of?"))
is_main = models.BooleanField(_("is main"), default=False) # is_main = models.BooleanField(_("is main"), default=False)
is_active = models.BooleanField(_("active"), default=True, is_active = models.BooleanField(_("active"), default=True,
help_text=_("Designates whether this account should be treated as active. " help_text=_("Designates whether this account should be treated as active. "
"Unselect this instead of deleting accounts.")) "Unselect this instead of deleting accounts."))
@ -53,6 +53,13 @@ class SystemUser(models.Model):
except type(self).account.field.rel.to.DoesNotExist: except type(self).account.field.rel.to.DoesNotExist:
return self.is_active return self.is_active
@property
def is_main(self):
# On account creation main_systemuser_id is still None
if self.account.main_systemuser_id:
return self.account.main_systemuser_id == self.pk
return self.account.username == self.username
def set_password(self, raw_password): def set_password(self, raw_password):
self.password = make_password(raw_password) self.password = make_password(raw_password)
@ -63,7 +70,7 @@ class SystemUser(models.Model):
} }
basehome = settings.SYSTEMUSERS_HOME % context basehome = settings.SYSTEMUSERS_HOME % context
else: else:
basehome = self.account.systemusers.get(is_main=True).get_home() basehome = self.account.main_systemuser.get_home()
basehome = basehome.replace('/./', '/') basehome = basehome.replace('/./', '/')
home = os.path.join(basehome, self.home) home = os.path.join(basehome, self.home)
# Chrooting # Chrooting

View file

@ -143,6 +143,7 @@ class SystemUserMixin(object):
self.validate_user(username) self.validate_user(username)
self.delete(username) self.delete(username)
self.validate_delete(username) self.validate_delete(username)
self.assertRaises(Exception, self.delete, self.account.username)
def test_add_group(self): def test_add_group(self):
username = '%s_systemuser' % random_ascii(10) username = '%s_systemuser' % random_ascii(10)
@ -190,7 +191,7 @@ class RESTSystemUserMixin(SystemUserMixin):
self.rest_login() self.rest_login()
# create main user # create main user
self.save(self.account.username) self.save(self.account.username)
self.addCleanup(self.delete, self.account.username) self.addCleanup(self.delete_account, self.account.username)
@save_response_on_error @save_response_on_error
def add(self, username, password, shell='/dev/null'): def add(self, username, password, shell='/dev/null'):
@ -230,7 +231,7 @@ class AdminSystemUserMixin(SystemUserMixin):
self.admin_login() self.admin_login()
# create main user # create main user
self.save(self.account.username) self.save(self.account.username)
self.addCleanup(self.delete, self.account.username) self.addCleanup(self.delete_account, self.account.username)
@snapshot_on_error @snapshot_on_error
def add(self, username, password, shell='/dev/null'): def add(self, username, password, shell='/dev/null'):
@ -245,10 +246,6 @@ class AdminSystemUserMixin(SystemUserMixin):
password_field = self.selenium.find_element_by_id('id_password2') password_field = self.selenium.find_element_by_id('id_password2')
password_field.send_keys(password) password_field.send_keys(password)
account_input = self.selenium.find_element_by_id('id_account')
account_select = Select(account_input)
account_select.select_by_value(str(self.account.pk))
shell_input = self.selenium.find_element_by_id('id_shell') shell_input = self.selenium.find_element_by_id('id_shell')
shell_select = Select(shell_input) shell_select = Select(shell_input)
shell_select.select_by_value(shell) shell_select.select_by_value(shell)
@ -261,6 +258,10 @@ class AdminSystemUserMixin(SystemUserMixin):
user = SystemUser.objects.get(username=username) user = SystemUser.objects.get(username=username)
self.admin_delete(user) self.admin_delete(user)
@snapshot_on_error
def delete_account(self, username):
self.admin_delete(self.account)
@snapshot_on_error @snapshot_on_error
def disable(self, username): def disable(self, username):
user = SystemUser.objects.get(username=username) user = SystemUser.objects.get(username=username)
@ -332,7 +333,7 @@ class AdminSystemUserTest(AdminSystemUserMixin, BaseLiveServerTestCase):
@snapshot_on_error @snapshot_on_error
def test_delete_account(self): def test_delete_account(self):
home = self.account.systemusers.get(is_main=True).get_home() home = self.account.main_systemuser.get_home()
delete = reverse('admin:accounts_account_delete', args=(self.account.pk,)) delete = reverse('admin:accounts_account_delete', args=(self.account.pk,))
url = self.live_server_url + delete url = self.live_server_url + delete

View file

@ -46,8 +46,8 @@ class WebAppServiceMixin(object):
def get_context(self, webapp): def get_context(self, webapp):
return { return {
'user': webapp.account.username, 'user': webapp.get_username(),
'group': webapp.account.username, 'group': webapp.get_groupname(),
'app_name': webapp.name, 'app_name': webapp.name,
'type': webapp.type, 'type': webapp.type,
'app_path': webapp.get_path().rstrip('/'), 'app_path': webapp.get_path().rstrip('/'),

View file

@ -53,6 +53,12 @@ class WebApp(models.Model):
} }
return settings.WEBAPPS_BASE_ROOT % context return settings.WEBAPPS_BASE_ROOT % context
def get_username(self):
return self.account.username
def get_groupname(self):
return self.get_username()
class WebAppOption(models.Model): class WebAppOption(models.Model):
webapp = models.ForeignKey(WebApp, verbose_name=_("Web application"), webapp = models.ForeignKey(WebApp, verbose_name=_("Web application"),

View file

@ -13,10 +13,10 @@ class WebsiteOptionInline(admin.TabularInline):
model = WebsiteOption model = WebsiteOption
extra = 1 extra = 1
class Media: # class Media:
css = { # css = {
'all': ('orchestra/css/hide-inline-id.css',) # 'all': ('orchestra/css/hide-inline-id.css',)
} # }
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """ """ Make value input widget bigger """

View file

@ -164,8 +164,8 @@ class Apache2Backend(ServiceController):
'site_name': site.name, 'site_name': site.name,
'ip': settings.WEBSITES_DEFAULT_IP, 'ip': settings.WEBSITES_DEFAULT_IP,
'site_unique_name': site.unique_name, 'site_unique_name': site.unique_name,
'user': site.account.username, 'user': site.get_username(),
'group': site.account.username, 'group': site.get_groupname(),
'sites_enabled': sites_enabled, 'sites_enabled': sites_enabled,
'sites_available': "%s.conf" % os.path.join(sites_available, site.unique_name), 'sites_available': "%s.conf" % os.path.join(sites_available, site.unique_name),
'logs': os.path.join(settings.WEBSITES_BASE_APACHE_LOGS, site.unique_name), 'logs': os.path.join(settings.WEBSITES_BASE_APACHE_LOGS, site.unique_name),

View file

@ -51,6 +51,17 @@ class Website(models.Model):
return 'https' return 'https'
raise TypeError('No protocol for port "%s"' % self.port) raise TypeError('No protocol for port "%s"' % self.port)
def get_absolute_url(self):
domain = self.domains.first()
if domain:
return '%s://%s' % (self.protocol, domain)
def get_username(self):
return self.account.username
def get_groupname(self):
return self.get_username()
class WebsiteOption(models.Model): class WebsiteOption(models.Model):
website = models.ForeignKey(Website, verbose_name=_("web site"), website = models.ForeignKey(Website, verbose_name=_("web site"),
@ -94,5 +105,10 @@ class Content(models.Model):
if not self.path.startswith('/'): if not self.path.startswith('/'):
self.path = '/' + self.path self.path = '/' + self.path
def get_absolute_url(self):
domain = self.website.domains.first()
if domain:
return '%s://%s%s' % (self.website.protocol, domain, self.path)
services.register(Website) services.register(Website)

View file

@ -46,7 +46,7 @@ def read_async(fd):
return '' return ''
def run(command, display=True, error_codes=[0], silent=False, stdin=''): def run(command, display=False, error_codes=[0], silent=False, stdin=''):
""" Subprocess wrapper for running commands """ """ Subprocess wrapper for running commands """
if display: if display:
sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command) sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command)

View file

@ -69,6 +69,8 @@ class BaseTestCase(TestCase, AppDependencyMixin):
class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase): class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
# Avoid problems with the overlaping menu when clicking
settings.ADMIN_TOOLS_MENU = 'admin_tools.menu.Menu'
cls.vdisplay = Xvfb() cls.vdisplay = Xvfb()
cls.vdisplay.start() cls.vdisplay.start()
cls.selenium = WebDriver() cls.selenium = WebDriver()
@ -180,4 +182,3 @@ def save_response_on_error(test):
dumpfile.write(self.rest.last_response.content) dumpfile.write(self.rest.last_response.content)
raise raise
return inner return inner