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.64 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
"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