models.py
Python script, ASCII text executable
1__all__ = [ 2"RepoAccess", 3"RepoFavourite", 4"Repo", 5"UserFollow", 6"UserNotification", 7"User", 8"Notification", 9"PostVote", 10"Post", 11"Commit", 12"PullRequest", 13] 14 15import subprocess 16from app import app, db, bcrypt 17import git 18from datetime import datetime 19from enum import Enum 20from PIL import Image 21from cairosvg import svg2png 22import os 23import config 24import cairosvg 25import random 26import celery_tasks 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) 95default_page_length = db.Column(db.SmallInteger, nullable=False, default=32, server_default="32") 96max_post_nesting = db.Column(db.SmallInteger, nullable=False, default=3, server_default="3") 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