diff --git a/evapp/views.py b/evapp/views.py
index 6da4f7a..67097f3 100644
--- a/evapp/views.py
+++ b/evapp/views.py
@@ -189,7 +189,7 @@ class EvaFormView(LoginRequiredMixin, CookieWizardView):
# update values in data dictionary with keys from *_CHOICES if present there
choices = {**DEPARTMENT_CHOICES, **TRANSPONDER_CHOICES,
- **OS_CHOICES, **LANG_CHOICES, **VENDOR_CHOICES, **KEYBOARD_CHOICES}
+ **OS_CHOICES, **LANG_CHOICES, **KEYBOARD_CHOICES}
data.update({k:choices[v] for k,v in data.items() \
if isinstance(v,collections.abc.Hashable) \
and v in choices})
diff --git a/home/__init__.py b/home/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/home/admin.py b/home/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/home/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/home/apps.py b/home/apps.py
new file mode 100644
index 0000000..75167e0
--- /dev/null
+++ b/home/apps.py
@@ -0,0 +1,9 @@
+# import django´s base Appconfig class
+from django.apps import AppConfig
+
+# configuration class for the 'home' application
+class HomeConfig(AppConfig):
+ # set the default type of primary key field for models in this app
+ default_auto_field = 'django.db.models.BigAutoField'
+ # give the application a name
+ name = 'home'
diff --git a/home/migrations/__init__.py b/home/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/home/models.py b/home/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/home/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/home/templates/home/index.html b/home/templates/home/index.html
new file mode 100644
index 0000000..df32d03
--- /dev/null
+++ b/home/templates/home/index.html
@@ -0,0 +1,97 @@
+
+{% load i18n %}
+{% load static %}
+
+
+
+
+ Button Navigation
+
+
+
+ Choose Your Destination
+
+
+
+

+
+
+
+
+
+

+
+
+
+
+

