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