From caa087deb67e6322a2d36e9939de853035e1d6ec Mon Sep 17 00:00:00 2001
From: Marc Aymerich <glicerinu@gmail.com>
Date: Wed, 15 Jul 2015 10:35:21 +0000
Subject: [PATCH] Added SOA settings for domains

---
 TODO.md                                       | 38 +---------------
 orchestra/admin/options.py                    | 45 +++++++++----------
 orchestra/contrib/accounts/forms.py           |  7 +--
 orchestra/contrib/accounts/models.py          |  5 ++-
 orchestra/contrib/bills/filters.py            | 19 ++++----
 orchestra/contrib/domains/admin.py            | 14 +++++-
 .../migrations/0002_auto_20150715_1017.py     | 39 ++++++++++++++++
 orchestra/contrib/domains/models.py           | 29 +++++++++---
 orchestra/contrib/domains/settings.py         |  4 +-
 9 files changed, 117 insertions(+), 83 deletions(-)
 create mode 100644 orchestra/contrib/domains/migrations/0002_auto_20150715_1017.py

diff --git a/TODO.md b/TODO.md
index 38a64a31..653251de 100644
--- a/TODO.md
+++ b/TODO.md
@@ -13,11 +13,6 @@
 
 * backend logs with hal logo
 
-# LAST version of this shit http://wkhtmltopdf.org/downloads.h otml
-#apt-get install xfonts-75dpi
-#wget http://download.gna.org/wkhtmltopdf/0.12/0.12.2.1/wkhtmltox-0.12.2.1_linux-jessie-amd64.deb
-#dpkg -i wkhtmltox-0.12.2.1_linux-jessie-amd64.deb 
-
 * help_text on readonly_fields specialy Bill.state. (eg. A bill is in OPEN state when bla bla )
 
 * create log file at /var/log/orchestra.log and rotate
