Skip to content

Commit

Permalink
Merge 0ce531c into 6efe7c5
Browse files Browse the repository at this point in the history
  • Loading branch information
mebesius committed Mar 23, 2019
2 parents 6efe7c5 + 0ce531c commit b841541
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 50 deletions.
23 changes: 17 additions & 6 deletions bounce/db/club.py
Expand Up @@ -64,6 +64,16 @@ def can_update(editor_role):
return editor_role in [Roles.president.value, Roles.admin.value]


def select_by_club_id(session, identifier):
"""
Returns the club with the given identifier or None if
there is no such club.
"""
# Anyone should be able to read info on the club (including non-members)
club = session.query(Club).filter(Club.identifier == identifier).first()
return None if club is None else club.to_dict()


def select(session, name):
"""
Returns the club with the given name or None if
Expand Down Expand Up @@ -115,16 +125,17 @@ def insert(session, name, description, website_url, facebook_url,
twitter_url=twitter_url)
session.add(club)
session.commit()
return club


def update(session, name, editors_role, new_name, description, website_url,
facebook_url, instagram_url, twitter_url):
def update(session, identifier, editors_role, new_name, description,
website_url, facebook_url, instagram_url, twitter_url):
"""Updates an existing club in the Clubs table and returns the
updated club."""
# Only Presidents and Admins can update
if not can_update(editors_role):
raise PermissionError("Permission denied for updating the club.")
club = session.query(Club).filter(Club.name == name).first()
club = session.query(Club).filter(Club.identifier == identifier).first()
if new_name:
club.name = new_name
if description:
Expand All @@ -141,11 +152,11 @@ def update(session, name, editors_role, new_name, description, website_url,
return club.to_dict()


def delete(session, name, editors_role):
"""Deletes the club with the given name."""
def delete(session, identifier, editors_role):
"""Deletes the club with the given identifier."""
# Only Presidents can delete
if not can_delete(editors_role):
raise PermissionError("Permission denied for deleting the club.")

session.query(Club).filter(Club.name == name).delete()
session.query(Club).filter(Club.identifier == identifier).delete()
session.commit()
34 changes: 34 additions & 0 deletions bounce/db/membership.py
Expand Up @@ -214,6 +214,40 @@ def select_all(session, club_name, editors_role):
return results


def select_by_club_id(session, club_id, user_id, editors_role):
"""
Returns the membership for the given user of the specified club.
"""
# All members can read all memberships
if not can_select(editors_role):
raise PermissionError('Permission denied for selecting membership.')

query = f"""
SELECT users.id AS user_id,
memberships.created_at, memberships.position,
memberships.role, users.full_name, users.username
FROM memberships INNER JOIN users ON (
memberships.user_id = users.id
)
WHERE memberships.club_id IN (
SELECT id FROM clubs WHERE id = :club_id
)
AND user_id = :user_id
"""
result_proxy = session.execute(query, {
'club_id': club_id,
'user_id': user_id,
})
results = []
for row in result_proxy.fetchall():
member_info = {}
for i, key in enumerate(result_proxy.keys()):
# for i, key in row:
member_info[key] = row[i]
results.append(member_info)
return results


def select(session, club_name, user_id, editors_role):
"""
Returns returns the membership for the given user of the specified club.
Expand Down
53 changes: 26 additions & 27 deletions bounce/server/api/clubs.py
Expand Up @@ -17,38 +17,38 @@


class ClubEndpoint(Endpoint):
"""Handles requests to /clubs/<name>."""
"""Handles requests to /clubs/<identifier>."""

__uri__ = "/clubs/<name:string>"
__uri__ = "/clubs/<identifier:string>"

@validate(None, GetClubResponse)
async def get(self, session, _, name):
"""Handles a GET /clubs/<name> request by returning the club with
the given name."""
# Decode the name, since special characters will be URL-encoded
name = unquote(name)
async def get(self, session, _, identifier):
"""Handles a GET /clubs/<identifier> request by returning the club with
the given identifier."""
# Decode the identifier, since special characters will be URL-encoded
identifier = unquote(identifier)
# Fetch club data from DB
club_data = club.select(session, name)
club_data = club.select_by_club_id(session, identifier)
if not club_data:
# Failed to find a club with that name
raise APIError('No such club', status=404)
return response.json(club_data, status=200)

