Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add enrollment preprocessing CLI tool #2011

Merged
merged 71 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
83995b9
add enrolment_preprocessor and user export endpoint
Kakadus Aug 29, 2023
a2cad5b
enrollment
Kakadus Sep 25, 2023
9c032be
no python fiddling
Kakadus Sep 25, 2023
1fa6d46
add tools to black
Kakadus Oct 9, 2023
ff58fe8
rewrite something
Kakadus Oct 9, 2023
1a70516
remove filter
Kakadus Oct 9, 2023
20bbd79
Merge branch 'main' into enrolment-preprocessor
Kakadus Oct 9, 2023
6353023
remove filter test
Kakadus Oct 9, 2023
09d6bf6
cleanup advanced title logic
Kakadus Oct 9, 2023
6a4c740
enable mypy for evap AND tools
Kakadus Oct 16, 2023
7084323
extract run_preprocessor for tests
Kakadus Oct 16, 2023
f311982
remove import
Kakadus Oct 23, 2023
e77b75b
add user export option for staff index and staff user index
Kakadus Oct 23, 2023
86d73ed
Merge branch 'main' into enrolment-preprocessor
Kakadus Oct 23, 2023
2ac9e40
add tools to pytest config
Kakadus Oct 23, 2023
ead305d
add tools to coverage.run
Kakadus Oct 23, 2023
4f3e437
fix links
Kakadus Oct 23, 2023
43839c4
pylint tools
Kakadus Oct 23, 2023
d81787d
add changes to preprocessor
Kakadus Oct 23, 2023
9db3678
fix test
Kakadus Oct 23, 2023
4937a76
fix spelling and test
Kakadus Oct 30, 2023
37a6caa
dont modify inplace
Kakadus Oct 30, 2023
b67c1c8
add newline for visual separation
Kakadus Oct 30, 2023
ce8c35e
switch to positional
Kakadus Oct 30, 2023
7b693bb
try to delete test
Kakadus Oct 30, 2023
06f0ec9
Merge branch 'main' into enrolment-preprocessor
Kakadus Oct 30, 2023
12f2495
move create_memory_csv_file
Kakadus Oct 30, 2023
4410d0d
no default preprocessor decision
Kakadus Nov 6, 2023
cf1c9b1
fix annotations
Kakadus Nov 13, 2023
1fd0a17
use AttachmentResponse
Kakadus Nov 13, 2023
fd04517
revert breaking changes!
Kakadus Nov 13, 2023
c6a29f7
Update tools/enrollment_preprocessor.py
Kakadus Nov 13, 2023
ac96b31
fix argparse arguments
Kakadus Dec 4, 2023
02ebfee
fix csv format
Kakadus Dec 4, 2023
d4ef115
exclude title row
Kakadus Dec 4, 2023
b920544
Merge remote-tracking branch 'upstream/main' into enrolment-preprocessor
Kakadus Dec 4, 2023
97e6132
fix delimiter in test
Kakadus Dec 4, 2023
f348c6b
use consistent header row and skip them
Kakadus Dec 4, 2023
b45e878
remove j0 pylint setting in action
Kakadus Dec 11, 2023
0d30caf
apply changes from review
Kakadus Dec 22, 2023
1c2d287
add whitespace stripping
Kakadus Dec 22, 2023
5a86e4f
skip empty users with empty emails
Kakadus Dec 22, 2023
a43bda0
add test for new requirements
Kakadus Dec 22, 2023
ee4b6c5
Merge branch 'main' into enrolment-preprocessor
Kakadus Dec 22, 2023
ba7e365
reformat
Kakadus Dec 22, 2023
a67c239
rewrite tests and get cleaning right
Kakadus Dec 22, 2023
f64e813
deduplicate imported users
Kakadus Jan 8, 2024
8fb4ca2
add enrollment preprocessor features
Kakadus Jan 15, 2024
9ff5162
use right delimiter
Kakadus Jan 15, 2024
f5d79f6
ansi to octal
Kakadus Jan 23, 2024
2c31322
format
Kakadus Jan 23, 2024
5fc3e43
ntfs fix
Kakadus Jan 23, 2024
3ee4eaa
do not write file if not changed
Kakadus Jan 23, 2024
70f522c
add latest features: Everything works(tm)
Kakadus Feb 12, 2024
0d74cf2
Merge remote-tracking branch 'upstream' into enrolment-preprocessor
Kakadus Feb 12, 2024
d052cf0
split it up
Kakadus Feb 12, 2024
4adaf88
correctly fix the test
Kakadus Feb 12, 2024
85d6efe
add test that tests everything
Kakadus Feb 12, 2024
f1891d6
reformat
Kakadus Feb 12, 2024
8515bb2
reformat with recent black
Kakadus Feb 12, 2024
4648114
apply changes from code review
Kakadus Feb 19, 2024
3b3a7e8
clean cells directly and reparse xlsx. Refactor as well
Kakadus Feb 19, 2024
a951dfc
simplify
Kakadus Feb 19, 2024
f4b9868
apply review suggestions
Kakadus Feb 26, 2024
a7e2c4d
fix duplicated conflicts
Kakadus Mar 4, 2024
3911607
add test
Kakadus Mar 4, 2024
92647fa
Merge remote-tracking branch 'upstream/main' into enrolment-preprocessor
Kakadus Mar 4, 2024
3b12335
sort for easier tests
Kakadus Mar 4, 2024
3327eba
remove outdated code in tests
Kakadus Mar 4, 2024
ca2400a
notify if changes were necessary
Kakadus Mar 4, 2024
ed54126
f./x path output
Kakadus Mar 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion evap/evaluation/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def vote_end_datetime(vote_end_date):
return date_to_datetime(vote_end_date) + datetime.timedelta(hours=24 + settings.EVALUATION_END_OFFSET_HOURS)


