class ReputationEnum(Base, EnumMixin): """An enum for a ReputationChange. Inherits -------- EnumMixin Attributes ---------- default_delta : int An integer representing the default reputation change value. That is to say, the amount by which the event will change the user's reputation. """ default_delta = db.Column(db.Integer, nullable=False) entity = db.Column(db.String(128)) display = db.Column(db.String(128))
class TagRequest(Base, FollowableMixin, VotableMixin): __vote__ = TagRequestVote __reputable__ = 'requester' __approvable__ = 'approve_tag_requests' __margin_approvable__ = 'VOTES_FOR_REQUEST_APPROVAL' __margin_rejectable__ = 'VOTES_FOR_REQUEST_REJECTION' tag = db.Column(db.String(127), index=True) weight = db.Column(db.Integer, default=0, index=True) approved = db.Column(db.Boolean, default=False, index=True) rejected = db.Column(db.Boolean, default=False, index=True) timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) wiki_id = db.Column(db.Integer, db.ForeignKey('wiki.id'), nullable=False) wiki = db.relationship('Wiki', backref=backref('tagrequest', uselist=False)) requester_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True) requester = db.relationship('User', backref='tag_requests') @property def url(self): return url_for('requests.view_tag_request', request_id=self.id) def __init__(self, *args, **kwargs): description = kwargs.pop('description', None) description = "This wiki is blank." if not description else description super().__init__(*args, **kwargs) self.wiki = Wiki(body=description, entity_string=str(self)) def __repr__(self): return f'<Request for {self.tag}>'
class EnumMixin: """Any enumerated class that has more than 4 types should be use this EnumMixin. LineEnum and Right seem to be the biggest examples. Attributes ---------- enum : str A string for holding an enumerated type. """ enum = db.Column(db.String(128), index=True) def __repr__(self): return f"<{type(self).__name__} {self.enum}>" def __str__(self): if hasattr(self, 'display'): return self.display else: return self.enum
class Wiki(Base): """An actual wiki. It has some idiosyncracies I'm not fond of, notably in init_on_load. But it's modelled after my Annotation system. """ entity_string = db.Column(db.String(191), index=True) current = db.relationship('WikiEdit', primaryjoin='and_(WikiEdit.entity_id==Wiki.id,' 'WikiEdit.current==True)', uselist=False, lazy='joined') edits = db.relationship('WikiEdit', primaryjoin='WikiEdit.entity_id==Wiki.id', lazy='dynamic') edit_pending = db.relationship( 'WikiEdit', primaryjoin='and_(WikiEdit.entity_id==Wiki.id,' 'WikiEdit.approved==False, WikiEdit.rejected==False)', passive_deletes=False) @orm.reconstructor def __init_on_load__(self): """The primary purpose of this method is to load the entity reference. """ # This is a hack and needs to be improved. # Basically, we rely on the backref defined on the entity in order to # get the entity. I really want to make this more explicit and less # hacky. self.entity = list( filter(None, [ self.writer, self.text, self.tag, self.edition, self.textrequest, self.tagrequest ]))[0] def __init__(self, *args, **kwargs): """Creating a new wiki also populates the first edit.""" body = kwargs.pop('body', None) body = 'This wiki is currently blank.' if not body else body super().__init__(*args, **kwargs) self.versions.append( WikiEdit(current=True, body=body, approved=True, reason='Initial Version.')) def __repr__(self): return f'<Wiki HEAD {str(self.entity)} at version {self.current.num}>' def edit(self, editor, body, reason): """Edit the wiki, creatinga new WikiEdit.""" edit = WikiEdit(wiki=self, num=self.current.num + 1, editor=editor, body=body, reason=reason) db.session.add(edit) if editor.is_authorized('immediate_wiki_edits'): edit.approved = True self.current.current = False edit.current = True flash("The edit has been applied.") else: flash("The edit has been submitted for peer review.")
class TOC(EnumeratedMixin, Base): num = db.Column(db.Integer, index=True) precedence = db.Column(db.Integer, default=1, index=True) body = db.Column(db.String(200), index=True) haslines = db.Column(db.Boolean, index=True, default=False) prev_id = db.Column(db.Integer, db.ForeignKey('toc.id'), index=True) prev = db.relationship('TOC', uselist=False, remote_side='TOC.id', foreign_keys=[prev_id], backref=backref('next', uselist=False, remote_side='TOC.prev_id')) parent_id = db.Column(db.Integer, db.ForeignKey('toc.id'), index=True) parent = db.relationship('TOC', uselist=False, remote_side='TOC.id', foreign_keys=[parent_id], backref=backref('children', remote_side='TOC.parent_id', lazy='dynamic')) edition_id = db.Column(db.Integer, db.ForeignKey('edition.id'), index=True) edition = db.relationship('Edition', backref=backref('toc', lazy='dynamic')) text = association_proxy('edition', 'text') @property def url(self): if self.haslines: return url_for('main.read', text_url=self.text.url_name, edition_num=self.edition.num, toc_id=self.id) return None @property def section(self): parents = self.parents nums = [str(p.num) for p in parents] nums.append(str(self.num)) return '.'.join(nums) @property def parents(self): def _parents(toc, struct): """Recursive function for the purpose of this method.""" struct.insert(0, toc) if toc.parent: return _parents(toc.parent, struct) else: return struct if self.parent: return _parents(self.parent, []) else: return [] def __repr__(self): s = ['<'] for parent in self.parents: s.append(f'{parent.enum} {parent.num} ') s.append(f'{self.enum} {self.num} of {self.edition}>') return ''.join(s) def __str__(self): return self.body
class Text(Base, FollowableMixin, LinkableMixin, SearchLinesMixin): __linkable__ = 'title' """The text-object. A text is more a categorical, or philosophical concept. In essence, a book can have any number of editions, ranging from different translations to re-edited or updated versions (which is more common with non-fiction texts, and usually just consists of an added preface/introduction). Because of this dynamic, a text can is just a shell with a wiki that editions point to. Editions are the heart of the program. But the Text has a primary edition to which all annotations should be directed for a work unless they regard textual issues that differ between translations or editions. Attributes ---------- title : string The title string of the object sort_title : string A string by which these objects will be sorted (i.e., so that it doesn't contain modifier words like "the" or "a". wiki_id : int The id of the wiki object. published : Date A date corresponding to the publication date of the first edition of the text irl. timestamp : DateTime This is just a timestamp marking when the text was added to the database. It's really unnecessary, tbh. Eventually to be eliminated. followers : BaseQuery An SQLA BaseQuery object for all the followers of the text. wiki : :class:`Wiki` A wiki object describing the text. editions : BaseQuery An SQLA BaseQuery object for all the editions pointing to this text. primary : :class:`Edition` An edition object that represents the primary edition of the text (the one we direct all annotations to unless they're about textual issues, etc.) url : string A url string for the main view page of the text object. url_name : string A string that can be used in a url for the text_url parameter. """ @classmethod def get_by_url(cls, url): """This is a helper function that takes the output of url_name and uses it to get the object that matches it. """ return cls.query.filter_by(title=url.replace('_', ' ')) @classmethod def get_object_by_link(cls, name): """Get the object by the name.""" if not cls.__linkable__: raise AttributeError("Class does not have a __linkable__ " "attribute.") obj = cls.query.filter(getattr(cls, cls.__linkable__) == name).first() return obj @classmethod def link(cls, name): """Override the LinkableMixin link method.""" idents = name.split(':') obj = cls.get_object_by_link(idents[0]) if not obj: return name else: if len(idents) == 1: return f'<a href="{obj.url}">{name}</a>' else: try: return obj.link_edition(idents[1:]) except AttributeError: return name def link_edition(self, idents): """This is specifically to link other editions and line numbers.""" edition_num = idents[0] if idents[0].isdigit() else 1 edition = self.editions.filter_by(num=edition_num).first() if not edition: raise AttributeError("No edition.") line_nums = None for ident in idents: if 'l' in ident: line_nums = ident if line_nums: url = url_for('main.lines', text_url=self.url_name, edition_num=edition_num, nums=line_nums) return f"<a href=\"{url}\">{str(edition)} {line_nums}</a>" else: return f"<a href=\"{edition.url}\">{str(edition)}</a>" title = db.Column(db.String(128), index=True) sort_title = db.Column(db.String(128), index=True) wiki_id = db.Column(db.Integer, db.ForeignKey('wiki.id'), nullable=False) published = db.Column(db.Date) timestamp = db.Column(db.DateTime, default=datetime.utcnow()) wiki = db.relationship('Wiki', backref=backref('text', uselist=False)) editions = db.relationship('Edition', lazy='dynamic') primary = db.relationship( 'Edition', primaryjoin='and_(Edition.text_id==Text.id,Edition.primary==True)', uselist=False) @property def url(self): """This returns the actual internally-resolved url for the text's main view page. """ return url_for('main.text', text_url=self.url_name) @property def url_name(self): """Returns the name of the object (title) translated in to a url-utilizable string (i.e., no spaces). Notes ----- This method right now is very simple. The issue might become more complex when we have works with titles that have punctuation marks in them. eventually we might have to modify this method to translate those characters into url-acceptable characters (e.g., escape them). """ return self.title.replace(' ', '_') def __init__(self, *args, **kwargs): """This init method creates a wiki for the object with the supplied description. """ description = kwargs.pop('description', None) description = "This wiki is blank." if not description else description super().__init__(*args, **kwargs) self.wiki = Wiki(body=description, entity_string=str(self)) def __repr__(self): return f'<Text {self.id}: {self.title}>' def __str__(self): return self.title
class Writer(Base, FollowableMixin, LinkableMixin, SearchLinesMixin): """The writer model. This used to be a lot more complicated but has become fairly elegant. All historical contributors to the text are writers, be they editors, translators, authors, or whatever the heck else we end up coming up with (perhaps with the exception of annotator, we'll see). Attributes ---------- name : string The full name of the writer family_name : string The family name of the writer. We store this as an actual value in the db instead of computing it on the fly because of varying naming conventions (e.g., Chinese names). And we need it for sorting purposes. birth_date : Date The birthdate of the writer. death_date : Date The deathdate of the writer. wiki_id : int The id of the descriptive wiki object. timestamp : datetime The timestamp of when the writer was added to the database. Superfluous. followers : BaseQuery An SQLA BaseQuery of all of the followers of the writer. connections : BaseQuery An SQLA BaseQuery of all of the connection objects for the writer (i.e., the writer's relationships to various editions). wiki : :class:`Wiki` The descriptive wiki object. annotations : BaseQuery An SQLA BaseQuery for all of the annotations on all of the editions the writer is responsible for. """ __linkable__ = 'name' name = db.Column(db.String(128), index=True) family_name = db.Column(db.String(128), index=True) birth_date = db.Column(db.Date, index=True) death_date = db.Column(db.Date, index=True) wiki_id = db.Column(db.Integer, db.ForeignKey('wiki.id'), nullable=False) timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) connections = db.relationship('WriterConnection', lazy='dynamic') wiki = db.relationship('Wiki', backref=backref('writer', uselist=False)) annotations = db.relationship( 'Annotation', secondary='join(WriterConnection, Edition)', primaryjoin='Writer.id==WriterConnection.writer_id', secondaryjoin='Annotation.edition_id==Edition.id', lazy='dynamic') @property def url(self): """The url for the main view page for the writer.""" return url_for('main.writer', writer_url=self.url_name) @property def url_name(self): """A string that can be used for urls (i.e., it has all the spaces replaced with underscores. """ return self.name.replace(' ', '_') def __init__(self, *args, **kwargs): """Creates a descriptive wiki as well.""" description = kwargs.pop('description', None) description = 'This writer does not have a biography yet.'\ if not description else description super().__init__(*args, **kwargs) self.wiki = Wiki(body=description, entity_string=str(self)) @orm.reconstructor def __init_on_load__(self): """Creates a dictionary mapping the writer's role to the editions.""" self.works = defaultdict(list) for conn in self.connections.all(): self.works[conn.enum].append(conn.edition) def __repr__(self): return f'<Writer: {self.name}>' def __str__(self): return self.name @classmethod def get_by_url(cls, name): """This is a helper function that takes the output of url_name and uses it to get the object that matches it. """ return cls.query.filter_by(name=url.replace('_', ' '))
class Edition(Base, FollowableMixin, SearchLinesMixin): """The Edition model. This is actually more central to the app than the Text object. The edition has all of the writer connections, annotations, and lines connected to it. Attributes ---------- num : int The edition number of the edition. I.e., edition 1, 2, etc. primary : bool A boolean switch to represent whether or not the specific edition is the primary edition for it's parent text. Only one *should* be primary. If more than one is primary, this is an error and needs to be corrected. published : DateTime The publication date of the specific edition. timestamp : DateTime Simple timestamp for when the Edition was created. connections : BaseQuery A filterable BaseQuery of WriterConnections. text_title : str A proxy for the title of the text. Mostly used so that the Annotation can query it easily with it's own association_proxy. tochide : boolean A flag for disabling the showing of the toc dispay enum because the toc body is complete on it's own. verse : boolean A flag for disabling the line concatenation that happens on cell phones. It will eventually become (either) more useful, or less. Unsure yet. """ _title = db.Column(db.String(235), default=None) num = db.Column(db.Integer, default=1) text_id = db.Column(db.Integer, db.ForeignKey('text.id')) primary = db.Column(db.Boolean, default=False) wiki_id = db.Column(db.Integer, db.ForeignKey('wiki.id'), nullable=False) published = db.Column(db.DateTime) timestamp = db.Column(db.DateTime, default=datetime.utcnow()) tochide = db.Column(db.Boolean, default=True) verse = db.Column(db.Boolean, default=False) annotations = db.relationship('Annotation', lazy='dynamic') connections = db.relationship('WriterConnection', lazy='dynamic') wiki = db.relationship('Wiki', backref=backref('edition', uselist=False)) text = db.relationship('Text') text_title = association_proxy('text', 'title') @property def url(self): """A string title of the edition. Basically consists of the parent text's title and an asterisk for the primary edition or 'Edition #'. """ return url_for('main.edition', text_url=self.text.url_name, edition_num=self.num) @property def title(self): """Returns the title representation string of the edition.""" if self._title: return f'{self._title}*' if self.primary else self._title return (f'{self.text_title}*' if self.primary else f"{self.text_title} - Ed. #{self.num}") @property def edition_title(self): """A string for the edition similar to title but *not* including the parent text's title. """ if self._title: return f'{self._title}*' if self.primary else self._title return (f"Edition #{self.num} - Primary" if self.primary else f"Edition #{self.num}") @property def tocstruct(self): byparentid = defaultdict(list) for toc in self.toc: byparentid[toc.parent_id].append(toc) return byparentid def __init__(self, *args, **kwargs): """Creates a wiki for the edition with the provided description.""" description = kwargs.pop('description', None) description = 'This wiki is blank.' if not description else description super().__init__(*args, **kwargs) self.wiki = Wiki(body=description, entity_string=str(self)) @orm.reconstructor def __init_on_load__(self): """Creates a defaultdict of lists of writers based on their connection type (e.g., author, editor, translator, etc.) """ self._create_writers() def _create_writers(self): """This creates the _writers attr. It's abstracted for use by writers() and __init_on_load__(). """ self._writers = defaultdict(list) for conn in self.connections.all(): self._writers[conn.enum].append(conn.writer) @property def writers(self): """This by using a property to access the writerslist I ensure that it will always exist before it is accessed. This prevents the error I get when first creating this thing where I try to access writers and it doesn't exist when indexing on a first run through. """ if not hasattr(self, '_writers'): self._create_writers() return self._writers def __repr__(self): return f'<{self.title}>' def __str__(self): return self.title def get_lines(self, nums): if len(nums) >= 2: line_query = self.lines.filter(Line.num >= nums[0], Line.num <= nums[-1]) else: line_query = self.lines.filter(Line.num == nums[0]) return line_query
class Tag(Base, FollowableMixin, LinkableMixin): __linkable__ = 'tag' """A class representing tags. Attributes ---------- tag : str The name of the tag locked : bool The locked status of the tag (i.e., whether it is available for ordinary users to apply to their annotations). wiki_id : int The id of the tag's wiki. wiki : :class:`Wiki` The tag's :class:`Wiki` object. annotations : :class:`BaseQuery` A :class:`BaseQuery` object of all of the annotations which currently have this tag applied to them. """ @classmethod def intersect(cls, tags): """Get the annotations at the intersection of multiple tags. Parameters ---------- tags : tuple A tuple of strings corresponding to the names of the tags to be intersected. Returns ------- :class:`BaseQuery` A :class:`BaseQuery` object that can be used to get all of the annotations. Raises ------ TypeError If the first argument is not a tuple. TypeError If the elements of the tuple are not all strings. """ if not isinstance(tags, tuple): raise TypeError('Tags argument must be a tuple of strings.') if not all(isinstance(tag, str) for tag in tags): raise TypeError("The tags tuple must consist of only strings.") queries = [] for tag in tags: queries.append(cls.query.filter_by(tag=tag).first().annotations) query = queries[0].intersect(*queries[1:]) return query @classmethod def union(cls, tags): """Get the annotations at the union of multiple tags. Parameters ---------- tags : tuple A tuple of strings corresponding to the names of the tags to be unioned. Returns ------- BaseQuery A :class:`BaseQuery` object that can be used to get all of the annotations. Raises ------ TypeError If the first argument is not a tuple. TypeError If the elements of the tuple are not all strings. """ if not isinstance(tags, tuple): raise TypeError('Tags argument must be a tuple of strings.') if not all(isinstance(tag, str) for tag in tags): raise TypeError("The tags tuple must consist of only strings.") queries = [] for tag in tags: queries.append(cls.query.filter_by(tag=tag).first().annotations) query = queries[0].union(*queries[1:]) return query tag = db.Column(db.String(128), index=True, unique=True) locked = db.Column(db.Boolean, default=False) wiki_id = db.Column(db.Integer, db.ForeignKey('wiki.id'), nullable=False) wiki = db.relationship('Wiki', backref=backref('tag', uselist=False)) annotations = db.relationship( 'Annotation', secondary='join(tags, Edit, and_(tags.c.edit_id==Edit.id,' 'Edit.current==True))', primaryjoin='Tag.id==tags.c.tag_id', secondaryjoin='and_(Edit.entity_id==Annotation.id,' 'Annotation.active==True)', lazy='dynamic') @property def url(self): """The url for the main view page for the tag.""" return url_for('main.tag', tag=self.tag) def __init__(self, *args, **kwargs): """Creation of a tag also creates a :class:`Wiki`.""" description = kwargs.pop('description', None) description = ("This tag has no description yet." if not description else description) super().__init__(*args, **kwargs) self.wiki = Wiki(body=description, entity_string=str(self)) def __repr__(self): return f'<Tag {self.id}: {self.tag}>' def __str__(self): return f'<div class="tag">{self.tag}</div>'
class EditMixin: """A mixin to store edits. This is to be used for anything that can be edited so that we can have an edit history. Notes ----- Currently, the only methods I have implemented here are rollback and reject because of the different naming strategies of the votes. THIS CAN BE CORRECTED. I believe I have to investigate Meta classes or something like that to make sure that something is implemented or something. But this EditMixin can be tied in in such a way that those methods can be collapsed. I need to do this. Attributes ---------- num : int The number of the edit in the edit history. current : bool A boolean indicating withether this is the current state of the parent object or not. There should only ever be one per object. There should always be at least one. If there are more than or less than one for an object THAT IS A PROBLEM. weight : int The weight of the object (basically the difference between upvotes and downvotes, but a bit more complicated than that. approved : bool Whether the edit has been approved or not. rejected : bool Whether the edit has been rejected or not. `approved` and `rejected` should not both be True, but they can both be False (namely, immediately after being created, before it is reviewed). reason : str A string explaining the reason for the edit. Will probably be abused and ignored. But good practice. timestamp : DateTime When the edit was made. body : str The actual body of the Edit. """ num = db.Column(db.Integer, default=1) current = db.Column(db.Boolean, index=True, default=False) weight = db.Column(db.Integer, default=0) approved = db.Column(db.Boolean, index=True, default=False) rejected = db.Column(db.Boolean, index=True, default=False) reason = db.Column(db.String(191)) timestamp = db.Column(db.DateTime, default=datetime.utcnow(), index=True) body = db.Column(db.Text) @declared_attr def editor_id(cls): """The id of the editor.""" return db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, default=1) @declared_attr def editor(cls): """The actual user object of the editor. The backref is the class name lowercased with an `s` appended """ return db.relationship( 'User', backref=backref(f'{cls.__name__.lower()}s', lazy='dynamic')) @declared_attr def previous(cls): """The previous edit.""" return db.relationship( f'{cls.__name__}', primaryjoin=f'and_(remote({cls.__name__}.entity_id)' f'==foreign({cls.__name__}.entity_id),' f'remote({cls.__name__}.num)==foreign({cls.__name__}.num-1),' f'remote({cls.__name__}.rejected)==False)') @declared_attr def priors(cls): """A list of all prior edits""" return db.relationship( f'{cls.__name__}', primaryjoin=f'and_(remote({cls.__name__}.entity_id)==' f'foreign({cls.__name__}.entity_id),' f'remote({cls.__name__}.num)<=foreign({cls.__name__}.num-1))', uselist=True) def __repr__(self): return f"<Edit {self.num} on "
class User(UserMixin, Base): """The User class. Inherits -------- UserMixin Attributes ---------- displayname : str We follow StackExchange in using displaynames that are non-unique and can be changed. The user is primarily defined by his email. email : str The primary string-based identifier of the user. Must be unique. Have not worked on email validation yet, need to. password_hash : str The FlaskLogin defined password hashing structure. I want to investigate the security of this and make modifications before going live (e.g., check algorithm, salt, etc.). reputation : int The user's reputation. This is affected by :class:`ReputationChange` objects. locked : int This is a boolean to lock the user's account from logging in for security purposes. about_me : str A text of seemingly any length that allows the user to describe themselves. I might convert this to a wiki. Not sure if it's worth it or not. Probably not. About me history? Extreme. On the other hand, it leverages the existing wiki system just like Wikipedia does. last_seen : DateTime A timestamp for when the user last made a database-modification. rights : list A list of all of the :class:`AdminRight` objects the user has. annotations : BaseQuery An SQLA BaseQuery of all the annotations the user has authored. followed_<class>s : BaseQuery A BaseQuery of all of the <class>'s that the user is currently following. The <class> is the class's name lowercased (same as the table name). followers : BaseQuery A BaseQuery of all of the User's that follow this User. """ displayname = db.Column(db.String(64), index=True) email = db.Column(db.String(128), index=True, unique=True) password_hash = db.Column(db.String(128)) reputation = db.Column(db.Integer, default=0) locked = db.Column(db.Boolean, default=False) about_me = db.Column(db.Text) last_seen = db.Column(db.DateTime, default=datetime.utcnow) reputation_changes = db.relationship('ReputationChange', lazy='dynamic') rights = db.relationship('AdminRight', secondary='rights') annotations = db.relationship('Annotation', lazy='dynamic') active_flags = db.relationship( 'UserFlag', primaryjoin='and_(User.id==UserFlag.user_id,' 'UserFlag.resolver_id==None)', passive_deletes=True) # Because this is a self-referential many-to-many it is defined explicitly # as opposed to using my FollowableMixin followed_users = db.relationship( 'User', secondary=db.Table( 'user_flrs', db.Column('follower_id', db.Integer, db.ForeignKey('user.id')), db.Column('followed_id', db.Integer, db.ForeignKey('user.id'))), primaryjoin='user_flrs.c.follower_id==User.id', secondaryjoin='user_flrs.c.followed_id==User.id', backref=db.backref('followers', lazy='dynamic'), lazy='dynamic') @property def url(self): """A string that represents the url for this user object (i.e., the user's profile page. """ return url_for("user.profile", user_id=self.id) @property def readable_reputation(self): """A string that represents a nicely formatted reputation of this user. """ if self.reputation >= 1000000: return f'{round(self.reputation/1000000)}m' elif self.reputation >= 1000: return f'{round(self.reputation/1000)}k' else: return f'{self.reputation}' def avatar(self, size): """A link to the gravatar for this user. I will probably eventually want to simply eliminate avatars. Especially gravatars as they are a security vulnerability. """ digest = md5(self.email.lower().encode('utf-8')).hexdigest() return f'https://www.gravatar.com/avatar/{digest}?d=identicon&s={size}' def __repr__(self): return f"<User {self.displayname}>" def __str__(self): return self.displayname def update_last_seen(self): """A method that will update the user's last seen timestamp.""" self.last_seen = datetime.utcnow() def repchange(self, enumstring): """Change a user's reputation given the enumstring.""" enum = ReputationEnum.query.filter_by(enum=enumstring).first() if not enum: return repchange = ReputationChange(enum=enum, user=self, delta=enum.default_delta) if self.reputation + repchange.delta <= 0: repchange.delta = -self.reputation self.reputation += repchange.delta return repchange def rollback_repchange(self, repchange): """Rollback a user's reputation given a reputationchange.""" if not repchange: return if self.reputation - repchange.delta < 0: delta = -self.reputation else: delta = repchange.delta self.reputation -= delta db.session.delete(repchange) # Password routes def set_password(self, password): """Set the password for the user.""" self.password_hash = generate_password_hash(password) def check_password(self, password): """Check that the password provided is the user's current password.""" return check_password_hash(self.password_hash, password) def get_reset_password_token(self, expires_in=600): """Generate a reset_password token for the user's emailed link to reset the password. """ return jwt.encode( { 'reset_password': self.id, 'exp': time() + expires_in }, app.config['SECRET_KEY'], algorithm='HS256').decode('utf-8') @staticmethod def verify_reset_password_token(token): """A static method to verify a reset password token's validity.""" try: id = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])['reset_password'] except: return return User.query.get(id) # admin authorization methods def is_authorized(self, right): """Check if a user is authorized with a particular right.""" r = AdminRight.query.filter_by(enum=right).first() if not r: return False return r in self.rights or (r.min_rep and self.reputation >= r.min_rep) def is_auth_all(self, rights): """This is like is_authorized but takes a list and only returns true if the user is authorized for all the rights in the list. """ for right in rights: if not self.is_authorized(right): return False return True def is_auth_any(self, rights): """Tests if user is authorized for *any* of the rights in the list.""" for right in rights: if self.is_authorized(right): return True return False def authorize(self, right): """Authorize a user with a right. Notes ----- This is different from is_authorized (in fact, it uses is_authorized): this method will throw a 403 abort if the user is not authorized. It simplifies some logic. """ if self.is_authorized(right): pass else: abort(403) def get_vote(self, obj): """Get the vote on the object. If the object does not have a __vote__ attribute, it's not going to work and a TypeError will be raised. This method works for every votable class and can be tested as a Falsey for whether or whether not the user has voted on the object. """ if not obj.__vote__: raise TypeError("The requested object is missing an `__vote__` " "attribute.") vote_cls = obj.__vote__ return vote_cls.query.filter(vote_cls.voter == self, vote_cls.entity == obj).first()