diff --git a/TODO.md b/TODO.md
index df7df2d0..238c05a0 100644
--- a/TODO.md
+++ b/TODO.md
@@ -149,20 +149,16 @@
* Resource used_list_display=True, allocated_list_displat=True, allow resources to show up on list_display
-* Move plugins back from apps to orchestra main app
-
* BackendLog.updated_at (tasks that run over several minutes when finished they do not appear first on the changelist) (like celery tasks.when)
-* Validate a model path exists between resource.content_type and backend.model
-
* Periodic task for cleaning old monitoring data
-* Generate reports of Account contracted services
-
* Create an admin service_view with icons (like SaaS app)
* Fix ftp traffic
* Resource graph for each related object
-* contacts filter by email_usage fix exact for contains
+* Rename apache logs ending on .log in order to logrotate easily
+
+* SaaS wordpress multiple blogs per user? separate users from sites?
diff --git a/orchestra/apps/contacts/actions.py b/orchestra/admin/actions.py
similarity index 72%
rename from orchestra/apps/contacts/actions.py
rename to orchestra/admin/actions.py
index 315afab2..75ac3d9e 100644
--- a/orchestra/apps/contacts/actions.py
+++ b/orchestra/admin/actions.py
@@ -3,8 +3,9 @@ from django.core.mail import send_mass_mail
from django.shortcuts import render
from django.utils.translation import ungettext, ugettext_lazy as _
-from orchestra.admin.utils import change_url
+from .. import settings
+from .utils import change_url
from .forms import SendEmailForm
@@ -13,18 +14,19 @@ class SendEmail(object):
short_description = _("Send email")
form = SendEmailForm
template = 'admin/orchestra/generic_confirmation.html'
+ default_from = settings.ORCHESTRA_DEFAULT_SUPPORT_FROM_EMAIL
__name__ = 'semd_email'
def __call__(self, modeladmin, request, queryset):
""" make this monster behave like a function """
self.modeladmin = modeladmin
self.queryset = queryset
- opts = modeladmin.model._meta
- app_label = opts.app_label
+ self.opts = modeladmin.model._meta
+ app_label = self.opts.app_label
self.context = {
'action_name': _("Send email"),
'action_value': self.__name__,
- 'opts': opts,
+ 'opts': self.opts,
'app_label': app_label,
'queryset': queryset,
'action_checkbox_name': admin.helpers.ACTION_CHECKBOX_NAME,
@@ -34,14 +36,17 @@ class SendEmail(object):
def write_email(self, request):
if not request.user.is_superuser:
raise PermissionDenied
- form = self.form()
+ initial={
+ 'email_from': self.default_from,
+ 'to': ' '.join(self.queryset.values_list('email', flat=True))
+ }
+ form = self.form(initial=initial)
if request.POST.get('post'):
- form = self.form(request.POST)
+ form = self.form(request.POST, initial=initial)
if form.is_valid():
options = {
'email_from': form.cleaned_data['email_from'],
- 'cc': form.cleaned_data['cc'],
- 'bcc': form.cleaned_data['bcc'],
+ 'extra_to': form.cleaned_data['extra_to'],
'subject': form.cleaned_data['subject'],
'message': form.cleaned_data['message'],
@@ -50,7 +55,7 @@ class SendEmail(object):
opts = self.modeladmin.model._meta
app_label = opts.app_label
self.context.update({
- 'title': _("Send e-mail to contacts"),
+ 'title': _("Send e-mail to %s") % self.opts.verbose_name_plural,
'content_title': "",
'form': form,
'submit_value': _("Continue"),
@@ -61,31 +66,36 @@ class SendEmail(object):
def confirm_email(self, request, **options):
num = len(self.queryset)
email_from = options['email_from']
- bcc = options['bcc']
- to = options['cc']
+ extra_to = options['extra_to']
subject = options['subject']
message = options['message']
# The user has already confirmed
if request.POST.get('post') == 'email_confirmation':
+ emails = []
for contact in self.queryset.all():
- to.append(contact.email)
- send_mass_mail(subject, message, email_from, to, bcc)
+ emails.append((subject, message, email_from, [contact.email]))
+ if extra_to:
+ emails.append((subject, message, email_from, extra_to))
+ send_mass_mail(emails)
msg = ungettext(
_("Message has been sent to %s.") % str(contact),
- _("Message has been sent to %i contacts.") % num,
+ _("Message has been sent to %i %s.") % (num, self.opts.verbose_name_plural),
num
)
self.modeladmin.message_user(request, msg)
return None
form = self.form(initial={
+ 'email_from': email_from,
+ 'extra_to': ', '.join(extra_to),
'subject': subject,
'message': message
})
self.context.update({
'title': _("Are you sure?"),
'content_message': _(
- "Are you sure you want to send the following message to the following contacts?"),
+ "Are you sure you want to send the following message to the following %s?"
+ ) % self.opts.verbose_name_plural,
'display_objects': ["%s (%s)" % (contact, contact.email) for contact in self.queryset],
'form': form,
'subject': subject,
diff --git a/orchestra/admin/forms.py b/orchestra/admin/forms.py
index ef00dfa3..5d84fe1d 100644
--- a/orchestra/admin/forms.py
+++ b/orchestra/admin/forms.py
@@ -2,10 +2,13 @@ from functools import partial
from django import forms
from django.contrib.admin import helpers
+from django.core import validators
from django.forms.models import modelformset_factory, BaseModelFormSet
from django.template import Template, Context
from django.utils.translation import ugettext_lazy as _
+from orchestra.forms.widgets import ShowTextWidget, ReadOnlyWidget
+
from ..core.validators import validate_password
@@ -129,3 +132,39 @@ class AdminPasswordChangeForm(forms.Form):
return ['password']
changed_data = property(_get_changed_data)
+
+class SendEmailForm(forms.Form):
+ email_from = forms.EmailField(label=_("From"),
+ widget=forms.TextInput(attrs={'size':'118'}))
+ to = forms.CharField(label="To", required=False,
+ widget=ShowTextWidget())
+ extra_to = forms.CharField(label="To (extra)", required=False,
+ widget=forms.TextInput(attrs={'size':'118'}))
+ subject = forms.CharField(label=_("Subject"),
+ widget=forms.TextInput(attrs={'size':'118'}))
+ message = forms.CharField(label=_("Message"),
+ widget=forms.Textarea(attrs={'cols': 118, 'rows': 15}))
+
+ def __init__(self, *args, **kwargs):
+ super(SendEmailForm, self).__init__(*args, **kwargs)
+ initial = kwargs.get('initial')
+ if 'to' in initial:
+ self.fields['to'].widget = ReadOnlyWidget(initial['to'])
+ else:
+ self.fields.pop('to')
+
+ def clean_comma_separated_emails(self, value):
+ clean_value = []
+ for email in value.split(','):
+ email = email.strip()
+ if email:
+ try:
+ validators.validate_email(email)
+ except validators.ValidationError:
+ raise validators.ValidationError("Comma separated email addresses.")
+ clean_value.append(email)
+ return clean_value
+
+ def clean_extra_to(self):
+ extra_to = self.cleaned_data['extra_to']
+ return self.clean_comma_separated_emails(extra_to)
diff --git a/orchestra/apps/accounts/actions.py b/orchestra/apps/accounts/actions.py
index 57d9856a..7a6bd349 100644
--- a/orchestra/apps/accounts/actions.py
+++ b/orchestra/apps/accounts/actions.py
@@ -39,13 +39,22 @@ list_contacts.verbose_name = _("List contacts")
def service_report(modeladmin, request, queryset):
accounts = []
- for account in queryset:
+ fields = []
+ # First we get related manager names to fire a prefetch related
+ for name, field in queryset.model._meta._name_map.iteritems():
+ model = field[0].model
+ if model in services.get() and model != queryset.model:
+ fields.append((model, name))
+ sorted(fields, key=lambda i: i[0]._meta.verbose_name_plural.lower())
+ fields = [field for model, field in fields]
+
+ for account in queryset.prefetch_related(*fields):
items = []
- for service in services.get():
- if service != type(account):
- items.append((service._meta, service.objects.filter(account=account)))
- sorted(items, key=lambda i: i[0].verbose_name_plural.lower())
+ for field in fields:
+ related_manager = getattr(account, field)
+ items.append((related_manager.model._meta, related_manager.all()))
accounts.append((account, items))
+
context = {
'accounts': accounts,
'date': timezone.now().today()
diff --git a/orchestra/apps/accounts/admin.py b/orchestra/apps/accounts/admin.py
index 4b126704..9ceda689 100644
--- a/orchestra/apps/accounts/admin.py
+++ b/orchestra/apps/accounts/admin.py
@@ -13,6 +13,7 @@ from django.utils.six.moves.urllib.parse import parse_qsl
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
+from orchestra.admin.actions import SendEmail
from orchestra.admin.utils import wrap_admin_view, admin_link, set_url_query, change_url
from orchestra.core import services, accounts
from orchestra.forms import UserChangeForm
@@ -61,7 +62,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
filter_horizontal = ()
change_readonly_fields = ('username', 'main_systemuser_link')
change_form_template = 'admin/accounts/account/change_form.html'
- actions = [disable, list_contacts, service_report]
+ actions = [disable, list_contacts, service_report, SendEmail()]
change_view_actions = [disable, service_report]
list_select_related = ('billcontact',)
ordering = ()
diff --git a/orchestra/apps/accounts/templates/admin/accounts/account/service_report.html b/orchestra/apps/accounts/templates/admin/accounts/account/service_report.html
index d59fac6a..3e6c84d3 100644
--- a/orchestra/apps/accounts/templates/admin/accounts/account/service_report.html
+++ b/orchestra/apps/accounts/templates/admin/accounts/account/service_report.html
@@ -1,7 +1,7 @@
-{% load utils %}
+{% load utils i18n %}
- {% block title %}Service Report{% endblock %}
+ {% block title %}Account service report{% endblock %}
{% block head %}{% endblock %}
-Service report generated on {{ date | date }}
+{% trans "Service report generated on" %} {{ date | date }}
{% for account, items in accounts %}
- {{ account.get_full_name }} ({{ account.username }})
+
- {{ account.get_type_display }} account registered on {{ account.date_joined | date }}
+ {{ account.get_type_display }} {% trans "account registered on" %} {{ account.date_joined | date }}
{% for opts, related in items %}
- {{ opts.verbose_name_plural|capfirst }}
{% for obj in related %}
- - {{ obj }}{% if not obj|isactive %} (disabled){% endif %}
+ - {{ obj }}{% if not obj|isactive %} ({% trans "disabled" %}){% endif %}
{% endfor %}
{% endfor %}
diff --git a/orchestra/apps/contacts/admin.py b/orchestra/apps/contacts/admin.py
index 2ce161ef..7bd1d9d2 100644
--- a/orchestra/apps/contacts/admin.py
+++ b/orchestra/apps/contacts/admin.py
@@ -3,11 +3,12 @@ from django.contrib import admin
from django.utils.translation import ugettext, ugettext_lazy as _
from orchestra.admin import AtLeastOneRequiredInlineFormSet, ExtendedModelAdmin
+from orchestra.admin.actions import SendEmail
from orchestra.admin.utils import insertattr, admin_link, change_url
from orchestra.apps.accounts.admin import AccountAdmin, AccountAdminMixin
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
-from .actions import SendEmail
+from .filters import EmailUsageListFilter
from .models import Contact
@@ -16,7 +17,7 @@ class ContactAdmin(AccountAdminMixin, ExtendedModelAdmin):
'dispaly_name', 'email', 'phone', 'phone2', 'country', 'account_link'
)
# TODO email usage custom filter contains
- list_filter = ('email_usage',)
+ list_filter = (EmailUsageListFilter,)
search_fields = (
'account__username', 'account__full_name', 'short_name', 'full_name', 'phone', 'phone2',
'email'
diff --git a/orchestra/apps/contacts/filters.py b/orchestra/apps/contacts/filters.py
new file mode 100644
index 00000000..a41dca74
--- /dev/null
+++ b/orchestra/apps/contacts/filters.py
@@ -0,0 +1,16 @@
+from django.contrib.admin import SimpleListFilter
+from django.utils.translation import ugettext_lazy as _
+
+from .models import Contact
+
+
+class EmailUsageListFilter(SimpleListFilter):
+ title = _("email usages")
+ parameter_name = 'email_usages'
+
+ def lookups(self, request, model_admin):
+ return Contact.email_usage.field.choices
+
+ def queryset(self, request, queryset):
+ value = self.value().split(',')
+ return queryset.filter(email_usages=value)
diff --git a/orchestra/apps/contacts/forms.py b/orchestra/apps/contacts/forms.py
deleted file mode 100644
index 591c1b2a..00000000
--- a/orchestra/apps/contacts/forms.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from django import forms
-from django.core import validators
-from django.utils.translation import ungettext, ugettext_lazy as _
-
-from orchestra.forms.widgets import ShowTextWidget
-
-from . import settings
-
-
-class SendEmailForm(forms.Form):
- email_from = forms.EmailField(label=_("From"),
- initial=settings.CONTACTS_DEFAULT_FROM_EMAIL,
- widget=forms.TextInput(attrs={'size':'118'}))
- cc = forms.CharField(label="CC", required=False,
- widget=forms.TextInput(attrs={'size':'118'}))
- bcc = forms.CharField(label="BCC", required=False,
- widget=forms.TextInput(attrs={'size':'118'}))
- subject = forms.CharField(label=_("Subject"),
- widget=forms.TextInput(attrs={'size':'118'}))
- message = forms.CharField(label=_("Message"),
- widget=forms.Textarea(attrs={'cols': 118, 'rows': 15}))
-
- def clean_space_separated_emails(self, value):
- value = value.split()
- for email in value:
- try:
- validators.validate_email(email)
- except validators.ValidationError:
- raise validators.ValidationError("Space separated emails.")
- return value
-
- def clean_cc(self):
- return self.clean_space_separated_emails(self.cleaned_data['cc'])
-
- def clean_bcc(self):
- return self.clean_space_separated_emails(self.cleaned_data['bcc'])
diff --git a/orchestra/apps/contacts/settings.py b/orchestra/apps/contacts/settings.py
index 737c8b48..46468ef8 100644
--- a/orchestra/apps/contacts/settings.py
+++ b/orchestra/apps/contacts/settings.py
@@ -2,18 +2,22 @@ from django.conf import settings
from django_countries import data
-CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES',
- ('SUPPORT', 'ADMIN', 'BILLING', 'TECH', 'ADDS', 'EMERGENCY')
-)
+CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES', (
+ 'SUPPORT',
+ 'ADMIN',
+ 'BILLING',
+ 'TECH',
+ 'ADDS',
+ 'EMERGENCY'
+))
CONTACTS_DEFAULT_CITY = getattr(settings, 'CONTACTS_DEFAULT_CITY', 'Barcelona')
-CONTACTS_COUNTRIES = getattr(settings, 'CONTACTS_COUNTRIES', ((k,v) for k,v in data.COUNTRIES.iteritems()))
+CONTACTS_COUNTRIES = getattr(settings, 'CONTACTS_COUNTRIES', (
+ (k,v) for k,v in data.COUNTRIES.iteritems()
+))
CONTACTS_DEFAULT_COUNTRY = getattr(settings, 'CONTACTS_DEFAULT_COUNTRY', 'ES')
-
-
-CONTACTS_DEFAULT_FROM_EMAIL = getattr(settings, 'CONTACTS_DEFAULT_FROM_EMAIL', 'support@orchestra.lan')
diff --git a/orchestra/apps/domains/filters.py b/orchestra/apps/domains/filters.py
index 02c8b11f..be3fba20 100644
--- a/orchestra/apps/domains/filters.py
+++ b/orchestra/apps/domains/filters.py
@@ -1,11 +1,10 @@
from django.contrib.admin import SimpleListFilter
-from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
class TopDomainListFilter(SimpleListFilter):
""" Filter Nodes by group according to request.user """
- title = _("Top domains")
+ title = _("top domains")
parameter_name = 'top_domain'
def lookups(self, request, model_admin):
diff --git a/orchestra/apps/miscellaneous/admin.py b/orchestra/apps/miscellaneous/admin.py
index 986a56cd..de58b6db 100644
--- a/orchestra/apps/miscellaneous/admin.py
+++ b/orchestra/apps/miscellaneous/admin.py
@@ -8,8 +8,8 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import admin_link
from orchestra.apps.accounts.admin import AccountAdminMixin
-from orchestra.apps.plugins import PluginModelAdapter
-from orchestra.apps.plugins.admin import SelectPluginAdminMixin
+from orchestra.plugins import PluginModelAdapter
+from orchestra.plugins.admin import SelectPluginAdminMixin
from . import settings
from .models import MiscService, Miscellaneous
@@ -26,7 +26,9 @@ class MiscServiceAdmin(ExtendedModelAdmin):
)
list_editable = ('is_active',)
list_filter = ('has_identifier', 'has_amount', 'is_active')
- fields = ('verbose_name', 'name', 'description', 'has_identifier', 'has_amount', 'is_active')
+ fields = (
+ 'verbose_name', 'name', 'description', 'has_identifier', 'has_amount', 'is_active'
+ )
prepopulated_fields = {'name': ('verbose_name',)}
change_readonly_fields = ('name',)
@@ -51,9 +53,12 @@ class MiscServiceAdmin(ExtendedModelAdmin):
class MiscellaneousAdmin(AccountAdminMixin, SelectPluginAdminMixin, admin.ModelAdmin):
- list_display = ('__unicode__', 'service_link', 'amount', 'dispaly_active', 'account_link')
+ list_display = (
+ '__unicode__', 'service_link', 'amount', 'dispaly_active', 'account_link'
+ )
list_filter = ('service__name', 'is_active')
list_select_related = ('service', 'account')
+ search_fields = ('identifier', 'description')
plugin_field = 'service'
plugin = MiscServicePlugin
diff --git a/orchestra/apps/orchestration/backends.py b/orchestra/apps/orchestration/backends.py
index 8f055832..e8a9c8b8 100644
--- a/orchestra/apps/orchestration/backends.py
+++ b/orchestra/apps/orchestration/backends.py
@@ -5,7 +5,7 @@ from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
-from orchestra.apps import plugins
+from orchestra import plugins
from . import methods
diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py
index 70056795..c343ff85 100644
--- a/orchestra/apps/payments/admin.py
+++ b/orchestra/apps/payments/admin.py
@@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ChangeViewActionsMixin, ExtendedModelAdmin
from orchestra.admin.utils import admin_colored, admin_link
from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
-from orchestra.apps.plugins.admin import SelectPluginAdminMixin
+from orchestra.plugins.admin import SelectPluginAdminMixin
from . import actions
from .methods import PaymentMethod
diff --git a/orchestra/apps/payments/methods/creditcard.py b/orchestra/apps/payments/methods/creditcard.py
index 111b4e5d..942b6d30 100644
--- a/orchestra/apps/payments/methods/creditcard.py
+++ b/orchestra/apps/payments/methods/creditcard.py
@@ -2,7 +2,7 @@ from django import forms
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
-from orchestra.apps.plugins.forms import PluginDataForm
+from orchestra.plugins.forms import PluginDataForm
from .options import PaymentMethod
diff --git a/orchestra/apps/payments/methods/options.py b/orchestra/apps/payments/methods/options.py
index 46636dfb..3e69910b 100644
--- a/orchestra/apps/payments/methods/options.py
+++ b/orchestra/apps/payments/methods/options.py
@@ -2,7 +2,7 @@ from dateutil import relativedelta
from django import forms
from django.core.exceptions import ValidationError
-from orchestra.apps import plugins
+from orchestra import plugins
from orchestra.utils.functional import cached
from orchestra.utils.python import import_class
diff --git a/orchestra/apps/payments/methods/sepadirectdebit.py b/orchestra/apps/payments/methods/sepadirectdebit.py
index 48722cc7..7217cbfa 100644
--- a/orchestra/apps/payments/methods/sepadirectdebit.py
+++ b/orchestra/apps/payments/methods/sepadirectdebit.py
@@ -12,7 +12,7 @@ from django_iban.forms import IBANFormField
from django_iban.validators import IBANValidator, IBAN_COUNTRY_CODE_LENGTH
from rest_framework import serializers
-from orchestra.apps.plugins.forms import PluginDataForm
+from orchestra.plugins.forms import PluginDataForm
from .. import settings
from .options import PaymentMethod
diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py
index 4b72f52c..ff0462ab 100644
--- a/orchestra/apps/resources/models.py
+++ b/orchestra/apps/resources/models.py
@@ -86,6 +86,21 @@ class Resource(models.Model):
def clean(self):
self.verbose_name = self.verbose_name.strip()
+ # Validate that model path exists between ct and each monitor.model
+ monitor_errors = []
+ for monitor in self.monitors:
+ try:
+ self.get_model_path(monitor)
+ except (RuntimeError, LookupError):
+ monitor_errors.append(monitor)
+ if monitor_errors:
+ raise validators.ValidationError({
+ 'monitors': [
+ _("Path does not exists between '%s' and '%s'") % (
+ get_model(ServiceMonitor.get_backend(monitor).model)._meta.model_name,
+ self.content_type.model_class()._meta.model_name,
+ ) for error in monitor_errors
+ ]})
def save(self, *args, **kwargs):
created = not self.pk
@@ -99,6 +114,13 @@ class Resource(models.Model):
super(Resource, self).delete(*args, **kwargs)
name = 'monitor.%s' % str(self)
+ def get_model_path(self, monitor):
+ """ returns a model path between self.content_type and monitor.model """
+ resource_model = self.content_type.model_class()
+ model_path = ServiceMonitor.get_backend(monitor).model
+ monitor_model = get_model(model_path)
+ return get_model_field_path(monitor_model, resource_model)
+
def sync_periodic_task(self):
name = 'monitor.%s' % str(self)
if self.pk and self.crontab:
@@ -190,17 +212,14 @@ class ResourceData(models.Model):
today = timezone.now()
datasets = []
for monitor in resource.monitors:
- resource_model = self.content_type.model_class()
- model_path = ServiceMonitor.get_backend(monitor).model
- monitor_model = get_model(model_path)
- if resource_model == monitor_model:
+ path = self.resource.get_model_path(monitor)
+ if path == []:
dataset = MonitorData.objects.filter(
monitor=monitor,
content_type=self.content_type_id,
object_id=self.object_id
)
else:
- path = get_model_field_path(monitor_model, resource_model)
fields = '__'.join(path)
objects = monitor_model.objects.filter(**{fields: self.object_id})
pks = objects.values_list('id', flat=True)
diff --git a/orchestra/apps/resources/validators.py b/orchestra/apps/resources/validators.py
index 2a75aba9..803f29ec 100644
--- a/orchestra/apps/resources/validators.py
+++ b/orchestra/apps/resources/validators.py
@@ -1,8 +1,11 @@
from django.core.validators import ValidationError
+from django.utils.translation import ugettext_lazy as _
def validate_scale(value):
try:
int(eval(value))
- except ValueError:
- raise ValidationError(_("%s value is not a valid scale expression"))
+ except Exception, e:
+ raise ValidationError(
+ _("'%s' is not a valid scale expression. (%s)") % (value, str(e))
+ )
diff --git a/orchestra/apps/saas/admin.py b/orchestra/apps/saas/admin.py
index 4152671f..ed125b4d 100644
--- a/orchestra/apps/saas/admin.py
+++ b/orchestra/apps/saas/admin.py
@@ -2,7 +2,7 @@ from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.accounts.admin import AccountAdminMixin
-from orchestra.apps.plugins.admin import SelectPluginAdminMixin
+from orchestra.plugins.admin import SelectPluginAdminMixin
from .models import SaaS
from .services import SoftwareService
diff --git a/orchestra/apps/saas/services/moodle.py b/orchestra/apps/saas/services/moodle.py
index 4236428e..97277108 100644
--- a/orchestra/apps/saas/services/moodle.py
+++ b/orchestra/apps/saas/services/moodle.py
@@ -1,7 +1,7 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
-from orchestra.apps.plugins.forms import PluginDataForm
+from orchestra.plugins.forms import PluginDataForm
from .options import SoftwareService
diff --git a/orchestra/apps/saas/services/options.py b/orchestra/apps/saas/services/options.py
index 631dbd35..9439c13b 100644
--- a/orchestra/apps/saas/services/options.py
+++ b/orchestra/apps/saas/services/options.py
@@ -3,8 +3,8 @@ from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
-from orchestra.apps import plugins
-from orchestra.apps.plugins.forms import PluginDataForm
+from orchestra import plugins
+from orchestra.plugins.forms import PluginDataForm
from orchestra.core import validators
from orchestra.forms import widgets
from orchestra.utils.functional import cached
@@ -72,6 +72,7 @@ class SoftwareServiceForm(PluginDataForm):
obj.set_password(self.cleaned_data["password1"])
return obj
+
class SoftwareService(plugins.Plugin):
form = SoftwareServiceForm
serializer = None
diff --git a/orchestra/apps/services/handlers.py b/orchestra/apps/services/handlers.py
index 0b51cdfa..80525769 100644
--- a/orchestra/apps/services/handlers.py
+++ b/orchestra/apps/services/handlers.py
@@ -7,7 +7,7 @@ from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
-from orchestra.apps import plugins
+from orchestra import plugins
from orchestra.utils.humanize import text2int
from orchestra.utils.python import AttrDict
diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py
index 67512c89..9d607217 100644
--- a/orchestra/conf/base_settings.py
+++ b/orchestra/conf/base_settings.py
@@ -86,7 +86,6 @@ INSTALLED_APPS = (
'orchestra.apps.miscellaneous',
'orchestra.apps.bills',
'orchestra.apps.payments',
- 'orchestra.apps.plugins',
# Third-party apps
'django_extensions',
diff --git a/orchestra/models/utils.py b/orchestra/models/utils.py
index e3c1cdc5..f4375e84 100644
--- a/orchestra/models/utils.py
+++ b/orchestra/models/utils.py
@@ -47,3 +47,4 @@ def get_model_field_path(origin, target):
new_path = list(path)
new_path.append(field.name)
queue.append((new_model, new_path))
+ raise LookupError("Path does not exists between '%s' and '%s' models" % (origin, target))
diff --git a/orchestra/apps/plugins/__init__.py b/orchestra/plugins/__init__.py
similarity index 100%
rename from orchestra/apps/plugins/__init__.py
rename to orchestra/plugins/__init__.py
diff --git a/orchestra/apps/plugins/admin.py b/orchestra/plugins/admin.py
similarity index 100%
rename from orchestra/apps/plugins/admin.py
rename to orchestra/plugins/admin.py
diff --git a/orchestra/apps/plugins/forms.py b/orchestra/plugins/forms.py
similarity index 100%
rename from orchestra/apps/plugins/forms.py
rename to orchestra/plugins/forms.py
diff --git a/orchestra/apps/plugins/options.py b/orchestra/plugins/options.py
similarity index 100%
rename from orchestra/apps/plugins/options.py
rename to orchestra/plugins/options.py
diff --git a/orchestra/settings.py b/orchestra/settings.py
index 268d3889..b1d588d7 100644
--- a/orchestra/settings.py
+++ b/orchestra/settings.py
@@ -31,3 +31,8 @@ API_ROOT_VIEW = getattr(settings, 'API_ROOT_VIEW', 'orchestra.api.root.APIRoot')
ORCHESTRA_MIGRATION_MODE = getattr(settings, 'ORCHESTRA_MIGRATION_MODE', False)
+
+
+ORCHESTRA_DEFAULT_SUPPORT_FROM_EMAIL = getattr(settings, 'ORCHESTRA_DEFAULT_SUPPORT_FROM_EMAIL',
+ 'support@orchestra.lan'
+)
diff --git a/orchestra/apps/plugins/templates/admin/plugins/select_plugin.html b/orchestra/templates/admin/plugins/select_plugin.html
similarity index 100%
rename from orchestra/apps/plugins/templates/admin/plugins/select_plugin.html
rename to orchestra/templates/admin/plugins/select_plugin.html