Renamed mails apps to mailboxes

This commit is contained in:
Marc 2014-10-17 10:04:47 +00:00
parent 0dcdb4ba79
commit 1e8e57c979
38 changed files with 252 additions and 178 deletions

View file

@ -36,7 +36,7 @@ Django-orchestra can be installed on any Linux system, however it is **strongly
5. Create and configure a Postgres database
```bash
sudo python manage.py setuppostgres
sudo python manage.py setuppostgres --db_password <password>
python manage.py syncdb
python manage.py migrate
```

View file

@ -136,3 +136,12 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* update_fields=[] doesn't trigger post save!
* lists -> SaaS ?
* move bill contact to bills apps
* autocreate <account>.orchestra.lan
* Backend optimization
* fields = ()
* ignore_fields = ()
* based on a merge set of save(update_fields)

View file

@ -112,6 +112,7 @@ class ChangeAddFieldsMixin(object):
add_fieldsets = ()
add_form = None
change_readonly_fields = ()
add_inlines = ()
def get_readonly_fields(self, request, obj=None):
fields = super(ChangeAddFieldsMixin, self).get_readonly_fields(request, obj=obj)
@ -129,9 +130,10 @@ class ChangeAddFieldsMixin(object):
def get_inline_instances(self, request, obj=None):
""" add_inlines and inline.parent_object """
self.inlines = getattr(self, 'add_inlines', self.inlines)
if obj:
self.inlines = type(self).inlines
else:
self.inlines = self.add_inlines or self.inlines
inlines = super(ChangeAddFieldsMixin, self).get_inline_instances(request, obj=obj)
for inline in inlines:
inline.parent_object = obj

View file

@ -7,13 +7,14 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import admin_date
from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.admin.utils import admin_date, insertattr
from orchestra.apps.accounts.admin import AccountAdminMixin, AccountAdmin
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
from . import settings
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
from .filters import BillTypeListFilter, HasBillContactListFilter
from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine, BillContact
PAYMENT_STATE_COLORS = {
@ -164,3 +165,27 @@ admin.site.register(AmendmentInvoice, BillAdmin)
admin.site.register(Fee, BillAdmin)
admin.site.register(AmendmentFee, BillAdmin)
admin.site.register(ProForma, BillAdmin)
class BillContactInline(admin.StackedInline):
model = BillContact
fields = ('name', 'address', ('city', 'zipcode'), 'country', 'vat')
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(45)
return super(BillContactInline, self).formfield_for_dbfield(db_field, **kwargs)
def has_bill_contact(account):
return hasattr(account, 'billcontact')
has_bill_contact.boolean = True
has_bill_contact.admin_order_field = 'billcontact'
insertattr(AccountAdmin, 'inlines', BillContactInline)
insertattr(AccountAdmin, 'list_display', has_bill_contact)
insertattr(AccountAdmin, 'list_filter', HasBillContactListFilter)

View file

@ -10,6 +10,3 @@ from .serializers import BillSerializer
class BillViewSet(AccountApiMixin, viewsets.ModelViewSet):
model = Bill
serializer_class = BillSerializer
router.register(r'bills', BillViewSet)

View file

@ -22,13 +22,12 @@ class BillTypeListFilter(SimpleListFilter):
('proforma', _("Pro-forma")),
)
def queryset(self, request, queryset):
return queryset
def value(self):
return self.request.path.split('/')[-2]
def choices(self, cl):
for lookup, title in self.lookup_choices:
yield {
@ -37,3 +36,20 @@ class BillTypeListFilter(SimpleListFilter):
'display': title,
}
class HasBillContactListFilter(SimpleListFilter):
""" Filter Nodes by group according to request.user """
title = _("has bill contact")
parameter_name = 'bill'
def lookups(self, request, model_admin):
return (
('True', _("Yes")),
('False', _("No")),
)
def queryset(self, request, queryset):
if self.value() == 'True':
return queryset.filter(billcontact__isnull=False)
if self.value() == 'False':
return queryset.filter(billcontact__isnull=True)

View file

@ -16,6 +16,22 @@ from orchestra.utils.html import html_to_pdf
from . import settings
class BillContact(models.Model):
account = models.OneToOneField('accounts.Account', verbose_name=_("account"),
related_name='billcontact')
name = models.CharField(_("name"), max_length=256)
address = models.TextField(_("address"))
city = models.CharField(_("city"), max_length=128,
default=settings.BILLS_CONTACT_DEFAULT_CITY)
zipcode = models.PositiveIntegerField(_("zip code"))
country = models.CharField(_("country"), max_length=20,
default=settings.BILLS_CONTACT_DEFAULT_COUNTRY)
vat = models.CharField(_("VAT number"), max_length=64)
def __unicode__(self):
return self.name
class BillManager(models.Manager):
def get_queryset(self):
queryset = super(BillManager, self).get_queryset()
@ -73,11 +89,11 @@ class Bill(models.Model):
@cached_property
def seller(self):
return Account.get_main().invoicecontact
return Account.get_main().billcontact
@cached_property
def buyer(self):
return self.account.invoicecontact
return self.account.billcontact
@cached_property
def payment_state(self):

