]> git.lizzy.rs Git - cheatdb.git/blob - app/models.py
Increase comment length limit to 2000
[cheatdb.git] / app / models.py
1 # ContentDB
2 # Copyright (C) 2018  rubenwardy
3 #
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.
8 #
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.
13 #
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/>.
16
17
18 import enum, datetime
19
20 from app import app, gravatar
21 from urllib.parse import urlparse
22
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
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
31
32
33 # Initialise database
34 db = SQLAlchemy(app)
35 migrate = Migrate(app, db)
36 make_searchable(db.metadata)
37
38
39 class ArticleQuery(BaseQuery, SearchQueryMixin):
40         pass
41
42
43 class UserRank(enum.Enum):
44         BANNED         = 0
45         NOT_JOINED     = 1
46         NEW_MEMBER     = 2
47         MEMBER         = 3
48         TRUSTED_MEMBER = 4
49         EDITOR         = 5
50         MODERATOR      = 6
51         ADMIN          = 7
52
53         def atLeast(self, min):
54                 return self.value >= min.value
55
56         def getTitle(self):
57                 return self.name.replace("_", " ").title()
58
59         def toName(self):
60                 return self.name.lower()
61
62         def __str__(self):
63                 return self.name
64
65         @classmethod
66         def choices(cls):
67                 return [(choice, choice.getTitle()) for choice in cls]
68
69         @classmethod
70         def coerce(cls, item):
71                 return item if type(item) == UserRank else UserRank[item]
72
73
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         DELETE_RELEASE     = "DELETE_RELEASE"
82         ADD_SCREENSHOTS    = "ADD_SCREENSHOTS"
83         REIMPORT_META      = "REIMPORT_META"
84         APPROVE_SCREENSHOT = "APPROVE_SCREENSHOT"
85         APPROVE_RELEASE    = "APPROVE_RELEASE"
86         APPROVE_NEW        = "APPROVE_NEW"
87         CHANGE_RELEASE_URL = "CHANGE_RELEASE_URL"
88         CHANGE_USERNAMES   = "CHANGE_USERNAMES"
89         CHANGE_RANK        = "CHANGE_RANK"
90         CHANGE_EMAIL       = "CHANGE_EMAIL"
91         EDIT_EDITREQUEST   = "EDIT_EDITREQUEST"
92         SEE_THREAD         = "SEE_THREAD"
93         CREATE_THREAD      = "CREATE_THREAD"
94         COMMENT_THREAD     = "COMMENT_THREAD"
95         LOCK_THREAD        = "LOCK_THREAD"
96         DELETE_REPLY       = "DELETE_REPLY"
97         EDIT_REPLY         = "EDIT_REPLY"
98         UNAPPROVE_PACKAGE  = "UNAPPROVE_PACKAGE"
99         TOPIC_DISCARD      = "TOPIC_DISCARD"
100         CREATE_TOKEN       = "CREATE_TOKEN"
101         EDIT_MAINTAINERS   = "EDIT_MAINTAINERS"
102         CHANGE_PROFILE_URLS = "CHANGE_PROFILE_URLS"
103
104         # Only return true if the permission is valid for *all* contexts
105         # See Package.checkPerm for package-specific contexts
106         def check(self, user):
107                 if not user.is_authenticated:
108                         return False
109
110                 if self == Permission.APPROVE_NEW or \
111                                 self == Permission.APPROVE_CHANGES    or \
112                                 self == Permission.APPROVE_RELEASE    or \
113                                 self == Permission.APPROVE_SCREENSHOT or \
114                                 self == Permission.SEE_THREAD:
115                         return user.rank.atLeast(UserRank.EDITOR)
116                 else:
117                         raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
118
119 def display_name_default(context):
120     return context.get_current_parameters()["username"]
121
122 class User(db.Model, UserMixin):
123         id           = db.Column(db.Integer, primary_key=True)
124
125         # User authentication information
126         username     = db.Column(db.String(50, collation="NOCASE"), nullable=False, unique=True, index=True)
127         password     = db.Column(db.String(255), nullable=False, server_default="")
128         reset_password_token = db.Column(db.String(100), nullable=False, server_default="")
129
130         rank         = db.Column(db.Enum(UserRank))
131
132         # Account linking
133         github_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
134         forums_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
135
136         # Access token for webhook setup
137         github_access_token = db.Column(db.String(50), nullable=True, server_default=None)
138
139         # User email information
140         email         = db.Column(db.String(255), nullable=True, unique=True)
141         email_confirmed_at  = db.Column(db.DateTime())
142
143         # User information
144         profile_pic   = db.Column(db.String(255), nullable=True, server_default=None)
145         active        = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
146         display_name  = db.Column(db.String(100), nullable=False, default=display_name_default)
147
148         # Links
149         website_url   = db.Column(db.String(255), nullable=True, default=None)
150         donate_url    = db.Column(db.String(255), nullable=True, default=None)
151
152         # Content
153         notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id")
154
155         # causednotifs  = db.relationship("Notification", backref="causer", lazy="dynamic")
156         packages      = db.relationship("Package", backref=db.backref("author", lazy="joined"), lazy="dynamic")
157         requests      = db.relationship("EditRequest", backref="author", lazy="dynamic")
158         threads       = db.relationship("Thread", backref="author", lazy="dynamic")
159         tokens        = db.relationship("APIToken", backref="owner", lazy="dynamic")
160         replies       = db.relationship("ThreadReply", backref="author", lazy="dynamic")
161
162         def __init__(self, username=None, active=False, email=None, password=""):
163                 self.username = username
164                 self.email_confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
165                 self.display_name = username
166                 self.active = active
167                 self.email = email
168                 self.password = password
169                 self.rank = UserRank.NOT_JOINED
170
171         def hasPassword(self):
172                 return self.password != ""
173
174         def canAccessTodoList(self):
175                 return Permission.APPROVE_NEW.check(self) or \
176                                 Permission.APPROVE_RELEASE.check(self) or \
177                                 Permission.APPROVE_CHANGES.check(self)
178
179         def isClaimed(self):
180                 return self.rank.atLeast(UserRank.NEW_MEMBER)
181
182         def getProfilePicURL(self):
183                 if self.profile_pic:
184                         return self.profile_pic
185                 else:
186                         return gravatar(self.email or "")
187
188         def checkPerm(self, user, perm):
189                 if not user.is_authenticated:
190                         return False
191
192                 if type(perm) == str:
193                         perm = Permission[perm]
194                 elif type(perm) != Permission:
195                         raise Exception("Unknown permission given to User.checkPerm()")
196
197                 # Members can edit their own packages, and editors can edit any packages
198                 if perm == Permission.CHANGE_AUTHOR:
199                         return user.rank.atLeast(UserRank.EDITOR)
200                 elif perm == Permission.CHANGE_RANK or perm == Permission.CHANGE_USERNAMES:
201                         return user.rank.atLeast(UserRank.MODERATOR)
202                 elif perm == Permission.CHANGE_EMAIL or perm == Permission.CHANGE_PROFILE_URLS:
203                         return user == self or user.rank.atLeast(UserRank.ADMIN)
204                 elif perm == Permission.CREATE_TOKEN:
205                         if user == self:
206                                 return user.rank.atLeast(UserRank.MEMBER)
207                         else:
208                                 return user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank)
209                 else:
210                         raise Exception("Permission {} is not related to users".format(perm.name))
211
212         def canCommentRL(self):
213                 one_min_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=1)
214                 if ThreadReply.query.filter_by(author=self) \
215                                 .filter(ThreadReply.created_at > one_min_ago).count() >= 3:
216                         return False
217
218                 hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
219                 if ThreadReply.query.filter_by(author=self) \
220                                 .filter(ThreadReply.created_at > hour_ago).count() >= 20:
221                         return False
222
223                 return True
224
225         def canOpenThreadRL(self):
226                 hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
227                 return Thread.query.filter_by(author=self) \
228                         .filter(Thread.created_at > hour_ago).count() < 2
229
230         def __eq__(self, other):
231                 if other is None:
232                         return False
233
234                 if not self.is_authenticated or not other.is_authenticated:
235                         return False
236
237                 assert self.id > 0
238                 return self.id == other.id
239
240 class UserEmailVerification(db.Model):
241         id      = db.Column(db.Integer, primary_key=True)
242         user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
243         email   = db.Column(db.String(100))
244         token   = db.Column(db.String(32))
245         user    = db.relationship("User", foreign_keys=[user_id])
246
247 class Notification(db.Model):
248         id         = db.Column(db.Integer, primary_key=True)
249
250         user_id    = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
251         user       = db.relationship("User", foreign_keys=[user_id])
252
253         causer_id  = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
254         causer     = db.relationship("User", foreign_keys=[causer_id])
255
256         title      = db.Column(db.String(100), nullable=False)
257         url        = db.Column(db.String(200), nullable=True)
258
259         package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
260         package    = db.relationship("Package", foreign_keys=[package_id])
261
262         created_at = db.Column(db.DateTime, nullable=True, default=datetime.datetime.utcnow)
263
264         def __init__(self, user, causer, title, url, package=None):
265                 if len(title) > 100:
266                         title = title[:99] + "…"
267
268                 self.user    = user
269                 self.causer  = causer
270                 self.title   = title
271                 self.url     = url
272                 self.package = package
273
274
275 class License(db.Model):
276         id      = db.Column(db.Integer, primary_key=True)
277         name    = db.Column(db.String(50), nullable=False, unique=True)
278         is_foss = db.Column(db.Boolean,    nullable=False, default=True)
279
280         def __init__(self, v, is_foss=True):
281                 self.name = v
282                 self.is_foss = is_foss
283
284         def __str__(self):
285                 return self.name
286
287
288 class PackageType(enum.Enum):
289         MOD  = "Mod"
290         GAME = "Game"
291         TXP  = "Texture Pack"
292
293         def toName(self):
294                 return self.name.lower()
295
296         def __str__(self):
297                 return self.name
298
299         @classmethod
300         def get(cls, name):
301                 try:
302                         return PackageType[name.upper()]
303                 except KeyError:
304                         return None
305
306         @classmethod
307         def choices(cls):
308                 return [(choice, choice.value) for choice in cls]
309
310         @classmethod
311         def coerce(cls, item):
312                 return item if type(item) == PackageType else PackageType[item]
313
314
315 class PackagePropertyKey(enum.Enum):
316         name          = "Name"
317         title         = "Title"
318         short_desc     = "Short Description"
319         desc          = "Description"
320         type          = "Type"
321         license       = "License"
322         media_license = "Media License"
323         tags          = "Tags"
324         provides      = "Provides"
325         repo          = "Repository"
326         website       = "Website"
327         issueTracker  = "Issue Tracker"
328         forums        = "Forum Topic ID"
329
330         def convert(self, value):
331                 if self == PackagePropertyKey.tags:
332                         return ",".join([t.title for t in value])
333                 elif self == PackagePropertyKey.provides:
334                         return ",".join([t.name for t in value])
335                 else:
336                         return str(value)
337
338 provides = db.Table("provides",
339         db.Column("package_id",    db.Integer, db.ForeignKey("package.id"), primary_key=True),
340     db.Column("metapackage_id", db.Integer, db.ForeignKey("meta_package.id"), primary_key=True)
341 )
342
343 Tags = db.Table("tags",
344     db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True),
345     db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True)
346 )
347
348 maintainers = db.Table("maintainers",
349     db.Column("user_id", db.Integer, db.ForeignKey("user.id"), primary_key=True),
350     db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True)
351 )
352
353 class Dependency(db.Model):
354         id              = db.Column(db.Integer, primary_key=True)
355         depender_id     = db.Column(db.Integer, db.ForeignKey("package.id"),     nullable=True)
356         package_id      = db.Column(db.Integer, db.ForeignKey("package.id"),     nullable=True)
357         package         = db.relationship("Package", foreign_keys=[package_id])
358         meta_package_id = db.Column(db.Integer, db.ForeignKey("meta_package.id"), nullable=True)
359         optional        = db.Column(db.Boolean, nullable=False, default=False)
360         __table_args__  = (db.UniqueConstraint("depender_id", "package_id", "meta_package_id", name="_dependency_uc"), )
361
362         def __init__(self, depender=None, package=None, meta=None, optional=False):
363                 if depender is None:
364                         return
365
366                 self.depender = depender
367                 self.optional = optional
368
369                 packageProvided = package is not None
370                 metaProvided = meta is not None
371
372                 if packageProvided and not metaProvided:
373                         self.package = package
374                 elif metaProvided and not packageProvided:
375                         self.meta_package = meta
376                 else:
377                         raise Exception("Either meta or package must be given, but not both!")
378
379         def getName(self):
380                 if self.meta_package:
381                         return self.meta_package.name
382                 elif self.package:
383                         return self.package.name
384                 else:
385                         assert False
386
387         def __str__(self):
388                 if self.package is not None:
389                         return self.package.author.username + "/" + self.package.name
390                 elif self.meta_package is not None:
391                         return self.meta_package.name
392                 else:
393                         raise Exception("Meta and package are both none!")
394
395         @staticmethod
396         def SpecToList(depender, spec, cache={}):
397                 retval = []
398                 arr = spec.split(",")
399
400                 import re
401                 pattern1 = re.compile("^([a-z0-9_]+)$")
402                 pattern2 = re.compile("^([A-Za-z0-9_]+)/([a-z0-9_]+)$")
403
404                 for x in arr:
405                         x = x.strip()
406                         if x == "":
407                                 continue
408
409                         if pattern1.match(x):
410                                 meta = MetaPackage.GetOrCreate(x, cache)
411                                 retval.append(Dependency(depender, meta=meta))
412                         else:
413                                 m = pattern2.match(x)
414                                 username = m.group(1)
415                                 name     = m.group(2)
416                                 user = User.query.filter_by(username=username).first()
417                                 if user is None:
418                                         raise Exception("Unable to find user " + username)
419
420                                 package = Package.query.filter_by(author=user, name=name).first()
421                                 if package is None:
422                                         raise Exception("Unable to find package " + name + " by " + username)
423
424                                 retval.append(Dependency(depender, package=package))
425
426                 return retval
427
428
429 class Package(db.Model):
430         query_class  = ArticleQuery
431
432         id           = db.Column(db.Integer, primary_key=True)
433
434         # Basic details
435         author_id    = db.Column(db.Integer, db.ForeignKey("user.id"))
436         name         = db.Column(db.Unicode(100), nullable=False)
437         title        = db.Column(db.Unicode(100), nullable=False)
438         short_desc   = db.Column(db.Unicode(200), nullable=False)
439         desc         = db.Column(db.UnicodeText, nullable=True)
440         type         = db.Column(db.Enum(PackageType))
441         created_at   = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
442
443         name_valid = db.CheckConstraint("name ~* '^[a-z0-9_]+$'")
444
445         search_vector = db.Column(TSVectorType("name", "title", "short_desc", "desc", \
446                         weights={ "name": "A", "title": "B", "short_desc": "C", "desc": "D" }))
447
448         license_id   = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
449         license      = db.relationship("License", foreign_keys=[license_id])
450         media_license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
451         media_license    = db.relationship("License", foreign_keys=[media_license_id])
452
453         approved     = db.Column(db.Boolean, nullable=False, default=False)
454         soft_deleted = db.Column(db.Boolean, nullable=False, default=False)
455
456         score        = db.Column(db.Float, nullable=False, default=0)
457         score_downloads = db.Column(db.Float, nullable=False, default=0)
458         downloads     = db.Column(db.Integer, nullable=False, default=0)
459
460         review_thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=True, default=None)
461         review_thread    = db.relationship("Thread", foreign_keys=[review_thread_id])
462
463         # Downloads
464         repo         = db.Column(db.String(200), nullable=True)
465         website      = db.Column(db.String(200), nullable=True)
466         issueTracker = db.Column(db.String(200), nullable=True)
467         forums       = db.Column(db.Integer,     nullable=True)
468
469         provides = db.relationship("MetaPackage", \
470                         secondary=provides, lazy="select", order_by=db.asc("name"), \
471                         backref=db.backref("packages", lazy="dynamic", order_by=db.desc("score")))
472
473         dependencies = db.relationship("Dependency", backref="depender", lazy="dynamic", foreign_keys=[Dependency.depender_id])
474
475         tags = db.relationship("Tag", secondary=Tags, lazy="select",
476                         backref=db.backref("packages", lazy=True))
477
478         releases = db.relationship("PackageRelease", backref="package",
479                         lazy="dynamic", order_by=db.desc("package_release_releaseDate"))
480
481         screenshots = db.relationship("PackageScreenshot", backref="package",
482                         lazy="dynamic", order_by=db.asc("package_screenshot_id"))
483
484         requests = db.relationship("EditRequest", backref="package",
485                         lazy="dynamic")
486
487         maintainers = db.relationship("User", secondary=maintainers, lazy="subquery")
488
489         def __init__(self, package=None):
490                 if package is None:
491                         return
492
493                 self.author_id = package.author_id
494                 self.created_at = package.created_at
495                 self.approved = package.approved
496
497                 self.maintainers.append(self.author)
498
499                 for e in PackagePropertyKey:
500                         setattr(self, e.name, getattr(package, e.name))
501
502         def getId(self):
503                 return "{}/{}".format(self.author.username, self.name)
504
505         def getIsFOSS(self):
506                 return self.license.is_foss and self.media_license.is_foss
507
508         def getIsOnGitHub(self):
509                 if self.repo is None:
510                         return False
511
512                 url = urlparse(self.repo)
513                 return url.netloc == "github.com"
514
515         def getGitHubFullName(self):
516                 if self.repo is None:
517                         return None
518
519                 url = urlparse(self.repo)
520                 if url.netloc != "github.com":
521                         return None
522
523                 import re
524                 m = re.search(r"^\/([^\/]+)\/([^\/]+)\/?$", url.path)
525                 if m is None:
526                         return
527
528                 user = m.group(1)
529                 repo = m.group(2).replace(".git", "")
530
531                 return (user,repo)
532
533         def getSortedDependencies(self, is_hard=None):
534                 query = self.dependencies
535                 if is_hard is not None:
536                         query = query.filter_by(optional=not is_hard)
537
538                 deps = query.all()
539                 deps.sort(key = lambda x: x.getName())
540                 return deps
541
542         def getSortedHardDependencies(self):
543                 return self.getSortedDependencies(True)
544
545         def getSortedOptionalDependencies(self):
546                 return self.getSortedDependencies(False)
547
548         def getState(self):
549                 if self.approved:
550                         return "approved"
551                 elif self.review_thread_id:
552                         return "thread"
553                 elif (self.type == PackageType.GAME or \
554                                         self.type == PackageType.TXP) and \
555                                 self.screenshots.count() == 0:
556                         return "wip"
557                 elif not self.getDownloadRelease():
558                         return "wip"
559                 elif "Other" in self.license.name or "Other" in self.media_license.name:
560                         return "license"
561                 else:
562                         return "ready"
563
564         def getAsDictionaryKey(self):
565                 return {
566                         "name": self.name,
567                         "author": self.author.display_name,
568                         "type": self.type.toName(),
569                 }
570
571         def getAsDictionaryShort(self, base_url, version=None):
572                 tnurl = self.getThumbnailURL(1)
573                 release = self.getDownloadRelease(version=version)
574                 return {
575                         "name": self.name,
576                         "title": self.title,
577                         "author": self.author.username,
578                         "short_description": self.short_desc,
579                         "type": self.type.toName(),
580                         "release": release and release.id,
581                         "thumbnail": (base_url + tnurl) if tnurl is not None else None
582                 }
583
584         def getAsDictionary(self, base_url, version=None):
585                 tnurl = self.getThumbnailURL(1)
586                 release = self.getDownloadRelease(version=version)
587                 return {
588                         "author": self.author.username,
589                         "name": self.name,
590                         "title": self.title,
591                         "short_description": self.short_desc,
592                         "desc": self.desc,
593                         "type": self.type.toName(),
594                         "created_at": self.created_at.isoformat(),
595
596                         "license": self.license.name,
597                         "media_license": self.media_license.name,
598
599                         "repo": self.repo,
600                         "website": self.website,
601                         "issue_tracker": self.issueTracker,
602                         "forums": self.forums,
603
604                         "provides": [x.name for x in self.provides],
605                         "thumbnail": (base_url + tnurl) if tnurl is not None else None,
606                         "screenshots": [base_url + ss.url for ss in self.screenshots],
607
608                         "url": base_url + self.getDownloadURL(),
609                         "release": release and release.id,
610
611                         "score": round(self.score * 10) / 10
612                 }
613
614         def getThumbnailURL(self, level=2):
615                 screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first()
616                 return screenshot.getThumbnailURL(level) if screenshot is not None else None
617
618         def getMainScreenshotURL(self, absolute=False):
619                 screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first()
620                 if screenshot is None:
621                         return None
622
623                 if absolute:
624                         from app.utils import abs_url
625                         return abs_url(screenshot.url)
626                 else:
627                         return screenshot.url
628
629         def getDetailsURL(self, absolute=False):
630                 if absolute:
631                         from app.utils import abs_url_for
632                         return abs_url_for("packages.view",
633                                         author=self.author.username, name=self.name)
634                 else:
635                         return url_for("packages.view",
636                                         author=self.author.username, name=self.name)
637
638         def getEditURL(self):
639                 return url_for("packages.create_edit",
640                                 author=self.author.username, name=self.name)
641
642         def getApproveURL(self):
643                 return url_for("packages.approve",
644                                 author=self.author.username, name=self.name)
645
646         def getRemoveURL(self):
647                 return url_for("packages.remove",
648                                 author=self.author.username, name=self.name)
649
650         def getNewScreenshotURL(self):
651                 return url_for("packages.create_screenshot",
652                                 author=self.author.username, name=self.name)
653
654         def getCreateReleaseURL(self):
655                 return url_for("packages.create_release",
656                                 author=self.author.username, name=self.name)
657
658         def getCreateEditRequestURL(self):
659                 return url_for("create_edit_editrequest_page",
660                                 author=self.author.username, name=self.name)
661
662         def getBulkReleaseURL(self):
663                 return url_for("packages.bulk_change_release",
664                         author=self.author.username, name=self.name)
665
666         def getDownloadURL(self):
667                 return url_for("packages.download",
668                                 author=self.author.username, name=self.name)
669
670         def getEditMaintainersURL(self):
671                 return url_for("packages.edit_maintainers",
672                                 author=self.author.username, name=self.name)
673
674         def getRemoveSelfMaintainerURL(self):
675                 return url_for("packages.remove_self_maintainers",
676                                 author=self.author.username, name=self.name)
677
678         def getUpdateFromReleaseURL(self):
679                 return url_for("packages.update_from_release",
680                                 author=self.author.username, name=self.name)
681
682         def getReviewURL(self):
683                 return url_for('packages.review',
684                                 author=self.author.username, name=self.name)
685
686         def getDownloadRelease(self, version=None):
687                 for rel in self.releases:
688                         if rel.approved and (version is None or
689                                         ((rel.min_rel is None or rel.min_rel_id <= version.id) and \
690                                         (rel.max_rel is None or rel.max_rel_id >= version.id))):
691                                 return rel
692
693                 return None
694
695         def checkPerm(self, user, perm):
696                 if not user.is_authenticated:
697                         return False
698
699                 if type(perm) == str:
700                         perm = Permission[perm]
701                 elif type(perm) != Permission:
702                         raise Exception("Unknown permission given to Package.checkPerm()")
703
704                 isOwner = user == self.author
705                 isMaintainer = isOwner or user.rank.atLeast(UserRank.EDITOR) or user in self.maintainers
706
707                 if perm == Permission.CREATE_THREAD:
708                         return user.rank.atLeast(UserRank.MEMBER)
709
710                 # Members can edit their own packages, and editors can edit any packages
711                 elif perm == Permission.MAKE_RELEASE or perm == Permission.ADD_SCREENSHOTS:
712                         return isMaintainer
713
714                 elif perm == Permission.EDIT_PACKAGE or perm == Permission.REIMPORT_META or \
715                                 perm == Permission.APPROVE_CHANGES or perm == Permission.APPROVE_RELEASE:
716                         return isMaintainer and user.rank.atLeast(UserRank.MEMBER if self.approved else UserRank.NEW_MEMBER)
717
718                 # Anyone can change the package name when not approved, but only editors when approved
719                 elif perm == Permission.CHANGE_NAME:
720                         return not self.approved or user.rank.atLeast(UserRank.EDITOR)
721
722                 # Editors can change authors and approve new packages
723                 elif perm == Permission.APPROVE_NEW or perm == Permission.CHANGE_AUTHOR:
724                         return user.rank.atLeast(UserRank.EDITOR)
725
726                 elif perm == Permission.APPROVE_SCREENSHOT:
727                         return isMaintainer and user.rank.atLeast(UserRank.TRUSTED_MEMBER if self.approved else UserRank.NEW_MEMBER)
728
729                 elif perm == Permission.EDIT_MAINTAINERS:
730                         return isOwner or user.rank.atLeast(UserRank.MODERATOR)
731
732                 elif perm == Permission.UNAPPROVE_PACKAGE or perm == Permission.DELETE_PACKAGE:
733                         return user.rank.atLeast(UserRank.EDITOR)
734
735                 elif perm == Permission.CHANGE_RELEASE_URL:
736                         return user.rank.atLeast(UserRank.MODERATOR)
737
738                 else:
739                         raise Exception("Permission {} is not related to packages".format(perm.name))
740
741         def getScoreDict(self):
742                 return {
743                         "author": self.author.username,
744                         "name": self.name,
745                         "score": self.score,
746                         "score_downloads": self.score_downloads,
747                         "score_reviews": self.score - self.score_downloads,
748                         "downloads": self.downloads
749                 }
750
751         def recalcScore(self):
752                 review_scores = [ 100 * r.asSign() for r in self.reviews ]
753                 self.score = self.score_downloads + sum(review_scores)
754
755
756 class MetaPackage(db.Model):
757         id           = db.Column(db.Integer, primary_key=True)
758         name         = db.Column(db.String(100), unique=True, nullable=False)
759         dependencies = db.relationship("Dependency", backref="meta_package", lazy="dynamic")
760
761         mp_name_valid = db.CheckConstraint("name ~* '^[a-z0-9_]+$'")
762
763         def __init__(self, name=None):
764                 self.name = name
765
766         def __str__(self):
767                 return self.name
768
769         @staticmethod
770         def ListToSpec(list):
771                 return ",".join([str(x) for x in list])
772
773         @staticmethod
774         def GetOrCreate(name, cache={}):
775                 mp = cache.get(name)
776                 if mp is None:
777                         mp = MetaPackage.query.filter_by(name=name).first()
778
779                 if mp is None:
780                         mp = MetaPackage(name)
781                         db.session.add(mp)
782
783                 cache[name] = mp
784                 return mp
785
786         @staticmethod
787         def SpecToList(spec, cache={}):
788                 retval = []
789                 arr = spec.split(",")
790
791                 import re
792                 pattern = re.compile("^([a-z0-9_]+)$")
793
794                 for x in arr:
795                         x = x.strip()
796                         if x == "":
797                                 continue
798
799                         if not pattern.match(x):
800                                 continue
801
802                         retval.append(MetaPackage.GetOrCreate(x, cache))
803
804                 return retval
805
806 class Tag(db.Model):
807         id              = db.Column(db.Integer,    primary_key=True)
808         name            = db.Column(db.String(100), unique=True, nullable=False)
809         title           = db.Column(db.String(100), nullable=False)
810         backgroundColor = db.Column(db.String(6),   nullable=False)
811         textColor       = db.Column(db.String(6),   nullable=False)
812
813         def __init__(self, title, backgroundColor="000000", textColor="ffffff"):
814                 self.title           = title
815                 self.backgroundColor = backgroundColor
816                 self.textColor       = textColor
817
818                 import re
819                 regex = re.compile("[^a-z_]")
820                 self.name = regex.sub("", self.title.lower().replace(" ", "_"))
821
822
823 class MinetestRelease(db.Model):
824         id       = db.Column(db.Integer, primary_key=True)
825         name     = db.Column(db.String(100), unique=True, nullable=False)
826         protocol = db.Column(db.Integer, nullable=False, default=0)
827
828         def __init__(self, name=None, protocol=0):
829                 self.name = name
830                 self.protocol = protocol
831
832         def getActual(self):
833                 return None if self.name == "None" else self
834
835         @classmethod
836         def get(cls, version, protocol_num):
837                 if version:
838                         parts = version.strip().split(".")
839                         if len(parts) >= 2:
840                                 major_minor = parts[0] + "." + parts[1]
841                                 query = MinetestRelease.query.filter(MinetestRelease.name.like("{}%".format(major_minor)))
842                                 if protocol_num:
843                                         query = query.filter_by(protocol=protocol_num)
844
845                                 release = query.one_or_none()
846                                 if release:
847                                         return release
848
849                 if protocol_num:
850                         return MinetestRelease.query.filter_by(protocol=protocol_num).first()
851
852                 return None
853
854
855 class PackageRelease(db.Model):
856         id           = db.Column(db.Integer, primary_key=True)
857
858         package_id   = db.Column(db.Integer, db.ForeignKey("package.id"))
859         title        = db.Column(db.String(100), nullable=False)
860         releaseDate  = db.Column(db.DateTime,    nullable=False)
861         url          = db.Column(db.String(200), nullable=False)
862         approved     = db.Column(db.Boolean, nullable=False, default=False)
863         task_id      = db.Column(db.String(37), nullable=True)
864         commit_hash  = db.Column(db.String(41), nullable=True, default=None)
865         downloads    = db.Column(db.Integer, nullable=False, default=0)
866
867         min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
868         min_rel    = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])
869
870         max_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
871         max_rel    = db.relationship("MinetestRelease", foreign_keys=[max_rel_id])
872
873         # If the release is approved, then the task_id must be null and the url must be present
874         CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)")
875
876         def getAsDictionary(self):
877                 return {
878                         "id": self.id,
879                         "title": self.title,
880                         "url": self.url if self.url != "" else None,
881                         "release_date": self.releaseDate.isoformat(),
882                         "commit": self.commit_hash,
883                         "downloads": self.downloads,
884                         "min_protocol": self.min_rel and self.min_rel.protocol,
885                         "max_protocol": self.max_rel and self.max_rel.protocol
886                 }
887
888         def getEditURL(self):
889                 return url_for("packages.edit_release",
890                                 author=self.package.author.username,
891                                 name=self.package.name,
892                                 id=self.id)
893
894         def getDeleteURL(self):
895                 return url_for("packages.delete_release",
896                                 author=self.package.author.username,
897                                 name=self.package.name,
898                                 id=self.id)
899
900         def getDownloadURL(self):
901                 return url_for("packages.download_release",
902                                 author=self.package.author.username,
903                                 name=self.package.name,
904                                 id=self.id)
905
906         def __init__(self):
907                 self.releaseDate = datetime.datetime.now()
908
909         def approve(self, user):
910                 if not self.package.checkPerm(user, Permission.APPROVE_RELEASE):
911                         return False
912
913                 assert self.task_id is None and self.url is not None and self.url != ""
914
915                 self.approved = True
916                 return True
917
918         def checkPerm(self, user, perm):
919                 if not user.is_authenticated:
920                         return False
921
922                 if type(perm) == str:
923                         perm = Permission[perm]
924                 elif type(perm) != Permission:
925                         raise Exception("Unknown permission given to PackageRelease.checkPerm()")
926
927                 isOwner = user == self.package.author
928
929                 if perm == Permission.DELETE_RELEASE:
930                         if user.rank.atLeast(UserRank.ADMIN):
931                                 return True
932
933                         if not (isOwner or user.rank.atLeast(UserRank.EDITOR)):
934                                 return False
935
936                         if not self.package.approved or self.task_id is not None:
937                                 return True
938
939                         count = PackageRelease.query \
940                                         .filter_by(package_id=self.package_id) \
941                                         .filter(PackageRelease.id > self.id) \
942                                         .count()
943
944                         return count > 0
945                 else:
946                         raise Exception("Permission {} is not related to releases".format(perm.name))
947
948
949 # class PackageReview(db.Model):
950 #       id         = db.Column(db.Integer, primary_key=True)
951 #       package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
952 #       thread_id  = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
953 #       recommend  = db.Column(db.Boolean, nullable=False, default=True)
954
955
956 class PackageScreenshot(db.Model):
957         id         = db.Column(db.Integer, primary_key=True)
958         package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
959         title      = db.Column(db.String(100), nullable=False)
960         url        = db.Column(db.String(100), nullable=False)
961         approved   = db.Column(db.Boolean, nullable=False, default=False)
962
963
964         def getEditURL(self):
965                 return url_for("packages.edit_screenshot",
966                                 author=self.package.author.username,
967                                 name=self.package.name,
968                                 id=self.id)
969
970         def getThumbnailURL(self, level=2):
971                 return self.url.replace("/uploads/", ("/thumbnails/{:d}/").format(level))
972
973
974 class APIToken(db.Model):
975         id           = db.Column(db.Integer, primary_key=True)
976         access_token = db.Column(db.String(34), unique=True)
977
978         name         = db.Column(db.String(100), nullable=False)
979         owner_id     = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
980         # owner is created using backref
981
982         created_at   = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
983
984         package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
985         package    = db.relationship("Package", foreign_keys=[package_id])
986
987         def canOperateOnPackage(self, package):
988                 if self.package and self.package != package:
989                         return False
990
991                 return package.author == self.owner
992
993
994 class EditRequest(db.Model):
995         id           = db.Column(db.Integer, primary_key=True)
996
997         package_id   = db.Column(db.Integer, db.ForeignKey("package.id"))
998         author_id    = db.Column(db.Integer, db.ForeignKey("user.id"))
999
1000         title        = db.Column(db.String(100), nullable=False)
1001         desc         = db.Column(db.String(1000), nullable=True)
1002
1003         # 0 - open
1004         # 1 - merged
1005         # 2 - rejected
1006         status       = db.Column(db.Integer, nullable=False, default=0)
1007
1008         changes = db.relationship("EditRequestChange", backref="request",
1009                         lazy="dynamic")
1010
1011         def getURL(self):
1012                 return url_for("view_editrequest_page",
1013                                 author=self.package.author.username,
1014                                 name=self.package.name,
1015                                 id=self.id)
1016
1017         def getApproveURL(self):
1018                 return url_for("approve_editrequest_page",
1019                                 author=self.package.author.username,
1020                                 name=self.package.name,
1021                                 id=self.id)
1022
1023         def getRejectURL(self):
1024                 return url_for("reject_editrequest_page",
1025                                 author=self.package.author.username,
1026                                 name=self.package.name,
1027                                 id=self.id)
1028
1029         def getEditURL(self):
1030                 return url_for("create_edit_editrequest_page",
1031                                 author=self.package.author.username,
1032                                 name=self.package.name,
1033                                 id=self.id)
1034
1035         def applyAll(self, package):
1036                 for change in self.changes:
1037                         change.apply(package)
1038
1039
1040         def checkPerm(self, user, perm):
1041                 if not user.is_authenticated:
1042                         return False
1043
1044                 if type(perm) == str:
1045                         perm = Permission[perm]
1046                 elif type(perm) != Permission:
1047                         raise Exception("Unknown permission given to EditRequest.checkPerm()")
1048
1049                 isOwner = user == self.author
1050
1051                 # Members can edit their own packages, and editors can edit any packages
1052                 if perm == Permission.EDIT_EDITREQUEST:
1053                         return isOwner or user.rank.atLeast(UserRank.EDITOR)
1054
1055                 else:
1056                         raise Exception("Permission {} is not related to packages".format(perm.name))
1057
1058
1059
1060
1061 class EditRequestChange(db.Model):
1062         id           = db.Column(db.Integer, primary_key=True)
1063
1064         request_id   = db.Column(db.Integer, db.ForeignKey("edit_request.id"))
1065         key          = db.Column(db.Enum(PackagePropertyKey), nullable=False)
1066
1067         # TODO: make diff instead
1068         oldValue     = db.Column(db.Text, nullable=True)
1069         newValue     = db.Column(db.Text, nullable=True)
1070
1071         def apply(self, package):
1072                 if self.key == PackagePropertyKey.tags:
1073                         package.tags.clear()
1074                         for tagTitle in self.newValue.split(","):
1075                                 tag = Tag.query.filter_by(title=tagTitle.strip()).first()
1076                                 package.tags.append(tag)
1077
1078                 else:
1079                         setattr(package, self.key.name, self.newValue)
1080
1081
1082 watchers = db.Table("watchers",
1083     db.Column("user_id", db.Integer, db.ForeignKey("user.id"), primary_key=True),
1084     db.Column("thread_id", db.Integer, db.ForeignKey("thread.id"), primary_key=True)
1085 )
1086
1087 class Thread(db.Model):
1088         id         = db.Column(db.Integer, primary_key=True)
1089
1090         package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
1091         package    = db.relationship("Package", foreign_keys=[package_id])
1092
1093         review_id  = db.Column(db.Integer, db.ForeignKey("package_review.id"), nullable=True)
1094         review     = db.relationship("PackageReview", foreign_keys=[review_id])
1095
1096         author_id  = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
1097         title      = db.Column(db.String(100), nullable=False)
1098         private    = db.Column(db.Boolean, server_default="0", nullable=False)
1099
1100         locked     = db.Column(db.Boolean, server_default="0", nullable=False)
1101
1102         created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
1103
1104         replies    = db.relationship("ThreadReply", backref="thread", lazy="dynamic", \
1105                         order_by=db.asc("thread_reply_id"))
1106
1107         watchers   = db.relationship("User", secondary=watchers, lazy="subquery", \
1108                         backref=db.backref("watching", lazy=True))
1109
1110         def getViewURL(self):
1111                 return url_for("threads.view", id=self.id)
1112
1113         def getSubscribeURL(self):
1114                 return url_for("threads.subscribe", id=self.id)
1115
1116         def getUnsubscribeURL(self):
1117                 return url_for("threads.unsubscribe", id=self.id)
1118
1119         def checkPerm(self, user, perm):
1120                 if not user.is_authenticated:
1121                         return perm == Permission.SEE_THREAD and not self.private
1122
1123                 if type(perm) == str:
1124                         perm = Permission[perm]
1125                 elif type(perm) != Permission:
1126                         raise Exception("Unknown permission given to Thread.checkPerm()")
1127
1128                 isMaintainer = user == self.author or (self.package is not None and self.package.author == user)
1129                 if self.package:
1130                         isMaintainer = isMaintainer or user in self.package.maintainers
1131
1132                 canSee = not self.private or isMaintainer or user.rank.atLeast(UserRank.EDITOR)
1133
1134                 if perm == Permission.SEE_THREAD:
1135                         return canSee
1136
1137                 elif perm == Permission.COMMENT_THREAD:
1138                         return canSee and (not self.locked or user.rank.atLeast(UserRank.MODERATOR))
1139
1140                 elif perm == Permission.LOCK_THREAD:
1141                         return user.rank.atLeast(UserRank.MODERATOR)
1142
1143                 else:
1144                         raise Exception("Permission {} is not related to threads".format(perm.name))
1145
1146 class ThreadReply(db.Model):
1147         id         = db.Column(db.Integer, primary_key=True)
1148         thread_id  = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
1149         comment    = db.Column(db.String(2000), nullable=False)
1150         author_id  = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
1151         created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
1152
1153         def checkPerm(self, user, perm):
1154                 if not user.is_authenticated:
1155                         return False
1156
1157                 if type(perm) == str:
1158                         perm = Permission[perm]
1159                 elif type(perm) != Permission:
1160                         raise Exception("Unknown permission given to ThreadReply.checkPerm()")
1161
1162                 if perm == Permission.EDIT_REPLY:
1163                         return user == self.author and user.rank.atLeast(UserRank.MEMBER) and not self.thread.locked
1164
1165                 elif perm == Permission.DELETE_REPLY:
1166                         return user.rank.atLeast(UserRank.MODERATOR) and self.thread.replies[0] != self
1167
1168                 else:
1169                         raise Exception("Permission {} is not related to threads".format(perm.name))
1170
1171
1172 class PackageReview(db.Model):
1173         id         = db.Column(db.Integer, primary_key=True)
1174
1175         package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
1176         package    = db.relationship("Package", foreign_keys=[package_id], backref=db.backref("reviews", lazy=True))
1177
1178         created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
1179
1180         author_id  = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
1181         author     = db.relationship("User", foreign_keys=[author_id], backref=db.backref("reviews", lazy=True))
1182
1183         recommends = db.Column(db.Boolean, nullable=False)
1184
1185         thread     = db.relationship("Thread", uselist=False, back_populates="review")
1186
1187         def asSign(self):
1188                 return 1 if self.recommends else -1
1189
1190         def getEditURL(self):
1191                 return self.package.getReviewURL()
1192
1193         def getDeleteURL(self):
1194                 return url_for("packages.delete_review",
1195                                 author=self.package.author.username,
1196                                 name=self.package.name)
1197
1198
1199 class AuditSeverity(enum.Enum):
1200         NORMAL = 0 # Normal user changes
1201         EDITOR = 1 # Editor changes
1202         MODERATION = 2 # Destructive / moderator changes
1203
1204         def __str__(self):
1205                 return self.name
1206
1207         def getTitle(self):
1208                 return self.name.replace("_", " ").title()
1209
1210         @classmethod
1211         def choices(cls):
1212                 return [(choice, choice.getTitle()) for choice in cls]
1213
1214         @classmethod
1215         def coerce(cls, item):
1216                 return item if type(item) == AuditSeverity else AuditSeverity[item]
1217
1218
1219
1220 class AuditLogEntry(db.Model):
1221         id         = db.Column(db.Integer, primary_key=True)
1222
1223         created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
1224
1225         causer_id  = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
1226         causer     = db.relationship("User", foreign_keys=[causer_id])
1227
1228         severity   = db.Column(db.Enum(AuditSeverity), nullable=False)
1229
1230         title      = db.Column(db.String(100), nullable=False)
1231         url        = db.Column(db.String(200), nullable=True)
1232
1233         package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
1234         package    = db.relationship("Package", foreign_keys=[package_id])
1235
1236         description = db.Column(db.Text, nullable=True, default=None)
1237
1238         def __init__(self, causer, severity, title, url, package=None, description=None):
1239                 if len(title) > 100:
1240                         title = title[:99] + "…"
1241
1242                 self.causer   = causer
1243                 self.severity = severity
1244                 self.title    = title
1245                 self.url      = url
1246                 self.package  = package
1247                 self.description = description
1248
1249
1250
1251
1252 REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \
1253                 "minetest.net", "dropboxusercontent.com", "4shared.com", \
1254                 "digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net", \
1255                 "imageshack.com", "imgur.com"]
1256
1257 class ForumTopic(db.Model):
1258         topic_id  = db.Column(db.Integer, primary_key=True, autoincrement=False)
1259         author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
1260         author    = db.relationship("User")
1261
1262         wip       = db.Column(db.Boolean, server_default="0")
1263         discarded = db.Column(db.Boolean, server_default="0")
1264
1265         type      = db.Column(db.Enum(PackageType), nullable=False)
1266         title     = db.Column(db.String(200), nullable=False)
1267         name      = db.Column(db.String(30), nullable=True)
1268         link      = db.Column(db.String(200), nullable=True)
1269
1270         posts     = db.Column(db.Integer, nullable=False)
1271         views     = db.Column(db.Integer, nullable=False)
1272
1273         created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
1274
1275         def getRepoURL(self):
1276                 if self.link is None:
1277                         return None
1278
1279                 for item in REPO_BLACKLIST:
1280                         if item in self.link:
1281                                 return None
1282
1283                 return self.link.replace("repo.or.cz/w/", "repo.or.cz/")
1284
1285         def getAsDictionary(self):
1286                 return {
1287                         "author": self.author.username,
1288                         "name":   self.name,
1289                         "type":   self.type.toName(),
1290                         "title":  self.title,
1291                         "id":     self.topic_id,
1292                         "link":   self.link,
1293                         "posts":  self.posts,
1294                         "views":  self.views,
1295                         "is_wip": self.wip,
1296                         "discarded":  self.discarded,
1297                         "created_at": self.created_at.isoformat(),
1298                 }
1299
1300         def checkPerm(self, user, perm):
1301                 if not user.is_authenticated:
1302                         return False
1303
1304                 if type(perm) == str:
1305                         perm = Permission[perm]
1306                 elif type(perm) != Permission:
1307                         raise Exception("Unknown permission given to ForumTopic.checkPerm()")
1308
1309                 if perm == Permission.TOPIC_DISCARD:
1310                         return self.author == user or user.rank.atLeast(UserRank.EDITOR)
1311
1312                 else:
1313                         raise Exception("Permission {} is not related to topics".format(perm.name))
1314
1315
1316 # Setup Flask-User
1317 user_manager = UserManager(app, db, User)
1318
1319 if app.config.get("LOG_SQL"):
1320         import logging
1321         logging.basicConfig()
1322         logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)