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 164has_site = db.Column(db.SmallInteger, nullable=False, default=0, # 0 means no site, 1 means it's got a site, 2 means it's the user's primary site 165server_default="0") # (the one accessible at username.localhost) 166site_branch = db.Column(db.String(64), nullable=True) 167 168last_post_id = db.Column(db.Integer, nullable=False, default=0) 169 170def __init__(self, owner, name, visibility): 171self.route = f"/{owner.username}/{name}" 172self.name = name 173self.owner_name = owner.username 174self.owner = owner 175self.visibility = visibility 176 177# Add the owner as an admin 178repo_access = RepoAccess(owner, self, 2) 179db.session.add(repo_access) 180 181# Create the directory 182if not os.path.exists(os.path.join(config.REPOS_PATH, self.owner_name, self.name)): 183subprocess.run(["git", "init", self.name], 184cwd=os.path.join(config.REPOS_PATH, self.owner_name)) 185 186 187class Commit(db.Model): 188identifier = db.Column(db.String(227), unique=True, nullable=False, primary_key=True) 189sha = db.Column(db.String(128), nullable=False) 190repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 191owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 192owner_identity = db.Column(db.String(321)) 193receive_date = db.Column(db.DateTime, default=datetime.now) 194author_date = db.Column(db.DateTime) 195message = db.Column(db.UnicodeText) 196repo = db.relationship("Repo", back_populates="commits") 197owner = db.relationship("User", back_populates="commits") 198 199def __init__(self, sha, owner, repo, date, message, owner_identity): 200self.identifier = f"{repo.route}/{sha}" 201self.sha = sha 202self.repo_name = repo.route 203self.repo = repo 204self.owner_name = owner.username 205self.owner = owner 206self.author_date = datetime.fromtimestamp(int(date)) 207self.message = message 208self.owner_identity = owner_identity 209 210notification = Notification({"type": "commit", "repo": repo.route, "commit": sha}) 211db.session.add(notification) 212db.session.flush() # save the notification to get the ID 213 214# Send a notification to all users who have enabled commit notifications for this repo 215for relationship in RepoFavourite.query.filter_by(repo_route=repo.route, notify_commit=True).all(): 216user = relationship.user 217user_notification = UserNotification(user, notification, 1) 218db.session.add(user_notification) 219db.session.flush() 220celery_tasks.send_notification.apply_async(args=[user_notification.id]) 221 222 223class Post(db.Model): 224identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True) 225number = db.Column(db.Integer, nullable=False) 226repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 227owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 228votes = db.relationship("PostVote", back_populates="post") 229vote_sum = db.Column(db.Integer, nullable=False, default=0) 230 231parent_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True) 232root_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True) 233state = db.Column(db.SmallInteger, nullable=True, default=1) 234 235date = db.Column(db.DateTime, default=datetime.now) 236last_updated = db.Column(db.DateTime, default=datetime.now) 237subject = db.Column(db.Unicode(384)) 238message = db.Column(db.UnicodeText) 239repo = db.relationship("Repo", back_populates="posts") 240owner = db.relationship("User", back_populates="posts") 241parent = db.relationship("Post", back_populates="children", 242primaryjoin="Post.parent_id==Post.identifier", 243foreign_keys="[Post.parent_id]", remote_side="Post.identifier") 244root = db.relationship("Post", 245primaryjoin="Post.root_id==Post.identifier", 246foreign_keys="[Post.root_id]", remote_side="Post.identifier") 247children = db.relationship("Post", 248remote_side="Post.parent_id", 249primaryjoin="Post.identifier==Post.parent_id", 250foreign_keys="[Post.parent_id]") 251 252def __init__(self, owner, repo, parent, subject, message): 253self.identifier = f"{repo.route}/{repo.last_post_id}" 254self.number = repo.last_post_id 255self.repo_name = repo.route 256self.repo = repo 257self.owner_name = owner.username 258self.owner = owner 259self.subject = subject 260self.message = message 261self.parent = parent 262if parent and parent.parent: 263self.root = parent.parent 264elif parent: 265self.root = parent 266else: 267self.root = None 268repo.last_post_id += 1 269 270def update_date(self): 271self.last_updated = datetime.now() 272with db.session.no_autoflush: 273if self.parent is not None: 274self.parent.update_date() 275 276 277class UserNotification(db.Model): 278id = db.Column(db.Integer, primary_key=True) 279user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 280notification_id = db.Column(db.BigInteger, db.ForeignKey("notification.id")) 281attention_level = db.Column(db.SmallInteger, nullable=False) # 0 is read 282read_time = db.Column(db.DateTime, nullable=True) 283 284user = db.relationship("User", back_populates="notifications") 285notification = db.relationship("Notification", back_populates="notifications") 286 287__table_args__ = (db.UniqueConstraint("user_username", "notification_id", name="_user_notification_uc"),) 288 289def __init__(self, user, notification, level): 290self.user_username = user.username 291self.notification_id = notification.id 292self.attention_level = level 293 294def mark_read(self): 295self.read_time = datetime.utcnow() 296self.attention_level = 0 297 298def mark_unread(self): 299self.attention_level = 4 300 301 302class UserFollow(db.Model): 303id = db.Column(db.Integer, primary_key=True) 304follower_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False) 305followed_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False) 306 307follower = db.relationship("User", back_populates="followers", foreign_keys=[follower_username]) 308followed = db.relationship("User", back_populates="follows", foreign_keys=[followed_username]) 309 310def __init__(self, follower_username, followed_username): 311self.follower_username = follower_username 312self.followed_username = followed_username 313 314 315class Notification(db.Model): 316id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) 317data = db.Column(db.dialects.postgresql.JSONB, nullable=False, default={}) 318notifications = db.relationship("UserNotification", back_populates="notification") 319timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now) 320 321def __init__(self, json): 322self.data = json 323 324 325class PullRequest(db.Model): 326id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) 327head_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False) 328base_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False) 329owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 330state = db.Column(db.SmallInteger, nullable=False, default=0) # 0 pending, 1 merged, 2 rejected 331 332head = db.relationship("Repo", back_populates="heads", foreign_keys=[head_route]) 333base = db.relationship("Repo", back_populates="bases", foreign_keys=[base_route]) 334 335head_branch = db.Column(db.String(64), nullable=False) 336base_branch = db.Column(db.String(64), nullable=False) 337 338owner = db.relationship("User", back_populates="prs") 339timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now) 340 341def __init__(self, head, head_branch, base, base_branch, owner): 342self.head = head 343self.base = base 344self.head_branch = head_branch 345self.base_branch = base_branch 346self.owner = owner 347