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