Merge all php webapps into one

This commit is contained in:
Marc Aymerich 2015-03-12 14:05:23 +00:00
parent e80f921601
commit fd119f434d
21 changed files with 497 additions and 374 deletions

View File

@ -199,4 +199,11 @@ Php binaries should have this format: /usr/bin/php5.2-cgi
* Orchestra global search box on the header, based https://github.com/django/django/blob/master/django/contrib/admin/options.py#L866 and iterating over all registered services and inspectin its admin.search_fields * Orchestra global search box on the header, based https://github.com/django/django/blob/master/django/contrib/admin/options.py#L866 and iterating over all registered services and inspectin its admin.search_fields
* contain error on plugin missing key (plugin dissabled) * contain error on plugin missing key (plugin dissabled): NOP, fail hard is better than silently
* contact.alternative_phone on a phone.tooltip, email:to
* better validate options and directives (url locations, filesystem paths, etc..)
* filter php deprecated options out based on version
* Todo get php_version for fcgid wrapper

View File

@ -56,13 +56,13 @@ class MailSystemUserBackend(ServiceController):
def delete(self, mailbox): def delete(self, mailbox):
context = self.get_context(mailbox) context = self.get_context(mailbox)
self.append('mv %(home)s %(home)s.deleted' % context)
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""
{ sleep 2 && killall -u %(user)s -s KILL; } & { sleep 2 && killall -u %(user)s -s KILL; } &
killall -u %(user)s || true killall -u %(user)s || true
userdel %(user)s || true userdel %(user)s || true
groupdel %(user)s || true""") % context groupdel %(user)s || true""") % context
) )
self.append('mv %(home)s %(home)s.deleted' % context)
def get_context(self, mailbox): def get_context(self, mailbox):
context = { context = {

View File

@ -25,8 +25,7 @@ STATE_COLORS = {
class RouteAdmin(admin.ModelAdmin): class RouteAdmin(admin.ModelAdmin):
list_display = [ list_display = [
'id', 'backend', 'host', 'match', 'display_model', 'display_actions', 'backend', 'host', 'match', 'display_model', 'display_actions', 'is_active'
'is_active'
] ]
list_editable = ['host', 'match', 'is_active'] list_editable = ['host', 'match', 'is_active']
list_filter = ['host', 'is_active', 'backend'] list_filter = ['host', 'is_active', 'backend']
@ -65,7 +64,7 @@ class RouteAdmin(admin.ModelAdmin):
""" Include dynamic help text for existing objects """ """ Include dynamic help text for existing objects """
form = super(RouteAdmin, self).get_form(request, obj=obj, **kwargs) form = super(RouteAdmin, self).get_form(request, obj=obj, **kwargs)
if obj: if obj:
form.base_fields['backend'].help_text = self.BACKEND_HELP_TEXT[obj.backend] form.base_fields['backend'].help_text = self.BACKEND_HELP_TEXT.get(obj.backend, '')
return form return form

View File

@ -10,6 +10,7 @@ from orchestra.utils.python import import_class
from . import settings from . import settings
from .helpers import send_report from .helpers import send_report
from .models import BackendLog from .models import BackendLog
from .signals import pre_action, post_action
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -65,8 +66,17 @@ def execute(operations, async=False):
else: else:
scripts[key][1].append(operation) scripts[key][1].append(operation)
# Get and call backend action method # Get and call backend action method
method = getattr(scripts[key][0], operation.action) backend = scripts[key][0]
method = getattr(backend, operation.action)
kwargs = {
'sender': backend.__class__,
'backend': backend,
'instance': operation.instance,
'action': operation.action,
}
pre_action.send(**kwargs)
method(operation.instance) method(operation.instance)
post_action.send(**kwargs)
# Execute scripts on each server # Execute scripts on each server
threads = [] threads = []
executions = [] executions = []

View File

@ -7,9 +7,9 @@ from django.http.response import HttpResponseServerError
from orchestra.utils.python import OrderedSet from orchestra.utils.python import OrderedSet
from .manager import router
from .backends import ServiceBackend from .backends import ServiceBackend
from .helpers import message_user from .helpers import message_user
from .manager import router
from .models import BackendLog from .models import BackendLog
from .models import BackendOperation as Operation from .models import BackendOperation as Operation
@ -103,7 +103,7 @@ class OperationsMiddleware(object):
pass pass
else: else:
update_fields = kwargs.get('update_fields', None) update_fields = kwargs.get('update_fields', None)
if update_fields: if update_fields is not None:
# "update_fileds=[]" is a convention for explicitly executing backend # "update_fileds=[]" is a convention for explicitly executing backend
# i.e. account.disable() # i.e. account.disable()
if update_fields != []: if update_fields != []:

View File

@ -0,0 +1,6 @@
import django.dispatch
pre_action = django.dispatch.Signal(providing_args=['backend', 'instance', 'action'])
post_action = django.dispatch.Signal(providing_args=['backend', 'instance', 'action'])

View File

@ -53,6 +53,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin)
inlines = [WebAppOptionInline] inlines = [WebAppOptionInline]
readonly_fields = ('account_link',) readonly_fields = ('account_link',)
change_readonly_fields = ('name', 'type') change_readonly_fields = ('name', 'type')
search_fuelds = ('name', 'account__username')
list_prefetch_related = ('content_set__website',) list_prefetch_related = ('content_set__website',)
plugin = AppType plugin = AppType
plugin_field = 'type' plugin_field = 'type'

View File

@ -0,0 +1,177 @@
import os
import textwrap
from django.template import Template, Context
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceController
from . import WebAppServiceMixin
from .. import settings
class PHPBackend(WebAppServiceMixin, ServiceController):
verbose_name = _("PHP FPM/FCGID")
default_route_match = "webapp.type == 'php'"
def save(self, webapp):
context = self.get_context(webapp)
if webapp.type_instance.is_fpm:
self.save_fpm(webapp, context)
self.delete_fcgid(webapp, context)
elif webapp.type_instance.is_fcgid:
self.save_fcgid(webapp, context)
self.delete_fpm(webapp, context)
def save_fpm(self, webapp, context):
self.create_webapp_dir(context)
self.set_under_construction(context)
self.append(textwrap.dedent("""\
{
echo -e '%(fpm_config)s' | diff -N -I'^\s*;;' %(fpm_path)s -
} || {
echo -e '%(fpm_config)s' > %(fpm_path)s
UPDATEDFPM=1
}""") % context
)
def save_fcgid(self, webapp, context):
self.create_webapp_dir(context)
self.set_under_construction(context)
self.append("mkdir -p %(wrapper_dir)s" % context)
self.append(textwrap.dedent("""\
{
echo -e '%(wrapper)s' | diff -N -I'^\s*#' %(wrapper_path)s -
} || {
echo -e '%(wrapper)s' > %(wrapper_path)s; UPDATED_APACHE=1
}""") % context
)
self.append("chmod +x %(wrapper_path)s" % context)
self.append("chown -R %(user)s:%(group)s %(wrapper_dir)s" % context)
if context['cmd_options']:
self.append(textwrap.dedent("""
{
echo -e '%(cmd_options)s' | diff -N -I'^\s*#' %(cmd_options_path)s -
} || {
echo -e '%(cmd_options)s' > %(cmd_options_path)s; UPDATED_APACHE=1
}""" ) % context
)
else:
self.append("rm -f %(cmd_options_path)s" % context)
def delete(self, webapp):
context = self.get_context(webapp)
if webapp.type_instance.is_fpm:
self.delete_fpm(webapp, context)
elif webapp.type_instance.is_fcgid:
self.delete_fcgid(webapp, context)
self.delete_webapp_dir(context)
def delete_fpm(self, webapp, context):
self.append("rm -f %(fpm_path)s" % context)
def delete_fcgid(self, webapp, context):
self.append("rm -f %(wrapper_path)s" % context)
self.append("rm -f %(cmd_options_path)s" % context)
def commit(self):
if self.content:
self.append(textwrap.dedent("""
if [[ $UPDATEDFPM == 1 ]]; then
service php5-fpm reload
service php5-fpm start
fi""")
)
self.append(textwrap.dedent("""\
if [[ $UPDATED_APACHE == 1 ]]; then
service apache2 reload
fi""")
)
def get_fpm_config(self, webapp, context):
context.update({
'init_vars': webapp.type_instance.get_php_init_vars(),
'max_children': webapp.get_options().get('processes', False),
'request_terminate_timeout': webapp.get_options().get('timeout', False),
})
context['fpm_listen'] = webapp.type_instance.FPM_LISTEN % context
fpm_config = Template(textwrap.dedent("""\
;; {{ banner }}
[{{ user }}]
user = {{ user }}
group = {{ group }}
listen = {{ fpm_listen | safe }}
listen.owner = {{ user }}
listen.group = {{ group }}
pm = ondemand
{% if max_children %}pm.max_children = {{ max_children }}{% endif %}
{% if request_terminate_timeout %}request_terminate_timeout = {{ request_terminate_timeout }}{% endif %}
{% for name, value in init_vars.iteritems %}
php_admin_value[{{ name | safe }}] = {{ value | safe }}{% endfor %}
"""
))
return fpm_config.render(Context(context))
def get_fcgid_wrapper(self, webapp, context):
opt = webapp.type_instance
# Format PHP init vars
init_vars = opt.get_php_init_vars()
if init_vars:
init_vars = [ '-d %s="%s"' % (k,v) for k,v in init_vars.iteritems() ]
init_vars = ', '.join(init_vars)
context.update({
'php_binary': os.path.normpath(settings.WEBAPPS_PHP_CGI_BINARY_PATH % context),
'php_rc': os.path.normpath(settings.WEBAPPS_PHP_CGI_RC_DIR % context),
'php_ini_scan': os.path.normpath(settings.WEBAPPS_PHP_CGI_INI_SCAN_DIR % context),
'php_init_vars': init_vars,
})
return textwrap.dedent("""\
#!/bin/sh
# %(banner)s
export PHPRC=%(php_rc)s
export PHP_INI_SCAN_DIR=%(php_ini_scan)s
exec %(php_binary)s %(php_init_vars)s""") % context
def get_fcgid_cmd_options(self, webapp, context):
maps = {
'MaxProcesses': webapp.get_options().get('processes', None),
'IOTimeout': webapp.get_options().get('timeout', None),
}
cmd_options = []
for directive, value in maps.iteritems():
if value:
cmd_options.append("%s %s" % (directive, value))
if cmd_options:
head = '# %(banner)s\nFcgidCmdOptions %(wrapper_path)s' % context
cmd_options.insert(0, head)
return ' \\\n '.join(cmd_options)
def update_fcgid_context(self, webapp, context):
wrapper_path = webapp.type_instance.FCGID_WRAPPER_PATH % context
context.update({
'wrapper': self.get_fcgid_wrapper(webapp, context),
'wrapper_path': wrapper_path,
'wrapper_dir': os.path.dirname(wrapper_path),
})
context.update({
'cmd_options': self.get_fcgid_cmd_options(webapp, context),
'cmd_options_path': settings.WEBAPPS_FCGID_CMD_OPTIONS_PATH % context,
})
def update_fpm_context(self, webapp, context):
context.update({
'fpm_config': self.get_fpm_config(webapp, context),
'fpm_path': settings.WEBAPPS_PHPFPM_POOL_PATH % context,
})
return context
def get_context(self, webapp):
context = super(PHPBackend, self).get_context(webapp)
context.update({
'php_version': webapp.type_instance.get_php_version(),
'php_version_number': webapp.type_instance.get_php_version_number(),
})
self.update_fcgid_context(webapp, context)
self.update_fpm_context(webapp, context)
return context

View File

@ -1,97 +0,0 @@
import os
import textwrap
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceController
from . import WebAppServiceMixin
from .. import settings
class PHPFcgidBackend(WebAppServiceMixin, ServiceController):
""" Per-webapp fcgid application """
verbose_name = _("PHP-Fcgid")
directive = 'fcgid'
default_route_match = "webapp.type_class.php_execution == 'fcgid'"
def save(self, webapp):
context = self.get_context(webapp)
self.create_webapp_dir(context)
self.set_under_construction(context)
self.append("mkdir -p %(wrapper_dir)s" % context)
self.append(textwrap.dedent("""\
{
echo -e '%(wrapper)s' | diff -N -I'^\s*#' %(wrapper_path)s -
} || {
echo -e '%(wrapper)s' > %(wrapper_path)s; UPDATED_APACHE=1
}""") % context
)
self.append("chmod +x %(wrapper_path)s" % context)
self.append("chown -R %(user)s:%(group)s %(wrapper_dir)s" % context)
if context['cmd_options']:
self.append(textwrap.dedent("""
{
echo -e '%(cmd_options)s' | diff -N -I'^\s*#' %(cmd_options_path)s -
} || {
echo -e '%(cmd_options)s' > %(cmd_options_path)s; UPDATED_APACHE=1
}""" ) % context
)
else:
self.append("rm -f %(cmd_options_path)s" % context)
def delete(self, webapp):
context = self.get_context(webapp)
self.append("rm -f '%(wrapper_path)s'" % context)
self.append("rm -f '%(cmd_options_path)s'" % context)
self.delete_webapp_dir(context)
def commit(self):
self.append('if [[ $UPDATED_APACHE == 1 ]]; then service apache2 reload; fi')
def get_fcgid_wrapper(self, webapp, context):
opt = webapp.type_instance
# Format PHP init vars
init_vars = opt.get_php_init_vars()
if init_vars:
init_vars = [ '-d %s="%s"' % (k,v) for k,v in init_vars.iteritems() ]
init_vars = ', '.join(init_vars)
context.update({
'php_binary': opt.get_php_binary_path(),
'php_rc': opt.get_php_rc_path(),
'php_init_vars': init_vars,
})
return textwrap.dedent("""\
#!/bin/sh
# %(banner)s
export PHPRC=%(php_rc)s
exec %(php_binary)s %(php_init_vars)s""") % context
def get_fcgid_cmd_options(self, webapp, context):
maps = {
'MaxProcesses': webapp.get_options().get('processes', None),
'IOTimeout': webapp.get_options().get('timeout', None),
}
cmd_options = []
for directive, value in maps.iteritems():
if value:
cmd_options.append("%s %s" % (directive, value))
if cmd_options:
head = '# %(banner)s\nFcgidCmdOptions %(wrapper_path)s' % context
cmd_options.insert(0, head)
return ' \\\n '.join(cmd_options)
def get_context(self, webapp):
context = super(PHPFcgidBackend, self).get_context(webapp)
wrapper_path = settings.WEBAPPS_FCGID_WRAPPER_PATH % context
context.update({
'wrapper': self.get_fcgid_wrapper(webapp, context),
'wrapper_path': wrapper_path,
'wrapper_dir': os.path.dirname(wrapper_path),
})
context.update({
'cmd_options': self.get_fcgid_cmd_options(webapp, context),
'cmd_options_path': settings.WEBAPPS_FCGID_CMD_OPTIONS_PATH % context,
})
return context

View File

@ -1,78 +0,0 @@
import os
import textwrap
from django.template import Template, Context
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceController
from . import WebAppServiceMixin
from .. import settings
class PHPFPMBackend(WebAppServiceMixin, ServiceController):
""" Per-webapp php application """
verbose_name = _("PHP-FPM")
default_route_match = "webapp.type_class.php_execution == 'fpm'"
def save(self, webapp):
context = self.get_context(webapp)
self.create_webapp_dir(context)
self.set_under_construction(context)
self.append(textwrap.dedent("""\
{
echo -e '%(fpm_config)s' | diff -N -I'^\s*;;' %(fpm_path)s -
} || {
echo -e '%(fpm_config)s' > %(fpm_path)s
UPDATEDFPM=1
}""") % context
)
def delete(self, webapp):
context = self.get_context(webapp)
self.append("rm '%(fpm_path)s'" % context)
self.delete_webapp_dir(context)
def commit(self):
if not self.cmds:
return
super(PHPFPMBackend, self).commit()
self.append(textwrap.dedent("""
if [[ $UPDATEDFPM == 1 ]]; then
service php5-fpm reload
service php5-fpm start
fi"""))
def get_fpm_config(self, webapp, context):
context.update({
'init_vars': webapp.type_instance.get_php_init_vars(),
'fpm_port': webapp.get_fpm_port(),
'max_children': webapp.get_options().get('processes', False),
'request_terminate_timeout': webapp.get_options().get('timeout', False),
})
context['fpm_listen'] = settings.WEBAPPS_FPM_LISTEN % context
fpm_config = Template(textwrap.dedent("""\
;; {{ banner }}
[{{ user }}]
user = {{ user }}
group = {{ group }}
listen = {{ fpm_listen | safe }}
listen.owner = {{ user }}
listen.group = {{ group }}
pm = ondemand
{% if max_children %}pm.max_children = {{ max_children }}{% endif %}
{% if request_terminate_timeout %}request_terminate_timeout = {{ request_terminate_timeout }}{% endif %}
{% for name, value in init_vars.iteritems %}
php_admin_value[{{ name | safe }}] = {{ value | safe }}{% endfor %}
"""
))
return fpm_config.render(Context(context))
def get_context(self, webapp):
context = super(PHPFPMBackend, self).get_context(webapp)
context.update({
'fpm_config': self.get_fpm_config(webapp, context),
'fpm_path': settings.WEBAPPS_PHPFPM_POOL_PATH % context,
})
return context

View File

@ -27,50 +27,48 @@ WEBAPPS_FCGID_CMD_OPTIONS_PATH = getattr(settings, 'WEBAPPS_FCGID_CMD_OPTIONS_PA
WEBAPPS_PHP_ERROR_LOG_PATH = getattr(settings, 'WEBAPPS_PHP_ERROR_LOG_PATH', WEBAPPS_PHP_ERROR_LOG_PATH = getattr(settings, 'WEBAPPS_PHP_ERROR_LOG_PATH',
'') '')
WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', ( WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', (
'orchestra.apps.webapps.types.php.PHPFPMApp', 'orchestra.apps.webapps.types.php.PHPApp',
'orchestra.apps.webapps.types.php.PHPFCGIDApp',
'orchestra.apps.webapps.types.misc.StaticApp', 'orchestra.apps.webapps.types.misc.StaticApp',
'orchestra.apps.webapps.types.misc.WebalizerApp', 'orchestra.apps.webapps.types.misc.WebalizerApp',
'orchestra.apps.webapps.types.saas.WordPressMuApp', 'orchestra.apps.webapps.types.saas.WordPressMuApp',
'orchestra.apps.webapps.types.saas.DokuWikiMuApp', 'orchestra.apps.webapps.types.saas.DokuWikiMuApp',
'orchestra.apps.webapps.types.saas.DrupalMuApp', 'orchestra.apps.webapps.types.saas.DrupalMuApp',
'orchestra.apps.webapps.types.misc.SymbolicLinkApp', 'orchestra.apps.webapps.types.misc.SymbolicLinkApp',
'orchestra.apps.webapps.types.wordpress.WordPressFPMApp', 'orchestra.apps.webapps.types.wordpress.WordPressApp',
'orchestra.apps.webapps.types.wordpress.WordPressFCGIDApp',
)) ))
WEBAPPS_PHP_VERSIONS = getattr(settings, 'WEBAPPS_PHP_VERSIONS', (
WEBAPPS_PHP_FCGID_VERSIONS = getattr(settings, 'WEBAPPS_PHP_FCGID_VERSIONS', ( # Execution modle choose by ending with -fpm or -cgi
('5.4', '5.4'), ('php-5.4-fpm', 'PHP 5.4 FPM'),
('5.3', '5.3'), ('php-5.4-cgi', 'PHP 5.4 FCGID'),
('5.2', '5.2'), ('php-5.3-cgi', 'PHP 5.3 FCGID'),
('4', '4'), ('php-5.2-cgi', 'PHP 5.2 FCGID'),
('php-4-cgi', 'PHP 4 FCGID'),
)) ))
WEBAPPS_PHP_FCGID_DEFAULT_VERSION = getattr(settings, 'WEBAPPS_PHP_FCGID_DEFAULT_VERSION', WEBAPPS_DEFAULT_PHP_VERSION = getattr(settings, 'WEBAPPS_DEFAULT_PHP_VERSION',
'5.4') '5.4-cgi')
WEBAPPS_PHP_CGI_BINARY_PATH = getattr(settings, 'WEBAPPS_PHP_CGI_BINARY_PATH', WEBAPPS_PHP_CGI_BINARY_PATH = getattr(settings, 'WEBAPPS_PHP_CGI_BINARY_PATH',
# Path of the cgi binary used by fcgid # Path of the cgi binary used by fcgid
'/usr/bin/php%(php_version)s-cgi') '/usr/bin/php%(php_version_number)s-cgi')
WEBAPPS_PHP_CGI_RC_PATH = getattr(settings, 'WEBAPPS_PHP_CGI_RC_PATH',
WEBAPPS_PHP_CGI_RC_DIR = getattr(settings, 'WEBAPPS_PHP_CGI_RC_DIR',
# Path to php.ini # Path to php.ini
'/etc/php%(php_version)s/cgi/') '/etc/php%(php_version_number)s/cgi/')
WEBAPPS_PHP_FPM_VERSIONS = getattr(settings, 'WEBAPPS_PHP_FPM_VERSIONS', ( WEBAPPS_PHP_CGI_INI_SCAN_DIR = getattr(settings, 'WEBAPPS_PHP_CGI_INI_SCAN_DIR',
('5.4', '5.4'), # Path to php.ini
)) '/etc/php%(php_version_number)s/cgi/conf.d')
WEBAPPS_PHP_FPM_DEFAULT_VERSION = getattr(settings, 'WEBAPPS_PHP_DEFAULT_VERSION',
'5.4')
WEBAPPS_UNDER_CONSTRUCTION_PATH = getattr(settings, 'WEBAPPS_UNDER_CONSTRUCTION_PATH', WEBAPPS_UNDER_CONSTRUCTION_PATH = getattr(settings, 'WEBAPPS_UNDER_CONSTRUCTION_PATH',
# Server-side path where a under construction stock page is # Server-side path where a under construction stock page is

View File

@ -20,7 +20,6 @@ class AppType(plugins.Plugin):
unique_name = False unique_name = False
option_groups = (AppOption.FILESYSTEM, AppOption.PROCESS, AppOption.PHP) option_groups = (AppOption.FILESYSTEM, AppOption.PROCESS, AppOption.PHP)
# TODO generic name like 'execution' ? # TODO generic name like 'execution' ?
php_execution = None
@classmethod @classmethod
@cached @cached

View File

@ -9,7 +9,7 @@ from orchestra.plugins.forms import PluginDataForm
from ..options import AppOption from ..options import AppOption
from . import AppType from . import AppType
from .php import PHPAppType from .php import PHPApp
class StaticApp(AppType): class StaticApp(AppType):
@ -48,7 +48,7 @@ class SymbolicLinkSerializer(serializers.Serializer):
path = serializers.CharField(label=_("Path")) path = serializers.CharField(label=_("Path"))
class SymbolicLinkApp(PHPAppType): class SymbolicLinkApp(PHPApp):
name = 'symbolic-link' name = 'symbolic-link'
verbose_name = "Symbolic link" verbose_name = "Symbolic link"
form = SymbolicLinkForm form = SymbolicLinkForm

View File

@ -1,4 +1,5 @@
import os import os
import re
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -12,12 +13,46 @@ from .. import settings
from . import AppType from . import AppType
class PHPAppType(AppType): help_message = _("Version of PHP used to execute this webapp. <br>"
FPM = 'fpm' "Changing the PHP version may result in application malfunction, "
FCGID = 'fcgid' "make sure that everything continue to work as expected.")
class PHPAppForm(PluginDataForm):
php_version = forms.ChoiceField(label=_("PHP version"),
choices=settings.WEBAPPS_PHP_VERSIONS,
initial=settings.WEBAPPS_DEFAULT_PHP_VERSION,
help_text=help_message)
class PHPAppSerializer(serializers.Serializer):
php_version = serializers.ChoiceField(label=_("PHP version"),
choices=settings.WEBAPPS_PHP_VERSIONS,
default=settings.WEBAPPS_DEFAULT_PHP_VERSION,
help_text=help_message)
class PHPApp(AppType):
name = 'php'
verbose_name = "PHP"
help_text = _("This creates a PHP application under ~/webapps/&lt;app_name&gt;<br>")
form = PHPAppForm
serializer = PHPAppSerializer
icon = 'orchestra/icons/apps/PHP.png'
php_version = 5.4 DEFAULT_PHP_VERSION = settings.WEBAPPS_DEFAULT_PHP_VERSION
fpm_listen = settings.WEBAPPS_FPM_LISTEN PHP_DISABLED_FUNCTIONS = settings.WEBAPPS_PHP_DISABLED_FUNCTIONS
PHP_ERROR_LOG_PATH = settings.WEBAPPS_PHP_ERROR_LOG_PATH
FPM_LISTEN = settings.WEBAPPS_FPM_LISTEN
FCGID_WRAPPER_PATH = settings.WEBAPPS_FCGID_WRAPPER_PATH
@property
def is_fpm(self):
return self.get_php_version().endswith('-fpm')
@property
def is_fcgid(self):
return self.get_php_version().endswith('-cgi')
def get_context(self): def get_context(self):
""" context used to format settings """ """ context used to format settings """
@ -46,95 +81,37 @@ class PHPAppType(AppType):
enabled_functions += enabled_functions.get().value.split(',') enabled_functions += enabled_functions.get().value.split(',')
if enabled_functions: if enabled_functions:
disabled_functions = [] disabled_functions = []
for function in settings.WEBAPPS_PHP_DISABLED_FUNCTIONS: for function in self.PHP_DISABLED_FUNCTIONS:
if function not in enabled_functions: if function not in enabled_functions:
disabled_functions.append(function) disabled_functions.append(function)
init_vars['dissabled_functions'] = ','.join(disabled_functions) init_vars['dissabled_functions'] = ','.join(disabled_functions)
if settings.WEBAPPS_PHP_ERROR_LOG_PATH and 'error_log' not in init_vars: if self.PHP_ERROR_LOG_PATH and 'error_log' not in init_vars:
context = self.get_context() context = self.get_context()
error_log_path = os.path.normpath(settings.WEBAPPS_PHP_ERROR_LOG_PATH % context) error_log_path = os.path.normpath(self.PHP_ERROR_LOG_PATH % context)
init_vars['error_log'] = error_log_path init_vars['error_log'] = error_log_path
return init_vars return init_vars
help_message = _("Version of PHP used to execute this webapp. <br>"
"Changing the PHP version may result in application malfunction, "
"make sure that everything continue to work as expected.")
class PHPFPMAppForm(PluginDataForm):
php_version = forms.ChoiceField(label=_("PHP version"),
choices=settings.WEBAPPS_PHP_FPM_VERSIONS,
initial=settings.WEBAPPS_PHP_FPM_DEFAULT_VERSION,
help_text=help_message)
class PHPFPMAppSerializer(serializers.Serializer):
php_version = serializers.ChoiceField(label=_("PHP version"),
choices=settings.WEBAPPS_PHP_FPM_VERSIONS,
default=settings.WEBAPPS_PHP_FPM_DEFAULT_VERSION,
help_text=help_message)
class PHPFPMApp(PHPAppType):
name = 'php-fpm'
php_execution = PHPAppType.FPM
verbose_name = "PHP FPM"
help_text = _("This creates a PHP application under ~/webapps/&lt;app_name&gt;<br>"
"PHP-FPM will be used to execute PHP files.")
icon = 'orchestra/icons/apps/PHPFPM.png'
form = PHPFPMAppForm
serializer = PHPFPMAppSerializer
def get_directive(self): def get_directive(self):
context = self.get_directive_context() context = self.get_directive_context()
socket_type = 'unix' if self.is_fpm:
if ':' in self.fpm_listen: socket_type = 'unix'
socket_type = 'tcp' if ':' in self.FPM_LISTEN:
socket = self.fpm_listen % context socket_type = 'tcp'
return ('fpm', socket_type, socket, self.instance.get_path()) socket = self.FPM_LISTEN % context
return ('fpm', socket_type, socket, self.instance.get_path())
elif self.is_fcgid:
class PHPFCGIDAppForm(PluginDataForm): wrapper_path = os.path.normpath(self.FCGID_WRAPPER_PATH % context)
php_version = forms.ChoiceField(label=_("PHP version"), return ('fcgid', self.instance.get_path(), wrapper_path)
choices=settings.WEBAPPS_PHP_FCGID_VERSIONS, else:
initial=settings.WEBAPPS_PHP_FCGID_DEFAULT_VERSION, raise ValueError("Unknown directive for php version '%s'" % php_version)
help_text=help_message)
class PHPFCGIDAppSerializer(serializers.Serializer):
php_version = serializers.ChoiceField(label=_("PHP version"),
choices=settings.WEBAPPS_PHP_FCGID_VERSIONS,
default=settings.WEBAPPS_PHP_FCGID_DEFAULT_VERSION,
help_text=help_message)
class PHPFCGIDApp(PHPAppType):
name = 'php-fcgid'
php_execution = PHPAppType.FCGID
verbose_name = "PHP FCGID"
help_text = _("This creates a PHP application under ~/webapps/&lt;app_name&gt;<br>"
"Apache-mod-fcgid will be used to execute PHP files.")
icon = 'orchestra/icons/apps/PHPFCGI.png'
form = PHPFCGIDAppForm
serializer = PHPFCGIDAppSerializer
def get_directive(self): def get_php_version(self):
context = self.get_directive_context() default_version = self.DEFAULT_PHP_VERSION
wrapper_path = os.path.normpath(settings.WEBAPPS_FCGID_PATH % context) return self.instance.data.get('php_version', default_version)
return ('fcgid', self.instance.get_path(), wrapper_path)
def get_php_binary_path(self): def get_php_version_number(self):
default_version = settings.WEBAPPS_PHP_FCGID_DEFAULT_VERSION php_version = self.get_php_version()
context = { number = re.findall(r'[0-9]+\.?[0-9]+', php_version)
'php_version': self.instance.data.get('php_version', default_version) if len(number) > 1:
} raise ValueError("Multiple version number matches for '%'" % php_version)
return os.path.normpath(settings.WEBAPPS_PHP_CGI_BINARY_PATH % context) return number[0]
def get_php_rc_path(self):
default_version = settings.WEBAPPS_PHP_FCGID_DEFAULT_VERSION
context = {
'php_version': self.instance.data.get('php_version', default_version)
}
return os.path.normpath(settings.WEBAPPS_PHP_CGI_RC_PATH % context)

View File

@ -9,11 +9,10 @@ from orchestra.utils.python import random_ascii
from .. import settings from .. import settings
from .php import (PHPAppType, PHPFCGIDApp, PHPFPMApp, PHPFCGIDAppForm, PHPFCGIDAppSerializer, from .php import PHPApp, PHPAppForm, PHPAppSerializer
PHPFPMAppForm, PHPFPMAppSerializer)
class WordPressAbstractAppForm(PluginDataForm): class WordPressAppForm(PHPAppForm):
db_name = forms.CharField(label=_("Database name"), db_name = forms.CharField(label=_("Database name"),
help_text=_("Database used for this webapp.")) help_text=_("Database used for this webapp."))
db_user = forms.CharField(label=_("Database user"),) db_user = forms.CharField(label=_("Database user"),)
@ -21,16 +20,20 @@ class WordPressAbstractAppForm(PluginDataForm):
help_text=_("Initial database password.")) help_text=_("Initial database password."))
class WordPressAbstractAppSerializer(serializers.Serializer): class WordPressAppSerializer(PHPAppSerializer):
db_name = serializers.CharField(label=_("Database name"), required=False) db_name = serializers.CharField(label=_("Database name"), required=False)
db_user = serializers.CharField(label=_("Database user"), required=False) db_user = serializers.CharField(label=_("Database user"), required=False)
db_pass = serializers.CharField(label=_("Database user password"), required=False) db_pass = serializers.CharField(label=_("Database user password"), required=False)
class WordPressAbstractApp(object): class WordPressApp(PHPApp):
icon = 'orchestra/icons/apps/WordPress.png' name = 'wordpress'
verbose_name = "WordPress"
serializer = WordPressAppSerializer
change_form = WordPressAppForm
change_readonly_fileds = ('db_name', 'db_user', 'db_pass',) change_readonly_fileds = ('db_name', 'db_user', 'db_pass',)
help_text = _("Visit http://&lt;domain.lan&gt;/wp-admin/install.php to finish the installation.") help_text = _("Visit http://&lt;domain.lan&gt;/wp-admin/install.php to finish the installation.")
icon = 'orchestra/icons/apps/WordPress.png'
def get_db_name(self): def get_db_name(self):
db_name = 'wp_%s_%s' % (self.instance.name, self.instance.account) db_name = 'wp_%s_%s' % (self.instance.name, self.instance.account)
@ -46,7 +49,7 @@ class WordPressAbstractApp(object):
return random_ascii(10) return random_ascii(10)
def validate(self): def validate(self):
super(WordPressAbstractApp, self).validate() super(WordPressApp, self).validate()
create = not self.instance.pk create = not self.instance.pk
if create: if create:
db = Database(name=self.get_db_name(), account=self.instance.account) db = Database(name=self.get_db_name(), account=self.instance.account)
@ -79,7 +82,7 @@ class WordPressAbstractApp(object):
else: else:
# Trigger related backends # Trigger related backends
for related in self.get_related(): for related in self.get_related():
related.save() related.save(updated_fields=[])
def delete(self): def delete(self):
for related in self.get_related(): for related in self.get_related():
@ -101,23 +104,3 @@ class WordPressAbstractApp(object):
else: else:
related.append(db) related.append(db)
return related return related
class WordPressFPMApp(WordPressAbstractApp, PHPFPMApp):
name = 'wordpress-fpm'
php_execution = PHPAppType.FPM
verbose_name = "WordPress (FPM)"
serializer = type('WordPressFPMSerializer',
(WordPressAbstractAppSerializer, PHPFPMAppSerializer), {})
change_form = type('WordPressFPMForm',
(WordPressAbstractAppForm, PHPFPMAppForm), {})
class WordPressFCGIDApp(WordPressAbstractApp, PHPFCGIDApp):
name = 'wordpress-fcgid'
php_execution = PHPAppType.FCGID
verbose_name = "WordPress (FCGID)"
serializer = type('WordPressFCGIDSerializer',
(WordPressAbstractAppSerializer, PHPFCGIDAppSerializer), {})
change_form = type('WordPressFCGIDForm',
(WordPressAbstractAppForm, PHPFCGIDAppForm), {})