View file

@ -1,8 +1,10 @@
from rest_framework import serializers
from orchestra.api import router
from orchestra.apps.accounts.models import Account
from orchestra.apps.accounts.serializers import AccountSerializerMixin
from .models import Bill, BillLine
from .models import Bill, BillLine, BillContact
class BillLineSerializer(serializers.HyperlinkedModelSerializer):
@ -20,3 +22,12 @@ class BillSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeriali
'url', 'number', 'type', 'total', 'is_sent', 'created_on', 'due_on',
'comments', 'html', 'lines'
)
class BillContactSerializer(AccountSerializerMixin, serializers.ModelSerializer):
class Meta:
model = BillContact
fields = ('name', 'address', 'city', 'zipcode', 'country', 'vat')
router.insert(Account, 'billcontact', BillContactSerializer, required=False)

View file

@ -3,39 +3,58 @@ from django.conf import settings
BILLS_NUMBER_LENGTH = getattr(settings, 'BILLS_NUMBER_LENGTH', 4)
BILLS_INVOICE_NUMBER_PREFIX = getattr(settings, 'BILLS_INVOICE_NUMBER_PREFIX', 'I')
BILLS_AMENDMENT_INVOICE_NUMBER_PREFIX = getattr(settings, 'BILLS_AMENDMENT_INVOICE_NUMBER_PREFIX', 'A')
BILLS_FEE_NUMBER_PREFIX = getattr(settings, 'BILLS_FEE_NUMBER_PREFIX', 'F')
BILLS_AMENDMENT_FEE_NUMBER_PREFIX = getattr(settings, 'BILLS_AMENDMENT_FEE_NUMBER_PREFIX', 'B')
BILLS_PROFORMA_NUMBER_PREFIX = getattr(settings, 'BILLS_PROFORMA_NUMBER_PREFIX', 'P')
BILLS_DEFAULT_TEMPLATE = getattr(settings, 'BILLS_DEFAULT_TEMPLATE', 'bills/microspective.html')
BILLS_DEFAULT_TEMPLATE = getattr(settings, 'BILLS_DEFAULT_TEMPLATE',
'bills/microspective.html')
BILLS_FEE_TEMPLATE = getattr(settings, 'BILLS_FEE_TEMPLATE', 'bills/microspective-fee.html')
BILLS_PROFORMA_TEMPLATE = getattr(settings, 'BILLS_PROFORMA_TEMPLATE', 'bills/microspective-proforma.html')
BILLS_FEE_TEMPLATE = getattr(settings, 'BILLS_FEE_TEMPLATE',
'bills/microspective-fee.html')
BILLS_PROFORMA_TEMPLATE = getattr(settings, 'BILLS_PROFORMA_TEMPLATE',
'bills/microspective-proforma.html')
BILLS_CURRENCY = getattr(settings, 'BILLS_CURRENCY', 'euro')
BILLS_SELLER_PHONE = getattr(settings, 'BILLS_SELLER_PHONE', '111-112-11-222')
BILLS_SELLER_EMAIL = getattr(settings, 'BILLS_SELLER_EMAIL', 'sales@orchestra.lan')
BILLS_SELLER_WEBSITE = getattr(settings, 'BILLS_SELLER_WEBSITE', 'www.orchestra.lan')
BILLS_SELLER_BANK_ACCOUNT = getattr(settings, 'BILLS_SELLER_BANK_ACCOUNT', '0000 0000 00 00000000 (Orchestra Bank)')
BILLS_SELLER_BANK_ACCOUNT = getattr(settings, 'BILLS_SELLER_BANK_ACCOUNT',
'0000 0000 00 00000000 (Orchestra Bank)')
BILLS_EMAIL_NOTIFICATION_TEMPLATE = getattr(settings, 'BILLS_EMAIL_NOTIFICATION_TEMPLATE',
'bills/bill-notification.email')
BILLS_EMAIL_NOTIFICATION_TEMPLATE = getattr(settings, 'BILLS_EMAIL_NOTIFICATION_TEMPLATE',
'bills/bill-notification.email')
BILLS_ORDER_MODEL = getattr(settings, 'BILLS_ORDER_MODEL', 'orders.Order')
BILLS_CONTACT_DEFAULT_CITY = getattr(settings, 'BILLS_CONTACT_DEFAULT_CITY', 'Barcelona')
BILLS_CONTACT_DEFAULT_COUNTRY = getattr(settings, 'BILLS_CONTACT_DEFAULT_COUNTRY', 'Spain')

