diff --git a/TODO.md b/TODO.md index 97ee72c8..3dc05241 100644 --- a/TODO.md +++ b/TODO.md @@ -427,3 +427,5 @@ Case # bill changelist: dates: (closed_on, created_on, updated_on) # Resource data inline show info: link to monitor data, and history chart: link to monitor data of each item + +# DIsplay message email content nicelly diff --git a/orchestra/contrib/accounts/actions.py b/orchestra/contrib/accounts/actions.py index b6bf042b..9c93ff07 100644 --- a/orchestra/contrib/accounts/actions.py +++ b/orchestra/contrib/accounts/actions.py @@ -18,7 +18,7 @@ from . import settings def list_contacts(modeladmin, request, queryset): - ids = queryset.values_list('id', flat=True) + ids = queryset.order_by().values_list('id', flat=True).distinct() if not ids: messages.warning(request, "Select at least one account.") return @@ -28,6 +28,17 @@ def list_contacts(modeladmin, request, queryset): list_contacts.short_description = _("List contacts") +def list_accounts(modeladmin, request, queryset): + accounts = queryset.order_by().values_list('account_id', flat=True).distinct() + if not accounts: + messages.warning(request, "Select at least one instance.") + return + url = reverse('admin:contacts_contact_changelist') + url += '?id__in=%s' % ','.join(map(str, accounts)) + return redirect(url) +list_accounts.short_description = _("List accounts") + + def service_report(modeladmin, request, queryset): # TODO resources accounts = [] diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py index 347ad6ba..fb70ef14 100644 --- a/orchestra/contrib/bills/admin.py +++ b/orchestra/contrib/bills/admin.py @@ -13,6 +13,7 @@ from django.shortcuts import redirect from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import admin_date, insertattr, admin_link +from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin from orchestra.forms.widgets import paddingCheckboxSelectMultiple @@ -212,7 +213,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): actions = [ actions.manage_lines, actions.download_bills, actions.close_bills, actions.send_bills, actions.amend_bills, actions.bill_report, actions.service_report, - actions.close_send_download_bills, + actions.close_send_download_bills, list_accounts, ] change_readonly_fields = ('account_link', 'type', 'is_open', 'amend_of_link', 'amend_links') readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state') diff --git a/orchestra/contrib/contacts/admin.py b/orchestra/contrib/contacts/admin.py index 965e4732..f761fc28 100644 --- a/orchestra/contrib/contacts/admin.py +++ b/orchestra/contrib/contacts/admin.py @@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin import AtLeastOneRequiredInlineFormSet, ExtendedModelAdmin from orchestra.admin.actions import SendEmail from orchestra.admin.utils import insertattr, change_url +from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import AccountAdmin, AccountAdminMixin from orchestra.forms.widgets import paddingCheckboxSelectMultiple @@ -59,7 +60,7 @@ class ContactAdmin(AccountAdminMixin, ExtendedModelAdmin): 'fields': ('address', ('zipcode', 'city'), 'country') }), ) - actions = [SendEmail(),] + actions = (SendEmail(), list_accounts) def dispaly_name(self, contact): return str(contact) diff --git a/orchestra/contrib/databases/admin.py b/orchestra/contrib/databases/admin.py index 184968e4..a0cc213d 100644 --- a/orchestra/contrib/databases/admin.py +++ b/orchestra/contrib/databases/admin.py @@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin from orchestra.admin.utils import change_url +from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import SelectAccountAdminMixin from .forms import DatabaseCreationForm, DatabaseUserChangeForm, DatabaseUserCreationForm @@ -42,6 +43,7 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): filter_horizontal = ['users'] filter_by_account_fields = ('users',) list_prefetch_related = ('users',) + actions = (list_accounts,) def display_users(self, db): links = [] @@ -90,6 +92,7 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten readonly_fields = ('account_link', 'display_databases',) filter_by_account_fields = ('databases',) list_prefetch_related = ('databases',) + actions = (list_accounts,) def display_databases(self, user): links = [] diff --git a/orchestra/contrib/domains/admin.py b/orchestra/contrib/domains/admin.py index efc05559..db328b1e 100644 --- a/orchestra/contrib/domains/admin.py +++ b/orchestra/contrib/domains/admin.py @@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import admin_link, change_url +from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import AccountAdminMixin from orchestra.utils import apps @@ -57,7 +58,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin): change_readonly_fields = ('name', 'serial') search_fields = ('name', 'account__username') add_form = BatchDomainCreationAdminForm - actions = (edit_records, set_soa) + actions = (edit_records, set_soa, list_accounts) change_view_actions = (view_zone, edit_records) top_link = admin_link('top') diff --git a/orchestra/contrib/domains/forms.py b/orchestra/contrib/domains/forms.py index 66c0bf10..50bbd312 100644 --- a/orchestra/contrib/domains/forms.py +++ b/orchestra/contrib/domains/forms.py @@ -80,15 +80,15 @@ class ValidateZoneMixin(object): def clean(self): """ Checks if everything is consistent """ super(ValidateZoneMixin, self).clean() - if any(formset.errors): + if any(self.errors): return - if formset.instance.name: + if self.instance.name: records = [] - for form in formset.forms: + for form in self.forms: data = form.cleaned_data if data and not data['DELETE']: records.append(data) - domain = domain_for_validation(formset.instance, records) + domain = domain_for_validation(self.instance, records) validators.validate_zone(domain.render_zone()) diff --git a/orchestra/contrib/domains/models.py b/orchestra/contrib/domains/models.py index 3e4890e8..b1053311 100644 --- a/orchestra/contrib/domains/models.py +++ b/orchestra/contrib/domains/models.py @@ -273,14 +273,14 @@ class Record(models.Model): ) VALIDATORS = { - MX: validators.validate_mx_record, - NS: validators.validate_zone_label, - A: validate_ipv4_address, - AAAA: validate_ipv6_address, - CNAME: validators.validate_zone_label, - TXT: validate_ascii, - SRV: validators.validate_srv_record, - SOA: validators.validate_soa_record, + MX: (validators.validate_mx_record,), + NS: (validators.validate_zone_label,), + A: (validate_ipv4_address,), + AAAA: (validate_ipv6_address,), + CNAME: (validators.validate_zone_label,), + TXT: (validate_ascii, validators.validate_quoted_record), + SRV: (validators.validate_srv_record,), + SOA: (validators.validate_soa_record,), } domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records') @@ -300,12 +300,13 @@ class Record(models.Model): if self.type != self.TXT: self.value = self.value.lower().strip() if self.type: - try: - self.VALIDATORS[self.type](self.value) - except ValidationError as error: - raise ValidationError({ - 'value': error, - }) + for validator in self.VALIDATORS.get(self.type, []): + try: + validator(self.value) + except ValidationError as error: + raise ValidationError({ + 'value': error, + }) def get_ttl(self): return self.ttl or settings.DOMAINS_DEFAULT_TTL diff --git a/orchestra/contrib/domains/validators.py b/orchestra/contrib/domains/validators.py index 569b81e1..b1c02610 100644 --- a/orchestra/contrib/domains/validators.py +++ b/orchestra/contrib/domains/validators.py @@ -107,6 +107,16 @@ def validate_soa_record(value): raise ValidationError(msg) +def validate_quoted_record(value): + value = value.strip() + if ' ' in value and (value[0] != '"' or value[-1] != '"'): + raise ValidationError( + _("This record value contains spaces, you must enclose the string in double quotes; " + "otherwise, individual words will be separately quoted and break up the record " + "into multiple parts.") + ) + + def validate_zone(zone): """ Ultimate zone file validation using named-checkzone """ zone_name = zone.split()[0][:-1] diff --git a/orchestra/contrib/lists/admin.py b/orchestra/contrib/lists/admin.py index f4861270..9f5e8e90 100644 --- a/orchestra/contrib/lists/admin.py +++ b/orchestra/contrib/lists/admin.py @@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin from orchestra.admin.actions import disable 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 @@ -53,7 +54,7 @@ class ListAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModel add_form = ListCreationForm list_select_related = ('account', 'address_domain',) filter_by_account_fields = ['address_domain'] - actions = (disable,) + actions = (disable, list_accounts) address_domain_link = admin_link('address_domain', order='address_domain__name') diff --git a/orchestra/contrib/mailboxes/admin.py b/orchestra/contrib/mailboxes/admin.py index c31c41dd..4398945d 100644 --- a/orchestra/contrib/mailboxes/admin.py +++ b/orchestra/contrib/mailboxes/admin.py @@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin from orchestra.admin.actions import disable from orchestra.admin.utils import admin_link, change_url +from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import SelectAccountAdminMixin from orchestra.contrib.accounts.filters import IsActiveListFilter @@ -69,7 +70,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo add_form = MailboxCreationForm form = MailboxChangeForm list_prefetch_related = ('addresses__domain',) - actions = (disable,) + actions = (disable, list_accounts) def __init__(self, *args, **kwargs): super(MailboxAdmin, self).__init__(*args, **kwargs) diff --git a/orchestra/contrib/mailer/admin.py b/orchestra/contrib/mailer/admin.py index 78483270..c95bcb93 100644 --- a/orchestra/contrib/mailer/admin.py +++ b/orchestra/contrib/mailer/admin.py @@ -1,3 +1,6 @@ +import base64 +import email + from django import forms from django.contrib import admin from django.core.urlresolvers import reverse @@ -12,7 +15,7 @@ from .engine import send_pending from .models import Message, SMTPLog -COLORS = { +COLORS = { Message.QUEUED: 'purple', Message.SENT: 'green', Message.DEFERRED: 'darkorange', @@ -29,6 +32,21 @@ class MessageAdmin(admin.ModelAdmin): ) list_filter = ('state', 'priority', 'retries') list_prefetch_related = ('logs__id') + fieldsets = ( + (None, { + 'fields': ('state', 'priority', ('retries', 'last_retry_delta'), + 'display_full_subject', 'display_to_address', 'display_from_address', + 'display_content'), + }), + (_("Edit"), { + 'classes': ('collapse',), + 'fields': ('subject', 'to_address', 'from_address', 'content'), + }), + ) + readonly_fields = ( + 'retries', 'last_retry_delta', 'display_full_subject', 'display_to_address', + 'display_from_address', 'display_content', + ) colored_state = admin_colored('state', colors=COLORS) created_at_delta = admin_date('created_at') @@ -52,6 +70,37 @@ class MessageAdmin(admin.ModelAdmin): num_logs.admin_order_field = 'logs__count' num_logs.allow_tags = True + def display_content(self, instance): + part = email.message_from_string(instance.content) + payload = part.get_payload() + if isinstance(payload, list): + for part in payload: + payload = part.get_payload() + if part.get_content_type() == 'text/html': + # prioritize HTML + payload = '
%s
' % payload + break + if part.get('Content-Transfer-Encoding') == 'base64': + payload = base64.b64decode(payload) + payload = payload.decode(part.get_charsets()[0]) + if part.get_content_type() == 'text/plain': + payload = payload.replace('\n', '
') + return payload + display_content.short_description = _("Content") + display_content.allow_tags = True + + def display_full_subject(self, instance): + return instance.subject + display_full_subject.short_description = _("Subject") + + def display_from_address(self, instance): + return instance.from_address + display_from_address.short_description = _("From address") + + def display_to_address(self, instance): + return instance.to_address + display_to_address.short_description = _("To address") + def get_urls(self): from django.conf.urls import url urls = super(MessageAdmin, self).get_urls() @@ -77,6 +126,7 @@ class MessageAdmin(admin.ModelAdmin): kwargs['widget'] = forms.TextInput(attrs={'size':'100'}) return super(MessageAdmin, self).formfield_for_dbfield(db_field, **kwargs) + class SMTPLogAdmin(admin.ModelAdmin): list_display = ( 'id', 'message_link', 'colored_result', 'date_delta', 'log_message' diff --git a/orchestra/contrib/orders/admin.py b/orchestra/contrib/orders/admin.py index 9f687067..baa0dfe8 100644 --- a/orchestra/contrib/orders/admin.py +++ b/orchestra/contrib/orders/admin.py @@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import admin_link, admin_date, change_url +from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import AccountAdminMixin from orchestra.utils.humanize import naturaldate @@ -49,13 +50,18 @@ class MetricStorageInline(admin.TabularInline): class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin): list_display = ( 'id', 'service_link', 'account_link', 'content_object_link', - 'display_registered_on', 'display_billed_until', 'display_cancelled_on', 'display_metric' + 'display_registered_on', 'display_billed_until', 'display_cancelled_on', + 'display_metric' + ) + list_filter = ( + ActiveOrderListFilter, IgnoreOrderListFilter, BilledOrderListFilter, 'service' ) - list_filter = (ActiveOrderListFilter, IgnoreOrderListFilter, BilledOrderListFilter, 'service') default_changelist_filters = ( ('ignore', '0'), ) - actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored, report) + actions = ( + BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored, report, list_accounts + ) change_view_actions = (BillSelectedOrders(), mark_as_ignored, mark_as_not_ignored) date_hierarchy = 'registered_on' inlines = (MetricStorageInline,) @@ -111,7 +117,9 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin): display_billed_until.admin_order_field = 'billed_until' def display_metric(self, order): - """ dispalys latest metric value, don't uses latest() because not loosing prefetch_related """ + """ + dispalys latest metric value, don't uses latest() because not loosing prefetch_related + """ try: metric = order.metrics.all()[0] except IndexError: diff --git a/orchestra/contrib/payments/admin.py b/orchestra/contrib/payments/admin.py index d94a1ceb..ccbfe126 100644 --- a/orchestra/contrib/payments/admin.py +++ b/orchestra/contrib/payments/admin.py @@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ChangeViewActionsMixin, ExtendedModelAdmin from orchestra.admin.utils import admin_colored, admin_link +from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin from orchestra.plugins.admin import SelectPluginAdminMixin @@ -91,7 +92,7 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): actions.process_transactions, actions.mark_as_executed, actions.mark_as_secured, actions.mark_as_rejected, ) - actions = change_view_actions + (actions.report,) + actions = change_view_actions + (actions.report, list_accounts) filter_by_account_fields = ('bill', 'source') change_readonly_fields = ('amount', 'currency') readonly_fields = ( diff --git a/orchestra/contrib/plans/admin.py b/orchestra/contrib/plans/admin.py index 16d08931..58febe68 100644 --- a/orchestra/contrib/plans/admin.py +++ b/orchestra/contrib/plans/admin.py @@ -2,6 +2,7 @@ from django.contrib import admin from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import insertattr +from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import AccountAdminMixin from orchestra.contrib.services.models import Service @@ -29,6 +30,7 @@ class ContractedPlanAdmin(AccountAdminMixin, admin.ModelAdmin): list_filter = ('plan__name',) list_select_related = ('plan', 'account') search_fields = ('account__username', 'plan__name', 'id') + actions = (list_accounts,) admin.site.register(Plan, PlanAdmin) diff --git a/orchestra/contrib/saas/admin.py b/orchestra/contrib/saas/admin.py index de7a7578..b5eedece 100644 --- a/orchestra/contrib/saas/admin.py +++ b/orchestra/contrib/saas/admin.py @@ -3,6 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin from orchestra.admin.actions import disable +from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import AccountAdminMixin from orchestra.plugins.admin import SelectPluginAdminMixin @@ -18,7 +19,7 @@ class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMi plugin = SoftwareService plugin_field = 'service' plugin_title = 'Software as a Service' - actions = (disable,) + actions = (disable, list_accounts) def display_site_domain(self, saas): site_domain = saas.get_site_domain() diff --git a/orchestra/contrib/saas/backends/phplist.py b/orchestra/contrib/saas/backends/phplist.py index 9e2a2299..94ceecae 100644 --- a/orchestra/contrib/saas/backends/phplist.py +++ b/orchestra/contrib/saas/backends/phplist.py @@ -14,7 +14,8 @@ from .. import settings class PhpListSaaSBackend(ServiceController): """ Creates a new phplist instance on a phpList multisite installation. - The site is created by means of creating a new database per phpList site, but all sites share the same code. + The site is created by means of creating a new database per phpList site, + but all sites share the same code. // config/config.php $site = array_shift((explode(".",$_SERVER['HTTP_HOST']))); @@ -28,7 +29,8 @@ class PhpListSaaSBackend(ServiceController): def _save(self, saas, server): admin_link = 'https://%s/admin/' % saas.get_site_domain() print('admin_link:', admin_link) - admin_content = requests.get(admin_link, verify=settings.SAAS_PHPLIST_VERIFY_SSL).content.decode('utf8') + admin_content = requests.get(admin_link, verify=settings.SAAS_PHPLIST_VERIFY_SSL) + admin_content = admin_content.content.decode('utf8') if admin_content.startswith('Cannot connect to Database'): raise RuntimeError("Database is not yet configured") install = re.search(r'([^"]+firstinstall[^"]+)', admin_content) @@ -68,7 +70,7 @@ class PhpListSaaSBackend(ServiceController): --execute='UPDATE phplist_admin SET password="%(digest)s" where ID=1; \\ UPDATE phplist_user_user SET password="%(digest)s" where ID=1;' \\ %(db_name)s""") % context - print(cmd) + print('cmd:', cmd) sshrun(server.get_address(), cmd) def save(self, saas): diff --git a/orchestra/contrib/systemusers/admin.py b/orchestra/contrib/systemusers/admin.py index 7e2957e6..c27577b3 100644 --- a/orchestra/contrib/systemusers/admin.py +++ b/orchestra/contrib/systemusers/admin.py @@ -3,6 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin from orchestra.admin.actions import disable +from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import SelectAccountAdminMixin from orchestra.contrib.accounts.filters import IsActiveListFilter @@ -42,7 +43,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende form = SystemUserChangeForm ordering = ('-id',) change_view_actions = (set_permission, disable) - actions = (delete_selected,) + change_view_actions + actions = (delete_selected, list_accounts) + change_view_actions def display_main(self, user): return user.is_main diff --git a/orchestra/contrib/vps/admin.py b/orchestra/contrib/vps/admin.py index 01908c36..d81c85d0 100644 --- a/orchestra/contrib/vps/admin.py +++ b/orchestra/contrib/vps/admin.py @@ -4,6 +4,7 @@ from django.contrib.auth.admin import UserAdmin from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin +from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import AccountAdminMixin from .forms import VPSChangeForm, VPSCreationForm @@ -37,6 +38,7 @@ class VPSAdmin(AccountAdminMixin, ExtendedModelAdmin): 'fields': ('password1', 'password2',) }), ) + actions = (list_accounts,) def get_urls(self): useradmin = UserAdmin(VPS, self.admin_site) diff --git a/orchestra/contrib/webapps/admin.py b/orchestra/contrib/webapps/admin.py index 4fa6c1b9..527a7cc4 100644 --- a/orchestra/contrib/webapps/admin.py +++ b/orchestra/contrib/webapps/admin.py @@ -6,6 +6,7 @@ from django.utils.translation import ugettext, ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import change_url, get_modeladmin +from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import AccountAdminMixin from orchestra.forms.widgets import DynamicHelpTextSelect from orchestra.plugins.admin import SelectPluginAdminMixin @@ -58,6 +59,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) plugin = AppType plugin_field = 'type' plugin_title = _("Web application type") + actions = (list_accounts,) def display_websites(self, webapp): websites = [] diff --git a/orchestra/contrib/websites/admin.py b/orchestra/contrib/websites/admin.py index 0463ecd8..a8c7dec3 100644 --- a/orchestra/contrib/websites/admin.py +++ b/orchestra/contrib/websites/admin.py @@ -5,10 +5,10 @@ from django.db.models import Q from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ - from orchestra.admin import ExtendedModelAdmin from orchestra.admin.actions import disable from orchestra.admin.utils import admin_link, change_url +from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin from orchestra.forms.widgets import DynamicHelpTextSelect @@ -69,7 +69,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): filter_by_account_fields = ['domains'] list_prefetch_related = ('domains', 'content_set__webapp') search_fields = ('name', 'account__username', 'domains__name', 'content__webapp__name') - actions = (disable,) + actions = (disable, list_accounts) def display_domains(self, website): domains = []