Dont delete lists when deleting domains

This commit is contained in:
Marc Aymerich 2016-04-15 09:56:10 +00:00
parent 27aec2e5f0
commit 682a8947d3
17 changed files with 89 additions and 61 deletions

View File

@ -443,21 +443,12 @@ mkhomedir_helper or create ssh homes with bash.rc and such
# Reversion # Reversion
# Disable/enable SaaS and VPS # Disable/enable SaaS and VPS
# AGO
# Don't show lines with size 0? # Don't show lines with size 0?
# pending orders with recharge do not show up # pending orders with recharge do not show up
# Traffic of disabled accounts doesn't get disabled # Traffic of disabled accounts doesn't get disabled
# is_active list filter account dissabled filtering support
# URL encode "Order description" on clone # URL encode "Order description" on clone
# Service CLONE METRIC doesn't work # Service CLONE METRIC doesn't work
# Show warning when saving order and metricstorage date is inconistent with registered date! # Show warning when saving order and metricstorage date is inconistent with registered date!
# Warn user if changes are not saved
# exclude from change list action, support for multiple exclusion # exclude from change list action, support for multiple exclusion
# support for better edditing bill lines and sublines

View File

@ -92,7 +92,10 @@ def action_to_view(action, modeladmin):
def change_url(obj): def change_url(obj):
if obj is not None: if obj is not None:
cls = type(obj)
opts = obj._meta opts = obj._meta
if cls._deferred:
opts = cls.__base__._meta
view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name) view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name)
return reverse(view_name, args=(obj.pk,)) return reverse(view_name, args=(obj.pk,))
raise NoReverseMatch raise NoReverseMatch

View File

@ -12,6 +12,8 @@ class HasMainUserListFilter(SimpleListFilter):
return ( return (
('True', _("Yes")), ('True', _("Yes")),
('False', _("No")), ('False', _("No")),
('account', _("Account disabled")),
('object', _("Object disabled")),
) )
def queryset(self, request, queryset): def queryset(self, request, queryset):
@ -30,4 +32,8 @@ class IsActiveListFilter(HasMainUserListFilter):
return queryset.filter(is_active=True, account__is_active=True) return queryset.filter(is_active=True, account__is_active=True)
elif self.value() == 'False': elif self.value() == 'False':
return queryset.filter(Q(is_active=False) | Q(account__is_active=False)) return queryset.filter(Q(is_active=False) | Q(account__is_active=False))
elif self.value() == 'account':
return queryset.filter(account__is_active=False)
elif self.value() == 'object':
return queryset.filter(is_active=False)
return queryset return queryset

View File

@ -289,7 +289,9 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
'closed_on_display', 'updated_on_display', 'display_total_with_subtotals', 'closed_on_display', 'updated_on_display', 'display_total_with_subtotals',
) )
inlines = [BillLineInline, ClosedBillLineInline] inlines = [BillLineInline, ClosedBillLineInline]
#date_hierarchy = 'closed_on' date_hierarchy = 'closed_on'
# TODO when merged https://github.com/django/django/pull/5213
#approximate_date_hierarchy = admin.ApproximateWith.MONTHS
created_on_display = admin_date('created_on', short_description=_("Created")) created_on_display = admin_date('created_on', short_description=_("Created"))
closed_on_display = admin_date('closed_on', short_description=_("Closed")) closed_on_display = admin_date('closed_on', short_description=_("Closed"))
@ -426,7 +428,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
qs = qs.prefetch_related( qs = qs.prefetch_related(
Prefetch('amends', queryset=Bill.objects.filter(is_open=False), to_attr='closed_amends') Prefetch('amends', queryset=Bill.objects.filter(is_open=False), to_attr='closed_amends')
) )
return qs return qs.defer('html')
def change_view(self, request, object_id, **kwargs): def change_view(self, request, object_id, **kwargs):
# TODO raise404, here and everywhere # TODO raise404, here and everywhere

View File

@ -123,6 +123,8 @@ class Bill(models.Model):
@classmethod @classmethod
def get_class_type(cls): def get_class_type(cls):
if cls._deferred:
cls = cls.__base__
return cls.__name__.upper() return cls.__name__.upper()
@cached_property @cached_property
@ -212,6 +214,10 @@ class Bill(models.Model):
def get_type(self): def get_type(self):
return self.type or self.get_class_type() return self.type or self.get_class_type()
@property
def is_amend(self):
return self.type in self.AMEND_MAP.values()
def get_amend_type(self): def get_amend_type(self):
amend_type = self.AMEND_MAP.get(self.type) amend_type = self.AMEND_MAP.get(self.type)
if amend_type is None: if amend_type is None:
@ -220,6 +226,8 @@ class Bill(models.Model):
def get_number(self): def get_number(self):
cls = type(self) cls = type(self)
if cls._deferred:
cls = cls.__base__
bill_type = self.get_type() bill_type = self.get_type()
if bill_type == self.BILL: if bill_type == self.BILL:
raise TypeError('This method can not be used on BILL instances') raise TypeError('This method can not be used on BILL instances')

