def error_check(self, report_data): """ Check all input for errors. The called functions will throw exceptions themselves. We are not trying to catch them, but to trigger them before we try to commit everything to the DB. This should prevent inconsistent data in the DB. :param report_data: :return: """ section_api = SectionApi(autocommit=False) question_api = QuestionApi(autocommit=False) risk_factor_api = RiskFactorApi() # Database check if self.db_check(report_data) is not True: raise Exception(_('An unexpected error occurred.')) # Check for attributes if 'sections' not in report_data: raise RequiredAttributeMissing( _e['attr_missing'].format('sections')) cleaned_report = self.parse_input_data(report_data) # Check for duplicates (two sections with the same title) duplicate_sections = [] for section in report_data['sections']: if 'questions' not in section: raise RequiredAttributeMissing( _e['attr_missing'].format('questions')) # Check for attributes cleaned_section = section_api.parse_input_data(section) # Duplicate check if section['title'] not in duplicate_sections: duplicate_sections.append(section['title']) else: raise DatabaseItemAlreadyExists(_e['item_already_in'].format( 'Section', section['title'], 'Report', report_data['id'])) duplicate_questions = [] for question in section['questions']: # Check for attributes cleaned_question = question_api.parse_input_data(question) # Check for duplicates if question['question'] not in duplicate_questions: duplicate_questions.append(question['question']) else: raise DatabaseItemAlreadyExists( _e['item_already_in'].format('Question', question['question'], 'Section', section['id'])) # Check for answers # We must use the cleaned data, because 'answers' is not required, # and attempting to loop over None gets an error. for answer_id in cleaned_question['answers']: answer = question_api.get_answer(answer_id) # Check for risk_factor risk_factor = risk_factor_api.read( cleaned_question['risk_factor_id']) return True
def v_risk_factor_create(): form = RiskFactorCreateForm() form.lang.choices = [(l.id, l.lang) for l in lang_api.list()] if request.method == 'POST' and form.validate_on_submit(): a_api = RiskFactorApi() input_data = { 'risk_factor': form.risk_factor.data, 'value': form.value.data, 'lang_id': form.lang.data } try: a_api.create(input_data) except RequiredAttributeMissing: flash(_('Missing required form input.')) return redirect(url_for('admin.v_risk_factor_create')) except DatabaseItemAlreadyExists: flash(_('An risk_factor called {0} already exists.').format(input_data['risk_factor'])) return redirect(url_for('admin.v_risk_factor_create')) except Exception as e: flash(_('An unexpected error occurred.')) print(e) return redirect(url_for('admin.v_risk_factor_create')) else: flash(_('risk_factor created successfully.')) return redirect(url_for('admin.v_risk_factor_list')) else: return render_template('admin/risk_factor/create.html', action_url=url_for('admin.v_risk_factor_create'), form=form)
def v_risk_factor_delete(id): form = GenericDeleteForm() a_api = RiskFactorApi() try: existing_risk_factor = a_api.read(id) except DatabaseItemDoesNotExist: flash(_('This risk factor does not exist.')) return redirect(url_for('admin.v_risk_factor_list')) except Exception as e: flash(_('An unexpected error occurred.')) print(e) # TODO: logging return redirect(url_for('admin.v_risk_factor_list')) if request.method == 'POST' and form.validate_on_submit(): try: if a_api.delete(id) is True: flash(_('Risk factor removed.')) return redirect(url_for('admin.v_risk_factor_list')) else: flash(_('Risk factor could not be removed.')) return redirect(url_for('admin.v_risk_factor_list')) except Exception as e: flash(_('An unexpected error occurred.')) print(e) return redirect(url_for('admin.v_risk_factor_list')) else: return render_template('admin/generic/delete.html', action_url=url_for('admin.v_risk_factor_delete', id=id), item_type=str(existing_risk_factor), item_identifier=existing_risk_factor.risk_factor, form=form)
def test_update(self): en = LangApi().by_lang('en') r = RiskFactorApi().create({'risk_factor': 'Test', 'lang_id': en.id}) r_b = RiskFactorApi().update(r.id, { 'risk_factor': 'Foobar', 'lang_id': en.id }) assert r_b.risk_factor == 'Foobar' assert r_b == RiskFactorApi().read(r.id) assert RiskFactorApi().read(r.id).risk_factor == 'Foobar' self.assertIsInstance(r_b, RiskFactor)
def v_report_edit(report_id): a_report = ReportApi() a_answer = AnswerApi() a_risk_factor = RiskFactorApi() a_lang = LangApi() try: existing_report = a_report.read(report_id) except DatabaseItemDoesNotExist: flash(_('No report with id {0} exists.').format(report_id)) return redirect(url_for('admin.v_report_list')) # Do not use fallback_locale for the answers and risk_factor choices: if they don't exist, the administrator # must create them. report_lang = a_lang.read(existing_report.lang_id) return render_template('admin/report/edit.html', report=existing_report, all_risk_factors=a_risk_factor.by_lang(report_lang.lang), all_answers=a_answer.by_lang(report_lang.lang), languages=lang_api.list())
def test_create(self): en = LangApi().by_lang('en') r = RiskFactorApi().create({'risk_factor': 'Test', 'lang_id': en.id}) assert r in scoremodel.db.session assert r.value == 1 r_a = RiskFactorApi().create({ 'risk_factor': 'ValueTest', 'lang_id': en.id, 'value': 5 }) assert r_a.value == 5 self.assertRaises(DatabaseItemAlreadyExists, RiskFactorApi().create, { 'risk_factor': 'Test', 'lang_id': en.id }) self.assertIsInstance(r, RiskFactor)
def v_user_report_summary(user_id, user_report_id): if current_user.id != user_id: flash(_('You can only view your own reports.')) abort(403) user_report = user_report_api.read(user_report_id) question_answers = {} all_scores = {} for section in user_report.template.sections: all_scores[section.id] = 0 for question_answer in user_report.question_answers: question_answers[question_answer.question_id] = question_answer multiplication_factor = SectionApi().multiplication_factor( question_answer.question_template.section_id) all_scores[question_answer.question_template.section.id] += question_answer.score * \ multiplication_factor highest_unanswered = [] for question in ReportApi().questions_by_combined_weight( user_report.template.id): if question['question_id'] not in question_answers or question_answers[question['question_id']].score < \ question['max_score']: try: highest_unanswered.append(QuestionApi().read( question['question_id'])) except DatabaseItemDoesNotExist: pass if len(highest_unanswered) >= 5: visible_unanswered = highest_unanswered[:5] else: visible_unanswered = highest_unanswered benchmarks_by_question = {} for bm_r in user_report.template.benchmark_reports: for bm in bm_r.benchmarks: if bm.question_id in benchmarks_by_question: benchmarks_by_question[bm.question_id].append(bm) else: benchmarks_by_question[bm.question_id] = [bm] # Create a color-range for the risk_factors risk_factors = [r.risk_factor for r in RiskFactorApi().list()] colored_risk_factors = Color().range(risk_factors) return render_template( 'public/summary.html', report_template=user_report.template, user_report=user_report, user_report_creation_time='{:%Y-%m-%d %H:%M:%S}'.format( user_report.creation_time), highest_unanswered=visible_unanswered, benchmarks_by_question=benchmarks_by_question, question_answers_by_id=question_answers, colored_risk_factors=colored_risk_factors)
def test_complex(self): en = LangApi().by_lang('en') r = ReportApi().create({'title': 'Test', 'lang_id': en.id}) s = SectionApi().create({'title': 'Test', 'report_id': r.id}) ri = RiskFactorApi().create({'risk_factor': 'Test', 'lang_id': en.id}) a = AnswerApi().create({'answer': 'Test', 'lang_id': en.id}) q = QuestionApi().create({'question': 'Test', 'weight': 1, 'section_id': s.id, 'answers': [a.id], 'risk_factor_id': ri.id}) assert q in scoremodel.db.session self.assertIsInstance(q, Question)
def v_risk_factor_edit(id): form = RiskFactorCreateForm() form.lang.choices = [(l.id, l.lang) for l in lang_api.list()] a_api = RiskFactorApi() try: existing_risk_factor = a_api.read(risk_factor_id=id) except DatabaseItemDoesNotExist: flash(_('No risk_factor with id {0} exists.').format(id)) return redirect(url_for('admin.v_risk_factor_list')) except Exception as e: flash(_('An unexpected error occurred.')) return redirect(url_for('admin.v_risk_factor_list')) if request.method == 'POST' and form.validate_on_submit(): input_data = { 'risk_factor': form.risk_factor.data, 'value': form.value.data, 'lang_id': form.lang.data } try: a_api.update(risk_factor_id=id, input_data=input_data) except DatabaseItemDoesNotExist: flash(_('No risk_factor with id {0}.').format(id)) return redirect(url_for('admin.v_risk_factor_list')) except Exception as e: flash(_('An unexpected error occurred.')) return redirect(url_for('admin.v_risk_factor_list')) else: flash(_('Update successful.')) return redirect(url_for('admin.v_risk_factor_list')) else: # Fill in the values form.risk_factor.data = existing_risk_factor.risk_factor form.value.data = existing_risk_factor.value form.lang.data = existing_risk_factor.lang_id return render_template('admin/risk_factor/create.html', form=form, action_url=url_for('admin.v_risk_factor_edit', id=id))
def v_user_report_check(user_id, user_report_id): if current_user.id != user_id: flash(_('You can only view your own reports.')) abort(403) user_report = user_report_api.read(user_report_id) # Get all question_answers for this report and order them by question_id, so we can compare # question.answer.answer_id to question_answers['question_id'].answer_id question_answers = {} all_scores = {} highest_answers = {} for section in user_report.template.sections: all_scores[section.id] = 0 for question_answer in user_report.question_answers: question = question_answer.question_template question_answers[question.id] = question_answer multiplication_factor = SectionApi().multiplication_factor( question.section_id) all_scores[question. section_id] += question_answer.score * multiplication_factor sorted_answers = sorted(question.answers, key=lambda a: a.value, reverse=True) if len(sorted_answers) > 0: highest_answers[question.id] = sorted_answers[0].value else: highest_answers[question.id] = 0 benchmarks_by_section = user_report_api.benchmarks_by_section( user_report_id) # Create a color-range for the risk_factors risk_factors = [r.risk_factor for r in RiskFactorApi().list()] colored_risk_factors = Color().range(risk_factors) return render_template( 'public/report.html', report_template=user_report.template, user_report=user_report, user_report_creation_time='{:%Y-%m-%d %H:%M:%S}'.format( user_report.creation_time), question_answers=question_answers, all_scores=all_scores, benchmarks_by_section=benchmarks_by_section, highest_answers=highest_answers, colored_risk_factors=colored_risk_factors)
def v_user_report_section(user_id, user_report_id, section_id): section_api = SectionApi() if current_user.id != user_id: flash(_('You can only view your own reports.')) abort(403) return try: user_report = user_report_api.read(user_report_id) except DatabaseItemDoesNotExist as e: abort(404) return except Exception as e: flash(_('An unexpected error occurred.')) return redirect(url_for('site.v_index')) # Check whether current section is in this user_report if section_id not in [ section.id for section in user_report.template.sections ]: abort(404) return current_section = section_api.read(section_id) # Get all question_answers for this report and order them by question_id, so we can compare # question.answer.answer_id to question_answers['question_id'].answer_id question_answers = {} for question_answer in user_report.question_answers: question_answers[question_answer.question_id] = question_answer benchmarks_by_section = user_report_api.benchmarks_by_section( user_report_id) # Create a color-range for the risk_factors risk_factors = [r.risk_factor for r in RiskFactorApi().list()] colored_risk_factors = Color().range(risk_factors) return render_template('public/section.html', title=current_section.title, section=current_section, user_report_id=user_report_id, question_answers=question_answers, next_section=current_section.next_in_report, previous_section=current_section.previous_in_report, benchmarks_by_section=benchmarks_by_section, colored_risk_factors=colored_risk_factors)
def v_risk_factor_list(): a_api = RiskFactorApi() l_risk_factors = a_api.list() return render_template('admin/generic/list.html', items=l_risk_factors, item_type='risk_factor', name='risk_factor', canonical_name=_('Risk Factor'))
def __init__(self, question_id=None, autocommit=True): self.question_id = question_id self.a_section = scoremodel.modules.api.section.SectionApi() self.a_risk_factor = RiskFactorApi() self.autocommit = autocommit
class QuestionApi(GenericApi): simple_attributes = [ 'question', 'context', 'risk', 'example', 'weight', 'order_in_section', 'section_id', 'action', 'risk_factor_id' ] complex_params = ['answers'] # These should be a list in input_data possible_params = [ 'question', 'context', 'risk', 'example', 'weight', 'order_in_section', 'action', 'risk_factor_id', 'answers', 'section_id' ] required_params = ['question', 'weight', 'section_id'] def __init__(self, question_id=None, autocommit=True): self.question_id = question_id self.a_section = scoremodel.modules.api.section.SectionApi() self.a_risk_factor = RiskFactorApi() self.autocommit = autocommit def create(self, input_data): """ Create a new question. The data input variable contains all the attributes for the "question" entity in the database as a dict. For simple attributes, this is a string or integer value, but for actions, answers and risk factors, it is a dictionary containing the attributes for the respective entity in the database. The function will fail when a question with the same "question" attribute already exists in the same section. Submitting actions, answers or risk factors that already exist will not result in an error. The function returns the question sqlalchemy object. :param input_data :return: """ cleaned_data = self.parse_input_data(input_data) # Check whether this question already exists if self.db_exists(cleaned_data['question'], cleaned_data['section_id']): raise DatabaseItemAlreadyExists(_e['item_exists'].format( Question, cleaned_data['question'])) risk_factor = None if cleaned_data['risk_factor_id'] is not None: risk_factor = self.a_risk_factor.read( cleaned_data['risk_factor_id']) created_question = self.db_create( cleaned_data, self.a_section.read(cleaned_data['section_id']), self.db_get_answers(cleaned_data['answers']), risk_factor) return created_question def read(self, question_id): """ Get a question from the database by its ID :param question_id: :return: """ existing_question = Question.query.filter( Question.id == question_id).first() if existing_question is None: raise DatabaseItemDoesNotExist(_e['item_not_exists'].format( Question, question_id)) return existing_question def update(self, question_id, input_data): """ Update a question identified by question_id. The variable input_data must contain all variables, both those that are to be changed and those that remain the same. If you only send the changed ones, the others will be set to None. It follows the same logic as self.create(), but it doesn't die when the question already exists (but it does when it doesn't). :param question_id: :param input_data: :param section_id: :return: """ existing_question = self.read(question_id) cleaned_data = self.parse_input_data(input_data) risk_factor = None if cleaned_data['risk_factor_id'] is not None: risk_factor = self.a_risk_factor.read( cleaned_data['risk_factor_id']) updated_question = self.db_update( existing_question, cleaned_data, self.a_section.read(cleaned_data['section_id']), self.db_get_answers(cleaned_data['answers']), risk_factor) return updated_question def delete(self, question_id): """ Delete a question by its ID. Fails when it doesn't exist. :param question_id: :return: """ existing_question = self.read(question_id) db.session.delete(existing_question) self.store() return True def parse_input_data(self, input_data): """ Clean the input data dict: remove all non-supported attributes and check whether all the required parameters have been filled. All missing parameters are set to None :param input_data: :return: """ # Solve legacy applications that use risk_factors (a list) instead of risk_factor (an object). Multiple # risk factors for one question are no longer supported. if 'risk_factors' in input_data: raise RequiredAttributeMissing( _('Error: risk_factors was provided!')) return self.clean_input_data(Question, input_data, self.possible_params, self.required_params, self.complex_params) def get_answer(self, answer_id): # Check whether this answer exists a_answer = AnswerApi() o_answer = a_answer.read(answer_id) return o_answer def list(self): raise MethodNotImplemented def remove_answers(self, question_entity): for answer in question_entity.answers: question_entity.answers.remove(answer) self.store() return question_entity def query(self, question_question): """ Select a question by its name ("question"): this attribute is unique :param question_question: :return: """ existing_question = Question.query.filter( Question.question == question_question).first() if not existing_question: raise DatabaseItemDoesNotExist(_e['item_not_exists'].format( Question, question_question)) return existing_question def store(self): if self.autocommit: db.session.commit() def db_get_answers(self, answer_ids): """ From a list of answer ids, get the list of answer database objects :param answer_ids: :return: """ answers = [] for answer_id in answer_ids: answers.append(self.get_answer(answer_id)) return answers def db_update(self, existing_question, cleaned_data, section, answers, risk_factor): """ See self.db_create() why this function exists. :param existing_question: :param cleaned_data: :param section: :param answers: :param risk_factor: :return: """ existing_question = self.update_simple_attributes( existing_question, self.simple_attributes, cleaned_data) # Update the section existing_question.section = section # Update answers existing_question = self.remove_answers(existing_question) if answers is not None and type(answers) is list: for answer in answers: existing_question.answers.append(answer) # Update risk factors if risk_factor is not None: existing_question.risk_factor = risk_factor # Set maximum_score sorted_answers = sorted(existing_question.answers, key=lambda answer: answer.value, reverse=True) if len(sorted_answers) > 0: existing_question.maximum_score = sorted_answers[0].value * existing_question.weight *\ existing_question.risk_factor.value else: existing_question.maximum_score = 0 self.store() # Update the maximum_score of the parent section scoremodel.modules.api.section.SectionApi().set_maximum_score( existing_question.section_id) return existing_question def db_create(self, cleaned_data, section, answers=None, risk_factor=None): """ Create a question. This is a collection of all the write actions to the database, so we can wrap them in a transaction. We have to separate the "read" (query) actions as SQLAlchemy commits everything before querying (http://docs.sqlalchemy.org/en/latest/orm/session_basics.html). :param cleaned_data: :param section: :param answers: :param risk_factor: :return: """ new_question = Question(question=cleaned_data['question'], context=cleaned_data['context'], risk=cleaned_data['risk'], example=cleaned_data['example'], weight=cleaned_data['weight'], order=cleaned_data['order_in_section'], action=cleaned_data['action']) db.session.add(new_question) # Add to the section new_question.section = section # Add the answers if answers is not None and type(answers) is list: for answer in answers: new_question.answers.append(answer) # Add the risk factor if risk_factor is not None: new_question.risk_factor = risk_factor # Store everything in the database self.store() # Set maximum_score sorted_answers = sorted(new_question.answers, key=lambda a: a.value, reverse=True) if len(sorted_answers) > 0: new_question.maximum_score = sorted_answers[ 0].value * new_question.weight * new_question.risk_factor.value else: new_question.maximum_score = 0 self.store() # Update the maximum_score of the parent section scoremodel.modules.api.section.SectionApi().set_maximum_score( new_question.section_id) # Return the question object return new_question def db_exists(self, question_question, section_id): """ :param question_question: :param section_id: :return: """ existing_question = Question.query.filter( and_(Question.question == question_question, Question.section_id == section_id)).first() if existing_question: return True return False def maximum_score(self, question_id): """ Compute the maximum score for a question. value of highest answer * value of the risk_factor * weight of the question :param question_id: :return: """ existing_question = self.read(question_id) return existing_question.maximum_score
def test_delete(self): en = LangApi().by_lang('en') r = RiskFactorApi().create({'risk_factor': 'Test', 'lang_id': en.id}) assert RiskFactorApi().delete(r.id) is True assert r not in scoremodel.db.session
def test_read(self): en = LangApi().by_lang('en') r = RiskFactorApi().create({'risk_factor': 'Test', 'lang_id': en.id}) assert r == RiskFactorApi().read(r.id)