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