Lots of changes, I guess

This commit is contained in:
Kumi 2023-07-03 11:24:31 +02:00
parent ff17665cb0
commit a7589e604c
Signed by: kumi
GPG key ID: ECBCC9082395383F
8 changed files with 156 additions and 67 deletions

2
.gitignore vendored
View file

@ -5,7 +5,7 @@ settings.ini
completionmail.ini completionmail.ini
venv/ venv/
output/ output/
reports/*.py reports/*
!reports/__init__.py !reports/__init__.py
.vscode .vscode
*.old *.old

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "reportmonster" name = "reportmonster"
version = "0.9.6" version = "0.9.7"
authors = [ authors = [
{ name="Kumi Systems e.U.", email="office@kumi.systems" }, { name="Kumi Systems e.U.", email="office@kumi.systems" },
] ]

0
reports/__init__.py Normal file
View file

View file

@ -1,7 +1,7 @@
import configparser import configparser
from pathlib import Path from pathlib import Path
from typing import Union from typing import Union, List
from .vessel import Vessel from .vessel import Vessel
from .user import User from .user import User
@ -37,16 +37,17 @@ class MonsterConfig:
except KeyError: except KeyError:
try: try:
import pyadonis import pyadonis
self.pyadonis = Path(pyadonis.__path__[0]) self.pyadonis = Path(pyadonis.__path__[0])
except ImportError: except ImportError:
print(f"PyAdonis is not defined in the MONSTER section of {path}, some features may be missing.") print(
f"PyAdonis is not defined in the MONSTER section of {path}, some features may be missing."
)
def __init__(self, path: Union[str, Path]) -> None: def __init__(self, path: Union[str, Path]) -> None:
"""Initialize a new (empty) MonsterConfig object """Initialize a new (empty) MonsterConfig object"""
""" self.vessels: List[Vessel] = []
self.vessels = [] self.users: List[User] = []
self.users = []
self.pyadonis = None self.pyadonis = None
if path: if path:

View file

@ -10,8 +10,7 @@ import socket
class Connection: class Connection:
"""Class representing an SSH/SFTP connection to a Vessel """Class representing an SSH/SFTP connection to a Vessel"""
"""
def __init__(self, vessel): def __init__(self, vessel):
"""Initialize a new Connection to a Vessel """Initialize a new Connection to a Vessel
@ -24,9 +23,14 @@ class Connection:
self._client = SSHClient() self._client = SSHClient()
self._client.load_system_host_keys() self._client.load_system_host_keys()
self._client.set_missing_host_key_policy(WarningPolicy) self._client.set_missing_host_key_policy(WarningPolicy)
self._client.connect(vessel.host, 22, vessel.ssh_username, self._client.connect(
vessel.ssh_password, timeout=vessel.ssh_timeout, vessel.host,
passphrase=vessel.ssh_passphrase) 22,
vessel.ssh_username,
vessel.ssh_password,
timeout=vessel.ssh_timeout,
passphrase=vessel.ssh_passphrase,
)
self._transport = self._client.get_transport() self._transport = self._client.get_transport()
self._transport.set_keepalive(10) self._transport.set_keepalive(10)
self._sftp = self._client.open_sftp() self._sftp = self._client.open_sftp()
@ -36,14 +40,14 @@ class Connection:
(self._vessel.host, 22), (self._vessel.host, 22),
ssh_username=self._vessel.ssh_username, ssh_username=self._vessel.ssh_username,
ssh_private_key_password=self._vessel.ssh_passphrase, ssh_private_key_password=self._vessel.ssh_passphrase,
remote_bind_address=("127.0.0.1", remote)) remote_bind_address=("127.0.0.1", remote),
)
self._process.start() self._process.start()
return self._process.local_bind_port return self._process.local_bind_port
def __del__(self): def __del__(self):
"""Close SSH connection when ending Connection """Close SSH connection when ending Connection"""
"""
self._client.close() self._client.close()
if self._process: if self._process:

View file

@ -9,18 +9,23 @@ from .logger import Logger
logger = Logger() logger = Logger()
class Database: class Database:
"""Class wrapping MySQL database connection """Class wrapping MySQL database connection"""
"""
def __init__(self, vessel): def __init__(self, vessel):
"""Initialize a new Database object """Initialize a new Database object"""
"""
self.vessel = vessel self.vessel = vessel
self._con = None self._con = None
self._ssh = None self._ssh = None
def _execute(self, query: str, parameters: Optional[tuple] = None, ctype: Optional[MySQLdb.cursors.BaseCursor] = None, retry: bool = True): def _execute(
self,
query: str,
parameters: Optional[tuple] = None,
ctype: Optional[MySQLdb.cursors.BaseCursor] = None,
retry: bool = True,
):
"""Execute a query on the database """Execute a query on the database
Args: Args:
@ -45,15 +50,20 @@ class Database:
def _connect(self) -> None: def _connect(self) -> None:
if self.vessel.ssh: if self.vessel.ssh:
self._ssh = Connection(self.vessel) self._ssh = Connection(self.vessel)
port = self._ssh.forward_tcp(3306) port = self._ssh.forward_tcp(3306)
host = "127.0.0.1" host = "127.0.0.1"
else: else:
port = 3306 port = 3306
host = self.vessel.host host = self.vessel.host
self._con = MySQLdb.connect(host=host, user=self.vessel.username, self._con = MySQLdb.connect(
passwd=self.vessel.password, db=self.vessel.database, port=port) host=host,
user=self.vessel.username,
passwd=self.vessel.password,
db=self.vessel.database,
port=port,
)
def commit(self) -> None: def commit(self) -> None:
"""Commit the current database transaction """Commit the current database transaction
@ -64,7 +74,9 @@ class Database:
""" """
self._con.commit() self._con.commit()
def getCursor(self, ctype: Optional[MySQLdb.cursors.BaseCursor] = None) -> MySQLdb.cursors.BaseCursor: def getCursor(
self, ctype: Optional[MySQLdb.cursors.BaseCursor] = None
) -> MySQLdb.cursors.BaseCursor:
"""Return a cursor to operate on the MySQL database """Return a cursor to operate on the MySQL database
Returns: Returns:
@ -73,7 +85,6 @@ class Database:
return self._con.cursor(ctype) return self._con.cursor(ctype)
def __del__(self): def __del__(self):
"""Close database connection on removal of the Database object """Close database connection on removal of the Database object"""
"""
if self._con: if self._con:
self._con.close() self._con.close()

View file

@ -10,8 +10,8 @@ from bcrypt import hashpw, gensalt
class User: class User:
"""Class describing a User """Class describing a User"""
"""
@classmethod @classmethod
def fromConfig(cls, config: SectionProxy): def fromConfig(cls, config: SectionProxy):
"""Create User object from a User section in the Config file """Create User object from a User section in the Config file
@ -29,7 +29,6 @@ class User:
return cls(config.name.split()[1], config["Password"]) return cls(config.name.split()[1], config["Password"])
def __init__(self, username: str, password: str) -> None: def __init__(self, username: str, password: str) -> None:
"""Initialize new Vessel object """Initialize new Vessel object
@ -39,6 +38,5 @@ class User:
self.username = username self.username = username
self.password = password self.password = password
def validatePasword(self, password) -> bool: def validatePasword(self, password) -> bool:
return password == self.password return password == self.password

View file

@ -1,4 +1,4 @@
from classes.database import Database from .database import Database
from configparser import SectionProxy from configparser import SectionProxy
from typing import Optional, Union from typing import Optional, Union
@ -8,18 +8,18 @@ from MySQLdb.cursors import DictCursor
from MySQLdb._exceptions import IntegrityError from MySQLdb._exceptions import IntegrityError
from bcrypt import hashpw, gensalt from bcrypt import hashpw, gensalt
from const import * from ..const import *
class Vessel: class Vessel:
"""Class describing a Vessel """Class describing a Vessel"""
"""
@classmethod @classmethod
def fromConfig(cls, config: SectionProxy): def fromConfig(cls, config: SectionProxy):
"""Create Vessel object from a Vessel section in the Config file """Create Vessel object from a Vessel section in the Config file
Args: Args:
config (configparser.SectionProxy): Vessel section defining a config (configparser.SectionProxy): Vessel section defining a
Vessel Vessel
Raises: Raises:
@ -50,14 +50,37 @@ class Vessel:
database = config["Database"] database = config["Database"]
if "SSH" in config.keys(): if "SSH" in config.keys():
if int(config["SSH"]) == 1: if config.getint("SSH") == 1:
ssh = True ssh = True
return cls(config.name.split()[1], config["Host"], username, password, database, ssh, ssh_username, ssh_password, ssh_timeout, ssh_passphrase) ssh_username = config.get("SSHUser")
def __init__(self, name: str, host: str, username: Optional[str] = None, return cls(
password: Optional[str] = None, database: Optional[str] = None, config.name.split()[1],
ssh = False, ssh_username = None, ssh_password = None, ssh_timeout = None, ssh_passphrase = None) -> None: config["Host"],
username,
password,
database,
ssh,
ssh_username,
ssh_password,
ssh_timeout,
ssh_passphrase,
)
def __init__(
self,
name: str,
host: str,
username: Optional[str] = None,
password: Optional[str] = None,
database: Optional[str] = None,
ssh=False,
ssh_username=None,
ssh_password=None,
ssh_timeout=None,
ssh_passphrase=None,
) -> None:
"""Initialize new Vessel object """Initialize new Vessel object
Args: Args:
@ -76,7 +99,7 @@ class Vessel:
self.db = self.connect() self.db = self.connect()
def connect(self): def connect(self) -> Database:
return Database(self) return Database(self)
def reconnect(self): def reconnect(self):
@ -98,7 +121,9 @@ class Vessel:
results = self.db._execute(QUERY_USER_INFO_FIELD, ctype=DictCursor) results = self.db._execute(QUERY_USER_INFO_FIELD, ctype=DictCursor)
return results return results
def getUserInfoData(self, field: Optional[int] = None, user: Optional[int] = None) -> list: def getUserInfoData(
self, field: Optional[int] = None, user: Optional[int] = None
) -> list:
query = QUERY_USER_INFO_DATA query = QUERY_USER_INFO_DATA
parameters = [] parameters = []
@ -119,7 +144,9 @@ class Vessel:
return results return results
def getUsers(self, username: Optional[str] = None, id: Optional[int] = None) -> dict: def getUsers(
self, username: Optional[str] = None, id: Optional[int] = None
) -> dict:
query = QUERY_USER query = QUERY_USER
parameters = tuple() parameters = tuple()
@ -147,7 +174,9 @@ class Vessel:
for value in odata: for value in odata:
try: try:
users[value["userid"]]["custom_fields"][ofield["shortname"]] = value["data"] users[value["userid"]]["custom_fields"][
ofield["shortname"]
] = value["data"]
except KeyError: except KeyError:
pass pass
@ -155,7 +184,10 @@ class Vessel:
def getHTMLCerts(self, after: int = 0, before: Optional[int] = None): def getHTMLCerts(self, after: int = 0, before: Optional[int] = None):
before = before or Vessel.getTimestamp() before = before or Vessel.getTimestamp()
results = self.db._execute(f"{QUERY_HTML_CERT_ISSUES} {QUERY_WHERE_TIMESTAMPS % {'column': 'timecreated', 'after': after, 'before': before}}", ctype=DictCursor) results = self.db._execute(
f"{QUERY_HTML_CERT_ISSUES} {QUERY_WHERE_TIMESTAMPS % {'column': 'timecreated', 'after': after, 'before': before}}",
ctype=DictCursor,
)
ocerts = self.db._execute(QUERY_HTML_CERT, ctype=DictCursor) ocerts = self.db._execute(QUERY_HTML_CERT, ctype=DictCursor)
certs = dict() certs = dict()
@ -175,7 +207,10 @@ class Vessel:
def getCustomCerts(self, after: int = 0, before: Optional[int] = None): def getCustomCerts(self, after: int = 0, before: Optional[int] = None):
before = before or Vessel.getTimestamp() before = before or Vessel.getTimestamp()
results = self.db._execute(f"{QUERY_CUSTOM_CERT_ISSUES} {QUERY_WHERE_TIMESTAMPS % {'column': 'timecreated', 'after': after, 'before': before}}", ctype=DictCursor) results = self.db._execute(
f"{QUERY_CUSTOM_CERT_ISSUES} {QUERY_WHERE_TIMESTAMPS % {'column': 'timecreated', 'after': after, 'before': before}}",
ctype=DictCursor,
)
ocerts = self.db._execute(QUERY_CUSTOM_CERT, ctype=DictCursor) ocerts = self.db._execute(QUERY_CUSTOM_CERT, ctype=DictCursor)
certs = dict() certs = dict()
@ -195,7 +230,10 @@ class Vessel:
def getCerts(self, after: int = 0, before: Optional[int] = None): def getCerts(self, after: int = 0, before: Optional[int] = None):
before = before or Vessel.getTimestamp() before = before or Vessel.getTimestamp()
return sorted(self.getHTMLCerts(after, before) + self.getCustomCerts(after, before), key=lambda d: d["timecreated"]) return sorted(
self.getHTMLCerts(after, before) + self.getCustomCerts(after, before),
key=lambda d: d["timecreated"],
)
def setPassword(self, username: str, password: str): def setPassword(self, username: str, password: str):
hashed = hashpw(password.encode(), gensalt(prefix=b"2b")) hashed = hashpw(password.encode(), gensalt(prefix=b"2b"))
@ -218,11 +256,16 @@ class Vessel:
if not enrol: if not enrol:
self.createEnrol(courseid, enrol) self.createEnrol(courseid, enrol)
enrol = list(filter(lambda x: x["courseid"] == courseid , self.getEnrols(enrol))) enrol = list(
filter(lambda x: x["courseid"] == courseid, self.getEnrols(enrol))
)
assert enrol assert enrol
self.db._execute(QUERY_ENROL_USER, (enrol[0]["id"], userid, Vessel.getTimestamp(), Vessel.getTimestamp())) self.db._execute(
QUERY_ENROL_USER,
(enrol[0]["id"], userid, Vessel.getTimestamp(), Vessel.getTimestamp()),
)
def getEnrolments(self): def getEnrolments(self):
results = list(self.db._execute(QUERY_ENROLMENTS, ctype=DictCursor)) results = list(self.db._execute(QUERY_ENROLMENTS, ctype=DictCursor))
@ -230,16 +273,30 @@ class Vessel:
def createUser(self, username, password, email, firstname, lastname): def createUser(self, username, password, email, firstname, lastname):
email = email or f"{username}@pin.seachefsacademy.com" email = email or f"{username}@pin.seachefsacademy.com"
self.db._execute(QUERY_USER_CREATE, (username, email, firstname, lastname, Vessel.getTimestamp(), Vessel.getTimestamp())) self.db._execute(
QUERY_USER_CREATE,
(
username,
email,
firstname,
lastname,
Vessel.getTimestamp(),
Vessel.getTimestamp(),
),
)
self.setPassword(username, password) self.setPassword(username, password)
def assignRole(self, userid: int, courseid: int, roleid: int = 5): def assignRole(self, userid: int, courseid: int, roleid: int = 5):
contextid = self.getCourseContext(courseid)[0]["id"] contextid = self.getCourseContext(courseid)[0]["id"]
self.db._execute(QUERY_ASSIGN_ROLE, (roleid, contextid, userid, Vessel.getTimestamp())) self.db._execute(
QUERY_ASSIGN_ROLE, (roleid, contextid, userid, Vessel.getTimestamp())
)
def getRole(self, userid: int, courseid: int) -> Optional[int]: def getRole(self, userid: int, courseid: int) -> Optional[int]:
contextid = self.getCourseContext(courseid)[0]["id"] contextid = self.getCourseContext(courseid)[0]["id"]
results = self.db._execute(QUERY_GET_ROLE, (contextid, userid), ctype=DictCursor) results = self.db._execute(
QUERY_GET_ROLE, (contextid, userid), ctype=DictCursor
)
if results: if results:
return results[0]["roleid"] return results[0]["roleid"]
@ -249,7 +306,10 @@ class Vessel:
return results[0]["id"] return results[0]["id"]
def setEmail(self, userid: int, email: str): def setEmail(self, userid: int, email: str):
email = email or f"{self.getUsers(id=userid)[userid]['username']}@pin.seachefsacademy.com" email = (
email
or f"{self.getUsers(id=userid)[userid]['username']}@pin.seachefsacademy.com"
)
self.db._execute(QUERY_USER_SET_EMAIL, (email, userid)) self.db._execute(QUERY_USER_SET_EMAIL, (email, userid))
def setName(self, userid: int, first: str, last: str): def setName(self, userid: int, first: str, last: str):
@ -260,23 +320,32 @@ class Vessel:
return results return results
def getCourseByContext(self, contextid: int) -> Optional[int]: def getCourseByContext(self, contextid: int) -> Optional[int]:
results = self.db._execute(QUERY_COURSE_CONTEXT_REVERSE, (contextid,), ctype=DictCursor) results = self.db._execute(
QUERY_COURSE_CONTEXT_REVERSE, (contextid,), ctype=DictCursor
)
if results: if results:
return results[0]["instanceid"] return results[0]["instanceid"]
def getCourseModules(self, courseid: int): def getCourseModules(self, courseid: int):
results = list(self.db._execute(QUERY_COURSE_MODULES, (courseid,), ctype=DictCursor)) results = list(
self.db._execute(QUERY_COURSE_MODULES, (courseid,), ctype=DictCursor)
)
return results return results
def getCourseModuleCompletion(self, moduleid: int): def getCourseModuleCompletion(self, moduleid: int):
results = list(self.db._execute(QUERY_MODULE_COMPLETION, (moduleid,), ctype=DictCursor)) results = list(
self.db._execute(QUERY_MODULE_COMPLETION, (moduleid,), ctype=DictCursor)
)
return results return results
def setCourseModuleCompletion(self, moduleid: int, userid: int): def setCourseModuleCompletion(self, moduleid: int, userid: int):
try: try:
self.db._execute(QUERY_INSERT_MODULE_COMPLETION, (moduleid, userid, Vessel.getTimestamp())) self.db._execute(
QUERY_INSERT_MODULE_COMPLETION,
(moduleid, userid, Vessel.getTimestamp()),
)
except IntegrityError: except IntegrityError:
pass # Module completion record already exists pass # Module completion record already exists
self.db._execute(QUERY_UPDATE_MODULE_COMPLETION, (moduleid, userid)) self.db._execute(QUERY_UPDATE_MODULE_COMPLETION, (moduleid, userid))
def setCourseCompletion(self, courseid: int, userid: int): def setCourseCompletion(self, courseid: int, userid: int):
@ -287,3 +356,9 @@ class Vessel:
def writeLog(self, event, data): def writeLog(self, event, data):
self.db._execute(QUERY_LOG_INSERT, (event, data)) self.db._execute(QUERY_LOG_INSERT, (event, data))
def getCourseCompletions(self, courseid: int):
results = list(
self.db._execute(QUERY_COURSE_COMPLETION, (courseid,), ctype=DictCursor)
)
return results