diff --git a/TODO.md b/TODO.md
index d0b87b3d..81ba8821 100644
--- a/TODO.md
+++ b/TODO.md
@@ -166,7 +166,7 @@
* webapp compat webapp-options
* webapps modeled on classes instead of settings?
-* Change account and orders
+* Service.account change and orders consistency
* Mix webapps type with backends (two for the price of one)
@@ -181,13 +181,10 @@ Multi-tenant WebApps
* Howto upgrade webapp PHP version? SetHandler php54-cgi ? or create a new app
-* prevent @pangea.org email addresses on contacts
-
-
-
+* prevent @pangea.org email addresses on contacts, enforce at least one email without @pangea.org
* fcgid kill instead of apache reload?
-
-* chomod user:group
* username maximum as group user in UNIX
+
+* forms autocomplete="off"
diff --git a/orchestra/admin/actions.py b/orchestra/admin/actions.py
index 1f5a32d1..7d5e00de 100644
--- a/orchestra/admin/actions.py
+++ b/orchestra/admin/actions.py
@@ -100,7 +100,7 @@ class SendEmail(object):
'content_message': _(
"Are you sure you want to send the following message to the following %s?"
) % self.opts.verbose_name_plural,
- 'display_objects': ["%s (%s)" % (contact, contact.email) for contact in self.queryset],
+ 'display_objects': [u"%s (%s)" % (contact, contact.email) for contact in self.queryset],
'form': form,
'subject': subject,
'message': message,
diff --git a/orchestra/api/options.py b/orchestra/api/options.py
index d94e0bd7..64fb911e 100644
--- a/orchestra/api/options.py
+++ b/orchestra/api/options.py
@@ -1,82 +1,15 @@
+from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.module_loading import autodiscover_modules
from rest_framework.routers import DefaultRouter, Route, flatten, replace_methodname
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
-def collectionlink(**kwargs):
- """
- Used to mark a method on a ViewSet collection that should be routed for GET requests.
- """
- # TODO deprecate in favour of DRF2.0 own method
- def decorator(func):
- func.collection_bind_to_methods = ['get']
- func.kwargs = kwargs
- return func
- return decorator
-
-
class LinkHeaderRouter(DefaultRouter):
- def __init__(self, *args, **kwargs):
- """ collection view method route """
- super(LinkHeaderRouter, self).__init__(*args, **kwargs)
- self.routes.insert(0, Route(
- url=r'^{prefix}/{collectionmethodname}{trailing_slash}$',
- mapping={
- '{httpmethod}': '{collectionmethodname}',
- },
- name='{basename}-{methodnamehyphen}',
- initkwargs={}
- ))
-
- def get_routes(self, viewset):
- """ allow links and actions to be bound to a collection view """
- known_actions = flatten([route.mapping.values() for route in self.routes])
- dynamic_routes = []
- collection_dynamic_routes = []
- for methodname in dir(viewset):
- attr = getattr(viewset, methodname)
- bind = getattr(attr, 'bind_to_methods', None)
- httpmethods = getattr(attr, 'collection_bind_to_methods', bind)
- if httpmethods:
- if methodname in known_actions:
- msg = ('Cannot use @action or @link decorator on method "%s" '
- 'as it is an existing route' % methodname)
- raise ImproperlyConfigured(msg)
- httpmethods = [method.lower() for method in httpmethods]
- if bind:
- dynamic_routes.append((httpmethods, methodname))
- else:
- collection_dynamic_routes.append((httpmethods, methodname))
-
- ret = []
- for route in self.routes:
- # Dynamic routes (@link or @action decorator)
- if route.mapping == {'{httpmethod}': '{methodname}'}:
- replace = replace_methodname
- routes = dynamic_routes
- elif route.mapping == {'{httpmethod}': '{collectionmethodname}'}:
- replace = replace_collectionmethodname
- routes = collection_dynamic_routes
- else:
- ret.append(route)
- continue
- for httpmethods, methodname in routes:
- initkwargs = route.initkwargs.copy()
- initkwargs.update(getattr(viewset, methodname).kwargs)
- ret.append(Route(
- url=replace(route.url, methodname),
- mapping={ httpmethod: methodname for httpmethod in httpmethods },
- name=replace(route.name, methodname),
- initkwargs=initkwargs,
- ))
- return ret
-
def get_api_root_view(self):
""" returns the root view, with all the linked collections """
APIRoot = import_class(settings.API_ROOT_VIEW)
@@ -110,6 +43,6 @@ class LinkHeaderRouter(DefaultRouter):
# Create a router and register our viewsets with it.
-router = LinkHeaderRouter()
+router = LinkHeaderRouter(trailing_slash=django_settings.APPEND_SLASH)
autodiscover = lambda: (autodiscover_modules('api'), autodiscover_modules('serializers'))
diff --git a/orchestra/api/root.py b/orchestra/api/root.py
index 977d4f08..5517fe9e 100644
--- a/orchestra/api/root.py
+++ b/orchestra/api/root.py
@@ -17,8 +17,8 @@ class APIRoot(views.APIView):
'<%s>; rel="%s"' % (token_url, 'api-get-auth-token'),
]
body = {
- 'accountancy': [],
- 'services': [],
+ 'accountancy': {},
+ 'services': {},
}
if not request.user.is_anonymous():
list_name = '{basename}-list'
@@ -44,12 +44,11 @@ class APIRoot(views.APIView):
group = 'accountancy'
menu = accounts[model].menu
if group and menu:
- body[group].append({
+ body[group][basename] = {
'url': url,
- 'name': basename,
'verbose_name': model._meta.verbose_name,
'verbose_name_plural': model._meta.verbose_name_plural,
- })
+ }
headers = {
'Link': ', '.join(links)
}
diff --git a/orchestra/apps/bills/api.py b/orchestra/apps/bills/api.py
index c195a10d..f7f6054c 100644
--- a/orchestra/apps/bills/api.py
+++ b/orchestra/apps/bills/api.py
@@ -1,12 +1,29 @@
+from django.http import HttpResponse
from rest_framework import viewsets
+from rest_framework.decorators import detail_route
from orchestra.api import router
from orchestra.apps.accounts.api import AccountApiMixin
+from orchestra.utils.html import html_to_pdf
from .models import Bill
from .serializers import BillSerializer
+
class BillViewSet(AccountApiMixin, viewsets.ModelViewSet):
model = Bill
serializer_class = BillSerializer
+
+ @detail_route(methods=['get'])
+ def document(self, request, pk):
+ bill = self.get_object()
+ content_type = request.META.get('HTTP_ACCEPT')
+ if content_type == 'application/pdf':
+ pdf = html_to_pdf(bill.html or bill.render())
+ return HttpResponse(pdf, content_type='application/pdf')
+ else:
+ return HttpResponse(bill.html or bill.render())
+
+
+router.register('bills', BillViewSet)
diff --git a/orchestra/apps/bills/serializers.py b/orchestra/apps/bills/serializers.py
index ec53df4c..b4dc2548 100644
--- a/orchestra/apps/bills/serializers.py
+++ b/orchestra/apps/bills/serializers.py
@@ -14,13 +14,14 @@ class BillLineSerializer(serializers.HyperlinkedModelSerializer):
class BillSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
- lines = BillLineSerializer(source='billlines')
+# lines = BillLineSerializer(source='lines')
class Meta:
model = Bill
fields = (
'url', 'number', 'type', 'total', 'is_sent', 'created_on', 'due_on',
- 'comments', 'html', 'lines'
+ 'comments',
+# 'lines'
)
diff --git a/orchestra/apps/databases/backends.py b/orchestra/apps/databases/backends.py
index 5a40d4f7..acdf8ac2 100644
--- a/orchestra/apps/databases/backends.py
+++ b/orchestra/apps/databases/backends.py
@@ -38,6 +38,7 @@ class MySQLBackend(ServiceController):
return
context = self.get_context(database)
self.append("mysql -e 'DROP DATABASE `%(database)s`;'" % context)
+ self.append("mysql mysql -e 'DELETE FROM db WHERE db = `%(database)s`;'" % context)
def commit(self):
self.append("mysql -e 'FLUSH PRIVILEGES;'")
diff --git a/orchestra/apps/mailboxes/backends.py b/orchestra/apps/mailboxes/backends.py
index acaa541c..96488856 100644
--- a/orchestra/apps/mailboxes/backends.py
+++ b/orchestra/apps/mailboxes/backends.py
@@ -36,7 +36,7 @@ class PasswdVirtualUserBackend(ServiceController):
fi""" % context
))
self.append("mkdir -p %(home)s" % context)
- self.append("chown %(uid)s.%(gid)s %(home)s" % context)
+ self.append("chown %(uid)s:%(gid)s %(home)s" % context)
def set_mailbox(self, context):
self.append(textwrap.dedent("""
diff --git a/orchestra/apps/systemusers/backends.py b/orchestra/apps/systemusers/backends.py
index 4f720994..e8cbf872 100644
--- a/orchestra/apps/systemusers/backends.py
+++ b/orchestra/apps/systemusers/backends.py
@@ -25,7 +25,7 @@ class SystemUserBackend(ServiceController):
useradd %(username)s --home %(home)s --password '%(password)s' --shell %(shell)s %(groups_arg)s
fi
mkdir -p %(home)s
- chown %(username)s.%(username)s %(home)s""" % context
+ chown %(username)s:%(username)s %(home)s""" % context
))
for member in settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS:
context['member'] = member
diff --git a/orchestra/apps/systemusers/models.py b/orchestra/apps/systemusers/models.py
index 0703fe03..029d4d38 100644
--- a/orchestra/apps/systemusers/models.py
+++ b/orchestra/apps/systemusers/models.py
@@ -21,8 +21,13 @@ class SystemUserQuerySet(models.QuerySet):
class SystemUser(models.Model):
- """ System users """
- username = models.CharField(_("username"), max_length=64, unique=True,
+ """
+ System users
+
+ Username max_length determined by min(user, group) on common LINUX systems; min(32, 16)
+ """
+ # TODO max_length
+ username = models.CharField(_("username"), max_length=32, unique=True,
help_text=_("Required. 64 characters or fewer. Letters, digits and ./-/_ only."),
validators=[validators.validate_username])
password = models.CharField(_("password"), max_length=128)
diff --git a/orchestra/apps/webapps/backends/__init__.py b/orchestra/apps/webapps/backends/__init__.py
index 2b3165c1..eb8d92fb 100644
--- a/orchestra/apps/webapps/backends/__init__.py
+++ b/orchestra/apps/webapps/backends/__init__.py
@@ -18,7 +18,7 @@ class WebAppServiceMixin(object):
path="${path}/${dir}"
[ -d $path ] || {
mkdir "${path}"
- chown %(user)s.%(group)s "${path}"
+ chown %(user)s:%(group)s "${path}"
}
done
""" % context))
diff --git a/orchestra/apps/webapps/backends/phpfcgid.py b/orchestra/apps/webapps/backends/phpfcgid.py
index d7c7257c..a811572c 100644
--- a/orchestra/apps/webapps/backends/phpfcgid.py
+++ b/orchestra/apps/webapps/backends/phpfcgid.py
@@ -27,7 +27,7 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController):
echo -e '%(wrapper_content)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)
+ self.append("chown -R %(user)s:%(group)s %(wrapper_dir)s" % context)
def delete(self, webapp):
if not self.valid_directive(webapp):
diff --git a/orchestra/apps/webapps/settings.py b/orchestra/apps/webapps/settings.py
index 8ab5c8b7..2afabc3f 100644
--- a/orchestra/apps/webapps/settings.py
+++ b/orchestra/apps/webapps/settings.py
@@ -124,6 +124,12 @@ WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTIO
WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
# { name: ( verbose_name, [help_text], validation_regex ) }
+ # Filesystem
+ 'public-root': (
+ _("Public root"),
+ _("Document root relative to webapps/<webapp>/"),
+ r'[^ ]+',
+ ),
# Processes
'timeout': (
_("Process timeout"),
@@ -220,6 +226,12 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
"(Integer between 0 and 999)."),
r'^[0-9]{1,3}$'
),
+ 'PHP-max_input_vars': (
+ _("PHP - Max input vars"),
+ _("How many input variables may be accepted (limit is applied to $_GET, $_POST and $_COOKIE superglobal separately) "
+ "(Integer between 0 and 9999)."),
+ r'^[0-9]{1,4}$'
+ ),
'PHP-memory_limit': (
_("PHP - Memory limit"),
_("This sets the maximum amount of memory in bytes that a script is allowed to allocate "
@@ -269,7 +281,12 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
r'^(On|Off|on|off)$'
),
'PHP-suhosin.post.max_vars': (
- _("PHP - Suhosin post max vars"),
+ _("PHP - Suhosin POST max vars"),
+ _("Number between 0 and 9999."),
+ r'^[0-9]{1,4}$'
+ ),
+ 'PHP-suhosin.get.max_vars': (
+ _("PHP - Suhosin GET max vars"),
_("Number between 0 and 9999."),
r'^[0-9]{1,4}$'
),
diff --git a/orchestra/apps/websites/backends/webalizer.py b/orchestra/apps/websites/backends/webalizer.py
index 51e0ba66..13d5a12f 100644
--- a/orchestra/apps/websites/backends/webalizer.py
+++ b/orchestra/apps/websites/backends/webalizer.py
@@ -17,7 +17,7 @@ class WebalizerBackend(ServiceController):
self.append("[[ ! -e %(webalizer_path)s/index.html ]] && "
"echo 'Webstats are coming soon' > %(webalizer_path)s/index.html" % context)
self.append("echo '%(webalizer_conf)s' > %(webalizer_conf_path)s" % context)
- self.append("chown %(user)s.www-data %(webalizer_path)s" % context)
+ self.append("chown %(user)s:www-data %(webalizer_path)s" % context)
def delete(self, content):
context = self.get_context(content)
diff --git a/orchestra/apps/websites/models.py b/orchestra/apps/websites/models.py
index 3b91fd4d..36e4e261 100644
--- a/orchestra/apps/websites/models.py
+++ b/orchestra/apps/websites/models.py
@@ -30,7 +30,7 @@ class Website(models.Model):
@property
def unique_name(self):
- return "%s-%s" % (self.account, self.name)
+ return "%s-%i" % (self.name, self.pk)
@cached
def get_options(self):
diff --git a/orchestra/apps/websites/settings.py b/orchestra/apps/websites/settings.py
index 78065ade..dac9862e 100644
--- a/orchestra/apps/websites/settings.py
+++ b/orchestra/apps/websites/settings.py
@@ -26,8 +26,13 @@ WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', {
),
'redirect': (
_("HTTPD - Redirection"),
- _("[permanent] <website path> <destination URL>"),
- r'^(permanent\s[^ ]+|[^ ]+)\s[^ ]+$',
+ _("<website path> <destination URL>"),
+ r'^[^ ]+\s[^ ]+$',
+ ),
+ 'proxy': (
+ _("HTTPD - Proxy"),
+ _("<website path> <target URL>"),
+ r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$',
),
'ssl_ca': (
"HTTPD - SSL CA",
diff --git a/orchestra/conf/devel_settings.py b/orchestra/conf/devel_settings.py
index 06ae7d07..bba15433 100644
--- a/orchestra/conf/devel_settings.py
+++ b/orchestra/conf/devel_settings.py
@@ -11,7 +11,7 @@ CELERY_SEND_TASK_ERROR_EMAILS = False
# When DEBUG is enabled Django appends every executed SQL statement to django.db.connection.queries
# this will grow unbounded in a long running process environment like celeryd
-if "celeryd" in sys.argv or 'celeryev' in sys.argv or 'celerybeat' in sys.argv:
+if set(('celeryd', 'celeryev', 'celerycam', 'celerybeat')).intersection(sys.argv):
DEBUG = False
# Django debug toolbar
diff --git a/orchestra/templates/rest_framework/api.html b/orchestra/templates/rest_framework/api.html
index 05f93fd5..0773c4a4 100644
--- a/orchestra/templates/rest_framework/api.html
+++ b/orchestra/templates/rest_framework/api.html
@@ -1,5 +1,5 @@
{% extends "rest_framework/base.html" %}
-{% load rest_framework utils %}
+{% load rest_framework utils staticfiles %}
{% block head %}
{{ block.super }}
@@ -17,7 +17,7 @@
{% else %}
diff --git a/orchestra/utils/html.py b/orchestra/utils/html.py
index 746ca973..84079237 100644
--- a/orchestra/utils/html.py
+++ b/orchestra/utils/html.py
@@ -6,6 +6,6 @@ def html_to_pdf(html):
return run(
'PATH=$PATH:/usr/local/bin/\n'
'xvfb-run -a -s "-screen 0 640x4800x16" '
- 'wkhtmltopdf --footer-center "Page [page] of [topage]" --footer-font-size 9 - -',
- stdin=html.encode('utf-8'), display=False
- )
+ 'wkhtmltopdf -q --footer-center "Page [page] of [topage]" --footer-font-size 9 - -',
+ stdin=html.encode('utf-8'), force_unicode=False
+ ).stdout
diff --git a/orchestra/utils/system.py b/orchestra/utils/system.py
index 0b25a1e9..fc6264b9 100644
--- a/orchestra/utils/system.py
+++ b/orchestra/utils/system.py
@@ -21,11 +21,10 @@ def check_root(func):
return wrapped
-class _AttributeUnicode(unicode):
+class _Attribute(object):
""" Simple string subclass to allow arbitrary attribute access. """
- @property
- def stdout(self):
- return unicode(self)
+ def __init__(self, stdout):
+ self.stdout = stdout
def make_async(fd):
@@ -46,7 +45,7 @@ def read_async(fd):
return u''
-def runiterator(command, display=False, error_codes=[0], silent=False, stdin=''):
+def runiterator(command, display=False, error_codes=[0], silent=False, stdin='', force_unicode=True):
""" Subprocess wrapper for running commands concurrently """
if display:
sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command)
@@ -62,29 +61,29 @@ def runiterator(command, display=False, error_codes=[0], silent=False, stdin='')
make_async(p.stderr)
# Async reading of stdout and sterr
+ # TODO cleanup
while True:
# TODO https://github.com/isagalaev/ijson/issues/15
- stdout = unicode()
- sdterr = unicode()
+ stdout = unicode() if force_unicode else ''
+ sdterr = unicode() if force_unicode else ''
# Get complete unicode chunks
while True:
select.select([p.stdout, p.stderr], [], [])
stdoutPiece = read_async(p.stdout)
stderrPiece = read_async(p.stderr)
try:
- stdout += stdoutPiece.decode("utf8")
- sdterr += stderrPiece.decode("utf8")
- except UnicodeDecodeError:
+ stdout += unicode(stdoutPiece.decode("utf8")) if force_unicode else stdoutPiece
+ sdterr += unicode(stderrPiece.decode("utf8")) if force_unicode else stderrPiece
+ except UnicodeDecodeError, e:
pass
else:
break
-
if display and stdout:
sys.stdout.write(stdout)
- if display and stderrPiece:
+ if display and stderr:
sys.stderr.write(stderr)
- state = _AttributeUnicode(stdout)
+ state = _Attribute(stdout)
state.stderr = sdterr
state.return_code = p.poll()
yield state
@@ -95,8 +94,8 @@ def runiterator(command, display=False, error_codes=[0], silent=False, stdin='')
raise StopIteration
-def run(command, display=False, error_codes=[0], silent=False, stdin='', async=False):
- iterator = runiterator(command, display, error_codes, silent, stdin)
+def run(command, display=False, error_codes=[0], silent=False, stdin='', async=False, force_unicode=True):
+ iterator = runiterator(command, display, error_codes, silent, stdin, force_unicode)
iterator.next()
if async:
return iterator
@@ -109,7 +108,7 @@ def run(command, display=False, error_codes=[0], silent=False, stdin='', async=F
return_code = state.return_code
- out = _AttributeUnicode(stdout.strip())
+ out = _Attribute(stdout.strip())
err = stderr.strip()
out.failed = False