From a74f6d0ee441b7487490f23c8926e1fbbcd9be36 Mon Sep 17 00:00:00 2001 From: Simon Caminada Date: Wed, 21 Mar 2018 20:26:24 +0100 Subject: [PATCH] memberzone --- private/js/modules/control-panel.js | 89 +++++++ private/js/modules/downloads.js | 47 ++++ private/scss/_config.scss | 1 + private/scss/main.scss | 2 + private/scss/modules/_control-panel.scss | 195 ++++++++++++++++ private/scss/modules/_downloads.scss | 66 ++++++ .../modules/plugins/_download_section.scss | 2 + requirements.in | 1 + settings.py | 1 + src/memberzone/__init__.py | 0 src/memberzone/admin.py | 120 ++++++++++ src/memberzone/forms.py | 8 + src/memberzone/migrations/0001_initial.py | 158 +++++++++++++ .../migrations/0002_auto_20180321_1256.py | 24 ++ src/memberzone/migrations/__init__.py | 0 src/memberzone/models.py | 219 ++++++++++++++++++ src/memberzone/storage.py | 56 +++++ .../templates/memberzone/change_password.html | 16 ++ .../memberzone/change_password_done.html | 11 + ...ember_registration_confirmation_email.html | 14 ++ .../memberzone/includes/_downloads.html | 57 +++++ .../templates/memberzone/includes/_field.html | 8 + .../templates/memberzone/overview.html | 176 ++++++++++++++ .../templates/memberzone/profile_edit.html | 17 ++ .../memberzone/profile_edit_done.html | 11 + src/memberzone/templates/memberzone/task.html | 139 +++++++++++ .../templates/memberzone/task_success.html | 20 ++ .../templates/registration/login.html | 16 ++ src/memberzone/templatetags/__init__.py | 0 src/memberzone/templatetags/task_tags.py | 34 +++ src/memberzone/urls.py | 44 ++++ src/memberzone/views.py | 170 ++++++++++++++ src/project/templates/main.html | 2 +- src/project/templates/project/assets/dots.svg | 6 + src/project/templates/project/assets/tick.svg | 4 + .../project/includes/navigation.html | 2 +- .../plugins/content/download_section.html | 3 + src/project/templatetags/util_tags.py | 2 +- src/project/urls.py | 4 +- 39 files changed, 1741 insertions(+), 4 deletions(-) create mode 100644 private/js/modules/control-panel.js create mode 100644 private/js/modules/downloads.js create mode 100644 private/scss/modules/_control-panel.scss create mode 100644 private/scss/modules/_downloads.scss create mode 100644 src/memberzone/__init__.py create mode 100644 src/memberzone/admin.py create mode 100644 src/memberzone/forms.py create mode 100644 src/memberzone/migrations/0001_initial.py create mode 100644 src/memberzone/migrations/0002_auto_20180321_1256.py create mode 100644 src/memberzone/migrations/__init__.py create mode 100644 src/memberzone/models.py create mode 100644 src/memberzone/storage.py create mode 100644 src/memberzone/templates/memberzone/change_password.html create mode 100644 src/memberzone/templates/memberzone/change_password_done.html create mode 100644 src/memberzone/templates/memberzone/email/member_registration_confirmation_email.html create mode 100644 src/memberzone/templates/memberzone/includes/_downloads.html create mode 100644 src/memberzone/templates/memberzone/includes/_field.html create mode 100644 src/memberzone/templates/memberzone/overview.html create mode 100644 src/memberzone/templates/memberzone/profile_edit.html create mode 100644 src/memberzone/templates/memberzone/profile_edit_done.html create mode 100644 src/memberzone/templates/memberzone/task.html create mode 100644 src/memberzone/templates/memberzone/task_success.html create mode 100644 src/memberzone/templates/registration/login.html create mode 100644 src/memberzone/templatetags/__init__.py create mode 100644 src/memberzone/templatetags/task_tags.py create mode 100644 src/memberzone/urls.py create mode 100644 src/memberzone/views.py create mode 100644 src/project/templates/project/assets/dots.svg create mode 100644 src/project/templates/project/assets/tick.svg diff --git a/private/js/modules/control-panel.js b/private/js/modules/control-panel.js new file mode 100644 index 0000000..994e7f6 --- /dev/null +++ b/private/js/modules/control-panel.js @@ -0,0 +1,89 @@ +$(function() { + 'use strict'; + + var $body = $('body'); + + $body.on('click', '.control__item__open', function(event) { + event.preventDefault(); + var $button = $(this); + var $control_item = $button.parents('.control__item'); + $control_item.addClass('loading'); + + $.get($button.attr('data-href'), function(data) { + var $control_item_content = $control_item.find('.control__item__content'); + var $control_item_content_main = $control_item_content.find('.control__item__content__main'); + + var cleaned_data = data.replace(/autofocus/g, ''); + $control_item_content_main.html(cleaned_data); + + $control_item_content.height($control_item_content_main.outerHeight(true)); + + $control_item.addClass('active'); + + window.on_transitionend($control_item_content, function(event) { + if (!event || event.target === $control_item_content[0]) { + $control_item_content.off(window.transitionend); + $control_item_content.removeAttr('style'); + $control_item.removeClass('loading'); + } + }); + }); + }); + + $body.on('click', '.control__item__close', function(event) { + event.preventDefault(); + var $button = $(this); + var $control_item = $button.parents('.control__item'); + + var $control_item_content = $control_item.find('.control__item__content'); + var $control_item_content_main = $control_item_content.find('.control__item__content__main'); + $control_item_content.height($control_item_content_main.outerHeight(true)); + + window.on_transitionend($control_item_content, function(event) { + if (!event || event.target === $control_item_content[0]) { + $control_item_content.off(window.transitionend); + $control_item_content_main.html(''); + } + }); + + window.requestAnimationFrame(function() { + $control_item.removeClass('active'); + $control_item_content.removeAttr('style'); + }); + }); + + $body.on('submit', '.control__item form', function(event) { + event.preventDefault(); + var $form = $(this); + $.ajax({ + type: $form.attr('method'), + url: $form.attr('action'), + data: $form.serialize(), + success: function(data) { + if ($(data).hasClass('control__item__success')) { + var $control_item = $form.parents('.control__item'); + $control_item.find('.control__item__close').trigger('click'); + } else { + $form.replaceWith(data); + } + } + }); + }); + + var $task_form = $('.task__form'); + + if ($task_form.hasClass('errors')) { + window.location = '#form'; + } + + $task_form.formset(); + $task_form.on('formAdded', function(event) { + var $title = $(event.target).find('h3'); + var id = parseInt($title.attr('data-id').match(/\d+/)[0]); + $title.html($title.html().replace('%(id)', id + 1)); + }); + + $task_form.on('formDeleted', function(event) { + $(event.target).hide(); + }); +}); \ No newline at end of file diff --git a/private/js/modules/downloads.js b/private/js/modules/downloads.js new file mode 100644 index 0000000..d5d04be --- /dev/null +++ b/private/js/modules/downloads.js @@ -0,0 +1,47 @@ +$(function() { + 'use strict'; + + var $body = $('body'); + + var download_texts = []; + + $('.downloads__item__text').each(function() { + var text = $(this).text().toLowerCase(); + text = text + '' + $(this).next().text().toLowerCase(); + download_texts.push({ + $element: $(this).parents('.downloads__item__frame'), + text: text + }); + }); + + $body.on('input', '#downloads_search', function(event) { + var query = $(this).val().toLowerCase(); + var query_list = $.trim(query).split(' '); + var matches = []; + + for (var i = 0; i < download_texts.length; i++) { + var download_text_item = download_texts[i]; + var matched = false; + if (!matched) { + for (var ii = 0; ii < query_list.length; ii++) { + var query_item = query_list[ii]; + if (download_text_item.text.indexOf(query_item) >= 0) { + matched = true; + matches.push(download_text_item.$element); + } + } + } + } + + $('.downloads__item__frame').each(function() { + $(this).parents('.downloads__section').css('display', 'none'); + $(this).css('display', 'none'); + }); + for (i = 0; i < matches.length; i++) { + matches[i].removeAttr('style'); + matches[i].parents('.downloads__section').removeAttr('style'); + } + }); + + $('#downloads_search').trigger('input'); +}); \ No newline at end of file diff --git a/private/scss/_config.scss b/private/scss/_config.scss index 954af0b..b563aa8 100644 --- a/private/scss/_config.scss +++ b/private/scss/_config.scss @@ -1,4 +1,5 @@ $white: #FFFFFF; +$white_gray: #F9F9F9; $light_gray: #F4F4F4; $medium_light_gray: #E6E6E6; $gray: #ADADAD; diff --git a/private/scss/main.scss b/private/scss/main.scss index 36be604..579f000 100644 --- a/private/scss/main.scss +++ b/private/scss/main.scss @@ -13,6 +13,8 @@ @import "modules/_contact.scss"; @import "modules/_content.scss"; @import "modules/_admin_editor.scss"; +@import "modules/_downloads.scss"; +@import "modules/_control-panel.scss"; @import "modules/plugins/_quote.scss"; @import "modules/plugins/_slider.scss"; @import "modules/plugins/_section.scss"; diff --git a/private/scss/modules/_control-panel.scss b/private/scss/modules/_control-panel.scss new file mode 100644 index 0000000..cc0ea32 --- /dev/null +++ b/private/scss/modules/_control-panel.scss @@ -0,0 +1,195 @@ +.control_panel { + width: 100%; + padding: em(100px) 0; + will-change: transform, height; + &.reveal_animation { + transition: background $reveal_duration $reveal_timing_function; + &.reveal { + background: transparent; + } + } + .load__replace { + margin-top: em(40px); + text-align: left; + } +} + +.control__list { + li { + display: block; + } +} + +.control_panel__content { + max-width: em(1440px); + margin: 0 auto; + padding: 0 em(80px); + font-size: 0; + @media screen and (max-width: $large_breakpoint) { + padding: 0 em(60px); + } + @media screen and (max-width: $medium_breakpoint) { + max-width: em(600px); + padding: 0 em(30px); + } +} + +.control__item { + display: block; + background: $white_gray; + text-decoration: none; + width: 100%; + margin-bottom: em(15px); + position: relative; +} + +.control__item__open { + opacity: 1; + z-index: 2; + transition: opacity 0.2s $easeOutQuad; + .active & { + opacity: 0; + z-index: 1; + } +} + +.control__item__close { + opacity: 0; + z-index: 1; + transition: opacity 0.2s $easeOutQuad; + .active & { + opacity: 1; + z-index: 2; + } +} + +.control__item__success { + max-width: em(350px); + .tag { + display: block; + } + .button { + margin-top: em(20px); + } +} + +.control__item__title { + display: block; + width: 100%; + padding: em(22px) em(30px); + position: relative; + font-size: em(18px); + color: $dark_gray; + .control__item--button & { + padding-right: em(240px); + .button { + position: absolute; + top: em(16px); + right: em(30px); + width: em(210px); + } + @media screen and (max-width: $small_breakpoint) { + padding-right: em(30px); + padding-bottom: em(75px); + .button { + top: auto; + right: auto; + left: em(30px); + bottom: em(22px); + } + } + } + .control__item--status & { + padding-left: em(85px); + } + .control__item--arrow & { + padding-right: em(85px); + &:hover { + .control__item__arrow { + transform: translateX(em(5px)); + } + } + } +} + +.control__item__content { + height: 0; + overflow: hidden; + transition: height 0.5s $easeOutQuad; + will-change: height; + .active & { + height: auto; + } +} + +.control__item__content__main { + border-top: 1px solid $gray; + padding: em(30px); +} + +.control__item__fields { + margin: em(10px) 0 em(50px); +} + +.control__item__status { + display: inline-block; + position: absolute; + top: 50%; + left: em(20px); + margin-top: em(-20px); + border-radius: 50%; + border: 2px solid $gray; + svg { + display: block; + width: em(36px); + height: em(36px); + fill: $white; + } + .control__item--status--active & { + background: $green; + border-color: $green; + } + .control__item--status--expired & { + background: $red; + border-color: $red; + } +} + +.control__item__arrow { + display: inline-block; + position: absolute; + top: 50%; + right: em(20px); + margin-top: em(-20px); + transform: none; + transition: transform 0.3s $easeOutQuart; + svg { + display: block; + width: em(40px); + height: em(40px); + transform: rotate(-90deg); + } + .fill { + fill: $green; + } +} + +.todo { + display: inline-block; + vertical-align: top; + width: 48%; + margin-right: 4%; + @media screen and (max-width: $large_breakpoint) { + width: 100%; + margin: 0 0 em(40px) 0; + } +} + +.settings { + display: inline-block; + vertical-align: top; + width: 48%; + @media screen and (max-width: $large_breakpoint) { + width: 100%; + } +} \ No newline at end of file diff --git a/private/scss/modules/_downloads.scss b/private/scss/modules/_downloads.scss new file mode 100644 index 0000000..3d69af8 --- /dev/null +++ b/private/scss/modules/_downloads.scss @@ -0,0 +1,66 @@ +.downloads__frame { + width: 100%; + padding: em(80px) 0 em(150px); + position: relative; + will-change: transform; + .downloads { + li { + width: 33.3333%; + } + } +} + +.downloads__content { + max-width: em(1440px); + margin: 0 auto; + padding: 0 em(80px); + @media screen and (max-width: $large_breakpoint) { + padding: 0 em(60px); + } + @media screen and (max-width: $medium_breakpoint) { + max-width: em(600px); + padding: 0 em(30px); + } +} + +.downloads__section { + margin-top: em(80px); + .downloads--flat & { + margin-top: 0; + } +} + +.downloads__section__title { + width: 100%; + overflow: hidden; + h3 { + display: inline-block; + position: relative; + &:after { + content: ''; + display: block; + position: absolute; + background: $white; + width: 100vw; + height: em(3px); + opacity: 0.15; + top: em(10.5px); + left: 100%; + margin-left: em(20px); + } + } +} + +.downloads__section__title--flat { + h3 { + &:after { + display: none; + } + } +} + +.downloads__list { + display: block; + margin: 0 em(-10px); + font-size: 0; +} diff --git a/private/scss/modules/plugins/_download_section.scss b/private/scss/modules/plugins/_download_section.scss index 25c5d2d..014520d 100644 --- a/private/scss/modules/plugins/_download_section.scss +++ b/private/scss/modules/plugins/_download_section.scss @@ -67,6 +67,8 @@ font-size: em(11px); color: $white; font-weight: 500; + padding: 0 em(5px); + text-shadow: 0 1px 0 $green, 1px 0 0 $green, 0 -1px 0 $green, -1px 0 0 $green; &:before, &:after { display: block; content: ''; diff --git a/requirements.in b/requirements.in index 94ac202..4a006d2 100644 --- a/requirements.in +++ b/requirements.in @@ -10,6 +10,7 @@ https://control.divio.com/api/v1/apps/serve/djangocms-picture/2.0.6/005e8663-d1c https://control.divio.com/api/v1/apps/serve/djangocms-text-ckeditor/3.5.3/a7b5179f-cea5-4af8-b235-6b7f709c4e6a/djangocms-text-ckeditor-3.5.3.tar.gz#egg=djangocms-text-ckeditor==3.5.3 https://control.divio.com/api/v1/apps/serve/django-filer/1.3.0.1/bcb7d25b-6922-48a9-a252-9bc165f6403e/django-filer-1.3.0.1.tar.gz#egg=django-filer==1.3.0.1 # +lxml Whoosh==2.7.4 aldryn-search==0.5.0 django-fontawesome==0.3.1 diff --git a/settings.py b/settings.py index 345f76e..ee6bea6 100644 --- a/settings.py +++ b/settings.py @@ -25,6 +25,7 @@ aldryn_addons.settings.load(locals()) INSTALLED_APPS.extend([ 'project', + 'memberzone', 'fontawesome', 'haystack', 'aldryn_search', diff --git a/src/memberzone/__init__.py b/src/memberzone/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/memberzone/admin.py b/src/memberzone/admin.py new file mode 100644 index 0000000..9f19583 --- /dev/null +++ b/src/memberzone/admin.py @@ -0,0 +1,120 @@ +import xlwt +import json + +from aldryn_forms.utils import get_user_model +from django.conf.urls import url +from django.contrib.admin.utils import unquote +from django.http import HttpResponse +from django.utils.text import slugify +from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ + +from memberzone.models import MemberTask, MemberTaskFormField, MemberTaskRegistration, MemberDownloadFile, \ + MemberDownloadTag, Profile, MemberDownloadSection + +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin + +User = get_user_model() + + +class ProfileInline(admin.StackedInline): + model = Profile + can_delete = False + + +class UserAdmin(BaseUserAdmin): + inlines = [ProfileInline] + fieldsets = ( + (None, {'fields': ('username', 'password')}), + (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups')}), + ) + + +admin.site.unregister(User) +admin.site.register(User, UserAdmin) + + +class MemberTaskFormFieldInlineAdmin(admin.TabularInline): + model = MemberTaskFormField + extra = 0 + prepopulated_fields = { + 'name': ('label',) + } + + +@admin.register(MemberTask) +class MemberTaskAdmin(admin.ModelAdmin): + list_display = ('title', 'publish_date', 'deadline') + list_filter = ('groups',) + filter_horizontal = ('groups',) + prepopulated_fields = { + 'slug': ('title',) + } + inlines = [MemberTaskFormFieldInlineAdmin, ] + + def get_urls(self): + info = self.model._meta.app_label, self.model._meta.model_name + return [ + url( + r'^(.+)/export/$', + self.admin_site.admin_view(self.participants_export_view), + name='%s_%s_participants_export' % info, + ), + ] + super(MemberTaskAdmin, self).get_urls() + + def participants_export_view(self, request, object_id, *args, **kwargs): + obj = self.get_object(request, unquote(object_id)) + wb = xlwt.Workbook() + ws = wb.add_sheet('Teilnehmerliste') + r = 0 + c = 0 + fields = [] + for field in obj.fields.all(): + ws.write(r, c, field.label) + fields.append(field.name.replace('-', '_')) + c += 1 + for participant in obj.registrations.all(): + for data_set in json.loads(participant.form_data): + r += 1 + c = 0 + for field in fields: + value = data_set.get(field, '') + if isinstance(value, list): + value = ', '.join(value) + ws.write(r, c, value) + c += 1 + + response = HttpResponse(content_type="application/ms-excel") + response['Content-Disposition'] = 'attachment; filename=%s-teilnehmer.xls' % slugify(obj.title) + wb.save(response) + return response + + +@admin.register(MemberTaskRegistration) +class MemberTaskRegistrationAdmin(admin.ModelAdmin): + readonly_fields = ['task', 'user', 'form_data', 'cdate'] + + def has_delete_permission(self, request, obj=None): + return False + + def has_add_permission(self, request): + return False + + +@admin.register(MemberDownloadSection) +class MemberDownloadSectionAdmin(admin.ModelAdmin): + list_display = ('title', 'ordering') + list_editable = ['ordering'] + + +@admin.register(MemberDownloadTag) +class MemberDownloadTagAdmin(admin.ModelAdmin): + pass + + +@admin.register(MemberDownloadFile) +class MemberDownloadFileAdmin(admin.ModelAdmin): + list_display = ('label', 'ordering') + list_editable = ['ordering'] + list_filter = ('section', 'groups') + filter_horizontal = ('groups', 'tags') diff --git a/src/memberzone/forms.py b/src/memberzone/forms.py new file mode 100644 index 0000000..752a23b --- /dev/null +++ b/src/memberzone/forms.py @@ -0,0 +1,8 @@ +from django.contrib.auth.forms import AuthenticationForm + + +class LoginForm(AuthenticationForm): + def __init__(self, *args, **kwargs): + super(LoginForm, self).__init__(*args, **kwargs) + for _, value in self.fields.items(): + value.widget.attrs['placeholder'] = value.label diff --git a/src/memberzone/migrations/0001_initial.py b/src/memberzone/migrations/0001_initial.py new file mode 100644 index 0000000..ac9a6fa --- /dev/null +++ b/src/memberzone/migrations/0001_initial.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-03-21 12:32 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import djangocms_text_ckeditor.fields +import filer.fields.folder +import filer.fields.image + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('auth', '0008_alter_user_username_max_length'), + migrations.swappable_dependency(settings.FILER_IMAGE_MODEL), + ('filer', '0007_auto_20161016_1055'), + ] + + operations = [ + migrations.CreateModel( + name='MemberDownloadFile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=512, null=True, verbose_name='Name')), + ('file', models.FileField(max_length=512, upload_to='protected_files')), + ('thumbnail', models.FileField(blank=True, null=True, upload_to='protected_thumbnails')), + ('ordering', models.IntegerField(default=50, verbose_name='Sortierung')), + ('groups', models.ManyToManyField(related_name='files', to='auth.Group', verbose_name='Mitgliedergruppen')), + ], + options={ + 'verbose_name_plural': 'Mitglieder Downloads', + 'verbose_name': 'Mitglieder Download', + 'ordering': ['ordering', 'name'], + }, + ), + migrations.CreateModel( + name='MemberDownloadSection', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=100, verbose_name='Title')), + ('ordering', models.IntegerField(default=50, verbose_name='Sortierung')), + ], + options={ + 'verbose_name_plural': 'Mitglieder Download Sections', + 'verbose_name': 'Mitglieder Download Section', + 'ordering': ['ordering'], + }, + ), + migrations.CreateModel( + name='MemberDownloadTag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='Name')), + ], + options={ + 'verbose_name_plural': 'Mitglieder Download Tags', + 'verbose_name': 'Mitglieder Download Tag', + }, + ), + migrations.CreateModel( + name='MemberTask', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=100, verbose_name='Title')), + ('slug', models.SlugField(unique=True, verbose_name='slug')), + ('sub_title', models.CharField(blank=True, max_length=100, verbose_name='Untertitle')), + ('bodytext', djangocms_text_ckeditor.fields.HTMLField(verbose_name='Inhalt')), + ('publish_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Veröffentlichungsdatum')), + ('deadline', models.DateTimeField(blank=True, null=True, verbose_name='Anmeldeschluss')), + ('max_num', models.IntegerField(default=1, verbose_name='max. Formulare')), + ('submit_text', models.CharField(default='Als gelesen markieren', max_length=100, verbose_name='Formular Button Text')), + ('form_bodytext', djangocms_text_ckeditor.fields.HTMLField(blank=True, default='

