1 from flask import Flask, url_for
2 from flask_sqlalchemy import SQLAlchemy
4 from datetime import datetime
5 from sqlalchemy.orm import validates
6 from flask_user import login_required, UserManager, UserMixin, SQLAlchemyAdapter
13 class UserRank(enum.Enum):
21 def atLeast(self, min):
22 return self.value >= min.value
25 return self.name.replace("_", " ").title()
28 return self.name.lower()
35 return [(choice, choice.getTitle()) for choice in cls]
38 def coerce(cls, item):
39 return item if type(item) == UserRank else UserRank[item]
42 class Permission(enum.Enum):
43 EDIT_PACKAGE = "EDIT_PACKAGE"
44 APPROVE_CHANGES = "APPROVE_CHANGES"
45 DELETE_PACKAGE = "DELETE_PACKAGE"
46 CHANGE_AUTHOR = "CHANGE_AUTHOR"
47 MAKE_RELEASE = "MAKE_RELEASE"
48 APPROVE_RELEASE = "APPROVE_RELEASE"
49 APPROVE_NEW = "APPROVE_NEW"
50 CHANGE_RELEASE_URL = "CHANGE_RELEASE_URL"
51 CHANGE_RANK = "CHANGE_RANK"
52 EDIT_EDITREQUEST = "EDIT_EDITREQUEST"
54 # Only return true if the permission is valid for *all* contexts
55 # See Package.checkPerm for package-specific contexts
56 def check(self, user):
57 if not user.is_authenticated:
60 if self == Permission.APPROVE_NEW or \
61 self == Permission.APPROVE_CHANGES or \
62 self == Permission.APPROVE_RELEASE:
63 return user.rank.atLeast(UserRank.EDITOR)
65 raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
68 class User(db.Model, UserMixin):
69 id = db.Column(db.Integer, primary_key=True)
71 # User authentication information
72 username = db.Column(db.String(50), nullable=False, unique=True)
73 password = db.Column(db.String(255), nullable=False, server_default="")
74 reset_password_token = db.Column(db.String(100), nullable=False, server_default="")
76 rank = db.Column(db.Enum(UserRank))
79 github_username = db.Column(db.String(50), nullable=True, unique=True)
80 forums_username = db.Column(db.String(50), nullable=True, unique=True)
82 # User email information
83 email = db.Column(db.String(255), nullable=True, unique=True)
84 confirmed_at = db.Column(db.DateTime())
87 active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
88 display_name = db.Column(db.String(100), nullable=False, server_default="")
91 notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id")
93 # causednotifs = db.relationship("Notification", backref="causer", lazy="dynamic")
94 packages = db.relationship("Package", backref="author", lazy="dynamic")
95 requests = db.relationship("EditRequest", backref="author", lazy="dynamic")
97 def __init__(self, username):
100 self.username = username
101 self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
102 self.display_name = username
103 self.rank = UserRank.NOT_JOINED
106 return self.rank.atLeast(UserRank.NEW_MEMBER)
108 def checkPerm(self, user, perm):
109 if not user.is_authenticated:
112 if type(perm) == str:
113 perm = Permission[perm]
114 elif type(perm) != Permission:
115 raise Exception("Unknown permission given to User.checkPerm()")
117 # Members can edit their own packages, and editors can edit any packages
118 if perm == Permission.CHANGE_AUTHOR:
119 return user.rank.atLeast(UserRank.EDITOR)
120 elif perm == Permission.CHANGE_RANK:
121 return user.rank.atLeast(UserRank.MODERATOR)
123 raise Exception("Permission {} is not related to users".format(perm.name))
126 class Notification(db.Model):
127 id = db.Column(db.Integer, primary_key=True)
128 user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
129 causer_id = db.Column(db.Integer, db.ForeignKey("user.id"))
130 user = db.relationship("User", foreign_keys=[user_id])
131 causer = db.relationship("User", foreign_keys=[causer_id])
133 title = db.Column(db.String(100), nullable=False)
134 url = db.Column(db.String(200), nullable=True)
136 def __init__(self, us, cau, titl, ur):
143 class License(db.Model):
144 id = db.Column(db.Integer, primary_key=True)
145 name = db.Column(db.String(50), nullable=False, unique=True)
146 packages = db.relationship("Package", backref="license", lazy="dynamic")
148 def __init__(self, v):
155 class PackageType(enum.Enum):
161 return self.name.lower()
168 return [(choice, choice.value) for choice in cls]
171 def coerce(cls, item):
172 return item if type(item) == PackageType else PackageType[item]
175 class PackagePropertyKey(enum.Enum):
178 shortDesc = "Short Description"
185 issueTracker = "Issue Tracker"
186 forums = "Forum Topic ID"
188 def convert(self, value):
189 if self == PackagePropertyKey.tags:
190 return ','.join([t.title for t in value])
194 tags = db.Table('tags',
195 db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True),
196 db.Column('package_id', db.Integer, db.ForeignKey('package.id'), primary_key=True)
199 class Package(db.Model):
200 id = db.Column(db.Integer, primary_key=True)
203 author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
204 name = db.Column(db.String(100), nullable=False)
205 title = db.Column(db.String(100), nullable=False)
206 shortDesc = db.Column(db.String(200), nullable=False)
207 desc = db.Column(db.Text, nullable=True)
208 type = db.Column(db.Enum(PackageType))
210 license_id = db.Column(db.Integer, db.ForeignKey("license.id"))
212 approved = db.Column(db.Boolean, nullable=False, default=False)
215 repo = db.Column(db.String(200), nullable=True)
216 website = db.Column(db.String(200), nullable=True)
217 issueTracker = db.Column(db.String(200), nullable=True)
218 forums = db.Column(db.Integer, nullable=False)
221 tags = db.relationship('Tag', secondary=tags, lazy='subquery',
222 backref=db.backref('packages', lazy=True))
224 releases = db.relationship("PackageRelease", backref="package",
225 lazy="dynamic", order_by=db.desc("package_release_releaseDate"))
227 screenshots = db.relationship("PackageScreenshot", backref="package",
230 requests = db.relationship("EditRequest", backref="package",
233 def getAsDictionary(self, base_url):
237 "author": self.author.display_name,
238 "shortDesc": self.shortDesc,
239 "type": self.type.toName(),
240 "license": self.license.name,
242 "url": base_url + self.getDownloadURL(),
243 "release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
244 "screenshots": [base_url + ss.url for ss in self.screenshots]
247 def getDetailsURL(self):
248 return url_for("package_page",
249 author=self.author.username, name=self.name)
251 def getEditURL(self):
252 return url_for("create_edit_package_page",
253 author=self.author.username, name=self.name)
255 def getApproveURL(self):
256 return url_for("approve_package_page",
257 author=self.author.username, name=self.name)
259 def getNewScreenshotURL(self):
260 return url_for("create_screenshot_page",
261 author=self.author.username, name=self.name)
263 def getCreateReleaseURL(self):
264 return url_for("create_release_page",
265 author=self.author.username, name=self.name)
267 def getCreateEditRequestURL(self):
268 return url_for("create_edit_editrequest_page",
269 author=self.author.username, name=self.name)
271 def getDownloadURL(self):
272 return url_for("package_download_page",
273 author=self.author.username, name=self.name)
275 def getMainScreenshotURL(self):
276 screenshot = self.screenshots.first()
277 return screenshot.url if screenshot is not None else None
279 def getDownloadRelease(self):
280 for rel in self.releases:
286 def checkPerm(self, user, perm):
287 if not user.is_authenticated:
290 if type(perm) == str:
291 perm = Permission[perm]
292 elif type(perm) != Permission:
293 raise Exception("Unknown permission given to Package.checkPerm()")
295 isOwner = user == self.author
297 # Members can edit their own packages, and editors can edit any packages
298 if perm == Permission.MAKE_RELEASE:
299 return isOwner or user.rank.atLeast(UserRank.EDITOR)
301 if perm == Permission.EDIT_PACKAGE or perm == Permission.APPROVE_CHANGES:
302 return user.rank.atLeast(UserRank.MEMBER if isOwner else UserRank.EDITOR)
304 # Editors can change authors, approve new packages, and approve releases
305 elif perm == Permission.CHANGE_AUTHOR or perm == Permission.APPROVE_NEW \
306 or perm == Permission.APPROVE_RELEASE:
307 return user.rank.atLeast(UserRank.EDITOR)
309 # Moderators can delete packages
310 elif perm == Permission.DELETE_PACKAGE or perm == Permission.CHANGE_RELEASE_URL:
311 return user.rank.atLeast(UserRank.MODERATOR)
314 raise Exception("Permission {} is not related to packages".format(perm.name))
317 id = db.Column(db.Integer, primary_key=True)
318 name = db.Column(db.String(100), unique=True, nullable=False)
319 title = db.Column(db.String(100), nullable=False)
320 backgroundColor = db.Column(db.String(6), nullable=False)
321 textColor = db.Column(db.String(6), nullable=False)
323 def __init__(self, title, backgroundColor="000000", textColor="ffffff"):
325 self.backgroundColor = backgroundColor
326 self.textColor = textColor
329 regex = re.compile('[^a-z_]')
330 self.name = regex.sub("", self.title.lower().replace(" ", "_"))
332 class PackageRelease(db.Model):
333 id = db.Column(db.Integer, primary_key=True)
335 package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
336 title = db.Column(db.String(100), nullable=False)
337 releaseDate = db.Column(db.DateTime, nullable=False)
338 url = db.Column(db.String(100), nullable=False)
339 approved = db.Column(db.Boolean, nullable=False, default=False)
340 task_id = db.Column(db.String(32), nullable=True)
343 def getEditURL(self):
344 return url_for("edit_release_page",
345 author=self.package.author.username,
346 name=self.package.name,
350 self.releaseDate = datetime.now()
352 class PackageScreenshot(db.Model):
353 id = db.Column(db.Integer, primary_key=True)
354 package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
355 title = db.Column(db.String(100), nullable=False)
356 url = db.Column(db.String(100), nullable=False)
358 def getThumbnailURL(self):
359 return self.url # TODO
361 class EditRequest(db.Model):
362 id = db.Column(db.Integer, primary_key=True)
364 package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
365 author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
367 title = db.Column(db.String(100), nullable=False)
368 desc = db.Column(db.String(1000), nullable=True)
373 status = db.Column(db.Integer, nullable=False, default=0)
375 changes = db.relationship("EditRequestChange", backref="request",
379 return url_for("view_editrequest_page",
380 author=self.package.author.username,
381 name=self.package.name,
384 def getApproveURL(self):
385 return url_for("approve_editrequest_page",
386 author=self.package.author.username,
387 name=self.package.name,
390 def getRejectURL(self):
391 return url_for("reject_editrequest_page",
392 author=self.package.author.username,
393 name=self.package.name,
396 def getEditURL(self):
397 return url_for("create_edit_editrequest_page",
398 author=self.package.author.username,
399 name=self.package.name,
402 def applyAll(self, package):
403 for change in self.changes:
404 change.apply(package)
407 def checkPerm(self, user, perm):
408 if not user.is_authenticated:
411 if type(perm) == str:
412 perm = Permission[perm]
413 elif type(perm) != Permission:
414 raise Exception("Unknown permission given to EditRequest.checkPerm()")
416 isOwner = user == self.author
418 # Members can edit their own packages, and editors can edit any packages
419 if perm == Permission.EDIT_EDITREQUEST:
420 return isOwner or user.rank.atLeast(UserRank.EDITOR)
423 raise Exception("Permission {} is not related to packages".format(perm.name))
428 class EditRequestChange(db.Model):
429 id = db.Column(db.Integer, primary_key=True)
431 request_id = db.Column(db.Integer, db.ForeignKey("edit_request.id"))
432 key = db.Column(db.Enum(PackagePropertyKey), nullable=False)
434 # TODO: make diff instead
435 oldValue = db.Column(db.Text, nullable=True)
436 newValue = db.Column(db.Text, nullable=True)
438 def apply(self, package):
439 if self.key == PackagePropertyKey.tags:
441 for tagTitle in self.newValue.split(","):
442 tag = Tag.query.filter_by(title=tagTitle.strip()).first()
443 package.tags.append(tag)
445 setattr(package, self.key.name, self.newValue)
448 db_adapter = SQLAlchemyAdapter(db, User) # Register the User model
449 user_manager = UserManager(db_adapter, app) # Initialize Flask-User