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 %}
+
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 %}
+
\ 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 %}
+
+ {% for label, value in data_set %}
+ {{ label }}: {{ value }}
+ {% endfor %}
+
+{% 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 %}
+
+
+
{{ section.title }}
+
+ {% include 'project/plugins/content/download_section.html' with instance=section %}
+{#
#}
+{# {% for file in files %}#}
+{#
#}
+{# {% endfor %}#}
+{#
#}
+
+ {% 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 @@
+
+ {{ field.label }}:
+ {{ field }}
+ {% if field.errors %}
+ {{ field.errors.0 }}
+ {% endif %}
+
\ 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' %}
+
+
+
+
+
+
+
{% trans 'Aktuell' %}
+
+
+
+ {% if page_obj.has_next or open_tasks and paginator.count > 0 %}
+
+ {% endif %}
+
+
+
+
+
{% trans 'Einstellungen' %}
+
+
+
+
+
+
+
+ {% if request.user.company_responsible %}
+
+
+
+ {% endif %}
+
+
+
+
+
+ {% 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 %}
+
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 %}
+
\ 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 %}
+
+ {% 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 %}
+
+{% 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 %}
+
+{% 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')),
]