Spaces:
Runtime error
Runtime error
Jessica Walkenhorst
commited on
Commit
β’
fbc21be
1
Parent(s):
4ac520e
Feature/add UI (#6)
Browse files* Add first version of streamlit ui
* Add a new test
* Add to Makefile
- Makefile +6 -1
- app.py +72 -0
- poetry.lock +28 -1
- pyproject.toml +1 -0
- src/maorganizer/datawrangling.py +25 -15
- src/maorganizer/ui.py +140 -0
- tests/test_attendancelist.py +15 -6
- tests/test_datawrangling.py +1 -1
Makefile
CHANGED
@@ -1,3 +1,6 @@
|
|
|
|
|
|
|
|
1 |
isort:
|
2 |
./bin/run-isort.sh
|
3 |
|
@@ -7,7 +10,9 @@ flake8:
|
|
7 |
black:
|
8 |
./bin/run-black.sh
|
9 |
|
|
|
|
|
10 |
test:
|
11 |
poetry run pytest
|
12 |
|
13 |
-
prepare:
|
|
|
1 |
+
ui:
|
2 |
+
poetry run streamlit run ui.py
|
3 |
+
|
4 |
isort:
|
5 |
./bin/run-isort.sh
|
6 |
|
|
|
10 |
black:
|
11 |
./bin/run-black.sh
|
12 |
|
13 |
+
tidy: isort black flake8
|
14 |
+
|
15 |
test:
|
16 |
poetry run pytest
|
17 |
|
18 |
+
prepare: tidy test
|
app.py
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pathlib import Path
|
2 |
+
import streamlit as st
|
3 |
+
|
4 |
+
from maorganizer.datawrangling import Person
|
5 |
+
|
6 |
+
from maorganizer.ui import TASKS, render_xlsx_download_button, create_file_uploader, create_task_selector
|
7 |
+
|
8 |
+
st.title("π
Meeting Attendance Organizer")
|
9 |
+
st.markdown("This app fullfills a simple need: Take a list of names of people attending a meeting and peform one (or multiple) of the following tasks:")
|
10 |
+
st.markdown("""* βοΈ Split their names into first name and surname\n* π Compare two lists with each other and see who is new on the second list\n * π Find people in a list by either searching for their complete names or parts of their name\n * πΎ Write any of the results back out, so you can share it with others""")
|
11 |
+
|
12 |
+
st.header("π Step 1: Upload your Files")
|
13 |
+
st.markdown("Upload the file(s) containing your meeting attendees. The expected format is a single column containing the attendees' full names. If you column name is not Name, you will be able to specify the column name after uploading the data. Additional columns will be ignored.")
|
14 |
+
|
15 |
+
meetings = {}
|
16 |
+
meetings = create_file_uploader()
|
17 |
+
|
18 |
+
task = create_task_selector()
|
19 |
+
|
20 |
+
st.header("π₯ Step 3: Let's Go!")
|
21 |
+
st.subheader(f"You are going to ... {task}")
|
22 |
+
if not meetings:
|
23 |
+
st.info("β¬ You need to upload some data first β¬")
|
24 |
+
|
25 |
+
if meetings:
|
26 |
+
if task == TASKS.SPLIT.value:
|
27 |
+
|
28 |
+
filename = st.selectbox("Choose a file π", options=list(meetings.keys()), key=task)
|
29 |
+
#filename = render_file_selector(meetings, key=task)
|
30 |
+
|
31 |
+
render_xlsx_download_button({'Full list of Attendees': meetings[filename]},
|
32 |
+
filename=f"processed-attendees-{Path(filename).stem}.xlsx",
|
33 |
+
key=TASKS.SPLIT.value+'download')
|
34 |
+
|
35 |
+
elif task == TASKS.FIND.value:
|
36 |
+
|
37 |
+
filename = st.selectbox("π Choose a file", options=list(meetings.keys()), key=task)
|
38 |
+
attendees = meetings[filename]
|
39 |
+
|
40 |
+
textinput = st.text_input("π Who are you looking for? If you are looking for more than one, separate them by comma.")
|
41 |
+
st.markdown("β οΈ By default, the algorithm will surface any names that contain your search terms as substrings (e.g. if you search for Jon, it will surface both Jon and Jonathan). Tick the box below in case you want to display only entries where one of the names matches your search string exactly.")
|
42 |
+
st.markdown("β οΈβ οΈ Note that either way the search is case-insensitive.")
|
43 |
+
exact_match = st.checkbox("Find Exact Name")
|
44 |
+
|
45 |
+
if textinput.strip():
|
46 |
+
st.header("Search Results")
|
47 |
+
if exact_match:
|
48 |
+
people_to_find = [Person(word.strip()) for word in textinput.split(',')]
|
49 |
+
|
50 |
+
for to_find, found in attendees.find_people(people_to_find).items():
|
51 |
+
st.subheader(f"**{to_find.name}**")
|
52 |
+
st.markdown(f"{', '.join([p.name for p in found]) if found else 'Sorry, none found.'}")
|
53 |
+
else:
|
54 |
+
words_to_find = [word.strip() for word in textinput.split(',')]
|
55 |
+
|
56 |
+
for word_to_find, people_found in attendees.find_words(words_to_find).items():
|
57 |
+
st.subheader(f"**{word_to_find}**")
|
58 |
+
st.markdown(f"{', '.join([p.name for p in people_found]) if people_found else 'Sorry, none found.'}")
|
59 |
+
|
60 |
+
elif task == TASKS.COMPARE.value:
|
61 |
+
col1, col2 = st.columns(2)
|
62 |
+
with col1:
|
63 |
+
filename_old = st.selectbox("Choose your original file", options=list(meetings.keys()), key=task)
|
64 |
+
with col2:
|
65 |
+
filename_new = st.selectbox("Choose your updated file", options=set(meetings.keys()) - {filename_old})
|
66 |
+
|
67 |
+
listcomparison = (
|
68 |
+
{'Original List': meetings[filename_old],
|
69 |
+
'Updated List - Full': meetings[filename_new],
|
70 |
+
'Updated List - Only Updates': meetings[filename_old].update(meetings[filename_new])})
|
71 |
+
|
72 |
+
render_xlsx_download_button(listcomparison, filename=f"{Path(filename_old).stem}-updated.xlsx", key=TASKS.COMPARE.value+'download')
|
poetry.lock
CHANGED
@@ -308,6 +308,14 @@ category = "main"
|
|
308 |
optional = false
|
309 |
python-versions = ">=3.6"
|
310 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
311 |
[[package]]
|
312 |
name = "exceptiongroup"
|
313 |
version = "1.1.0"
|
@@ -1089,6 +1097,17 @@ category = "main"
|
|
1089 |
optional = false
|
1090 |
python-versions = ">=3.8"
|
1091 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1092 |
[[package]]
|
1093 |
name = "packaging"
|
1094 |
version = "23.0"
|
@@ -1865,7 +1884,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
|
|
1865 |
[metadata]
|
1866 |
lock-version = "1.1"
|
1867 |
python-versions = "^3.10"
|
1868 |
-
content-hash = "
|
1869 |
|
1870 |
[metadata.files]
|
1871 |
aiofiles = [
|
@@ -2181,6 +2200,10 @@ entrypoints = [
|
|
2181 |
{file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"},
|
2182 |
{file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"},
|
2183 |
]
|
|
|
|
|
|
|
|
|
2184 |
exceptiongroup = [
|
2185 |
{file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"},
|
2186 |
{file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"},
|
@@ -2456,6 +2479,10 @@ numpy = [
|
|
2456 |
{file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"},
|
2457 |
{file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"},
|
2458 |
]
|
|
|
|
|
|
|
|
|
2459 |
packaging = [
|
2460 |
{file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
|
2461 |
{file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
|
|
|
308 |
optional = false
|
309 |
python-versions = ">=3.6"
|
310 |
|
311 |
+
[[package]]
|
312 |
+
name = "et-xmlfile"
|
313 |
+
version = "1.1.0"
|
314 |
+
description = "An implementation of lxml.xmlfile for the standard library"
|
315 |
+
category = "main"
|
316 |
+
optional = false
|
317 |
+
python-versions = ">=3.6"
|
318 |
+
|
319 |
[[package]]
|
320 |
name = "exceptiongroup"
|
321 |
version = "1.1.0"
|
|
|
1097 |
optional = false
|
1098 |
python-versions = ">=3.8"
|
1099 |
|
1100 |
+
[[package]]
|
1101 |
+
name = "openpyxl"
|
1102 |
+
version = "3.1.2"
|
1103 |
+
description = "A Python library to read/write Excel 2010 xlsx/xlsm files"
|
1104 |
+
category = "main"
|
1105 |
+
optional = false
|
1106 |
+
python-versions = ">=3.6"
|
1107 |
+
|
1108 |
+
[package.dependencies]
|
1109 |
+
et-xmlfile = "*"
|
1110 |
+
|
1111 |
[[package]]
|
1112 |
name = "packaging"
|
1113 |
version = "23.0"
|
|
|
1884 |
[metadata]
|
1885 |
lock-version = "1.1"
|
1886 |
python-versions = "^3.10"
|
1887 |
+
content-hash = "9857bd72535987e589103fdfd65fcf409ea02207f0c0d0bfc62531770c009a0f"
|
1888 |
|
1889 |
[metadata.files]
|
1890 |
aiofiles = [
|
|
|
2200 |
{file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"},
|
2201 |
{file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"},
|
2202 |
]
|
2203 |
+
et-xmlfile = [
|
2204 |
+
{file = "et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"},
|
2205 |
+
{file = "et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c"},
|
2206 |
+
]
|
2207 |
exceptiongroup = [
|
2208 |
{file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"},
|
2209 |
{file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"},
|
|
|
2479 |
{file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"},
|
2480 |
{file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"},
|
2481 |
]
|
2482 |
+
openpyxl = [
|
2483 |
+
{file = "openpyxl-3.1.2-py2.py3-none-any.whl", hash = "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5"},
|
2484 |
+
{file = "openpyxl-3.1.2.tar.gz", hash = "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184"},
|
2485 |
+
]
|
2486 |
packaging = [
|
2487 |
{file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
|
2488 |
{file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
|
pyproject.toml
CHANGED
@@ -9,6 +9,7 @@ readme = "README.md"
|
|
9 |
python = "^3.10"
|
10 |
pandas = "^1.5.3"
|
11 |
streamlit = "^1.17.0"
|
|
|
12 |
|
13 |
[tool.poetry.group.dev.dependencies]
|
14 |
black = "^23.1.0"
|
|
|
9 |
python = "^3.10"
|
10 |
pandas = "^1.5.3"
|
11 |
streamlit = "^1.17.0"
|
12 |
+
openpyxl = "^3.1.2"
|
13 |
|
14 |
[tool.poetry.group.dev.dependencies]
|
15 |
black = "^23.1.0"
|
src/maorganizer/datawrangling.py
CHANGED
@@ -7,9 +7,7 @@ import pandas as pd
|
|
7 |
|
8 |
DATAFOLDER = Path().cwd() / "data"
|
9 |
|
10 |
-
|
11 |
-
|
12 |
-
FILENAME = f"participants-Meetup-{MONTH}"
|
13 |
|
14 |
|
15 |
@dataclass
|
@@ -17,7 +15,9 @@ class Person:
|
|
17 |
name: str
|
18 |
|
19 |
def __post_init__(self):
|
20 |
-
self.name =
|
|
|
|
|
21 |
|
22 |
def __hash__(self):
|
23 |
return hash(self.name)
|
@@ -38,27 +38,31 @@ class Person:
|
|
38 |
def lastname(self):
|
39 |
return " ".join(self.name.split(" ")[1:])
|
40 |
|
|
|
|
|
|
|
41 |
|
42 |
@dataclass
|
43 |
class Attendancelist:
|
44 |
participants: Set[Person]
|
45 |
|
|
|
|
|
|
|
46 |
def load_from_file(
|
47 |
-
filename: pathlib.PosixPath, cname: str =
|
48 |
):
|
49 |
-
if
|
50 |
-
df = pd.read_csv(filename, sep=sep)
|
51 |
-
elif filename.suffix in [".xlsx", ".xls"]:
|
52 |
df = pd.read_excel(filename)
|
53 |
elif filename.suffix == ".csv":
|
54 |
-
df = pd.read_csv(filename)
|
55 |
else:
|
56 |
raise ValueError(
|
57 |
"Unsupported filetype, please specify a separator or choose one "
|
58 |
"of the following filetypes: .xlsx, .xls, .csv"
|
59 |
)
|
60 |
|
61 |
-
return Attendancelist(
|
62 |
|
63 |
@property
|
64 |
def n_attendees(self):
|
@@ -82,15 +86,21 @@ class Attendancelist:
|
|
82 |
raise ValueError(
|
83 |
"Unsupported filetype, please choose one of the following: .xlsx, .csv"
|
84 |
)
|
85 |
-
|
86 |
def to_file(self) -> str:
|
87 |
-
return self.to_df().to_csv(index=False).encode(
|
88 |
|
89 |
def update(self, other: "Attendancelist"):
|
90 |
return Attendancelist(other.participants - self.participants)
|
91 |
|
92 |
-
def
|
93 |
return {p for p in self.participants if p.is_similar(somebody)}
|
94 |
|
95 |
-
def
|
96 |
-
return {p: self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
|
8 |
DATAFOLDER = Path().cwd() / "data"
|
9 |
|
10 |
+
NAMECOLUMN = "Name"
|
|
|
|
|
11 |
|
12 |
|
13 |
@dataclass
|
|
|
15 |
name: str
|
16 |
|
17 |
def __post_init__(self):
|
18 |
+
self.name = " ".join(
|
19 |
+
[namepart for namepart in self.name.strip().title().split(" ") if namepart]
|
20 |
+
)
|
21 |
|
22 |
def __hash__(self):
|
23 |
return hash(self.name)
|
|
|
38 |
def lastname(self):
|
39 |
return " ".join(self.name.split(" ")[1:])
|
40 |
|
41 |
+
def name_contains(self, text) -> bool:
|
42 |
+
return text in self.name.lower()
|
43 |
+
|
44 |
|
45 |
@dataclass
|
46 |
class Attendancelist:
|
47 |
participants: Set[Person]
|
48 |
|
49 |
+
def load_from_df(df, cname: str = NAMECOLUMN):
|
50 |
+
return Attendancelist({Person(name) for name in df[cname]})
|
51 |
+
|
52 |
def load_from_file(
|
53 |
+
filename: pathlib.PosixPath, cname: str = NAMECOLUMN, sep: str = None
|
54 |
):
|
55 |
+
if filename.suffix in [".xlsx", ".xls"]:
|
|
|
|
|
56 |
df = pd.read_excel(filename)
|
57 |
elif filename.suffix == ".csv":
|
58 |
+
df = pd.read_csv(filename, sep=sep)
|
59 |
else:
|
60 |
raise ValueError(
|
61 |
"Unsupported filetype, please specify a separator or choose one "
|
62 |
"of the following filetypes: .xlsx, .xls, .csv"
|
63 |
)
|
64 |
|
65 |
+
return Attendancelist.load_from_df(df, cname)
|
66 |
|
67 |
@property
|
68 |
def n_attendees(self):
|
|
|
86 |
raise ValueError(
|
87 |
"Unsupported filetype, please choose one of the following: .xlsx, .csv"
|
88 |
)
|
89 |
+
|
90 |
def to_file(self) -> str:
|
91 |
+
return self.to_df().to_csv(index=False).encode("utf-8")
|
92 |
|
93 |
def update(self, other: "Attendancelist"):
|
94 |
return Attendancelist(other.participants - self.participants)
|
95 |
|
96 |
+
def find_person(self, somebody: Person):
|
97 |
return {p for p in self.participants if p.is_similar(somebody)}
|
98 |
|
99 |
+
def find_people(self, people: List[Person]):
|
100 |
+
return {p: self.find_person(p) for p in people}
|
101 |
+
|
102 |
+
def find_word(self, word: str):
|
103 |
+
return {p for p in self.participants if p.name_contains(word.lower())}
|
104 |
+
|
105 |
+
def find_words(self, words: List[str]):
|
106 |
+
return {word: self.find_word(word) for word in words}
|
src/maorganizer/ui.py
ADDED
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import io
|
2 |
+
from enum import Enum
|
3 |
+
from pathlib import Path
|
4 |
+
from typing import Dict, Tuple
|
5 |
+
|
6 |
+
import pandas as pd
|
7 |
+
import streamlit as st
|
8 |
+
|
9 |
+
from maorganizer.datawrangling import NAMECOLUMN, Attendancelist
|
10 |
+
|
11 |
+
CSV_EXTENSIONS = [".csv", ".txt"]
|
12 |
+
EXCEL_EXTENSIONS = [".xls", ".xlsx"]
|
13 |
+
ACCEPTED_EXTENSIONS = CSV_EXTENSIONS + EXCEL_EXTENSIONS
|
14 |
+
|
15 |
+
|
16 |
+
SEPARATORTYPES = {"TAB": "\t", "COMMA": ","}
|
17 |
+
|
18 |
+
|
19 |
+
class TASKS(str, Enum):
|
20 |
+
SPLIT = "βοΈ ... split attendees into first and last name and download results"
|
21 |
+
COMPARE = " π ... compare two meetings with each other and find updates"
|
22 |
+
FIND = " π ... find specific attendees"
|
23 |
+
|
24 |
+
def __str__(self) -> str: # makes enum values duck-type to strings
|
25 |
+
return str.__str__(self)
|
26 |
+
|
27 |
+
|
28 |
+
def load_df_from_uploaded_data(filename, data, sep=None) -> pd.DataFrame:
|
29 |
+
if Path(filename).suffix in EXCEL_EXTENSIONS:
|
30 |
+
df = pd.read_excel(data)
|
31 |
+
elif Path(filename).suffix in CSV_EXTENSIONS:
|
32 |
+
df = pd.read_csv(filename, sep=sep)
|
33 |
+
else:
|
34 |
+
raise ValueError(
|
35 |
+
f"Please choose one of the following extensions: {', '.join(ACCEPTED_EXTENSIONS)}"
|
36 |
+
)
|
37 |
+
return df
|
38 |
+
|
39 |
+
|
40 |
+
def make_attendance_data_from_file_uploads(
|
41 |
+
uploaded_files, sep=None, cname=NAMECOLUMN
|
42 |
+
) -> Dict:
|
43 |
+
return {
|
44 |
+
file.name: Attendancelist.load_from_df(
|
45 |
+
load_df_from_uploaded_data(file.name, file, sep), cname=cname
|
46 |
+
)
|
47 |
+
for file in uploaded_files
|
48 |
+
}
|
49 |
+
|
50 |
+
|
51 |
+
def load_data(uploaded_files) -> Tuple[Dict, bool]:
|
52 |
+
try:
|
53 |
+
data = make_attendance_data_from_file_uploads(
|
54 |
+
uploaded_files, sep=None, cname=NAMECOLUMN
|
55 |
+
)
|
56 |
+
except KeyError:
|
57 |
+
contains_csvs = sum(
|
58 |
+
[Path(file.name).suffix in CSV_EXTENSIONS for file in uploaded_files]
|
59 |
+
)
|
60 |
+
if contains_csvs:
|
61 |
+
separator = st.radio(
|
62 |
+
"We detected text files in your input. What is their separator?",
|
63 |
+
sorted(SEPARATORTYPES.keys()),
|
64 |
+
)
|
65 |
+
|
66 |
+
namecolumn = st.text_input(
|
67 |
+
"Column header of your file's name column", NAMECOLUMN
|
68 |
+
)
|
69 |
+
try:
|
70 |
+
data = make_attendance_data_from_file_uploads(
|
71 |
+
uploaded_files, sep=SEPARATORTYPES[separator], cname=namecolumn
|
72 |
+
)
|
73 |
+
except KeyError:
|
74 |
+
st.error(
|
75 |
+
f"We could not find a column {namecolumn} in your data. Please use the options above to specify your column separator and the column name of your name column."
|
76 |
+
)
|
77 |
+
data = {}
|
78 |
+
|
79 |
+
if data:
|
80 |
+
st.success(
|
81 |
+
"Successfully loaded the following files:\\\n\\\n"
|
82 |
+
+ "\\\n".join([f"{k} - {v.n_attendees} attendees" for k, v in data.items()])
|
83 |
+
)
|
84 |
+
return data
|
85 |
+
|
86 |
+
|
87 |
+
def render_file_selector(meetings, key):
|
88 |
+
show_processed_list = st.checkbox("Display the processed list of attendees")
|
89 |
+
if show_processed_list:
|
90 |
+
filename = st.selectbox(
|
91 |
+
"Select the file to display", options=list(meetings.keys()), key=key
|
92 |
+
)
|
93 |
+
attendees = meetings[filename]
|
94 |
+
|
95 |
+
st.write(attendees.to_df())
|
96 |
+
|
97 |
+
|
98 |
+
def create_file_uploader():
|
99 |
+
uploaded_files = st.file_uploader(
|
100 |
+
label="π Upload your files", accept_multiple_files=True
|
101 |
+
)
|
102 |
+
if uploaded_files:
|
103 |
+
meetings = load_data(uploaded_files)
|
104 |
+
render_file_selector(meetings, "file_upload")
|
105 |
+
return meetings
|
106 |
+
else:
|
107 |
+
return {}
|
108 |
+
|
109 |
+
|
110 |
+
def create_task_selector():
|
111 |
+
st.header("π Step 2: Choose a Task")
|
112 |
+
task = st.radio("I would like to ...", [task.value for task in TASKS])
|
113 |
+
|
114 |
+
if task == TASKS.SPLIT.value:
|
115 |
+
st.markdown("β **Description:** Split a list of names into first and surname.")
|
116 |
+
elif task == TASKS.COMPARE.value:
|
117 |
+
st.markdown(
|
118 |
+
"β **Description:** Compare two attendee lists with each and find attendees who have recently joined."
|
119 |
+
)
|
120 |
+
elif task == TASKS.FIND.value:
|
121 |
+
st.markdown(
|
122 |
+
"β **Description:** Find attendees in a list by either first name or surname or by substrings."
|
123 |
+
)
|
124 |
+
return task
|
125 |
+
|
126 |
+
|
127 |
+
def render_xlsx_download_button(data, filename, key) -> None:
|
128 |
+
with io.BytesIO() as output:
|
129 |
+
with pd.ExcelWriter(output, engine="openpyxl") as writer:
|
130 |
+
for sheetname, attendees in data.items():
|
131 |
+
attendees.to_df().to_excel(writer, sheet_name=sheetname, index=False)
|
132 |
+
writer.save()
|
133 |
+
|
134 |
+
st.download_button(
|
135 |
+
label="πΎ Download Results",
|
136 |
+
data=output.getvalue(),
|
137 |
+
file_name=filename,
|
138 |
+
mime="application/vnd.ms-excel",
|
139 |
+
key=key,
|
140 |
+
)
|
tests/test_attendancelist.py
CHANGED
@@ -2,21 +2,30 @@ from maorganizer.datawrangling import Attendancelist, Person
|
|
2 |
|
3 |
|
4 |
def test_attendancelist_finds_person_by_substring():
|
5 |
-
assert Attendancelist(
|
6 |
-
Person("zaphod")
|
7 |
-
) == {Person("Zaphod Beeblebrox")}
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
|
9 |
|
10 |
def test_attendancelists_finds_multiple_people_if_existent():
|
11 |
assert Attendancelist(
|
12 |
{Person("zaphod beeblebrox"), Person("zaphod prefix"), Person("ford prefix")}
|
13 |
-
).
|
|
|
|
|
|
|
14 |
|
15 |
|
16 |
-
def
|
17 |
assert Attendancelist(
|
18 |
{Person("zaphod beeblebrox"), Person("ford prefix"), Person("Marvin")}
|
19 |
-
).
|
20 |
Person(name="Ford Prefix"): {Person(name="Ford Prefix")},
|
21 |
Person(name="Zaphod"): {Person(name="Zaphod Beeblebrox")},
|
22 |
}
|
|
|
2 |
|
3 |
|
4 |
def test_attendancelist_finds_person_by_substring():
|
5 |
+
assert Attendancelist(
|
6 |
+
{Person("zaphod beeblebrox"), Person("ford prefix")}
|
7 |
+
).find_word("aph") == {Person("Zaphod Beeblebrox")}
|
8 |
+
|
9 |
+
|
10 |
+
def test_attendancelist_finds_person_by_namepart():
|
11 |
+
assert Attendancelist(
|
12 |
+
{Person("zaphod beeblebrox"), Person("ford prefix")}
|
13 |
+
).find_person(Person("zaphod")) == {Person("Zaphod Beeblebrox")}
|
14 |
|
15 |
|
16 |
def test_attendancelists_finds_multiple_people_if_existent():
|
17 |
assert Attendancelist(
|
18 |
{Person("zaphod beeblebrox"), Person("zaphod prefix"), Person("ford prefix")}
|
19 |
+
).find_person(Person("zaphod")) == {
|
20 |
+
Person("Zaphod Beeblebrox"),
|
21 |
+
Person("Zaphod Prefix"),
|
22 |
+
}
|
23 |
|
24 |
|
25 |
+
def test_find_people_finds_alls():
|
26 |
assert Attendancelist(
|
27 |
{Person("zaphod beeblebrox"), Person("ford prefix"), Person("Marvin")}
|
28 |
+
).find_people({Person("zaphod"), Person("ford prefix")}) == {
|
29 |
Person(name="Ford Prefix"): {Person(name="Ford Prefix")},
|
30 |
Person(name="Zaphod"): {Person(name="Zaphod Beeblebrox")},
|
31 |
}
|
tests/test_datawrangling.py
CHANGED
@@ -40,4 +40,4 @@ def test_whitespace_gets_deleted_from_edges_of_name():
|
|
40 |
|
41 |
|
42 |
def test_multiple_whitespace_gets_correctly_deleted_from_inside_a_name():
|
43 |
-
assert Person("Zaphod Beeblebrox") == Person("Zaphod Beeblebrox")
|
|
|
40 |
|
41 |
|
42 |
def test_multiple_whitespace_gets_correctly_deleted_from_inside_a_name():
|
43 |
+
assert Person("Zaphod Beeblebrox") == Person("Zaphod Beeblebrox")
|