diff --git a/data/themes/django/desc.xml b/data/themes/django/desc.xml new file mode 100644 index 000000000..b018e2bf6 --- /dev/null +++ b/data/themes/django/desc.xml @@ -0,0 +1,6 @@ + + + + Test theme for Django port + Frederic Peters + diff --git a/data/themes/django/templates/base.html b/data/themes/django/templates/base.html new file mode 100644 index 000000000..43675ec25 --- /dev/null +++ b/data/themes/django/templates/base.html @@ -0,0 +1,34 @@ + + + + {% block page-title %}{{ page_title }}{% endblock %} + + {{ script|safe }} + {% block extrascripts %} + {% endblock %} + + +
+
+
+ {% block header %} +

WIP/DJANGO - {% if title %}{{ title }}{% else %}{{ site_name }}{% endif %}

+ {% endblock %} +
+
+ {% block content %} + {{ prelude }} + + {% if breadcrumb %} + + {% 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()