Random fixes

This commit is contained in:
Marc 2014-10-11 16:21:51 +00:00
parent 1c8ef622b0
commit 920f8efcd5
18 changed files with 215 additions and 112 deletions

View file

@ -90,7 +90,7 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* mail backend related_models = ('resources__content_type') ??
* ignore orders (mark orders as ignored)
* ignore orders (mark orders as ignored), ignore orchestra related orders by default (or do not generate them on the first place) ignore superuser orders?
* Domain backend PowerDNS Bind validation support?
@ -115,8 +115,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* Separate panel from server passwords? Store passwords on panel? set_password special backend operation?
* be more explicit about which backends are resources and which are service handling
* What fields we really need on contacts? name email phone and what more?
* Redirect junk emails and delete every 30 days?

View file

@ -86,8 +86,11 @@ class ChangeViewActionsMixin(object):
action = getattr(self, action)
view = action_to_view(action, self)
view.url_name = getattr(action, 'url_name', action.__name__)
view.verbose_name = getattr(action, 'verbose_name',
verbose_name = getattr(action, 'verbose_name',
view.url_name.capitalize().replace('_', ' '))
if hasattr(verbose_name, '__call__'):
verbose_name = verbose_name(obj)
view.verbose_name = verbose_name
view.css_class = getattr(action, 'css_class', 'historylink')
view.description = getattr(action, 'description', '')
views.append(view)
@ -186,7 +189,7 @@ class SelectPluginAdminMixin(object):
def add_view(self, request, form_url='', extra_context=None):
""" Redirects to select account view if required """
if request.user.is_superuser:
plugin_value = request.GET.get(self.plugin_field)
plugin_value = request.GET.get(self.plugin_field) or request.POST.get(self.plugin_field)
if plugin_value or self.plugin.get_plugins() == 1:
self.plugin_value = plugin_value
if not plugin_value:

View file

@ -4,37 +4,17 @@ import zipfile
from django.contrib import messages
from django.contrib.admin import helpers
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseServerError
from django.http import HttpResponse
from django.shortcuts import render
from django.utils.encoding import force_text
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext, ugettext_lazy as _
from orchestra.admin.forms import adminmodelformset_factory
from orchestra.admin.utils import get_object_from_url
from orchestra.admin.utils import get_object_from_url, change_url
from orchestra.utils.html import html_to_pdf
from .forms import SelectSourceForm
def validate_contact(bill):
""" checks if all the preconditions for bill generation are met """
msg = ''
if not hasattr(bill.account, 'invoicecontact'):
account = force_text(bill.account)
link = reverse('admin:accounts_account_change', args=(bill.account_id,))
link += '#invoicecontact-group'
msg += _('Related account "%s" doesn\'t have a declared invoice contact\n') % account
msg += _('You should <a href="%s">provide</a> one') % link
main = type(bill).account.field.rel.to.get_main()
if not hasattr(main, 'invoicecontact'):
account = force_text(main)
link = reverse('admin:accounts_account_change', args=(main.id,))
link += '#invoicecontact-group'
msg += _('Main account "%s" doesn\'t have a declared invoice contact\n') % account
msg += _('You should <a href="%s">provide</a> one') % link
if msg:
# TODO custom template
return HttpResponseServerError(mark_safe(msg))
from .helpers import validate_contact
def download_bills(modeladmin, request, queryset):
@ -42,15 +22,14 @@ def download_bills(modeladmin, request, queryset):
stringio = StringIO.StringIO()
archive = zipfile.ZipFile(stringio, 'w')
for bill in queryset:
html = bill.html or bill.render()
pdf = html_to_pdf(html)
pdf = html_to_pdf(bill.html or bill.render())
archive.writestr('%s.pdf' % bill.number, pdf)
archive.close()
response = HttpResponse(stringio.getvalue(), content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="orchestra-bills.zip"'
return response
bill = queryset.get()
pdf = html_to_pdf(bill.html)
pdf = html_to_pdf(bill.html or bill.render())
return HttpResponse(pdf, content_type='application/pdf')
download_bills.verbose_name = _("Download")
download_bills.url_name = 'download'
@ -58,9 +37,8 @@ download_bills.url_name = 'download'
def view_bill(modeladmin, request, queryset):
bill = queryset.get()
error = validate_contact(bill)
if error:
return error
if not validate_contact(request, bill):
return
html = bill.html or bill.render()
return HttpResponse(html)
view_bill.verbose_name = _("View")
@ -73,20 +51,34 @@ def close_bills(modeladmin, request, queryset):
messages.warning(request, _("Selected bills should be in open state"))
return
for bill in queryset:
error = validate_contact(bill)
if error:
return error
if not validate_contact(request, bill):
return
SelectSourceFormSet = adminmodelformset_factory(modeladmin, SelectSourceForm, extra=0)
formset = SelectSourceFormSet(queryset=queryset)
if request.POST.get('post') == 'generic_confirmation':
formset = SelectSourceFormSet(request.POST, request.FILES, queryset=queryset)
if formset.is_valid():
transactions = []
for form in formset.forms:
source = form.cleaned_data['source']
form.instance.close(payment=source)
transaction = form.instance.close(payment=source)
if transaction:
transactions.append(transaction)
for bill in queryset:
modeladmin.log_change(request, bill, 'Closed')
messages.success(request, _("Selected bills have been closed"))
if transactions:
num = len(transactions)
if num == 1:
url = change_url(transactions[0])
else:
url = reverse('admin:transactions_transaction_changelist')
url += 'id__in=%s' % ','.join([str(t.id) for t in transactions])
message = ungettext(
_('<a href="%s">One related transaction</a> has been created') % url,
_('<a href="%s">%i related transactions</a> have been created') % (url, num),
num)
messages.success(request, mark_safe(message))
return
opts = modeladmin.model._meta
context = {
@ -110,11 +102,10 @@ close_bills.url_name = 'close'
def send_bills(modeladmin, request, queryset):
for bill in queryset:
error = validate_contact(bill)
if error:
return error
if not validate_contact(request, bill):
return
for bill in queryset:
bill.send()
modeladmin.log_change(request, bill, 'Sent')
send_bills.verbose_name = _("Send")
send_bills.verbose_name = lambda bill: _("Resend" if getattr(bill, 'is_sent', False) else "Send")
send_bills.url_name = 'send'

View file

@ -1,5 +1,5 @@
from django import forms
from django.contrib import admin, messages
from django.contrib import admin
from django.contrib.admin.utils import unquote
from django.core.urlresolvers import reverse
from django.db import models
@ -11,7 +11,7 @@ from orchestra.admin.utils import admin_date
from orchestra.apps.accounts.admin import AccountAdminMixin
from . import settings
from .actions import download_bills, view_bill, close_bills, send_bills
from .actions import download_bills, view_bill, close_bills, send_bills, validate_contact
from .filters import BillTypeListFilter
from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine
@ -55,9 +55,9 @@ class BillLineInline(admin.TabularInline):
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_display = (
'number', 'type_link', 'account_link', 'created_on_display',
'num_lines', 'display_total', 'display_payment_state', 'is_open'
'num_lines', 'display_total', 'display_payment_state', 'is_open', 'is_sent'
)
list_filter = (BillTypeListFilter, 'is_open',)
list_filter = (BillTypeListFilter, 'is_open', 'is_sent')
add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')
fieldsets = (
(None, {
@ -97,8 +97,13 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
type_link.admin_order_field = 'type'
def display_payment_state(self, bill):
topts = bill.transactions.model._meta
url = reverse('admin:%s_%s_changelist' % (topts.app_label, topts.module_name))
t_opts = bill.transactions.model._meta
transactions = bill.transactions.all()
if len(transactions) == 1:
args = (transactions[0].pk,)
url = reverse('admin:%s_%s_change' % (t_opts.app_label, t_opts.module_name), args=args)
else:
url = reverse('admin:%s_%s_changelist' % (t_opts.app_label, t_opts.module_name))
url += '?bill=%i' % bill.pk
state = bill.get_payment_state_display().upper()
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
@ -120,7 +125,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
return fieldsets
def get_change_view_actions(self, obj=None):
actions = super(BillAdmin, self).get_change_view_actions()
actions = super(BillAdmin, self).get_change_view_actions(obj=obj)
exclude = []
if obj:
if not obj.is_open:
@ -143,19 +148,13 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
def get_queryset(self, request):
qs = super(BillAdmin, self).get_queryset(request)
qs = qs.annotate(models.Count('lines'))
qs = qs.prefetch_related('lines', 'lines__sublines')
qs = qs.prefetch_related('lines', 'lines__sublines', 'transactions')
return qs
def change_view(self, request, object_id, **kwargs):
bill = self.get_object(request, unquote(object_id))
# TODO raise404, here and everywhere
if not hasattr(bill.account, 'invoicecontact'):
create_link = reverse('admin:accounts_account_change', args=(bill.account_id,))
create_link += '#invoicecontact-group'
messages.warning(request, mark_safe(_(
'Be aware, related contact doesn\'t have a billing contact defined, '
'bill can not be generated until one is <a href="%s">provided</a>' % create_link
)))
bill = self.get_object(request, unquote(object_id))
validate_contact(request, bill, error=False)
return super(BillAdmin, self).change_view(request, object_id, **kwargs)

View file

@ -0,0 +1,28 @@
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.utils.encoding import force_text
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
def validate_contact(request, bill, error=True):
""" checks if all the preconditions for bill generation are met """
msg = _('{relation} account "{account}" does not have a declared invoice contact. '
'You should <a href="{url}#invoicecontact-group">provide one</a>')
valid = True
send = messages.error if error else messages.warning
if not hasattr(bill.account, 'invoicecontact'):
account = force_text(bill.account)
url = reverse('admin:accounts_account_change', args=(bill.account_id,))
message = msg.format(relation=_("Related"), account=account, url=url)
send(request, mark_safe(message))
valid = False
main = type(bill).account.field.rel.to.get_main()
if not hasattr(main, 'invoicecontact'):
account = force_text(main)
url = reverse('admin:accounts_account_change', args=(main.id,))
message = msg.format(relation=_("Main"), account=account, url=url)
send(request, mark_safe(message))
valid = False
return valid

View file

@ -81,7 +81,7 @@ class Bill(models.Model):
@cached_property
def payment_state(self):
if self.is_open:
if self.is_open or self.get_type() == self.PROFORMA:
return self.OPEN
secured = self.transactions.secured().amount()
if secured >= self.total:
@ -136,12 +136,14 @@ class Bill(models.Model):
self.due_on = self.get_due_date(payment=payment)
self.total = self.get_total()
self.html = self.render(payment=payment)
transaction = None
if self.get_type() != self.PROFORMA:
self.transactions.create(bill=self, source=payment, amount=self.total)
transaction = self.transactions.create(bill=self, source=payment, amount=self.total)
self.closed_on = timezone.now()
self.is_open = False
self.is_sent = False
self.save()
return transaction
def send(self):
html = self.html or self.render()

View file

@ -1,2 +1,6 @@
{% if subject %}Bill {{ bill.number }}{% endif %}
{% if message %}Find attached your invoice{% endif %}
{% if message %}Dear {{ bill.account.username }},
Find your {{ bill.get_type.lower }} attached.
If you have any question, please write us at support@orchestra.lan
{% endif %}

View file

@ -1,6 +1,7 @@
from functools import partial
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from orchestra.utils import plugins
@ -111,6 +112,11 @@ class ServiceBackend(plugins.Plugin):
class ServiceController(ServiceBackend):
actions = ('save', 'delete')
abstract = True
@classmethod
def get_verbose_name(cls):
return _("[S] %s") % super(ServiceController, cls).get_verbose_name()
@classmethod
def get_backends(cls):

View file

@ -2,10 +2,11 @@ from django.contrib import admin, messages
from django.core.urlresolvers import reverse
from django.db import transaction
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext
from django.utils.translation import ungettext, ugettext_lazy as _
from django.shortcuts import render
from orchestra.admin.utils import change_url
from .forms import (BillSelectedOptionsForm, BillSelectConfirmationForm,
BillSelectRelatedForm)
@ -38,7 +39,7 @@ class BillSelectedOrders(object):
self.options = dict(
billing_point=form.cleaned_data['billing_point'],
fixed_point=form.cleaned_data['fixed_point'],
is_proforma=form.cleaned_data['is_proforma'],
proforma=form.cleaned_data['proforma'],
new_open=form.cleaned_data['new_open'],
)
if int(request.POST.get('step')) != 3:
@ -78,13 +79,17 @@ class BillSelectedOrders(object):
if int(request.POST.get('step')) >= 3:
bills = self.queryset.bill(commit=True, **self.options)
for order in self.queryset:
self.modeladmin.log_change(request, order, 'Billed')
self.modeladmin.log_change(request, order, _("Billed"))
if not bills:
msg = _("Selected orders do not have pending billing")
self.modeladmin.message_user(request, msg, messages.WARNING)
else:
ids = ','.join([str(bill.id) for bill in bills])
num = len(bills)
if num == 1:
url = change_url(bills[0])
else:
url = reverse('admin:bills_bill_changelist')
ids = ','.join([str(bill.id) for bill in bills])
url += '?id__in=%s' % ids
num = len(bills)
msg = ungettext(

View file

@ -18,7 +18,7 @@ class BillSelectedOptionsForm(AdminFormMixin, forms.Form):
label=_("Fixed point"),
help_text=_("Deisgnates whether you want the billing point to be an "
"exact date, or adapt it to the billing period."))
is_proforma = forms.BooleanField(initial=False, required=False,
proforma = forms.BooleanField(initial=False, required=False,
label=_("Pro-forma (billing simulation)"),
help_text=_("Creates a Pro Forma instead of billing the orders."))
new_open = forms.BooleanField(initial=False, required=False,
@ -49,7 +49,7 @@ class BillSelectRelatedForm(AdminFormMixin, forms.Form):
required=False)
billing_point = forms.DateField(widget=forms.HiddenInput())
fixed_point = forms.BooleanField(widget=forms.HiddenInput(), required=False)
is_proforma = forms.BooleanField(widget=forms.HiddenInput(), required=False)
proforma = forms.BooleanField(widget=forms.HiddenInput(), required=False)
create_new_open = forms.BooleanField(widget=forms.HiddenInput(), required=False)
def __init__(self, *args, **kwargs):
@ -63,5 +63,5 @@ class BillSelectRelatedForm(AdminFormMixin, forms.Form):
class BillSelectConfirmationForm(AdminFormMixin, forms.Form):
billing_point = forms.DateField(widget=forms.HiddenInput())
fixed_point = forms.BooleanField(widget=forms.HiddenInput(), required=False)
is_proforma = forms.BooleanField(widget=forms.HiddenInput(), required=False)
proforma = forms.BooleanField(widget=forms.HiddenInput(), required=False)
create_new_open = forms.BooleanField(widget=forms.HiddenInput(), required=False)

View file

@ -92,7 +92,7 @@ class TransactionAdmin(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdm
class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
list_display = ('id', 'file_url', 'display_transactions', 'created_at')
fields = ('data', 'file_url', 'display_transactions', 'created_at')
fields = ('data', 'file_url', 'created_at')
readonly_fields = ('file_url', 'display_transactions', 'created_at')
inlines = [TransactionInline]
actions = (actions.mark_process_as_executed, actions.abort, actions.commit)
@ -111,16 +111,17 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
# Because of values_list this query doesn't benefit from prefetch_related
tx_ids = process.transactions.values_list('id', flat=True)
for tx_id in tx_ids:
ids.append(str(tx_id))
ids.append('#%i' % tx_id)
counter += 1
if counter > 10:
counter = 0
lines.append(','.join(ids))
ids = []
lines.append(','.join(ids))
transactions = '<br'.join(lines)
url = reverse('admin:payments_transaction_changelist')
url += '?processes=%i' % process.id
return '<a href="%s">%s</a>' % (url, '<br>'.join(lines))
return '<a href="%s">%s</a>' % (url, transactions)
display_transactions.short_description = _("Transactions")
display_transactions.allow_tags = True

View file

@ -175,10 +175,10 @@ class SEPADirectDebit(PaymentMethod):
def get_debt_transactions(cls, transactions, process):
for transaction in transactions:
transaction.process = process
transaction.state = transaction.WAITTING_EXECUTION
transaction.save(update_fields=['state', 'process'])
account = transaction.account
data = transaction.source.data
transaction.state = transaction.WAITTING_CONFIRMATION
transaction.save(update_fields=['state'])
yield E.DrctDbtTxInf( # Direct Debit Transaction Info
E.PmtId( # Payment Id
E.EndToEndId(str(transaction.id)) # Payment Id/End to End
@ -191,7 +191,7 @@ class SEPADirectDebit(PaymentMethod):
E.MndtRltdInf( # Mandate Related Info
E.MndtId(str(account.id)), # Mandate Id
E.DtOfSgntr( # Date of Signature
account.register_date.strftime("%Y-%m-%d")
account.date_joined.strftime("%Y-%m-%d")
)
)
),
@ -216,10 +216,10 @@ class SEPADirectDebit(PaymentMethod):
def get_credit_transactions(transactions, process):
for transaction in transactions:
transaction.process = process
transaction.state = transaction.WAITTING_EXECUTION
transaction.save(update_fields=['state', 'process'])
account = transaction.account
data = transaction.source.data
transaction.state = transaction.WAITTING_CONFIRMATION
transaction.save(update_fields=['state'])
yield E.CdtTrfTxInf( # Credit Transfer Transaction Info
E.PmtId( # Payment Id
E.EndToEndId(str(transaction.id)) # Payment Id/End to End

View file

@ -104,7 +104,7 @@ class Transaction(models.Model):
objects = TransactionQuerySet.as_manager()
def __unicode__(self):
return "Transaction {}".format(self.id)
return "Transaction #{}".format(self.id)
@property
def account(self):
@ -162,7 +162,7 @@ class TransactionProcess(models.Model):
verbose_name_plural = _("Transaction processes")
def __unicode__(self):
return str(self.id)
return '#%i' % self.id
def mark_as_executed(self):
assert self.state == self.CREATED

View file

@ -3,6 +3,7 @@ import datetime
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceBackend
@ -13,15 +14,19 @@ class ServiceMonitor(ServiceBackend):
MEMORY = 'memory'
CPU = 'cpu'
# TODO UNITS
actions = ('monitor', 'exceeded', 'recovery')
abstract = True
@classmethod
def get_backends(cls):
""" filter monitor classes """
for backend in cls.plugins:
if backend != ServiceMonitor and ServiceMonitor in backend.__mro__:
yield backend
""" filter controller classes """
return [
plugin for plugin in cls.plugins if ServiceMonitor in plugin.__mro__
]
@classmethod
def get_verbose_name(cls):
return _("[M] %s") % super(ServiceMonitor, cls).get_verbose_name()
@cached_property
def current_date(self):

View file

@ -1,4 +1,5 @@
from django import forms
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.forms import PluginDataForm
@ -6,16 +7,24 @@ from orchestra.forms import PluginDataForm
from .options import SoftwareService
class WordpressForm(PluginDataForm):
class WordPressForm(PluginDataForm):
username = forms.CharField(label=_("Username"), max_length=64)
password = forms.CharField(label=_("Password"), max_length=64)
site_name = forms.CharField(label=_("Site name"), max_length=64,
help_text=_("URL will be &lt;site_name&gt;.blogs.orchestra.lan"))
email = forms.EmailField(label=_("Email"))
def __init__(self, *args, **kwargs):
super(WordPressForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance')
if instance:
url = 'http://%s.%s' % (instance.data['site_name'], 'blogs.orchestra.lan')
url = '<a href="%s">%s</a>' % (url, url)
self.fields['site_name'].help_text = mark_safe(url)
class WordpressService(SoftwareService):
verbose_name = "WordPress"
form = WordpressForm
form = WordPressForm
description_field = 'site_name'
icon = 'saas/icons/WordPress.png'

View file

@ -28,7 +28,7 @@
{% else %}
<ul>
{% for plugin in plugins %}
<li><a style="font-size:small;" href="../?{{ field }}={{ plugin.get_name }}&{{ request.META.QUERY_STRING }}">{{ plugin.verbose_name }}</<a></li>
<li><a style="font-size:small;" href="../?{{ field }}={{ plugin.get_plugin_name }}&{{ request.META.QUERY_STRING }}">{{ plugin.verbose_name }}</<a></li>
{% endfor %}
</ul>
{% endif %}

View file

@ -24,22 +24,26 @@ class Plugin(object):
raise KeyError('This plugin is not registered')
@classmethod
def get_plugin_choices(cls):
plugins = cls.get_plugins()
choices = []
for p in plugins:
def get_verbose_name(cls):
# don't evaluate p.verbose_name ugettext_lazy
verbose = getattr(p.verbose_name, '_proxy____args', [p.verbose_name])
verbose = getattr(cls.verbose_name, '_proxy____args', [cls.verbose_name])
if verbose[0]:
verbose = p.verbose_name
return cls.verbose_name
else:
verbose = p.get_plugin_name()
choices.append((p.get_plugin_name(), verbose))
return sorted(choices, key=lambda e: e[0])
return cls.get_plugin_name()
@classmethod
def get_plugin_choices(cls):
choices = []
for plugin in cls.get_plugins():
verbose = plugin.get_verbose_name()
choices.append((plugin.get_plugin_name(), verbose))
return sorted(choices, key=lambda e: e[1])
class PluginMount(type):
def __init__(cls, name, bases, attrs):
if not attrs.get('abstract', False):
if not hasattr(cls, 'plugins'):
# This branch only executes when processing the mount point itself.
# So, since this is a new plugin type, not an implementation, this

View file

@ -0,0 +1,48 @@
apt-get install mailman
newlist mailman
echo 'mailman: "|/var/lib/mailman/mail/mailman post mailman"
mailman-admin: "|/var/lib/mailman/mail/mailman admin mailman"
mailman-bounces: "|/var/lib/mailman/mail/mailman bounces mailman"
mailman-confirm: "|/var/lib/mailman/mail/mailman confirm mailman"
mailman-join: "|/var/lib/mailman/mail/mailman join mailman"
mailman-leave: "|/var/lib/mailman/mail/mailman leave mailman"
mailman-owner: "|/var/lib/mailman/mail/mailman owner mailman"
mailman-request: "|/var/lib/mailman/mail/mailman request mailman"
mailman-subscribe: "|/var/lib/mailman/mail/mailman subscribe mailman"
mailman-unsubscribe: "|/var/lib/mailman/mail/mailman unsubscribe mailman"' >> /etc/aliases
postalias /etc/aliases
/etc/init.d/mailman start
# postifx
relay_domains = $mydestination, lists.orchestra.lan
relay_recipient_maps = hash:/var/lib/mailman/data/aliases
transport_maps = hash:/etc/postfix/transport
mailman_destination_recipient_limit = 1
echo "lists.orchestra.lan mailman:" >> /etc/postfix/transport
postmap /etc/postfix/transport
echo 'MTA = "Postfix"
POSTFIX_STYLE_VIRTUAL_DOMAINS = ["lists.orchestra.lan"]
# alias for postmaster, abuse and mailer-daemon
DEB_LISTMASTER = "postmaster@orchestra.lan"' >> /etc/mailman/mm_cfg.py
sed -i "s/DEFAULT_EMAIL_HOST\s*=\s*.*/DEFAULT_EMAIL_HOST = 'lists.orchestra.lan'/" /etc/mailman/mm_cfg.py
sed -i "s/DEFAULT_URL_HOST\s*=\s*.*/DEFAULT_URL_HOST = 'lists.orchestra.lan'/" /etc/mailman/mm_cfg.py