From f4843b93ea02835d0305ef01e0f94a39798d0e3a Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Sun, 17 May 2015 17:01:30 +0200 Subject: [PATCH] statics and theme management (#7070) Templates and statics upload to organization dir --- debian/debian_config.py | 2 + debian/nginx-example.conf | 7 +- debian/u-auth.dirs | 1 + uauth/context_processors.py | 14 +++ uauth/organization/forms.py | 7 ++ .../templates/organization/manage.html | 1 + .../templates/organization/theme.html | 45 +++++++ .../templates/organization/upload.html | 21 ++++ uauth/organization/urls.py | 5 + uauth/organization/views.py | 112 +++++++++++++++++- uauth/settings.py | 6 +- .../css/icons/icon-ressources-hover.png | Bin 0 -> 3371 bytes uauth/static/css/icons/icon-ressources.png | Bin 0 -> 3352 bytes uauth/static/css/style.css | 3 +- uauth/templates/uauth/organization.html | 2 +- uauth/views.py | 1 + 16 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 uauth/context_processors.py create mode 100644 uauth/organization/templates/organization/theme.html create mode 100644 uauth/organization/templates/organization/upload.html create mode 100644 uauth/static/css/icons/icon-ressources-hover.png create mode 100644 uauth/static/css/icons/icon-ressources.png 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 %} {% 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 %} +

{% trans "Theme" %}

+{% trans "Upload static" %} +{% trans "Upload template" %} + +

{% trans "Templates" %}

+
+ + + + + + {% for template in templates %} + + {% empty %} + + {% endfor %} + +
{% trans "Filename" %}
{{ template }}
{% trans "No templates uploaded yet" %}
+
+ +

{% trans "Statics" %}

+
+ + + + + + {% for static in statics %} + + {% empty %} + + {% endfor %} + +
{% trans "Filename" %}
{{ static }}
{% trans "No statics uploaded yet" %}
+
+{% 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 }} + {% trans 'Theme' %} +{% endblock %} + +{% block appbar %} +

{% trans "Theme" %}

+{% trans "Upload static" %} +{% trans "Upload template" %} +{% endblock %} + +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} +

+

+{% 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[\w]+)/$', view_user, name='view-user'), url(r'^users/(?P[\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 0000000000000000000000000000000000000000..09d6614311acec0cf903ca70e812633e87847339 GIT binary patch literal 3371 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 BVz&SQ literal 0 HcmV?d00001 diff --git a/uauth/static/css/icons/icon-ressources.png b/uauth/static/css/icons/icon-ressources.png new file mode 100644 index 0000000000000000000000000000000000000000..d48f51bb95453b91e8e6f2cad21762f85e03686e GIT binary patch literal 3352 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 iBY;c&HPx;!1pN;&AhVGHHBPet0000