You're looking at it

Homepage: https://roundabout-host.com

By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 models.py

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