Skip to content

Commit 184b0cc

Browse files
committed
Add one on one API
1 parent 9b37215 commit 184b0cc

File tree

9 files changed

+293
-5
lines changed

9 files changed

+293
-5
lines changed

snowflake/acl/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .one_on_one import *

snowflake/acl/one_on_one.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from flask_login import current_user
2+
3+
from snowflake.models import OneOnOne, User
4+
5+
6+
def can_view_one_on_one(one_on_one: OneOnOne, user: User = current_user):
7+
return user.id == one_on_one.user_id or user.id == one_on_one.created_by_id
8+
9+
10+
def can_delete_one_on_one(one_on_one: OneOnOne, user: User = current_user):
11+
return can_view_one_on_one(one_on_one, user)

snowflake/app.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@
2424

2525
app.register_blueprint(api.healthcheck.blueprint, url_prefix="/api/healthcheck")
2626
app.register_blueprint(index.blueprint)
27+
28+
app.register_blueprint(api.notifications.blueprint, url_prefix="/api/notifications")
29+
app.register_blueprint(api.one_on_ones.blueprint, url_prefix="/api/one_on_ones")
2730
app.register_blueprint(api.token.blueprint, url_prefix="/api/tokens")
2831
app.register_blueprint(api.users.blueprint, url_prefix="/api/users")
29-
app.register_blueprint(api.notifications.blueprint, url_prefix="/api/notifications")
32+
3033
app.register_blueprint(login.blueprint, url_prefix="/login")
3134
app.register_blueprint(register.blueprint, url_prefix="/register")
3235
app.register_blueprint(profile.blueprint, url_prefix="/profile")
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from . import users
1+
from . import healthcheck
22
from . import notifications
3+
from . import one_on_ones
34
from . import token
4-
from . import healthcheck
5+
from . import users
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
from datetime import datetime
2+
3+
from flask import Blueprint, request
4+
from flask_login import login_required, current_user
5+
from marshmallow import ValidationError
6+
7+
from .response import not_found, unauthorized, no_content, bad_request, validation_error
8+
from ... import acl
9+
from ...db import transaction, db
10+
from ...models import OneOnOne, OneOnOneActionItem
11+
from ...schemas.one_on_one import OneOnOneSchema, GetOneOnOneSchema, CreateOneOnOneSchema, OneOnOneActionItemSchema, \
12+
CreateOrEditOneOnOneActionItemSchema
13+
14+
blueprint = Blueprint('api.one_on_ones', __name__)
15+
16+
one_on_one_schema = OneOnOneSchema()
17+
one_on_one_action_item_schema = OneOnOneActionItemSchema()
18+
get_one_on_one_schema = GetOneOnOneSchema()
19+
create_one_on_one_schema = CreateOneOnOneSchema()
20+
create_or_edit_one_on_one_action_item_schema = CreateOrEditOneOnOneActionItemSchema()
21+
22+
23+
@blueprint.route('', methods=['GET'])
24+
@login_required
25+
def list_all():
26+
return one_on_one_schema.jsonify(OneOnOne.get_by_user(current_user), many=True)
27+
28+
29+
@blueprint.route('', methods=['PUT'])
30+
@login_required
31+
def create():
32+
if not request.is_json:
33+
return bad_request()
34+
35+
try:
36+
one_on_one: OneOnOne = create_one_on_one_schema.load(request.json)
37+
one_on_one.created_by = current_user
38+
one_on_one.created_at = datetime.now()
39+
40+
with transaction():
41+
OneOnOne.create(one_on_one)
42+
43+
return get_one_on_one_schema.jsonify(one_on_one)
44+
except ValidationError as e:
45+
return validation_error(e.messages)
46+
47+
48+
@blueprint.route('/<_id>', methods=['GET'])
49+
@login_required
50+
def get(_id: int):
51+
one_on_one = OneOnOne.get(_id)
52+
53+
if not one_on_one:
54+
return not_found()
55+
56+
if not acl.can_view_one_on_one(one_on_one):
57+
return unauthorized()
58+
59+
return get_one_on_one_schema.jsonify(one_on_one)
60+
61+
62+
@blueprint.route('/<one_on_one_id>/action_items', methods=['GET'])
63+
@login_required
64+
def get_action_items(one_on_one_id: int):
65+
one_on_one = OneOnOne.get(one_on_one_id)
66+
67+
if not one_on_one:
68+
return not_found()
69+
70+
if not acl.can_view_one_on_one(one_on_one):
71+
return unauthorized()
72+
73+
return one_on_one_action_item_schema.jsonify(one_on_one.action_items, many=True)
74+
75+
76+
@blueprint.route('/<one_on_one_id>/action_items/<action_item_id>', methods=['GET'])
77+
@login_required
78+
def get_action_item(one_on_one_id: int, action_item_id: int):
79+
one_on_one = OneOnOne.get(one_on_one_id)
80+
81+
if not one_on_one:
82+
return not_found()
83+
84+
if not acl.can_view_one_on_one(one_on_one):
85+
return unauthorized()
86+
87+
action_item = OneOnOneActionItem.get(action_item_id)
88+
89+
if not action_item:
90+
return not_found()
91+
92+
if not action_item.one_on_one_id == one_on_one.id:
93+
return not_found()
94+
95+
return one_on_one_action_item_schema.jsonify(action_item)
96+
97+
98+
@blueprint.route('/<one_on_one_id>/action_items/<action_item_id>', methods=['DELETE'])
99+
@login_required
100+
def delete_action_item(one_on_one_id: int, action_item_id: int):
101+
one_on_one = OneOnOne.get(one_on_one_id)
102+
103+
if not one_on_one:
104+
return not_found()
105+
106+
if not acl.can_view_one_on_one(one_on_one):
107+
return unauthorized()
108+
109+
action_item = OneOnOneActionItem.get(action_item_id)
110+
111+
if not action_item:
112+
return not_found()
113+
114+
if not action_item.one_on_one_id == one_on_one.id:
115+
return not_found()
116+
117+
with transaction():
118+
db.session.remove(one_on_one)
119+
120+
return no_content()
121+
122+
123+
@blueprint.route('/<one_on_one_id>/action_items', methods=['PUT'])
124+
@login_required
125+
def create_action_item(one_on_one_id: int):
126+
if not request.is_json:
127+
return bad_request()
128+
129+
one_on_one = OneOnOne.get(one_on_one_id)
130+
131+
if not one_on_one:
132+
return not_found()
133+
134+
if not acl.can_view_one_on_one(one_on_one):
135+
return unauthorized()
136+
137+
try:
138+
action_item: OneOnOneActionItem = create_or_edit_one_on_one_action_item_schema.load(request.json)
139+
action_item.one_on_one = one_on_one
140+
action_item.created_by = current_user
141+
action_item.created_at = datetime.now()
142+
143+
with transaction():
144+
OneOnOneActionItem.create(action_item)
145+
146+
return one_on_one_action_item_schema.jsonify(action_item)
147+
except ValidationError as e:
148+
return validation_error(e.messages)
149+
150+
151+
@blueprint.route('/<one_on_one_id>/action_items/<action_item_id>', methods=['PATCH'])
152+
@login_required
153+
def edit_action_item(one_on_one_id: int, action_item_id: int):
154+
if not request.is_json:
155+
return bad_request()
156+
157+
one_on_one = OneOnOne.get(one_on_one_id)
158+
159+
if not one_on_one:
160+
return not_found()
161+
162+
if not acl.can_view_one_on_one(one_on_one):
163+
return unauthorized()
164+
165+
action_item = OneOnOneActionItem.get(action_item_id)
166+
167+
try:
168+
action_item: OneOnOneActionItem = create_or_edit_one_on_one_action_item_schema.load(request.json, action_item)
169+
170+
with transaction():
171+
db.session.add(action_item)
172+
173+
return one_on_one_action_item_schema.jsonify(action_item)
174+
except ValidationError as e:
175+
return validation_error(e.messages)
176+
177+
178+
@blueprint.route('/<_id>', methods=['DELETE'])
179+
@login_required
180+
def delete_one_on_one(_id: int):
181+
one_on_one = OneOnOne.get(_id)
182+
183+
if not one_on_one:
184+
return not_found()
185+
186+
if not acl.can_delete_one_on_one(one_on_one):
187+
return unauthorized()
188+
189+
with transaction():
190+
db.session.remove(one_on_one)
191+
192+
return no_content()

