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
24 changes: 20 additions & 4 deletions medcat-trainer/webapp/api/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.contrib.auth.views import PasswordResetView
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import Q
from django.http import HttpResponseBadRequest, HttpResponseServerError, HttpResponse
from django.shortcuts import render
from django.utils import timezone
Expand Down Expand Up @@ -515,13 +516,28 @@ def add_concept(request):


@api_view(http_method_names=['POST'])
@permission_classes([permissions.IsAuthenticated])
def import_cdb_concepts(request):
user = request.user
if not user.is_superuser:
return HttpResponseBadRequest('User is not super user, and not allowed to download project outputs')
cdb_id = request.data.get('cdb_id')
if cdb_id is None or len(ConceptDB.objects.filter(id=cdb_id)) == 0:
return HttpResponseBadRequest(f'No CDB found for cdb_id{cdb_id}')
if cdb_id is None or not ConceptDB.objects.filter(id=cdb_id).exists():
return HttpResponseBadRequest('No CDB found for the provided cdb_id')

# Staff/superusers may import for any CDB. Other authenticated users must be a
# project admin (member or group administrator) of at least one project that
# already references this CDB via cdb_search_filter, so project setup doesn't
# require elevation.
if not (user.is_superuser or user.is_staff):
authorised = ProjectAnnotateEntities.objects.filter(
Q(cdb_search_filter__id=cdb_id),
Q(members=user) | Q(group__administrators=user),
).exists()
if not authorised:
return Response(
{'error': 'You do not have permission to import concepts for this CDB'},
status=403,
)

import_concepts_from_cdb(cdb_id)
return Response({'message': 'submitted cdb import job.'})

Expand Down
4 changes: 2 additions & 2 deletions medcat-trainer/webapp/frontend/src/styles/_admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,8 @@
transition: color 0.2s ease;
}

