You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

305 lines
10 KiB

  1. from datetime import datetime
  2. import hashlib
  3. from werkzeug.security import generate_password_hash, check_password_hash
  4. from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
  5. from markdown import markdown
  6. import bleach
  7. from itsdangerous import BadSignature
  8. from flask import current_app
  9. from flask_login import UserMixin, AnonymousUserMixin
  10. from . import db, login_manager
  11. class Permission:
  12. FOLLOW = 1
  13. COMMENT = 2
  14. WRITE = 4
  15. MODERATE = 8
  16. ADMIN = 16
  17. class Role(db.Model):
  18. __tablename__ = 'roles'
  19. id = db.Column(db.Integer, primary_key=True)
  20. name = db.Column(db.String(64), unique=True)
  21. default = db.Column(db.Boolean, default=False, index=True)
  22. permissions = db.Column(db.Integer)
  23. users = db.relationship('User', backref='role', lazy='dynamic')
  24. def __init__(self, **kwargs):
  25. super(Role, self).__init__(**kwargs)
  26. if self.permissions is None:
  27. self.permissions = 0
  28. @staticmethod
  29. def insert_roles():
  30. roles = {
  31. 'User': [Permission.FOLLOW, Permission.COMMENT, Permission.WRITE],
  32. 'Moderator': [Permission.FOLLOW, Permission.COMMENT,
  33. Permission.WRITE, Permission.MODERATE],
  34. 'Administrator': [Permission.FOLLOW, Permission.COMMENT,
  35. Permission.WRITE, Permission.MODERATE,
  36. Permission.ADMIN]
  37. }
  38. default_role = 'User'
  39. for r in roles:
  40. role = Role.query.filter_by(name=r).first()
  41. if role is None:
  42. role = Role(name=r)
  43. role.reset_permissions()
  44. for perm in roles[r]:
  45. role.add_permission(perm)
  46. role.default = (role.name == default_role)
  47. db.session.add(role)
  48. db.session.commit()
  49. def add_permission(self, perm):
  50. if not self.has_permission(perm):
  51. self.permissions += perm
  52. def remove_permission(self, perm):
  53. if self.has_permission(perm):
  54. self.permissions -= perm
  55. def reset_permissions(self):
  56. self.permissions = 0
  57. def has_permission(self, perm):
  58. return self.permissions & perm == perm
  59. def __repr__(self):
  60. return '<Role %r>' % self.name
  61. class Follow(db.Model):
  62. __tablename__ = 'follows'
  63. follower_id = db.Column(db.Integer, db.ForeignKey('users.id'),
  64. primary_key=True)
  65. followed_id = db.Column(db.Integer, db.ForeignKey('users.id'),
  66. primary_key=True)
  67. timestamp = db.Column(db.DateTime, default=datetime.utcnow)
  68. class User(UserMixin, db.Model):
  69. __tablename__ = 'users'
  70. id = db.Column(db.Integer, primary_key=True)
  71. email = db.Column(db.String(64), unique=True, index=True)
  72. username = db.Column(db.String(64), unique=True, index=True)
  73. role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
  74. password_hash = db.Column(db.String(128))
  75. confirmed = db.Column(db.Boolean, default=False)
  76. name = db.Column(db.String(64))
  77. location = db.Column(db.String(64))
  78. about_me = db.Column(db.Text())
  79. member_since = db.Column(db.DateTime(), default=datetime.utcnow)
  80. last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
  81. avatar_hash = db.Column(db.String(32))
  82. posts = db.relationship('Post', backref='author', lazy='dynamic')
  83. followed = db.relationship('Follow',
  84. foreign_keys=[Follow.follower_id],
  85. backref=db.backref('follower', lazy='joined'),
  86. lazy='dynamic',
  87. cascade='all, delete-orphan')
  88. followers = db.relationship('Follow',
  89. foreign_keys=[Follow.followed_id],
  90. backref=db.backref('followed', lazy='joined'),
  91. lazy='dynamic',
  92. cascade='all, delete-orphan')
  93. comments = db.relationship('Comment', backref='author', lazy='dynamic')
  94. def __init__(self, **kwargs):
  95. super(User, self).__init__(**kwargs)
  96. if self.role is None:
  97. if self.email == current_app.config['FLASKY_ADMIN']:
  98. self.role = Role.query.filter_by(name='Administrator').first()
  99. else:
  100. self.role = Role.query.filter_by(default=True).first()
  101. if self.email is not None and self.avatar_hash is None:
  102. self.avatar_hash = self.gravatar_hash()
  103. self.follow(self)
  104. @property
  105. def password(self):
  106. raise AttributeError('Password is not a readable attribute')
  107. @password.setter
  108. def password(self, password):
  109. self.password_hash = generate_password_hash(password)
  110. def verify_password(self, password):
  111. return check_password_hash(self.password_hash, password)
  112. def generate_confirmation_token(self, expiration=3600):
  113. s = Serializer(current_app.config['SECRET_KEY'], expiration)
  114. return s.dumps({'confirm': self.id}).decode('utf-8')
  115. def confirm(self, token):
  116. s = Serializer(current_app.config['SECRET_KEY'])
  117. try:
  118. data = s.loads(token.encode('utf-8'))
  119. except BadSignature:
  120. return False
  121. if data.get('confirm') != self.id:
  122. return False
  123. self.confirmed = True
  124. db.session.add(self)
  125. return True
  126. def generate_reset_token(self, expiration=3600):
  127. s = Serializer(current_app.config['SECRET_KEY'], expiration)
  128. return s.dumps({'reset': self.id}).decode('utf-8')
  129. @staticmethod
  130. def reset_password(token, new_password):
  131. s = Serializer(current_app.config['SECRET_KEY'])
  132. try:
  133. data = s.loads(token.encode('utf-8'))
  134. except BadSignature:
  135. return False
  136. user = User.query.get(data.get('reset'))
  137. if user is None:
  138. return False
  139. user.password = new_password
  140. db.session.add(user)
  141. return True
  142. def generate_email_change_token(self, new_email, expiration=3600):
  143. s = Serializer(current_app.config['SECRET_KEY'], expiration)
  144. return s.dumps(
  145. {'change_email': self.id, 'new_email': new_email}).decode('utf-8')
  146. def change_email(self, token):
  147. s = Serializer(current_app.config['SECRET_KEY'])
  148. try:
  149. data = s.loads(token.encode('utf-8'))
  150. except BadSignature:
  151. return False
  152. if data.get('change_email') != self.id:
  153. return False
  154. new_email = data.get('new_email')
  155. if new_email is None:
  156. return False
  157. if self.query.filter_by(email=new_email).first() is not None:
  158. return False
  159. self.email = new_email
  160. self.gravatar_hash = self.gravatar_hash()
  161. db.session.add(self)
  162. return True
  163. def can(self, perm):
  164. return self.role is not None and self.role.has_permission(perm)
  165. def is_administrator(self):
  166. return self.can(Permission.ADMIN)
  167. def ping(self):
  168. self.last_seen = datetime.utcnow()
  169. db.session.add(self)
  170. db.session.commit()
  171. def gravatar_hash(self):
  172. return hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()
  173. def gravatar(self, size=100, default='identicon', rating='g'):
  174. url = 'https://secure.gravatar.com/avatar'
  175. hash = self.avatar_hash or self.gravatar_hash()
  176. return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
  177. url=url, hash=hash, size=size, default=default, rating=rating)
  178. def follow(self, user):
  179. if not self.is_following(user):
  180. f = Follow(follower=self, followed=user)
  181. db.session.add(f)
  182. def unfollow(self, user):
  183. f = self.followed.filter_by(followed_id=user.id).first()
  184. if f:
  185. db.session.delete(f)
  186. def is_following(self, user):
  187. if user.id is None:
  188. return False
  189. return self.followed.filter_by(
  190. followed_id=user.id).first() is not None
  191. def is_followed_by(self, user):
  192. if user.id is None:
  193. return False
  194. return self.followers.filter_by(
  195. follower_id=user.id).first() is not None
  196. @property
  197. def followed_posts(self):
  198. return Post.query.join(Follow, Follow.followed_id == Post.author_id) \
  199. .filter(Follow.follower_id == self.id)
  200. @staticmethod
  201. def add_self_follows():
  202. for user in User.query.all():
  203. if not user.is_following(user):
  204. user.follow(user)
  205. db.session.add(user)
  206. db.session.commit()
  207. def __repr__(self):
  208. return '<User %r>' % self.username
  209. class Post(db.Model):
  210. __tablename__ = 'posts'
  211. id = db.Column(db.Integer, primary_key=True)
  212. body = db.Column(db.Text)
  213. body_html = db.Column(db.Text)
  214. timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
  215. author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
  216. comments = db.relationship('Comment', backref='post', lazy='dynamic')
  217. @staticmethod
  218. def on_changed_body(target, value, oldvalue, initiator):
  219. allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code',
  220. 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul',
  221. 'h1', 'h2', 'h3', 'p']
  222. md = markdown(value, output_format='html')
  223. clean_md = bleach.clean(md, tags=allowed_tags, strip=True)
  224. target.body_html = bleach.linkify(clean_md)
  225. db.event.listen(Post.body, 'set', Post.on_changed_body)
  226. class Comment(db.Model):
  227. __tablename__ = 'comments'
  228. id = db.Column(db.Integer, primary_key=True)
  229. body = db.Column(db.Text)
  230. body_html = db.Column(db.Text)
  231. timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
  232. disabled = db.Column(db.Boolean)
  233. author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
  234. post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))
  235. @staticmethod
  236. def on_changed_body(target, value, oldvalue, initiator):
  237. allowed_tags = ['a', 'abbr', 'acronym', 'b', 'code', 'em', 'i',
  238. 'strong']
  239. md = markdown(value, output_format='html')
  240. clean_md = bleach.clean(md, tags=allowed_tags, strip=True)
  241. target.body_html = bleach.linkify(clean_md)
  242. db.event.listen(Comment.body, 'set', Comment.on_changed_body)
  243. class AnonymousUser(AnonymousUserMixin):
  244. def can(self, perm):
  245. return False
  246. def is_administrator(self):
  247. return False
  248. login_manager.anonymous_user = AnonymousUser
  249. @login_manager.user_loader
  250. def load_user(user_id):
  251. return User.query.get(int(user_id))