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 • 23.01 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
184
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
185
server_default="0") # (the one accessible at username.localhost)
186
site_branch = db.Column(db.String(64), nullable=True)
187
188
last_post_id = db.Column(db.Integer, nullable=False, default=0, server_default="0")
189
last_comment_id = db.Column(db.Integer, nullable=False, default=0, server_default="0")
190
191
def __init__(self, owner, name, visibility):
192
self.route = f"/{owner.username}/{name}"
193
self.name = name
194
self.owner_name = owner.username
195
self.owner = owner
196
self.visibility = visibility
197
198
# Add the owner as an admin
199
repo_access = RepoAccess(owner, self, 2)
200
db.session.add(repo_access)
201
202
# Create the directory
203
if not os.path.exists(os.path.join(config.REPOS_PATH, self.owner_name, self.name)):
204
subprocess.run(["git", "init", self.name],
205
cwd=os.path.join(config.REPOS_PATH, self.owner_name))
206
207
208
class Commit(db.Model):
209
identifier = db.Column(db.String(227), unique=True, nullable=False, primary_key=True)
210
sha = db.Column(db.String(128), nullable=False)
211
repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False)
212
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
213
owner_identity = db.Column(db.String(321))
214
receive_date = db.Column(db.DateTime, default=datetime.now)
215
author_date = db.Column(db.DateTime)
216
message = db.Column(db.UnicodeText)
217
repo = db.relationship("Repo", back_populates="commits")
218
owner = db.relationship("User", back_populates="commits")
219
220
comments = db.relationship("Comment", back_populates="commit")
221
222
def __init__(self, sha, owner, repo, date, message, owner_identity):
223
self.identifier = f"{repo.route}/{sha}"
224
self.sha = sha
225
self.repo_name = repo.route
226
self.repo = repo
227
self.owner_name = owner.username
228
self.owner = owner
229
self.author_date = datetime.fromtimestamp(int(date))
230
self.message = message
231
self.owner_identity = owner_identity
232
233
notification = Notification({"type": "commit", "repo": repo.route, "commit": sha})
234
db.session.add(notification)
235
db.session.commit() # save the notification to get the ID
236
237
# Send a notification to all users who have enabled commit notifications for this repo
238
for relationship in RepoFavourite.query.filter_by(repo_route=repo.route, notify_commit=True).all():
239
user = relationship.user
240
user_notification = UserNotification(user, notification, 1)
241
db.session.add(user_notification)
242
db.session.commit()
243
celery_tasks.send_notification.apply_async(args=[user_notification.id])
244
245
246
class Post(db.Model):
247
identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True)
248
number = db.Column(db.Integer, nullable=False)
249
repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False)
250
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
251
votes = db.relationship("PostVote", back_populates="post")
252
vote_sum = db.Column(db.Integer, nullable=False, default=0)
253
254
parent_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True)
255
root_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True)
256
state = db.Column(db.SmallInteger, nullable=True, default=1)
257
258
date = db.Column(db.DateTime, default=datetime.now)
259
last_updated = db.Column(db.DateTime, default=datetime.now)
260
subject = db.Column(db.Unicode(384))
261
message = db.Column(db.UnicodeText)
262
html = db.Column(db.UnicodeText)
263
repo = db.relationship("Repo", back_populates="posts")
264
owner = db.relationship("User", back_populates="posts")
265
parent = db.relationship("Post", back_populates="children",
266
primaryjoin="Post.parent_id==Post.identifier",
267
foreign_keys="[Post.parent_id]", remote_side="Post.identifier")
268
root = db.relationship("Post",
269
primaryjoin="Post.root_id==Post.identifier",
270
foreign_keys="[Post.root_id]", remote_side="Post.identifier")
271
children = db.relationship("Post",
272
remote_side="Post.parent_id",
273
primaryjoin="Post.identifier==Post.parent_id",
274
foreign_keys="[Post.parent_id]")
275
resolved_by = db.relationship("PullRequestResolvesThread", back_populates="post")
276
277
def __init__(self, owner, repo, parent, subject, message):
278
self.identifier = f"{repo.route}/{repo.last_post_id}"
279
self.number = repo.last_post_id
280
self.repo_name = repo.route
281
self.repo = repo
282
self.owner_name = owner.username
283
self.owner = owner
284
self.subject = subject
285
self.message = message
286
self.html = markdown.markdown2html(message).prettify()
287
self.parent = parent
288
if parent and parent.parent:
289
self.root = parent.parent
290
elif parent:
291
self.root = parent
292
else:
293
self.root = None
294
repo.last_post_id += 1
295
296
notification = Notification({"type": "post", "repo": repo.route, "post": self.identifier})
297
db.session.add(notification)
298
db.session.commit() # save the notification to get the ID
299
300
# Send a notification to all users who have enabled forum notifications for this repo
301
for relationship in RepoFavourite.query.filter_by(repo_route=repo.route, notify_forum=True).all():
302
user = relationship.user
303
user_notification = UserNotification(user, notification, 1)
304
db.session.add(user_notification)
305
db.session.commit()
306
celery_tasks.send_notification.apply_async(args=[user_notification.id])
307
308
def update_date(self):
309
self.last_updated = datetime.now()
310
with db.session.no_autoflush:
311
if self.parent is not None:
312
self.parent.update_date()
313
314
315
class Comment(db.Model):
316
identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True)
317
number = db.Column(db.Integer, nullable=False)
318
repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False)
319
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
320
commit_identifier = db.Column(db.String(227), db.ForeignKey("commit.identifier"), nullable=False)
321
pr_id = db.Column(db.BigInteger, db.ForeignKey("pull_request.id"), nullable=True)
322
323
file = db.Column(db.String(256), nullable=True)
324
line_number = db.Column(db.Integer, nullable=True)
325
line_type = db.Column(db.SmallInteger, nullable=True, default=0, server_default="0") # 0 is deleted, 1 is modified
326
327
state = db.Column(db.SmallInteger, nullable=True, default=1)
328
review = db.Column(db.SmallInteger, nullable=True, default=0)
329
330
date = db.Column(db.DateTime, default=datetime.now)
331
message = db.Column(db.UnicodeText)
332
html = db.Column(db.UnicodeText)
333
334
repo = db.relationship("Repo", back_populates="comments")
335
owner = db.relationship("User", back_populates="comments")
336
commit = db.relationship("Commit", back_populates="comments")
337
338
def __init__(self, owner, repo, commit, message, file, line_number, pr=None):
339
self.identifier = f"{repo.route}/{repo.last_comment_id}"
340
self.number = repo.last_comment_id
341
self.repo_name = repo.route
342
self.repo = repo
343
self.owner_name = owner.username
344
self.owner = owner
345
self.commit_identifier = commit.identifier
346
self.commit = commit
347
self.message = message
348
self.html = markdown.markdown2html(message).prettify()
349
self.file = file
350
self.line_number = int(line_number[1:])
351
self.line_type = int(line_number[0] == "+")
352
if pr:
353
self.pr = pr
354
355
repo.last_comment_id += 1
356
357
@property
358
def text(self):
359
return self.html
360
361
@text.setter
362
def text(self, value):
363
self.html = markdown.markdown2html(value).prettify()
364
self.message = value # message is stored in markdown format for future editing or plaintext display
365
366
367
class UserNotification(db.Model):
368
id = db.Column(db.Integer, primary_key=True)
369
user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
370
notification_id = db.Column(db.BigInteger, db.ForeignKey("notification.id"))
371
attention_level = db.Column(db.SmallInteger, nullable=False) # 0 is read
372
read_time = db.Column(db.DateTime, nullable=True)
373
374
user = db.relationship("User", back_populates="notifications")
375
notification = db.relationship("Notification", back_populates="notifications")
376
377
__table_args__ = (db.UniqueConstraint("user_username", "notification_id", name="_user_notification_uc"),)
378
379
def __init__(self, user, notification, level):
380
self.user_username = user.username
381
self.notification_id = notification.id
382
self.attention_level = level
383
384
def mark_read(self):
385
self.read_time = datetime.utcnow()
386
self.attention_level = 0
387
388
def mark_unread(self):
389
self.attention_level = 4
390
391
392
class UserFollow(db.Model):
393
id = db.Column(db.Integer, primary_key=True)
394
follower_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False)
395
followed_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False)
396
397
follower = db.relationship("User", back_populates="followers", foreign_keys=[follower_username])
398
followed = db.relationship("User", back_populates="follows", foreign_keys=[followed_username])
399
400
def __init__(self, follower_username, followed_username):
401
self.follower_username = follower_username
402
self.followed_username = followed_username
403
404
405
class Notification(db.Model):
406
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
407
data = db.Column(db.dialects.postgresql.JSONB, nullable=False, default={})
408
notifications = db.relationship("UserNotification", back_populates="notification")
409
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now)
410
411
def __init__(self, json):
412
self.data = json
413
414
415
class PullRequestResolvesThread(db.Model):
416
id = db.Column(db.Integer, primary_key=True)
417
pr_id = db.Column(db.BigInteger, db.ForeignKey("pull_request.id"), nullable=False)
418
post_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=False)
419
420
pr = db.relationship("PullRequest", back_populates="resolves")
421
post = db.relationship("Post", back_populates="resolved_by")
422
423
def __init__(self, pr, post):
424
self.pr = pr
425
self.post = post
426
427
428
class PullRequest(db.Model):
429
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
430
head_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False)
431
base_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False)
432
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
433
state = db.Column(db.SmallInteger, nullable=False, default=0) # 0 pending, 1 merged, 2 rejected
434
435
head = db.relationship("Repo", back_populates="heads", foreign_keys=[head_route])
436
base = db.relationship("Repo", back_populates="bases", foreign_keys=[base_route])
437
438
head_branch = db.Column(db.String(64), nullable=False)
439
base_branch = db.Column(db.String(64), nullable=False)
440
441
owner = db.relationship("User", back_populates="prs")
442
resolves = db.relationship("PullRequestResolvesThread", back_populates="pr")
443
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now)
444
445
def __init__(self, head, head_branch, base, base_branch, owner):
446
self.head = head
447
self.base = base
448
self.head_branch = head_branch
449
self.base_branch = base_branch
450
self.owner = owner
451
452
@property
453
def resolves_list(self):
454
return " ".join([str(post.post.number) for post in self.resolves])
455
456
@resolves_list.setter
457
def resolves_list(self, value):
458
link_to = [Post.query.filter_by(number=int(number), repo=self.base).first() for number in value.split()]
459
resolved_posts = [post.post for post in self.resolves]
460
no_longer_resolves = list(set(resolved_posts) - set(link_to))
461
for post in no_longer_resolves:
462
db.session.delete(PullRequestResolvesThread.query.filter_by(pr=self, post=post).first())
463
464
for post in link_to:
465
if post not in resolved_posts and post is not None and not post.parent: # only top-level posts can be resolved
466
db.session.add(PullRequestResolvesThread(self, post))
467
468
db.session.commit()
469
470
471
class EmailChangeRequest(db.Model):
472
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
473
user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
474
new_email = db.Column(db.String(254), nullable=False)
475
code = db.Column(db.String(64), nullable=False)
476
expires_on = db.Column(db.DateTime, nullable=False)
477
478
user = db.relationship("User", back_populates="email_change_requests")
479
480
def __init__(self, user, new_email):
481
self.user = user
482
self.new_email = new_email
483
self.code = hex(secrets.randbits(256)).removeprefix("0x")
484
self.expires_on = datetime.now() + timedelta(days=1)
485
486