Jetzt anmelden

', verbose_name='Formular Beschreibung')), + ('success_title', models.CharField(default='Ihre Anmeldung wurde erfasst', max_length=200, verbose_name='Bestätigungstitel')), + ('success_bodytext', djangocms_text_ckeditor.fields.HTMLField(verbose_name='Bestätigungstext')), + ('folder', filer.fields.folder.FilerFolderField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='filer.Folder', verbose_name='Bilder')), + ('groups', models.ManyToManyField(related_name='tasks', to='auth.Group', verbose_name='Mitglieder Gruppe')), + ('image', filer.fields.image.FilerImageField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.FILER_IMAGE_MODEL, verbose_name='Bild')), + ], + options={ + 'verbose_name_plural': 'Mitglieder Aufgaben', + 'verbose_name': 'Mitglieder Aufgabe', + 'ordering': ['-publish_date'], + }, + ), + migrations.CreateModel( + name='MemberTaskFormField', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[('text', 'Text Feld (einzeilig)'), ('textarea', 'Text Feld (mehrzeilig)'), ('email', 'E-Mail Feld'), ('radio', 'Auswahlfeld (eine auswahl)'), ('checkbox', 'Auswahlfeld (mehrere auswahlen)')], max_length=30)), + ('label', models.CharField(max_length=100, verbose_name='Label')), + ('name', models.SlugField(verbose_name='Name')), + ('choices', models.TextField(blank=True, help_text='Werte die bei einem Auswahlfeld zur verfügung stehen sollen. Ein wert pro Zeile', verbose_name='Auswahlwerte')), + ('required', models.BooleanField(default=False, verbose_name='Pflichtfeld')), + ('ordering', models.IntegerField(default=50, verbose_name='Sortierung')), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='memberzone.MemberTask', verbose_name='Mitglieder Aufgabe')), + ], + options={ + 'verbose_name_plural': 'Formularfelder', + 'verbose_name': 'Formularfeld', + 'ordering': ['ordering'], + }, + ), + migrations.CreateModel( + name='MemberTaskRegistration', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('form_data', models.TextField(verbose_name='Form Content')), + ('cdate', models.DateTimeField(auto_now_add=True)), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to='memberzone.MemberTask', verbose_name='Mitglieder Aufgabe')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='registrations', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'Mitglieder Aufgaben Registierungen', + 'verbose_name': 'Mitglieder Aufgabe Registierung', + }, + ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Vorname')), + ('last_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Nachname')), + ('street', models.CharField(blank=True, max_length=255, null=True, verbose_name='Strasse')), + ('zip', models.IntegerField(blank=True, null=True, verbose_name='PLZ')), + ('place', models.CharField(blank=True, max_length=255, null=True, verbose_name='Ort')), + ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-Mail')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'User Profile', + 'verbose_name': 'User Profil', + }, + ), + migrations.AddField( + model_name='memberdownloadfile', + name='section', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='memberzone.MemberDownloadSection', verbose_name='Download Section'), + ), + migrations.AddField( + model_name='memberdownloadfile', + name='tags', + field=models.ManyToManyField(blank=True, null=True, related_name='files', to='memberzone.MemberDownloadTag', verbose_name='Suchbegriffe'), + ), + migrations.AlterUniqueTogether( + name='membertaskregistration', + unique_together=set([('user', 'task')]), + ), + migrations.AlterUniqueTogether( + name='membertaskformfield', + unique_together=set([('task', 'name')]), + ), + ] diff --git a/src/memberzone/migrations/0002_auto_20180321_1256.py b/src/memberzone/migrations/0002_auto_20180321_1256.py new file mode 100644 index 0000000..c171b8b --- /dev/null +++ b/src/memberzone/migrations/0002_auto_20180321_1256.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.11 on 2018-03-21 12:56 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('memberzone', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='memberdownloadfile', + name='thumbnail', + ), + migrations.AddField( + model_name='memberdownloadfile', + name='description', + field=models.TextField(blank=True, null=True, verbose_name='Beschreibung'), + ), + ] diff --git a/src/memberzone/migrations/__init__.py b/src/memberzone/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/memberzone/models.py b/src/memberzone/models.py new file mode 100644 index 0000000..77f0a92 --- /dev/null +++ b/src/memberzone/models.py @@ -0,0 +1,219 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group +from django.core.files.storage import default_storage +from djangocms_text_ckeditor.fields import HTMLField +from django import forms +from django.conf import settings +from django.db import models +from django.urls import reverse_lazy +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ +from filer.fields.folder import FilerFolderField +from filer.fields.image import FilerImageField +from filer.models import Image as ImageModel + +from memberzone.storage import PrivateS3MediaStorage + + +class Profile(models.Model): + user = models.OneToOneField(get_user_model(), related_name='profile', on_delete=models.CASCADE) + first_name = models.CharField(verbose_name=_('Vorname'), max_length=255) + last_name = models.CharField(verbose_name=_('Nachname'), max_length=255) + street = models.CharField(verbose_name=_('Strasse'), max_length=255, null=True, blank=True) + zip = models.IntegerField(verbose_name=_('PLZ'), null=True, blank=True) + place = models.CharField(verbose_name=_('Ort'), max_length=255, null=True, blank=True) + email = models.EmailField(verbose_name=_('E-Mail'), null=True, blank=True) + + class Meta: + verbose_name = _('User Profil') + verbose_name_plural = _('User Profile') + + def __str__(self): + return '{} {}'.format(self.first_name, self.last_name) + + +class MemberTask(models.Model): + groups = models.ManyToManyField(Group, verbose_name=_('Mitglieder Gruppe'), related_name='tasks') + title = models.CharField(max_length=100, verbose_name=_('Title')) + slug = models.SlugField(verbose_name=_('slug'), unique=True) + sub_title = models.CharField(max_length=100, verbose_name=_('Untertitle'), blank=True) + bodytext = HTMLField(verbose_name=_('Inhalt'), configuration='simple_ckeditor') + image = FilerImageField(verbose_name=_('Bild'), blank=True, null=True) + folder = FilerFolderField(verbose_name=_('Bilder'), blank=True, null=True) + publish_date = models.DateTimeField(verbose_name=_('Veröffentlichungsdatum'), default=timezone.now) + deadline = models.DateTimeField(verbose_name=_('Anmeldeschluss'), blank=True, null=True) + + max_num = models.IntegerField(verbose_name=_('max. Formulare'), default=1) + submit_text = models.CharField(max_length=100, verbose_name=_('Formular Button Text'), + default=_('Als gelesen markieren')) + form_bodytext = HTMLField(verbose_name=_('Formular Beschreibung'), configuration='simple_ckeditor', blank=True, + default='

