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