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