Skip to content

Commit

Permalink
Merge pull request #33 from brighthive/feat/add-organization
Browse files Browse the repository at this point in the history
Organization model
  • Loading branch information
reginafcompton committed Jul 30, 2020
2 parents b62e612 + ced7f47 commit 127c170
Show file tree
Hide file tree
Showing 19 changed files with 749 additions and 295 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
**/docs/build

**.DS_Store

.env
2 changes: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pytest-cov = "==2.8.1"
expects = "==0.9.0"
sphinx = "==2.4.1"
doc8 = "==0.8.0"
pytest-flask = "*"
pytest-mock = "*"

[packages]
flask = "==1.1.1"
Expand Down
326 changes: 167 additions & 159 deletions Pipfile.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions authserver/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from authserver.api.health import health_api_bp
from authserver.api.data_trust import data_trust_bp
from authserver.api.user import user_bp
from authserver.api.organization import organization_bp
from authserver.api.client import client_bp
from authserver.api.oauth2 import oauth2_bp
from authserver.api.role import role_bp
Expand Down
127 changes: 127 additions & 0 deletions authserver/api/organization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import json
from datetime import datetime

from flask import Blueprint
from flask_restful import Api, Resource, request

from authserver.db import db, Organization, OrganizationSchema
from authserver.utilities import ResponseBody, require_oauth


class OrganizationResource(Resource):
"""An Organization Resource.
This resource defines an Organization, which may have one or more associated Users.
"""

def __init__(self):
self.organization_schema = OrganizationSchema()
self.organizations_schema = OrganizationSchema(many=True)
self.response_handler = ResponseBody()

@require_oauth()
def get(self, id: str = None):
if not id:
organizations = Organization.query.all()
organizations_obj = self.organizations_schema.dump(organizations).data
return self.response_handler.get_all_response(organizations_obj)
else:
organization = Organization.query.filter_by(id=id).first()
if organization:
organization_obj = self.organization_schema.dump(organization).data
return self.response_handler.get_one_response(organization_obj, request={'id': id})
else:
return self.response_handler.not_found_response(id)

@require_oauth()
def post(self):
try:
request_data = request.get_json(force=True)
except Exception as e:
return self.response_handler.empty_request_body_response()

if not request_data:
return self.response_handler.empty_request_body_response()

data, errors = self.organization_schema.load(request_data)
if errors:
return self.response_handler.custom_response(code=422, messages=errors)

try:
organization = Organization(name=request_data['name'])
db.session.add(organization)
db.session.commit()
except Exception as e:
db.session.rollback()
exception_name = type(e).__name__
return self.response_handler.exception_response(exception_name, request=request_data)
return self.response_handler.successful_creation_response('Organization', organization.id, request_data)

@require_oauth()
def delete(self, id: str = None):
if id is None:
return self.response_handler.method_not_allowed_response()
try:
organization = Organization.query.filter_by(id=id).first()
if organization:
organization_obj = self.organization_schema.dump(organization).data
db.session.delete(organization)
db.session.commit()
return self.response_handler.successful_delete_response('Organization', id, organization_obj)
else:
return self.response_handler.not_found_response(id)
except Exception:
return self.response_handler.not_found_response(id)

@require_oauth()
def put(self, id: str = None):
if id is None:
return self.response_handler.method_not_allowed_response()

return self._update(request, id, False)

@require_oauth()
def patch(self, id: str = None):
if id is None:
return self.response_handler.method_not_allowed_response()

return self._update(request, id)

def _update(self, request, id: str, partial=True):
"""General update function for PUT and PATCH.
Using Marshmallow, the logic for PUT and PATCH differ by a single parameter. This method abstracts that logic
and allows for switching the Marshmallow validation to partial for PATCH and complete for PUT.
"""
try:
request_data = request.get_json(force=True)
except Exception as e:
return self.response_handler.empty_request_body_response()

