From 1ed44bc7451166f77c0505057669a33d4a4ad44c Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Tue, 24 Feb 2015 09:34:26 +0000 Subject: [PATCH] Upgraded to DRF2.4.x --- TODO.md | 11 ++- orchestra/admin/actions.py | 2 +- orchestra/api/options.py | 71 +------------------ orchestra/api/root.py | 9 ++- orchestra/apps/bills/api.py | 17 +++++ orchestra/apps/bills/serializers.py | 5 +- orchestra/apps/databases/backends.py | 1 + orchestra/apps/mailboxes/backends.py | 2 +- orchestra/apps/systemusers/backends.py | 2 +- orchestra/apps/systemusers/models.py | 9 ++- orchestra/apps/webapps/backends/__init__.py | 2 +- orchestra/apps/webapps/backends/phpfcgid.py | 2 +- orchestra/apps/webapps/settings.py | 19 ++++- orchestra/apps/websites/backends/webalizer.py | 2 +- orchestra/apps/websites/models.py | 2 +- orchestra/apps/websites/settings.py | 9 ++- orchestra/conf/devel_settings.py | 2 +- orchestra/templates/rest_framework/api.html | 4 +- orchestra/utils/html.py | 6 +- orchestra/utils/system.py | 31 ++++---- 20 files changed, 91 insertions(+), 117 deletions(-) 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