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 197notification = Notification({"type": "commit", "repo": repo.route, "commit": sha}) 198db.session.add(notification) 199 200for user in RepoFavourite.query.filter_by(repo_route=repo.route, notify_commit=True).all(): 201db.session.add(UserNotification(user.user, notification, 1)) 202db.session.commit() 203 204 205class Post(db.Model): 206identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True) 207number = db.Column(db.Integer, nullable=False) 208repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 209owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 210votes = db.relationship("PostVote", back_populates="post") 211vote_sum = db.Column(db.Integer, nullable=False, default=0) 212 213parent_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True) 214root_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True) 215state = db.Column(db.SmallInteger, nullable=True, default=1) 216 217date = db.Column(db.DateTime, default=datetime.now) 218last_updated = db.Column(db.DateTime, default=datetime.now) 219subject = db.Column(db.Unicode(384)) 220message = db.Column(db.UnicodeText) 221repo = db.relationship("Repo", back_populates="posts") 222owner = db.relationship("User", back_populates="posts") 223parent = db.relationship("Post", back_populates="children", 224primaryjoin="Post.parent_id==Post.identifier", 225foreign_keys="[Post.parent_id]", remote_side="Post.identifier") 226root = db.relationship("Post", 227primaryjoin="Post.root_id==Post.identifier", 228foreign_keys="[Post.root_id]", remote_side="Post.identifier") 229children = db.relationship("Post", 230remote_side="Post.parent_id", 231primaryjoin="Post.identifier==Post.parent_id", 232foreign_keys="[Post.parent_id]") 233 234def __init__(self, owner, repo, parent, subject, message): 235self.identifier = f"{repo.route}/{repo.last_post_id}" 236self.number = repo.last_post_id 237self.repo_name = repo.route 238self.repo = repo 239self.owner_name = owner.username 240self.owner = owner 241self.subject = subject 242self.message = message 243self.parent = parent 244if parent and parent.parent: 245self.root = parent.parent 246elif parent: 247self.root = parent 248else: 249self.root = None 250repo.last_post_id += 1 251 252def update_date(self): 253self.last_updated = datetime.now() 254with db.session.no_autoflush: 255if self.parent is not None: 256self.parent.update_date() 257 258 259class UserNotification(db.Model): 260id = db.Column(db.Integer, primary_key=True) 261user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 262notification_id = db.Column(db.BigInteger, db.ForeignKey("notification.id")) 263attention_level = db.Column(db.SmallInteger, nullable=False) # 0 is read 264read_time = db.Column(db.DateTime, nullable=True) 265 266user = db.relationship("User", back_populates="notifications") 267notification = db.relationship("Notification", back_populates="notifications") 268 269__table_args__ = (db.UniqueConstraint("user_username", "notification_id", name="_user_notification_uc"),) 270 271def __init__(self, user, notification, level): 272self.user_username = user.username 273self.notification_id = notification.id 274self.attention_level = level 275 276def mark_read(self): 277self.read_time = datetime.utcnow() 278self.attention_level = 0 279 280def mark_unread(self): 281self.attention_level = 4 282 283 284class UserFollow(db.Model): 285id = db.Column(db.Integer, primary_key=True) 286follower_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False) 287followed_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False) 288 289follower = db.relationship("User", back_populates="followers", foreign_keys=[follower_username]) 290followed = db.relationship("User", back_populates="follows", foreign_keys=[followed_username]) 291 292def __init__(self, follower_username, followed_username): 293self.follower_username = follower_username 294self.followed_username = followed_username 295 296 297class Notification(db.Model): 298id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) 299data = db.Column(db.dialects.postgresql.JSONB, nullable=False, default={}) 300notifications = db.relationship("UserNotification", back_populates="notification") 301timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now) 302 303def __init__(self, json): 304self.data = json 305 306 307class PullRequest(db.Model): 308id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) 309head_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False) 310base_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False) 311owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 312state = db.Column(db.SmallInteger, nullable=False, default=0) # 0 pending, 1 merged, 2 rejected 313 314head = db.relationship("Repo", back_populates="heads", foreign_keys=[head_route]) 315base = db.relationship("Repo", back_populates="bases", foreign_keys=[base_route]) 316 317head_branch = db.Column(db.String(64), nullable=False) 318base_branch = db.Column(db.String(64), nullable=False) 319 320owner = db.relationship("User", back_populates="prs") 321timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now) 322 323def __init__(self, head, head_branch, base, base_branch, owner): 324self.head = head 325self.base = base 326self.head_branch = head_branch 327self.base_branch = base_branch 328self.owner = owner 329