Merge pull request #8 from ribaguifi/dev/django2.1-admin

Refactor admin code to support Django 2.1
This commit is contained in:
Santiago L 2021-05-24 12:55:35 +02:00 committed by GitHub
commit 7b59931bcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 278 additions and 296 deletions

View File

@ -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):
</div>
</div>""")
)
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)

View File

@ -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
@ -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('<a href="%s" title="%s" %s>%s</a>' % (url, title, extra, display))
return format_html('<a href="{}" title="{}" {}>{}</a>', url, title, extra, display_)
@admin_field
@ -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 '<span title="{0}">{1}</span>'.format(date, escape(natural))
return format_html('<span title="{0}">{1}</span>', date, natural)
def get_object_from_url(modeladmin, request):

View File

@ -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:

View File

@ -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 _('<a href="%(url)s">%(plus)s Add to %(name)s</a>') % 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):
@ -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 '<img src="%s" alt="False">' % static('admin/img/icon-no.svg')
@ -215,14 +216,12 @@ class AccountAdminMixin(object):
return '<img style="width:13px" src="%s" alt="False" title="%s">' % (static('admin/img/inline-delete.svg'), msg)
return '<img src="%s" alt="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):
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):

View File

@ -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:

View File

@ -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
@ -15,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,
@ -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()
@ -78,7 +80,6 @@ class BillLineInline(admin.TabularInline):
return '<a href="%s" title="%s">%s <img src="%s"></img></a>' % (url, content, total, img)
return '<a href="%s">%s</a>' % (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 """
@ -104,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('&nbsp;'*4+subline.description)
descriptions.append('&nbsp;' * 4 + subline.description)
return '<br>'.join(descriptions)
display_description.short_description = _("Description")
display_description.allow_tags = True
@mark_safe
def display_subtotal(self, line):
subtotals = ['&nbsp;' + str(line.subtotal)]
for subline in line.sublines.all():
subtotals.append(str(subline.total))
return '<br>'.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
@ -242,6 +242,7 @@ class BillLineManagerAdmin(BillLineAdmin):
class BillAdminMixin(AccountAdminMixin):
@mark_safe
def display_total_with_subtotals(self, bill):
if bill.pk:
currency = settings.BILLS_CURRENCY.lower()
@ -251,10 +252,10 @@ class BillAdminMixin(AccountAdminMixin):
subtotals.append(_("Taxes %s%% VAT %s &%s;") % (tax, subtotal[1], currency))
subtotals = '\n'.join(subtotals)
return '<span title="%s">%s &%s;</span>' % (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'
@mark_safe
def display_payment_state(self, bill):
if bill.pk:
t_opts = bill.transactions.model._meta
@ -276,7 +277,6 @@ class BillAdminMixin(AccountAdminMixin):
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
return '<a href="{url}" style="color:{color}" title="{title}">{name}</a>'.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):
@ -376,16 +376,14 @@ class BillAdmin(BillAdminMixin, ExtendedModelAdmin):
def display_total(self, bill):
currency = settings.BILLS_CURRENCY.lower()
return '%s &%s;' % (bill.compute_total(), currency)
display_total.allow_tags = True
return format_html('{} &{};', bill.compute_total(), currency)
display_total.short_description = _("total")
display_total.admin_order_field = 'approx_total'
def type_link(self, bill):
bill_type = bill.type.lower()
url = reverse('admin:bills_%s_changelist' % bill_type)
return '<a href="%s">%s</a>' % (url, bill.get_type_display())
type_link.allow_tags = True
return format_html('<a href="{}">{}</a>', url, bill.get_type_display())
type_link.short_description = _("type")
type_link.admin_order_field = 'type'
@ -479,7 +477,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)

View File

@ -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,))

View File

@ -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)

View File

@ -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)

View File

@ -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 = '<a href="%s">%s</a>' % (change_url(user), user.username)
link = format_html('<a href="{}">{}</a>', change_url(user), user.username)
links.append(link)
return '<br>'.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 = '<a href="%s">%s</a>' % (change_url(db), db.name)
link = format_html('<a href="{}">{}</a>', change_url(db), db.name)
links.append(link)
return '<br>'.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:

View File

@ -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 <a href='../password/'>this form</a>. "
"<a onclick='return showAddAnotherPopup(this);' href='../hash/'>Show hash</a>."))
class Meta:
model = DatabaseUser
fields = ('username', 'password', 'type', 'account')
def clean_password(self):
return self.initial["password"]

View File

