models.py
Python script, ASCII text executable
1import subprocess 2 3from app import app, db, bcrypt 4import git 5from datetime import datetime 6from enum import Enum 7from PIL import Image 8from cairosvg import svg2png 9import os 10import config 11import cairosvg 12import random 13 14__all__ = [ 15"RepoAccess", 16"RepoFavourite", 17"Repo", 18"UserFollow", 19"UserNotification", 20"User", 21"Notification", 22"PostVote", 23"Post", 24"Commit", 25"PullRequest", 26] 27 28with (app.app_context()): 29class RepoAccess(db.Model): 30id = db.Column(db.Integer, primary_key=True) 31user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 32repo_route = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 33access_level = db.Column(db.SmallInteger(), nullable=False) # 0 read-only, 1 read-write, 2 admin 34 35user = db.relationship("User", back_populates="repo_access") 36repo = db.relationship("Repo", back_populates="repo_access") 37 38__table_args__ = (db.UniqueConstraint("user_username", "repo_route", name="_user_repo_uc"),) 39 40def __init__(self, user, repo, level): 41self.user_username = user.username 42self.repo_route = repo.route 43self.access_level = level 44 45 46class RepoFavourite(db.Model): 47id = db.Column(db.Integer, primary_key=True) 48user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 49repo_route = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 50 51notify_commit = db.Column(db.Boolean) 52 53user = db.relationship("User", back_populates="favourites") 54repo = db.relationship("Repo", back_populates="favourites") 55 56__table_args__ = (db.UniqueConstraint("user_username", "repo_route", name="_user_repo_uc1"),) 57 58def __init__(self, user, repo): 59self.user_username = user.username 60self.repo_route = repo.route 61 62 63class PostVote(db.Model): 64id = db.Column(db.Integer, primary_key=True) 65user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 66post_identifier = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=False) 67vote_score = db.Column(db.SmallInteger(), nullable=False) 68 69user = db.relationship("User", back_populates="votes") 70post = db.relationship("Post", back_populates="votes") 71 72__table_args__ = (db.UniqueConstraint("user_username", "post_identifier", name="_user_post_uc"),) 73 74def __init__(self, user, post, score): 75self.user_username = user.username 76self.post_identifier = post.identifier 77self.vote_score = score 78 79 80class User(db.Model): 81username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True) 82display_name = db.Column(db.Unicode(128), unique=False, nullable=True) 83bio = db.Column(db.Unicode(16384), unique=False, nullable=True) 84password_hashed = db.Column(db.String(60), nullable=False) 85email = db.Column(db.String(254), nullable=True) 86company = db.Column(db.Unicode(64), nullable=True) 87company_url = db.Column(db.String(256), nullable=True) 88url = db.Column(db.String(256), nullable=True) 89show_mail = db.Column(db.Boolean, default=False, nullable=False) 90location = db.Column(db.Unicode(64), nullable=True) 91creation_date = db.Column(db.DateTime, default=datetime.utcnow) 92 93repositories = db.relationship("Repo", back_populates="owner") 94followers = db.relationship("UserFollow", back_populates="followed", foreign_keys="[UserFollow.followed_username]") 95follows = db.relationship("UserFollow", back_populates="follower", foreign_keys="[UserFollow.follower_username]") 96repo_access = db.relationship("RepoAccess", back_populates="user") 97votes = db.relationship("PostVote", back_populates="user") 98favourites = db.relationship("RepoFavourite", back_populates="user") 99 100commits = db.relationship("Commit", back_populates="owner") 101posts = db.relationship("Post", back_populates="owner") 102prs = db.relationship("PullRequest", back_populates="owner") 103notifications = db.relationship("UserNotification", back_populates="user") 104 105def __init__(self, username, password, email=None, display_name=None): 106self.username = username 107self.password_hashed = bcrypt.generate_password_hash(password, config.HASHING_ROUNDS).decode("utf-8") 108self.email = email 109self.display_name = display_name 110 111# Create the user's directory 112if not os.path.exists(os.path.join(config.REPOS_PATH, username)): 113os.makedirs(os.path.join(config.REPOS_PATH, username)) 114if not os.path.exists(os.path.join(config.USERDATA_PATH, username)): 115os.makedirs(os.path.join(config.USERDATA_PATH, username)) 116 117avatar_name = random.choice(os.listdir(config.DEFAULT_AVATARS_PATH)) 118if os.path.join(config.DEFAULT_AVATARS_PATH, avatar_name).endswith(".svg"): 119cairosvg.svg2png(url=os.path.join(config.DEFAULT_AVATARS_PATH, avatar_name), 120write_to="/tmp/roundabout-avatar.png") 121avatar = Image.open("/tmp/roundabout-avatar.png") 122else: 123avatar = Image.open(os.path.join(config.DEFAULT_AVATARS_PATH, avatar_name)) 124avatar.thumbnail(config.AVATAR_SIZE) 125avatar.save(os.path.join(config.USERDATA_PATH, username, "avatar.png")) 126 127# Create the configuration repo 128config_repo = Repo(self, ".config", 0) 129db.session.add(config_repo) 130db.session.commit() 131 132 133class Repo(db.Model): 134route = db.Column(db.String(98), unique=True, nullable=False, primary_key=True) 135owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 136name = db.Column(db.String(64), nullable=False) 137owner = db.relationship("User", back_populates="repositories") 138visibility = db.Column(db.SmallInteger(), nullable=False) 139info = db.Column(db.Unicode(512), nullable=True) 140url = db.Column(db.String(256), nullable=True) 141creation_date = db.Column(db.DateTime, default=datetime.utcnow) 142 143default_branch = db.Column(db.String(64), nullable=True, default="") 144 145commits = db.relationship("Commit", back_populates="repo") 146posts = db.relationship("Post", back_populates="repo") 147repo_access = db.relationship("RepoAccess", back_populates="repo") 148favourites = db.relationship("RepoFavourite", back_populates="repo") 149heads = db.relationship("PullRequest", back_populates="head", foreign_keys="[PullRequest.head_route]") 150bases = db.relationship("PullRequest", back_populates="base", foreign_keys="[PullRequest.base_route]") 151 152last_post_id = db.Column(db.Integer, nullable=False, default=0) 153 154def __init__(self, owner, name, visibility): 155self.route = f"/{owner.username}/{name}" 156self.name = name 157self.owner_name = owner.username 158self.owner = owner 159self.visibility = visibility 160 161# Add the owner as an admin 162repo_access = RepoAccess(owner, self, 2) 163db.session.add(repo_access) 164 165# Create the directory 166if not os.path.exists(os.path.join(config.REPOS_PATH, self.owner_name, self.name)): 167subprocess.run(["git", "init", self.name], 168cwd=os.path.join(config.REPOS_PATH, self.owner_name)) 169 170 171class Commit(db.Model): 172identifier = db.Column(db.String(227), unique=True, nullable=False, primary_key=True) 173sha = db.Column(db.String(128), nullable=False) 174repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 175owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 176owner_identity = db.Column(db.String(321)) 177receive_date = db.Column(db.DateTime, default=datetime.now) 178author_date = db.Column(db.DateTime) 179message = db.Column(db.UnicodeText) 180repo = db.relationship("Repo", back_populates="commits") 181owner = db.relationship("User", back_populates="commits") 182 183def __init__(self, sha, owner, repo, date, message, owner_identity): 184self.identifier = f"{repo.route}/{sha}" 185self.sha = sha 186self.repo_name = repo.route 187self.repo = repo 188self.owner_name = owner.username 189self.owner = owner 190self.author_date = datetime.fromtimestamp(int(date)) 191self.message = message 192self.owner_identity = owner_identity 193 194 195class Post(db.Model): 196identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True) 197number = db.Column(db.Integer, nullable=False) 198repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 199owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 200votes = db.relationship("PostVote", back_populates="post") 201vote_sum = db.Column(db.Integer, nullable=False, default=0) 202 203parent_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True) 204root_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True) 205state = db.Column(db.SmallInteger, nullable=True, default=1) 206 207date = db.Column(db.DateTime, default=datetime.now) 208last_updated = db.Column(db.DateTime, default=datetime.now) 209subject = db.Column(db.Unicode(384)) 210message = db.Column(db.UnicodeText) 211repo = db.relationship("Repo", back_populates="posts") 212owner = db.relationship("User", back_populates="posts") 213parent = db.relationship("Post", back_populates="children", 214primaryjoin="Post.parent_id==Post.identifier", 215foreign_keys="[Post.parent_id]", remote_side="Post.identifier") 216root = db.relationship("Post", 217primaryjoin="Post.root_id==Post.identifier", 218foreign_keys="[Post.root_id]", remote_side="Post.identifier") 219children = db.relationship("Post", 220remote_side="Post.parent_id", 221primaryjoin="Post.identifier==Post.parent_id", 222foreign_keys="[Post.parent_id]") 223 224def __init__(self, owner, repo, parent, subject, message): 225self.identifier = f"{repo.route}/{repo.last_post_id}" 226self.number = repo.last_post_id 227self.repo_name = repo.route 228self.repo = repo 229self.owner_name = owner.username 230self.owner = owner 231self.subject = subject 232self.message = message 233self.parent = parent 234if parent and parent.parent: 235self.root = parent.parent 236elif parent: 237self.root = parent 238else: 239self.root = None 240repo.last_post_id += 1 241 242def update_date(self): 243self.last_updated = datetime.now() 244with db.session.no_autoflush: 245if self.parent is not None: 246self.parent.update_date() 247 248 249class UserNotification(db.Model): 250id = db.Column(db.Integer, primary_key=True) 251user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 252notification_id = db.Column(db.BigInteger, db.ForeignKey("notification.id")) 253attention_level = db.Column(db.SmallInteger, nullable=False) # 0 is read 254read_time = db.Column(db.DateTime, nullable=True) 255 256user = db.relationship("User", back_populates="notifications") 257notification = db.relationship("Notification", back_populates="notifications") 258 259__table_args__ = (db.UniqueConstraint("user_username", "notification_id", name="_user_notification_uc"),) 260 261def __init__(self, user, notification, level): 262self.user_username = user.username 263self.notification_id = notification.id 264self.attention_level = level 265 266def read(self): 267self.read_time = datetime.utcnow 268self.attention_level = 0 269 270 271class UserFollow(db.Model): 272id = db.Column(db.Integer, primary_key=True) 273follower_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False) 274followed_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False) 275 276follower = db.relationship("User", back_populates="followers", foreign_keys=[follower_username]) 277followed = db.relationship("User", back_populates="follows", foreign_keys=[followed_username]) 278 279def __init__(self, follower_username, followed_username): 280self.follower_username = follower_username 281self.followed_username = followed_username 282 283 284class Notification(db.Model): 285id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) 286data = db.Column(db.dialects.postgresql.JSONB, nullable=False, default={}) 287notifications = db.relationship("UserNotification", back_populates="notification") 288timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now) 289 290def __init__(self, json): 291self.data = json 292 293 294class PullRequest(db.Model): 295id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) 296head_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False) 297base_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False) 298owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 299state = db.Column(db.SmallInteger, nullable=False, default=0) # 0 pending, 1 merged, 2 rejected 300 301head = db.relationship("Repo", back_populates="heads", foreign_keys=[head_route]) 302base = db.relationship("Repo", back_populates="bases", foreign_keys=[base_route]) 303 304head_branch = db.Column(db.String(64), nullable=False) 305base_branch = db.Column(db.String(64), nullable=False) 306 307owner = db.relationship("User", back_populates="prs") 308timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now) 309 310def __init__(self, head, head_branch, base, base_branch, owner): 311self.head = head 312self.base = base 313self.head_branch = head_branch 314self.base_branch = base_branch 315self.owner = owner 316