From 2f5659e6aca137aa963ccf3bc035d097edb81cbf Mon Sep 17 00:00:00 2001 From: Amitosh Swain Mahapatra Date: Sun, 31 Jan 2021 01:12:30 +0530 Subject: [PATCH 01/10] WIP: Add appreciations API --- snowflake/acl/__init__.py | 9 +- snowflake/acl/appreciations.py | 13 ++ snowflake/app.py | 1 + snowflake/controllers/api/__init__.py | 1 + snowflake/controllers/api/appreciations.py | 142 +++++++++++++++++++++ snowflake/models/appreciation.py | 22 +++- snowflake/models/comment.py | 3 +- snowflake/models/like.py | 10 +- snowflake/schemas/appreciation.py | 41 ++++++ snowflake/templates/home.html | 4 +- tests/schemas/test_appreciation.py | 94 ++++++++++++++ 11 files changed, 327 insertions(+), 13 deletions(-) create mode 100644 snowflake/acl/appreciations.py create mode 100644 snowflake/controllers/api/appreciations.py create mode 100644 snowflake/schemas/appreciation.py create mode 100644 tests/schemas/test_appreciation.py diff --git a/snowflake/acl/__init__.py b/snowflake/acl/__init__.py index c460b23..fd752e7 100644 --- a/snowflake/acl/__init__.py +++ b/snowflake/acl/__init__.py @@ -1,3 +1,10 @@ +from .appreciations import can_create_appreciations, can_view_appreciations, can_view_appreciation from .one_on_one import can_view_one_on_one, can_delete_one_on_one -__all__ = ['can_view_one_on_one', 'can_delete_one_on_one'] +__all__ = [ + 'can_view_one_on_one', + 'can_delete_one_on_one', + 'can_view_appreciations', + 'can_view_appreciation', + 'can_create_appreciations' +] diff --git a/snowflake/acl/appreciations.py b/snowflake/acl/appreciations.py new file mode 100644 index 0000000..e906d43 --- /dev/null +++ b/snowflake/acl/appreciations.py @@ -0,0 +1,13 @@ +from ..models import Appreciation + + +def can_view_appreciations(): + return True + + +def can_create_appreciations(): + return True + + +def can_view_appreciation(_: Appreciation): + return True diff --git a/snowflake/app.py b/snowflake/app.py index 262c1c9..40c3ee1 100644 --- a/snowflake/app.py +++ b/snowflake/app.py @@ -24,6 +24,7 @@ app.register_blueprint(api.healthcheck.blueprint, url_prefix="/api/healthcheck") app.register_blueprint(index.blueprint) +app.register_blueprint(api.appreciations.blueprint, url_prefix="/api/appreciations") app.register_blueprint(api.notifications.blueprint, url_prefix="/api/notifications") app.register_blueprint(api.one_on_ones.blueprint, url_prefix="/api/one_on_ones") app.register_blueprint(api.token.blueprint, url_prefix="/api/tokens") diff --git a/snowflake/controllers/api/__init__.py b/snowflake/controllers/api/__init__.py index 143b82d..8e6b4a4 100644 --- a/snowflake/controllers/api/__init__.py +++ b/snowflake/controllers/api/__init__.py @@ -1,3 +1,4 @@ +from . import appreciations from . import healthcheck from . import notifications from . import one_on_ones diff --git a/snowflake/controllers/api/appreciations.py b/snowflake/controllers/api/appreciations.py new file mode 100644 index 0000000..b5e1aaa --- /dev/null +++ b/snowflake/controllers/api/appreciations.py @@ -0,0 +1,142 @@ +import re +from datetime import datetime + +from flask import Blueprint, request +from flask_login import login_required, current_user + +from .response import bad_request, not_found, unauthorized +from ... import acl +from ...models import Appreciation, Like, User, Mention +from ...schemas.appreciation import AppreciationSchema, CreateAppreciationSchema +from ...services import notification + +blueprint = Blueprint('api.appreciations', __name__) + +appreciation_schema = AppreciationSchema() +create_appreciation_schema = CreateAppreciationSchema() + + +def get_appreciation_viewer_like(appreciation: Appreciation): + return Like.get_by_appreciation_and_user(appreciation, current_user) + + +def appreciation_view(appreciation: Appreciation): + return { + 'id': appreciation.id, + 'content': appreciation.content, + 'created_at': appreciation.created_at, + 'created_by': appreciation.created_by, + 'like_count': appreciation.like_count, + 'comment_count': appreciation.comment_count, + 'mentions': appreciation.mentions, + + 'viewer_like': get_appreciation_viewer_like(appreciation), + } + + +@login_required +@blueprint.route('', methods=['GET']) +def list_all_appreciations(): + if not acl.can_view_appreciations(): + return unauthorized() + + formatted_appreciations = [appreciation_view(a) for a in Appreciation.get_all()] + return appreciation_schema.jsonify(formatted_appreciations, many=True) + + +@login_required +@blueprint.route('', methods=['PUT']) +def create_appreciation(): + if not request.is_json: + return bad_request() + + if not acl.can_create_appreciations(): + return unauthorized() + + appreciation: Appreciation = create_appreciation_schema.load(request.json) + + appreciation.created_by = current_user + appreciation.created_at = datetime.now() + + Appreciation.create(appreciation) + + mentions = re.findall(r'@[a-zA-Z0-9._]+', appreciation.content) + + for mention_text in mentions: + user = User.get_by_username(mention_text[1:]) + if user is None: + continue + mention = Mention(user=user, appreciation=appreciation) + Mention.create(mention) + + notification.notify_appreciation(appreciation) + + return appreciation_schema.jsonify(appreciation) + + +@login_required +@blueprint.route('/<_id>', methods=['GET']) +def get_appreciation(_id): + appreciation = Appreciation.get(_id) + + if not appreciation: + return not_found() + + if not acl.can_view_appreciation(appreciation): + return unauthorized() + + return appreciation_schema.jsonify(appreciation) + + +@login_required +@blueprint.route('/<_id>', methods=['PATCH']) +def update_appreciation(_id): + return not_found() + + +@login_required +@blueprint.route('/<_id>', methods=['DELETE']) +def delete_appreciation(_id): + pass + + +@login_required +@blueprint.route('//likes', methods=['GET']) +def get_appreciation_likes(appreciation_id): # pylint: disable=unused-argument + pass + + +@login_required +@blueprint.route('//likes', methods=['PUT']) +def like(appreciation_id): # pylint: disable=unused-argument + pass + + +@login_required +@blueprint.route('//likes/', methods=['DELETE']) +def delete_like(appreciation_id, like_id): # pylint: disable=unused-argument + pass + + +@login_required +@blueprint.route('//comments', methods=['GET']) +def get_comments(appreciation_id): # pylint: disable=unused-argument + pass + + +@login_required +@blueprint.route('//comments', methods=['GET']) +def create_comment(appreciation_id): # pylint: disable=unused-argument + pass + + +@login_required +@blueprint.route('//comments/', methods=['GET']) +def update_comment(appreciation_id, comment_id): # pylint: disable=unused-argument + pass + + +@login_required +@blueprint.route('//comments/', methods=['GET']) +def delete_comment(appreciation_id, comment_id): # pylint: disable=unused-argument + pass diff --git a/snowflake/models/appreciation.py b/snowflake/models/appreciation.py index a292b1e..beab51c 100644 --- a/snowflake/models/appreciation.py +++ b/snowflake/models/appreciation.py @@ -1,5 +1,6 @@ from datetime import datetime +from .comment import Comment from .like import Like from .user import User from ..db import db @@ -21,6 +22,14 @@ class Appreciation(db.Model): def creator(self): return self.created_by + @property + def like_count(self): + return Like.query.filter_by(appreciation=self).count() + + @property + def comment_count(self): + return Comment.query.filter_by(appreciation=self).count() + @staticmethod def create(appreciation): db.session.add(appreciation) @@ -31,11 +40,10 @@ def get_all(): return Appreciation.query.order_by(Appreciation.created_at.desc()).all() def get_like_count(self): - return Like.query.filter_by(appreciation=self).count() + return self.like_count def get_comment_count(self): - return db.session.scalar('SELECT COUNT(*) FROM comment c WHERE c.appreciation_id = :id', - {'id': self.id}) + return self.comment_count def is_liked_by(self, user: User): return db.session.scalar( @@ -46,12 +54,15 @@ def is_liked_by(self, user: User): {'appreciation_id': self.id, 'user_id': user.id}) > 0 @staticmethod - def get(id_): + def get(id_) -> 'Appreciation': return Appreciation.query.get(id_) def get_mentions(self): return self.mentions + def get_comments(self): + return self.comments + @staticmethod def count_by_user(user: User): return Appreciation.query.filter_by(created_by=user).count() @@ -80,6 +91,3 @@ def most_appreciated(): }) return result - - def get_comments(self): - return self.comments diff --git a/snowflake/models/comment.py b/snowflake/models/comment.py index 78204ca..e2907a8 100644 --- a/snowflake/models/comment.py +++ b/snowflake/models/comment.py @@ -1,6 +1,5 @@ from datetime import datetime -from .appreciation import Appreciation from ..db import db @@ -13,7 +12,7 @@ class Comment(db.Model): created_by = db.relationship('User') appreciation_id = db.Column(db.BigInteger, db.ForeignKey('appreciation.id'), nullable=False) - appreciation: Appreciation = db.relationship('Appreciation') + appreciation = db.relationship('Appreciation') @staticmethod def create(comment): diff --git a/snowflake/models/like.py b/snowflake/models/like.py index 51d8672..a5e5f70 100644 --- a/snowflake/models/like.py +++ b/snowflake/models/like.py @@ -1,7 +1,11 @@ from datetime import datetime +from typing import TYPE_CHECKING from ..db import db +if TYPE_CHECKING: + from . import Appreciation + class Like(db.Model): id = db.Column(db.BigInteger, primary_key=True) @@ -12,7 +16,7 @@ class Like(db.Model): created_by = db.relationship('User', backref=db.backref('likes', lazy=True)) appreciation_id = db.Column(db.String, db.ForeignKey('appreciation.id'), nullable=False) - appreciation = db.relationship('Appreciation') + appreciation: 'Appreciation' = db.relationship('Appreciation') @staticmethod def create(like): @@ -28,3 +32,7 @@ def dislike(appreciation, user): like = Like.query.filter_by(appreciation_id=appreciation.id, user_id=user.id).first() db.session.delete(like) db.session.commit() + + @staticmethod + def get_by_appreciation_and_user(appreciation, user): + return Like.query.filter_by(appreciation=appreciation, user=user).first() diff --git a/snowflake/schemas/appreciation.py b/snowflake/schemas/appreciation.py new file mode 100644 index 0000000..1c8fc82 --- /dev/null +++ b/snowflake/schemas/appreciation.py @@ -0,0 +1,41 @@ +from marshmallow.fields import Integer, String, DateTime, List +from marshmallow_sqlalchemy.fields import Nested + +from .base import BaseSQLAlchemySchema, BaseSchema +from ..marshmallow import marshmallow +from ..models import Like +from ..schemas.user import UserSchema + + +class LikeSchema(BaseSQLAlchemySchema): + class Meta: + model = Like + + id = marshmallow.auto_field() + created_by = Nested(UserSchema) + + +class MentionSchema(BaseSchema): + user = Nested(UserSchema) + + +class AppreciationSchema(BaseSchema): + id = Integer() + content = String() + created_at = DateTime() + + created_by = Nested(UserSchema) + + like_count = Integer() + comment_count = Integer() + viewer_like = Nested(LikeSchema, exclude=("user",)) + + mentions = List(Nested(MentionSchema)) + + +class CreateAppreciationSchema(BaseSchema): + class Meta: + model = True + load_instance = True + + content = String() diff --git a/snowflake/templates/home.html b/snowflake/templates/home.html index 2fea312..dd4ae80 100644 --- a/snowflake/templates/home.html +++ b/snowflake/templates/home.html @@ -92,7 +92,7 @@ class="clear-button has-text-danger level-item is-clickable" data-toggle-modal="#likes-{{ appreciation.id }}"> - {% set like_count = appreciation.get_like_count() %} + {% set like_count = appreciation.like_count %} {{ like_count }} {{ choose_plural(like_count, 'like', 'likes') }} @@ -103,7 +103,7 @@ - {{ appreciation.get_comment_count() }} + {{ appreciation.comment_count }}