From b813943176bc463245c1f587464a23d0a825d3fa Mon Sep 17 00:00:00 2001
From: Isaac Jandalala
Date: Mon, 19 Jan 2026 14:04:26 +0000
Subject: [PATCH 1/5] Process feedback from DocBot.
---
.env-example | 2 +
kubernetes/helm/templates/rnacentral.yaml | 10 ++++
rnacentral/portal/urls.py | 8 ++--
rnacentral/portal/views.py | 57 ++++++++++++++++++++++-
rnacentral/rnacentral/settings.py | 4 ++
5 files changed, 77 insertions(+), 4 deletions(-)
diff --git a/.env-example b/.env-example
index d3d4fb9a5..aa0173d3c 100644
--- a/.env-example
+++ b/.env-example
@@ -5,3 +5,5 @@ EBI_SEARCH_ENDPOINT=http://example.com # if not specified, www will be used
SECRET_KEY=super_secret_key # if not specified, it uses get_random_secret_key
DJANGO_DEBUG=True # if not specified, it uses DJANGO_DEBUG=False
LOCAL_DEVELOPMENT=True # use this variable if you need the django-debug-toolbar, coverage and mock packages
+DOORBELL_API_KEY=your_doorbell_api_key # Doorbell.io API key for feedback integration
+DOORBELL_API_ID=your_doorbell_app_id # Doorbell.io application ID
diff --git a/kubernetes/helm/templates/rnacentral.yaml b/kubernetes/helm/templates/rnacentral.yaml
index 39663e806..b54cb4c6f 100644
--- a/kubernetes/helm/templates/rnacentral.yaml
+++ b/kubernetes/helm/templates/rnacentral.yaml
@@ -62,6 +62,16 @@ spec:
env:
- name: RNACENTRAL_ENV
value: {{ .Values.setEnv }}
+ - name: DOORBELL_API_KEY
+ valueFrom:
+ secretKeyRef:
+ name: doorbell-credentials
+ key: DOORBELL_API_KEY
+ - name: DOORBELL_API_ID
+ valueFrom:
+ secretKeyRef:
+ name: doorbell-credentials
+ key: DOORBELL_API_ID
envFrom:
- secretRef:
name: {{ .Values.database }}
diff --git a/rnacentral/portal/urls.py b/rnacentral/portal/urls.py
index 2a4495ca1..830c09e4f 100644
--- a/rnacentral/portal/urls.py
+++ b/rnacentral/portal/urls.py
@@ -243,9 +243,11 @@
views.gene_detail,
name="gene-detail"
- )
-
-
+ ),
+ # relay DocBot feedback to Doorbell.io
+ re_path(
+ r"^api/internal/feedback-relay/?$", views.docbot_feedback, name="docbot-feedback"
+ ),
]
diff --git a/rnacentral/portal/views.py b/rnacentral/portal/views.py
index 67a21f306..fdbc06611 100644
--- a/rnacentral/portal/views.py
+++ b/rnacentral/portal/views.py
@@ -29,11 +29,12 @@
from urllib.parse import urlparse
from django.conf import settings
-from django.http import Http404, HttpResponse, HttpResponseForbidden
+from django.http import Http404, HttpResponse, HttpResponseForbidden, JsonResponse
from django.shortcuts import redirect, render
from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string
from django.views.decorators.cache import cache_control, cache_page, never_cache
+from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import TemplateView
from portal.config.expert_databases import expert_dbs
from portal.config.go_dataset import go_set
@@ -1026,6 +1027,60 @@ def get_nested_tree(data, container):
json_lineage_tree = get_nested_tree(nodes, {})
return json.dumps(json_lineage_tree)
+@csrf_exempt
+def docbot_feedback(request):
+ """
+ Post DocBot feedback to Doorbell.io.
+ Only allow POST requests from whitelisted hosts, then forward to Doorbell.io.
+ """
+ if request.method != "POST":
+ return HttpResponseForbidden("Only POST requests are allowed.")
+
+ # Whitelist of allowed hosts
+ allowed_hosts = ["wwwint.ebi.ac.uk"]
+ if settings.DEBUG:
+ allowed_hosts.extend(["localhost:8000", "127.0.0.1"])
+
+ referer = request.META.get("HTTP_REFERER", "")
+ if not any(host in referer for host in allowed_hosts):
+ return HttpResponseForbidden("Invalid referer.")
+
+ # Parse JSON body from request
+ try:
+ data = json.loads(request.body)
+ except json.JSONDecodeError:
+ return JsonResponse(
+ {"status": "error", "message": "Invalid JSON in request body."}, status=400
+ )
+
+ doorbell_api_key = settings.DOORBELL_API_KEY
+ doorbell_api_id = settings.DOORBELL_API_ID
+ doorbell_url = f"https://doorbell.io/api/applications/{doorbell_api_id}/submit?key={doorbell_api_key}"
+
+ feedback_data = {
+ "message": data.get("message", ""),
+ "email": data.get("email", ""),
+ "name": data.get("name", ""),
+ "url": data.get("url", referer),
+ "properties": {
+ "source": "DocBot",
+ },
+ }
+
+ try:
+ response = requests.post(doorbell_url, json=feedback_data)
+ if response.status_code == 201:
+ return JsonResponse({"status": "success"})
+ else:
+ return JsonResponse(
+ {"status": "error", "message": "Failed to submit feedback."}, status=500
+ )
+ except requests.RequestException:
+ return JsonResponse(
+ {"status": "error", "message": "An error occurred while submitting feedback."},
+ status=500,
+ )
+
def handler500(request, *args, **argv):
"""
diff --git a/rnacentral/rnacentral/settings.py b/rnacentral/rnacentral/settings.py
index 9d1315abe..7437d65bc 100644
--- a/rnacentral/rnacentral/settings.py
+++ b/rnacentral/rnacentral/settings.py
@@ -344,6 +344,10 @@
"EBI_SEARCH_ENDPOINT", "https://www.ebi.ac.uk/ebisearch/ws/rest/rnacentral"
)
+# Doorbell.io feedback integration
+DOORBELL_API_KEY = os.getenv("DOORBELL_API_KEY", "")
+DOORBELL_API_ID = os.getenv("DOORBELL_API_ID", "")
+
RELEASE_ANNOUNCEMENT_URL = (
"https://blog.rnacentral.org/2025/10/rnacentral-release-26.html"
)
From 58aa374b1a44033bdadfadfb938de6d3f8591f05 Mon Sep 17 00:00:00 2001
From: Isaac Jandalala
Date: Mon, 19 Jan 2026 14:55:08 +0000
Subject: [PATCH 2/5] Remove port from allowed hosts.
---
rnacentral/portal/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rnacentral/portal/views.py b/rnacentral/portal/views.py
index fdbc06611..7d4486e6e 100644
--- a/rnacentral/portal/views.py
+++ b/rnacentral/portal/views.py
@@ -1039,7 +1039,7 @@ def docbot_feedback(request):
# Whitelist of allowed hosts
allowed_hosts = ["wwwint.ebi.ac.uk"]
if settings.DEBUG:
- allowed_hosts.extend(["localhost:8000", "127.0.0.1"])
+ allowed_hosts.extend(["localhost", "127.0.0.1"])
referer = request.META.get("HTTP_REFERER", "")
if not any(host in referer for host in allowed_hosts):
From 0ec6fe70149e0a2caa7b59a24c99ab6c7a11f294 Mon Sep 17 00:00:00 2001
From: Isaac Jandalala
Date: Tue, 20 Jan 2026 12:40:43 +0000
Subject: [PATCH 3/5] Add page size options and search filter to relationships.
---
rnacentral/apiv1/views.py | 32 ++++---
.../sequence/relationships/relationships.js | 85 ++++++++++++++++++-
2 files changed, 103 insertions(+), 14 deletions(-)
diff --git a/rnacentral/apiv1/views.py b/rnacentral/apiv1/views.py
index f01a895b9..bd9601071 100644
--- a/rnacentral/apiv1/views.py
+++ b/rnacentral/apiv1/views.py
@@ -1244,9 +1244,9 @@ class RelationshipsView(generics.ListAPIView):
def get_queryset(self):
"""Return relationship data for a given URS and taxid from RNA-KG API"""
-
+
node_id = f"{self.kwargs['pk']}_{self.kwargs['taxid']}"
-
+
# Get the relationships using the original endpoint
rna_kg_url = "https://rna-kg.biodata.di.unimi.it/api/v1/incoming/id"
relationships_params = {
@@ -1254,32 +1254,44 @@ def get_queryset(self):
'node_id_scheme': 'RNAcentral',
'filter_rnacentral_rels': 'false'
}
-
+
try:
relationships_response = requests.get(rna_kg_url, params=relationships_params, timeout=10)
relationships_response.raise_for_status()
relationships_data = relationships_response.json()
relationships = relationships_data.get('relationships', [])
-
+
+ # Apply search filter if provided
+ search_query = self.request.query_params.get('search', '').strip().lower()
+ if search_query:
+ filtered_relationships = []
+ for rel in relationships:
+ node_props = rel.get('node_properties', {})
+ label = (node_props.get('Label') or rel.get('node_id') or '').lower()
+ description = (node_props.get('Description') or '').lower()
+ if search_query in label or search_query in description:
+ filtered_relationships.append(rel)
+ relationships = filtered_relationships
+
# Create a mock queryset-like object for pagination
class RelationshipQuerySet:
def __init__(self, data):
self.data = data
-
+
def __iter__(self):
return iter(self.data)
-
+
def __len__(self):
return len(self.data)
-
+
def count(self):
return len(self.data)
-
+
def __getitem__(self, key):
return self.data[key]
-
+
return RelationshipQuerySet(relationships)
-
+
except Exception as e:
# Log the error if you have logging set up
# print(f"Error fetching RNA-KG relationships data: {e}")
diff --git a/rnacentral/portal/static/js/components/sequence/relationships/relationships.js b/rnacentral/portal/static/js/components/sequence/relationships/relationships.js
index a38035fab..b70ea9a4a 100644
--- a/rnacentral/portal/static/js/components/sequence/relationships/relationships.js
+++ b/rnacentral/portal/static/js/components/sequence/relationships/relationships.js
@@ -29,18 +29,63 @@
ctrl.pageSize = 20;
ctrl.showPagination = false;
ctrl.rnaSequenceRnakgId = null;
+ ctrl.filterText = '';
+ ctrl.serverSearch = '';
+ ctrl.pageSizeOptions = [10, 20, 50, 100];
+
+ ctrl.filterByTargetOrDescription = function(rel) {
+ if (!ctrl.filterText) {
+ return true;
+ }
+ var searchText = ctrl.filterText.toLowerCase();
+ var target = (rel.node_properties.Label || rel.node_id || '').toLowerCase();
+ var description = (rel.node_properties.Description || '').toLowerCase();
+ return target.indexOf(searchText) !== -1 || description.indexOf(searchText) !== -1;
+ };
+
+ ctrl.onFilterChange = function() {
+ // Clear server search message when filter text is manually cleared
+ if (!ctrl.filterText && ctrl.serverSearch) {
+ ctrl.serverSearch = '';
+ ctrl.loadRelationships(1);
+ }
+ };
+
+ ctrl.onSearchKeypress = function(event) {
+ if (event.keyCode === 13) {
+ ctrl.searchAll();
+ }
+ };
+
+ ctrl.searchAll = function() {
+ ctrl.serverSearch = ctrl.filterText;
+ ctrl.loadRelationships(1);
+ };
+
+ ctrl.clearFilter = function() {
+ ctrl.filterText = '';
+ ctrl.serverSearch = '';
+ ctrl.loadRelationships(1);
+ };
+
+ ctrl.changePageSize = function() {
+ ctrl.loadRelationships(1);
+ };
ctrl.loadRelationships = function(page, append) {
if (!ctrl.taxid || !ctrl.upi) {
return;
}
-
+
page = page || 1;
append = append || false;
ctrl.loading = true;
ctrl.error = false;
-
- var relationshipsUrl = '/api/v1/rna/' + ctrl.upi + '/relationships/' + ctrl.taxid + '?page=' + page;
+
+ var relationshipsUrl = '/api/v1/rna/' + ctrl.upi + '/relationships/' + ctrl.taxid + '?page=' + page + '&page_size=' + ctrl.pageSize;
+ if (ctrl.serverSearch) {
+ relationshipsUrl += '&search=' + encodeURIComponent(ctrl.serverSearch);
+ }
$http.get(relationshipsUrl)
.then(function(response) {
@@ -147,6 +192,38 @@
Showing {{ctrl.getStartRecord()}} - {{ctrl.getEndRecord()}} of {{ctrl.totalCount}} relationships.
+
+
+
+
+
+
+
+
+
+
+
+ Filtering current page. Press Enter or click "Search all" to search all pages.
+
+
+ Showing results matching "{{ctrl.serverSearch}}" across all pages.
+
+