snowflake/controllers/api/response.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ def bad_request(message="Bad request"):
99
return error_body(message), 400
1010

1111

12+
def validation_error(messages):
13+
return jsonify({
14+
'message': 'Validation error',
15+
'errors': messages
16+
}), 400
17+
18+
1219
def not_found(message="Not found"):
1320
return error_body(message), 404
1421

@@ -19,3 +26,7 @@ def forbidden(message="Forbidden"):
1926

2027
def unauthorized(message="Unauthorized"):
2128
return error_body(message), 401
29+
30+
31+
def no_content():
32+
return '', 204

snowflake/models/one_on_one_action_item.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
class OneOnOneActionItem(db.Model):
55
id = db.Column(db.BigInteger, primary_key=True)
6-
state = db.Column(db.Boolean)
6+
state = db.Column(db.Boolean, default=False)
77
content = db.Column(db.String)
88

99
one_on_one_id = db.Column(db.BigInteger, db.ForeignKey('one_on_one.id'), nullable=False)
@@ -22,5 +22,5 @@ def update(self):
2222
db.session.commit()
2323

2424
@staticmethod
25-
def get(_id):
25+
def get(_id) -> 'OneOnOneActionItem':
2626
return OneOnOneActionItem.query.get(_id)

snowflake/schemas/fields.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from marshmallow import ValidationError
2+
from marshmallow.fields import Field
3+
4+
from ..models import User
5+
6+
7+
class UserByUsername(Field):
8+
def _deserialize(self, value: str, attr, data, **kwargs):
9+
user = User.get_by_username(value)
10+
11+
if not user:
12+
raise ValidationError(f"User {value} not found")
13+
14+
return user
15+
16+
def _serialize(self, value: User, attr, obj, **kwargs):
17+
if value is None:
18+
return None
19+
20+
return value.username

