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.py CHANGED
@@ -4,83 +4,105 @@
4
4
 
5
5
  """Extract or modify Chapter Data of the ISOC AMS (Salesforce) Database.
6
6
 
7
- This module consists of a Class ISOC_AMS wrapping _ISOC_AMS which subclasses
8
- the webdriver.<browser> of Selenium. Up to now ownly firefox and chrome
9
- drivers are implemented and tested.
10
-
11
- The ISOC_AMS class provides the following properties:
12
- members_list:
13
- a list of Chapter members (according to AMS) with data (and links)
14
- pending_applicants_list:
15
- a list of pending appplicants (according to AMS) for a Chapter
16
- membership with data (and links)
17
- these properties are initialized on first access ... and it will take time
18
-
19
- The ISOC_AMS class provides the following methods:
20
- build_members_list:
21
- to build a list of Chapter members with data (and links)
22
- build_pending_applicants_list:
23
- to build a list of pending appplicants for a Chapter membership with data (and links)
24
- deny_applicants:
25
- to deny Chapter membership for a list of applicants
26
- approve_applicants:
27
- to approve Chapter membership for a list of applicants
28
- delete_members:
29
- to revoke Chapter membership for members from the members list
30
- difference_from_expected:
31
- to reread AMS and check if all operations were successfull (not ever
32
- problem can be detected by the methods)
33
-
34
- ISOC_AMS will log you in to ISOC.ORG and check your authorization at
35
- instantiation.
36
-
37
- To select a webdriver, an ISOC_AMS_WEBDRIVER environment variable can be used.
38
- E.g.
39
- ISOC_AMS_WEBDRIVER=Firefox
40
-
41
- Default is Firefox. Only Firefox and Chrome are allowed for now.
42
-
43
- Example
44
- _______
45
- from isoc_ams import ISOC_AMS
46
- userid, password = "myuserid", "mysecret"
47
-
48
- # this will log you in
49
- # and instantiate an ISOC_AMS object
50
- ams = ISOC_AMS(userid, password)
51
-
52
- # this will read the list of members,
53
- # registered as chapters members
54
- members = ams.members_list
55
-
56
- # print the results
57
- for isoc_id, member in members.items():
58
- print(isoc_id,
59
- member["first name"],
60
- member["last name"],
61
- member["email"],
62
- )
63
- # select members to be deleted
64
- deletees = <...> # various formats are allowed for operation methods
65
- delete_members(deletees)
66
-
67
- # check if all went well
68
- print(difference_from_expected())
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.1"
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
- Since it is about web driving the activities on the website are logged
107
- to check what's going on (on the Website)'. Default is logging to
108
- stdout.
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
- Args
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.StringIO | str = sys.stdout,
126
- headless: bool = True):
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._members_list: dict | None = None
132
- self._pending_applications_list: dict | None = None
133
- self._ams = _ISOC_AMS(user, password, logfile)
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
- Returns
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
- So ISOC-ID is used as key for the entries
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 applicants.
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
- Returns
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
- So ISOC-ID is used as key for the entries
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
- Args
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
- deletes delete_list entries from AMS-list of Chapter members
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 = str(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
- self._ams.delete(self._members_list[deletee])
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
- self._ams.strong_msg("ISOC-ID", deletee,
203
- "is not in AMS Chapter members list" )
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
- Args
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
- approves pending members on approve_list as Chapter members
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 = str(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._ams.approve(self._pending_applications_list[approvee])
221
- del self._pending_applications_list[approvee]
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
- self._ams.strong_msg("ISOC-ID", approvee,
224
- "is not in pending applications list" )
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
- Args
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 are denied for
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 = str(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._ams.deny(self._pending_applications_list[denyee],
245
- reason)
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
- self._ams.strong_msg("ISOC-ID", denyee,
249
- "is not in pending applications list" )
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
- Returns
255
- -------
256
- A dict containing deviations of the inteded outcome:
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._ams.log(date=False)
271
- self._ams.log("collect differences from expected result after operations")
272
-
273
- not_deleted = {}
274
- not_approved = {}
275
- not_removed_from_pending = {}
276
- new_members_list = self._ams.build_members_list()
277
- for nm in new_members_list:
278
- if nm not in self._members_list:
279
- not_deleted[nm] = new_members_list[nm]
280
- for nm in self._members_list:
281
- if nm not in new_members_list:
282
- not_approved[nm] = self._members_list[nm]
283
- new_pending_applications_list = self._ams.build_pending_applicants_list()
284
- for np in new_pending_applications_list:
285
- if np not in self._pending_applications_list:
286
- not_removed_from_pending[np] = new_pending_applications_list[np]
287
-
288
- return {"not deleted from members": not_deleted,
289
- "not approved from pending applicants list": not_approved,
290
- "not removed from pending applicants list": not_removed_from_pending}
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, user: str, password: str, logfile: str = sys.stdout):
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
- # self.log("switching to window", name)
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
- # self.log("switching to NEW window", name)
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
- self.strong_msg(message)
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, user: str, password: str):
492
+ def login(self, credentials):
368
493
  # Sign on user and navigate to the Chapter leaders page,
369
494
 
370
- self.log(date=False)
371
- self.log("logging in")
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
- % (user, password),
511
+ % credentials,
387
512
  elem)
388
513
 
389
514
  # self.set_window_size(1600, 300)
390
- self.log("log in started")
515
+ dlog("log in started")
391
516
  # community portal
392
- self.waitfor(EC.presence_of_element_located,
393
- "siteforceStarterBody",
394
- by=By.CLASS_NAME,
395
- message="timelimit exceeded while waiting "
396
- "for Community portal to open")
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
- self.log("now on community portal")
535
+ dlog("now on community portal")
399
536
 
400
537
  # open chapter Leader Portal
401
538
  self.get("https://community.internetsociety.org/leader")
402
- self.log("waiting for Chapter Leader portal")
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
- self.log("Chapter Leader portal OK")
423
- self.log(date=False)
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
- self.log(date=False)
443
- self.log("start build members list")
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
- self.log("members list finished")
457
- self.log(date=False)
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
- self.log(date=False)
472
- self.log("start build pending applications")
473
- self.log("Creating page for Pending Applications")
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
- self.log("pending application list finished")
483
- self.log(date=False)
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
- self.log("Creating page for", subject)
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
- self.log(subject, "page created")
637
+ dlog(subject, "page created")
500
638
 
501
639
  def load_report(self, subject):
502
- self.log("Loading", subject)
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
- self.log("got list of", subject)
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
- self.log('collecting the following fields: "ISOC-ID", "first name", '
673
+ dlog('collecting the following fields: "ISOC-ID", "first name", '
536
674
  '"last name", "email"')
537
675
  if reader == self.get_member_contacts:
538
- self.log('collecting the following fields: '
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
- self.log('collecting the following fields: "name", "email", '
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
- self.log("Total (records expected):", total)
568
- self.log("Waiting for Total to stabilise")
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
- self.log("Total (records expected):", total)
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
- self.log("calling reader with", len(rows), "table rows, ",
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
- self.log("Total was updated, now:", total)
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
- self.log("records collected / total", len(data), " /", total)
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
- self.log(date=False)
646
- self.log("start denial for", entry["name"])
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
- self.log("select a reason for denial to feed AMS's couriosity")
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
- self.log("Waiting for combobox, chose 'other'")
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
- self.log(f"we'll give '{reason}' as reason")
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
- self.log("finally click next")
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
- self.strong_msg("Timeout: Maybe operation was not performed")
710
- self.log(date=False)
838
+ strong_msg("Timeout: Maybe operation was not performed")
839
+ log(date=False)
711
840
  return False
712
- self.log("done")
841
+ log("done")
713
842
  return True
714
843
 
715
844
  def approve(self, entry):
716
- self.log(date=False)
717
- self.log("start approval for", entry["name"])
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
- self.log("starting with approval")
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
- self.log("finally click next")
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
- self.strong_msg("Timeout: Maybe operation was not performed")
752
- self.log(date=False)
880
+ strong_msg("Timeout: Maybe operation was not performed",
881
+ level=logging.ERROR)
882
+ log(date=False)
753
883
  return False
754
- self.log("done")
884
+ log("done")
755
885
  return True
756
886
 
757
887
 
758
888
  def delete(self, entry):
759
- self.log(date=False)
889
+ log(date=False)
760
890
  name = entry["first name"] + " " + entry["last name"]
761
- self.log("start delete", name, "from AMS Chapter members list" )
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
- self.strong_msg("Timeout: Maybe operation was not performed")
785
- self.log(date=False)
914
+ strong_msg("Timeout: Maybe operation was not performed",
915
+ level=logging.ERROR)
916
+ log(date=False)
786
917
  return False
787
- self.log("done")
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
- ams = ISOC_AMS(
800
- user_id,
801
- password,
802
- headless=headless)
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
- print("\nMEMBERS")
960
+ strong_msg("MEMBERS")
807
961
  i = 0
808
962
  for k, v in members.items():
809
963
  i += 1
810
- print(i, k + ":", v["first name"], v["last name"], v["email"], v["action link"])
964
+ log(i, k, v["first name"], v["last name"], v["email"], date=False)
811
965
 
812
- print("\nPENDING APPLICATIONS")
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
- print(i, k, v["name"], v["email"], v["action link"],
818
- v["date"].isoformat()[:19])
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)