View File

@ -39,7 +39,7 @@
<table id="summary"> <table id="summary">
<tr class="header"> <tr class="header">
<th class="title column-name">{% trans "Services" %}</th> <th class="title column-name">{% trans "Service" %}</th>
<th class="title column-active">{% trans "Active" %}</th> <th class="title column-active">{% trans "Active" %}</th>
<th class="title column-cancelled">{% trans "Cancelled" %}</th> <th class="title column-cancelled">{% trans "Cancelled" %}</th>
<th class="title column-nominal-price">{% trans "Nominal price" %}</th> <th class="title column-nominal-price">{% trans "Nominal price" %}</th>

View File

@ -9,10 +9,10 @@ from orchestra.admin.utils import admin_link
from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.actions import list_accounts
from orchestra.contrib.accounts.admin import SelectAccountAdminMixin from orchestra.contrib.accounts.admin import SelectAccountAdminMixin
from orchestra.contrib.accounts.filters import IsActiveListFilter from orchestra.contrib.accounts.filters import IsActiveListFilter
from orchestra.forms import UserCreationForm, NonStoredUserChangeForm
from . import settings from . import settings
from .filters import HasCustomAddressListFilter from .filters import HasCustomAddressListFilter
from .forms import ListCreationForm, ListChangeForm
from .models import List from .models import List
@ -54,8 +54,8 @@ class ListAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModel
list_filter = (IsActiveListFilter, HasCustomAddressListFilter) list_filter = (IsActiveListFilter, HasCustomAddressListFilter)
readonly_fields = ('account_link',) readonly_fields = ('account_link',)
change_readonly_fields = ('name',) change_readonly_fields = ('name',)
form = ListChangeForm form = NonStoredUserChangeForm
add_form = ListCreationForm add_form = UserCreationForm
list_select_related = ('account', 'address_domain',) list_select_related = ('account', 'address_domain',)
filter_by_account_fields = ['address_domain'] filter_by_account_fields = ['address_domain']
actions = (disable, enable, list_accounts) actions = (disable, enable, list_accounts)

View File

@ -10,3 +10,4 @@ class ListsConfig(AppConfig):
def ready(self): def ready(self):
from .models import List from .models import List
services.register(List, icon='email-alter.png') services.register(List, icon='email-alter.png')
from . import signals

View File

@ -1,21 +0,0 @@
from django.utils.translation import ugettext_lazy as _
from orchestra.forms import UserCreationForm, NonStoredUserChangeForm
class CleanAddressMixin(object):
def clean_address_domain(self):
name = self.cleaned_data.get('address_name')
domain = self.cleaned_data.get('address_domain')
if name and not domain:
msg = _("Domain should be selected for provided address name")
raise forms.ValidationError(msg)
return domain
class ListCreationForm(CleanAddressMixin, UserCreationForm):
pass
class ListChangeForm(CleanAddressMixin, NonStoredUserChangeForm):
pass

View File

@ -1,3 +1,4 @@
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -25,7 +26,7 @@ class List(models.Model):
help_text=_("Default list address &lt;name&gt;@%s") % settings.LISTS_DEFAULT_DOMAIN) help_text=_("Default list address &lt;name&gt;@%s") % settings.LISTS_DEFAULT_DOMAIN)
address_name = models.CharField(_("address name"), max_length=128, address_name = models.CharField(_("address name"), max_length=128,
validators=[validate_name], blank=True) validators=[validate_name], blank=True)
address_domain = models.ForeignKey(settings.LISTS_DOMAIN_MODEL, address_domain = models.ForeignKey(settings.LISTS_DOMAIN_MODEL, on_delete=models.SET_NULL,
verbose_name=_("address domain"), blank=True, null=True) verbose_name=_("address domain"), blank=True, null=True)
admin_email = models.EmailField(_("admin email"), admin_email = models.EmailField(_("admin email"),
help_text=_("Administration email address")) help_text=_("Administration email address"))
@ -55,6 +56,12 @@ class List(models.Model):
def active(self): def active(self):
return self.is_active and self.account.is_active return self.is_active and self.account.is_active
def clean(self):
if self.address_name and not self.address_domain_id:
raise ValidationError({
'address_domain': _("Domain should be selected for provided address name."),
})
def disable(self): def disable(self):
self.is_active = False self.is_active = False
self.save(update_fields=('is_active',)) self.save(update_fields=('is_active',))