+
+
+
+
+
+
+
+
+
diff --git a/home/tests.py b/home/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/home/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/home/urls.py b/home/urls.py
new file mode 100644
index 0000000..d4d8f55
--- /dev/null
+++ b/home/urls.py
@@ -0,0 +1,9 @@
+# home/urls.py
+
+from django.urls import path
+from . import views # import views from the current app
+
+# Define the URL patterns for the 'home' app
+urlpatterns = [
+ path('', views.index, name='home-index'), # Map the URL '' to the index view with the name 'home-index'
+]
\ No newline at end of file
diff --git a/home/views.py b/home/views.py
new file mode 100644
index 0000000..7a10a70
--- /dev/null
+++ b/home/views.py
@@ -0,0 +1,15 @@
+from django.shortcuts import render
+
+# Create your views here.
+# Import the render function from Django to render templates
+from django.shortcuts import render
+
+def index(request):
+ # Context dictionary to pass dynamic data to the template
+ context = {
+ 'title': 'Welcome to Pycouse!',
+ 'author': 'Brian',
+ }
+ # Render the 'index.html' template located in the 'home' directory
+ # Pass the context dictionary to the template for dynamic content
+ return render(request, 'home/index.html', context)
\ No newline at end of file
diff --git a/offboarding/__init__.py b/offboarding/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/offboarding/admin.py b/offboarding/admin.py
new file mode 100644
index 0000000..067bbae
--- /dev/null
+++ b/offboarding/admin.py
@@ -0,0 +1,6 @@
+from django.contrib import admin
+from .models import Employee
+
+admin.site.register([
+ Employee,
+ ])
diff --git a/offboarding/apps.py b/offboarding/apps.py
new file mode 100644
index 0000000..4f4a80f
--- /dev/null
+++ b/offboarding/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class EvappConfig(AppConfig):
+ name = 'offboarding'
diff --git a/offboarding/forms.py b/offboarding/forms.py
new file mode 100644
index 0000000..662df53
--- /dev/null
+++ b/offboarding/forms.py
@@ -0,0 +1,67 @@
+from django.db import models
+from django.forms import ModelForm, DateInput, Form, ChoiceField, RadioSelect
+from django.core.exceptions import ValidationError
+
+from .models import Employee
+
+# class EmployeeForm(ModelForm):
+# class Meta:
+# model = Employee
+# fields = '__all__'
+# widgets = {'firstdate_employment': DateInput(attrs={'type': 'date'}),
+# 'firstdate_presence': DateInput(attrs={'type': 'date'}),}
+
+class DummyForm(ModelForm):
+ class Meta:
+ model = Employee
+ fields = []
+
+class EvaForm(ModelForm):
+ '''this base class provides the required css class for all forms'''
+ required_css_class = 'required'
+
+TYPE_CHOICES = {'IN': 'Eintritt', 'CHANGE': 'Veränderung', 'OUT': 'Austritt'}
+
+class PersonalForm(EvaForm):
+ # TODO: comment this back in to use implementation of change or exit process
+ # choice = ChoiceField(choices=TYPE_CHOICES.items(), widget=RadioSelect,
+ # label='Welcher Prozess soll angestoßen werden?')
+
+ class Meta:
+ model = Employee
+ fields = ['firstname', 'lastname', 'department', 'team', 'add_to_wikimediade',]
+
+class WorkingForm(EvaForm):
+
+ def clean(self):
+ data = self.cleaned_data
+ if data['works_in_gs'] and data['desk'] is None:
+ raise ValidationError('Wer nicht remote arbeitet braucht einen Schreibtisch!')
+ return data
+
+ class Meta:
+ model = Employee
+ fields = ['firstdate_employment', 'firstdate_presence', 'jobdescription_german',
+ 'jobdescription_english', 'works_in_gs', 'desk',]
+ widgets = {'firstdate_employment': DateInput(attrs={'type': 'date'}),
+ 'firstdate_presence': DateInput(attrs={'type': 'date'}),}
+
+class ITForm(EvaForm):
+
+ class Meta:
+ model = Employee
+ fields = [
+ 'framework', 'os', 'keyboard', 'mobile', 'landline',
+ 'comment', 'language', 'accounts', 'lists', 'rebu2go' ]
+
+class OfficeForm(EvaForm):
+ class Meta:
+ model = Employee
+ fields = ['transponder', 'special', 'post_office_box',]
+
+class ChangeForm(EvaForm):
+ class Meta:
+ model = Employee
+ fields = ['firstdate_employment', 'jobdescription_german', 'jobdescription_english',
+ 'desk', 'comment', 'accounts', 'lists', 'transponder']
+ widgets = {'firstdate_employment': DateInput(attrs={'type': 'date'}),}
diff --git a/offboarding/migrations/0001_initial.py b/offboarding/migrations/0001_initial.py
new file mode 100644
index 0000000..8ec82ff
--- /dev/null
+++ b/offboarding/migrations/0001_initial.py
@@ -0,0 +1,46 @@
+# Generated by Django 3.1.4 on 2021-09-13 12:41
+
+from django.db import migrations, models
+import multiselectfield.db.fields
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Employee',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('firstname', models.CharField(max_length=50, verbose_name='Vorname')),
+ ('lastname', models.CharField(max_length=50, verbose_name='Nachname')),
+ ('department', models.CharField(choices=[('PROG', 'Programme'), ('SOFT', 'Softwareentwicklung'), ('CENT', 'Central'), ('VOR', 'Vorstand')], max_length=5, verbose_name='Bereich')),
+ ('team', models.CharField(blank=True, max_length=20, null=True)),
+ ('add_to_wikimediade', models.BooleanField(default=False, verbose_name='Soll auf wikimedia.de irgendwo stehen?')),
+ ('firstdate_employment', models.DateField(null=True, verbose_name='Erster Arbeitstag')),
+ ('firstdate_presence', models.DateField(null=True, verbose_name='Erster Tag der Anwesenheit in der Geschäftsstelle')),
+ ('jobdescription_german', models.CharField(max_length=100, null=True, verbose_name='Stellenbezeichnung(deutsch)')),
+ ('jobdescription_english', models.CharField(max_length=100, null=True, verbose_name='Job description(english)')),
+ ('remote', models.BooleanField(default=True, verbose_name='Braucht Arbeitsplatz in der Geschäftsstelle?')),
+ ('desk', models.CharField(blank=True, max_length=100, null=True, verbose_name='Wo soll der Arbeitsplatz sein?')),
+ ('vendor', models.CharField(choices=[('STANDARD', 'Dell Latitude'), ('LENOVO', 'Lenovo Thinkpad'), ('MAC', 'Mac (nur für Grafiker_innen)')], default='STANDARD', max_length=8, verbose_name='Hersteller')),
+ ('os', models.CharField(choices=[('UBU', 'Ubuntu (Standard)'), ('WIN', 'Windows (bitte Begründung angeben)'), ('MOS', 'Mac OS (nur wenn Mac gewählt)')], default='UBU', max_length=3, verbose_name='Betriebssystem')),
+ ('screen', models.BooleanField(default=False, verbose_name='Zusätzlicher Monitor? Einer ist standard.')),
+ ('mobile', models.BooleanField(default=False, max_length=6, verbose_name='Diensttelefon (Handy)')),
+ ('landline', models.BooleanField(default=False, verbose_name='Festnetznummer (Sipgate)')),
+ ('keyboard', models.CharField(choices=[('DE', 'Deutsch'), ('US', 'USA'), ('OT', 'Anderes (Bitte unten angeben)')], default='DE', max_length=2, verbose_name='Tastaturlayout')),
+ ('comment', models.TextField(blank=True, max_length=500, null=True, verbose_name='zusätzliche IT-Anforderungen')),
+ ('language', models.CharField(choices=[('GER', 'Deutsch'), ('ENG', 'English')], default='GER', max_length=3, verbose_name='Sprache für Onboarding')),
+ ('accounts', multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('OTRSWMDE', 'OTRS Ticketsystem'), ('CIVIC1', 'Civic CRM (allgemein)'), ('CIVIC2', 'Civic CRM (Mailings, impliziert allgemein)'), ('WEB', 'www.wikimedia.de (edit)'), ('BLOG', 'blog.wikimedia.de (edit)'), ('FORUM', 'forum.wikimedia.de')], max_length=37, null=True, verbose_name='Zusätzliche Accounts')),
+ ('lists', models.CharField(blank=True, max_length=100, null=True, verbose_name='Zusätzliche Mailinglisten')),
+ ('rebu2go', models.BooleanField(default=False, verbose_name='Rebu2Go-Zugang benötigt?')),
+ ('transponder', models.CharField(choices=[('NORM', 'Allgemeiner Transponder'), ('SPECIAL', 'Besondere Schließungen (bitte angeben)'), ('NOTRANS', 'Kein Transponder')], default='NORM', max_length=7)),
+ ('special', models.TextField(blank=True, max_length=500, null=True, verbose_name='Besondere Schließungen hier eintragen')),
+ ('post_office_box', models.BooleanField(default=True, verbose_name='Postfach am Empfang benötigt?')),
+ ],
+ ),
+ ]
diff --git a/offboarding/migrations/0002_auto_20210914_1055.py b/offboarding/migrations/0002_auto_20210914_1055.py
new file mode 100644
index 0000000..0797935
--- /dev/null
+++ b/offboarding/migrations/0002_auto_20210914_1055.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.4 on 2021-09-14 10:55
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('evapp', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='employee',
+ old_name='remote',
+ new_name='works_in_gs',
+ ),
+ ]
diff --git a/offboarding/migrations/0003_auto_20220208_0955.py b/offboarding/migrations/0003_auto_20220208_0955.py
new file mode 100644
index 0000000..e91daf4
--- /dev/null
+++ b/offboarding/migrations/0003_auto_20220208_0955.py
@@ -0,0 +1,28 @@
+# Generated by Django 3.1.4 on 2022-02-08 09:55
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('evapp', '0002_auto_20210914_1055'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='employee',
+ name='department',
+ field=models.CharField(choices=[('PROG', 'Programme'), ('SOFT', 'Softwareentwicklung'), ('CENT', 'Central'), ('KOMEV', 'Kommunikation und Events'), ('VOR', 'Vorstand')], max_length=5, verbose_name='Bereich'),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='team',
+ field=models.CharField(blank=True, max_length=50, null=True),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='works_in_gs',
+ field=models.BooleanField(default=True, verbose_name='Braucht Arbeitsplatz in der Geschäftsstelle?)'),
+ ),
+ ]
diff --git a/offboarding/migrations/0004_alter_employee_accounts_alter_employee_department.py b/offboarding/migrations/0004_alter_employee_accounts_alter_employee_department.py
new file mode 100644
index 0000000..3456ec8
--- /dev/null
+++ b/offboarding/migrations/0004_alter_employee_accounts_alter_employee_department.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.2.4 on 2023-08-11 10:28
+
+from django.db import migrations, models
+import multiselectfield.db.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('evapp', '0003_auto_20220208_0955'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='employee',
+ name='accounts',
+ field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('OTRSWMDE', 'OTRS Ticketsystem'), ('CIVIC1', 'Civic CRM (allgemein)'), ('CIVIC2', 'Civic CRM (Mailings, impliziert allgemein)'), ('WEB', 'www.wikimedia.de (edit)'), ('BLOG', 'blog.wikimedia.de (edit)'), ('FORUM', 'forum.wikimedia.de')], max_length=10, null=True, verbose_name='Zusätzliche Accounts'),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='department',
+ field=models.CharField(choices=[('COENG', 'Communitys & Engagement'), ('SOFT', 'Softwareentwicklung'), ('CENT', 'Central'), ('KOMAD', 'Kommunikation & Advocacy'), ('VOR', 'Vorstand')], max_length=5, verbose_name='Bereich'),
+ ),
+ ]
diff --git a/offboarding/migrations/0005_alter_employee_accounts.py b/offboarding/migrations/0005_alter_employee_accounts.py
new file mode 100644
index 0000000..ca42bb6
--- /dev/null
+++ b/offboarding/migrations/0005_alter_employee_accounts.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.4 on 2023-08-17 11:08
+
+from django.db import migrations
+import multiselectfield.db.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('evapp', '0004_alter_employee_accounts_alter_employee_department'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='employee',
+ name='accounts',
+ field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('OTRSWMDE', 'OTRS Ticketsystem'), ('CIVIC1', 'Civic CRM (allgemein)'), ('CIVIC2', 'Civic CRM (Mailings, impliziert allgemein)'), ('WEB', 'www.wikimedia.de (edit)'), ('BLOG', 'blog.wikimedia.de (edit)'), ('FORUM', 'forum.wikimedia.de')], max_length=40, null=True, verbose_name='Zusätzliche Accounts'),
+ ),
+ ]
diff --git a/offboarding/migrations/0006_remove_employee_vendor_employee_framework_and_more.py b/offboarding/migrations/0006_remove_employee_vendor_employee_framework_and_more.py
new file mode 100644
index 0000000..b34e5fd
--- /dev/null
+++ b/offboarding/migrations/0006_remove_employee_vendor_employee_framework_and_more.py
@@ -0,0 +1,32 @@
+# Generated by Django 4.2.16 on 2024-11-26 09:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('evapp', '0005_alter_employee_accounts'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='employee',
+ name='vendor',
+ ),
+ migrations.AddField(
+ model_name='employee',
+ name='framework',
+ field=models.CharField(blank=True, max_length=300, null=True, verbose_name='Möchten Sie vom Standard des Frameworks abweichen, und wenn ja, warum?'),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='add_to_wikimediade',
+ field=models.CharField(choices=[('NEIN', 'Nein'), ('JA', 'Ja')], default=True, max_length=5, verbose_name='Soll die Person bei Ansprechpartner*innen auf der WMDE-Webseite mit aufgenommen werden?'),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='os',
+ field=models.CharField(choices=[('FED', 'Fedora (Standard)'), ('WIN', 'Windows (bitte Begründung angeben)'), ('MOS', 'Mac OS (nur wenn Mac gewählt)'), ('UBU', 'Ubuntu')], default='FED', max_length=3, verbose_name='Betriebssystem'),
+ ),
+ ]
diff --git a/offboarding/migrations/0007_remove_employee_screen_and_more.py b/offboarding/migrations/0007_remove_employee_screen_and_more.py
new file mode 100644
index 0000000..87124e9
--- /dev/null
+++ b/offboarding/migrations/0007_remove_employee_screen_and_more.py
@@ -0,0 +1,32 @@
+# Generated by Django 4.2.17 on 2024-12-17 14:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('evapp', '0006_remove_employee_vendor_employee_framework_and_more'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='employee',
+ name='screen',
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='add_to_wikimediade',
+ field=models.CharField(choices=[('NEIN', 'Nein'), ('JA', 'Ja')], max_length=5, verbose_name='Soll die Person bei Ansprechpartner*innen auf der WMDE-Webseite mit aufgenommen werden?'),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='framework',
+ field=models.CharField(blank=True, max_length=300, null=True, verbose_name='Möchten Sie vom Standard des Frameworks (Laptop) abweichen, und wenn ja, warum?'),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='id',
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ ]
diff --git a/offboarding/migrations/__init__.py b/offboarding/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/offboarding/models.py b/offboarding/models.py
new file mode 100644
index 0000000..e07fcac
--- /dev/null
+++ b/offboarding/models.py
@@ -0,0 +1,86 @@
+from django.db import models
+from multiselectfield import MultiSelectField
+from django.utils.translation import gettext_lazy as _
+
+# ATTENTION!!!
+# No key should be used twice in any of these dicts because of the
+# suboptimal implementation in views.EvaFormView.beautify_data()
+#
+
+DEPARTMENT_CHOICES = {'COENG': _('Communitys & Engagement'),
+ 'SOFT': _('Softwareentwicklung'),
+ 'CENT': 'Central',
+ 'KOMAD': _('Kommunikation & Advocacy'),
+ 'VOR': _('Vorstand'),}
+
+#VENDOR_CHOICES = {'STANDARD': 'Dell Latitude',
+# 'LENOVO': 'Lenovo Thinkpad',
+# 'MAC': _('Mac (nur für Grafiker_innen)')}
+
+OS_CHOICES = {'FED': 'Fedora (Standard)',
+ 'WIN': _('Windows (bitte Begründung angeben)'),
+ 'MOS': _('Mac OS (nur wenn Mac gewählt)'),
+ 'UBU': _('Ubuntu')}
+
+LANG_CHOICES = {'GER': 'Deutsch',
+ 'ENG': 'English',}
+
+KEYBOARD_CHOICES = {'DE': 'Deutsch',
+ 'US': 'USA',
+ 'OT': _('Anderes (Bitte unten angeben)')}
+
+ACCOUNT_CHOICES = {'OTRSWMDE': 'OTRS Ticketsystem',
+ 'CIVIC1': _('Civic CRM (allgemein)'),
+ 'CIVIC2': _("Civic CRM (Mailings, impliziert allgemein)"),
+ 'WEB': 'www.wikimedia.de (edit)',
+ 'BLOG': 'blog.wikimedia.de (edit)',
+ 'FORUM': 'forum.wikimedia.de',
+ }
+
+TRANSPONDER_CHOICES = {'NORM': _('Allgemeiner Transponder'),
+ 'SPECIAL': _('Besondere Schließungen (bitte angeben)'),
+ 'NOTRANS': _('Kein Transponder'),}
+
+JANEIN_CHOICES = {'NEIN': ('Nein'),
+ 'JA': _('Ja'),}
+
+class Employee(models.Model):
+
+ # email adress of user. should not be necessary if we use openauth one day
+ # usermail = models.EmailField(max_length=50, verbose_name="Deine Mailadresse (Ansprechpartner_in)", default='bestechefin@wikimedia.de')
+
+ # personal data
+ firstname = models.CharField(max_length=50, verbose_name=_("Vorname"))
+ lastname = models.CharField(max_length=50, verbose_name=_("Nachname"))
+ # intern = models.BooleanField(verbose_name='Interne_r Mitarbeiter_in?', default=True)
+ department = models.CharField(max_length=5, choices=DEPARTMENT_CHOICES.items(), verbose_name=_('Bereich'))
+ team = models.CharField(max_length=50, null=True, blank=True) # TODO? better with choices?
+ add_to_wikimediade = models.CharField(max_length=5, choices=JANEIN_CHOICES.items(), verbose_name=_("Soll die Person bei Ansprechpartner*innen auf der WMDE-Webseite mit aufgenommen werden?"))
+
+ # general work related stuff
+ firstdate_employment = models.DateField(null=True, verbose_name=_("Erster Arbeitstag"))
+ firstdate_presence = models.DateField(null=True, verbose_name=_("Erster Tag der Anwesenheit in der Geschäftsstelle"))
+ jobdescription_german = models.CharField(null=True, max_length=100, verbose_name="Stellenbezeichnung(deutsch)")
+ jobdescription_english = models.CharField(null=True, max_length=100, verbose_name="Job description(english)")
+ works_in_gs = models.BooleanField(verbose_name=_('Braucht Arbeitsplatz in der Geschäftsstelle?)'), default=True)
+ desk = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Wo soll der Arbeitsplatz sein?"))
+
+ # IT related stuff
+ #vendor = models.CharField(max_length=8, choices=VENDOR_CHOICES.items(), default='STANDARD', verbose_name=_('Hersteller'))
+ framework = models.CharField(max_length=300, null=True, blank=True, verbose_name=_("Möchten Sie vom Standard des Frameworks (Laptop) abweichen, und wenn ja, warum?"))
+ os = models.CharField(max_length=3, choices=OS_CHOICES.items(), default='FED', verbose_name=_('Betriebssystem'))
+ #screen = models.BooleanField(default=False, verbose_name=_('Zusätzlicher Monitor? Einer ist standard.'))
+ mobile = models.BooleanField(max_length=6, default=False, verbose_name=_('Diensttelefon (Handy)'))
+ landline = models.BooleanField(default = False, verbose_name=_('Festnetznummer (Sipgate)'))
+ # sim = models.BooleanField(default=False, verbose_name="Mobilfunkvertrag")
+ keyboard = models.CharField(max_length=2, choices=KEYBOARD_CHOICES.items(), default='DE', verbose_name=_("Tastaturlayout"))
+ comment = models.TextField(max_length=500, null=True, blank=True, verbose_name=_("zusätzliche IT-Anforderungen"))
+ language = models.CharField(max_length=3, choices=LANG_CHOICES.items(), default="GER", verbose_name=_("Sprache für Onboarding"))
+ accounts = MultiSelectField(choices=ACCOUNT_CHOICES.items(), max_length=40, null=True, blank=True, verbose_name=_("Zusätzliche Accounts"))
+ lists = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Zusätzliche Mailinglisten"))
+ rebu2go = models.BooleanField(verbose_name=_("Rebu2Go-Zugang benötigt?"), default=False)
+
+ # office related stuff
+ transponder = models.CharField(max_length=7, choices=TRANSPONDER_CHOICES.items(), default='NORM')
+ special = models.TextField(max_length=500, null=True, blank=True, verbose_name=_("Besondere Schließungen hier eintragen"))
+ post_office_box = models.BooleanField(default=True, verbose_name=_('Postfach am Empfang benötigt?'))
diff --git a/offboarding/settings.py b/offboarding/settings.py
new file mode 100644
index 0000000..10e3d99
--- /dev/null
+++ b/offboarding/settings.py
@@ -0,0 +1,61 @@
+# temporary setting while change and exit is not yet fully implemented
+ONLY_ONBOARDING = True
+
+# sender mail adress also used for MAILTEST mode
+EVA_MAIL = 'it-support@wikimedia.de'
+
+# these Fields should be included in every mail
+BASIC_DATA = ['firstname', 'lastname', 'firstdate_employment', 'firstdate_presence',
+ 'jobdescription_german', 'jobdescription_english',]
+
+# for every department: 'MAIL' => mail adress, 'DATA': additional fields to include
+# also one copy with all fields to the person filling the form.
+MAILS = {
+ 'IT': {
+ 'MAIL': 'wmde-it@wikimedia.de',
+ 'DATA': [
+ 'laptop', 'os', 'comment', 'email', 'landline', 'lists', 'mobile',
+ 'department', 'accounts', 'language', 'screen', 'works_in_gs', 'desk',
+ 'keyboard',
+ ],
+ },
+ 'OFFICE': {
+ 'MAIL': 'office@wikimedia.de',
+ 'DATA': [
+ 'transponder', 'special', 'post_office_box', 'mobile',
+ 'works_in_gs', 'desk',
+ ],
+ },
+ 'KOMM': {
+ 'MAIL': 'presse@wikimedia.de',
+ 'DATA': [
+ 'department', 'team', 'add_to_wikimediade'
+ ],
+ },
+ 'CENTRAL': {
+ 'MAIL': 'anna.noelte@wikimedia.de',
+ 'DATA': [
+ 'department', 'team', 'language', 'mobile', 'rebu2go'
+ ],
+ },
+ 'HR': {
+ 'MAIL': 'personal@wikimedia.de',
+ 'DATA': [
+ 'department', 'team', 'language',
+ ]
+ },
+ 'FINANCE': {
+ 'MAIL': 'claudia.langrock@wikimedia.de',
+ 'DATA': [
+ 'rebu2go'
+ ]
+ },
+ 'SUBMITTER': {
+ 'MAIL': 'submitter@wikimedia.de',
+ 'DATA': [
+ 'department', 'team', 'add_to_wikimediade', 'remote', 'desk', 'vendor',
+ 'os', 'screen', 'mobile', 'landline', 'keyboard', 'comment', 'language',
+ 'accounts', 'lists', 'rebu2go', 'transponder', 'special', 'post_office_box'
+ ]
+ }
+ }
diff --git a/offboarding/static/evapp/logo.png b/offboarding/static/evapp/logo.png
new file mode 100644
index 0000000..cec78db
Binary files /dev/null and b/offboarding/static/evapp/logo.png differ
diff --git a/offboarding/templates/account/login.html b/offboarding/templates/account/login.html
new file mode 100644
index 0000000..7a0ea12
--- /dev/null
+++ b/offboarding/templates/account/login.html
@@ -0,0 +1,27 @@
+{% load i18n %}
+{% load static %}
+
+
+
+
+
+{{ form.media }}
+
+
+
+
+{% load account socialaccount %}
+
+{% block content %}
+
+
+
+ E (V A) - Eintritt, (Veränderung, Austritt)
+
+
+Bitte via Wolke einloggen:
+{% include "socialaccount/snippets/provider_list.html" with process="login" %}
+
+
+
+{% endblock %}
diff --git a/offboarding/templates/offboarding/dataloop.txt b/offboarding/templates/offboarding/dataloop.txt
new file mode 100644
index 0000000..6e484a9
--- /dev/null
+++ b/offboarding/templates/offboarding/dataloop.txt
@@ -0,0 +1,5 @@
+{% load i18n %}
+{% autoescape off %}
+{% for key, value in data.items %}{% if key == 'laptop' %} {{ key }}: {{ value | safe}}{% else %}
+{% trans key %}: {{ value }}{% endif %}{% endfor %}
+{% endautoescape %}
diff --git a/offboarding/templates/offboarding/department_mail.txt b/offboarding/templates/offboarding/department_mail.txt
new file mode 100644
index 0000000..8645617
--- /dev/null
+++ b/offboarding/templates/offboarding/department_mail.txt
@@ -0,0 +1,31 @@
+{% load i18n %}
+
+(english below)
+
+Hallo!
+
+Es gibt einen Neuzugang bei Wikimedia! Hier ( https://wiki.wikimedia.de/wiki/Onboarding ) kannst Du nachsehen,
+welche Schritte jetzt für Deine Abteilung nötig werden. Im Folgenden alle Daten,
+die Du dafür brauchst:
+
+{% include 'evapp/dataloop.txt' %}
+
+Wenn Du Fragen hast, melde Dich bei {{contact}}.
+
+Grüße, Deine E.V.A.
+
+-------------------------
+{% language 'en' %}
+Hi!
+
+There is a new employee at Wikimedia! Here ( https://wiki.wikimedia.de/wiki/Onboarding ) you can see, which
+steps are now necessary for your department.
+
+All Data you need for this:
+
+{% include 'evapp/dataloop.txt' %}
+
+If you have any questions please write to {{contact}}.
+
+Regards, Your E.V.A.
+{% endlanguage %}
diff --git a/offboarding/templates/offboarding/employee_form.html b/offboarding/templates/offboarding/employee_form.html
new file mode 100644
index 0000000..7f6a5e5
--- /dev/null
+++ b/offboarding/templates/offboarding/employee_form.html
@@ -0,0 +1,110 @@
+
+{% load i18n %}
+{% load static %}
+
+
+
+
+
+{{ form.media }}
+
+
+
+
+{% load socialaccount %}
+{% if user.is_authenticated %}
+{% block content %}
+
+{% get_current_language as LANGUAGE_CODE %}
+
+
+
+
+
+ {% if TESTMODE %}
+ {% translate "WARNUNG! Test-MODUS aktiviert. Es werden keine Mails verschickt!" %}
+ {% endif %}
+
+ E (V A) - Austritt, (Veränderung, Austritt)
+ {% translate "Du bist eingeloggt als" %} {{ user.email }}
+
+
+
{% translate "Schritt" %} {{ wizard.steps.step1 }} {% translate "von" %} {{ wizard.steps.count }}
+ {% if wizard.steps.step1 == 1 %}
+ {% translate "Angaben zur Person" %} {% endif %}
+{% if choice == 'IN' %}
+ {% if wizard.steps.step1 == 2 %}
+ {% translate "Angaben zum neuen Arbeitsverhältnis" %}
+ {% elif wizard.steps.step1 == 3 %}
+ {% translate "IT-relevante Angaben" %}
+ {% elif wizard.steps.step1 == 4 %}
+ {% translate "Office-relevante Angaben" %}
+ {% endif %}
+{% else %}
+ {% if wizard.steps.step1 == 2 %}
+ Veränderungsrelevante Angaben
+ {% endif %}
+{% endif %}
+{% if datatable == True %}
+ {% translate "Bestätigungsschritt" %}
+{% endif %}
+
+
+
+ {% if datatable == True %}
+
+ {% for key, value in data.items %}
+ {{ key }} | {{ value }} |
+ {% endfor %}
+
+ {% endif %}
+
+
+
+ {% translate "logout" %}
+
+{% endblock %}
+{% else %}
+{% translate "Bitte einloggen!" %}
+{% endif %}
diff --git a/offboarding/templates/registration/login.html b/offboarding/templates/registration/login.html
new file mode 100644
index 0000000..cbceed1
--- /dev/null
+++ b/offboarding/templates/registration/login.html
@@ -0,0 +1,35 @@
+{% block content %}
+
+ {% if form.errors %}
+ Your username and password didn't match. Please try again.
+ {% endif %}
+
+ {% if next %}
+ {% if user.is_authenticated %}
+ Your account doesn't have access to this page. To proceed,
+ please login with an account that has access.
+ {% else %}
+ Please login to see this page.
+ {% endif %}
+ {% endif %}
+
+
+
+ {# Assumes you setup the password_reset view in your URLconf #}
+ Lost password?
+
+{% endblock %}
diff --git a/offboarding/tests.py b/offboarding/tests.py
new file mode 100644
index 0000000..909b612
--- /dev/null
+++ b/offboarding/tests.py
@@ -0,0 +1,127 @@
+from django.test import TestCase
+from django.test import Client
+from django.contrib.auth.models import User
+from django.conf import settings
+from django.http import HttpResponse
+from django.core import mail
+from django.utils import translation
+
+from .forms import ITForm, WorkingForm, OfficeForm, DummyForm
+
+class LoginTestCase(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.user = User.objects.create_user('vladimir', 'vladimir@reiherzehe.com', 'reiherzehe')
+ self.client.login(username='vladimir', password='reiherzehe')
+ self.response = self.client.get('/')
+
+ def testLogin(self):
+ self.assertContains(self.response, 'Du bist eingeloggt als vladimir@reiherzehe.com', status_code=200)
+ response_en = self.client.get('/', HTTP_ACCEPT_LANGUAGE='en-us')
+ self.assertContains(response_en, 'You are logged in as vladimir@reiherzehe.com', status_code=200)
+ self.assertContains(response_en, 'Firstname', status_code=200)
+ response_en = self.client.get('/', HTTP_ACCEPT_LANGUAGE='en')
+ self.assertContains(response_en, 'You are logged in as vladimir@reiherzehe.com', status_code=200)
+ self.assertContains(response_en, 'Firstname', status_code=200)
+
+ def testDebugWarning(self):
+ with self.settings(DEBUG=True):
+ self.response = self.client.get('/') # we need to do it again with DEBUG = True
+ self.assertContains(self.response, "WARNUNG! Test-MODUS aktiviert. Es werden keine Mails verschickt!", status_code=200)
+ with self.settings(DEBUG=False) and self.settings(MAILTEST=False):
+ self.response = self.client.get('/') # we need to do it again with DEBUG = False
+ self.assertNotContains(self.response, "WARNUNG! Test-MODUS aktiviert. Es werden keine Mails verschickt!", status_code=200)
+
+ def _postform(self, data, expected_form):
+ '''helper function to manage the Wizzard'''
+ response = self.client.post('/', data, follow=True)
+ # print(type(response))
+ self.assertEqual(200, self.response.status_code)
+ if not type(response) == HttpResponse:
+ if 'form' in response.context:
+ self.assertFalse(response.context['form'].errors)
+ else:
+ raise "NO FORM FOUND"
+ self.assertEqual(
+ type(response.context['wizard']['form']),
+ expected_form
+ )
+ return response
+
+ def test_department(self):
+ self.assertContains(self.response, 'Programme', status_code=200)
+ self.assertContains(self.response, 'Kommunikation und Events', status_code=200)
+
+ def test_wizzard_in(self):
+ ''' this test goes through the whole onboarding process of the EvaFormView from start to end '''
+
+ self.assertEqual(200, self.response.status_code)
+
+ response = self._postform({
+ 'eva_form_view-current_step': '0',
+ '0-firstname': 'Ara',
+ '0-lastname': 'Seva',
+ '0-department': 'CENT',
+ '0-team': 'Community Communications',
+ '0-choice': 'IN',
+ }, WorkingForm)
+
+ response = self._postform({
+ 'eva_form_view-current_step': '1',
+ '1-firstdate_employment': '2021-01-01',
+ '1-firstdate_presence': '2021-01-01',
+ '1-jobdescription_german': 'hau drauf',
+ '1-jobdescription_english': 'und schluss',
+ '1-works_in_gs': False
+ }, ITForm)
+
+ response = self._postform({
+ 'eva_form_view-current_step': '2',
+ '2-vendor': 'STANDARD',
+ '2-os': 'UBU',
+ '2-keyboard': 'DE',
+ '2-language': 'GER'
+ }, OfficeForm)
+
+ response = self._postform({
+ 'eva_form_view-current_step': '3',
+ '3-transponder': 'NORM'
+ }, DummyForm)
+
+ response = self._postform({
+ 'eva_form_view-current_step': '5',
+ }, DummyForm)
+
+
+
+
+ def test_mail(self):
+ self.test_wizzard_in()
+ # print(mail.outbox[0].body)
+ self.assertGreater(len(mail.outbox), 2)
+ self.assertIn("Vorname", mail.outbox[0].body)
+ self.assertIn("Firstname", mail.outbox[0].body)
+ for i in (0,1,3):
+ self.assertIn("Handy", mail.outbox[i].body)
+ self.assertIn("Ara Seva", mail.outbox[0].subject)
+
+class NoLoginTestCase(TestCase):
+ def setUp(self):
+ self.client = Client()
+
+ def test_details(self):
+ response = self.client.get('/')
+ self.assertEqual(response.status_code, 302)
+ response2 = self.client.get(response.url)
+ self.assertContains( response2, 'Bitte via Wolke einloggen:', status_code=200)
+
+class ITFORMTestCase(TestCase):
+ def test_mac(self):
+ form = ITForm(data={"vendor": 'MAC', 'os': 'UBU'})
+ #print (form.errors)
+ self.assertEqual(form.non_field_errors(), ['Ein MAC sollte Mac OS installiert haben'])
+
+ def test_ubu(self):
+ form = ITForm(data={"vendor": 'STANDARD', 'os': 'UBU'})
+ #print (form.errors)
+ self.assertNotEqual(form.non_field_errors(), ['Ein MAC sollte Mac OS installiert haben'])
diff --git a/offboarding/urls.py b/offboarding/urls.py
new file mode 100644
index 0000000..7d0a9d1
--- /dev/null
+++ b/offboarding/urls.py
@@ -0,0 +1,13 @@
+from django.urls import path
+
+from .views import EvaFormView, success, long_process, change_process
+
+urlpatterns = [
+ path('', EvaFormView.as_view(condition_dict = {'1': long_process,
+ '2': long_process,
+ '3': long_process,
+
+ '4': change_process,}),
+ name='evaform'),
+ path('success', success, name='success')
+ ]
diff --git a/offboarding/views.py b/offboarding/views.py
new file mode 100644
index 0000000..74fefc7
--- /dev/null
+++ b/offboarding/views.py
@@ -0,0 +1,217 @@
+from smtplib import SMTPException
+import collections
+
+from django.views.generic.edit import CreateView
+from django.urls import reverse
+from django.http import HttpResponse, HttpResponseRedirect
+from django.core.mail import send_mail, BadHeaderError
+from django.template.loader import get_template
+from formtools.wizard.views import CookieWizardView
+from django.shortcuts import render
+from django.conf import settings
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.utils.translation import gettext_lazy as _
+
+from .models import Employee, DEPARTMENT_CHOICES, OS_CHOICES, \
+ LANG_CHOICES, ACCOUNT_CHOICES, TRANSPONDER_CHOICES, KEYBOARD_CHOICES, JANEIN_CHOICES
+from .forms import PersonalForm, WorkingForm, ITForm, OfficeForm, DummyForm,\
+ ChangeForm, TYPE_CHOICES
+from .settings import MAILS, EVA_MAIL, BASIC_DATA, ONLY_ONBOARDING
+
+def success(request):
+ return HttpResponse(f"Vielen Dank! Du hast E.V.A. erfolgreich ausgefüllt. Die Mails an die Abteilungen wurden versendet. Kopien gehen an {request.user.email}.")
+
+def long_process(wizard):
+ '''this method is called via urls.py to determine if a form is part of the IN-Process'''
+
+ if ONLY_ONBOARDING:
+ wizard.set_choice('IN')
+ return True
+ else:
+ data = wizard.get_cleaned_data_for_step('0') or {}
+ # print(data)
+ if data.get('choice') != 'CHANGE':
+ wizard.set_choice('IN')
+ # print('PROZESS IN')
+ return True
+ else:
+ wizard.set_choice('CHANGE')
+ # print('PROZESS NOT IN')
+ return False
+
+def change_process(wizard):
+ ''' this method is called via urls.py to determine if the form is part of the change process'''
+ # print('CHANGE PROZESS')
+ return not long_process(wizard)
+
+
+class EvaFormView(LoginRequiredMixin, CookieWizardView):
+ template_name = 'offboarding/employee_form.html'
+ form_list = [PersonalForm, WorkingForm, ITForm, OfficeForm, ChangeForm, DummyForm]
+ instance = None
+ choice = 'IN'
+
+ # maybe we dont need this, if *_process() would be class methods,
+ # but unsure if this would work fine with the entries in urls.py
+ def set_choice(self, c):
+ self.choice = c
+
+ def generate_email(self, data):
+ (first, *_) = data['firstname'].split(maxsplit=1)
+ (last, *_) = data['lastname'].split(maxsplit=1)
+ name = first + '.' + last
+ #if not data['intern']:
+ # mail = name + '_ext@wikimedia.de'
+ #else:
+ mail = name + '@wikimedia.de'
+ data['email'] = mail
+
+ def get_all_cleaned_data(self):
+ '''this method deletes data which is only used temporary and is not in the modell,
+ it also changes the mail adress of the employee in some circumstances'''
+
+ data = super().get_all_cleaned_data()
+ self.generate_email(data)
+
+ # print("delete CHOICE FROM DATA")
+ if 'choice' in data:
+ del data['choice']
+ return data
+
+
+ def get_context_data(self, form, **kwargs):
+ '''this method is called to give context data to the template'''
+
+ #print('GETCONTEXT')
+ context = super().get_context_data(form=form, **kwargs)
+ testmode = settings.DEBUG or settings.MAILTEST
+ context.update({'choice': self.choice,
+ 'choice_string': TYPE_CHOICES[self.choice],
+ 'TESTMODE': testmode})
+
+ # deliver context for forms if we are in the last step
+ if (self.steps.step1 == 5 or (self.choice != 'IN' and self.steps.step1 == 3)):
+ context.update({'data': self.beautify_data(self.get_all_cleaned_data()),
+ 'datatable': True,})
+ return context
+
+ def get_form_instance(self,step):
+ ''' this method assures, that we use the same model instance for all steps'''
+
+ if self.instance == None:
+ self.instance = Employee()
+ return self.instance
+
+
+ def done(self, form_list, **kwargs):
+ '''this method is called from CookieWizardView after all forms are filled'''
+
+ print ('INSTANCE_DICT')
+ print(self.instance_dict)
+
+ # save data to database
+ for form in form_list:
+ form.save()
+
+ # send data to departments
+ for dep in MAILS:
+ response = self.send_mail_to_department(dep)
+
+ if not settings.DEBUG:
+ self.instance.delete()
+
+ if response:
+ return response
+ else:
+ return HttpResponseRedirect('success')
+
+
+ def send_mail_to_department(self, department):
+ 'send a mail to the given department with the nececcary notifications'
+
+ print(f'send mail to department {department}...')
+
+ contact = self.request.user.email
+ data = self.get_all_cleaned_data()
+ # some data should be in every mail
+ newdata = {k: v for k, v in data.items() if (k in BASIC_DATA)}
+ # only the relevant data should be in the context
+ newdata.update({k: v for k, v in data.items() if (k in MAILS[department]['DATA'])})
+
+ context = {'data': self.beautify_data(newdata), 'contact': contact}
+ firstname = data['firstname']
+ lastname = data['lastname']
+ firstday = data['firstdate_employment']
+ try:
+ mail_template = get_template(f'evapp/department_mail.txt')
+ if settings.MAILTEST:
+ send_mail(
+ f'EVA: Neuzugang {firstname} {lastname} {firstday} (MAILTEST)',
+ mail_template.render(context),
+ EVA_MAIL,
+ [EVA_MAIL],
+ fail_silently=False)
+ elif department != "SUBMITTER":
+ send_mail(
+ f'EVA: Neuzugang {firstname} {lastname} {firstday}',
+ mail_template.render(context),
+ EVA_MAIL,
+ [MAILS[department]['MAIL']],
+ fail_silently=False)
+ else:
+ send_mail(
+ f'EVA: Neuzugang {firstname} {lastname} {firstday}',
+ mail_template.render(context),
+ EVA_MAIL,
+ [contact],
+ fail_silently=False)
+ except BadHeaderError as error:
+ print(error)
+ self.instance.delete()
+ return HttpResponse(f'{error}Invalid header found. Data not saved!')
+ except SMTPException as error:
+ print(error)
+ self.instance.delete()
+ return HttpResponse(f'{error}
Error in sending mails (propably wrong adress?). Data not saved!')
+ except Exception as error:
+ print(error)
+ # self.instance.delete()
+ return HttpResponse(f'{error}
Error in sending mails. Data not saved! Please contact ' + EVA_MAIL)
+ return False
+
+ def beautify_data(self, data):
+ ''' # use long form for contextdata instead of short form if available
+ #
+ # ATTENTION!
+ # This implementation works only for unique keys over all of these dicts from model.py
+ #
+ '''
+
+ # update values in data dictionary with keys from *_CHOICES if present there
+ choices = {**DEPARTMENT_CHOICES, **TRANSPONDER_CHOICES,
+ **OS_CHOICES, **LANG_CHOICES, **KEYBOARD_CHOICES}
+ data.update({k:choices[v] for k,v in data.items() \
+ if isinstance(v,collections.abc.Hashable) \
+ and v in choices})
+
+ # replace values in accounts array from *_CHOICES
+ if 'accounts' in data:
+ data['accounts'] = [ACCOUNT_CHOICES[c] for c in data['accounts']]
+
+ # replace keys in data dictionary with verbose_name
+ # a bit ugly workaround here: we need to store 'email' away, because it es not in the modell
+ mail = ''
+ if 'email' in data:
+ mail = data.pop('email')
+ newdata = {self.instance._meta.get_field(k).verbose_name.title() : v for k,v in data.items()}
+ if mail:
+ newdata['Email'] = mail
+
+ # translate booleans
+ newdata.update({k:'Ja' for k,v in newdata.items() if isinstance(v,bool) and v == True})
+ newdata.update({k:'Nein' for k,v in newdata.items() if isinstance(v,bool) and v == False})
+ # handle some special data types
+ newdata.update({k:'' for k,v in newdata.items() if v == None})
+ newdata.update({k:'' for k,v in newdata.items() if v == []})
+
+ return newdata
diff --git a/settings.py b/settings.py
new file mode 120000
index 0000000..3dd5fee
--- /dev/null
+++ b/settings.py
@@ -0,0 +1 @@
+settings_development.py
\ No newline at end of file
diff --git a/veränderung/__init__.py b/veränderung/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/veränderung/admin.py b/veränderung/admin.py
new file mode 100644
index 0000000..067bbae
--- /dev/null
+++ b/veränderung/admin.py
@@ -0,0 +1,6 @@
+from django.contrib import admin
+from .models import Employee
+
+admin.site.register([
+ Employee,
+ ])
diff --git a/veränderung/apps.py b/veränderung/apps.py
new file mode 100644
index 0000000..f8024ad
--- /dev/null
+++ b/veränderung/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class EvappConfig(AppConfig):
+ name = 'veränderung'
diff --git a/veränderung/forms.py b/veränderung/forms.py
new file mode 100644
index 0000000..662df53
--- /dev/null
+++ b/veränderung/forms.py
@@ -0,0 +1,67 @@
+from django.db import models
+from django.forms import ModelForm, DateInput, Form, ChoiceField, RadioSelect
+from django.core.exceptions import ValidationError
+
+from .models import Employee
+
+# class EmployeeForm(ModelForm):
+# class Meta:
+# model = Employee
+# fields = '__all__'
+# widgets = {'firstdate_employment': DateInput(attrs={'type': 'date'}),
+# 'firstdate_presence': DateInput(attrs={'type': 'date'}),}
+
+class DummyForm(ModelForm):
+ class Meta:
+ model = Employee
+ fields = []
+
+class EvaForm(ModelForm):
+ '''this base class provides the required css class for all forms'''
+ required_css_class = 'required'
+
+TYPE_CHOICES = {'IN': 'Eintritt', 'CHANGE': 'Veränderung', 'OUT': 'Austritt'}
+
+class PersonalForm(EvaForm):
+ # TODO: comment this back in to use implementation of change or exit process
+ # choice = ChoiceField(choices=TYPE_CHOICES.items(), widget=RadioSelect,
+ # label='Welcher Prozess soll angestoßen werden?')
+
+ class Meta:
+ model = Employee
+ fields = ['firstname', 'lastname', 'department', 'team', 'add_to_wikimediade',]
+
+class WorkingForm(EvaForm):
+
+ def clean(self):
+ data = self.cleaned_data
+ if data['works_in_gs'] and data['desk'] is None:
+ raise ValidationError('Wer nicht remote arbeitet braucht einen Schreibtisch!')
+ return data
+
+ class Meta:
+ model = Employee
+ fields = ['firstdate_employment', 'firstdate_presence', 'jobdescription_german',
+ 'jobdescription_english', 'works_in_gs', 'desk',]
+ widgets = {'firstdate_employment': DateInput(attrs={'type': 'date'}),
+ 'firstdate_presence': DateInput(attrs={'type': 'date'}),}
+
+class ITForm(EvaForm):
+
+ class Meta:
+ model = Employee
+ fields = [
+ 'framework', 'os', 'keyboard', 'mobile', 'landline',
+ 'comment', 'language', 'accounts', 'lists', 'rebu2go' ]
+
+class OfficeForm(EvaForm):
+ class Meta:
+ model = Employee
+ fields = ['transponder', 'special', 'post_office_box',]
+
+class ChangeForm(EvaForm):
+ class Meta:
+ model = Employee
+ fields = ['firstdate_employment', 'jobdescription_german', 'jobdescription_english',
+ 'desk', 'comment', 'accounts', 'lists', 'transponder']
+ widgets = {'firstdate_employment': DateInput(attrs={'type': 'date'}),}
diff --git a/veränderung/migrations/0001_initial.py b/veränderung/migrations/0001_initial.py
new file mode 100644
index 0000000..8ec82ff
--- /dev/null
+++ b/veränderung/migrations/0001_initial.py
@@ -0,0 +1,46 @@
+# Generated by Django 3.1.4 on 2021-09-13 12:41
+
+from django.db import migrations, models
+import multiselectfield.db.fields
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Employee',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('firstname', models.CharField(max_length=50, verbose_name='Vorname')),
+ ('lastname', models.CharField(max_length=50, verbose_name='Nachname')),
+ ('department', models.CharField(choices=[('PROG', 'Programme'), ('SOFT', 'Softwareentwicklung'), ('CENT', 'Central'), ('VOR', 'Vorstand')], max_length=5, verbose_name='Bereich')),
+ ('team', models.CharField(blank=True, max_length=20, null=True)),
+ ('add_to_wikimediade', models.BooleanField(default=False, verbose_name='Soll auf wikimedia.de irgendwo stehen?')),
+ ('firstdate_employment', models.DateField(null=True, verbose_name='Erster Arbeitstag')),
+ ('firstdate_presence', models.DateField(null=True, verbose_name='Erster Tag der Anwesenheit in der Geschäftsstelle')),
+ ('jobdescription_german', models.CharField(max_length=100, null=True, verbose_name='Stellenbezeichnung(deutsch)')),
+ ('jobdescription_english', models.CharField(max_length=100, null=True, verbose_name='Job description(english)')),
+ ('remote', models.BooleanField(default=True, verbose_name='Braucht Arbeitsplatz in der Geschäftsstelle?')),
+ ('desk', models.CharField(blank=True, max_length=100, null=True, verbose_name='Wo soll der Arbeitsplatz sein?')),
+ ('vendor', models.CharField(choices=[('STANDARD', 'Dell Latitude'), ('LENOVO', 'Lenovo Thinkpad'), ('MAC', 'Mac (nur für Grafiker_innen)')], default='STANDARD', max_length=8, verbose_name='Hersteller')),
+ ('os', models.CharField(choices=[('UBU', 'Ubuntu (Standard)'), ('WIN', 'Windows (bitte Begründung angeben)'), ('MOS', 'Mac OS (nur wenn Mac gewählt)')], default='UBU', max_length=3, verbose_name='Betriebssystem')),
+ ('screen', models.BooleanField(default=False, verbose_name='Zusätzlicher Monitor? Einer ist standard.')),
+ ('mobile', models.BooleanField(default=False, max_length=6, verbose_name='Diensttelefon (Handy)')),
+ ('landline', models.BooleanField(default=False, verbose_name='Festnetznummer (Sipgate)')),
+ ('keyboard', models.CharField(choices=[('DE', 'Deutsch'), ('US', 'USA'), ('OT', 'Anderes (Bitte unten angeben)')], default='DE', max_length=2, verbose_name='Tastaturlayout')),
+ ('comment', models.TextField(blank=True, max_length=500, null=True, verbose_name='zusätzliche IT-Anforderungen')),
+ ('language', models.CharField(choices=[('GER', 'Deutsch'), ('ENG', 'English')], default='GER', max_length=3, verbose_name='Sprache für Onboarding')),
+ ('accounts', multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('OTRSWMDE', 'OTRS Ticketsystem'), ('CIVIC1', 'Civic CRM (allgemein)'), ('CIVIC2', 'Civic CRM (Mailings, impliziert allgemein)'), ('WEB', 'www.wikimedia.de (edit)'), ('BLOG', 'blog.wikimedia.de (edit)'), ('FORUM', 'forum.wikimedia.de')], max_length=37, null=True, verbose_name='Zusätzliche Accounts')),
+ ('lists', models.CharField(blank=True, max_length=100, null=True, verbose_name='Zusätzliche Mailinglisten')),
+ ('rebu2go', models.BooleanField(default=False, verbose_name='Rebu2Go-Zugang benötigt?')),
+ ('transponder', models.CharField(choices=[('NORM', 'Allgemeiner Transponder'), ('SPECIAL', 'Besondere Schließungen (bitte angeben)'), ('NOTRANS', 'Kein Transponder')], default='NORM', max_length=7)),
+ ('special', models.TextField(blank=True, max_length=500, null=True, verbose_name='Besondere Schließungen hier eintragen')),
+ ('post_office_box', models.BooleanField(default=True, verbose_name='Postfach am Empfang benötigt?')),
+ ],
+ ),
+ ]
diff --git a/veränderung/migrations/0002_auto_20210914_1055.py b/veränderung/migrations/0002_auto_20210914_1055.py
new file mode 100644
index 0000000..0797935
--- /dev/null
+++ b/veränderung/migrations/0002_auto_20210914_1055.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.4 on 2021-09-14 10:55
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('evapp', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='employee',
+ old_name='remote',
+ new_name='works_in_gs',
+ ),
+ ]
diff --git a/veränderung/migrations/0003_auto_20220208_0955.py b/veränderung/migrations/0003_auto_20220208_0955.py
new file mode 100644
index 0000000..e91daf4
--- /dev/null
+++ b/veränderung/migrations/0003_auto_20220208_0955.py
@@ -0,0 +1,28 @@
+# Generated by Django 3.1.4 on 2022-02-08 09:55
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('evapp', '0002_auto_20210914_1055'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='employee',
+ name='department',
+ field=models.CharField(choices=[('PROG', 'Programme'), ('SOFT', 'Softwareentwicklung'), ('CENT', 'Central'), ('KOMEV', 'Kommunikation und Events'), ('VOR', 'Vorstand')], max_length=5, verbose_name='Bereich'),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='team',
+ field=models.CharField(blank=True, max_length=50, null=True),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='works_in_gs',
+ field=models.BooleanField(default=True, verbose_name='Braucht Arbeitsplatz in der Geschäftsstelle?)'),
+ ),
+ ]
diff --git a/veränderung/migrations/0004_alter_employee_accounts_alter_employee_department.py b/veränderung/migrations/0004_alter_employee_accounts_alter_employee_department.py
new file mode 100644
index 0000000..3456ec8
--- /dev/null
+++ b/veränderung/migrations/0004_alter_employee_accounts_alter_employee_department.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.2.4 on 2023-08-11 10:28
+
+from django.db import migrations, models
+import multiselectfield.db.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('evapp', '0003_auto_20220208_0955'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='employee',
+ name='accounts',
+ field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('OTRSWMDE', 'OTRS Ticketsystem'), ('CIVIC1', 'Civic CRM (allgemein)'), ('CIVIC2', 'Civic CRM (Mailings, impliziert allgemein)'), ('WEB', 'www.wikimedia.de (edit)'), ('BLOG', 'blog.wikimedia.de (edit)'), ('FORUM', 'forum.wikimedia.de')], max_length=10, null=True, verbose_name='Zusätzliche Accounts'),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='department',
+ field=models.CharField(choices=[('COENG', 'Communitys & Engagement'), ('SOFT', 'Softwareentwicklung'), ('CENT', 'Central'), ('KOMAD', 'Kommunikation & Advocacy'), ('VOR', 'Vorstand')], max_length=5, verbose_name='Bereich'),
+ ),
+ ]
diff --git a/veränderung/migrations/0005_alter_employee_accounts.py b/veränderung/migrations/0005_alter_employee_accounts.py
new file mode 100644
index 0000000..ca42bb6
--- /dev/null
+++ b/veränderung/migrations/0005_alter_employee_accounts.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.4 on 2023-08-17 11:08
+
+from django.db import migrations
+import multiselectfield.db.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('evapp', '0004_alter_employee_accounts_alter_employee_department'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='employee',
+ name='accounts',
+ field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('OTRSWMDE', 'OTRS Ticketsystem'), ('CIVIC1', 'Civic CRM (allgemein)'), ('CIVIC2', 'Civic CRM (Mailings, impliziert allgemein)'), ('WEB', 'www.wikimedia.de (edit)'), ('BLOG', 'blog.wikimedia.de (edit)'), ('FORUM', 'forum.wikimedia.de')], max_length=40, null=True, verbose_name='Zusätzliche Accounts'),
+ ),
+ ]
diff --git a/veränderung/migrations/0006_remove_employee_vendor_employee_framework_and_more.py b/veränderung/migrations/0006_remove_employee_vendor_employee_framework_and_more.py
new file mode 100644
index 0000000..b34e5fd
--- /dev/null
+++ b/veränderung/migrations/0006_remove_employee_vendor_employee_framework_and_more.py
@@ -0,0 +1,32 @@
+# Generated by Django 4.2.16 on 2024-11-26 09:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('evapp', '0005_alter_employee_accounts'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='employee',
+ name='vendor',
+ ),
+ migrations.AddField(
+ model_name='employee',
+ name='framework',
+ field=models.CharField(blank=True, max_length=300, null=True, verbose_name='Möchten Sie vom Standard des Frameworks abweichen, und wenn ja, warum?'),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='add_to_wikimediade',
+ field=models.CharField(choices=[('NEIN', 'Nein'), ('JA', 'Ja')], default=True, max_length=5, verbose_name='Soll die Person bei Ansprechpartner*innen auf der WMDE-Webseite mit aufgenommen werden?'),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='os',
+ field=models.CharField(choices=[('FED', 'Fedora (Standard)'), ('WIN', 'Windows (bitte Begründung angeben)'), ('MOS', 'Mac OS (nur wenn Mac gewählt)'), ('UBU', 'Ubuntu')], default='FED', max_length=3, verbose_name='Betriebssystem'),
+ ),
+ ]
diff --git a/veränderung/migrations/0007_remove_employee_screen_and_more.py b/veränderung/migrations/0007_remove_employee_screen_and_more.py
new file mode 100644
index 0000000..87124e9
--- /dev/null
+++ b/veränderung/migrations/0007_remove_employee_screen_and_more.py
@@ -0,0 +1,32 @@
+# Generated by Django 4.2.17 on 2024-12-17 14:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('evapp', '0006_remove_employee_vendor_employee_framework_and_more'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='employee',
+ name='screen',
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='add_to_wikimediade',
+ field=models.CharField(choices=[('NEIN', 'Nein'), ('JA', 'Ja')], max_length=5, verbose_name='Soll die Person bei Ansprechpartner*innen auf der WMDE-Webseite mit aufgenommen werden?'),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='framework',
+ field=models.CharField(blank=True, max_length=300, null=True, verbose_name='Möchten Sie vom Standard des Frameworks (Laptop) abweichen, und wenn ja, warum?'),
+ ),
+ migrations.AlterField(
+ model_name='employee',
+ name='id',
+ field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ ]
diff --git a/veränderung/migrations/__init__.py b/veränderung/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/veränderung/models.py b/veränderung/models.py
new file mode 100644
index 0000000..e07fcac
--- /dev/null
+++ b/veränderung/models.py
@@ -0,0 +1,86 @@
+from django.db import models
+from multiselectfield import MultiSelectField
+from django.utils.translation import gettext_lazy as _
+
+# ATTENTION!!!
+# No key should be used twice in any of these dicts because of the
+# suboptimal implementation in views.EvaFormView.beautify_data()
+#
+
+DEPARTMENT_CHOICES = {'COENG': _('Communitys & Engagement'),
+ 'SOFT': _('Softwareentwicklung'),
+ 'CENT': 'Central',
+ 'KOMAD': _('Kommunikation & Advocacy'),
+ 'VOR': _('Vorstand'),}
+
+#VENDOR_CHOICES = {'STANDARD': 'Dell Latitude',
+# 'LENOVO': 'Lenovo Thinkpad',
+# 'MAC': _('Mac (nur für Grafiker_innen)')}
+
+OS_CHOICES = {'FED': 'Fedora (Standard)',
+ 'WIN': _('Windows (bitte Begründung angeben)'),
+ 'MOS': _('Mac OS (nur wenn Mac gewählt)'),
+ 'UBU': _('Ubuntu')}
+
+LANG_CHOICES = {'GER': 'Deutsch',
+ 'ENG': 'English',}
+
+KEYBOARD_CHOICES = {'DE': 'Deutsch',
+ 'US': 'USA',
+ 'OT': _('Anderes (Bitte unten angeben)')}
+
+ACCOUNT_CHOICES = {'OTRSWMDE': 'OTRS Ticketsystem',
+ 'CIVIC1': _('Civic CRM (allgemein)'),
+ 'CIVIC2': _("Civic CRM (Mailings, impliziert allgemein)"),
+ 'WEB': 'www.wikimedia.de (edit)',
+ 'BLOG': 'blog.wikimedia.de (edit)',
+ 'FORUM': 'forum.wikimedia.de',
+ }
+
+TRANSPONDER_CHOICES = {'NORM': _('Allgemeiner Transponder'),
+ 'SPECIAL': _('Besondere Schließungen (bitte angeben)'),
+ 'NOTRANS': _('Kein Transponder'),}
+
+JANEIN_CHOICES = {'NEIN': ('Nein'),
+ 'JA': _('Ja'),}
+
+class Employee(models.Model):
+
+ # email adress of user. should not be necessary if we use openauth one day
+ # usermail = models.EmailField(max_length=50, verbose_name="Deine Mailadresse (Ansprechpartner_in)", default='bestechefin@wikimedia.de')
+
+ # personal data
+ firstname = models.CharField(max_length=50, verbose_name=_("Vorname"))
+ lastname = models.CharField(max_length=50, verbose_name=_("Nachname"))
+ # intern = models.BooleanField(verbose_name='Interne_r Mitarbeiter_in?', default=True)
+ department = models.CharField(max_length=5, choices=DEPARTMENT_CHOICES.items(), verbose_name=_('Bereich'))
+ team = models.CharField(max_length=50, null=True, blank=True) # TODO? better with choices?
+ add_to_wikimediade = models.CharField(max_length=5, choices=JANEIN_CHOICES.items(), verbose_name=_("Soll die Person bei Ansprechpartner*innen auf der WMDE-Webseite mit aufgenommen werden?"))
+
+ # general work related stuff
+ firstdate_employment = models.DateField(null=True, verbose_name=_("Erster Arbeitstag"))
+ firstdate_presence = models.DateField(null=True, verbose_name=_("Erster Tag der Anwesenheit in der Geschäftsstelle"))
+ jobdescription_german = models.CharField(null=True, max_length=100, verbose_name="Stellenbezeichnung(deutsch)")
+ jobdescription_english = models.CharField(null=True, max_length=100, verbose_name="Job description(english)")
+ works_in_gs = models.BooleanField(verbose_name=_('Braucht Arbeitsplatz in der Geschäftsstelle?)'), default=True)
+ desk = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Wo soll der Arbeitsplatz sein?"))
+
+ # IT related stuff
+ #vendor = models.CharField(max_length=8, choices=VENDOR_CHOICES.items(), default='STANDARD', verbose_name=_('Hersteller'))
+ framework = models.CharField(max_length=300, null=True, blank=True, verbose_name=_("Möchten Sie vom Standard des Frameworks (Laptop) abweichen, und wenn ja, warum?"))
+ os = models.CharField(max_length=3, choices=OS_CHOICES.items(), default='FED', verbose_name=_('Betriebssystem'))
+ #screen = models.BooleanField(default=False, verbose_name=_('Zusätzlicher Monitor? Einer ist standard.'))
+ mobile = models.BooleanField(max_length=6, default=False, verbose_name=_('Diensttelefon (Handy)'))
+ landline = models.BooleanField(default = False, verbose_name=_('Festnetznummer (Sipgate)'))
+ # sim = models.BooleanField(default=False, verbose_name="Mobilfunkvertrag")
+ keyboard = models.CharField(max_length=2, choices=KEYBOARD_CHOICES.items(), default='DE', verbose_name=_("Tastaturlayout"))
+ comment = models.TextField(max_length=500, null=True, blank=True, verbose_name=_("zusätzliche IT-Anforderungen"))
+ language = models.CharField(max_length=3, choices=LANG_CHOICES.items(), default="GER", verbose_name=_("Sprache für Onboarding"))
+ accounts = MultiSelectField(choices=ACCOUNT_CHOICES.items(), max_length=40, null=True, blank=True, verbose_name=_("Zusätzliche Accounts"))
+ lists = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Zusätzliche Mailinglisten"))
+ rebu2go = models.BooleanField(verbose_name=_("Rebu2Go-Zugang benötigt?"), default=False)
+
+ # office related stuff
+ transponder = models.CharField(max_length=7, choices=TRANSPONDER_CHOICES.items(), default='NORM')
+ special = models.TextField(max_length=500, null=True, blank=True, verbose_name=_("Besondere Schließungen hier eintragen"))
+ post_office_box = models.BooleanField(default=True, verbose_name=_('Postfach am Empfang benötigt?'))
diff --git a/veränderung/settings.py b/veränderung/settings.py
new file mode 100644
index 0000000..10e3d99
--- /dev/null
+++ b/veränderung/settings.py
@@ -0,0 +1,61 @@
+# temporary setting while change and exit is not yet fully implemented
+ONLY_ONBOARDING = True
+
+# sender mail adress also used for MAILTEST mode
+EVA_MAIL = 'it-support@wikimedia.de'
+
+# these Fields should be included in every mail
+BASIC_DATA = ['firstname', 'lastname', 'firstdate_employment', 'firstdate_presence',
+ 'jobdescription_german', 'jobdescription_english',]
+
+# for every department: 'MAIL' => mail adress, 'DATA': additional fields to include
+# also one copy with all fields to the person filling the form.
+MAILS = {
+ 'IT': {
+ 'MAIL': 'wmde-it@wikimedia.de',
+ 'DATA': [
+ 'laptop', 'os', 'comment', 'email', 'landline', 'lists', 'mobile',
+ 'department', 'accounts', 'language', 'screen', 'works_in_gs', 'desk',
+ 'keyboard',
+ ],
+ },
+ 'OFFICE': {
+ 'MAIL': 'office@wikimedia.de',
+ 'DATA': [
+ 'transponder', 'special', 'post_office_box', 'mobile',
+ 'works_in_gs', 'desk',
+ ],
+ },
+ 'KOMM': {
+ 'MAIL': 'presse@wikimedia.de',
+ 'DATA': [
+ 'department', 'team', 'add_to_wikimediade'
+ ],
+ },
+ 'CENTRAL': {
+ 'MAIL': 'anna.noelte@wikimedia.de',
+ 'DATA': [
+ 'department', 'team', 'language', 'mobile', 'rebu2go'
+ ],
+ },
+ 'HR': {
+ 'MAIL': 'personal@wikimedia.de',
+ 'DATA': [
+ 'department', 'team', 'language',
+ ]
+ },
+ 'FINANCE': {
+ 'MAIL': 'claudia.langrock@wikimedia.de',
+ 'DATA': [
+ 'rebu2go'
+ ]
+ },
+ 'SUBMITTER': {
+ 'MAIL': 'submitter@wikimedia.de',
+ 'DATA': [
+ 'department', 'team', 'add_to_wikimediade', 'remote', 'desk', 'vendor',
+ 'os', 'screen', 'mobile', 'landline', 'keyboard', 'comment', 'language',
+ 'accounts', 'lists', 'rebu2go', 'transponder', 'special', 'post_office_box'
+ ]
+ }
+ }
diff --git a/veränderung/static/evapp/logo.png b/veränderung/static/evapp/logo.png
new file mode 100644
index 0000000..cec78db
Binary files /dev/null and b/veränderung/static/evapp/logo.png differ
diff --git a/veränderung/templates/account/login.html b/veränderung/templates/account/login.html
new file mode 100644
index 0000000..7a0ea12
--- /dev/null
+++ b/veränderung/templates/account/login.html
@@ -0,0 +1,27 @@
+{% load i18n %}
+{% load static %}
+
+
+
+
+
+{{ form.media }}
+
+
+
+
+{% load account socialaccount %}
+
+{% block content %}
+
+
+
+ E (V A) - Eintritt, (Veränderung, Austritt)
+
+
+Bitte via Wolke einloggen:
+{% include "socialaccount/snippets/provider_list.html" with process="login" %}
+
+
+
+{% endblock %}
diff --git a/veränderung/templates/registration/login.html b/veränderung/templates/registration/login.html
new file mode 100644
index 0000000..cbceed1
--- /dev/null
+++ b/veränderung/templates/registration/login.html
@@ -0,0 +1,35 @@
+{% block content %}
+
+ {% if form.errors %}
+ Your username and password didn't match. Please try again.
+ {% endif %}
+
+ {% if next %}
+ {% if user.is_authenticated %}
+ Your account doesn't have access to this page. To proceed,
+ please login with an account that has access.
+ {% else %}
+ Please login to see this page.
+ {% endif %}
+ {% endif %}
+
+
+
+ {# Assumes you setup the password_reset view in your URLconf #}
+ Lost password?
+
+{% endblock %}
diff --git a/veränderung/templates/veränderung/dataloop.txt b/veränderung/templates/veränderung/dataloop.txt
new file mode 100644
index 0000000..6e484a9
--- /dev/null
+++ b/veränderung/templates/veränderung/dataloop.txt
@@ -0,0 +1,5 @@
+{% load i18n %}
+{% autoescape off %}
+{% for key, value in data.items %}{% if key == 'laptop' %} {{ key }}: {{ value | safe}}{% else %}
+{% trans key %}: {{ value }}{% endif %}{% endfor %}
+{% endautoescape %}
diff --git a/veränderung/templates/veränderung/department_mail.txt b/veränderung/templates/veränderung/department_mail.txt
new file mode 100644
index 0000000..8645617
--- /dev/null
+++ b/veränderung/templates/veränderung/department_mail.txt
@@ -0,0 +1,31 @@
+{% load i18n %}
+
+(english below)
+
+Hallo!
+
+Es gibt einen Neuzugang bei Wikimedia! Hier ( https://wiki.wikimedia.de/wiki/Onboarding ) kannst Du nachsehen,
+welche Schritte jetzt für Deine Abteilung nötig werden. Im Folgenden alle Daten,
+die Du dafür brauchst:
+
+{% include 'evapp/dataloop.txt' %}
+
+Wenn Du Fragen hast, melde Dich bei {{contact}}.
+
+Grüße, Deine E.V.A.
+
+-------------------------
+{% language 'en' %}
+Hi!
+
+There is a new employee at Wikimedia! Here ( https://wiki.wikimedia.de/wiki/Onboarding ) you can see, which
+steps are now necessary for your department.
+
+All Data you need for this:
+
+{% include 'evapp/dataloop.txt' %}
+
+If you have any questions please write to {{contact}}.
+
+Regards, Your E.V.A.
+{% endlanguage %}
diff --git a/veränderung/templates/veränderung/employee_form.html b/veränderung/templates/veränderung/employee_form.html
new file mode 100644
index 0000000..26ba681
--- /dev/null
+++ b/veränderung/templates/veränderung/employee_form.html
@@ -0,0 +1,110 @@
+
+{% load i18n %}
+{% load static %}
+
+
+
+
+
+{{ form.media }}
+
+
+
+
+{% load socialaccount %}
+{% if user.is_authenticated %}
+{% block content %}
+
+{% get_current_language as LANGUAGE_CODE %}
+
+
+
+
+
+ {% if TESTMODE %}
+ {% translate "WARNUNG! Test-MODUS aktiviert. Es werden keine Mails verschickt!" %}
+ {% endif %}
+
+ E (V A) - Veränderung, (Veränderung, Austritt)
+ {% translate "Du bist eingeloggt als" %} {{ user.email }}
+
+
+
{% translate "Schritt" %} {{ wizard.steps.step1 }} {% translate "von" %} {{ wizard.steps.count }}
+ {% if wizard.steps.step1 == 1 %}
+ {% translate "Angaben zur Person" %} {% endif %}
+{% if choice == 'IN' %}
+ {% if wizard.steps.step1 == 2 %}
+ {% translate "Angaben zum neuen Arbeitsverhältnis" %}
+ {% elif wizard.steps.step1 == 3 %}
+ {% translate "IT-relevante Angaben" %}
+ {% elif wizard.steps.step1 == 4 %}
+ {% translate "Office-relevante Angaben" %}
+ {% endif %}
+{% else %}
+ {% if wizard.steps.step1 == 2 %}
+ Veränderungsrelevante Angaben
+ {% endif %}
+{% endif %}
+{% if datatable == True %}
+ {% translate "Bestätigungsschritt" %}
+{% endif %}
+
+
+
+ {% if datatable == True %}
+
+ {% for key, value in data.items %}
+ {{ key }} | {{ value }} |
+ {% endfor %}
+
+ {% endif %}
+
+
+
+ {% translate "logout" %}
+
+{% endblock %}
+{% else %}
+{% translate "Bitte einloggen!" %}
+{% endif %}
diff --git a/veränderung/tests.py b/veränderung/tests.py
new file mode 100644
index 0000000..909b612
--- /dev/null
+++ b/veränderung/tests.py
@@ -0,0 +1,127 @@
+from django.test import TestCase
+from django.test import Client
+from django.contrib.auth.models import User
+from django.conf import settings
+from django.http import HttpResponse
+from django.core import mail
+from django.utils import translation
+
+from .forms import ITForm, WorkingForm, OfficeForm, DummyForm
+
+class LoginTestCase(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.user = User.objects.create_user('vladimir', 'vladimir@reiherzehe.com', 'reiherzehe')
+ self.client.login(username='vladimir', password='reiherzehe')
+ self.response = self.client.get('/')
+
+ def testLogin(self):
+ self.assertContains(self.response, 'Du bist eingeloggt als vladimir@reiherzehe.com', status_code=200)
+ response_en = self.client.get('/', HTTP_ACCEPT_LANGUAGE='en-us')
+ self.assertContains(response_en, 'You are logged in as vladimir@reiherzehe.com', status_code=200)
+ self.assertContains(response_en, 'Firstname', status_code=200)
+ response_en = self.client.get('/', HTTP_ACCEPT_LANGUAGE='en')
+ self.assertContains(response_en, 'You are logged in as vladimir@reiherzehe.com', status_code=200)
+ self.assertContains(response_en, 'Firstname', status_code=200)
+
+ def testDebugWarning(self):
+ with self.settings(DEBUG=True):
+ self.response = self.client.get('/') # we need to do it again with DEBUG = True
+ self.assertContains(self.response, "WARNUNG! Test-MODUS aktiviert. Es werden keine Mails verschickt!", status_code=200)
+ with self.settings(DEBUG=False) and self.settings(MAILTEST=False):
+ self.response = self.client.get('/') # we need to do it again with DEBUG = False
+ self.assertNotContains(self.response, "WARNUNG! Test-MODUS aktiviert. Es werden keine Mails verschickt!", status_code=200)
+
+ def _postform(self, data, expected_form):
+ '''helper function to manage the Wizzard'''
+ response = self.client.post('/', data, follow=True)
+ # print(type(response))
+ self.assertEqual(200, self.response.status_code)
+ if not type(response) == HttpResponse:
+ if 'form' in response.context:
+ self.assertFalse(response.context['form'].errors)
+ else:
+ raise "NO FORM FOUND"
+ self.assertEqual(
+ type(response.context['wizard']['form']),
+ expected_form
+ )
+ return response
+
+ def test_department(self):
+ self.assertContains(self.response, 'Programme', status_code=200)
+ self.assertContains(self.response, 'Kommunikation und Events', status_code=200)
+
+ def test_wizzard_in(self):
+ ''' this test goes through the whole onboarding process of the EvaFormView from start to end '''
+
+ self.assertEqual(200, self.response.status_code)
+
+ response = self._postform({
+ 'eva_form_view-current_step': '0',
+ '0-firstname': 'Ara',
+ '0-lastname': 'Seva',
+ '0-department': 'CENT',
+ '0-team': 'Community Communications',
+ '0-choice': 'IN',
+ }, WorkingForm)
+
+ response = self._postform({
+ 'eva_form_view-current_step': '1',
+ '1-firstdate_employment': '2021-01-01',
+ '1-firstdate_presence': '2021-01-01',
+ '1-jobdescription_german': 'hau drauf',
+ '1-jobdescription_english': 'und schluss',
+ '1-works_in_gs': False
+ }, ITForm)
+
+ response = self._postform({
+ 'eva_form_view-current_step': '2',
+ '2-vendor': 'STANDARD',
+ '2-os': 'UBU',
+ '2-keyboard': 'DE',
+ '2-language': 'GER'
+ }, OfficeForm)
+
+ response = self._postform({
+ 'eva_form_view-current_step': '3',
+ '3-transponder': 'NORM'
+ }, DummyForm)
+
+ response = self._postform({
+ 'eva_form_view-current_step': '5',
+ }, DummyForm)
+
+
+
+
+ def test_mail(self):
+ self.test_wizzard_in()
+ # print(mail.outbox[0].body)
+ self.assertGreater(len(mail.outbox), 2)
+ self.assertIn("Vorname", mail.outbox[0].body)
+ self.assertIn("Firstname", mail.outbox[0].body)
+ for i in (0,1,3):
+ self.assertIn("Handy", mail.outbox[i].body)
+ self.assertIn("Ara Seva", mail.outbox[0].subject)
+
+class NoLoginTestCase(TestCase):
+ def setUp(self):
+ self.client = Client()
+
+ def test_details(self):
+ response = self.client.get('/')
+ self.assertEqual(response.status_code, 302)
+ response2 = self.client.get(response.url)
+ self.assertContains( response2, 'Bitte via Wolke einloggen:', status_code=200)
+
+class ITFORMTestCase(TestCase):
+ def test_mac(self):
+ form = ITForm(data={"vendor": 'MAC', 'os': 'UBU'})
+ #print (form.errors)
+ self.assertEqual(form.non_field_errors(), ['Ein MAC sollte Mac OS installiert haben'])
+
+ def test_ubu(self):
+ form = ITForm(data={"vendor": 'STANDARD', 'os': 'UBU'})
+ #print (form.errors)
+ self.assertNotEqual(form.non_field_errors(), ['Ein MAC sollte Mac OS installiert haben'])
diff --git a/veränderung/urls.py b/veränderung/urls.py
new file mode 100644
index 0000000..7d0a9d1
--- /dev/null
+++ b/veränderung/urls.py
@@ -0,0 +1,13 @@
+from django.urls import path
+
+from .views import EvaFormView, success, long_process, change_process
+
+urlpatterns = [
+ path('', EvaFormView.as_view(condition_dict = {'1': long_process,
+ '2': long_process,
+ '3': long_process,
+
+ '4': change_process,}),
+ name='evaform'),
+ path('success', success, name='success')
+ ]
diff --git a/veränderung/views.py b/veränderung/views.py
new file mode 100644
index 0000000..1d33197
--- /dev/null
+++ b/veränderung/views.py
@@ -0,0 +1,217 @@
+from smtplib import SMTPException
+import collections
+
+from django.views.generic.edit import CreateView
+from django.urls import reverse
+from django.http import HttpResponse, HttpResponseRedirect
+from django.core.mail import send_mail, BadHeaderError
+from django.template.loader import get_template
+from formtools.wizard.views import CookieWizardView
+from django.shortcuts import render
+from django.conf import settings
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.utils.translation import gettext_lazy as _
+
+from .models import Employee, DEPARTMENT_CHOICES, OS_CHOICES, \
+ LANG_CHOICES, ACCOUNT_CHOICES, TRANSPONDER_CHOICES, KEYBOARD_CHOICES, JANEIN_CHOICES
+from .forms import PersonalForm, WorkingForm, ITForm, OfficeForm, DummyForm,\
+ ChangeForm, TYPE_CHOICES
+from .settings import MAILS, EVA_MAIL, BASIC_DATA, ONLY_ONBOARDING
+
+def success(request):
+ return HttpResponse(f"Vielen Dank! Du hast E.V.A. erfolgreich ausgefüllt. Die Mails an die Abteilungen wurden versendet. Kopien gehen an {request.user.email}.")
+
+def long_process(wizard):
+ '''this method is called via urls.py to determine if a form is part of the IN-Process'''
+
+ if ONLY_ONBOARDING:
+ wizard.set_choice('IN')
+ return True
+ else:
+ data = wizard.get_cleaned_data_for_step('0') or {}
+ # print(data)
+ if data.get('choice') != 'CHANGE':
+ wizard.set_choice('IN')
+ # print('PROZESS IN')
+ return True
+ else:
+ wizard.set_choice('CHANGE')
+ # print('PROZESS NOT IN')
+ return False
+
+def change_process(wizard):
+ ''' this method is called via urls.py to determine if the form is part of the change process'''
+ # print('CHANGE PROZESS')
+ return not long_process(wizard)
+
+
+class EvaFormView(LoginRequiredMixin, CookieWizardView):
+ template_name = 'veränderung/employee_form.html'
+ form_list = [PersonalForm, WorkingForm, ITForm, OfficeForm, ChangeForm, DummyForm]
+ instance = None
+ choice = 'IN'
+
+ # maybe we dont need this, if *_process() would be class methods,
+ # but unsure if this would work fine with the entries in urls.py
+ def set_choice(self, c):
+ self.choice = c
+
+ def generate_email(self, data):
+ (first, *_) = data['firstname'].split(maxsplit=1)
+ (last, *_) = data['lastname'].split(maxsplit=1)
+ name = first + '.' + last
+ #if not data['intern']:
+ # mail = name + '_ext@wikimedia.de'
+ #else:
+ mail = name + '@wikimedia.de'
+ data['email'] = mail
+
+ def get_all_cleaned_data(self):
+ '''this method deletes data which is only used temporary and is not in the modell,
+ it also changes the mail adress of the employee in some circumstances'''
+
+ data = super().get_all_cleaned_data()
+ self.generate_email(data)
+
+ # print("delete CHOICE FROM DATA")
+ if 'choice' in data:
+ del data['choice']
+ return data
+
+
+ def get_context_data(self, form, **kwargs):
+ '''this method is called to give context data to the template'''
+
+ #print('GETCONTEXT')
+ context = super().get_context_data(form=form, **kwargs)
+ testmode = settings.DEBUG or settings.MAILTEST
+ context.update({'choice': self.choice,
+ 'choice_string': TYPE_CHOICES[self.choice],
+ 'TESTMODE': testmode})
+
+ # deliver context for forms if we are in the last step
+ if (self.steps.step1 == 5 or (self.choice != 'IN' and self.steps.step1 == 3)):
+ context.update({'data': self.beautify_data(self.get_all_cleaned_data()),
+ 'datatable': True,})
+ return context
+
+ def get_form_instance(self,step):
+ ''' this method assures, that we use the same model instance for all steps'''
+
+ if self.instance == None:
+ self.instance = Employee()
+ return self.instance
+
+
+ def done(self, form_list, **kwargs):
+ '''this method is called from CookieWizardView after all forms are filled'''
+
+ print ('INSTANCE_DICT')
+ print(self.instance_dict)
+
+ # save data to database
+ for form in form_list:
+ form.save()
+
+ # send data to departments
+ for dep in MAILS:
+ response = self.send_mail_to_department(dep)
+
+ if not settings.DEBUG:
+ self.instance.delete()
+
+ if response:
+ return response
+ else:
+ return HttpResponseRedirect('success')
+
+
+ def send_mail_to_department(self, department):
+ 'send a mail to the given department with the nececcary notifications'
+
+ print(f'send mail to department {department}...')
+
+ contact = self.request.user.email
+ data = self.get_all_cleaned_data()
+ # some data should be in every mail
+ newdata = {k: v for k, v in data.items() if (k in BASIC_DATA)}
+ # only the relevant data should be in the context
+ newdata.update({k: v for k, v in data.items() if (k in MAILS[department]['DATA'])})
+
+ context = {'data': self.beautify_data(newdata), 'contact': contact}
+ firstname = data['firstname']
+ lastname = data['lastname']
+ firstday = data['firstdate_employment']
+ try:
+ mail_template = get_template(f'evapp/department_mail.txt')
+ if settings.MAILTEST:
+ send_mail(
+ f'EVA: Neuzugang {firstname} {lastname} {firstday} (MAILTEST)',
+ mail_template.render(context),
+ EVA_MAIL,
+ [EVA_MAIL],
+ fail_silently=False)
+ elif department != "SUBMITTER":
+ send_mail(
+ f'EVA: Neuzugang {firstname} {lastname} {firstday}',
+ mail_template.render(context),
+ EVA_MAIL,
+ [MAILS[department]['MAIL']],
+ fail_silently=False)
+ else:
+ send_mail(
+ f'EVA: Neuzugang {firstname} {lastname} {firstday}',
+ mail_template.render(context),
+ EVA_MAIL,
+ [contact],
+ fail_silently=False)
+ except BadHeaderError as error:
+ print(error)
+ self.instance.delete()
+ return HttpResponse(f'{error}Invalid header found. Data not saved!')
+ except SMTPException as error:
+ print(error)
+ self.instance.delete()
+ return HttpResponse(f'{error}
Error in sending mails (propably wrong adress?). Data not saved!')
+ except Exception as error:
+ print(error)
+ # self.instance.delete()
+ return HttpResponse(f'{error}
Error in sending mails. Data not saved! Please contact ' + EVA_MAIL)
+ return False
+
+ def beautify_data(self, data):
+ ''' # use long form for contextdata instead of short form if available
+ #
+ # ATTENTION!
+ # This implementation works only for unique keys over all of these dicts from model.py
+ #
+ '''
+
+ # update values in data dictionary with keys from *_CHOICES if present there
+ choices = {**DEPARTMENT_CHOICES, **TRANSPONDER_CHOICES,
+ **OS_CHOICES, **LANG_CHOICES, **KEYBOARD_CHOICES}
+ data.update({k:choices[v] for k,v in data.items() \
+ if isinstance(v,collections.abc.Hashable) \
+ and v in choices})
+
+ # replace values in accounts array from *_CHOICES
+ if 'accounts' in data:
+ data['accounts'] = [ACCOUNT_CHOICES[c] for c in data['accounts']]
+
+ # replace keys in data dictionary with verbose_name
+ # a bit ugly workaround here: we need to store 'email' away, because it es not in the modell
+ mail = ''
+ if 'email' in data:
+ mail = data.pop('email')
+ newdata = {self.instance._meta.get_field(k).verbose_name.title() : v for k,v in data.items()}
+ if mail:
+ newdata['Email'] = mail
+
+ # translate booleans
+ newdata.update({k:'Ja' for k,v in newdata.items() if isinstance(v,bool) and v == True})
+ newdata.update({k:'Nein' for k,v in newdata.items() if isinstance(v,bool) and v == False})
+ # handle some special data types
+ newdata.update({k:'' for k,v in newdata.items() if v == None})
+ newdata.update({k:'' for k,v in newdata.items() if v == []})
+
+ return newdata