Upgraded to DRF2.4.x
This commit is contained in:
parent
1fe98f434d
commit
1ed44bc745
11
TODO.md
11
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? <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?
|
||||
|
||||
|
||||
* chomod user:group
|
||||
* username maximum as group user in UNIX
|
||||
|
||||
* forms autocomplete="off"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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;'")
|
||||
|
|
|
@ -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("""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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}$'
|
||||
),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -26,8 +26,13 @@ WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', {
|
|||
),
|
||||
'redirect': (
|
||||
_("HTTPD - Redirection"),
|
||||
_("<tt>[permanent] <website path> <destination URL></tt>"),
|
||||
r'^(permanent\s[^ ]+|[^ ]+)\s[^ ]+$',
|
||||
_("<tt><website path> <destination URL></tt>"),
|
||||
r'^[^ ]+\s[^ ]+$',
|
||||
),
|
||||
'proxy': (
|
||||
_("HTTPD - Proxy"),
|
||||
_("<tt><website path> <target URL></tt>"),
|
||||
r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$',
|
||||
),
|
||||
'ssl_ca': (
|
||||
"HTTPD - SSL CA",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 @@
|
|||
<b class="caret"></b>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>{% optional_logout request %}</li>
|
||||
<li>{% optional_logout request user %}</li>
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue