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