&:has(.form-control:disabled) label,
&:has(input:disabled) label {
&:has(.form-control:disabled) label:not(.checkbox-label),
&:has(input:disabled) label:not(.checkbox-label) {
color: #6c757d;
opacity: 0.7;
}
Expand Down
93 changes: 92 additions & 1 deletion medcat-trainer/webapp/frontend/src/views/ProjectAdmin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,22 @@
</div>
<div class="form-group form-group-inline">
<label>Annotation Guideline Link</label>
<input v-model="formData.annotation_guideline_link" type="url" class="form-control" placeholder="https://..." />
<input
v-model="formData.annotation_guideline_link"
type="url"
class="form-control"
placeholder="https://..."
:disabled="useDefaultGuideline"
/>
<label class="checkbox-label default-guideline-toggle">
<input
v-model="useDefaultGuideline"
type="checkbox"
class="checkbox-input"
@change="onUseDefaultGuidelineChange"
/>
<span class="checkbox-text">Use Default Guideline</span>
</label>
</div>
</div>
</div>
Expand Down Expand Up @@ -589,6 +604,8 @@ export default {
cloneName: '',
saving: false,
useBackupOption: false,
useDefaultGuideline: false,
defaultAnnotationGuidelineUrl: 'https://docs.google.com/document/d/1U8qpw5hDZgMGRxF7J1ur1fy4krA7Ijbb8ip_SUS5z9c/edit?tab=t.0#heading=h.imad4ksd7q78',
selectedCuiFilterConcepts: [],
includeSubConcepts: false,
showCuiFilterTextarea: false,
Expand Down Expand Up @@ -717,6 +734,7 @@ export default {
}
// Show backup options if CDB or Vocab are set
this.useBackupOption = !!(project.concept_db || project.vocab)
this.useDefaultGuideline = project.annotation_guideline_link === this.defaultAnnotationGuidelineUrl
// Initialize CUI filter concepts from existing cuis
if (project.cuis) {
this.syncPillsFromCuiText()
Expand All @@ -729,6 +747,7 @@ export default {
this.showCreateForm = false
this.editingProject = null
this.useBackupOption = false
this.useDefaultGuideline = false
this.validationErrors = {}
this.selectedCuiFilterConcepts = []
this.includeSubConcepts = false
Expand Down Expand Up @@ -768,10 +787,16 @@ export default {
members: []
}
this.useBackupOption = false
this.useDefaultGuideline = false
this.selectedCuiFilterConcepts = []
this.includeSubConcepts = false
this.showCuiFilterTextarea = false
},
onUseDefaultGuidelineChange() {
this.formData.annotation_guideline_link = this.useDefaultGuideline
? this.defaultAnnotationGuidelineUrl
: ''
},
handleCuiFileChange(event) {
const file = event.target.files[0]
if (file) {
Expand Down Expand Up @@ -924,6 +949,13 @@ export default {
}
payload.cdb_search_filter = conceptDbIdForFilter != null ? [conceptDbIdForFilter] : []

// Detect whether the project's underlying concept DB changed (covers new projects
// and model-pack swaps that point at a different CDB). Used after save to verify
// that the Solr "Concepts Imported" index exists for the resolved CDB.
const previousCdbId = this.editingProject?.cdb_search_filter?.[0] ?? null
const newCdbId = conceptDbIdForFilter
const cdbChanged = newCdbId != null && newCdbId !== previousCdbId

// Ensure members are integers
if (Array.isArray(payload.members)) {
payload.members = payload.members
Expand Down Expand Up @@ -970,6 +1002,14 @@ export default {

// If we get here, the request was successful
this.$toast?.success(`Project ${this.editingProject ? 'updated' : 'created'} successfully`)

// When the resolved CDB changed (new project, or model pack now points at a
// different CDB), make sure "Concepts Imported" is green and trigger an import
// if it isn't. Failures here must not block the save flow.
if (cdbChanged) {
await this.ensureConceptsImported(newCdbId)
}

this.closeForm()
await this.fetchProjects()
} catch (error) {
Expand Down Expand Up @@ -999,6 +1039,29 @@ export default {
this.saving = false
}
},
async ensureConceptsImported(cdbId) {
// Mirrors the "Concepts Imported" check from ProjectList.vue: ask Solr whether the
// CDB's collection exists, and if it doesn't, kick off the (background) import job
// exposed at /api/import-cdb-concepts/. The backend authorises project admins of
// any project that references this CDB (plus staff/superusers); errors surface as
// a toast and never block the save flow.
try {
const statusResp = await this.$http.get(`/api/concept-db-search-index-created/?cdbs=${cdbId}`)
const results = statusResp.data?.results || {}
if (results[cdbId]) {
return
}
await this.$http.post('/api/import-cdb-concepts/', { cdb_id: cdbId })
this.$toast?.success('Started importing concepts for the selected model. This may take a few minutes to complete.')
} catch (error) {
console.error('Error checking/importing concepts for cdb', cdbId, error)
const respData = error.response?.data
const errorMsg = typeof respData === 'string' && respData
? respData
: (respData?.error || respData?.message || 'Failed to verify or trigger concept import for the selected model')
this.$toast?.error(errorMsg)
}
},
cloneProject(project) {
this.projectToClone = project
this.cloneName = `${project.name} (Clone)`
Expand Down Expand Up @@ -1700,6 +1763,34 @@ export default {
color: var(--color-text);
opacity: 0.7;
}

.default-guideline-toggle {
display: flex;
align-items: center;
gap: 8px;
margin: 6px 0 0 0;
padding: 0;
cursor: pointer;
font-size: 0.85rem;
color: var(--color-text);

.checkbox-input {
margin: 0;
width: 14px;
height: 14px;
accent-color: $primary;
cursor: pointer;
flex-shrink: 0;
}

.checkbox-text {
line-height: 1.4;
}

&:hover {
opacity: 0.85;
}
}
}

.checkbox-group {
Expand Down