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