diff --git a/TODO.md b/TODO.md index 135d1726..7a3ab562 100644 --- a/TODO.md +++ b/TODO.md @@ -437,3 +437,11 @@ mkhomedir_helper or create ssh homes with bash.rc and such # Warning websites with ssl options without https protocol # Schedule cancellation + +# Multiple domains wordpress + +# TODO: separate ports for fpm version + +# Reversion +# implement re-enable account +# Disable/enable saas and VPS diff --git a/orchestra/admin/actions.py b/orchestra/admin/actions.py index 8b15ff3a..e97d7e40 100644 --- a/orchestra/admin/actions.py +++ b/orchestra/admin/actions.py @@ -1,3 +1,5 @@ +from functools import partial + from django.contrib import admin from django.core.mail import send_mass_mail from django.shortcuts import render @@ -108,23 +110,36 @@ class SendEmail(object): return render(request, self.template, self.context) -@action_with_confirmation() -def disable(modeladmin, request, queryset): +def base_disable(modeladmin, request, queryset, disable=True): num = 0 + action_name = _("disabled") if disable else _("enabled") for obj in queryset: - obj.disable() - modeladmin.log_change(request, obj, _("Disabled")) + obj.disable() if disable else obj.enable() + modeladmin.log_change(request, obj, action_name.capitalize()) num += 1 opts = modeladmin.model._meta context = { + 'action_name': action_name, 'verbose_name': opts.verbose_name, 'verbose_name_plural': opts.verbose_name_plural, 'num': num } msg = ungettext( - _("Selected %(verbose_name)s and related services has been disabled.") % context, - _("%(num)s selected %(verbose_name_plural)s and related services have been disabled.") % context, + _("Selected %(verbose_name)s and related services has been %(action_name)s.") % context, + _("%(num)s selected %(verbose_name_plural)s and related services have been %(action_name)s.") % context, num) modeladmin.message_user(request, msg) + + +@action_with_confirmation() +def disable(modeladmin, request, queryset): + return base_disable(modeladmin, request, queryset) disable.url_name = 'disable' disable.short_description = _("Disable") + + +@action_with_confirmation() +def enable(modeladmin, request, queryset): + return base_disable(modeladmin, request, queryset, disable=False) +enable.url_name = 'enable' +enable.short_description = _("Enable") diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py index c9a63fcd..50361e18 100644 --- a/orchestra/admin/options.py +++ b/orchestra/admin/options.py @@ -149,7 +149,7 @@ class ChangeViewActionsMixin(object): kwargs['extra_context']['object_tools_items'] = [ action.__dict__ for action in self.get_change_view_actions(obj) ] - return super(ChangeViewActionsMixin, self).change_view(request, object_id, **kwargs) + return super().change_view(request, object_id, **kwargs) class ChangeAddFieldsMixin(object): diff --git a/orchestra/contrib/accounts/actions.py b/orchestra/contrib/accounts/actions.py index 2dc16117..f05b610a 100644 --- a/orchestra/contrib/accounts/actions.py +++ b/orchestra/contrib/accounts/actions.py @@ -1,3 +1,5 @@ +from functools import partial, wraps + from django.contrib import messages from django.contrib.admin import helpers from django.contrib.admin.utils import NestedObjects, quote @@ -186,21 +188,21 @@ def delete_related_services(modeladmin, request, queryset): delete_related_services.short_description = _("Delete related services") -def disable_selected(modeladmin, request, queryset): +def disable_selected(modeladmin, request, queryset, disable=True): opts = modeladmin.model._meta app_label = opts.app_label - + verbose_action_name = _("disabled") if disable else _("enabled") # The user has already confirmed the deletion. # Do the disable and return a None to display the change list view again. if request.POST.get('post'): n = 0 for account in queryset: - account.disable() - modeladmin.log_change(request, account, _("Disabled")) + account.disable() if disable else account.enable() + modeladmin.log_change(request, account, verbose_action_name.capitalize()) n += 1 modeladmin.message_user(request, ungettext( - _("One account has been successfully disabled."), - _("%i accounts have been successfully disabled.") % n, + _("One account has been successfully %s.") % verbose_action_name, + _("%i accounts have been successfully %s.") % (n, verbose_action_name), n) ) return None @@ -248,6 +250,8 @@ def disable_selected(modeladmin, request, queryset): context = dict( admin_site.each_context(request), + action_name='disable_selected' if disable else 'enable_selected', + disable=disable, title=_("Are you sure?"), objects_name=objects_name, deletable_objects=display, @@ -259,5 +263,11 @@ def disable_selected(modeladmin, request, queryset): template = 'admin/%s/%s/disable_selected_confirmation.html' % (app_label, opts.model_name) return TemplateResponse(request, template, context) disable_selected.short_description = _("Disable selected accounts") -disable_selected.url = 'disable' +disable_selected.url_name = 'disable' disable_selected.tool_description = _("Disable") + + +enable_selected = partial(disable_selected, disable=False) +enable_selected.__name__ = 'enable_selected' +enable_selected.url_name = 'enable' +enable_selected.tool_description = _("Enable") diff --git a/orchestra/contrib/accounts/admin.py b/orchestra/contrib/accounts/admin.py index 5abb40d7..f9f039e1 100644 --- a/orchestra/contrib/accounts/admin.py +++ b/orchestra/contrib/accounts/admin.py @@ -20,7 +20,8 @@ from orchestra.admin.utils import wrap_admin_view, admin_link, set_url_query from orchestra.core import services, accounts from orchestra.forms import UserChangeForm -from .actions import list_contacts, service_report, delete_related_services, disable_selected +from .actions import (list_contacts, service_report, delete_related_services, disable_selected, + enable_selected) from .filters import HasMainUserListFilter from .forms import AccountCreationForm from .models import Account @@ -64,9 +65,10 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin) change_readonly_fields = ('username', 'main_systemuser_link', 'is_active') change_form_template = 'admin/accounts/account/change_form.html' actions = ( - disable_selected, delete_related_services, list_contacts, service_report, SendEmail() + disable_selected, enable_selected, delete_related_services, list_contacts, service_report, + SendEmail() ) - change_view_actions = (disable_selected, service_report) + change_view_actions = (disable_selected, service_report, enable_selected) ordering = () main_systemuser_link = admin_link('main_systemuser') @@ -111,6 +113,14 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin) else: super(AccountAdmin, self).save_model(request, obj, form, change) + def get_change_view_actions(self, obj=None): + views = super().get_change_view_actions(obj=obj) + if obj is not None: + if obj.is_active: + return [view for view in views if view.url_name != 'enable'] + return [view for view in views if view.url_name != 'disable'] + return views + def get_actions(self, request): actions = super(AccountAdmin, self).get_actions(request) if 'delete_selected' in actions: diff --git a/orchestra/contrib/accounts/models.py b/orchestra/contrib/accounts/models.py index a0d264e6..3fe5327a 100644 --- a/orchestra/contrib/accounts/models.py +++ b/orchestra/contrib/accounts/models.py @@ -83,6 +83,11 @@ class Account(auth.AbstractBaseUser): self.save(update_fields=('is_active',)) self.notify_related() + def enable(self): + self.is_active = True + self.save(update_fields=('is_active',)) + self.notify_related() + def get_services_to_disable(self): for rel in self._meta.get_all_related_objects(): source = getattr(rel, 'related_model', rel.model) diff --git a/orchestra/contrib/accounts/templates/admin/accounts/account/disable_selected_confirmation.html b/orchestra/contrib/accounts/templates/admin/accounts/account/disable_selected_confirmation.html index 0479bc20..7ada7249 100644 --- a/orchestra/contrib/accounts/templates/admin/accounts/account/disable_selected_confirmation.html +++ b/orchestra/contrib/accounts/templates/admin/accounts/account/disable_selected_confirmation.html @@ -8,42 +8,28 @@ {% trans 'Home' %}{{ opts.app_config.verbose_name }}{{ opts.verbose_name_plural|capfirst }} -› {% trans 'Disable accounts' %} +› {% if disable%}{% blocktrans %}Disable {{ objects_name }}{% endblocktrans %}{% else %}{% blocktrans %}Enable {{ objects_name }}{% endblocktrans %}{% endif %} {% endblock %} {% block content %} -{% if perms_lacking %} -