Jetzt anmelden

') + + success_title = models.CharField(verbose_name=_('Bestätigungstitel'), max_length=200, + default=_('Ihre Anmeldung wurde erfasst')) + success_bodytext = HTMLField(verbose_name=_('Bestätigungstext'), configuration='simple_ckeditor') + + class Meta: + verbose_name = _('Mitglieder Aufgabe') + verbose_name_plural = _('Mitglieder Aufgaben') + ordering = ['-publish_date'] + + def __str__(self): + return self.title + + @property + def images(self): + return [x for x in self.folder.files if isinstance(x, ImageModel)] + + def get_absolute_url(self): + return reverse_lazy('memberzone:task', args=[self.slug]) + + @property + def is_expired(self): + return self.deadline and self.deadline <= timezone.now() + + @property + def form_class(self): + fields = self.fields.all() + + class MyDynamicForm(forms.Form): + def __init__(self, *args, **kwargs): + super(MyDynamicForm, self).__init__(*args, **kwargs) + for f in fields: + self.fields[f.name.replace('-', '_')] = f.form_field + + return MyDynamicForm + + +class MemberTaskFormField(models.Model): + FIELD_TYPES = [ + ('text', _('Text Feld (einzeilig)')), + ('textarea', _('Text Feld (mehrzeilig)')), + ('email', _('E-Mail Feld')), + ('radio', _('Auswahlfeld (eine auswahl)')), + ('checkbox', _('Auswahlfeld (mehrere auswahlen)')), + ] + task = models.ForeignKey(MemberTask, related_name='fields', verbose_name=_('Mitglieder Aufgabe')) + type = models.CharField(max_length=30, choices=FIELD_TYPES) + label = models.CharField(max_length=100, verbose_name=_('Label')) + name = models.SlugField(verbose_name=_('Name')) + choices = models.TextField(verbose_name=_('Auswahlwerte'), + help_text=_( + 'Werte die bei einem Auswahlfeld zur verfügung stehen sollen. Ein wert pro Zeile'), + blank=True) + required = models.BooleanField(default=False, verbose_name=_('Pflichtfeld')) + + ordering = models.IntegerField(default=50, verbose_name=_('Sortierung')) + + class Meta: + verbose_name = _('Formularfeld') + verbose_name_plural = _('Formularfelder') + ordering = ['ordering'] + unique_together = ['task', 'name'] + + def __str__(self): + return self.label + + @property + def form_choices(self): + return [(x, x) for x in self.choices.splitlines()] + + @property + def form_field(self): + if self.type == 'email': + return forms.EmailField(label=self.label, required=self.required) + elif self.type == 'textarea': + return forms.CharField(label=self.label, required=self.required, widget=forms.Textarea()) + elif self.type == 'radio': + return forms.ChoiceField(label=self.label, required=self.required, choices=self.form_choices, + widget=forms.RadioSelect) + elif self.type == 'checkbox': + return forms.MultipleChoiceField(label=self.label, required=self.required, choices=self.form_choices, + widget=forms.CheckboxSelectMultiple) + else: + return forms.CharField(label=self.label, required=self.required) + + +class MemberTaskRegistration(models.Model): + task = models.ForeignKey(MemberTask, related_name='registrations', verbose_name=_('Mitglieder Aufgabe')) + user = models.ForeignKey(get_user_model(), related_name='registrations') + form_data = models.TextField(verbose_name=_('Form Content')) + cdate = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name = _('Mitglieder Aufgabe Registierung') + verbose_name_plural = _('Mitglieder Aufgaben Registierungen') + unique_together = ['user', 'task'] + + def __str__(self): + return '{} {}'.format(self.user, self.task) + + +class MemberDownloadSection(models.Model): + title = models.CharField(max_length=100, verbose_name=_('Title')) + ordering = models.IntegerField(default=50, verbose_name=_('Sortierung')) + + class Meta: + verbose_name = _('Mitglieder Download Section') + verbose_name_plural = _('Mitglieder Download Sections') + ordering = ['ordering'] + + def __str__(self): + return self.title + + +class MemberDownloadTag(models.Model): + name = models.CharField(max_length=100, verbose_name=_('Name')) + + class Meta: + verbose_name = _('Mitglieder Download Tag') + verbose_name_plural = _('Mitglieder Download Tags') + + def __str__(self): + return self.name + + +if getattr(settings, 'DEFAULT_STORAGE_DSN', None): + protected_file_storage = PrivateS3MediaStorage() +else: + protected_file_storage = default_storage + + +class MemberDownloadFile(models.Model): + section = models.ForeignKey(MemberDownloadSection, verbose_name=_('Download Section'), related_name='files') + groups = models.ManyToManyField(Group, verbose_name=_('Mitgliedergruppen'), related_name='files') + tags = models.ManyToManyField(MemberDownloadTag, verbose_name=_('Suchbegriffe'), related_name='files', blank=True, + null=True) + + name = models.CharField(max_length=512, verbose_name=_('Name'), blank=True, null=True) + description = models.TextField(verbose_name=_('Beschreibung'), blank=True, null=True) + file = models.FileField(upload_to='protected_files', max_length=512, storage=protected_file_storage) + + ordering = models.IntegerField(default=50, verbose_name=_('Sortierung')) + + class Meta: + verbose_name = _('Mitglieder Download') + verbose_name_plural = _('Mitglieder Downloads') + ordering = ['ordering', 'name'] + + def __str__(self): + return self.label + + @property + def url(self): + return self.file.url + + @property + def extension(self): + return self.file.name.split('.')[-1] + + @property + def label(self): + if self.name: + return self.name + else: + return self.file.name + + @property + def tag_list(self): + return ', '.join(list(self.tags.values_list('name', flat=True))) diff --git a/src/memberzone/storage.py b/src/memberzone/storage.py new file mode 100644 index 0000000..bb3be4b --- /dev/null +++ b/src/memberzone/storage.py @@ -0,0 +1,56 @@ +import re +from aldryn_django.storage import S3MediaStorage +from django.conf import settings + +from boto.s3.connection import ( + SubdomainCallingFormat, + OrdinaryCallingFormat, +) + + +class PrivateS3MediaStorage(S3MediaStorage): + def __init__(self): + bucket_name = settings.AWS_MEDIA_STORAGE_BUCKET_NAME + + if '.' in bucket_name: + calling_format = OrdinaryCallingFormat() + else: + calling_format = SubdomainCallingFormat() + + # We cannot use a function call or a partial here. Instead, we have to + # create a subclass because django tries to recreate a new object by + # calling the __init__ of the returned object (with no arguments). + super(S3MediaStorage, self).__init__( + access_key=settings.AWS_MEDIA_ACCESS_KEY_ID, + secret_key=settings.AWS_MEDIA_SECRET_ACCESS_KEY, + bucket_name=bucket_name, + location=settings.AWS_MEDIA_BUCKET_PREFIX, + host=settings.AWS_MEDIA_STORAGE_HOST, + custom_domain=settings.AWS_MEDIA_DOMAIN, + calling_format=calling_format, + # Setting an ACL requires us to grant the user the PutObjectAcl + # permission as well, even if it matches the default bucket ACL. + # XXX: Ideally we would thus set it to `None`, but due to how + # easy_thumbnails works internally, that causes thumbnail + # generation to fail... + default_acl='private', + querystring_auth=True, + ) + # MEDIA_HEADERS is a list of tuples containing a regular expression + # to match against a path, and a dictionary of HTTP headers to be + # returned with the resource identified by the path when it is + # requested. + # The headers are applied in the order they where declared, and + # processing stops at the first match. + # E.g.: + # + # MEDIA_HEADERS = [ + # (r'media/cache/.*', { + # 'Cache-Control': 'max-age={}'.format(3600 * 24 * 365), + # }) + # ] + # + media_headers = getattr(settings, 'MEDIA_HEADERS', []) + self.media_headers = [ + (re.compile(r), headers) for r, headers in media_headers + ] diff --git a/src/memberzone/templates/memberzone/change_password.html b/src/memberzone/templates/memberzone/change_password.html new file mode 100644 index 0000000..8e5db3a --- /dev/null +++ b/src/memberzone/templates/memberzone/change_password.html @@ -0,0 +1,16 @@ +{% load i18n %} +
+ {% csrf_token %} +
+ {% include 'project/content/includes/_field.html' with field=form.old_password label=True small=True %} + {% include 'project/content/includes/_field.html' with field=form.new_password1 label=True small=True %} + {% include 'project/content/includes/_field.html' with field=form.new_password2 label=True small=True %} +
+ +
diff --git a/src/memberzone/templates/memberzone/change_password_done.html b/src/memberzone/templates/memberzone/change_password_done.html new file mode 100644 index 0000000..bf3a1f9 --- /dev/null +++ b/src/memberzone/templates/memberzone/change_password_done.html @@ -0,0 +1,11 @@ +{% load i18n %} +
+ {% trans 'Ihr Passwort wurden erfolgreich geändert' %} + + + {% include 'project/assets/close.svg' %} + {% trans 'Schliessen' %} + + {% include 'project/assets/close.svg' %} + +
\ No newline at end of file diff --git a/src/memberzone/templates/memberzone/email/member_registration_confirmation_email.html b/src/memberzone/templates/memberzone/email/member_registration_confirmation_email.html new file mode 100644 index 0000000..7a7f5b5 --- /dev/null +++ b/src/memberzone/templates/memberzone/email/member_registration_confirmation_email.html @@ -0,0 +1,14 @@ + +{{ subject }} + +