View file

@ -141,6 +141,3 @@
</div>
{% endblock %}
{% endblock %}

View file

@ -6,8 +6,8 @@ from orchestra.admin import AtLeastOneRequiredInlineFormSet
from orchestra.admin.utils import insertattr
from orchestra.apps.accounts.admin import AccountAdmin, AccountAdminMixin
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
from .filters import HasInvoiceContactListFilter
from .models import Contact, InvoiceContact
from .models import Contact
class ContactAdmin(AccountAdminMixin, admin.ModelAdmin):
@ -69,20 +69,7 @@ class ContactAdmin(AccountAdminMixin, admin.ModelAdmin):
admin.site.register(Contact, ContactAdmin)
class InvoiceContactInline(admin.StackedInline):
model = InvoiceContact
fields = ('name', 'address', ('city', 'zipcode'), 'country', 'vat')
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(45)
return super(InvoiceContactInline, self).formfield_for_dbfield(db_field, **kwargs)
class ContactInline(InvoiceContactInline):
class ContactInline(admin.StackedInline):
model = Contact
formset = AtLeastOneRequiredInlineFormSet
extra = 0
@ -93,18 +80,17 @@ class ContactInline(InvoiceContactInline):
def get_extra(self, request, obj=None, **kwargs):
return 0 if obj and obj.contacts.exists() else 1
def has_invoice(account):
return hasattr(account, 'invoicecontact')
has_invoice.boolean = True
has_invoice.admin_order_field = 'invoicecontact'
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(45)
return super(ContactInline, self).formfield_for_dbfield(db_field, **kwargs)
insertattr(AccountAdmin, 'inlines', ContactInline)
insertattr(AccountAdmin, 'inlines', InvoiceContactInline)
insertattr(AccountAdmin, 'list_display', has_invoice)
insertattr(AccountAdmin, 'list_filter', HasInvoiceContactListFilter)
search_fields = (
'contacts__short_name', 'contacts__full_name', 'contacts__phone',
'contacts__phone2', 'contacts__email'

View file

@ -3,8 +3,8 @@ from rest_framework import viewsets
from orchestra.api import router
from orchestra.apps.accounts.api import AccountApiMixin
from .models import Contact, InvoiceContact
from .serializers import ContactSerializer, InvoiceContactSerializer
from .models import Contact
from .serializers import ContactSerializer
class ContactViewSet(AccountApiMixin, viewsets.ModelViewSet):
@ -12,10 +12,5 @@ class ContactViewSet(AccountApiMixin, viewsets.ModelViewSet):
serializer_class = ContactSerializer
class InvoiceContactViewSet(AccountApiMixin, viewsets.ModelViewSet):
model = InvoiceContact
serializer_class = InvoiceContactSerializer
router.register(r'contacts', ContactViewSet)
router.register(r'invoicecontacts', InvoiceContactViewSet)

View file

@ -1,20 +0,0 @@
from django.contrib.admin import SimpleListFilter
from django.utils.translation import ugettext_lazy as _
class HasInvoiceContactListFilter(SimpleListFilter):
""" Filter Nodes by group according to request.user """
title = _("has invoice contact")
parameter_name = 'invoice'
def lookups(self, request, model_admin):
return (
('True', _("Yes")),
('False', _("No")),
)
def queryset(self, request, queryset):
if self.value() == 'True':
return queryset.filter(invoicecontact__isnull=False)
if self.value() == 'False':
return queryset.filter(invoicecontact__isnull=True)

View file

@ -50,20 +50,4 @@ class Contact(models.Model):
return self.short_name
class InvoiceContact(models.Model):
account = models.OneToOneField('accounts.Account', verbose_name=_("account"),
related_name='invoicecontact')
name = models.CharField(_("name"), max_length=256)
address = models.TextField(_("address"))
city = models.CharField(_("city"), max_length=128,
default=settings.CONTACTS_DEFAULT_CITY)
zipcode = models.PositiveIntegerField(_("zip code"))
country = models.CharField(_("country"), max_length=20,
default=settings.CONTACTS_DEFAULT_COUNTRY)
vat = models.CharField(_("VAT number"), max_length=64)
def __unicode__(self):
return self.name
accounts.register(Contact)

View file

@ -3,7 +3,7 @@ from rest_framework import serializers
from orchestra.api.serializers import MultiSelectField
from orchestra.apps.accounts.serializers import AccountSerializerMixin
from .models import Contact, InvoiceContact
from .models import Contact
class ContactSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
@ -14,9 +14,3 @@ class ContactSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri
'url', 'short_name', 'full_name', 'email', 'email_usage', 'phone',
'phone2', 'address', 'city', 'zipcode', 'country'
)
class InvoiceContactSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
class Meta:
model = InvoiceContact
fields = ('url', 'name', 'address', 'city', 'zipcode', 'country', 'vat')

