From f21091d0c2c0a38f416df4e9f39fad74f1dbc6a1 Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 13 May 2014 13:46:40 +0000 Subject: [PATCH] Random improvements --- TODO.md | 6 +++ orchestra/api/options.py | 46 +++-------------- orchestra/apps/domains/api.py | 17 +++--- orchestra/apps/domains/backends.py | 1 + orchestra/apps/issues/admin.py | 69 +++++++++++++++---------- orchestra/apps/issues/api.py | 10 ++-- orchestra/apps/issues/forms.py | 2 +- orchestra/apps/issues/helpers.py | 4 ++ orchestra/apps/issues/models.py | 19 +++++-- orchestra/apps/issues/serializers.py | 10 ++-- orchestra/apps/orchestration/manager.py | 11 ++-- orchestra/apps/users/models.py | 3 +- orchestra/apps/webapps/api.py | 13 ++--- orchestra/apps/websites/api.py | 13 ++--- orchestra/settings.py | 2 + orchestra/utils/python.py | 7 +++ 16 files changed, 123 insertions(+), 110 deletions(-) diff --git a/TODO.md b/TODO.md index 40ba2b24..2e366607 100644 --- a/TODO.md +++ b/TODO.md @@ -35,3 +35,9 @@ TODO * git deploy in addition to FTP? * env vars instead of multiple settings files: https://devcenter.heroku.com/articles/config-vars ? * optional chroot shell? + +* make sure prefetch_related() is used correctly +Remember that, as always with QuerySets, any subsequent chained methods which imply a different database query will ignore previously cached results, and retrieve data using a fresh database query. +* profile select_related vs prefetch_related + +* use HTTP OPTIONS instead of configuration endpoint, or rename to settings? diff --git a/orchestra/api/options.py b/orchestra/api/options.py index 30c71a21..c540784e 100644 --- a/orchestra/api/options.py +++ b/orchestra/api/options.py @@ -1,13 +1,12 @@ -from django import conf from django.core.exceptions import ImproperlyConfigured -from rest_framework import views -from rest_framework.response import Response -from rest_framework.reverse import reverse from rest_framework.routers import DefaultRouter, Route, flatten, replace_methodname -from .. import settings -from ..utils.apps import autodiscover as module_autodiscover +from orchestra import settings +from orchestra.utils.apps import autodiscover as module_autodiscover +from orchestra.utils.python import import_class + from .helpers import insert_links, replace_collectionmethodname +from .root import APIRoot def collectionlink(**kwargs): @@ -79,39 +78,8 @@ class LinkHeaderRouter(DefaultRouter): def get_api_root_view(self): """ returns the root view, with all the linked collections """ - class APIRoot(views.APIView): - def get(instance, request, format=None): - root_url = reverse('api-root', request=request, format=format) - token_url = reverse('api-token-auth', request=request, format=format) - links = [ - '<%s>; rel="%s"' % (root_url, 'api-root'), - '<%s>; rel="%s"' % (token_url, 'api-get-auth-token'), - ] - if not request.user.is_anonymous(): - list_name = '{basename}-list' - detail_name = '{basename}-detail' - for prefix, viewset, basename in self.registry: - singleton_pk = getattr(viewset, 'singleton_pk', False) - if singleton_pk: - url_name = detail_name.format(basename=basename) - kwargs = { 'pk': singleton_pk(viewset(), request) } - else: - url_name = list_name.format(basename=basename) - kwargs = {} - url = reverse(url_name, request=request, format=format, kwargs=kwargs) - links.append('<%s>; rel="%s"' % (url, url_name)) - # Add user link - url_name = detail_name.format(basename='user') - kwargs = { 'pk': request.user.pk } - url = reverse(url_name, request=request, format=format, kwargs=kwargs) - links.append('<%s>; rel="%s"' % (url, url_name)) - headers = { 'Link': ', '.join(links) } - content = { - name: getattr(settings, name, None) - for name in ['SITE_NAME', 'SITE_VERBOSE_NAME'] - } - content['INSTALLED_APPS'] = conf.settings.INSTALLED_APPS - return Response(content, headers=headers) + APIRoot = import_class(settings.API_ROOT_VIEW) + APIRoot.router = self return APIRoot.as_view() def register(self, prefix, viewset, base_name=None): diff --git a/orchestra/apps/domains/api.py b/orchestra/apps/domains/api.py index 989a77bc..b5089bec 100644 --- a/orchestra/apps/domains/api.py +++ b/orchestra/apps/domains/api.py @@ -2,7 +2,7 @@ from rest_framework import viewsets from rest_framework.decorators import link from rest_framework.response import Response -from orchestra.api import router, collectionlink +from orchestra.api import router from orchestra.apps.accounts.api import AccountApiMixin from . import settings @@ -19,17 +19,18 @@ class DomainViewSet(AccountApiMixin, viewsets.ModelViewSet): qs = super(DomainViewSet, self).get_queryset() return qs.prefetch_related('records') - @collectionlink() - def configuration(self, request): - names = ['DOMAINS_DEFAULT_A', 'DOMAINS_DEFAULT_MX', 'DOMAINS_DEFAULT_NS'] - return Response({ - name: getattr(settings, name, None) for name in names - }) - @link() def view_zone(self, request, pk=None): domain = self.get_object() return Response({'zone': domain.render_zone()}) + + def metadata(self, request): + ret = super(DomainViewSet, self).metadata(request) + names = ['DOMAINS_DEFAULT_A', 'DOMAINS_DEFAULT_MX', 'DOMAINS_DEFAULT_NS'] + ret['settings'] = { + name.lower(): getattr(settings, name, None) for name in names + } + return ret router.register(r'domains', DomainViewSet) diff --git a/orchestra/apps/domains/backends.py b/orchestra/apps/domains/backends.py index aea7206b..8e84c097 100644 --- a/orchestra/apps/domains/backends.py +++ b/orchestra/apps/domains/backends.py @@ -75,6 +75,7 @@ class Bind9MasterDomainBackend(ServiceBackend): class Bind9SlaveDomainBackend(Bind9MasterDomainBackend): verbose_name = _("Bind9 slave domain") related_models = (('domains.Domain', 'origin'),) + def save(self, domain): context = self.get_context(domain) self.update_conf(context) diff --git a/orchestra/apps/issues/admin.py b/orchestra/apps/issues/admin.py index 6c7cc56e..7b867e85 100644 --- a/orchestra/apps/issues/admin.py +++ b/orchestra/apps/issues/admin.py @@ -8,12 +8,12 @@ from django.db import models from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils.html import strip_tags -from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from markdown import markdown from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin#, ChangeViewActions from orchestra.admin.utils import (link, colored, wrap_admin_view, display_timesince) +from orchestra.apps.contacts import settings as contacts_settings from .actions import (reject_tickets, resolve_tickets, take_tickets, close_tickets, mark_as_unread, mark_as_read, set_default_queue) @@ -52,15 +52,15 @@ class MessageReadOnlyInline(admin.TabularInline): 'all': ('orchestra/css/hide-inline-id.css',) } - def content_html(self, obj): + def content_html(self, msg): context = { - 'number': obj.number, - 'time': display_timesince(obj.created_on), - 'author': link('author')(self, obj), + 'number': msg.number, + 'time': display_timesince(msg.created_on), + 'author': link('author')(self, msg) if msg.author else msg.author_name, } summary = _("#%(number)i Updated by %(author)s about %(time)s") % context header = '%s
' % summary - content = markdown(obj.content) + content = markdown(msg.content) content = content.replace('>\n', '>') content = '
%s
' % content return header + content @@ -111,8 +111,9 @@ class TicketInline(admin.TabularInline): owner_link = link('owner') def ticket_id(self, instance): - return mark_safe('%s' % link()(self, instance)) + return '%s' % link()(self, instance) ticket_id.short_description = '#' + ticket_id.allow_tags = True def colored_state(self, instance): return colored('state', STATE_COLORS, bold=False)(instance) @@ -165,7 +166,6 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView ) readonly_fieldsets = ( (None, { - 'classes': ('wide',), 'fields': ('display_summary', ('display_queue', 'display_owner'), ('display_state', 'display_priority'), @@ -174,7 +174,7 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView ) fieldsets = readonly_fieldsets + ( ('Update', { - 'classes': ('collapse', 'wide'), + 'classes': ('collapse',), 'fields': ('subject', ('queue', 'owner',), ('state', 'priority'), @@ -183,7 +183,6 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView ) add_fieldsets = ( (None, { - 'classes': ('wide',), 'fields': ('subject', ('queue', 'owner',), ('state', 'priority'), @@ -204,17 +203,21 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView display_owner = link('owner') def display_summary(self, ticket): - author_url = link('creator')(self, ticket) - created = display_timesince(ticket.created_on) - messages = ticket.messages.order_by('-created_on') - updated = '' - if messages: - updated_on = display_timesince(messages[0].created_on) - updated_by = link('author')(self, messages[0]) - updated = '. Updated by %s about %s' % (updated_by, updated_on) - msg = '

