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