+
+ {% block header %}
+
WIP/DJANGO - {% if title %}{{ title }}{% else %}{{ site_name }}{% endif %}
+ {% endblock %}
+
+
+ {% block content %}
+ {{ prelude }}
+
+ {% if breadcrumb %}
+
{{ breadcrumb|safe }}
+ {% endif %}
+
+ {% block body %}
+ {{ body|safe }}
+ {% endblock %}
+
+ {% endblock %}
+
+
+
+
diff --git a/data/themes/django/templates/home.html b/data/themes/django/templates/home.html
new file mode 100644
index 000000000..2875d94f1
--- /dev/null
+++ b/data/themes/django/templates/home.html
@@ -0,0 +1,20 @@
+{% extends "base.html"%}
+
+{% block body %}
+
+
+
HELLO WORLD
+
+{% regroup forms by category as category_list %}
+{% for category in category_list %}
+{% if category.grouper %}
{{ category.grouper }}
{% endif %}
+
+{% endfor %}
+
+
+
+{% endblock %}
diff --git a/data/themes/django/wcs.css b/data/themes/django/wcs.css
new file mode 100644
index 000000000..ca8980cf1
--- /dev/null
+++ b/data/themes/django/wcs.css
@@ -0,0 +1,12 @@
+@import url(../../qo/css/sofresh.css);
+
+#page {
+ -webkit-transform: rotate(2deg);
+ -webkit-transition: all 200ms ease-out;
+ -webkit-filter: grayscale(100%);
+}
+
+#page:hover {
+ -webkit-transform: rotate(0deg);
+ -webkit-filter: none;
+}
diff --git a/tests/test_hobo.py b/tests/test_hobo.py
index 269316378..4fb462f03 100644
--- a/tests/test_hobo.py
+++ b/tests/test_hobo.py
@@ -198,6 +198,7 @@ HOBO_JSON = {
'foobar': 'http://example.net',
'email_signature': 'Hello world.',
'default_from_email': 'noreply@example.net',
+ 'theme': 'clapotis-les-canards',
},
'users': [
{
@@ -248,7 +249,7 @@ def test_update_configuration():
assert pub.cfg['misc']['sitename'] == 'Test wcs'
assert pub.cfg['emails']['footer'] == 'Hello world.'
assert pub.cfg['emails']['from'] == 'noreply@example.net'
- assert pub.cfg['branding']['theme'] == 'publik'
+ assert pub.cfg['branding']['theme'] == 'publik-base'
def test_update_profile():
profile = HOBO_JSON.get('profile')
diff --git a/wcs/__init__.py b/wcs/__init__.py
index 40887c377..2ae0be35a 100644
--- a/wcs/__init__.py
+++ b/wcs/__init__.py
@@ -22,3 +22,5 @@ import qommon
import qommon.form
sys.modules['form'] = qommon.form
+
+import compat
diff --git a/wcs/compat.py b/wcs/compat.py
index 38a9944f9..47d541055 100644
--- a/wcs/compat.py
+++ b/wcs/compat.py
@@ -18,13 +18,17 @@ import ConfigParser
import os
from threading import Lock
+from contextlib import contextmanager
-from quixote import get_publisher
+from quixote import get_publisher, get_request
from quixote.errors import PublishError
from quixote.http_request import Upload
from django.http import HttpResponse
from django.conf import settings
+from django.template import loader, RequestContext, TemplateDoesNotExist
+from django.template.response import TemplateResponse
+from django.views.generic.base import TemplateView
from .qommon import template
from .qommon.publisher import get_cfg, set_publisher_class
@@ -34,7 +38,7 @@ from .qommon.http_response import HTTPResponse
def init_publisher_if_needed():
if get_publisher() is not None:
- return
+ return get_publisher()
# initialize publisher in first request
config = ConfigParser.ConfigParser()
if settings.WCS_LEGACY_CONFIG_FILE:
@@ -45,6 +49,31 @@ def init_publisher_if_needed():
for i, extra in enumerate(settings.WCS_EXTRA_MODULES):
config.set('extra', 'cmd_line_extra_%d' % i, extra)
CompatWcsPublisher.configure(config)
+ return CompatWcsPublisher.create_publisher()
+
+
+class TemplateWithFallbackView(TemplateView):
+ quixote_response = None
+
+ def get(self, request, *args, **kwargs):
+ try:
+ loader.get_template(self.template_name)
+ except TemplateDoesNotExist:
+ return quixote(self.request)
+ context = self.get_context_data(**kwargs)
+ return self.render_to_response(context)
+
+ def render_to_response(self, context, **response_kwargs):
+ django_response = super(TemplateWithFallbackView, self).render_to_response(context, **response_kwargs)
+ if self.quixote_response and self.quixote_response.status_code != '200':
+ django_response.status_code = self.quixote_response.status_code
+ django_response.reason_phrase = self.quixote_response.reason_phrase
+ for name, value in self.quixote_response.generate_headers():
+ if name == 'Content-Length':
+ continue
+ django_response[name] = value
+
+ return django_response
class CompatHTTPRequest(HTTPRequest):
@@ -98,7 +127,33 @@ class CompatWcsPublisher(WcsPublisher):
return output
if not hasattr(response, 'filter') or not response.filter:
return output
- return self.render_response(output)
+ if request.META.get('HTTP_X_POPUP') == 'true':
+ return '' % output
+ if response.filter and response.filter.get('admin_ezt'):
+ return self.render_response(output)
+
+ current_theme = get_cfg('branding', {}).get('theme', 'default')
+ theme_directory = template.get_theme_directory(current_theme)
+ if not os.path.exists(os.path.join(theme_directory, 'templates')):
+ return self.render_response(output)
+
+ if not os.path.exists(os.path.join(theme_directory, 'templates/wcs/base.html')):
+ return self.render_response(output)
+
+ template_name = 'wcs/base.html'
+ vars = template.get_decorate_vars(output, response)
+ context = RequestContext(request, vars)
+ django_response = TemplateResponse(request,
+ template_name,
+ context,
+ content_type=response.content_type,
+ status=response.status_code)
+
+ return django_response
+
+ def set_app_dir(self, request):
+ super(CompatWcsPublisher, self).set_app_dir(request)
+ settings.THEME_SKELETON_URL = self.get_site_option('theme_skeleton_url')
def process_request(self, request):
self._set_request(request)
@@ -113,8 +168,12 @@ class CompatWcsPublisher(WcsPublisher):
output = self.filter_output(request, output)
- content = output
- django_response = HttpResponse(content,
+ if isinstance(output, TemplateResponse):
+ django_response = output
+ django_response.render()
+ else:
+ content = output
+ django_response = HttpResponse(content,
content_type=response.content_type,
status=response.status_code,
reason=response.reason_phrase)
@@ -138,4 +197,36 @@ def quixote(request):
compat_request = CompatHTTPRequest(request)
return pub.process_request(compat_request)
+
+@contextmanager
+def request(request):
+ pub = get_publisher()
+ compat_request = CompatHTTPRequest(request)
+ pub.init_publish(compat_request)
+ pub._set_request(compat_request)
+ compat_request.process_inputs()
+ yield
+ pub._clear_request()
+
+
+class PublishErrorMiddleware(object):
+ def process_exception(self, request, exception):
+ if not isinstance(exception, PublishError):
+ return None
+ request = get_request()
+ exception_body = exception.render()
+
+ django_response = HttpResponse(exception_body,
+ content_type=request.response.content_type,
+ status=request.response.status_code,
+ reason=request.response.reason_phrase)
+
+ for name, value in request.response.generate_headers():
+ if name == 'Content-Length':
+ continue
+ django_response[name] = value
+
+ return django_response
+
+
set_publisher_class(CompatWcsPublisher)
diff --git a/wcs/ctl/check_hobos.py b/wcs/ctl/check_hobos.py
index ce2994413..9ca2c2e39 100644
--- a/wcs/ctl/check_hobos.py
+++ b/wcs/ctl/check_hobos.py
@@ -32,10 +32,20 @@ from qommon.storage import atomic_write
from wcs.admin.settings import UserFieldsFormDef
from wcs.fields import StringField, EmailField
+# TODO: import this from django settings
+THEMES_DIRECTORY = os.environ.get('THEMES_DIRECTORY', '/usr/share/publik/themes')
+
class NoChange(Exception):
pass
+def atomic_symlink(src, dst):
+ if os.path.exists(dst) and os.readlink(dst) == src:
+ return
+ if os.path.exists(dst + '.tmp'):
+ os.unlink(dst + '.tmp')
+ os.symlink(src, dst + '.tmp')
+ os.rename(dst + '.tmp', dst)
class CmdCheckHobos(Command):
name = 'hobo_deploy'
@@ -146,8 +156,13 @@ class CmdCheckHobos(Command):
if not pub.cfg.get('emails'):
pub.cfg['emails'] = {}
- if not pub.cfg.get('branding'):
- pub.cfg['branding'] = {'theme': 'publik'}
+ theme_id = self.all_services.get('variables', {}).get('theme')
+ if theme_id:
+ pub.cfg['branding'] = {'theme': 'publik-base'}
+ tenant_dir = pub.app_dir
+ theme_dir = os.path.join(tenant_dir, 'theme')
+ target_dir = os.path.join(THEMES_DIRECTORY, 'publik-base')
+ atomic_symlink(target_dir, theme_dir)
variables = self.all_services.get('variables') or {}
variables.update(service.get('variables') or {})
diff --git a/wcs/forms/root.py b/wcs/forms/root.py
index 5e949a665..ab9dbf034 100644
--- a/wcs/forms/root.py
+++ b/wcs/forms/root.py
@@ -1476,6 +1476,67 @@ class RootDirectory(AccessControlled, Directory):
from wcs.api import ApiFormdefsDirectory
return ApiFormdefsDirectory(self.category)._q_index()
+ def get_context(self):
+ from wcs.api import is_url_signed, get_user_from_api_query_string
+ user = get_user_from_api_query_string() or get_request().user
+ list_all_forms = (user and user.is_admin) or (is_url_signed() and user is None)
+
+ list_forms = []
+
+ if self.category:
+ formdefs = FormDef.select(lambda x: (
+ str(x.category_id) == str(self.category.id) and (
+ not x.is_disabled() or x.disabled_redirection)),
+ order_by = 'name')
+ else:
+ formdefs = FormDef.select(lambda x: not x.is_disabled() or x.disabled_redirection,
+ order_by='name',
+ ignore_errors=True)
+
+ charset = get_publisher().site_charset
+
+ for formdef in formdefs:
+ authentication_required = False
+ if formdef.roles and not list_all_forms:
+ if not user:
+ if not formdef.always_advertise:
+ continue
+ authentication_required = True
+ elif logged_users_role().id not in formdef.roles:
+ for q in user.roles or []:
+ if q in formdef.roles:
+ break
+ else:
+ if not formdef.always_advertise:
+ continue
+ authentication_required = True
+
+ formdict = {'title': unicode(formdef.name, charset),
+ 'slug': formdef.url_name,
+ 'url': formdef.get_url(),
+ 'authentication_required': authentication_required}
+
+ formdict['redirection'] = bool(formdef.is_disabled() and
+ formdef.disabled_redirection)
+
+ # we include the count of submitted forms so it's possible to sort
+ # them by popularity
+ formdict['count'] = formdef.data_class().count()
+
+ if formdef.category:
+ formdict['category'] = unicode(formdef.category.name, charset)
+ formdict['category_position'] = (formdef.category.position or 0)
+ else:
+ formdict['category_position'] = sys.maxint
+
+ list_forms.append(formdict)
+
+ list_forms.sort(lambda x, y: cmp(x['category_position'], y['category_position']))
+ for formdict in list_forms:
+ del formdict['category_position']
+
+ return list_forms
+
def get_categories(self, user):
result = []
formdefs = FormDef.select(
diff --git a/wcs/publisher.py b/wcs/publisher.py
index 90a1c6d2c..89d782049 100644
--- a/wcs/publisher.py
+++ b/wcs/publisher.py
@@ -253,10 +253,10 @@ class WcsPublisher(StubWcsPublisher):
z.close()
return results
- def try_publish(self, request):
+ def init_publish(self, request):
if request.get_header('X_WCS_IFRAME_MODE', '') in ('true', 'yes'):
request.response.iframe_mode = True
- return QommonPublisher.try_publish(self, request)
+ return QommonPublisher.init_publish(self, request)
def get_object_visitors(self, object_key):
session_manager = self.session_manager_class()
diff --git a/wcs/qommon/publisher.py b/wcs/qommon/publisher.py
index 9e4d18bbb..e8d17702c 100644
--- a/wcs/qommon/publisher.py
+++ b/wcs/qommon/publisher.py
@@ -540,12 +540,12 @@ class QommonPublisher(Publisher, object):
return True
return False
- def try_publish(self, request):
+ def init_publish(self, request):
self.substitutions.reset()
-
try:
self.set_app_dir(request)
except ImmediateRedirectException, e:
+ self._set_request(request)
return redirect(e.location)
from vendor import pystatsd
@@ -642,6 +642,9 @@ class QommonPublisher(Publisher, object):
self.substitutions.feed(request)
for extra_source in self.extra_sources:
self.substitutions.feed(extra_source(self, request))
+
+ def try_publish(self, request):
+ self.init_publish(request)
return Publisher.try_publish(self, request)
def get_site_language(self):
diff --git a/wcs/qommon/template.py b/wcs/qommon/template.py
index be0f8f747..db75a3ea9 100644
--- a/wcs/qommon/template.py
+++ b/wcs/qommon/template.py
@@ -289,6 +289,9 @@ def get_decorate_vars(body, response, generate_breadcrumb=True):
body = str(body)
+ if get_request().get_header('x-popup') == 'true':
+ return {}
+
kwargs = {}
for k, v in response.filter.items():
if v:
@@ -410,7 +413,7 @@ def decorate(body, response):
current_theme = get_cfg('branding', {}).get('theme', 'default')
if response.page_template_key or not template_ezt:
# the theme can provide a default template
- possible_filenames = ['template.py']
+ possible_filenames = []
if response.page_template_key:
possible_filenames.append('template.%s.%s.ezt' % (
get_publisher().APP_NAME, response.page_template_key))
@@ -429,12 +432,7 @@ def decorate(body, response):
for dname in possible_dirnames:
filename = os.path.join(dname, fname)
if os.path.exists(filename):
- if fname == 'template.py':
- template_ezt = get_template_from_script(filename)
- if template_ezt is None:
- continue
- else:
- template_ezt = file(filename).read()
+ template_ezt = file(filename).read()
break
else:
continue
diff --git a/wcs/settings.py b/wcs/settings.py
index fc83e7160..b7dd35b5f 100644
--- a/wcs/settings.py
+++ b/wcs/settings.py
@@ -68,7 +68,6 @@ STATIC_URL = '/static/'
# Additional locations of static files
STATICFILES_DIRS = (
- os.path.join(PROJECT_PATH, 'wcs', 'static'),
)
# List of finder classes that know how to find static files in
@@ -84,6 +83,7 @@ SECRET_KEY = 'k16cal%1fnochq4xbxqgdns-21lt9lxeof5*%j(0ief3=db32&'
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
+ 'wcs.utils.TemplateLoader',
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
# 'django.template.loaders.eggs.Loader',
@@ -98,6 +98,7 @@ MIDDLEWARE_CLASSES = (
#'django.contrib.messages.middleware.MessageMiddleware',
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'wcs.compat.PublishErrorMiddleware',
)
ROOT_URLCONF = 'wcs.urls'
@@ -109,6 +110,16 @@ TEMPLATE_DIRS = (
os.path.join(PROJECT_PATH, 'wcs', 'templates'),
)
+TEMPLATE_CONTEXT_PROCESSORS = (
+ "django.contrib.auth.context_processors.auth",
+ "django.core.context_processors.debug",
+ "django.core.context_processors.i18n",
+ "django.core.context_processors.media",
+ "django.core.context_processors.static",
+ "django.core.context_processors.tz",
+ "django.contrib.messages.context_processors.messages",
+)
+
INSTALLED_APPS = (
#'django.contrib.auth',
#'django.contrib.contenttypes',
@@ -117,6 +128,7 @@ INSTALLED_APPS = (
#'django.contrib.messages',
#'django.contrib.staticfiles',
#'django.contrib.admin',
+ 'gadjo',
)
WCS_LEGACY_CONFIG_FILE = None
diff --git a/wcs/urls.py b/wcs/urls.py
index eb85bafb0..1c96d509d 100644
--- a/wcs/urls.py
+++ b/wcs/urls.py
@@ -14,8 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, see
.
+from django.conf import settings
from django.conf.urls import patterns, url
urlpatterns = patterns('',
- url(r'', 'wcs.compat.quixote', name='quixote'),
+ url(r'^$', 'wcs.views.home', name='home'),
)
+
+# other URLs are handled by the quixote handler
+urlpatterns += patterns('', url(r'', 'wcs.compat.quixote', name='quixote'))
diff --git a/wcs/utils.py b/wcs/utils.py
new file mode 100644
index 000000000..60668cb89
--- /dev/null
+++ b/wcs/utils.py
@@ -0,0 +1,54 @@
+# w.c.s. - web application for online forms
+# Copyright (C) 2005-2013 Entr'ouvert
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see
.
+
+import os
+
+import django.template.loaders.filesystem
+from django.conf import settings
+
+from .qommon.template import get_theme_directory
+from .qommon.publisher import get_cfg, get_publisher
+
+class TemplateLoader(django.template.loaders.filesystem.Loader):
+ def get_template_sources(self, template_name, template_dirs=None):
+ if not template_dirs:
+ template_dirs = []
+
+ # theme set by hobo
+ theme = get_publisher().get_site_option('theme', 'variables')
+
+ # templates from tenant directory
+ if theme:
+ template_dirs.append(os.path.join(get_publisher().app_dir,
+ 'templates', 'variants', theme))
+ template_dirs.append(os.path.join(get_publisher().app_dir, 'theme', 'templates'))
+ template_dirs.append(os.path.join(get_publisher().app_dir, 'templates'))
+
+ current_theme = get_cfg('branding', {}).get('theme', 'default')
+ theme_directory = get_theme_directory(current_theme)
+ if theme_directory:
+ # templates from theme directory
+ theme_directory = os.path.join(theme_directory, 'templates')
+
+ if theme:
+ # template theme set by hobo
+ template_dirs.append(os.path.join(theme_directory, 'variants', theme))
+
+ template_dirs.append(theme_directory)
+
+ template_dirs = tuple(template_dirs) + settings.TEMPLATE_DIRS
+
+ return super(TemplateLoader, self).get_template_sources(template_name, template_dirs)
diff --git a/wcs/views.py b/wcs/views.py
index 8765d2024..ecb6da052 100644
--- a/wcs/views.py
+++ b/wcs/views.py
@@ -14,3 +14,29 @@
# You should have received a copy of the GNU General Public License
# along with this program; if not, see
.
+from quixote import get_request
+
+from .forms.root import RootDirectory as FormsRootDirectory
+from .qommon.admin.texts import TextsDirectory
+from . import compat
+
+
+class Home(compat.TemplateWithFallbackView):
+ template_name = 'home.html'
+
+ def get_context_data(self, **kwargs):
+ context = super(Home, self).get_context_data(**kwargs)
+
+ with compat.request(self.request):
+ user = get_request().user
+ if user:
+ context['message'] = TextsDirectory.get_html_text('welcome-logged')
+ else:
+ context['message'] = TextsDirectory.get_html_text('welcome-unlogged')
+
+ root_directory = FormsRootDirectory()
+ context['forms'] = root_directory.get_context()
+
+ return context
+
+home = Home.as_view()