def get_parameter_from_url_or_session(request, parameter, default=False):
def get_parameter_from_url_or_session(request, parameter, default=False) -> bool:
result = request.GET.get(parameter, None)
if result is None: # if no parameter is given take session value
result = request.session.get(parameter, default)
Expand Down
44 changes: 44 additions & 0 deletions evap/staff/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import csv
import datetime
import os
from abc import ABC, abstractmethod
from io import BytesIO
from itertools import cycle
from typing import Literal
from unittest.mock import PropertyMock, patch

Expand Down Expand Up @@ -529,6 +531,48 @@ def test_wrong_files_dont_crash(self):
self.assertIn("An error happened when processing the file", reply)


class TestUserExportView(WebTestStaffMode):
url = "/staff/user/export"

@classmethod
def setUpTestData(cls) -> None:
cls.manager = make_manager()
baker.make(UserProfile, _quantity=5, _fill_optional=["first_name_given", "last_name", "email", "title"])
baker.make(
UserProfile,
_quantity=5,
_fill_optional=["first_name_given", "last_name", "email", "title"],
is_active=False,
)
# the titles are not filled by baker
titles = cycle(("", "Some", "Custom", "Titles"))
for user in UserProfile.objects.iterator():
user.title = next(titles)
user.save()

def assertUsersExported(self, response, user_objects):
reader = csv.reader(response.text.strip().split("\n"))
# skip header
next(reader)
self.assertSetEqual({tuple(row) for row in reader}, user_objects)

def test_export_all(self):
user_objects = {
(user.title or "", user.last_name or "", user.first_name or "", user.email or "")
for user in UserProfile.objects.iterator()
}
response = self.app.get(self.url, user=self.manager)
self.assertUsersExported(response, user_objects)

def test_export_filtered(self):
user_objects = {
(user.title or "", user.last_name or "", user.first_name or "", user.email or "")
for user in UserProfile.objects.exclude(is_active=False).iterator()
}
response = self.app.get(self.url, {"filter_users": True}, user=self.manager)
self.assertUsersExported(response, user_objects)


class TestUserImportView(WebTestStaffMode):
url = "/staff/user/import"