Added by %s about %s%s

' % (author_url, created, updated) - return mark_safe(msg) + context = { + 'creator': link('creator')(self, ticket) if ticket.creator else ticket.creator_name, + 'created': display_timesince(ticket.created_on), + 'updated': '', + } + msg = ticket.messages.last() + if msg: + context.update({ + 'updated': display_timesince(msg.created_on), + 'updater': link('author')(self, msg) if msg.author else msg.author_name, + }) + context['updated'] = '. Updated by %(updater)s about %(updated)s' % context + return '

Added by %(creator)s about %(created)s%(updated)s

' % context display_summary.short_description = 'Summary' + display_summary.allow_tags = True def display_priority(self, ticket): """ State colored for change_form """ @@ -302,15 +305,17 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): #TODO ChangeView def message_preview_view(self, request): """ markdown preview render via ajax """ data = request.POST.get("data") - data_formated = markdown(strip_tags(data)) + data_formated = markdowt_tn(strip_tags(data)) return HttpResponse(data_formated) + + def queryset(self, request): + """ Order by structured name and imporve performance """ + qs = super(TicketAdmin, self).queryset(request) + return qs.select_related('queue', 'owner', 'creator') class QueueAdmin(admin.ModelAdmin): - # TODO notify - list_display = [ - 'name', 'default', 'num_tickets' - ] + list_display = ['name', 'default', 'num_tickets'] actions = [set_default_queue] inlines = [TicketInline] ordering = ['name'] @@ -324,9 +329,21 @@ class QueueAdmin(admin.ModelAdmin): num = queue.tickets.count() url = reverse('admin:issues_ticket_changelist') url += '?my_tickets=False&queue=%i' % queue.pk - return mark_safe('%d' % (url, num)) + return '%d' % (url, num) num_tickets.short_description = _("Tickets") num_tickets.admin_order_field = 'tickets__count' + num_tickets.allow_tags = True + + def get_list_display(self, request): + """ show notifications """ + list_display = list(self.list_display) + for value, verbose in contacts_settings.CONTACTS_EMAIL_USAGES: + def display_notify(queue, notify=value): + return notify in queue.notify + display_notify.short_description = verbose + display_notify.boolean = True + list_display.append(display_notify) + return list_display def queryset(self, request): qs = super(QueueAdmin, self).queryset(request) diff --git a/orchestra/apps/issues/api.py b/orchestra/apps/issues/api.py index 94630552..aa811c3f 100644 --- a/orchestra/apps/issues/api.py +++ b/orchestra/apps/issues/api.py @@ -16,17 +16,19 @@ class TicketViewSet(viewsets.ModelViewSet): @action() def mark_as_read(self, request, pk=None): ticket = self.get_object() - ticket.mark_as_read() - return Response({'status': 'Ticket marked as readed'}) + ticket.mark_as_read_by(request.user) + return Response({'status': 'Ticket marked as read'}) @action() def mark_as_unread(self, request, pk=None): ticket = self.get_object() - ticket.mark_as_unread() - return Response({'status': 'Ticket marked as unreaded'}) + ticket.mark_as_unread_by(request.user) + return Response({'status': 'Ticket marked as unread'}) def get_queryset(self): qs = super(TicketViewSet, self).get_queryset() + qs = qs.select_related('creator', 'queue') + qs = qs.prefetch_related('messages__author') return qs.filter(creator__account=self.request.user.account_id) diff --git a/orchestra/apps/issues/forms.py b/orchestra/apps/issues/forms.py index 42984681..a051e7d7 100644 --- a/orchestra/apps/issues/forms.py +++ b/orchestra/apps/issues/forms.py @@ -92,7 +92,7 @@ class TicketForm(forms.ModelForm): description = description.replace('>\n', '#Ha9G9-?8') description = description.replace('\n', '
') description = description.replace('#Ha9G9-?8', '>\n') - description = '
%s
' % description + description = '
%s
' % description widget = ReadOnlyWidget(description, description) self.fields['display_description'].widget = widget diff --git a/orchestra/apps/issues/helpers.py b/orchestra/apps/issues/helpers.py index d15f4325..68df9c49 100644 --- a/orchestra/apps/issues/helpers.py +++ b/orchestra/apps/issues/helpers.py @@ -32,5 +32,9 @@ def get_ticket_changes(modeladmin, request, ticket): old_value = getattr(ticket, attr) new_value = form.cleaned_data[attr] if old_value != new_value: + choices = dict(form.fields[attr].choices) + if old_value in choices: + old_value = choices[old_value] + new_value = choices[new_value] changes[attr] = (old_value, new_value) return changes diff --git a/orchestra/apps/issues/models.py b/orchestra/apps/issues/models.py index 0c8eb749..48ccfd4a 100644 --- a/orchestra/apps/issues/models.py +++ b/orchestra/apps/issues/models.py @@ -57,7 +57,8 @@ class Ticket(models.Model): ) creator = models.ForeignKey(get_user_model(), verbose_name=_("created by"), - related_name='tickets_created') + related_name='tickets_created', null=True) + creator_name = models.CharField(_("creator name"), max_length=256, blank=True) owner = models.ForeignKey(get_user_model(), null=True, blank=True, related_name='tickets_owned', verbose_name=_("assigned to")) queue = models.ForeignKey(Queue, related_name='tickets', null=True, blank=True) @@ -104,6 +105,8 @@ class Ticket(models.Model): def save(self, *args, **kwargs): """ notify stakeholders of new ticket """ new_issue = not self.pk + if not self.creator_name and self.creator: + self.creator_name = self.creator.get_full_name() super(Ticket, self).save(*args, **kwargs) if new_issue: # PK should be available for rendering the template @@ -119,16 +122,16 @@ class Ticket(models.Model): return self.cc.split(',') if self.cc else [] def mark_as_read_by(self, user): - TicketTracker.objects.get_or_create(ticket=self, user=user) + self.trackers.get_or_create(user=user) def mark_as_unread_by(self, user): - TicketTracker.objects.filter(ticket=self, user=user).delete() + self.trackers.filter(user=user).delete() def mark_as_unread(self): - TicketTracker.objects.filter(ticket=self).delete() + self.trackers.all().delete() def is_read_by(self, user): - return TicketTracker.objects.filter(ticket=self, user=user).exists() + return self.trackers.filter(user=user).exists() def reject(self): self.state = Ticket.REJECTED @@ -152,9 +155,13 @@ class Message(models.Model): related_name='messages') author = models.ForeignKey(get_user_model(), verbose_name=_("author"), related_name='ticket_messages') + author_name = models.CharField(_("author name"), max_length=256, blank=True) content = models.TextField(_("content")) created_on = models.DateTimeField(_("created on"), auto_now_add=True) + class Meta: + get_latest_by = "created_on" + def __unicode__(self): return u"#%i" % self.id @@ -162,7 +169,9 @@ class Message(models.Model): """ notify stakeholders of ticket update """ if not self.pk: self.ticket.mark_as_unread() + self.ticket.mark_as_read_by(self.author) self.ticket.notify(message=self) + self.author_name = self.author.get_full_name() super(Message, self).save(*args, **kwargs) @property diff --git a/orchestra/apps/issues/serializers.py b/orchestra/apps/issues/serializers.py index 817bad05..cf411a12 100644 --- a/orchestra/apps/issues/serializers.py +++ b/orchestra/apps/issues/serializers.py @@ -13,8 +13,8 @@ class QueueSerializer(serializers.HyperlinkedModelSerializer): class MessageSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Message - fields = ('id', 'author', 'content', 'created_on') - read_only_fields = ('author', 'created_on') + fields = ('id', 'author', 'author_name', 'content', 'created_on') + read_only_fields = ('author', 'author_name', 'created_on') def get_identity(self, data): return data.get('id') @@ -32,10 +32,10 @@ class TicketSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Ticket fields = ( - 'url', 'id', 'creator', 'owner', 'queue', 'subject', 'description', - 'state', 'messages', 'is_read' + 'url', 'id', 'creator', 'creator_name', 'owner', 'queue', 'subject', + 'description', 'state', 'messages', 'is_read' ) - read_only_fields = ('creator', 'owner') + read_only_fields = ('creator', 'creator_name', 'owner') def get_is_read(self, obj): return obj.is_read_by(self.context['request'].user) diff --git a/orchestra/apps/orchestration/manager.py b/orchestra/apps/orchestration/manager.py index ff8aee30..fdd22e04 100644 --- a/orchestra/apps/orchestration/manager.py +++ b/orchestra/apps/orchestration/manager.py @@ -2,17 +2,12 @@ import threading from django import db +from orchestra.utils.python import import_class + from . import settings from .helpers import send_report -def get_router(): - module = '.'.join(settings.ORCHESTRATION_ROUTER.split('.')[:-1]) - cls = settings.ORCHESTRATION_ROUTER.split('.')[-1] - module = __import__(module, fromlist=[module]) - return getattr(module, cls) - - def as_task(execute): def wrapper(*args, **kwargs): with db.transaction.commit_manually(): @@ -37,7 +32,7 @@ def close_connection(execute): def execute(operations): """ generates and executes the operations on the servers """ - router = get_router() + router = import_class(settings.ORCHESTRATION_ROUTER) # Generate scripts per server+backend scripts = {} for operation in operations: diff --git a/orchestra/apps/users/models.py b/orchestra/apps/users/models.py index e1ba1d66..ef6f12be 100644 --- a/orchestra/apps/users/models.py +++ b/orchestra/apps/users/models.py @@ -37,9 +37,8 @@ class User(auth.AbstractBaseUser): REQUIRED_FIELDS = ['email'] def get_full_name(self): - """ Returns the first_name plus the last_name, with a space in between """ full_name = '%s %s' % (self.first_name, self.last_name) - return full_name.strip() + return full_name.strip() or self.username def get_short_name(self): """ Returns the short name for the user """ diff --git a/orchestra/apps/webapps/api.py b/orchestra/apps/webapps/api.py index 3e8eac7d..cf938e00 100644 --- a/orchestra/apps/webapps/api.py +++ b/orchestra/apps/webapps/api.py @@ -1,7 +1,7 @@ from rest_framework import viewsets from rest_framework.response import Response -from orchestra.api import router, collectionlink +from orchestra.api import router from orchestra.apps.accounts.api import AccountApiMixin from . import settings @@ -14,15 +14,16 @@ class WebAppViewSet(AccountApiMixin, viewsets.ModelViewSet): serializer_class = WebAppSerializer filter_fields = ('name',) - @collectionlink() - def configuration(self, request): + def metadata(self, request): + ret = super(WebAppViewSet, self).metadata(request) names = [ 'WEBAPPS_BASE_ROOT', 'WEBAPPS_TYPES', 'WEBAPPS_WEBAPP_OPTIONS', 'WEBAPPS_PHP_DISABLED_FUNCTIONS', 'WEBAPPS_DEFAULT_TYPE' ] - return Response({ - name: getattr(settings, name, None) for name in names - }) + ret['settings'] = { + name.lower(): getattr(settings, name, None) for name in names + } + return ret router.register(r'webapps', WebAppViewSet) diff --git a/orchestra/apps/websites/api.py b/orchestra/apps/websites/api.py index 7936cf2c..090076c7 100644 --- a/orchestra/apps/websites/api.py +++ b/orchestra/apps/websites/api.py @@ -1,7 +1,7 @@ from rest_framework import viewsets from rest_framework.response import Response -from orchestra.api import router, collectionlink +from orchestra.api import router from orchestra.apps.accounts.api import AccountApiMixin from . import settings @@ -14,12 +14,13 @@ class WebsiteViewSet(AccountApiMixin, viewsets.ModelViewSet): serializer_class = WebsiteSerializer filter_fields = ('name',) - @collectionlink() - def configuration(self, request): + def metadata(self, request): + ret = super(WebsiteViewSet, self).metadata(request) names = ['WEBSITES_OPTIONS', 'WEBSITES_PORT_CHOICES'] - return Response({ - name: getattr(settings, name, None) for name in names - }) + ret['settings'] = { + name.lower(): getattr(settings, name, None) for name in names + } + return ret router.register(r'websites', WebsiteViewSet) diff --git a/orchestra/settings.py b/orchestra/settings.py index 75b8585b..0663969d 100644 --- a/orchestra/settings.py +++ b/orchestra/settings.py @@ -26,3 +26,5 @@ STOP_SERVICES = getattr(settings, 'STOP_SERVICES', [('uwsgi', 'nginx'), 'celerybeat', 'celeryd', 'celeryevcam', 'postgresql'] ) + +API_ROOT_VIEW = getattr(settings, 'API_ROOT_VIEW', 'orchestra.api.root.APIRoot') diff --git a/orchestra/utils/python.py b/orchestra/utils/python.py index adba5e05..c90a0ba1 100644 --- a/orchestra/utils/python.py +++ b/orchestra/utils/python.py @@ -1,6 +1,13 @@ import collections +def import_class(cls): + module = '.'.join(cls.split('.')[:-1]) + cls = cls.split('.')[-1] + module = __import__(module, fromlist=[module]) + return getattr(module, cls) + + class OrderedSet(collections.MutableSet): def __init__(self, iterable=None): self.end = end = []