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"
53 # Only return true if the permission is valid for *all* contexts
54 # See Package.checkPerm for package-specific contexts
55 def check(self, user):
56 if not user.is_authenticated:
59 if self == Permission.APPROVE_NEW or \
60 self == Permission.APPROVE_CHANGES or \
61 self == Permission.APPROVE_RELEASE:
62 return user.rank.atLeast(UserRank.EDITOR)
64 raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
67 class User(db.Model, UserMixin):
68 id = db.Column(db.Integer, primary_key=True)
70 # User authentication information
71 username = db.Column(db.String(50), nullable=False, unique=True)
72 password = db.Column(db.String(255), nullable=False, server_default="")
73 reset_password_token = db.Column(db.String(100), nullable=False, server_default="")
75 rank = db.Column(db.Enum(UserRank))
78 github_username = db.Column(db.String(50), nullable=True, unique=True)
79 forums_username = db.Column(db.String(50), nullable=True, unique=True)
81 # User email information
82 email = db.Column(db.String(255), nullable=True, unique=True)
83 confirmed_at = db.Column(db.DateTime())
86 active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
87 display_name = db.Column(db.String(100), nullable=False, server_default="")
90 packages = db.relationship("Package", backref="author", lazy="dynamic")
91 requests = db.relationship("EditRequest", backref="author", lazy="dynamic")
93 def __init__(self, username):
96 self.username = username
97 self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
98 self.display_name = username
99 self.rank = UserRank.NOT_JOINED
102 return self.rank.atLeast(UserRank.NEW_MEMBER)
104 def checkPerm(self, user, perm):
105 if not user.is_authenticated:
108 if type(perm) == str:
109 perm = Permission[perm]
110 elif type(perm) != Permission:
111 raise Exception("Unknown permission given to User.checkPerm()")
113 # Members can edit their own packages, and editors can edit any packages
114 if perm == Permission.CHANGE_AUTHOR:
115 return user.rank.atLeast(UserRank.EDITOR)
116 elif perm == Permission.CHANGE_RANK:
117 return user.rank.atLeast(UserRank.MODERATOR)
119 raise Exception("Permission {} is not related to users".format(perm.name))
121 class License(db.Model):
122 id = db.Column(db.Integer, primary_key=True)
123 name = db.Column(db.String(50), nullable=False, unique=True)
124 packages = db.relationship("Package", backref="license", lazy="dynamic")
126 def __init__(self, v):
133 class PackageType(enum.Enum):
139 return self.name.lower()
146 return [(choice, choice.value) for choice in cls]
149 def coerce(cls, item):
150 return item if type(item) == PackageType else PackageType[item]
153 class PackagePropertyKey(enum.Enum):
156 shortDesc = "Short Description"
163 issueTracker = "Issue Tracker"
164 forums = "Forum Topic ID"
166 def convert(self, value):
167 if self == PackagePropertyKey.tags:
168 return ','.join([t.title for t in value])
172 tags = db.Table('tags',
173 db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True),
174 db.Column('package_id', db.Integer, db.ForeignKey('package.id'), primary_key=True)
177 class Package(db.Model):
178 id = db.Column(db.Integer, primary_key=True)
181 author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
182 name = db.Column(db.String(100), nullable=False)
183 title = db.Column(db.String(100), nullable=False)
184 shortDesc = db.Column(db.String(200), nullable=False)
185 desc = db.Column(db.Text, nullable=True)
186 type = db.Column(db.Enum(PackageType))
188 license_id = db.Column(db.Integer, db.ForeignKey("license.id"))
190 approved = db.Column(db.Boolean, nullable=False, default=False)
193 repo = db.Column(db.String(200), nullable=True)
194 website = db.Column(db.String(200), nullable=True)
195 issueTracker = db.Column(db.String(200), nullable=True)
196 forums = db.Column(db.Integer, nullable=False)
199 tags = db.relationship('Tag', secondary=tags, lazy='subquery',
200 backref=db.backref('packages', lazy=True))
202 releases = db.relationship("PackageRelease", backref="package",
203 lazy="dynamic", order_by=db.desc("package_release_releaseDate"))
205 screenshots = db.relationship("PackageScreenshot", backref="package",
208 requests = db.relationship("EditRequest", backref="package",
211 def getAsDictionary(self, base_url):
215 "author": self.author.display_name,
216 "shortDesc": self.shortDesc,
217 "type": self.type.toName(),
218 "license": self.license.name,
220 "url": base_url + self.getDownloadURL(),
221 "release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
222 "screenshots": [base_url + ss.url for ss in self.screenshots]
225 def getDetailsURL(self):
226 return url_for("package_page",
227 type=self.type.toName(),
228 author=self.author.username, name=self.name)
230 def getEditURL(self):
231 return url_for("create_edit_package_page",
232 type=self.type.toName(),
233 author=self.author.username, name=self.name)
235 def getApproveURL(self):
236 return url_for("approve_package_page",
237 type=self.type.toName(),
238 author=self.author.username, name=self.name)
240 def getNewScreenshotURL(self):
241 return url_for("create_screenshot_page",
242 type=self.type.toName(),
243 author=self.author.username, name=self.name)
245 def getCreateReleaseURL(self):
246 return url_for("create_release_page",
247 type=self.type.toName(),
248 author=self.author.username, name=self.name)
250 def getCreateEditRequestURL(self):
251 return url_for("create_editrequest_page",
252 ptype=self.type.toName(),
253 author=self.author.username, name=self.name)
255 def getDownloadURL(self):
256 return url_for("package_download_page",
257 type=self.type.toName(),
258 author=self.author.username, name=self.name)
260 def getMainScreenshotURL(self):
261 screenshot = self.screenshots.first()
262 return screenshot.url if screenshot is not None else None
264 def getDownloadRelease(self):
265 for rel in self.releases:
271 def checkPerm(self, user, perm):
272 if not user.is_authenticated:
275 if type(perm) == str:
276 perm = Permission[perm]
277 elif type(perm) != Permission:
278 raise Exception("Unknown permission given to Package.checkPerm()")
280 isOwner = user == self.author
282 # Members can edit their own packages, and editors can edit any packages
283 if perm == Permission.MAKE_RELEASE:
284 return isOwner or user.rank.atLeast(UserRank.EDITOR)
286 if perm == Permission.EDIT_PACKAGE or perm == Permission.APPROVE_CHANGES:
287 return user.rank.atLeast(UserRank.MEMBER if isOwner else UserRank.EDITOR)
289 # Editors can change authors, approve new packages, and approve releases
290 elif perm == Permission.CHANGE_AUTHOR or perm == Permission.APPROVE_NEW \
291 or perm == Permission.APPROVE_RELEASE:
292 return user.rank.atLeast(UserRank.EDITOR)
294 # Moderators can delete packages
295 elif perm == Permission.DELETE_PACKAGE or perm == Permission.CHANGE_RELEASE_URL:
296 return user.rank.atLeast(UserRank.MODERATOR)
299 raise Exception("Permission {} is not related to packages".format(perm.name))
302 id = db.Column(db.Integer, primary_key=True)
303 name = db.Column(db.String(100), unique=True, nullable=False)
304 title = db.Column(db.String(100), nullable=False)
305 backgroundColor = db.Column(db.String(6), nullable=False)
306 textColor = db.Column(db.String(6), nullable=False)
308 def __init__(self, title, backgroundColor="000000", textColor="ffffff"):
310 self.backgroundColor = backgroundColor
311 self.textColor = textColor
314 regex = re.compile('[^a-z_]')
315 self.name = regex.sub("", self.title.lower().replace(" ", "_"))
317 class PackageRelease(db.Model):
318 id = db.Column(db.Integer, primary_key=True)
320 package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
321 title = db.Column(db.String(100), nullable=False)
322 releaseDate = db.Column(db.DateTime, nullable=False)
323 url = db.Column(db.String(100), nullable=False)
324 approved = db.Column(db.Boolean, nullable=False, default=False)
325 task_id = db.Column(db.String(32), nullable=True)
328 def getEditURL(self):
329 return url_for("edit_release_page",
330 type=self.package.type.toName(),
331 author=self.package.author.username,
332 name=self.package.name,
336 self.releaseDate = datetime.now()
338 class PackageScreenshot(db.Model):
339 id = db.Column(db.Integer, primary_key=True)
340 package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
341 title = db.Column(db.String(100), nullable=False)
342 url = db.Column(db.String(100), nullable=False)
344 def getThumbnailURL(self):
345 return self.url # TODO
347 class EditRequest(db.Model):
348 id = db.Column(db.Integer, primary_key=True)
350 package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
351 author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
353 title = db.Column(db.String(100), nullable=False)
354 desc = db.Column(db.String(1000), nullable=True)
356 status = db.Column(db.Integer, nullable=False, default=0)
358 changes = db.relationship("EditRequestChange", backref="request",
362 return url_for("view_editrequest_page",
363 ptype=self.package.type.toName(),
364 author=self.package.author.username,
365 name=self.package.name,
368 def getApproveURL(self):
369 return url_for("approve_editrequest_page",
370 ptype=self.package.type.toName(),
371 author=self.package.author.username,
372 name=self.package.name,
375 def getRejectURL(self):
376 return url_for("reject_editrequest_page",
377 ptype=self.package.type.toName(),
378 author=self.package.author.username,
379 name=self.package.name,
382 def applyAll(self, package):
383 for change in self.changes:
384 change.apply(package)
388 class EditRequestChange(db.Model):
389 id = db.Column(db.Integer, primary_key=True)
391 request_id = db.Column(db.Integer, db.ForeignKey("edit_request.id"))
392 key = db.Column(db.Enum(PackagePropertyKey), nullable=False)
394 # TODO: make diff instead
395 oldValue = db.Column(db.Text, nullable=True)
396 newValue = db.Column(db.Text, nullable=True)
398 def apply(self, package):
399 if self.key == PackagePropertyKey.tags:
401 for tagTitle in self.newValue.split(","):
402 tag = Tag.query.filter_by(title=tagTitle.strip()).first()
403 package.tags.append(tag)
405 setattr(package, self.key.name, self.newValue)
408 db_adapter = SQLAlchemyAdapter(db, User) # Register the User model
409 user_manager = UserManager(db_adapter, app) # Initialize Flask-User