Upgraded to DRF2.4.x

This commit is contained in:
Marc Aymerich 2015-02-24 09:34:26 +00:00
parent 1fe98f434d
commit 1ed44bc745
20 changed files with 91 additions and 117 deletions

11
TODO.md
View File

@ -166,7 +166,7 @@
* webapp compat webapp-options * webapp compat webapp-options
* webapps modeled on classes instead of settings? * 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) * Mix webapps type with backends (two for the price of one)
@ -181,13 +181,10 @@ Multi-tenant WebApps
* Howto upgrade webapp PHP version? <FilesMatch \.php$> SetHandler php54-cgi</FilesMatch> ? or create a new app * Howto upgrade webapp PHP version? <FilesMatch \.php$> SetHandler php54-cgi</FilesMatch> ? 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? * fcgid kill instead of apache reload?
* chomod user:group
* username maximum as group user in UNIX * username maximum as group user in UNIX
* forms autocomplete="off"

View File

@ -100,7 +100,7 @@ class SendEmail(object):
'content_message': _( 'content_message': _(
"Are you sure you want to send the following message to the following %s?" "Are you sure you want to send the following message to the following %s?"
) % self.opts.verbose_name_plural, ) % 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, 'form': form,
'subject': subject, 'subject': subject,
'message': message, 'message': message,

View File

@ -1,82 +1,15 @@
from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils.module_loading import autodiscover_modules from django.utils.module_loading import autodiscover_modules
from rest_framework.routers import DefaultRouter, Route, flatten, replace_methodname from rest_framework.routers import DefaultRouter, Route, flatten, replace_methodname
from orchestra import settings from orchestra import settings
#from orchestra.utils.apps import autodiscover as module_autodiscover
from orchestra.utils.python import import_class from orchestra.utils.python import import_class
from .helpers import insert_links, replace_collectionmethodname 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): 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): def get_api_root_view(self):
""" returns the root view, with all the linked collections """ """ returns the root view, with all the linked collections """
APIRoot = import_class(settings.API_ROOT_VIEW) APIRoot = import_class(settings.API_ROOT_VIEW)
@ -110,6 +43,6 @@ class LinkHeaderRouter(DefaultRouter):
# Create a router and register our viewsets with it. # 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')) autodiscover = lambda: (autodiscover_modules('api'), autodiscover_modules('serializers'))

View File

@ -17,8 +17,8 @@ class APIRoot(views.APIView):
'<%s>; rel="%s"' % (token_url, 'api-get-auth-token'), '<%s>; rel="%s"' % (token_url, 'api-get-auth-token'),
] ]
body = { body = {
'accountancy': [], 'accountancy': {},
'services': [], 'services': {},
} }
if not request.user.is_anonymous(): if not request.user.is_anonymous():
list_name = '{basename}-list' list_name = '{basename}-list'
@ -44,12 +44,11 @@ class APIRoot(views.APIView):
group = 'accountancy' group = 'accountancy'
menu = accounts[model].menu menu = accounts[model].menu
if group and menu: if group and menu:
body[group].append({ body[group][basename] = {
'url': url, 'url': url,
'name': basename,
'verbose_name': model._meta.verbose_name, 'verbose_name': model._meta.verbose_name,
'verbose_name_plural': model._meta.verbose_name_plural, 'verbose_name_plural': model._meta.verbose_name_plural,
}) }
headers = { headers = {
'Link': ', '.join(links) 'Link': ', '.join(links)
} }

View File

@ -1,12 +1,29 @@
from django.http import HttpResponse
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.decorators import detail_route
from orchestra.api import router from orchestra.api import router
from orchestra.apps.accounts.api import AccountApiMixin from orchestra.apps.accounts.api import AccountApiMixin
from orchestra.utils.html import html_to_pdf
from .models import Bill from .models import Bill
from .serializers import BillSerializer from .serializers import BillSerializer
class BillViewSet(AccountApiMixin, viewsets.ModelViewSet): class BillViewSet(AccountApiMixin, viewsets.ModelViewSet):
model = Bill model = Bill
serializer_class = BillSerializer 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)

View File

@ -14,13 +14,14 @@ class BillLineSerializer(serializers.HyperlinkedModelSerializer):
class BillSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): class BillSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
lines = BillLineSerializer(source='billlines') # lines = BillLineSerializer(source='lines')
class Meta: class Meta:
model = Bill model = Bill
fields = ( fields = (
'url', 'number', 'type', 'total', 'is_sent', 'created_on', 'due_on', 'url', 'number', 'type', 'total', 'is_sent', 'created_on', 'due_on',
'comments', 'html', 'lines' 'comments',
# 'lines'
) )

