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