diff --git a/TODO.md b/TODO.md index ea35ff7d..7d67e710 100644 --- a/TODO.md +++ b/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 diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py index 86da90c8..904df2f2 100644 --- a/orchestra/admin/utils.py +++ b/orchestra/admin/utils.py @@ -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 diff --git a/orchestra/contrib/accounts/filters.py b/orchestra/contrib/accounts/filters.py index 6b8b5541..91a10708 100644 --- a/orchestra/contrib/accounts/filters.py +++ b/orchestra/contrib/accounts/filters.py @@ -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 diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py index 9b721b9c..28cc7bfd 100644 --- a/orchestra/contrib/bills/admin.py +++ b/orchestra/contrib/bills/admin.py @@ -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 diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py index 254da7a1..2f5acdc0 100644 --- a/orchestra/contrib/bills/models.py +++ b/orchestra/contrib/bills/models.py @@ -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') diff --git a/orchestra/contrib/bills/templates/admin/bills/billline/report.html b/orchestra/contrib/bills/templates/admin/bills/billline/report.html index 4227a433..c43c940c 100644 --- a/orchestra/contrib/bills/templates/admin/bills/billline/report.html +++ b/orchestra/contrib/bills/templates/admin/bills/billline/report.html @@ -39,7 +39,7 @@
{% trans "Services" %} | +{% trans "Service" %} | {% trans "Active" %} | {% trans "Cancelled" %} | {% trans "Nominal price" %} | diff --git a/orchestra/contrib/lists/admin.py b/orchestra/contrib/lists/admin.py index c07d4dfd..8332fe10 100644 --- a/orchestra/contrib/lists/admin.py +++ b/orchestra/contrib/lists/admin.py @@ -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) diff --git a/orchestra/contrib/lists/apps.py b/orchestra/contrib/lists/apps.py index 7e6e772c..f4d2b062 100644 --- a/orchestra/contrib/lists/apps.py +++ b/orchestra/contrib/lists/apps.py @@ -10,3 +10,4 @@ class ListsConfig(AppConfig): def ready(self): from .models import List services.register(List, icon='email-alter.png') + from . import signals diff --git a/orchestra/contrib/lists/forms.py b/orchestra/contrib/lists/forms.py deleted file mode 100644 index c83b0f3a..00000000 --- a/orchestra/contrib/lists/forms.py +++ /dev/null @@ -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 diff --git a/orchestra/contrib/lists/models.py b/orchestra/contrib/lists/models.py index cd5af8a6..bf20d4d4 100644 --- a/orchestra/contrib/lists/models.py +++ b/orchestra/contrib/lists/models.py @@ -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',)) diff --git a/orchestra/contrib/lists/signals.py b/orchestra/contrib/lists/signals.py new file mode 100644 index 00000000..1b2d54f9 --- /dev/null +++ b/orchestra/contrib/lists/signals.py @@ -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')) + diff --git a/orchestra/contrib/payments/admin.py b/orchestra/contrib/payments/admin.py index 139dc6a4..e8caa2d8 100644 --- a/orchestra/contrib/payments/admin.py +++ b/orchestra/contrib/payments/admin.py @@ -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('%i' % (color, state, trans.id)) counter += 1 + len(str(trans.id)) - if counter > 125: + if counter > 100: counter = 0 lines.append(','.join(ids)) ids = [] diff --git a/orchestra/contrib/payments/methods/options.py b/orchestra/contrib/payments/methods/options.py index 4e661459..5e19cbd6 100644 --- a/orchestra/contrib/payments/methods/options.py +++ b/orchestra/contrib/payments/methods/options.py @@ -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 = {} diff --git a/orchestra/contrib/payments/methods/sepadirectdebit.py b/orchestra/contrib/payments/methods/sepadirectdebit.py index 9fe453c0..96ac00cb 100644 --- a/orchestra/contrib/payments/methods/sepadirectdebit.py +++ b/orchestra/contrib/payments/methods/sepadirectdebit.py @@ -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: diff --git a/orchestra/contrib/saas/admin.py b/orchestra/contrib/saas/admin.py index a317374a..b3b0739a 100644 --- a/orchestra/contrib/saas/admin.py +++ b/orchestra/contrib/saas/admin.py @@ -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 diff --git a/orchestra/contrib/systemusers/backends.py b/orchestra/contrib/systemusers/backends.py index 6e844398..83c15b77 100644 --- a/orchestra/contrib/systemusers/backends.py +++ b/orchestra/contrib/systemusers/backends.py @@ -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: diff --git a/orchestra/contrib/systemusers/validators.py b/orchestra/contrib/systemusers/validators.py index 705ab96c..fbe4e9a0 100644 --- a/orchestra/contrib/systemusers/validators.py +++ b/orchestra/contrib/systemusers/validators.py @@ -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,
---|