{{ subject }}

+
+{% for data_set in data %} + +{% endfor %} + + diff --git a/src/memberzone/templates/memberzone/includes/_downloads.html b/src/memberzone/templates/memberzone/includes/_downloads.html new file mode 100644 index 0000000..d24826b --- /dev/null +++ b/src/memberzone/templates/memberzone/includes/_downloads.html @@ -0,0 +1,57 @@ +{% load i18n static util_tags thumbnail %} + + +
+
+ {% if title != False %} +

{% trans title %}

+ {% endif %} + {% if search %} +
+
+ + +
+
+ {% endif %} + {% for section in download_sections %} + + {% endfor %} +
+
\ No newline at end of file diff --git a/src/memberzone/templates/memberzone/includes/_field.html b/src/memberzone/templates/memberzone/includes/_field.html new file mode 100644 index 0000000..0563722 --- /dev/null +++ b/src/memberzone/templates/memberzone/includes/_field.html @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/src/memberzone/templates/memberzone/overview.html b/src/memberzone/templates/memberzone/overview.html new file mode 100644 index 0000000..d6e9ceb --- /dev/null +++ b/src/memberzone/templates/memberzone/overview.html @@ -0,0 +1,176 @@ +{% extends 'main.html' %} +{% load i18n cms_tags thumbnail %} + +{% load i18n static task_tags %} + + + +{% block content %} +
+ {% include 'project/includes/content_intro.html' %} +
+ + +
+
+ + +
+
+ + {% include 'memberzone/includes/_downloads.html' with search=True title='Downloads' %} + +
+
+{% endblock %} \ No newline at end of file diff --git a/src/memberzone/templates/memberzone/profile_edit.html b/src/memberzone/templates/memberzone/profile_edit.html new file mode 100644 index 0000000..399b887 --- /dev/null +++ b/src/memberzone/templates/memberzone/profile_edit.html @@ -0,0 +1,17 @@ +{% load i18n %} +
+ {% csrf_token %} +
+ {% include 'memberzone/includes/_field.html' with field=form.gender label=True small=True %} + {% include 'memberzone/includes/_field.html' with field=form.title label=True small=True %} + {% include 'memberzone/includes/_field.html' with field=form.first_name label=True small=True %} + {% include 'memberzone/includes/_field.html' with field=form.last_name label=True small=True %} +
+ +
diff --git a/src/memberzone/templates/memberzone/profile_edit_done.html b/src/memberzone/templates/memberzone/profile_edit_done.html new file mode 100644 index 0000000..b670f65 --- /dev/null +++ b/src/memberzone/templates/memberzone/profile_edit_done.html @@ -0,0 +1,11 @@ +{% load i18n %} +
+ {% trans 'Ihre Benutzerdaten wurden erfolgreich geändert' %} + + + {% include 'project/assets/close.svg' %} + {% trans 'Schliessen' %} + + {% include 'project/assets/close.svg' %} + +
\ No newline at end of file diff --git a/src/memberzone/templates/memberzone/task.html b/src/memberzone/templates/memberzone/task.html new file mode 100644 index 0000000..afbfa21 --- /dev/null +++ b/src/memberzone/templates/memberzone/task.html @@ -0,0 +1,139 @@ +{% extends 'project/content.html' %} +{% load i18n static thumbnail task_tags %} +https://source.unsplash.com/random/1200x900 + +{% block body_class %}header-close{% endblock %} +{% block header_menu_url %}{% url 'memberzone:overview' %}{% endblock %} + +{% block intro_class %}intro--image{% endblock %} +{% block intro_image %}{% if object.image %} + {% thumbnail object.image "1200x900" crop="center" subject_location=object.image.subject_location %} +{% endif %}{% endblock %} + +{% block content_content %} +
+
+
+
{{ object.bodytext | add_animation_classes | safe }}
+
+
+
+ + {% include 'project/content/includes/_grid.html' with grid=grid last=True %} + +
+
+
+
+
+ {% if formset and not object.is_expired %} + {{ object.form_bodytext | add_animation_classes | safe }} + {% if formset.errors %} + + {% trans 'Bitte korrigieren Sie die markierten Felder.' %} + + {% endif %} + {% if formset.non_form_errors %} + + {{ formset.non_form_errors }} + + {% endif %} +
+ {% csrf_token %} + {{ formset.management_form }} + +
+ {% for form in formset %} +
+ {% if object.max_num > 1 %} +
+

