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