Jessica Walkenhorst commited on
Commit
4f16d99
β€’
1 Parent(s): 05a3f3b

Refactor code (#15)

Browse files
Dockerfile CHANGED
@@ -14,11 +14,11 @@ ENV POETRY_CACHE_DIR=/opt/.cache
14
 
15
  WORKDIR /home/
16
 
17
- COPY pyproject.toml poetry.lock app.py README.md ./
18
  COPY src ./src/
19
 
20
  RUN poetry install --without dev
21
 
22
  EXPOSE 7860
23
 
24
- ENTRYPOINT ["poetry", "run", "streamlit", "run", "app.py", "--server.port", "7860"]
 
14
 
15
  WORKDIR /home/
16
 
17
+ COPY pyproject.toml poetry.lock README.md ./
18
  COPY src ./src/
19
 
20
  RUN poetry install --without dev
21
 
22
  EXPOSE 7860
23
 
24
+ ENTRYPOINT ["poetry", "run", "streamlit", "run", "src/maorganizer/ui.py", "--server.port", "7860"]
app.py DELETED
@@ -1,79 +0,0 @@
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
- # all these beautiful emojis from https://emojidb.org/file-emojis
9
-
10
- st.title("πŸ“… Meeting Attendance Organizer")
11
- 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:")
12
- 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""")
13
-
14
- st.header("πŸ“‚ Step 1: Upload your Files")
15
- 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.")
16
-
17
-
18
- meetings = {}
19
- meetings = create_file_uploader()
20
-
21
- task = create_task_selector()
22
-
23
- st.header("πŸ”₯ Step 3: Let's Go!")
24
- st.subheader(f"You are going to ... {task}")
25
- if not meetings:
26
- st.info("⬆ You need to upload some data first ⬆")
27
-
28
- if meetings:
29
- if task == TASKS.SPLIT.value:
30
-
31
- filename = st.selectbox("Choose a file πŸ“„", options=list(meetings.keys()), key=task)
32
-
33
- render_xlsx_download_button({'Full list of Attendees': meetings[filename]},
34
- filename=f"processed-attendees-{Path(filename).stem}.xlsx",
35
- key=TASKS.SPLIT.value+'download')
36
-
37
- elif task == TASKS.FIND.value:
38
-
39
- filename = st.selectbox("πŸ“„ Choose a file", options=list(meetings.keys()), key=task)
40
- attendees = meetings[filename]
41
-
42
- textinput = st.text_input("πŸ”Ž Who are you looking for? If you are looking for more than one, separate them by comma.")
43
- 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.")
44
- st.markdown("⚠️⚠️ Note that either way the search is case-insensitive.")
45
- exact_match = st.checkbox("Find Exact Name")
46
-
47
- if textinput.strip():
48
- st.header("Search Results")
49
- if exact_match:
50
- people_to_find = [Person(word.strip()) for word in textinput.split(',')]
51
-
52
- for to_find, found in attendees.find_people(people_to_find).items():
53
- st.subheader(f"**{to_find.name}**")
54
- st.markdown(f"{', '.join([p.name for p in found]) if found else 'Sorry, none found.'}")
55
- else:
56
- words_to_find = [word.strip() for word in textinput.split(',')]
57
-
58
- for word_to_find, people_found in attendees.find_words(words_to_find).items():
59
- st.subheader(f"**{word_to_find}**")
60
- st.markdown(f"{', '.join([p.name for p in people_found]) if people_found else 'Sorry, none found.'}")
61
-
62
- elif task == TASKS.COMPARE.value:
63
- col1, col2 = st.columns(2)
64
- with col1:
65
- filename_old = st.selectbox("Choose your original file", options=list(meetings.keys()), key=task)
66
- with col2:
67
- filename_new = st.selectbox("Choose your updated file", options=set(meetings.keys()) - {filename_old})
68
-
69
- # filename_new gets automatically populated if there is more than one file
70
- # so if there is none, it's because there is only a single file available and no options left for filename_new
71
- if filename_new is None:
72
- st.info("⬆ Please upload a second file. ⬆")
73
- else:
74
- listcomparison = (
75
- {'Original List': meetings[filename_old],
76
- 'Updated List - Full': meetings[filename_new],
77
- 'Updated List - Only Updates': meetings[filename_old].update(meetings[filename_new])})
78
-
79
- render_xlsx_download_button(listcomparison, filename=f"{Path(filename_old).stem}-updated.xlsx", key=TASKS.COMPARE.value+'download')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/maorganizer/datawrangling.py CHANGED
@@ -5,6 +5,9 @@ from typing import List, Set
5
 
6
  import pandas as pd
7
 
 
 
 
8
  DATAFOLDER = Path().cwd() / "data"
9
 
10
  NAMECOLUMN = "Name"
@@ -46,15 +49,15 @@ class Person:
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(
@@ -62,7 +65,7 @@ class Attendancelist:
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):
@@ -99,8 +102,11 @@ class Attendancelist:
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}
 
5
 
6
  import pandas as pd
7
 
8
+ CSV_EXTENSIONS = [".csv", ".txt"]
9
+ EXCEL_EXTENSIONS = [".xls", ".xlsx"]
10
+
11
  DATAFOLDER = Path().cwd() / "data"
12
 
13
  NAMECOLUMN = "Name"
 
49
  class Attendancelist:
50
  participants: Set[Person]
51
 
52
+ def from_df(df, cname: str = NAMECOLUMN):
53
  return Attendancelist({Person(name) for name in df[cname]})
54
 
55
+ def from_file(
56
  filename: pathlib.PosixPath, cname: str = NAMECOLUMN, sep: str = None
57
  ):
58
+ if filename.suffix in EXCEL_EXTENSIONS:
59
  df = pd.read_excel(filename)
60
+ elif filename.suffix in CSV_EXTENSIONS:
61
  df = pd.read_csv(filename, sep=sep)
62
  else:
63
  raise ValueError(
 
65
  "of the following filetypes: .xlsx, .xls, .csv"
66
  )
67
 
68
+ return Attendancelist.from_df(df, cname)
69
 
70
  @property
71
  def n_attendees(self):
 
102
  def find_people(self, people: List[Person]):
103
  return {p: self.find_person(p) for p in people}
104
 
105
+ def find_by_string(self, word: str, exact=False):
106
+ if exact:
107
+ return self.find_person(Person(word))
108
+ else:
109
+ return {p for p in self.participants if p.name_contains(word.lower())}
110
 
111
+ def find_by_strings(self, to_be_found: List[str], exact=False):
112
+ return {tbf: self.find_by_string(tbf, exact=exact) for tbf in to_be_found}
src/maorganizer/ui.py CHANGED
@@ -1,77 +1,250 @@
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
- try:
31
- df = pd.read_excel(data)
32
- # If engine does not recognize excel as excel, it is likely to be
33
- # a text format "disguised" as xls.
34
- # (example: PyData Meeting files have .xls extension, but are in fact text files)
35
- except ValueError as e:
36
- if (
37
- str(e)
38
- == "Excel file format cannot be determined, you must specify an engine manually."
39
- ):
40
- st.info(
41
- f"Your {Path(filename).suffix} file does not seem to be an excel file.\\\n\\\n"
42
- "\- Trying to parse it as text file."
43
- )
44
- df = pd.read_csv(data, sep=sep)
45
- else:
46
- raise ValueError(e)
47
- elif Path(filename).suffix in CSV_EXTENSIONS:
48
- df = pd.read_csv(data, sep=sep)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  else:
50
- raise ValueError(
51
- f"Please choose one of the following extensions: {', '.join(ACCEPTED_EXTENSIONS)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  )
53
- return df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
 
56
- def make_attendance_data_from_file_uploads(
57
- uploaded_files, sep=None, cname=NAMECOLUMN
58
- ) -> Dict:
59
- return {
60
- file.name: Attendancelist.load_from_df(
61
- load_df_from_uploaded_data(file.name, file, sep), cname=cname
62
  )
63
- for file in uploaded_files
64
- }
 
65
 
66
 
67
- def load_data(uploaded_files) -> Tuple[Dict, bool]:
68
  def _files_contain_csv(uploaded_files) -> bool:
69
  return bool(
70
  sum([Path(file.name).suffix in CSV_EXTENSIONS for file in uploaded_files])
71
  )
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  try:
74
- data = make_attendance_data_from_file_uploads(
75
  uploaded_files, sep=None, cname=NAMECOLUMN
76
  )
77
 
@@ -96,7 +269,7 @@ def load_data(uploaded_files) -> Tuple[Dict, bool]:
96
  )
97
 
98
  try:
99
- data = make_attendance_data_from_file_uploads(
100
  uploaded_files, sep=separator, cname=columnname
101
  )
102
  except KeyError:
@@ -105,69 +278,69 @@ def load_data(uploaded_files) -> Tuple[Dict, bool]:
105
  " Please use the options above to specify your column separator (if text/csv file)"
106
  " and the column name of the column containing your attendees' names."
107
  )
108
- data = {}
109
 
110
- if data:
111
  st.success(
112
  "Successfully loaded the following files:\\\n\\\n"
113
- + "\\\n".join([f"{k} - {v.n_attendees} attendees" for k, v in data.items()])
114
- )
115
- return data
116
-
117
-
118
- def render_file_selector(meetings, key):
119
- show_processed_list = st.checkbox("Display the processed list of attendees")
120
- if show_processed_list:
121
- filename = st.selectbox(
122
- "Select the file to display", options=list(meetings.keys()), key=key
123
  )
124
- attendees = meetings[filename]
125
 
126
- st.write(attendees.to_df())
127
 
 
 
 
 
 
 
 
128
 
129
- def create_file_uploader():
130
  uploaded_files = st.file_uploader(
131
  label="πŸ“„ Upload your files", accept_multiple_files=True
132
  )
 
 
133
  if uploaded_files:
134
  meetings = load_data(uploaded_files)
135
- render_file_selector(meetings, "file_upload")
136
- return meetings
137
- else:
138
- return {}
139
 
 
140
 
141
- def create_task_selector():
142
- st.header("πŸ“ Step 2: Choose a Task")
143
- task = st.radio("I would like to ...", [task.value for task in TASKS])
144
 
145
- if task == TASKS.SPLIT.value:
146
- st.markdown("❔ **Description:** Split a list of names into first and surname.")
147
- elif task == TASKS.COMPARE.value:
148
- st.markdown(
149
- "❔ **Description:** Compare two attendee lists with each"
150
- " and find attendees who have recently joined."
151
- )
152
- elif task == TASKS.FIND.value:
153
- st.markdown(
154
- "❔ **Description:** Find attendees in a list by either first name"
155
- " or surname or by substrings."
156
- )
157
- return task
158
 
159
 
160
- def render_xlsx_download_button(data, filename, key) -> None:
161
- with io.BytesIO() as output:
162
- with pd.ExcelWriter(output, engine="openpyxl") as writer:
163
- for sheetname, attendees in data.items():
164
- attendees.to_df().to_excel(writer, sheet_name=sheetname, index=False)
165
- writer.save()
166
 
167
- st.download_button(
168
- label="πŸ’Ύ Download Results",
169
- data=output.getvalue(),
170
- file_name=filename,
171
- mime="application/vnd.ms-excel",
172
- key=key,
173
- )
 
 
 
 
 
 
 
 
 
 
1
  import io
2
+ import types
3
+ from dataclasses import dataclass
4
  from pathlib import Path
5
+ from typing import Dict
6
 
7
  import pandas as pd
8
  import streamlit as st
9
 
10
+ from maorganizer.datawrangling import (
11
+ CSV_EXTENSIONS,
12
+ EXCEL_EXTENSIONS,
13
+ NAMECOLUMN,
14
+ Attendancelist,
15
+ )
16
 
 
 
17
  ACCEPTED_EXTENSIONS = CSV_EXTENSIONS + EXCEL_EXTENSIONS
18
 
 
19
  SEPARATORTYPES = {"TAB": "\t", "COMMA": ","}
20
 
21
 
22
+ @dataclass
23
+ class Task:
24
+ abbreviation: str
25
+ short_description: str
26
+ long_description: str
27
+ run: types.FunctionType
28
 
 
 
29
 
30
+ @dataclass
31
+ class Tasks:
32
+ tasks: list[Task]
33
 
34
+ def __post_init__(self):
35
+ assert len(self.abbreviations) == len(set(self.abbreviations))
36
+ assert len(self.short_descriptions) == len(set(self.abbreviations))
37
+
38
+ @property
39
+ def abbreviations(self):
40
+ return [task.abbreviation for task in self.tasks]
41
+
42
+ @property
43
+ def short_descriptions(self):
44
+ return [task.short_description for task in self.tasks]
45
+
46
+
47
+ def split_names(meetings: Dict[str, Attendancelist]) -> None:
48
+ filename = st.selectbox(
49
+ "Choose a file πŸ“„", options=list(meetings.keys()), key="split_names"
50
+ )
51
+
52
+ render_xlsx_download_button(
53
+ {"Full list of Attendees": meetings[filename]},
54
+ filename=f"processed-attendees-{Path(filename).stem}.xlsx",
55
+ )
56
+
57
+
58
+ def find_attendees(meetings: Dict[str, Attendancelist]) -> None:
59
+ def _render_findings(to_be_found, people_found):
60
+ st.subheader(f"**{to_be_found}**")
61
+ st.markdown(
62
+ f"{', '.join([p.name for p in people_found]) if people_found else 'Sorry, none found.'}"
63
+ )
64
+
65
+ filename = st.selectbox(
66
+ "πŸ“„ Choose a file", options=list(meetings.keys()), key="find_attendees"
67
+ )
68
+ attendees = meetings[filename]
69
+
70
+ textinput = st.text_input(
71
+ "πŸ”Ž Who are you looking for? If you are looking for more than one, separate them by comma."
72
+ )
73
+ if textinput:
74
+ searchterms = [(word.strip()) for word in textinput.strip().split(",")]
75
  else:
76
+ searchterms = []
77
+
78
+ st.markdown(
79
+ "⚠️ By default, the algorithm will surface any names that contain your search terms"
80
+ " as substrings (e.g. if you search for Jon, it will surface both Jon and Jonathan)."
81
+ " Tick the box below in case you want to display only entries where one of the names"
82
+ " matches your search string exactly."
83
+ )
84
+ st.markdown("⚠️⚠️ Note that either way the search is case-insensitive.")
85
+ exact_match = st.checkbox("Find Exact Name")
86
+
87
+ if searchterms:
88
+ st.header("Search Results")
89
+
90
+ for to_be_found, found in attendees.find_by_strings(
91
+ searchterms, exact=exact_match
92
+ ).items():
93
+ _render_findings(to_be_found, found)
94
+
95
+
96
+ def compare_meetings(meetings: Dict[str, Attendancelist]) -> None:
97
+ col1, col2 = st.columns(2)
98
+ with col1:
99
+ filename_old = st.selectbox(
100
+ "Choose your original file", options=list(meetings.keys())
101
+ )
102
+ with col2:
103
+ filename_new = st.selectbox(
104
+ "Choose your updated file", options=set(meetings.keys()) - {filename_old}
105
  )
106
+
107
+ # filename_new gets automatically populated if there is more than one file
108
+ # so if there is none, it's because there is only a single file available
109
+ # and no options left for filename_new
110
+ if filename_new is None:
111
+ st.info("⬆ Please upload a second file. ⬆")
112
+ else:
113
+ listcomparison = {
114
+ "Original List": meetings[filename_old],
115
+ "Updated List - Full": meetings[filename_new],
116
+ "Updated List - Only Updates": meetings[filename_old].update(
117
+ meetings[filename_new]
118
+ ),
119
+ }
120
+
121
+ render_xlsx_download_button(
122
+ listcomparison,
123
+ filename=f"{Path(filename_old).stem}-updated.xlsx",
124
+ )
125
+
126
+
127
+ def create_tasks() -> Tasks:
128
+ return Tasks(
129
+ [
130
+ Task(
131
+ abbreviation="SPLIT",
132
+ short_description=(
133
+ "βœ‚οΈ ... split attendees into first"
134
+ "and last name and download results"
135
+ ),
136
+ long_description=(
137
+ "❔ **Description:** Split a list of names"
138
+ " into first and surname."
139
+ ),
140
+ run=split_names,
141
+ ),
142
+ Task(
143
+ abbreviation="COMPARE",
144
+ short_description=" πŸ‘€ ... compare two meetings with each other and find updates",
145
+ long_description=(
146
+ "❔ **Description:** Compare two attendee lists"
147
+ " with each and find attendees who have recently joined."
148
+ ),
149
+ run=compare_meetings,
150
+ ),
151
+ Task(
152
+ abbreviation="FIND",
153
+ short_description=" πŸ”Ž ... find specific attendees",
154
+ long_description=(
155
+ "❔ **Description:** Find attendees in a list"
156
+ " by either first name or surname or by substrings."
157
+ ),
158
+ run=find_attendees,
159
+ ),
160
+ ]
161
+ )
162
+
163
+
164
+ def render_xlsx_download_button(
165
+ meetings: Dict[str, Attendancelist], filename: str
166
+ ) -> None:
167
+ with io.BytesIO() as output:
168
+ with pd.ExcelWriter(output, engine="openpyxl") as writer:
169
+ for sheetname, attendees in meetings.items():
170
+ attendees.to_df().to_excel(writer, sheet_name=sheetname, index=False)
171
+
172
+ st.download_button(
173
+ label="πŸ’Ύ Download Results",
174
+ data=output.getvalue(),
175
+ file_name=filename,
176
+ mime="application/vnd.ms-excel",
177
+ )
178
+
179
+
180
+ def select_task(tasks: Tasks) -> Task:
181
+ selected_task = st.radio("I would like to ...", tasks.short_descriptions)
182
+
183
+ for task in tasks.tasks:
184
+ if task.short_description == selected_task:
185
+ st.markdown(task.long_description)
186
+ break
187
+
188
+ return task
189
 
190
 
191
+ def render_file_analysis(meetings: Dict[str, Attendancelist]) -> None:
192
+ show_processed_list = st.checkbox("Display the processed list of attendees")
193
+ if show_processed_list:
194
+ filename = st.selectbox(
195
+ "Select the file to display", options=list(meetings.keys())
 
196
  )
197
+ attendees = meetings[filename]
198
+
199
+ st.write(attendees.to_df())
200
 
201
 
202
+ def load_data(uploaded_files) -> Dict[str, Attendancelist]:
203
  def _files_contain_csv(uploaded_files) -> bool:
204
  return bool(
205
  sum([Path(file.name).suffix in CSV_EXTENSIONS for file in uploaded_files])
206
  )
207
 
208
+ def _load_df_from_uploaded_data(filename: str, data, sep=None) -> pd.DataFrame:
209
+ if Path(filename).suffix in EXCEL_EXTENSIONS:
210
+ try:
211
+ df = pd.read_excel(data)
212
+ # If engine does not recognize excel as excel, it is likely to be
213
+ # a text format "disguised" as xls.
214
+ # (example: PyData Meeting files have .xls extension, but are in fact text files)
215
+ except ValueError as e:
216
+ if str(e) == (
217
+ "Excel file format cannot be determined,"
218
+ " you must specify an engine manually."
219
+ ):
220
+ st.info(
221
+ f"Your {Path(filename).suffix} file does not seem"
222
+ " to be an excel file.\\\n\\\n"
223
+ "\- Trying to parse it as text file."
224
+ )
225
+ df = pd.read_csv(data, sep=sep)
226
+ else:
227
+ raise ValueError(e)
228
+ elif Path(filename).suffix in CSV_EXTENSIONS:
229
+ df = pd.read_csv(data, sep=sep)
230
+ else:
231
+ raise ValueError(
232
+ f"Please choose one of the following extensions: {', '.join(ACCEPTED_EXTENSIONS)}"
233
+ )
234
+ return df
235
+
236
+ def _make_attendance_data_from_file_uploads(
237
+ uploaded_files, sep=None, cname=NAMECOLUMN
238
+ ) -> Dict[str, Attendancelist]:
239
+ return {
240
+ file.name: Attendancelist.from_df(
241
+ _load_df_from_uploaded_data(file.name, file, sep), cname=cname
242
+ )
243
+ for file in uploaded_files
244
+ }
245
+
246
  try:
247
+ meetings = _make_attendance_data_from_file_uploads(
248
  uploaded_files, sep=None, cname=NAMECOLUMN
249
  )
250
 
 
269
  )
270
 
271
  try:
272
+ meetings = _make_attendance_data_from_file_uploads(
273
  uploaded_files, sep=separator, cname=columnname
274
  )
275
  except KeyError:
 
278
  " Please use the options above to specify your column separator (if text/csv file)"
279
  " and the column name of the column containing your attendees' names."
280
  )
281
+ meetings = {}
282
 
283
+ if meetings:
284
  st.success(
285
  "Successfully loaded the following files:\\\n\\\n"
286
+ + "\\\n".join(
287
+ [f"{k} - {v.n_attendees} attendees" for k, v in meetings.items()]
288
+ )
 
 
 
 
 
 
 
289
  )
290
+ return meetings
291
 
 
292
 
293
+ def upload_files() -> Dict[str, Attendancelist]:
294
+ st.markdown(
295
+ "Upload the file(s) containing your meeting attendees. The expected format is a single"
296
+ " column containing the attendees' full names. If you column name is not Name,"
297
+ " you will be able to specify the column name after uploading the data."
298
+ " Additional columns will be ignored."
299
+ )
300
 
 
301
  uploaded_files = st.file_uploader(
302
  label="πŸ“„ Upload your files", accept_multiple_files=True
303
  )
304
+
305
+ meetings = {}
306
  if uploaded_files:
307
  meetings = load_data(uploaded_files)
308
+ render_file_analysis(meetings)
 
 
 
309
 
310
+ return meetings
311
 
 
 
 
312
 
313
+ def render_intro() -> None:
314
+ st.title("πŸ“… Meeting Attendance Organizer")
315
+ st.markdown(
316
+ "This app fullfills a simple need: Take a list of names of people"
317
+ " attending a meeting and peform one (or multiple) of the following tasks:"
318
+ )
319
+ st.markdown(
320
+ "* βœ‚οΈ Split their names into first name and surname\n"
321
+ "* πŸ‘€ Compare two lists with each other and see who is new on the second list\n"
322
+ "* πŸ”Ž Find people in a list by either searching for their complete names or parts"
323
+ " of their name\n"
324
+ "* πŸ’Ύ Write any of the results back out, so you can share it with others"
325
+ )
326
 
327
 
328
+ def main():
329
+ render_intro()
 
 
 
 
330
 
331
+ st.header("πŸ“‚ Step 1: Upload your Files")
332
+ meetings = upload_files()
333
+
334
+ st.header("πŸ“ Step 2: Choose a Task")
335
+ task = select_task(tasks=create_tasks())
336
+
337
+ st.header("πŸ”₯ Step 3: Let's Go!")
338
+ st.subheader(f"You are going to ... {task.short_description}")
339
+ if not meetings:
340
+ st.info("⬆ You need to upload some data first ⬆")
341
+ else:
342
+ task.run(meetings)
343
+
344
+
345
+ if __name__ == "__main__":
346
+ main()
tests/test_attendancelist.py CHANGED
@@ -4,7 +4,7 @@ from maorganizer.datawrangling import Attendancelist, Person
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():
 
4
  def test_attendancelist_finds_person_by_substring():
5
  assert Attendancelist(
6
  {Person("zaphod beeblebrox"), Person("ford prefix")}
7
+ ).find_by_string("aph") == {Person("Zaphod Beeblebrox")}
8
 
9
 
10
  def test_attendancelist_finds_person_by_namepart():