Dont delete lists when deleting domains
This commit is contained in:
parent
27aec2e5f0
commit
682a8947d3
9
TODO.md
9
TODO.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -10,3 +10,4 @@ class ListsConfig(AppConfig):
|
|||
def ready(self):
|
||||
from .models import List
|
||||
services.register(List, icon='email-alter.png')
|
||||
from . import signals
|
||||
|
|
|
@ -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
|
|
@ -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 <name>@%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',))
|
||||
|
|
19
orchestra/contrib/lists/signals.py
Normal file
19
orchestra/contrib/lists/signals.py
Normal 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'))
|
||||
|
|
@ -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 = []
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue