2 # Copyright (C) 2018 rubenwardy
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <https://www.gnu.org/licenses/>.
18 from flask import Flask, url_for
19 from flask_sqlalchemy import SQLAlchemy
20 from flask_migrate import Migrate
22 from datetime import datetime
23 from sqlalchemy.orm import validates
24 from flask_user import login_required, UserManager, UserMixin, SQLAlchemyAdapter
29 migrate = Migrate(app, db)
32 class UserRank(enum.Enum):
40 def atLeast(self, min):
41 return self.value >= min.value
44 return self.name.replace("_", " ").title()
47 return self.name.lower()
54 return [(choice, choice.getTitle()) for choice in cls]
57 def coerce(cls, item):
58 return item if type(item) == UserRank else UserRank[item]
61 class Permission(enum.Enum):
62 EDIT_PACKAGE = "EDIT_PACKAGE"
63 APPROVE_CHANGES = "APPROVE_CHANGES"
64 DELETE_PACKAGE = "DELETE_PACKAGE"
65 CHANGE_AUTHOR = "CHANGE_AUTHOR"
66 MAKE_RELEASE = "MAKE_RELEASE"
67 APPROVE_RELEASE = "APPROVE_RELEASE"
68 APPROVE_NEW = "APPROVE_NEW"
69 CHANGE_RELEASE_URL = "CHANGE_RELEASE_URL"
70 CHANGE_DNAME = "CHANGE_DNAME"
71 CHANGE_RANK = "CHANGE_RANK"
72 CHANGE_EMAIL = "CHANGE_EMAIL"
73 EDIT_EDITREQUEST = "EDIT_EDITREQUEST"
75 # Only return true if the permission is valid for *all* contexts
76 # See Package.checkPerm for package-specific contexts
77 def check(self, user):
78 if not user.is_authenticated:
81 if self == Permission.APPROVE_NEW or \
82 self == Permission.APPROVE_CHANGES or \
83 self == Permission.APPROVE_RELEASE:
84 return user.rank.atLeast(UserRank.EDITOR)
86 raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
89 class User(db.Model, UserMixin):
90 id = db.Column(db.Integer, primary_key=True)
92 # User authentication information
93 username = db.Column(db.String(50), nullable=False, unique=True)
94 password = db.Column(db.String(255), nullable=False, server_default="")
95 reset_password_token = db.Column(db.String(100), nullable=False, server_default="")
97 rank = db.Column(db.Enum(UserRank))
100 github_username = db.Column(db.String(50), nullable=True, unique=True)
101 forums_username = db.Column(db.String(50), nullable=True, unique=True)
103 # User email information
104 email = db.Column(db.String(255), nullable=True, unique=True)
105 confirmed_at = db.Column(db.DateTime())
108 active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
109 display_name = db.Column(db.String(100), nullable=False, server_default="")
112 notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id")
114 # causednotifs = db.relationship("Notification", backref="causer", lazy="dynamic")
115 packages = db.relationship("Package", backref="author", lazy="dynamic")
116 requests = db.relationship("EditRequest", backref="author", lazy="dynamic")
118 def __init__(self, username):
121 self.username = username
122 self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
123 self.display_name = username
124 self.rank = UserRank.NOT_JOINED
126 def canAccessTodoList(self):
127 return Permission.APPROVE_NEW.check(self) or \
128 Permission.APPROVE_RELEASE.check(self) or \
129 Permission.APPROVE_CHANGES.check(self)
132 return self.rank.atLeast(UserRank.NEW_MEMBER)
134 def checkPerm(self, user, perm):
135 if not user.is_authenticated:
138 if type(perm) == str:
139 perm = Permission[perm]
140 elif type(perm) != Permission:
141 raise Exception("Unknown permission given to User.checkPerm()")
143 # Members can edit their own packages, and editors can edit any packages
144 if perm == Permission.CHANGE_AUTHOR:
145 return user.rank.atLeast(UserRank.EDITOR)
146 elif perm == Permission.CHANGE_RANK or perm == Permission.CHANGE_DNAME:
147 return user.rank.atLeast(UserRank.MODERATOR)
148 elif perm == Permission.CHANGE_EMAIL:
149 return user == self or (user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank))
151 raise Exception("Permission {} is not related to users".format(perm.name))
153 class UserEmailVerification(db.Model):
154 id = db.Column(db.Integer, primary_key=True)
155 user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
156 email = db.Column(db.String(100))
157 token = db.Column(db.String(32))
158 user = db.relationship("User", foreign_keys=[user_id])
160 class Notification(db.Model):
161 id = db.Column(db.Integer, primary_key=True)
162 user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
163 causer_id = db.Column(db.Integer, db.ForeignKey("user.id"))
164 user = db.relationship("User", foreign_keys=[user_id])
165 causer = db.relationship("User", foreign_keys=[causer_id])
167 title = db.Column(db.String(100), nullable=False)
168 url = db.Column(db.String(200), nullable=True)
170 def __init__(self, us, cau, titl, ur):
177 class License(db.Model):
178 id = db.Column(db.Integer, primary_key=True)
179 name = db.Column(db.String(50), nullable=False, unique=True)
180 packages = db.relationship("Package", backref="license", lazy="dynamic")
182 def __init__(self, v):
189 class PackageType(enum.Enum):
195 return self.name.lower()
202 return [(choice, choice.value) for choice in cls]
205 def coerce(cls, item):
206 return item if type(item) == PackageType else PackageType[item]
209 class PackagePropertyKey(enum.Enum):
212 shortDesc = "Short Description"
219 issueTracker = "Issue Tracker"
220 forums = "Forum Topic ID"
222 def convert(self, value):
223 if self == PackagePropertyKey.tags:
224 return ",".join([t.title for t in value])
228 tags = db.Table("tags",
229 db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True),
230 db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True)
233 harddeps = db.Table("harddeps",
234 db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True),
235 db.Column("dependency_id", db.Integer, db.ForeignKey("package.id"), primary_key=True)
238 softdeps = db.Table("softdeps",
239 db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True),
240 db.Column("dependency_id", db.Integer, db.ForeignKey("package.id"), primary_key=True)
243 class Package(db.Model):
244 id = db.Column(db.Integer, primary_key=True)
247 author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
248 name = db.Column(db.String(100), nullable=False)
249 title = db.Column(db.String(100), nullable=False)
250 shortDesc = db.Column(db.String(200), nullable=False)
251 desc = db.Column(db.Text, nullable=True)
252 type = db.Column(db.Enum(PackageType))
254 license_id = db.Column(db.Integer, db.ForeignKey("license.id"))
256 approved = db.Column(db.Boolean, nullable=False, default=False)
259 repo = db.Column(db.String(200), nullable=True)
260 website = db.Column(db.String(200), nullable=True)
261 issueTracker = db.Column(db.String(200), nullable=True)
262 forums = db.Column(db.Integer, nullable=False)
264 tags = db.relationship("Tag", secondary=tags, lazy="subquery",
265 backref=db.backref("packages", lazy=True))
267 harddeps = db.relationship("Package",
269 primaryjoin=id==harddeps.c.package_id,
270 secondaryjoin=id==harddeps.c.dependency_id,
271 backref="dependents")
273 softdeps = db.relationship("Package",
275 primaryjoin=id==softdeps.c.package_id,
276 secondaryjoin=id==softdeps.c.dependency_id,
277 backref="softdependents")
279 releases = db.relationship("PackageRelease", backref="package",
280 lazy="dynamic", order_by=db.desc("package_release_releaseDate"))
282 screenshots = db.relationship("PackageScreenshot", backref="package",
285 requests = db.relationship("EditRequest", backref="package",
288 def getAsDictionary(self, base_url):
292 "author": self.author.display_name,
293 "shortDesc": self.shortDesc,
294 "type": self.type.toName(),
295 "license": self.license.name,
297 "url": base_url + self.getDownloadURL(),
298 "release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
299 "screenshots": [base_url + ss.url for ss in self.screenshots]
302 def getDetailsURL(self):
303 return url_for("package_page",
304 author=self.author.username, name=self.name)
306 def getEditURL(self):
307 return url_for("create_edit_package_page",
308 author=self.author.username, name=self.name)
310 def getApproveURL(self):
311 return url_for("approve_package_page",
312 author=self.author.username, name=self.name)
314 def getNewScreenshotURL(self):
315 return url_for("create_screenshot_page",
316 author=self.author.username, name=self.name)
318 def getCreateReleaseURL(self):
319 return url_for("create_release_page",
320 author=self.author.username, name=self.name)
322 def getCreateEditRequestURL(self):
323 return url_for("create_edit_editrequest_page",
324 author=self.author.username, name=self.name)
326 def getDownloadURL(self):
327 return url_for("package_download_page",
328 author=self.author.username, name=self.name)
330 def getMainScreenshotURL(self):
331 screenshot = self.screenshots.first()
332 return screenshot.url if screenshot is not None else None
334 def getDownloadRelease(self):
335 for rel in self.releases:
341 def checkPerm(self, user, perm):
342 if not user.is_authenticated:
345 if type(perm) == str:
346 perm = Permission[perm]
347 elif type(perm) != Permission:
348 raise Exception("Unknown permission given to Package.checkPerm()")
350 isOwner = user == self.author
352 # Members can edit their own packages, and editors can edit any packages
353 if perm == Permission.MAKE_RELEASE:
354 return isOwner or user.rank.atLeast(UserRank.EDITOR)
356 if perm == Permission.EDIT_PACKAGE or perm == Permission.APPROVE_CHANGES:
357 return user.rank.atLeast(UserRank.MEMBER if isOwner else UserRank.EDITOR)
359 # Editors can change authors, approve new packages, and approve releases
360 elif perm == Permission.CHANGE_AUTHOR or perm == Permission.APPROVE_NEW \
361 or perm == Permission.APPROVE_RELEASE:
362 return user.rank.atLeast(UserRank.EDITOR)
364 # Moderators can delete packages
365 elif perm == Permission.DELETE_PACKAGE or perm == Permission.CHANGE_RELEASE_URL:
366 return user.rank.atLeast(UserRank.MODERATOR)
369 raise Exception("Permission {} is not related to packages".format(perm.name))
372 id = db.Column(db.Integer, primary_key=True)
373 name = db.Column(db.String(100), unique=True, nullable=False)
374 title = db.Column(db.String(100), nullable=False)
375 backgroundColor = db.Column(db.String(6), nullable=False)
376 textColor = db.Column(db.String(6), nullable=False)
378 def __init__(self, title, backgroundColor="000000", textColor="ffffff"):
380 self.backgroundColor = backgroundColor
381 self.textColor = textColor
384 regex = re.compile("[^a-z_]")
385 self.name = regex.sub("", self.title.lower().replace(" ", "_"))
387 class PackageRelease(db.Model):
388 id = db.Column(db.Integer, primary_key=True)
390 package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
391 title = db.Column(db.String(100), nullable=False)
392 releaseDate = db.Column(db.DateTime, nullable=False)
393 url = db.Column(db.String(200), nullable=False)
394 approved = db.Column(db.Boolean, nullable=False, default=False)
395 task_id = db.Column(db.String(37), nullable=True)
398 def getEditURL(self):
399 return url_for("edit_release_page",
400 author=self.package.author.username,
401 name=self.package.name,
405 self.releaseDate = datetime.now()
407 class PackageScreenshot(db.Model):
408 id = db.Column(db.Integer, primary_key=True)
409 package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
410 title = db.Column(db.String(100), nullable=False)
411 url = db.Column(db.String(100), nullable=False)
413 def getThumbnailURL(self):
414 return self.url # TODO
416 class EditRequest(db.Model):
417 id = db.Column(db.Integer, primary_key=True)
419 package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
420 author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
422 title = db.Column(db.String(100), nullable=False)
423 desc = db.Column(db.String(1000), nullable=True)
428 status = db.Column(db.Integer, nullable=False, default=0)
430 changes = db.relationship("EditRequestChange", backref="request",
434 return url_for("view_editrequest_page",
435 author=self.package.author.username,
436 name=self.package.name,
439 def getApproveURL(self):
440 return url_for("approve_editrequest_page",
441 author=self.package.author.username,
442 name=self.package.name,
445 def getRejectURL(self):
446 return url_for("reject_editrequest_page",
447 author=self.package.author.username,
448 name=self.package.name,
451 def getEditURL(self):
452 return url_for("create_edit_editrequest_page",
453 author=self.package.author.username,
454 name=self.package.name,
457 def applyAll(self, package):
458 for change in self.changes:
459 change.apply(package)
462 def checkPerm(self, user, perm):
463 if not user.is_authenticated:
466 if type(perm) == str:
467 perm = Permission[perm]
468 elif type(perm) != Permission:
469 raise Exception("Unknown permission given to EditRequest.checkPerm()")
471 isOwner = user == self.author
473 # Members can edit their own packages, and editors can edit any packages
474 if perm == Permission.EDIT_EDITREQUEST:
475 return isOwner or user.rank.atLeast(UserRank.EDITOR)
478 raise Exception("Permission {} is not related to packages".format(perm.name))
483 class EditRequestChange(db.Model):
484 id = db.Column(db.Integer, primary_key=True)
486 request_id = db.Column(db.Integer, db.ForeignKey("edit_request.id"))
487 key = db.Column(db.Enum(PackagePropertyKey), nullable=False)
489 # TODO: make diff instead
490 oldValue = db.Column(db.Text, nullable=True)
491 newValue = db.Column(db.Text, nullable=True)
493 def apply(self, package):
494 if self.key == PackagePropertyKey.tags:
496 for tagTitle in self.newValue.split(","):
497 tag = Tag.query.filter_by(title=tagTitle.strip()).first()
498 package.tags.append(tag)
500 setattr(package, self.key.name, self.newValue)
503 db_adapter = SQLAlchemyAdapter(db, User) # Register the User model
504 user_manager = UserManager(db_adapter, app) # Initialize Flask-User