From 5389f425ceb5e78ca6e48dd36bf33225684528a3 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Wed, 12 May 2021 13:55:47 +0200 Subject: [PATCH 01/21] mark_safe display_websites & display_addresses --- orchestra/contrib/domains/admin.py | 6 +++++- orchestra/utils/html.py | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/orchestra/contrib/domains/admin.py b/orchestra/contrib/domains/admin.py index 47b5e14d..f20849dd 100644 --- a/orchestra/contrib/domains/admin.py +++ b/orchestra/contrib/domains/admin.py @@ -3,6 +3,8 @@ from django.urls import reverse from django.db import models from django.db.models.functions import Concat, Coalesce from django.templatetags.static import static +from django.utils.html import format_html +from django.utils.safestring import mark_safe from django.utils.translation import ugettext, ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin @@ -83,6 +85,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin): display_is_top.boolean = True display_is_top.admin_order_field = 'top' + @mark_safe def display_websites(self, domain): if apps.isinstalled('orchestra.contrib.websites'): websites = domain.websites.all() @@ -92,7 +95,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin): site_link = get_on_site_link(website.get_absolute_url()) admin_url = change_url(website) title = _("Edit website") - link = '%s %s' % ( + link = format_html('{} {}', admin_url, title, website.name, site_link) links.append(link) return '
'.join(links) @@ -108,6 +111,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin): display_websites.short_description = _("Websites") display_websites.allow_tags = True + @mark_safe def display_addresses(self, domain): if apps.isinstalled('orchestra.contrib.mailboxes'): add_url = reverse('admin:mailboxes_address_add') diff --git a/orchestra/utils/html.py b/orchestra/utils/html.py index f888c256..c8741542 100644 --- a/orchestra/utils/html.py +++ b/orchestra/utils/html.py @@ -1,6 +1,7 @@ import textwrap from django.templatetags.static import static +from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ from orchestra.utils.sys import run @@ -31,6 +32,6 @@ def get_on_site_link(url): context = { 'title': _("View on site %s") % url, 'url': url, - 'image': '' % static('orchestra/images/view-on-site.png'), + 'image': format_html('', static('orchestra/images/view-on-site.png')), } - return '%(image)s' % context + return format_html('{image}', **context) From aebbd424fcfb52674dd9a2768f5483d4fedfe29a Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Wed, 12 May 2021 14:16:28 +0200 Subject: [PATCH 02/21] Fix admin list_display with HTML content --- orchestra/admin/utils.py | 4 ++-- orchestra/contrib/accounts/admin.py | 1 + orchestra/contrib/bills/admin.py | 8 ++++++-- orchestra/contrib/websites/admin.py | 2 ++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py index ccf22b49..764edf84 100644 --- a/orchestra/admin/utils.py +++ b/orchestra/admin/utils.py @@ -10,7 +10,7 @@ from django.urls import reverse, NoReverseMatch from django.db import models from django.shortcuts import redirect from django.utils import timezone -from django.utils.html import escape +from django.utils.html import escape, format_html from django.utils.safestring import mark_safe from orchestra.models.utils import get_field_value @@ -158,7 +158,7 @@ def admin_date(*args, **kwargs): date = date.strftime("%Y-%m-%d %H:%M:%S %Z") else: date = date.strftime("%Y-%m-%d") - return '{1}'.format(date, escape(natural)) + return format_html('{1}', date, natural) def get_object_from_url(modeladmin, request): diff --git a/orchestra/contrib/accounts/admin.py b/orchestra/contrib/accounts/admin.py index 2008fb6b..a046a4a5 100644 --- a/orchestra/contrib/accounts/admin.py +++ b/orchestra/contrib/accounts/admin.py @@ -207,6 +207,7 @@ class AccountAdminMixin(object): account = None list_select_related = ('account',) + @mark_safe def display_active(self, instance): if not instance.is_active: return 'False' % static('admin/img/icon-no.svg') diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py index 560819c8..c7c4fc43 100644 --- a/orchestra/contrib/bills/admin.py +++ b/orchestra/contrib/bills/admin.py @@ -7,6 +7,7 @@ from django.db import models from django.db.models import F, Sum, Prefetch from django.db.models.functions import Coalesce from django.templatetags.static import static +from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from django.shortcuts import redirect @@ -67,6 +68,7 @@ class BillLineInline(admin.TabularInline): order_link = admin_link('order', display='pk') + @mark_safe def display_total(self, line): if line.pk: total = line.compute_total() @@ -242,6 +244,7 @@ class BillLineManagerAdmin(BillLineAdmin): class BillAdminMixin(AccountAdminMixin): + @mark_safe def display_total_with_subtotals(self, bill): if bill.pk: currency = settings.BILLS_CURRENCY.lower() @@ -255,6 +258,7 @@ class BillAdminMixin(AccountAdminMixin): display_total_with_subtotals.short_description = _("total") display_total_with_subtotals.admin_order_field = 'approx_total' + @mark_safe def display_payment_state(self, bill): if bill.pk: t_opts = bill.transactions.model._meta @@ -376,7 +380,7 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin): def display_total(self, bill): currency = settings.BILLS_CURRENCY.lower() - return '%s &%s;' % (bill.compute_total(), currency) + return format_html('{} &{};', bill.compute_total(), currency) display_total.allow_tags = True display_total.short_description = _("total") display_total.admin_order_field = 'approx_total' @@ -384,7 +388,7 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin): def type_link(self, bill): bill_type = bill.type.lower() url = reverse('admin:bills_%s_changelist' % bill_type) - return '%s' % (url, bill.get_type_display()) + return format_html('{}', url, bill.get_type_display()) type_link.allow_tags = True type_link.short_description = _("type") type_link.admin_order_field = 'type' diff --git a/orchestra/contrib/websites/admin.py b/orchestra/contrib/websites/admin.py index fc54b4bc..8e597d86 100644 --- a/orchestra/contrib/websites/admin.py +++ b/orchestra/contrib/websites/admin.py @@ -3,6 +3,7 @@ from django.contrib import admin from django.urls import resolve from django.db.models import Q from django.utils.encoding import force_text +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin @@ -78,6 +79,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): search_fields = ('name', 'account__username', 'domains__name', 'content__webapp__name') actions = (disable, enable, list_accounts) + @mark_safe def display_domains(self, website): domains = [] for domain in website.domains.all(): From 48ef1f21e3b679de756aa5aa5e1d2e8619e0c3b5 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Wed, 12 May 2021 14:38:17 +0200 Subject: [PATCH 03/21] Navigate through FK field to related model Fix regression introduced by 7d975637d53d3711b5f8fbe49241d685c4bcf98d when there is a misunderstanding while replacing deprecated rel.to --- orchestra/contrib/accounts/actions.py | 2 +- orchestra/contrib/accounts/forms.py | 2 +- orchestra/contrib/bills/helpers.py | 2 +- orchestra/contrib/lists/serializers.py | 2 +- orchestra/contrib/mailboxes/models.py | 2 +- orchestra/contrib/orders/signals.py | 2 +- orchestra/contrib/saas/services/helpers.py | 4 ++-- orchestra/contrib/systemusers/models.py | 2 +- orchestra/contrib/websites/serializers.py | 4 ++-- orchestra/permissions/options.py | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/orchestra/contrib/accounts/actions.py b/orchestra/contrib/accounts/actions.py index da58022c..b4bcff22 100644 --- a/orchestra/contrib/accounts/actions.py +++ b/orchestra/contrib/accounts/actions.py @@ -175,7 +175,7 @@ def delete_related_services(modeladmin, request, queryset): for model, objs in collector.model_objs.items(): count = 0 # discount main systemuser - if model is modeladmin.model.main_systemuser.field.model: + if model is modeladmin.model.main_systemuser.field.related_model: count = len(objs) - 1 # Discount account elif model is not modeladmin.model and model in registered_services: diff --git a/orchestra/contrib/accounts/forms.py b/orchestra/contrib/accounts/forms.py index c3e308b1..a420c266 100644 --- a/orchestra/contrib/accounts/forms.py +++ b/orchestra/contrib/accounts/forms.py @@ -47,7 +47,7 @@ def create_account_creation_form(): # Previous validation error return errors = {} - systemuser_model = Account.main_systemuser.field.model + systemuser_model = Account.main_systemuser.field.related_model if systemuser_model.objects.filter(username=account.username).exists(): errors['username'] = _("A system user with this name already exists.") for model, key, related_kwargs, __ in create_related: diff --git a/orchestra/contrib/bills/helpers.py b/orchestra/contrib/bills/helpers.py index e5986b5c..23c72fcc 100644 --- a/orchestra/contrib/bills/helpers.py +++ b/orchestra/contrib/bills/helpers.py @@ -21,7 +21,7 @@ def validate_contact(request, bill, error=True): message = msg.format(relation=_("Related"), account=account, url=url) send(request, mark_safe(message)) valid = False - main = type(bill).account.field.model.objects.get_main() + main = type(bill).account.field.related_model.objects.get_main() if not hasattr(main, 'billcontact'): account = force_text(main) url = reverse('admin:accounts_account_change', args=(main.id,)) diff --git a/orchestra/contrib/lists/serializers.py b/orchestra/contrib/lists/serializers.py index c4a666fd..317e3e77 100644 --- a/orchestra/contrib/lists/serializers.py +++ b/orchestra/contrib/lists/serializers.py @@ -12,7 +12,7 @@ from .models import List class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer): class Meta: - model = List.address_domain.field.model + model = List.address_domain.field.related_model fields = ('url', 'id', 'name') diff --git a/orchestra/contrib/mailboxes/models.py b/orchestra/contrib/mailboxes/models.py index 8581d0ec..a38078ca 100644 --- a/orchestra/contrib/mailboxes/models.py +++ b/orchestra/contrib/mailboxes/models.py @@ -44,7 +44,7 @@ class Mailbox(models.Model): def active(self): try: return self.is_active and self.account.is_active - except type(self).account.field.model.DoesNotExist: + except type(self).account.field.related_model.DoesNotExist: return self.is_active def disable(self): diff --git a/orchestra/contrib/orders/signals.py b/orchestra/contrib/orders/signals.py index c1b541e8..1778a3da 100644 --- a/orchestra/contrib/orders/signals.py +++ b/orchestra/contrib/orders/signals.py @@ -15,7 +15,7 @@ def cancel_orders(sender, **kwargs): if sender._meta.app_label not in settings.ORDERS_EXCLUDED_APPS: instance = kwargs['instance'] # Account delete will delete all related orders, no need to maintain order consistency - if isinstance(instance, Order.account.field.model): + if isinstance(instance, Order.account.field.related_model): return if type(instance) in services: for order in Order.objects.by_object(instance).active(): diff --git a/orchestra/contrib/saas/services/helpers.py b/orchestra/contrib/saas/services/helpers.py index 7418bc9d..a08855fe 100644 --- a/orchestra/contrib/saas/services/helpers.py +++ b/orchestra/contrib/saas/services/helpers.py @@ -42,7 +42,7 @@ def clean_custom_url(saas): ) except Website.DoesNotExist: # get or create domain - Domain = Website.domains.field.model + Domain = Website.domains.field.related_model try: domain = Domain.objects.get(name=url.netloc) except Domain.DoesNotExist: @@ -110,7 +110,7 @@ def create_or_update_directive(saas): account=account, ) except Website.DoesNotExist: - Domain = Website.domains.field.model + Domain = Website.domains.field.related_model domain = Domain.objects.get(name=url.netloc) # Create new website for custom_url tgt_server = Server.objects.get(name='web.pangea.lan') diff --git a/orchestra/contrib/systemusers/models.py b/orchestra/contrib/systemusers/models.py index 92576646..b270bcad 100644 --- a/orchestra/contrib/systemusers/models.py +++ b/orchestra/contrib/systemusers/models.py @@ -61,7 +61,7 @@ class SystemUser(models.Model): def active(self): try: return self.is_active and self.account.is_active - except type(self).account.field.model.DoesNotExist: + except type(self).account.field.related_model.DoesNotExist: return self.is_active @cached_property diff --git a/orchestra/contrib/websites/serializers.py b/orchestra/contrib/websites/serializers.py index f38203a3..49eb2b6f 100644 --- a/orchestra/contrib/websites/serializers.py +++ b/orchestra/contrib/websites/serializers.py @@ -13,13 +13,13 @@ from .validators import validate_domain_protocol class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer): class Meta: - model = Website.domains.field.model + model = Website.domains.field.related_model fields = ('url', 'id', 'name') class RelatedWebAppSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer): class Meta: - model = Content.webapp.field.model + model = Content.webapp.field.related_model fields = ('url', 'id', 'name', 'type') diff --git a/orchestra/permissions/options.py b/orchestra/permissions/options.py index b37d2363..7e48196a 100644 --- a/orchestra/permissions/options.py +++ b/orchestra/permissions/options.py @@ -90,7 +90,7 @@ class RelatedPermission(Permission): if obj is None: parent = cls for relation in relations: - parent = getattr(parent, relation).field.model + parent = getattr(parent, relation).field.related_model else: parent = functools.reduce(getattr, relations, obj) From 7183174f4c6431503823e246c2ebbada458185e3 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 13 May 2021 10:57:48 +0200 Subject: [PATCH 04/21] Handle empty address on Server.clean() --- orchestra/contrib/orchestration/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/orchestra/contrib/orchestration/models.py b/orchestra/contrib/orchestration/models.py index 95e0e4d4..14e8db2d 100644 --- a/orchestra/contrib/orchestration/models.py +++ b/orchestra/contrib/orchestration/models.py @@ -51,8 +51,9 @@ class Server(models.Model): def clean(self): self.name = self.name.strip() - self.address = self.address.strip() - if self.name and not self.address: + if self.address: + self.address = self.address.strip() + elif self.name: validate = OrValidator(validate_ip_address, validate_hostname) validate_hostname(self.name) try: From 5a21f766b49560f47c7501a10aacaac4922b62a6 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 13 May 2021 11:52:34 +0200 Subject: [PATCH 05/21] Add required param `renderer` to Widget.render Added on Django 1.11 and required since 2.1 The renderer argument is added to the Widget.render() method. https://docs.djangoproject.com/en/2.1/releases/1.11/#id2 --- orchestra/forms/widgets.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/orchestra/forms/widgets.py b/orchestra/forms/widgets.py index a3a686df..0235db61 100644 --- a/orchestra/forms/widgets.py +++ b/orchestra/forms/widgets.py @@ -17,9 +17,9 @@ class SpanWidget(forms.Widget): self.original = kwargs.pop('original', '') self.display = kwargs.pop('display', None) super(SpanWidget, self).__init__(*args, **kwargs) - - def render(self, name, value, attrs=None): - final_attrs = self.build_attrs(attrs, name=name) + + def render(self, name, value, attrs=None, renderer=None): + final_attrs = self.build_attrs(attrs, extra_attrs={'name':name}) original = self.original or value display = original if self.display is None else self.display # Display icon @@ -29,10 +29,10 @@ class SpanWidget(forms.Widget): tag = self.tag[:-1] endtag = '/'.join((self.tag[0], self.tag[1:])) return mark_safe('%s%s >%s%s' % (tag, forms.utils.flatatt(final_attrs), display, endtag)) - + def value_from_datadict(self, data, files, name): return self.original - + def _has_changed(self, initial, data): return False @@ -61,7 +61,7 @@ class DynamicHelpTextSelect(forms.Select): attrs.update(kwargs.get('attrs', {})) kwargs['attrs'] = attrs super(DynamicHelpTextSelect, self).__init__(*args, **kwargs) - + def get_dynamic_help_text(self, target, help_text): return textwrap.dedent("""\ siteoptions = {help_text}; From 8dc792b8511ddbed12097e11a7899b06e62661a2 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 13 May 2021 12:37:17 +0200 Subject: [PATCH 06/21] Fix render() of PaddingCheckboxSelectMultiple widget --- orchestra/contrib/bills/admin.py | 4 ++-- orchestra/contrib/contacts/admin.py | 16 ++++++++-------- orchestra/contrib/orchestration/forms.py | 4 ++-- orchestra/forms/widgets.py | 14 +++++++------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py index c7c4fc43..7ef0c78e 100644 --- a/orchestra/contrib/bills/admin.py +++ b/orchestra/contrib/bills/admin.py @@ -16,7 +16,7 @@ from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import admin_date, insertattr, admin_link, change_url from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin -from orchestra.forms.widgets import paddingCheckboxSelectMultiple +from orchestra.forms.widgets import PaddingCheckboxSelectMultiple from . import settings, actions from .filters import (BillTypeListFilter, HasBillContactListFilter, TotalListFilter, @@ -483,7 +483,7 @@ class BillContactInline(admin.StackedInline): if db_field.name == 'address': kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2}) if db_field.name == 'email_usage': - kwargs['widget'] = paddingCheckboxSelectMultiple(45) + kwargs['widget'] = PaddingCheckboxSelectMultiple(45) return super().formfield_for_dbfield(db_field, **kwargs) diff --git a/orchestra/contrib/contacts/admin.py b/orchestra/contrib/contacts/admin.py index f761fc28..82adc381 100644 --- a/orchestra/contrib/contacts/admin.py +++ b/orchestra/contrib/contacts/admin.py @@ -7,7 +7,7 @@ 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 +from orchestra.forms.widgets import PaddingCheckboxSelectMultiple from .filters import EmailUsageListFilter from .models import Contact @@ -61,18 +61,18 @@ class ContactAdmin(AccountAdminMixin, ExtendedModelAdmin): }), ) actions = (SendEmail(), list_accounts) - + def dispaly_name(self, contact): return str(contact) dispaly_name.short_description = _("Name") dispaly_name.admin_order_field = 'short_name' - + def formfield_for_dbfield(self, db_field, **kwargs): """ Make value input widget bigger """ if db_field.name == 'address': kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2}) if db_field.name == 'email_usage': - kwargs['widget'] = paddingCheckboxSelectMultiple(130) + kwargs['widget'] = PaddingCheckboxSelectMultiple(130) return super(ContactAdmin, self).formfield_for_dbfield(db_field, **kwargs) @@ -86,14 +86,14 @@ class ContactInline(admin.StackedInline): fields = ( ('short_name', 'full_name'), 'email', 'email_usage', ('phone', 'phone2'), ) - + def get_extra(self, request, obj=None, **kwargs): return 0 if obj and obj.contacts.exists() else 1 - + def get_view_on_site_url(self, obj=None): if obj: return change_url(obj) - + def formfield_for_dbfield(self, db_field, **kwargs): """ Make value input widget bigger """ if db_field.name == 'short_name': @@ -101,7 +101,7 @@ class ContactInline(admin.StackedInline): if db_field.name == 'address': kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2}) if db_field.name == 'email_usage': - kwargs['widget'] = paddingCheckboxSelectMultiple(45) + kwargs['widget'] = PaddingCheckboxSelectMultiple(45) return super(ContactInline, self).formfield_for_dbfield(db_field, **kwargs) diff --git a/orchestra/contrib/orchestration/forms.py b/orchestra/contrib/orchestration/forms.py index bd3f96ad..7ea33538 100644 --- a/orchestra/contrib/orchestration/forms.py +++ b/orchestra/contrib/orchestration/forms.py @@ -1,6 +1,6 @@ from django import forms -from orchestra.forms.widgets import SpanWidget, paddingCheckboxSelectMultiple +from orchestra.forms.widgets import SpanWidget, PaddingCheckboxSelectMultiple class RouteForm(forms.ModelForm): @@ -16,5 +16,5 @@ class RouteForm(forms.ModelForm): else: self.fields['backend'].widget = SpanWidget() actions = backend_class.actions - self.fields['async_actions'].widget = paddingCheckboxSelectMultiple(45) + self.fields['async_actions'].widget = PaddingCheckboxSelectMultiple(45) self.fields['async_actions'].choices = ((action, action) for action in actions) diff --git a/orchestra/forms/widgets.py b/orchestra/forms/widgets.py index 0235db61..74a0fd29 100644 --- a/orchestra/forms/widgets.py +++ b/orchestra/forms/widgets.py @@ -37,17 +37,17 @@ class SpanWidget(forms.Widget): return False -def paddingCheckboxSelectMultiple(padding): +class PaddingCheckboxSelectMultiple(forms.CheckboxSelectMultiple): """ Ugly hack to render this widget nicely on Django admin """ - widget = forms.CheckboxSelectMultiple() - old_render = widget.render + def __init__(self, padding, attrs=None, choices=()): + super().__init__(attrs=attrs, choices=choices) + self.padding = padding + def render(self, *args, **kwargs): - value = old_render(self, *args, **kwargs) + value = super().render(*args, **kwargs) value = re.sub(r'^
    ]+)>', - r'
      ' % padding, value, 1) + r'
        ' % self.padding, value, 1) return mark_safe(value) - widget.render = render - return widget class DynamicHelpTextSelect(forms.Select): From 13b4ac5eee66f11b034d7fe5fd1e3d14180ca34b Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 13 May 2021 14:42:05 +0200 Subject: [PATCH 07/21] Add required param `renderer` to ReadOnlyPasswordHashWidget --- orchestra/contrib/databases/forms.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/orchestra/contrib/databases/forms.py b/orchestra/contrib/databases/forms.py index 83e2895d..aa4c720a 100644 --- a/orchestra/contrib/databases/forms.py +++ b/orchestra/contrib/databases/forms.py @@ -17,11 +17,11 @@ class DatabaseUserCreationForm(forms.ModelForm): password2 = forms.CharField(label=_("Password confirmation"), required=False, widget=forms.PasswordInput, help_text=_("Enter the same password as above, for verification.")) - + class Meta: model = DatabaseUser fields = ('username', 'account', 'type') - + def clean_password2(self): password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") @@ -40,11 +40,11 @@ class DatabaseCreationForm(DatabaseUserCreationForm): 'invalid': _("This value may contain 16 characters or fewer, only letters, numbers and " "@/./+/-/_ characters.")}) user = forms.ModelChoiceField(required=False, queryset=DatabaseUser.objects) - + class Meta: model = Database fields = ('username', 'account', 'type') - + def __init__(self, *args, **kwargs): super(DatabaseCreationForm, self).__init__(*args, **kwargs) account_id = self.initial.get('account', self.initial_account) @@ -53,13 +53,13 @@ class DatabaseCreationForm(DatabaseUserCreationForm): choices = [ (u.pk, "%s (%s)" % (u, u.get_type_display())) for u in qs ] self.fields['user'].queryset = qs self.fields['user'].choices = [(None, '--------'),] + choices - + def clean_username(self): username = self.cleaned_data.get('username') if DatabaseUser.objects.filter(username=username).exists(): raise ValidationError("Provided username already exists.") return username - + def clean_password2(self): username = self.cleaned_data.get('username') password1 = self.cleaned_data.get('password1') @@ -70,14 +70,14 @@ class DatabaseCreationForm(DatabaseUserCreationForm): msg = _("The two password fields didn't match.") raise ValidationError(msg) return password2 - + def clean_user(self): user = self.cleaned_data.get('user') if user and user.type != self.cleaned_data.get('type'): msg = _("Database type and user type doesn't match") raise ValidationError(msg) return user - + def clean(self): cleaned_data = super(DatabaseCreationForm, self).clean() if 'user' in cleaned_data and 'username' in cleaned_data: @@ -91,7 +91,7 @@ class DatabaseCreationForm(DatabaseUserCreationForm): class ReadOnlySQLPasswordHashField(ReadOnlyPasswordHashField): class ReadOnlyPasswordHashWidget(forms.Widget): - def render(self, name, value, attrs): + def render(self, name, value, attrs, renderer=None): original = ReadOnlyPasswordHashField.widget().render(name, value, attrs) if 'Invalid' not in original: return original @@ -114,10 +114,10 @@ class DatabaseUserChangeForm(forms.ModelForm): "this user's password, but you can change the password " "using this form. " "Show hash.")) - + class Meta: model = DatabaseUser fields = ('username', 'password', 'type', 'account') - + def clean_password(self): return self.initial["password"] From a6c5aa32df188939e1c92d7da205a773836d8d5a Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 17 May 2021 12:54:16 +0200 Subject: [PATCH 08/21] Fix Mailbox creation. Direct assignment to the reverse side of a many-to-many set is prohibited. Use addresses.set() instead. --- orchestra/contrib/mailboxes/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orchestra/contrib/mailboxes/admin.py b/orchestra/contrib/mailboxes/admin.py index 0336c052..77e549f3 100644 --- a/orchestra/contrib/mailboxes/admin.py +++ b/orchestra/contrib/mailboxes/admin.py @@ -217,7 +217,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo elif obj.custom_filtering: messages.warning(request, msg) super(MailboxAdmin, self).save_model(request, obj, form, change) - obj.addresses = form.cleaned_data['addresses'] + obj.addresses.set(form.cleaned_data['addresses']) class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): From 3b4bb51925e70b02f96810ddbc8909d462428113 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 17 May 2021 13:20:18 +0200 Subject: [PATCH 09/21] Fix display format on SaaS & Sever admin list mark_safe generated HTML --- orchestra/contrib/orchestration/admin.py | 4 ++-- orchestra/contrib/saas/admin.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/orchestra/contrib/orchestration/admin.py b/orchestra/contrib/orchestration/admin.py index 60737d09..a5ddcb08 100644 --- a/orchestra/contrib/orchestration/admin.py +++ b/orchestra/contrib/orchestration/admin.py @@ -179,12 +179,12 @@ class ServerAdmin(ExtendedModelAdmin): change_view_actions = actions def display_ping(self, instance): - return self._remote_state[instance.pk][0] + return mark_safe(self._remote_state[instance.pk][0]) display_ping.short_description = _("Ping") display_ping.allow_tags = True def display_uptime(self, instance): - return self._remote_state[instance.pk][1] + return mark_safe(self._remote_state[instance.pk][1]) display_uptime.short_description = _("Uptime") display_uptime.allow_tags = True diff --git a/orchestra/contrib/saas/admin.py b/orchestra/contrib/saas/admin.py index b3b0739a..1d34dfad 100644 --- a/orchestra/contrib/saas/admin.py +++ b/orchestra/contrib/saas/admin.py @@ -1,5 +1,6 @@ from django.contrib import admin from django.core.exceptions import ObjectDoesNotExist +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin @@ -26,7 +27,8 @@ class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMi plugin_field = 'service' plugin_title = 'Software as a Service' actions = (disable, enable, list_accounts) - + + @mark_safe def display_url(self, saas): site_domain = saas.get_site_domain() site_link = '%s' % (site_domain, site_domain) @@ -48,7 +50,7 @@ class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMi display_url.short_description = _("URL") display_url.allow_tags = True display_url.admin_order_field = 'name' - + def get_fields(self, *args, **kwargs): fields = super(SaaSAdmin, self).get_fields(*args, **kwargs) if not self.plugin_instance.allow_custom_url: From b24ddf75464fe0e634734e4350fd23097ea1361a Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 17 May 2021 13:22:08 +0200 Subject: [PATCH 10/21] Handle empty ping response --- orchestra/contrib/orchestration/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/orchestra/contrib/orchestration/utils.py b/orchestra/contrib/orchestration/utils.py index 9e4dd51d..df59f8c9 100644 --- a/orchestra/contrib/orchestration/utils.py +++ b/orchestra/contrib/orchestration/utils.py @@ -14,7 +14,12 @@ def retrieve_state(servers): state = {} for server, ping, uptime in zip(servers, pings, uptimes): ping = join(ping, silent=True) - ping = ping.stdout.splitlines()[-1].decode() + + try: + ping = ping.stdout.splitlines()[-1].decode() + except IndexError: + ping = '' + if ping.startswith('rtt'): ping = '%s ms' % ping.split('/')[4] else: From f0683660aee98a5b6d3b2fb6890895af4a016fe7 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 17 May 2021 14:15:12 +0200 Subject: [PATCH 11/21] Fix display format on bills, orders & services Drop `allow_tags` attribute which has been removed on Django 2.0 --- orchestra/contrib/bills/admin.py | 1 - orchestra/contrib/orders/admin.py | 17 ++++++++--------- orchestra/contrib/services/admin.py | 4 ++-- orchestra/contrib/webapps/admin.py | 3 ++- orchestra/contrib/websites/admin.py | 7 ++++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py index 7ef0c78e..3d7b9534 100644 --- a/orchestra/contrib/bills/admin.py +++ b/orchestra/contrib/bills/admin.py @@ -80,7 +80,6 @@ class BillLineInline(admin.TabularInline): return '%s ' % (url, content, total, img) return '%s' % (url, total) display_total.short_description = _("Total") - display_total.allow_tags = True def formfield_for_dbfield(self, db_field, **kwargs): """ Make value input widget bigger """ diff --git a/orchestra/contrib/orders/admin.py b/orchestra/contrib/orders/admin.py index 79076067..43838e4f 100644 --- a/orchestra/contrib/orders/admin.py +++ b/orchestra/contrib/orders/admin.py @@ -1,9 +1,10 @@ +from datetime import datetime from django import forms from django.contrib import admin from django.urls import reverse, NoReverseMatch from django.db.models import Prefetch from django.utils import timezone -from django.utils.html import escape +from django.utils.html import escape, format_html from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -112,9 +113,8 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin): display_cancelled_on = admin_date('cancelled_on') def display_description(self, order): - return order.description[:64] + return format_html(order.description[:64]) display_description.short_description = _("Description") - display_description.allow_tags = True display_description.admin_order_field = 'description' def content_object_link(self, order): @@ -125,13 +125,13 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin): # Does not has admin return order.content_object_repr description = str(order.content_object) - return '{description}'.format( + return format_html('{description}', url=url, description=description) return order.content_object_repr content_object_link.short_description = _("Content object") - content_object_link.allow_tags = True content_object_link.admin_order_field = 'content_object_repr' + @mark_safe def bills_links(self, order): bills = [] make_link = admin_link() @@ -139,7 +139,6 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin): bills.append(make_link(line.bill)) return '
        '.join(bills) bills_links.short_description = _("Bills") - bills_links.allow_tags = True def display_billed_until(self, order): billed_until = order.billed_until @@ -156,12 +155,12 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin): red = True elif billed_until < timezone.now().date(): red = True - color = 'style="color:red;"' if red else '' - return '{human}'.format( + color = mark_safe('style="color:red;"') if red else '' + return format_html( + '{human}', raw=escape(str(billed_until)), color=color, human=human, ) display_billed_until.short_description = _("billed until") - display_billed_until.allow_tags = True display_billed_until.admin_order_field = 'billed_until' def display_metric(self, order): diff --git a/orchestra/contrib/services/admin.py b/orchestra/contrib/services/admin.py index b8e20634..ecbe1e2d 100644 --- a/orchestra/contrib/services/admin.py +++ b/orchestra/contrib/services/admin.py @@ -4,6 +4,7 @@ from django.contrib import admin from django.urls import reverse from django.template.response import TemplateResponse from django.utils import timezone +from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ChangeViewActionsMixin @@ -69,10 +70,9 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin): num = service.orders__count url = reverse('admin:orders_order_changelist') url += '?service__id__exact=%i&is_active=True' % service.pk - return '%d' % (url, num) + return format_html('{}', url, num) num_orders.short_description = _("Orders") num_orders.admin_order_field = 'orders__count' - num_orders.allow_tags = True def get_queryset(self, request): qs = super(ServiceAdmin, self).get_queryset(request) diff --git a/orchestra/contrib/webapps/admin.py b/orchestra/contrib/webapps/admin.py index 9d93ee6a..12cacb2c 100644 --- a/orchestra/contrib/webapps/admin.py +++ b/orchestra/contrib/webapps/admin.py @@ -2,6 +2,7 @@ from django import forms from django.contrib import admin from django.urls import reverse from django.utils.encoding import force_text +from django.utils.safestring import mark_safe from django.utils.translation import ugettext, ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin @@ -66,6 +67,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) display_type = display_plugin_field('type') + @mark_safe def display_websites(self, webapp): websites = [] for content in webapp.content_set.all(): @@ -82,7 +84,6 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) websites.append('%s%s' % (add_url, plus, ugettext("Add website"))) return '
        '.join(websites) display_websites.short_description = _("web sites") - display_websites.allow_tags = True def display_detail(self, webapp): try: diff --git a/orchestra/contrib/websites/admin.py b/orchestra/contrib/websites/admin.py index 8e597d86..a6a68c55 100644 --- a/orchestra/contrib/websites/admin.py +++ b/orchestra/contrib/websites/admin.py @@ -3,6 +3,7 @@ from django.contrib import admin from django.urls import resolve from django.db.models import Q from django.utils.encoding import force_text +from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -87,9 +88,9 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): domains.append('%s' % (url, url)) return '
        '.join(domains) display_domains.short_description = _("domains") - display_domains.allow_tags = True display_domains.admin_order_field = 'domains' + @mark_safe def display_webapps(self, website): webapps = [] for content in website.content_set.all(): @@ -102,9 +103,9 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): pass url = change_url(webapp) name = "%s on %s" % (webapp.name, content.path or '/') - webapps.append('%s %s' % (url, detail, name, site_link)) + webapp_info = format_html('{} {}', url, detail, name, site_link) + webapps.append(webapp_info) return '
        '.join(webapps) - display_webapps.allow_tags = True display_webapps.short_description = _("Web apps") def formfield_for_dbfield(self, db_field, **kwargs): From f13fea50303eea8ee811a591174214071354b6b6 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 20 May 2021 13:58:16 +0200 Subject: [PATCH 12/21] Fix display format on accounts, databases... domains, mailboxes & mailer Drop `allow_tags` attribute which has been removed on Django 2.0 --- orchestra/contrib/accounts/admin.py | 1 - orchestra/contrib/databases/admin.py | 20 ++++++------ orchestra/contrib/domains/admin.py | 16 ++++------ orchestra/contrib/mailboxes/admin.py | 48 +++++++++++++--------------- orchestra/contrib/mailer/admin.py | 11 +++---- 5 files changed, 44 insertions(+), 52 deletions(-) diff --git a/orchestra/contrib/accounts/admin.py b/orchestra/contrib/accounts/admin.py index a046a4a5..2b9a4773 100644 --- a/orchestra/contrib/accounts/admin.py +++ b/orchestra/contrib/accounts/admin.py @@ -216,7 +216,6 @@ class AccountAdminMixin(object): return 'False' % (static('admin/img/inline-delete.svg'), msg) return 'False' % static('admin/img/icon-yes.svg') display_active.short_description = _("active") - display_active.allow_tags = True display_active.admin_order_field = 'is_active' def account_link(self, instance): diff --git a/orchestra/contrib/databases/admin.py b/orchestra/contrib/databases/admin.py index dc56f9db..d50c23d4 100644 --- a/orchestra/contrib/databases/admin.py +++ b/orchestra/contrib/databases/admin.py @@ -1,6 +1,8 @@ from django.conf.urls import url from django.contrib import admin from django.contrib.auth.admin import UserAdmin +from django.utils.html import format_html +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin @@ -49,17 +51,17 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): filter_by_account_fields = ('users',) list_prefetch_related = ('users',) actions = (list_accounts, save_selected) - + + @mark_safe def display_users(self, db): links = [] for user in db.users.all(): - link = '%s' % (change_url(user), user.username) + link = format_html('{}', change_url(user), user.username) links.append(link) return '
        '.join(links) display_users.short_description = _("Users") - display_users.allow_tags = True display_users.admin_order_field = 'users__username' - + def save_model(self, request, obj, form, change): super(DatabaseAdmin, self).save_model(request, obj, form, change) if not change: @@ -98,24 +100,24 @@ class DatabaseUserAdmin(SelectAccountAdminMixin, ChangePasswordAdminMixin, Exten filter_by_account_fields = ('databases',) list_prefetch_related = ('databases',) actions = (list_accounts, save_selected) - + + @mark_safe def display_databases(self, user): links = [] for db in user.databases.all(): - link = '%s' % (change_url(db), db.name) + link = format_html('{}', change_url(db), db.name) links.append(link) return '
        '.join(links) display_databases.short_description = _("Databases") - display_databases.allow_tags = True display_databases.admin_order_field = 'databases__name' - + def get_urls(self): useradmin = UserAdmin(DatabaseUser, self.admin_site) return [ url(r'^(\d+)/password/$', self.admin_site.admin_view(useradmin.user_change_password)) ] + super(DatabaseUserAdmin, self).get_urls() - + def save_model(self, request, obj, form, change): """ set password """ if not change: diff --git a/orchestra/contrib/domains/admin.py b/orchestra/contrib/domains/admin.py index f20849dd..a1dd6141 100644 --- a/orchestra/contrib/domains/admin.py +++ b/orchestra/contrib/domains/admin.py @@ -74,9 +74,8 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin): def structured_name(self, domain): if domain.is_top: return domain.name - return ' '*4 + domain.name + return mark_safe(' '*4 + domain.name) structured_name.short_description = _("name") - structured_name.allow_tags = True structured_name.admin_order_field = 'structured_name' def display_is_top(self, domain): @@ -101,15 +100,14 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin): return '
        '.join(links) add_url = reverse('admin:websites_website_add') add_url += '?account=%i&domains=%i' % (domain.account_id, domain.pk) - image = '' % static('orchestra/images/add.png') - add_link = '%s' % ( - add_url, _("Add website"), image + add_link = format_html( + '', add_url, + _("Add website"), static('orchestra/images/add.png'), ) return _("No website %s") % (add_link) return '---' display_websites.admin_order_field = 'websites__name' display_websites.short_description = _("Websites") - display_websites.allow_tags = True @mark_safe def display_addresses(self, domain): @@ -130,10 +128,9 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin): return '---' display_addresses.short_description = _("Addresses") display_addresses.admin_order_field = 'addresses__count' - display_addresses.allow_tags = True + @mark_safe def implicit_records(self, domain): - defaults = [] types = set(domain.records.values_list('type', flat=True)) ttl = settings.DOMAINS_DEFAULT_TTL lines = [] @@ -145,14 +142,13 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin): value=record.value ) if not domain.record_is_implicit(record, types): - line = '%s' % line + line = format_html('{}', line) if record.type is Record.SOA: lines.insert(0, line) else: lines.append(line) return '
        '.join(lines) implicit_records.short_description = _("Implicit records") - implicit_records.allow_tags = True def get_fieldsets(self, request, obj=None): """ Add SOA fields when domain is top """ diff --git a/orchestra/contrib/mailboxes/admin.py b/orchestra/contrib/mailboxes/admin.py index 77e549f3..f1b54feb 100644 --- a/orchestra/contrib/mailboxes/admin.py +++ b/orchestra/contrib/mailboxes/admin.py @@ -6,6 +6,7 @@ from django.contrib import admin, messages from django.urls import reverse from django.db.models import F, Count, Value as V from django.db.models.functions import Concat +from django.utils.html import format_html, format_html_join from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -82,6 +83,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo if settings.MAILBOXES_LOCAL_DOMAIN: type(self).actions = self.actions + (SendMailboxEmail(),) + @mark_safe def display_addresses(self, mailbox): # Get from forwards cache = caches.get_request_cache() @@ -93,7 +95,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo qs = qs.values_list('id', 'email', 'forward') for addr_id, email, mbox in qs: url = reverse('admin:mailboxes_address_change', args=(addr_id,)) - link = '%s' % (url, email) + link = format_html('{}', url, email) try: cached_forwards[mbox].append(link) except KeyError: @@ -107,26 +109,23 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo addresses = [] for addr in mailbox.addresses.all(): url = change_url(addr) - addresses.append('%s' % (url, addr.email)) + addresses.append(format_html('{}', url, addr.email)) return '
        '.join(addresses+forwards) display_addresses.short_description = _("Addresses") - display_addresses.allow_tags = True def display_forwards(self, mailbox): - forwards = [] - for addr in mailbox.get_forwards(): - url = change_url(addr) - forwards.append('%s' % (url, addr.email)) - return '
        '.join(forwards) + forwards = mailbox.get_forwards() + return format_html_join( + '
        ', '{}', + [(change_url(addr), addr.email) for addr in forwards] + ) display_forwards.short_description = _("Forward from") - display_forwards.allow_tags = True + @mark_safe def display_filtering(self, mailbox): - """ becacuse of allow_tags = True """ return mailbox.get_filtering_display() display_filtering.short_description = _("Filtering") display_filtering.admin_order_field = 'filtering' - display_filtering.allow_tags = True def formfield_for_dbfield(self, db_field, **kwargs): if db_field.name == 'filtering': @@ -247,29 +246,27 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): def email_link(self, address): link = self.domain_link(address) - return "%s@%s" % (address.name, link) + return format_html("{}@{}", address.name, link) email_link.short_description = _("Email") - email_link.allow_tags = True def display_mailboxes(self, address): - boxes = [] - for mailbox in address.mailboxes.all(): - url = change_url(mailbox) - boxes.append('%s' % (url, mailbox.name)) - return '
        '.join(boxes) + boxes = address.mailboxes.all() + return format_html_join( + '
        ', '{}', + [(change_url(mailbox), mailbox.name) for mailbox in boxes] + ) display_mailboxes.short_description = _("Mailboxes") - display_mailboxes.allow_tags = True display_mailboxes.admin_order_field = 'mailboxes__count' def display_all_mailboxes(self, address): - boxes = [] - for mailbox in address.get_mailboxes(): - url = change_url(mailbox) - boxes.append('%s' % (url, mailbox.name)) - return '
        '.join(boxes) + boxes = address.get_mailboxes() + return format_html_join( + '
        ', '{}', + [(change_url(mailbox), mailbox.name) for mailbox in boxes] + ) display_all_mailboxes.short_description = _("Mailboxes links") - display_all_mailboxes.allow_tags = True + @mark_safe def display_forward(self, address): forward_mailboxes = {m.name: m for m in address.get_forward_mailboxes()} values = [] @@ -281,7 +278,6 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): values.append(forward) return '
        '.join(values) display_forward.short_description = _("Forward") - display_forward.allow_tags = True display_forward.admin_order_field = 'forward' def formfield_for_dbfield(self, db_field, **kwargs): diff --git a/orchestra/contrib/mailer/admin.py b/orchestra/contrib/mailer/admin.py index 1eaabad0..3e7371d5 100644 --- a/orchestra/contrib/mailer/admin.py +++ b/orchestra/contrib/mailer/admin.py @@ -6,6 +6,8 @@ from django.contrib import admin from django.urls import reverse from django.db.models import Count from django.shortcuts import redirect +from django.utils.html import format_html +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin @@ -60,11 +62,10 @@ class MessageAdmin(ExtendedModelAdmin): def display_subject(self, instance): subject = instance.subject if len(subject) > 64: - return subject[:64] + '…' + return mark_safe(subject[:64] + '…') return subject display_subject.short_description = _("Subject") display_subject.admin_order_field = 'subject' - display_subject.allow_tags = True def display_retries(self, instance): num_logs = instance.logs__count @@ -74,10 +75,9 @@ class MessageAdmin(ExtendedModelAdmin): else: url = reverse('admin:mailer_smtplog_changelist') url += '?&message=%i' % instance.pk - return '%d' % (url, instance.retries) + return format_html('{}', url, instance.retries) display_retries.short_description = _("Retries") display_retries.admin_order_field = 'retries' - display_retries.allow_tags = True def display_content(self, instance): part = email.message_from_string(instance.content) @@ -99,9 +99,8 @@ class MessageAdmin(ExtendedModelAdmin): payload = payload.decode(charset) if part.get_content_type() == 'text/plain': payload = payload.replace('\n', '
        ').replace(' ', ' ') - return payload + return mark_safe(payload) display_content.short_description = _("Content") - display_content.allow_tags = True def display_full_subject(self, instance): return instance.subject From a2927f761606ce5df50a484d365a2f65252946ac Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 20 May 2021 14:02:10 +0200 Subject: [PATCH 13/21] Add required param `renderer` to MarkDownWidget --- orchestra/contrib/issues/forms.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/orchestra/contrib/issues/forms.py b/orchestra/contrib/issues/forms.py index 30de94f6..292c85aa 100644 --- a/orchestra/contrib/issues/forms.py +++ b/orchestra/contrib/issues/forms.py @@ -13,7 +13,7 @@ from .models import Queue, Ticket class MarkDownWidget(forms.Textarea): """ MarkDown textarea widget with syntax preview """ - + markdown_url = static('issues/markdown_syntax.html') markdown_help_text = ( 'markdown format' % (markdown_url, markdown_url) ) markdown_help_text = 'HTML not allowed, you can use %s' % markdown_help_text - - def render(self, name, value, attrs): + + def render(self, name, value, attrs, renderer=None): widget_id = attrs['id'] if attrs and 'id' in attrs else 'id_%s' % name textarea = super(MarkDownWidget, self).render(name, value, attrs) preview = ('preview'\ @@ -35,18 +35,18 @@ class MessageInlineForm(forms.ModelForm): """ Add message form """ created_on = forms.CharField(label="Created On", required=False) content = forms.CharField(widget=MarkDownWidget(), required=False) - + class Meta: fields = ('author', 'author_name', 'created_on', 'content') - + def __init__(self, *args, **kwargs): super(MessageInlineForm, self).__init__(*args, **kwargs) self.fields['created_on'].widget = SpanWidget(display='') - + def clean_content(self): """ clean HTML tags """ return strip_tags(self.cleaned_data['content']) - + def save(self, *args, **kwargs): if self.instance.pk is None: self.instance.author = self.user @@ -58,7 +58,7 @@ class UsersIterator(forms.models.ModelChoiceIterator): def __init__(self, *args, **kwargs): self.ticket = kwargs.pop('ticket', False) super(forms.models.ModelChoiceIterator, self).__init__(*args, **kwargs) - + def __iter__(self): yield ('', '---------') users = get_user_model().objects.exclude(is_active=False).order_by('name') @@ -73,14 +73,14 @@ class UsersIterator(forms.models.ModelChoiceIterator): class TicketForm(forms.ModelForm): display_description = forms.CharField(label=_("Description"), required=False) description = forms.CharField(widget=MarkDownWidget(attrs={'class':'vLargeTextField'})) - + class Meta: model = Ticket fields = ( 'creator', 'creator_name', 'owner', 'queue', 'subject', 'description', 'priority', 'state', 'cc', 'display_description' ) - + def __init__(self, *args, **kwargs): super(TicketForm, self).__init__(*args, **kwargs) ticket = kwargs.get('instance', False) @@ -101,7 +101,7 @@ class TicketForm(forms.ModelForm): description = '
        %s
        ' % description widget = SpanWidget(display=description) self.fields['display_description'].widget = widget - + def clean_description(self): """ clean HTML tags """ return strip_tags(self.cleaned_data['description']) From 6d8a2ced5398c46f03cbab620bf4dfb450969a22 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Thu, 20 May 2021 14:08:09 +0200 Subject: [PATCH 14/21] Context shoud be dict on render_email_template() template.Context intance is no longer accepted --- orchestra/utils/mail.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/orchestra/utils/mail.py b/orchestra/utils/mail.py index caa4589e..25b256bf 100644 --- a/orchestra/utils/mail.py +++ b/orchestra/utils/mail.py @@ -10,12 +10,9 @@ def render_email_template(template, context): Renders an email template with this format: {% if subject %}Subject{% endif %} {% if message %}Email body{% endif %} - - context can be a dictionary or a template.Context instance + + context must be a dict """ - if isinstance(context, dict): - context = Context(context) - if not 'site' in context: from orchestra import settings url = urlparse(settings.ORCHESTRA_SITE_URL) From e6495a967bffde0f9367573bb3bbb652b24b01df Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Fri, 21 May 2021 10:07:59 +0200 Subject: [PATCH 15/21] Handle HTML safe rendering on issues, plans & saas Drop `allow_tags` attribute which has been removed on Django 2.0 --- orchestra/contrib/issues/admin.py | 20 ++++++++++---------- orchestra/contrib/plans/admin.py | 4 ++-- orchestra/contrib/saas/admin.py | 1 - 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/orchestra/contrib/issues/admin.py b/orchestra/contrib/issues/admin.py index 5dcc32ac..66881ddd 100644 --- a/orchestra/contrib/issues/admin.py +++ b/orchestra/contrib/issues/admin.py @@ -5,7 +5,8 @@ from django.urls import reverse from django.db import models from django.http import HttpResponse from django.shortcuts import get_object_or_404 -from django.utils.html import strip_tags +from django.utils.html import format_html, strip_tags +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from markdown import markdown @@ -50,6 +51,7 @@ class MessageReadOnlyInline(admin.TabularInline): 'all': ('orchestra/css/hide-inline-id.css',) } + @mark_safe def content_html(self, msg): context = { 'number': msg.number, @@ -58,12 +60,13 @@ class MessageReadOnlyInline(admin.TabularInline): } summary = _("#%(number)i Updated by %(author)s about %(time)s") % context header = '%s
        ' % summary + content = markdown(msg.content) content = content.replace('>\n', '>') content = '
        %s
        ' % content + return header + content content_html.short_description = _("Content") - content_html.allow_tags = True def has_add_permission(self, request): return False @@ -111,10 +114,10 @@ class TicketInline(admin.TabularInline): colored_state = admin_colored('state', colors=STATE_COLORS, bold=False) colored_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False) + @mark_safe def ticket_id(self, instance): return '%s' % admin_link()(instance) ticket_id.short_description = '#' - ticket_id.allow_tags = True class TicketAdmin(ExtendedModelAdmin): @@ -192,6 +195,7 @@ class TicketAdmin(ExtendedModelAdmin): display_state = admin_colored('state', colors=STATE_COLORS, bold=False) display_priority = admin_colored('priority', colors=PRIORITY_COLORS, bold=False) + @mark_safe def display_summary(self, ticket): context = { 'creator': admin_link('creator')(self, ticket) if ticket.creator else ticket.creator_name, @@ -207,14 +211,12 @@ class TicketAdmin(ExtendedModelAdmin): context['updated'] = '. Updated by %(updater)s about %(updated)s' % context return '

        Added by %(creator)s about %(created)s%(updated)s

        ' % context display_summary.short_description = 'Summary' - display_summary.allow_tags = True def unbold_id(self, ticket): """ Unbold id if ticket is read """ if ticket.is_read_by(self.user): - return '%s' % ticket.pk + return format_html('{}', ticket.pk) return ticket.pk - unbold_id.allow_tags = True unbold_id.short_description = "#" unbold_id.admin_order_field = 'id' @@ -222,8 +224,7 @@ class TicketAdmin(ExtendedModelAdmin): """ Bold subject when tickets are unread for request.user """ if ticket.is_read_by(self.user): return ticket.subject - return "%s" % ticket.subject - bold_subject.allow_tags = True + return format_html("{}", ticket.subject) bold_subject.short_description = _("Subject") bold_subject.admin_order_field = 'subject' @@ -297,10 +298,9 @@ class QueueAdmin(admin.ModelAdmin): num = queue.tickets__count url = reverse('admin:issues_ticket_changelist') url += '?queue=%i' % queue.pk - return '%d' % (url, num) + return format_html('{}', url, num) num_tickets.short_description = _("Tickets") num_tickets.admin_order_field = 'tickets__count' - num_tickets.allow_tags = True def get_list_display(self, request): """ show notifications """ diff --git a/orchestra/contrib/plans/admin.py b/orchestra/contrib/plans/admin.py index c283d5e4..df75ca24 100644 --- a/orchestra/contrib/plans/admin.py +++ b/orchestra/contrib/plans/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from django.urls import reverse from django.db import models +from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin @@ -33,10 +34,9 @@ class PlanAdmin(ExtendedModelAdmin): num = plan.contracts__count url = reverse('admin:plans_contractedplan_changelist') url += '?plan__name={}'.format(plan.name) - return '{1}'.format(url, num) + return format_html('{1}', url, num) num_contracts.short_description = _("Contracts") num_contracts.admin_order_field = 'contracts__count' - num_contracts.allow_tags = True def get_queryset(self, request): qs = super(PlanAdmin, self).get_queryset(request) diff --git a/orchestra/contrib/saas/admin.py b/orchestra/contrib/saas/admin.py index 1d34dfad..cdf5088c 100644 --- a/orchestra/contrib/saas/admin.py +++ b/orchestra/contrib/saas/admin.py @@ -48,7 +48,6 @@ class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMi links.append(link) return '
        '.join(links) display_url.short_description = _("URL") - display_url.allow_tags = True display_url.admin_order_field = 'name' def get_fields(self, *args, **kwargs): From 4f695c2e6ebe4441387641da6b71989bf5c071dd Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Fri, 21 May 2021 10:47:27 +0200 Subject: [PATCH 16/21] Handle HTML safe rendering on orchestration, resources & history Drop `allow_tags` attribute which has been removed on Django 2.0 --- orchestra/contrib/history/admin.py | 23 ++++++++++++----------- orchestra/contrib/orchestration/admin.py | 10 +++------- orchestra/contrib/resources/admin.py | 9 ++++----- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/orchestra/contrib/history/admin.py b/orchestra/contrib/history/admin.py index ebd80e46..bc32e734 100644 --- a/orchestra/contrib/history/admin.py +++ b/orchestra/contrib/history/admin.py @@ -1,12 +1,14 @@ from django.contrib import admin -from django.utils.translation import ugettext_lazy as _ -from django.urls import reverse, NoReverseMatch -from django.contrib.admin.templatetags.admin_urls import add_preserved_filters -from django.http import HttpResponseRedirect -from django.contrib.admin.utils import unquote from django.contrib.admin.templatetags.admin_static import static +from django.contrib.admin.templatetags.admin_urls import add_preserved_filters +from django.contrib.admin.utils import unquote +from django.http import HttpResponseRedirect +from django.urls import NoReverseMatch, reverse +from django.utils.html import format_html +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ -from orchestra.admin.utils import admin_link, admin_date +from orchestra.admin.utils import admin_date, admin_link class LogEntryAdmin(admin.ModelAdmin): @@ -34,11 +36,12 @@ class LogEntryAdmin(admin.ModelAdmin): user_link = admin_link('user') display_action_time = admin_date('action_time', short_description=_("Time")) + @mark_safe def display_message(self, log): - edit = '' % { + edit = format_html('', **{ 'url': reverse('admin:admin_logentry_change', args=(log.pk,)), 'img': static('admin/img/icon-changelink.svg'), - } + }) if log.is_addition(): return _('Added "%(link)s". %(edit)s') % { 'link': self.content_object_link(log), @@ -57,7 +60,6 @@ class LogEntryAdmin(admin.ModelAdmin): } display_message.short_description = _("Message") display_message.admin_order_field = 'action_flag' - display_message.allow_tags = True def display_action(self, log): if log.is_addition(): @@ -75,10 +77,9 @@ class LogEntryAdmin(admin.ModelAdmin): url = reverse(view, args=(log.object_id,)) except NoReverseMatch: return log.object_repr - return '%s' % (url, log.object_repr) + return format_html('{}', url, log.object_repr) content_object_link.short_description = _("Content object") content_object_link.admin_order_field = 'object_repr' - content_object_link.allow_tags = True def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): """ Add rel_opts and object to context """ diff --git a/orchestra/contrib/orchestration/admin.py b/orchestra/contrib/orchestration/admin.py index a5ddcb08..4f5ddc4b 100644 --- a/orchestra/contrib/orchestration/admin.py +++ b/orchestra/contrib/orchestration/admin.py @@ -51,19 +51,18 @@ class RouteAdmin(ExtendedModelAdmin): def display_model(self, route): try: - return escape(route.backend_class.model) + return route.backend_class.model except KeyError: - return "NOT AVAILABLE" + return mark_safe("NOT AVAILABLE") display_model.short_description = _("model") - display_model.allow_tags = True + @mark_safe def display_actions(self, route): try: return '
        '.join(route.backend_class.get_actions()) except KeyError: return "NOT AVAILABLE" display_actions.short_description = _("actions") - display_actions.allow_tags = True def formfield_for_dbfield(self, db_field, **kwargs): """ Provides dynamic help text on backend form field """ @@ -120,7 +119,6 @@ class BackendOperationInline(admin.TabularInline): return _("Deleted {0}").format(operation.instance_repr or '-'.join( (escape(operation.content_type), escape(operation.object_id)))) return link - instance_link.allow_tags = True instance_link.short_description = _("Instance") def has_add_permission(self, *args, **kwargs): @@ -181,12 +179,10 @@ class ServerAdmin(ExtendedModelAdmin): def display_ping(self, instance): return mark_safe(self._remote_state[instance.pk][0]) display_ping.short_description = _("Ping") - display_ping.allow_tags = True def display_uptime(self, instance): return mark_safe(self._remote_state[instance.pk][1]) display_uptime.short_description = _("Uptime") - display_uptime.allow_tags = True def get_queryset(self, request): """ Order by structured name and imporve performance """ diff --git a/orchestra/contrib/resources/admin.py b/orchestra/contrib/resources/admin.py index f758851e..b965c4ee 100644 --- a/orchestra/contrib/resources/admin.py +++ b/orchestra/contrib/resources/admin.py @@ -11,6 +11,7 @@ from django.db.models import Q from django.shortcuts import redirect from django.templatetags.static import static from django.utils.functional import cached_property +from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import ungettext, ugettext_lazy as _ @@ -105,10 +106,9 @@ class ResourceAdmin(ExtendedModelAdmin): def content_object_link(data): ct = data.content_type url = reverse('admin:%s_%s_change' % (ct.app_label, ct.model), args=(data.object_id,)) - return '%s' % (url, data.content_object_repr) + return format_html('{}', url, data.content_object_repr) content_object_link.short_description = _("Content object") content_object_link.admin_order_field = 'content_object_repr' -content_object_link.allow_tags = True class ResourceDataAdmin(ExtendedModelAdmin): @@ -155,10 +155,9 @@ class ResourceDataAdmin(ExtendedModelAdmin): if rdata.used is None: return '' url = reverse('admin:resources_resourcedata_used_monitordata', args=(rdata.pk,)) - return '%s %s' % (url, rdata.used, rdata.unit) + return format_html('{} {}', url, rdata.used, rdata.unit) display_used.short_description = _("Used") display_used.admin_order_field = 'used' - display_used.allow_tags = True def has_add_permission(self, *args, **kwargs): return False @@ -304,6 +303,7 @@ def resource_inline_factory(resources): self.verbose_name_plural = mark_safe(_("Resources") + ' ' + link) return super(ResourceInline, self).get_fieldsets(request, obj) + @mark_safe def display_used(self, rdata): update = '' history = '' @@ -329,7 +329,6 @@ def resource_inline_factory(resources): return _("Unknonw %s %s") % (update, history) return _("No monitor") display_used.short_description = _("Used") - display_used.allow_tags = True def has_add_permission(self, *args, **kwargs): """ Hidde add another """ From 06c226d302812d856c9cf81af743de94260b5bd5 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Fri, 21 May 2021 11:17:06 +0200 Subject: [PATCH 17/21] Handle HTML safe rendering on webapps & miscellaneous Drop `allow_tags` attribute which has been removed on Django 2.0 --- orchestra/contrib/miscellaneous/admin.py | 7 +++---- orchestra/contrib/webapps/admin.py | 17 +---------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/orchestra/contrib/miscellaneous/admin.py b/orchestra/contrib/miscellaneous/admin.py index 2f7d699c..ff4920e6 100644 --- a/orchestra/contrib/miscellaneous/admin.py +++ b/orchestra/contrib/miscellaneous/admin.py @@ -2,6 +2,7 @@ from django import forms from django.contrib import admin from django.urls import reverse from django.db import models +from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -38,15 +39,13 @@ class MiscServiceAdmin(ExtendedModelAdmin): actions = (disable, enable) def display_name(self, misc): - return '%s' % (misc.description, misc.name) + return format_html('{}', misc.description, misc.name) display_name.short_description = _("name") - display_name.allow_tags = True display_name.admin_order_field = 'name' def display_verbose_name(self, misc): - return '%s' % (misc.description, misc.verbose_name) + return format_html('{}', misc.description, misc.verbose_name) display_verbose_name.short_description = _("verbose name") - display_verbose_name.allow_tags = True display_verbose_name.admin_order_field = 'verbose_name' def num_instances(self, misc): diff --git a/orchestra/contrib/webapps/admin.py b/orchestra/contrib/webapps/admin.py index 12cacb2c..29247079 100644 --- a/orchestra/contrib/webapps/admin.py +++ b/orchestra/contrib/webapps/admin.py @@ -89,23 +89,8 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) try: return webapp.type_instance.get_detail() except KeyError: - return "Not available" + return mark_safe("Not available") display_detail.short_description = _("detail") - display_detail.allow_tags = True -# def get_form(self, request, obj=None, **kwargs): -# form = super(WebAppAdmin, self).get_form(request, obj, **kwargs) -# if obj: -# - -# def formfield_for_dbfield(self, db_field, **kwargs): -# """ Make value input widget bigger """ -# if db_field.name == 'type': -# # Help text based on select widget -# kwargs['widget'] = DynamicHelpTextSelect( -# 'this.id.replace("name", "value")', self.TYPE_HELP_TEXT -# ) -# kwargs['help_text'] = self.TYPE_HELP_TEXT.get(db_field.default, '') -# return super(WebAppAdmin, self).formfield_for_dbfield(db_field, **kwargs) admin.site.register(WebApp, WebAppAdmin) From 9953124a95ed051e1d21734517f998e268459e2c Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 24 May 2021 11:19:30 +0200 Subject: [PATCH 18/21] Replace Context by dict Since Django 1.10 template objects returned by get_template() and select_template() no longer accept a Context in their render() method. --- orchestra/admin/forms.py | 28 ++++---- orchestra/contrib/bills/models.py | 6 +- orchestra/contrib/webapps/backends/php.py | 42 ++++++------ orchestra/contrib/websites/backends/apache.py | 64 +++++++++---------- orchestra/utils/mail.py | 1 - 5 files changed, 70 insertions(+), 71 deletions(-) diff --git a/orchestra/admin/forms.py b/orchestra/admin/forms.py index 236b057c..2490873f 100644 --- a/orchestra/admin/forms.py +++ b/orchestra/admin/forms.py @@ -5,7 +5,7 @@ from django import forms from django.contrib.admin import helpers from django.core import validators from django.forms.models import modelformset_factory, BaseModelFormSet -from django.template import Template, Context +from django.template import Template from django.utils.translation import ugettext_lazy as _ from orchestra.forms.widgets import SpanWidget @@ -28,9 +28,9 @@ class AdminFormMixin(object): ' {% include "admin/includes/fieldset.html" %}' '{% endfor %}' ) - context = Context({ + context = { 'adminform': adminform - }) + } return template.render(context) @@ -71,9 +71,9 @@ class AdminFormSet(BaseModelFormSet): """) ) - context = Context({ + context = { 'formset': self - }) + } return template.render(context) @@ -93,7 +93,7 @@ class AdminPasswordChangeForm(forms.Form): required=False, validators=[validate_password]) password2 = forms.CharField(label=_("Password (again)"), widget=forms.PasswordInput, required=False) - + def __init__(self, user, *args, **kwargs): self.related = kwargs.pop('related', []) self.raw = kwargs.pop('raw', False) @@ -109,7 +109,7 @@ class AdminPasswordChangeForm(forms.Form): self.fields['password2_%i' % ix] = forms.CharField(label=_("Password (again)"), widget=forms.PasswordInput, required=False) setattr(self, 'clean_password2_%i' % ix, partial(self.clean_password2, ix=ix)) - + def clean_password2(self, ix=''): if ix != '': ix = '_%i' % ix @@ -129,7 +129,7 @@ class AdminPasswordChangeForm(forms.Form): code='password_mismatch', ) return password2 - + def clean_password(self, ix=''): if ix != '': ix = '_%i' % ix @@ -146,14 +146,14 @@ class AdminPasswordChangeForm(forms.Form): code='bad_hash', ) return password - + def clean(self): if not self.password_provided: raise forms.ValidationError( self.error_messages['password_missing'], code='password_missing', ) - + def save(self, commit=True): """ Saves the new password. @@ -182,7 +182,7 @@ class AdminPasswordChangeForm(forms.Form): if commit: rel.save(update_fields=['password']) return self.user - + def _get_changed_data(self): data = super().changed_data for name in self.fields.keys(): @@ -202,7 +202,7 @@ class SendEmailForm(forms.Form): widget=forms.TextInput(attrs={'size': '118'})) message = forms.CharField(label=_("Message"), widget=forms.Textarea(attrs={'cols': 118, 'rows': 15})) - + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) initial = kwargs.get('initial') @@ -210,7 +210,7 @@ class SendEmailForm(forms.Form): self.fields['to'].widget = SpanWidget(original=initial['to']) else: self.fields.pop('to') - + def clean_comma_separated_emails(self, value): clean_value = [] for email in value.split(','): @@ -222,7 +222,7 @@ class SendEmailForm(forms.Form): raise validators.ValidationError("Comma separated email addresses.") clean_value.append(email) return clean_value - + def clean_extra_to(self): extra_to = self.cleaned_data['extra_to'] return self.clean_comma_separated_emails(extra_to) diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py index b39661a9..90765ca3 100644 --- a/orchestra/contrib/bills/models.py +++ b/orchestra/contrib/bills/models.py @@ -6,7 +6,7 @@ from django.core.validators import ValidationError, RegexValidator from django.db import models from django.db.models import F, Sum from django.db.models.functions import Coalesce -from django.template import loader, Context +from django.template import loader from django.utils import timezone, translation from django.utils.encoding import force_text from django.utils.functional import cached_property @@ -303,7 +303,7 @@ class Bill(models.Model): with translation.override(language or self.account.language): if payment is False: payment = self.account.paymentsources.get_default() - context = Context({ + context = { 'bill': self, 'lines': self.lines.all().prefetch_related('sublines'), 'seller': self.seller, @@ -318,7 +318,7 @@ class Bill(models.Model): 'payment': payment and payment.get_bill_context(), 'default_due_date': self.get_due_date(payment=payment), 'now': timezone.now(), - }) + } template_name = 'BILLS_%s_TEMPLATE' % self.get_type() template = getattr(settings, template_name, settings.BILLS_DEFAULT_TEMPLATE) bill_template = loader.get_template(template) diff --git a/orchestra/contrib/webapps/backends/php.py b/orchestra/contrib/webapps/backends/php.py index d0383beb..360f3eb1 100644 --- a/orchestra/contrib/webapps/backends/php.py +++ b/orchestra/contrib/webapps/backends/php.py @@ -2,7 +2,7 @@ import os import textwrap from collections import OrderedDict -from django.template import Template, Context +from django.template import Template from django.utils.translation import ugettext_lazy as _ from orchestra.contrib.orchestration import ServiceController @@ -17,7 +17,7 @@ class PHPController(WebAppServiceMixin, ServiceController): It handles switching between these two PHP process management systemes. """ MERGE = settings.WEBAPPS_MERGE_PHP_WEBAPPS - + verbose_name = _("PHP FPM/FCGID") default_route_match = "webapp.type.endswith('php')" doc_settings = (settings, ( @@ -30,7 +30,7 @@ class PHPController(WebAppServiceMixin, ServiceController): 'WEBAPPS_PHPFPM_POOL_PATH', 'WEBAPPS_PHP_MAX_REQUESTS', )) - + def save(self, webapp): self.delete_old_config(webapp) context = self.get_context(webapp) @@ -81,7 +81,7 @@ class PHPController(WebAppServiceMixin, ServiceController): } """) % context ) - + def save_fcgid(self, webapp, context): self.append("mkdir -p %(wrapper_dir)s" % context) self.append(textwrap.dedent(""" @@ -118,7 +118,7 @@ class PHPController(WebAppServiceMixin, ServiceController): ) else: self.append("rm -f %(cmd_options_path)s\n" % context) - + def delete(self, webapp): context = self.get_context(webapp) self.delete_old_config(webapp) @@ -127,13 +127,13 @@ class PHPController(WebAppServiceMixin, ServiceController): # elif webapp.type_instance.is_fcgid: # self.delete_fcgid(webapp, context) self.delete_webapp_dir(context) - + def has_sibilings(self, webapp, context): return type(webapp).objects.filter( account=webapp.account_id, data__contains='"php_version":"%s"' % context['php_version'], ).exclude(id=webapp.pk).exists() - + def all_versions_to_delete(self, webapp, context, preserve=False): context_copy = dict(context) for php_version, verbose in settings.WEBAPPS_PHP_VERSIONS: @@ -144,13 +144,13 @@ class PHPController(WebAppServiceMixin, ServiceController): context_copy['php_version_number'] = php_version_number if not self.MERGE or not self.has_sibilings(webapp, context_copy): yield context_copy - + def delete_fpm(self, webapp, context, preserve=False): """ delete all pools in order to efectively support changing php-fpm version """ for context_copy in self.all_versions_to_delete(webapp, context, preserve): context_copy['fpm_path'] = settings.WEBAPPS_PHPFPM_POOL_PATH % context_copy self.append("rm -f %(fpm_path)s" % context_copy) - + def delete_fcgid(self, webapp, context, preserve=False): """ delete all pools in order to efectively support changing php-fcgid version """ for context_copy in self.all_versions_to_delete(webapp, context, preserve): @@ -160,13 +160,13 @@ class PHPController(WebAppServiceMixin, ServiceController): }) self.append("rm -f %(wrapper_path)s" % context_copy) self.append("rm -f %(cmd_options_path)s" % context_copy) - + def prepare(self): super(PHPController, self).prepare() self.append(textwrap.dedent(""" BACKEND="PHPController" echo "$BACKEND" >> /dev/shm/reload.apache2 - + function coordinate_apache_reload () { # Coordinate Apache reload with other concurrent backends (e.g. Apache2Controller) is_last=0 @@ -203,7 +203,7 @@ class PHPController(WebAppServiceMixin, ServiceController): fi }""") ) - + def commit(self): context = { 'reload_pool': settings.WEBAPPS_PHPFPM_RELOAD_POOL, @@ -217,7 +217,7 @@ class PHPController(WebAppServiceMixin, ServiceController): """) % context ) super(PHPController, self).commit() - + def get_fpm_config(self, webapp, context): options = webapp.type_instance.get_options() context.update({ @@ -231,11 +231,11 @@ class PHPController(WebAppServiceMixin, ServiceController): [{{ user }}-{{app_name}}] user = {{ user }} group = {{ group }} - + listen = {{ fpm_listen | safe }} listen.owner = {{ user }} listen.group = {{ group }} - + pm = ondemand pm.max_requests = {{ max_requests }} pm.max_children = {{ max_children }} @@ -245,8 +245,8 @@ class PHPController(WebAppServiceMixin, ServiceController): php_admin_value[{{ name | safe }}] = {{ value | safe }}{% endfor %} """ )) - return fpm_config.render(Context(context)) - + return fpm_config.render(context) + def get_fcgid_wrapper(self, webapp, context): opt = webapp.type_instance # Format PHP init vars @@ -268,7 +268,7 @@ class PHPController(WebAppServiceMixin, ServiceController): export PHP_INI_SCAN_DIR=%(php_ini_scan)s export PHP_FCGI_MAX_REQUESTS=%(max_requests)s exec %(php_binary_path)s%(php_init_vars)s""") % context - + def get_fcgid_cmd_options(self, webapp, context): options = webapp.type_instance.get_options() maps = OrderedDict( @@ -288,7 +288,7 @@ class PHPController(WebAppServiceMixin, ServiceController): ) % context cmd_options.insert(0, head) return ' \\\n '.join(cmd_options) - + def update_fcgid_context(self, webapp, context): wrapper_path = settings.WEBAPPS_FCGID_WRAPPER_PATH % context context.update({ @@ -301,14 +301,14 @@ class PHPController(WebAppServiceMixin, ServiceController): 'cmd_options_path': settings.WEBAPPS_FCGID_CMD_OPTIONS_PATH % context, }) return context - + def update_fpm_context(self, webapp, context): context.update({ 'fpm_config': self.get_fpm_config(webapp, context), 'fpm_path': settings.WEBAPPS_PHPFPM_POOL_PATH % context, }) return context - + def get_context(self, webapp): context = super().get_context(webapp) context.update({ diff --git a/orchestra/contrib/websites/backends/apache.py b/orchestra/contrib/websites/backends/apache.py index 2cee8c4e..ad664eea 100644 --- a/orchestra/contrib/websites/backends/apache.py +++ b/orchestra/contrib/websites/backends/apache.py @@ -2,7 +2,7 @@ import os import re import textwrap -from django.template import Template, Context +from django.template import Template from django.utils.translation import ugettext_lazy as _ from orchestra.contrib.orchestration import ServiceController @@ -20,7 +20,7 @@ class Apache2Controller(ServiceController): """ HTTP_PORT = 80 HTTPS_PORT = 443 - + model = 'websites.Website' related_models = ( ('websites.Content', 'website'), @@ -37,7 +37,7 @@ class Apache2Controller(ServiceController): 'WEBSITES_DEFAULT_IPS', 'WEBSITES_SAAS_DIRECTIVES', )) - + def get_extra_conf(self, site, context, ssl=False): extra_conf = self.get_content_directives(site, context) directives = site.get_directives() @@ -53,7 +53,7 @@ class Apache2Controller(ServiceController): # Order extra conf directives based on directives (longer first) extra_conf = sorted(extra_conf, key=lambda a: len(a[0]), reverse=True) return '\n'.join([conf for location, conf in extra_conf]) - + def render_virtual_host(self, site, context, ssl=False): context.update({ 'port': self.HTTPS_PORT if ssl else self.HTTP_PORT, @@ -78,8 +78,8 @@ class Apache2Controller(ServiceController): {{ line | safe }}{% endfor %} """) - ).render(Context(context)) - + ).render(context) + def render_redirect_https(self, context): context['port'] = self.HTTP_PORT return Template(textwrap.dedent(""" @@ -96,8 +96,8 @@ class Apache2Controller(ServiceController): RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} """) - ).render(Context(context)) - + ).render(context) + def save(self, site): context = self.get_context(site) if context['server_name']: @@ -133,7 +133,7 @@ class Apache2Controller(ServiceController): [[ $(a2dissite %(site_unique_name)s) =~ "already disabled" ]] || UPDATED_APACHE=1\ """) % context ) - + def delete(self, site): context = self.get_context(site) self.append(textwrap.dedent(""" @@ -142,14 +142,14 @@ class Apache2Controller(ServiceController): rm -f %(sites_available)s\ """) % context ) - + def prepare(self): super(Apache2Controller, self).prepare() # Coordinate apache restart with php backend in order not to overdo it self.append(textwrap.dedent(""" BACKEND="Apache2Controller" echo "$BACKEND" >> /dev/shm/reload.apache2 - + function coordinate_apache_reload () { # Coordinate Apache reload with other concurrent backends (e.g. PHPController) is_last=0 @@ -186,12 +186,12 @@ class Apache2Controller(ServiceController): fi }""") ) - + def commit(self): """ reload Apache2 if necessary """ self.append("coordinate_apache_reload") super(Apache2Controller, self).commit() - + def get_directives(self, directive, context): method, args = directive[0], directive[1:] try: @@ -200,7 +200,7 @@ class Apache2Controller(ServiceController): context = (self.__class__.__name__, method) raise AttributeError("%s does not has suport for '%s' directive." % context) return method(context, *args) - + def get_content_directives(self, site, context): directives = [] for content in site.content_set.all(): @@ -208,19 +208,19 @@ class Apache2Controller(ServiceController): self.set_content_context(content, context) directives += self.get_directives(directive, context) return directives - + def get_static_directives(self, context, app_path): context['app_path'] = os.path.normpath(app_path % context) directive = self.get_location_filesystem_map(context) return [ (context['location'], directive), ] - + def get_location_filesystem_map(self, context): if not context['location']: return 'DocumentRoot %(app_path)s' % context return 'Alias %(location)s %(app_path)s' % context - + def get_fpm_directives(self, context, socket, app_path): if ':' in socket: # TCP socket @@ -243,7 +243,7 @@ class Apache2Controller(ServiceController): return [ (context['location'], directives), ] - + def get_fcgid_directives(self, context, app_path, wrapper_path): context.update({ 'app_path': os.path.normpath(app_path), @@ -274,7 +274,7 @@ class Apache2Controller(ServiceController): return [ (context['location'], directives), ] - + def get_uwsgi_directives(self, context, socket): # requires apache2 mod_proxy_uwsgi context['socket'] = socket @@ -283,7 +283,7 @@ class Apache2Controller(ServiceController): return [ (context['location'], directives), ] - + def get_ssl(self, directives): cert = directives.get('ssl-cert') key = directives.get('ssl-key') @@ -305,7 +305,7 @@ class Apache2Controller(ServiceController): return [ ('', '\n'.join(ssl_config)), ] - + def get_security(self, directives): rules = [] location = '/' @@ -329,7 +329,7 @@ class Apache2Controller(ServiceController): """) % '\n '.join(rules) security.append((location, rules)) return security - + def get_redirects(self, directives): redirects = [] for redirect in directives.get('redirect', []): @@ -342,7 +342,7 @@ class Apache2Controller(ServiceController): (location, redirect) ) return redirects - + def get_proxies(self, directives): proxies = [] for proxy in directives.get('proxy', []): @@ -360,7 +360,7 @@ class Apache2Controller(ServiceController): (location, proxy) ) return proxies - + def get_saas(self, directives): saas = [] for name, values in directives.items(): @@ -372,20 +372,20 @@ class Apache2Controller(ServiceController): directive = settings.WEBSITES_SAAS_DIRECTIVES[name] saas += self.get_directives(directive, context) return saas - + def get_username(self, site): option = site.get_directives().get('user_group') if option: return option[0] return site.get_username() - + def get_groupname(self, site): option = site.get_directives().get('user_group') if option and ' ' in option: user, group = option.split() return group return site.get_groupname() - + def get_server_names(self, site): server_name = None server_alias = [] @@ -395,7 +395,7 @@ class Apache2Controller(ServiceController): else: server_alias.append(domain.name) return server_name, server_alias - + def get_context(self, site): base_apache_conf = settings.WEBSITES_BASE_APACHE_CONF sites_available = os.path.join(base_apache_conf, 'sites-available') @@ -419,7 +419,7 @@ class Apache2Controller(ServiceController): if not context['ips']: raise ValueError("WEBSITES_DEFAULT_IPS is empty.") return context - + def set_content_context(self, content, context): content_context = { 'type': content.webapp.type, @@ -442,7 +442,7 @@ class Apache2Traffic(ServiceMonitor): doc_settings = (settings, ('WEBSITES_TRAFFIC_IGNORE_HOSTS',) ) - + def prepare(self): super(Apache2Traffic, self).prepare() ignore_hosts = '\\|'.join(settings.WEBSITES_TRAFFIC_IGNORE_HOSTS) @@ -490,11 +490,11 @@ class Apache2Traffic(ServiceMonitor): }' || [[ $? == 1 ]] && true } | xargs echo ${OBJECT_ID} }""") % context) - + def monitor(self, site): context = self.get_context(site) self.append('monitor {object_id} "{last_date}" {log_file}'.format(**context)) - + def get_context(self, site): return { 'log_file': '%s{,.1}' % site.get_www_access_log_path(), diff --git a/orchestra/utils/mail.py b/orchestra/utils/mail.py index 25b256bf..71f08657 100644 --- a/orchestra/utils/mail.py +++ b/orchestra/utils/mail.py @@ -2,7 +2,6 @@ from urllib.parse import urlparse from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string -from django.template import Context def render_email_template(template, context): From 28c03ac6c828d6946cd9346d8d676c9e6abc3f73 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 24 May 2021 12:36:49 +0200 Subject: [PATCH 19/21] Handle HTML safe rendering on accounts, bills & payments Drop `allow_tags` attribute which has been removed on Django 2.0 --- orchestra/contrib/accounts/admin.py | 3 +-- orchestra/contrib/bills/admin.py | 11 +++-------- orchestra/contrib/payments/admin.py | 9 +++++---- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/orchestra/contrib/accounts/admin.py b/orchestra/contrib/accounts/admin.py index 2b9a4773..e44c656b 100644 --- a/orchestra/contrib/accounts/admin.py +++ b/orchestra/contrib/accounts/admin.py @@ -158,6 +158,7 @@ class AccountListAdmin(AccountAdmin): actions = None change_list_template = 'admin/accounts/account/select_account_list.html' + @mark_safe def select_account(self, instance): # TODO get query string from request.META['QUERY_STRING'] to preserve filters context = { @@ -167,7 +168,6 @@ class AccountListAdmin(AccountAdmin): } return _('%(plus)s Add to %(name)s') % context select_account.short_description = _("account") - select_account.allow_tags = True select_account.admin_order_field = 'username' def changelist_view(self, request, extra_context=None): @@ -222,7 +222,6 @@ class AccountAdminMixin(object): account = instance.account if instance.pk else self.account return admin_link()(account) account_link.short_description = _("account") - account_link.allow_tags = True account_link.admin_order_field = 'account__username' def get_form(self, request, obj=None, **kwargs): diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py index 3d7b9534..8d23ca04 100644 --- a/orchestra/contrib/bills/admin.py +++ b/orchestra/contrib/bills/admin.py @@ -105,27 +105,26 @@ class ClosedBillLineInline(BillLineInline): readonly_fields = fields can_delete = False + @mark_safe def display_description(self, line): descriptions = [line.description] for subline in line.sublines.all(): - descriptions.append(' '*4+subline.description) + descriptions.append(' ' * 4 + subline.description) return '
        '.join(descriptions) display_description.short_description = _("Description") - display_description.allow_tags = True + @mark_safe def display_subtotal(self, line): subtotals = [' ' + str(line.subtotal)] for subline in line.sublines.all(): subtotals.append(str(subline.total)) return '
        '.join(subtotals) display_subtotal.short_description = _("Subtotal") - display_subtotal.allow_tags = True def display_total(self, line): if line.pk: return line.compute_total() display_total.short_description = _("Total") - display_total.allow_tags = True def has_add_permission(self, request): return False @@ -253,7 +252,6 @@ class BillAdminMixin(AccountAdminMixin): subtotals.append(_("Taxes %s%% VAT %s &%s;") % (tax, subtotal[1], currency)) subtotals = '\n'.join(subtotals) return '%s &%s;' % (subtotals, bill.compute_total(), currency) - display_total_with_subtotals.allow_tags = True display_total_with_subtotals.short_description = _("total") display_total_with_subtotals.admin_order_field = 'approx_total' @@ -279,7 +277,6 @@ class BillAdminMixin(AccountAdminMixin): color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey') return '{name}'.format( url=url, color=color, name=state, title=title) - display_payment_state.allow_tags = True display_payment_state.short_description = _("Payment") def get_queryset(self, request): @@ -380,7 +377,6 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin): def display_total(self, bill): currency = settings.BILLS_CURRENCY.lower() return format_html('{} &{};', bill.compute_total(), currency) - display_total.allow_tags = True display_total.short_description = _("total") display_total.admin_order_field = 'approx_total' @@ -388,7 +384,6 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin): bill_type = bill.type.lower() url = reverse('admin:bills_%s_changelist' % bill_type) return format_html('{}', url, bill.get_type_display()) - type_link.allow_tags = True type_link.short_description = _("type") type_link.admin_order_field = 'type' diff --git a/orchestra/contrib/payments/admin.py b/orchestra/contrib/payments/admin.py index f753231b..45b67a48 100644 --- a/orchestra/contrib/payments/admin.py +++ b/orchestra/contrib/payments/admin.py @@ -1,6 +1,8 @@ from django.contrib import admin from django.urls import reverse from django.http import HttpResponseRedirect +from django.utils.html import format_html +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ChangeViewActionsMixin, ExtendedModelAdmin @@ -154,6 +156,7 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): return [] return [action for action in actions if action.__name__ not in exclude] + @mark_safe def display_state(self, obj): state = admin_colored('state', colors=STATE_COLORS)(obj) help_text = obj.get_state_help() @@ -161,7 +164,6 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): return state display_state.admin_order_field = 'state' display_state.short_description = _("State") - display_state.allow_tags = True class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin): @@ -184,10 +186,10 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin): def file_url(self, process): if process.file: - return '%s' % (process.file.url, process.file.name) - file_url.allow_tags = True + return format_html('{}', process.file.url, process.file.name) file_url.admin_order_field = 'file' + @mark_safe def display_transactions(self, process): ids = [] lines = [] @@ -207,7 +209,6 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin): url += '?process_id=%i' % process.id return '%s' % (url, transactions) display_transactions.short_description = _("Transactions") - display_transactions.allow_tags = True def has_add_permission(self, *args, **kwargs): return False From 47eb0f1efe073c1b3d7c932a6d4ad4789bec17a6 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 24 May 2021 12:37:36 +0200 Subject: [PATCH 20/21] Rename local var display because shadows built-in --- orchestra/admin/utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/orchestra/admin/utils.py b/orchestra/admin/utils.py index 764edf84..e38ceb72 100644 --- a/orchestra/admin/utils.py +++ b/orchestra/admin/utils.py @@ -113,21 +113,21 @@ def admin_link(*args, **kwargs): return '---' if not getattr(obj, 'pk', None): return '---' - display = kwargs.get('display') - if display: - display = getattr(obj, display, display) + display_ = kwargs.get('display') + if display_: + display_ = getattr(obj, display_, display_) else: - display = obj + display_ = obj try: url = change_url(obj) except NoReverseMatch: # Does not has admin - return str(display) + return str(display_) extra = '' if kwargs['popup']: - extra = 'onclick="return showAddAnotherPopup(this);"' + extra = mark_safe('onclick="return showAddAnotherPopup(this);"') title = "Change %s" % obj._meta.verbose_name - return mark_safe('%s' % (url, title, extra, display)) + return format_html('{}', url, title, extra, display_) @admin_field From 0e10d2b14248780a078c556c37f7bd6b4cdd3ff4 Mon Sep 17 00:00:00 2001 From: Santiago Lamora Date: Mon, 24 May 2021 12:53:50 +0200 Subject: [PATCH 21/21] Bump python-dateutil to 2.7.0+ --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 69fb6965..ae2fa60f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ ecdsa==0.11 Pygments==1.6 django-filter==2.2.0 jsonfield==0.9.22 -python-dateutil==2.2 +python-dateutil>=2.7.0 https://github.com/glic3rinu/passlib/archive/master.zip django-iban==0.3.0 requests