if not request_data:
return self.response_handler.empty_request_body_response()

organization = Organization.query.filter_by(id=id).first()
if not organization:
return self.response_handler.not_found_response(id)

data, errors = self.organization_schema.load(request_data, partial=partial)
if errors:
return self.response_handler.custom_response(code=422, messages=errors)

for k, v in data.items():
if hasattr(organization, k):
setattr(organization, k, v)
try:
organization.date_last_updated = datetime.utcnow()
db.session.commit()
return self.response_handler.successful_update_response('Organization', id, data)
except Exception as e:
db.session.rollback()
exception_name = type(e).__name__
return self.response_handler.exception_response(exception_name, request=request_data)


organization_bp = Blueprint('organization_ep', __name__)
organization_api = Api(organization_bp)
organization_api.add_resource(OrganizationResource, '/organizations', '/organizations/<string:id>')
11 changes: 8 additions & 3 deletions authserver/api/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ def get(self):
'username': user.username,
'firstname': user.firstname,
'lastname': user.lastname,
'organization': user.organization,
'organization': {
'id': user.organization.id,
'name': user.organization.name
},
'email_address': user.email_address,
'telephone': user.telephone,
'active': user.active,
Expand Down Expand Up @@ -69,11 +72,13 @@ def get(self, id: str = None):
if not id:
users = User.query.all()
users_obj = self.users_schema.dump(users).data
return self.response_handler.get_all_response(users_obj)
users_obj_clean = [{k: v for k, v in user.items() if k != 'organization_id'} for user in users_obj]
return self.response_handler.get_all_response(users_obj_clean)
else:
user = User.query.filter_by(id=id).first()
if user:
user_obj = self.user_schema.dump(user).data
user_obj.pop('organization_id')
return self.response_handler.get_one_response(user_obj, request={'id': id})
else:
return self.response_handler.not_found_response(id)
Expand Down Expand Up @@ -109,7 +114,7 @@ def post(self, id: str = None):
return self.response_handler.custom_response(code=422, messages=errors)
try:
user = User(request_data['username'], request_data['password'], firstname=request_data['firstname'], lastname=request_data['lastname'],
organization=request_data['organization'], email_address=request_data['email_address'],
organization_id=request_data['organization_id'], email_address=request_data['email_address'],
data_trust_id=request_data['data_trust_id'])
if 'telephone' in request_data.keys():
user.telephone = request_data['telephone']
Expand Down
3 changes: 2 additions & 1 deletion authserver/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from flask_sqlalchemy import SQLAlchemy

from authserver.api import (client_bp, data_trust_bp, health_api_bp, oauth2_bp,
role_bp, user_bp, home_bp)
role_bp, user_bp, organization_bp, home_bp)
from authserver.config import ConfigurationFactory
from authserver.db import db
from authserver.utilities import config_oauth
Expand Down Expand Up @@ -83,6 +83,7 @@ def after_request(response):
app.register_blueprint(health_api_bp)
app.register_blueprint(data_trust_bp)
app.register_blueprint(user_bp)
app.register_blueprint(organization_bp)
app.register_blueprint(client_bp)
app.register_blueprint(oauth2_bp)
app.register_blueprint(role_bp)
Expand Down
2 changes: 1 addition & 1 deletion authserver/db/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from authserver.db.models import db, DataTrust, DataTrustSchema, User, UserSchema,\
OAuth2Client, OAuth2ClientSchema, OAuth2AuthorizationCode, OAuth2Token, Role,\
RoleSchema, AuthorizedClient
RoleSchema, AuthorizedClient, Organization, OrganizationSchema
2 changes: 1 addition & 1 deletion authserver/db/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from authserver.db.models.models import db, DataTrust, DataTrustSchema, User,\
UserSchema, OAuth2Client, OAuth2ClientSchema, OAuth2AuthorizationCode, OAuth2Token,\
Role, RoleSchema, AuthorizedClient
Role, RoleSchema, AuthorizedClient, Organization, OrganizationSchema
64 changes: 54 additions & 10 deletions authserver/db/models/models.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
"""Auth Server Database Models."""

