isoc-ams 0.0.2__py2.py3-none-any.whl → 0.1.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of isoc-ams might be problematic. Click here for more details.

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