@verify_token()
@validate(PutClubRequest, GetClubResponse)
async def put(self, session, request, name, id_from_token=None):
"""Handles a PUT /clubs/<name> request by updating the club with
async def put(self, session, request, identifier, id_from_token=None):
"""Handles a PUT /clubs/<identifier> request by updating the club with
the given name and returning the updated club info."""
# Decode the name, since special characters will be URL-encoded
name = unquote(name)
# Decode the identifier, since special characters will be URL-encoded
identifier = unquote(identifier)
body = util.strip_whitespace(request.json)
try:
editor_attr = membership.select(session, name, id_from_token,
Roles.president.value)
editor_attr = membership.select_by_club_id(
session, identifier, id_from_token, Roles.president.value)
editors_role = editor_attr[0]['role']
updated_club = club.update(
session,
name,
identifier,
editors_role,
new_name=body.get('name', None),
description=body.get('description', None),
Expand All @@ -62,17 +62,16 @@ async def put(self, session, request, name, id_from_token=None):

@verify_token()
@validate(DeleteClubRequest, None)
async def delete(self, session, _, name, id_from_token=None):
"""Handles a DELETE /clubs/<name> request by deleting the club with
the given name."""
# Decode the name, since special characters will be URL-encoded

name = unquote(name)
async def delete(self, session, _, identifier, id_from_token=None):
"""Handles a DELETE /clubs/<identifier> request by deleting the club with
the given identifier."""
# Decode the identifier, since special characters will be URL-encoded
identifier = unquote(identifier)
try:
membership_attr = membership.select(session, name, id_from_token,
Roles.president.value)
membership_attr = membership.select_by_club_id(
session, identifier, id_from_token, Roles.president.value)
editors_role = membership_attr[0]['role']
club.delete(session, name, editors_role)
club.delete(session, identifier, editors_role)
except PermissionError:
raise APIError('Forbidden', status=403)
return response.text('', status=204)
Expand All @@ -84,13 +83,13 @@ class ClubsEndpoint(Endpoint):
__uri__ = '/clubs'

@verify_token()
@validate(PostClubsRequest, None)
@validate(PostClubsRequest, GetClubResponse)
async def post(self, session, request, id_from_token=None):
"""Handles a POST /clubs request by creating a new club."""
# Put the club in the DB
body = util.strip_whitespace(request.json)
try:
club.insert(
club_row = club.insert(
session,
name=body.get('name', None),
description=body.get('description', None),
Expand All @@ -104,7 +103,7 @@ async def post(self, session, request, id_from_token=None):
membership.insert(session, body.get('name', None), id_from_token,
Roles.president.value, Roles.president.value,
'Owner')
return response.text('', status=201)
return response.json(club_row.to_dict(), status=201)


class SearchClubsEndpoint(Endpoint):
Expand Down
14 changes: 7 additions & 7 deletions requirements.txt
Expand Up @@ -9,17 +9,17 @@ bcrypt==3.1.4
cffi==1.11.5 # via bcrypt
click==6.7
ecdsa==0.13 # via python-jose
future==0.16.0 # via python-jose
future==0.17.1 # via python-jose
httptools==0.0.11 # via sanic
jsonschema==2.6.0
psycopg2==2.7.4
pyasn1==0.4.4 # via rsa
pycparser==2.18 # via cffi
pyasn1==0.4.5 # via rsa
pycparser==2.19 # via cffi
python-jose==3.0.0
rsa==3.4.2 # via python-jose
rsa==4.0 # via python-jose
sanic==0.7.0
six==1.11.0 # via bcrypt, python-jose
six==1.12.0 # via bcrypt, python-jose
sqlalchemy==1.2.8
ujson==1.35 # via sanic
uvloop==0.11.2 # via sanic
websockets==6.0 # via sanic
uvloop==0.12.0 # via sanic
websockets==7.0 # via sanic
19 changes: 9 additions & 10 deletions tests/api/endpoints/test_2_clubs.py
Expand Up @@ -63,10 +63,9 @@ def test_put_club__success(server):
_, response = server.app.test_client.get('/users/founder')
founder_id = response.json['id']
founder_token = util.create_jwt(founder_id, server.config.secret)

# test if the club is successfully edited by President
_, response = server.app.test_client.put(
'/clubs/test',
'/clubs/1',
data=json.dumps({
'name': 'newtest',
'description': 'club with a new description',
Expand Down Expand Up @@ -104,7 +103,7 @@ def test_put_club__success(server):

# test if the club is successfully edited by Admin
_, response = server.app.test_client.put(
'/clubs/newtest',
'/clubs/1',
data=json.dumps({
'name': 'newtest',
'description': 'club with a newer description',
Expand All @@ -122,10 +121,9 @@ def test_put_club__failure(server):
_, response = server.app.test_client.get('/users/founder')
user_id = response.json['id']
token = util.create_jwt(user_id, server.config.secret)

# bad json data
_, response = server.app.test_client.put(
'/clubs/newtest',
'/clubs/1',
data=json.dumps({
'garbage': True
}),
Expand Down Expand Up @@ -165,7 +163,7 @@ def test_put_club__failure(server):
# now try editing the club with the Member membership
token = util.create_jwt(user_id, server.config.secret)
_, response = server.app.test_client.put(
'/clubs/newtest',
'/clubs/1',
data=json.dumps({
'name': 'newtest',
'description': 'new description',
Expand All @@ -175,7 +173,7 @@ def test_put_club__failure(server):


def test_get_club__success(server):
_, response = server.app.test_client.get('/clubs/newtest')
_, response = server.app.test_client.get('/clubs/1')
assert response.status == 200
assert response.json['name'] == 'newtest'
assert response.json['description'] == 'club with a newer description'
Expand All @@ -184,7 +182,8 @@ def test_get_club__success(server):


def test_get_club__failure(server):
_, response = server.app.test_client.get('/clubs/doesnotexist')
# use id that isn't in the club at this point
_, response = server.app.test_client.get('/clubs/9478')
assert response.status == 404


Expand All @@ -196,7 +195,7 @@ def test_delete_club__failure(server):

# fail when a user with a Member membership tries to delete the club
_, response = server.app.test_client.delete(
'/clubs/newtest', headers={'Authorization': token})
'/clubs/1', headers={'Authorization': token})
assert response.status == 403


Expand All @@ -208,7 +207,7 @@ def test_delete_club__success(server):
token = util.create_jwt(editor_id, server.config.secret)

_, response = server.app.test_client.delete(
'/clubs/newtest', headers={'Authorization': token})
'/clubs/1', headers={'Authorization': token})
assert response.status == 204


Expand Down

0 comments on commit b841541

Please sign in to comment.