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 • 17.28 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
]
14
15
import secrets
16
import subprocess
17
from app import app, db, bcrypt
18
import git
19
from datetime import datetime, timedelta
20
from enum import Enum
21
from PIL import Image
22
from cairosvg import svg2png
23
import os
24
import config
25
import cairosvg
26
import random
27
import celery_tasks
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
email_change_requests = db.relationship("EmailChangeRequest", back_populates="user")
103
repo_access = db.relationship("RepoAccess", back_populates="user")
104
votes = db.relationship("PostVote", back_populates="user")
105
favourites = db.relationship("RepoFavourite", back_populates="user")
106
107
commits = db.relationship("Commit", back_populates="owner")
108
posts = db.relationship("Post", back_populates="owner")
109
prs = db.relationship("PullRequest", back_populates="owner")
110
notifications = db.relationship("UserNotification", back_populates="user")
111
112
def __init__(self, username, password, email=None, display_name=None):
113
self.username = username
114
self.password_hashed = bcrypt.generate_password_hash(password, config.HASHING_ROUNDS).decode("utf-8")
115
self.email = email
116
self.display_name = display_name
117
118
# Create the user's directory
119
if not os.path.exists(os.path.join(config.REPOS_PATH, username)):
120
os.makedirs(os.path.join(config.REPOS_PATH, username))
121
if not os.path.exists(os.path.join(config.USERDATA_PATH, username)):
122
os.makedirs(os.path.join(config.USERDATA_PATH, username))
123
124
avatar_name = random.choice(os.listdir(config.DEFAULT_AVATARS_PATH))
125
if os.path.join(config.DEFAULT_AVATARS_PATH, avatar_name).endswith(".svg"):
126
cairosvg.svg2png(url=os.path.join(config.DEFAULT_AVATARS_PATH, avatar_name),
127
write_to="/tmp/roundabout-avatar.png")
128
avatar = Image.open("/tmp/roundabout-avatar.png")
129
else:
130
avatar = Image.open(os.path.join(config.DEFAULT_AVATARS_PATH, avatar_name))
131
avatar.thumbnail(config.AVATAR_SIZE)
132
avatar.save(os.path.join(config.USERDATA_PATH, username, "avatar.png"))
133
134
# Create the configuration repo
135
config_repo = Repo(self, ".config", 0)
136
db.session.add(config_repo)
137
notification = Notification({"type": "welcome"})
138
db.session.add(notification)
139
db.session.commit()
140
141
user_notification = UserNotification(self, notification, 1)
142
db.session.add(user_notification)
143
db.session.flush()
144
celery_tasks.send_notification.apply_async(args=[user_notification.id])
145
146
147
class Repo(db.Model):
148
route = db.Column(db.String(98), unique=True, nullable=False, primary_key=True)
149
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
150
name = db.Column(db.String(64), nullable=False)
151
owner = db.relationship("User", back_populates="repositories")
152
visibility = db.Column(db.SmallInteger(), nullable=False)
153
info = db.Column(db.Unicode(512), nullable=True)
154
url = db.Column(db.String(256), nullable=True)
155
creation_date = db.Column(db.DateTime, default=datetime.utcnow)
156
157
default_branch = db.Column(db.String(64), nullable=True, default="")
158
159
commits = db.relationship("Commit", back_populates="repo")
160
posts = db.relationship("Post", back_populates="repo")
161
repo_access = db.relationship("RepoAccess", back_populates="repo")
162
favourites = db.relationship("RepoFavourite", back_populates="repo")
163
heads = db.relationship("PullRequest", back_populates="head", foreign_keys="[PullRequest.head_route]")
164
bases = db.relationship("PullRequest", back_populates="base", foreign_keys="[PullRequest.base_route]")
165
166
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
167
server_default="0") # (the one accessible at username.localhost)
168
site_branch = db.Column(db.String(64), nullable=True)
169
170
last_post_id = db.Column(db.Integer, nullable=False, default=0)
171
172
def __init__(self, owner, name, visibility):
173
self.route = f"/{owner.username}/{name}"
174
self.name = name
175
self.owner_name = owner.username
176
self.owner = owner
177
self.visibility = visibility
178
179
# Add the owner as an admin
180
repo_access = RepoAccess(owner, self, 2)
181
db.session.add(repo_access)
182
183
# Create the directory
184
if not os.path.exists(os.path.join(config.REPOS_PATH, self.owner_name, self.name)):
185
subprocess.run(["git", "init", self.name],
186
cwd=os.path.join(config.REPOS_PATH, self.owner_name))
187
188
189
class Commit(db.Model):
190
identifier = db.Column(db.String(227), unique=True, nullable=False, primary_key=True)
191
sha = db.Column(db.String(128), nullable=False)
192
repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False)
193
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
194
owner_identity = db.Column(db.String(321))
195
receive_date = db.Column(db.DateTime, default=datetime.now)
196
author_date = db.Column(db.DateTime)
197
message = db.Column(db.UnicodeText)
198
repo = db.relationship("Repo", back_populates="commits")
199
owner = db.relationship("User", back_populates="commits")
200
201
def __init__(self, sha, owner, repo, date, message, owner_identity):
202
self.identifier = f"{repo.route}/{sha}"
203
self.sha = sha
204
self.repo_name = repo.route
205
self.repo = repo
206
self.owner_name = owner.username
207
self.owner = owner
208
self.author_date = datetime.fromtimestamp(int(date))
209
self.message = message
210
self.owner_identity = owner_identity
211
212
notification = Notification({"type": "commit", "repo": repo.route, "commit": sha})
213
db.session.add(notification)
214
db.session.flush() # save the notification to get the ID
215
216
# Send a notification to all users who have enabled commit notifications for this repo
217
for relationship in RepoFavourite.query.filter_by(repo_route=repo.route, notify_commit=True).all():
218
user = relationship.user
219
user_notification = UserNotification(user, notification, 1)
220
db.session.add(user_notification)
221
db.session.flush()
222
celery_tasks.send_notification.apply_async(args=[user_notification.id])
223
224
225
class Post(db.Model):
226
identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True)
227
number = db.Column(db.Integer, nullable=False)
228
repo_name = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False)
229
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
230
votes = db.relationship("PostVote", back_populates="post")
231
vote_sum = db.Column(db.Integer, nullable=False, default=0)
232
233
parent_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True)
234
root_id = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True)
235
state = db.Column(db.SmallInteger, nullable=True, default=1)
236
237
date = db.Column(db.DateTime, default=datetime.now)
238
last_updated = db.Column(db.DateTime, default=datetime.now)
239
subject = db.Column(db.Unicode(384))
240
message = db.Column(db.UnicodeText)
241
repo = db.relationship("Repo", back_populates="posts")
242
owner = db.relationship("User", back_populates="posts")
243
parent = db.relationship("Post", back_populates="children",
244
primaryjoin="Post.parent_id==Post.identifier",
245
foreign_keys="[Post.parent_id]", remote_side="Post.identifier")
246
root = db.relationship("Post",
247
primaryjoin="Post.root_id==Post.identifier",
248
foreign_keys="[Post.root_id]", remote_side="Post.identifier")
249
children = db.relationship("Post",
250
remote_side="Post.parent_id",
251
primaryjoin="Post.identifier==Post.parent_id",
252
foreign_keys="[Post.parent_id]")
253
254
def __init__(self, owner, repo, parent, subject, message):
255
self.identifier = f"{repo.route}/{repo.last_post_id}"
256
self.number = repo.last_post_id
257
self.repo_name = repo.route
258
self.repo = repo
259
self.owner_name = owner.username
260
self.owner = owner
261
self.subject = subject
262
self.message = message
263
self.parent = parent
264
if parent and parent.parent:
265
self.root = parent.parent
266
elif parent:
267
self.root = parent
268
else:
269
self.root = None
270
repo.last_post_id += 1
271
272
def update_date(self):
273
self.last_updated = datetime.now()
274
with db.session.no_autoflush:
275
if self.parent is not None:
276
self.parent.update_date()
277
278
279
class UserNotification(db.Model):
280
id = db.Column(db.Integer, primary_key=True)
281
user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
282
notification_id = db.Column(db.BigInteger, db.ForeignKey("notification.id"))
283
attention_level = db.Column(db.SmallInteger, nullable=False) # 0 is read
284
read_time = db.Column(db.DateTime, nullable=True)
285
286
user = db.relationship("User", back_populates="notifications")
287
notification = db.relationship("Notification", back_populates="notifications")
288
289
__table_args__ = (db.UniqueConstraint("user_username", "notification_id", name="_user_notification_uc"),)
290
291
def __init__(self, user, notification, level):
292
self.user_username = user.username
293
self.notification_id = notification.id
294
self.attention_level = level
295
296
def mark_read(self):
297
self.read_time = datetime.utcnow()
298
self.attention_level = 0
299
300
def mark_unread(self):
301
self.attention_level = 4
302
303
304
class UserFollow(db.Model):
305
id = db.Column(db.Integer, primary_key=True)
306
follower_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False)
307
followed_username = db.Column(db.String(32), db.ForeignKey("user.username", ondelete="CASCADE"), nullable=False)
308
309
follower = db.relationship("User", back_populates="followers", foreign_keys=[follower_username])
310
followed = db.relationship("User", back_populates="follows", foreign_keys=[followed_username])
311
312
def __init__(self, follower_username, followed_username):
313
self.follower_username = follower_username
314
self.followed_username = followed_username
315
316
317
class Notification(db.Model):
318
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
319
data = db.Column(db.dialects.postgresql.JSONB, nullable=False, default={})
320
notifications = db.relationship("UserNotification", back_populates="notification")
321
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now)
322
323
def __init__(self, json):
324
self.data = json
325
326
327
class PullRequest(db.Model):
328
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
329
head_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False)
330
base_route = db.Column(db.String(98), db.ForeignKey("repo.route", ondelete="CASCADE"), nullable=False)
331
owner_name = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
332
state = db.Column(db.SmallInteger, nullable=False, default=0) # 0 pending, 1 merged, 2 rejected
333
334
head = db.relationship("Repo", back_populates="heads", foreign_keys=[head_route])
335
base = db.relationship("Repo", back_populates="bases", foreign_keys=[base_route])
336
337
head_branch = db.Column(db.String(64), nullable=False)
338
base_branch = db.Column(db.String(64), nullable=False)
339
340
owner = db.relationship("User", back_populates="prs")
341
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.now)
342
343
def __init__(self, head, head_branch, base, base_branch, owner):
344
self.head = head
345
self.base = base
346
self.head_branch = head_branch
347
self.base_branch = base_branch
348
self.owner = owner
349
350
class EmailChangeRequest(db.Model):
351
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
352
user_username = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
353
new_email = db.Column(db.String(254), nullable=False)
354
code = db.Column(db.String(64), nullable=False)
355
expires_on = db.Column(db.DateTime, nullable=False)
356
357
user = db.relationship("User", back_populates="email_change_requests")
358
359
def __init__(self, user, new_email):
360
self.user = user
361
self.new_email = new_email
362
self.code = hex(secrets.randbits(256)).removeprefix("0x")
363
self.expires_on = datetime.now() + timedelta(days=1)
364
365