def finances_edit_attr(activity_id): request_data = request.get_json() data = { 'activity_id': activity_id, 'attr': request_data['attr'], 'value': request_data['value'], 'finances_id': request_data['finances_id'], } # Run currency conversion if: # we now set to automatic # we change the currency and have set to automatic if (data.get("attr") == "transaction_date") and (data.get("value") == ""): return abort(500) if (data.get("attr") == "currency_automatic") and (data.get("value") == True): # Handle update, and then return required data update_status = qexchangerates.automatic_currency_conversion( finances_id=data["finances_id"], force_update=True) return jsonify(update_status.as_dict()) elif data.get("attr") in ("currency", "transaction_date"): update_curency = qfinances.update_attr(data) update_status = qexchangerates.automatic_currency_conversion( finances_id=data["finances_id"], force_update=False) return jsonify(update_status.as_simple_dict()) elif data["attr"] == "mtef_sector": data["attr"] = 'mtef-sector' # FIXME make consistent update_status = qfinances.update_finances_classification(data) else: update_status = qfinances.update_attr(data) if update_status: return jsonify(update_status.as_simple_dict()) return abort(500)
def api_iati_search(): args = request.get_json() title = args["title"] # NB DSv2 uses "," rather than "|" reporting_org_code = args["reporting_org_code"] def clean_data(doc): def clean_activity(activity): title = get_narrative(activity.find("title")) description = get_narrative(activity.find("description")) return { 'iati_identifier': activity.find("iati-identifier").text, 'title': title, 'description': description, #FIXME include AfDB multinational data for now 'country_data': True #len( #activity.xpath("//iati-activity[recipient-country/@code='LR' or \ #transaction/recipient-country/@code='LR']")) > 0 } return { "results": [ clean_activity(activity) for activity in doc.xpath("//iati-activity") ] } # Try to get IATI Identifier results if (args.get("iati_identifier")) and (args.get('iati_identifier').strip() != ''): iati_identifiers = "{}|{}".format( args.get('iati_identifier'), "|".join( list( map( lambda ro: "{}-{}".format(ro, args.get('iati_identifier')), args['reporting_org_code'].split("|"))))) print("iati_identifiers are", iati_identifiers) print("DS URL IS {}".format( DSV1_IATI_IDENTIFIER_URL.format(iati_identifiers))) r = requests.get(DSV1_IATI_IDENTIFIER_URL.format(iati_identifiers)) if r.status_code == 200: data = clean_data(etree.fromstring(r.text)) if len(data['results']) > 0: return jsonify(data) return jsonify({'msg': 'No results found', 'results': []}) # Disable searching by title for now print("DS URL IS {}".format( DSV1_TITLE_URL.format(urlencode_text(title), reporting_org_code))) r = requests.get( DSV1_TITLE_URL.format(title.encode("utf-8"), reporting_org_code)) if r.status_code == 200: data = clean_data(etree.fromstring(r.text)) return jsonify(data)
def activity_delete(activity_id): activity = qactivity.get_activity(activity_id) if (((activity.domestic_external == 'domestic') and ('piu-desk-officer' in current_user.roles_list)) or (getattr(current_user, "administrator")) or ('admin' in current_user.roles_list)): result = qactivity.delete_activity(activity_id) else: abort(403) if result: return jsonify({'msg': "Successfully deleted that activity"}), 200 else: return jsonify({'msg': "Sorry, unable to delete that activity"}), 500
def api_activities_results_design(activity_id): if request.method == "POST": result = qactivity.save_results_data(activity_id, request.json.get("results")) if not result: return jsonify(error="Error, could not save data."), 500 activity = models.Activity.query.get(activity_id) if activity is None: return abort(404) results = activity.results return jsonify(activity_id=activity.id, activity_title=activity.title, results=jsonify_results_design(results))
def reset_password(): email_address = request.json.get('email_address', '') if email_address != "": if quser.make_password_reset_key(email_address): return make_response( jsonify({ 'msg': 'Sent an email to {} - please check your email for further instructions on how to reset your password.' .format(email_address) }), 200) else: return make_response( jsonify({'msg': 'Please enter an email address.'}), 400)
def api_activities_documents(activity_id): if request.method == "POST": title = request.form.get('title') category_code = request.form.get('category') file = request.files['file'] document = qdocuments.add_document(activity_id, title, category_code, file) return jsonify(document=document.as_dict()) else: activity = models.Activity.query.get(activity_id) if activity is None: return abort(404) documents = [document.as_dict() for document in activity.documents] return jsonify(documents=documents)
def user_permissions_edit(user_id): if request.method == "GET": user = quser.user(user_id) if user is None: return abort(404) user_organisations = list( map(lambda uo: uo.as_dict(), user.organisations)) user_roles = list(map(lambda uo: uo.as_dict(), user.userroles)) roles = list(map(lambda r: r.as_dict(), models.Role.query.all())) organisations = list( map(lambda o: o.as_dict(), qorganisations.get_organisations())) permission_values = [{ "name": "View projects", "value": "view" }, { "name": "Edit projects", "value": "edit" }, { "name": "Results data entry", "value": "results-data-entry" }, { "name": "Results data design", "value": "results-data-design" }] return jsonify(permissions=user_organisations, organisations=organisations, permission_values=permission_values, user_roles=user_roles, roles=roles) elif request.method == "POST": data = request.get_json() data["user_id"] = user_id if data["action"] == "add": op = quser.addOrganisationPermission(data) if not op: return "False" return jsonify(op.as_dict()) elif data["action"] == "delete": op = quser.deleteOrganisationPermission(data) if not op: return "error" return "ok" elif data["action"] == "edit": op = quser.updateOrganisationPermission(data) if not op: return "error" return "ok" return "error, unknown action"
def counterpart_funding(): next_fy = util.FY("current").fiscal_year.name fiscal_year = request.args.get("fiscal_year", next_fy) start_date, end_date = qactivity.get_earliest_latest_dates_filter( {'key': 'domestic_external', 'val': 'external'}) next_fy_end_date = util.FY("next").date("end") fys = [str(fy) for fy in util.fys_in_date_range( start_date, max(end_date, next_fy_end_date))] def annotate_activity(activity): return { 'id': activity.id, 'title': activity.title, 'reporting_org_name': activity.reporting_org.name, 'sector_name': ", ".join([sector.codelist_code.name for sector in activity.classification_data.get('mtef-sector', {}).get('entries', [])]), 'ministry_name': ", ".join([ministry.codelist_code.name for ministry in activity.classification_data.get('aligned-ministry-agency', {}).get('entries', [])]), 'gol_requested': activity._fy_counterpart_funding, 'donor_planned': activity._fy_forwardspends } activities = [annotate_activity(activity) for activity in qcounterpart_funding.annotate_activities_with_aggregates(fiscal_year)] return jsonify( fy=util.FY("next").fy_fy(), activities=activities, fiscalYears=fys, fiscalYear=str(fiscal_year) )
def api_all_activity_locations(): """GET returns a list of all locations.""" query = models.ActivityLocation.query.join(models.Activity) query = qactivity.filter_activities_for_permissions(query) query = query.outerjoin(models.ActivityFinances).filter( models.ActivityFinances.transaction_date >= '2019-01-01') activitylocations = query.all() locations = [{ 'locationID': activitylocation.id, 'latitude': activitylocation.locations.latitude, 'longitude': activitylocation.locations.longitude, 'latlng': [ activitylocation.locations.latitude, activitylocation.locations.longitude ], 'name': activitylocation.locations.name, "title": activitylocation.activity.title, "id": activitylocation.activity_id } for activitylocation in activitylocations] return jsonify(locations=locations)
def api_activities_finances_by_id(activity_id): activity = qactivity.get_activity(activity_id) if activity is None: return abort(404) finances = qactivity.get_finances_by_activity_id( activity_id, request.args.get('by_year')) return jsonify(finances=OrderedDict(finances))
def reporting_orgs_summary(): reporting_orgs = qorganisations.get_reporting_orgs() response_statuses = [ resp.as_dict() for resp in qmonitoring.response_statuses() ] orgs = generate_reporting_organisation_checklist(reporting_orgs, response_statuses) def generate_summary(list_of_quarters, orgs): out = dict( map(lambda q: (q, { 'True': 0, 'False': 0, "Total": 0 }), list_of_quarters.keys())) for ro in orgs: for disb_q, disb_v in ro["disbursements"].items(): if disb_v["status"]["name"] == "Donor responded with data": out[disb_q]['True'] += 1 else: out[disb_q]['False'] += 1 out[disb_q]["Total"] += 1 return out list_of_quarters = util.Last4Quarters().list_of_quarters() summary = generate_summary(list_of_quarters, orgs) return jsonify(summary=summary, current_year=util.FY("current").fy_fy(), previous_year=util.FY("previous").fy_fy(), list_of_quarters=list_of_quarters)
def activity_log(): offset = (int(request.args.get("page", 1)) - 1) * 10 count = models.ActivityLog.query.count() user_id = request.args.get("user_id", None) activitylogs = quser.activitylog(offset=offset, user_id=user_id) def simple_log(al): return { "id": al.id, "user": { "id": al.user_id, "username": al.user.username }, "activity": { "id": al.activity_id, "title": al.activity.title }, "mode": { "id": al.mode, "text": al.mode_text }, "date": al.log_date.replace(microsecond=0).isoformat(), "target": { "id": al.target_id, "text": al.target_text } } return jsonify( count=count, items=[simple_log(activitylog) for activitylog in activitylogs])
def users_delete(): if "admin" not in current_user.roles_list: return make_response( jsonify({'msg': "You must be an administrator to delete users."}), 403) if current_user.username == request.get_json().get("username"): return make_response( jsonify({'msg': "You cannot delete your own user."}), 400) if quser.deleteUser(request.get_json().get('username')): return make_response(jsonify({'msg': "Successfully deleted user."}), 200) return make_response( jsonify({ 'msg': "Sorry, there was an error and that user could not be deleted." }), 500)
def api_iati_fetch_data(activity_id): iati_identifier = request.json.get("iatiIdentifier") activity_ids = request.json.get("activityIDs") import_options = request.json.get("importOptions") activities_fields_options = request.json.get("activitiesFieldsOptions") #iati_document_result = qimport_iati.import_documents(activity_id, iati_identifier) return jsonify(status=qimport_iati.import_data( activity_id, iati_identifier, activity_ids, import_options, activities_fields_options))
def reset_password_with_key(): if (request.json.get("email_address", "") != "") and (request.json.get( "reset_password_key", "") != ""): if quser.check_password_reset(request.json["email_address"], request.json["reset_password_key"]): return make_response( jsonify({ 'msg': 'Key authenticated - please provide a new password.' }), 200) return make_response( jsonify({ 'msg': 'Sorry, could not reset that passsword key. Please try resetting your password again, and make sure you use the reset key within 24 hours.' }), 400) else: return make_response( jsonify({'msg': "Please enter an email address and key."}), 400)
def reporting_orgs_user(): if request.method == "POST": if ("organisation_id" in request.json) and ("response_id" in request.json): qmonitoring.update_organisation_response(request.json) return jsonify(result=True) #FIXME change to roles if ("user_id" in request.args) or ("admin" not in current_user.roles_list): reporting_orgs = qorganisations.get_reporting_orgs( user_id=request.args.get('user_id', current_user.id)) user = models.User.query.get( request.args.get('user_id', current_user.id)) user_name = user.name user_id = user.id else: reporting_orgs = qorganisations.get_reporting_orgs() user_name = None user_id = None if "admin" in current_user.roles_list: users = [{ 'value': user.id, 'text': user.name } for user in quser.users_with_role('desk-officer')] else: users = [] response_statuses = [ resp.as_dict() for resp in qmonitoring.response_statuses() ] orgs = generate_reporting_organisation_checklist(reporting_orgs, response_statuses) data_collection_calendar = qmonitoring.generate_data_collection_calendar() return jsonify(orgs=orgs, previous_year=util.FY("previous").fy_fy(), current_year=util.FY("current").fy_fy(), next_year=util.FY("next").fy_fy(), list_of_quarters=util.Last4Quarters().list_of_quarters(), users=users, user_name=user_name, user_id=user_id, statuses=response_statuses, data_collection_calendar=data_collection_calendar)
def api_activities_finances(): activity_ids = request.json.get('activity_ids') by_year = request.args.get('by_year') activities = [{ 'id': activity_id, 'finances': OrderedDict(qactivity.get_finances_by_activity_id( activity_id, by_year)) } for activity_id in activity_ids] return jsonify(activities=activities)
def users_new(): if request.method == "GET": user = { "view": current_user.permissions_dict["view"], "edit": 'none', "recipient_country_code": "LR" } roles = list(map(lambda r: r.as_dict(), models.Role.query.all())) return make_response(jsonify(user=user, roles=roles), 200) elif request.method == "POST": user = quser.addUser(request.json) if user: return make_response( jsonify({ 'msg': 'Successfully created user!', 'user': user.as_dict() }), 200) else: return make_response( jsonify({'msg': "Sorry, couldn't create that user!"}), 500)
def reporting_orgs(): reporting_orgs = qorganisations.get_reporting_orgs() response_statuses = [ resp.as_dict() for resp in qmonitoring.response_statuses() ] orgs = generate_reporting_organisation_checklist(reporting_orgs, response_statuses) return jsonify(orgs=orgs, previous_year=util.FY("previous").fy_fy(), current_year=util.FY("current").fy_fy(), next_year=util.FY("next").fy_fy(), list_of_quarters=util.Last4Quarters().list_of_quarters())
def login(): if request.method == "POST" and "username" in request.json: if not request.is_json: return make_response(jsonify({"msg": "Missing JSON in request"}), 400) username = request.json.get('username', None) password = request.json.get('password', None) if not username: return make_response( jsonify({"msg": "Missing username parameter"}), 400) if not password: return make_response( jsonify({"msg": "Missing password parameter"}), 400) user = models.User.query.filter_by(username=username).first() if not (user and user.check_password(password) and user.is_active()): return make_response(jsonify({"msg": "Bad username or password"}), 401) # Identity can be any data that is json serializable ret = { 'access_token': create_access_token(identity=username), 'refresh_token': create_refresh_token(identity=username) } return make_response(jsonify(ret), 200) return make_response(jsonify({"msg": "Please login to continue."}), 200)
def api_activity_summaries(): activity_ids = request.json.get('activity_ids') fields = [ 'id', 'title', 'description', 'objectives', 'deliverables', 'papd_alignment', 'start_date', 'end_date', 'activity_status' ] fields_special = [ 'implementing_organisations', 'funding_organisations', # 'classifications', ] fields_len = [ 'results', 'documents', 'policy_markers', 'locations', 'counterpart_funding' ] all_fields = fields + fields_special + fields_len def get_activity_summary(activity_id): activity = qactivity.get_activity(activity_id) f = dict([(field, getattr(activity, field)) for field in fields]) flen = dict([(field, "{} {}".format(len(getattr(activity, field)), field)) for field in fields_len]) f.update(flen) f['implementing_organisations'] = "; ".join([ organisation.name for organisation in activity.implementing_organisations ]) f['funding_organisations'] = "; ".join([ organisation.name for organisation in activity.funding_organisations ]) f['locations'] = "; ".join( [location.locations.name for location in activity.locations]) #f['classifications'] = dict(filter(lambda clsf: clsf[0]!='mtef-sector', activity.classification_data_dict.items())) return f activity_summaries = [ get_activity_summary(activity_id) for activity_id in activity_ids ] all_fields.pop(0) # Delete ID # Group results by field summaries_by_field = dict([(field, {}) for field in all_fields]) for activity in activity_summaries: for field in all_fields: summaries_by_field[field][activity['id']] = activity[field] # Get only fields with multiple distinct values def filter_unique(field): return len(set([str(val) for val in field[1].values()])) > 1 unique_filters = dict(filter(filter_unique, summaries_by_field.items())) return jsonify(fields=unique_filters)
def users_edit(user_id): user = quser.user(user_id) if user is None: return abort(404) roles = list(map(lambda r: r.as_dict(), models.Role.query.all())) def annotate_user(user): _user = user.as_dict() _user['view'] = user.permissions_dict["view"] _user['edit'] = user.permissions_dict["edit"] return _user if request.method == "GET": return jsonify(user=annotate_user(user), roles=roles) elif request.method == "POST": data = request.json data["id"] = user_id if quser.updateUser(data): return make_response( jsonify({'msg': "Successfully updated user!"}), 200) else: return make_response( jsonify({'msg': "Sorry, couldn't update that user!"}), 500)
def reset_password_new_password(): if request.json.get("password") != request.json.get("password_2"): return make_response( jsonify( {'msg': 'Please make sure you enter the same password twice.'}), 400) elif request.json.get("password") == "": return make_response(jsonify({'msg': 'Please enter a password.'}), 400) else: if quser.process_reset_password( email_address=request.json.get("email_address"), reset_password_key=request.json.get("reset_password_key"), password=request.json.get("password")): return make_response( jsonify({ 'msg': 'Password successfully changed! Please login with your new password.' }), 200) else: return make_response( jsonify({ 'msg': 'Sorry, something went wrong, and your password could not be changed.' }), 400)
def activity_log_detail(activitylog_id): al = quser.activitylog_detail(activitylog_id) def get_object(target, target_text, id): if not getattr(models, target).query.get(id): return None, None if target == "ActivityLocation": return "Location", getattr( models, target).query.get(id).locations.as_dict() elif target == "ActivityFinances": return "Financial data", getattr( models, target).query.get(id).as_simple_dict() elif target == "ActivityFinancesCodelistCode": return "Financial data", getattr( models, target).query.get(id).activityfinances.as_simple_dict() else: return target_text.title(), getattr( models, target).query.get(id).as_dict() return None, None _obj_title, _obj = get_object(al.target, al.target_text, al.target_id) return jsonify( data={ "id": al.id, "user": { "id": al.user_id, "username": al.user.username }, "activity": { "id": al.activity_id, "title": al.activity.title }, "mode": { "id": al.mode, "text": al.mode_text }, "date": al.log_date.replace(microsecond=0).isoformat(), "target": { "id": al.target_id, "text": al.target_text, "obj_title": _obj_title, "obj": _obj }, "value": json.loads(al.value) if al.value else None, "old_value": json.loads(al.old_value) if al.old_value else None })
def project_development_tracking(): current_fy = util.FY("previous").fiscal_year.name fiscal_year = request.args.get("fiscal_year", current_fy) activities = models.Activity.query.filter_by( domestic_external="domestic" ).all() milestones = models.Milestone.query.filter_by( domestic_external="domestic" ).order_by( models.Milestone.milestone_order ).all() sum_appropriations = qreports.sum_transactions( fiscal_year, 0, 'domestic', 'sum_appropriations', 'C') sum_allotments = qreports.sum_transactions( fiscal_year, 0, 'domestic', 'sum_allotments', '99-A') sum_disbursements = qreports.sum_transactions( fiscal_year, 0, 'domestic', 'sum_disbursements', 'D') def annotate_activity(activity): out = OrderedDict({ 'id': activity.id, 'title': activity.title, 'implementer': ", ".join( [implementer.name for implementer in activity.implementing_organisations] ), 'sum_appropriations': sum_appropriations.get(activity.id, 0.00), 'sum_allotments': sum_allotments.get(activity.id, 0.00), 'sum_disbursements': sum_disbursements.get(activity.id, 0.00) }) [out.update({ms['name']: ms['achieved']}) for ms in activity.milestones_data] return out milestone_data = [annotate_activity(activity) for activity in activities] start_date, end_date = qactivity.get_earliest_latest_dates_filter( {'key': 'domestic_external', 'val': 'domestic'}) if start_date is not None: fys = [str(fy) for fy in util.fys_in_date_range(start_date, end_date)] else: fys = [] return jsonify(activities=milestone_data, milestones=[milestone.name for milestone in milestones], fiscalYears=fys, fiscalYear=str(fiscal_year))
def api_activity_forwardspends(activity_id): activity = qactivity.get_activity(activity_id) if activity is None: return abort(404) # GET returns a list of all forward spend data for a given activity_id. if request.method == "GET": data = qactivity.get_activity(activity_id).forwardspends forwardspends = [ fs_db.as_dict() for fs_db in qactivity.get_activity(activity_id).forwardspends ] # Return fiscal years here years = sorted( set( map(lambda fs: util.date_to_fy_fq_present(fs["value_date"])[0], forwardspends))) out = OrderedDict() for year in years: out[year] = OrderedDict({ "year": "FY{}".format(util.fy_to_fyfy_present(str(year))), "total_value": 0.00 }) for forwardspend in sorted(forwardspends, key=lambda k: k["value_date"]): year, fq = util.date_to_fy_fq_present( forwardspend["period_start_date"]) out[year]["Q{}".format(fq)] = forwardspend out[year]["total_value"] += forwardspend['value'] out = list(out.values()) quarters = util.make_quarters_text_present() return jsonify(forwardspends=out, quarters=quarters) # POST updates value for a given forwardspend_id. else: request_data = request.get_json() if request_data["value"] in (None, " ", ""): value = 0 else: value = request_data["value"] data = {"id": request_data["id"], "value": value} update_status = qfinances.update_fs_attr(data) if update_status is True: return "success" return "error"
def api_activities_filters(): reporting_orgs = qorganisations.get_reporting_orgs() implementing_orgs = qorganisations.get_implementing_orgs() organisation_types = qorganisations.get_organisation_types() cl = get_codelists() _cl_domestic_external = [{ "id": "domestic", "name": "Domestic (PSIP / PIU)" }, { "id": "external", "name": "External (Aid / AMCU)" }] filters_codelists = [ ("Reported by", "reporting_org_id", reporting_orgs), ("Type of Implementer", "implementing_org_type", organisation_types), ("Implementer", "implementing_org", implementing_orgs), ("Sector", "mtef-sector", cl["mtef-sector"]), ("Aligned Ministry / Agency", "aligned-ministry-agency", cl["aligned-ministry-agency"]), ("PAPD Pillar", "papd-pillar", cl["papd-pillar"]), ("SDG Goals", "sdg-goals", cl["sdg-goals"]), ("Activity Status", "activity_status", cl["ActivityStatus"]), ("Aid Type", "aid_type", cl["AidType"]), ("Bilateral / Multilateral", "collaboration_type", cl["CollaborationType"]), ("Domestic / External", "domestic_external", _cl_domestic_external), ] earliest, latest = qactivity.get_earliest_latest_dates(force=True) activity_dates = { "earliest": earliest, "latest": latest, } return jsonify(filters=list( map( lambda f: { "label": f[0], "name": f[1], "codes": list( map(lambda fo: fo.as_dict() if type(fo) != dict else fo, f[2])), }, filters_codelists)), activity_dates=activity_dates)
def api_activities_country(): arguments = request.args.to_dict() activities = qactivity.list_activities_by_filters(arguments) activity_commitments, activity_disbursements, activity_projected_disbursements = qactivity.activity_C_D_FSs( ) def round_or_zero(value): if not value: return 0 return round(value) def make_pct(value1, value2): if value2 == 0: return None return (value1 / value2) * 100 return jsonify(activities=[{ 'title': activity.title, 'reporting_org': activity.reporting_org.name, 'id': activity.id, 'updated_date': activity.updated_date.date().isoformat(), 'total_commitments': round_or_zero(activity_commitments.get(activity.id)), 'total_disbursements': round_or_zero(activity_disbursements.get(activity.id)), 'total_projected_disbursements': round_or_zero(activity_projected_disbursements.get(activity.id)), 'pct_disbursements_projected': make_pct(activity_disbursements.get(activity.id, 0), activity_projected_disbursements.get(activity.id, 0)), 'pct_disbursements_committed': make_pct(activity_disbursements.get(activity.id, 0), activity_commitments.get(activity.id, 0)), 'user': activity.user.username, 'user_id': activity.user.id, "permissions": activity.permissions } for activity in activities])
def aid_disbursements_api(): _current_fy = util.FY("previous") current_fy = _current_fy.fiscal_year.name fiscal_year = request.args.get("fiscal_year", current_fy) start_of_fy = _current_fy.fiscal_year.start days_since_fy_began = ( (datetime.datetime.utcnow().date()-start_of_fy).days) progress_time = min(round(days_since_fy_began/365.0*100.0, 2), 100.0) start_date, end_date = qactivity.get_earliest_latest_dates_filter( {'key': 'domestic_external', 'val': 'external'}) start_date = max(start_date, current_app.config['EARLIEST_DATE']) fys = [str(fy) for fy in util.fys_in_date_range(start_date, end_date)] return jsonify(activities=qreports.make_forwardspends_disbursements_data(fiscal_year), progress_time=progress_time, days_since_fy_began=days_since_fy_began, fy_start_day=datetime.datetime.strftime( start_of_fy, "%d.%m.%Y"), fiscalYears=fys, fiscalYear=str(fiscal_year))
def results(): def annotate_activity(activity): return { 'id': activity.id, 'title': activity.title, 'reporting_org_name': activity.reporting_org.name, 'implementer_name': ", ".join([org.name for org in activity.implementing_organisations]), 'results_average': round(activity.results_average) if activity.results_average is not None else None, 'results_average_status': activity.results_average_status } activities = [annotate_activity(activity) for activity in models.Activity.query.filter( models.Activity.results.any(), models.Activity.domestic_external == 'external' ).all() ] return jsonify( activities=activities )