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