Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
.ropeproject
node_modules
bower_components
ckanext/activityinfo/data/responses_debug_dir

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

# C extensions
*.so

# Distribution / packaging
.Python
env/
Expand Down
27 changes: 27 additions & 0 deletions ckanext/activityinfo/actions/activity_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,30 @@ def act_info_get_databases(context, data_dict):
raise ActivityInfoConnectionError(error)

return databases


def act_info_get_forms(context, data_dict):
'''
Action function to get ActivityInfo forms for a database.
'''
toolkit.check_access('act_info_get_forms', context, data_dict)
user = context.get('user')
database_id = data_dict.get('database_id')
if not database_id:
raise toolkit.ValidationError({'database_id': 'Missing value'})

log.debug(f"Getting ActivityInfo forms for database {database_id} and user {user}")
token = get_user_token(user)
aic = ActivityInfoClient(api_key=token)
try:
data = aic.get_forms(database_id, include_db_data=True)
except HTTPError as e:
error = f"Error retrieving forms for database {database_id} and user {user}: {e}"
log.error(error)
raise ActivityInfoConnectionError(error)

ret = {
'forms': data['forms'],
'database': data['database']
}
return ret
21 changes: 17 additions & 4 deletions ckanext/activityinfo/auth/activity_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,23 @@
log = logging.getLogger(__name__)


def require_activity_info_token_decorator(func):
def wrapper(context, data_dict):
user = context.get('user')
token = get_user_token(user)
if not token:
return {'success': False, 'msg': f"No ActivityInfo token found for user {user}.", 'activity_info_token': None}
return {'success': True, 'activity_info_token': token}
return wrapper


@toolkit.auth_disallow_anonymous_access
@require_activity_info_token_decorator
def act_info_get_databases(context, data_dict):
user = context.get('user')
token = get_user_token(user)
if not token:
return {'success': False, 'msg': f"No ActivityInfo token found for user {user}."}
return {'success': True}


@toolkit.auth_disallow_anonymous_access
@require_activity_info_token_decorator
def act_info_get_forms(context, data_dict):
return {'success': True}
48 changes: 33 additions & 15 deletions ckanext/activityinfo/blueprints/activity_info.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import logging
from flask import Blueprint
from ckan.common import current_user
from ckan.plugins import toolkit
from ckanext.activityinfo.exceptions import ActivityInfoConnectionError
from ckanext.activityinfo.utils import get_activity_info_user_plugin_extras, get_user_token


log = logging.getLogger(__name__)
Expand All @@ -10,7 +12,9 @@

@activityinfo_bp.route('/')
def index():
extra_vars = {}
extra_vars = {
'api_key': get_user_token(current_user.name),
}
return toolkit.render('activity_info/index.html', extra_vars)


Expand All @@ -34,6 +38,28 @@ def databases():
return toolkit.render('activity_info/databases.html', extra_vars)


@activityinfo_bp.route('/databases/<database_id>/forms')
def forms(database_id):
try:
data = toolkit.get_action('act_info_get_forms')(
context={'user': toolkit.c.user},
data_dict={'database_id': database_id}
)
except (ActivityInfoConnectionError, toolkit.ValidationError) as e:
message = f"Could not retrieve ActivityInfo forms: {e}"
log.error(message)
toolkit.h.flash_error(message)
return toolkit.redirect_to('activity_info.databases')

log.info(f"Retrieved {data}")
extra_vars = {
'forms': data['forms'],
'database_id': database_id,
'database': data['database'],
}
return toolkit.render('activity_info/forms.html', extra_vars)


@activityinfo_bp.route('/update-api-key', methods=['POST'])
def update_api_key():
"""Create or update the current ActivityInfo API key for the logged-in user."""
Expand All @@ -44,18 +70,13 @@ def update_api_key():
toolkit.h.flash_error(message)
return toolkit.redirect_to('activity_info.index')