@@ -175,9 +170,6 @@ require_once(‘/etc/moodles/’.$moodle_host.‘config.php’);``` moodle/drupl
 * Autocomplete admin fields like <site_name>.phplist... with js
 
 * allow empty metric pack for default rates? changes on rating algo
-# don't produce lines with cost == 0 or quantity 0 ? maybe minimal quantity for billing? like 0.1 ? or minimal price? per line or per bill?
-
-# lines too long on invoice, double lines or cut
 
 * payment methods icons
 * use server.name | server.address on python backends, like gitlab instead of settings?
@@ -373,23 +365,10 @@ method(
     arg, arg, arg)
 
 
-# dovecot sieve only allolws one fucking active script. refactor mailbox shit to replace active script symlink by orchestra. Create a generic wrapper that includes al filters (rc, imp and orchestra)
-http://wiki2.dovecot.org/Pigeonhole/Sieve/Examples
-
-
-
-# orders ignorign default filter is not very effective, because of selecting all orders for billing will select ignored too
-
-
-# mail system users group? which one is more convinient? if main group does not exists, backend will fail!
 
 Bash/Python/PHPBackend
 
-
-# bill action view on a separate process. check memory consumption without debug (236m)
-
 # services.handler as generator in order to save memory? not swell like a balloon
-# mailboxes group username instead of mainuser
 
 import uwsgi
 from uwsgidecorators import timer
@@ -406,34 +385,23 @@ uwsgi --reload /tmp/project-master.pid
 # or if uwsgi was started with touch-reload=/tmp/somefile
 touch /tmp/somefile
 
-
 # Change zone ttl
 # batch zone edditing
-# inherit registers from parent?
 
 # datetime metric storage granularity: otherwise innacurate detection of billed metric on order.billed_on
 
 # Serializers.validation migration to DRF3: grep -r 'attrs, source' *|grep -v '~'
 serailzer self.instance on create.
 
-# set_password serializer: "just-the-password" not {"password": "password"}
-
-# use namedtuples?
-
-# Negative transactionsx
-
-
 * check certificate: websites directive ssl + domains search on miscellaneous
 
-
-# Merge websites locations
 # ValueError: Unable to configure handler 'file': [Errno 13] Permission denied: '/home/orchestra/panel/orchestra.log'
 
 # billing invoice link on related invoices not overflow nginx GET vars
 
 * backendLog store method and language... and use it for display_script with correct lexer
 
-# process monitor data to represent state, or maybe create new resource datas when period expires?
+# Compute Resource Data history from Monitor Data.
 
 @register.filter
 def comma(value):
@@ -444,12 +412,8 @@ def comma(value):
     return value
 
 
-
 # payment/bill report allow to change template using a setting variable
 # Payment transaction stats, graps over time
-# order stats: service, cost, top profit, etc
-# TODO remove bill.total 
-
 
 reporter.stories_filed = F('stories_filed') + 1
 reporter.save()
diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py
index c3a132d4..1c1aebfb 100644
--- a/orchestra/admin/options.py
+++ b/orchestra/admin/options.py
@@ -1,3 +1,5 @@
+from urllib import parse
+
 from django import forms
 from django.conf.urls import url
 from django.contrib import admin, messages
@@ -34,30 +36,27 @@ class ChangeListDefaultFilter(object):
     default_changelist_filters = ()
     
     def changelist_view(self, request, extra_context=None):
-        defaults = []
-        for key, value in self.default_changelist_filters:
-            set_url_query(request, key, value)
-            defaults.append(key)
-        # hack response cl context in order to hook default filter awaearness
-        # into search_form.html template
-        response = super(ChangeListDefaultFilter, self).changelist_view(request, extra_context)
-        if hasattr(response, 'context_data') and 'cl' in response.context_data:
-            response.context_data['cl'].default_changelist_filters = defaults
-        return response
 #        defaults = []
-#        querystring = request.META['QUERY_STRING']
-#        redirect = False
-#        for field, value in self.default_changelist_filters:
-#            if field not in queryseting:
-#                redirect = True
-#                querystring[field] = value
-#        if redirect:
-#            raise
-#            if not request.META.get('HTTP_REFERER', '').startswith(request.build_absolute_uri()):
-#                querystring = '&'.join('%s=%s' % filed, value in querystring.items())
-#                from django.http import HttpResponseRedirect
-#                return HttpResponseRedirect(request.path + '?%s' % querystring)
-#        return super(ChangeListDefaultFilter, self).changelist_view(request, extra_context=extra_context)
+#        for key, value in self.default_changelist_filters:
+#            set_url_query(request, key, value)
+#            defaults.append(key)
+#        # hack response cl context in order to hook default filter awaearness
+#        # into search_form.html template
+#        response = super(ChangeListDefaultFilter, self).changelist_view(request, extra_context)
+#        if hasattr(response, 'context_data') and 'cl' in response.context_data:
+#            response.context_data['cl'].default_changelist_filters = defaults
+#        return response
+        querystring = request.META['QUERY_STRING']
+        querydict = parse.parse_qs(querystring)
+        redirect = False
+        for field, value in self.default_changelist_filters:
+            if field not in querydict:
+                redirect = True
+                querydict[field] = value
+        if redirect:
+            querystring = parse.urlencode(querydict, doseq=True)
+            return HttpResponseRedirect(request.path + '?%s' % querystring)
+        return super(ChangeListDefaultFilter, self).changelist_view(request, extra_context=extra_context)
 
 
 class AtLeastOneRequiredInlineFormSet(BaseInlineFormSet):
diff --git a/orchestra/contrib/accounts/forms.py b/orchestra/contrib/accounts/forms.py
index df9a6803..a351f0d3 100644
--- a/orchestra/contrib/accounts/forms.py
+++ b/orchestra/contrib/accounts/forms.py
@@ -22,8 +22,8 @@ def create_account_creation_form():
         model = apps.get_model(model)
         field_name = 'create_%s' % model._meta.model_name
         label = _("Create %s") % model._meta.verbose_name
-        fields[field_name] = forms.BooleanField(initial=True, required=False, label=label,
-                help_text=help_text)
+        fields[field_name] = forms.BooleanField(
+            initial=True, required=False, label=label, help_text=help_text)
         
     def clean(self):
         """ unique usernames between accounts and system users """
@@ -55,7 +55,8 @@ def create_account_creation_form():
             raise ValidationError(errors)
     
     def save_model(self, account):
-        account.save(active_systemuser=self.cleaned_data['enable_systemuser'])
+        enable_systemuser=self.cleaned_data['enable_systemuser']
+        account.save(active_systemuser=enable_systemuser)
     
     def save_related(self, account):
         for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED:
diff --git a/orchestra/contrib/accounts/models.py b/orchestra/contrib/accounts/models.py
index bf6a8a32..7b44de32 100644
--- a/orchestra/contrib/accounts/models.py
+++ b/orchestra/contrib/accounts/models.py
@@ -65,8 +65,9 @@ class Account(auth.AbstractBaseUser):
             was_active = Account.objects.filter(pk=self.pk).values_list('is_active', flat=True)[0]
         super(Account, self).save(*args, **kwargs)
         if created:
-            self.main_systemuser = self.systemusers.create(account=self, username=self.username,
-                password=self.password, is_active=active_systemuser)
+            self.main_systemuser = self.systemusers.create(
+                account=self, username=self.username, password=self.password,
+                is_active=active_systemuser)
             self.save(update_fields=('main_systemuser',))
         elif was_active != self.is_active:
             self.notify_related()
diff --git a/orchestra/contrib/bills/filters.py b/orchestra/contrib/bills/filters.py
index cd706656..cb1b38e3 100644
--- a/orchestra/contrib/bills/filters.py
+++ b/orchestra/contrib/bills/filters.py
@@ -55,11 +55,11 @@ class TotalListFilter(SimpleListFilter):
     
     def queryset(self, request, queryset):
         if self.value() == 'gt':
-            return queryset.filter(computed_total__gt=0)
+            return queryset.filter(approx_total__gt=0)
         elif self.value() == 'eq':
-            return queryset.filter(computed_total=0)
+            return queryset.filter(approx_total=0)
         elif self.value() == 'lt':
-            return queryset.filter(computed_total__lt=0)
+            return queryset.filter(approx_total__lt=0)
         return queryset
 
 
@@ -94,16 +94,17 @@ class PaymentStateListFilter(SimpleListFilter):
         )
     
     def queryset(self, request, queryset):
+        # FIXME use queryset.computed_total instead of approx_total, bills.admin.BillAdmin.get_queryset
         Transaction = queryset.model.transactions.related.related_model
         if self.value() == 'OPEN':
             return queryset.filter(Q(is_open=True)|Q(type=queryset.model.PROFORMA))
         elif self.value() == 'PAID':
-            zeros = queryset.filter(computed_total=0, computed_total__isnull=True)
+            zeros = queryset.filter(approx_total=0, approx_total__isnull=True)
             zeros = zeros.values_list('id', flat=True)
             ammounts = Transaction.objects.exclude(bill_id__in=zeros).secured().group_by('bill_id')
             paid = []
-            relevant = queryset.exclude(computed_total=0, computed_total__isnull=True, is_open=True)
-            for bill_id, total in relevant.values_list('id', 'computed_total'):
+            relevant = queryset.exclude(approx_total=0, approx_total__isnull=True, is_open=True)
+            for bill_id, total in relevant.values_list('id', 'approx_total'):
                 try:
                     ammount = sum([t.ammount for t in ammounts[bill_id]])
                 except KeyError:
@@ -112,8 +113,8 @@ class PaymentStateListFilter(SimpleListFilter):
                     if abs(total) <= abs(ammount):
                         paid.append(bill_id)
             return queryset.filter(
-                Q(computed_total=0) |
-                Q(computed_total__isnull=True) |
+                Q(approx_total=0) |
+                Q(approx_total__isnull=True) |
                 Q(id__in=paid)
             ).exclude(is_open=True)
         elif self.value() == 'PENDING':
@@ -122,7 +123,7 @@ class PaymentStateListFilter(SimpleListFilter):
             non_rejected = non_rejected.values_list('id', flat=True).distinct()
             return queryset.filter(pk__in=non_rejected)
         elif self.value() == 'BAD_DEBT':
-            closed = queryset.filter(is_open=False).exclude(computed_total=0)
+            closed = queryset.filter(is_open=False).exclude(approx_total=0)
             return closed.filter(
                 Q(transactions__state=Transaction.REJECTED) |
                 Q(transactions__isnull=True)
diff --git a/orchestra/contrib/domains/admin.py b/orchestra/contrib/domains/admin.py
index f2ce1a53..f3ff71fc 100644
--- a/orchestra/contrib/domains/admin.py
+++ b/orchestra/contrib/domains/admin.py
@@ -60,7 +60,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
     fields = ('name', 'account_link')
     inlines = [RecordInline, DomainInline]
     list_filter = [TopDomainListFilter]
-    change_readonly_fields = ('name',)
+    change_readonly_fields = ('name', 'serial')
     search_fields = ('name', 'account__username')
     add_form = BatchDomainCreationAdminForm
     change_view_actions = [view_zone]
@@ -93,6 +93,18 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
     display_websites.short_description = _("Websites")
     display_websites.allow_tags = True
     
+    def get_fieldsets(self, request, obj=None):
+        """ Add SOA fields when domain is top """
+        fieldsets = super(DomainAdmin, self).get_fieldsets(request, obj)
+        if obj and obj.is_top:
+            fieldsets += (
+                (_("SOA"), {
+                    'classes': ('collapse',),
+                    'fields': ('serial', 'refresh', 'retry', 'expire', 'min_ttl'),
+                }),
+            )
+        return fieldsets
+    
     def get_queryset(self, request):
         """ Order by structured name and imporve performance """
         qs = super(DomainAdmin, self).get_queryset(request)
diff --git a/orchestra/contrib/domains/migrations/0002_auto_20150715_1017.py b/orchestra/contrib/domains/migrations/0002_auto_20150715_1017.py
new file mode 100644
index 00000000..7b6d9ad1
--- /dev/null
+++ b/orchestra/contrib/domains/migrations/0002_auto_20150715_1017.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('domains', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='domain',
+            name='expire',
+            field=models.IntegerField(null=True, blank=True, help_text='The upper limit in seconds before a zone is considered no longer authoritative (4w by default).', verbose_name='expire'),
+        ),
+        migrations.AddField(
+            model_name='domain',
+            name='min_ttl',
+            field=models.IntegerField(null=True, blank=True, help_text='The negative result TTL (for example, how long a resolver should consider a negative result for a subdomain to be valid before retrying) (1h by default).', verbose_name='refresh'),
+        ),
+        migrations.AddField(
+            model_name='domain',
+            name='refresh',
+            field=models.IntegerField(null=True, blank=True, help_text='The number of seconds before the zone should be refreshed (1d by default).', verbose_name='refresh'),
+        ),
+        migrations.AddField(
+            model_name='domain',
+            name='retry',
+            field=models.IntegerField(null=True, blank=True, help_text='The number of seconds before a failed refresh should be retried (2h by default).', verbose_name='retry'),
+        ),
+        migrations.AlterField(
+            model_name='record',
+            name='value',
+            field=models.CharField(max_length=256, help_text='MX, NS and CNAME records sould end with a dot.', verbose_name='value'),
+        ),
+    ]
diff --git a/orchestra/contrib/domains/models.py b/orchestra/contrib/domains/models.py
index 3e0bebd3..050d6d53 100644
--- a/orchestra/contrib/domains/models.py
+++ b/orchestra/contrib/domains/models.py
@@ -19,8 +19,25 @@ class Domain(models.Model):
         related_name='domains', help_text=_("Automatically selected for subdomains."))
     top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set',
         editable=False)
-    serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial,
-        help_text=_("Serial number"))
+    serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False,
+        help_text=_("A timestamp that changes whenever you update your domain."))
+    refresh = models.IntegerField(_("refresh"), null=True, blank=True,
+        validators=[validators.validate_zone_interval],
+        help_text=_("The number of seconds before the zone should be refreshed "
+                    "(<tt>%s</tt> by default).") % settings.DOMAINS_DEFAULT_REFRESH)
+    retry = models.IntegerField(_("retry"), null=True, blank=True,
+        validators=[validators.validate_zone_interval],
+        help_text=_("The number of seconds before a failed refresh should be retried "
+                    "(<tt>%s</tt> by default).") % settings.DOMAINS_DEFAULT_RETRY)
+    expire = models.IntegerField(_("expire"), null=True, blank=True,
+        validators=[validators.validate_zone_interval],
+        help_text=_("The upper limit in seconds before a zone is considered no longer "
+                    "authoritative (<tt>%s</tt> by default).") % settings.DOMAINS_DEFAULT_EXPIRE)
+    min_ttl = models.IntegerField(_("refresh"), null=True, blank=True,
+        validators=[validators.validate_zone_interval],
+        help_text=_("The negative result TTL (for example, how long a resolver should "
+                    "consider a negative result for a subdomain to be valid before retrying) "
+                    "(<tt>%s</tt> by default).") % settings.DOMAINS_DEFAULT_MIN_TTL)
     
     def __str__(self):
         return self.name
@@ -153,10 +170,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,
-                    settings.DOMAINS_DEFAULT_RETRY,
-                    settings.DOMAINS_DEFAULT_EXPIRATION,
-                    settings.DOMAINS_DEFAULT_MIN_CACHING_TIME
+                    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,
                 ]
                 records.insert(0, AttrDict(
                     type=Record.SOA,
diff --git a/orchestra/contrib/domains/settings.py b/orchestra/contrib/domains/settings.py
index e5808172..eab0e8fe 100644
--- a/orchestra/contrib/domains/settings.py
+++ b/orchestra/contrib/domains/settings.py
@@ -36,13 +36,13 @@ DOMAINS_DEFAULT_RETRY = Setting('DOMAINS_DEFAULT_RETRY',
 )
 
 
-DOMAINS_DEFAULT_EXPIRATION = Setting('DOMAINS_DEFAULT_EXPIRATION',
+DOMAINS_DEFAULT_EXPIRE = Setting('DOMAINS_DEFAULT_EXPIRE',
     '4w',
     validators=[validate_zone_interval],
 )
 
 
-DOMAINS_DEFAULT_MIN_CACHING_TIME = Setting('DOMAINS_DEFAULT_MIN_CACHING_TIME',
+DOMAINS_DEFAULT_MIN_TTL = Setting('DOMAINS_DEFAULT_MIN_TTL',
     '1h',
     validators=[validate_zone_interval],
 )