Skip to content

Commit

Permalink
Merge branch 'master' into 41_add_ability_to_get_distinct_values
Browse files Browse the repository at this point in the history
  • Loading branch information
keiranjprice101 authored Sep 11, 2019
2 parents 2f85368 + bb0a2a3 commit d8a8499
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 13 deletions.
166 changes: 159 additions & 7 deletions common/database_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
from abc import ABC, abstractmethod

from sqlalchemy import asc, desc
from sqlalchemy.orm import aliased

from common.exceptions import MissingRecordError, BadFilterError, BadRequestError
from common.models.db_models import INVESTIGATIONUSER, INVESTIGATION, INSTRUMENT, FACILITYCYCLE, \
INVESTIGATIONINSTRUMENT, FACILITY
from common.session_manager import session_manager

log = logging.getLogger()
Expand Down Expand Up @@ -54,6 +57,10 @@ def __init__(self, table):
self.include_related_entities = False
self.is_distinct_fields_query = False

def commit_changes(self):
log.info("Closing DB session")
self.session.close()

def execute_query(self):
self.commit_changes()

Expand Down Expand Up @@ -137,6 +144,7 @@ def __init__(self, field, value, operation):
self.operation = operation

def apply_filter(self, query):

if self.operation == "eq":
query.base_query = query.base_query.filter(getattr(query.table, self.field) == self.value)
elif self.operation == "like":
Expand Down Expand Up @@ -329,15 +337,14 @@ def update_row_from_id(table, id, new_values):
update_query.execute_query()


def get_rows_by_filter(table, filters):
def get_filtered_read_query_results(filter_handler, filters, query):
"""
Given a list of filters supplied in json format, returns entities that match the filters from the given table
:param table: The table to checked
:param filters: The list of filters to be applied
:return: A list of the rows returned in dictionary form
Given a filter handler, list of filters and a query. Apply the filters and execute the query
:param filter_handler: The filter handler to apply the filters
:param filters: The filters to be applied
:param query: The query for the filters to be applied to
:return: The results of the query as a list of dictionaries
"""
query = ReadQuery(table)
filter_handler = FilterOrderHandler()
try:
for query_filter in filters:
if len(query_filter) == 0:
Expand All @@ -351,10 +358,12 @@ def get_rows_by_filter(table, filters):
if query.include_related_entities:
return _get_results_with_include(filters, results)
return list(map(lambda x: x.to_dict(), results))

finally:
query.session.close()



def _get_results_with_include(filters, results):
"""
Given a list of entities and a list of filters, use the include filter to nest the included entities requested in
Expand All @@ -381,6 +390,18 @@ def _get_distinct_fields_as_dicts(results):
dictionaries.append(dictionary)
return dictionaries

def get_rows_by_filter(table, filters):
"""
Given a list of filters supplied in json format, returns entities that match the filters from the given table
:param table: The table to checked
:param filters: The list of filters to be applied
:return: A list of the rows returned in dictionary form
"""
query = ReadQuery(table)
filter_handler = FilterOrderHandler()
return get_filtered_read_query_results(filter_handler, filters, query)



def get_first_filtered_row(table, filters):
"""
Expand Down Expand Up @@ -439,3 +460,134 @@ def patch_entities(table, json_list):
raise BadRequestError(f" Bad request made, request: {json_list}")

return results


class UserInvestigationsQuery(ReadQuery):
"""
The query class used for the /users/<:id>/investigations endpoint
"""

def __init__(self, user_id):
super().__init__(INVESTIGATION)
self.base_query = self.base_query.join(INVESTIGATIONUSER).filter(INVESTIGATIONUSER.USER_ID == user_id)


def get_investigations_for_user(user_id, filters):
"""
Given a user id and a list of filters, return a filtered list of all investigations that belong to that user
:param user_id: The id of the user
:param filters: The list of filters
:return: A list of dictionary representations of the investigation entities
"""
query = UserInvestigationsQuery(user_id)
filter_handler = FilterOrderHandler()
return get_filtered_read_query_results(filter_handler, filters, query)


class UserInvestigationsCountQuery(CountQuery):
"""
The query class used for /users/<:id>/investigations/count
"""

def __init__(self, user_id):
super().__init__(INVESTIGATION)
self.base_query = self.base_query.join(INVESTIGATIONUSER).filter(INVESTIGATIONUSER.USER_ID == user_id)


def get_investigations_for_user_count(user_id, filters):
"""
Given a user id and a list of filters, return the count of all investigations that belong to that user
:param user_id: The id of the user
:param filters: The list of filters
:return: The count
"""
count_query = UserInvestigationsCountQuery(user_id)
filter_handler = FilterOrderHandler()
for query_filter in filters:
if len(query_filter) == 0:
pass
else:
filter_handler.add_filter(QueryFilterFactory.get_query_filter(query_filter))
filter_handler.apply_filters(count_query)
return count_query.get_count()