View File

@ -0,0 +1,19 @@
from django.apps import apps
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from . import settings
from .models import List
DOMAIN_MODEL = apps.get_model(settings.LISTS_DOMAIN_MODEL)
@receiver(pre_delete, sender=DOMAIN_MODEL, dispatch_uid="lists.clean_address_name")
def clean_address_name(sender, **kwargs):
domain = kwargs['instance']
for list in List.objects.filter(address_domain_id=domain.pk):
list.address_name = ''
list.address_domain_id = None
list.save(update_fields=('address_name', 'address_domain_id'))

View File

@ -22,6 +22,13 @@ STATE_COLORS = {
Transaction.REJECTED: 'red', Transaction.REJECTED: 'red',
} }
PROCESS_STATE_COLORS = {
TransactionProcess.CREATED: 'blue',
TransactionProcess.EXECUTED: 'olive',
TransactionProcess.ABORTED: 'red',
TransactionProcess.COMMITED: 'green',
}
class PaymentSourceAdmin(SelectPluginAdminMixin, AccountAdminMixin, admin.ModelAdmin): class PaymentSourceAdmin(SelectPluginAdminMixin, AccountAdminMixin, admin.ModelAdmin):
list_display = ('label', 'method', 'number', 'account_link', 'is_active') list_display = ('label', 'method', 'number', 'account_link', 'is_active')
@ -61,8 +68,8 @@ class TransactionInline(admin.TabularInline):
class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = ( list_display = (
'id', 'bill_link', 'account_link', 'source_link', 'display_created_at', 'display_modified_at', 'display_state', 'id', 'bill_link', 'account_link', 'source_link', 'display_created_at',
'amount', 'process_link' 'display_modified_at', 'display_state', 'amount', 'process_link'
) )
list_filter = ('source__method', 'state') list_filter = ('source__method', 'state')
fieldsets = ( fieldsets = (
@ -142,7 +149,10 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin): class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
list_display = ('id', 'file_url', 'display_transactions', 'display_created_at') list_display = (
'id', 'file_url', 'display_transactions', 'display_state', 'display_created_at',
)
list_filter = ('state',)
fields = ('data', 'file_url', 'created_at') fields = ('data', 'file_url', 'created_at')
readonly_fields = ('data', 'file_url', 'display_transactions', 'created_at') readonly_fields = ('data', 'file_url', 'display_transactions', 'created_at')
list_prefetch_related = ('transactions',) list_prefetch_related = ('transactions',)
@ -152,6 +162,7 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
) )
actions = change_view_actions + (actions.delete_selected,) actions = change_view_actions + (actions.delete_selected,)
display_state = admin_colored('state', colors=PROCESS_STATE_COLORS)
display_created_at = admin_date('created_at', short_description=_("Created")) display_created_at = admin_date('created_at', short_description=_("Created"))
def file_url(self, process): def file_url(self, process):
@ -169,7 +180,7 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
state = trans.get_state_display() state = trans.get_state_display()
ids.append('<span style="color:%s" title="%s">%i</span>' % (color, state, trans.id)) ids.append('<span style="color:%s" title="%s">%i</span>' % (color, state, trans.id))
counter += 1 + len(str(trans.id)) counter += 1 + len(str(trans.id))
if counter > 125: if counter > 100:
counter = 0 counter = 0
lines.append(','.join(ids)) lines.append(','.join(ids))
ids = [] ids = []

View File

@ -13,7 +13,7 @@ from .. import settings
class PaymentMethod(plugins.Plugin, metaclass=plugins.PluginMount): class PaymentMethod(plugins.Plugin, metaclass=plugins.PluginMount):
label_field = 'label' label_field = 'label'
number_field = 'number' number_field = 'number'
process_credit = False allow_recharge = False
due_delta = relativedelta.relativedelta(months=1) due_delta = relativedelta.relativedelta(months=1)
plugin_field = 'method' plugin_field = 'method'
state_help = {} state_help = {}

View File

