isoc-ams 0.0.2__py2.py3-none-any.whl → 0.1.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of isoc-ams might be problematic. Click here for more details.
- isoc_ams-0.1.0.dist-info/METADATA +291 -0
- isoc_ams-0.1.0.dist-info/RECORD +5 -0
- isoc_ams.py +372 -261
- isoc_ams-0.0.2.dist-info/METADATA +0 -261
- isoc_ams-0.0.2.dist-info/RECORD +0 -5
- {isoc_ams-0.0.2.dist-info → isoc_ams-0.1.0.dist-info}/WHEEL +0 -0
- {isoc_ams-0.0.2.dist-info → isoc_ams-0.1.0.dist-info}/licenses/LICENSE +0 -0
isoc_ams.py
CHANGED
|
@@ -4,88 +4,105 @@
|
|
|
4
4
|
|
|
5
5
|
"""Extract or modify Chapter Data of the ISOC AMS (Salesforce) Database.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
7
|
+
DESCRIPTION
|
|
8
|
+
|
|
9
|
+
This module consists of a Class ISOC_AMS wrapping _ISOC_AMS which subclasses
|
|
10
|
+
the webdriver.<browser> of Selenium. Up to now ownly firefox and chrome
|
|
11
|
+
drivers are implemented and tested.
|
|
12
|
+
|
|
13
|
+
CLASS
|
|
14
|
+
PROPERTIES
|
|
15
|
+
The ISOC_AMS class provides the following properties:
|
|
16
|
+
members_list:
|
|
17
|
+
a list of Chapter members (according to AMS) with data (and links)
|
|
18
|
+
pending_applicants_list:
|
|
19
|
+
a list of pending appplicants (according to AMS) for a Chapter
|
|
20
|
+
membership with data (and links)
|
|
21
|
+
these properties are initialized after login ... and this will take time
|
|
22
|
+
|
|
23
|
+
METHODS
|
|
24
|
+
The ISOC_AMS class provides the following methods:
|
|
25
|
+
build_members_list:
|
|
26
|
+
to build a list of Chapter members with data (and links)
|
|
27
|
+
build_pending_applicants_list:
|
|
28
|
+
to build a list of pending appplicants for a Chapter membership with
|
|
29
|
+
data (and links)
|
|
30
|
+
deny_applicants:
|
|
31
|
+
to deny Chapter membership for a list of applicants
|
|
32
|
+
approve_applicants:
|
|
33
|
+
to approve Chapter membership for a list of applicants
|
|
34
|
+
delete_members:
|
|
35
|
+
to revoke Chapter membership for members from the members list
|
|
36
|
+
difference_from_expected:
|
|
37
|
+
to reread AMS and check if all operations were successfull (not ever
|
|
38
|
+
problem can be detected by the methods)
|
|
39
|
+
|
|
40
|
+
ISOC_AMS will log you in to ISOC.ORG and check your authorization at
|
|
41
|
+
instantiation.
|
|
42
|
+
|
|
43
|
+
To select a webdriver, an ISOC_AMS_WEBDRIVER environment variable can be used.
|
|
44
|
+
E.g.
|
|
45
|
+
ISOC_AMS_WEBDRIVER=Firefox
|
|
46
|
+
|
|
47
|
+
Default is Firefox. Only Firefox and Chrome are allowed for now.
|
|
48
|
+
|
|
49
|
+
FUNCTIONS
|
|
50
|
+
3 functions are provided to support logging:
|
|
51
|
+
log, dlog, strong_message
|
|
52
|
+
(see below)
|
|
53
|
+
|
|
54
|
+
EXAMPLE
|
|
55
|
+
|
|
56
|
+
from isoc_ams import ISOC_AMS
|
|
57
|
+
userid, password = "myuserid", "mysecret"
|
|
58
|
+
|
|
59
|
+
# this will log you in
|
|
60
|
+
# and instantiate an ISOC_AMS object
|
|
61
|
+
ams = ISOC_AMS(userid, password)
|
|
62
|
+
|
|
63
|
+
# this will read the list of members,
|
|
64
|
+
# registered as chapters members
|
|
65
|
+
members = ams.members_list
|
|
66
|
+
|
|
67
|
+
# print the results
|
|
68
|
+
for isoc_id, member in members.items():
|
|
69
|
+
print(isoc_id,
|
|
70
|
+
member["first name"],
|
|
71
|
+
member["last name"],
|
|
72
|
+
member["email"],
|
|
73
|
+
)
|
|
74
|
+
# select members to be deleted
|
|
75
|
+
deletees = <...> # various formats are allowed for operation methods
|
|
76
|
+
delete_members(deletees)
|
|
77
|
+
|
|
78
|
+
# check if all went well
|
|
79
|
+
difference_from_expected()
|
|
80
|
+
|
|
81
|
+
CHANGELOG
|
|
82
|
+
Version 0.0.2
|
|
83
|
+
Allow input if executed as module
|
|
84
|
+
Add dryrun to ISOC_AMS class
|
|
85
|
+
Version 0.1.0
|
|
86
|
+
Improved logging
|
|
87
|
+
minor bug fixes
|
|
75
88
|
"""
|
|
76
|
-
__version__ = "0.0
|
|
89
|
+
__version__ = "0.1.0"
|
|
77
90
|
|
|
78
91
|
from selenium import webdriver
|
|
79
92
|
from selenium.webdriver.common.by import By
|
|
80
93
|
from selenium.webdriver.support.wait import WebDriverWait, TimeoutException
|
|
81
94
|
from selenium.webdriver.support import expected_conditions as EC
|
|
82
95
|
from datetime import datetime
|
|
96
|
+
import logging
|
|
83
97
|
|
|
84
98
|
import io
|
|
85
99
|
import time
|
|
86
100
|
import sys
|
|
87
101
|
import os
|
|
88
102
|
|
|
103
|
+
_logger = logging.getLogger("AMS")
|
|
104
|
+
_logger.setLevel(logging.DEBUG)
|
|
105
|
+
|
|
89
106
|
_dr = os.environ.get("ISOC_AMS_WEBDRIVER", "firefox").lower()
|
|
90
107
|
|
|
91
108
|
if _dr == "firefox":
|
|
@@ -104,22 +121,98 @@ def _WaitForTextInElement(element):
|
|
|
104
121
|
return element.text
|
|
105
122
|
return _predicate
|
|
106
123
|
|
|
124
|
+
#
|
|
125
|
+
# logging
|
|
126
|
+
#
|
|
127
|
+
|
|
128
|
+
def _init_logging(logfile, debuglog):
|
|
129
|
+
|
|
130
|
+
_logger.normalLogFormat = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s',
|
|
131
|
+
'%Y-%m-%d %H:%M:%S')
|
|
132
|
+
_logger.blankLogFormat = logging.Formatter('%(message)s')
|
|
133
|
+
|
|
134
|
+
if type(logfile) is str:
|
|
135
|
+
lfh = logging.FileHandler(logfile)
|
|
136
|
+
elif isinstance(logfile, io.TextIOBase):
|
|
137
|
+
lfh = logging.StreamHandler(logfile)
|
|
138
|
+
elif logfile is None:
|
|
139
|
+
lfh = logging.NullHandler()
|
|
140
|
+
lfh.setLevel(logging.INFO)
|
|
141
|
+
lfh.setFormatter(_logger.normalLogFormat)
|
|
142
|
+
_logger.addHandler(lfh)
|
|
143
|
+
|
|
144
|
+
if type(debuglog) is str:
|
|
145
|
+
dlh = logging.FileHandler(debuglog)
|
|
146
|
+
elif isinstance(debuglog, io.TextIOBase):
|
|
147
|
+
dlh = logging.StreamHandler(debuglog)
|
|
148
|
+
elif debuglog is None:
|
|
149
|
+
dlh = logging.NullHandler()
|
|
150
|
+
dlh.setLevel(logging.DEBUG)
|
|
151
|
+
dlh.setFormatter(_logger.normalLogFormat)
|
|
152
|
+
_logger.addHandler(dlh)
|
|
153
|
+
|
|
154
|
+
#
|
|
155
|
+
# utilities
|
|
156
|
+
#
|
|
157
|
+
|
|
158
|
+
def log(*args, date: bool = True, level: int = logging.INFO):
|
|
159
|
+
"""Write to log.
|
|
160
|
+
|
|
161
|
+
ARGUMENTS
|
|
162
|
+
args: tuple of message parts
|
|
163
|
+
level: logging level
|
|
164
|
+
date: if False ommit time and level info in logrecord
|
|
165
|
+
"""
|
|
166
|
+
if len(args) > 0:
|
|
167
|
+
msg = (len(args) * "%s ") % args
|
|
168
|
+
else:
|
|
169
|
+
msg = ""
|
|
170
|
+
if date:
|
|
171
|
+
_logger.log(level, msg)
|
|
172
|
+
else:
|
|
173
|
+
for h in _logger.handlers:
|
|
174
|
+
h.setFormatter(_logger.blankLogFormat)
|
|
175
|
+
_logger.log(level, msg)
|
|
176
|
+
for h in _logger.handlers:
|
|
177
|
+
h.setFormatter(_logger.normalLogFormat)
|
|
178
|
+
|
|
179
|
+
def dlog(*args, date: bool = True):
|
|
180
|
+
""" Short for log(*args, date=True, level=logging.DEBUG)."""
|
|
181
|
+
log(*args, date=True, level=logging.DEBUG)
|
|
182
|
+
|
|
183
|
+
def strong_msg(*args, date: bool = True, level: int = logging.INFO):
|
|
184
|
+
"""Write to log emphasized message.
|
|
185
|
+
|
|
186
|
+
ARGUMENTS
|
|
187
|
+
args: tuple of message parts
|
|
188
|
+
level: logging level
|
|
189
|
+
date: if False ommit time and level info in logrecord
|
|
190
|
+
"""
|
|
191
|
+
x = 0
|
|
192
|
+
for t in args:
|
|
193
|
+
x += len(str(t)) + 1
|
|
194
|
+
x = x + 1 + 30
|
|
195
|
+
log("\n" + x * "*", date=False, level=level)
|
|
196
|
+
log(*args, date=date, level=level)
|
|
197
|
+
log(x * "*", date=False, level=level)
|
|
198
|
+
|
|
199
|
+
|
|
107
200
|
|
|
108
201
|
class ISOC_AMS:
|
|
109
202
|
"""Perform admin operations on a Chaper's members list stored in AMS.
|
|
110
203
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
204
|
+
DESCRIPTION
|
|
205
|
+
|
|
206
|
+
This is the main class to interface with the ISOC-AMS system.
|
|
114
207
|
|
|
115
208
|
By default all operations run headless. If you want to follow it on
|
|
116
209
|
a browser window use headless=False.
|
|
117
210
|
|
|
118
|
-
|
|
119
|
-
____
|
|
211
|
+
ARGUMENTS
|
|
120
212
|
user: username (email) for ISO.ORG login
|
|
121
213
|
password: password for ISO.ORG login
|
|
122
|
-
logfile: where to write ISOC_AMS log output
|
|
214
|
+
logfile: where to write ISOC_AMS info-log output
|
|
215
|
+
debuglog: where to write ISOC_AMS debug-level log output
|
|
123
216
|
headless: run without GUI
|
|
124
217
|
dryrun: only check input, no actions
|
|
125
218
|
"""
|
|
@@ -127,33 +220,35 @@ class ISOC_AMS:
|
|
|
127
220
|
def __init__(self,
|
|
128
221
|
user: str,
|
|
129
222
|
password: str,
|
|
130
|
-
logfile: io.
|
|
223
|
+
logfile: io.TextIOBase | str | None = sys.stdout,
|
|
224
|
+
debuglog: io.TextIOBase | str | None = None,
|
|
131
225
|
headless: bool = True,
|
|
132
226
|
dryrun: bool = False,):
|
|
133
227
|
if _dr == "firefox" and headless:
|
|
134
228
|
_options.add_argument("--headless")
|
|
135
229
|
elif _dr == "chrome" and headless:
|
|
136
230
|
_options.add_argument("--headless=new")
|
|
137
|
-
self._members_list: dict | None = None
|
|
138
|
-
self._pending_applications_list: dict | None = None
|
|
139
231
|
self._dryrun = dryrun
|
|
140
|
-
|
|
232
|
+
_init_logging(logfile, debuglog)
|
|
233
|
+
self._ams = _ISOC_AMS()
|
|
141
234
|
if self._dryrun:
|
|
142
|
-
|
|
235
|
+
strong_msg("START DRYRUN")
|
|
143
236
|
else:
|
|
144
|
-
|
|
237
|
+
strong_msg("START")
|
|
145
238
|
self._ams.login((user, password))
|
|
239
|
+
self._members_list = self._ams.build_members_list()
|
|
240
|
+
self._pending_applications_list = self._ams.build_pending_applicants_list()
|
|
146
241
|
|
|
147
242
|
|
|
148
243
|
@property
|
|
149
244
|
def members_list(self) -> dict:
|
|
150
245
|
"""Collects data about Chapter members.
|
|
151
246
|
|
|
247
|
+
DESCRIPTION
|
|
152
248
|
Collects the relevant data about ISOC members
|
|
153
249
|
registered as Chapter members in AMS
|
|
154
250
|
|
|
155
|
-
|
|
156
|
-
-------
|
|
251
|
+
RETURNS
|
|
157
252
|
dictionary with the following scheme:
|
|
158
253
|
{<ISOC-ID>:
|
|
159
254
|
{"first name": <first name>,
|
|
@@ -164,46 +259,42 @@ class ISOC_AMS:
|
|
|
164
259
|
...
|
|
165
260
|
}
|
|
166
261
|
|
|
167
|
-
|
|
262
|
+
ISOC-ID are used as keys for the entries
|
|
168
263
|
"""
|
|
169
|
-
if self._members_list is None:
|
|
170
|
-
self._members_list = self._ams.build_members_list()
|
|
171
264
|
return self._members_list
|
|
172
265
|
|
|
173
266
|
@property
|
|
174
267
|
def pending_applications_list(self) -> dict:
|
|
175
|
-
"""Collects data about pending Chapter
|
|
268
|
+
"""Collects data about pending Chapter applications.
|
|
176
269
|
|
|
270
|
+
DESCRIPTION
|
|
177
271
|
Collects the relevant data about pending Chapter applicants
|
|
178
272
|
registered as pending Chapter applicants in AMS
|
|
179
273
|
|
|
180
|
-
|
|
181
|
-
-------
|
|
274
|
+
RETURNS
|
|
182
275
|
dictionary with the following scheme:
|
|
183
276
|
{<ISOC-ID>:
|
|
184
277
|
{"name": <name>,
|
|
185
278
|
"email": <Email address>',
|
|
186
279
|
"action link": <url of page to edit this entry>
|
|
280
|
+
"date": <date of application>
|
|
187
281
|
},
|
|
188
282
|
...
|
|
189
283
|
}
|
|
190
284
|
---------------------------------------------
|
|
191
|
-
|
|
285
|
+
ISOC-ID are used as keys for the entries
|
|
192
286
|
"""
|
|
193
|
-
if self._pending_applications_list is None:
|
|
194
|
-
self._pending_applications_list = \
|
|
195
|
-
self._ams.build_pending_applicants_list()
|
|
196
287
|
return self._pending_applications_list
|
|
197
288
|
|
|
198
289
|
def delete_members(self, delete_list: list | dict | str | int):
|
|
199
290
|
"""Delete Member(s) from AMS-list of Chapter members.
|
|
200
291
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
delete_list: list of dict-entrys, or ISOC-IDs, or single entry
|
|
204
|
-
or ISOC-ID
|
|
292
|
+
DESCRIPTION
|
|
293
|
+
deletes delete_list entries from AMS-list of Chapter members
|
|
205
294
|
|
|
206
|
-
|
|
295
|
+
ARGUMENTS
|
|
296
|
+
delete_list: list of dict-entrys, or ISOC-IDs, or single entry
|
|
297
|
+
or an ISOC-ID
|
|
207
298
|
"""
|
|
208
299
|
if type(delete_list) in (str, int):
|
|
209
300
|
delete_list = [delete_list]
|
|
@@ -212,51 +303,58 @@ class ISOC_AMS:
|
|
|
212
303
|
deletee = str(deletee)
|
|
213
304
|
if not self._dryrun:
|
|
214
305
|
self._ams.delete(self._members_list[deletee])
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
306
|
+
log("Deleted", deletee,
|
|
307
|
+
self._members_list[deletee]["first name"],
|
|
308
|
+
self._members_list[deletee]["last name"])
|
|
218
309
|
del self._members_list[deletee]
|
|
219
310
|
else:
|
|
220
|
-
|
|
221
|
-
|
|
311
|
+
log("ISOC-ID", deletee,
|
|
312
|
+
"is not in AMS Chapter members list",
|
|
313
|
+
level=logging.ERROR)
|
|
222
314
|
|
|
223
315
|
|
|
224
316
|
def approve_pending_applications(self, approve_list: list | dict | str | int):
|
|
225
317
|
"""Approve pending Members as Chapter members.
|
|
226
318
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
approve_list: list of dict-entrys, or ISOC-IDs, or single entry
|
|
230
|
-
or ISOC-ID
|
|
319
|
+
DESCRIPTION
|
|
320
|
+
approves pending members on approve_list as Chapter members
|
|
231
321
|
|
|
232
|
-
|
|
322
|
+
ARGUMENTS
|
|
323
|
+
approve_list: list of dict-entrys, or ISOC-IDs, or single entry
|
|
324
|
+
or ISOC-ID
|
|
233
325
|
"""
|
|
234
326
|
if type(approve_list) in (int, str):
|
|
235
327
|
approve_list = [approve_list]
|
|
236
328
|
for approvee in map(str, approve_list):
|
|
237
329
|
if approvee in self._pending_applications_list:
|
|
238
|
-
if not self.
|
|
239
|
-
self.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
330
|
+
if approvee not in self._members_list:
|
|
331
|
+
if not self._dryrun:
|
|
332
|
+
self._ams.approve(self._pending_applications_list[approvee])
|
|
333
|
+
log("Approved", approvee,
|
|
334
|
+
self._pending_applications_list[approvee]["name"])
|
|
335
|
+
del self._pending_applications_list[approvee]
|
|
336
|
+
else:
|
|
337
|
+
log(self._pending_applications_list[approvee]["name"],
|
|
338
|
+
approvee,
|
|
339
|
+
"not approved - is already registered as member",
|
|
340
|
+
level=logging.ERROR)
|
|
243
341
|
else:
|
|
244
|
-
|
|
245
|
-
|
|
342
|
+
log("ISOC-ID", approvee,
|
|
343
|
+
"is not in pending applications list",
|
|
344
|
+
level=logging.ERROR)
|
|
246
345
|
|
|
247
346
|
def deny_pending_applications(self,
|
|
248
347
|
deny_list: list | dict | str | int,
|
|
249
348
|
reason: str = "Timeout, did not apply"):
|
|
250
349
|
"""Denies pending Members Chapter membership.
|
|
251
350
|
|
|
252
|
-
|
|
253
|
-
|
|
351
|
+
DESCRIPTION
|
|
352
|
+
denies Chapter membership for members on deny_list
|
|
353
|
+
|
|
354
|
+
ARGUMENTS
|
|
254
355
|
deny_list: list of dict-entrys, or ISOC-IDs, or single entry
|
|
255
356
|
or ISOC-ID
|
|
256
|
-
reason: All denied applicants
|
|
257
|
-
|
|
258
|
-
denies Chapter membership for members on deny_list
|
|
259
|
-
|
|
357
|
+
reason: All denied applicants have to be denied for a reason
|
|
260
358
|
"""
|
|
261
359
|
if type(deny_list) in (str, int):
|
|
262
360
|
deny_list = [deny_list],
|
|
@@ -265,19 +363,23 @@ class ISOC_AMS:
|
|
|
265
363
|
if not self._dryrun:
|
|
266
364
|
self._ams.deny(self._pending_applications_list[denyee],
|
|
267
365
|
reason)
|
|
268
|
-
|
|
366
|
+
log("Denied", denyee,
|
|
269
367
|
self._pending_applications_list[denyee]["name"])
|
|
270
368
|
del self._pending_applications_list[denyee]
|
|
271
369
|
else:
|
|
272
|
-
|
|
273
|
-
|
|
370
|
+
log("ISOC-ID", denyee,
|
|
371
|
+
"is not in pending applications list",
|
|
372
|
+
level=logging.ERROR)
|
|
274
373
|
|
|
275
|
-
def difference_from_expected(self) -> dict:
|
|
374
|
+
def difference_from_expected(self, test=None) -> dict | str:
|
|
276
375
|
"""Compare intended outcome of operations with real outcome.
|
|
277
376
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
377
|
+
DESCRIPTION
|
|
378
|
+
Compares the contents of the ISOC-AMS database with the expected result of
|
|
379
|
+
operations
|
|
380
|
+
|
|
381
|
+
RETURNS
|
|
382
|
+
A dict containing deviations of the inteded outcome:
|
|
281
383
|
{
|
|
282
384
|
"not deleted from members":
|
|
283
385
|
All entries in AMS-Chapter-Members that were supposed
|
|
@@ -289,35 +391,57 @@ class ISOC_AMS:
|
|
|
289
391
|
All entries in pending applications that should be
|
|
290
392
|
removed - either since approved or since denied
|
|
291
393
|
}
|
|
292
|
-
|
|
394
|
+
Or a string with the result of the comoarision.
|
|
293
395
|
"""
|
|
294
396
|
if not self._dryrun:
|
|
295
397
|
|
|
296
|
-
|
|
398
|
+
log(date=False)
|
|
297
399
|
|
|
298
|
-
|
|
299
|
-
|
|
400
|
+
strong_msg("Check if actions ended up in AMS database")
|
|
401
|
+
log("we have to read the AMS Database tables again to find deviations from expected result after actions :(")
|
|
402
|
+
log("", date=False)
|
|
300
403
|
|
|
301
404
|
not_deleted = {}
|
|
302
405
|
not_approved = {}
|
|
303
406
|
not_removed_from_pending = {}
|
|
304
407
|
new_members_list = self._ams.build_members_list()
|
|
408
|
+
dlog("Check members list")
|
|
305
409
|
for nm in new_members_list:
|
|
306
410
|
if nm not in self._members_list:
|
|
411
|
+
dlog(new_members_list[nm]["first name"],
|
|
412
|
+
new_members_list[nm]["last name"],
|
|
413
|
+
"("+nm+")",
|
|
414
|
+
"was not deleted")
|
|
307
415
|
not_deleted[nm] = new_members_list[nm]
|
|
308
416
|
for nm in self._members_list:
|
|
309
417
|
if nm not in new_members_list:
|
|
418
|
+
dlog(self._members_list[nm]["first name"],
|
|
419
|
+
self._members_list[nm]["last name"],
|
|
420
|
+
"("+nm+")",
|
|
421
|
+
"was not approved")
|
|
310
422
|
not_approved[nm] = self._members_list[nm]
|
|
311
423
|
new_pending_applications_list = self._ams.build_pending_applicants_list()
|
|
312
424
|
for np in new_pending_applications_list:
|
|
313
425
|
if np not in self._pending_applications_list:
|
|
426
|
+
dlog(self._members_list[nm]["name"],
|
|
427
|
+
"("+nm+")",
|
|
428
|
+
"was not removed from pending aoolications")
|
|
314
429
|
not_removed_from_pending[np] = new_pending_applications_list[np]
|
|
315
430
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
431
|
+
result = {}
|
|
432
|
+
if not_deleted:
|
|
433
|
+
result["not deleted from members"] = not_deleted
|
|
434
|
+
if not_approved:
|
|
435
|
+
result["not approved from pending applicants list"] = not_approved
|
|
436
|
+
if not_removed_from_pending:
|
|
437
|
+
result["not removed from pending applicants list"] = not_removed_from_pending
|
|
438
|
+
if not result:
|
|
439
|
+
result = "everything OK"
|
|
440
|
+
dlog(result)
|
|
441
|
+
return result
|
|
319
442
|
else:
|
|
320
|
-
|
|
443
|
+
dlog("DRYRUN: No results expected")
|
|
444
|
+
return "Dryrun: No results expected"
|
|
321
445
|
|
|
322
446
|
class _ISOC_AMS(Driver):
|
|
323
447
|
|
|
@@ -325,41 +449,13 @@ class _ISOC_AMS(Driver):
|
|
|
325
449
|
|
|
326
450
|
super().__init__(_options)
|
|
327
451
|
self.windows = {}
|
|
328
|
-
self.logfile = logfile
|
|
329
|
-
if type(self.logfile) is str:
|
|
330
|
-
self.logfile = open(self.log, "a")
|
|
331
452
|
|
|
332
453
|
def __del__(self):
|
|
333
454
|
self.quit()
|
|
334
455
|
|
|
335
|
-
#
|
|
336
|
-
# utilities
|
|
337
|
-
#
|
|
338
|
-
|
|
339
|
-
def log(self, *args, date=True, **kwargs):
|
|
340
|
-
if date:
|
|
341
|
-
print("AMS", datetime.now().isoformat(" ", timespec="seconds"),
|
|
342
|
-
*args,
|
|
343
|
-
file=self.logfile,
|
|
344
|
-
**kwargs)
|
|
345
|
-
else:
|
|
346
|
-
print(
|
|
347
|
-
*args,
|
|
348
|
-
file=self.logfile,
|
|
349
|
-
**kwargs)
|
|
350
|
-
|
|
351
|
-
def strong_msg(self, *args, **kwargs):
|
|
352
|
-
x = 0
|
|
353
|
-
for t in args:
|
|
354
|
-
x += len(str(t)) + 1
|
|
355
|
-
x = x + 1 + 30
|
|
356
|
-
self.log("\n" + x * "*", date=False)
|
|
357
|
-
self.log(*args, **kwargs)
|
|
358
|
-
self.log(x * "*", date=False)
|
|
359
|
-
|
|
360
456
|
def activate_window(self, name: str, url: str | None = None, refresh: bool = False):
|
|
361
457
|
if self.windows.get(name):
|
|
362
|
-
|
|
458
|
+
dlog("switching to window", name)
|
|
363
459
|
self.switch_to.window(self.windows[name])
|
|
364
460
|
if refresh:
|
|
365
461
|
self.navigate().refresh()
|
|
@@ -367,7 +463,7 @@ class _ISOC_AMS(Driver):
|
|
|
367
463
|
self.get(url)
|
|
368
464
|
return True
|
|
369
465
|
elif url:
|
|
370
|
-
|
|
466
|
+
dlog("switching to NEW window", name)
|
|
371
467
|
self.switch_to.new_window('tab')
|
|
372
468
|
self.windows[name] = self.current_window_handle
|
|
373
469
|
self.get(url)
|
|
@@ -386,7 +482,7 @@ class _ISOC_AMS(Driver):
|
|
|
386
482
|
elem = WebDriverWait(self, timeout).until(cond)
|
|
387
483
|
return elem
|
|
388
484
|
except TimeoutException:
|
|
389
|
-
|
|
485
|
+
strong_msg(message, level=logging.ERROR)
|
|
390
486
|
raise
|
|
391
487
|
|
|
392
488
|
#
|
|
@@ -396,8 +492,8 @@ class _ISOC_AMS(Driver):
|
|
|
396
492
|
def login(self, credentials):
|
|
397
493
|
# Sign on user and navigate to the Chapter leaders page,
|
|
398
494
|
|
|
399
|
-
|
|
400
|
-
|
|
495
|
+
log(date=False)
|
|
496
|
+
log("logging in")
|
|
401
497
|
|
|
402
498
|
# go to community home page after succesfullogin
|
|
403
499
|
self.get("https://community.internetsociety.org/s/home-community")
|
|
@@ -416,19 +512,31 @@ class _ISOC_AMS(Driver):
|
|
|
416
512
|
elem)
|
|
417
513
|
|
|
418
514
|
# self.set_window_size(1600, 300)
|
|
419
|
-
|
|
515
|
+
dlog("log in started")
|
|
420
516
|
# community portal
|
|
421
|
-
self.waitfor(EC.presence_of_element_located,
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
517
|
+
# self.waitfor(EC.presence_of_element_located,
|
|
518
|
+
# "siteforceStarterBody",
|
|
519
|
+
# by=By.CLASS_NAME,
|
|
520
|
+
# message=)
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
elem = WebDriverWait(self, 10).until(
|
|
524
|
+
EC.any_of(
|
|
525
|
+
EC.presence_of_element_located((By.CLASS_NAME, "siteforceStarterBody")),
|
|
526
|
+
EC.visibility_of_element_located((By.CSS_SELECTOR, "form div.error p"))))
|
|
527
|
+
except TimeoutException:
|
|
528
|
+
strong_msg("timelimit exceeded while waiting "
|
|
529
|
+
"for Community portal to open", level=logging.ERROR)
|
|
530
|
+
raise
|
|
531
|
+
if elem.tag_name == "p":
|
|
532
|
+
strong_msg(elem.text, level=logging.ERROR)
|
|
533
|
+
exit(1)
|
|
426
534
|
|
|
427
|
-
|
|
535
|
+
dlog("now on community portal")
|
|
428
536
|
|
|
429
537
|
# open chapter Leader Portal
|
|
430
538
|
self.get("https://community.internetsociety.org/leader")
|
|
431
|
-
|
|
539
|
+
dlog("waiting for Chapter Leader portal")
|
|
432
540
|
|
|
433
541
|
# look if menue appears to be ready (and grab link to reports page)
|
|
434
542
|
reports_ref = self.waitfor(EC.element_to_be_clickable,
|
|
@@ -448,8 +556,8 @@ class _ISOC_AMS(Driver):
|
|
|
448
556
|
)
|
|
449
557
|
|
|
450
558
|
self.windows["leader"] = self.current_window_handle
|
|
451
|
-
|
|
452
|
-
|
|
559
|
+
log("Now on Chapter Leader portal")
|
|
560
|
+
log(date=False)
|
|
453
561
|
|
|
454
562
|
# get lists (in an extra "reports" tab)
|
|
455
563
|
self.reports_link = reports_ref.get_attribute('href')
|
|
@@ -468,8 +576,8 @@ class _ISOC_AMS(Driver):
|
|
|
468
576
|
# reason is Active Chapter Members doesn't give us the link to
|
|
469
577
|
# act on the list (to delete members)
|
|
470
578
|
|
|
471
|
-
|
|
472
|
-
|
|
579
|
+
log(date=False)
|
|
580
|
+
log("start build members list")
|
|
473
581
|
self.create_report_page("Members",
|
|
474
582
|
"Active Chapter Members")
|
|
475
583
|
self.load_report("Members")
|
|
@@ -482,8 +590,8 @@ class _ISOC_AMS(Driver):
|
|
|
482
590
|
|
|
483
591
|
for k, v in members.items():
|
|
484
592
|
v["action link"] = contacts.get(v["email"])
|
|
485
|
-
|
|
486
|
-
|
|
593
|
+
log("members list finished / ", len(members), "collected")
|
|
594
|
+
log(date=False)
|
|
487
595
|
return members
|
|
488
596
|
|
|
489
597
|
def build_pending_applicants_list(self) -> dict:
|
|
@@ -497,9 +605,9 @@ class _ISOC_AMS(Driver):
|
|
|
497
605
|
# reason is the page referred to in the reports page doesn't give
|
|
498
606
|
# us the ISOC-ID
|
|
499
607
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
608
|
+
log(date=False)
|
|
609
|
+
log("start build pending applications")
|
|
610
|
+
dlog("Creating page for Pending Applications")
|
|
503
611
|
msg = "timelimit exceeded while waiting " \
|
|
504
612
|
"for report page for Pending Application report"
|
|
505
613
|
cond = (EC.presence_of_element_located,
|
|
@@ -507,13 +615,14 @@ class _ISOC_AMS(Driver):
|
|
|
507
615
|
"table")
|
|
508
616
|
self.activate_window("report",
|
|
509
617
|
url=self.group_application_link)
|
|
618
|
+
dlog("Pending applications", "page created")
|
|
510
619
|
pendings = self.get_table(self.get_pendings)
|
|
511
|
-
|
|
512
|
-
|
|
620
|
+
log("Pending applications list finished / ", len(pendings), "collected")
|
|
621
|
+
log(date=False)
|
|
513
622
|
return pendings
|
|
514
623
|
|
|
515
624
|
def create_report_page(self, subject, button_title):
|
|
516
|
-
|
|
625
|
+
dlog("Creating page for", subject)
|
|
517
626
|
msg = "timelimit exceeded while waiting " \
|
|
518
627
|
"for report page for " + subject + " report"
|
|
519
628
|
self.activate_window("report",
|
|
@@ -525,10 +634,10 @@ class _ISOC_AMS(Driver):
|
|
|
525
634
|
))
|
|
526
635
|
time.sleep(1)
|
|
527
636
|
self.execute_script('arguments[0].click();', elem)
|
|
528
|
-
|
|
637
|
+
dlog(subject, "page created")
|
|
529
638
|
|
|
530
639
|
def load_report(self, subject):
|
|
531
|
-
|
|
640
|
+
dlog("Loading", subject)
|
|
532
641
|
cond = EC.presence_of_element_located;
|
|
533
642
|
val = "iframe"
|
|
534
643
|
msg = "timelimit exceeded while waiting " \
|
|
@@ -546,7 +655,7 @@ class _ISOC_AMS(Driver):
|
|
|
546
655
|
"iframe.isView")))
|
|
547
656
|
self.waitfor(EC.presence_of_element_located, "//table//tbody//td",
|
|
548
657
|
message=msg)
|
|
549
|
-
|
|
658
|
+
dlog("got list of", subject)
|
|
550
659
|
|
|
551
660
|
def get_table(self, reader: callable):
|
|
552
661
|
# this is a wrapper for reading tables
|
|
@@ -561,19 +670,15 @@ class _ISOC_AMS(Driver):
|
|
|
561
670
|
break
|
|
562
671
|
return int(s[:i])
|
|
563
672
|
if reader == self.get_members:
|
|
564
|
-
|
|
673
|
+
dlog('collecting the following fields: "ISOC-ID", "first name", '
|
|
565
674
|
'"last name", "email"')
|
|
566
675
|
if reader == self.get_member_contacts:
|
|
567
|
-
|
|
676
|
+
dlog('collecting the following fields: '
|
|
568
677
|
'"action link" (for taking actions), '
|
|
569
678
|
'"email" (to connect with members list)')
|
|
570
679
|
if reader == self.get_pendings:
|
|
571
|
-
|
|
680
|
+
dlog('collecting the following fields: "name", "email", '
|
|
572
681
|
'"action link", "date"')
|
|
573
|
-
# if reader == self.get_pendings:
|
|
574
|
-
# self.log('collecting the following fields: "name", "email", '
|
|
575
|
-
# '"contact link", "action link", "date"')
|
|
576
|
-
|
|
577
682
|
|
|
578
683
|
if reader == self.get_pendings:
|
|
579
684
|
tableselector = "table.uiVirtualDataTable tbody tr"
|
|
@@ -593,33 +698,32 @@ class _ISOC_AMS(Driver):
|
|
|
593
698
|
total_elem = self.find_element(By.CSS_SELECTOR, total_selector)
|
|
594
699
|
WebDriverWait(self, 10).until(_WaitForTextInElement(total_elem))
|
|
595
700
|
total = getint(total_elem.text)
|
|
596
|
-
|
|
597
|
-
|
|
701
|
+
dlog("Total (records expected):", total)
|
|
702
|
+
dlog("Waiting for Total to stabilise")
|
|
598
703
|
# wait a few seconds for total to become stable
|
|
599
704
|
time.sleep(3)
|
|
600
705
|
total = getint(total_elem.text)
|
|
601
|
-
|
|
706
|
+
dlog("Total (records expected):", total)
|
|
602
707
|
data = {}
|
|
603
708
|
while total > len(data):
|
|
604
709
|
time.sleep(3)
|
|
605
710
|
rows = self.find_elements(
|
|
606
711
|
By.CSS_SELECTOR, tableselector)
|
|
607
|
-
|
|
712
|
+
dlog("calling reader with", len(rows), "table rows, ",
|
|
608
713
|
"(collected records so far:", len(data),")")
|
|
609
714
|
scr_to = reader(rows, data)
|
|
610
715
|
if getint(total_elem.text) != total:
|
|
611
716
|
total = getint(total_elem.text)
|
|
612
|
-
|
|
717
|
+
dlog("Total was updated, now:", total)
|
|
613
718
|
if len(data) < total:
|
|
614
719
|
self.execute_script('arguments[0].scrollIntoView(true);', scr_to)
|
|
615
720
|
else:
|
|
616
|
-
|
|
721
|
+
dlog("records collected / total", len(data), " /", total)
|
|
617
722
|
return data
|
|
618
723
|
|
|
619
724
|
def get_members(self, rows, members):
|
|
620
725
|
for row in rows:
|
|
621
726
|
cells = row.find_elements(By.CSS_SELECTOR, "td")
|
|
622
|
-
# self.log(row.text.replace("\n"," / "))
|
|
623
727
|
if cells and cells[0].text and cells[0].text not in members.keys():
|
|
624
728
|
member = {}
|
|
625
729
|
member["first name"] = cells[1].text
|
|
@@ -631,9 +735,7 @@ class _ISOC_AMS(Driver):
|
|
|
631
735
|
|
|
632
736
|
def get_member_contacts(self, rows, members):
|
|
633
737
|
for row in rows:
|
|
634
|
-
# self.log(row.text.replace("\n"," / "))
|
|
635
738
|
cells = row.find_elements(By.CSS_SELECTOR, "td")
|
|
636
|
-
# self.log(len(cells), "cells")
|
|
637
739
|
if cells and \
|
|
638
740
|
len(cells) > 11 and \
|
|
639
741
|
cells[11].text and \
|
|
@@ -647,7 +749,6 @@ class _ISOC_AMS(Driver):
|
|
|
647
749
|
def get_pendings(self, rows, pendings):
|
|
648
750
|
for row in rows:
|
|
649
751
|
cells = row.find_elements(By.CSS_SELECTOR, ".slds-cell-edit")
|
|
650
|
-
# self.log(row.text.replace("\n"," / "))
|
|
651
752
|
if cells and cells[3].text:
|
|
652
753
|
pending = {}
|
|
653
754
|
pending["name"] = cells[4].text
|
|
@@ -660,7 +761,6 @@ class _ISOC_AMS(Driver):
|
|
|
660
761
|
get_attribute('href')
|
|
661
762
|
pending["date"] = datetime.strptime(
|
|
662
763
|
cells[10].text, "%m/%d/%Y")
|
|
663
|
-
# . strftime("%Y-%m-%d ") + " 00:00:00"
|
|
664
764
|
pendings[cells[6].text] = pending
|
|
665
765
|
orow = row
|
|
666
766
|
return orow
|
|
@@ -671,8 +771,8 @@ class _ISOC_AMS(Driver):
|
|
|
671
771
|
|
|
672
772
|
def deny(self, entry, reason):
|
|
673
773
|
time_to_wait = 100
|
|
674
|
-
|
|
675
|
-
|
|
774
|
+
log(date=False)
|
|
775
|
+
log("start denial for", entry["name"])
|
|
676
776
|
# operation will take place in an own tab
|
|
677
777
|
self.activate_window("action",
|
|
678
778
|
url=entry["action link"])
|
|
@@ -692,7 +792,7 @@ class _ISOC_AMS(Driver):
|
|
|
692
792
|
until(EC.presence_of_element_located((
|
|
693
793
|
By.CSS_SELECTOR, 'button.slds-modal__close')))
|
|
694
794
|
|
|
695
|
-
|
|
795
|
+
dlog("select a reason for denial to feed AMS's couriosity")
|
|
696
796
|
elem = self.waitfor(EC.element_to_be_clickable,
|
|
697
797
|
"//div"
|
|
698
798
|
"[contains(concat(' ',normalize-space(@class),' '),"
|
|
@@ -702,7 +802,7 @@ class _ISOC_AMS(Driver):
|
|
|
702
802
|
time.sleep(1) # for what ist worth?
|
|
703
803
|
self.execute_script('arguments[0].click();', elem)
|
|
704
804
|
###
|
|
705
|
-
|
|
805
|
+
dlog("Waiting for combobox, chose 'other'")
|
|
706
806
|
|
|
707
807
|
elem = self.waitfor(EC.element_to_be_clickable,
|
|
708
808
|
"//lightning-base-combobox-item"
|
|
@@ -718,11 +818,11 @@ class _ISOC_AMS(Driver):
|
|
|
718
818
|
"//input",
|
|
719
819
|
message="timelimit exceeded while waiting "
|
|
720
820
|
"for deny reason 'Other - Details'")
|
|
721
|
-
|
|
821
|
+
log(f"we'll give '{reason}' as reason")
|
|
722
822
|
time.sleep(1)
|
|
723
823
|
# elem.send_keys(reason)
|
|
724
824
|
self.execute_script(f'arguments[0].value="{reason}";', elem)
|
|
725
|
-
|
|
825
|
+
dlog("finally click next")
|
|
726
826
|
|
|
727
827
|
elem = self.waitfor(EC.element_to_be_clickable,
|
|
728
828
|
"//flowruntime-navigation-bar"
|
|
@@ -735,15 +835,15 @@ class _ISOC_AMS(Driver):
|
|
|
735
835
|
try:
|
|
736
836
|
WebDriverWait(self, 15).until(EC.staleness_of(d_close))
|
|
737
837
|
except TimeoutException:
|
|
738
|
-
|
|
739
|
-
|
|
838
|
+
strong_msg("Timeout: Maybe operation was not performed")
|
|
839
|
+
log(date=False)
|
|
740
840
|
return False
|
|
741
|
-
|
|
841
|
+
log("done")
|
|
742
842
|
return True
|
|
743
843
|
|
|
744
844
|
def approve(self, entry):
|
|
745
|
-
|
|
746
|
-
|
|
845
|
+
log(date=False)
|
|
846
|
+
log("start approval for", entry["name"])
|
|
747
847
|
|
|
748
848
|
self.activate_window("action",
|
|
749
849
|
url=entry["action link"])
|
|
@@ -756,7 +856,7 @@ class _ISOC_AMS(Driver):
|
|
|
756
856
|
"waiting for details page for " +
|
|
757
857
|
entry["name"] + " to complete")
|
|
758
858
|
|
|
759
|
-
|
|
859
|
+
dlog("starting with approval")
|
|
760
860
|
time.sleep(1) # for what ist worth?
|
|
761
861
|
self.execute_script('arguments[0].click();', elem)
|
|
762
862
|
|
|
@@ -764,7 +864,7 @@ class _ISOC_AMS(Driver):
|
|
|
764
864
|
until(EC.presence_of_element_located((
|
|
765
865
|
By.CSS_SELECTOR, 'button.slds-modal__close')))
|
|
766
866
|
|
|
767
|
-
|
|
867
|
+
dlog("finally click next")
|
|
768
868
|
elem = self.waitfor(EC.element_to_be_clickable,
|
|
769
869
|
"//flowruntime-navigation-bar"
|
|
770
870
|
"/footer"
|
|
@@ -777,17 +877,18 @@ class _ISOC_AMS(Driver):
|
|
|
777
877
|
try:
|
|
778
878
|
WebDriverWait(self, 15).until(EC.staleness_of(d_close))
|
|
779
879
|
except TimeoutException:
|
|
780
|
-
|
|
781
|
-
|
|
880
|
+
strong_msg("Timeout: Maybe operation was not performed",
|
|
881
|
+
level=logging.ERROR)
|
|
882
|
+
log(date=False)
|
|
782
883
|
return False
|
|
783
|
-
|
|
884
|
+
log("done")
|
|
784
885
|
return True
|
|
785
886
|
|
|
786
887
|
|
|
787
888
|
def delete(self, entry):
|
|
788
|
-
|
|
889
|
+
log(date=False)
|
|
789
890
|
name = entry["first name"] + " " + entry["last name"]
|
|
790
|
-
|
|
891
|
+
log("start delete", name, "from AMS Chapter members list" )
|
|
791
892
|
|
|
792
893
|
self.activate_window("action",
|
|
793
894
|
url=entry["action link"])
|
|
@@ -810,10 +911,11 @@ class _ISOC_AMS(Driver):
|
|
|
810
911
|
try:
|
|
811
912
|
WebDriverWait(self, 15).until(EC.staleness_of(d_close))
|
|
812
913
|
except TimeoutException:
|
|
813
|
-
|
|
814
|
-
|
|
914
|
+
strong_msg("Timeout: Maybe operation was not performed",
|
|
915
|
+
level=logging.ERROR)
|
|
916
|
+
log(date=False)
|
|
815
917
|
return False
|
|
816
|
-
|
|
918
|
+
log("done")
|
|
817
919
|
return True
|
|
818
920
|
|
|
819
921
|
|
|
@@ -828,62 +930,71 @@ if __name__ == "__main__":
|
|
|
828
930
|
dryrun = False
|
|
829
931
|
if "-d" in sys.argv:
|
|
830
932
|
dryrun = True
|
|
933
|
+
debug = False
|
|
934
|
+
if "--debug" in sys.argv:
|
|
935
|
+
debug = True
|
|
831
936
|
|
|
832
937
|
print("Username", end=":")
|
|
833
938
|
user_id = input()
|
|
834
939
|
password = getpass()
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
940
|
+
if debug:
|
|
941
|
+
ams = ISOC_AMS(
|
|
942
|
+
user_id,
|
|
943
|
+
password,
|
|
944
|
+
headless=headless,
|
|
945
|
+
dryrun=dryrun,
|
|
946
|
+
logfile=None,
|
|
947
|
+
debuglog=sys.stderr,
|
|
948
|
+
)
|
|
949
|
+
else:
|
|
950
|
+
ams = ISOC_AMS(
|
|
951
|
+
user_id,
|
|
952
|
+
password,
|
|
953
|
+
headless=headless,
|
|
954
|
+
dryrun=dryrun,
|
|
955
|
+
logfile=sys.stdout,
|
|
956
|
+
)
|
|
840
957
|
members = ams.members_list
|
|
841
958
|
pendings = ams.pending_applications_list
|
|
842
959
|
|
|
843
|
-
|
|
960
|
+
strong_msg("MEMBERS")
|
|
844
961
|
i = 0
|
|
845
962
|
for k, v in members.items():
|
|
846
963
|
i += 1
|
|
847
|
-
|
|
964
|
+
log(i, k, v["first name"], v["last name"], v["email"], date=False)
|
|
848
965
|
|
|
849
|
-
|
|
966
|
+
strong_msg("PENDING APPLICATIONS")
|
|
850
967
|
i = 0
|
|
851
968
|
for k, v in pendings.items():
|
|
852
969
|
i += 1
|
|
853
970
|
# print(i, k, v)
|
|
854
|
-
|
|
971
|
+
log(i, k, v["name"], v["email"], v["date"].isoformat()[:10], date=False)
|
|
855
972
|
|
|
856
973
|
if inp:
|
|
857
|
-
|
|
974
|
+
log('READING COMMANDS:')
|
|
858
975
|
import re
|
|
859
976
|
patt = re.compile(r'(approve|deny|delete):?\s*([\d, ]+)')
|
|
860
977
|
func = {"approve": ams.approve_pending_applications,
|
|
861
978
|
"deny": ams.deny_pending_applications,
|
|
862
|
-
"delete": ams.delete_members
|
|
979
|
+
"delete": ams.delete_members,
|
|
863
980
|
}
|
|
864
981
|
splitter = re.compile(r'[\s,]+')
|
|
865
982
|
for rec in sys.stdin:
|
|
983
|
+
rec = rec.strip()
|
|
866
984
|
if m := patt.match(rec):
|
|
867
985
|
command = m.group(1)
|
|
868
986
|
keys = splitter.split(m.group(2))
|
|
869
987
|
func[command](keys)
|
|
870
988
|
else:
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
for k, v in data[1].items():
|
|
884
|
-
if "members" in data[0]:
|
|
885
|
-
print(" ", v["first name"], v["last name"], v["email"], "("+k+")")
|
|
886
|
-
else:
|
|
887
|
-
print(" ", v["name"], v["email"], "("+k+")")
|
|
888
|
-
if devs == 0:
|
|
889
|
-
print("All results as expected")
|
|
989
|
+
log(rec, "contains an error", level=logging.ERROR)
|
|
990
|
+
log("EOF of command input")
|
|
991
|
+
|
|
992
|
+
result = ams.difference_from_expected()
|
|
993
|
+
if type(result) is not str:
|
|
994
|
+
for data in result.items():
|
|
995
|
+
log(data[0])
|
|
996
|
+
for k, v in data[1].items():
|
|
997
|
+
if "members" in data[0]:
|
|
998
|
+
log(" ", v["first name"], v["last name"], v["email"], "("+k+")", date=False)
|
|
999
|
+
else:
|
|
1000
|
+
log(" ", v["name"], v["email"], "("+k+")", date=False)
|