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