snowflake/schemas/one_on_one.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from marshmallow.fields import List
2+
from marshmallow_sqlalchemy.fields import Nested
3+
4+
from .fields import UserByUsername
5+
from .user import UserSchema
6+
from ..marshmallow import marshmallow
7+
from ..models import OneOnOne, OneOnOneActionItem
8+
9+
10+
class OneOnOneActionItemSchema(marshmallow.SQLAlchemySchema):
11+
class Meta:
12+
model = OneOnOneActionItem
13+
14+
id = marshmallow.auto_field()
15+
state = marshmallow.auto_field()
16+
content = marshmallow.auto_field()
17+
18+
created_by = Nested(UserSchema)
19+
20+
21+
class CreateOrEditOneOnOneActionItemSchema(marshmallow.SQLAlchemySchema):
22+
class Meta:
23+
model = OneOnOneActionItem
24+
load_instance = True
25+
26+
state = marshmallow.auto_field()
27+
content = marshmallow.auto_field()
28+
29+
30+
class OneOnOneSchema(marshmallow.SQLAlchemySchema):
31+
class Meta:
32+
model = OneOnOne
33+
34+
id = marshmallow.auto_field()
35+
created_at = marshmallow.auto_field()
36+
created_by = Nested(UserSchema)
37+
user = Nested(UserSchema)
38+
39+
40+
class GetOneOnOneSchema(OneOnOneSchema):
41+
action_items = List(Nested(OneOnOneActionItemSchema))
42+
43+
44+
class CreateOneOnOneSchema(marshmallow.SQLAlchemySchema):
45+
class Meta:
46+
model = OneOnOne
47+
load_instance = True
48+
49+
user = UserByUsername()

0 commit comments

Comments
 (0)