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 mark_read(self): 267self.read_time = datetime.utcnow() 268self.attention_level = 0 269 270def mark_unread(self): 271self.attention_level = 4 272 273 274class UserFollow(db.Model): 275id = db.Column(db.Integer, primary_key=True) 276follower_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False) 277followed_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False) 278 279follower = db.relationship("User", back_populates="followers", foreign_keys=[follower_username]) 280followed = db.relationship("User", back_populates="follows", foreign_keys=[followed_username]) 281 282def __init__(self, follower_username, followed_username): 283self.follower_username = follower_username 284self.followed_username = followed_username 285 286 287class Notification(db.Model): 288id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) 289data = db.Column(db.dialects.postgresql.JSONB, nullable=False, default={}) 290notifications = db.relationship("UserNotification", back_populates="notification") 291timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now) 292 293def __init__(self, json): 294self.data = json 295 296 297class PullRequest(db.Model): 298id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) 299head_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False) 300base_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False) 301owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 302state = db.Column(db.SmallInteger, nullable=False, default=0) # 0 pending, 1 merged, 2 rejected 303 304head = db.relationship("Repo", back_populates="heads", foreign_keys=[head_route]) 305base = db.relationship("Repo", back_populates="bases", foreign_keys=[base_route]) 306 307head_branch = db.Column(db.String(64), nullable=False) 308base_branch = db.Column(db.String(64), nullable=False) 309 310owner = db.relationship("User", back_populates="prs") 311timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now) 312 313def __init__(self, head, head_branch, base, base_branch, owner): 314self.head = head 315self.base = base 316self.head_branch = head_branch 317self.base_branch = base_branch 318self.owner = owner 319