commit f4843b93ea02835d0305ef01e0f94a39798d0e3a
Author: Serghei Mihai <smihai@entrouvert.com>
Date:   Sun May 17 17:01:30 2015 +0200

    statics and theme management (#7070)
    
    Templates and statics upload to organization dir

diff --git a/debian/debian_config.py b/debian/debian_config.py
index 8698793..c6a4ecc 100644
--- a/debian/debian_config.py
+++ b/debian/debian_config.py
@@ -23,4 +23,6 @@ METADATAS_DIR = os.path.join(VAR_DIR, 'metadatas')
 
 SECRET_KEY = file('/etc/%s/secret' % PROJECT_NAME).read()
 
+ORGANIZATIONS_DIR = os.path.join(VAR_DIR, 'organizations')
+
 execfile(os.path.join(ETC_DIR, 'settings.py'))
diff --git a/debian/nginx-example.conf b/debian/nginx-example.conf
index 37a51a8..16a3832 100644
--- a/debian/nginx-example.conf
+++ b/debian/nginx-example.conf
@@ -9,10 +9,11 @@ server {
         access_log  /var/log/nginx/u-auth.example.org-access.log combined;
         error_log  /var/log/nginx/u-auth.example.org-error.log;
 
-        location ~ ^/static/(.+)$ {
+        location ~ ^(.*)/static/(.+)$ {
             root /;
-            try_files /var/lib/u-auth/static/$1
-                      /var/lib/u-auth/collectstatic/$1
+            try_files /var/lib/u-auth/organizations/$1/static/$2
+                      /var/lib/u-auth/static/$2
+                      /var/lib/u-auth/collectstatic/$2
                       =404;
         }
 
diff --git a/debian/u-auth.dirs b/debian/u-auth.dirs
index 0e857be..a998fe0 100644
--- a/debian/u-auth.dirs
+++ b/debian/u-auth.dirs
@@ -1,5 +1,6 @@
 /etc/u-auth
 /usr/lib/u-auth
+/var/lib/u-auth/organizations
 /var/lib/u-auth/collectstatic
 /var/lib/u-auth/static
 /var/lib/u-auth/templates
diff --git a/uauth/context_processors.py b/uauth/context_processors.py
new file mode 100644
index 0000000..a5ad489
--- /dev/null
+++ b/uauth/context_processors.py
@@ -0,0 +1,14 @@
+import os
+
+from django.conf import settings
+from django.template.loader import get_template, TemplateDoesNotExist
+
+def theme_base(request):
+    if request.session.get('organization'):
+        try:
+            base = get_template('base.html', [os.path.join(settings.ORGANIZATIONS_DIR,
+                                request.session['organization'], 'templates')])
+        except TemplateDoesNotExist:
+            base = get_template('uauth/base.html')
+
+    return {'theme_base': base}
diff --git a/uauth/organization/forms.py b/uauth/organization/forms.py
index 6bfb5b2..2c77f71 100644
--- a/uauth/organization/forms.py
+++ b/uauth/organization/forms.py
@@ -31,3 +31,10 @@ class LocalAccountCreateForm(LocalAccountForm):
 
 class UsersImportForm(forms.Form):
     users_file = forms.FileField(_('Users file'))
+
+
+class TemplateForm(forms.Form):
+    template_file = forms.FileField(_('Template file'))
+
+class StaticForm(forms.Form):
+    static_file = forms.FileField(_('Static file'))
diff --git a/uauth/organization/templates/organization/manage.html b/uauth/organization/templates/organization/manage.html
index e47c5d3..6435f23 100644
--- a/uauth/organization/templates/organization/manage.html
+++ b/uauth/organization/templates/organization/manage.html
@@ -4,5 +4,6 @@
 {% block content %}
 <ul class="apps">
   <li class="users"><a href="{% url 'manage-users' organization.slug %}">{% trans 'Users' %}</a></li>
+  <li class="theme"><a href="{% url 'manage-theme' organization.slug %}">{% trans 'Theme' %}</a></li>
 </ul>
 {% endblock %}
diff --git a/uauth/organization/templates/organization/theme.html b/uauth/organization/templates/organization/theme.html
new file mode 100644
index 0000000..7b5e0de
--- /dev/null
+++ b/uauth/organization/templates/organization/theme.html
@@ -0,0 +1,45 @@
+{% extends "organization/base.html" %}
+{% load i18n %}
+
+{% block page-title %}
+{% trans 'Theme management' %}
+{% endblock %}
+
+{% block appbar %}
+<h2>{% trans "Theme" %}</h2>
+<a href="{% url "static-upload" organization.slug %}" rel="popup">{% trans "Upload static" %}</a>
+<a href="{% url "template-upload" organization.slug %}" rel="popup">{% trans "Upload template" %}</a>
+
+<h3>{% trans "Templates" %}</h3>
+<form action='{% url "template-delete" organization.slug %}'>
+<table>
+  <thead>
+    <tr><td>{% trans "Filename" %}</td><td></td></tr>
+  </thead>
+  <tbody>
+  {% for template in templates %}
+  <tr><td>{{ template }}</td><td> <button name="template" value="{{ template }}" class="icon-delete">{% trans "Remove" %}</button></td></tr>
+  {% empty %}
+  <tr><td colspan=2>{% trans "No templates uploaded yet" %}</td></tr>
+  {% endfor %}
+  </tbody>
+</table>
+</form>
+
+<h3>{% trans "Statics" %}</h3>
+<form action='{% url "static-delete" organization.slug %}'>
+  <table>
+    <thead>
+      <tr><td>{% trans "Filename" %}</td><td></td></tr>
+    </thead>
+    <tbody>
+  {% for static in statics %}
+  <tr><td>{{ static }}</td><td><button name="static" value="{{ static }}" class="icon-delete">{% trans "Remove" %}</button></td></tr>
+  {% empty %}
+  <tr><td>{% trans "No statics uploaded yet" %}</td></tr>
+  {% endfor %}
+    </tbody>
+  </table>
+  </form>
+{% endblock %}
+
diff --git a/uauth/organization/templates/organization/upload.html b/uauth/organization/templates/organization/upload.html
new file mode 100644
index 0000000..8577479
--- /dev/null
+++ b/uauth/organization/templates/organization/upload.html
@@ -0,0 +1,21 @@
+{% extends "organization/base.html" %}
+{% load i18n %}
+
+{% block more-user-links %}
+{{ block.super }}
+  <a href="{% url "manage-theme" organization.slug %}">{% trans 'Theme' %}</a>
+{% endblock %}
+
+{% block appbar %}
+<h2>{% trans "Theme" %}</h2>
+<a href="{% url "static-upload" organization.slug %}" rel="popup">{% trans "Upload static" %}</a>
+<a href="{% url "template-upload" organization.slug %}" rel="popup">{% trans "Upload template" %}</a>
+{% endblock %}
+
+{% block content %}
+<form method="post" enctype="multipart/form-data">
+  {% csrf_token %}
+  {{ form.as_p }}
+  <p><button name="upload">{% trans "Upload" %}</button>
+</form>
+{% endblock %}
diff --git a/uauth/organization/urls.py b/uauth/organization/urls.py
index a169b26..7dd2f02 100644
--- a/uauth/organization/urls.py
+++ b/uauth/organization/urls.py
@@ -9,4 +9,9 @@ urlpatterns = patterns('',
     url(r'^users/import$', import_users, name='import-users'),
     url(r'^users/(?P<pk>[\w]+)/$', view_user, name='view-user'),
     url(r'^users/(?P<pk>[\w]+)/edit$', edit_user, name='edit-user'),
+    url(r'^theme/?$', theme, name='manage-theme'),
+    url(r'^theme/template/upload$', template_upload, name='template-upload'),
+    url(r'^theme/template/delete$', template_delete, name='template-delete'),
+    url(r'^theme/static/upload$', static_upload, name='static-upload'),
+    url(r'^theme/static/delete$', static_delete, name='static-delete'),
 )
diff --git a/uauth/organization/views.py b/uauth/organization/views.py
index fb66fd2..911a7d8 100644
--- a/uauth/organization/views.py
+++ b/uauth/organization/views.py
@@ -1,6 +1,8 @@
+import os
 import csv
 import datetime
 
+from django.conf import settings
 from django.utils.translation import ugettext as _
 from django.core.urlresolvers import reverse_lazy
 from django.shortcuts import render, redirect
@@ -15,7 +17,7 @@ from django_tables2 import RequestConfig
 
 from .utils import create_user, create_or_update_users
 from .models import LocalAccount, Organization
-from .forms import LocalAccountCreateForm, LocalAccountForm, UsersImportForm
+from .forms import *
 from .tables import AccountTable
 
 
@@ -153,3 +155,111 @@ class ImportUsersView(OrganizationMixin, TemplateView):
             return self.render_to_response(context)
 
 import_users = ImportUsersView.as_view()
+
+
+class ThemeView(OrganizationMixin, TemplateView):
+    template_name = 'organization/theme.html'
+
+    def get_success_url(self):
+        return reverse_lazy('manage-theme', kwargs={'organization_slug': self.kwargs['organization_slug']})
+
+    def get_context_data(self, **kwargs):
+        ctx = super(ThemeView, self).get_context_data(**kwargs)
+        organization = ctx['organization']
+        templates_dir = os.path.join(settings.ORGANIZATIONS_DIR,
+                                organization.slug, 'templates')
+        statics_dir = os.path.join(settings.ORGANIZATIONS_DIR,
+                                organization.slug, 'static')
+        ctx['templates'] = []
+        ctx['statics'] = []
+        if os.path.exists(templates_dir):
+            ctx['templates'] = os.listdir(templates_dir)
+        if os.path.exists(statics_dir):
+            ctx['statics'] = os.listdir(statics_dir)
+        ctx['templates_dir'] = templates_dir
+        ctx['statics_dir'] = statics_dir
+        return ctx
+
+theme = ThemeView.as_view()
+
+class UploadMixin(object):
+    template_name = "organization/upload.html"
+
+    def get_context_data(self, **kwargs):
+        ctx = super(UploadMixin, self).get_context_data(**kwargs)
+        ctx['form'] = self.form_class()
+        return ctx
+
+    def post(self, request, *args, **kwargs):
+        form = self.form_class(request.POST, request.FILES)
+        context = self.get_context_data(**kwargs)
+        context['form'] = form
+        organization = context['organization']
+        destination_dir = os.path.join(settings.ORGANIZATIONS_DIR,
+                                       organization.slug, self.upload_dir)
+        if form.is_valid():
+            data = form.cleaned_data[self.filename_param]
+            if not os.path.exists(destination_dir):
+                os.makedirs(destination_dir)
+            try:
+                with open(os.path.join(destination_dir, data.name), 'w') as template:
+                    template.write(data.read())
+                messages.info(request, _('File "%s" successfully uploaded') % data.name)
+            except OSError:
+                messages.error(request, _('An error occured while uploading file "%s"') % data.name)
+            return redirect(self.get_success_url())
+        else:
+            return self.render_to_response(context)
+
+
+class TemplateUpload(UploadMixin, ThemeView):
+    form_class = TemplateForm
+    filename_param = 'template_file'
+    upload_dir = 'templates'
+
+template_upload = TemplateUpload.as_view()
+
+
+class TemplateDelete(ThemeView):
+
+    def get(self, request, *args, **kwargs):
+        ctx = self.get_context_data(**kwargs)
+        template = request.GET.get('template')
+        if os.path.exists(os.path.join(ctx['templates_dir'], template)):
+            try:
+                os.remove(os.path.join(ctx['templates_dir'], template))
+                messages.info(request, _('Template %s successfully removed') % template)
+            except IOError:
+                messages.error(request, _('An error occured while removing file %s') % template)
+        else:
+            messages.error(request, _('Unknown template %s') % template)
+        return redirect(self.get_success_url())
+
+
+template_delete = TemplateDelete.as_view()
+
+
+class StaticUpload(UploadMixin, ThemeView):
+    form_class = StaticForm
+    filename_param = 'static_file'
+    upload_dir = 'static'
+
+static_upload = StaticUpload.as_view()
+
+
+class StaticDelete(ThemeView):
+
+    def get(self, request, *args, **kwargs):
+        ctx = self.get_context_data(**kwargs)
+        static = request.GET.get('static')
+        if os.path.exists(os.path.join(ctx['statics_dir'], static)):
+            try:
+                os.remove(os.path.join(ctx['statics_dir'], static))
+                messages.info(request, _('Static file %s successfully removed') % static)
+            except IOError:
+                messages.error(request, _('An error occured while removing file %s') % static)
+        else:
+            messages.error(request, _('Unknown static %s') % static)
+        return redirect(self.get_success_url())
+
+static_delete = StaticDelete.as_view()
diff --git a/uauth/settings.py b/uauth/settings.py
index 4ff8a09..39b5ff5 100644
--- a/uauth/settings.py
+++ b/uauth/settings.py
@@ -54,7 +54,9 @@ MIDDLEWARE_CLASSES = (
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
 )
 
-TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + ('django.core.context_processors.request',)
+TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + \
+                              ('django.core.context_processors.request',
+                               'uauth.context_processors.theme_base',)
 
 ROOT_URLCONF = 'uauth.urls'
 
@@ -97,6 +99,8 @@ LDAP_CONF = {
     'dn': 'ou=radius,dc=entrouvert,dc=org',
 }
 
+ORGANIZATIONS_DIR = os.path.join(BASE_DIR, 'organizations')
+
 AUTHENTICATION_BACKENDS = global_settings.AUTHENTICATION_BACKENDS + (
     'mellon.backends.SAMLBackend',
     'uauth.backends.LocalAccountPasswordBackend',
diff --git a/uauth/static/css/icons/icon-ressources-hover.png b/uauth/static/css/icons/icon-ressources-hover.png
new file mode 100644
index 0000000..09d6614
Binary files /dev/null and b/uauth/static/css/icons/icon-ressources-hover.png differ
diff --git a/uauth/static/css/icons/icon-ressources.png b/uauth/static/css/icons/icon-ressources.png
new file mode 100644
index 0000000..d48f51b
Binary files /dev/null and b/uauth/static/css/icons/icon-ressources.png differ
diff --git a/uauth/static/css/style.css b/uauth/static/css/style.css
index 12bb306..24d40ce 100644
--- a/uauth/static/css/style.css
+++ b/uauth/static/css/style.css
@@ -74,4 +74,5 @@ div.example {
 }
 
 /* icons */
-li.users a {background-image: url(icons/icon-personnes.png);}
\ No newline at end of file
+li.users a {background-image: url(icons/icon-personnes.png);}
+li.theme a {background-image: url(icons/icon-ressources.png);}
\ No newline at end of file
diff --git a/uauth/templates/uauth/organization.html b/uauth/templates/uauth/organization.html
index dbdf0b1..a02269d 100644
--- a/uauth/templates/uauth/organization.html
+++ b/uauth/templates/uauth/organization.html
@@ -1,4 +1,4 @@
-{% extends "uauth/base.html" %}
+{% extends theme_base %}
 {% load i18n %}
 
 {% block more-user-links %}
diff --git a/uauth/views.py b/uauth/views.py
index e9450be..726c61f 100644
--- a/uauth/views.py
+++ b/uauth/views.py
@@ -77,6 +77,7 @@ class OrganizationPageView(LoginMixin, FormView):
         context = super(OrganizationPageView, self).get_context_data(**kwargs)
         idps = get_idp_list()
         organization = Organization.objects.get(slug=self.kwargs['organization_slug'])
+        self.request.session['organization'] = organization.slug
         self.request.session[organization.slug] = self.request.GET.urlencode()
         relay = signing.dumps({'organization': organization.slug})
         context.update({'idps': idps,
