Random improvements
This commit is contained in:
parent
5619141514
commit
9534e6e571
20
TODO.md
20
TODO.md
|
@ -12,7 +12,6 @@ TODO
|
|||
* enforce an emergency email contact and account to contact contacts about problems when mailserver is down
|
||||
|
||||
* add `BackendLog` retry action
|
||||
* move invoice contact to invoices app?
|
||||
* PHPbBckendMiixin with get_php_ini
|
||||
* Apache: `IncludeOptional /etc/apache2/extra-vhos[t]/account-site-custom.con[f]`
|
||||
* 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)
|
||||
|
||||
|
||||
textwrap.dedent( \\)
|
||||
* textwrap.dedent( \\)
|
||||
|
||||
* accounts
|
||||
* 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)
|
||||
* webapp options cfig agnostic
|
||||
|
||||
* Disable menu on tests, fucking overlapping
|
||||
* service.name / verbose_name instead of .description ?
|
||||
* miscellaneous.name / verbose_name
|
||||
* service.invoice_name
|
||||
|
||||
* 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
|
||||
|
|
|
@ -63,6 +63,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
|
|||
change_form_template = 'admin/accounts/account/change_form.html'
|
||||
actions = [disable]
|
||||
change_view_actions = actions
|
||||
list_select_related = ('billcontact',)
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
""" Make value input widget bigger """
|
||||
|
@ -99,12 +100,6 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
|
|||
fieldsets.insert(1, (_("Related services"), {'fields': fields}))
|
||||
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):
|
||||
super(AccountAdmin, self).save_model(request, obj, form, change)
|
||||
if not change:
|
||||
|
@ -152,6 +147,7 @@ class AccountAdminMixin(object):
|
|||
change_list_template = 'admin/accounts/account/change_list.html'
|
||||
change_form_template = 'admin/accounts/account/change_form.html'
|
||||
account = None
|
||||
list_select_related = ('account',)
|
||||
|
||||
def account_link(self, instance):
|
||||
account = instance.account if instance.pk else self.account
|
||||
|
@ -167,11 +163,6 @@ class AccountAdminMixin(object):
|
|||
self.account = obj.account
|
||||
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):
|
||||
""" Filter by account """
|
||||
formfield = super(AccountAdminMixin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -20,6 +21,12 @@ class AccountViewSet(SetPasswordApiMixin, viewsets.ModelViewSet):
|
|||
def get_queryset(self):
|
||||
qs = super(AccountViewSet, self).get_queryset()
|
||||
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)
|
||||
|
|
|
@ -9,7 +9,12 @@ from .models import Account
|
|||
|
||||
|
||||
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:
|
||||
model = get_model(model)
|
||||
field_name = 'create_%s' % model._meta.model_name
|
||||
|
@ -28,6 +33,9 @@ def create_account_creation_form():
|
|||
except KeyError:
|
||||
# Previous validation error
|
||||
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:
|
||||
model = get_model(model)
|
||||
kwargs = {
|
||||
|
@ -44,9 +52,10 @@ def create_account_creation_form():
|
|||
model = get_model(model)
|
||||
field_name = 'create_%s' % model._meta.model_name
|
||||
if self.cleaned_data[field_name]:
|
||||
for key, value in related_kwargs.iteritems():
|
||||
related_kwargs[key] = eval(value, {'account': account})
|
||||
model.objects.create(account=account, **related_kwargs)
|
||||
kwargs = {
|
||||
key: eval(value, {'account': account}) for key, value in related_kwargs.iteritems()
|
||||
}
|
||||
model.objects.create(account=account, **kwargs)
|
||||
|
||||
fields.update({
|
||||
'create_related_fields': fields.keys(),
|
||||
|
|
|
@ -17,6 +17,8 @@ class Account(auth.AbstractBaseUser):
|
|||
help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."),
|
||||
validators=[validators.RegexValidator(r'^[\w.-]+$',
|
||||
_("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)
|
||||
last_name = models.CharField(_("last name"), max_length=30, blank=True)
|
||||
email = models.EmailField(_('email address'), help_text=_("Used for password recovery"))
|
||||
|
@ -50,14 +52,22 @@ class Account(auth.AbstractBaseUser):
|
|||
def is_staff(self):
|
||||
return self.is_superuser
|
||||
|
||||
@property
|
||||
def main_systemuser(self):
|
||||
return self.systemusers.get(is_main=True)
|
||||
# @property
|
||||
# def main_systemuser(self):
|
||||
# return self.systemusers.get(is_main=True)
|
||||
|
||||
@classmethod
|
||||
def get_main(cls):
|
||||
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):
|
||||
self.first_name = self.first_name.strip()
|
||||
self.last_name = self.last_name.strip()
|
||||
|
@ -124,15 +134,17 @@ class Account(auth.AbstractBaseUser):
|
|||
if self.is_active and self.is_superuser:
|
||||
return True
|
||||
return auth._user_has_module_perms(self, app_label)
|
||||
|
||||
|
||||
def get_related_passwords(self):
|
||||
related = []
|
||||
for model, key, kwargs, __ in settings.ACCOUNTS_CREATE_RELATED:
|
||||
if 'password' not in kwargs:
|
||||
related = [
|
||||
self.main_systemuser,
|
||||
]
|
||||
for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED:
|
||||
if 'password' not in related_kwargs:
|
||||
continue
|
||||
model = get_model(model)
|
||||
kwargs = {
|
||||
key: eval(kwargs[key], {'account': self})
|
||||
key: eval(related_kwargs[key], {'account': self})
|
||||
}
|
||||
try:
|
||||
rel = model.objects.get(account=self, **kwargs)
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
||||
|
@ -26,15 +30,6 @@ ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', 1)
|
|||
|
||||
ACCOUNTS_CREATE_RELATED = getattr(settings, 'ACCOUNTS_CREATE_RELATED', (
|
||||
# <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',
|
||||
'name',
|
||||
{
|
||||
|
|
|
@ -36,7 +36,7 @@ class BillLineInline(admin.TabularInline):
|
|||
if sublines:
|
||||
content = '\n'.join(['%s: %s' % (sub.description, sub.total) for sub in sublines])
|
||||
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
|
||||
display_total.short_description = _("Total")
|
||||
display_total.allow_tags = True
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.contrib import admin
|
|||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
|
||||
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.forms.widgets import paddingCheckboxSelectMultiple
|
||||
|
||||
|
@ -74,15 +74,20 @@ class ContactInline(admin.StackedInline):
|
|||
formset = AtLeastOneRequiredInlineFormSet
|
||||
extra = 0
|
||||
fields = (
|
||||
'short_name', 'full_name', 'email', 'email_usage', ('phone', 'phone2'),
|
||||
'address', ('city', 'zipcode'), 'country',
|
||||
('short_name', 'full_name'), 'email', 'email_usage', ('phone', 'phone2'),
|
||||
)
|
||||
|
||||
def get_extra(self, request, obj=None, **kwargs):
|
||||
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):
|
||||
""" Make value input widget bigger """
|
||||
if db_field.name == 'short_name':
|
||||
kwargs['widget'] = forms.TextInput(attrs={'size':'15'})
|
||||
if db_field.name == 'address':
|
||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||
if db_field.name == 'email_usage':
|
||||
|
|
|
@ -296,10 +296,13 @@ class AdminDatabaseMixin(DatabaseTestMixin):
|
|||
self.selenium.get(url)
|
||||
|
||||
user = DatabaseUser.objects.get(username=username, type=self.db_type)
|
||||
users_input = self.selenium.find_element_by_id('id_users')
|
||||
users_select = Select(users_input)
|
||||
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()
|
||||
|
||||
save = self.selenium.find_element_by_name('_save')
|
||||
save.submit()
|
||||
self.assertNotEqual(url, self.selenium.current_url)
|
||||
|
@ -310,13 +313,23 @@ class AdminDatabaseMixin(DatabaseTestMixin):
|
|||
url = self.live_server_url + change_url(database)
|
||||
self.selenium.get(url)
|
||||
|
||||
# remove user "username"
|
||||
user = DatabaseUser.objects.get(username=username, type=self.db_type)
|
||||
users_input = self.selenium.find_element_by_id('id_users')
|
||||
users_select = Select(users_input)
|
||||
users_select.deselect_by_value(str(user.pk))
|
||||
|
||||
user = DatabaseUser.objects.get(username=username2, type=self.db_type)
|
||||
users_to = self.selenium.find_element_by_id('id_users_to')
|
||||
users_select = Select(users_to)
|
||||
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.submit()
|
||||
|
|
|
@ -20,19 +20,22 @@ class RecordInline(admin.TabularInline):
|
|||
formset = RecordInlineFormSet
|
||||
verbose_name_plural = _("Extra records")
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('orchestra/css/hide-inline-id.css',)
|
||||
}
|
||||
|
||||
# class Media:
|
||||
# css = {
|
||||
# 'all': ('orchestra/css/hide-inline-id.css',)
|
||||
# }
|
||||
#
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
""" Make value input widget bigger """
|
||||
if db_field.name == 'value':
|
||||
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)
|
||||
|
||||
|
||||
class DomainInline(admin.TabularInline):
|
||||
# TODO account, and record sumary fields
|
||||
model = Domain
|
||||
fields = ('domain_link',)
|
||||
readonly_fields = ('domain_link',)
|
||||
|
@ -47,6 +50,7 @@ class DomainInline(admin.TabularInline):
|
|||
|
||||
|
||||
class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin):
|
||||
# TODO name link
|
||||
fields = ('name', 'account')
|
||||
list_display = (
|
||||
'structured_name', 'display_is_top', 'websites', 'account_link'
|
||||
|
|
|
@ -91,7 +91,7 @@ class Bind9MasterDomainBackend(ServiceController):
|
|||
'zone_path': settings.DOMAINS_ZONE_PATH % {'name': domain.name},
|
||||
'subdomains': domain.subdomains.all(),
|
||||
'banner': self.get_banner(),
|
||||
'slaves': '; '.join(self.get_slaves(domain)) or '"none"',
|
||||
'slaves': '; '.join(self.get_slaves(domain)) or '',
|
||||
}
|
||||
context.update({
|
||||
'conf_path': settings.DOMAINS_MASTERS_PATH,
|
||||
|
@ -133,7 +133,7 @@ class Bind9SlaveDomainBackend(Bind9MasterDomainBackend):
|
|||
'name': domain.name,
|
||||
'banner': self.get_banner(),
|
||||
'subdomains': domain.subdomains.all(),
|
||||
'masters': '; '.join(self.get_masters(domain)) or '"none"',
|
||||
'masters': '; '.join(self.get_masters(domain)) or '',
|
||||
}
|
||||
context.update({
|
||||
'conf_path': settings.DOMAINS_SLAVES_PATH,
|
||||
|
|
|
@ -42,6 +42,9 @@ class Domain(models.Model):
|
|||
# don't cache, don't replace by top_id
|
||||
return not bool(self.top)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return 'http://%s' % self.name
|
||||
|
||||
def get_records(self):
|
||||
""" proxy method, needed for input validation, see helpers.domain_for_validation """
|
||||
return self.records.all()
|
||||
|
|
|
@ -180,6 +180,7 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin):
|
|||
'description')
|
||||
}),
|
||||
)
|
||||
list_select_related = ('queue', 'owner', 'creator')
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
|
@ -285,11 +286,6 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin):
|
|||
data = request.POST.get("data")
|
||||
data_formated = markdown(strip_tags(data))
|
||||
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):
|
||||
|
|
|
@ -43,6 +43,12 @@ class List(models.Model):
|
|||
|
||||
def set_password(self, password):
|
||||
self.password = password
|
||||
|
||||
def get_absolute_url(self):
|
||||
context = {
|
||||
'name': self.name
|
||||
}
|
||||
return settings.LISTS_LIST_URL % context
|
||||
|
||||
|
||||
services.register(List)
|
||||
|
|
|
@ -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_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',
|
||||
'/var/log/mailman/post')
|
||||
|
||||
|
||||
LISTS_MAILMAN_ROOT_PATH = getattr(settings, 'LISTS_MAILMAN_ROOT_PATH',
|
||||
'/var/lib/mailman/')
|
||||
|
||||
|
|
|
@ -70,11 +70,15 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
|||
display_addresses.allow_tags = True
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
""" not collapsed filtering when exists """
|
||||
fieldsets = super(MailboxAdmin, self).get_fieldsets(request, obj=obj)
|
||||
if obj and obj.filtering == obj.CUSTOM:
|
||||
# not collapsed filtering when exists
|
||||
fieldsets = copy.deepcopy(fieldsets)
|
||||
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
|
||||
|
||||
def get_form(self, *args, **kwargs):
|
||||
|
|
|
@ -14,7 +14,6 @@ from django.core.management.base import CommandError
|
|||
from django.core.urlresolvers import reverse
|
||||
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.resources.models import Resource
|
||||
from orchestra.utils.system import run, sshrun
|
||||
|
@ -303,9 +302,9 @@ class AdminMailboxMixin(MailboxMixin):
|
|||
url = self.live_server_url + reverse('admin:mailboxes_mailbox_add')
|
||||
self.selenium.get(url)
|
||||
|
||||
account_input = self.selenium.find_element_by_id('id_account')
|
||||
account_select = Select(account_input)
|
||||
account_select.select_by_value(str(self.account.pk))
|
||||
# account_input = self.selenium.find_element_by_id('id_account')
|
||||
# account_select = Select(account_input)
|
||||
# account_select.select_by_value(str(self.account.pk))
|
||||
|
||||
name_field = self.selenium.find_element_by_id('id_name')
|
||||
name_field.send_keys(username)
|
||||
|
|
|
@ -10,7 +10,7 @@ from .models import MiscService, Miscellaneous
|
|||
|
||||
|
||||
class MiscServiceAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'num_instances')
|
||||
list_display = ('name', 'verbose_name', 'num_instances')
|
||||
|
||||
def num_instances(self, misc):
|
||||
""" return num slivers as a link to slivers changelist view """
|
||||
|
|
|
@ -3,11 +3,16 @@ from django.utils.functional import cached_property
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import services
|
||||
from orchestra.core.validators import validate_name
|
||||
|
||||
|
||||
class MiscService(models.Model):
|
||||
name = models.CharField(_("name"), max_length=256)
|
||||
description = models.TextField(_("description"), blank=True)
|
||||
name = models.CharField(_("name"), max_length=32, unique=True, validators=[validate_name],
|
||||
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,
|
||||
help_text=_("Designates whether this service has <tt>amount</tt> "
|
||||
"property or not."))
|
||||
|
@ -17,6 +22,12 @@ class MiscService(models.Model):
|
|||
|
||||
def __unicode__(self):
|
||||
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):
|
||||
|
|
|
@ -181,7 +181,7 @@ class Order(models.Model):
|
|||
if metric is not None:
|
||||
MetricStorage.store(self, 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(
|
||||
id=self.id, description=description, metric=metric))
|
||||
if self.description != description:
|
||||
|
|
|
@ -31,8 +31,18 @@ def process_transactions(modeladmin, request, queryset):
|
|||
if not processes:
|
||||
return
|
||||
opts = modeladmin.model._meta
|
||||
num = len(queryset)
|
||||
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"),
|
||||
'processes': processes,
|
||||
'opts': opts,
|
||||
|
|
|
@ -92,6 +92,7 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
filter_by_account_fields = ('bill', 'source')
|
||||
change_readonly_fields = ('amount', 'currency')
|
||||
readonly_fields = ('bill_link', 'display_state', 'process_link', 'account_link', 'source_link')
|
||||
list_select_related = ('account', 'source', 'bill__account')
|
||||
|
||||
bill_link = admin_link('bill')
|
||||
source_link = admin_link('source')
|
||||
|
@ -99,10 +100,6 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
account_link = admin_link('bill__account')
|
||||
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):
|
||||
actions = super(TransactionAdmin, self).get_change_view_actions()
|
||||
exclude = []
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
{% extends "admin/orchestra/generic_confirmation.html" %}
|
||||
{% load i18n admin_urls utils %}
|
||||
|
||||
|
||||
{% 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>
|
||||
{% for proc in processes %}
|
||||
<li> <a href="{{ proc.id }}">Process #{{ proc.id }}</a>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.contrib import admin, messages
|
||||
from django.contrib.contenttypes import generic
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
|
@ -14,6 +15,8 @@ from .models import Resource, ResourceData, MonitorData
|
|||
|
||||
|
||||
class ResourceAdmin(ExtendedModelAdmin):
|
||||
# TODO error after saving: u"Key 'name' not found in 'ResourceForm'"
|
||||
# prepopulated_fields = {'name': ('verbose_name',)}
|
||||
list_display = (
|
||||
'id', 'verbose_name', 'content_type', 'period', 'on_demand',
|
||||
'default_allocation', 'unit', 'disable_trigger', 'crontab',
|
||||
|
@ -21,11 +24,11 @@ class ResourceAdmin(ExtendedModelAdmin):
|
|||
list_filter = (UsedContentTypeFilter, 'period', 'on_demand', 'disable_trigger')
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('name', 'content_type', 'period'),
|
||||
'fields': ('verbose_name', 'name', 'content_type', 'period'),
|
||||
}),
|
||||
(_("Configuration"), {
|
||||
'fields': ('verbose_name', 'unit', 'scale', 'on_demand',
|
||||
'default_allocation', 'disable_trigger', 'is_active'),
|
||||
'fields': ('unit', 'scale', 'on_demand', 'default_allocation', 'disable_trigger',
|
||||
'is_active'),
|
||||
}),
|
||||
(_("Monitoring"), {
|
||||
'fields': ('monitors', 'crontab'),
|
||||
|
@ -36,10 +39,10 @@ class ResourceAdmin(ExtendedModelAdmin):
|
|||
def add_view(self, request, **kwargs):
|
||||
""" Warning user if the node is not fully configured """
|
||||
if request.method == 'POST':
|
||||
messages.warning(request, _(
|
||||
"Restarting orchestra and celerybeat is required to fully apply changes. "
|
||||
messages.warning(request, mark_safe(_(
|
||||
"Restarting orchestra and celerybeat is required to fully apply changes.<br> "
|
||||
"Remember that new allocated values will be applied when objects are saved."
|
||||
))
|
||||
)))
|
||||
return super(ResourceAdmin, self).add_view(request, **kwargs)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
|
|
|
@ -8,9 +8,12 @@ from djcelery.models import PeriodicTask, CrontabSchedule
|
|||
|
||||
from orchestra.core import validators
|
||||
from orchestra.models import queryset, fields
|
||||
from orchestra.utils.paths import get_project_root
|
||||
from orchestra.utils.system import run
|
||||
|
||||
from . import helpers
|
||||
from .backends import ServiceMonitor
|
||||
from .validators import validate_scale
|
||||
|
||||
|
||||
class ResourceQuerySet(models.QuerySet):
|
||||
|
@ -34,16 +37,15 @@ class Resource(models.Model):
|
|||
_related = set() # keeps track of related models for resource cleanup
|
||||
|
||||
name = models.CharField(_("name"), max_length=32,
|
||||
help_text=_('Required. 32 characters or fewer. Lowercase letters, '
|
||||
'digits and hyphen only.'),
|
||||
help_text=_("Required. 32 characters or fewer. Lowercase letters, "
|
||||
"digits and hyphen only."),
|
||||
validators=[validators.validate_name])
|
||||
verbose_name = models.CharField(_("verbose name"), max_length=256)
|
||||
content_type = models.ForeignKey(ContentType,
|
||||
help_text=_("Model where this resource will be hooked."))
|
||||
period = models.CharField(_("period"), max_length=16, choices=PERIODS,
|
||||
default=LAST,
|
||||
help_text=_("Operation used for aggregating this resource monitored"
|
||||
"data."))
|
||||
help_text=_("Operation used for aggregating this resource monitored data."))
|
||||
on_demand = models.BooleanField(_("on demand"), default=False,
|
||||
help_text=_("If enabled the resource will not be pre-allocated, "
|
||||
"but allocated under the application demand"))
|
||||
|
@ -53,8 +55,8 @@ class Resource(models.Model):
|
|||
"on demand resource"))
|
||||
unit = models.CharField(_("unit"), max_length=16,
|
||||
help_text=_("The unit in which this resource is measured. "
|
||||
"For example GB, KB or subscribers"))
|
||||
scale = models.PositiveIntegerField(_("scale"),
|
||||
"For example GB, KB or subscribers"))
|
||||
scale = models.CharField(_("scale"), max_length=32, validators=[validate_scale],
|
||||
help_text=_("Scale in which this resource monitoring resoults should "
|
||||
"be prorcessed to match with unit. e.g. <tt>10**9</tt>"))
|
||||
disable_trigger = models.BooleanField(_("disable trigger"), default=False,
|
||||
|
@ -79,6 +81,9 @@ class Resource(models.Model):
|
|||
def __unicode__(self):
|
||||
return "{}-{}".format(str(self.content_type), self.name)
|
||||
|
||||
def clean(self):
|
||||
self.verbose_name = self.verbose_name.strip()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
created = not self.pk
|
||||
super(Resource, self).save(*args, **kwargs)
|
||||
|
@ -102,7 +107,7 @@ class Resource(models.Model):
|
|||
task.save(update_fields=['crontab'])
|
||||
# This only work on tests (multiprocessing used on real deployments)
|
||||
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):
|
||||
super(Resource, self).delete(*args, **kwargs)
|
||||
|
|
8
orchestra/apps/resources/validators.py
Normal file
8
orchestra/apps/resources/validators.py
Normal 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"))
|
|
@ -1,7 +1,7 @@
|
|||
from django.contrib.admin import helpers
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import render, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.safestring import mark_safe
|
||||
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)
|
||||
view_help.url_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)
|
||||
|
|
|
@ -9,7 +9,7 @@ from orchestra.admin.filters import UsedContentTypeFilter
|
|||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||
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
|
||||
|
||||
|
||||
|
@ -42,7 +42,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
|||
}),
|
||||
(_("Billing options"), {
|
||||
'classes': ('wide',),
|
||||
'fields': ('billing_period', 'billing_point', 'is_fee')
|
||||
'fields': ('billing_period', 'billing_point', 'is_fee', 'order_description')
|
||||
}),
|
||||
(_("Pricing options"), {
|
||||
'classes': ('wide',),
|
||||
|
@ -51,7 +51,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
|||
}),
|
||||
)
|
||||
inlines = [RateInline]
|
||||
actions = [update_orders]
|
||||
actions = [update_orders, clone]
|
||||
change_view_actions = actions + [view_help]
|
||||
|
||||
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()]
|
||||
queryset = db_field.rel.to.objects
|
||||
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'})
|
||||
return super(ServiceAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||
|
||||
|
|
|
@ -62,6 +62,16 @@ class ServiceHandler(plugins.Plugin):
|
|||
}
|
||||
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):
|
||||
not_cachable = self.billing_point == self.FIXED_DATE and options.get('fixed_point')
|
||||
if not_cachable or bp is None:
|
||||
|
|
|
@ -10,6 +10,7 @@ from django.utils.module_loading import autodiscover_modules
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.core import caches, services, accounts
|
||||
from orchestra.core.validators import validate_name
|
||||
from orchestra.models import queryset
|
||||
|
||||
from . import settings, rating
|
||||
|
@ -17,7 +18,8 @@ from .handlers import ServiceHandler
|
|||
|
||||
|
||||
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,
|
||||
help_text=_("Designates whether this plan is used by default or not."))
|
||||
is_combinable = models.BooleanField(_("combinable"), default=True,
|
||||
|
@ -29,7 +31,10 @@ class Plan(models.Model):
|
|||
return self.name
|
||||
|
||||
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):
|
||||
|
@ -147,6 +152,12 @@ class Service(models.Model):
|
|||
is_fee = models.BooleanField(_("fee"), default=False,
|
||||
help_text=_("Designates whether this service should be billed as "
|
||||
" 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
|
||||
metric = models.CharField(_("metric"), max_length=256, blank=True,
|
||||
help_text=_(
|
||||
|
|
|
@ -34,8 +34,7 @@ class JobBillingTest(BaseBillingTest):
|
|||
if not account:
|
||||
account = self.create_account()
|
||||
description = 'Random Job %s' % random_ascii(10)
|
||||
service, __ = MiscService.objects.get_or_create(name='job', description=description,
|
||||
has_amount=True)
|
||||
service, __ = MiscService.objects.get_or_create(name='job', has_amount=True)
|
||||
return account.miscellaneous.create(service=service, description=description, amount=amount)
|
||||
|
||||
def test_job(self):
|
||||
|
|
|
@ -43,7 +43,7 @@ class BaseTrafficBillingTest(BaseBillingTest):
|
|||
period=Resource.MONTHLY_SUM,
|
||||
verbose_name='Account Traffic',
|
||||
unit='GB',
|
||||
scale=10**9,
|
||||
scale='10**9',
|
||||
on_demand=True,
|
||||
monitors='FTPTraffic',
|
||||
)
|
||||
|
|
|
@ -4,18 +4,20 @@ from django.contrib import admin
|
|||
from django.contrib.admin.util import unquote
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
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.utils import wrap_admin_view
|
||||
from orchestra.apps.accounts.admin import SelectAccountAdminMixin
|
||||
from orchestra.forms import UserCreationForm, UserChangeForm
|
||||
|
||||
from .filters import IsMainListFilter
|
||||
from .models import SystemUser
|
||||
|
||||
|
||||
class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||
list_display = ('username', 'account_link', 'shell', 'home', 'is_active', 'is_main')
|
||||
list_filter = ('is_active', 'is_main', 'shell')
|
||||
list_display = ('username', 'account_link', 'shell', 'home', 'display_active', 'display_main')
|
||||
list_filter = ('is_active', 'shell', IsMainListFilter)
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('username', 'password', 'account_link', 'is_active')
|
||||
|
@ -26,7 +28,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
|
|||
)
|
||||
add_fieldsets = (
|
||||
(None, {
|
||||
'fields': ('username', 'password1', 'password2', 'account')
|
||||
'fields': ('account_link', 'username', 'password1', 'password2')
|
||||
}),
|
||||
(_("System"), {
|
||||
'fields': ('home', 'shell', 'groups'),
|
||||
|
@ -41,6 +43,17 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
|
|||
form = UserChangeForm
|
||||
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):
|
||||
""" exclude self reference on groups """
|
||||
form = super(SystemUserAdmin, self).get_form(request, obj=obj, **kwargs)
|
||||
|
@ -51,6 +64,10 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
|
|||
formfield = form.base_fields['groups']
|
||||
formfield.queryset = formfield.queryset.exclude(id=obj.id)
|
||||
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)
|
||||
|
|
|
@ -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.apps.accounts.api import AccountApiMixin
|
||||
|
@ -11,6 +12,12 @@ class SystemUserViewSet(AccountApiMixin, SetPasswordApiMixin, viewsets.ModelView
|
|||
model = SystemUser
|
||||
serializer_class = SystemUserSerializer
|
||||
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)
|
||||
|
|
23
orchestra/apps/systemusers/filters.py
Normal file
23
orchestra/apps/systemusers/filters.py
Normal 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'))
|
||||
|
|
@ -36,7 +36,7 @@ class SystemUser(models.Model):
|
|||
groups = models.ManyToManyField('self', blank=True,
|
||||
help_text=_("A new group will be created for the user. "
|
||||
"Which additional groups would you like them to be a member of?"))
|
||||
is_main = models.BooleanField(_("is main"), default=False)
|
||||
# is_main = models.BooleanField(_("is main"), default=False)
|
||||
is_active = models.BooleanField(_("active"), default=True,
|
||||
help_text=_("Designates whether this account should be treated as active. "
|
||||
"Unselect this instead of deleting accounts."))
|
||||
|
@ -53,6 +53,13 @@ class SystemUser(models.Model):
|
|||
except type(self).account.field.rel.to.DoesNotExist:
|
||||
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):
|
||||
self.password = make_password(raw_password)
|
||||
|
||||
|
@ -63,7 +70,7 @@ class SystemUser(models.Model):
|
|||
}
|
||||
basehome = settings.SYSTEMUSERS_HOME % context
|
||||
else:
|
||||
basehome = self.account.systemusers.get(is_main=True).get_home()
|
||||
basehome = self.account.main_systemuser.get_home()
|
||||
basehome = basehome.replace('/./', '/')
|
||||
home = os.path.join(basehome, self.home)
|
||||
# Chrooting
|
||||
|
|
|
@ -143,6 +143,7 @@ class SystemUserMixin(object):
|
|||
self.validate_user(username)
|
||||
self.delete(username)
|
||||
self.validate_delete(username)
|
||||
self.assertRaises(Exception, self.delete, self.account.username)
|
||||
|
||||
def test_add_group(self):
|
||||
username = '%s_systemuser' % random_ascii(10)
|
||||
|
@ -190,7 +191,7 @@ class RESTSystemUserMixin(SystemUserMixin):
|
|||
self.rest_login()
|
||||
# create main user
|
||||
self.save(self.account.username)
|
||||
self.addCleanup(self.delete, self.account.username)
|
||||
self.addCleanup(self.delete_account, self.account.username)
|
||||
|
||||
@save_response_on_error
|
||||
def add(self, username, password, shell='/dev/null'):
|
||||
|
@ -230,7 +231,7 @@ class AdminSystemUserMixin(SystemUserMixin):
|
|||
self.admin_login()
|
||||
# create main user
|
||||
self.save(self.account.username)
|
||||
self.addCleanup(self.delete, self.account.username)
|
||||
self.addCleanup(self.delete_account, self.account.username)
|
||||
|
||||
@snapshot_on_error
|
||||
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.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_select = Select(shell_input)
|
||||
shell_select.select_by_value(shell)
|
||||
|
@ -261,6 +258,10 @@ class AdminSystemUserMixin(SystemUserMixin):
|
|||
user = SystemUser.objects.get(username=username)
|
||||
self.admin_delete(user)
|
||||
|
||||
@snapshot_on_error
|
||||
def delete_account(self, username):
|
||||
self.admin_delete(self.account)
|
||||
|
||||
@snapshot_on_error
|
||||
def disable(self, username):
|
||||
user = SystemUser.objects.get(username=username)
|
||||
|
@ -332,7 +333,7 @@ class AdminSystemUserTest(AdminSystemUserMixin, BaseLiveServerTestCase):
|
|||
|
||||
@snapshot_on_error
|
||||
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,))
|
||||
url = self.live_server_url + delete
|
||||
|
|
|
@ -46,8 +46,8 @@ class WebAppServiceMixin(object):
|
|||
|
||||
def get_context(self, webapp):
|
||||
return {
|
||||
'user': webapp.account.username,
|
||||
'group': webapp.account.username,
|
||||
'user': webapp.get_username(),
|
||||
'group': webapp.get_groupname(),
|
||||
'app_name': webapp.name,
|
||||
'type': webapp.type,
|
||||
'app_path': webapp.get_path().rstrip('/'),
|
||||
|
|
|
@ -52,6 +52,12 @@ class WebApp(models.Model):
|
|||
'app_name': self.name,
|
||||
}
|
||||
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):
|
||||
|
|
|
@ -13,10 +13,10 @@ class WebsiteOptionInline(admin.TabularInline):
|
|||
model = WebsiteOption
|
||||
extra = 1
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('orchestra/css/hide-inline-id.css',)
|
||||
}
|
||||
# class Media:
|
||||
# css = {
|
||||
# 'all': ('orchestra/css/hide-inline-id.css',)
|
||||
# }
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
""" Make value input widget bigger """
|
||||
|
|
|
@ -164,8 +164,8 @@ class Apache2Backend(ServiceController):
|
|||
'site_name': site.name,
|
||||
'ip': settings.WEBSITES_DEFAULT_IP,
|
||||
'site_unique_name': site.unique_name,
|
||||
'user': site.account.username,
|
||||
'group': site.account.username,
|
||||
'user': site.get_username(),
|
||||
'group': site.get_groupname(),
|
||||
'sites_enabled': sites_enabled,
|
||||
'sites_available': "%s.conf" % os.path.join(sites_available, site.unique_name),
|
||||
'logs': os.path.join(settings.WEBSITES_BASE_APACHE_LOGS, site.unique_name),
|
||||
|
|
|
@ -50,6 +50,17 @@ class Website(models.Model):
|
|||
if self.port == 443:
|
||||
return 'https'
|
||||
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):
|
||||
|
@ -93,6 +104,11 @@ class Content(models.Model):
|
|||
def clean(self):
|
||||
if not self.path.startswith('/'):
|
||||
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)
|
||||
|
|
|
@ -46,7 +46,7 @@ def read_async(fd):
|
|||
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 """
|
||||
if display:
|
||||
sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command)
|
||||
|
|
|
@ -69,6 +69,8 @@ class BaseTestCase(TestCase, AppDependencyMixin):
|
|||
class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Avoid problems with the overlaping menu when clicking
|
||||
settings.ADMIN_TOOLS_MENU = 'admin_tools.menu.Menu'
|
||||
cls.vdisplay = Xvfb()
|
||||
cls.vdisplay.start()
|
||||
cls.selenium = WebDriver()
|
||||
|
@ -180,4 +182,3 @@ def save_response_on_error(test):
|
|||
dumpfile.write(self.rest.last_response.content)
|
||||
raise
|
||||
return inner
|
||||
|
||||
|
|
Loading…
Reference in a new issue