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.86 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
repo = db.relationship("Repo", back_populates="posts")
255
owner = db.relationship("User", back_populates="posts")
256
parent = db.relationship("Post", back_populates="children",
257
primaryjoin="Post.parent_id==Post.identifier",
258
foreign_keys="[Post.parent_id]", remote_side="Post.identifier")
259
root = db.relationship("Post",
260
primaryjoin="Post.root_id==Post.identifier",
261
foreign_keys="[Post.root_id]", remote_side="Post.identifier")
262
children = db.relationship("Post",
263
remote_side="Post.parent_id",
264
primaryjoin="Post.identifier==Post.parent_id",
265
foreign_keys="[Post.parent_id]")
266
267
def __init__(self, owner, repo, parent, subject, message):
268
self.identifier = f"{repo.route}/{repo.last_post_id}"
269
self.number = repo.last_post_id
270
self.repo_name = repo.route
271
self.repo = repo
272
self.owner_name = owner.username
273
self.owner = owner
274
self.subject = subject
275
self.message = message
276
self.parent = parent
277
if parent and parent.parent:
278
self.root = parent.parent
279
elif parent:
280
self.root = parent
281
else:
282
self.root = None
283
repo.last_post_id += 1
284
285
notification = Notification({"type": "post", "repo": repo.route, "post": self.identifier})
286
db.session.add(notification)
287
db.session.commit() # save the notification to get the ID
288
289
# Send a notification to all users who have enabled forum notifications for this repo
290
for relationship in RepoFavourite.query.filter_by(repo_route=repo.route, notify_forum=True).all():
291
user = relationship.user
292
user_notification = UserNotification(user, notification, 1)
293
db.session.add(user_notification)
294
db.session.commit()
295
celery_tasks.send_notification.apply_async(args=[user_notification.id])
296
297
def update_date(self):
298
self.last_updated = datetime.now()
299
with db.session.no_autoflush:
300
if self.parent is not None:
301
self.parent.update_date()
302
303
304
class Comment(db.Model):
305
identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True)
306
number = db.Column(db.Integer, nullable=False)
307
repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False)
308
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
309
commit_identifier = db.Column(db.String(227), db.ForeignKey("commit.identifier"), nullable=False)
310
pr_id = db.Column(db.BigInteger, db.ForeignKey("pull_request.id"), nullable=True)
311
312
file = db.Column(db.String(256), nullable=True)
313
line_number = db.Column(db.Integer, nullable=True)
314
line_type = db.Column(db.SmallInteger, nullable=True, default=0, server_default="0") # 0 is deleted, 1 is modified
315
316
state = db.Column(db.SmallInteger, nullable=True, default=1)
317
review = db.Column(db.SmallInteger, nullable=True, default=0)
318
319
date = db.Column(db.DateTime, default=datetime.now)
320
message = db.Column(db.UnicodeText)
321
html = db.Column(db.UnicodeText)
322
323
repo = db.relationship("Repo", back_populates="comments")
324
owner = db.relationship("User", back_populates="comments")
325
commit = db.relationship("Commit", back_populates="comments")
326
327
def __init__(self, owner, repo, commit, message, file, line_number, pr=None):
328
self.identifier = f"{repo.route}/{repo.last_comment_id}"
329
self.number = repo.last_comment_id
330
self.repo_name = repo.route
331
self.repo = repo
332
self.owner_name = owner.username
333
self.owner = owner
334
self.commit_identifier = commit.identifier
335
self.commit = commit
336
self.message = message
337
self.html = markdown.markdown2html(message).prettify()
338
self.file = file
339
self.line_number = int(line_number[1:])
340
self.line_type = int(line_number[0] == "+")
341
if pr:
342
self.pr = pr
343
344
repo.last_comment_id += 1
345
346
@property
347
def text(self):
348
return self.html
349
350
@text.setter
351
def text(self, value):
352
self.html = markdown.markdown2html(value).prettify()
353
self.message = value # message is stored in markdown format for future editing or plaintext display
354
355
356
class UserNotification(db.Model):
357
id = db.Column(db.Integer, primary_key=True)
358
user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
359
notification_id = db.Column(db.BigInteger, db.ForeignKey("notification.id"))
360
attention_level = db.Column(db.SmallInteger, nullable=False) # 0 is read
361
read_time = db.Column(db.DateTime, nullable=True)
362
363
user = db.relationship("User", back_populates="notifications")
364
notification = db.relationship("Notification", back_populates="notifications")
365
366
__table_args__ = (db.UniqueConstraint("user_username", "notification_id", name="_user_notification_uc"),)
367
368
def __init__(self, user, notification, level):
369
self.user_username = user.username
370
self.notification_id = notification.id
371
self.attention_level = level
372
373
def mark_read(self):
374
self.read_time = datetime.utcnow()
375
self.attention_level = 0
376
377
def mark_unread(self):
378
self.attention_level = 4
379
380
381
class UserFollow(db.Model):
382
id = db.Column(db.Integer, primary_key=True)
383
follower_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False)
384
followed_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False)
385
386
follower = db.relationship("User", back_populates="followers", foreign_keys=[follower_username])
387
followed = db.relationship("User", back_populates="follows", foreign_keys=[followed_username])
388
389
def __init__(self, follower_username, followed_username):
390
self.follower_username = follower_username
391
self.followed_username = followed_username
392
393
394
class Notification(db.Model):
395
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
396
data = db.Column(db.dialects.postgresql.JSONB, nullable=False, default={})
397
notifications = db.relationship("UserNotification", back_populates="notification")
398
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now)
399
400
def __init__(self, json):
401
self.data = json
402
403
404
class PullRequest(db.Model):
405
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
406
head_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False)
407
base_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False)
408
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
409
state = db.Column(db.SmallInteger, nullable=False, default=0) # 0 pending, 1 merged, 2 rejected
410
411
head = db.relationship("Repo", back_populates="heads", foreign_keys=[head_route])
412
base = db.relationship("Repo", back_populates="bases", foreign_keys=[base_route])
413
414
head_branch = db.Column(db.String(64), nullable=False)
415
base_branch = db.Column(db.String(64), nullable=False)
416
417
owner = db.relationship("User", back_populates="prs")
418
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now)
419
420
def __init__(self, head, head_branch, base, base_branch, owner):
421
self.head = head
422
self.base = base
423
self.head_branch = head_branch
424
self.base_branch = base_branch
425
self.owner = owner
426
427
428
class EmailChangeRequest(db.Model):
429
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
430
user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
431
new_email = db.Column(db.String(254), nullable=False)
432
code = db.Column(db.String(64), nullable=False)
433
expires_on = db.Column(db.DateTime, nullable=False)
434
435
user = db.relationship("User", back_populates="email_change_requests")
436
437
def __init__(self, user, new_email):
438
self.user = user
439
self.new_email = new_email
440
self.code = hex(secrets.randbits(256)).removeprefix("0x")
441
self.expires_on = datetime.now() + timedelta(days=1)
442
443