Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
9349b4c
Merge pull request #516 from MetaCell/release/5.2.0
afonsobspinto Oct 9, 2025
b9cd889
SCKAN-444 feat: Add new axiom to neurondm mapping
afonsobspinto Oct 9, 2025
8067fcb
fix: Remove unnecessary escape character
afonsobspinto Oct 9, 2025
1e1da7a
Merge pull request #517 from MetaCell/feature/SCKAN-444
afonsobspinto Oct 9, 2025
e09590d
SCKAN-439 feat: Add new expert consultant model
afonsobspinto Oct 10, 2025
ae5539d
chore: Replace regular string with raw strings for url_path params
afonsobspinto Oct 10, 2025
4dc7c86
SCKAN-439 feat: Add new expert consultant to frontend
afonsobspinto Oct 10, 2025
84dd432
SCKAN-439 feat: Add django command to migrate alerts to new property
afonsobspinto Oct 10, 2025
09fa44c
SCKAN-439 feat: Connect expert_consultants in ingestion
afonsobspinto Oct 10, 2025
11f9637
Fix: Remove double uri validators
afonsobspinto Oct 10, 2025
9ca28bd
Update applications/composer/backend/composer/management/commands/mig…
afonsobspinto Oct 10, 2025
f74fed3
Merge pull request #518 from MetaCell/feature/SCKAN-439
afonsobspinto Oct 10, 2025
b1d056b
SCKAN-437 feat: Update relationships model
afonsobspinto Oct 13, 2025
3241229
SCKAN-437 feat: Adapt frontend to new relationship types
afonsobspinto Oct 13, 2025
2fa82e0
SCKAN-437 feat: Drop anatomical_single options
afonsobspinto Oct 13, 2025
6eeabb8
SCKAN-437 fix: Add free text updates on Blur
afonsobspinto Oct 13, 2025
a246536
SCKAN-437 feat: Customize CustomEntitiesDropdown typography
afonsobspinto Oct 13, 2025
8baef1e
SCKAN-437 feat: Update export
afonsobspinto Oct 14, 2025
b8be938
style: Apply linting
afonsobspinto Oct 14, 2025
9d9ee1b
SCKAN-437 fix: Consolidate multi select triples
afonsobspinto Oct 14, 2025
6abd230
SCKAN-437 fix: Correct equality check
afonsobspinto Oct 14, 2025
0ce5d20
SCKAN-437 fix: Add ownership checks on dynamic relationship changes
afonsobspinto Oct 14, 2025
9ce03bc
Merge pull request #519 from MetaCell/feature/SCKAN-437
afonsobspinto Oct 14, 2025
d050d4d
SCKAN-429 feat: Integrate custom code execution in ingestion
afonsobspinto Oct 14, 2025
0caf75c
SCKAN-429 feat: Split ingestion into pre-processing and database inge…
afonsobspinto Oct 15, 2025
68882f1
SCKAN-429 feat: Add ingestion trigger from django admin
afonsobspinto Oct 15, 2025
dd5d914
SCKAN-429 feat: Move ingestion to argo workflows
afonsobspinto Oct 15, 2025
42e7fc2
SCKAN-429 chore: Add updated codefresh specification
afonsobspinto Oct 15, 2025
dad0943
SCKAN-429 chore: Update neurondm task dockerfile
afonsobspinto Oct 15, 2025
4fd342f
SCKAN-429 chore: Bump python version on neurondm task dockerfile
afonsobspinto Oct 15, 2025
3279a77
SCKAN-429 feat: Update custom ingestion help text.
afonsobspinto Oct 15, 2025
d06dd24
SCKAN-429 fix: Adapt neurondm standalone to be django independent
afonsobspinto Oct 16, 2025
1c216fa
SCKAN-429 chore: Use latest cloud-harness
afonsobspinto Oct 16, 2025
b2f2fbf
SCKAN-429 chore: Update timezone from CET to "Europe/Paris"
afonsobspinto Oct 16, 2025
e4261a8
SCKAN-429 feat: Update neurondm model serialization
afonsobspinto Oct 16, 2025
ff5e67f
SCKAN-429 feat: Update neurondm model deserialization
afonsobspinto Oct 16, 2025
26c72d2
SCKAN-429 typo: Use correct property name
afonsobspinto Oct 16, 2025
8d59459
SCKAN-429 typo: Bump django filters version
afonsobspinto Oct 16, 2025
531362c
Merge pull request #522 from MetaCell/release/5.2.0
afonsobspinto Oct 17, 2025
5b6a9fb
chore: Remove silk
afonsobspinto Oct 17, 2025
e8322cf
SCKAN-429 feat: Improve admin performance
afonsobspinto Oct 17, 2025
8e1fdc0
SCKAN-429 fix: Correct ingestion output file paths
afonsobspinto Oct 17, 2025
8ed517a
Merge branch 'develop' into feature/SCKAN-429
afonsobspinto Oct 18, 2025
2c0e7c9
SCKAN-429 fix: Use int instead of str for relationship ids comparation
afonsobspinto Oct 18, 2025
f973be8
SCKAN-429 feat: Update ingestion files location
afonsobspinto Oct 18, 2025
d1bf06d
Merge pull request #523 from MetaCell/feature/SCKAN-429
afonsobspinto Oct 20, 2025
a2ecaf2
fix: Add missing logger_service in cs ingestion services (affects loc…
afonsobspinto Oct 22, 2025
0420881
SCKAN-447 fix: Force https scheme
afonsobspinto Oct 22, 2025
d2e3eca
Merge pull request #525 from MetaCell/feature/SCKAN-447
afonsobspinto Oct 22, 2025
ab4b213
bumping version up
ddelpiano Oct 27, 2025
04d5d19
fix: Delete any ConnectivityStatementTriple where triple is NULL
afonsobspinto Oct 28, 2025
e15e004
Merge pull request #527 from MetaCell/feature/fix-triple-migration
ddelpiano Oct 28, 2025
f139c84
SCKAN-449 feat: Add region layer support for custom code relationships
afonsobspinto Oct 29, 2025
ff699a2
Merge pull request #528 from MetaCell/feature/SCKAN-449
ddelpiano Oct 29, 2025
3f0e4a1
SCKAN-450 feat: Add reassign population indices command
afonsobspinto Nov 5, 2025
3be8635
SCKAN-450 fix: Set population index on invalid statements
afonsobspinto Nov 5, 2025
36a0439
SCKAN-450 feat: Apply copilot optmizations
afonsobspinto Nov 5, 2025
5e41e5e
SCKAN-450 feat: Show population_index in admin
afonsobspinto Nov 5, 2025
bba5974
changing KS admin view, added filter for population set
ddelpiano Nov 6, 2025
ffa44b5
fixing issue with data integration following ingestion
ddelpiano Nov 6, 2025
54cdb9e
bumped version up for security advice
ddelpiano Nov 6, 2025
4c83d00
Merge pull request #531 from MetaCell/feature/SCKAN-450
afonsobspinto Nov 6, 2025
870acc9
admin view removed has been exported flag
ddelpiano Nov 13, 2025
dfa51e3
updating stage pipeline
ddelpiano Nov 13, 2025
4280f5a
chore: Update stage codefresh pipeline
afonsobspinto Nov 13, 2025
a092eed
Merge pull request #532 from MetaCell/feature/update-stage
ddelpiano Nov 13, 2025
6ef1b8d
wrapping secrets for pipeline
ddelpiano Nov 13, 2025
cf5b9f6
wrapping secrets for pipeline 2
ddelpiano Nov 13, 2025
e35517a
stage pipeline fix 3
ddelpiano Nov 13, 2025
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
4 changes: 1 addition & 3 deletions applications/composer/backend/backend/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@

LANGUAGE_CODE = "en-us"

TIME_ZONE = "CET"
TIME_ZONE = "Europe/Paris"

USE_I18N = True

Expand Down Expand Up @@ -412,10 +412,8 @@
if DEBUG:
INSTALLED_APPS += [
"debug_toolbar",
'silk',
]
MIDDLEWARE += [
'silk.middleware.SilkyMiddleware',
"debug_toolbar.middleware.DebugToolbarMiddleware",
]
DEBUG_TOOLBAR_CONFIG = {
Expand Down
1 change: 0 additions & 1 deletion applications/composer/backend/backend/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,4 @@
urlpatterns += [
# Debug toolbar
path("__debug__/", include("debug_toolbar.urls")),
path('silk/', include('silk.urls', namespace='silk'))
]
218 changes: 192 additions & 26 deletions applications/composer/backend/composer/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@
from composer.models import (
AlertType,
ConnectivityStatementTriple,
ConnectivityStatementText,
ConnectivityStatementAnatomicalEntity,
Phenotype,
Relationship,
Sex,
PopulationSet,
ConnectivityStatement,
Provenance,
ExpertConsultant,
ExportBatch,
Note,
Profile,
Expand All @@ -45,7 +48,7 @@
Region,
AnatomicalEntityIntersection,
AnatomicalEntity,
CSState
CSState,
)


Expand All @@ -64,6 +67,11 @@ class ProvenanceInline(admin.StackedInline):
extra = 1


class ExpertConsultantInline(admin.StackedInline):
model = ExpertConsultant
extra = 1


class SynonymInline(admin.StackedInline):
model = Synonym
extra = 1
Expand Down Expand Up @@ -104,9 +112,50 @@ class StatementAlertInline(admin.StackedInline):


class RelationshipAdmin(admin.ModelAdmin):
list_display = ("title", "predicate_name", "predicate_uri", "type", "order")
list_display = ("title", "predicate_name", "predicate_uri", "type", "order", "has_custom_code")
ordering = ("order",)
search_fields = ("title", "predicate_name", "predicate_uri")
fieldsets = (
(None, {
'fields': ('title', 'predicate_name', 'predicate_uri', 'type', 'order')
}),
('Custom Ingestion Code', {
'classes': ('collapse',),
'fields': ('custom_ingestion_code',),
'description': (
'Add custom Python code to extract data from NeuroDM during ingestion. '
),
}),
)

def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if 'custom_ingestion_code' in form.base_fields:
form.base_fields['custom_ingestion_code'].widget = forms.Textarea(attrs={
'rows': 15,
'cols': 100,
'style': 'font-family: monospace; font-size: 12px;'
})
form.base_fields['custom_ingestion_code'].help_text = mark_safe(
"Optional Python code to extract data from NeuroDM for this relationship during ingestion.<br>"
"The code has access to:<br>"
"• <code>fc</code>: dict with neuron properties (id, label, species, phenotype, etc.)<br>"
"• <code>fc[\"_neuron\"]</code>: the NeuroDM neuron object<br><br>"
"The code must define a <code>result</code> variable with the output:<br>"
"• For TRIPLE relationships: list of dicts [{'name': str, 'uri': str}, ...]<br>"
"• For TEXT relationships: list of strings or single string<br>"
"• For ANATOMICAL_ENTITY relationships:<br>"
"&nbsp;&nbsp;- Simple entities: list of URI strings ['http://purl.obolibrary.org/obo/UBERON_0001234', ...]<br>"
"&nbsp;&nbsp;- Region-layer pairs: list of dicts [{'region': 'region_uri', 'layer': 'layer_uri'}, ...]<br>"
"&nbsp;&nbsp;- Mixed: list combining both formats<br>"
"&nbsp;&nbsp;- Note: Region-layer pairs respect the 'update_anatomical_entities' flag<br><br>"
"Errors are logged to the ingestion anomalies file and the relationship will be skipped."
)
return form

@admin.display(description="Has Custom Code", boolean=True)
def has_custom_code(self, obj):
return bool(obj.custom_ingestion_code and obj.custom_ingestion_code.strip())

class TripleAdmin(admin.ModelAdmin):
list_display = ("name", "uri", "relationship")
Expand All @@ -117,7 +166,9 @@ class TripleAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if "relationship" in form.base_fields:
form.base_fields["relationship"].queryset = Relationship.objects.exclude(type=RelationshipType.TEXT)
form.base_fields["relationship"].queryset = Relationship.objects.exclude(
type__in=[RelationshipType.TEXT, RelationshipType.ANATOMICAL_MULTI]
)
return form

class ConnectivityStatementInline(nested_admin.NestedStackedInline):
Expand Down Expand Up @@ -267,8 +318,52 @@ class DestinationInline(admin.TabularInline):
class ConnectivityStatementTripleInline(admin.TabularInline):
model = ConnectivityStatementTriple
extra = 1
autocomplete_fields = ("relationship", "triple")
fields = ("relationship", "triple", "free_text")
autocomplete_fields = ("relationship",)
raw_id_fields = ("triples",)
fields = ("relationship", "triples")

def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if "relationship" in form.base_fields:
# Only show triple relationship types
form.base_fields["relationship"].queryset = Relationship.objects.filter(
type__in=[RelationshipType.TRIPLE_SINGLE, RelationshipType.TRIPLE_MULTI]
)
return form


class ConnectivityStatementTextInline(admin.TabularInline):
model = ConnectivityStatementText
extra = 1
autocomplete_fields = ("relationship",)
fields = ("relationship", "text")

def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if "relationship" in form.base_fields:
# Only show text relationship types
form.base_fields["relationship"].queryset = Relationship.objects.filter(
type=RelationshipType.TEXT
)
return form


class ConnectivityStatementAnatomicalEntityInline(admin.TabularInline):
model = ConnectivityStatementAnatomicalEntity
extra = 1
autocomplete_fields = ("relationship",)
raw_id_fields = ("anatomical_entities",)
fields = ("relationship", "anatomical_entities")

def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if "relationship" in form.base_fields:
# Only show anatomical entity relationship types
form.base_fields["relationship"].queryset = Relationship.objects.filter(
type__in=[RelationshipType.ANATOMICAL_MULTI]
)
return form


class ConnectivityStatementAdmin(
SortableAdminBase, FSMTransitionMixin, admin.ModelAdmin
Expand All @@ -279,25 +374,24 @@ class ConnectivityStatementAdmin(
readonly_fields = (
"state",
"curie_id",
"has_statement_been_exported",
"reference_uri",
"population_index"
)
exclude = ("journey_path", "statement_prefix", "statement_suffix", "population_index")
exclude = ("journey_path", "statement_prefix", "statement_suffix", )
autocomplete_fields = ("sentence", "origins")
date_hierarchy = "modified_date"
list_display = (
"sentence",
"pmid",
"pmcid",
"short_ks",
"population_set_name",
"population_index",
"tag_list",
"state",
"has_notes",
"owner",
)
list_display_links = ("sentence", "pmid", "pmcid", "short_ks", "state")
list_filter = ("state", "owner", "tags__tag")
list_select_related = ("sentence", "origins", "destinations")
list_display_links = ("sentence", "short_ks", "state")
list_filter = ("state", "population", "owner", "tags__tag")
list_select_related = ("sentence", "population", "owner", "origins", "destinations")
search_fields = (
"sentence__title",
"sentence__text",
Expand All @@ -309,8 +403,9 @@ class ConnectivityStatementAdmin(

fieldsets = ()

inlines = (ProvenanceInline, NoteConnectivityStatementInline,
ViaInline, DestinationInline, StatementAlertInline, ConnectivityStatementTripleInline)
inlines = (ProvenanceInline, ExpertConsultantInline, NoteConnectivityStatementInline,
ViaInline, DestinationInline, StatementAlertInline, ConnectivityStatementTripleInline,
ConnectivityStatementTextInline, ConnectivityStatementAnatomicalEntityInline)

def _filter_admin_transitions(self, transitions_generator):
"""
Expand All @@ -337,17 +432,9 @@ def delete_queryset(self, request, queryset):
def short_ks(self, obj):
return str(obj)

@admin.display(description="PMID")
def pmid(self, obj):
return obj.sentence.pmid

@admin.display(description="PMCID")
def pmcid(self, obj):
return obj.sentence.pmcid

@admin.display(description="REFERENCE")
def reference(self, obj):
return str(obj)
@admin.display(description="Population Set")
def population_set_name(self, obj):
return obj.population.name if obj.population else "-"


class ExportBatchAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -391,6 +478,45 @@ class IngestSentenceForm(forms.Form):
file = forms.FileField(label="CSV file")


class IngestStatementsForm(forms.Form):
"""Form for configuring connectivity statement ingestion parameters"""
update_upstream = forms.BooleanField(
required=False,
initial=False,
label="Update upstream statements",
help_text="Set this flag to update upstream statements."
)
update_anatomical_entities = forms.BooleanField(
required=False,
initial=False,
label="Update anatomical entities",
help_text="Set this flag to try move anatomical entities to specific layer, region."
)
disable_overwrite = forms.BooleanField(
required=False,
initial=False,
label="Disable overwrite",
help_text="Set this flag to prevent overwriting existing statements."
)
full_imports = forms.CharField(
required=False,
widget=forms.Textarea(attrs={'rows': 3, 'placeholder': 'Enter URIs separated by commas or new lines'}),
label="Full imports",
help_text="List of full imports to include in the ingestion (comma or newline separated)."
)
label_imports = forms.CharField(
required=False,
widget=forms.Textarea(attrs={'rows': 3, 'placeholder': 'Enter labels separated by commas or new lines'}),
label="Label imports",
help_text="List of label imports to include in the ingestion (comma or newline separated)."
)
population_file = forms.FileField(
required=False,
label="Population file",
help_text="Text file containing population URIs (one per line). Only statements matching these URIs will be processed."
)


# Custom view for ingesting sentences from a CSV file
def ingest_sentences_view(request):
output = None
Expand Down Expand Up @@ -423,11 +549,51 @@ def ingest_sentences_view(request):
return render(request, "admin/ingest_sentences.html", context)


# Custom view for downloading ingestion log files
def download_logs_view(request):
"""
Admin page with links to download ingestion log files.
"""
context = admin.site.each_context(request)
context.update({
"title": "Download Ingestion Logs",
"anomalies_url": reverse('composer-api:ingestion-logs') + '?log_type=anomalies',
"ingested_url": reverse('composer-api:ingestion-logs') + '?log_type=ingested',
})
return render(request, "admin/download_logs.html", context)


# Custom view for ingesting connectivity statements
def ingest_statements_view(request):
"""
Admin page for configuring and triggering connectivity statement ingestion.
"""
context = admin.site.each_context(request)
if request.method == "POST":
form = IngestStatementsForm(request.POST, request.FILES)
if form.is_valid():
context.update({
"form": form,
"title": "Ingest Connectivity Statements",
})
return render(request, "admin/ingest_statements.html", context)
else:
form = IngestStatementsForm()

context.update({
"form": form,
"title": "Ingest Connectivity Statements",
})
return render(request, "admin/ingest_statements.html", context)


def custom_admin_urls(original_get_urls):
def get_urls():
urls = original_get_urls()
custom_urls = [
path('ingest-sentences/', admin.site.admin_view(ingest_sentences_view), name='ingest-sentences'),
path('ingest-statements/', admin.site.admin_view(ingest_statements_view), name='ingest-statements'),
path('download-logs/', admin.site.admin_view(download_logs_view), name='download-logs'),
]
return custom_urls + urls
return get_urls
Expand Down
Loading