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
                "EmailChangeRequest", 
        
            14
                "Comment", 
        
            15
                "PullRequestResolvesThread", 
        
            16
                "Label", 
        
            17
                "PostLabel", 
        
            18
            ] 
        
            19
             
        
            20
            import secrets 
        
            21
            import subprocess 
        
            22
             
        
            23
            import markdown 
        
            24
            from app import app, db, bcrypt 
        
            25
            import git 
        
            26
            from datetime import datetime, timedelta 
        
            27
            from enum import Enum 
        
            28
            from PIL import Image 
        
            29
            from cairosvg import svg2png 
        
            30
            import os 
        
            31
            import config 
        
            32
            import cairosvg 
        
            33
            import random 
        
            34
            import celery_tasks 
        
            35
             
        
            36
            with (app.app_context()): 
        
            37
                class RepoAccess(db.Model): 
        
            38
                    id = db.Column(db.Integer, primary_key=True) 
        
            39
                    user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            40
                    repo_route = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 
        
            41
                    access_level = db.Column(db.SmallInteger(), nullable=False)  # 0 read-only, 1 read-write, 2 admin 
        
            42
             
        
            43
                    user = db.relationship("User", back_populates="repo_access") 
        
            44
                    repo = db.relationship("Repo", back_populates="repo_access") 
        
            45
             
        
            46
                    __table_args__ = (db.UniqueConstraint("user_username", "repo_route", name="_user_repo_uc"),) 
        
            47
             
        
            48
                    def __init__(self, user, repo, level): 
        
            49
                        self.user_username = user.username 
        
            50
                        self.repo_route = repo.route 
        
            51
                        self.access_level = level 
        
            52
             
        
            53
             
        
            54
                class RepoFavourite(db.Model): 
        
            55
                    id = db.Column(db.Integer, primary_key=True) 
        
            56
                    user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            57
                    repo_route = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 
        
            58
             
        
            59
                    notify_commit = db.Column(db.Boolean, default=False, nullable=False) 
        
            60
                    notify_forum = db.Column(db.Boolean, default=False, nullable=False) 
        
            61
                    notify_pr = db.Column(db.Boolean, default=False, nullable=False) 
        
            62
                    notify_admin = db.Column(db.Boolean, default=False, nullable=False) 
        
            63
             
        
            64
                    user = db.relationship("User", back_populates="favourites") 
        
            65
                    repo = db.relationship("Repo", back_populates="favourites") 
        
            66
             
        
            67
                    __table_args__ = (db.UniqueConstraint("user_username", "repo_route", name="_user_repo_uc1"),) 
        
            68
             
        
            69
                    def __init__(self, user, repo): 
        
            70
                        self.user_username = user.username 
        
            71
                        self.repo_route = repo.route 
        
            72
             
        
            73
             
        
            74
                class PostVote(db.Model): 
        
            75
                    id = db.Column(db.Integer, primary_key=True) 
        
            76
                    user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            77
                    post_identifier = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=False) 
        
            78
                    vote_score = db.Column(db.SmallInteger(), nullable=False) 
        
            79
             
        
            80
                    user = db.relationship("User", back_populates="votes") 
        
            81
                    post = db.relationship("Post", back_populates="votes") 
        
            82
             
        
            83
                    __table_args__ = (db.UniqueConstraint("user_username", "post_identifier", name="_user_post_uc"),) 
        
            84
             
        
            85
                    def __init__(self, user, post, score): 
        
            86
                        self.user_username = user.username 
        
            87
                        self.post_identifier = post.identifier 
        
            88
                        self.vote_score = score 
        
            89
             
        
            90
             
        
            91
                class User(db.Model): 
        
            92
                    username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True) 
        
            93
                    display_name = db.Column(db.Unicode(128), unique=False, nullable=True) 
        
            94
                    bio = db.Column(db.Unicode(16384), unique=False, nullable=True) 
        
            95
                    password_hashed = db.Column(db.String(60), nullable=False) 
        
            96
                    email = db.Column(db.String(254), nullable=True) 
        
            97
                    company = db.Column(db.Unicode(64), nullable=True) 
        
            98
                    company_URL = db.Column(db.String(256), nullable=True) 
        
            99
                    URL = db.Column(db.String(256), nullable=True) 
        
            100
                    show_mail = db.Column(db.Boolean, default=False, nullable=False) 
        
            101
                    location = db.Column(db.Unicode(64), nullable=True) 
        
            102
                    creation_date = db.Column(db.DateTime, default=datetime.utcnow) 
        
            103
                    default_page_length = db.Column(db.SmallInteger, nullable=False, default=32, server_default="32") 
        
            104
                    max_post_nesting = db.Column(db.SmallInteger, nullable=False, default=3, server_default="3") 
        
            105
             
        
            106
                    repositories = db.relationship("Repo", back_populates="owner", cascade="all, delete-orphan") 
        
            107
                    followers = db.relationship("UserFollow", back_populates="followed", foreign_keys="[UserFollow.followed_username]") 
        
            108
                    follows = db.relationship("UserFollow", back_populates="follower", foreign_keys="[UserFollow.follower_username]") 
        
            109
                    email_change_requests = db.relationship("EmailChangeRequest", back_populates="user") 
        
            110
                    repo_access = db.relationship("RepoAccess", back_populates="user") 
        
            111
                    votes = db.relationship("PostVote", back_populates="user") 
        
            112
                    favourites = db.relationship("RepoFavourite", back_populates="user") 
        
            113
             
        
            114
                    commits = db.relationship("Commit", back_populates="owner") 
        
            115
                    posts = db.relationship("Post", back_populates="owner") 
        
            116
                    comments = db.relationship("Comment", back_populates="owner") 
        
            117
                    prs = db.relationship("PullRequest", back_populates="owner") 
        
            118
                    notifications = db.relationship("UserNotification", back_populates="user") 
        
            119
             
        
            120
                    def __init__(self, username, password, email=None, display_name=None): 
        
            121
                        self.username = username 
        
            122
                        self.password_hashed = bcrypt.generate_password_hash(password, config.HASHING_ROUNDS).decode("utf-8") 
        
            123
                        self.email = "" 
        
            124
                        if email: 
        
            125
                            email_change_request = EmailChangeRequest(self, email) 
        
            126
                            db.session.add(email_change_request) 
        
            127
                            db.session.flush() 
        
            128
                        self.display_name = display_name 
        
            129
             
        
            130
                        # Create the user's directory 
        
            131
                        if not os.path.exists(os.path.join(config.REPOS_PATH, username)): 
        
            132
                            os.makedirs(os.path.join(config.REPOS_PATH, username)) 
        
            133
                        if not os.path.exists(os.path.join(config.USERDATA_PATH, username)): 
        
            134
                            os.makedirs(os.path.join(config.USERDATA_PATH, username)) 
        
            135
             
        
            136
                        avatar_name = random.choice(os.listdir(config.DEFAULT_AVATARS_PATH)) 
        
            137
                        if os.path.join(config.DEFAULT_AVATARS_PATH, avatar_name).endswith(".svg"): 
        
            138
                            cairosvg.svg2png(url=os.path.join(config.DEFAULT_AVATARS_PATH, avatar_name), 
        
            139
                                             write_to="/tmp/roundabout-avatar.png") 
        
            140
                            avatar = Image.open("/tmp/roundabout-avatar.png") 
        
            141
                        else: 
        
            142
                            avatar = Image.open(os.path.join(config.DEFAULT_AVATARS_PATH, avatar_name)) 
        
            143
                        avatar.thumbnail(config.AVATAR_SIZE) 
        
            144
                        avatar.save(os.path.join(config.USERDATA_PATH, username, "avatar.png")) 
        
            145
             
        
            146
                        # Create the configuration repo 
        
            147
                        config_repo = Repo(self, ".config", 0) 
        
            148
                        db.session.add(config_repo) 
        
            149
                        notification = Notification({"type": "welcome"}) 
        
            150
                        db.session.add(notification) 
        
            151
                        db.session.commit() 
        
            152
             
        
            153
                        user_notification = UserNotification(self, notification, 1) 
        
            154
                        db.session.add(user_notification) 
        
            155
                        db.session.commit() 
        
            156
                        celery_tasks.send_notification.apply_async(args=[user_notification.id]) 
        
            157
             
        
            158
             
        
            159
                class Repo(db.Model): 
        
            160
                    route = db.Column(db.String(98), unique=True, nullable=False, primary_key=True) 
        
            161
                    owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            162
                    name = db.Column(db.String(64), nullable=False) 
        
            163
                    owner = db.relationship("User", back_populates="repositories") 
        
            164
                    visibility = db.Column(db.SmallInteger(), nullable=False) 
        
            165
                    info = db.Column(db.Unicode(512), nullable=True) 
        
            166
                    url = db.Column(db.String(256), nullable=True) 
        
            167
                    creation_date = db.Column(db.DateTime, default=datetime.utcnow) 
        
            168
             
        
            169
                    default_branch = db.Column(db.String(64), nullable=True, default="") 
        
            170
             
        
            171
                    commits = db.relationship("Commit", back_populates="repo", cascade="all, delete-orphan") 
        
            172
                    posts = db.relationship("Post", back_populates="repo", cascade="all, delete-orphan") 
        
            173
                    comments = db.relationship("Comment", back_populates="repo", 
        
            174
                                               cascade="all, delete-orphan") 
        
            175
                    repo_access = db.relationship("RepoAccess", back_populates="repo", 
        
            176
                                                  cascade="all, delete-orphan") 
        
            177
                    favourites = db.relationship("RepoFavourite", back_populates="repo", 
        
            178
                                                 cascade="all, delete-orphan") 
        
            179
                    heads = db.relationship("PullRequest", back_populates="head", 
        
            180
                                            foreign_keys="[PullRequest.head_route]", 
        
            181
                                            cascade="all, delete-orphan") 
        
            182
                    bases = db.relationship("PullRequest", back_populates="base", 
        
            183
                                            foreign_keys="[PullRequest.base_route]", 
        
            184
                                            cascade="all, delete-orphan") 
        
            185
                    labels = db.relationship("Label", back_populates="repo", cascade="all, delete-orphan") 
        
            186
             
        
            187
                    has_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 
        
            188
                                         server_default="0")                                 # (the one accessible at username.localhost) 
        
            189
                    site_branch = db.Column(db.String(64), nullable=True) 
        
            190
             
        
            191
                    last_post_id = db.Column(db.Integer, nullable=False, default=0, server_default="0") 
        
            192
                    last_comment_id = db.Column(db.Integer, nullable=False, default=0, server_default="0") 
        
            193
             
        
            194
                    def __init__(self, owner, name, visibility): 
        
            195
                        self.route = f"/{owner.username}/{name}" 
        
            196
                        self.name = name 
        
            197
                        self.owner_name = owner.username 
        
            198
                        self.owner = owner 
        
            199
                        self.visibility = visibility 
        
            200
             
        
            201
                        # Add the owner as an admin 
        
            202
                        repo_access = RepoAccess(owner, self, 2) 
        
            203
                        db.session.add(repo_access) 
        
            204
             
        
            205
                        # Create the directory 
        
            206
                        if not os.path.exists(os.path.join(config.REPOS_PATH, self.owner_name, self.name)): 
        
            207
                            subprocess.run(["git", "init", self.name], 
        
            208
                                           cwd=os.path.join(config.REPOS_PATH, self.owner_name)) 
        
            209
             
        
            210
             
        
            211
                class Commit(db.Model): 
        
            212
                    identifier = db.Column(db.String(227), unique=True, nullable=False, primary_key=True) 
        
            213
                    sha = db.Column(db.String(128), nullable=False) 
        
            214
                    repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 
        
            215
                    owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            216
                    owner_identity = db.Column(db.String(321)) 
        
            217
                    receive_date = db.Column(db.DateTime, default=datetime.now) 
        
            218
                    author_date = db.Column(db.DateTime) 
        
            219
                    message = db.Column(db.UnicodeText) 
        
            220
                    repo = db.relationship("Repo", back_populates="commits") 
        
            221
                    owner = db.relationship("User", back_populates="commits") 
        
            222
             
        
            223
                    comments = db.relationship("Comment", back_populates="commit") 
        
            224
             
        
            225
                    def __init__(self, sha, owner, repo, date, message, owner_identity): 
        
            226
                        self.identifier = f"{repo.route}/{sha}" 
        
            227
                        self.sha = sha 
        
            228
                        self.repo_name = repo.route 
        
            229
                        self.repo = repo 
        
            230
                        self.owner_name = owner.username 
        
            231
                        self.owner = owner 
        
            232
                        self.author_date = datetime.fromtimestamp(int(date)) 
        
            233
                        self.message = message 
        
            234
                        self.owner_identity = owner_identity 
        
            235
             
        
            236
                        notification = Notification({"type": "commit", "repo": repo.route, "commit": sha}) 
        
            237
                        db.session.add(notification) 
        
            238
                        db.session.commit()              # save the notification to get the ID 
        
            239
             
        
            240
                        # Send a notification to all users who have enabled commit notifications for this repo 
        
            241
                        for relationship in RepoFavourite.query.filter_by(repo_route=repo.route, notify_commit=True).all(): 
        
            242
                            user = relationship.user 
        
            243
                            user_notification = UserNotification(user, notification, 1) 
        
            244
                            db.session.add(user_notification) 
        
            245
                            db.session.commit() 
        
            246
                            celery_tasks.send_notification.apply_async(args=[user_notification.id]) 
        
            247
             
        
            248
             
        
            249
                class Label(db.Model): 
        
            250
                    identifier = db.Column(db.String(162), unique=True, nullable=False, primary_key=True) 
        
            251
                    repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 
        
            252
                    name = db.Column(db.Unicode(64), nullable=False) 
        
            253
                    colour = db.Column(db.Integer, nullable=False, server_default="0") 
        
            254
             
        
            255
                    repo = db.relationship("Repo", back_populates="labels") 
        
            256
                    posts = db.relationship("PostLabel", back_populates="label") 
        
            257
             
        
            258
                    def __init__(self, repo, name, colour): 
        
            259
                        self.identifier = f"{repo.route}/" + secrets.token_hex(32)    # randomise label IDs 
        
            260
                        self.name = name 
        
            261
                        self.colour = int(colour.removeprefix("#"), 16) 
        
            262
                        self.repo_name = repo.route 
        
            263
             
        
            264
                    @property 
        
            265
                    def colour_hex(self): 
        
            266
                        return f"#{self.colour:06x}" 
        
            267
             
        
            268
                    @colour_hex.setter 
        
            269
                    def colour_hex(self, value): 
        
            270
                        self.colour = int(value.removeprefix("#"), 16) 
        
            271
             
        
            272
             
        
            273
                class PostLabel(db.Model): 
        
            274
                    id = db.Column(db.Integer, primary_key=True) 
        
            275
                    post_identifier = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=False) 
        
            276
                    label_identifier = db.Column(db.String(162), db.ForeignKey("label.identifier"), nullable=False) 
        
            277
             
        
            278
                    post = db.relationship("Post", back_populates="labels") 
        
            279
                    label = db.relationship("Label", back_populates="posts") 
        
            280
             
        
            281
                    def __init__(self, post, label): 
        
            282
                        self.post_identifier = post.identifier 
        
            283
                        self.post = post 
        
            284
                        self.label = label 
        
            285
             
        
            286
             
        
            287
                class Post(db.Model): 
        
            288
                    identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True) 
        
            289
                    number = db.Column(db.Integer, nullable=False) 
        
            290
                    repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 
        
            291
                    owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            292
                    votes = db.relationship("PostVote", back_populates="post") 
        
            293
                    vote_sum = db.Column(db.Integer, nullable=False, default=0) 
        
            294
             
        
            295
                    parent_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True) 
        
            296
                    root_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True) 
        
            297
                    state = db.Column(db.SmallInteger, nullable=True, default=1) 
        
            298
             
        
            299
                    date = db.Column(db.DateTime, default=datetime.now) 
        
            300
                    last_updated = db.Column(db.DateTime, default=datetime.now) 
        
            301
                    subject = db.Column(db.Unicode(384)) 
        
            302
                    message = db.Column(db.UnicodeText) 
        
            303
                    html = db.Column(db.UnicodeText) 
        
            304
                    repo = db.relationship("Repo", back_populates="posts") 
        
            305
                    owner = db.relationship("User", back_populates="posts") 
        
            306
                    parent = db.relationship("Post", back_populates="children", 
        
            307
                                             primaryjoin="Post.parent_id==Post.identifier", 
        
            308
                                             foreign_keys="[Post.parent_id]", remote_side="Post.identifier") 
        
            309
                    root = db.relationship("Post", 
        
            310
                                           primaryjoin="Post.root_id==Post.identifier", 
        
            311
                                           foreign_keys="[Post.root_id]", remote_side="Post.identifier", post_update=True) 
        
            312
                    children = db.relationship("Post", 
        
            313
                                               remote_side="Post.parent_id", 
        
            314
                                               primaryjoin="Post.identifier==Post.parent_id", 
        
            315
                                               foreign_keys="[Post.parent_id]") 
        
            316
                    resolved_by = db.relationship("PullRequestResolvesThread", back_populates="post") 
        
            317
                    labels = db.relationship("PostLabel", back_populates="post") 
        
            318
             
        
            319
                    def __init__(self, owner, repo, parent, subject, message): 
        
            320
                        self.identifier = f"{repo.route}/{repo.last_post_id}" 
        
            321
                        self.number = repo.last_post_id 
        
            322
                        self.repo_name = repo.route 
        
            323
                        self.repo = repo 
        
            324
                        self.owner_name = owner.username 
        
            325
                        self.owner = owner 
        
            326
                        self.subject = subject 
        
            327
                        self.message = message 
        
            328
                        self.html = markdown.markdown2html(message).prettify() 
        
            329
                        self.parent = parent 
        
            330
                        if parent: 
        
            331
                            self.root = parent.root 
        
            332
                        else: 
        
            333
                            self.root = self 
        
            334
                        repo.last_post_id += 1 
        
            335
             
        
            336
                        notification = Notification({"type": "post", "repo": repo.route, "post": self.identifier}) 
        
            337
                        db.session.add(notification) 
        
            338
                        db.session.commit()              # save the notification to get the ID 
        
            339
             
        
            340
                        # Send a notification to all users who have enabled forum notifications for this repo 
        
            341
                        for relationship in RepoFavourite.query.filter_by(repo_route=repo.route, notify_forum=True).all(): 
        
            342
                            user = relationship.user 
        
            343
                            user_notification = UserNotification(user, notification, 1) 
        
            344
                            db.session.add(user_notification) 
        
            345
                            db.session.commit() 
        
            346
                            celery_tasks.send_notification.apply_async(args=[user_notification.id]) 
        
            347
             
        
            348
                    def update_date(self): 
        
            349
                        self.last_updated = datetime.now() 
        
            350
                        with db.session.no_autoflush: 
        
            351
                            if self.parent is not None: 
        
            352
                                self.parent.update_date() 
        
            353
                 
        
            354
                 
        
            355
                class Comment(db.Model): 
        
            356
                    identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True) 
        
            357
                    number = db.Column(db.Integer, nullable=False) 
        
            358
                    repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False) 
        
            359
                    owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            360
                    commit_identifier = db.Column(db.String(227), db.ForeignKey("commit.identifier"), nullable=False) 
        
            361
                    pr_id = db.Column(db.BigInteger, db.ForeignKey("pull_request.id"), nullable=True) 
        
            362
             
        
            363
                    file = db.Column(db.String(256), nullable=True) 
        
            364
                    line_number = db.Column(db.Integer, nullable=True) 
        
            365
                    line_type = db.Column(db.SmallInteger, nullable=True, default=0, server_default="0")            # 0 is deleted, 1 is modified 
        
            366
             
        
            367
                    state = db.Column(db.SmallInteger, nullable=True, default=1) 
        
            368
                    review = db.Column(db.SmallInteger, nullable=True, default=0) 
        
            369
             
        
            370
                    date = db.Column(db.DateTime, default=datetime.now) 
        
            371
                    message = db.Column(db.UnicodeText) 
        
            372
                    html = db.Column(db.UnicodeText) 
        
            373
             
        
            374
                    repo = db.relationship("Repo", back_populates="comments") 
        
            375
                    owner = db.relationship("User", back_populates="comments") 
        
            376
                    commit = db.relationship("Commit", back_populates="comments") 
        
            377
             
        
            378
                    def __init__(self, owner, repo, commit, message, file, line_number, pr=None): 
        
            379
                        self.identifier = f"{repo.route}/{repo.last_comment_id}" 
        
            380
                        self.number = repo.last_comment_id 
        
            381
                        self.repo_name = repo.route 
        
            382
                        self.repo = repo 
        
            383
                        self.owner_name = owner.username 
        
            384
                        self.owner = owner 
        
            385
                        self.commit_identifier = commit.identifier 
        
            386
                        self.commit = commit 
        
            387
                        self.message = message 
        
            388
                        self.html = markdown.markdown2html(message).prettify() 
        
            389
                        self.file = file 
        
            390
                        self.line_number = int(line_number[1:]) 
        
            391
                        self.line_type = int(line_number[0] == "+") 
        
            392
                        if pr: 
        
            393
                            self.pr = pr 
        
            394
             
        
            395
                        repo.last_comment_id += 1 
        
            396
             
        
            397
                    @property 
        
            398
                    def text(self): 
        
            399
                        return self.html 
        
            400
             
        
            401
                    @text.setter 
        
            402
                    def text(self, value): 
        
            403
                        self.html = markdown.markdown2html(value).prettify() 
        
            404
                        self.message = value     # message is stored in markdown format for future editing or plaintext display 
        
            405
             
        
            406
             
        
            407
                class UserNotification(db.Model): 
        
            408
                    id = db.Column(db.Integer, primary_key=True) 
        
            409
                    user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            410
                    notification_id = db.Column(db.BigInteger, db.ForeignKey("notification.id")) 
        
            411
                    attention_level = db.Column(db.SmallInteger, nullable=False)  # 0 is read 
        
            412
                    read_time = db.Column(db.DateTime, nullable=True) 
        
            413
             
        
            414
                    user = db.relationship("User", back_populates="notifications") 
        
            415
                    notification = db.relationship("Notification", back_populates="notifications") 
        
            416
             
        
            417
                    __table_args__ = (db.UniqueConstraint("user_username", "notification_id", name="_user_notification_uc"),) 
        
            418
             
        
            419
                    def __init__(self, user, notification, level): 
        
            420
                        self.user_username = user.username 
        
            421
                        self.notification_id = notification.id 
        
            422
                        self.attention_level = level 
        
            423
             
        
            424
                    def mark_read(self): 
        
            425
                        self.read_time = datetime.utcnow() 
        
            426
                        self.attention_level = 0 
        
            427
             
        
            428
                    def mark_unread(self): 
        
            429
                        self.attention_level = 4 
        
            430
             
        
            431
             
        
            432
                class UserFollow(db.Model): 
        
            433
                    id = db.Column(db.Integer, primary_key=True) 
        
            434
                    follower_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False) 
        
            435
                    followed_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False) 
        
            436
             
        
            437
                    follower = db.relationship("User", back_populates="followers", foreign_keys=[follower_username]) 
        
            438
                    followed = db.relationship("User", back_populates="follows", foreign_keys=[followed_username]) 
        
            439
             
        
            440
                    def __init__(self, follower_username, followed_username): 
        
            441
                        self.follower_username = follower_username 
        
            442
                        self.followed_username = followed_username 
        
            443
             
        
            444
             
        
            445
                class Notification(db.Model): 
        
            446
                    id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) 
        
            447
                    data = db.Column(db.dialects.postgresql.JSONB, nullable=False, default={}) 
        
            448
                    notifications = db.relationship("UserNotification", back_populates="notification") 
        
            449
                    timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now) 
        
            450
             
        
            451
                    def __init__(self, json): 
        
            452
                        self.data = json 
        
            453
             
        
            454
             
        
            455
                class PullRequestResolvesThread(db.Model): 
        
            456
                    id = db.Column(db.Integer, primary_key=True) 
        
            457
                    pr_id = db.Column(db.BigInteger, db.ForeignKey("pull_request.id"), nullable=False) 
        
            458
                    post_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=False) 
        
            459
             
        
            460
                    pr = db.relationship("PullRequest", back_populates="resolves") 
        
            461
                    post = db.relationship("Post", back_populates="resolved_by") 
        
            462
             
        
            463
                    def __init__(self, pr, post): 
        
            464
                        self.pr = pr 
        
            465
                        self.post = post 
        
            466
             
        
            467
             
        
            468
                class PullRequest(db.Model): 
        
            469
                    id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) 
        
            470
                    head_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False) 
        
            471
                    base_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False) 
        
            472
                    owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            473
                    state = db.Column(db.SmallInteger, nullable=False, default=0)                # 0 pending, 1 merged, 2 rejected 
        
            474
             
        
            475
                    head = db.relationship("Repo", back_populates="heads", foreign_keys=[head_route]) 
        
            476
                    base = db.relationship("Repo", back_populates="bases", foreign_keys=[base_route]) 
        
            477
             
        
            478
                    head_branch = db.Column(db.String(64), nullable=False) 
        
            479
                    base_branch = db.Column(db.String(64), nullable=False) 
        
            480
             
        
            481
                    owner = db.relationship("User", back_populates="prs") 
        
            482
                    resolves = db.relationship("PullRequestResolvesThread", back_populates="pr") 
        
            483
                    timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now) 
        
            484
             
        
            485
                    def __init__(self, head, head_branch, base, base_branch, owner): 
        
            486
                        self.head = head 
        
            487
                        self.base = base 
        
            488
                        self.head_branch = head_branch 
        
            489
                        self.base_branch = base_branch 
        
            490
                        self.owner = owner 
        
            491
             
        
            492
                    @property 
        
            493
                    def resolves_list(self): 
        
            494
                        return " ".join([str(post.post.number) for post in self.resolves]) 
        
            495
             
        
            496
                    @resolves_list.setter 
        
            497
                    def resolves_list(self, value): 
        
            498
                        link_to = [Post.query.filter_by(number=int(number), repo=self.base).first() for number in value.split()] 
        
            499
                        resolved_posts = [post.post for post in self.resolves] 
        
            500
                        no_longer_resolves = list(set(resolved_posts) - set(link_to)) 
        
            501
                        for post in no_longer_resolves: 
        
            502
                            db.session.delete(PullRequestResolvesThread.query.filter_by(pr=self, post=post).first()) 
        
            503
             
        
            504
                        for post in link_to: 
        
            505
                            if post not in resolved_posts and post is not None and not post.parent:    # only top-level posts can be resolved 
        
            506
                                db.session.add(PullRequestResolvesThread(self, post)) 
        
            507
             
        
            508
                        db.session.commit() 
        
            509
             
        
            510
             
        
            511
                class EmailChangeRequest(db.Model): 
        
            512
                    id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) 
        
            513
                    user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False) 
        
            514
                    new_email = db.Column(db.String(254), nullable=False) 
        
            515
                    code = db.Column(db.String(64), nullable=False) 
        
            516
                    expires_on = db.Column(db.DateTime, nullable=False) 
        
            517
             
        
            518
                    user = db.relationship("User", back_populates="email_change_requests") 
        
            519
             
        
            520
                    def __init__(self, user, new_email): 
        
            521
                        self.user = user 
        
            522
                        self.new_email = new_email 
        
            523
                        self.code = hex(secrets.randbits(256)).removeprefix("0x") 
        
            524
                        self.expires_on = datetime.now() + timedelta(days=1) 
        
            525
             
        
            526