diff --git a/INSTALL.md b/INSTALL.md index 99255631..91a637ec 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -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 python manage.py syncdb python manage.py migrate ``` diff --git a/TODO.md b/TODO.md index aa42b62e..74d56f1c 100644 --- a/TODO.md +++ b/TODO.md @@ -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 .orchestra.lan + +* Backend optimization + * fields = () + * ignore_fields = () + * based on a merge set of save(update_fields) diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py index 2330fd69..78da86e1 100644 --- a/orchestra/admin/options.py +++ b/orchestra/admin/options.py @@ -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 diff --git a/orchestra/apps/bills/admin.py b/orchestra/apps/bills/admin.py index 88d5ddad..d440c801 100644 --- a/orchestra/apps/bills/admin.py +++ b/orchestra/apps/bills/admin.py @@ -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) diff --git a/orchestra/apps/bills/api.py b/orchestra/apps/bills/api.py index b96346fc..c195a10d 100644 --- a/orchestra/apps/bills/api.py +++ b/orchestra/apps/bills/api.py @@ -10,6 +10,3 @@ from .serializers import BillSerializer class BillViewSet(AccountApiMixin, viewsets.ModelViewSet): model = Bill serializer_class = BillSerializer - - -router.register(r'bills', BillViewSet) diff --git a/orchestra/apps/bills/filters.py b/orchestra/apps/bills/filters.py index 2e8c89b1..27e3ab17 100644 --- a/orchestra/apps/bills/filters.py +++ b/orchestra/apps/bills/filters.py @@ -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) diff --git a/orchestra/apps/bills/models.py b/orchestra/apps/bills/models.py index 09c489ee..3de1bab1 100644 --- a/orchestra/apps/bills/models.py +++ b/orchestra/apps/bills/models.py @@ -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): diff --git a/orchestra/apps/bills/serializers.py b/orchestra/apps/bills/serializers.py index 0c15ebde..ec53df4c 100644 --- a/orchestra/apps/bills/serializers.py +++ b/orchestra/apps/bills/serializers.py @@ -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) diff --git a/orchestra/apps/bills/settings.py b/orchestra/apps/bills/settings.py index 75702302..b4e30ada 100644 --- a/orchestra/apps/bills/settings.py +++ b/orchestra/apps/bills/settings.py @@ -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') diff --git a/orchestra/apps/bills/templates/bills/microspective.html b/orchestra/apps/bills/templates/bills/microspective.html index 6cbf2cfc..06e2fa05 100644 --- a/orchestra/apps/bills/templates/bills/microspective.html +++ b/orchestra/apps/bills/templates/bills/microspective.html @@ -141,6 +141,3 @@ {% endblock %} {% endblock %} - - - diff --git a/orchestra/apps/contacts/admin.py b/orchestra/apps/contacts/admin.py index d076feab..8ddb85b8 100644 --- a/orchestra/apps/contacts/admin.py +++ b/orchestra/apps/contacts/admin.py @@ -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' diff --git a/orchestra/apps/contacts/api.py b/orchestra/apps/contacts/api.py index 29332da6..72574f5f 100644 --- a/orchestra/apps/contacts/api.py +++ b/orchestra/apps/contacts/api.py @@ -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) + diff --git a/orchestra/apps/contacts/filters.py b/orchestra/apps/contacts/filters.py deleted file mode 100644 index eade8b81..00000000 --- a/orchestra/apps/contacts/filters.py +++ /dev/null @@ -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) diff --git a/orchestra/apps/contacts/models.py b/orchestra/apps/contacts/models.py index b9adbcc3..16824c6e 100644 --- a/orchestra/apps/contacts/models.py +++ b/orchestra/apps/contacts/models.py @@ -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) diff --git a/orchestra/apps/contacts/serializers.py b/orchestra/apps/contacts/serializers.py index 555e3125..41f3240d 100644 --- a/orchestra/apps/contacts/serializers.py +++ b/orchestra/apps/contacts/serializers.py @@ -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') diff --git a/orchestra/apps/databases/backends.py b/orchestra/apps/databases/backends.py index cf53ae1c..5a78d2b1 100644 --- a/orchestra/apps/databases/backends.py +++ b/orchestra/apps/databases/backends.py @@ -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 '" diff --git a/orchestra/apps/databases/models.py b/orchestra/apps/databases/models.py index 7aec82c4..c907a8ee 100644 --- a/orchestra/apps/databases/models.py +++ b/orchestra/apps/databases/models.py @@ -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]) diff --git a/orchestra/apps/mails/__init__.py b/orchestra/apps/mailboxes/__init__.py similarity index 100% rename from orchestra/apps/mails/__init__.py rename to orchestra/apps/mailboxes/__init__.py diff --git a/orchestra/apps/mails/admin.py b/orchestra/apps/mailboxes/admin.py similarity index 97% rename from orchestra/apps/mails/admin.py rename to orchestra/apps/mailboxes/admin.py index 52321c9f..7e4e663e 100644 --- a/orchestra/apps/mails/admin.py +++ b/orchestra/apps/mailboxes/admin.py @@ -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 = '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

' % 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 += '
  • %s
  • ' % (url, name) value = '' % value diff --git a/orchestra/apps/mails/api.py b/orchestra/apps/mailboxes/api.py similarity index 100% rename from orchestra/apps/mails/api.py rename to orchestra/apps/mailboxes/api.py diff --git a/orchestra/apps/mails/backends.py b/orchestra/apps/mailboxes/backends.py similarity index 92% rename from orchestra/apps/mails/backends.py rename to orchestra/apps/mailboxes/backends.py index d2ed5e26..40bbb8e3 100644 --- a/orchestra/apps/mails/backends.py +++ b/orchestra/apps/mailboxes/backends.py @@ -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") diff --git a/orchestra/apps/mails/filters.py b/orchestra/apps/mailboxes/filters.py similarity index 100% rename from orchestra/apps/mails/filters.py rename to orchestra/apps/mailboxes/filters.py diff --git a/orchestra/apps/mails/forms.py b/orchestra/apps/mailboxes/forms.py similarity index 100% rename from orchestra/apps/mails/forms.py rename to orchestra/apps/mailboxes/forms.py diff --git a/orchestra/apps/mails/models.py b/orchestra/apps/mailboxes/models.py similarity index 92% rename from orchestra/apps/mails/models.py rename to orchestra/apps/mailboxes/models.py index b6c445d1..d183cd9e 100644 --- a/orchestra/apps/mails/models.py +++ b/orchestra/apps/mailboxes/models.py @@ -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, diff --git a/orchestra/apps/mails/serializers.py b/orchestra/apps/mailboxes/serializers.py similarity index 85% rename from orchestra/apps/mails/serializers.py rename to orchestra/apps/mailboxes/serializers.py index 98aec736..88314a71 100644 --- a/orchestra/apps/mails/serializers.py +++ b/orchestra/apps/mailboxes/serializers.py @@ -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) diff --git a/orchestra/apps/mails/settings.py b/orchestra/apps/mailboxes/settings.py similarity index 53% rename from orchestra/apps/mails/settings.py rename to orchestra/apps/mailboxes/settings.py index 9281f5be..97d447e4 100644 --- a/orchestra/apps/mails/settings.py +++ b/orchestra/apps/mailboxes/settings.py @@ -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') diff --git a/orchestra/apps/mails/tests/__init__.py b/orchestra/apps/mailboxes/tests/__init__.py similarity index 100% rename from orchestra/apps/mails/tests/__init__.py rename to orchestra/apps/mailboxes/tests/__init__.py diff --git a/orchestra/apps/mails/tests/functional_tests/__init__.py b/orchestra/apps/mailboxes/tests/functional_tests/__init__.py similarity index 100% rename from orchestra/apps/mails/tests/functional_tests/__init__.py rename to orchestra/apps/mailboxes/tests/functional_tests/__init__.py diff --git a/orchestra/apps/mails/tests/functional_tests/tests.py b/orchestra/apps/mailboxes/tests/functional_tests/tests.py similarity index 98% rename from orchestra/apps/mails/tests/functional_tests/tests.py rename to orchestra/apps/mailboxes/tests/functional_tests/tests.py index b3718a31..136113c6 100644 --- a/orchestra/apps/mails/tests/functional_tests/tests.py +++ b/orchestra/apps/mailboxes/tests/functional_tests/tests.py @@ -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') diff --git a/orchestra/apps/mails/validators.py b/orchestra/apps/mailboxes/validators.py similarity index 92% rename from orchestra/apps/mails/validators.py rename to orchestra/apps/mailboxes/validators.py index 03e0c1da..ce57a501 100644 --- a/orchestra/apps/mails/validators.py +++ b/orchestra/apps/mailboxes/validators.py @@ -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: diff --git a/orchestra/apps/orchestration/methods.py b/orchestra/apps/orchestration/methods.py index 72a063b4..129ee984 100644 --- a/orchestra/apps/orchestration/methods.py +++ b/orchestra/apps/orchestration/methods.py @@ -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() diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py index 5522de52..510166f9 100644 --- a/orchestra/apps/services/models.py +++ b/orchestra/apps/services/models.py @@ -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 """ diff --git a/orchestra/apps/services/tests/functional_tests/test_mailbox.py b/orchestra/apps/services/tests/functional_tests/test_mailbox.py index 8ee1cb82..852fdabc 100644 --- a/orchestra/apps/services/tests/functional_tests/test_mailbox.py +++ b/orchestra/apps/services/tests/functional_tests/test_mailbox.py @@ -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 diff --git a/orchestra/apps/systemusers/backends.py b/orchestra/apps/systemusers/backends.py index 4589a942..9165e5d8 100644 --- a/orchestra/apps/systemusers/backends.py +++ b/orchestra/apps/systemusers/backends.py @@ -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' diff --git a/orchestra/bin/orchestra-admin b/orchestra/bin/orchestra-admin index 1364d16d..10952210 100755 --- a/orchestra/bin/orchestra-admin +++ b/orchestra/bin/orchestra-admin @@ -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 ... diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index ecbecc28..674b4999 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -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', diff --git a/orchestra/conf/devel_settings.py b/orchestra/conf/devel_settings.py index 82e2fc53..06ae7d07 100644 --- a/orchestra/conf/devel_settings.py +++ b/orchestra/conf/devel_settings.py @@ -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' diff --git a/orchestra/templatetags/utils.py b/orchestra/templatetags/utils.py index 372f4943..cda4559d 100644 --- a/orchestra/templatetags/utils.py +++ b/orchestra/templatetags/utils.py @@ -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()