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