Skip to content

Commit

Permalink
Update memberships and schema
Browse files Browse the repository at this point in the history
  • Loading branch information
bfbachmann committed Aug 13, 2018
1 parent e27c85e commit d468bd1
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 52 deletions.
8 changes: 4 additions & 4 deletions bounce/db/club.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ def to_dict(self):
'id': self.identifier,
'name': self.name,
'description': self.description,
'website_url': self.website_url,
'facebook_url': self.facebook_url,
'instagram_url': self.instagram_url,
'twitter_url': self.twitter_url,
'website_url': self.website_url or '',
'facebook_url': self.facebook_url or '',
'instagram_url': self.instagram_url or '',
'twitter_url': self.twitter_url or '',
'created_at': self.created_at,
}

Expand Down
46 changes: 34 additions & 12 deletions bounce/db/membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ class Membership(BASE):
to a club they're a member of.
"""
__tablename__ = 'memberships'

identifier = Column('id', Integer, primary_key=True)
user_id = Column(
'user_id',
Integer,
Expand All @@ -39,7 +37,6 @@ class Membership(BASE):
def to_dict(self):
"""Returns a dict representation of a Membership."""
return {
'id': self.identifier,
'user_id': self.user_id,
'club_id': self.club_id,
'created_at': self.created_at,
Expand All @@ -53,17 +50,42 @@ def insert(session, user_id, club_id):
session.commit()


def select(session, club_id, user_id):
def select(session, club_name, user_id=None):
"""
Returns all memberships for the given club. If user_id is given, returns
only the membership for the given user.
"""
Returns the membership for the user and club with the given IDs.
query = f"""
SELECT users.id AS user_id,
memberships.created_at, 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 name = '{club_name}'
)
"""
membership = session.query(Membership).filter(
Club.identifier == club_id, User.identifier == user_id).first()
return None if membership is None else membership.to_dict()
if user_id:
query += f' AND user_id = {user_id}'
result_proxy = session.execute(query)
results = []
for row in result_proxy.fetchall():
results.append({
key: row[i] for i, key in enumerate(result_proxy.keys())
})
return results


def delete(session, club_id, user_id):
"""Deletes the membership for the user and club with the given IDs."""
session.query(Membership).filter_by(
club_id=club_id, user_id=user_id).delete()
def delete(session, club_name, user_id=None):
"""
Deletes all memberships for the given club. If user_id is given, deletes
only the membership for the given user.
"""
query = f"""
DELETE FROM memberships
WHERE memberships.club_id IN (
SELECT id FROM clubs WHERE name = '{club_name}'
)
"""
if user_id:
query += f' AND user_id = {user_id}'
session.execute(query)
session.commit()
3 changes: 0 additions & 3 deletions bounce/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ def __init__(self, config, endpoints):
endpoints (list[Endpoint]): list of Endpoints this server serves
requests at
"""
# import pdb
# pdb.set_trace()

