Refactores plugins, fixes on mailing contacts and improved resource validation

This commit is contained in:
Marc Aymerich 2014-11-24 14:39:41 +00:00
parent e962e5d7b2
commit 4732925f63
31 changed files with 188 additions and 105 deletions

10
TODO.md
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{% load utils %}
{% load utils i18n %}
<html>
<head>
<title>{% block title %}Service Report{% endblock %}</title>
<title>{% block title %}Account service report{% endblock %}</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
{% block head %}{% endblock %}
<style type="text/css">
@ -11,11 +11,11 @@
float: none !important;
font-family: Arial, 'Liberation Sans', 'DejaVu Sans', sans-serif;
font-size: 12px;
color: #2E2E2E;
color: #444;
}
#date {
float: right;
color: #666;
color: rgb(102, 102, 102);
}
.account-content {
margin: 0px 0px 40px 20px;
@ -23,27 +23,38 @@
.item-title {
list-style-type: none;
font-weight: bold;
color: #666;
}
.items-ul {
padding: 0px;
margin: 5px 0px 10px 20px;
}
.related {
list-style: disc;
}
hr {
margin-top: -9px;
}
a {
text-decoration: none;
color: rgb(91, 128, 178);
}
</style>
</head>
<body>
<div id="date">Service report generated on {{ date | date }}</div>
<div id="date">{% trans "Service report generated on" %} {{ date | date }}</div>
{% for account, items in accounts %}
<h3>{{ account.get_full_name }} ({{ account.username }})</h3>
<h3>{{ account.get_full_name }} - <a href="{{ account|admin_url }}">{{ account.username }}</a></h3>
<hr>
<div class="account-content">
{{ account.get_type_display }} account registered on {{ account.date_joined | date }}<br>
{{ account.get_type_display }} {% trans "account registered on" %} {{ account.date_joined | date }}<br>
<ul class="items-ul">
{% for opts, related in items %}
<li class="item-title">{{ opts.verbose_name_plural|capfirst }}</li>
<ul>
{% for obj in related %}
<li>{{ obj }}{% if not obj|isactive %} (disabled){% endif %}</li>
<li class="related"><a href="{{ obj|admin_url }}">{{ obj }}</a>{% if not obj|isactive %} ({% trans "disabled" %}){% endif %}</li>
{% endfor %}
</ul>
{% endfor %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -86,7 +86,6 @@ INSTALLED_APPS = (
'orchestra.apps.miscellaneous',
'orchestra.apps.bills',
'orchestra.apps.payments',
'orchestra.apps.plugins',
# Third-party apps
'django_extensions',

View File

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

View File

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