@ -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
@ -72,9 +74,8 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
def structured_name(self, domain):
if domain.is_top:
return domain.name
return '&nbsp;'*4 + domain.name
return mark_safe('&nbsp;'*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):
@ -83,6 +84,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,22 +94,22 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
site_link = get_on_site_link(website.get_absolute_url())
admin_url = change_url(website)
title = _("Edit website")
link = '<a href="%s" title="%s">%s %s</a>' % (
link = format_html('<a href="{}" title="{}">{} {}</a>',
admin_url, title, website.name, site_link)
links.append(link)
return '<br>'.join(links)
add_url = reverse('admin:websites_website_add')
add_url += '?account=%i&domains=%i' % (domain.account_id, domain.pk)
image = '<img src="%s"></img>' % static('orchestra/images/add.png')
add_link = '<a href="%s" title="%s">%s</a>' % (
add_url, _("Add website"), image
add_link = format_html(
'<a href="{}" title="{}"><img src="{}" /></a>', 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):
if apps.isinstalled('orchestra.contrib.mailboxes'):
add_url = reverse('admin:mailboxes_address_add')
@ -126,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 = []
@ -141,14 +142,13 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
value=record.value
)
if not domain.record_is_implicit(record, types):
line = '<strike>%s</strike>' % line
line = format_html('<strike>{}</strike>', line)
if record.type is Record.SOA:
lines.insert(0, line)
else:
lines.append(line)
return '<br>'.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 """

View File

@ -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 = '<a href="%(url)s"><img src="%(img)s"></img></a>' % {
edit = format_html('<a href="{url}"><img src="{img}"></img></a>', **{
'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 '<a href="%s">%s</a>' % (url, log.object_repr)
return format_html('<a href="{}">{}</a>', 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 """

View File

@ -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 = '<strong style="color:#666;">%s</strong><hr />' % summary
content = markdown(msg.content)
content = content.replace('>\n', '>')
content = '<div style="padding-left:20px;">%s</div>' % 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 '<b>%s</b>' % 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 '<h4>Added by %(creator)s about %(created)s%(updated)s</h4>' % 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 '<span style="font-weight:normal;font-size:11px;">%s</span>' % ticket.pk
return format_html('<span style="font-weight:normal;font-size:11px;">{}</span>', 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 "<strong class='unread'>%s</strong>" % ticket.subject
bold_subject.allow_tags = True
return format_html("<strong class='unread'>{}</strong>", 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 '<a href="%s">%d</a>' % (url, num)
return format_html('<a href="{}">{}</a>', 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 """

View File

@ -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 = (
'<a href="%s" onclick=\'window.open("%s", "", "resizable=yes, '
@ -21,8 +21,8 @@ class MarkDownWidget(forms.Textarea):
'return false;\'>markdown format</a>' % (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 = ('<a class="load-preview" href="#" data-field="{0}">preview</a>'\
@ -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 = '<div style="padding-left: 95px;">%s</div>' % 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'])

View File

@ -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')

View File

@ -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 = '<a href="%s">%s</a>' % (url, email)
link = format_html('<a href="{}">{}</a>', 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('<a href="%s">%s</a>' % (url, addr.email))
addresses.append(format_html('<a href="{}">{}</a>', url, addr.email))
return '<br>'.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('<a href="%s">%s</a>' % (url, addr.email))
return '<br>'.join(forwards)
forwards = mailbox.get_forwards()
return format_html_join(
'<br>', '<a href="{}">{}</a>',
[(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':
@ -217,7 +216,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):
@ -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('<a href="%s">%s</a>' % (url, mailbox.name))
return '<br>'.join(boxes)
boxes = address.mailboxes.all()
return format_html_join(
'<br>', '<a href="{}">{}</a>',
[(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('<a href="%s">%s</a>' % (url, mailbox.name))
return '<br>'.join(boxes)
boxes = address.get_mailboxes()
return format_html_join(
'<br>', '<a href="{}">{}</a>',
[(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 '<br>'.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):

View File

@ -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):

View File

@ -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] + '&hellip;'
return mark_safe(subject[:64] + '&hellip;')
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 '<a href="%s" onclick="return showAddAnotherPopup(this);">%d</a>' % (url, instance.retries)
return format_html('<a href="{}" onclick="return showAddAnotherPopup(this);">{}</a>', 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', '<br>').replace(' ', '&nbsp;')
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

View File

@ -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 '<span title="%s">%s</span>' % (misc.description, misc.name)
return format_html('<span title="{}">{}</span>', 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 '<span title="%s">%s</span>' % (misc.description, misc.verbose_name)
return format_html('<span title="{}">{}</span>', 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):

View File

@ -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 "<span style='color: red;'>NOT AVAILABLE</span>"
return mark_safe("<span style='color: red;'>NOT AVAILABLE</span>")
display_model.short_description = _("model")
display_model.allow_tags = True
@mark_safe
def display_actions(self, route):
try:
return '<br>'.join(route.backend_class.get_actions())
except KeyError:
return "<span style='color: red;'>NOT AVAILABLE</span>"
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):
@ -179,14 +177,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
def get_queryset(self, request):
""" Order by structured name and imporve performance """

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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 '<a href="{url}">{description}</a>'.format(
return format_html('<a href="{url}">{description}</a>',
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 '<br>'.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 '<span title="{raw}" {color}>{human}</span>'.format(
color = mark_safe('style="color:red;"') if red else ''
return format_html(
'<span title="{raw}" {color}>{human}</span>',
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):

View File

@ -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():

View File

@ -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 '<a href="%s">%s</a>' % (process.file.url, process.file.name)
file_url.allow_tags = True
return format_html('<a href="{}">{}</a>', 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 '<a href="%s">%s</a>' % (url, transactions)
display_transactions.short_description = _("Transactions")
display_transactions.allow_tags = True
def has_add_permission(self, *args, **kwargs):
return False

View File

@ -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 '<a href="{0}">{1}</a>'.format(url, num)
return format_html('<a href="{0}">{1}</a>', 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)

View File

@ -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 '<a href="%s">%s</a>' % (url, data.content_object_repr)
return format_html('<a href="{}">{}</a>', 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 '<a href="%s">%s %s</a>' % (url, rdata.used, rdata.unit)
return format_html('<a href="{}">{} {}</a>', 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 """

View File

@ -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 = '<a href="http://%s">%s</a>' % (site_domain, site_domain)
@ -46,9 +48,8 @@ class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMi
links.append(link)
return '<br>'.join(links)
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:

View File

@ -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')

View File

@ -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 '<a href="%s">%d</a>' % (url, num)
return format_html('<a href="{}">{}</a>', 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)

View File

@ -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

View File

@ -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,29 +84,13 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin)
websites.append('<a href="%s">%s%s</a>' % (add_url, plus, ugettext("Add website")))
return '<br>'.join(websites)
display_websites.short_description = _("web sites")
display_websites.allow_tags = True
def display_detail(self, webapp):
try:
return webapp.type_instance.get_detail()
except KeyError:
return "<span style='color:red;'>Not available</span>"
return mark_safe("<span style='color:red;'>Not available</span>")
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)

View File

@ -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({

View File

@ -3,6 +3,8 @@ 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 _
from orchestra.admin import ExtendedModelAdmin
@ -78,6 +80,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():
@ -85,9 +88,9 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
domains.append('<a href="%s">%s</a>' % (url, url))
return '<br>'.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():
@ -100,9 +103,9 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
pass
url = change_url(webapp)
name = "%s on %s" % (webapp.name, content.path or '/')
webapps.append('<a href="%s" title="%s">%s %s</a>' % (url, detail, name, site_link))
webapp_info = format_html('<a href="{}" title="{}">{}</a> {}', url, detail, name, site_link)
webapps.append(webapp_info)
return '<br>'.join(webapps)
display_webapps.allow_tags = True
display_webapps.short_description = _("Web apps")
def formfield_for_dbfield(self, db_field, **kwargs):

View File

@ -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 %}
</VirtualHost>
""")
).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}
</VirtualHost>
""")
).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):
</IfModule>""") % '\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(),

View File

@ -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')

View File

@ -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,25 +29,25 @@ 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
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'^<ul id=([^>]+)>',
r'<ul id=\1 style="padding-left:%ipx">' % padding, value, 1)
r'<ul id=\1 style="padding-left:%ipx">' % self.padding, value, 1)
return mark_safe(value)
widget.render = render
return widget
class DynamicHelpTextSelect(forms.Select):
@ -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};

View File

@ -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)

View File

@ -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': '<img src="%s"></img>' % static('orchestra/images/view-on-site.png'),
'image': format_html('<img src="{}"></img>', static('orchestra/images/view-on-site.png')),
}
return '<a href="%(url)s" title="%(title)s">%(image)s</a>' % context
return format_html('<a href="{url}" title="{title}">{image}</a>', **context)

View File

@ -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):
@ -10,12 +9,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)

View File

@ -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