isoc-ams 0.0.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.
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: isoc-ams
3
+ Version: 0.0.1
4
+ Summary: A Python 3 module to cope with ISOC-AMS.
5
+ Author-email: Klaus Birkenbihl <klaus.birkenbihl@isoc.de>
6
+ Maintainer: Klaus Birkenbihl
7
+ Description-Content-Type: text/markdown
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ License-File: LICENSE
10
+ Requires-Dist: selenium>4
11
+ Project-URL: Home, https://github.com/birkenbihl/isoc-ams
12
+
13
+
14
+ # isoc-ams
15
+
16
+ A Python Interface to access the 'Advanced Members Administration System' (AMS) of the 'Internet Society' (ISOC). This especially usefulfor ISOC Chapter Admins who want to synchronize their Chapter Database with AMS (semi)automatically.
17
+
18
+ After 10 years+ of sorrow, millions minutes of waiting for answers from the AMS web interface, tons of useless clicks, many (in fact) rejected requests to provide an API access: the author decided to build an API himself. Even if it might not be more than a demonstrator for the functionality needed. Anyhow (see below): for now it is running on a weekly basis doing a great job in avoiding manual work.
19
+
20
+ Unfortunately the constraints are severe:
21
+ - access had to be through the web interface since this is the only interface provided. As a consequence it is slow, sometimes unreliable and hard to implement. At least there are working implementations of the "W3C webdriver" recommendtion. One of them is Selenium used for this project.
22
+ - the existing web interface is far from being stable or guarateed. So changes to the web interface might spoil the whole project. There is great chance that few weeks from now a new "super dooper" AMS will be announced and as always after these announcements things will get worse.
23
+ - tests are close to impossible. There is no such thing as a TEST AMS.
24
+
25
+ Is there a possible good exit? Well, maybe some day soon - in 10 or 20 years if ISOC still exists - there will be an API provided by ISOC that makes this project obsolete. Or at least may be an all-mighty AI will step in. Let's dream on!
26
+
27
+ ## Features
28
+ ISOC maintains two main Lists that are relevant for the operation of this interface:
29
+ - a list of ISOC members registered as members of the Chapter
30
+ - a list of ISOC members that applied for a Chapter membership.
31
+
32
+ Consequently isoc-ams provides methods for the following tasks:
33
+ 1. read list of ISOC members registered as Chapter members
34
+ 1. read list of ISOC members that applied for a Chapter membership
35
+ 1. approve ISOC AMS applications
36
+ 1. deny ISOC AMS applications
37
+ 1. delete members from ISOC AMS Chapters Member list
38
+ 1. add members to ISOC AMS Chapters Member list (Chapter admins are not authorized to do this. So the author suggest to write a mail to ams-support.)
39
+
40
+ Don't forget: it takes time and you may see many kinds of errors. Often the cure is "try again later". Any expectation of flawless is not appropriate.
41
+
42
+ So here we go:
43
+
44
+ ## Installation
45
+
46
+ Install isoc-ams with pip.
47
+
48
+ ```bash
49
+ python -m pip install -U isoc-ams
50
+ ```
51
+
52
+ Best would be to use a virtual environment (venv).
53
+
54
+ ## Running isoc_ams
55
+
56
+ You may select a webdriver of your choice (provided it is one of "firefox" or "chrome") by setting an environment variable ISOC_AMS_WEBDRIVER e.g.:
57
+ ```bash
58
+ ISOC_AMS_WEBDRIVER=firefox
59
+ ```
60
+ Recommended (and default) is "firefox".
61
+
62
+ Since crazy things may happen it is important to keep track of what is going on. So ISOC_AMS lets you know what it is doing.
63
+ by providing a logfile (goes to stdout by default).
64
+
65
+ So this happens if we call the module with:
66
+ ```bash
67
+ python -m isoc_ams
68
+ ```
69
+
70
+ ```
71
+ Username: xxx
72
+ Password:
73
+
74
+ AMS 2025-07-03 10:49:07 logging in
75
+ AMS 2025-07-03 10:49:11 log in started
76
+ AMS 2025-07-03 10:49:20 now on community portal
77
+ AMS 2025-07-03 10:49:25 waiting for Chapter Leader portal
78
+ AMS 2025-07-03 10:49:27 Chapter Leader portal OK
79
+
80
+
81
+ AMS 2025-07-03 10:49:27 start build members list
82
+ AMS 2025-07-03 10:49:27 Creating page for Members
83
+ AMS 2025-07-03 10:49:33 Members page created
84
+ AMS 2025-07-03 10:49:33 Loading Members
85
+ AMS 2025-07-03 10:49:39 got list of Members
86
+ AMS 2025-07-03 10:49:39 collecting the following fields: "ISOC-ID", "first name", "last name", "email"
87
+ AMS 2025-07-03 10:49:39 Total (records expected): 38
88
+ AMS 2025-07-03 10:49:39 Waiting for Total to stabilise
89
+ AMS 2025-07-03 10:49:42 Total (records expected): 59
90
+ AMS 2025-07-03 10:49:45 calling reader with 31 table rows, (collected records so far: 0 )
91
+ AMS 2025-07-03 10:49:50 calling reader with 32 table rows, (collected records so far: 29 )
92
+ AMS 2025-07-03 10:49:54 calling reader with 24 table rows, (collected records so far: 53 )
93
+ AMS 2025-07-03 10:49:55 records collected / total 59 / 59
94
+ AMS 2025-07-03 10:49:55 Creating page for Member Contacts
95
+ AMS 2025-07-03 10:50:00 Member Contacts page created
96
+ AMS 2025-07-03 10:50:00 Loading Member Contacts
97
+ AMS 2025-07-03 10:50:04 got list of Member Contacts
98
+ AMS 2025-07-03 10:50:04 collecting the following fields: "action link" (for taking actions), "email" (to connect with members list)
99
+ AMS 2025-07-03 10:50:04 Total (records expected): 8
100
+ AMS 2025-07-03 10:50:04 Waiting for Total to stabilise
101
+ AMS 2025-07-03 10:50:07 Total (records expected): 59
102
+ AMS 2025-07-03 10:50:10 calling reader with 30 table rows, (collected records so far: 0 )
103
+ AMS 2025-07-03 10:50:14 calling reader with 31 table rows, (collected records so far: 28 )
104
+ AMS 2025-07-03 10:50:18 calling reader with 25 table rows, (collected records so far: 51 )
105
+ AMS 2025-07-03 10:50:18 records collected / total 59 / 59
106
+ AMS 2025-07-03 10:50:18 members list finished
107
+
108
+
109
+ AMS 2025-07-03 10:50:18 start build pending applications
110
+ AMS 2025-07-03 10:50:18 Creating page for Pending Applications
111
+ AMS 2025-07-03 10:50:22 collecting the following fields: "name", "email", "action link", "date"
112
+ AMS 2025-07-03 10:50:24 Total (records expected): 8
113
+ AMS 2025-07-03 10:50:24 Waiting for Total to stabilise
114
+ AMS 2025-07-03 10:50:27 Total (records expected): 8
115
+ AMS 2025-07-03 10:50:30 calling reader with 8 table rows, (collected records so far: 0 )
116
+ AMS 2025-07-03 10:50:31 records collected / total 8 / 8
117
+ AMS 2025-07-03 10:50:31 pending application list finished
118
+
119
+
120
+ MEMBERS
121
+ 1 ...
122
+ 2 ...
123
+ ...
124
+
125
+ PENDING APPLICATIONS
126
+ 1 ...
127
+ 2 ...
128
+ ...
129
+ ```
130
+
131
+ Normally isoc_ams wont show any browser output - running headless. To do debugging it might useful to follow the activities in the browser. If you call isoc_ams with a -h option the browser will open.
132
+
133
+ ## Using the API
134
+
135
+ isoc_ams unleashes its full power when used as API to make things happen without human intervention. Check the file "isoc_de_ams_main.py" as an example for fully automatic synchronizing of local membership administration with AMS.
136
+
137
+ Here an excerpt of the output:
138
+ ```
139
+ Pending Applications:
140
+
141
+ the following pending applications will be approved:
142
+ ...
143
+ the following pending applications will be denied:
144
+ ...
145
+ the following pending applications will be invited:
146
+ ...
147
+ the following pending applications will be waiting:
148
+ ...
149
+
150
+ Members:
151
+ the following members will be deleted from AMS:
152
+ ...
153
+ for the following members a nagging mail will be sent to AMS-support (we are not authorized to fix it!):
154
+ ...
155
+ the following members are in sync
156
+ ...
157
+
158
+ AMS 2025-07-03 12:00:32 start delete ... from AMS Chapter members list
159
+ ...
160
+
161
+ Dear MAS-support team,
162
+ this is an automatic, complimentary Message from the ISOC German Chapter
163
+ Members Administration System (ISOC.DE MAS).
164
+
165
+ The following individuals are legally registered paying members
166
+ of ISOC.DE - many of them for more than 25 years.
167
+ ...
168
+ Uwe Mayer, xxx@yyy.com (ISOC-ID=1234567)
169
+ ...
170
+
171
+ Thank you,
172
+ Your ISOC.DE MAS support team
173
+ ...
174
+
175
+ DEVIATIONS FROM EXPECTED RESULTS
176
+ not deleted from members
177
+ ...
178
+ not approved from pending applicants list
179
+ ...
180
+ not removed from pending applicants list
181
+ ...
182
+ ```
183
+ See file isoc_ams.doc for doc on the API interface.
184
+
185
+ Have fun!
186
+
@@ -0,0 +1,5 @@
1
+ isoc_ams.py,sha256=_856QxwvSovWra0iOyOafpYm6K_aVVg_D14X4drq62M,31231
2
+ isoc_ams-0.0.1.dist-info/licenses/LICENSE,sha256=BJcMew_kvBWHjs-_Hd_OMHnxOLmeczeT88V4gFCT21U,1067
3
+ isoc_ams-0.0.1.dist-info/WHEEL,sha256=Dyt6SBfaasWElUrURkknVFAZDHSTwxg3PaTza7RSbkY,100
4
+ isoc_ams-0.0.1.dist-info/METADATA,sha256=nfcgOmUP9GbfkcXdb4UIJtqoYy-dRFkdT-PFxLOG8eQ,7746
5
+ isoc_ams-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 birkenbihl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
isoc_ams.py ADDED
@@ -0,0 +1,818 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # -*- coding: utf-8 -*-
4
+
5
+ """Extract or modify Chapter Data of the ISOC AMS (Salesforce) Database.
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
+ """
71
+ __version__ = "0.0.1"
72
+
73
+ from selenium import webdriver
74
+ from selenium.webdriver.common.by import By
75
+ from selenium.webdriver.support.wait import WebDriverWait, TimeoutException
76
+ from selenium.webdriver.support import expected_conditions as EC
77
+ from datetime import datetime
78
+
79
+ import io
80
+ import time
81
+ import sys
82
+ import os
83
+
84
+ _dr = os.environ.get("ISOC_AMS_WEBDRIVER", "firefox").lower()
85
+
86
+ if _dr == "firefox":
87
+ _options = webdriver.FirefoxOptions()
88
+ Driver = webdriver.Firefox
89
+ elif _dr == "chrome":
90
+ _options = webdriver.ChromeOptions()
91
+ Driver = webdriver.Chrome
92
+ else:
93
+ sys.exit("Selenium Driver " + _dr + " not implemented.")
94
+
95
+
96
+ def _WaitForTextInElement(element):
97
+
98
+ def _predicate(_):
99
+ return element.text
100
+ return _predicate
101
+
102
+
103
+ class ISOC_AMS:
104
+ """Perform admin operations on a Chaper's members list stored in AMS.
105
+
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.
109
+
110
+ By default all operations run headless. If you want to follow it on
111
+ a browser window use headless=False.
112
+
113
+ Args
114
+ ____
115
+ user: username (email) for ISO.ORG login
116
+ password: password for ISO.ORG login
117
+ logfile: where to write ISOC_AMS log output
118
+ headless: run without GUI
119
+
120
+ """
121
+
122
+ def __init__(self,
123
+ user: str,
124
+ password: str,
125
+ logfile: io.StringIO | str = sys.stdout,
126
+ headless: bool = True):
127
+ if _dr == "firefox" and headless:
128
+ _options.add_argument("--headless")
129
+ elif _dr == "chrome" and headless:
130
+ _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)
134
+
135
+ @property
136
+ def members_list(self) -> dict:
137
+ """Collects data about Chapter members.
138
+
139
+ Collects the relevant data about ISOC members
140
+ registered as Chapter members in AMS
141
+
142
+ Returns
143
+ -------
144
+ dictionary with the following scheme:
145
+ {<ISOC-ID>:
146
+ {"first name": <first name>,
147
+ "last name": <last name>,
148
+ "email": <Email address>',
149
+ "action link": <url of page to edit this entry>
150
+ },
151
+ ...
152
+ }
153
+
154
+ So ISOC-ID is used as key for the entries
155
+ """
156
+ if self._members_list is None:
157
+ self._members_list = self._ams.build_members_list()
158
+ return self._members_list
159
+
160
+ @property
161
+ def pending_applications_list(self) -> dict:
162
+ """Collects data about pending Chapter applicants.
163
+
164
+ Collects the relevant data about pending Chapter applicants
165
+ registered as pending Chapter applicants in AMS
166
+
167
+ Returns
168
+ -------
169
+ dictionary with the following scheme:
170
+ {<ISOC-ID>:
171
+ {"name": <name>,
172
+ "email": <Email address>',
173
+ "action link": <url of page to edit this entry>
174
+ },
175
+ ...
176
+ }
177
+ ---------------------------------------------
178
+ So ISOC-ID is used as key for the entries
179
+ """
180
+ if self._pending_applications_list is None:
181
+ self._pending_applications_list = \
182
+ self._ams.build_pending_applicants_list()
183
+ return self._pending_applications_list
184
+
185
+ def delete_members(self, delete_list: list | dict | str | int):
186
+ """Delete Member(s) from AMS-list of Chapter members.
187
+
188
+ Args
189
+ ----
190
+ delete_list: list of dict-entrys, or ISOC-IDs, or single entry
191
+ or ISOC-ID
192
+
193
+ deletes delete_list entries from AMS-list of Chapter members
194
+ """
195
+ if type(delete_list) in (str, int):
196
+ delete_list = str(delete_list),
197
+ for deletee in delete_list:
198
+ if deletee in self._members_list:
199
+ self._ams.delete(self._members_list[deletee])
200
+ del self._members_list[deletee]
201
+ else:
202
+ self._ams.strong_msg("ISOC-ID", deletee,
203
+ "is not in AMS Chapter members list" )
204
+
205
+
206
+ def approve_pending_applications(self, approve_list: list | dict | str | int):
207
+ """Approve pending Members as Chapter members.
208
+
209
+ Args
210
+ ----
211
+ approve_list: list of dict-entrys, or ISOC-IDs, or single entry
212
+ or ISOC-ID
213
+
214
+ approves pending members on approve_list as Chapter members
215
+ """
216
+ if type(approve_list) in (int, str):
217
+ approve_list = str(approve_list),
218
+ for approvee in approve_list:
219
+ if approvee in self._pending_applications_list:
220
+ self._ams.approve(self._pending_applications_list[approvee])
221
+ del self._pending_applications_list[approvee]
222
+ else:
223
+ self._ams.strong_msg("ISOC-ID", approvee,
224
+ "is not in pending applications list" )
225
+
226
+ def deny_pending_applications(self,
227
+ deny_list: list | dict | str | int,
228
+ reason: str = "Timeout, did not apply"):
229
+ """Denies pending Members Chapter membership.
230
+
231
+ Args
232
+ ----
233
+ deny_list: list of dict-entrys, or ISOC-IDs, or single entry
234
+ or ISOC-ID
235
+ reason: All denied applicants are denied for
236
+
237
+ denies Chapter membership for members on deny_list
238
+
239
+ """
240
+ if type(deny_list) in (str, int):
241
+ deny_list = str(deny_list),
242
+ for denyee in deny_list:
243
+ if denyee in self._pending_applications_list:
244
+ self._ams.deny(self._pending_applications_list[denyee],
245
+ reason)
246
+ del self._pending_applications_list[denyee]
247
+ else:
248
+ self._ams.strong_msg("ISOC-ID", denyee,
249
+ "is not in pending applications list" )
250
+
251
+ def difference_from_expected(self) -> dict:
252
+ """Compare intended outcome of operations with real outcome.
253
+
254
+ Returns
255
+ -------
256
+ A dict containing deviations of the inteded outcome:
257
+ {
258
+ "not deleted from members":
259
+ All entries in AMS-Chapter-Members that were supposed
260
+ to be deleted,
261
+ "not approved from pending applicants list":
262
+ All entries in pending applications that were supposed
263
+ to be approved but were not added to the AMS-Chapter-Members
264
+ "not removed from pending applicants list":
265
+ All entries in pending applications that should be
266
+ removed - either since approved or since denied
267
+ }
268
+
269
+ """
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}
291
+
292
+ class _ISOC_AMS(Driver):
293
+
294
+ def __init__(self, user: str, password: str, logfile: str = sys.stdout):
295
+
296
+ super().__init__(_options)
297
+ self.windows = {}
298
+ self.logfile = logfile
299
+ if type(self.logfile) is str:
300
+ self.logfile = open(self.log, "a")
301
+ self.login(user, password)
302
+
303
+ def __del__(self):
304
+ self.quit()
305
+
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
+ def activate_window(self, name: str, url: str | None = None, refresh: bool = False):
332
+ if self.windows.get(name):
333
+ # self.log("switching to window", name)
334
+ self.switch_to.window(self.windows[name])
335
+ if refresh:
336
+ self.navigate().refresh()
337
+ if url:
338
+ self.get(url)
339
+ return True
340
+ elif url:
341
+ # self.log("switching to NEW window", name)
342
+ self.switch_to.new_window('tab')
343
+ self.windows[name] = self.current_window_handle
344
+ self.get(url)
345
+ return True
346
+ else:
347
+ sys.exit('neither name nor url specified for "activate_window"'
348
+ 'or window "' + name + '" not found')
349
+
350
+
351
+ def waitfor(self, cond, val, timeout=20, message="", by=By.XPATH):
352
+ try:
353
+ if val:
354
+ elem = WebDriverWait(self, timeout).until(
355
+ cond((by, val)))
356
+ else:
357
+ elem = WebDriverWait(self, timeout).until(cond)
358
+ return elem
359
+ except TimeoutException:
360
+ self.strong_msg(message)
361
+ raise
362
+
363
+ #
364
+ # setup session, init windows
365
+ #
366
+
367
+ def login(self, user: str, password: str):
368
+ # Sign on user and navigate to the Chapter leaders page,
369
+
370
+ self.log(date=False)
371
+ self.log("logging in")
372
+
373
+ # go to community home page after succesfullogin
374
+ self.get("https://community.internetsociety.org/s/home-community")
375
+ # login
376
+ elem = self.waitfor(EC.element_to_be_clickable,
377
+ "next",
378
+ by=By.ID,
379
+ message="timelimit exceeded while waiting "
380
+ "for login page to complete")
381
+ # we use JS to fill the logi form, since sendkeys doesn'twork properly
382
+ self.execute_script(
383
+ "document.getElementById('signInName').value='%s';"
384
+ "document.getElementById('password').value='%s';"
385
+ "arguments[0].click();"
386
+ % (user, password),
387
+ elem)
388
+
389
+ # self.set_window_size(1600, 300)
390
+ self.log("log in started")
391
+ # 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")
397
+
398
+ self.log("now on community portal")
399
+
400
+ # open chapter Leader Portal
401
+ self.get("https://community.internetsociety.org/leader")
402
+ self.log("waiting for Chapter Leader portal")
403
+
404
+ # look if menue appears to be ready (and grab link to reports page)
405
+ reports_ref = self.waitfor(EC.element_to_be_clickable,
406
+ "//a[starts-with(@href,"
407
+ "'/leader/s/report/')]",
408
+ message="timelimit exceeded while waiting "
409
+ "for Chapter Leader portal to open"
410
+ )
411
+ # since group applications from the report page don't provide an ISOC ID
412
+ # we need it from the leader page menue
413
+ group_application_ref = self.waitfor(
414
+ EC.element_to_be_clickable,
415
+ "//a[starts-with(@href,"
416
+ "'/leader/s/isoc-group-application/')]",
417
+ message="timelimit exceeded while waiting "
418
+ "for Chapter Leader portal to open"
419
+ )
420
+
421
+ self.windows["leader"] = self.current_window_handle
422
+ self.log("Chapter Leader portal OK")
423
+ self.log(date=False)
424
+
425
+ # get lists (in an extra "reports" tab)
426
+ self.reports_link = reports_ref.get_attribute('href')
427
+ self.group_application_link = group_application_ref.get_attribute('href')
428
+ self.reports_page_ready = (EC.element_to_be_clickable,
429
+ "//table//lightning-button//button")
430
+ #
431
+ # functions to aquire data
432
+ #
433
+
434
+ def build_members_list(self) -> dict:
435
+
436
+ # we have to scrape data from 2 pages called:
437
+ # Active Chapter Members
438
+ # Active Members with Contact Info
439
+ # reason is Active Chapter Members doesn't give us the link to
440
+ # act on the list (to delete members)
441
+
442
+ self.log(date=False)
443
+ self.log("start build members list")
444
+ self.create_report_page("Members",
445
+ "Active Chapter Members")
446
+ self.load_report("Members")
447
+ members = self.get_table(self.get_members)
448
+
449
+ self.create_report_page("Member Contacts",
450
+ "Active Members with Contact Info")
451
+ self.load_report("Member Contacts")
452
+ contacts = self.get_table(self.get_member_contacts)
453
+
454
+ for k, v in members.items():
455
+ v["action link"] = contacts.get(v["email"])
456
+ self.log("members list finished")
457
+ self.log(date=False)
458
+ return members
459
+
460
+ def build_pending_applicants_list(self) -> dict:
461
+ """Collect the relevant dataabout members registered as chapters members.
462
+
463
+ Returns:
464
+ A 2 level dictionary
465
+ ISOC-ID of pending applicants: dictionary with relevant fields
466
+ """
467
+ # we have to scrape data from the page provided via the menue
468
+ # reason is the page referred to in the reports page doesn't give
469
+ # us the ISOC-ID
470
+
471
+ self.log(date=False)
472
+ self.log("start build pending applications")
473
+ self.log("Creating page for Pending Applications")
474
+ msg = "timelimit exceeded while waiting " \
475
+ "for report page for Pending Application report"
476
+ cond = (EC.presence_of_element_located,
477
+ # "table.slds-table td.cellcontainer a.forceOutputLookup")
478
+ "table")
479
+ self.activate_window("report",
480
+ url=self.group_application_link)
481
+ pendings = self.get_table(self.get_pendings)
482
+ self.log("pending application list finished")
483
+ self.log(date=False)
484
+ return pendings
485
+
486
+ def create_report_page(self, subject, button_title):
487
+ self.log("Creating page for", subject)
488
+ msg = "timelimit exceeded while waiting " \
489
+ "for report page for " + subject + " report"
490
+ self.activate_window("report",
491
+ url=self.reports_link)
492
+ elem = WebDriverWait(self, 30).until(EC.element_to_be_clickable((
493
+ By.XPATH,
494
+ "//table//lightning-button"
495
+ "//button[@title='%s']" % button_title)
496
+ ))
497
+ time.sleep(1)
498
+ self.execute_script('arguments[0].click();', elem)
499
+ self.log(subject, "page created")
500
+
501
+ def load_report(self, subject):
502
+ self.log("Loading", subject)
503
+ cond = EC.presence_of_element_located;
504
+ val = "iframe"
505
+ msg = "timelimit exceeded while waiting " \
506
+ "waiting for list of " + subject
507
+ iframe = self.waitfor(EC.presence_of_element_located, "iframe.isView",
508
+ message=msg, timeout=30, by=By.CSS_SELECTOR)
509
+
510
+ # this is so strange: this page doesnt hold all columns (fields) if
511
+ # they don't fit on the iframe. So we have to set a new (big) width
512
+ # to receive the required data
513
+ self.execute_script('arguments[0].style.width = "4000px";', iframe)
514
+
515
+ WebDriverWait(self, 5).until(
516
+ EC.frame_to_be_available_and_switch_to_it((By.CSS_SELECTOR,
517
+ "iframe.isView")))
518
+ self.waitfor(EC.presence_of_element_located, "//table//tbody//td",
519
+ message=msg)
520
+ self.log("got list of", subject)
521
+
522
+ def get_table(self, reader: callable):
523
+ # this is a wrapper for reading tables
524
+ # the reading itself is done by the reader argument
525
+ def getint(s: str) -> int:
526
+ # get integer from start of string
527
+ i = 0
528
+ for c in s:
529
+ if c.isdigit():
530
+ i += 1
531
+ else:
532
+ break
533
+ return int(s[:i])
534
+ if reader == self.get_members:
535
+ self.log('collecting the following fields: "ISOC-ID", "first name", '
536
+ '"last name", "email"')
537
+ if reader == self.get_member_contacts:
538
+ self.log('collecting the following fields: '
539
+ '"action link" (for taking actions), '
540
+ '"email" (to connect with members list)')
541
+ if reader == self.get_pendings:
542
+ self.log('collecting the following fields: "name", "email", '
543
+ '"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
+
549
+ if reader == self.get_pendings:
550
+ tableselector = "table.uiVirtualDataTable tbody tr"
551
+ total_elem = self.waitfor(
552
+ EC.presence_of_element_located,
553
+ "//force-list-view-manager-status-info/span/span",
554
+ message="timeout waiting for Metrics",
555
+ timeout=30)
556
+ else:
557
+ tableselector = "table.data-grid-full-table tbody tr"
558
+ self.waitfor(EC.presence_of_element_located,
559
+ "span.metricsAnnouncement",
560
+ by=By.CSS_SELECTOR,
561
+ message="timeout waiting for Metrics",
562
+ timeout=30)
563
+ total_selector = "div.metricsValue"
564
+ total_elem = self.find_element(By.CSS_SELECTOR, total_selector)
565
+ WebDriverWait(self, 10).until(_WaitForTextInElement(total_elem))
566
+ total = getint(total_elem.text)
567
+ self.log("Total (records expected):", total)
568
+ self.log("Waiting for Total to stabilise")
569
+ # wait a few seconds for total to become stable
570
+ time.sleep(3)
571
+ total = getint(total_elem.text)
572
+ self.log("Total (records expected):", total)
573
+ data = {}
574
+ while total > len(data):
575
+ time.sleep(3)
576
+ rows = self.find_elements(
577
+ By.CSS_SELECTOR, tableselector)
578
+ self.log("calling reader with", len(rows), "table rows, ",
579
+ "(collected records so far:", len(data),")")
580
+ scr_to = reader(rows, data)
581
+ if getint(total_elem.text) != total:
582
+ total = getint(total_elem.text)
583
+ self.log("Total was updated, now:", total)
584
+ if len(data) < total:
585
+ self.execute_script('arguments[0].scrollIntoView(true);', scr_to)
586
+ else:
587
+ self.log("records collected / total", len(data), " /", total)
588
+ return data
589
+
590
+ def get_members(self, rows, members):
591
+ for row in rows:
592
+ cells = row.find_elements(By.CSS_SELECTOR, "td")
593
+ # self.log(row.text.replace("\n"," / "))
594
+ if cells and cells[0].text and cells[0].text not in members.keys():
595
+ member = {}
596
+ member["first name"] = cells[1].text
597
+ member["last name"] = cells[2].text
598
+ member["email"] = cells[7].text
599
+ members[cells[0].text] = member
600
+ orow = row
601
+ return orow
602
+
603
+ def get_member_contacts(self, rows, members):
604
+ for row in rows:
605
+ # self.log(row.text.replace("\n"," / "))
606
+ cells = row.find_elements(By.CSS_SELECTOR, "td")
607
+ # self.log(len(cells), "cells")
608
+ if cells and \
609
+ len(cells) > 11 and \
610
+ cells[11].text and \
611
+ cells[11].text not in members.keys():
612
+ lnk = cells[1].find_element(By.CSS_SELECTOR, "a[href]"). \
613
+ get_attribute('href')
614
+ members[cells[11].text] = lnk
615
+ orow = row
616
+ return orow
617
+
618
+ def get_pendings(self, rows, pendings):
619
+ for row in rows:
620
+ cells = row.find_elements(By.CSS_SELECTOR, ".slds-cell-edit")
621
+ # self.log(row.text.replace("\n"," / "))
622
+ if cells and cells[3].text:
623
+ pending = {}
624
+ pending["name"] = cells[4].text
625
+ pending["email"] = cells[5].text
626
+ # pending["contact link"] = cells[4]. \
627
+ # find_element(By.CSS_SELECTOR, "a[href]"). \
628
+ # get_attribute('href')
629
+ pending["action link"] = cells[3]. \
630
+ find_element(By.CSS_SELECTOR, "a[href]"). \
631
+ get_attribute('href')
632
+ pending["date"] = datetime.strptime(
633
+ cells[10].text, "%m/%d/%Y")
634
+ # . strftime("%Y-%m-%d ") + " 00:00:00"
635
+ pendings[cells[6].text] = pending
636
+ orow = row
637
+ return orow
638
+
639
+ #
640
+ # operations on data
641
+ #
642
+
643
+ def deny(self, entry, reason):
644
+ time_to_wait = 100
645
+ self.log(date=False)
646
+ self.log("start denial for", entry["name"])
647
+ # operation will take place in an own tab
648
+ self.activate_window("action",
649
+ url=entry["action link"])
650
+
651
+ elem = self.waitfor(EC.element_to_be_clickable,
652
+ '//button'
653
+ '[contains(text(),'
654
+ '"Deny Applicant")]',
655
+ message="timelimit exceeded while waiting "
656
+ "waiting for details page for " +
657
+ entry["name"] + " to complete")
658
+
659
+ time.sleep(1) # for what ist worth?
660
+ self.execute_script('arguments[0].click();', elem)
661
+
662
+ d_close = WebDriverWait(self, 10, 0.3). \
663
+ until(EC.presence_of_element_located((
664
+ By.CSS_SELECTOR, 'button.slds-modal__close')))
665
+
666
+ self.log("select a reason for denial to feed AMS's couriosity")
667
+ elem = self.waitfor(EC.element_to_be_clickable,
668
+ "//div"
669
+ "[contains(concat(' ',normalize-space(@class),' '),"
670
+ "'slds-dropdown-trigger')]",
671
+ message="timelimit exceeded while waiting "
672
+ "for deny reason box")
673
+ time.sleep(1) # for what ist worth?
674
+ self.execute_script('arguments[0].click();', elem)
675
+ ###
676
+ self.log("Waiting for combobox, chose 'other'")
677
+
678
+ elem = self.waitfor(EC.element_to_be_clickable,
679
+ "//lightning-base-combobox-item"
680
+ "[@data-value='Other']",
681
+ message="timelimit exceeded while waiting "
682
+ "for deny reason 'Other'")
683
+ time.sleep(1) # for what ist worth?
684
+ self.execute_script('arguments[0].click();', elem)
685
+
686
+ elem = self.waitfor(EC.presence_of_element_located,
687
+ "//flowruntime-record-field"
688
+ "//lightning-primitive-input-simple"
689
+ "//input",
690
+ message="timelimit exceeded while waiting "
691
+ "for deny reason 'Other - Details'")
692
+ self.log(f"we'll give '{reason}' as reason")
693
+ time.sleep(1)
694
+ # elem.send_keys(reason)
695
+ self.execute_script(f'arguments[0].value="{reason}";', elem)
696
+ self.log("finally click next")
697
+
698
+ elem = self.waitfor(EC.element_to_be_clickable,
699
+ "//flowruntime-navigation-bar"
700
+ "/footer"
701
+ "//lightning-button/button",
702
+ message="timelimit exceeded while waiting "
703
+ "for 'Next' button to complete")
704
+ time.sleep(2) # for what ist worth?
705
+ self.execute_script('arguments[0].click();', elem)
706
+ try:
707
+ WebDriverWait(self, 15).until(EC.staleness_of(d_close))
708
+ except TimeoutException:
709
+ self.strong_msg("Timeout: Maybe operation was not performed")
710
+ self.log(date=False)
711
+ return False
712
+ self.log("done")
713
+ return True
714
+
715
+ def approve(self, entry):
716
+ self.log(date=False)
717
+ self.log("start approval for", entry["name"])
718
+
719
+ self.activate_window("action",
720
+ url=entry["action link"])
721
+
722
+ elem = self.waitfor(EC.presence_of_element_located,
723
+ '//button'
724
+ '[contains(text(),'
725
+ '"Approve Applicant")]',
726
+ message="timelimit exceeded while waiting "
727
+ "waiting for details page for " +
728
+ entry["name"] + " to complete")
729
+
730
+ self.log("starting with approval")
731
+ time.sleep(1) # for what ist worth?
732
+ self.execute_script('arguments[0].click();', elem)
733
+
734
+ d_close = WebDriverWait(self, 10, 0.3). \
735
+ until(EC.presence_of_element_located((
736
+ By.CSS_SELECTOR, 'button.slds-modal__close')))
737
+
738
+ self.log("finally click next")
739
+ elem = self.waitfor(EC.element_to_be_clickable,
740
+ "//flowruntime-navigation-bar"
741
+ "/footer"
742
+ "//lightning-button/button",
743
+ message="timelimit exceeded while waiting "
744
+ "for 'Next' button to complete")
745
+ time.sleep(1) # for what ist worth?
746
+ self.execute_script('arguments[0].click();', elem)
747
+
748
+ try:
749
+ WebDriverWait(self, 15).until(EC.staleness_of(d_close))
750
+ except TimeoutException:
751
+ self.strong_msg("Timeout: Maybe operation was not performed")
752
+ self.log(date=False)
753
+ return False
754
+ self.log("done")
755
+ return True
756
+
757
+
758
+ def delete(self, entry):
759
+ self.log(date=False)
760
+ name = entry["first name"] + " " + entry["last name"]
761
+ self.log("start delete", name, "from AMS Chapter members list" )
762
+
763
+ self.activate_window("action",
764
+ url=entry["action link"])
765
+
766
+ elem = self.waitfor(EC.element_to_be_clickable,
767
+ "//runtime_platform_actions-action-renderer"
768
+ "[@title='Terminate']"
769
+ "//button",
770
+ message="timelimit exceeded while waiting "
771
+ "waiting for details page for " +
772
+ name + " to complete")
773
+
774
+ time.sleep(1) # for what ist worth?
775
+ self.execute_script('arguments[0].click();', elem)
776
+
777
+ d_close = WebDriverWait(self, 10, 0.3). \
778
+ until(EC.presence_of_element_located((
779
+ By.CSS_SELECTOR, 'button.slds-modal__close')))
780
+
781
+ try:
782
+ WebDriverWait(self, 15).until(EC.staleness_of(d_close))
783
+ except TimeoutException:
784
+ self.strong_msg("Timeout: Maybe operation was not performed")
785
+ self.log(date=False)
786
+ return False
787
+ self.log("done")
788
+ return True
789
+
790
+
791
+ if __name__ == "__main__":
792
+ from getpass import getpass
793
+ headless = True
794
+ if "-h" in sys.argv:
795
+ headless=False
796
+ print("Username", end=":")
797
+ user_id = input()
798
+ password = getpass()
799
+ ams = ISOC_AMS(
800
+ user_id,
801
+ password,
802
+ headless=headless)
803
+ members = ams.members_list
804
+ pendings = ams.pending_applications_list
805
+
806
+ print("\nMEMBERS")
807
+ i = 0
808
+ for k, v in members.items():
809
+ i += 1
810
+ print(i, k + ":", v["first name"], v["last name"], v["email"], v["action link"])
811
+
812
+ print("\nPENDING APPLICATIONS")
813
+ i = 0
814
+ for k, v in pendings.items():
815
+ i += 1
816
+ # print(i, k, v)
817
+ print(i, k, v["name"], v["email"], v["action link"],
818
+ v["date"].isoformat()[:19])