import time
import json
import bcrypt
import time
from datetime import datetime
from uuid import uuid4

import bcrypt
from authlib.integrations.sqla_oauth2 import (OAuth2AuthorizationCodeMixin,
OAuth2ClientMixin,
OAuth2TokenMixin)
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from marshmallow import Schema, fields, pre_load, validate
from sqlalchemy import text as sa_text
from sqlalchemy.dialects.postgresql.json import JSONB
from marshmallow import Schema, fields, pre_load, validate
from flask_marshmallow import Marshmallow
from uuid import uuid4
from authlib.integrations.sqla_oauth2 import OAuth2ClientMixin, OAuth2AuthorizationCodeMixin, OAuth2TokenMixin


db = SQLAlchemy()
ma = Marshmallow()
Expand Down Expand Up @@ -63,6 +67,43 @@ class Meta:
date_last_updated = fields.DateTime(dump_only=True)


class Organization(db.Model):
"""Data Trust Organization."""
__tablename__ = 'organizations'

id = db.Column(db.String, primary_key=True)
name = db.Column(db.String(40), unique=True, nullable=False)
url = db.Column(db.String)
date_created = db.Column(db.TIMESTAMP)
date_last_updated = db.Column(db.TIMESTAMP)

def __init__(self, name, url=None):
self.id = str(uuid4()).replace('-', '')
self.name = name
self.url = url
self.date_created = datetime.utcnow()
self.date_last_updated = datetime.utcnow()

def __str__(self):
return '{} - {}'.format(self.id, self.name)


class OrganizationSchema(ma.Schema):
"""Organization Schema
A marshmallow schema for validating the Organization model.
"""

class Meta:
ordered = True

id = fields.String(dump_only=True)
name = fields.String(required=True)
url = fields.String()
date_created = fields.DateTime(dump_only=True)
date_last_updated = fields.DateTime(dump_only=True)


class User(db.Model):
"""Data Trust User."""
__tablename__ = 'users'
Expand All @@ -72,7 +113,9 @@ class User(db.Model):
username = db.Column(db.String(40), unique=True, nullable=False)
firstname = db.Column(db.String(40), nullable=False)
lastname = db.Column(db.String(40), nullable=False)
organization = db.Column(db.String(120), nullable=False)
organization = db.relationship('Organization', backref='users', lazy='subquery')
organization_id = db.Column(
db.String, db.ForeignKey('organizations.id', ondelete='CASCADE'), nullable=False)
email_address = db.Column(db.String(40), nullable=False)
telephone = db.Column(db.String(20), nullable=True)
active = db.Column(db.Boolean, nullable=False, default=True)
Expand Down Expand Up @@ -100,13 +143,13 @@ def verify_password(self, password: str):
def get_user_id(self):
return self.id

def __init__(self, username, password, firstname, lastname, organization, email_address, data_trust_id, telephone=None):
def __init__(self, username, password, firstname, lastname, organization_id, email_address, data_trust_id, telephone=None):
self.id = str(uuid4()).replace('-', '')
self.username = username
self.firstname = firstname
self.lastname = lastname
self.password = password
self.organization = organization
self.organization_id = organization_id
self.email_address = email_address
self.telephone = telephone
self.data_trust_id = data_trust_id
Expand All @@ -131,7 +174,8 @@ class Meta:
password = fields.String(required=True)
firstname = fields.String(required=True)
lastname = fields.String(required=True)
organization = fields.String(required=True)
organization_id = fields.String(required=True)
organization = fields.Nested(OrganizationSchema(), dump_only=True)
email_address = fields.Email(required=True)
telephone = fields.String()
active = fields.Boolean(dump_only=True)
Expand Down

0 comments on commit 127c170

Please sign in to comment.