forked from CMS/memberPlumbing
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
66853e1156 | |||
363be0ba8c | |||
d8b3958c87 | |||
68b4b10c51 | |||
88b2610513 | |||
3f17cd9ec2 | |||
5c53f7f88c | |||
97c4dbc1ee | |||
3595a24d85 | |||
c1430e2f9a | |||
a5a787e0f7 | |||
6b7194c15a | |||
855f9b652d | |||
69bcb71091 | |||
63bd8efaf2 | |||
af6ecb2864 | |||
ce2a0f4c1d | |||
995b6f9763 | |||
f94a27699c | |||
34539eb630 | |||
3849aca918 | |||
cfccc433dd | |||
a2dd00f414 | |||
5a39c5cae9 | |||
7a22f43ccf |
@ -4,7 +4,7 @@ This repo contains a set of scripts to sync data around for the Claremont MakerS
|
||||
|
||||
## Setup
|
||||
|
||||
This project uses [Poetry](https://python-poetry.org/) for dependency management. Typical usage is first running `poetry install` to create a virtualenv and install dependencies, then running `poetry run bin/<script>` to start a specific script.
|
||||
This project uses [Poetry](https://python-poetry.org/) for dependency management. Typical usage is first running `poetry install` to create a virtualenv and install dependencies, then running `poetry run <script>` to start a specific script.
|
||||
|
||||
## Config
|
||||
|
||||
@ -12,7 +12,7 @@ Many of the scripts use data from a `config.yaml` in the current working directo
|
||||
|
||||
## Scripts
|
||||
|
||||
The primary entry points have scripts in [`bin`](./bin/). They assume that memberPlumbing is in `PYTHONPATH`, which can either be done with `poetry` or manually set.
|
||||
The primary entry points have scripts entries (`tool.poetry.scripts`) in [`pyproject.toml`](./pyproject.toml). They assume that they are being run from a module, so must be run with `poetry run <script>` or `python -m memberPlumbing.<script>`.
|
||||
|
||||
### `doorUpdater`
|
||||
|
||||
@ -26,6 +26,10 @@ Retrieves member information from MembershipWorks and pushes it out to [UCS](htt
|
||||
|
||||
Retrieves account and transaction information from MembershipWorks, and pushes it to a MariaDB database for use in other projects. Schemas are defined with [peewee](peewee-orm.com) in [`memberPlumbing/mw_models.py`](./memberPlumbing/mw_models.py).
|
||||
|
||||
### `upcomingEvents`
|
||||
|
||||
Retrieves upcoming events from MembershipWorks and formats them for a WordPress post.
|
||||
|
||||
### `hidEvents`
|
||||
|
||||
Retrieves events from the HID Evo Solo door controllers, and pushes them to a SQL database.
|
||||
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from memberPlumbing import doorUpdater
|
||||
|
||||
doorUpdater.main()
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from memberPlumbing import hidEvents
|
||||
|
||||
hidEvents.main()
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from memberPlumbing import sqlExport
|
||||
|
||||
sqlExport.main()
|
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from memberPlumbing import ucsAccounts
|
||||
|
||||
ucsAccounts.main()
|
@ -34,6 +34,12 @@ DOOR_PASSWORD: ""
|
||||
MEMBERSHIPWORKS_USERNAME: ""
|
||||
MEMBERSHIPWORKS_PASSWORD: ""
|
||||
|
||||
# arguments for https://udm-rest-client.readthedocs.io/en/latest/udm_rest_client.html#udm_rest_client.udm.UDM
|
||||
UCS:
|
||||
url: ""
|
||||
username: ""
|
||||
password: ""
|
||||
|
||||
MEMBERSHIPWORKS_DB:
|
||||
database: ""
|
||||
user: ""
|
||||
|
@ -2,8 +2,9 @@ import csv
|
||||
from io import StringIO
|
||||
|
||||
import requests
|
||||
import datetime
|
||||
|
||||
BASE_URL = "https://api.membershipworks.com/v1/"
|
||||
BASE_URL = "https://api.membershipworks.com"
|
||||
|
||||
# extracted from `SF._is.crm` in https://cdn.membershipworks.com/all.js
|
||||
CRM = {
|
||||
@ -72,21 +73,30 @@ class MembershipWorksRemoteError(Exception):
|
||||
|
||||
class MembershipWorks:
|
||||
def __init__(self):
|
||||
self.sess = requests.Session()
|
||||
self.org_info = None
|
||||
self.auth_token = None
|
||||
self.org_num = None
|
||||
|
||||
def login(self, username, password):
|
||||
"""Authenticate against the membershipworks api"""
|
||||
r = requests.post(
|
||||
BASE_URL + "usr",
|
||||
data={"_st": "all", "eml": username, "org": "10000", "pwd": password},
|
||||
r = self.sess.post(
|
||||
BASE_URL + "/v2/account/session",
|
||||
data={"eml": username, "pwd": password},
|
||||
headers={"X-Org": "10000"},
|
||||
)
|
||||
if r.status_code != 200 or "SF" not in r.json():
|
||||
raise MembershipWorksRemoteError("login", r)
|
||||
self.org_info = r.json()
|
||||
self.auth_token = self.org_info["SF"]
|
||||
self.org_num = self.org_info["org"]
|
||||
self.sess.headers.update(
|
||||
{
|
||||
"X-Org": str(self.org_num),
|
||||
"X-Role": "admin",
|
||||
"Authorization": "Bearer " + self.auth_token,
|
||||
}
|
||||
)
|
||||
|
||||
def _inject_auth(self, kwargs):
|
||||
# TODO: should probably be a decorator or something
|
||||
@ -97,12 +107,12 @@ class MembershipWorks:
|
||||
kwargs["params"] = {}
|
||||
kwargs["params"]["SF"] = self.auth_token
|
||||
|
||||
def _get(self, *args, **kwargs):
|
||||
def _get_v1(self, *args, **kwargs):
|
||||
self._inject_auth(kwargs)
|
||||
# TODO: should probably do some error handling in here
|
||||
return requests.get(*args, **kwargs)
|
||||
|
||||
def _post(self, *args, **kwargs):
|
||||
def _post_v1(self, *args, **kwargs):
|
||||
self._inject_auth(kwargs)
|
||||
# TODO: should probably do some error handling in here
|
||||
return requests.post(*args, **kwargs)
|
||||
@ -141,46 +151,44 @@ class MembershipWorks:
|
||||
for dek in self.org_info["dek"]:
|
||||
# TODO: there must be a better way. this is stupid
|
||||
if dek["dek"] == 1:
|
||||
ret["folders"][dek["lbl"]] = dek["_id"]
|
||||
ret["folders"][dek["lbl"]] = dek["did"]
|
||||
elif "cur" in dek:
|
||||
ret["levels"][dek["lbl"]] = dek["_id"]
|
||||
ret["levels"][dek["lbl"]] = dek["did"]
|
||||
elif "mux" in dek:
|
||||
ret["addons"][dek["lbl"]] = dek["_id"]
|
||||
ret["addons"][dek["lbl"]] = dek["did"]
|
||||
else:
|
||||
ret["labels"][dek["lbl"]] = dek["_id"]
|
||||
ret["labels"][dek["lbl"]] = dek["did"]
|
||||
|
||||
return ret
|
||||
|
||||
def get_member_ids(self, folders):
|
||||
folder_map = self._parse_flags()["folders"]
|
||||
|
||||
r = self._get(
|
||||
BASE_URL + "ylp",
|
||||
params={
|
||||
"lbl": ",".join([folder_map[f] for f in folders]),
|
||||
"org": self.org_num,
|
||||
"var": "_id,nam,ctc",
|
||||
},
|
||||
r = self.sess.get(
|
||||
BASE_URL + "/v2/accounts",
|
||||
params={"dek": ",".join([folder_map[f] for f in folders])},
|
||||
)
|
||||
if r.status_code != 200 or "usr" not in r.json():
|
||||
raise MembershipWorksRemoteError("user listing", r)
|
||||
|
||||
# get list of member ID matching the search
|
||||
return [user["uid"] for user in r.json()["usr"]]
|
||||
# dedup with set() to work around people with alt uids
|
||||
# TODO: figure out why people have alt uids
|
||||
return set(user["uid"] for user in r.json()["usr"])
|
||||
|
||||
# TODO: has issues with aliasing header names:
|
||||
# ex: "Personal Studio Space" Label vs Membership Addon/Field
|
||||
def get_members(self, folders, columns):
|
||||
""" Pull the members csv from the membershipworks api
|
||||
folders: a list of the names of the folders to get
|
||||
(see folder_map in this function for mapping to ids)
|
||||
columns: which columns to get"""
|
||||
"""Pull the members csv from the membershipworks api
|
||||
folders: a list of the names of the folders to get
|
||||
(see folder_map in this function for mapping to ids)
|
||||
columns: which columns to get"""
|
||||
ids = self.get_member_ids(folders)
|
||||
|
||||
# get members CSV
|
||||
# TODO: maybe can just use previous get instead? would return JSON
|
||||
r = self._post(
|
||||
BASE_URL + "csv",
|
||||
r = self._post_v1(
|
||||
BASE_URL + "/v1/csv",
|
||||
data={
|
||||
"_rt": "946702800", # unknown
|
||||
"mux": "", # unknown
|
||||
@ -190,20 +198,24 @@ class MembershipWorks:
|
||||
)
|
||||
if r.status_code != 200:
|
||||
raise MembershipWorksRemoteError("csv generation", r)
|
||||
|
||||
if r.text[0] == "\ufeff":
|
||||
r.encoding = r.encoding + "-sig"
|
||||
|
||||
return list(csv.DictReader(StringIO(r.text)))
|
||||
|
||||
def get_transactions(self, start_date, end_date, json=False):
|
||||
"""Get the transactions between start_date and end_date
|
||||
|
||||
Dates can be datetime.date or datetime.datetime
|
||||
Dates can be datetime.date or datetime.datetime
|
||||
|
||||
json gets a different version of the transactions list,
|
||||
which contains a different set information
|
||||
json gets a different version of the transactions list,
|
||||
which contains a different set information
|
||||
"""
|
||||
r = self._get(
|
||||
BASE_URL + "csv",
|
||||
r = self._get_v1(
|
||||
BASE_URL + "/v1/csv",
|
||||
params={
|
||||
"crm": "12,13,14,18,19", # transaction types, see CRM
|
||||
"crm": ",".join(str(k) for k in CRM.keys()),
|
||||
**({"txl": ""} if json else {}),
|
||||
"sdp": start_date.strftime("%s"),
|
||||
"edp": end_date.strftime("%s"),
|
||||
@ -214,6 +226,9 @@ class MembershipWorks:
|
||||
if json:
|
||||
return r.json()
|
||||
else:
|
||||
if r.text[0] == "\ufeff":
|
||||
r.encoding = r.encoding + "-sig"
|
||||
|
||||
return list(csv.DictReader(StringIO(r.text)))
|
||||
|
||||
def get_all_members(self):
|
||||
@ -222,3 +237,29 @@ class MembershipWorks:
|
||||
fields = self._all_fields()
|
||||
members = self.get_members(folders, ",".join(fields.keys()))
|
||||
return members
|
||||
|
||||
def get_events_list(self, start_date: datetime.datetime):
|
||||
"""Retrive a list of events since start_date"""
|
||||
r = self.sess.get(
|
||||
BASE_URL + "/v2/events",
|
||||
params={
|
||||
"sdp": start_date.strftime("%s"),
|
||||
},
|
||||
)
|
||||
return r.json()
|
||||
|
||||
def get_event_by_eid(self, eid: str):
|
||||
"""Retrieve a specific event by its event id (eid)"""
|
||||
r = self.sess.get(
|
||||
BASE_URL + "/v2/event",
|
||||
params={"eid": eid},
|
||||
)
|
||||
return r.json()
|
||||
|
||||
def get_event_by_url(self, url: str):
|
||||
"""Retrieve a specific event by its url"""
|
||||
r = self.sess.get(
|
||||
BASE_URL + "/v2/event",
|
||||
params={"url": url},
|
||||
)
|
||||
return r.json()
|
||||
|
@ -72,15 +72,9 @@ class MembershipworksMember(Member):
|
||||
else:
|
||||
self.credentials = set()
|
||||
|
||||
self.onHold = data["Account on Hold"] != ""
|
||||
self.limitedOperations = (
|
||||
data[
|
||||
"Access Permitted Using Membership Level Schedule During COVID-19 Limited Operations"
|
||||
]
|
||||
== "Y"
|
||||
)
|
||||
self.staffedLimitedOperations = (
|
||||
data["Access Permitted During COVID-19 Staffed Period Only"] == "Y"
|
||||
self.onHold = (
|
||||
data["Account on Hold"] != ""
|
||||
or data["CMS Membership on hold"] == "CMS Membership on hold"
|
||||
)
|
||||
self.formerMember = formerMember
|
||||
|
||||
@ -106,13 +100,7 @@ class MembershipworksMember(Member):
|
||||
|
||||
schedules = []
|
||||
if door.name in self.doorAccess and not self.onHold and not self.formerMember:
|
||||
# members should get their normal schedules
|
||||
if self.limitedOperations or "CMS Staff" in self.levels:
|
||||
schedules = self.schedules + doorLevels
|
||||
|
||||
# members should get only the staffed hours schedule
|
||||
if self.staffedLimitedOperations:
|
||||
schedules += ["Staffed Hours"]
|
||||
schedules = self.schedules + doorLevels
|
||||
|
||||
dm = DoorMember(
|
||||
door,
|
||||
@ -133,7 +121,6 @@ class MembershipworksMember(Member):
|
||||
return (
|
||||
super().__str__()
|
||||
+ f"""OnHold? {self.onHold}
|
||||
Limited Operations Access? {self.limitedOperations}
|
||||
Former Member? {self.formerMember}
|
||||
"""
|
||||
)
|
||||
@ -342,7 +329,7 @@ def main():
|
||||
config = Config()
|
||||
membershipworks = config.membershipworks
|
||||
membershipworks_attributes = (
|
||||
"_id,nam,phn,eml,lvl,lbl,xws,xms,xsc,xas,xfd,xac,xcf,xeh,xse,xlo,xxc"
|
||||
"_id,nam,phn,eml,lvl,lbl,xws,xms,xsc,xas,xfd,xac,xcf,xeh,xse"
|
||||
)
|
||||
|
||||
memberData = membershipworks.get_members(
|
||||
|
@ -97,7 +97,11 @@ def main():
|
||||
config = Config()
|
||||
database.init(
|
||||
**config.HID_DB,
|
||||
**{"charset": "utf8", "sql_mode": "PIPES_AS_CONCAT", "use_unicode": True,}
|
||||
**{
|
||||
"charset": "utf8",
|
||||
"sql_mode": "PIPES_AS_CONCAT",
|
||||
"use_unicode": True,
|
||||
}
|
||||
)
|
||||
HIDEvent.create_table()
|
||||
for door in config.doors.values():
|
||||
|
@ -22,6 +22,12 @@ def do_import(config):
|
||||
]
|
||||
).on_conflict(action="update", preserve=[Flag.name, Flag.type]).execute()
|
||||
|
||||
print("Getting folder membership...")
|
||||
folders = {
|
||||
folder_id: membershipworks.get_member_ids([folder_name])
|
||||
for folder_name, folder_id in membershipworks._parse_flags()["folders"].items()
|
||||
}
|
||||
|
||||
print("Getting/Updating members...")
|
||||
members = membershipworks.get_all_members()
|
||||
for m in members:
|
||||
@ -41,13 +47,14 @@ def do_import(config):
|
||||
|
||||
# update member's flags
|
||||
for type, flags in membershipworks._parse_flags().items():
|
||||
if type != "folders": # currently no way to retrieve this info
|
||||
for flag, id in flags.items():
|
||||
ml = MemberFlag(uid=member["Account ID"], flag_id=id)
|
||||
if member[flag]:
|
||||
ml.magic_save()
|
||||
else:
|
||||
ml.delete_instance()
|
||||
for flag, id in flags.items():
|
||||
ml = MemberFlag(uid=member["Account ID"], flag_id=id)
|
||||
if (type == "folders" and member["Account ID"] in folders[id]) or (
|
||||
type != "folders" and member[flag]
|
||||
):
|
||||
ml.magic_save()
|
||||
else:
|
||||
ml.delete_instance()
|
||||
|
||||
print("Getting/Updating transactions...")
|
||||
# Deduping these is hard, so just recreate the data every time
|
||||
@ -75,7 +82,11 @@ def main():
|
||||
config = Config()
|
||||
database.init(
|
||||
**config.MEMBERSHIPWORKS_DB,
|
||||
**{"charset": "utf8", "sql_mode": "PIPES_AS_CONCAT", "use_unicode": True,}
|
||||
**{
|
||||
"charset": "utf8",
|
||||
"sql_mode": "PIPES_AS_CONCAT",
|
||||
"use_unicode": True,
|
||||
}
|
||||
)
|
||||
|
||||
do_import(config)
|
||||
|
@ -1,106 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import subprocess
|
||||
|
||||
from udm_rest_client.udm import UDM
|
||||
from udm_rest_client.exceptions import NoObject, UdmError
|
||||
|
||||
from .config import Config
|
||||
|
||||
LDAP_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||
USER_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||
GROUP_BASE = "cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||
GROUPS_REGEX = "|".join(["Certified: .*", "Access .*\?", "CMS .*", "Volunteer: .*"])
|
||||
GROUPS_REGEX = "|".join(
|
||||
["Certified: .*", "Access .*\?", "CMS .*", "Volunteer: .*", "Database .*"]
|
||||
)
|
||||
RAND_PW_LEN = 20
|
||||
|
||||
|
||||
def makeGroups(members):
|
||||
# From an API error message:
|
||||
# A group name must start and end with a letter, number or underscore.
|
||||
# In between additionally spaces, dashes and dots are allowed.
|
||||
def sanitize_group_name(name):
|
||||
sanitized_body = re.sub(r"[^0-9A-Za-z_ -.]", ".", name)
|
||||
sanitized_start_end = re.sub("^[^0-9A-Za-z_]|[^0-9A-Za-z_]$", "_", sanitized_body)
|
||||
|
||||
return "MW_" + sanitized_start_end
|
||||
|
||||
|
||||
# From an API error message: "Username must only contain numbers, letters and dots!"
|
||||
def sanitize_user_name(name):
|
||||
return re.sub(r"[^0-9a-z.]", ".", name.lower()).strip(".")
|
||||
|
||||
|
||||
async def make_groups(group_mod, members):
|
||||
existing_group_names = [g.props.name async for g in group_mod.search()]
|
||||
|
||||
groups = [
|
||||
key.replace(":", ".").replace("?", "")
|
||||
for key in members[0].keys()
|
||||
if re.match(GROUPS_REGEX, key) is not None
|
||||
sanitize_group_name(group_name)
|
||||
for group_name in members[0].keys()
|
||||
if re.match(GROUPS_REGEX, group_name) is not None
|
||||
]
|
||||
for group in groups:
|
||||
subprocess.call(
|
||||
[
|
||||
"udm",
|
||||
"groups/group",
|
||||
"create",
|
||||
"--position",
|
||||
GROUP_BASE,
|
||||
"--set",
|
||||
"name=" + group,
|
||||
]
|
||||
)
|
||||
for group_name in groups:
|
||||
if group_name not in existing_group_names:
|
||||
group = await group_mod.new()
|
||||
group.props.name = group_name
|
||||
await group.save()
|
||||
|
||||
|
||||
def makeSets(props):
|
||||
return sum([["--set", key + "=" + value] for key, value in props.items()], [])
|
||||
|
||||
|
||||
def makeAppendGroups(member):
|
||||
groups = [
|
||||
key.replace(":", ".").replace("?", "")
|
||||
for key, value in member.items()
|
||||
if re.match(GROUPS_REGEX, key) is not None and value != ""
|
||||
]
|
||||
return sum(
|
||||
[["--append", "groups=cn=" + group + "," + GROUP_BASE] for group in groups], []
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
async def _main():
|
||||
config = Config()
|
||||
|
||||
members = config.membershipworks.get_members(
|
||||
["Members", "CMS Staff"], "lvl,phn,eml,lbl,nam,end,_id"
|
||||
)
|
||||
makeGroups(members)
|
||||
|
||||
for member in members:
|
||||
randomPass = "".join(
|
||||
random.choice(string.ascii_letters + string.digits)
|
||||
for x in range(0, RAND_PW_LEN)
|
||||
)
|
||||
username = member["Account Name"].lower().replace(" ", ".")
|
||||
async with UDM(**config.UCS) as udm:
|
||||
user_mod = udm.get("users/user")
|
||||
group_mod = udm.get("groups/group")
|
||||
|
||||
props = {
|
||||
"title": "", # Title
|
||||
"firstname": member["First Name"],
|
||||
"lastname": member["Last Name"], # (c)
|
||||
"username": username, # (cmr)
|
||||
"description": "", # Description
|
||||
"password": randomPass, # (c) Password
|
||||
# "mailPrimaryAddress": member["Email"], # Primary e-mail address
|
||||
# "displayName": "", # Display name
|
||||
# "birthday": "", # Birthdate
|
||||
# "jpegPhoto": "", # Picture of the user (JPEG format)
|
||||
"employeeNumber": member["Account ID"],
|
||||
# "employeeType": "", # Employee type
|
||||
"homedrive": "H:", # Windows home drive
|
||||
"sambahome": "\\\\ucs\\" + username, # Windows home path
|
||||
"profilepath": "%LOGONSERVER%\\%USERNAME%\\windows-profiles\\default", # Windows profile directory
|
||||
"disabled": "1" if member["Account on Hold"] != "" else "0",
|
||||
# "userexpiry": member["Renewal Date"],
|
||||
"pwdChangeNextLogin": "1", # User has to change password on next login
|
||||
# "sambaLogonHours": "", # Permitted times for Windows logins
|
||||
"e-mail": member["Email"], # ([]) E-mail address
|
||||
"phone": member["Phone"], # Telephone number
|
||||
# "PasswordRecoveryMobile": member["Phone"], # Mobile phone number
|
||||
"PasswordRecoveryEmail": member["Email"],
|
||||
}
|
||||
await make_groups(group_mod, members)
|
||||
|
||||
subprocess.call(
|
||||
["udm", "users/user", "create", "--position", LDAP_BASE] + makeSets(props)
|
||||
)
|
||||
for member in members:
|
||||
username = sanitize_user_name(member["Account Name"])
|
||||
|
||||
# remove props we don't want to reset
|
||||
props.pop("password")
|
||||
props.pop("pwdChangeNextLogin")
|
||||
try: # try to get an existing user to update
|
||||
user = await user_mod.get(f"uid={username},{USER_BASE}")
|
||||
except NoObject: # create a new user
|
||||
# TODO: search by employeeNumber and rename users when needed
|
||||
user = await user_mod.new()
|
||||
|
||||
subprocess.call(
|
||||
["udm", "users/user", "modify", "--dn", "uid=" + username + "," + LDAP_BASE]
|
||||
+ makeSets(props)
|
||||
+ makeAppendGroups(member)
|
||||
)
|
||||
# set a random password and ensure it is changed at next login
|
||||
user.props.password = "".join(
|
||||
random.choice(string.ascii_letters + string.digits)
|
||||
for x in range(0, RAND_PW_LEN)
|
||||
)
|
||||
user.props.pwdChangeNextLogin = True
|
||||
|
||||
user.props.update(
|
||||
{
|
||||
"title": "", # Title
|
||||
"firstname": member["First Name"],
|
||||
"lastname": member["Last Name"], # (c)
|
||||
"username": username, # (cmr)
|
||||
"description": "", # Description
|
||||
# "password": "", # (c) Password
|
||||
# "mailPrimaryAddress": member["Email"], # Primary e-mail address
|
||||
# "displayName": "", # Display name
|
||||
# "birthday": "", # Birthdate
|
||||
# "jpegPhoto": "", # Picture of the user (JPEG format)
|
||||
"employeeNumber": member["Account ID"],
|
||||
# "employeeType": "", # Employee type
|
||||
"homedrive": "H:", # Windows home drive
|
||||
"sambahome": "\\\\ucs\\" + username, # Windows home path
|
||||
"profilepath": "%LOGONSERVER%\\%USERNAME%\\windows-profiles\\default", # Windows profile directory
|
||||
"disabled": member["Account on Hold"] != "",
|
||||
# "userexpiry": member["Renewal Date"],
|
||||
# "pwdChangeNextLogin": "1", # User has to change password on next login
|
||||
# "sambaLogonHours": "", # Permitted times for Windows logins
|
||||
"e-mail": [member["Email"]], # ([]) E-mail address
|
||||
"phone": [member["Phone"]], # Telephone number
|
||||
# "PasswordRecoveryMobile": member["Phone"], # Mobile phone number
|
||||
"PasswordRecoveryEmail": member["Email"],
|
||||
}
|
||||
)
|
||||
|
||||
new_groups = [
|
||||
"cn=" + sanitize_group_name(group) + "," + GROUP_BASE
|
||||
for group, value in member.items()
|
||||
if re.match(GROUPS_REGEX, group) is not None and value != ""
|
||||
]
|
||||
# groups not from this script
|
||||
other_old_groups = [
|
||||
g for g in user.props.groups if not g[3:].startswith("MW_")
|
||||
]
|
||||
user.props.groups = other_old_groups + new_groups
|
||||
|
||||
try:
|
||||
await user.save()
|
||||
except UdmError:
|
||||
print("Failed to save user", username)
|
||||
print(user.props)
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
asyncio.run(_main())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
76
memberPlumbing/upcomingEvents.py
Normal file
76
memberPlumbing/upcomingEvents.py
Normal file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from .config import Config
|
||||
from .MembershipWorks import MembershipWorks
|
||||
|
||||
|
||||
def format_event(membershipworks: MembershipWorks, event):
|
||||
event_details = membershipworks.get_event_by_eid(event["eid"])
|
||||
url = (
|
||||
"https://claremontmakerspace.org/events/#!event/register/"
|
||||
+ event_details["url"]
|
||||
)
|
||||
if "lgo" in event_details:
|
||||
img = f"""<img class="aligncenter" width="500" height="500" src="{event_details['lgo']['l']}">"""
|
||||
else:
|
||||
img = ""
|
||||
# print(json.dumps(event_details))
|
||||
return f"""<h2 style="text-align: center;">
|
||||
<a href="{url}">
|
||||
{img}
|
||||
{event_details['ttl']}
|
||||
</a>
|
||||
</h2>
|
||||
<div>{event_details['szp']} — {event_details['ezp']}</div>
|
||||
<div>
|
||||
{event_details['dtl']}
|
||||
</div>
|
||||
|
||||
<a href="{url}">Register for this class now!</a>"""
|
||||
|
||||
|
||||
def main():
|
||||
config = Config()
|
||||
|
||||
membershipworks = config.membershipworks
|
||||
events = membershipworks.get_events_list(datetime.now())
|
||||
if "error" in events:
|
||||
print("Error:", events["error"])
|
||||
return
|
||||
|
||||
events_list = "\n<hr />\n\n".join(
|
||||
format_event(membershipworks, event)
|
||||
for event in events["evt"]
|
||||
if event["ttl"] != "[TEMPLATE FOR COPYING]"
|
||||
)
|
||||
header = """<p><img class="aligncenter size-medium wp-image-2319" src="https://claremontmakerspace.org/wp-content/uploads/2019/03/CMS-Logo-b-y-g-300x168.png" alt="" width="300" height="168" /></a></p>
|
||||
<p>Greetings Upper Valley Makers:</p>
|
||||
<p>We have an exciting list of upcoming classes at the Claremont MakerSpace that we think might interest you.</p>
|
||||
<strong>For most classes and events, CMS MEMBERSHIP IS NOT REQUIRED.</strong> That said, members receive a discount on registration and there are some classes/events that are for members only (this will be clearly noted in the event description).
|
||||
|
||||
<strong>Class policies</strong> (liability waiver, withdrawal, cancellation, etc.) can be found <a href="https://claremontmakerspace.org/class-policies/" data-wpel-link="internal">here</a>.
|
||||
|
||||
<strong>Please note: </strong>The Claremont MakerSpace currently requires masks for all visitors and members.
|
||||
|
||||
<strong>Instructors:</strong> Interested in teaching a class at CMS? Please fill out our <a href="https://docs.google.com/forms/d/e/1FAIpQLSdJyEVRJxzIczG784VkOm_DsNyv-VXRXYzlis8qlMdEOvHGpQ/viewform" target="_blank" rel="noreferrer noopener external" data-wpel-link="external">Class Proposal Form</a><a href="https://docs.google.com/forms/d/e/1FAIpQLSdJyEVRJxzIczG784VkOm_DsNyv-VXRXYzlis8qlMdEOvHGpQ/viewform" target="_blank" rel="noreferrer noopener external" data-wpel-link="external">.</a>
|
||||
|
||||
<strong>Tours:</strong> Want to see what the Claremont MakerSpace is all about? Tours are by appointment only due to COVID-19 restrictions. <a href="https://tickets.claremontmakerspace.org/open.php" target="_blank" rel="noreferrer noopener external" data-wpel-link="external">Contact Us</a> to schedule your tour where you can learn about all the awesome tools that the CMS offers access to, as well as how membership, classes, and studio spaces work.
|
||||
|
||||
<hr />
|
||||
"""
|
||||
|
||||
footer = """
|
||||
<hr />
|
||||
|
||||
<div>Happy Makin’!</div>
|
||||
<div>We are grateful for all of the public support that our 501(c)(3), non-profit organization receives. If you’d like to make a donation,please visit the <a href="https://claremontmakerspace.org/support/"><strong>Support Us page</strong></a> of our website.</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
print(header, events_list, footer)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
1110
poetry.lock
generated
1110
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -17,15 +17,23 @@ authors = ["Adam Goldsmith <adam@adamgoldsmith.name>"]
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7"
|
||||
requests = "^2.23.0"
|
||||
"ruamel.yaml" = "^0.16.10"
|
||||
"ruamel.yaml" = "^0.17.20"
|
||||
bitstring = "^3.1.6"
|
||||
lxml = "^4.5.0"
|
||||
peewee = "^3.13.2"
|
||||
mysqlclient = "^1.4.6"
|
||||
mysqlclient = "^2.1.0"
|
||||
udm-rest-client = "^1.0.6"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^19.10b0"
|
||||
isort = "^4.3.21"
|
||||
black = "^22.3.0"
|
||||
isort = "^5.10.1"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
doorUpdater = 'memberPlumbing.doorUpdater:main'
|
||||
hidEvents = 'memberPlumbing.hidEvents:main'
|
||||
sqlExport = 'memberPlumbing.sqlExport:main'
|
||||
ucsAccounts = 'memberPlumbing.ucsAccounts:main'
|
||||
upcomingEvents = 'memberPlumbing.upcomingEvents:main'
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
|
@ -7,4 +7,4 @@ User=adam
|
||||
Type=oneshot
|
||||
TimeoutStartSec=600
|
||||
WorkingDirectory=/home/adam/memberPlumbing/
|
||||
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run /home/adam/memberPlumbing/bin/doorUpdater
|
||||
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run doorUpdater
|
||||
|
@ -9,4 +9,4 @@ User=adam
|
||||
Type=oneshot
|
||||
TimeoutStartSec=600
|
||||
WorkingDirectory=/home/adam/memberPlumbing/
|
||||
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run /home/adam/memberPlumbing/bin/hidEvents
|
||||
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run hidEvents
|
||||
|
@ -9,4 +9,4 @@ User=adam
|
||||
Type=oneshot
|
||||
TimeoutStartSec=600
|
||||
WorkingDirectory=/home/adam/memberPlumbing/
|
||||
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run /home/adam/memberPlumbing/bin/sqlExport
|
||||
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run sqlExport
|
||||
|
10
systemd/ucsAccounts.service
Normal file
10
systemd/ucsAccounts.service
Normal file
@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Update UCS Accounts
|
||||
OnFailure=status-email-admin@%n.service
|
||||
|
||||
[Service]
|
||||
User=adam
|
||||
Type=oneshot
|
||||
TimeoutStartSec=600
|
||||
WorkingDirectory=/home/adam/memberPlumbing/
|
||||
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run ucsAccounts
|
9
systemd/ucsAccounts.timer
Normal file
9
systemd/ucsAccounts.timer
Normal file
@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Hourly UCS Accounts update
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*:0/15
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
Loading…
Reference in New Issue
Block a user