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
# Disable/enable SaaS and VPS
# AGO
# Don't show lines with size 0?
# pending orders with recharge do not show up
# Traffic of disabled accounts doesn't get disabled
# is_active list filter account dissabled filtering support
# URL encode "Order description" on clone
# Service CLONE METRIC doesn't work
# 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
# support for better edditing bill lines and sublines

View File

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

View File

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

View File

@ -289,7 +289,9 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
'closed_on_display', 'updated_on_display', 'display_total_with_subtotals',
)
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"))
closed_on_display = admin_date('closed_on', short_description=_("Closed"))
@ -426,7 +428,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
qs = qs.prefetch_related(
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):
# TODO raise404, here and everywhere

View File

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

View File

@ -39,7 +39,7 @@
<table id="summary">
<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-cancelled">{% trans "Cancelled" %}</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.admin import SelectAccountAdminMixin
from orchestra.contrib.accounts.filters import IsActiveListFilter
from orchestra.forms import UserCreationForm, NonStoredUserChangeForm
from . import settings
from .filters import HasCustomAddressListFilter
from .forms import ListCreationForm, ListChangeForm
from .models import List
@ -54,8 +54,8 @@ class ListAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModel
list_filter = (IsActiveListFilter, HasCustomAddressListFilter)
readonly_fields = ('account_link',)
change_readonly_fields = ('name',)
form = ListChangeForm
add_form = ListCreationForm
form = NonStoredUserChangeForm
add_form = UserCreationForm
list_select_related = ('account', 'address_domain',)
filter_by_account_fields = ['address_domain']
actions = (disable, enable, list_accounts)

View File

@ -10,3 +10,4 @@ class ListsConfig(AppConfig):
def ready(self):
from .models import List
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.utils.functional import cached_property
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)
address_name = models.CharField(_("address name"), max_length=128,
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)
admin_email = models.EmailField(_("admin email"),
help_text=_("Administration email address"))
@ -55,6 +56,12 @@ class List(models.Model):
def active(self):
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):
self.is_active = False
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',
}
PROCESS_STATE_COLORS = {
TransactionProcess.CREATED: 'blue',
TransactionProcess.EXECUTED: 'olive',
TransactionProcess.ABORTED: 'red',
TransactionProcess.COMMITED: 'green',
}
class PaymentSourceAdmin(SelectPluginAdminMixin, AccountAdminMixin, admin.ModelAdmin):
list_display = ('label', 'method', 'number', 'account_link', 'is_active')
@ -61,8 +68,8 @@ class TransactionInline(admin.TabularInline):
class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
list_display = (
'id', 'bill_link', 'account_link', 'source_link', 'display_created_at', 'display_modified_at', 'display_state',
'amount', 'process_link'
'id', 'bill_link', 'account_link', 'source_link', 'display_created_at',
'display_modified_at', 'display_state', 'amount', 'process_link'
)
list_filter = ('source__method', 'state')
fieldsets = (
@ -142,7 +149,10 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
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')
readonly_fields = ('data', 'file_url', 'display_transactions', 'created_at')
list_prefetch_related = ('transactions',)
@ -152,6 +162,7 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
)
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"))
def file_url(self, process):
@ -169,7 +180,7 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
state = trans.get_state_display()
ids.append('<span style="color:%s" title="%s">%i</span>' % (color, state, trans.id))
counter += 1 + len(str(trans.id))
if counter > 125:
if counter > 100:
counter = 0
lines.append(','.join(ids))
ids = []

View File

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

View File

@ -45,7 +45,7 @@ class SEPADirectDebit(PaymentMethod):
verbose_name = _("SEPA Direct Debit")
label_field = 'name'
number_field = 'iban'
process_credit = True
allow_recharge = True
form = SEPADirectDebitForm
serializer = SEPADirectDebitSerializer
due_delta = datetime.timedelta(days=5)
@ -96,7 +96,7 @@ class SEPADirectDebit(PaymentMethod):
)
sepa = sepa.Document(
E.CstmrCdtTrfInitn(
cls.get_header(context),
cls.get_header(context, process),
E.PmtInf( # Payment Info
E.PmtInfId(str(process.id)), # Payment Id
E.PmtMtd("TRF"), # Payment Method
@ -239,7 +239,7 @@ class SEPADirectDebit(PaymentMethod):
)
@classmethod
def get_credit_transactions(transactions, process):
def get_credit_transactions(cls, transactions, process):
import lxml.builder
from lxml.builder import E
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.contrib.accounts.actions import list_accounts
from orchestra.contrib.accounts.admin import AccountAdminMixin
from orchestra.contrib.accounts.filters import IsActiveListFilter
from orchestra.plugins.admin import SelectPluginAdminMixin
from orchestra.utils.apps import isinstalled
from orchestra.utils.html import get_on_site_link
@ -17,8 +18,8 @@ from .services import SoftwareService
class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin):
list_display = ('name', 'service', 'display_url', 'account_link', 'is_active')
list_filter = ('service', 'is_active', CustomURLListFilter)
list_display = ('name', 'service', 'display_url', 'account_link', 'display_active')
list_filter = ('service', IsActiveListFilter, CustomURLListFilter)
search_fields = ('name', 'account__username')
change_readonly_fields = ('service',)
plugin = SoftwareService

View File

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

View File

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