self._config = config
self._app = Sanic()
self._engine = None
Expand Down
10 changes: 9 additions & 1 deletion bounce/server/api/clubs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Request handlers for the /clubs endpoint."""

from urllib.parse import unquote

from sanic import response
from sqlalchemy.exc import IntegrityError

Expand All @@ -18,6 +20,8 @@ class ClubEndpoint(Endpoint):
async def get(self, _, 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)
# Fetch club data from DB
club_data = club.select(self.server.db_session, name)
if not club_data:
Expand All @@ -29,6 +33,8 @@ async def get(self, _, name):
async def put(self, request, name):
"""Handles a PUT /clubs/<name> 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)
body = request.json
updated_club = club.update(
self.server.db_session,
Expand All @@ -43,7 +49,9 @@ async def put(self, request, name):

async def delete(self, _, name):
"""Handles a DELETE /clubs/<name> request by deleting the club with
the given name."""
the given name. """
# Decode the name, since special characters will be URL-encoded
name = unquote(name)
club.delete(self.server.db_session, name)
return response.text('', status=204)

Expand Down
70 changes: 55 additions & 15 deletions bounce/server/api/membership.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,73 @@
"""Request handlers for the /users endpoint."""

from urllib.parse import unquote

from sanic import response
from sqlalchemy.exc import IntegrityError

from . import APIError, Endpoint, util, verify_token
from ...db import membership
from ...db import club, membership
from ..resource import validate
from ..resource.membership import (GetMembershipResponse,
from ..resource.membership import (DeleteMembershipRequest,
GetMembershipRequest, GetMembershipResponse,
PostMembershipRequest, PutMembershipRequest)


class MembershipEndpoint(Endpoint):
"""Handles requests to /memberships/<club_id>."""
"""Handles requests to /memberships/<club_name>."""

__uri__ = "/memberships/<club_name:string>"

@validate(GetMembershipRequest, GetMembershipResponse)
async def get(self, request, club_name):
"""
Handles a GET /memberships/<club_name>?user_id=<user_id> request
by returning the membership that associates the given user with the
given club. If no user ID is given, returns all memberships for the
given club.
"""
# Decode the club name
club_name = unquote(club_name)
user_id = request.args.get('user_id', None)

__uri__ = "/memberships/<club_id:string>"
# Make sure the club exists
club_row = club.select(self.server.db_session, club_name)
if not club_row:
raise APIError('No such club', status=404)

# Fetch the club's memberships
membership_info = membership.select(
self.server.db_session, club_name, user_id=user_id)
return response.json(membership_info, status=200)

@verify_token()
async def delete(self, _, club_id, id_from_token=None):
@validate(DeleteMembershipRequest, None)
async def delete(self, request, club_name, id_from_token=None):
"""
Handles a DELETE /memberships/<club_id> request by deleting the
user's membership with the club with the given id.
Handles a DELETE /memberships/<club_name>?user_id=<user_id> request
by deleting the membership that associates the given user with the
given club. If no user ID is given, deletes all memberships for the
given club.
"""
# Fetch the membership using the user ID and club ID
membership_row = membership.select(self.server.db_session, club_id,
id_from_token)
if not membership_row:
raise APIError('No such membership', status=404)
# Delete the membership
membership.delete(self.server.db_session, club_id, id_from_token)
# TODO: fix this when we have user roles set up. A user should only be
# able to delete their own memberships and memberships on clubs they
# are an admin/owner of.

# Decode the club name
club_name = unquote(club_name)
user_id = request.args.get('user_id', None)

if id_from_token != user_id:
# Regular members can only delete their own memberships
raise APIError('Forbidden', status=403)

# Make sure the club exists
club_row = club.select(self.server.db_session, club_name)
if not club_row:
raise APIError('No such club', status=404)

# Delete the memberships
membership.delete(self.server.db_session, club_name, user_id=user_id)
return response.text('', status=204)


Expand All @@ -45,5 +85,5 @@ async def post(self, request):
membership.insert(self.server.db_session, body['user_id'],
body['club_id'])
except IntegrityError:
raise APIError('Membership already exists', status=409)
raise APIError('Invalid user or club ID', status=400)
return response.text('', status=201)
61 changes: 47 additions & 14 deletions bounce/server/resource/membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,50 @@
from . import ResourceMeta


class GetMembershipRequest(metaclass=ResourceMeta):
"""Defines the schema for a GET /membership/<club_name> request."""
__params__ = {
'type': 'object',
'additionalProperties': False,
'properties': {
'user_id': {
'type': 'string',
'minimum': 0,
},
}
}


class GetMembershipResponse(metaclass=ResourceMeta):
"""Defines the schema for a GET /membership/<club_name> response."""
__body__ = {
'type': 'array',
'items': {
'type': 'object',
'required': [
'user_id',
'created_at',
'full_name',
'username',
],
'properties': {
'user_id': {
'type': 'integer'
},
'created_at': {
'type': 'integer',
},
'full_name': {
'type': 'string',
},
'username': {
'type': 'string',
},
}
}
}


class PostMembershipRequest(metaclass=ResourceMeta):
"""Defines the schema for a POST /membership request."""
__body__ = {
Expand All @@ -29,10 +73,6 @@ class PutMembershipRequest(metaclass=ResourceMeta):
'additionalProperties': False,
'properties': {
'user_id': {
'type': 'string',
'minimum': 0,
},
'membership_id': {
'type': 'integer',
'minimum': 0,
},
Expand All @@ -43,21 +83,14 @@ class PutMembershipRequest(metaclass=ResourceMeta):
}


class GetMembershipResponse(metaclass=ResourceMeta):
"""Defines the schema for a GET /users/<username> response."""
__body__ = {
class DeleteMembershipRequest(metaclass=ResourceMeta):
"""Defines the schema for a DELETE /members/<club_name> request."""
__params__ = {
'type': 'object',
'required': ['user_id', 'club_id', 'created_at'],
'additionalProperties': False,
'properties': {
'user_id': {
'type': 'string'
},
'club_id': {
'type': 'string',
},
'created_at': {
'type': 'integer',
},
}
}
6 changes: 3 additions & 3 deletions schema/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ CREATE TABLE users (

DROP TABLE IF EXISTS memberships;
CREATE TABLE memberships (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id INT REFERENCES users(id),
club_id INT REFERENCES clubs(id),
user_id INT REFERENCES users(id) ON DELETE CASCADE,
club_id INT REFERENCES clubs(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, club_id),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now() at time zone 'utc')
);

0 comments on commit d468bd1

Please sign in to comment.