View file

@ -13,6 +13,8 @@ class MySQLBackend(ServiceController):
model = 'databases.Database'
def save(self, database):
if database.type != database.MYSQL:
return
context = self.get_context(database)
# Not available on delete()
context['owner'] = database.owner
@ -32,6 +34,8 @@ class MySQLBackend(ServiceController):
))
def delete(self, database):
if database.type != database.MYSQL:
return
context = self.get_context(database)
self.append("mysql -e 'DROP DATABASE `%(database)s`;'" % context)
@ -50,6 +54,8 @@ class MySQLUserBackend(ServiceController):
model = 'databases.DatabaseUser'
def save(self, user):
if user.type != user.MYSQL:
return
context = self.get_context(user)
self.append(textwrap.dedent("""\
mysql -e 'CREATE USER "%(username)s"@"%(host)s";' || true \
@ -61,6 +67,8 @@ class MySQLUserBackend(ServiceController):
))
def delete(self, user):
if user.type != user.MYSQL:
return
context = self.get_context(user)
self.append(textwrap.dedent("""\
mysql -e 'DROP USER "%(username)s"@"%(host)s";' \
@ -83,6 +91,8 @@ class MysqlDisk(ServiceMonitor):
verbose_name = _("MySQL disk")
def exceeded(self, db):
if db.type != db.MYSQL:
return
context = self.get_context(db)
self.append(textwrap.dedent("""\
mysql -e 'UPDATE db SET Insert_priv="N", Create_priv="N" WHERE Db="%(db_name)s";' \
@ -90,6 +100,8 @@ class MysqlDisk(ServiceMonitor):
))
def recovery(self, db):
if db.type != db.MYSQL:
return
context = self.get_context(db)
self.append(textwrap.dedent("""\
mysql -e 'UPDATE db SET Insert_priv="Y", Create_priv="Y" WHERE Db="%(db_name)s";' \
@ -97,6 +109,8 @@ class MysqlDisk(ServiceMonitor):
))
def monitor(self, db):
if db.type != db.MYSQL:
return
context = self.get_context(db)
self.append(textwrap.dedent("""\
echo %(db_id)s $(mysql -B -e '"

View file

@ -41,8 +41,8 @@ Database.users.through._meta.unique_together = (('database', 'databaseuser'),)
class DatabaseUser(models.Model):
MYSQL = 'mysql'
POSTGRESQL = 'postgresql'
MYSQL = Database.MYSQL
POSTGRESQL = Database.POSTGRESQL
username = models.CharField(_("username"), max_length=16, # MySQL usernames 16 char long
validators=[validators.validate_name])

View file

@ -76,7 +76,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdm
def addresses_field(self, mailbox):
""" Address form field with "Add address" button """
account = mailbox.account
add_url = reverse('admin:mails_address_add')
add_url = reverse('admin:mailboxes_address_add')
add_url += '?account=%d&mailboxes=%s' % (account.pk, mailbox.pk)
img = '<img src="/static/admin/img/icon_addlink.gif" width="10" height="10" alt="Add Another">'
onclick = 'onclick="return showAddAnotherPopup(this);"'
@ -84,7 +84,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdm
add_url=add_url, onclick=onclick, img=img)
value = '%s<br><br>' % add_link
for pk, name, domain in mailbox.addresses.values_list('pk', 'name', 'domain__name'):
url = reverse('admin:mails_address_change', args=(pk,))
url = reverse('admin:mailboxes_address_change', args=(pk,))
name = '%s@%s' % (name, domain)
value += '<li><a href="%s">%s</a></li>' % (url, name)
value = '<ul>%s</ul>' % value

View file

@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
class PasswdVirtualUserBackend(ServiceController):
verbose_name = _("Mail virtual user (passwd-file)")
model = 'mails.Mailbox'
model = 'mailboxes.Mailbox'
# TODO related_models = ('resources__content_type') ?? needed for updating disk usage from resource.data
DEFAULT_GROUP = 'postfix'
@ -86,7 +86,7 @@ class PasswdVirtualUserBackend(ServiceController):
def commit(self):
context = {
'virtual_mailbox_maps': settings.MAILS_VIRTUAL_MAILBOX_MAPS_PATH
'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH
}
self.append(
"[[ $UPDATED_VIRTUAL_MAILBOX_MAPS == 1 ]] && { postmap %(virtual_mailbox_maps)s; }"
@ -102,11 +102,11 @@ class PasswdVirtualUserBackend(ServiceController):
'gid': 10000 + mailbox.pk,
'group': self.DEFAULT_GROUP,
'quota': self.get_quota(mailbox),
'passwd_path': settings.MAILS_PASSWD_PATH,
'passwd_path': settings.MAILBOXES_PASSWD_PATH,
'home': mailbox.get_home(),
'banner': self.get_banner(),
'virtual_mailbox_maps': settings.MAILS_VIRTUAL_MAILBOX_MAPS_PATH,
'mailbox_domain': settings.MAILS_VIRTUAL_MAILBOX_DEFAULT_DOMAIN,
'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH,
'mailbox_domain': settings.MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN,
}
context['extra_fields'] = self.get_extra_fields(mailbox, context)
context['passwd'] = '{username}:{password}:{uid}:{gid}::{home}::{extra_fields}'.format(**context)
@ -115,7 +115,7 @@ class PasswdVirtualUserBackend(ServiceController):
class PostfixAddressBackend(ServiceController):
verbose_name = _("Postfix address")
model = 'mails.Address'
model = 'mailboxes.Address'
def include_virtual_alias_domain(self, context):
self.append(textwrap.dedent("""
@ -185,8 +185,8 @@ class PostfixAddressBackend(ServiceController):
def get_context_files(self):
return {
'virtual_alias_domains': settings.MAILS_VIRTUAL_ALIAS_DOMAINS_PATH,
'virtual_alias_maps': settings.MAILS_VIRTUAL_ALIAS_MAPS_PATH
'virtual_alias_domains': settings.MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH,
'virtual_alias_maps': settings.MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH
}
def get_context(self, address):
@ -194,7 +194,7 @@ class PostfixAddressBackend(ServiceController):
context.update({
'domain': address.domain,
'email': address.email,
'mailbox_domain': settings.MAILS_VIRTUAL_MAILBOX_DEFAULT_DOMAIN,
'mailbox_domain': settings.MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN,
})
return context
@ -205,7 +205,7 @@ class AutoresponseBackend(ServiceController):
class MaildirDisk(ServiceMonitor):
model = 'mails.Mailbox'
model = 'mailboxes.Mailbox'
resource = ServiceMonitor.DISK
verbose_name = _("Maildir disk usage")

View file

@ -22,14 +22,14 @@ class Mailbox(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='mailboxes')
filtering = models.CharField(max_length=16,
choices=[(k, v[0]) for k,v in settings.MAILS_MAILBOX_FILTERINGS.iteritems()],
default=settings.MAILS_MAILBOX_DEFAULT_FILTERING)
choices=[(k, v[0]) for k,v in settings.MAILBOXES_MAILBOX_FILTERINGS.iteritems()],
default=settings.MAILBOXES_MAILBOX_DEFAULT_FILTERING)
custom_filtering = models.TextField(_("filtering"), blank=True,
validators=[validators.validate_sieve],
help_text=_("Arbitrary email filtering in sieve language. "
"This overrides any automatic junk email filtering"))
is_active = models.BooleanField(_("active"), default=True)
# addresses = models.ManyToManyField('mails.Address',
# addresses = models.ManyToManyField('mailboxes.Address',
# verbose_name=_("addresses"),
# related_name='mailboxes', blank=True)
@ -54,7 +54,7 @@ class Mailbox(models.Model):
'name': self.name,
'username': self.name,
}
home = settings.MAILS_HOME % context
home = settings.MAILBOXES_HOME % context
return home.rstrip('/')
def clean(self):
@ -62,7 +62,7 @@ class Mailbox(models.Model):
self.custom_filtering = ''
def get_filtering(self):
__, filtering = settings.MAILS_MAILBOX_FILTERINGS[self.filtering]
__, filtering = settings.MAILBOXES_MAILBOX_FILTERINGS[self.filtering]
if isinstance(filtering, basestring):
return filtering
return filtering(self)
@ -83,7 +83,7 @@ class Mailbox(models.Model):
class Address(models.Model):
name = models.CharField(_("name"), max_length=64,
validators=[validators.validate_emailname])
domain = models.ForeignKey(settings.MAILS_DOMAIN_MODEL,
domain = models.ForeignKey(settings.MAILBOXES_DOMAIN_MODEL,
verbose_name=_("domain"),
related_name='addresses')
mailboxes = models.ManyToManyField(Mailbox,

View file

@ -10,10 +10,33 @@ from orchestra.core.validators import validate_password
from .models import Mailbox, Address
class RelatedDomainSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
class Meta:
model = Address.domain.field.rel.to
fields = ('url', 'name')
def from_native(self, data, files=None):
queryset = self.opts.model.objects.filter(account=self.account)
return get_object_or_404(queryset, name=data['name'])
class RelatedAddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
domain = RelatedDomainSerializer()
class Meta:
model = Address
fields = ('url', 'name', 'domain', 'forward')
def from_native(self, data, files=None):
queryset = self.opts.model.objects.filter(account=self.account)
return get_object_or_404(queryset, name=data['name'])
class MailboxSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
password = serializers.CharField(max_length=128, label=_('Password'),
validators=[validate_password], write_only=True, required=False,
widget=widgets.PasswordInput)
addresses = RelatedAddressSerializer(many=True, read_only=True)
class Meta:
model = Mailbox
@ -48,16 +71,6 @@ class RelatedMailboxSerializer(AccountSerializerMixin, serializers.HyperlinkedMo
return get_object_or_404(queryset, name=data['name'])
class RelatedDomainSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
class Meta:
model = Address.domain.field.rel.to
fields = ('url', 'name')
def from_native(self, data, files=None):
queryset = self.opts.model.objects.filter(account=self.account)
return get_object_or_404(queryset, name=data['name'])
class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
domain = RelatedDomainSerializer()
mailboxes = RelatedMailboxSerializer(many=True, allow_add_remove=True, required=False)

View file

@ -4,40 +4,39 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _
MAILS_DOMAIN_MODEL = getattr(settings, 'MAILS_DOMAIN_MODEL', 'domains.Domain')
MAILBOXES_DOMAIN_MODEL = getattr(settings, 'MAILBOXES_DOMAIN_MODEL', 'domains.Domain')
MAILS_HOME = getattr(settings, 'MAILS_HOME', '/home/%(name)s/')
MAILBOXES_HOME = getattr(settings, 'MAILBOXES_HOME', '/home/%(name)s/')
MAILS_SIEVETEST_PATH = getattr(settings, 'MAILS_SIEVETEST_PATH', '/dev/shm')
MAILBOXES_SIEVETEST_PATH = getattr(settings, 'MAILBOXES_SIEVETEST_PATH', '/dev/shm')
MAILS_SIEVETEST_BIN_PATH = getattr(settings, 'MAILS_SIEVETEST_BIN_PATH',
MAILBOXES_SIEVETEST_BIN_PATH = getattr(settings, 'MAILBOXES_SIEVETEST_BIN_PATH',
'%(orchestra_root)s/bin/sieve-test')
MAILS_VIRTUAL_MAILBOX_MAPS_PATH = getattr(settings, 'MAILS_VIRTUAL_MAILBOX_MAPS_PATH',
MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH = getattr(settings, 'MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH',
'/etc/postfix/virtual_mailboxes')
MAILS_VIRTUAL_ALIAS_MAPS_PATH = getattr(settings, 'MAILS_VIRTUAL_ALIAS_MAPS_PATH',
MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH = getattr(settings, 'MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH',
'/etc/postfix/virtual_aliases')
MAILS_VIRTUAL_ALIAS_DOMAINS_PATH = getattr(settings, 'MAILS_VIRTUAL_ALIAS_DOMAINS_PATH',
MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH = getattr(settings, 'MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH',
'/etc/postfix/virtual_domains')
MAILS_VIRTUAL_MAILBOX_DEFAULT_DOMAIN = getattr(settings, 'MAILS_VIRTUAL_MAILBOX_DEFAULT_DOMAIN',
MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN = getattr(settings, 'MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN',
'orchestra.lan')
MAILS_PASSWD_PATH = getattr(settings, 'MAILS_PASSWD_PATH',
MAILBOXES_PASSWD_PATH = getattr(settings, 'MAILBOXES_PASSWD_PATH',
'/etc/dovecot/passwd')
MAILS_MAILBOX_FILTERINGS = getattr(settings, 'MAILS_MAILBOX_FILTERINGS', {
MAILBOXES_MAILBOX_FILTERINGS = getattr(settings, 'MAILBOXES_MAILBOX_FILTERINGS', {
# value: (verbose_name, filter)
'DISABLE': (_("Disable"), ''),
'REJECT': (_("Reject spam"), textwrap.dedent("""
@ -56,4 +55,4 @@ MAILS_MAILBOX_FILTERINGS = getattr(settings, 'MAILS_MAILBOX_FILTERINGS', {
})
MAILS_MAILBOX_DEFAULT_FILTERING = getattr(settings, 'MAILS_MAILBOX_DEFAULT_FILTERING', 'REDIRECT')
MAILBOXES_MAILBOX_DEFAULT_FILTERING = getattr(settings, 'MAILBOXES_MAILBOX_DEFAULT_FILTERING', 'REDIRECT')

View file

@ -228,7 +228,7 @@ class MailboxMixin(object):
imap.create(folder)
self.validate_mailbox(username)
token = random_ascii(100)
self.send_email("%s@%s" % (username, settings.MAILS_VIRTUAL_MAILBOX_DEFAULT_DOMAIN), token)
self.send_email("%s@%s" % (username, settings.MAILBOXES_VIRTUAL_MAILBOX_DEFAULT_DOMAIN), token)
home = Mailbox.objects.get(name=username).get_home()
sshrun(self.MASTER_SERVER,
"grep '%s' %s/Maildir/.%s/new/*" % (token, home, folder), display=False)
@ -300,7 +300,7 @@ class AdminMailboxMixin(MailboxMixin):
@snapshot_on_error
def add(self, username, password, quota=None, filtering=None):
url = self.live_server_url + reverse('admin:mails_mailbox_add')
url = self.live_server_url + reverse('admin:mailboxes_mailbox_add')
self.selenium.get(url)
account_input = self.selenium.find_element_by_id('id_account')
@ -346,7 +346,7 @@ class AdminMailboxMixin(MailboxMixin):
@snapshot_on_error
def add_address(self, username, name, domain):
url = self.live_server_url + reverse('admin:mails_address_add')
url = self.live_server_url + reverse('admin:mailboxes_address_add')
self.selenium.get(url)
name_field = self.selenium.find_element_by_id('id_name')

View file

@ -40,13 +40,13 @@ def validate_forward(value):
def validate_sieve(value):
sieve_name = '%s.sieve' % hashlib.md5(value).hexdigest()
path = os.path.join(settings.MAILS_SIEVETEST_PATH, sieve_name)
path = os.path.join(settings.MAILBOXES_SIEVETEST_PATH, sieve_name)
with open(path, 'wb') as f:
f.write(value)
context = {
'orchestra_root': paths.get_orchestra_root()
}
sievetest = settings.MAILS_SIEVETEST_BIN_PATH % context
sievetest = settings.MAILBOXES_SIEVETEST_BIN_PATH % context
try:
test = run(' '.join([sievetest, path, '/dev/null']), display=False)
except CommandError:

View file

@ -19,18 +19,22 @@ transports = {}
def BashSSH(backend, log, server, cmds):
from .models import BackendLog
# TODO save remote file into a root read only directory to avoid users sniffing passwords and stuff
script = '\n'.join(['set -e', 'set -o pipefail'] + cmds + ['exit 0'])
script = script.replace('\r', '')
log.script = script
digest = hashlib.md5(script).hexdigest()
path = os.path.join(settings.ORCHESTRATION_TEMP_SCRIPT_PATH, digest)
remote_path = "%s.remote" % path
log.script = '# %s\n%s' % (remote_path, script)
log.save(update_fields=['script'])
logger.debug('%s is going to be executed on %s' % (backend, server))
channel = None
ssh = None
try:
logger.debug('%s is going to be executed on %s' % (backend, server))
# Avoid "Argument list too long" on large scripts by genereting a file
# and scping it to the remote server
digest = hashlib.md5(script).hexdigest()
path = os.path.join(settings.ORCHESTRATION_TEMP_SCRIPT_PATH, digest)
with open(path, 'w') as script_file:
script_file.write(script)
@ -50,19 +54,19 @@ def BashSSH(backend, log, server, cmds):
# Copy script to remote server
sftp = paramiko.SFTPClient.from_transport(transport)
sftp.put(path, "%s.remote" % path)
sftp.put(path, remote_path)
sftp.close()
os.remove(path)
# Execute it
context = {
'path': "%s.remote" % path,
'remote_path': remote_path,
'digest': digest
}
cmd = (
"[[ $(md5sum %(path)s|awk {'print $1'}) == %(digest)s ]] && bash %(path)s\n"
"[[ $(md5sum %(remote_path)s|awk {'print $1'}) == %(digest)s ]] && bash %(remote_path)s\n"
"RETURN_CODE=$?\n"
# TODO "rm -fr %(path)s\n"
# TODO "rm -fr %(remote_path)s\n"
"exit $RETURN_CODE" % context
)
channel = transport.open_session()

View file

@ -1,17 +1,16 @@
import decimal
from django.contrib.contenttypes.models import ContentType
from django.core.validators import ValidationError
from django.db import models
from django.db.models import Q
from django.db.models.loading import get_model
from django.contrib.contenttypes.models import ContentType
from django.core.validators import ValidationError
from django.utils.functional import cached_property
from django.utils.module_loading import autodiscover_modules
from django.utils.translation import ugettext_lazy as _
from orchestra.core import caches, services, accounts
from orchestra.models import queryset
#from orchestra.utils.apps import autodiscover
from . import settings, rating
from .handlers import ServiceHandler
@ -187,8 +186,6 @@ class Service(models.Model):
cache.set(key, services)
return services
# FIXME some times caching is nasty, do we really have to? make get_plugin more efficient?
# @property
@cached_property
def handler(self):
""" Accessor of this service handler instance """

View file

@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from freezegun import freeze_time
from orchestra.apps.mails.models import Mailbox
from orchestra.apps.mailboxes.models import Mailbox
from orchestra.apps.resources.models import Resource, ResourceData
from orchestra.utils.tests import random_ascii

View file

@ -35,6 +35,9 @@ class SystemUserBackend(ServiceController):
self.append("killall -u %(username)s || true" % context)
self.append("userdel %(username)s || true" % context)
self.append("groupdel %(username)s || true" % context)
self.delete_home(context, user)
def delete_home(self, context, user):
if user.is_main:
# TODO delete instead of this shit
context['deleted'] = context['home'].rstrip('/') + '.deleted'

View file

@ -130,7 +130,8 @@ function install_requirements () {
libxml2-dev \
libxslt1-dev \
wkhtmltopdf \
xvfb"
xvfb \
ca-certificates"
PIP="django==1.7 \
django-celery-email==1.0.4 \
@ -139,8 +140,8 @@ function install_requirements () {
IPy==0.81 \
django-extensions==1.1.1 \
django-transaction-signals==1.0.0 \
django-celery==3.1.10 \
celery==3.1.13 \
django-celery==3.1.16 \
celery==3.1.16 \
kombu==3.0.23 \
billiard==3.3.0.18 \
Markdown==2.4 \
@ -153,7 +154,8 @@ function install_requirements () {
jsonfield==0.9.22 \
lxml==3.3.5 \
python-dateutil==2.2 \
django-iban==0.3.0"
django-iban==0.3.0 \
requests"
if $testing; then
APT="${APT} \
@ -169,7 +171,6 @@ function install_requirements () {
django-debug-toolbar==1.2.1 \
django-nose==1.2 \
sqlparse \
requests \
--allow-external orchestra-orm --allow-unverified orchestra-orm"
fi
@ -180,7 +181,10 @@ function install_requirements () {
update-locale LANG=en_US.UTF-8
fi
# Install ca certificates
run apt-get update
run apt-get install -y $APT
# Install ca certificates before executing pip install
if [[ ! -e /usr/local/share/ca-certificates/cacert.org ]]; then
mkdir -p /usr/local/share/ca-certificates/cacert.org
wget -P /usr/local/share/ca-certificates/cacert.org \
@ -189,8 +193,6 @@ function install_requirements () {
update-ca-certificates
fi
run apt-get update
run apt-get install -y $APT
run pip install $PIP
# Some versions of rabbitmq-server will not start automatically by default unless ...

View file

@ -66,6 +66,8 @@ TEMPLATE_CONTEXT_PROCESSORS =(
INSTALLED_APPS = (
# django-orchestra apps
'orchestra',
'orchestra.apps.accounts',
'orchestra.apps.contacts',
'orchestra.apps.orchestration',
'orchestra.apps.domains',
'orchestra.apps.systemusers',
@ -73,7 +75,7 @@ INSTALLED_APPS = (
# 'orchestra.apps.users.roles.mail',
# 'orchestra.apps.users.roles.jabber',
# 'orchestra.apps.users.roles.posix',
'orchestra.apps.mails',
'orchestra.apps.mailboxes',
'orchestra.apps.lists',
'orchestra.apps.webapps',
'orchestra.apps.websites',
@ -99,7 +101,6 @@ INSTALLED_APPS = (
'rest_framework',
'rest_framework.authtoken',
'passlib.ext.django',
'django_nose',
# Django.contrib
'django.contrib.auth',
@ -109,8 +110,7 @@ INSTALLED_APPS = (
'django.contrib.staticfiles',
'django.contrib.admin.apps.SimpleAdminConfig',
'orchestra.apps.accounts',
'orchestra.apps.contacts',
# Last to load
'orchestra.apps.resources',
)
@ -175,8 +175,8 @@ FLUENT_DASHBOARD_APP_ICONS = {
# Services
'webs/web': 'web.png',
'mail/address': 'X-office-address-book.png',
'mails/mailbox': 'email.png',
'mails/address': 'X-office-address-book.png',
'mailboxes/mailbox': 'email.png',
'mailboxes/address': 'X-office-address-book.png',
'lists/list': 'email-alter.png',
'domains/domain': 'domain.png',
'multitenance/tenant': 'apps.png',

View file

@ -15,9 +15,14 @@ if "celeryd" in sys.argv or 'celeryev' in sys.argv or 'celerybeat' in sys.argv:
DEBUG = False
# Django debug toolbar
INSTALLED_APPS += ('debug_toolbar', )
INSTALLED_APPS += (
'debug_toolbar',
'django_nose',
)
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
INTERNAL_IPS = ('127.0.0.1', '10.0.3.1',) #10.0.3.1 is the lxcbr0 ip
INTERNAL_IPS = (
'127.0.0.1',
'10.0.3.1',
)
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'

View file

@ -4,11 +4,17 @@ from django.forms import CheckboxInput
from orchestra import get_version
from orchestra.admin.utils import change_url
from orchestra.utils.apps import isinstalled
register = template.Library()
@register.filter(name='isinstalled')
def app_is_installed(app_name):
return isinstalled(app_name)
@register.simple_tag(name="version")
def orchestra_version():
return get_version()