def delete_communication(framework_slug, comm_type, filepath): if comm_type not in _comm_types: abort(404) framework = get_framework_or_404(data_api_client, framework_slug) if request.method == "POST": if "confirm" not in request.form: abort(400, "Expected 'confirm' parameter in POST request") communications_bucket = s3.S3( current_app.config['DM_COMMUNICATIONS_BUCKET']) full_path = _get_comm_type_root(framework_slug, comm_type) / filepath # do this check ourselves - deleting an object in S3 silently has no effect, forwarding this behaviour to the # user is a confusing thing to do if not communications_bucket.path_exists(str(full_path)): abort(404, f"{filepath} not present in S3 bucket") communications_bucket.delete_key(str(full_path)) flash( f"{comm_type.capitalize()} ‘{filepath}’ was deleted for {framework['name']}." ) return redirect( url_for('.manage_communications', framework_slug=framework_slug)) return render_template( 'confirm_communications_deletion.html', framework=framework, comm_type=comm_type, filepath=filepath, )
def manage_communications(framework_slug): communications_bucket = s3.S3( current_app.config['DM_COMMUNICATIONS_BUCKET']) framework = get_framework_or_404(data_api_client, framework_slug) # generate a dict of comm_type: seq of s3 object dicts comm_type_objs = { comm_type: tuple({ **bucket_item, # annotate on to object dicts their paths relative to comm_type_root "rel_path": PurePath(bucket_item["path"]).relative_to( _get_comm_type_root(framework_slug, comm_type)), } for bucket_item in communications_bucket.list( str(_get_comm_type_root(framework_slug, comm_type)), load_timestamps=True, )) for comm_type in _comm_types } return render_template( 'manage_communications.html', comm_type_objs=comm_type_objs, framework=framework, )
def list_countersigned_agreement_file(supplier_id, framework_slug): supplier = data_api_client.get_supplier(supplier_id)['suppliers'] framework = data_api_client.get_framework(framework_slug)['frameworks'] supplier_framework = data_api_client.get_supplier_framework_info(supplier_id, framework_slug)['frameworkInterest'] if not supplier_framework['onFramework'] or supplier_framework['agreementStatus'] in (None, 'draft'): abort(404) agreements_bucket = s3.S3( current_app.config['DM_AGREEMENTS_BUCKET'], endpoint_url=current_app.config.get("DM_S3_ENDPOINT_URL") ) countersigned_agreement_document = agreements_bucket.get_key(supplier_framework.get('countersignedPath')) remove_countersigned_agreement_confirm = convert_to_boolean(request.args.get('remove_countersigned_agreement')) countersigned_agreement = [] if countersigned_agreement_document: last_modified = datetimeformat(parse_date(countersigned_agreement_document['last_modified'])) document_name = degenerate_document_path_and_return_doc_name(supplier_framework.get('countersignedPath')) countersigned_agreement = [{"last_modified": last_modified, "document_name": document_name}] return render_template( "suppliers/upload_countersigned_agreement.html", supplier=supplier, framework=framework, countersigned_agreement=countersigned_agreement, remove_countersigned_agreement_confirm=remove_countersigned_agreement_confirm )
def upload_countersigned_agreement_file(supplier_code, framework_slug): agreements_bucket = s3.S3(current_app.config['DM_AGREEMENTS_BUCKET']) errors = {} if request.files.get('countersigned_agreement'): the_file = request.files['countersigned_agreement'] if not file_is_pdf(the_file): errors['countersigned_agreement'] = 'not_pdf' if 'countersigned_agreement' not in errors.keys(): filename = get_agreement_document_path( framework_slug, supplier_code, COUNTERSIGNED_AGREEMENT_FILENAME) agreements_bucket.save(filename, the_file) data_api_client.create_audit_event( audit_type=AuditTypes.upload_countersigned_agreement, user=current_user.email_address, object_type='suppliers', object_id=supplier_code, data={'upload_countersigned_agreement': filename}) flash('countersigned_agreement', 'upload_countersigned_agreement') if len(errors) > 0: for category, message in errors.items(): flash(category, message) return redirect( url_for('.list_countersigned_agreement_file', supplier_code=supplier_code, framework_slug=framework_slug))
def remove_countersigned_agreement_file(supplier_id, framework_slug): supplier_framework = data_api_client.get_supplier_framework_info( supplier_id, framework_slug)['frameworkInterest'] document = supplier_framework.get('countersignedPath') agreements_bucket = s3.S3(current_app.config['DM_AGREEMENTS_BUCKET']) if request.method == 'GET': return redirect( url_for('.list_countersigned_agreement_file', supplier_id=supplier_id, framework_slug=framework_slug) + "?remove_countersigned_agreement=true") if request.method == 'POST': # Remove path first - as we don't want path to exist in DB with no corresponding file in S3 # But an orphaned file in S3 wouldn't be so bad data_api_client.update_framework_agreement( supplier_framework['agreementId'], {"countersignedAgreementPath": None}, current_user.email_address) agreements_bucket.delete_key(document) data_api_client.create_audit_event( audit_type=AuditTypes.delete_countersigned_agreement, user=current_user.email_address, object_type='suppliers', object_id=supplier_id, data={'upload_countersigned_agreement': document}) return redirect( url_for('.list_countersigned_agreement_file', supplier_id=supplier_id, framework_slug=framework_slug))
def view_signed_agreement(supplier_id, framework_slug): # not properly validating this - all we do is pass it through next_status = request.args.get("next_status") supplier = data_api_client.get_supplier(supplier_id)['suppliers'] framework = data_api_client.get_framework(framework_slug)['frameworks'] if not framework.get('frameworkAgreementVersion'): abort(404) supplier_framework = data_api_client.get_supplier_framework_info( supplier_id, framework_slug)['frameworkInterest'] if not supplier_framework.get('agreementReturned'): abort(404) lot_names = [] if framework["status"] in ("live", "expired"): # If the framework is live or expired we don't need to filter drafts, we only care about successful services service_iterator = data_api_client.find_services_iter( supplier_id=supplier_id, framework=framework_slug) for service in service_iterator: if service['lotName'] not in lot_names: lot_names.append(service['lotName']) else: # If the framework has not yet become live we need to filter out unsuccessful services service_iterator = data_api_client.find_draft_services_iter( supplier_id=supplier_id, framework=framework_slug) for service in service_iterator: if service["status"] == "submitted" and service[ 'lotName'] not in lot_names: lot_names.append(service['lotName']) agreements_bucket = s3.S3(current_app.config['DM_AGREEMENTS_BUCKET']) if framework_slug in OLD_COUNTERSIGNING_FLOW_FRAMEWORKS: is_e_signature_flow = False # Fetch path to supplier's signature page path = supplier_framework['agreementPath'] template = "suppliers/view_signed_agreement.html" else: is_e_signature_flow = True # Fetch path to combined countersigned agreement, if available path = supplier_framework.get('countersignedPath') template = "suppliers/view_esignature_agreement.html" url = get_signed_url(agreements_bucket, path, current_app.config['DM_ASSETS_URL']) if path else "" agreement_ext = get_extension(path) if path else "" if not url: current_app.logger.info(f'No agreement file found for {path}') return render_template( template, company_details=get_company_details_from_supplier(supplier), supplier=supplier, framework=framework, supplier_framework=supplier_framework, lot_names=list(sorted(lot_names)), agreement_url=url, agreement_ext=agreement_ext, next_status=next_status, is_e_signature_flow=is_e_signature_flow)
def download_supplier_file(framework_slug, filepath): uploader = s3.S3(current_app.config['DM_COMMUNICATIONS_BUCKET']) url = get_signed_document_url( uploader, "{}/communications/{}".format(framework_slug, filepath)) if not url: abort(404) return redirect(url)
def signature_upload(framework_slug): framework = get_framework(data_api_client, framework_slug) return_supplier_framework_info_if_on_framework_or_abort( data_api_client, framework_slug) agreements_bucket = s3.S3(current_app.config['DM_AGREEMENTS_BUCKET']) signature_page = get_most_recently_uploaded_agreement_file_or_none( agreements_bucket, framework_slug) upload_error = None if request.method == 'POST': # No file chosen for upload and file already exists on s3 so can use existing and progress if not request.files['signature_page'].filename and signature_page: return redirect( url_for(".contract_review", framework_slug=framework_slug)) if not file_is_image( request.files['signature_page']) and not file_is_pdf( request.files['signature_page']): upload_error = "The file must be a PDF, JPG or PNG" elif not file_is_less_than_5mb(request.files['signature_page']): upload_error = "The file must be less than 5MB" elif file_is_empty(request.files['signature_page']): upload_error = "The file must not be empty" if not upload_error: upload_path = get_agreement_document_path( framework_slug, current_user.supplier_code, '{}{}'.format( SIGNED_AGREEMENT_PREFIX, get_extension(request.files['signature_page'].filename))) agreements_bucket.save(upload_path, request.files['signature_page'], acl='private') session['signature_page'] = request.files[ 'signature_page'].filename data_api_client.create_audit_event( audit_type=AuditTypes.upload_signed_agreement, user=current_user.email_address, object_type="suppliers", object_id=current_user.supplier_code, data={ "upload_signed_agreement": request.files['signature_page'].filename, "upload_path": upload_path }) return redirect( url_for(".contract_review", framework_slug=framework_slug)) status_code = 400 if upload_error else 200 return render_template_with_csrf( "frameworks/signature_upload.html", status_code=status_code, framework=framework, signature_page=signature_page, upload_error=upload_error, )
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 upload_countersigned_agreement_file(supplier_id, framework_slug): supplier_framework = data_api_client.get_supplier_framework_info( supplier_id, framework_slug)['frameworkInterest'] if not supplier_framework['onFramework'] or supplier_framework[ 'agreementStatus'] in (None, 'draft'): abort(404) agreement_id = supplier_framework['agreementId'] agreements_bucket = s3.S3(current_app.config['DM_AGREEMENTS_BUCKET']) errors = {} if request.files.get('countersigned_agreement'): the_file = request.files['countersigned_agreement'] if not file_is_pdf(the_file): errors['countersigned_agreement'] = 'not_pdf' flash(COUNTERSIGNED_AGREEMENT_NOT_PDF_MESSAGE) if 'countersigned_agreement' not in errors.keys(): supplier_name = supplier_framework.get( 'declaration', {}).get('nameOfOrganisation') if not supplier_name: supplier_name = data_api_client.get_supplier( supplier_id)['suppliers']['name'] if supplier_framework['agreementStatus'] not in [ 'approved', 'countersigned' ]: data_api_client.approve_agreement_for_countersignature( agreement_id, current_user.email_address, current_user.id) path = generate_timestamped_document_upload_path( framework_slug, supplier_id, 'agreements', COUNTERPART_FILENAME) download_filename = generate_download_filename( supplier_id, COUNTERPART_FILENAME, supplier_name) agreements_bucket.save(path, the_file, acl='bucket-owner-full-control', move_prefix=None, download_filename=download_filename) data_api_client.update_framework_agreement( agreement_id, {"countersignedAgreementPath": path}, current_user.email_address) data_api_client.create_audit_event( audit_type=AuditTypes.upload_countersigned_agreement, user=current_user.email_address, object_type='suppliers', object_id=supplier_id, data={'upload_countersigned_agreement': path}) flash(UPLOAD_COUNTERSIGNED_AGREEMENT_MESSAGE) return redirect( url_for('.list_countersigned_agreement_file', supplier_id=supplier_id, framework_slug=framework_slug))
def service_submission_document(framework_slug, supplier_id, document_name): if current_user.supplier_id != supplier_id: abort(404) uploader = s3.S3(current_app.config['DM_SUBMISSIONS_BUCKET']) s3_url = get_signed_document_url(uploader, "{}/submissions/{}/{}".format(framework_slug, supplier_id, document_name)) if not s3_url: abort(404) return redirect(s3_url)
def download_supplier_user_research_report(framework_slug): reports_bucket = s3.S3(current_app.config['DM_REPORTS_BUCKET']) path = "{framework_slug}/reports/user-research-suppliers-on-{framework_slug}.csv" url = get_signed_url(reports_bucket, path.format(framework_slug=framework_slug), current_app.config['DM_ASSETS_URL']) if not url: abort(404) return redirect(url)
def download_agreement_file(supplier_id, framework_slug, document_name): supplier_framework = data_api_client.get_supplier_framework_info(supplier_id, framework_slug)['frameworkInterest'] if supplier_framework is None or not supplier_framework.get("declaration"): abort(404) agreements_bucket = s3.S3( current_app.config['DM_AGREEMENTS_BUCKET'], endpoint_url=current_app.config.get("DM_S3_ENDPOINT_URL") ) path = get_document_path(framework_slug, supplier_id, 'agreements', document_name) url = get_signed_url(agreements_bucket, path, current_app.config['DM_ASSETS_URL']) if not url: abort(404) return redirect(url)
def download_communication(framework_slug, comm_type, filepath): if comm_type not in _comm_types: abort(404) # ensure this is a real framework get_framework_or_404(data_api_client, framework_slug) bucket = s3.S3(current_app.config['DM_COMMUNICATIONS_BUCKET']) full_path = _get_comm_type_root(framework_slug, comm_type) / filepath url = get_signed_url(bucket, str(full_path), current_app.config["DM_ASSETS_URL"]) if not url: abort(404) return redirect(url)
def download_agreement_file(supplier_id, framework_slug, document_name): supplier_framework = data_api_client.get_supplier_framework_info(supplier_id, framework_slug)['frameworkInterest'] if not supplier_framework.get('declaration'): abort(404) agreements_bucket = s3.S3(current_app.config['DM_AGREEMENTS_BUCKET']) prefix = get_agreement_document_path(framework_slug, supplier_id, document_name) agreement_documents = agreements_bucket.list(prefix=prefix) if not len(agreement_documents): abort(404) path = agreement_documents[-1]['path'] url = get_signed_url(agreements_bucket, path, current_app.config['DM_ASSETS_URL']) if not url: abort(404) return redirect(url)
def manage_communications(framework_slug): communications_bucket = s3.S3(current_app.config['DM_COMMUNICATIONS_BUCKET']) framework = data_api_client.get_framework(framework_slug)['frameworks'] itt_pack = next(iter(communications_bucket.list(_get_itt_pack_path(framework_slug))), None) clarification = next(iter(communications_bucket.list(_get_path(framework_slug, 'updates/clarifications'))), None) communication = next(iter(communications_bucket.list(_get_path(framework_slug, 'updates/communications'))), None) return render_template( 'manage_communications.html', itt_pack=itt_pack, clarification=clarification, communication=communication, framework=framework )
def download_supplier_user_list_report(framework_slug, report_type): reports_bucket = s3.S3(current_app.config['DM_REPORTS_BUCKET']) if report_type == 'official': path = f"{framework_slug}/reports/official-details-for-suppliers-{framework_slug}.csv" elif report_type == 'accounts': path = f"{framework_slug}/reports/all-email-accounts-for-suppliers-{framework_slug}.csv" else: abort(404) url = get_signed_url(reports_bucket, path, current_app.config['DM_ASSETS_URL']) if not url: abort(404) return redirect(url)
def download_agreement_file(framework_slug, document_name): supplier_framework_info = get_supplier_framework_info( data_api_client, framework_slug) if supplier_framework_info is None or not supplier_framework_info.get( "declaration"): abort(404) agreements_bucket = s3.S3(current_app.config['DM_AGREEMENTS_BUCKET']) path = get_document_path(framework_slug, current_user.supplier_id, 'agreements', document_name) url = get_signed_url(agreements_bucket, path, current_app.config['DM_ASSETS_URL']) if not url: abort(404) return redirect(url)
def upload_communication(framework_slug): communications_bucket = s3.S3( current_app.config['DM_COMMUNICATIONS_BUCKET'], endpoint_url=current_app.config.get("DM_S3_ENDPOINT_URL")) errors = {} if request.files.get('communication'): the_file = request.files['communication'] if not (file_is_open_document_format(the_file) or file_is_csv(the_file)): errors['communication'] = 'not_open_document_format_or_csv' flash( 'Communication file is not an open document format or a CSV.', 'error') if 'communication' not in errors.keys(): path = "{}/communications/updates/communications/{}".format( framework_slug, the_file.filename) communications_bucket.save(path, the_file, acl='bucket-owner-full-control', download_filename=the_file.filename) flash('New communication was uploaded.') if request.files.get('clarification'): the_file = request.files['clarification'] if not file_is_pdf(the_file): errors['clarification'] = 'not_pdf' flash('Clarification file is not a PDF.', 'error') if 'clarification' not in errors.keys(): path = "{}/communications/updates/clarifications/{}".format( framework_slug, the_file.filename) communications_bucket.save(path, the_file, acl='bucket-owner-full-control', download_filename=the_file.filename) flash('New clarification was uploaded.') return redirect( url_for('.manage_communications', framework_slug=framework_slug))
def framework_updates(framework_slug, error_message=None, default_textbox_value=None): framework = get_framework(data_api_client, framework_slug) supplier_framework_info = get_supplier_framework_info( data_api_client, framework_slug) current_app.logger.info( "{framework_slug}-updates.viewed: user_id {user_id} supplier_id {supplier_id}", extra={ 'framework_slug': framework_slug, 'user_id': current_user.id, 'supplier_id': current_user.supplier_id }) communications_bucket = s3.S3( current_app.config['DM_COMMUNICATIONS_BUCKET']) file_list = communications_bucket.list( '{}/communications/updates/'.format(framework_slug), load_timestamps=True) files = { 'communications': [], 'clarifications': [], } for file in file_list: path_parts = file['path'].split('/') file['path'] = '/'.join(path_parts[2:]) files[path_parts[3]].append(file) return render_template( "frameworks/updates.html", framework=framework, clarification_question_name=CLARIFICATION_QUESTION_NAME, clarification_question_value=default_textbox_value, error_message=error_message, files=files, dates=content_loader.get_message(framework_slug, 'dates'), agreement_countersigned=bool( supplier_framework_info and supplier_framework_info['countersignedPath']), ), 200 if not error_message else 400
def list_countersigned_agreement_file(supplier_id, framework_slug): supplier = data_api_client.get_supplier(supplier_id)['suppliers'] framework = data_api_client.get_framework(framework_slug)['frameworks'] agreements_bucket = s3.S3(current_app.config['DM_AGREEMENTS_BUCKET']) path = get_agreement_document_path(framework_slug, supplier_id, COUNTERSIGNED_AGREEMENT_FILENAME) countersigned_agreement_document = agreements_bucket.get_key(path) if countersigned_agreement_document: countersigned_agreement = countersigned_agreement_document countersigned_agreement['last_modified'] = datetimeformat(parse_date( countersigned_agreement['last_modified'])) countersigned_agreement = [countersigned_agreement] else: countersigned_agreement = [] return render_template( "suppliers/upload_countersigned_agreement.html", supplier=supplier, framework=framework, countersigned_agreement=countersigned_agreement, countersigned_agreement_filename=COUNTERSIGNED_AGREEMENT_FILENAME )
def remove_countersigned_agreement_file(supplier_code, framework_slug): agreements_bucket = s3.S3(current_app.config['DM_AGREEMENTS_BUCKET']) document = get_agreement_document_path(framework_slug, supplier_code, COUNTERSIGNED_AGREEMENT_FILENAME) if request.method == 'GET': flash('countersigned_agreement', 'remove_countersigned_agreement') if request.method == 'POST': agreements_bucket.delete_key(document) data_api_client.create_audit_event( audit_type=AuditTypes.delete_countersigned_agreement, user=current_user.email_address, object_type='suppliers', object_id=supplier_code, data={'upload_countersigned_agreement': document}) return redirect( url_for('.list_countersigned_agreement_file', supplier_code=supplier_code, framework_slug=framework_slug))
def download_dos_outcomes(): # get the slug for the latest DOS framework iteration framework_slug = (sorted( filter( lambda fw: (fw["family"] == "digital-outcomes-and-specialists" and fw["status"] == "live"), data_api_client.find_frameworks()["frameworks"]), key=lambda fw: fw["frameworkLiveAtUTC"], reverse=True, )[0]["slug"]) reports_bucket = s3.S3( current_app.config["DM_REPORTS_BUCKET"], endpoint_url=current_app.config.get("DM_S3_ENDPOINT_URL")) url = get_signed_url(reports_bucket, f"{framework_slug}/reports/opportunity-data.csv", current_app.config["DM_ASSETS_URL"]) if not url: abort(404) return redirect(url)
def upload_communication(framework_slug): communications_bucket = s3.S3(current_app.config['DM_COMMUNICATIONS_BUCKET']) errors = {} if request.files.get('communication'): the_file = request.files['communication'] if not (file_is_pdf(the_file) or file_is_csv(the_file)): errors['communication'] = 'not_pdf_or_csv' if 'communication' not in list(errors.keys()): filename = _get_path(framework_slug, 'updates/communications') + '/' + the_file.filename communications_bucket.save(filename, the_file) flash('communication', 'upload_communication') if request.files.get('clarification'): the_file = request.files['clarification'] if not file_is_pdf(the_file): errors['clarification'] = 'not_pdf' if 'clarification' not in list(errors.keys()): filename = _get_path(framework_slug, 'updates/clarifications') + '/' + the_file.filename communications_bucket.save(filename, the_file) flash('clarification', 'upload_communication') if request.files.get('itt_pack'): the_file = request.files['itt_pack'] if not file_is_zip(the_file): errors['itt_pack'] = 'not_zip' if 'itt_pack' not in list(errors.keys()): filename = _get_itt_pack_path(framework_slug) communications_bucket.save(filename, the_file) flash('itt_pack', 'upload_communication') if len(errors) > 0: for category, message in list(errors.items()): flash(category, message) return redirect(url_for('.manage_communications', framework_slug=framework_slug))
def contract_review(framework_slug, agreement_id): framework = get_framework(data_api_client, framework_slug, allowed_statuses=['standstill', 'live']) # if there's no frameworkAgreementVersion key it means we're pre-G-Cloud 8 and shouldn't be using this route if not framework.get('frameworkAgreementVersion'): abort(404) supplier_framework = return_supplier_framework_info_if_on_framework_or_abort( data_api_client, framework_slug) agreement = data_api_client.get_framework_agreement( agreement_id)['agreement'] check_agreement_is_related_to_supplier_framework_or_abort( agreement, supplier_framework) # if framework agreement doesn't have a name or a role or the agreement file, then 404 if not (agreement.get('signedAgreementDetails') and agreement['signedAgreementDetails'].get('signerName') and agreement['signedAgreementDetails'].get('signerRole') and agreement.get('signedAgreementPath')): abort(404) agreements_bucket = s3.S3(current_app.config['DM_AGREEMENTS_BUCKET']) signature_page = agreements_bucket.get_key( agreement['signedAgreementPath']) form = ContractReviewForm() form_errors = None if request.method == 'POST': if form.validate_on_submit(): data_api_client.sign_framework_agreement( agreement_id, current_user.email_address, {'uploaderUserId': current_user.id}) try: email_body = render_template( 'emails/framework_agreement_with_framework_version_returned.html', framework_name=framework['name'], framework_slug=framework['slug'], framework_live_date=content_loader.get_message( framework_slug, 'dates')['framework_live_date'], # noqa ) send_email( returned_agreement_email_recipients(supplier_framework), email_body, current_app.config['DM_MANDRILL_API_KEY'], 'Your {} signature page has been received'.format( framework['name']), current_app.config["DM_GENERIC_NOREPLY_EMAIL"], current_app.config["FRAMEWORK_AGREEMENT_RETURNED_NAME"], ['{}-framework-agreement'.format(framework_slug)], ) except MandrillException as e: current_app.logger.error( "Framework agreement email failed to send. " "error {error} supplier_id {supplier_id} email_hash {email_hash}", extra={ 'error': six.text_type(e), 'supplier_id': current_user.supplier_id, 'email_hash': hash_email(current_user.email_address) }) abort(503, "Framework agreement email failed to send") session.pop('signature_page', None) flash( 'Your framework agreement has been returned to the Crown Commercial Service to be countersigned.', 'success') if feature.is_active('CONTRACT_VARIATION'): # Redirect to contract variation if it has not been signed if (framework.get('variations') and not supplier_framework['agreedVariations']): variation_slug = list(framework['variations'].keys())[0] return redirect( url_for('.view_contract_variation', framework_slug=framework_slug, variation_slug=variation_slug)) return redirect( url_for(".framework_dashboard", framework_slug=framework_slug)) else: form_errors = [{ 'question': form['authorisation'].label.text, 'input_name': 'authorisation' }] form.authorisation.description = u"I have the authority to return this agreement on behalf of {}.".format( supplier_framework['declaration']['nameOfOrganisation']) return render_template( "frameworks/contract_review.html", agreement=agreement, form=form, form_errors=form_errors, framework=framework, signature_page=signature_page, supplier_framework=supplier_framework, ), 400 if form_errors else 200
def signature_upload(framework_slug, agreement_id): framework = get_framework(data_api_client, framework_slug, allowed_statuses=['standstill', 'live']) # if there's no frameworkAgreementVersion key it means we're pre-G-Cloud 8 and shouldn't be using this route if not framework.get('frameworkAgreementVersion'): abort(404) supplier_framework = return_supplier_framework_info_if_on_framework_or_abort( data_api_client, framework_slug) agreement = data_api_client.get_framework_agreement( agreement_id)['agreement'] check_agreement_is_related_to_supplier_framework_or_abort( agreement, supplier_framework) agreements_bucket = s3.S3(current_app.config['DM_AGREEMENTS_BUCKET']) signature_page = agreements_bucket.get_key( agreement.get('signedAgreementPath')) upload_error = None if request.method == 'POST': # No file chosen for upload and file already exists on s3 so can use existing and progress if not request.files['signature_page'].filename and signature_page: return redirect( url_for(".contract_review", framework_slug=framework_slug, agreement_id=agreement_id)) if not file_is_image( request.files['signature_page']) and not file_is_pdf( request.files['signature_page']): upload_error = "The file must be a PDF, JPG or PNG" elif not file_is_less_than_5mb(request.files['signature_page']): upload_error = "The file must be less than 5MB" elif file_is_empty(request.files['signature_page']): upload_error = "The file must not be empty" if not upload_error: extension = get_extension(request.files['signature_page'].filename) upload_path = generate_timestamped_document_upload_path( framework_slug, current_user.supplier_id, 'agreements', '{}{}'.format(SIGNED_AGREEMENT_PREFIX, extension)) agreements_bucket.save( upload_path, request.files['signature_page'], acl='private', download_filename='{}-{}-{}{}'.format( sanitise_supplier_name(current_user.supplier_name), current_user.supplier_id, SIGNED_SIGNATURE_PAGE_PREFIX, extension), disposition_type= 'inline' # Embeddeding PDFs in admin pages requires 'inline' and not 'attachment' ) data_api_client.update_framework_agreement( agreement_id, {"signedAgreementPath": upload_path}, current_user.email_address) session['signature_page'] = request.files[ 'signature_page'].filename return redirect( url_for(".contract_review", framework_slug=framework_slug, agreement_id=agreement_id)) return render_template( "frameworks/signature_upload.html", agreement=agreement, framework=framework, signature_page=signature_page, upload_error=upload_error, ), 400 if upload_error else 200
def upload_framework_agreement(framework_slug): """ This is the route used to upload agreements for pre-G-Cloud 8 frameworks """ framework = get_framework(data_api_client, framework_slug, allowed_statuses=['standstill', 'live']) # if there's a frameworkAgreementVersion key it means we're on G-Cloud 8 or higher and shouldn't be using this route if framework.get('frameworkAgreementVersion'): abort(404) supplier_framework = return_supplier_framework_info_if_on_framework_or_abort( data_api_client, framework_slug) upload_error = None if not file_is_less_than_5mb(request.files['agreement']): upload_error = "Document must be less than 5MB" elif file_is_empty(request.files['agreement']): upload_error = "Document must not be empty" if upload_error is not None: return render_template( "frameworks/agreement.html", framework=framework, supplier_framework=supplier_framework, upload_error=upload_error, agreement_filename=AGREEMENT_FILENAME, ), 400 agreements_bucket = s3.S3(current_app.config['DM_AGREEMENTS_BUCKET']) extension = get_extension(request.files['agreement'].filename) path = generate_timestamped_document_upload_path( framework_slug, current_user.supplier_id, 'agreements', '{}{}'.format(SIGNED_AGREEMENT_PREFIX, extension)) agreements_bucket.save( path, request.files['agreement'], acl='private', download_filename='{}-{}-{}{}'.format( sanitise_supplier_name(current_user.supplier_name), current_user.supplier_id, SIGNED_AGREEMENT_PREFIX, extension)) agreement_id = data_api_client.create_framework_agreement( current_user.supplier_id, framework_slug, current_user.email_address)['agreement']['id'] data_api_client.update_framework_agreement(agreement_id, {"signedAgreementPath": path}, current_user.email_address) data_api_client.sign_framework_agreement( agreement_id, current_user.email_address, {"uploaderUserId": current_user.id}) try: email_body = render_template( 'emails/framework_agreement_uploaded.html', framework_name=framework['name'], supplier_name=current_user.supplier_name, supplier_id=current_user.supplier_id, user_name=current_user.name) send_email( current_app.config['DM_FRAMEWORK_AGREEMENTS_EMAIL'], email_body, current_app.config['DM_MANDRILL_API_KEY'], '{} framework agreement'.format(framework['name']), current_app.config["DM_GENERIC_NOREPLY_EMAIL"], '{} Supplier'.format(framework['name']), ['{}-framework-agreement'.format(framework_slug)], reply_to=current_user.email_address, ) except MandrillException as e: current_app.logger.error( "Framework agreement email failed to send. " "error {error} supplier_id {supplier_id} email_hash {email_hash}", extra={ 'error': six.text_type(e), 'supplier_id': current_user.supplier_id, 'email_hash': hash_email(current_user.email_address) }) abort(503, "Framework agreement email failed to send") return redirect( url_for('.framework_agreement', framework_slug=framework_slug))
def framework_dashboard(framework_slug): framework = get_framework(data_api_client, framework_slug) if request.method == 'POST': register_interest_in_framework(data_api_client, framework_slug) supplier_users = data_api_client.find_users( supplier_id=current_user.supplier_id) try: email_body = render_template( 'emails/{}_application_started.html'.format(framework_slug)) send_email([ user['emailAddress'] for user in supplier_users['users'] if user['active'] ], email_body, current_app.config['DM_MANDRILL_API_KEY'], 'You started a {} application'.format( framework['name']), current_app.config['CLARIFICATION_EMAIL_FROM'], current_app.config['CLARIFICATION_EMAIL_NAME'], ['{}-application-started'.format(framework_slug)]) except MandrillException as e: current_app.logger.error( "Application started email failed to send: {error}, supplier_id: {supplier_id}", extra={ 'error': six.text_type(e), 'supplier_id': current_user.supplier_id }) drafts, complete_drafts = get_drafts(data_api_client, framework_slug) supplier_framework_info = get_supplier_framework_info( data_api_client, framework_slug) declaration_status = get_declaration_status_from_info( supplier_framework_info) supplier_is_on_framework = get_supplier_on_framework_from_info( supplier_framework_info) # Do not show a framework dashboard for earlier G-Cloud iterations if declaration_status == 'unstarted' and framework['status'] == 'live': abort(404) application_made = supplier_is_on_framework or ( len(complete_drafts) > 0 and declaration_status == 'complete') lots_with_completed_drafts = [ lot for lot in framework['lots'] if count_drafts_by_lot(complete_drafts, lot['slug']) ] first_page = content_loader.get_manifest( framework_slug, 'declaration').get_next_editable_section_id() framework_dates = content_loader.get_message(framework_slug, 'dates') framework_urls = content_loader.get_message(framework_slug, 'urls') # filenames result_letter_filename = RESULT_LETTER_FILENAME countersigned_agreement_file = None if supplier_framework_info and supplier_framework_info['countersignedPath']: countersigned_agreement_file = degenerate_document_path_and_return_doc_name( supplier_framework_info['countersignedPath']) signed_agreement_document_name = None if supplier_is_on_framework and supplier_framework_info[ 'agreementReturned']: signed_agreement_document_name = degenerate_document_path_and_return_doc_name( supplier_framework_info['agreementPath']) key_list = s3.S3(current_app.config['DM_COMMUNICATIONS_BUCKET']).list( framework_slug, load_timestamps=True) key_list.reverse() base_communications_files = { "invitation": { "path": "communications/", "filename": "{}-invitation.pdf".format(framework_slug), }, "proposed_agreement": { "path": "communications/", "filename": "{}-proposed-framework-agreement.pdf".format(framework_slug), }, "final_agreement": { "path": "communications/", "filename": "{}-final-framework-agreement.pdf".format(framework_slug), }, "proposed_call_off": { "path": "communications/", "filename": "{}-proposed-call-off.pdf".format(framework_slug), }, "final_call_off": { "path": "communications/", "filename": "{}-final-call-off.pdf".format(framework_slug), }, "reporting_template": { "path": "communications/", "filename": "{}-reporting-template.xls".format(framework_slug), }, "supplier_updates": { "path": "communications/updates/", }, } # now we annotate these with last_modified information which also tells us whether the file exists communications_files = { label: dict( d, last_modified=get_last_modified_from_first_matching_file( key_list, framework_slug, d["path"] + d.get("filename", ""), ), ) for label, d in six.iteritems(base_communications_files) } return render_template( "frameworks/dashboard.html", application_made=application_made, communications_files=communications_files, completed_lots=tuple( dict(lot, complete_count=count_drafts_by_lot(complete_drafts, lot['slug'])) for lot in lots_with_completed_drafts), countersigned_agreement_file=countersigned_agreement_file, counts={ "draft": len(drafts), "complete": len(complete_drafts) }, declaration_status=declaration_status, signed_agreement_document_name=signed_agreement_document_name, first_page_of_declaration=first_page, framework=framework, framework_dates=framework_dates, framework_urls=framework_urls, result_letter_filename=result_letter_filename, supplier_framework=supplier_framework_info, supplier_is_on_framework=supplier_is_on_framework, ), 200
def get_bucket_name(stage): return 'digitalmarketplace-agreements-{0}-{0}'.format(stage) if __name__ == '__main__': arguments = docopt(__doc__) data_api_url = get_api_endpoint_from_stage(arguments['<stage>'], 'api') client = DataAPIClient(data_api_url, get_auth_token('api', arguments['<stage>'])) FRAMEWORKS = ['g-cloud-7', 'g-cloud-8', 'digital-outcomes-and-specialists'] BUCKET_NAME = get_bucket_name(arguments['<stage>']) BUCKET = s3.S3(BUCKET_NAME) print("STARTED AT {}".format(time.strftime('%X %x %Z'))) for framework_slug in FRAMEWORKS: # Get all supplier frameworks who have returned their agreement supplier_frameworks = client.find_framework_suppliers( framework_slug=framework_slug, agreement_returned=True)['supplierFrameworks'] for supplier_framework in supplier_frameworks: print("======================") print("Supplier ID: {}, Agreement ID: {}".format( supplier_framework['supplierId'], supplier_framework['agreementId'])) # Get their framework agreement framework_agreement = client.get_framework_agreement(
def contract_review(framework_slug): framework = get_framework(data_api_client, framework_slug) supplier_framework = return_supplier_framework_info_if_on_framework_or_abort( data_api_client, framework_slug) agreements_bucket = s3.S3(current_app.config['DM_AGREEMENTS_BUCKET']) signature_page = get_most_recently_uploaded_agreement_file_or_none( agreements_bucket, framework_slug) # if supplier_framework doesn't have a name or a role or the agreement file, then 404 if not (supplier_framework['agreementDetails'] and supplier_framework['agreementDetails'].get('signerName') and supplier_framework['agreementDetails'].get('signerRole') and signature_page): abort(404) form = ContractReviewForm(request.form) form_errors = None if request.method == 'POST': if form.validate(): data_api_client.register_framework_agreement_returned( current_user.supplier_code, framework_slug, current_user.email_address, current_user.id) email_recipients = [ supplier_framework['declaration']['primaryContactEmail'] ] if supplier_framework['declaration']['primaryContactEmail'].lower( ) != current_user.email_address.lower(): email_recipients.append(current_user.email_address) try: email_body = render_template( 'emails/framework_agreement_with_framework_version_returned.html', framework_name=framework['name'], framework_slug=framework['slug'], framework_live_date=content_loader.get_message( framework_slug, 'dates')['framework_live_date'], # noqa ) send_email( email_recipients, email_body, 'Your {} signature page has been received'.format( framework['name']), current_app.config["DM_GENERIC_NOREPLY_EMAIL"], current_app.config["FRAMEWORK_AGREEMENT_RETURNED_NAME"], ['{}-framework-agreement'.format(framework_slug)], ) except EmailError as e: rollbar.report_exc_info() current_app.logger.error( "Framework agreement email failed to send. " "error {error} supplier_code {supplier_code} email_hash {email_hash}", extra={ 'error': six.text_type(e), 'supplier_code': current_user.supplier_code, 'email_hash': hash_email(current_user.email_address) }) abort(503, "Framework agreement email failed to send") session.pop('signature_page', None) flash( 'Your framework agreement has been returned to the Crown Commercial Service to be countersigned.', 'success') return redirect( url_for(".framework_dashboard", framework_slug=framework_slug)) else: form_errors = [{ 'question': form['authorisation'].label.text, 'input_name': 'authorisation' }] form.authorisation.description = u"I have the authority to return this agreement on behalf of {}.".format( supplier_framework['declaration']['nameOfOrganisation']) status_code = 400 if form_errors else 200 return render_template_with_csrf( "frameworks/contract_review.html", status_code=status_code, form=form, form_errors=form_errors, framework=framework, signature_page=signature_page, supplier_framework=supplier_framework, )