user_dict = toolkit.get_action('user_show')(
context={'ignore_auth': True},
data_dict={'id': toolkit.c.user, 'include_plugin_extras': True}
)
plugin_extras = user_dict.get('plugin_extras')
if not plugin_extras:
plugin_extras = {}
plugin_extras = get_activity_info_user_plugin_extras(toolkit.c.user) or {}
activity_info_extras = plugin_extras.get('activity_info', {})
activity_info_extras['api_key'] = api_key
plugin_extras['activity_info'] = activity_info_extras
site_user = toolkit.get_action("get_site_user")({"ignore_auth": True}, {})
toolkit.get_action('user_patch')(
context={'user': toolkit.c.user},
context={'user': site_user['name']},
data_dict={
'id': toolkit.c.user,
'plugin_extras': plugin_extras
Expand All @@ -68,20 +89,17 @@ def update_api_key():
@activityinfo_bp.route('/remove-api-key', methods=['POST'])
def remove_api_key():
"""Remove the current ActivityInfo API key for the logged-in user."""
user_dict = toolkit.get_action('user_show')(
context={'ignore_auth': True},
data_dict={'id': toolkit.c.user, 'include_plugin_extras': True}
)
plugin_extras = user_dict.get('plugin_extras')
plugin_extras = get_activity_info_user_plugin_extras(toolkit.c.user) or {}
if not plugin_extras or 'activity_info' not in plugin_extras:
toolkit.h.flash_error('No ActivityInfo API key found to remove.')
return toolkit.redirect_to('activity_info.index')

activity_info_extras = plugin_extras.get('activity_info', {})
activity_info_extras.pop('api_key', None)
plugin_extras['activity_info'] = activity_info_extras
site_user = toolkit.get_action("get_site_user")({"ignore_auth": True}, {})
toolkit.get_action('user_patch')(
context={'user': toolkit.c.user},
context={'user': site_user['name']},
data_dict={
'id': toolkit.c.user,
'plugin_extras': plugin_extras
Expand Down
70 changes: 56 additions & 14 deletions ckanext/activityinfo/data/base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import logging
from pathlib import Path
import requests


log = logging.getLogger(__name__)


class ActivityInfoClient:
"""Base class for ActivityInfo API client."""

def __init__(self, base_url="https://www.activityinfo.org", api_key=None):
def __init__(self, base_url="https://www.activityinfo.org", api_key=None, debug=True):
self.base_url = base_url
self.api_key = api_key
self.debug = debug
self.responses_debug_dir = None
if self.debug:
here = Path(__file__).parent
self.responses_debug_dir = here / "responses_debug_dir"
self.responses_debug_dir.mkdir(exist_ok=True)
log.debug(f"ActivityInfoClient initialized with base_url: {self.base_url}, debug: {self.debug}")

def get_user_auth_headers(self):
"""
Expand All @@ -19,28 +31,58 @@ def get_user_auth_headers(self):

def get(self, endpoint, params=None):
"""Make a GET request to the ActivityInfo API."""
log.info(f"ActivityInfoClient Making GET request to {endpoint}")
headers = self.get_user_auth_headers()
url = f"{self.base_url}/{endpoint}"
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
log.info(f"ActivityInfoClient GET request to {endpoint} completed")
if self.debug:
# create all folders in path
Path(self.responses_debug_dir / endpoint).mkdir(parents=True, exist_ok=True)
with open(self.responses_debug_dir / endpoint / "response.json", "w") as f:
f.write(response.text)
return response.json()

def get_databases(self):
""" Fetch the list of databases for the authenticated user.
Docs: https://www.activityinfo.org/support/docs/api/reference/getDatabases.html
Returns:
A list of databases.
Reponse sample:
[
{
'databaseId': 'cqvxxxxxx',
'label': 'Some DB',
'description': '',
'ownerId': '2132xxxxx',
'billingAccountId': 5682xxxxx,
'suspended': False,
'publishedTemplate': False,
'languages': []
}
]
Reponse sample: see ckanext/activityinfo/data/samples/databases.json
"""
return self.get("resources/databases")

def get_database(self, database_id):
""" Fetch the details of a specific database.
Docs: https://www.activityinfo.org/support/docs/api/reference/getDatabaseTree.html
Args:
database_id (str): The ID of the database to fetch.
Returns:
A dictionary containing the details of the database.
This include resources by types: DATABASE, FOLDER, REPORT, FORM and SUB_FORM
Response sample: see ckanext/activityinfo/data/samples/database.json
"""
return self.get(f"resources/databases/{database_id}")

def get_forms(self, database_id, include_db_data=True):
""" Fetch the list of forms for a specific database.
There is not direct API endpoint
We get the database nad the resources -> list -> filter type=FORM
"""
database = self.get_database(database_id)
forms = [resource for resource in database["resources"] if resource["type"] == "FORM"]
data = {"forms": forms}
if include_db_data:
data["database"] = database
return data

def get_form(self, database_id, form_id):
""" Fetch the details of a specific form.
Args:
database_id (str): The ID of the database to fetch forms for.
form_id (str): The ID of the form to fetch.
Returns:
A dictionary containing the details of the form.
"""
return self.get(f"resources/databases/{database_id}/forms/{form_id}")
Loading