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/>.
20 from app import app, gravatar
21 from urllib.parse import urlparse
23 from flask import Flask, url_for
24 from flask_sqlalchemy import SQLAlchemy, BaseQuery
25 from flask_migrate import Migrate
26 from flask_user import login_required, UserManager, UserMixin, SQLAlchemyAdapter
27 from sqlalchemy import func, CheckConstraint
28 from sqlalchemy_searchable import SearchQueryMixin
29 from sqlalchemy_utils.types import TSVectorType
30 from sqlalchemy_searchable import make_searchable
35 migrate = Migrate(app, db)
36 make_searchable(db.metadata)
39 class ArticleQuery(BaseQuery, SearchQueryMixin):
43 class UserRank(enum.Enum):
53 def atLeast(self, min):
54 return self.value >= min.value
57 return self.name.replace("_", " ").title()
60 return self.name.lower()
67 return [(choice, choice.getTitle()) for choice in cls]
70 def coerce(cls, item):
71 return item if type(item) == UserRank else UserRank[item]
74 class Permission(enum.Enum):
75 EDIT_PACKAGE = "EDIT_PACKAGE"
76 APPROVE_CHANGES = "APPROVE_CHANGES"
77 DELETE_PACKAGE = "DELETE_PACKAGE"
78 CHANGE_AUTHOR = "CHANGE_AUTHOR"
79 CHANGE_NAME = "CHANGE_NAME"
80 MAKE_RELEASE = "MAKE_RELEASE"
81 ADD_SCREENSHOTS = "ADD_SCREENSHOTS"
82 APPROVE_SCREENSHOT = "APPROVE_SCREENSHOT"
83 APPROVE_RELEASE = "APPROVE_RELEASE"
84 APPROVE_NEW = "APPROVE_NEW"
85 CHANGE_RELEASE_URL = "CHANGE_RELEASE_URL"
86 CHANGE_DNAME = "CHANGE_DNAME"
87 CHANGE_RANK = "CHANGE_RANK"
88 CHANGE_EMAIL = "CHANGE_EMAIL"
89 EDIT_EDITREQUEST = "EDIT_EDITREQUEST"
90 SEE_THREAD = "SEE_THREAD"
91 CREATE_THREAD = "CREATE_THREAD"
92 UNAPPROVE_PACKAGE = "UNAPPROVE_PACKAGE"
93 TOPIC_DISCARD = "TOPIC_DISCARD"
94 CREATE_TOKEN = "CREATE_TOKEN"
96 # Only return true if the permission is valid for *all* contexts
97 # See Package.checkPerm for package-specific contexts
98 def check(self, user):
99 if not user.is_authenticated:
102 if self == Permission.APPROVE_NEW or \
103 self == Permission.APPROVE_CHANGES or \
104 self == Permission.APPROVE_RELEASE or \
105 self == Permission.APPROVE_SCREENSHOT or \
106 self == Permission.SEE_THREAD:
107 return user.rank.atLeast(UserRank.EDITOR)
109 raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
111 class User(db.Model, UserMixin):
112 id = db.Column(db.Integer, primary_key=True)
114 # User authentication information
115 username = db.Column(db.String(50, collation="NOCASE"), nullable=False, unique=True, index=True)
116 password = db.Column(db.String(255), nullable=True)
117 reset_password_token = db.Column(db.String(100), nullable=False, server_default="")
119 rank = db.Column(db.Enum(UserRank))
122 github_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
123 forums_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
125 # User email information
126 email = db.Column(db.String(255), nullable=True, unique=True)
127 confirmed_at = db.Column(db.DateTime())
130 profile_pic = db.Column(db.String(255), nullable=True, server_default=None)
131 active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
132 display_name = db.Column(db.String(100), nullable=False, server_default="")
135 website_url = db.Column(db.String(255), nullable=True, default=None)
136 donate_url = db.Column(db.String(255), nullable=True, default=None)
139 notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id")
141 # causednotifs = db.relationship("Notification", backref="causer", lazy="dynamic")
142 packages = db.relationship("Package", backref="author", lazy="dynamic")
143 requests = db.relationship("EditRequest", backref="author", lazy="dynamic")
144 threads = db.relationship("Thread", backref="author", lazy="dynamic")
145 tokens = db.relationship("APIToken", backref="owner", lazy="dynamic")
146 replies = db.relationship("ThreadReply", backref="author", lazy="dynamic")
148 def __init__(self, username, active=False, email=None, password=None):
149 self.username = username
150 self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
151 self.display_name = username
154 self.password = password
155 self.rank = UserRank.NOT_JOINED
157 def canAccessTodoList(self):
158 return Permission.APPROVE_NEW.check(self) or \
159 Permission.APPROVE_RELEASE.check(self) or \
160 Permission.APPROVE_CHANGES.check(self)
163 return self.rank.atLeast(UserRank.NEW_MEMBER)
165 def getProfilePicURL(self):
167 return self.profile_pic
169 return gravatar(self.email or "")
171 def checkPerm(self, user, perm):
172 if not user.is_authenticated:
175 if type(perm) == str:
176 perm = Permission[perm]
177 elif type(perm) != Permission:
178 raise Exception("Unknown permission given to User.checkPerm()")
180 # Members can edit their own packages, and editors can edit any packages
181 if perm == Permission.CHANGE_AUTHOR:
182 return user.rank.atLeast(UserRank.EDITOR)
183 elif perm == Permission.CHANGE_RANK or perm == Permission.CHANGE_DNAME:
184 return user.rank.atLeast(UserRank.MODERATOR)
185 elif perm == Permission.CHANGE_EMAIL:
186 return user == self or (user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank))
187 elif perm == Permission.CREATE_TOKEN:
189 return user.rank.atLeast(UserRank.MEMBER)
191 return user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank)
193 raise Exception("Permission {} is not related to users".format(perm.name))
195 def canCommentRL(self):
196 hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
197 return ThreadReply.query.filter_by(author=self) \
198 .filter(ThreadReply.created_at > hour_ago).count() < 4
200 def canOpenThreadRL(self):
201 hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
202 return Thread.query.filter_by(author=self) \
203 .filter(Thread.created_at > hour_ago).count() < 2
205 class UserEmailVerification(db.Model):
206 id = db.Column(db.Integer, primary_key=True)
207 user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
208 email = db.Column(db.String(100))
209 token = db.Column(db.String(32))
210 user = db.relationship("User", foreign_keys=[user_id])
212 class Notification(db.Model):
213 id = db.Column(db.Integer, primary_key=True)
214 user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
215 causer_id = db.Column(db.Integer, db.ForeignKey("user.id"))
216 user = db.relationship("User", foreign_keys=[user_id])
217 causer = db.relationship("User", foreign_keys=[causer_id])
219 title = db.Column(db.String(100), nullable=False)
220 url = db.Column(db.String(200), nullable=True)
222 def __init__(self, us, cau, titl, ur):
224 title = title[:99] + "…"
232 class License(db.Model):
233 id = db.Column(db.Integer, primary_key=True)
234 name = db.Column(db.String(50), nullable=False, unique=True)
235 is_foss = db.Column(db.Boolean, nullable=False, default=True)
237 def __init__(self, v, is_foss=True):
239 self.is_foss = is_foss
245 class PackageType(enum.Enum):
251 return self.name.lower()
259 return PackageType[name.upper()]
265 return [(choice, choice.value) for choice in cls]
268 def coerce(cls, item):
269 return item if type(item) == PackageType else PackageType[item]
272 class PackagePropertyKey(enum.Enum):
275 short_desc = "Short Description"
279 media_license = "Media License"
281 provides = "Provides"
284 issueTracker = "Issue Tracker"
285 forums = "Forum Topic ID"
287 def convert(self, value):
288 if self == PackagePropertyKey.tags:
289 return ",".join([t.title for t in value])
290 elif self == PackagePropertyKey.provides:
291 return ",".join([t.name for t in value])
295 provides = db.Table("provides",
296 db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True),
297 db.Column("metapackage_id", db.Integer, db.ForeignKey("meta_package.id"), primary_key=True)
300 tags = db.Table("tags",
301 db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True),
302 db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True)
305 class Dependency(db.Model):
306 id = db.Column(db.Integer, primary_key=True)
307 depender_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
308 package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
309 package = db.relationship("Package", foreign_keys=[package_id])
310 meta_package_id = db.Column(db.Integer, db.ForeignKey("meta_package.id"), nullable=True)
311 optional = db.Column(db.Boolean, nullable=False, default=False)
312 __table_args__ = (db.UniqueConstraint("depender_id", "package_id", "meta_package_id", name="_dependency_uc"), )
314 def __init__(self, depender=None, package=None, meta=None):
318 self.depender = depender
320 packageProvided = package is not None
321 metaProvided = meta is not None
323 if packageProvided and not metaProvided:
324 self.package = package
325 elif metaProvided and not packageProvided:
326 self.meta_package = meta
328 raise Exception("Either meta or package must be given, but not both!")
331 if self.package is not None:
332 return self.package.author.username + "/" + self.package.name
333 elif self.meta_package is not None:
334 return self.meta_package.name
336 raise Exception("Meta and package are both none!")
339 def SpecToList(depender, spec, cache={}):
341 arr = spec.split(",")
344 pattern1 = re.compile("^([a-z0-9_]+)$")
345 pattern2 = re.compile("^([A-Za-z0-9_]+)/([a-z0-9_]+)$")
352 if pattern1.match(x):
353 meta = MetaPackage.GetOrCreate(x, cache)
354 retval.append(Dependency(depender, meta=meta))
356 m = pattern2.match(x)
357 username = m.group(1)
359 user = User.query.filter_by(username=username).first()
361 raise Exception("Unable to find user " + username)
363 package = Package.query.filter_by(author=user, name=name).first()
365 raise Exception("Unable to find package " + name + " by " + username)
367 retval.append(Dependency(depender, package=package))
372 class Package(db.Model):
373 query_class = ArticleQuery
375 id = db.Column(db.Integer, primary_key=True)
378 author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
379 name = db.Column(db.Unicode(100), nullable=False)
380 title = db.Column(db.Unicode(100), nullable=False)
381 short_desc = db.Column(db.Unicode(200), nullable=False)
382 desc = db.Column(db.UnicodeText, nullable=True)
383 type = db.Column(db.Enum(PackageType))
384 created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
386 name_valid = db.CheckConstraint("name ~* '^[a-z0-9_]+$'")
388 search_vector = db.Column(TSVectorType("title", "short_desc", "desc", \
389 weights={ "title": "A", "short_desc": "B", "desc": "C" }))
391 license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
392 license = db.relationship("License", foreign_keys=[license_id])
393 media_license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
394 media_license = db.relationship("License", foreign_keys=[media_license_id])
396 approved = db.Column(db.Boolean, nullable=False, default=False)
397 soft_deleted = db.Column(db.Boolean, nullable=False, default=False)
399 score = db.Column(db.Float, nullable=False, default=0)
401 review_thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=True, default=None)
402 review_thread = db.relationship("Thread", foreign_keys=[review_thread_id])
405 repo = db.Column(db.String(200), nullable=True)
406 website = db.Column(db.String(200), nullable=True)
407 issueTracker = db.Column(db.String(200), nullable=True)
408 forums = db.Column(db.Integer, nullable=True)
410 provides = db.relationship("MetaPackage", secondary=provides, lazy="subquery",
411 backref=db.backref("packages", lazy="dynamic", order_by=db.desc("score")))
413 dependencies = db.relationship("Dependency", backref="depender", lazy="dynamic", foreign_keys=[Dependency.depender_id])
415 tags = db.relationship("Tag", secondary=tags, lazy="subquery",
416 backref=db.backref("packages", lazy=True))
418 releases = db.relationship("PackageRelease", backref="package",
419 lazy="dynamic", order_by=db.desc("package_release_releaseDate"))
421 screenshots = db.relationship("PackageScreenshot", backref="package",
422 lazy="dynamic", order_by=db.asc("package_screenshot_id"))
424 requests = db.relationship("EditRequest", backref="package",
427 def __init__(self, package=None):
431 self.author_id = package.author_id
432 self.created_at = package.created_at
433 self.approved = package.approved
435 for e in PackagePropertyKey:
436 setattr(self, e.name, getattr(package, e.name))
439 return self.license.is_foss and self.media_license.is_foss
444 elif self.review_thread_id:
446 elif (self.type == PackageType.GAME or \
447 self.type == PackageType.TXP) and \
448 self.screenshots.count() == 0:
450 elif not self.getDownloadRelease():
452 elif "Other" in self.license.name or "Other" in self.media_license.name:
457 def getAsDictionaryKey(self):
460 "author": self.author.display_name,
461 "type": self.type.toName(),
464 def getAsDictionaryShort(self, base_url, version=None, protonum=None):
465 tnurl = self.getThumbnailURL(1)
466 release = self.getDownloadRelease(version=version, protonum=protonum)
470 "author": self.author.display_name,
471 "short_description": self.short_desc,
472 "type": self.type.toName(),
473 "release": release and release.id,
474 "thumbnail": (base_url + tnurl) if tnurl is not None else None,
475 "score": round(self.score * 10) / 10
478 def getAsDictionary(self, base_url, version=None, protonum=None):
479 tnurl = self.getThumbnailURL(1)
480 release = self.getDownloadRelease(version=version, protonum=protonum)
482 "author": self.author.display_name,
485 "short_description": self.short_desc,
487 "type": self.type.toName(),
488 "created_at": self.created_at,
490 "license": self.license.name,
491 "media_license": self.media_license.name,
494 "website": self.website,
495 "issue_tracker": self.issueTracker,
496 "forums": self.forums,
498 "provides": [x.name for x in self.provides],
499 "thumbnail": (base_url + tnurl) if tnurl is not None else None,
500 "screenshots": [base_url + ss.url for ss in self.screenshots],
502 "url": base_url + self.getDownloadURL(),
503 "release": release and release.id,
505 "score": round(self.score * 10) / 10
508 def getThumbnailURL(self, level=2):
509 screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first()
510 return screenshot.getThumbnailURL(level) if screenshot is not None else None
512 def getMainScreenshotURL(self):
513 screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first()
514 return screenshot.url if screenshot is not None else None
516 def getDetailsURL(self):
517 return url_for("packages.view",
518 author=self.author.username, name=self.name)
520 def getEditURL(self):
521 return url_for("packages.create_edit",
522 author=self.author.username, name=self.name)
524 def getApproveURL(self):
525 return url_for("packages.approve",
526 author=self.author.username, name=self.name)
528 def getRemoveURL(self):
529 return url_for("packages.remove",
530 author=self.author.username, name=self.name)
532 def getNewScreenshotURL(self):
533 return url_for("packages.create_screenshot",
534 author=self.author.username, name=self.name)
536 def getCreateReleaseURL(self):
537 return url_for("packages.create_release",
538 author=self.author.username, name=self.name)
540 def getCreateEditRequestURL(self):
541 return url_for("create_edit_editrequest_page",
542 author=self.author.username, name=self.name)
544 def getBulkReleaseURL(self):
545 return url_for("packages.bulk_change_release",
546 author=self.author.username, name=self.name)
548 def getDownloadURL(self):
549 return url_for("packages.download",
550 author=self.author.username, name=self.name)
552 def getDownloadRelease(self, version=None, protonum=None):
553 if version is None and protonum is not None:
554 version = MinetestRelease.query.filter(MinetestRelease.protocol >= int(protonum)).first()
555 if version is not None:
561 for rel in self.releases:
562 if rel.approved and (version is None or
563 ((rel.min_rel is None or rel.min_rel_id <= version) and \
564 (rel.max_rel is None or rel.max_rel_id >= version))):
569 def getDownloadCount(self):
571 for release in self.releases:
572 counter += release.downloads
575 def checkPerm(self, user, perm):
576 if not user.is_authenticated:
579 if type(perm) == str:
580 perm = Permission[perm]
581 elif type(perm) != Permission:
582 raise Exception("Unknown permission given to Package.checkPerm()")
584 isOwner = user == self.author
586 if perm == Permission.CREATE_THREAD:
587 return user.rank.atLeast(UserRank.MEMBER)
589 # Members can edit their own packages, and editors can edit any packages
590 if perm == Permission.MAKE_RELEASE or perm == Permission.ADD_SCREENSHOTS:
591 return isOwner or user.rank.atLeast(UserRank.EDITOR)
593 if perm == Permission.EDIT_PACKAGE or perm == Permission.APPROVE_CHANGES:
595 return user.rank.atLeast(UserRank.MEMBER if self.approved else UserRank.NEW_MEMBER)
597 return user.rank.atLeast(UserRank.EDITOR)
599 # Anyone can change the package name when not approved, but only editors when approved
600 elif perm == Permission.CHANGE_NAME:
601 return not self.approved or user.rank.atLeast(UserRank.EDITOR)
603 # Editors can change authors and approve new packages
604 elif perm == Permission.APPROVE_NEW or perm == Permission.CHANGE_AUTHOR:
605 return user.rank.atLeast(UserRank.EDITOR)
607 elif perm == Permission.APPROVE_RELEASE or perm == Permission.APPROVE_SCREENSHOT:
608 return user.rank.atLeast(UserRank.TRUSTED_MEMBER if isOwner else UserRank.EDITOR)
610 # Moderators can delete packages
611 elif perm == Permission.DELETE_PACKAGE or perm == Permission.UNAPPROVE_PACKAGE \
612 or perm == Permission.CHANGE_RELEASE_URL:
613 return user.rank.atLeast(UserRank.MODERATOR)
616 raise Exception("Permission {} is not related to packages".format(perm.name))
618 def setStartScore(self):
619 downloads = db.session.query(func.sum(PackageRelease.downloads)). \
620 filter(PackageRelease.package_id == self.id).scalar() or 0
624 topic = self.forums and ForumTopic.query.get(self.forums)
626 months = (datetime.datetime.now() - topic.created_at).days / 30
628 forum_score = topic.views / max(years, 0.0416) + 80*min(max(months, 0.5), 6)
629 forum_bonus = topic.views + topic.posts
631 self.score = max(downloads, forum_score * 0.6) + forum_bonus
633 if self.getMainScreenshotURL() is None:
636 if not self.license.is_foss or not self.media_license.is_foss:
640 class MetaPackage(db.Model):
641 id = db.Column(db.Integer, primary_key=True)
642 name = db.Column(db.String(100), unique=True, nullable=False)
643 dependencies = db.relationship("Dependency", backref="meta_package", lazy="dynamic")
645 def __init__(self, name=None):
652 def ListToSpec(list):
653 return ",".join([str(x) for x in list])
656 def GetOrCreate(name, cache={}):
659 mp = MetaPackage.query.filter_by(name=name).first()
662 mp = MetaPackage(name)
669 def SpecToList(spec, cache={}):
671 arr = spec.split(",")
674 pattern = re.compile("^([a-z0-9_]+)$")
681 if not pattern.match(x):
684 retval.append(MetaPackage.GetOrCreate(x, cache))
689 id = db.Column(db.Integer, primary_key=True)
690 name = db.Column(db.String(100), unique=True, nullable=False)
691 title = db.Column(db.String(100), nullable=False)
692 backgroundColor = db.Column(db.String(6), nullable=False)
693 textColor = db.Column(db.String(6), nullable=False)
695 def __init__(self, title, backgroundColor="000000", textColor="ffffff"):
697 self.backgroundColor = backgroundColor
698 self.textColor = textColor
701 regex = re.compile("[^a-z_]")
702 self.name = regex.sub("", self.title.lower().replace(" ", "_"))
705 class MinetestRelease(db.Model):
706 id = db.Column(db.Integer, primary_key=True)
707 name = db.Column(db.String(100), unique=True, nullable=False)
708 protocol = db.Column(db.Integer, nullable=False, default=0)
710 def __init__(self, name=None):
714 return None if self.name == "None" else self
717 class PackageRelease(db.Model):
718 id = db.Column(db.Integer, primary_key=True)
720 package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
721 title = db.Column(db.String(100), nullable=False)
722 releaseDate = db.Column(db.DateTime, nullable=False)
723 url = db.Column(db.String(200), nullable=False)
724 approved = db.Column(db.Boolean, nullable=False, default=False)
725 task_id = db.Column(db.String(37), nullable=True)
726 commit_hash = db.Column(db.String(41), nullable=True, default=None)
727 downloads = db.Column(db.Integer, nullable=False, default=0)
729 min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
730 min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])
732 max_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
733 max_rel = db.relationship("MinetestRelease", foreign_keys=[max_rel_id])
735 # If the release is approved, then the task_id must be null and the url must be present
736 CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)")
738 def getEditURL(self):
739 return url_for("packages.edit_release",
740 author=self.package.author.username,
741 name=self.package.name,
744 def getDownloadURL(self):
745 return url_for("packages.download_release",
746 author=self.package.author.username,
747 name=self.package.name,
752 self.releaseDate = datetime.datetime.now()
754 def approve(self, user):
755 if self.package.approved and \
756 not self.package.checkPerm(user, Permission.APPROVE_RELEASE):
759 assert self.task_id is None and self.url is not None and self.url != ""
765 class PackageReview(db.Model):
766 id = db.Column(db.Integer, primary_key=True)
767 package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
768 thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
769 recommend = db.Column(db.Boolean, nullable=False, default=True)
772 class PackageScreenshot(db.Model):
773 id = db.Column(db.Integer, primary_key=True)
774 package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
775 title = db.Column(db.String(100), nullable=False)
776 url = db.Column(db.String(100), nullable=False)
777 approved = db.Column(db.Boolean, nullable=False, default=False)
780 def getEditURL(self):
781 return url_for("packages.edit_screenshot",
782 author=self.package.author.username,
783 name=self.package.name,
786 def getThumbnailURL(self, level=2):
787 return self.url.replace("/uploads/", ("/thumbnails/{:d}/").format(level))
790 class APIToken(db.Model):
791 id = db.Column(db.Integer, primary_key=True)
792 access_token = db.Column(db.String(34), unique=True)
793 name = db.Column(db.String(100), nullable=False)
794 owner_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
795 created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
797 def canOperateOnPackage(self, package):
798 return packages.count() == 0 or package in packages
801 class EditRequest(db.Model):
802 id = db.Column(db.Integer, primary_key=True)
804 package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
805 author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
807 title = db.Column(db.String(100), nullable=False)
808 desc = db.Column(db.String(1000), nullable=True)
813 status = db.Column(db.Integer, nullable=False, default=0)
815 changes = db.relationship("EditRequestChange", backref="request",
819 return url_for("view_editrequest_page",
820 author=self.package.author.username,
821 name=self.package.name,
824 def getApproveURL(self):
825 return url_for("approve_editrequest_page",
826 author=self.package.author.username,
827 name=self.package.name,
830 def getRejectURL(self):
831 return url_for("reject_editrequest_page",
832 author=self.package.author.username,
833 name=self.package.name,
836 def getEditURL(self):
837 return url_for("create_edit_editrequest_page",
838 author=self.package.author.username,
839 name=self.package.name,
842 def applyAll(self, package):
843 for change in self.changes:
844 change.apply(package)
847 def checkPerm(self, user, perm):
848 if not user.is_authenticated:
851 if type(perm) == str:
852 perm = Permission[perm]
853 elif type(perm) != Permission:
854 raise Exception("Unknown permission given to EditRequest.checkPerm()")
856 isOwner = user == self.author
858 # Members can edit their own packages, and editors can edit any packages
859 if perm == Permission.EDIT_EDITREQUEST:
860 return isOwner or user.rank.atLeast(UserRank.EDITOR)
863 raise Exception("Permission {} is not related to packages".format(perm.name))
868 class EditRequestChange(db.Model):
869 id = db.Column(db.Integer, primary_key=True)
871 request_id = db.Column(db.Integer, db.ForeignKey("edit_request.id"))
872 key = db.Column(db.Enum(PackagePropertyKey), nullable=False)
874 # TODO: make diff instead
875 oldValue = db.Column(db.Text, nullable=True)
876 newValue = db.Column(db.Text, nullable=True)
878 def apply(self, package):
879 if self.key == PackagePropertyKey.tags:
881 for tagTitle in self.newValue.split(","):
882 tag = Tag.query.filter_by(title=tagTitle.strip()).first()
883 package.tags.append(tag)
886 setattr(package, self.key.name, self.newValue)
889 watchers = db.Table("watchers",
890 db.Column("user_id", db.Integer, db.ForeignKey("user.id"), primary_key=True),
891 db.Column("thread_id", db.Integer, db.ForeignKey("thread.id"), primary_key=True)
894 class Thread(db.Model):
895 id = db.Column(db.Integer, primary_key=True)
897 package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
898 package = db.relationship("Package", foreign_keys=[package_id])
900 author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
901 title = db.Column(db.String(100), nullable=False)
902 private = db.Column(db.Boolean, server_default="0")
904 created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
906 replies = db.relationship("ThreadReply", backref="thread", lazy="dynamic")
908 watchers = db.relationship("User", secondary=watchers, lazy="subquery", \
909 backref=db.backref("watching", lazy=True))
912 def getSubscribeURL(self):
913 return url_for("threads.subscribe",
916 def getUnsubscribeURL(self):
917 return url_for("threads.unsubscribe",
920 def checkPerm(self, user, perm):
921 if not user.is_authenticated:
922 return not self.private
924 if type(perm) == str:
925 perm = Permission[perm]
926 elif type(perm) != Permission:
927 raise Exception("Unknown permission given to Thread.checkPerm()")
929 isOwner = user == self.author or (self.package is not None and self.package.author == user)
931 if perm == Permission.SEE_THREAD:
932 return not self.private or isOwner or user.rank.atLeast(UserRank.EDITOR)
935 raise Exception("Permission {} is not related to threads".format(perm.name))
937 class ThreadReply(db.Model):
938 id = db.Column(db.Integer, primary_key=True)
939 thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
940 comment = db.Column(db.String(500), nullable=False)
941 author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
942 created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
945 REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \
946 "minetest.net", "dropboxusercontent.com", "4shared.com", \
947 "digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net", \
948 "imageshack.com", "imgur.com"]
950 class ForumTopic(db.Model):
951 topic_id = db.Column(db.Integer, primary_key=True, autoincrement=False)
952 author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
953 author = db.relationship("User")
955 wip = db.Column(db.Boolean, server_default="0")
956 discarded = db.Column(db.Boolean, server_default="0")
958 type = db.Column(db.Enum(PackageType), nullable=False)
959 title = db.Column(db.String(200), nullable=False)
960 name = db.Column(db.String(30), nullable=True)
961 link = db.Column(db.String(200), nullable=True)
963 posts = db.Column(db.Integer, nullable=False)
964 views = db.Column(db.Integer, nullable=False)
966 created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
968 def getRepoURL(self):
969 if self.link is None:
972 for item in REPO_BLACKLIST:
973 if item in self.link:
976 return self.link.replace("repo.or.cz/w/", "repo.or.cz/")
978 def getAsDictionary(self):
980 "author": self.author.username,
982 "type": self.type.toName(),
989 "discarded": self.discarded,
990 "created_at": self.created_at.isoformat(),
993 def checkPerm(self, user, perm):
994 if not user.is_authenticated:
997 if type(perm) == str:
998 perm = Permission[perm]
999 elif type(perm) != Permission:
1000 raise Exception("Unknown permission given to ForumTopic.checkPerm()")
1002 if perm == Permission.TOPIC_DISCARD:
1003 return self.author == user or user.rank.atLeast(UserRank.EDITOR)
1006 raise Exception("Permission {} is not related to topics".format(perm.name))
1010 db_adapter = SQLAlchemyAdapter(db, User) # Register the User model
1011 user_manager = UserManager(db_adapter, app) # Initialize Flask-User