Added resource history and domain record massive edditing

This commit is contained in:
Marc Aymerich 2015-07-17 13:29:29 +00:00
parent 63c9f381bc
commit 0d9058d266
18 changed files with 765 additions and 67 deletions

View File

@ -423,3 +423,10 @@ Colaesce('total', 'computed_total')
Case Case
# case on payment transaction state ? case when trans.amount > # 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

View File

@ -47,7 +47,8 @@ class AdminFormSet(BaseModelFormSet):
def adminmodelformset_factory(modeladmin, form, formset=AdminFormSet, **kwargs): 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 formset.modeladmin = modeladmin
return formset return formset

View File

@ -200,10 +200,6 @@ class Bill(models.Model):
errors['amend_of'] = _("Related invoice is an amendment.") errors['amend_of'] = _("Related invoice is an amendment.")
if errors: if errors:
raise ValidationError(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): def get_payment_state_display(self):
value = self.payment_state value = self.payment_state

View File

@ -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.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): 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) return TemplateResponse(request, 'admin/domains/domain/view_zone.html', context)
view_zone.url_name = 'view-zone' view_zone.url_name = 'view-zone'
view_zone.verbose_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 = '<a href="%s">%s</a>' % (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

View File

@ -8,24 +8,17 @@ from orchestra.admin.utils import admin_link, change_url
from orchestra.contrib.accounts.admin import AccountAdminMixin from orchestra.contrib.accounts.admin import AccountAdminMixin
from orchestra.utils import apps from orchestra.utils import apps
from .actions import view_zone from .actions import view_zone, edit_records
from .filters import TopDomainListFilter from .filters import TopDomainListFilter
from .forms import RecordInlineFormSet, BatchDomainCreationAdminForm from .forms import RecordForm, RecordInlineFormSet, BatchDomainCreationAdminForm
from .models import Domain, Record from .models import Domain, Record
class RecordInline(admin.TabularInline): class RecordInline(admin.TabularInline):
model = Record model = Record
form = RecordForm
formset = RecordInlineFormSet formset = RecordInlineFormSet
verbose_name_plural = _("Extra records") 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): class DomainInline(admin.TabularInline):
@ -63,6 +56,7 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
change_readonly_fields = ('name', 'serial') change_readonly_fields = ('name', 'serial')
search_fields = ('name', 'account__username') search_fields = ('name', 'account__username')
add_form = BatchDomainCreationAdminForm add_form = BatchDomainCreationAdminForm
actions = (edit_records,)
change_view_actions = [view_zone] change_view_actions = [view_zone]
def structured_name(self, domain): def structured_name(self, domain):

View File

@ -2,6 +2,8 @@ from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin.forms import AdminFormSet
from . import validators from . import validators
from .helpers import domain_for_validation from .helpers import domain_for_validation
from .models import Domain from .models import Domain
@ -63,17 +65,35 @@ class BatchDomainCreationAdminForm(forms.ModelForm):
return cleaned_data 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): def clean(self):
""" Checks if everything is consistent """ """ Checks if everything is consistent """
super(RecordInlineFormSet, self).clean() super(ValidateZoneMixin, self).clean()
if any(self.errors): if any(formset.errors):
return return
if self.instance.name: if formset.instance.name:
records = [] records = []
for form in self.forms: for form in formset.forms:
data = form.cleaned_data data = form.cleaned_data
if data and not data['DELETE']: if data and not data['DELETE']:
records.append(data) records.append(data)
domain = domain_for_validation(self.instance, records) domain = domain_for_validation(formset.instance, records)
validators.validate_zone(domain.render_zone()) validators.validate_zone(domain.render_zone())
class RecordEditFormSet(ValidateZoneMixin, AdminFormSet):
pass
class RecordInlineFormSet(ValidateZoneMixin, forms.models.BaseInlineFormSet):
pass

View File

