]> git.lizzy.rs Git - cheatdb.git/blob - app/models.py
Add dependency support
[cheatdb.git] / app / models.py
1 # Content DB
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 from flask import Flask, url_for
19 from flask_sqlalchemy import SQLAlchemy
20 from app import app
21 from datetime import datetime
22 from sqlalchemy.orm import validates
23 from flask_user import login_required, UserManager, UserMixin, SQLAlchemyAdapter
24 import enum
25
26 # Initialise database
27 db = SQLAlchemy(app)
28
29
30 class UserRank(enum.Enum):
31         NOT_JOINED = 0
32         NEW_MEMBER = 1
33         MEMBER     = 2
34         EDITOR     = 3
35         MODERATOR  = 4
36         ADMIN      = 5
37
38         def atLeast(self, min):
39                 return self.value >= min.value
40
41         def getTitle(self):
42                 return self.name.replace("_", " ").title()
43
44         def toName(self):
45                 return self.name.lower()
46
47         def __str__(self):
48                 return self.name
49
50         @classmethod
51         def choices(cls):
52                 return [(choice, choice.getTitle()) for choice in cls]
53
54         @classmethod
55         def coerce(cls, item):
56                 return item if type(item) == UserRank else UserRank[item]
57
58
59 class Permission(enum.Enum):
60         EDIT_PACKAGE       = "EDIT_PACKAGE"
61         APPROVE_CHANGES    = "APPROVE_CHANGES"
62         DELETE_PACKAGE     = "DELETE_PACKAGE"
63         CHANGE_AUTHOR      = "CHANGE_AUTHOR"
64         MAKE_RELEASE       = "MAKE_RELEASE"
65         APPROVE_RELEASE    = "APPROVE_RELEASE"
66         APPROVE_NEW        = "APPROVE_NEW"
67         CHANGE_RELEASE_URL = "CHANGE_RELEASE_URL"
68         CHANGE_RANK        = "CHANGE_RANK"
69         CHANGE_EMAIL       = "CHANGE_EMAIL"
70         EDIT_EDITREQUEST   = "EDIT_EDITREQUEST"
71
72         # Only return true if the permission is valid for *all* contexts
73         # See Package.checkPerm for package-specific contexts
74         def check(self, user):
75                 if not user.is_authenticated:
76                         return False
77
78                 if self == Permission.APPROVE_NEW or \
79                                 self == Permission.APPROVE_CHANGES or \
80                                 self == Permission.APPROVE_RELEASE:
81                         return user.rank.atLeast(UserRank.EDITOR)
82                 else:
83                         raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
84
85
86 class User(db.Model, UserMixin):
87         id = db.Column(db.Integer, primary_key=True)
88
89         # User authentication information
90         username = db.Column(db.String(50), nullable=False, unique=True)
91         password = db.Column(db.String(255), nullable=False, server_default="")
92         reset_password_token = db.Column(db.String(100), nullable=False, server_default="")
93
94         rank = db.Column(db.Enum(UserRank))
95
96         # Account linking
97         github_username = db.Column(db.String(50), nullable=True, unique=True)
98         forums_username = db.Column(db.String(50), nullable=True, unique=True)
99
100         # User email information
101         email = db.Column(db.String(255), nullable=True, unique=True)
102         confirmed_at = db.Column(db.DateTime())
103
104         # User information
105         active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
106         display_name = db.Column(db.String(100), nullable=False, server_default="")
107
108         # Content
109         notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id")
110
111         # causednotifs  = db.relationship("Notification", backref="causer", lazy="dynamic")
112         packages      = db.relationship("Package", backref="author", lazy="dynamic")
113         requests      = db.relationship("EditRequest", backref="author", lazy="dynamic")
114
115         def __init__(self, username):
116                 import datetime
117
118                 self.username = username
119                 self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
120                 self.display_name = username
121                 self.rank = UserRank.NOT_JOINED
122
123         def canAccessTodoList(self):
124                 return Permission.APPROVE_NEW.check(self) or \
125                                 Permission.APPROVE_RELEASE.check(self) or \
126                                 Permission.APPROVE_CHANGES.check(self)
127
128         def isClaimed(self):
129                 return self.rank.atLeast(UserRank.NEW_MEMBER)
130
131         def checkPerm(self, user, perm):
132                 if not user.is_authenticated:
133                         return False
134
135                 if type(perm) == str:
136                         perm = Permission[perm]
137                 elif type(perm) != Permission:
138                         raise Exception("Unknown permission given to User.checkPerm()")
139
140                 # Members can edit their own packages, and editors can edit any packages
141                 if perm == Permission.CHANGE_AUTHOR:
142                         return user.rank.atLeast(UserRank.EDITOR)
143                 elif perm == Permission.CHANGE_RANK:
144                         return user.rank.atLeast(UserRank.MODERATOR)
145                 elif perm == Permission.CHANGE_EMAIL:
146                         return user == self or (user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank))
147                 else:
148                         raise Exception("Permission {} is not related to users".format(perm.name))
149
150 class UserEmailVerification(db.Model):
151         id      = db.Column(db.Integer, primary_key=True)
152         user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
153         email   = db.Column(db.String(100))
154         token   = db.Column(db.String(32))
155         user    = db.relationship("User", foreign_keys=[user_id])
156
157 class Notification(db.Model):
158         id        = db.Column(db.Integer, primary_key=True)
159         user_id   = db.Column(db.Integer, db.ForeignKey("user.id"))
160         causer_id = db.Column(db.Integer, db.ForeignKey("user.id"))
161         user      = db.relationship("User", foreign_keys=[user_id])
162         causer    = db.relationship("User", foreign_keys=[causer_id])
163
164         title     = db.Column(db.String(100), nullable=False)
165         url       = db.Column(db.String(200), nullable=True)
166
167         def __init__(self, us, cau, titl, ur):
168                 self.user   = us
169                 self.causer = cau
170                 self.title  = titl
171                 self.url    = ur
172
173
174 class License(db.Model):
175         id = db.Column(db.Integer, primary_key=True)
176         name = db.Column(db.String(50), nullable=False, unique=True)
177         packages = db.relationship("Package", backref="license", lazy="dynamic")
178
179         def __init__(self, v):
180                 self.name = v
181
182         def __str__(self):
183                 return self.name
184
185
186 class PackageType(enum.Enum):
187         MOD  = "Mod"
188         GAME = "Game"
189         TXP  = "Texture Pack"
190
191         def toName(self):
192                 return self.name.lower()
193
194         def __str__(self):
195                 return self.name
196
197         @classmethod
198         def choices(cls):
199                 return [(choice, choice.value) for choice in cls]
200
201         @classmethod
202         def coerce(cls, item):
203                 return item if type(item) == PackageType else PackageType[item]
204
205
206 class PackagePropertyKey(enum.Enum):
207         name         = "Name"
208         title        = "Title"
209         shortDesc    = "Short Description"
210         desc         = "Description"
211         type         = "Type"
212         license      = "License"
213         tags         = "Tags"
214         repo         = "Repository"
215         website      = "Website"
216         issueTracker = "Issue Tracker"
217         forums       = "Forum Topic ID"
218
219         def convert(self, value):
220                 if self == PackagePropertyKey.tags:
221                         return ",".join([t.title for t in value])
222                 else:
223                         return str(value)
224
225 tags = db.Table("tags",
226     db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True),
227     db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True)
228 )
229
230 harddeps = db.Table("harddeps",
231         db.Column("package_id",    db.Integer, db.ForeignKey("package.id"), primary_key=True),
232     db.Column("dependency_id", db.Integer, db.ForeignKey("package.id"), primary_key=True)
233 )
234
235 softdeps = db.Table("softdeps",
236         db.Column("package_id",    db.Integer, db.ForeignKey("package.id"), primary_key=True),
237     db.Column("dependency_id", db.Integer, db.ForeignKey("package.id"), primary_key=True)
238 )
239
240 class Package(db.Model):
241         id           = db.Column(db.Integer, primary_key=True)
242
243         # Basic details
244         author_id    = db.Column(db.Integer, db.ForeignKey("user.id"))
245         name         = db.Column(db.String(100), nullable=False)
246         title        = db.Column(db.String(100), nullable=False)
247         shortDesc    = db.Column(db.String(200), nullable=False)
248         desc         = db.Column(db.Text, nullable=True)
249         type         = db.Column(db.Enum(PackageType))
250
251         license_id   = db.Column(db.Integer, db.ForeignKey("license.id"))
252
253         approved     = db.Column(db.Boolean, nullable=False, default=False)
254
255         # Downloads
256         repo         = db.Column(db.String(200), nullable=True)
257         website      = db.Column(db.String(200), nullable=True)
258         issueTracker = db.Column(db.String(200), nullable=True)
259         forums       = db.Column(db.Integer,     nullable=False)
260
261         tags = db.relationship("Tag", secondary=tags, lazy="subquery",
262                         backref=db.backref("packages", lazy=True))
263
264         harddeps = db.relationship("Package",
265                                 secondary=harddeps,
266                                 primaryjoin=id==harddeps.c.package_id,
267                                 secondaryjoin=id==harddeps.c.dependency_id,
268                                 backref="dependents")
269
270         softdeps = db.relationship("Package",
271                                 secondary=softdeps,
272                                 primaryjoin=id==softdeps.c.package_id,
273                                 secondaryjoin=id==softdeps.c.dependency_id,
274                                 backref="softdependents")
275
276         releases = db.relationship("PackageRelease", backref="package",
277                         lazy="dynamic", order_by=db.desc("package_release_releaseDate"))
278
279         screenshots = db.relationship("PackageScreenshot", backref="package",
280                         lazy="dynamic")
281
282         requests = db.relationship("EditRequest", backref="package",
283                         lazy="dynamic")
284
285         def getAsDictionary(self, base_url):
286                 return {
287                         "name": self.name,
288                         "title": self.title,
289                         "author": self.author.display_name,
290                         "shortDesc": self.shortDesc,
291                         "type": self.type.toName(),
292                         "license": self.license.name,
293                         "repo": self.repo,
294                         "url": base_url + self.getDownloadURL(),
295                         "release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
296                         "screenshots": [base_url + ss.url for ss in self.screenshots]
297                 }
298
299         def getDetailsURL(self):
300                 return url_for("package_page",
301                                 author=self.author.username, name=self.name)
302
303         def getEditURL(self):
304                 return url_for("create_edit_package_page",
305                                 author=self.author.username, name=self.name)
306
307         def getApproveURL(self):
308                 return url_for("approve_package_page",
309                                 author=self.author.username, name=self.name)
310
311         def getNewScreenshotURL(self):
312                 return url_for("create_screenshot_page",
313                                 author=self.author.username, name=self.name)
314
315         def getCreateReleaseURL(self):
316                 return url_for("create_release_page",
317                                 author=self.author.username, name=self.name)
318
319         def getCreateEditRequestURL(self):
320                 return url_for("create_edit_editrequest_page",
321                                 author=self.author.username, name=self.name)
322
323         def getDownloadURL(self):
324                 return url_for("package_download_page",
325                                 author=self.author.username, name=self.name)
326
327         def getMainScreenshotURL(self):
328                 screenshot = self.screenshots.first()
329                 return screenshot.url if screenshot is not None else None
330
331         def getDownloadRelease(self):
332                 for rel in self.releases:
333                         if rel.approved:
334                                 return rel
335
336                 return None
337
338         def checkPerm(self, user, perm):
339                 if not user.is_authenticated:
340                         return False
341
342                 if type(perm) == str:
343                         perm = Permission[perm]
344                 elif type(perm) != Permission:
345                         raise Exception("Unknown permission given to Package.checkPerm()")
346
347                 isOwner = user == self.author
348
349                 # Members can edit their own packages, and editors can edit any packages
350                 if perm == Permission.MAKE_RELEASE:
351                         return isOwner or user.rank.atLeast(UserRank.EDITOR)
352
353                 if perm == Permission.EDIT_PACKAGE or perm == Permission.APPROVE_CHANGES:
354                         return user.rank.atLeast(UserRank.MEMBER if isOwner else UserRank.EDITOR)
355
356                 # Editors can change authors, approve new packages, and approve releases
357                 elif perm == Permission.CHANGE_AUTHOR or perm == Permission.APPROVE_NEW \
358                                 or perm == Permission.APPROVE_RELEASE:
359                         return user.rank.atLeast(UserRank.EDITOR)
360
361                 # Moderators can delete packages
362                 elif perm == Permission.DELETE_PACKAGE or perm == Permission.CHANGE_RELEASE_URL:
363                         return user.rank.atLeast(UserRank.MODERATOR)
364
365                 else:
366                         raise Exception("Permission {} is not related to packages".format(perm.name))
367
368 class Tag(db.Model):
369         id              = db.Column(db.Integer,    primary_key=True)
370         name            = db.Column(db.String(100), unique=True, nullable=False)
371         title           = db.Column(db.String(100), nullable=False)
372         backgroundColor = db.Column(db.String(6),   nullable=False)
373         textColor       = db.Column(db.String(6),   nullable=False)
374
375         def __init__(self, title, backgroundColor="000000", textColor="ffffff"):
376                 self.title           = title
377                 self.backgroundColor = backgroundColor
378                 self.textColor       = textColor
379
380                 import re
381                 regex = re.compile("[^a-z_]")
382                 self.name = regex.sub("", self.title.lower().replace(" ", "_"))
383
384 class PackageRelease(db.Model):
385         id           = db.Column(db.Integer, primary_key=True)
386
387         package_id   = db.Column(db.Integer, db.ForeignKey("package.id"))
388         title        = db.Column(db.String(100), nullable=False)
389         releaseDate  = db.Column(db.DateTime,        nullable=False)
390         url          = db.Column(db.String(100), nullable=False)
391         approved     = db.Column(db.Boolean, nullable=False, default=False)
392         task_id      = db.Column(db.String(32), nullable=True)
393
394
395         def getEditURL(self):
396                 return url_for("edit_release_page",
397                                 author=self.package.author.username,
398                                 name=self.package.name,
399                                 id=self.id)
400
401         def __init__(self):
402                 self.releaseDate = datetime.now()
403
404 class PackageScreenshot(db.Model):
405         id         = db.Column(db.Integer, primary_key=True)
406         package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
407         title      = db.Column(db.String(100), nullable=False)
408         url        = db.Column(db.String(100), nullable=False)
409
410         def getThumbnailURL(self):
411                 return self.url  # TODO
412
413 class EditRequest(db.Model):
414         id           = db.Column(db.Integer, primary_key=True)
415
416         package_id   = db.Column(db.Integer, db.ForeignKey("package.id"))
417         author_id    = db.Column(db.Integer, db.ForeignKey("user.id"))
418
419         title        = db.Column(db.String(100), nullable=False)
420         desc         = db.Column(db.String(1000), nullable=True)
421
422         # 0 - open
423         # 1 - merged
424         # 2 - rejected
425         status       = db.Column(db.Integer, nullable=False, default=0)
426
427         changes = db.relationship("EditRequestChange", backref="request",
428                         lazy="dynamic")
429
430         def getURL(self):
431                 return url_for("view_editrequest_page",
432                                 author=self.package.author.username,
433                                 name=self.package.name,
434                                 id=self.id)
435
436         def getApproveURL(self):
437                 return url_for("approve_editrequest_page",
438                                 author=self.package.author.username,
439                                 name=self.package.name,
440                                 id=self.id)
441
442         def getRejectURL(self):
443                 return url_for("reject_editrequest_page",
444                                 author=self.package.author.username,
445                                 name=self.package.name,
446                                 id=self.id)
447
448         def getEditURL(self):
449                 return url_for("create_edit_editrequest_page",
450                                 author=self.package.author.username,
451                                 name=self.package.name,
452                                 id=self.id)
453
454         def applyAll(self, package):
455                 for change in self.changes:
456                         change.apply(package)
457
458
459         def checkPerm(self, user, perm):
460                 if not user.is_authenticated:
461                         return False
462
463                 if type(perm) == str:
464                         perm = Permission[perm]
465                 elif type(perm) != Permission:
466                         raise Exception("Unknown permission given to EditRequest.checkPerm()")
467
468                 isOwner = user == self.author
469
470                 # Members can edit their own packages, and editors can edit any packages
471                 if perm == Permission.EDIT_EDITREQUEST:
472                         return isOwner or user.rank.atLeast(UserRank.EDITOR)
473
474                 else:
475                         raise Exception("Permission {} is not related to packages".format(perm.name))
476
477
478
479
480 class EditRequestChange(db.Model):
481         id           = db.Column(db.Integer, primary_key=True)
482
483         request_id   = db.Column(db.Integer, db.ForeignKey("edit_request.id"))
484         key          = db.Column(db.Enum(PackagePropertyKey), nullable=False)
485
486         # TODO: make diff instead
487         oldValue     = db.Column(db.Text, nullable=True)
488         newValue     = db.Column(db.Text, nullable=True)
489
490         def apply(self, package):
491                 if self.key == PackagePropertyKey.tags:
492                         package.tags.clear()
493                         for tagTitle in self.newValue.split(","):
494                                 tag = Tag.query.filter_by(title=tagTitle.strip()).first()
495                                 package.tags.append(tag)
496                 else:
497                         setattr(package, self.key.name, self.newValue)
498
499 # Setup Flask-User
500 db_adapter = SQLAlchemyAdapter(db, User)        # Register the User model
501 user_manager = UserManager(db_adapter, app)     # Initialize Flask-User