def check_role_member(self, member, unused): """Check out a role member.""" if member.is_active is False: issue(member, E_INACTIVE, f"Member of role {self.name} (fixable)") if self.newdata and (A_MEMBERS in self.newdata): fix = self.newdata else: fix = {} fix[A_MEMBERS] = self.data[A_MEMBERS].copy() fix[A_MEMBERS].remove({A_GUID: member.guid}) self.fixit(fix, f"Delete from role {self.name}") if member.username is None: issue(member, E_NO_LOGIN, f"Member of role {self.name}, so cannot login") if get_config(self.scm, C_ROLES, C_ROLE, self.name, C_IS_COACH): if member.is_coach is False: issue(member, E_COACH_ROLE, f"Role: {self.name}") if get_config(self.scm, C_ROLES, C_VOLUNTEER, C_MANDATORY): if member.is_volunteer is False: issue(member, E_VOLUNTEER, f"Role: {self.name} (fixable)") fix = {} fix[A_ISVOLUNTEER] = "1" member.fixit(fix, "Mark as volunteer") if member.last_login: if (self.scm.today - member.last_login).days > unused: issue( member, E_UNUSED_LOGIN, f"Role: {self.name} [Last login: {member.last_login}]", ) else: issue(member, E_UNUSED_LOGIN, f"Role: {self.name} [Never]")
def read_data(self, scm): """Read each file.""" self.scm = scm home = str(Path.home()) mydir = os.path.join(home, CONFIG_DIR) cfg = get_config(scm, C_FACEBOOK, C_FILES) if cfg: for facebook in cfg: face = FacebookPage() filename = os.path.join(mydir, facebook) res = face.readfile(filename, scm) if res: self.facebook.append(face) if face.parse() is False: return False else: return False return True cfg = get_config(scm, C_FACEBOOK, C_GROUPS) if cfg: for facebook in cfg: face = FacebookPage() res = face.readurl(facebook, scm) if res: self.facebook.append(face) else: return False return True return False
def _list_add(self, err): """Add a member to a confirmation list.""" msg = "Not confirmed" if err == E_CONFIRMATION_EXPIRED: msg = "Confirmation expired" xtype = " (Other)" if self.is_parent: xtype = " (Parent)" if self.is_polo: xtype = " (Water Polo)" cfg = get_config(self.scm, C_TYPES, CTYPE_POLO, C_NAME) if cfg: xtype = f" ({cfg})" if self.is_synchro: xtype = " (Synchro)" cfg = get_config(self.scm, C_TYPES, CTYPE_SYNCHRO, C_NAME) if cfg: xtype = f" ({cfg})" if self.is_swimmer: xtype = " (Swimmer)" msg += xtype self.scm.lists.add(msg, self)
def check_type(self, xtype): """Check the member type check box config.""" cfg = get_config(self.scm, C_TYPES, xtype, C_GROUPS) name = get_config(self.scm, C_TYPES, xtype, C_NAME) if name is None: name = xtype jobtitle = get_config(self.scm, C_TYPES, xtype, C_JOBTITLE) if cfg: found = False for group in cfg: if self.find_group(group): found = True break if found is False: if self.in_ignore_group is False: if self.in_ignore_swimmer is False: issue(self, E_TYPE_GROUP, name) if jobtitle: if self.jobtitle: return issue(self, E_NO_JOB, f"{name}", 0, "fixable") fix = {} fix["JobTitle"] = xtype.title() self.fixit(fix, f"Add jobtitle: {name}")
def update(self): """Create a new list to add to SCM.""" cfg = get_config(self.scm, C_LISTS) if cfg is None: return if get_config(self.scm, C_LISTS, C_EDIT) is not True: notify("List update prohibited by config.\n") return lists = get_config(self.scm, C_LISTS, C_LIST) self._suffix = get_config(self.scm, C_LISTS, C_SUFFIX) if lists: for xlist in lists: newlist = NewList(self.scm, xlist, self._url) self.newlists.append(newlist) newlist.populate() # Separate for loop, as add_to_list may have created some too for xlist in self.newlists: xlist.generate_data(self._suffix) if xlist.upload() is None: pass # not sure what to do, just carry on!
def check_two_groups(swimmer): """Check if swimmer in two groups.""" if get_config(swimmer.scm, C_GROUPS, C_GROUP) is None: return # No config, so ignore error. g_count = 0 errmsg = "" for group in swimmer.groups: nosession = get_config(swimmer.scm, C_GROUPS, C_GROUP, group.name, C_NO_CLUB_SESSIONS) if nosession is True: continue unique = get_config(swimmer.scm, C_GROUPS, C_GROUP, group.name, C_UNIQUE) if unique is None: unique = True if unique: g_count += 1 if g_count > 1: errmsg += f", {group.name}" else: errmsg += group.name if g_count > 1: if swimmer.print_exception(EXCEPTION_TWOGROUPS): issue(swimmer, E_TWO_GROUPS, errmsg)
def readfile(self, file, scm): """Read CSV file.""" self._filename = file self._scm = scm self.cfg_file = ntpath.basename(file) notify(f"Reading {self.cfg_file}...\n") cfg_dob_format = get_config(scm, C_FILES, self.cfg_file, C_MAPPING, C_DOB_FORMAT) if cfg_dob_format is None: cfg_dob_format = SCM_DATE_FORMAT cfg_dob = get_config(scm, C_FILES, self.cfg_file, C_MAPPING, A_DOB) if cfg_dob is None: cfg_dob = A_DOB cfg_cat = get_config(scm, C_FILES, self.cfg_file, C_MAPPING, A_ASA_CATEGORY) if cfg_cat is None: cfg_cat = A_ASA_CATEGORY try: count = 0 with open(file, newline="", encoding="utf-8-sig") as csvfile: csv_reader = csv.DictReader(csvfile) for row in csv_reader: count += 1 if count == 0: continue self._csv.append(row) # Fix DOB if cfg_dob in row: try: row[cfg_dob] = datetime.datetime.strptime( row[cfg_dob], cfg_dob_format) except ValueError as error: notify(f"Date format error in CSV:\n{error}\n") return False if cfg_cat in row: cat = CAT_RE.search(row[cfg_cat]) if cat: row[cfg_cat] = cat.group(0) notify(f"Read {file}...\n") return True except EnvironmentError: notify(f"Cannot open csv file: {file}\n") return False except csv.Error as error: notify(f"Error in csv file: {file}\n{error}\n") return False notify("Unknown error\n") return False
def check_conduct(member, my_codes): """Analyse a code of conduct.""" # pylint: disable=too-many-branches if get_config(member.scm, C_CONDUCT) is None: return type_dict = { CTYPE_SWIMMER: member.is_swimmer, CTYPE_PARENT: member.is_parent, CTYPE_COACH: member.is_coach, CTYPE_COMMITTEE: member.is_committee_member, CTYPE_VOLUNTEER: member.is_volunteer, CTYPE_POLO: member.is_polo, CTYPE_SYNCHRO: member.is_synchro, } codes = member.scm.conduct.entities for code in codes: ignores = get_config(member.scm, C_CONDUCT, code.name, C_IGNORE_GROUP) found_ignore = False if ignores: for ignore in ignores: if member.find_group(ignore): found_ignore = True if found_ignore: continue types = get_config(member.scm, C_CONDUCT, code.name, C_TYPES) if types is None: return for atype in types: found = False func = type_dict[atype] if func is True: for my_code in my_codes: if my_code == code: found = True break if found is False: issue(member, E_NO_CONDUCT, f"{code.name}") if code.newdata and (A_MEMBERS in code.newdata): fix = code.newdata else: fix = {} fix[A_MEMBERS] = code.data[A_MEMBERS].copy() add = {A_GUID: member.guid} fix[A_MEMBERS].append(add) code.fixit(fix, f"Add {member.name}")
def analyse(self): """Analyse the role.""" cfg = get_config(self.scm, C_ROLES, C_ROLE) unused = get_config(self.scm, C_ROLES, C_LOGIN, C_UNUSED) if len(self.members) == 0: issue(self, E_NO_SWIMMERS, "Role") return for member in self.members: self.check_role_member(member, unused) if cfg and (self.name in cfg): self.check_role_permissions(member)
def se_check(scm, members): """Check members against the SE database.""" home = str(Path.home()) cookiefile = os.path.join(home, CONFIG_DIR, SE_COOKIES) base_url = get_config(scm, C_SWIM_ENGLAND, C_BASE_URL) if base_url is False: notify("Swim England config missing\n") return None notify(f"Opening browser for {base_url}...\n") browser = start_browser(scm) if browser is None: return None read_cookies(browser, cookiefile, base_url, None) check_url = get_config(scm, C_SWIM_ENGLAND, C_CHECK_URL) test_id_url = get_config(scm, C_SWIM_ENGLAND, C_TEST_ID) browser.get(f"{check_url}{test_id_url}") try: browser.find_element_by_xpath("//table[1]/tbody/tr[2]/td[2]") except selenium.common.exceptions.NoSuchElementException: msg = """Please solve the 'I am not a robot', and then press enter here. If you cannot solve the capture try deleting the file 'se_cookies.json' in the config directory. """ interact_yesno(msg) write_cookies(browser, cookiefile, None) res = "" for member in members: if member.is_active is False: continue if member.asa_number is None: continue url = f"{check_url}{member.asa_number}" browser.get(url) res += check_member(browser, member) browser.close() return res
def check_asa(swimmer): """Check ASA (Swim England) number is OK.""" if swimmer.asa_number is None: cfg_synchro = get_config(swimmer.scm, C_TYPES, CTYPE_SYNCHRO, C_CHECK_SE_NUMBER) cfg_polo = get_config(swimmer.scm, C_TYPES, CTYPE_POLO, C_CHECK_SE_NUMBER) err = True if swimmer.is_polo and (cfg_polo is False): err = False if swimmer.is_synchro and (cfg_synchro is False): err = False if err: issue(swimmer, E_ASA)
def check_dbs(self, xtype): """Check DBS and Safeguarding.""" if self.print_exception(EXCEPTION_NODBS) is False: debug(f"DBS Exception ignored: {self.name}", 7) return dbs_date = self.set_date(A_DBS_RENEWAL_DATE) safe_date = self.set_date(A_SAFEGUARDING_RENEWAL_DATE) notice = get_config(self.scm, C_MEMBERS, C_DBS, C_EXPIRY) if dbs_date: days = (dbs_date - self.scm.today).days if days < 0: dbs_date_str = dbs_date.strftime(PRINT_DATE_FORMAT) issue(self, E_DBS_EXPIRED, f"{xtype}, expired {dbs_date_str}") elif days < notice: dbs_date_str = dbs_date.strftime(PRINT_DATE_FORMAT) issue(self, E_DBS_EXPIRED, f"{xtype}, expires {dbs_date_str}") else: issue(self, E_NO_DBS, f"{xtype}") if self.print_exception(EXCEPTION_NOSAFEGUARD) is False: debug(f"Safeguard Exception ignored: {self.name}", 7) return if safe_date: if (safe_date - self.scm.today).days < 0: safe_date_str = safe_date.strftime(PRINT_DATE_FORMAT) issue(self, E_SAFEGUARD_EXPIRED, f"{xtype}, expired {safe_date_str}") elif (safe_date - self.scm.today).days < notice: safe_date_str = safe_date.strftime(PRINT_DATE_FORMAT) issue(self, E_SAFEGUARD_EXPIRED, f"{xtype}, expires {safe_date_str}") else: issue(self, E_NO_SAFEGUARD, f"{xtype}")
def check_login(swimmer): """Check if the login is OK.""" if swimmer.username: min_age = get_config(swimmer.scm, C_SWIMMERS, C_USERNAME, C_MIN_AGE) if min_age: if swimmer.age and (swimmer.age < min_age): issue(swimmer, E_LOGIN_TOO_YOUNG, f"Age: {swimmer.age}")
def newstarter(self): """Is the member a new stater.""" grace = get_config(self.scm, C_MEMBERS, C_NEWSTARTER, C_GRACE) if grace and self._date_joined: if (self.scm.today - self._date_joined).days < grace: return True return False
def check_swim(self, swim): """Check a swim time to see if it as a record.""" check_event = swim[S_EVENT] if check_event in self.records: event = self.records[check_event] if swim[S_FTIME] >= event[S_FTIME]: return self.records[check_event] = swim sloc = swim[S_LOCATION] newrec = ( f"New record: {check_event}, {swim[S_NAME]}, {swim[S_TIMESTR]}, {sloc}\n" ) self.newrecords[check_event] = newrec if get_config(self.scm, C_RECORDS, C_OVERALL_FASTEST): split_event = swim[S_EVENT].split() split_event[1] = OVERALL o_event = " ".join(str(item) for item in split_event) if o_event in self.records: event = self.records[o_event] if swim[S_FTIME] >= event[S_FTIME]: return o_swim = swim.copy() o_swim[S_EVENT] = o_event self.records[o_event] = o_swim
def api_write(self, entity, create): """Write data back to SCM.""" club = self._config[C_CLUB] user_agent = USER_AGENT.replace("###CLUB_NAME###", club) headers = { "content-type": "application/json", "User-Agent": user_agent, "Authorization-Token": self._key, } if get_config(entity.scm, C_ALLOW_UPDATE) is False: notify("Update prohibited by config.\n") return None debug(f"URL:\n{entity.url}", 9) debug(f"Headers:\n{headers}", 8) data = entity.newdata if create: debug(f"Post request:\n{data}", 7) response = requests.post(entity.url, json=data, headers=headers) else: debug(f"Put request:\n{data}", 7) response = requests.put(entity.url, json=data, headers=headers) if response.ok: return response if response.status_code == 404: # Worked, but not found return False notify(f"\nErroring posting data {entity.name}\n") notify(response.reason) notify("\n") return None
def analyse(self): """Analyse the conduct class.""" if get_config(self.scm, C_CONDUCT) is None: return for code in self.entities: code.analyse()
def analyse_parent(parent): """Analyse a parent...""" # pylint: disable=too-many-branches active = False inactive = None for swimmer in parent.swimmers: if swimmer.is_active: active = True else: inactive = swimmer.name if active is False: if inactive is None: issue(parent, E_NO_CHILD, "fixable") fix = {} fix[A_ISPARENT] = "0" parent.fixit(fix, "Remove 'is parent'") else: issue(parent, E_INACTIVE, f"child {inactive}") newmember = True for swimmer in parent.swimmers: if swimmer.newstarter is False: newmember = False break if (newmember is True) and parent.swimmers: parent.set_joined_today() age = get_config(parent.scm, C_PARENTS, C_AGE, C_MIN_AGE) if parent.age and (parent.age < age): issue(parent, E_PARENT_AGE) age = get_config(parent.scm, C_PARENTS, C_AGE, C_CHILD) for swimmer in parent.swimmers: if active and swimmer.age and (swimmer.age >= age): issue(swimmer, E_PARENT_AGE_TOO_OLD, f"{swimmer.age}, {parent.name}") login = get_config(parent.scm, C_PARENTS, C_LOGIN, C_MANDATORY) if login and (parent.username is None) and parent.email: issue(parent, E_NO_LOGIN, "Parent (fixable)") fix = {} fix[A_USERNAME] = parent.email parent.fixit(fix, f"Create login, username: {parent.email}")
def __init__(self, entity, scm, url): """Initialise.""" super().__init__(entity, scm, url) self.coaches = [] self.swimmers = [] self.ignore_attendance = False self.exclude_max = False cfg = get_config(self.scm, C_SESSIONS, C_SESSION, self.name, C_IGNORE_ATTENDANCE) if cfg: self.ignore_attendance = cfg cfg = get_config(self.scm, C_SESSIONS, C_SESSION, self.name, C_EXCLUDE_MAX) if cfg: self.exclude_max = cfg
def check_ignore(self, member): """Return true if swimmer in ignore list.""" cfg_ignore = get_config(self._scm, C_FILES, self.cfg_file, C_IGNORE_GROUP) if cfg_ignore: for ignore in cfg_ignore: if member.find_group(ignore): return True return False
def start_browser(scm): """Start Browser.""" web_driver = get_config(scm, C_SELENIUM, C_WEB_DRIVER) client = get_config(scm, C_SELENIUM, C_BROWSER) if client is False: notify("Selenium config missing\n") return None browser = getattr(selenium.webdriver, client) try: return browser(web_driver) except selenium.common.exceptions.WebDriverException as error: notify( f"Failed to open {client} browser with: {web_driver}\n{error}\n") return None
def analyse_swimmer(swimmer): """Analyse a swimmer...""" # pylint: disable=too-many-branches if swimmer.in_ignore_group: return if swimmer.in_ignore_swimmer: return if len(swimmer.groups) == 0: if swimmer.print_exception(EXCEPTION_NOGROUPS): issue(swimmer, E_NO_GROUP) if swimmer.dob is None: issue(swimmer, E_DOB) if swimmer.gender is None: issue(swimmer, E_GENDER) if swimmer.date_joined is None: issue(swimmer, E_DATE_JOINED) check_asa(swimmer) check_lastseen(swimmer) check_two_groups(swimmer) check_login(swimmer) if swimmer.is_swimmer: check_parents(swimmer) check_max_sessions(swimmer) return if swimmer.is_synchro: if get_config(swimmer.scm, C_TYPES, CTYPE_SYNCHRO, C_PARENTS) is False: pass else: check_parents(swimmer) return if swimmer.is_polo: if get_config(swimmer.scm, C_TYPES, CTYPE_POLO, C_PARENTS) is False: pass else: check_parents(swimmer)
def analyse_coach(coach): """Analyse a coach...""" if coach.coach_role is False: if get_config(coach.scm, C_COACHES, C_ROLE, C_MANDATORY): issue(coach, E_NO_ROLE_COACH) if len(coach.coach_sessions) == 0: if coach.print_exception(EXCEPTION_NOSESSIONS): issue(coach, E_NO_SESSIONS) coach.check_dbs("Coach")
def check_parents(swimmer): """Check consistence between swimmer and parent email.""" # pylint: disable=too-many-branches email = None match = False count = 0 confirm_error = False confirm_verify = get_config(swimmer.scm, C_SWIMMERS, C_CONF_DIFF, C_VERIFY) max_age = get_config(swimmer.scm, C_SWIMMERS, C_PARENT, C_MAX_AGE) if swimmer.email: email = swimmer.email.split(";") for parent in swimmer.parents: count += 1 if parent.is_active is False: issue(parent, E_INACTIVE, f"Swimmer {swimmer.name}") if match is False: match = check_parent_email_match(email, parent) if confirm_verify: confirm_error = check_confirmed_diff(swimmer, parent) if confirm_error: issue(swimmer, E_CONFIRM_DIFF, f"Parent {parent.name}") if (swimmer.parents and swimmer.age and (match is False) and (swimmer.age <= max_age)): if swimmer.print_exception(EXCEPTION_EMAILDIFF): err = f"{swimmer.email} - {swimmer.parents[0].email}" issue(swimmer, E_EMAIL_MATCH, err) if count == 0: mandatory = get_config(swimmer.scm, C_SWIMMERS, C_PARENT, C_MANDATORY) if mandatory and max_age: if swimmer.age and (swimmer.age <= max_age): msg = f"{swimmer.first_group}, Age: {swimmer.age}" issue(swimmer, E_NO_PARENT, msg) if count > 2: issue(swimmer, E_NUM_PARENTS)
def check_role_permissions(self, member): """Check out a role permissions.""" lookup = get_config(self.scm, C_ROLES, C_ROLE, self.name) if lookup is None: return if (C_CHECK_PERMISSIONS in lookup) and lookup[C_CHECK_PERMISSIONS]: check_coach_permissions(member, self) if (C_CHECK_RESTRICTIONS in lookup) and lookup[C_CHECK_RESTRICTIONS]: if len(member.restricted) == 0: issue(member, E_NO_RESTRICTIONS, f"Role: {self.name}")
def analyse(self): """Analyse the conduct entry.""" # A better way of doing this would be to add # the attribute to the swimmer in linkage. # This approach breaks the model. Oh well. ignores = get_config(self.scm, C_CONDUCT, self.name, C_IGNORE_GROUP) c_date_str = get_config(self.scm, C_CONDUCT, self.name, C_DATE) if c_date_str is None: c_date_str = "1900-01-01" c_date = datetime.datetime.strptime(c_date_str, SCM_DATE_FORMAT) for member in self.data[A_MEMBERS]: if member[A_DATEAGREED]: m_date = datetime.datetime.strptime(member[A_DATEAGREED], SCM_DATE_FORMAT) if m_date >= c_date: continue person = self.scm.members.by_guid[member[A_GUID]] if person.confirmed_date: # Will get a not confirmed error later in not set found_ignore = False if ignores: for ignore in ignores: if person.find_group(ignore): found_ignore = True if found_ignore: continue issue(person, E_NO_CONDUCT_DATE, self.name, 0, person.first_group) codes = get_config(self.scm, C_LISTS, C_CONDUCT) if codes: for code in codes: if self.name == code: msg = f"{self.name} missing" self.scm.lists.add(msg, person)
def check_confirmation(self): """Check confimation status.""" expiry = get_config(self.scm, C_MEMBERS, C_CONFIRMATION, C_EXPIRY) align = get_config(self.scm, C_MEMBERS, C_CONFIRMATION, C_ALIGN_QUARTER) xlist = get_config(self.scm, C_LISTS, C_CONFIRMATION) q_offset = 0 if align: q_offset = self.scm.q_offset if self.confirmed_date: gap = (self.scm.today - self.confirmed_date).days if gap > (expiry + q_offset): issue(self, E_CONFIRMATION_EXPIRED, f"{self.first_group}") self.scm.members.count_not_confirmed += 1 if xlist: self._list_add(E_CONFIRMATION_EXPIRED) else: issue(self, E_NOT_CONFIRMED, f"{self.first_group}") self.scm.members.count_not_confirmed += 1 if xlist: self._list_add(E_NOT_CONFIRMED)
def check_confirmed_diff(swimmer, parent): """Check for differences in swimmer and parent.""" # pylint: disable=R0911 # Need them all child_mon = 0 parent_mon = 0 if swimmer.confirmed_date: child_mon = int((swimmer.confirmed_date.month - 1) / 3) * 3 if parent.confirmed_date: parent_mon = int((parent.confirmed_date.month - 1) / 3) * 3 if swimmer.age > get_config(swimmer.scm, C_SWIMMERS, C_PARENT, C_MAX_AGE): return False if child_mon == parent_mon: return False prefix = "Different confirmed dates" postfix = "- checking other details for consistency" debug(f"{prefix} {swimmer.name}, {parent.name} {postfix}", 8) if swimmer.email != parent.email: debug(f"email: {swimmer.email}: {parent.email}", 8) return True if swimmer.homephone != parent.homephone: debug(f"phone: {swimmer.homephone}: {parent.homephone}", 8) return True if swimmer.mobilephone != parent.mobilephone: debug(f"mobile: {swimmer.mobilephone}: {parent.mobilephone}", 8) return True if swimmer.address != parent.address: debug(f"address: {swimmer.address}: {parent.address}", 8) return True # Dates are different, but core attributes same # So set the confirmed date - to inhibit a confirm notice to parent if parent.confirmed_date is None: parent.set_confirmed(swimmer.confirmed_date) return False if swimmer.confirmed_date: if swimmer.confirmed_date > parent.confirmed_date: parent.set_confirmed(swimmer.confirmed_date) return False
def check_max_sessions(swimmer): """Check not exceeding max numbr of sessions allowed""" max_session = get_config(swimmer.scm, C_GROUPS, C_GROUP, swimmer.first_group, C_MAX_SESSIONS) if max_session: num_sessions = len(swimmer.sessions) if num_sessions > max_session: for session in swimmer.sessions: if session.exclude_max: num_sessions = num_sessions - 1 if num_sessions > max_session: sessions = swimmer.print_swimmer_sessions(False) issue(swimmer, E_MAX_SESSIONS, f"{len(swimmer.sessions)}: \n{sessions}")
def read_baseline(self): """Read baseline.""" home = str(Path.home()) mydir = os.path.join(home, CONFIG_DIR, RECORDS_DIR) self.records = Record() filename = os.path.join(mydir, F_BASELINE) res = self.records.read_baseline(filename, self.scm) if res and get_config(self.scm, C_RECORDS, C_RELAY): self.relay = RelayRecord() filename = os.path.join(mydir, F_RELAY_BASELINE) res = self.relay.read_baseline(filename, self.scm) return res