@ -45,7 +45,7 @@ class SEPADirectDebit(PaymentMethod):
verbose_name = _("SEPA Direct Debit") verbose_name = _("SEPA Direct Debit")
label_field = 'name' label_field = 'name'
number_field = 'iban' number_field = 'iban'
process_credit = True allow_recharge = True
form = SEPADirectDebitForm form = SEPADirectDebitForm
serializer = SEPADirectDebitSerializer serializer = SEPADirectDebitSerializer
due_delta = datetime.timedelta(days=5) due_delta = datetime.timedelta(days=5)
@ -96,7 +96,7 @@ class SEPADirectDebit(PaymentMethod):
) )
sepa = sepa.Document( sepa = sepa.Document(
E.CstmrCdtTrfInitn( E.CstmrCdtTrfInitn(
cls.get_header(context), cls.get_header(context, process),
E.PmtInf( # Payment Info E.PmtInf( # Payment Info
E.PmtInfId(str(process.id)), # Payment Id E.PmtInfId(str(process.id)), # Payment Id
E.PmtMtd("TRF"), # Payment Method E.PmtMtd("TRF"), # Payment Method
@ -239,7 +239,7 @@ class SEPADirectDebit(PaymentMethod):
) )
@classmethod @classmethod
def get_credit_transactions(transactions, process): def get_credit_transactions(cls, transactions, process):
import lxml.builder import lxml.builder
from lxml.builder import E from lxml.builder import E
for transaction in transactions: for transaction in transactions:

View File

@ -7,6 +7,7 @@ from orchestra.admin.actions import disable, enable
from orchestra.admin.utils import change_url from orchestra.admin.utils import change_url
from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.actions import list_accounts
from orchestra.contrib.accounts.admin import AccountAdminMixin from orchestra.contrib.accounts.admin import AccountAdminMixin
from orchestra.contrib.accounts.filters import IsActiveListFilter
from orchestra.plugins.admin import SelectPluginAdminMixin from orchestra.plugins.admin import SelectPluginAdminMixin
from orchestra.utils.apps import isinstalled from orchestra.utils.apps import isinstalled
from orchestra.utils.html import get_on_site_link from orchestra.utils.html import get_on_site_link
@ -17,8 +18,8 @@ from .services import SoftwareService
class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin): class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin):
list_display = ('name', 'service', 'display_url', 'account_link', 'is_active') list_display = ('name', 'service', 'display_url', 'account_link', 'display_active')
list_filter = ('service', 'is_active', CustomURLListFilter) list_filter = ('service', IsActiveListFilter, CustomURLListFilter)
search_fields = ('name', 'account__username') search_fields = ('name', 'account__username')
change_readonly_fields = ('service',) change_readonly_fields = ('service',)
plugin = SoftwareService plugin = SoftwareService

View File

@ -17,7 +17,7 @@ class UNIXUserController(ServiceController):
""" """
verbose_name = _("UNIX user") verbose_name = _("UNIX user")
model = 'systemusers.SystemUser' model = 'systemusers.SystemUser'
actions = ('save', 'delete', 'set_permission', 'validate_path_exists', 'create_link') actions = ('save', 'delete', 'set_permission', 'validate_paths_exist', 'create_link')
doc_settings = (settings, ( doc_settings = (settings, (
'SYSTEMUSERS_DEFAULT_GROUP_MEMBERS', 'SYSTEMUSERS_DEFAULT_GROUP_MEMBERS',
'SYSTEMUSERS_MOVE_ON_DELETE_PATH', 'SYSTEMUSERS_MOVE_ON_DELETE_PATH',
@ -215,15 +215,16 @@ class UNIXUserController(ServiceController):
EOF""") % context EOF""") % context
) )
def validate_path_exists(self, user): def validate_paths_exist(self, user):
context = { for path in user.paths_to_validate:
'path': user.path_to_validate, context = {
} 'path': path,
self.append(textwrap.dedent(""" }
if [[ ! -e '%(path)s' ]]; then self.append(textwrap.dedent("""
echo "%(path)s path does not exists." >&2 if [[ ! -e '%(path)s' ]]; then
fi""") % context echo "%(path)s path does not exists." >&2
) fi""") % context
)
def get_groups(self, user): def get_groups(self, user):
if user.is_main: if user.is_main:

View File

@ -8,9 +8,8 @@ from orchestra.contrib.orchestration import Operation
def validate_paths_exist(user, paths): def validate_paths_exist(user, paths):
operations = [] operations = []
for path in paths: user.paths_to_validate = paths
user.path_to_validate = path operations.extend(Operation.create_for_action(user, 'validate_paths_exist'))
operations.extend(Operation.create_for_action(user, 'validate_path_exists'))
logs = Operation.execute(operations) logs = Operation.execute(operations)
stderr = '\n'.join([log.stderr for log in logs]) stderr = '\n'.join([log.stderr for log in logs])
if 'path does not exists' in stderr: if 'path does not exists' in stderr:
@ -42,7 +41,7 @@ def validate_home(user, data, account):
if 'directory' in data and data['directory']: if 'directory' in data and data['directory']:
path = os.path.join(data['home'], data['directory']) path = os.path.join(data['home'], data['directory'])
try: try:
validate_path_exists(user, path) validate_paths_exist(user, (path,))
except ValidationError as err: except ValidationError as err:
raise ValidationError({ raise ValidationError({
'directory': err, 'directory': err,