class InstrumentFacilityCyclesQuery(ReadQuery):
def __init__(self, instrument_id):
super().__init__(FACILITYCYCLE)
investigationInstrument = aliased(INSTRUMENT)
self.base_query = self.base_query\
.join(FACILITYCYCLE.FACILITY) \
.join(FACILITY.INSTRUMENT) \
.join(FACILITY.INVESTIGATION) \
.join(INVESTIGATION.INVESTIGATIONINSTRUMENT) \
.join(investigationInstrument, INVESTIGATIONINSTRUMENT.INSTRUMENT) \
.filter(INSTRUMENT.ID == instrument_id) \
.filter(investigationInstrument.ID == INSTRUMENT.ID) \
.filter(INVESTIGATION.STARTDATE >= FACILITYCYCLE.STARTDATE) \
.filter(INVESTIGATION.STARTDATE <= FACILITYCYCLE.ENDDATE)


def get_facility_cycles_for_instrument(instrument_id, filters):
"""
Given an instrument_id get facility cycles where the instrument has investigations that occur within that cycle
:param filters: The filters to be applied to the query
:param instrument_id: The id of the instrument
:return: A list of facility cycle entities
"""
query = InstrumentFacilityCyclesQuery(instrument_id)
filter_handler = FilterOrderHandler()
return get_filtered_read_query_results(filter_handler, filters, query)


def get_facility_cycles_for_instrument_count(instrument_id, filters):
"""
Given an instrument_id get the facility cycles count where the instrument has investigations that occur within
that cycle
:param filters: The filters to be applied to the query
:param instrument_id: The id of the instrument
:return: The count of the facility cycles
"""
return len(get_facility_cycles_for_instrument(instrument_id, filters))


class InstrumentFacilityCycleInvestigationsQuery(ReadQuery):
def __init__(self, instrument_id, facility_cycle_id):
super().__init__(INVESTIGATION)
investigationInstrument = aliased(INSTRUMENT)
self.base_query = self.base_query\
.join(INVESTIGATION.FACILITY) \
.join(FACILITY.FACILITYCYCLE) \
.join(FACILITY.INSTRUMENT) \
.join(INVESTIGATION.INVESTIGATIONINSTRUMENT) \
.join(investigationInstrument, INVESTIGATIONINSTRUMENT.INSTRUMENT) \
.filter(INSTRUMENT.ID == instrument_id) \
.filter(FACILITYCYCLE.ID == facility_cycle_id) \
.filter(investigationInstrument.ID == INSTRUMENT.ID) \
.filter(INVESTIGATION.STARTDATE >= FACILITYCYCLE.STARTDATE) \
.filter(INVESTIGATION.STARTDATE <= FACILITYCYCLE.ENDDATE)


def get_investigations_for_instrument_in_facility_cycle(instrument_id, facility_cycle_id, filters):
"""
Given an instrument id and facility cycle id, get investigations that use the given instrument in the given cycle
:param filters: The filters to be applied to the query
:param instrument_id: The id of the instrument
:param facility_cycle_id: the ID of the facility cycle
:return: The investigations
"""
filter_handler = FilterOrderHandler()
query = InstrumentFacilityCycleInvestigationsQuery(instrument_id, facility_cycle_id)
return get_filtered_read_query_results(filter_handler, filters, query)


def get_investigations_for_instrument_in_facility_cycle_count(instrument_id, facility_cycle_id, filters):
"""
Given an instrument id and facility cycle id, get the count of the investigations that use the given instrument in
the given cycle
:param filters: The filters to be applied to the query
:param instrument_id: The id of the instrument
:param facility_cycle_id: the ID of the facility cycle
:return: The investigations count
"""
return len(get_investigations_for_instrument_in_facility_cycle(instrument_id, facility_cycle_id, filters))
44 changes: 38 additions & 6 deletions common/models/db_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from sqlalchemy import Index, Column, BigInteger, String, DateTime, ForeignKey, Integer, Float, FetchedValue
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm.collections import InstrumentedList

from common.exceptions import BadFilterError

Expand All @@ -25,23 +26,55 @@ def to_dict(self):
def to_nested_dict(self, includes):
"""
Given related models return a nested dictionary with the child or parent rows nested.
:param included_relations: string/list/dict - The related models to include.
:param includes: string/list/dict - The related models to include.
:return: A nested dictionary with the included models
"""
dictionary = self.to_dict()
try:
includes = includes if type(includes) is list else [includes]
for include in includes:
if type(include) is str:
related_entity = self.get_related_entity(include)
dictionary[related_entity.__tablename__] = related_entity.to_dict()
self._nest_string_include(dictionary, include)
elif type(include) is dict:
related_entity = self.get_related_entity(list(include)[0])
dictionary[related_entity.__tablename__] = related_entity.to_nested_dict(include[list(include)[0]])
self._nest_dictionary_include(dictionary, include)
except TypeError:
raise BadFilterError(f" Bad include relations provided: {includes}")
return dictionary