View File

@ -164,11 +164,11 @@ class Apache2Backend(ServiceController):
for rules in directives.get('sec_rule_remove', []): for rules in directives.get('sec_rule_remove', []):
for rule in rules.value.split(): for rule in rules.value.split():
config += "SecRuleRemoveById %i\n" % int(rule) config += "SecRuleRemoveById %i\n" % int(rule)
for modsecurity in directives.get('sec_rule_off', []): for modsecurity in directives.get('sec_engine', []):
config += textwrap.dedent("""\ config += textwrap.dedent("""\
<Location %s> <Location %s>
SecRuleEngine off SecRuleEngine off
</LocationMatch> </Location>
""") % modsecurity """) % modsecurity
return config return config

View File

@ -11,6 +11,7 @@ from .. import settings
class WebalizerBackend(ServiceController): class WebalizerBackend(ServiceController):
verbose_name = _("Webalizer Content") verbose_name = _("Webalizer Content")
model = 'websites.Content' model = 'websites.Content'
default_route_match = "content.webapp.type == 'webalizer'"
def save(self, content): def save(self, content):
context = self.get_context(content) context = self.get_context(content)

View File

@ -149,5 +149,5 @@ class SecEngine(SiteDirective):
name = 'sec_engine' name = 'sec_engine'
verbose_name = _("Modsecurity engine") verbose_name = _("Modsecurity engine")
help_text = _("URL location for disabling modsecurity engine.") help_text = _("URL location for disabling modsecurity engine.")
regex = r'^[^ ]+$' regex = r'^/[^ ]*$'
group = SiteDirective.SEC group = SiteDirective.SEC

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,63 +1,203 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <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:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" 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"
width="48px"
height="48px"
id="svg3109"
version="1.1" version="1.1"
width="300" inkscape:version="0.48.3.1 r9886"
height="159" sodipodi:docname="PHP.svg"
viewBox="0 0 300 160" inkscape:export-filename="/home/glic3rinu/orchestra/django-orchestra/orchestra/static/orchestra/icons/apps/PHP.png"
id="svg2943"> inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs <defs
id="defs2945"> id="defs3111">
<linearGradient <linearGradient
x1="150" inkscape:collect="always"
y1="84" xlink:href="#linearGradient4350"
x2="299" id="linearGradient2623"
y2="84" gradientUnits="userSpaceOnUse"
id="linearGradient3798" gradientTransform="matrix(0.972701,0,0,0.925949,-23.47387,-13.25574)"
gradientUnits="userSpaceOnUse"> x1="97.728783"
y1="54.517036"
x2="97.728783"
y2="42.400635" />
<linearGradient
id="linearGradient4350"
inkscape:collect="always">
<stop <stop
id="stop3800" id="stop4352"
style="stop-color:#dddce9;stop-opacity:1" offset="0"
offset="0" /> style="stop-color:#ffffff;stop-opacity:1;" />
<stop <stop
id="stop3802" id="stop4354"
style="stop-color:#5664a3;stop-opacity:1" offset="1"
offset="0.37" /> style="stop-color:#ffffff;stop-opacity:0;" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4286"
id="linearGradient2625"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-19,0.07465812)"
x1="88.75"
y1="22.673088"
x2="87.8125"
y2="45.579079" />
<linearGradient
id="linearGradient4286">
<stop <stop
id="stop3804" style="stop-color:#ffffff;stop-opacity:1;"
style="stop-color:#000000;stop-opacity:1" offset="0"
offset="1" /> id="stop4288" />
<stop
style="stop-color:#ffffff;stop-opacity:0.30508474;"
offset="1"
id="stop4290" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4350"
id="linearGradient2627"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-26,-16.91315)"
x1="95.03125"
y1="57.906303"
x2="95.03125"
y2="44.592937" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient4114"
id="radialGradient17299"
gradientUnits="userSpaceOnUse"
gradientTransform="scale(1.64399,0.608276)"
cx="15.115514"
cy="63.965389"
fx="15.115514"
fy="63.965389"
r="12.289036" />
<linearGradient
id="linearGradient4114"
inkscape:collect="always">
<stop
id="stop4116"
offset="0"
style="stop-color:#000000;stop-opacity:1;" />
<stop
id="stop4118"
offset="1"
style="stop-color:#000000;stop-opacity:0;" />
</linearGradient> </linearGradient>
<radialGradient <radialGradient
cx="77.914261" r="12.289036"
cy="-48.544521" fy="63.965389"
r="146" fx="15.115514"
fx="77.914261" cy="63.965389"
fy="-48.544521" cx="15.115514"
id="radialGradient3870" gradientTransform="scale(1.64399,0.608276)"
xlink:href="#linearGradient3798"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.5089497,0,0,1.3582164,-39.028917,76.957747)" /> id="radialGradient3107"
xlink:href="#linearGradient4114"
inkscape:collect="always" />
</defs> </defs>
<ellipse <sodipodi:namedview
cx="150" id="base"
cy="80" pagecolor="#ffffff"
rx="146" bordercolor="#666666"
ry="76" borderopacity="1.0"
id="ellipse3860" inkscape:pageopacity="0.0"
style="fill:#6c7eb7;stroke:url(#radialGradient3870);stroke-width:5.5" /> inkscape:pageshadow="2"
<path inkscape:zoom="7"
d="m 45,125 16,-81 37,0 c 16,1 24,9 24,23 0,24 -19,38 -36,37 l -18,0 -4,21 -19,0 z m 27,-36 5,-30 13,0 c 7,0 12,3 12,9 -1,17 -9,20 -18,21 l -12,0 z" inkscape:cx="24"
id="p" inkscape:cy="24"
style="fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linejoin:round" /> inkscape:current-layer="layer1"
<path showgrid="true"
d="m 116,104 16,-81 19,0 -4,21 18,0 c 16,1 22,9 20,19 l -7,41 -20,0 7,-37 c 1,-5 1,-8 -6,-8 l -15,0 -9,45 -19,0 z" inkscape:grid-bbox="true"
id="h" inkscape:document-units="px"
style="stroke:#ffffff;stroke-width:2;stroke-linejoin:round" /> inkscape:window-width="1920"
<use inkscape:window-height="1024"
transform="translate(134,0)" inkscape:window-x="0"
id="p2" inkscape:window-y="27"
xlink:href="#p" /> inkscape:window-maximized="1" />
</svg> <metadata
id="metadata3114">
<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></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<path
transform="matrix(0.99462358,0,0,1.2365616,-1.8004407,-14.308795)"
d="m 45.052803,38.908627 a 20.203051,7.4751287 0 1 1 -40.4061012,0 20.203051,7.4751287 0 1 1 40.4061012,0 z"
sodipodi:ry="7.4751287"
sodipodi:rx="20.203051"
sodipodi:cy="38.908627"
sodipodi:cx="24.849752"
id="path4112"
style="opacity:0.83257919;fill:url(#radialGradient3107);fill-opacity:1;stroke:none;display:inline"
sodipodi:type="arc" />
<g
id="g2615"
transform="matrix(1.4117127,0,0,1.4117127,-76.234629,-24.714637)">
<path
transform="matrix(0.945073,0,0,0.905454,-21.13524,5.644298)"
d="m 114.99324,33.06237 c 0,5.516156 -7.83542,9.987884 -17.500892,9.987884 -9.665476,0 -17.500893,-4.471728 -17.500893,-9.987884 0,-5.516155 7.835417,-9.987883 17.500893,-9.987883 9.665472,0 17.500892,4.471728 17.500892,9.987883 z"
sodipodi:ry="9.9878836"
sodipodi:rx="17.500893"
sodipodi:cy="33.06237"
sodipodi:cx="97.492348"
id="path19716"
style="fill:#5d65b4;fill-opacity:1;fill-rule:nonzero;stroke:#3d4384;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
sodipodi:type="arc" />
<path
sodipodi:type="arc"
style="fill:none;stroke:#9ca1d2;stroke-width:1.17854095;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
id="path19718"
sodipodi:cx="97.492348"
sodipodi:cy="33.06237"
sodipodi:rx="17.500893"
sodipodi:ry="9.9878836"
d="m 114.99324,33.06237 c 0,5.516156 -7.83542,9.987884 -17.500892,9.987884 -9.665476,0 -17.500893,-4.471728 -17.500893,-9.987884 0,-5.516155 7.835417,-9.987883 17.500893,-9.987883 9.665472,0 17.500892,4.471728 17.500892,9.987883 z"
transform="matrix(0.889513,0,0,0.809391,-15.71799,8.823718)" />
<path
inkscape:connector-curvature="0"
sodipodi:nodetypes="cscsscsc"
id="path19720"
d="m 70.665432,27.000328 c -8.770642,0.06413 -15.490098,3.890307 -15.734387,8.371883 -0.05012,0.919501 0.159559,1.78858 0.687905,2.617543 3.996698,-3.732848 10.440129,-2.778994 15.745611,-2.170193 5.324581,0.610992 11.669454,0.972529 15.313098,-2.145164 0.0072,-0.0062 -0.02725,-0.039 -0.01953,-0.04456 -1.516063,-3.804519 -8.019325,-6.629508 -15.776011,-6.629508 -0.06938,0 -0.147511,-4.57e-4 -0.216684,0 z"
style="opacity:0.55813952;fill:url(#linearGradient2623);fill-opacity:1;fill-rule:evenodd;stroke:none" />
<path
inkscape:connector-curvature="0"
id="path19724"
d="m 69.156251,29.324658 -1,5 c 0.08862,-1.599006 -1.095832,-2.75 -2.28125,-2.75 l -4.125,0 -1.625,8.46875 2.03125,0.03125 0.4375,-2.1875 2.1875,0 c 1.191119,0 2.622044,-0.936537 3.1875,-2.625 l -0.5,2.625 2.03125,0 0.90625,-4.75 1.28125,0 c 0.255223,0.002 0.466944,0.03366 0.625,0.09375 0.02953,0.01215 0.06869,0.04716 0.09375,0.0625 0.0048,0.0032 0.02661,-0.0033 0.03125,0 0.0044,0.0035 0.02699,0.02763 0.03125,0.03125 0.0041,0.0038 0.02738,0.02734 0.03125,0.03125 0.0035,0.0042 0.02799,0.02688 0.03125,0.03125 0.0031,0.0045 -0.0029,0.02656 0,0.03125 0.0026,0.0048 0.02882,0.02623 0.03125,0.03125 0.0022,0.0052 -0.002,0.0259 0,0.03125 0.0054,0.01657 0.02789,0.04433 0.03125,0.0625 0.0018,0.01248 -8.72e-4,0.04928 0,0.0625 4.07e-4,0.0136 5.36e-4,0.04812 0,0.0625 -10e-4,0.01477 0.002,0.04693 0,0.0625 -0.0037,0.02398 -0.02529,0.06788 -0.03125,0.09375 l -0.75,4.0625 2.0625,0 0.75,-4.09375 c 0.152547,-0.791704 -0.0036,-1.314355 -0.3125,-1.65625 -0.469323,-0.499058 -1.284638,-0.613618 -1.9375,-0.625 l -1.6875,0 0.4375,-2.1875 -1.96875,0 z m 6.4375,2.25 -1.625,8.46875 2.03125,0.03125 0.40625,-2.1875 2.21875,0 c 1.270525,0 2.80633,-1.058815 3.28125,-2.96875 0.47492,-1.909935 -0.8565,-3.34375 -2.1875,-3.34375 l -4.125,0 z m -12.125,1.53125 1.25,0 c 0.294812,10e-7 0.560277,0.04782 0.75,0.125 0.0352,0.01535 0.09356,0.04457 0.125,0.0625 0.03813,0.0232 0.09271,0.06668 0.125,0.09375 0.01618,0.01498 0.04775,0.04638 0.0625,0.0625 0.0048,0.0055 0.02665,0.02563 0.03125,0.03125 0.0044,0.0057 0.02696,0.02538 0.03125,0.03125 0.04136,0.05994 0.06748,0.146827 0.09375,0.21875 0.005,0.01462 0.02687,0.04742 0.03125,0.0625 0.0838,0.313843 0.04097,0.733356 -0.09375,1.21875 -0.08568,0.308687 -0.200401,0.565156 -0.34375,0.75 -0.01212,0.01502 -0.05004,0.04823 -0.0625,0.0625 -0.02526,0.02781 -0.0672,0.06882 -0.09375,0.09375 -0.01285,0.01124 -0.04926,0.05182 -0.0625,0.0625 -0.402786,0.31197 -0.983107,0.375 -1.75,0.375 -1.268714,-10e-7 -0.71875,0 -0.71875,0 l 0.625,-3.25 z m 13.84375,0 1.25,0 c 0.449239,10e-7 0.793344,0.107982 1,0.28125 0.01618,0.01498 0.04775,0.04638 0.0625,0.0625 0.05235,0.06048 0.0912,0.143519 0.125,0.21875 0.0058,0.01391 0.026,0.04812 0.03125,0.0625 0.005,0.01462 0.02687,0.04742 0.03125,0.0625 0.0838,0.313843 0.04097,0.733356 -0.09375,1.21875 -0.09638,0.347273 -0.23803,0.619815 -0.40625,0.8125 -0.01263,0.0139 -0.04954,0.04932 -0.0625,0.0625 -0.0066,0.0064 -0.02461,0.02502 -0.03125,0.03125 -0.01899,0.01729 -0.04265,0.04649 -0.0625,0.0625 -0.402785,0.31197 -0.983106,0.375 -1.75,0.375 -1.268712,-10e-7 -0.71875,0 -0.71875,0 l 0.625,-3.25 z"
style="opacity:0.69957084;fill:none;stroke:url(#linearGradient2625);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
inkscape:connector-curvature="0"
id="path19732"
d="m 69.156251,29.324658 -1,5 c 0.08862,-1.599006 -1.095832,-2.75 -2.28125,-2.75 l -4.125,0 -1.625,8.46875 2.03125,0.03125 0.4375,-2.1875 2.1875,0 c 1.191119,0 2.622044,-0.936537 3.1875,-2.625 l -0.5,2.625 2.03125,0 0.90625,-4.75 1.28125,0 c 0.255223,0.002 0.466944,0.03366 0.625,0.09375 0.02953,0.01215 0.06869,0.04716 0.09375,0.0625 0.0048,0.0032 0.02661,-0.0033 0.03125,0 0.0044,0.0035 0.02699,0.02763 0.03125,0.03125 0.0041,0.0038 0.02738,0.02734 0.03125,0.03125 0.0035,0.0042 0.02799,0.02688 0.03125,0.03125 0.0031,0.0045 -0.0029,0.02656 0,0.03125 0.0026,0.0048 0.02882,0.02623 0.03125,0.03125 0.0022,0.0052 -0.002,0.0259 0,0.03125 0.0054,0.01657 0.02789,0.04433 0.03125,0.0625 0.0018,0.01248 -8.72e-4,0.04928 0,0.0625 4.07e-4,0.0136 5.36e-4,0.04812 0,0.0625 -10e-4,0.01477 0.002,0.04693 0,0.0625 -0.0037,0.02398 -0.02529,0.06788 -0.03125,0.09375 l -0.75,4.0625 2.0625,0 0.75,-4.09375 c 0.152547,-0.791704 -0.0036,-1.314355 -0.3125,-1.65625 -0.469323,-0.499058 -1.284638,-0.613618 -1.9375,-0.625 l -1.6875,0 0.4375,-2.1875 -1.96875,0 z m 6.4375,2.25 -1.625,8.46875 2.03125,0.03125 0.40625,-2.1875 2.21875,0 c 1.270525,0 2.80633,-1.058815 3.28125,-2.96875 0.47492,-1.909935 -0.8565,-3.34375 -2.1875,-3.34375 l -4.125,0 z m -12.125,1.53125 1.25,0 c 0.294812,10e-7 0.560277,0.04782 0.75,0.125 0.0352,0.01535 0.09356,0.04457 0.125,0.0625 0.03813,0.0232 0.09271,0.06668 0.125,0.09375 0.01618,0.01498 0.04775,0.04638 0.0625,0.0625 0.0048,0.0055 0.02665,0.02563 0.03125,0.03125 0.0044,0.0057 0.02696,0.02538 0.03125,0.03125 0.04136,0.05994 0.06748,0.146827 0.09375,0.21875 0.005,0.01462 0.02687,0.04742 0.03125,0.0625 0.0838,0.313843 0.04097,0.733356 -0.09375,1.21875 -0.08568,0.308687 -0.200401,0.565156 -0.34375,0.75 -0.01212,0.01502 -0.05004,0.04823 -0.0625,0.0625 -0.02526,0.02781 -0.0672,0.06882 -0.09375,0.09375 -0.01285,0.01124 -0.04926,0.05182 -0.0625,0.0625 -0.402786,0.31197 -0.983107,0.375 -1.75,0.375 -1.268714,-10e-7 -0.71875,0 -0.71875,0 l 0.625,-3.25 z m 13.84375,0 1.25,0 c 0.449239,10e-7 0.793344,0.107982 1,0.28125 0.01618,0.01498 0.04775,0.04638 0.0625,0.0625 0.05235,0.06048 0.0912,0.143519 0.125,0.21875 0.0058,0.01391 0.026,0.04812 0.03125,0.0625 0.005,0.01462 0.02687,0.04742 0.03125,0.0625 0.0838,0.313843 0.04097,0.733356 -0.09375,1.21875 -0.09638,0.347273 -0.23803,0.619815 -0.40625,0.8125 -0.01263,0.0139 -0.04954,0.04932 -0.0625,0.0625 -0.0066,0.0064 -0.02461,0.02502 -0.03125,0.03125 -0.01899,0.01729 -0.04265,0.04649 -0.0625,0.0625 -0.402785,0.31197 -0.983106,0.375 -1.75,0.375 -1.268712,-10e-7 -0.71875,0 -0.71875,0 l 0.625,-3.25 z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none" />
<path
inkscape:connector-curvature="0"
id="path19738"
d="m 69.156249,29.336846 -1,4.90625 c 0.04837,-1.559124 -1.114354,-2.65625 -2.28125,-2.65625 l -4.125,0 -0.78125,4.125 c 0.665889,-0.113935 1.33972,-0.182287 2.03125,-0.21875 l 0.46875,-2.375 1.25,0 c 0.292618,-1e-6 0.555977,0.04984 0.75,0.125 0.02468,0.01021 0.07098,0.01981 0.09375,0.03125 0.02949,0.0158 0.0678,0.04454 0.09375,0.0625 0.011,0.0051 0.05173,0.02597 0.0625,0.03125 0.0062,0.0055 0.02536,0.02535 0.03125,0.03125 0.0056,0.0062 0.02594,0.02473 0.03125,0.03125 0.02035,0.02685 0.04389,0.06678 0.0625,0.09375 0.0055,0.01088 0.02595,0.0515 0.03125,0.0625 0.007,0.01296 0.02481,0.04906 0.03125,0.0625 0.0057,0.0054 0.0256,0.02581 0.03125,0.03125 0.0083,0.02123 0.02425,0.07148 0.03125,0.09375 0.09193,0.316681 0.04426,0.752767 -0.09375,1.25 -0.04909,0.176853 -0.118279,0.334053 -0.1875,0.46875 0.715226,0.03477 1.41411,0.08707 2.125,0.15625 0.04337,-0.09783 0.08799,-0.177253 0.125,-0.28125 l -0.0625,0.28125 c 0.672502,0.0661 1.343624,0.171135 2,0.25 l 0.53125,-2.75 1.28125,0 c 0.255223,0.002 0.466944,0.03366 0.625,0.09375 0.0028,-8.5e-5 0.02807,3.12e-4 0.03125,0 0.01111,0.0054 0.05211,0.02538 0.0625,0.03125 0.0056,0.0056 0.02571,0.02558 0.03125,0.03125 0.01109,0.01048 0.05219,0.05143 0.0625,0.0625 0.0056,0.0052 0.0259,0.02586 0.03125,0.03125 0.0084,0.01591 0.02626,0.05084 0.03125,0.0625 -2.37e-4,0.0032 7.3e-5,0.02848 0,0.03125 0.0058,0.0055 0.02551,0.02573 0.03125,0.03125 2e-6,0.01085 2.25e-4,0.05147 0,0.0625 -8e-5,0.0061 -1.02e-4,0.02785 0,0.03125 -9.3e-5,0.02098 0.0022,0.07099 0,0.09375 -9.26e-4,0.0035 0.0013,0.02729 0,0.03125 -0.0051,0.01302 -0.02785,0.05141 -0.03125,0.0625 l -0.4375,2.34375 c 0.670609,0.07685 1.341434,0.159629 2.03125,0.21875 l 0.46875,-2.5625 c 0.11067,-0.574373 0.06008,-1.019277 -0.09375,-1.34375 -0.04739,-0.09474 -0.12356,-0.204443 -0.1875,-0.28125 -0.02374,-0.02737 -0.06813,-0.06856 -0.09375,-0.09375 -0.0098,-0.0092 -0.04608,-0.04615 -0.0625,-0.0625 -0.0075,-0.0063 -0.02365,-0.02509 -0.03125,-0.03125 -0.478833,-0.372345 -1.198721,-0.458593 -1.78125,-0.46875 l -1.6875,0 0.4375,-2.1875 -1.96875,0 z m 6.4375,2.25 -0.9375,4.8125 c 2.24771,0.171748 4.53955,0.198619 6.656252,-0.125 0.251,-0.380091 0.4676,-0.836424 0.59375,-1.34375 0.47492,-1.909933 -0.8565,-3.34375 -2.187502,-3.34375 l -4.125,0 z m 1.71875,1.53125 1.25,0 c 0.28077,-1e-6 0.53335,0.02353 0.71875,0.09375 0.03546,0.01592 0.09667,0.04737 0.125,0.0625 0.02358,0.01345 0.07228,0.04764 0.09375,0.0625 0.011,0.0051 0.05173,0.02597 0.0625,0.03125 0.003,0.0035 0.0286,0.02736 0.03125,0.03125 0.0069,0.01272 0.02336,0.05102 0.03125,0.0625 0.0056,0.0055 0.02569,0.02573 0.03125,0.03125 0.0056,0.0055 0.02567,0.02575 0.03125,0.03125 0.0055,0.01088 0.02595,0.0515 0.03125,0.0625 0.007,0.01296 0.02481,0.04906 0.03125,0.0625 0.14748,0.328282 0.12647,0.806734 -0.03125,1.375 -0.07853,0.282964 -0.18499,0.509551 -0.3125,0.6875 -0.0331,0.0444 -0.08385,0.109473 -0.125,0.15625 -0.01624,0.01759 -0.04756,0.04847 -0.0625,0.0625 -0.0063,0.0058 -0.02483,0.02563 -0.03125,0.03125 -0.40372,0.339333 -0.98879,0.40625 -1.78125,0.40625 -1.26871,10e-7 -0.71875,0 -0.71875,0 l 0.625,-3.25 z"
style="opacity:0.12217198;fill:url(#linearGradient2627);fill-opacity:1;fill-rule:evenodd;stroke:none" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -64,7 +64,7 @@ def naturaldatetime(date, include_seconds=False):
if days == 0: if days == 0:
if hours == 0: if hours == 0:
if minutes > 0: if minutes > 0:
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
@ -77,7 +77,7 @@ def naturaldatetime(date, include_seconds=False):
).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