def ask_brief_clarification_question(brief_id): brief = get_brief(data_api_client, brief_id, allowed_statuses=['live']) if brief['clarificationQuestionsAreClosed']: abort(404) if not is_supplier_eligible_for_brief(data_api_client, current_user.supplier_id, brief): return _render_not_eligible_for_brief_error_page( brief, clarification_question=True) form = AskClarificationQuestionForm(brief) if form.validate_on_submit(): send_brief_clarification_question(data_api_client, brief, form.clarification_question.data) flash(CLARIFICATION_QUESTION_SENT_MESSAGE.format(brief=brief), "success") errors = govuk_errors(get_errors_from_wtform(form)) return render_template( "briefs/clarification_question.html", brief=brief, form=form, errors=errors, ), 200 if not errors else 400
def update_brief_submission(framework_slug, lot_slug, brief_id, section_id, question_id): get_framework_and_lot(framework_slug, lot_slug, data_api_client, allowed_statuses=['live'], must_allow_brief=True) brief = data_api_client.get_brief(brief_id)["briefs"] if not is_brief_correct(brief, framework_slug, lot_slug, current_user.id) or not brief_can_be_edited(brief): abort(404) content = content_loader.get_manifest( brief['frameworkSlug'], 'edit_brief').filter({'lot': brief['lotSlug']}) section = content.get_section(section_id) if section is None or not section.editable: abort(404) question = section.get_question(question_id) if not question: abort(404) update_data = question.get_data(request.form) try: data_api_client.update_brief(brief_id, update_data, updated_by=current_user.email_address, page_questions=question.form_fields) except HTTPError as e: update_data = section.unformat_data(update_data) errors = govuk_errors(section.get_error_messages(e.message)) # we need the brief_id to build breadcrumbs and the update_data to fill in the form. brief.update(update_data) return render_template("buyers/edit_brief_question.html", brief=brief, section=section, question=question, errors=errors), 400 if section.has_summary_page: return redirect( url_for(".view_brief_section_summary", framework_slug=brief['frameworkSlug'], lot_slug=brief['lotSlug'], brief_id=brief['id'], section_slug=section.slug)) return redirect( url_for(".view_brief_overview", framework_slug=brief['frameworkSlug'], lot_slug=brief['lotSlug'], brief_id=brief['id']))
def update_service(service_id, section_id, question_slug=None): service = data_api_client.get_service(service_id) if service is None: abort(404) service = service['services'] # we don't actually need the framework here; using this to 404 if framework for the service is not live # TODO remove `expired` from below. It's a temporary fix to allow access to DOS2 as it's expired. get_framework_or_404(data_api_client, service['frameworkSlug'], allowed_statuses=['live', 'expired']) content = content_loader.get_manifest( service['frameworkSlug'], 'edit_service_as_admin', ).filter(service, inplace_allowed=True) section = content.get_section(section_id) if question_slug is not None: # Overwrite section with single question section for 'question per page' editing. section = section.get_question_as_section(question_slug) if section is None or not section.editable: abort(404) errors = None posted_data = section.get_data(request.form) uploaded_documents, document_errors = upload_service_documents( s3.S3(current_app.config['DM_S3_DOCUMENT_BUCKET']), 'documents', current_app.config['DM_ASSETS_URL'], service, request.files, section) if document_errors: errors = section.get_error_messages(document_errors) else: posted_data.update(uploaded_documents) if not errors and section.has_changes_to_save(service, posted_data): try: data_api_client.update_service( service_id, posted_data, current_user.email_address, user_role='admin', ) except HTTPError as e: errors = section.get_error_messages(e.message) if errors: return render_template( "edit_section.html", section=section, service_data=section.unformat_data(dict(service, **posted_data)), service_id=service_id, errors=govuk_errors(errors), ), 400 return redirect(url_for(".view_service", service_id=service_id))
def award_brief_details(framework_slug, lot_slug, brief_id, brief_response_id): get_framework_and_lot( framework_slug, lot_slug, data_api_client, allowed_statuses=['live', 'expired'], must_allow_brief=True, ) brief = data_api_client.get_brief(brief_id)["briefs"] if not is_brief_correct(brief, framework_slug, lot_slug, current_user.id): abort(404) brief_response = data_api_client.get_brief_response( brief_response_id)["briefResponses"] if not brief_response.get( 'status') == 'pending-awarded' or not brief_response.get( 'briefId') == brief.get('id'): abort(404) # get questions content = content_loader.get_manifest(brief['frameworkSlug'], 'award_brief') section_id = content.get_next_editable_section_id() section = content.get_section(section_id) if request.method == "POST": award_data = section.get_data(request.form) try: data_api_client.update_brief_award_details( brief_id, brief_response_id, award_data, updated_by=current_user.email_address) except HTTPError as e: award_data = section.unformat_data(award_data) errors = govuk_errors(section.get_error_messages(e.message)) return render_template("buyers/award_details.html", brief=brief, data=award_data, errors=errors, pending_brief_response=brief_response, section=section), 400 flash(BRIEF_UPDATED_MESSAGE.format(brief=brief), "success") return redirect(url_for(".buyer_dos_requirements")) return render_template("buyers/award_details.html", brief=brief, data={}, pending_brief_response=brief_response, section=section), 200
def add_supplier_question(framework_slug, lot_slug, brief_id): get_framework_and_lot(framework_slug, lot_slug, data_api_client, allowed_statuses=['live', 'expired'], must_allow_brief=True) brief = data_api_client.get_brief(brief_id)["briefs"] if not is_brief_correct(brief, framework_slug, lot_slug, current_user.id, allowed_statuses=['live']): abort(404) content = content_loader.get_manifest(brief['frameworkSlug'], "clarification_question").filter({}) section = content.get_section(content.get_next_editable_section_id()) update_data = section.get_data(request.form) errors = {} status_code = 200 if request.method == "POST": try: data_api_client.add_brief_clarification_question( brief_id, update_data['question'], update_data['answer'], current_user.email_address) return redirect( url_for('.supplier_questions', framework_slug=brief['frameworkSlug'], lot_slug=brief['lotSlug'], brief_id=brief['id'])) except HTTPError as e: if e.status_code != 400: raise brief.update(update_data) errors = govuk_errors(section.get_error_messages(e.message)) status_code = 400 return render_template("buyers/edit_brief_question.html", brief=brief, section=section, question=section.questions[0], button_label="Publish question and answer", errors=errors), status_code
def process_login(): form = LoginForm() next_url = request.args.get('next') if form.validate_on_submit(): user_json = data_api_client.authenticate_user(form.email_address.data, form.password.data) if not user_json: current_app.logger.info( "login.fail: failed to log in {email_hash}", extra={'email_hash': hash_string(form.email_address.data)}) errors = govuk_errors({ "email_address": { "message": "Enter your email address", "input_name": "email_address", }, "password": { "message": "Enter your password", "input_name": "password", }, }) return render_template( "auth/login.html", form=form, errors=errors, error_summary_description_text=NO_ACCOUNT_MESSAGE, next=next_url), 403 user = User.from_json(user_json) login_user(user) current_app.logger.info("login.success: role={role} user={email_hash}", extra={ 'role': user.role, 'email_hash': hash_string(form.email_address.data) }) return redirect_logged_in_user(next_url) else: errors = get_errors_from_wtform(form) return render_template("auth/login.html", form=form, errors=errors, next=next_url), 400
def create_new_brief(framework_slug, lot_slug): framework, lot = get_framework_and_lot(framework_slug, lot_slug, data_api_client, allowed_statuses=['live'], must_allow_brief=True) content = content_loader.get_manifest(framework_slug, 'edit_brief').filter( {'lot': lot['slug']}) section = content.get_section(content.get_next_editable_section_id()) update_data = section.get_data(request.form) try: brief = data_api_client.create_brief( framework_slug, lot_slug, current_user.id, update_data, updated_by=current_user.email_address, page_questions=section.get_field_names())["briefs"] except HTTPError as e: update_data = section.unformat_data(update_data) errors = govuk_errors(section.get_error_messages(e.message)) return render_template("buyers/create_brief_question.html", data=update_data, brief={}, framework=framework, lot=lot, section=section, question=section.questions[0], errors=errors), 400 return redirect( url_for(".view_brief_overview", framework_slug=framework_slug, lot_slug=lot_slug, brief_id=brief['id']))
def test_govuk_errors(dm_errors, expected_output): assert govuk_errors(dm_errors) == expected_output
def edit_service_submission(framework_slug, lot_slug, service_id, section_id, question_slug=None): """ Also accepts URL parameter `force_continue_button` which will allow rendering of a 'Save and continue' button, used for when copying services. """ framework, lot = get_framework_and_lot_or_404(data_api_client, framework_slug, lot_slug, allowed_statuses=['open']) # Suppliers must have registered interest in a framework before they can edit draft services if not get_supplier_framework_info(data_api_client, framework_slug): abort(404) force_return_to_summary = framework['framework'] == "digital-outcomes-and-specialists" force_continue_button = request.args.get('force_continue_button') next_question = None try: draft = data_api_client.get_draft_service(service_id)['services'] except HTTPError as e: abort(e.status_code) if draft['lotSlug'] != lot_slug or draft['frameworkSlug'] != framework_slug: abort(404) if not is_service_associated_with_supplier(draft): abort(404) content = content_loader.get_manifest(framework_slug, 'edit_submission').filter(draft, inplace_allowed=True) section = content.get_section(section_id) if section and (question_slug is not None): next_question = section.get_question_by_slug(section.get_next_question_slug(question_slug)) section = section.get_question_as_section(question_slug) if section is None or not section.editable: abort(404) errors = None if request.method == "POST": update_data = section.get_data(request.form) if request.files: uploader = s3.S3(current_app.config['DM_SUBMISSIONS_BUCKET']) documents_url = url_for('.dashboard', _external=True) + '/assets/' # This utils method filters out any empty documents and validates against service document rules uploaded_documents, document_errors = upload_service_documents( uploader, 'submissions', documents_url, draft, request.files, section, public=False) if document_errors: errors = section.get_error_messages(document_errors, question_descriptor_from="question") else: update_data.update(uploaded_documents) if not errors and section.has_changes_to_save(draft, update_data): try: data_api_client.update_draft_service( service_id, update_data, current_user.email_address, page_questions=section.get_field_names() ) except HTTPError as e: update_data = section.unformat_data(update_data) errors = govuk_errors(section.get_error_messages(e.message, question_descriptor_from="question")) if not errors: if next_question and not force_return_to_summary: return redirect(url_for(".edit_service_submission", framework_slug=framework['slug'], lot_slug=draft['lotSlug'], service_id=service_id, section_id=section_id, question_slug=next_question.slug)) else: return redirect(url_for(".view_service_submission", framework_slug=framework['slug'], lot_slug=draft['lotSlug'], service_id=service_id, _anchor=section_id)) update_data.update( (k, draft[k]) for k in ('serviceName', 'lot', 'lotName',) if k in draft and k not in update_data ) service_data = update_data # fall through to regular GET path to display errors else: service_data = section.unformat_data(draft) session_timeout = displaytimeformat(datetime.utcnow() + timedelta(hours=1)) return render_template( "services/edit_submission_section.html", section=section, framework=framework, lot=lot, next_question=next_question, service_data=service_data, service_id=service_id, force_return_to_summary=force_return_to_summary, force_continue_button=force_continue_button, session_timeout=session_timeout, errors=errors, )
def edit_brief_response(brief_id, brief_response_id, question_id=None): edit_single_question_flow = request.endpoint.endswith( '.edit_single_question') brief = get_brief(data_api_client, brief_id, allowed_statuses=['live']) brief_response = data_api_client.get_brief_response( brief_response_id)['briefResponses'] if brief_response['briefId'] != brief['id'] or brief_response[ 'supplierId'] != current_user.supplier_id: abort(404) if not is_supplier_eligible_for_brief(data_api_client, current_user.supplier_id, brief): return _render_not_eligible_for_brief_error_page(brief) framework, lot = get_framework_and_lot( data_api_client, brief['frameworkSlug'], brief['lotSlug'], allowed_statuses=['live', 'expired']) max_day_rate = None role = brief.get('specialistRole') if role: brief_service = data_api_client.find_services( supplier_id=current_user.supplier_id, framework=brief['frameworkSlug'], status="published", lot=brief["lotSlug"], )["services"][0] max_day_rate = brief_service.get(role + "PriceMax") content = content_loader.get_manifest(brief['frameworkSlug'], 'edit_brief_response').filter({ 'lot': lot['slug'], 'brief': brief, 'max_day_rate': max_day_rate }) section = content.get_section(content.get_next_editable_section_id()) if section is None or not section.editable: abort(404) # If a question in a brief is optional and is unanswered by the buyer, the brief will have the key but will have no # data. The question will be skipped in the brief response flow (see below). If a user attempts to access the # question by directly visiting the url, this check will return a 404. It has been created specifically for nice to # have requirements, and works because briefs and responses share the same key for this question/response. if question_id in brief.keys() and not brief[question_id]: abort(404) # If a question is to be skipped in the normal flow (due to the reason above), we update the next_question_id. next_question_id = section.get_next_question_id(question_id) if next_question_id in brief.keys() and not brief[next_question_id]: next_question_id = section.get_next_question_id(next_question_id) def redirect_to_next_page(): return redirect( url_for('.edit_brief_response', brief_id=brief_id, brief_response_id=brief_response_id, question_id=next_question_id)) # If no question_id in url then redirect to first question if question_id is None: return redirect_to_next_page() question = section.get_question(question_id) if question is None: abort(404) # Unformat brief response into data for form service_data = question.unformat_data(brief_response) status_code = 200 errors = {} if request.method == 'POST': try: data_api_client.update_brief_response(brief_response_id, question.get_data( request.form), current_user.email_address, page_questions=[question.id]) except HTTPError as e: errors = govuk_errors(question.get_error_messages(e.message)) status_code = 400 service_data = question.unformat_data( question.get_data(request.form)) else: if next_question_id and not edit_single_question_flow: return redirect_to_next_page() else: if edit_single_question_flow: flash(APPLICATION_UPDATED_MESSAGE, "success") return redirect( url_for('.check_brief_response_answers', brief_id=brief_id, brief_response_id=brief_response_id)) previous_question_id = section.get_previous_question_id(question_id) # Skip previous question if the brief has no nice to have requirements if previous_question_id in brief.keys( ) and not brief[previous_question_id]: previous_question_id = section.get_previous_question_id( previous_question_id) previous_question_url = None if previous_question_id: previous_question_url = url_for('.edit_brief_response', brief_id=brief_id, brief_response_id=brief_response_id, question_id=previous_question_id) return render_template("briefs/edit_brief_response_question.html", brief=brief, errors=errors, is_last_page=False if next_question_id else True, previous_question_url=previous_question_url, question=question, service_data=service_data), status_code