from threading import RLock
from datetime import datetime, timedelta
import cherrypy
from .db import DBPoolManager
BUGSTATUS=["OPEN", "CLOSED", "UNCONF", "REJECT", "UPSTRM"]
BUGTYPES=["BUG", "DISCUS", "PATCH"]
class Site():
def __init__(self,dbpool:DBPoolManager):
self.dbpool=dbpool
cherrypy.engine.subscribe("db-started",self.on_db_connect)
self.bugcache={}
self.last_tracker_update=datetime.fromtimestamp(0)
self.tracker_update_lock=RLock()
def on_db_connect(self):
cherrypy.log("Setting up database",context="SITE")
with self.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute(
"CREATE TABLE IF NOT EXISTS trackers ("
"name VARCHAR(80) PRIMARY KEY,"
"homepage VARCHAR(1024)"
")"
)
cur.execute(
"CREATE TABLE IF NOT EXISTS bugs ("
"tracker VARCHAR(80),"
"bugid INT,"
"subject VARCHAR(1024),"
"description TEXT,"
"status VARCHAR(6),"
"type VARCHAR(6)"
")"
)
cur.execute(
"CREATE TABLE IF NOT EXISTS subscribers ("
"tracker VARCHAR(80),"
"bugid INT,"
"email VARCHAR(80)"
")"
)
cur.execute(
"CREATE TABLE IF NOT EXISTS permissiontable ("
"email VARCHAR(80),"
"tracker VARCHAR(80),"
"bugid INT)"
)
cur.execute(
"CREATE TABLE IF NOT EXISTS tokens ("
"email VARCHAR(80),"
"token VARCHAR(80),"
"tracker VARCHAR(80),"
"bugid INT,"
"usedin TINYTEXT DEFAULT NULL)"
)
conn.commit()
self.update_trackers()
self.update_all_bugpages(conn,cur)
def update_all_bugpages(self,conn,cur,trackerfilter=None):
if trackerfilter:
cherrypy.engine.publish("regen",trackerfilter,None)
else:
cur.execute("SELECT name FROM trackers")
for [tracker] in cur:
cherrypy.engine.publish("regen",tracker,None)
cur.execute("SELECT tracker,bugid FROM bugs")
for [tracker,bugid] in cur:
if trackerfilter and not trackerfilter==tracker:
continue
cherrypy.engine.publish("regen",tracker,bugid)
def update_trackers(self,force=False):
from os import getenv
import subprocess
def log(msg,traceback=False):
cherrypy.log(msg=msg,traceback=traceback,context="REFRESH")
with self.tracker_update_lock:
if not force and self.last_tracker_update+timedelta(minutes=5)>datetime.now():
return
log("Updating trackerlist")
try:
with self.dbpool.get_connection() as conn, conn.cursor() as cur:
script=getenv("ACIT_LIST_TRACKERS","/usr/lib/acit-list-trackers")
proc=subprocess.run(script,capture_output=True)
cur.execute("SELECT name FROM trackers")
old_trackers=[ item[0] for item in cur ]
if proc.stderr:
log("Refresh script generated STDERR:")
log(proc.stderr)
rows=proc.stdout.decode().split("\n")
for row in rows:
if row: # skip empty lines
data=row.split("\t")
if not len(data)==2:
log("Error processing line:\n> %s\nWeird value count (expected 2)."%row)
continue
name,homepage=data
#log(name)
cur.execute("REPLACE INTO trackers (name,homepage) VALUES (?,?)",(name,homepage))
conn.commit()
#self.update_all_bugpages(conn,cur,trackerfilter=name)
if not name in old_trackers:
cherrypy.engine.publish("regen",name,None)
self.last_tracker_update=datetime.now()
except Exception:
cherrypy.log("Error refreshing trackers using script.",traceback=True,context="SITE")
def getbug(self,tracker:str,bugid:int):
# make sure bug exists
with self.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("SELECT 1 FROM bugs WHERE tracker=? AND bugid=? LIMIT 1",(tracker,bugid))
if not cur.fetchone():
return None
# fetch already existent bug object if possible, otherwise make new one
if (tracker,bugid) in self.bugcache:
bug=self.bugcache[(tracker,bugid)]
else:
bug=Bug(self,tracker,bugid)
self.bugcache[(tracker,bugid)]=bug
return bug
def newbug(self,tracker:str,bugtype:str):
if not bugtype in BUGTYPES:
raise ValueError("Bugtype illegal")
with self.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("SELECT bugid FROM bugs WHERE tracker=? ORDER BY bugid DESC LIMIT 1",(tracker,))
nr=cur.fetchone()
if not nr:
nr=0
else:
nr=nr[0]
nr+=1
cur.execute("INSERT INTO bugs VALUES (?,?,?,?,?,?)",(tracker,nr,"","","OPEN",bugtype))
conn.commit()
return self.getbug(tracker=tracker,bugid=nr)
def gettracker(self,tracker:str):
# make sure bug exists
with self.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("SELECT 1 FROM bugs WHERE tracker=? LIMIT 1",(tracker,))
if not cur.fetchone():
return None
# fetch already existent bug object if possible, otherwise make new one
if (tracker,) in self.bugcache:
trackerobj=self.bugcache[(tracker,)]
else:
trackerobj=Tracker(self,tracker)
self.bugcache[(tracker,)]=trackerobj
return trackerobj
def findtrackers(self,query:str):
self.update_trackers()
with self.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("SELECT name FROM trackers")
trackers:list[str]=[ value[0] for value in cur.fetchall() ]
results=[]
for tracker in trackers:
if tracker==query:
return [tracker]
else:
if tracker.endswith(query):
results.append(tracker)
return results
class Bug():
def __init__(self,site:Site,tracker:str,bugid:int):
self.site=site
self.tracker=tracker
self.bugid=bugid
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("SELECT subject,description,status,type FROM bugs WHERE tracker=? AND bugid=? LIMIT 1",(tracker,bugid))
data=cur.fetchone()
if not data:
raise ValueError("Bug %s#%d does not exists!"%(tracker,bugid))
self._cache={
"subject":data[0],
"description":data[1],
"status":data[2],
"type":data[3]
}
# NOTE: this is a lot of repeated code, maybe we can rewrite this into a __setattr__ and __getattribute__ method?
def __repr__(self):
return "<acit.types.Bug\n\tsubject=%s\n\ttype=%s\n\tstatus=%s\n\t>"%(self.subject,self.type,self.status)
@property
def subject(self):
"The subject of this bug"
return self._cache["subject"]
@subject.setter
def subject(self,value):
if len(value)>1024:
raise ValueError("Subject length not allowed to be higher than 1024, this is %d"%len(value))
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("UPDATE bugs SET subject=? WHERE tracker=? AND bugid=?",(value,self.tracker,self.bugid))
conn.commit()
self._cache["subject"]=value
@property
def description(self):
"The description of this bug"
return self._cache["description"]
@description.setter
def description(self,value):
if len(value)>65535:
raise ValueError("Subject length not allowed to be higher than 65535, this is %d"%len(value))
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("UPDATE bugs SET description=? WHERE tracker=? AND bugid=?",(value,self.tracker,self.bugid))
conn.commit()
self._cache["description"]=value
@property
def status(self):
"The status of this bug"
return self._cache["status"]
@status.setter
def status(self,value):
if not value in BUGSTATUS:
raise ValueError("Invalid status given.")
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("UPDATE bugs SET status=? WHERE tracker=? AND bugid=?",(value,self.tracker,self.bugid))
conn.commit()
self._cache["status"]=value
@property
def type(self):
"The type of this bug"
return self._cache["type"]
@type.setter
def type(self,value):
if not value in BUGTYPES:
raise ValueError("Invalid type given.")
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("UPDATE bugs SET type=? WHERE tracker=? AND bugid=?",(value,self.tracker,self.bugid))
conn.commit()
self._cache["type"]=value
@property
def subscribers(self):
"""
List of emailaddresses subscribed. Readonly, to add or remove, use appropriate methods.
NOTE: this executes an SQL query.
"""
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("SELECT email FROM subscribers WHERE tracker=? AND bugid=?",(self.tracker,self.bugid))
return [ value[0] for value in cur ]
def addsubscriber(self,email):
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("INSERT INTO subscribers VALUES (?,?,?)",(self.tracker,self.bugid,email))
conn.commit()
def rmsubscriber(self,email):
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("DELETE FROM subscribers WHERE tracker=? AND bugid=? AND email=?)",(self.tracker,self.bugid,email))
conn.commit()
class Tracker():
def __init__(self,site:Site,tracker:str):
self.site=site
self.tracker=tracker
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("SELECT name,homepage FROM trackers WHERE name=? LIMIT 1",(tracker,))
data=cur.fetchone()
if not data:
raise ValueError("Tracker %s does not exists!"%tracker)
self._cache={
"name":data[0],
"homepage":data[1],
#"readme":data[2] # NOTE: could implement later, let's do that though
}
# NOTE: this is a lot of repeated code, maybe we can rewrite this into a __setattr__ and __getattribute__ method?
@property
def name(self):
"The name of this tracker. readonly, changing a tracker's name randomly seriously messes with the design."
return self._cache["name"]
@property
def homepage(self):
"The homepage of this tracker"
return self._cache["homepage"]
@homepage.setter
def homepage(self,value):
if len(value)>1024:
raise ValueError("Subject length not allowed to be higher than 1024, this is %d"%len(value))
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("UPDATE trackers SET homepage=? WHERE tracker=? AND bugid=?",(value,self.tracker,self.bugid))
conn.commit()
self._cache["homepage"]=value
@property
def readme_url(self):
"The readme_url of this tracker"
return self._cache["readme_url"]
@readme_url.setter
def readme_url(self,value):
if len(value)>1024:
raise ValueError("Subject length not allowed to be higher than 1024, this is %d"%len(value))
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("UPDATE trackers SET readme_url=? WHERE tracker=? AND bugid=?",(value,self.tracker,self.bugid))
conn.commit()
self._cache["readme_url"]=value
@property
def subscribers(self):
"""
List of emailaddresses subscribed. Readonly, to add or remove, use appropriate methods.
NOTE: this executes an SQL query.
"""
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("SELECT email FROM subscribers WHERE tracker=? AND bugid IS NULL",(self.tracker,))
return [ value[0] for value in cur ]
def addsubscriber(self,email):
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("INSERT INTO subscribers VALUES (?,NULL,?)",(self.tracker,email))
conn.commit()
def rmsubscriber(self,email):
with self.site.dbpool.get_connection() as conn, conn.cursor() as cur:
cur.execute("DELETE FROM subscribers WHERE tracker=? AND bugid IS NULL AND email=?)",(self.tracker,email))
conn.commit()