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