View File

@ -38,6 +38,7 @@ class MySQLBackend(ServiceController):
return return
context = self.get_context(database) context = self.get_context(database)
self.append("mysql -e 'DROP DATABASE `%(database)s`;'" % context) 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): def commit(self):
self.append("mysql -e 'FLUSH PRIVILEGES;'") self.append("mysql -e 'FLUSH PRIVILEGES;'")

View File

@ -36,7 +36,7 @@ class PasswdVirtualUserBackend(ServiceController):
fi""" % context fi""" % context
)) ))
self.append("mkdir -p %(home)s" % 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): def set_mailbox(self, context):
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""

View File

@ -25,7 +25,7 @@ class SystemUserBackend(ServiceController):
useradd %(username)s --home %(home)s --password '%(password)s' --shell %(shell)s %(groups_arg)s useradd %(username)s --home %(home)s --password '%(password)s' --shell %(shell)s %(groups_arg)s
fi fi
mkdir -p %(home)s 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: for member in settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS:
context['member'] = member context['member'] = member

View File

@ -21,8 +21,13 @@ class SystemUserQuerySet(models.QuerySet):
class SystemUser(models.Model): 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."), help_text=_("Required. 64 characters or fewer. Letters, digits and ./-/_ only."),
validators=[validators.validate_username]) validators=[validators.validate_username])
password = models.CharField(_("password"), max_length=128) password = models.CharField(_("password"), max_length=128)

View File

@ -18,7 +18,7 @@ class WebAppServiceMixin(object):
path="${path}/${dir}" path="${path}/${dir}"
[ -d $path ] || { [ -d $path ] || {
mkdir "${path}" mkdir "${path}"
chown %(user)s.%(group)s "${path}" chown %(user)s:%(group)s "${path}"
} }
done done
""" % context)) """ % context))

View File

@ -27,7 +27,7 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController):
echo -e '%(wrapper_content)s' > %(wrapper_path)s; UPDATED_APACHE=1 echo -e '%(wrapper_content)s' > %(wrapper_path)s; UPDATED_APACHE=1
}""" % context)) }""" % context))
self.append("chmod +x %(wrapper_path)s" % 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): def delete(self, webapp):
if not self.valid_directive(webapp): if not self.valid_directive(webapp):

View File

@ -124,6 +124,12 @@ WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTIO
WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', { WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
# { name: ( verbose_name, [help_text], validation_regex ) } # { name: ( verbose_name, [help_text], validation_regex ) }
# Filesystem
'public-root': (
_("Public root"),
_("Document root relative to webapps/&lt;webapp&gt;/"),
r'[^ ]+',
),
# Processes # Processes
'timeout': ( 'timeout': (
_("Process timeout"), _("Process timeout"),
@ -220,6 +226,12 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
"(Integer between 0 and 999)."), "(Integer between 0 and 999)."),
r'^[0-9]{1,3}$' 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': (
_("PHP - Memory limit"), _("PHP - Memory limit"),
_("This sets the maximum amount of memory in bytes that a script is allowed to allocate " _("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)$' r'^(On|Off|on|off)$'
), ),
'PHP-suhosin.post.max_vars': ( '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."), _("Number between 0 and 9999."),
r'^[0-9]{1,4}$' r'^[0-9]{1,4}$'
), ),

View File

@ -17,7 +17,7 @@ class WebalizerBackend(ServiceController):
self.append("[[ ! -e %(webalizer_path)s/index.html ]] && " self.append("[[ ! -e %(webalizer_path)s/index.html ]] && "
"echo 'Webstats are coming soon' > %(webalizer_path)s/index.html" % context) "echo 'Webstats are coming soon' > %(webalizer_path)s/index.html" % context)
self.append("echo '%(webalizer_conf)s' > %(webalizer_conf_path)s" % 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): def delete(self, content):
context = self.get_context(content) context = self.get_context(content)

View File

@ -30,7 +30,7 @@ class Website(models.Model):
@property @property
def unique_name(self): def unique_name(self):
return "%s-%s" % (self.account, self.name) return "%s-%i" % (self.name, self.pk)
@cached @cached
def get_options(self): def get_options(self):

View File