{% blocktrans %}Disabling the selected {{ objects_name }} would result in disabling related objects, but your account doesn't have permission to disable the following types of objects:{% endblocktrans %}

- -{% elif protected %} -

{% blocktrans %}Deleting the selected {{ objects_name }} would require deleting the following protected related objects:{% endblocktrans %}

- -{% else %} -

{% blocktrans %}Are you sure you want to disable the selected {{ objects_name }}? All of the following objects and their related items will be disabled:{% endblocktrans %}

-

{% trans "Objects" %}

- {% for deletable_object in deletable_objects %} - - {% endfor %} -
{% csrf_token %} -
- {% for obj in queryset %} - - {% endfor %} - - - - {% trans "No, take me back" %} -
-
+{% if disable%}

{% blocktrans %}Are you sure you want to disable selected {{ objects_name }}?{% endblocktrans %}

+{% else %}

{% blocktrans %}Are you sure you want to enable selected {{ objects_name }}?{% endblocktrans %}

{% endif %} +

{% trans "Objects" %}

+{% for deletable_object in deletable_objects %} + +{% endfor %} +
{% csrf_token %} +
+{% for obj in queryset %} + +{% endfor %} + + + +{% trans "No, take me back" %} +
+
{% endblock %} diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py index 6baffda2..3f6f2217 100644 --- a/orchestra/contrib/bills/admin.py +++ b/orchestra/contrib/bills/admin.py @@ -292,7 +292,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): state = bill.get_payment_state_display().upper() title = '' if bill.closed_amends: - state += '*' + state = '%s*' % state title = _("This bill has been amended, this value may not be valid.") color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey') return '{name}'.format( diff --git a/orchestra/contrib/domains/models.py b/orchestra/contrib/domains/models.py index 98fde905..15a14330 100644 --- a/orchestra/contrib/domains/models.py +++ b/orchestra/contrib/domains/models.py @@ -78,7 +78,10 @@ class Domain(models.Model): @property def is_top(self): # don't cache, don't replace by top_id - return not bool(self.top) + try: + return not bool(self.top) + except Domain.DoesNotExist: + return False @property def subdomains(self): diff --git a/orchestra/contrib/issues/actions.py b/orchestra/contrib/issues/actions.py index dba77702..3e6e1dde 100644 --- a/orchestra/contrib/issues/actions.py +++ b/orchestra/contrib/issues/actions.py @@ -11,7 +11,7 @@ from .helpers import markdown_formated_changes from .models import Queue, Ticket -def change_ticket_state_factory(action, final_state): +def change_ticket_state_factory(action, verbose_name, final_state): context = { 'action': action, 'form': ChangeReasonForm() @@ -40,30 +40,31 @@ def change_ticket_state_factory(action, final_state): 'count': queryset.count(), 'state': final_state.lower() } - msg = _("%s selected tickets are now %s.") % context + msg = _("%(count)s selected tickets are now %(state)s.") % context modeladmin.message_user(request, msg) else: context['form'] = form # action_with_confirmation must display form validation errors return True change_ticket_state.url_name = action - change_ticket_state.verbose_name = action - change_ticket_state.short_description = _('%s selected tickets') % action.capitalize() + change_ticket_state.tool_description = verbose_name + change_ticket_state.short_description = _('%s selected tickets') % verbose_name change_ticket_state.help_text = _('Mark ticket as %s.') % final_state.lower() change_ticket_state.__name__ = action return change_ticket_state action_map = { - Ticket.RESOLVED: 'resolve', - Ticket.REJECTED: 'reject', - Ticket.CLOSED: 'close' + Ticket.RESOLVED: ('resolve', _("Resolve")), + Ticket.REJECTED: ('reject', _("Reject")), + Ticket.CLOSED: ('close', _("Close")), } thismodule = sys.modules[__name__] -for state, name in action_map.items(): - action = change_ticket_state_factory(name, state) +for state, names in action_map.items(): + name, verbose_name = names + action = change_ticket_state_factory(name, verbose_name, state) setattr(thismodule, '%s_tickets' % name, action) @@ -89,6 +90,7 @@ def take_tickets(modeladmin, request, queryset): msg = _("%(count)s selected tickets are now owned by %(user)s.") % context modeladmin.message_user(request, msg) take_tickets.url_name = 'take' +take_tickets.tool_description = _("Take") take_tickets.short_description = _("Take selected tickets") take_tickets.help_text = _("Make yourself owner of the ticket.") diff --git a/orchestra/contrib/issues/admin.py b/orchestra/contrib/issues/admin.py index eac2b7c9..27580708 100644 --- a/orchestra/contrib/issues/admin.py +++ b/orchestra/contrib/issues/admin.py @@ -64,7 +64,6 @@ class MessageReadOnlyInline(admin.TabularInline): return header + content content_html.short_description = _("Content") content_html.allow_tags = True - def has_add_permission(self, request): return False @@ -125,11 +124,10 @@ class TicketAdmin(ExtendedModelAdmin): ) list_display_links = ('unbold_id', 'bold_subject') list_filter = ( - MyTicketsListFilter, 'queue__name', 'priority', TicketStateListFilter, + MyTicketsListFilter, 'queue', 'priority', TicketStateListFilter, ) default_changelist_filters = ( - ('my_tickets', lambda r: 'True' if not r.user.is_superuser else 'False'), - ('state', 'OPEN') + ('state', 'OPEN'), ) date_hierarchy = 'created_at' search_fields = ( @@ -298,7 +296,7 @@ class QueueAdmin(admin.ModelAdmin): def num_tickets(self, queue): num = queue.tickets__count url = reverse('admin:issues_ticket_changelist') - url += '?my_tickets=False&queue=%i' % queue.pk + url += '?queue=%i' % queue.pk return '%d' % (url, num) num_tickets.short_description = _("Tickets") num_tickets.admin_order_field = 'tickets__count' diff --git a/orchestra/contrib/issues/filters.py b/orchestra/contrib/issues/filters.py index c2e2e41d..142fd1d4 100644 --- a/orchestra/contrib/issues/filters.py +++ b/orchestra/contrib/issues/filters.py @@ -12,18 +12,11 @@ class MyTicketsListFilter(SimpleListFilter): def lookups(self, request, model_admin): return ( ('True', _("My Tickets")), - ('False', _("All")), ) def queryset(self, request, queryset): if self.value() == 'True': return queryset.involved_by(request.user) - - def choices(self, cl): - """ Remove default All """ - choices = iter(super(MyTicketsListFilter, self).choices(cl)) - next(choices) - return choices class TicketStateListFilter(SimpleListFilter): diff --git a/orchestra/contrib/issues/migrations/0003_auto_20160320_1127.py b/orchestra/contrib/issues/migrations/0003_auto_20160320_1127.py new file mode 100644 index 00000000..fbf2a1fe --- /dev/null +++ b/orchestra/contrib/issues/migrations/0003_auto_20160320_1127.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import datetime +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('issues', '0002_auto_20150709_1018'), + ] + + operations = [ + migrations.RemoveField( + model_name='message', + name='created_on', + ), + migrations.AddField( + model_name='message', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2016, 3, 20, 10, 27, 45, 766388, tzinfo=utc), verbose_name='created at'), + preserve_default=False, + ), + ] diff --git a/orchestra/contrib/issues/models.py b/orchestra/contrib/issues/models.py index e6ceea4e..590c2caf 100644 --- a/orchestra/contrib/issues/models.py +++ b/orchestra/contrib/issues/models.py @@ -1,5 +1,6 @@ from django.conf import settings as djsettings from django.db import models +from django.db.models import query, Q from django.utils.translation import ugettext_lazy as _ from orchestra.contrib.contacts import settings as contacts_settings @@ -32,6 +33,12 @@ class Queue(models.Model): super(Queue, self).save(*args, **kwargs) +class TicketQuerySet(query.QuerySet): + def involved_by(self, user, *args, **kwargs): + qset = Q(creator=user) | Q(owner=user) | Q(messages__author=user) + return self.filter(qset, *args, **kwargs).distinct() + + class Ticket(models.Model): HIGH = 'HIGH' MEDIUM = 'MEDIUM' @@ -65,13 +72,13 @@ class Ticket(models.Model): queue = models.ForeignKey(Queue, related_name='tickets', null=True, blank=True) subject = models.CharField(_("subject"), max_length=256) description = models.TextField(_("description")) - priority = models.CharField(_("priority"), max_length=32, choices=PRIORITIES, - default=MEDIUM) + priority = models.CharField(_("priority"), max_length=32, choices=PRIORITIES, default=MEDIUM) state = models.CharField(_("state"), max_length=32, choices=STATES, default=NEW) created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True) updated_at = models.DateTimeField(_("modified"), auto_now=True) - cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"), - blank=True) + cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"), blank=True) + + objects = TicketQuerySet.as_manager() class Meta: ordering = ['-updated_at'] @@ -158,7 +165,7 @@ class Message(models.Model): 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) + created_at = models.DateTimeField(_("created at"), auto_now_add=True) class Meta: get_latest_by = 'id' diff --git a/orchestra/contrib/issues/serializers.py b/orchestra/contrib/issues/serializers.py index c8855600..eb636c49 100644 --- a/orchestra/contrib/issues/serializers.py +++ b/orchestra/contrib/issues/serializers.py @@ -13,8 +13,8 @@ class QueueSerializer(serializers.HyperlinkedModelSerializer): class MessageSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Message - fields = ('id', 'author', 'author_name', 'content', 'created_on') - read_only_fields = ('author', 'author_name', 'created_on') + fields = ('id', 'author', 'author_name', 'content', 'created_at') + read_only_fields = ('author', 'author_name', 'created_at') def get_identity(self, data): return data.get('id') diff --git a/orchestra/contrib/issues/templates/issues/ticket_notification.mail b/orchestra/contrib/issues/templates/issues/ticket_notification.mail index 5e329672..c9b39e15 100644 --- a/orchestra/contrib/issues/templates/issues/ticket_notification.mail +++ b/orchestra/contrib/issues/templates/issues/ticket_notification.mail @@ -18,7 +18,7 @@ Issue #{{ ticket.id }} has been updated by {{ ticket_message.author }}. ----------------------------------------------------------------- Issue #{{ ticket.pk }}: {{ ticket.subject }} - * Author: {{ ticket.created_by }} + * Author: {{ ticket.creator_name }} * Status: {{ ticket.get_state_display }} * Priority: {{ ticket.get_priority_display }} * Visibility: {{ ticket.get_visibility_display }} diff --git a/orchestra/contrib/issues/templates/issues/ticket_notification_html.mail b/orchestra/contrib/issues/templates/issues/ticket_notification_html.mail index 8c801fc1..a1bf3229 100644 --- a/orchestra/contrib/issues/templates/issues/ticket_notification_html.mail +++ b/orchestra/contrib/issues/templates/issues/ticket_notification_html.mail @@ -41,7 +41,7 @@ Issue #{{ ticket.id }} has been updated by {{ ticket_message.author }}.

Issue #{{ ticket.pk }}: {{ ticket.subject }}