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