diff --git a/eva/settings_development.py b/eva/settings_development.py index fd37a2e..55986d6 100644 --- a/eva/settings_development.py +++ b/eva/settings_development.py @@ -39,6 +39,9 @@ ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ + 'home', + 'offboarding', + 'veränderung', 'evapp.apps.EvappConfig', 'django.contrib.admin', 'django.contrib.auth', @@ -160,3 +163,4 @@ ACCOUNT_EMAIL_VERIFICATION = 'none' # ACCOUNT_EMAIL_REQUIRED = True LOGIN_REDIRECT_URL = 'home' ACCOUNT_LOGOUT_ON_GET = True +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/eva/urls.py b/eva/urls.py index 0cc51da..49ac613 100644 --- a/eva/urls.py +++ b/eva/urls.py @@ -17,7 +17,10 @@ from django.contrib import admin from django.urls import path, include urlpatterns = [ - path('', include("evapp.urls")), + path('onboarding/', include('evapp.urls')), + path('', include("home.urls")), + path('offboarding/', include("offboarding.urls")), + path('veränderung/', include("veränderung.urls")), path('admin/', admin.site.urls), path('accounts/', include('allauth.urls')), diff --git a/evapp/admin.py b/evapp/admin.py index 067bbae..c467c22 100644 --- a/evapp/admin.py +++ b/evapp/admin.py @@ -1,6 +1,10 @@ +# import Django´s admin interface module from django.contrib import admin +# import the Employee model from the current app from .models import Employee +# Register the Employee model with the Django admin site +# This makes the Employee model manageable through the admin interface admin.site.register([ Employee, ]) diff --git a/evapp/apps.py b/evapp/apps.py index b9f0e6d..d5ded80 100644 --- a/evapp/apps.py +++ b/evapp/apps.py @@ -1,5 +1,7 @@ +# import django´s base AppConfig class from django.apps import AppConfig - +# define the configuration class for the 'evapp' application class EvappConfig(AppConfig): + # give the app a name name = 'evapp' diff --git a/evapp/forms.py b/evapp/forms.py index 662df53..1239e19 100644 --- a/evapp/forms.py +++ b/evapp/forms.py @@ -22,15 +22,13 @@ class EvaForm(ModelForm): TYPE_CHOICES = {'IN': 'Eintritt', 'CHANGE': 'Veränderung', 'OUT': 'Austritt'} +# Form to capture basic personal and department-related information 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',] - +# Form to capture working conditions and job description class WorkingForm(EvaForm): def clean(self): diff --git a/evapp/migrations/0007_remove_employee_screen_and_more.py b/evapp/migrations/0007_remove_employee_screen_and_more.py new file mode 100644 index 0000000..87124e9 --- /dev/null +++ b/evapp/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/evapp/static/evapp/offboarding.png b/evapp/static/evapp/offboarding.png new file mode 100644 index 0000000..850a007 Binary files /dev/null and b/evapp/static/evapp/offboarding.png differ diff --git a/evapp/static/evapp/onboarding1.png b/evapp/static/evapp/onboarding1.png new file mode 100644 index 0000000..aa7ca39 Binary files /dev/null and b/evapp/static/evapp/onboarding1.png differ diff --git a/evapp/static/evapp/veränderung.png b/evapp/static/evapp/veränderung.png new file mode 100644 index 0000000..1d7542f Binary files /dev/null and b/evapp/static/evapp/veränderung.png differ diff --git a/evapp/templates/account/login.html b/evapp/templates/account/login.html index 7a0ea12..73f29e5 100644 --- a/evapp/templates/account/login.html +++ b/evapp/templates/account/login.html @@ -1,12 +1,17 @@ {% load i18n %} {% load static %} + + + + {{ form.media }} + @@ -14,12 +19,17 @@ {% block content %}
+ + +

E (V A) - Eintritt, (Veränderung, Austritt)

+ Bitte via Wolke einloggen: + {% include "socialaccount/snippets/provider_list.html" with process="login" %}
diff --git a/evapp/templates/evapp/employee_form.html b/evapp/templates/evapp/employee_form.html index 9df6037..34944b2 100644 --- a/evapp/templates/evapp/employee_form.html +++ b/evapp/templates/evapp/employee_form.html @@ -38,6 +38,7 @@

E (V A) - Eintritt, (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 %} @@ -60,13 +61,15 @@ {% endif %}

+ {% if datatable == True %} - +
{% for key, value in data.items %} {% endfor %}
{{ key }}{{ value }}
{% endif %} +
{% csrf_token %} 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 %} + + {% endfor %} +
{{ key }}{{ value }}
+ {% endif %} + + + {% csrf_token %} + + {% comment %} + comment this back in if you want to use CHANGE and EXIT process + {% if wizard.steps.step1 > 1 %} + Du hast den Prozess "{{choice_string}}" ausgewählt. + {% endif %} + {% endcomment %} + {{ wizard.management_form }} + {% if wizard.form.forms %} + {{ wizard.form.management_form }} + {% for form in wizard.form.forms %} + {{ form }} + {% endfor %} + {% else %} + {{ wizard.form }} + {% endif %} +
+

+ * {% translate "Pflichtfeld" %} +

+ {% if wizard.steps.prev %} + + {% endif %} + {% if datatable == True %} + + {% else %} + + {% 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 %} + +
+ {% csrf_token %} + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+ + +
+ + {# 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 %} + +
+ {% csrf_token %} + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+ + +
+ + {# 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 %} + + {% endfor %} +
{{ key }}{{ value }}
+ {% endif %} + +
+ {% csrf_token %} + + {% comment %} + comment this back in if you want to use CHANGE and EXIT process + {% if wizard.steps.step1 > 1 %} + Du hast den Prozess "{{choice_string}}" ausgewählt. + {% endif %} + {% endcomment %} + {{ wizard.management_form }} + {% if wizard.form.forms %} + {{ wizard.form.management_form }} + {% for form in wizard.form.forms %} + {{ form }} + {% endfor %} + {% else %} + {{ wizard.form }} + {% endif %} +
+

+ * {% translate "Pflichtfeld" %} +

+ {% if wizard.steps.prev %} + + {% endif %} + {% if datatable == True %} + + {% else %} + + {% 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