isoc-ams 0.0.1__tar.gz
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-0.0.1/LICENSE +21 -0
- isoc_ams-0.0.1/PKG-INFO +186 -0
- isoc_ams-0.0.1/README.md +173 -0
- isoc_ams-0.0.1/isoc_ams.py +818 -0
- isoc_ams-0.0.1/pyproject.toml +26 -0
isoc_ams-0.0.1/LICENSE
ADDED
|
@@ -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-0.0.1/PKG-INFO
ADDED
|
@@ -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
|
+
|
isoc_ams-0.0.1/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
|
|
2
|
+
# isoc-ams
|
|
3
|
+
|
|
4
|
+
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.
|
|
5
|
+
|
|
6
|
+
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.
|
|
7
|
+
|
|
8
|
+
Unfortunately the constraints are severe:
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
- tests are close to impossible. There is no such thing as a TEST AMS.
|
|
12
|
+
|
|
13
|
+
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!
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
ISOC maintains two main Lists that are relevant for the operation of this interface:
|
|
17
|
+
- a list of ISOC members registered as members of the Chapter
|
|
18
|
+
- a list of ISOC members that applied for a Chapter membership.
|
|
19
|
+
|
|
20
|
+
Consequently isoc-ams provides methods for the following tasks:
|
|
21
|
+
1. read list of ISOC members registered as Chapter members
|
|
22
|
+
1. read list of ISOC members that applied for a Chapter membership
|
|
23
|
+
1. approve ISOC AMS applications
|
|
24
|
+
1. deny ISOC AMS applications
|
|
25
|
+
1. delete members from ISOC AMS Chapters Member list
|
|
26
|
+
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.)
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
So here we go:
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Install isoc-ams with pip.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python -m pip install -U isoc-ams
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Best would be to use a virtual environment (venv).
|
|
41
|
+
|
|
42
|
+
## Running isoc_ams
|
|
43
|
+
|
|
44
|
+
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.:
|
|
45
|
+
```bash
|
|
46
|
+
ISOC_AMS_WEBDRIVER=firefox
|
|
47
|
+
```
|
|
48
|
+
Recommended (and default) is "firefox".
|
|
49
|
+
|
|
50
|
+
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.
|
|
51
|
+
by providing a logfile (goes to stdout by default).
|
|
52
|
+
|
|
53
|
+
So this happens if we call the module with:
|
|
54
|
+
```bash
|
|
55
|
+
python -m isoc_ams
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
Username: xxx
|
|
60
|
+
Password:
|
|
61
|
+
|
|
62
|
+
AMS 2025-07-03 10:49:07 logging in
|
|
63
|
+
AMS 2025-07-03 10:49:11 log in started
|
|
64
|
+
AMS 2025-07-03 10:49:20 now on community portal
|
|
65
|
+
AMS 2025-07-03 10:49:25 waiting for Chapter Leader portal
|
|
66
|
+
AMS 2025-07-03 10:49:27 Chapter Leader portal OK
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
AMS 2025-07-03 10:49:27 start build members list
|
|
70
|
+
AMS 2025-07-03 10:49:27 Creating page for Members
|
|
71
|
+
AMS 2025-07-03 10:49:33 Members page created
|
|
72
|
+
AMS 2025-07-03 10:49:33 Loading Members
|
|
73
|
+
AMS 2025-07-03 10:49:39 got list of Members
|
|
74
|
+
AMS 2025-07-03 10:49:39 collecting the following fields: "ISOC-ID", "first name", "last name", "email"
|
|
75
|
+
AMS 2025-07-03 10:49:39 Total (records expected): 38
|
|
76
|
+
AMS 2025-07-03 10:49:39 Waiting for Total to stabilise
|
|
77
|
+
AMS 2025-07-03 10:49:42 Total (records expected): 59
|
|
78
|
+
AMS 2025-07-03 10:49:45 calling reader with 31 table rows, (collected records so far: 0 )
|
|
79
|
+
AMS 2025-07-03 10:49:50 calling reader with 32 table rows, (collected records so far: 29 )
|
|
80
|
+
AMS 2025-07-03 10:49:54 calling reader with 24 table rows, (collected records so far: 53 )
|
|
81
|
+
AMS 2025-07-03 10:49:55 records collected / total 59 / 59
|
|
82
|
+
AMS 2025-07-03 10:49:55 Creating page for Member Contacts
|
|
83
|
+
AMS 2025-07-03 10:50:00 Member Contacts page created
|
|
84
|
+
AMS 2025-07-03 10:50:00 Loading Member Contacts
|
|
85
|
+
AMS 2025-07-03 10:50:04 got list of Member Contacts
|
|
86
|
+
AMS 2025-07-03 10:50:04 collecting the following fields: "action link" (for taking actions), "email" (to connect with members list)
|
|
87
|
+
AMS 2025-07-03 10:50:04 Total (records expected): 8
|
|
88
|
+
AMS 2025-07-03 10:50:04 Waiting for Total to stabilise
|
|
89
|
+
AMS 2025-07-03 10:50:07 Total (records expected): 59
|
|
90
|
+
AMS 2025-07-03 10:50:10 calling reader with 30 table rows, (collected records so far: 0 )
|
|
91
|
+
AMS 2025-07-03 10:50:14 calling reader with 31 table rows, (collected records so far: 28 )
|
|
92
|
+
AMS 2025-07-03 10:50:18 calling reader with 25 table rows, (collected records so far: 51 )
|
|
93
|
+
AMS 2025-07-03 10:50:18 records collected / total 59 / 59
|
|
94
|
+
AMS 2025-07-03 10:50:18 members list finished
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
AMS 2025-07-03 10:50:18 start build pending applications
|
|
98
|
+
AMS 2025-07-03 10:50:18 Creating page for Pending Applications
|
|
99
|
+
AMS 2025-07-03 10:50:22 collecting the following fields: "name", "email", "action link", "date"
|
|
100
|
+
AMS 2025-07-03 10:50:24 Total (records expected): 8
|
|
101
|
+
AMS 2025-07-03 10:50:24 Waiting for Total to stabilise
|
|
102
|
+
AMS 2025-07-03 10:50:27 Total (records expected): 8
|
|
103
|
+
AMS 2025-07-03 10:50:30 calling reader with 8 table rows, (collected records so far: 0 )
|
|
104
|
+
AMS 2025-07-03 10:50:31 records collected / total 8 / 8
|
|
105
|
+
AMS 2025-07-03 10:50:31 pending application list finished
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
MEMBERS
|
|
109
|
+
1 ...
|
|
110
|
+
2 ...
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
PENDING APPLICATIONS
|
|
114
|
+
1 ...
|
|
115
|
+
2 ...
|
|
116
|
+
...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
## Using the API
|
|
122
|
+
|
|
123
|
+
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.
|
|
124
|
+
|
|
125
|
+
Here an excerpt of the output:
|
|
126
|
+
```
|
|
127
|
+
Pending Applications:
|
|
128
|
+
|
|
129
|
+
the following pending applications will be approved:
|
|
130
|
+
...
|
|
131
|
+
the following pending applications will be denied:
|
|
132
|
+
...
|
|
133
|
+
the following pending applications will be invited:
|
|
134
|
+
...
|
|
135
|
+
the following pending applications will be waiting:
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
Members:
|
|
139
|
+
the following members will be deleted from AMS:
|
|
140
|
+
...
|
|
141
|
+
for the following members a nagging mail will be sent to AMS-support (we are not authorized to fix it!):
|
|
142
|
+
...
|
|
143
|
+
the following members are in sync
|
|
144
|
+
...
|
|
145
|
+
|
|
146
|
+
AMS 2025-07-03 12:00:32 start delete ... from AMS Chapter members list
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
Dear MAS-support team,
|
|
150
|
+
this is an automatic, complimentary Message from the ISOC German Chapter
|
|
151
|
+
Members Administration System (ISOC.DE MAS).
|
|
152
|
+
|
|
153
|
+
The following individuals are legally registered paying members
|
|
154
|
+
of ISOC.DE - many of them for more than 25 years.
|
|
155
|
+
...
|
|
156
|
+
Uwe Mayer, xxx@yyy.com (ISOC-ID=1234567)
|
|
157
|
+
...
|
|
158
|
+
|
|
159
|
+
Thank you,
|
|
160
|
+
Your ISOC.DE MAS support team
|
|
161
|
+
...
|
|
162
|
+
|
|
163
|
+
DEVIATIONS FROM EXPECTED RESULTS
|
|
164
|
+
not deleted from members
|
|
165
|
+
...
|
|
166
|
+
not approved from pending applicants list
|
|
167
|
+
...
|
|
168
|
+
not removed from pending applicants list
|
|
169
|
+
...
|
|
170
|
+
```
|
|
171
|
+
See file isoc_ams.doc for doc on the API interface.
|
|
172
|
+
|
|
173
|
+
Have fun!
|
|
@@ -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])
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.2,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "isoc-ams"
|
|
7
|
+
dependencies = ["selenium>4"]
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Klaus Birkenbihl", email = "klaus.birkenbihl@isoc.de"}
|
|
10
|
+
]
|
|
11
|
+
maintainers = [
|
|
12
|
+
{name = "Klaus Birkenbihl"},
|
|
13
|
+
]
|
|
14
|
+
description = "A Python 3 module to cope with ISOC-AMS."
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
license = {file = "LICENSE"}
|
|
17
|
+
classifiers = [
|
|
18
|
+
"License :: OSI Approved :: MIT License"
|
|
19
|
+
]
|
|
20
|
+
dynamic = ["version"]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Home = "https://github.com/birkenbihl/isoc-ams"
|
|
24
|
+
|
|
25
|
+
[tool.flit.module]
|
|
26
|
+
name = "isoc_ams"
|