+ def getScoreDict(self):
+ return {
+ "author": self.author.username,
+ "name": self.name,
+ "score": self.score,
+ "score_downloads": self.score_downloads,
+ "score_reviews": self.score - self.score_downloads,
+ "downloads": self.downloads
+ }
+
+ def recalcScore(self):
+ review_scores = [ 100 * r.asSign() for r in self.reviews ]
+ self.score = self.score_downloads + sum(review_scores)
+
+
+class MetaPackage(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(100), unique=True, nullable=False)
+ dependencies = db.relationship("Dependency", backref="meta_package", lazy="dynamic")
+
+ def __init__(self, name=None):
+ self.name = name
+
+ def __str__(self):
+ return self.name
+
+ @staticmethod
+ def ListToSpec(list):
+ return ",".join([str(x) for x in list])
+
+ @staticmethod
+ def GetOrCreate(name, cache={}):
+ mp = cache.get(name)
+ if mp is None:
+ mp = MetaPackage.query.filter_by(name=name).first()
+
+ if mp is None:
+ mp = MetaPackage(name)
+ db.session.add(mp)
+
+ cache[name] = mp
+ return mp
+
+ @staticmethod
+ def SpecToList(spec, cache={}):
+ retval = []
+ arr = spec.split(",")
+
+ import re
+ pattern = re.compile("^([a-z0-9_]+)$")
+
+ for x in arr:
+ x = x.strip()
+ if x == "":
+ continue
+
+ if not pattern.match(x):
+ continue
+
+ retval.append(MetaPackage.GetOrCreate(x, cache))
+
+ return retval
+
+class Tag(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(100), unique=True, nullable=False)
+ title = db.Column(db.String(100), nullable=False)
+ backgroundColor = db.Column(db.String(6), nullable=False)
+ textColor = db.Column(db.String(6), nullable=False)
+
+ def __init__(self, title, backgroundColor="000000", textColor="ffffff"):
+ self.title = title
+ self.backgroundColor = backgroundColor
+ self.textColor = textColor
+
+ import re
+ regex = re.compile("[^a-z_]")
+ self.name = regex.sub("", self.title.lower().replace(" ", "_"))
+
+
+class MinetestRelease(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(100), unique=True, nullable=False)
+ protocol = db.Column(db.Integer, nullable=False, default=0)
+
+ def __init__(self, name=None, protocol=0):
+ self.name = name
+ self.protocol = protocol
+
+ def getActual(self):
+ return None if self.name == "None" else self
+
+ @classmethod
+ def get(cls, version, protocol_num):
+ if version:
+ parts = version.strip().split(".")
+ if len(parts) >= 2:
+ major_minor = parts[0] + "." + parts[1]
+ query = MinetestRelease.query.filter(MinetestRelease.name.like("{}%".format(major_minor)))
+ if protocol_num:
+ query = query.filter_by(protocol=protocol_num)
+
+ release = query.one_or_none()
+ if release:
+ return release
+
+ if protocol_num:
+ return MinetestRelease.query.filter_by(protocol=protocol_num).first()
+
+ return None
+
+
+class PackageRelease(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+
+ package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
+ title = db.Column(db.String(100), nullable=False)
+ releaseDate = db.Column(db.DateTime, nullable=False)
+ url = db.Column(db.String(200), nullable=False)
+ approved = db.Column(db.Boolean, nullable=False, default=False)
+ task_id = db.Column(db.String(37), nullable=True)
+ commit_hash = db.Column(db.String(41), nullable=True, default=None)
+ downloads = db.Column(db.Integer, nullable=False, default=0)
+
+ min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
+ min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])
+
+ max_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
+ max_rel = db.relationship("MinetestRelease", foreign_keys=[max_rel_id])
+
+ # If the release is approved, then the task_id must be null and the url must be present
+ CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)")
+
+ def getAsDictionary(self):
+ return {
+ "id": self.id,
+ "title": self.title,
+ "url": self.url if self.url != "" else None,
+ "release_date": self.releaseDate.isoformat(),
+ "commit": self.commit_hash,
+ "downloads": self.downloads,
+ "min_protocol": self.min_rel and self.min_rel.protocol,
+ "max_protocol": self.max_rel and self.max_rel.protocol
+ }
+
+ def getEditURL(self):
+ return url_for("packages.edit_release",
+ author=self.package.author.username,
+ name=self.package.name,
+ id=self.id)
+
+ def getDeleteURL(self):
+ return url_for("packages.delete_release",
+ author=self.package.author.username,
+ name=self.package.name,
+ id=self.id)
+
+ def getDownloadURL(self):
+ return url_for("packages.download_release",
+ author=self.package.author.username,
+ name=self.package.name,
+ id=self.id)
+
+ def __init__(self):
+ self.releaseDate = datetime.datetime.now()
+
+ def approve(self, user):
+ if not self.package.checkPerm(user, Permission.APPROVE_RELEASE):
+ return False
+
+ assert self.task_id is None and self.url is not None and self.url != ""
+
+ self.approved = True
+ return True
+
+ def checkPerm(self, user, perm):
+ if not user.is_authenticated:
+ return False
+
+ if type(perm) == str:
+ perm = Permission[perm]
+ elif type(perm) != Permission:
+ raise Exception("Unknown permission given to PackageRelease.checkPerm()")
+
+ isOwner = user == self.package.author
+
+ if perm == Permission.DELETE_RELEASE:
+ if user.rank.atLeast(UserRank.ADMIN):
+ return True
+
+ if not (isOwner or user.rank.atLeast(UserRank.EDITOR)):
+ return False
+
+ if not self.package.approved or self.task_id is not None:
+ return True
+
+ count = PackageRelease.query \
+ .filter_by(package_id=self.package_id) \
+ .filter(PackageRelease.id > self.id) \
+ .count()
+
+ return count > 0
+ else:
+ raise Exception("Permission {} is not related to releases".format(perm.name))
+
+
+# class PackageReview(db.Model):
+# id = db.Column(db.Integer, primary_key=True)
+# package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
+# thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
+# recommend = db.Column(db.Boolean, nullable=False, default=True)
+
+
+class PackageScreenshot(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
+ title = db.Column(db.String(100), nullable=False)
+ url = db.Column(db.String(100), nullable=False)
+ approved = db.Column(db.Boolean, nullable=False, default=False)
+
+
+ def getEditURL(self):
+ return url_for("packages.edit_screenshot",
+ author=self.package.author.username,
+ name=self.package.name,
+ id=self.id)
+
+ def getThumbnailURL(self, level=2):
+ return self.url.replace("/uploads/", ("/thumbnails/{:d}/").format(level))
+
+
+class APIToken(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ access_token = db.Column(db.String(34), unique=True)
+
+ name = db.Column(db.String(100), nullable=False)
+ owner_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
+ # owner is created using backref
+
+ created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
+
+ package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
+ package = db.relationship("Package", foreign_keys=[package_id])
+
+ def canOperateOnPackage(self, package):
+ if self.package and self.package != package:
+ return False
+
+ return package.author == self.owner
+
+
+class EditRequest(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+
+ package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
+ author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
+
+ title = db.Column(db.String(100), nullable=False)
+ desc = db.Column(db.String(1000), nullable=True)
+
+ # 0 - open
+ # 1 - merged
+ # 2 - rejected
+ status = db.Column(db.Integer, nullable=False, default=0)
+
+ changes = db.relationship("EditRequestChange", backref="request",
+ lazy="dynamic")
+
+ def getURL(self):
+ return url_for("view_editrequest_page",
+ author=self.package.author.username,
+ name=self.package.name,
+ id=self.id)
+
+ def getApproveURL(self):
+ return url_for("approve_editrequest_page",
+ author=self.package.author.username,
+ name=self.package.name,
+ id=self.id)
+
+ def getRejectURL(self):
+ return url_for("reject_editrequest_page",
+ author=self.package.author.username,
+ name=self.package.name,
+ id=self.id)
+
+ def getEditURL(self):
+ return url_for("create_edit_editrequest_page",
+ author=self.package.author.username,
+ name=self.package.name,
+ id=self.id)
+
+ def applyAll(self, package):
+ for change in self.changes:
+ change.apply(package)
+
+
+ def checkPerm(self, user, perm):
+ if not user.is_authenticated:
+ return False
+
+ if type(perm) == str:
+ perm = Permission[perm]
+ elif type(perm) != Permission:
+ raise Exception("Unknown permission given to EditRequest.checkPerm()")
+
+ isOwner = user == self.author
+
+ # Members can edit their own packages, and editors can edit any packages
+ if perm == Permission.EDIT_EDITREQUEST:
+ return isOwner or user.rank.atLeast(UserRank.EDITOR)
+
+ else:
+ raise Exception("Permission {} is not related to packages".format(perm.name))
+
+
+
+
+class EditRequestChange(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+
+ request_id = db.Column(db.Integer, db.ForeignKey("edit_request.id"))
+ key = db.Column(db.Enum(PackagePropertyKey), nullable=False)
+
+ # TODO: make diff instead
+ oldValue = db.Column(db.Text, nullable=True)
+ newValue = db.Column(db.Text, nullable=True)
+
+ def apply(self, package):
+ if self.key == PackagePropertyKey.tags:
+ package.tags.clear()
+ for tagTitle in self.newValue.split(","):
+ tag = Tag.query.filter_by(title=tagTitle.strip()).first()
+ package.tags.append(tag)
+
+ else:
+ setattr(package, self.key.name, self.newValue)
+
+
+watchers = db.Table("watchers",
+ db.Column("user_id", db.Integer, db.ForeignKey("user.id"), primary_key=True),
+ db.Column("thread_id", db.Integer, db.ForeignKey("thread.id"), primary_key=True)
+)
+
+class Thread(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+
+ package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
+ package = db.relationship("Package", foreign_keys=[package_id])
+
+ review_id = db.Column(db.Integer, db.ForeignKey("package_review.id"), nullable=True)
+ review = db.relationship("PackageReview", foreign_keys=[review_id])
+
+ author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
+ title = db.Column(db.String(100), nullable=False)
+ private = db.Column(db.Boolean, server_default="0", nullable=False)
+
+ locked = db.Column(db.Boolean, server_default="0", nullable=False)
+
+ created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
+
+ replies = db.relationship("ThreadReply", backref="thread", lazy="dynamic")
+
+ watchers = db.relationship("User", secondary=watchers, lazy="subquery", \
+ backref=db.backref("watching", lazy=True))
+
+ def getViewURL(self):
+ return url_for("threads.view", id=self.id)
+
+ def getSubscribeURL(self):
+ return url_for("threads.subscribe", id=self.id)
+
+ def getUnsubscribeURL(self):
+ return url_for("threads.unsubscribe", id=self.id)
+
+ def checkPerm(self, user, perm):
+ if not user.is_authenticated:
+ return perm == Permission.SEE_THREAD and not self.private
+
+ if type(perm) == str:
+ perm = Permission[perm]
+ elif type(perm) != Permission:
+ raise Exception("Unknown permission given to Thread.checkPerm()")
+
+ isMaintainer = user == self.author or (self.package is not None and self.package.author == user)
+ if self.package:
+ isMaintainer = isMaintainer or user in self.package.maintainers
+
+ canSee = not self.private or isMaintainer or user.rank.atLeast(UserRank.EDITOR)
+
+ if perm == Permission.SEE_THREAD:
+ return canSee
+
+ elif perm == Permission.COMMENT_THREAD:
+ return canSee and (not self.locked or user.rank.atLeast(UserRank.MODERATOR))
+
+ elif perm == Permission.LOCK_THREAD:
+ return user.rank.atLeast(UserRank.MODERATOR)
+
+ else:
+ raise Exception("Permission {} is not related to threads".format(perm.name))
+
+class ThreadReply(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
+ comment = db.Column(db.String(500), nullable=False)
+ author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
+ created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
+
+ def checkPerm(self, user, perm):
+ if not user.is_authenticated:
+ return False
+
+ if type(perm) == str:
+ perm = Permission[perm]
+ elif type(perm) != Permission:
+ raise Exception("Unknown permission given to ThreadReply.checkPerm()")
+
+ if perm == Permission.EDIT_REPLY:
+ return user == self.author and user.rank.atLeast(UserRank.MEMBER) and not self.thread.locked
+
+ elif perm == Permission.DELETE_REPLY:
+ return user.rank.atLeast(UserRank.MODERATOR) and self.thread.replies[0] != self
+
+ else:
+ raise Exception("Permission {} is not related to threads".format(perm.name))
+
+
+class PackageReview(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+
+ package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
+ package = db.relationship("Package", foreign_keys=[package_id], backref=db.backref("reviews", lazy=True))
+
+ created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
+
+ author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
+ author = db.relationship("User", foreign_keys=[author_id], backref=db.backref("reviews", lazy=True))
+
+ recommends = db.Column(db.Boolean, nullable=False)
+
+ thread = db.relationship("Thread", uselist=False, back_populates="review")
+
+ def asSign(self):
+ return 1 if self.recommends else -1
+
+ def getEditURL(self):
+ return self.package.getReviewURL()
+
+ def getDeleteURL(self):
+ return url_for("packages.delete_review",
+ author=self.package.author.username,
+ name=self.package.name)
+
+
+class AuditSeverity(enum.Enum):
+ NORMAL = 0 # Normal user changes
+ EDITOR = 1 # Editor changes
+ MODERATION = 2 # Destructive / moderator changes
+
+ def __str__(self):
+ return self.name
+
+ def getTitle(self):
+ return self.name.replace("_", " ").title()
+
+ @classmethod
+ def choices(cls):
+ return [(choice, choice.getTitle()) for choice in cls]
+
+ @classmethod
+ def coerce(cls, item):
+ return item if type(item) == AuditSeverity else AuditSeverity[item]
+
+
+
+class AuditLogEntry(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+
+ created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
+
+ causer_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
+ causer = db.relationship("User", foreign_keys=[causer_id])
+
+ severity = db.Column(db.Enum(AuditSeverity), nullable=False)
+
+ title = db.Column(db.String(100), nullable=False)
+ url = db.Column(db.String(200), nullable=True)
+
+ package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
+ package = db.relationship("Package", foreign_keys=[package_id])
+
+ description = db.Column(db.Text, nullable=True, default=None)
+
+ def __init__(self, causer, severity, title, url, package=None, description=None):
+ if len(title) > 100:
+ title = title[:99] + "…"
+
+ self.causer = causer
+ self.severity = severity
+ self.title = title
+ self.url = url
+ self.package = package
+ self.description = description
+
+
+
+
+REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \
+ "minetest.net", "dropboxusercontent.com", "4shared.com", \
+ "digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net", \
+ "imageshack.com", "imgur.com"]
+
+class ForumTopic(db.Model):
+ topic_id = db.Column(db.Integer, primary_key=True, autoincrement=False)
+ author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
+ author = db.relationship("User")
+
+ wip = db.Column(db.Boolean, server_default="0")
+ discarded = db.Column(db.Boolean, server_default="0")
+
+ type = db.Column(db.Enum(PackageType), nullable=False)
+ title = db.Column(db.String(200), nullable=False)
+ name = db.Column(db.String(30), nullable=True)
+ link = db.Column(db.String(200), nullable=True)
+
+ posts = db.Column(db.Integer, nullable=False)
+ views = db.Column(db.Integer, nullable=False)
+
+ created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
+
+ def getRepoURL(self):
+ if self.link is None:
+ return None
+
+ for item in REPO_BLACKLIST:
+ if item in self.link:
+ return None
+
+ return self.link.replace("repo.or.cz/w/", "repo.or.cz/")
+
+ def getAsDictionary(self):
+ return {
+ "author": self.author.username,
+ "name": self.name,
+ "type": self.type.toName(),
+ "title": self.title,
+ "id": self.topic_id,
+ "link": self.link,
+ "posts": self.posts,
+ "views": self.views,
+ "is_wip": self.wip,
+ "discarded": self.discarded,
+ "created_at": self.created_at.isoformat(),
+ }
+
+ def checkPerm(self, user, perm):
+ if not user.is_authenticated:
+ return False
+
+ if type(perm) == str:
+ perm = Permission[perm]
+ elif type(perm) != Permission:
+ raise Exception("Unknown permission given to ForumTopic.checkPerm()")
+
+ if perm == Permission.TOPIC_DISCARD:
+ return self.author == user or user.rank.atLeast(UserRank.EDITOR)
+
+ else:
+ raise Exception("Permission {} is not related to topics".format(perm.name))
+
+