def get(self, user_id): ''' Get a single user. ''' if not user_id: self.query() return with model.session_scope() as session: user_session = self.get_user_session(session) if user_id == 'current': user = user_session.user else: user = ( session.query(model.AppUser) .options(joinedload('organisation')) .options(joinedload('surveygroups')) .get(user_id)) if not user: raise errors.MissingDocError("No such user") policy = user_session.policy.derive({ 'user': user, 'surveygroups': user.surveygroups, }) policy.verify('user_view') # Check that user shares a common surveygroup with the requesting # user. # Allow admins to access users outside their surveygroups though. if not policy.check('admin'): policy.verify('surveygroup_interact') to_son = ToSon( r'/id$', r'/name$', r'/title$', r'/email$', r'/email_interval$', r'/role$', r'/deleted$', # Descend into nested objects r'/organisation$', r'/[0-9+]$', # Exclude password from response. Not really necessary because # 1. it's hashed and 2. it's not in the list above. But just to # be safe. r'!password', ) if policy.check('surveygroup_browse'): to_son.add(r'^/surveygroups$') son = to_son(user) self.set_header("Content-Type", "application/json") self.write(json_encode(son)) self.finish()
def get(self, program_id): ''' Get a single program. ''' if program_id == "": self.query() return with model.session_scope() as session: user_session = self.get_user_session(session) program = (session.query(model.Program).options( joinedload('surveygroups')).get(program_id)) if not program: raise errors.MissingDocError("No such program") policy = user_session.policy.derive({ 'program': program, 'surveygroups': program.surveygroups, }) policy.verify('surveygroup_interact') policy.verify('program_view') to_son = ToSon( r'/ob_type$', r'/id$', r'/tracking_id$', r'/title$', r'</description$', r'/created$', r'/deleted$', r'/is_editable$', r'^/error$', r'/has_quality$', r'/hide_aggregate$', r'/[0-9+]$', ) if policy.check('surveygroup_browse'): to_son.add(r'^/surveygroups$') if not policy.check('author'): to_son.exclude( r'/response_types.*score$', r'/response_types.*formula$', ) son = to_son(program) self.set_header("Content-Type", "application/json") self.write(json_encode(son)) self.finish()
def get(self, organisation_id): if organisation_id == "": self.query() return with model.session_scope() as session: user_session = self.get_user_session(session) org = (session.query(model.Organisation).options( joinedload('surveygroups')).get(organisation_id)) if not org: raise errors.MissingDocError("No such organisation") policy = user_session.policy.derive({ 'org': org, 'surveygroups': org.surveygroups, }) policy.verify('org_view') # Check that user shares a common surveygroup with this org. # Admins need access to orgs outside their surveygroups though. if not policy.check('admin'): policy.verify('surveygroup_interact') to_son = ToSon( r'/id$', r'/name$', r'/title$', r'/deleted$', r'/url$', r'/locations.*$', r'/meta.*$', r'/[0-9+]$', ) to_son.exclude( r'/locations/.*/organisation(_id)?$', r'/locations/.*/id$', r'/meta/organisation(_id)?$', r'/meta/id$', ) if policy.check('surveygroup_browse'): to_son.add(r'^/surveygroups$') son = to_son(org) self.set_header("Content-Type", "application/json") self.write(json_encode(son)) self.finish()
def execute(self): qnode_pairs = self.get_qnodes() measure_pairs = self.get_measures() to_son = ToSon( r'/id$', r'/survey_id$', r'/parent_id$', r'/title$', r'</description$', r'/response_type_id$', r'/seq$', # Descend r'/[0-9]+$', r'^/[0-9]+/[^/]+$', ) if self.include_scores: to_son.add(r'/weight$', ) qnode_pairs = self.realise_soft_deletion(qnode_pairs) qnode_pairs = self.remove_soft_deletion_dups(qnode_pairs) qnode_diff = [{ 'type': 'qnode', 'tags': [], 'pair': pair, } for pair in to_son(qnode_pairs)] start = perf() self.add_qnode_metadata(qnode_pairs, qnode_diff) self.add_metadata(qnode_pairs, qnode_diff) duration = perf() - start self.timing.append("Qnode metadata took %gs" % duration) self.remove_unchanged_fields(qnode_diff) measure_pairs = self.realise_soft_deletion(measure_pairs) measure_pairs = self.remove_soft_deletion_dups(measure_pairs) measure_diff = [{ 'type': 'measure', 'tags': [], 'pair': pair, } for pair in to_son(measure_pairs)] start = perf() self.add_measure_metadata(measure_pairs, measure_diff) self.add_metadata(measure_pairs, measure_diff) duration = perf() - start self.timing.append("Measure metadata took %gs" % duration) self.remove_unchanged_fields(measure_diff) diff = qnode_diff + measure_diff def path_key(diff_item): a, b = diff_item['pair'] if a and b: return 0, [int(c) for c in b['path'].split('.') if c != ''] elif b: return 0, [int(c) for c in b['path'].split('.') if c != ''] elif a: return 1, [int(c) for c in a['path'].split('.') if c != ''] else: return 2 start = perf() diff.sort(key=path_key) duration = perf() - start self.timing.append("Sorting took %gs" % duration) HA = model.Survey HB = aliased(model.Survey, name='survey_b') survey_a, survey_b = (self.session.query(HA, HB).join( HB, (HA.id == HB.id)).filter(HA.program_id == self.program_id_a, HB.program_id == self.program_id_b, HA.id == self.survey_id).first()) to_son = ToSon( r'/id$', r'/title$', r'</description$', ) top_level_diff = [{ 'type': 'program', 'tags': ['context'], 'pair': [to_son(survey_a.program), to_son(survey_b.program)] }, { 'type': 'survey', 'tags': ['context'], 'pair': [to_son(survey_a), to_son(survey_b)] }] self.remove_unchanged_fields(top_level_diff) return top_level_diff + diff
def query_by_level(self, level): level = int(level) program_id = self.get_argument('programId', '') survey_id = self.get_argument('surveyId', '') term = self.get_argument('term', '') parent_not = self.get_argument('parent__not', None) deleted = self.get_argument('deleted', '') if deleted != '': deleted = truthy(deleted) else: deleted = None if not survey_id: raise errors.ModelError("Survey ID required") with model.session_scope() as session: user_session = self.get_user_session(session) survey = ( session.query(model.Survey) .options(joinedload('program')) .options(joinedload('program.surveygroups')) .get((survey_id, program_id))) policy = user_session.policy.derive({ 'program': survey.program, 'survey': survey, 'surveygroups': survey.program.surveygroups, }) policy.verify('surveygroup_interact') policy.verify('qnode_view') # Use Postgres' WITH statement # http://www.postgresql.org/docs/9.1/static/queries-with.html # http://docs.sqlalchemy.org/en/rel_1_0/orm/query.html#sqlalchemy.orm.query.Query.cte # http://stackoverflow.com/a/28084743/320036 # Start by selecting root nodes QN1 = model.QuestionNode start = ( session.query( QN1, literal(0).label('level'), array([QN1.seq]).label('path'), (QN1.seq + 1).concat('.').label('pathstr'), (QN1.deleted).label('any_deleted')) .filter(QN1.parent_id == None, QN1.program_id == program_id, QN1.survey_id == survey_id) .cte(name='root', recursive=True)) # Now iterate down the tree to the desired level QN2 = aliased(model.QuestionNode, name='qnode2') recurse = ( session.query( QN2, (start.c.level + 1).label('level'), start.c.path.concat(QN2.seq).label('path'), start.c.pathstr.concat(QN2.seq + 1).concat('.').label( 'pathstr'), (start.c.any_deleted | QN2.deleted).label( 'any_deleted')) .filter(QN2.parent_id == start.c.id, QN2.program_id == start.c.program_id, QN2.survey_id == start.c.survey_id, start.c.level <= level)) # Combine iterated result with root cte = start.union_all(recurse) # Discard all but the lowest level subquery = ( session.query(cte.c.id, cte.c.pathstr, cte.c.any_deleted) .filter(cte.c.level == level) .order_by(cte.c.path) .subquery()) # Select again to get the actual qnodes query = ( session.query( model.QuestionNode, subquery.c.pathstr, subquery.c.any_deleted) .filter(model.QuestionNode.program_id == program_id) .join(subquery, model.QuestionNode.id == subquery.c.id)) if parent_not == '': query = query.filter( model.QuestionNode.parent_id != None) elif parent_not is not None: query = query.filter( model.QuestionNode.parent_id != parent_not) if term != '': query = query.filter( model.QuestionNode.title.ilike('%{}%'.format(term))) if deleted is not None: query = query.filter(subquery.c.any_deleted == deleted) query = self.paginate(query) to_son = ToSon( # Fields to match from any visited object r'/id$', r'/title$', r'/deleted$', r'/n_measures$' ) if truthy(self.get_argument('desc', False)): to_son.add(r'</description$') if user_session.user.role == 'clerk': to_son.exclude(r'/total_weight$') sons = [] for qnode, path, deleted in query.all(): son = to_son(qnode) son['path'] = path son['anyDeleted'] = deleted sons.append(son) self.set_header("Content-Type", "application/json") self.write(json_encode(sons)) self.finish()
def query(self): '''Get list.''' level = self.get_argument('level', '') if level: self.query_by_level(level) return program_id = self.get_argument('programId', '') survey_id = self.get_argument('surveyId', '') parent_id = self.get_argument('parentId', '') root = self.get_argument('root', None) term = self.get_argument('term', '') parent_not = self.get_argument('parent__not', '') deleted = self.get_argument('deleted', '') if root is not None and parent_id: raise errors.ModelError( "Can't specify parent ID when requesting roots") with model.session_scope() as session: user_session = self.get_user_session(session) if parent_id: parent = ( session.query(model.QuestionNode) .options(joinedload('survey')) .options(joinedload('program')) .options(joinedload('program.surveygroups')) .get((parent_id, program_id))) if not parent: raise errors.MissingDocError("No such parent category") if survey_id and survey_id != str(parent.survey_id): raise errors.ModelError("Category is not in that survey") survey = parent.survey elif survey_id: survey = ( session.query(model.Survey) .options(joinedload('program')) .options(joinedload('program.surveygroups')) .get((survey_id, program_id))) if not survey: raise errors.MissingDocError("No such survey") else: raise errors.ModelError("Survey or parent ID required") policy = user_session.policy.derive({ 'program': survey.program, 'survey': survey, 'surveygroups': survey.program.surveygroups, }) policy.verify('surveygroup_interact') policy.verify('qnode_view') query = ( session.query(model.QuestionNode) .filter(model.QuestionNode.program_id == program_id)) if survey_id: query = query.filter_by(survey_id=survey_id) if parent_id: query = query.filter_by(parent_id=parent_id) elif root is not None: query = query.filter_by(parent_id=None) if term: query = query.filter( model.QuestionNode.title.ilike('%{}%'.format(term))) if parent_not: query = query.filter( model.QuestionNode.parent_id != parent_not) if deleted: deleted = truthy(deleted) query = query.filter(model.QuestionNode.deleted == deleted) query = query.order_by(model.QuestionNode.seq, model.QuestionNode.deleted.desc()) query = self.paginate(query, optional=True) to_son = ToSon( # Fields to match from any visited object r'/ob_type$', r'/id$', r'/title$', r'/group$', r'/seq$', r'/deleted$', r'/n_measures$', r'/total_weight$', r'^/[0-9]+/error$', r'/parent$', r'/survey$', r'/survey/structure.*$', r'/survey/program$', # Descend into nested objects r'/[0-9]+$', ) if truthy(self.get_argument('desc', False)): to_son.add(r'</description$') if user_session.user.role == 'clerk': to_son.exclude(r'/total_weight$') sons = to_son(query.all()) # test use session to keep status #status_session = self.get_secure_cookie('status') #status_ids = status_session.decode('utf8') #status_array = status_ids.split(',') #for son in sons: # if son.id in status_array: # son['hideDetail'] = True ##################################### for son in sons: question=0 ids = [] pIds=[son.id] for pId in pIds: sIds = ( session.query(model.QuestionNode) .filter(model.QuestionNode.parent_id == pId)) if not sIds.first(): ids.append(pId) else: for cid in sIds: pIds.append(cid.id) qnodeMeasures = ( session.query(model.Measure, model.QnodeMeasure) .filter(model.Measure.id == model.QnodeMeasure.measure_id) .filter(model.QnodeMeasure.qnode_id.in_(ids))) for qnodeMeasure in qnodeMeasures: rt = ( session.query(model.ResponseType) .filter(model.ResponseType.id == qnodeMeasure.Measure.response_type_id).first()) seq = 0 for p in rt.parts: if "submeasure_seq" in p and p["submeasure_seq"] > question: seq = p["submeasure_seq"] else: question += 1 question = question + seq son['nQuestion'] = question # if son.id in status_array: # son['hideDetail'] = True self.set_header("Content-Type", "application/json") self.write(json_encode(sons)) self.finish()
def get(self, submission_id, measure_id): '''Get a single response.''' if not measure_id: self.query(submission_id) return version = self.get_argument('version', '') with model.session_scope() as session: user_session = self.get_user_session(session) response = (session.query(model.Response).get( (submission_id, measure_id))) if response: submission = response.submission dummy = False else: # Synthesise response so it can be returned. The session will # be rolled back to avoid actually making this change. submission = (session.query( model.Submission).get(submission_id)) if not submission: raise errors.MissingDocError("No such submission") qnode_measure = (session.query(model.QnodeMeasure).get( (submission.program_id, submission.survey_id, measure_id))) if not qnode_measure: raise errors.MissingDocError( "That survey has no such measure") response = model.Response( qnode_measure=qnode_measure, submission=submission, user_id=user_session.user.id, comment='', response_parts=[], variables={}, not_relevant=False, approval='draft', modified=datetime.datetime.fromtimestamp(0), ) dummy = True response_history = self.get_version(session, response, version) policy = user_session.policy.derive({ 'org': submission.organisation, 'submission': submission, 'surveygroups': submission.surveygroups, }) policy.verify('surveygroup_interact') policy.verify('response_view') to_son = ToSon( # Fields to match from any visited object r'/ob_type$', r'/id$', r'/title$', r'/name$', r'/description$', r'/seq$', r'/parent$', r'/parents$', r'/parents/[0-9]+$', # Fields to match from only the root object r'^/submission_id$', r'^/measure_id$', r'<^/comment$', r'^/response_parts.*$', r'^/not_relevant$', r'^/attachments$', r'^/audit_reason$', r'^/error$', r'^/approval$', r'^/version$', r'^/modified$', r'^/latest_modified$', r'^/quality$', # Descend r'/parent$', r'/measure$', r'/submission$', r'/user$', r'/qnode_measure$', r'/parents$', ) if dummy: to_son.add(r'!/user$') to_son.exclude( # The IDs of rnodes and responses are not part of the API r'^/id$', r'/parent/id$') if response_history is None: son = to_son(response) else: son = to_son(response_history) submission = (session.query(model.Submission).filter_by( id=response_history.submission_id).first()) measure = (session.query(model.Measure).filter_by( id=response_history.measure_id, program_id=submission.program_id).first()) qnode_measure = measure.get_qnode_measure(submission.survey_id) parent = model.ResponseNode.from_qnode(qnode_measure.qnode, submission) user = (session.query(model.AppUser).filter_by( id=response_history.user_id).first()) dummy_relations = { 'parent': parent, 'measure': measure, 'submission': submission, 'user': user, } son.update(to_son(dummy_relations)) # Always include the mtime of the most recent version. This is used # to avoid edit conflicts. dummy_relations = { 'latest_modified': response.modified, } son.update(to_son(dummy_relations)) def gather_variables(response): source_responses = { mv.source_qnode_measure: model.Response.from_measure(mv.source_qnode_measure, response.submission) for mv in response.qnode_measure.source_vars } source_variables = { source_qnode_measure: response and response.variables or {} for source_qnode_measure, response in source_responses.items() } variables_by_target = { mv.target_field: source_variables[mv.source_qnode_measure].get( mv.source_field) for mv in response.qnode_measure.source_vars } # Filter out blank/null variables return {k: v for k, v in variables_by_target.items() if v} son['sourceVars'] = gather_variables(response) # Explicit rollback to avoid committing dummy response. session.rollback() self.set_header("Content-Type", "application/json") self.write(json_encode(son)) self.finish()