Expand Down
1 change: 1 addition & 0 deletions evap/staff/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
path("user/", views.user_index, name="user_index"),
path("user/create", views.user_create, name="user_create"),
path("user/import", views.user_import, name="user_import"),
path("user/export", views.user_export, name="user_export"),
path("user/<int:user_id>/edit", views.user_edit, name="user_edit"),
path("user/list", views.user_list, name="user_list"),
path("user/delete", views.user_delete, name="user_delete"),
Expand Down
23 changes: 22 additions & 1 deletion evap/staff/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import codecs
import csv
import itertools
from collections import OrderedDict, defaultdict, namedtuple
from collections.abc import Container
from dataclasses import dataclass
from datetime import date, datetime
from io import BytesIO
from typing import Any, cast

import openpyxl
Expand All @@ -15,7 +17,7 @@
from django.dispatch import receiver
from django.forms import formset_factory
from django.forms.models import inlineformset_factory, modelformset_factory
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
from django.http import FileResponse, Http404, HttpRequest, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.html import format_html
Expand Down Expand Up @@ -2116,6 +2118,25 @@ def user_list(request):
return render(request, "staff_user_list.html", {"users": users, "filter_users": filter_users})


@manager_required
def user_export(request):
filter_users = get_parameter_from_url_or_session(request, "filter_users")

users = UserProfile.objects.all()
if filter_users:
users = users.exclude(is_active=False)

io = BytesIO()
Writer = codecs.getwriter("utf-8")
writer = csv.writer(Writer(io))
row = (_("Title"), _("Last name"), _("First name"), _("Email"))
writer.writerow(row)
writer.writerows((user.title, user.last_name, user.first_name, user.email) for user in users)

io.seek(0)
return FileResponse(io, as_attachment=True, filename="exported_users.csv")


@manager_required
def user_create(request):
form = UserForm(request.POST or None, instance=UserProfile())
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extend-exclude = """\

[tool.isort]
profile = "black"
src_paths = ["evap"]
src_paths = ["evap", "tools"]
line_length = 120
skip_gitignore = true
extend_skip_glob = ["**/migrations/*"]
Expand Down
60 changes: 60 additions & 0 deletions tools/enrolment_preprocessor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
import csv
import sys
from argparse import ArgumentParser
from dataclasses import dataclass

from openpyxl import load_workbook
from openpyxl.cell import Cell


@dataclass
class User:
title: str
last_name: str
first_name: str
email: str


def fix_users(users: dict[str, User], title: Cell | None, last_name: Cell, first_name: Cell, email: Cell):
imported = User(last_name=last_name.value, first_name=first_name.value, email=email.value, title=title.value or "" if title else "") # type: ignore[arg-type] # if schema is correct, all values are strings
existing = users.setdefault(imported.email, imported)
if existing != imported:
print("There is a conflict in the user data.")
print(f"existing: {existing}.")
print(f"imported: {imported}.")
if input("Do you want to keep the existing user? (y/n)")[0].lower() == "n":
return

email.value = existing.email
last_name.value = existing.last_name
first_name.value = existing.first_name
if title:
title.value = existing.title


if __name__ == "__main__":
args = sys.argv
if "python" in args[0]:
args = args[1:]
parser = ArgumentParser(description="Commandline tool to preprocess enrollment xlsx files.")
parser.add_argument(
"-ud", "--user-data", help="Path to a csv file containing an export of all existing users.", required=True
)
parser.add_argument(
"-ed", "--enrolment-data", help="Path to the enrolment data in xlsx format for import.", required=True
)
ns = parser.parse_args(args[1:])

workbook = load_workbook(ns.enrolment_data)
user_dict = {}
with open(ns.user_data, encoding="utf-8") as csvfile:
reader = csv.reader(csvfile)
for row in reader:
print(*row)
user_dict[row[0]] = User(*row)
for sheet_name in ["MA Belegungen", "BA Belegungen"]:
for row in workbook[sheet_name].iter_rows(min_row=2, min_col=2):
fix_users(user_dict, None, *row[:3])
fix_users(user_dict, *row[7:])
workbook.save(ns.enrolment_data)