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

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

Potentially problematic release.


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

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