def _nest_dictionary_include(self, dictionary, include):
"""
Given a dictionary of related entities names, nest the related entities into the given dictionary representation,
of the original entity.
:param dictionary: The dictionary representation of the original entity
:param include: The dictionary of related entity names to be nested.
"""
related_entity = self.get_related_entity(list(include)[0])
if not isinstance(related_entity, InstrumentedList):
dictionary[related_entity.__tablename__] = related_entity.to_nested_dict(include[list(include)[0]])
else:
for entity in related_entity:
if entity.__tablename__ in dictionary.keys():
dictionary[entity.__tablename__].append(entity.to_nested_dict(include[list(include)[0]]))
else:
dictionary[entity.__tablename__] = [entity.to_nested_dict(include[list(include)[0]])]

def _nest_string_include(self, dictionary, include):
"""
Given the name of a single related entity, nest the related entity into the given dictionary representation of
the original entity.
:param dictionary: The dictionary representation of an entity to be nested in.
:param include: The name of the related entity to be nested
"""
related_entity = self.get_related_entity(include)
if not isinstance(related_entity, InstrumentedList):
dictionary[related_entity.__tablename__] = related_entity.to_dict()
else:
for entity in related_entity:
if entity.__tablename__ in dictionary.keys():
dictionary[entity.__tablename__].append(entity.to_dict())
else:
dictionary[entity.__tablename__] = [entity.to_dict()]

def get_related_entity(self, entity):
"""
Given a string for the related entity name, return the related entity
Expand Down Expand Up @@ -549,7 +582,6 @@ class JOB(Base, EntityHelper):
backref='JOB')



class KEYWORD(Base, EntityHelper):
__tablename__ = 'KEYWORD'
__table_args__ = (
Expand Down
13 changes: 13 additions & 0 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
from src.resources.entities.user_groups_endpoints import *
from src.resources.entities.users_endpoints import *
from src.resources.non_entities.sessions_endpoints import *
from src.resources.table_endpoints.table_endpoints import UsersInvestigations, UsersInvestigationsCount, \
InstrumentsFacilityCycles, InstrumentsFacilityCyclesCount, InstrumentsFacilityCyclesInvestigations, \
InstrumentsFacilityCyclesInvestigationsCount
from src.swagger.swagger_generator import swagger_gen

swagger_gen.write_swagger_spec()
Expand Down Expand Up @@ -200,5 +203,15 @@
api.add_resource(UserGroupsCount, "/usergroups/count")
api.add_resource(UserGroupsFindOne, "/usergroups/findOne")

# Table specific endpoints
api.add_resource(UsersInvestigations, "/users/<int:id>/investigations")
api.add_resource(UsersInvestigationsCount, "/users/<int:id>/investigations/count")
api.add_resource(InstrumentsFacilityCycles, "/instruments/<int:id>/facilitycycles")
api.add_resource(InstrumentsFacilityCyclesCount, "/instruments/<int:id>/facilitycycles/count")
api.add_resource(InstrumentsFacilityCyclesInvestigations,
"/instruments/<int:instrument_id>/facilitycycles/<int:cycle_id>/investigations")
api.add_resource(InstrumentsFacilityCyclesInvestigationsCount,
"/instruments/<int:instrument_id>/facilitycycles/<int:cycle_id>/investigations/count")

if __name__ == "__main__":
app.run(debug=config.is_debug_mode())
Empty file.
50 changes: 50 additions & 0 deletions src/resources/table_endpoints/table_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from flask_restful import Resource

from common.database_helpers import get_investigations_for_user, get_investigations_for_user_count, \
get_facility_cycles_for_instrument, get_facility_cycles_for_instrument_count, \
get_investigations_for_instrument_in_facility_cycle, get_investigations_for_instrument_in_facility_cycle_count
from common.helpers import requires_session_id, queries_records, get_filters_from_query_string


class UsersInvestigations(Resource):
@requires_session_id
@queries_records
def get(self, id):
return get_investigations_for_user(id, get_filters_from_query_string()), 200


class UsersInvestigationsCount(Resource):
@requires_session_id
@queries_records
def get(self, id):
return get_investigations_for_user_count(id, get_filters_from_query_string()), 200


class InstrumentsFacilityCycles(Resource):
@requires_session_id
@queries_records
def get(self, id):
return get_facility_cycles_for_instrument(id, get_filters_from_query_string()), 200


class InstrumentsFacilityCyclesCount(Resource):
@requires_session_id
@queries_records
def get(self, id):
return get_facility_cycles_for_instrument_count(id, get_filters_from_query_string()), 200


class InstrumentsFacilityCyclesInvestigations(Resource):
@requires_session_id
@queries_records
def get(self, instrument_id, cycle_id):
return get_investigations_for_instrument_in_facility_cycle(instrument_id, cycle_id,
get_filters_from_query_string()), 200


class InstrumentsFacilityCyclesInvestigationsCount(Resource):
@requires_session_id
@queries_records
def get(self, instrument_id, cycle_id):
return get_investigations_for_instrument_in_facility_cycle_count(instrument_id, cycle_id,
get_filters_from_query_string()), 200

0 comments on commit d8a8499

Please sign in to comment.