@ -26,8 +26,13 @@ WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', {
), ),
'redirect': ( 'redirect': (
_("HTTPD - Redirection"), _("HTTPD - Redirection"),
_("<tt>[permanent] &lt;website path&gt; &lt;destination URL&gt;</tt>"), _("<tt>&lt;website path&gt; &lt;destination URL&gt;</tt>"),
r'^(permanent\s[^ ]+|[^ ]+)\s[^ ]+$', r'^[^ ]+\s[^ ]+$',
),
'proxy': (
_("HTTPD - Proxy"),
_("<tt>&lt;website path&gt; &lt;target URL&gt;</tt>"),
r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$',
), ),
'ssl_ca': ( 'ssl_ca': (
"HTTPD - SSL CA", "HTTPD - SSL CA",

View File

@ -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 # 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 # 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 DEBUG = False
# Django debug toolbar # Django debug toolbar

View File

@ -1,5 +1,5 @@
{% extends "rest_framework/base.html" %} {% extends "rest_framework/base.html" %}
{% load rest_framework utils %} {% load rest_framework utils staticfiles %}
{% block head %} {% block head %}
{{ block.super }} {{ block.super }}
@ -17,7 +17,7 @@
<b class="caret"></b> <b class="caret"></b>
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li>{% optional_logout request %}</li> <li>{% optional_logout request user %}</li>
</ul> </ul>
</li> </li>
{% else %} {% else %}

View File

@ -6,6 +6,6 @@ def html_to_pdf(html):
return run( return run(
'PATH=$PATH:/usr/local/bin/\n' 'PATH=$PATH:/usr/local/bin/\n'
'xvfb-run -a -s "-screen 0 640x4800x16" ' 'xvfb-run -a -s "-screen 0 640x4800x16" '
'wkhtmltopdf --footer-center "Page [page] of [topage]" --footer-font-size 9 - -', 'wkhtmltopdf -q --footer-center "Page [page] of [topage]" --footer-font-size 9 - -',
stdin=html.encode('utf-8'), display=False stdin=html.encode('utf-8'), force_unicode=False
) ).stdout

View File

@ -21,11 +21,10 @@ def check_root(func):
return wrapped return wrapped
class _AttributeUnicode(unicode): class _Attribute(object):
""" Simple string subclass to allow arbitrary attribute access. """ """ Simple string subclass to allow arbitrary attribute access. """
@property def __init__(self, stdout):
def stdout(self): self.stdout = stdout
return unicode(self)
def make_async(fd): def make_async(fd):
@ -46,7 +45,7 @@ def read_async(fd):
return u'' 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 """ """ Subprocess wrapper for running commands concurrently """
if display: if display:
sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command) 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) make_async(p.stderr)
# Async reading of stdout and sterr # Async reading of stdout and sterr
# TODO cleanup
while True: while True:
# TODO https://github.com/isagalaev/ijson/issues/15 # TODO https://github.com/isagalaev/ijson/issues/15
stdout = unicode() stdout = unicode() if force_unicode else ''
sdterr = unicode() sdterr = unicode() if force_unicode else ''
# Get complete unicode chunks # Get complete unicode chunks
while True: while True:
select.select([p.stdout, p.stderr], [], []) select.select([p.stdout, p.stderr], [], [])
stdoutPiece = read_async(p.stdout) stdoutPiece = read_async(p.stdout)
stderrPiece = read_async(p.stderr) stderrPiece = read_async(p.stderr)
try: try:
stdout += stdoutPiece.decode("utf8") stdout += unicode(stdoutPiece.decode("utf8")) if force_unicode else stdoutPiece
sdterr += stderrPiece.decode("utf8") sdterr += unicode(stderrPiece.decode("utf8")) if force_unicode else stderrPiece
except UnicodeDecodeError: except UnicodeDecodeError, e:
pass pass
else: else:
break break
if display and stdout: if display and stdout:
sys.stdout.write(stdout) sys.stdout.write(stdout)
if display and stderrPiece: if display and stderr:
sys.stderr.write(stderr) sys.stderr.write(stderr)
state = _AttributeUnicode(stdout) state = _Attribute(stdout)
state.stderr = sdterr state.stderr = sdterr
state.return_code = p.poll() state.return_code = p.poll()
yield state yield state
@ -95,8 +94,8 @@ def runiterator(command, display=False, error_codes=[0], silent=False, stdin='')
raise StopIteration raise StopIteration
def run(command, display=False, error_codes=[0], silent=False, stdin='', async=False): def run(command, display=False, error_codes=[0], silent=False, stdin='', async=False, force_unicode=True):
iterator = runiterator(command, display, error_codes, silent, stdin) iterator = runiterator(command, display, error_codes, silent, stdin, force_unicode)
iterator.next() iterator.next()
if async: if async:
return iterator return iterator
@ -109,7 +108,7 @@ def run(command, display=False, error_codes=[0], silent=False, stdin='', async=F
return_code = state.return_code return_code = state.return_code
out = _AttributeUnicode(stdout.strip()) out = _Attribute(stdout.strip())
err = stderr.strip() err = stderr.strip()
out.failed = False out.failed = False