@ -20,7 +20,7 @@ class Domain(models.Model):
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set', top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set',
editable=False) editable=False)
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, 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, refresh = models.IntegerField(_("refresh"), null=True, blank=True,
validators=[validators.validate_zone_interval], validators=[validators.validate_zone_interval],
help_text=_("The time a secondary DNS server waits before querying the primary DNS " 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, "%s." % settings.DOMAINS_DEFAULT_NAME_SERVER,
utils.format_hostmaster(settings.DOMAINS_DEFAULT_HOSTMASTER), utils.format_hostmaster(settings.DOMAINS_DEFAULT_HOSTMASTER),
str(self.serial), str(self.serial),
settings.DOMAINS_DEFAULT_REFRESH if self.refresh is None else self.refresh, self.refresh or settings.DOMAINS_DEFAULT_REFRESH,
settings.DOMAINS_DEFAULT_RETRY if self.retry is None else self.retry, self.retry or settings.DOMAINS_DEFAULT_RETRY,
settings.DOMAINS_DEFAULT_EXPIRE if self.expire is None else self.expire, self.expire or settings.DOMAINS_DEFAULT_EXPIRE,
settings.DOMAINS_DEFAULT_MIN_TTL if self.min_ttl is None else self.min_ttl, self.min_ttl or settings.DOMAINS_DEFAULT_MIN_TTL,
] ]
records.insert(0, AttrDict( records.insert(0, AttrDict(
type=Record.SOA, type=Record.SOA,
@ -272,13 +272,24 @@ class Record(models.Model):
(SOA, "SOA"), (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') domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records')
ttl = models.CharField(_("TTL"), max_length=8, blank=True, ttl = models.CharField(_("TTL"), max_length=8, blank=True,
help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL, help_text=_("Record TTL, defaults to %s") % settings.DOMAINS_DEFAULT_TTL,
validators=[validators.validate_zone_interval]) validators=[validators.validate_zone_interval])
type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES) type = models.CharField(_("type"), max_length=32, choices=TYPE_CHOICES)
value = models.CharField(_("value"), max_length=256, help_text=_("MX, NS and CNAME records " value = models.CharField(_("value"), max_length=256,
"sould end with a dot.")) help_text=_("MX, NS and CNAME records sould end with a dot."))
def __str__(self): def __str__(self):
return "%s %s IN %s %s" % (self.domain, self.get_ttl(), self.type, self.value) 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 # validate value
if self.type != self.TXT: if self.type != self.TXT:
self.value = self.value.lower().strip() self.value = self.value.lower().strip()
choices = { if self.type:
self.MX: validators.validate_mx_record, try:
self.NS: validators.validate_zone_label, self.VALIDATORS[self.type](self.value)
self.A: validate_ipv4_address, except ValidationError as error:
self.AAAA: validate_ipv6_address, raise ValidationError({
self.CNAME: validators.validate_zone_label, 'value': error,
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})
def get_ttl(self): def get_ttl(self):
return self.ttl or settings.DOMAINS_DEFAULT_TTL return self.ttl or settings.DOMAINS_DEFAULT_TTL

View File

@ -0,0 +1,20 @@
{% extends "admin/orchestra/generic_confirmation.html" %}
{% load static %}
{% block extrahead %}
{{ block.super }}
<script type="text/javascript" src="{% static 'admin/js/core.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/admin/RelatedObjectLookups.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/jquery.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/jquery.init.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/actions.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/collapse.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/inlines.js' %}"></script>
{% endblock %}
{% block formset %}
{% for formset in formsets %}
{{ formset.as_admin }}
{% endfor %}
{% endblock %}

View File

@ -1,5 +1,6 @@
from threading import local from threading import local
from django.contrib.admin.models import LogEntry
from django.core.urlresolvers import resolve from django.core.urlresolvers import resolve
from django.db import transaction from django.db import transaction
from django.db.models.signals import pre_delete, post_save, m2m_changed 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') @receiver(post_save, dispatch_uid='orchestration.post_save_collector')
def post_save_collector(sender, *args, **kwargs): def post_save_collector(sender, *args, **kwargs):
if sender not in [BackendLog, Operation]: if sender not in (BackendLog, Operation, LogEntry):
instance = kwargs.get('instance') instance = kwargs.get('instance')
OperationsMiddleware.collect(Operation.SAVE, **kwargs) OperationsMiddleware.collect(Operation.SAVE, **kwargs)
@receiver(pre_delete, dispatch_uid='orchestration.pre_delete_collector') @receiver(pre_delete, dispatch_uid='orchestration.pre_delete_collector')
def pre_delete_collector(sender, *args, **kwargs): 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) OperationsMiddleware.collect(Operation.DELETE, **kwargs)

View File

@ -62,5 +62,5 @@ def history(modeladmin, request, queryset):
context = { context = {
'resources': resources, 'resources': resources,
} }
return render(request, 'admin/resources/resourcedata/report.html', context) return render(request, 'admin/resources/resourcedata/history.html', context)
history.url_name = 'history' history.url_name = 'history'

View File

@ -254,16 +254,25 @@ def resource_inline_factory(resources):
display_updated = admin_date('updated_at', default=_("Never")) display_updated = admin_date('updated_at', default=_("Never"))
def display_used(self, data): def display_used(self, data):
from django.templatetags.static import static
update_link = '' update_link = ''
history_link = '' history_link = ''
if data.pk: if data.pk:
update_url = reverse('admin:resources_resourcedata_monitor', args=(data.pk,)) context = {
update_link = '<a href="%s"><strong>%s</strong></a>' % (update_url, _("Update")) 'title': _("Update"),
history_url = reverse('admin:resources_resourcedata_history', args=(data.pk,)) 'url': reverse('admin:resources_resourcedata_monitor', args=(data.pk,)),
popup = 'onclick="return showAddAnotherPopup(this);"' 'image': '<img src="%s"></img>' % static('orchestra/images/reload.png'),
history_link = '<a href="%s" %s>%s</a>' % (history_url, popup, _("History")) }
update = '<a href="%(url)s" title="%(title)s">%(image)s</a>' % context
context.update({
'title': _("Show history"),
'image': '<img src="%s"></img>' % static('orchestra/images/history.png'),
'url': reverse('admin:resources_resourcedata_history', args=(data.pk,)),
'popup': 'onclick="return showAddAnotherPopup(this);"',
})
history = '<a href="%(url)s" title="%(title)s" %(popup)s>%(image)s</a>' % context
if data.used is not None: 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 return _("Unknonw %s") % update_link
display_used.short_description = _("Used") display_used.short_description = _("Used")
display_used.allow_tags = True display_used.allow_tags = True

View File

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<title>Transaction Report</title> <title>Resource history</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<script src="/static/orchestra/js/Chart.min.js"></script> <script src="/static/orchestra/js/Chart.min.js"></script>
<style type="text/css"> <style type="text/css">
@ -64,7 +64,7 @@
} }
new Chart(income).Bar(barData, options); new Chart(income).Bar(barData, options);
</script> </script>
<table id="summary"> <table id="summary">
<tr class="header"> <tr class="header">
<th class="title column-name">{% trans "Date" %}</th> <th class="title column-name">{% trans "Date" %}</th>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,358 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:export-ydpi="90.000000"
inkscape:export-xdpi="90.000000"
inkscape:export-filename="/home/glic3/orchestra/django-orchestra/orchestra/static/orchestra/images/history.png"
width="16"
height="16"
id="svg11300"
sodipodi:version="0.32"
inkscape:version="0.91 r"
sodipodi:docname="history.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
version="1.1">
<defs
id="defs3">
<linearGradient
inkscape:collect="always"
id="linearGradient5204">
<stop
style="stop-color:#c4a000;stop-opacity:1;"
offset="0"
id="stop5206" />
<stop
style="stop-color:#c4a000;stop-opacity:0;"
offset="1"
id="stop5208" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5196">
<stop
style="stop-color:#c4a000;stop-opacity:1;"
offset="0"
id="stop5198" />
<stop
style="stop-color:#c4a000;stop-opacity:0;"
offset="1"
id="stop5200" />
</linearGradient>
<linearGradient
id="linearGradient12512">
<stop
style="stop-color:#ffffff;stop-opacity:1.0000000;"
offset="0.0000000"
id="stop12513" />
<stop
style="stop-color:#fff520;stop-opacity:0.89108908;"
offset="0.50000000"
id="stop12517" />
<stop
style="stop-color:#fff300;stop-opacity:0.0000000;"
offset="1.0000000"
id="stop12514" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient12512"
id="radialGradient278"
gradientUnits="userSpaceOnUse"
cx="55"
cy="125"
fx="55"
fy="125"
r="14.375" />
<linearGradient
id="linearGradient10653">
<stop
style="stop-color:#f3f4ff;stop-opacity:1.0000000;"
offset="0.0000000"
id="stop10655" />
<stop
style="stop-color:#9193af;stop-opacity:1.0000000;"
offset="1.0000000"
id="stop10657" />
</linearGradient>
<linearGradient
id="linearGradient42174">
<stop
style="stop-color:#a0a0a0;stop-opacity:1.0000000;"
offset="0.0000000"
id="stop42176" />
<stop
style="stop-color:#ffffff;stop-opacity:1.0000000;"
offset="1.0000000"
id="stop42178" />
</linearGradient>
<linearGradient
id="linearGradient2145">
<stop
style="stop-color:#fffffd;stop-opacity:1.0000000;"
offset="0.0000000"
id="stop2147" />
<stop
style="stop-color:#cbcbc9;stop-opacity:1.0000000;"
offset="1.0000000"
id="stop2149" />
</linearGradient>
<linearGradient
id="linearGradient37935">
<stop
id="stop37937"
offset="0.0000000"
style="stop-color:#9497b3;stop-opacity:1.0000000;" />
<stop
id="stop37939"
offset="1.0000000"
style="stop-color:#4c4059;stop-opacity:1.0000000;" />
</linearGradient>
<linearGradient
id="linearGradient2152">
<stop
id="stop2154"
offset="0.0000000"
style="stop-color:#9aa29a;stop-opacity:1.0000000;" />
<stop
id="stop2156"
offset="1.0000000"
style="stop-color:#b5beb5;stop-opacity:1.0000000;" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2152"
id="linearGradient4307"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.1323593,0,0,0.35150265,-12.192171,24.700536)"
x1="8.9156475"
y1="37.197018"
x2="9.8855038"
y2="52.090679" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient10653"
id="radialGradient4309"
gradientUnits="userSpaceOnUse"
cx="11.3292"
cy="10.58397"
fx="11.3292"
fy="10.58397"
r="15.532059"
gradientTransform="matrix(0.49213503,0,0,0.49213503,0.00830489,31.624507)" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient2145"
id="radialGradient4311"
gradientUnits="userSpaceOnUse"
cx="11.901996"
cy="10.045444"
fx="11.901996"
fy="10.045444"
r="29.292715"
gradientTransform="matrix(0.42187886,0,0,0.42187886,1.1156771,32.810317)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient42174"
id="linearGradient4313"
gradientUnits="userSpaceOnUse"
x1="6.342216"
y1="7.7893324"
x2="22.218424"
y2="25.884274"
gradientTransform="matrix(0.42187886,0,0,0.42187886,1.1156771,32.810317)" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient5196"
id="radialGradient5202"
cx="23.375"
cy="10.972863"
fx="23.375"
fy="10.972863"
r="3.3478093"
gradientTransform="matrix(2.3292353,0,0,2.4008659,-46.253114,13.661846)"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5204"
id="linearGradient5210"
x1="19.667364"
y1="4.2570662"
x2="20.329933"
y2="5.2845874"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.64158826,0,0,0.64158826,-6.8043677,32.387358)" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient37935"
id="radialGradient5212"
gradientUnits="userSpaceOnUse"
cx="8.7468252"
cy="6.8283234"
fx="8.7468252"
fy="6.8283234"
r="29.889715"
gradientTransform="matrix(0.51891397,0,0,0.51891397,-0.4268392,31.2037)" />
</defs>
<sodipodi:namedview
stroke="#c4a000"
fill="#babdb6"
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="0.25490196"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="22.627416"
inkscape:cx="2.7636966"
inkscape:cy="11.830339"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:grid-bbox="true"
inkscape:document-units="px"
inkscape:showpageshadow="false"
inkscape:window-width="1920"
inkscape:window-height="1024"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata4">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>Jakub Steiner</dc:title>
</cc:Agent>
</dc:creator>
<dc:source>http://jimmac.musichall.cz</dc:source>
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/2.0/" />
<dc:title>New Appointment</dc:title>
<dc:subject>
<rdf:Bag>
<rdf:li>appointment</rdf:li>
<rdf:li>new</rdf:li>
<rdf:li>meeting</rdf:li>
<rdf:li>rvsp</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/2.0/">
<cc:permits
rdf:resource="http://web.resource.org/cc/Reproduction" />
<cc:permits
rdf:resource="http://web.resource.org/cc/Distribution" />
<cc:requires
rdf:resource="http://web.resource.org/cc/Notice" />
<cc:requires
rdf:resource="http://web.resource.org/cc/Attribution" />
<cc:permits
rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
<cc:requires
rdf:resource="http://web.resource.org/cc/ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer"
transform="translate(0,-32)">
<path
sodipodi:nodetypes="cccc"
id="path14341"
d="m 6.1045434,32.312322 -5.20565338,6.051009 0.45627258,0.45064 4.7493808,-6.501649 z"
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:url(#linearGradient4307);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none"
inkscape:connector-curvature="0" />
<path
sodipodi:nodetypes="cccc"
id="path18921"
d="M 6.0608909,32.279726 1.4186351,38.717102 2.081818,39.302631 6.0608909,32.279726 Z"
style="fill:#fefefe;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1"
inkscape:connector-curvature="0" />
<circle
id="path27786"
style="fill:url(#radialGradient5212);fill-opacity:1;fill-rule:evenodd;stroke:#605773;stroke-width:0.36248955;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
cx="8.0055094"
cy="39.978912"
r="7.7373796" />
<circle
id="path35549"
style="fill:url(#radialGradient4311);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient4313);stroke-width:0.30012295;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
cx="7.9712114"
cy="39.944607"
r="6.2905145" />
<path
sodipodi:type="arc"
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:url(#radialGradient5202);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5210);stroke-width:0.36248916;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none"
id="path4120"
sodipodi:cx="8.1927567"
sodipodi:cy="40.006229"
sodipodi:rx="5.4535012"
sodipodi:ry="5.4535012"
d="m 3.8969257,36.646689 a 5.4535012,5.4535012 0 0 1 4.2686858,-2.093893 l 0.027145,5.453433 z"
sodipodi:start="3.8052902"
sodipodi:end="4.7074114" />
<circle
id="path34778"
style="fill:#f3f3f3;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.36248925;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
cx="8.1309881"
cy="40.029209"
r="0.91594833" />
<path
id="path35559"
d="M 7.4055247,39.313544 4.135537,36.5667"
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.36248925;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
id="path35561"
d="M 6.4021514,42.59715 7.4751372,40.968385"
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.72497851;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:nodetypes="cc"
inkscape:connector-curvature="0" />
<circle
id="path35563"
style="opacity:1;fill:#b6b9b1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.36871839;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
cx="7.9325924"
cy="34.972736"
r="0.61665297" />
<circle
id="path35565"
style="opacity:1;fill:#b6b9b1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.36871839;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
cx="7.9325924"
cy="44.839195"
r="0.61665297" />
<circle
id="path35567"
style="opacity:1;fill:#b6b9b1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.36871839;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
cx="2.9993658"
cy="39.905964"
r="0.61665297" />
<circle
id="path35569"
style="opacity:1;fill:#b6b9b1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.36871839;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
cx="12.865818"
cy="39.905964"
r="0.61665297" />
<circle
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#radialGradient4309);stroke-width:0.36248961;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
id="path10651"
cx="8.0055075"
cy="39.946884"
r="7.3380861" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

View File

@ -0,0 +1,207 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="16"
height="16"
id="svg2473"
inkscape:version="0.91 r"
sodipodi:docname="reload.svg"
inkscape:export-filename="/home/glic3/orchestra/django-orchestra/orchestra/static/orchestra/images/reload.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<metadata
id="metadata37">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1024"
id="namedview35"
showgrid="false"
inkscape:zoom="14.75"
inkscape:cx="-3.3220339"
inkscape:cy="8"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg2473" />
<defs
id="defs2475">
<linearGradient
id="linearGradient3719">
<stop
id="stop3721"
style="stop-color:#538ec6;stop-opacity:1"
offset="0" />
<stop
id="stop3723"
style="stop-color:#538ec6;stop-opacity:0"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3709">
<stop
id="stop3711"
style="stop-color:#6396cd;stop-opacity:0"
offset="0" />
<stop
id="stop3713"
style="stop-color:#6396cd;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3533">
<stop
id="stop3535"
style="stop-color:#93b9dd;stop-opacity:1"
offset="0" />
<stop
id="stop3545"
style="stop-color:#6396cd;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
x1="108.97799"
y1="230.02158"
x2="107.36588"
y2="224.30264"
id="linearGradient6283"
xlink:href="#linearGradient6277"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient6277">
<stop
id="stop6279"
style="stop-color:#ffffff;stop-opacity:1"
offset="0" />
<stop
id="stop6281"
style="stop-color:#ffffff;stop-opacity:0"
offset="1" />
</linearGradient>
<linearGradient
x1="107.25907"
y1="222.87531"
x2="108.83574"
y2="226.83432"
id="linearGradient6291"
xlink:href="#linearGradient6285"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient6285">
<stop
id="stop6287"
style="stop-color:#ffffff;stop-opacity:1"
offset="0" />
<stop
id="stop6289"
style="stop-color:#ffffff;stop-opacity:0"
offset="1" />
</linearGradient>
<linearGradient
x1="8"
y1="0"
x2="9"
y2="9"
id="linearGradient3697"
xlink:href="#linearGradient3533"
gradientUnits="userSpaceOnUse" />
<linearGradient
x1="107.98247"
y1="219.63542"
x2="108.84705"
y2="227.40942"
id="linearGradient3705"
xlink:href="#linearGradient3533"
gradientUnits="userSpaceOnUse" />
<linearGradient
x1="112.3054"
y1="227.40942"
x2="108.84705"
y2="230.86455"
id="linearGradient3715"
xlink:href="#linearGradient3709"
gradientUnits="userSpaceOnUse" />
<linearGradient
x1="110.58552"
y1="230.02382"
x2="114.06341"
y2="227.41991"
id="linearGradient3725"
xlink:href="#linearGradient3719"
gradientUnits="userSpaceOnUse" />
</defs>
<g
transform="translate(-103,-219)"
id="g6293"
style="stroke:#000000;stroke-opacity:1;display:inline;enable-background:new">
<path
d="m 112.89199,228.29933 a 4.75,4.75 0 0 1 -6.41224,0.8092"
transform="matrix(1.1501219,0,0,1.1521155,-15.186827,-33.014002)"
id="path4517-5"
style="color:#000000;fill:none;stroke:url(#linearGradient3725);stroke-width:3.47488189;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 116,221 0,4 -4,0"
id="path4519-1-9"
style="fill:none;stroke:#538ec6;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m 106.86806,229.35961 a 4.75,4.75 0 1 1 6.22978,-6.89468"
transform="matrix(1.1501219,0,0,1.1521155,-15.186827,-33.014002)"
id="path3717"
style="color:#000000;fill:none;stroke:#538ec6;stroke-width:3.47488189;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
</g>
<path
d="m 107.99194,229.83037 a 4.75,4.75 0 1 1 5.1059,-7.36544"
transform="matrix(1.1566222,0,0,1.1577034,-118.89492,-253.27267)"
id="path4517"
style="color:#000000;fill:none;stroke:url(#linearGradient3705);stroke-width:1.72836542;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 12.999998,2 0,4 -4.0000006,0"
id="path4519-7"
style="fill:none;stroke:url(#linearGradient3697);stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;enable-background:new" />
<path
d="m 11,5.5 -2.0000026,0 M 12.499998,2 l 0,1.3"
id="path4519-8-7"
style="opacity:0.23999999;fill:none;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;enable-background:new" />
<path
d="m 112.88153,228.31178 a 4.75,4.75 0 0 1 -5.33461,1.37241"
transform="matrix(1.1566222,0,0,1.1577034,-118.89492,-253.27267)"
id="path3707"
style="color:#000000;fill:none;stroke:url(#linearGradient3715);stroke-width:1.72836542;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 106.51558,229.134 a 4.75,4.75 0 1 1 6.58226,-6.66907"
transform="matrix(1.2684974,0,0,1.2629385,-131.05785,-276.9778)"
id="path4517-8"
style="opacity:0.23999999;color:#000000;fill:none;stroke:url(#linearGradient6291);stroke-width:0.79006732;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 111.90936,229.18578 a 4.75,4.75 0 1 1 1.18848,-6.72085"
transform="matrix(1.0467613,0,0,1.0546091,-106.88644,-230.0516)"
id="path4517-8-9"
style="opacity:0.23999999;color:#000000;fill:none;stroke:url(#linearGradient6283);stroke-width:0.95176643;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -32,6 +32,7 @@
<p>{{ content_message | safe }}</p> <p>{{ content_message | safe }}</p>
<ul>{{ display_objects | unordered_list }}</ul> <ul>{{ display_objects | unordered_list }}</ul>
<form action="" method="post">{% csrf_token %} <form action="" method="post">{% csrf_token %}
{% block form %}
{% if form %} {% if form %}
<fieldset class="module aligned"> <fieldset class="module aligned">
{{ form.non_field_errors }} {{ form.non_field_errors }}
@ -50,9 +51,12 @@
{% endfor %} {% endfor %}
</fieldset> </fieldset>
{% endif %} {% endif %}
{% endblock %}
{% block formset %}
{% if formset %} {% if formset %}
{{ formset.as_admin }} {{ formset.as_admin }}
{% endif %} {% endif %}
{% endblock %}
<div> <div>
{% for obj in queryset %} {% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" /> <input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />

View File

@ -40,7 +40,7 @@ def _un(singular__plural, n=None):
return ungettext(singular, plural, n) return ungettext(singular, plural, n)
def naturaldatetime(date, include_seconds=False): def naturaldatetime(date, include_seconds=True):
"""Convert datetime into a human natural date string.""" """Convert datetime into a human natural date string."""
if not date: if not date:
return '' return ''
@ -56,35 +56,35 @@ def naturaldatetime(date, include_seconds=False):
minutes = delta.seconds / 60 minutes = delta.seconds / 60
seconds = delta.seconds seconds = delta.seconds
ago = ' ago' ago = " ago"
if days < 0: if days < 0:
ago = '' ago = ""
days = abs(days) days = abs(days)
if days == 0: if days == 0:
if hours == 0: if hours == 0:
if minutes > 0: if minutes >= 1:
minutes = float(seconds)/60 minutes = float(seconds)/60
return ungettext( return ungettext(
_('{minutes:.1f} minute{ago}'), _("{minutes:.1f} minute{ago}"),
_('{minutes:.1f} minutes{ago}'), minutes _("{minutes:.1f} minutes{ago}"), minutes
).format(minutes=minutes, ago=ago) ).format(minutes=minutes, ago=ago)
else: else:
if include_seconds and seconds: if include_seconds:
return ungettext( return ungettext(
_('{seconds} second{ago}'), _("{seconds} second{ago}"),
_('{seconds} seconds{ago}'), seconds _("{seconds} seconds{ago}"), seconds
).format(seconds=seconds, ago=ago) ).format(seconds=seconds, ago=ago)
return _('just now') return _("just now")
else: else:
hours = float(minutes)/60 hours = float(minutes)/60
return ungettext( return ungettext(
_('{hours:.1f} hour{ago}'), _("{hours:.1f} hour{ago}"),
_('{hours:.1f} hours{ago}'), hours _("{hours:.1f} hours{ago}"), hours
).format(hours=hours, ago=ago) ).format(hours=hours, ago=ago)
if delta_midnight.days == 0: if delta_midnight.days == 0:
return _('yesterday at {time}').format(time=date.strftime('%H:%M')) return _("yesterday at {time}").format(time=date.strftime('%H:%M'))
count = 0 count = 0
for chunk, pluralizefun in OLDER_CHUNKS: for chunk, pluralizefun in OLDER_CHUNKS: