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