{{ forloop.counter }}. + Person

+ {% if forloop.counter0 > 0 %} + + {% include 'project/assets/close.svg' %} + {% trans 'Entfernen' %} + + {% endif %} +
+ {% endif %} + + {% if form.non_field_errors %} + + {{ formset.non_field_errors }} + + {% endif %} + + {% for field in form %} + {% if field.name == 'DELETE' %} + {% include 'project/content/includes/_field.html' with field=field label=True hidden=True %} + {% else %} + {% include 'project/content/includes/_field.html' with field=field label=True %} + {% endif %} + {% endfor %} +
+ {% endfor %} +
+ + + +
+ + {% if object.max_num > 1 %} + + + {% include 'project/assets/plus.svg' %} + {% trans 'Weitere Person hinzufügen' %} + + + {% endif %} +
+
+ {% else %} + {% if object.is_expired %} +

{% trans 'Die Anmeldungsfrist abgelaufen' %}

+

+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor + invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et +

+ {% else %} +

{% trans "Bereits abgeschlossen" %}

+

+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor + invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et + accusam et +

+ {% endif %} + {% endif %} +
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/src/memberzone/templates/memberzone/task_success.html b/src/memberzone/templates/memberzone/task_success.html new file mode 100644 index 0000000..7668c31 --- /dev/null +++ b/src/memberzone/templates/memberzone/task_success.html @@ -0,0 +1,20 @@ +{% extends 'project/dialog.html' %} +{% load i18n static %} + +{% block extra_meta %} + +{% endblock %} + +{% block dialog_content %} +
+ {{ object.success_bodytext | safe }} + + + + {% include 'project/assets/arrow-right-long.svg' %} + {% trans 'Zur Accountübersicht' %} + + {% include 'project/assets/arrow-right-long.svg' %} + +
+{% endblock %} \ No newline at end of file diff --git a/src/memberzone/templates/registration/login.html b/src/memberzone/templates/registration/login.html new file mode 100644 index 0000000..eb63eeb --- /dev/null +++ b/src/memberzone/templates/registration/login.html @@ -0,0 +1,16 @@ +{% extends 'main.html' %} +{% load i18n %} + + +{% block content %} +
+
+
+
+ {% csrf_token %} + {{ form }} +
+
+
+
+{% endblock %} diff --git a/src/memberzone/templatetags/__init__.py b/src/memberzone/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/memberzone/templatetags/task_tags.py b/src/memberzone/templatetags/task_tags.py new file mode 100644 index 0000000..cc5258d --- /dev/null +++ b/src/memberzone/templatetags/task_tags.py @@ -0,0 +1,34 @@ +import lxml.html +from django import template +from django.template.defaultfilters import stringfilter + +from memberzone.models import MemberTaskRegistration + +register = template.Library() + + +@register.filter(name='task_status') +def task_user_status(task, user): + try: + MemberTaskRegistration.objects.get(task=task, user=user) + except MemberTaskRegistration.DoesNotExist: + if task.is_expired: + return 'control__item--status--expired' + else: + return '' + else: + return 'control__item--status--active' + + +@register.filter +@stringfilter +def add_animation_classes(input_html): + document = lxml.html.fromstring('
{}
'.format(input_html)) + for el in document.xpath('//p|ul|ol|h2|h3'): + classes = el.attrib.get('class', '').split(' ') + if 'reveal' not in classes: + classes.append('reveal') + if 'reveal_animation' not in classes: + classes.append('reveal_animation') + el.attrib['class'] = ' '.join(classes) + return lxml.html.tostring(document)[5:-6] diff --git a/src/memberzone/urls.py b/src/memberzone/urls.py new file mode 100644 index 0000000..de5fcf7 --- /dev/null +++ b/src/memberzone/urls.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from django.conf.urls import url +from django.contrib.auth.decorators import login_required +from django.contrib.auth.views import LoginView, LogoutView, PasswordChangeView +from django.urls import reverse_lazy +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import TemplateView + +from memberzone.forms import LoginForm +from memberzone.views import OverviewView, MemberTaskDetailView, MemberTaskDetailSuccessView, ProfileEditView + +urlpatterns = [ + url(_(r'^login/$'), LoginView.as_view( + success_url=reverse_lazy('memberzone:overview'), + form_class=LoginForm + ), name='login'), + url(_(r'^logout/$'), LogoutView.as_view(next_page='login'), name='logout'), + + url(_(r'^account/change/password/$'), login_required(PasswordChangeView.as_view( + template_name='memberzone/change_password.html', + success_url=reverse_lazy('memberzone:change_password_done') + ), login_url=reverse_lazy('memberzone:login')), name='change_password'), + + url(_(r'^account/change/password/succeeded/$'), login_required(TemplateView.as_view( + template_name='memberzone/change_password_done.html' + ), login_url=reverse_lazy('memberzone:login')), name='change_password_done'), + + url(_(r'^account/edit/$'), login_required(ProfileEditView.as_view( + ), login_url=reverse_lazy('memberzone:login')), name='profile_edit'), + + url(_(r'^account/edit/succeeded/$'), login_required(TemplateView.as_view( + template_name='memberzone/profile_edit_done.html' + ), login_url=reverse_lazy('memberzone:login')), name='profile_edit_done'), + + url(_(r'^task/(?P[\w-]+)/$'), login_required(MemberTaskDetailView.as_view( + ), login_url=reverse_lazy('memberzone:login')), name='task'), + + url(_(r'^task/(?P[\w-]+)/success/$'), + login_required(MemberTaskDetailSuccessView.as_view(), login_url=reverse_lazy('memberzone:login')), + name='task_success'), + + url(_(r'^overview/$'), login_required(OverviewView.as_view(), login_url=reverse_lazy('memberzone:login')), + name='overview'), +] diff --git a/src/memberzone/views.py b/src/memberzone/views.py new file mode 100644 index 0000000..097ab86 --- /dev/null +++ b/src/memberzone/views.py @@ -0,0 +1,170 @@ +import json + +from django.conf import settings +from django.contrib.auth.forms import SetPasswordForm +from django.contrib.auth.tokens import default_token_generator +from django.contrib.auth.views import LoginView as DjangoLoginView +from django.contrib.sites.shortcuts import get_current_site +from django.core.mail import EmailMessage +from django.db.models import Q +from django.forms import formset_factory +from django.http import HttpResponseRedirect +from django.shortcuts import resolve_url +from django.template.loader import render_to_string +from django.urls import reverse, reverse_lazy +from django.utils import timezone +from django.utils.encoding import force_bytes, force_text +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import UpdateView, ListView, DetailView, FormView + +from memberzone.models import MemberTask, MemberTaskRegistration, MemberDownloadFile, Profile + + +class MemeberTaskQuerysetMixin(object): + def get_queryset(self): + return MemberTask.objects.filter(groups__in=self.request.user.groups.all()).distinct() + + +class OverviewView(MemeberTaskQuerysetMixin, ListView): + template_name = 'memberzone/overview.html' + open_tasks = None + + paginate_by = 3 + + def get_title(self): + return _('Grüezi, {first_name} {last_name}').format( + first_name=self.request.user.first_name, + last_name=self.request.user.last_name, + ) + + def get_download_sections(self): + file_list = MemberDownloadFile.objects.filter(groups__in=self.request.user.groups.all()) + sections = {} + for file in file_list: + if not sections.get(file.section_id, None): + sections[file.section_id] = { + 'section': file.section, + 'items': [] + } + sections[file.section_id]['items'].append(file) + + sections_list = [{'title': x['section'].title, 'items': x['items'], 'ordering': x['section'].ordering} for x in + sections.values()] + + return sorted(sections_list, key=lambda x: x['ordering']) + + def get_queryset(self): + queryset = super(OverviewView, self).get_queryset() + return queryset.exclude(pk__in=self.get_open_tasks().values_list('pk', flat=True)) + + def get_open_tasks(self): + yesterday = timezone.now() - timezone.timedelta(days=1) + all_user_registrations = MemberTaskRegistration.objects.filter(user=self.request.user, cdate__lte=yesterday) + queryset = MemeberTaskQuerysetMixin.get_queryset(self) + queryset.exclude(registrations__in=all_user_registrations).order_by('-registrations__cdate') + return queryset + + def get_context_data(self, **kwargs): + context = super(OverviewView, self).get_context_data(**kwargs) + context.update({ + 'download_sections': self.get_download_sections(), + 'open_tasks': None if self.request.GET.get(self.page_kwarg, None) else self.get_open_tasks(), + }) + return context + + +class MemberTaskDetailView(MemeberTaskQuerysetMixin, DetailView): + template_name = 'memberzone/task.html' + model = MemberTask + + def get_title(self): + return self.object.title + + def get_description(self): + return self.object.sub_title + + def get_formset(self): + try: + MemberTaskRegistration.objects.get(task=self.object, user=self.request.user) + except MemberTaskRegistration.DoesNotExist: + MemeberTaskFormset = formset_factory( + self.object.form_class, + extra=0, + min_num=1, + max_num=self.object.max_num, + can_delete=True + ) + formset_kwargs = {} + if self.request.method == 'POST': + formset_kwargs.update({ + 'data': self.request.POST + }) + return MemeberTaskFormset(**formset_kwargs) + else: + return None + + def formset_valid(self, formset): + registration = MemberTaskRegistration.objects.create( + task=self.object, + user=self.request.user, + form_data=json.dumps(formset.cleaned_data) + ) + if registration.task.fields.count() > 0: + self.send_confirmation_email(registration=registration) + return HttpResponseRedirect(reverse('memberzone:task_success', args=[self.object.slug])) + + def formset_invalid(self, formset): + return self.render_to_response(context=self.get_context_data(formset=formset, object=self.object)) + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + formset = self.get_formset() + return self.render_to_response(context=self.get_context_data(formset=formset, object=self.object)) + + def send_confirmation_email(self, registration): + subject = _('Anmeldebestätigung: {}').format(registration.task.title) + + confirmation_data = [] + for data_set in json.loads(registration.form_data): + confirmation_data_set = [] + for field in registration.task.fields.all(): + value = data_set.get(field.name.replace('-', '_'), '') + if isinstance(value, list): + value = ','.join(value) + confirmation_data_set.append((field.label, value,)) + confirmation_data.append(confirmation_data_set) + + message = render_to_string('memberzone/email/member_registration_confirmation_email.html', { + 'data': confirmation_data, + 'subject': subject, + }) + msg = EmailMessage(subject, message, settings.DEFAULT_FROM_EMAIL, to=[registration.user.email]) + msg.content_subtype = "html" + # msg.send() + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + formset = self.get_formset() + if formset.is_valid(): + return self.formset_valid(formset) + else: + return self.formset_invalid(formset) + + +class MemberTaskDetailSuccessView(MemeberTaskQuerysetMixin, DetailView): + template_name = 'memberzone/task_success.html' + model = MemberTask + + def get_title(self): + return self.object.success_title + + +class ProfileEditView(UpdateView): + model = Profile + fields = ['first_name', 'last_name', 'street', 'zip', 'place', 'email'] + template_name = 'memberzone/profile_edit.html' + success_url = reverse_lazy('memberzone:profile_edit_done') + + def get_object(self, queryset=None): + return self.request.user.profile diff --git a/src/project/templates/main.html b/src/project/templates/main.html index a08a470..fbd1ed3 100644 --- a/src/project/templates/main.html +++ b/src/project/templates/main.html @@ -118,7 +118,7 @@ {% endfor %} diff --git a/src/project/templatetags/util_tags.py b/src/project/templatetags/util_tags.py index 35f3caa..e076488 100644 --- a/src/project/templatetags/util_tags.py +++ b/src/project/templatetags/util_tags.py @@ -10,4 +10,4 @@ def page_image(id): try: return Page.objects.get(pk=id).imageextension.image except: - return None \ No newline at end of file + return None diff --git a/src/project/urls.py b/src/project/urls.py index e4bbac6..f6acab0 100644 --- a/src/project/urls.py +++ b/src/project/urls.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from django.conf.urls import url +from django.conf.urls import url, include from django.utils.translation import ugettext_lazy as _ from django.views.generic.base import TemplateView @@ -11,4 +11,6 @@ urlpatterns = [ url(r'^newsletter/subscription/success/$', TemplateView.as_view(template_name='project/newsletter/subscription.html'), kwargs={'success': True}, name='newsletter_subscription_success'), + + url(_(r'^memberzone/'), include('memberzone.urls', namespace='memberzone')), ]