diff --git a/TODO.md b/TODO.md index d604ba12..fc38504a 100644 --- a/TODO.md +++ b/TODO.md @@ -423,3 +423,10 @@ Colaesce('total', 'computed_total') Case # case on payment transaction state ? case when trans.amount > + +# bill changelist: dates: (closed_on, created_on, updated_on) + + +# Add record modeladmin action: select domains + add records (formset) to selected domains + +# Resource data inline show info: link to monitor data, and history chart: link to monitor data of each item diff --git a/orchestra/admin/forms.py b/orchestra/admin/forms.py index f83acd7c..4d82480f 100644 --- a/orchestra/admin/forms.py +++ b/orchestra/admin/forms.py @@ -47,7 +47,8 @@ class AdminFormSet(BaseModelFormSet): def adminmodelformset_factory(modeladmin, form, formset=AdminFormSet, **kwargs): - formset = modelformset_factory(modeladmin.model, form=form, formset=formset, **kwargs) + model = kwargs.pop('model', modeladmin.model) + formset = modelformset_factory(model, form=form, formset=formset, **kwargs) formset.modeladmin = modeladmin return formset diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py index 25c91f33..34c434a2 100644 --- a/orchestra/contrib/bills/models.py +++ b/orchestra/contrib/bills/models.py @@ -200,10 +200,6 @@ class Bill(models.Model): errors['amend_of'] = _("Related invoice is an amendment.") if errors: raise ValidationError(errors) - elif self.type in self.AMEND_MAP.values(): - raise ValidationError({ - 'amend_of': _("Type %s requires an amend of link.") % self.get_type_display() - }) def get_payment_state_display(self): value = self.payment_state diff --git a/orchestra/contrib/domains/actions.py b/orchestra/contrib/domains/actions.py index 7818e953..48297766 100644 --- a/orchestra/contrib/domains/actions.py +++ b/orchestra/contrib/domains/actions.py @@ -1,5 +1,17 @@ +import copy + +from django.contrib.admin import helpers +from django.shortcuts import render +from django.utils.safestring import mark_safe +from django.utils.translation import ungettext, ugettext_lazy as _ from django.template.response import TemplateResponse -from django.utils.translation import ugettext_lazy as _ + +from orchestra.admin.forms import adminmodelformset_factory +from orchestra.admin.utils import get_object_from_url, change_url +from orchestra.utils.python import AttrDict + +from .forms import RecordForm, RecordEditFormSet +from .models import Record def view_zone(modeladmin, request, queryset): @@ -12,3 +24,68 @@ def view_zone(modeladmin, request, queryset): return TemplateResponse(request, 'admin/domains/domain/view_zone.html', context) view_zone.url_name = 'view-zone' view_zone.verbose_name = _("View zone") + + +def edit_records(modeladmin, request, queryset): + formsets = [] + for domain in queryset.prefetch_related('records'): + modeladmin_copy = copy.copy(modeladmin) + modeladmin_copy.model = Record + link = '%s' % (change_url(domain), domain.name) + modeladmin_copy.verbose_name_plural = mark_safe(link) + RecordFormSet = adminmodelformset_factory( + modeladmin_copy, RecordForm, formset=RecordEditFormSet, extra=1, can_delete=True) + formset = RecordFormSet(queryset=domain.records.all(), prefix=domain.id) + formset.instance = domain + formset.cls = RecordFormSet + formsets.append(formset) + + if request.POST.get('post') == 'generic_confirmation': + posted_formsets = [] + all_valid = True + for formset in formsets: + instance = formset.instance + formset = formset.cls( + request.POST, request.FILES, queryset=formset.queryset, prefix=instance.id) + formset.instance = instance + if not formset.is_valid(): + all_valid = False + posted_formsets.append(formset) + formsets = posted_formsets + if all_valid: + for formset in formsets: + for form in formset.forms: + form.instance.domain_id = formset.instance.id + formset.save() + fake_form = AttrDict({ + 'changed_data': False + }) + change_message = modeladmin.construct_change_message(request, fake_form, [formset]) + modeladmin.log_change(request, formset.instance, change_message) + num = len(formsets) + message = ungettext( + _("Records for one selected domain have been updated."), + _("Records for %i selected domains have been updated.") % num, + num) + modeladmin.message_user(request, message) + return + + opts = modeladmin.model._meta + context = { + 'title': _("Edit records"), + 'action_name': 'Edit records', + 'action_value': 'edit_records', + 'display_objects': [], + 'queryset': queryset, + 'opts': opts, + 'app_label': opts.app_label, + 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, + 'formsets': formsets, + 'obj': get_object_from_url(modeladmin, request), + } + return render(request, 'admin/domains/domain/edit_records.html', context) + + +def add_records(modeladmin, request, queryset): + # TODO + pass diff --git a/orchestra/contrib/domains/admin.py b/orchestra/contrib/domains/admin.py index f3ff71fc..6ae92199 100644 --- a/orchestra/contrib/domains/admin.py +++ b/orchestra/contrib/domains/admin.py @@ -8,24 +8,17 @@ from orchestra.admin.utils import admin_link, change_url from orchestra.contrib.accounts.admin import AccountAdminMixin from orchestra.utils import apps -from .actions import view_zone +from .actions import view_zone, edit_records from .filters import TopDomainListFilter -from .forms import RecordInlineFormSet, BatchDomainCreationAdminForm +from .forms import RecordForm, RecordInlineFormSet, BatchDomainCreationAdminForm from .models import Domain, Record class RecordInline(admin.TabularInline): model = Record + form = RecordForm formset = RecordInlineFormSet verbose_name_plural = _("Extra records") - - def formfield_for_dbfield(self, db_field, **kwargs): - """ Make value input widget bigger """ - if db_field.name == 'value': - kwargs['widget'] = forms.TextInput(attrs={'size':'100'}) - if db_field.name == 'ttl': - kwargs['widget'] = forms.TextInput(attrs={'size':'10'}) - return super(RecordInline, self).formfield_for_dbfield(db_field, **kwargs) class DomainInline(admin.TabularInline): @@ -63,6 +56,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin): change_readonly_fields = ('name', 'serial') search_fields = ('name', 'account__username') add_form = BatchDomainCreationAdminForm + actions = (edit_records,) change_view_actions = [view_zone] def structured_name(self, domain): diff --git a/orchestra/contrib/domains/forms.py b/orchestra/contrib/domains/forms.py index 59c0fa67..b63b0e47 100644 --- a/orchestra/contrib/domains/forms.py +++ b/orchestra/contrib/domains/forms.py @@ -2,6 +2,8 @@ from django import forms from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ +from orchestra.admin.forms import AdminFormSet + from . import validators from .helpers import domain_for_validation from .models import Domain @@ -63,17 +65,35 @@ class BatchDomainCreationAdminForm(forms.ModelForm): return cleaned_data -class RecordInlineFormSet(forms.models.BaseInlineFormSet): +class RecordForm(forms.ModelForm): + class Meta: + fields = ('ttl', 'type', 'value') + + def __init__(self, *args, **kwargs): + super(RecordForm, self).__init__(*args, **kwargs) + self.fields['ttl'].widget = forms.TextInput(attrs={'size':'10'}) + self.fields['value'].widget = forms.TextInput(attrs={'size':'100'}) + + +class ValidateZoneMixin(object): def clean(self): """ Checks if everything is consistent """ - super(RecordInlineFormSet, self).clean() - if any(self.errors): + super(ValidateZoneMixin, self).clean() + if any(formset.errors): return - if self.instance.name: + if formset.instance.name: records = [] - for form in self.forms: + for form in formset.forms: data = form.cleaned_data if data and not data['DELETE']: records.append(data) - domain = domain_for_validation(self.instance, records) + domain = domain_for_validation(formset.instance, records) validators.validate_zone(domain.render_zone()) + + +class RecordEditFormSet(ValidateZoneMixin, AdminFormSet): + pass + + +class RecordInlineFormSet(ValidateZoneMixin, forms.models.BaseInlineFormSet): + pass diff --git a/orchestra/contrib/domains/models.py b/orchestra/contrib/domains/models.py index febc3223..e34fe531 100644 --- a/orchestra/contrib/domains/models.py +++ b/orchestra/contrib/domains/models.py @@ -20,7 +20,7 @@ class Domain(models.Model): top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set', editable=False) serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False, - help_text=_("A revision number that changes whenever you update your domain.")) + help_text=_("A revision number that changes whenever this domain is updated.")) refresh = models.IntegerField(_("refresh"), null=True, blank=True, validators=[validators.validate_zone_interval], help_text=_("The time a secondary DNS server waits before querying the primary DNS " @@ -182,10 +182,10 @@ class Domain(models.Model): "%s." % settings.DOMAINS_DEFAULT_NAME_SERVER, utils.format_hostmaster(settings.DOMAINS_DEFAULT_HOSTMASTER), str(self.serial), - settings.DOMAINS_DEFAULT_REFRESH if self.refresh is None else self.refresh, - settings.DOMAINS_DEFAULT_RETRY if self.retry is None else self.retry, - settings.DOMAINS_DEFAULT_EXPIRE if self.expire is None else self.expire, - settings.DOMAINS_DEFAULT_MIN_TTL if self.min_ttl is None else self.min_ttl, + self.refresh or settings.DOMAINS_DEFAULT_REFRESH, + self.retry or settings.DOMAINS_DEFAULT_RETRY, + self.expire or settings.DOMAINS_DEFAULT_EXPIRE, + self.min_ttl or settings.DOMAINS_DEFAULT_MIN_TTL, ] records.insert(0, AttrDict( type=Record.SOA, @@ -272,13 +272,24 @@ class Record(models.Model): (SOA, "SOA"), ) + VALIDATORS = { + MX: validators.validate_mx_record, + NS: validators.validate_zone_label, + A: validate_ipv4_address, + AAAA: validate_ipv6_address, + CNAME: validators.validate_zone_label, + TXT: validate_ascii, + SRV: validators.validate_srv_record, + SOA: validators.validate_soa_record, + } + domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records') ttl = models.CharField(_("TTL"), max_length=8, blank=True, help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL, validators=[validators.validate_zone_interval]) type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES) - value = models.CharField(_("value"), max_length=256, help_text=_("MX, NS and CNAME records " - "sould end with a dot.")) + value = models.CharField(_("value"), max_length=256, + help_text=_("MX, NS and CNAME records sould end with a dot.")) def __str__(self): return "%s %s IN %s %s" % (self.domain, self.get_ttl(), self.type, self.value) @@ -288,20 +299,13 @@ class Record(models.Model): # validate value if self.type != self.TXT: self.value = self.value.lower().strip() - choices = { - self.MX: validators.validate_mx_record, - self.NS: validators.validate_zone_label, - self.A: validate_ipv4_address, - self.AAAA: validate_ipv6_address, - self.CNAME: validators.validate_zone_label, - self.TXT: validate_ascii, - self.SRV: validators.validate_srv_record, - self.SOA: validators.validate_soa_record, - } - try: - choices[self.type](self.value) - except ValidationError as error: - raise ValidationError({'value': error}) + if self.type: + try: + self.VALIDATORS[self.type](self.value) + except ValidationError as error: + raise ValidationError({ + 'value': error, + }) def get_ttl(self): return self.ttl or settings.DOMAINS_DEFAULT_TTL diff --git a/orchestra/contrib/domains/templates/admin/domains/domain/edit_records.html b/orchestra/contrib/domains/templates/admin/domains/domain/edit_records.html new file mode 100644 index 00000000..48d3a0bd --- /dev/null +++ b/orchestra/contrib/domains/templates/admin/domains/domain/edit_records.html @@ -0,0 +1,20 @@ +{% extends "admin/orchestra/generic_confirmation.html" %} +{% load static %} + + +{% block extrahead %} +{{ block.super }} + + + + + + + +{% endblock %} + +{% block formset %} + {% for formset in formsets %} + {{ formset.as_admin }} + {% endfor %} +{% endblock %} diff --git a/orchestra/contrib/orchestration/middlewares.py b/orchestra/contrib/orchestration/middlewares.py index 97623939..86272342 100644 --- a/orchestra/contrib/orchestration/middlewares.py +++ b/orchestra/contrib/orchestration/middlewares.py @@ -1,5 +1,6 @@ from threading import local +from django.contrib.admin.models import LogEntry from django.core.urlresolvers import resolve from django.db import transaction from django.db.models.signals import pre_delete, post_save, m2m_changed @@ -15,14 +16,14 @@ from .models import BackendLog @receiver(post_save, dispatch_uid='orchestration.post_save_collector') def post_save_collector(sender, *args, **kwargs): - if sender not in [BackendLog, Operation]: + if sender not in (BackendLog, Operation, LogEntry): instance = kwargs.get('instance') OperationsMiddleware.collect(Operation.SAVE, **kwargs) @receiver(pre_delete, dispatch_uid='orchestration.pre_delete_collector') def pre_delete_collector(sender, *args, **kwargs): - if sender not in [BackendLog, Operation]: + if sender not in (BackendLog, Operation, LogEntry): OperationsMiddleware.collect(Operation.DELETE, **kwargs) diff --git a/orchestra/contrib/resources/actions.py b/orchestra/contrib/resources/actions.py index cbcb6eee..fc734b7d 100644 --- a/orchestra/contrib/resources/actions.py +++ b/orchestra/contrib/resources/actions.py @@ -62,5 +62,5 @@ def history(modeladmin, request, queryset): context = { 'resources': resources, } - return render(request, 'admin/resources/resourcedata/report.html', context) + return render(request, 'admin/resources/resourcedata/history.html', context) history.url_name = 'history' diff --git a/orchestra/contrib/resources/admin.py b/orchestra/contrib/resources/admin.py index 1a24dffe..b6b92b69 100644 --- a/orchestra/contrib/resources/admin.py +++ b/orchestra/contrib/resources/admin.py @@ -254,16 +254,25 @@ def resource_inline_factory(resources): display_updated = admin_date('updated_at', default=_("Never")) def display_used(self, data): + from django.templatetags.static import static update_link = '' history_link = '' if data.pk: - update_url = reverse('admin:resources_resourcedata_monitor', args=(data.pk,)) - update_link = '%s' % (update_url, _("Update")) - history_url = reverse('admin:resources_resourcedata_history', args=(data.pk,)) - popup = 'onclick="return showAddAnotherPopup(this);"' - history_link = '%s' % (history_url, popup, _("History")) + context = { + 'title': _("Update"), + 'url': reverse('admin:resources_resourcedata_monitor', args=(data.pk,)), + 'image': '' % static('orchestra/images/reload.png'), + } + update = '%(image)s' % context + context.update({ + 'title': _("Show history"), + 'image': '' % static('orchestra/images/history.png'), + 'url': reverse('admin:resources_resourcedata_history', args=(data.pk,)), + 'popup': 'onclick="return showAddAnotherPopup(this);"', + }) + history = '%(image)s' % context if data.used is not None: - return ' '.join(map(str, (data.used, data.resource.unit, update_link, history_link))) + return ' '.join(map(str, (data.used, data.resource.unit, update, history))) return _("Unknonw %s") % update_link display_used.short_description = _("Used") display_used.allow_tags = True diff --git a/orchestra/contrib/resources/templates/admin/resources/resourcedata/report.html b/orchestra/contrib/resources/templates/admin/resources/resourcedata/history.html similarity index 98% rename from orchestra/contrib/resources/templates/admin/resources/resourcedata/report.html rename to orchestra/contrib/resources/templates/admin/resources/resourcedata/history.html index bd67437b..b076dd78 100644 --- a/orchestra/contrib/resources/templates/admin/resources/resourcedata/report.html +++ b/orchestra/contrib/resources/templates/admin/resources/resourcedata/history.html @@ -2,7 +2,7 @@ - Transaction Report + Resource history