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.15 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
def update_date(self):
286
self.last_updated = datetime.now()
287
with db.session.no_autoflush:
288
if self.parent is not None:
289
self.parent.update_date()
290
291
292
class Comment(db.Model):
293
identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True)
294
number = db.Column(db.Integer, nullable=False)
295
repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False)
296
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
297
commit_identifier = db.Column(db.String(227), db.ForeignKey("commit.identifier"), nullable=False)
298
pr_id = db.Column(db.BigInteger, db.ForeignKey("pull_request.id"), nullable=True)
299
300
file = db.Column(db.String(256), nullable=True)
301
line_number = db.Column(db.Integer, nullable=True)
302
line_type = db.Column(db.SmallInteger, nullable=True, default=0, server_default="0") # 0 is deleted, 1 is modified
303
304
state = db.Column(db.SmallInteger, nullable=True, default=1)
305
review = db.Column(db.SmallInteger, nullable=True, default=0)
306
307
date = db.Column(db.DateTime, default=datetime.now)
308
message = db.Column(db.UnicodeText)
309
html = db.Column(db.UnicodeText)
310
311
repo = db.relationship("Repo", back_populates="comments")
312
owner = db.relationship("User", back_populates="comments")
313
commit = db.relationship("Commit", back_populates="comments")
314
315
def __init__(self, owner, repo, commit, message, file, line_number, pr=None):
316
self.identifier = f"{repo.route}/{repo.last_comment_id}"
317
self.number = repo.last_comment_id
318
self.repo_name = repo.route
319
self.repo = repo
320
self.owner_name = owner.username
321
self.owner = owner
322
self.commit_identifier = commit.identifier
323
self.commit = commit
324
self.message = message
325
self.html = markdown.markdown2html(message).prettify()
326
self.file = file
327
self.line_number = int(line_number[1:])
328
self.line_type = int(line_number[0] == "+")
329
if pr:
330
self.pr = pr
331
332
repo.last_comment_id += 1
333
334
@property
335
def text(self):
336
return self.html
337
338
@text.setter
339
def text(self, value):
340
self.html = markdown.markdown2html(value).prettify()
341
self.message = value # message is stored in markdown format for future editing or plaintext display
342
343
344
class UserNotification(db.Model):
345
id = db.Column(db.Integer, primary_key=True)
346
user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
347
notification_id = db.Column(db.BigInteger, db.ForeignKey("notification.id"))
348
attention_level = db.Column(db.SmallInteger, nullable=False) # 0 is read
349
read_time = db.Column(db.DateTime, nullable=True)
350
351
user = db.relationship("User", back_populates="notifications")
352
notification = db.relationship("Notification", back_populates="notifications")
353
354
__table_args__ = (db.UniqueConstraint("user_username", "notification_id", name="_user_notification_uc"),)
355
356
def __init__(self, user, notification, level):
357
self.user_username = user.username
358
self.notification_id = notification.id
359
self.attention_level = level
360
361
def mark_read(self):
362
self.read_time = datetime.utcnow()
363
self.attention_level = 0
364
365
def mark_unread(self):
366
self.attention_level = 4
367
368
369
class UserFollow(db.Model):
370
id = db.Column(db.Integer, primary_key=True)
371
follower_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False)
372
followed_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False)
373
374
follower = db.relationship("User", back_populates="followers", foreign_keys=[follower_username])
375
followed = db.relationship("User", back_populates="follows", foreign_keys=[followed_username])
376
377
def __init__(self, follower_username, followed_username):
378
self.follower_username = follower_username
379
self.followed_username = followed_username
380
381
382
class Notification(db.Model):
383
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
384
data = db.Column(db.dialects.postgresql.JSONB, nullable=False, default={})
385
notifications = db.relationship("UserNotification", back_populates="notification")
386
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now)
387
388
def __init__(self, json):
389
self.data = json
390
391
392
class PullRequest(db.Model):
393
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
394
head_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False)
395
base_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False)
396
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
397
state = db.Column(db.SmallInteger, nullable=False, default=0) # 0 pending, 1 merged, 2 rejected
398
399
head = db.relationship("Repo", back_populates="heads", foreign_keys=[head_route])
400
base = db.relationship("Repo", back_populates="bases", foreign_keys=[base_route])
401
402
head_branch = db.Column(db.String(64), nullable=False)
403
base_branch = db.Column(db.String(64), nullable=False)
404
405
owner = db.relationship("User", back_populates="prs")
406
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now)
407
408
def __init__(self, head, head_branch, base, base_branch, owner):
409
self.head = head
410
self.base = base
411
self.head_branch = head_branch
412
self.base_branch = base_branch
413
self.owner = owner
414
415
class EmailChangeRequest(db.Model):
416
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
417
user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
418
new_email = db.Column(db.String(254), nullable=False)
419
code = db.Column(db.String(64), nullable=False)
420
expires_on = db.Column(db.DateTime, nullable=False)
421
422
user = db.relationship("User", back_populates="email_change_requests")
423
424
def __init__(self, user, new_email):
425
self.user = user
426
self.new_email = new_email
427
self.code = hex(secrets.randbits(256)).removeprefix("0x")
428
self.expires_on = datetime.now() + timedelta(days=1)
429
430