diff --git a/medcat-trainer/webapp/api/api/views.py b/medcat-trainer/webapp/api/api/views.py
index e0c78ee54..a75b7d832 100644
--- a/medcat-trainer/webapp/api/api/views.py
+++ b/medcat-trainer/webapp/api/api/views.py
@@ -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
@@ -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.'})
diff --git a/medcat-trainer/webapp/frontend/src/styles/_admin.scss b/medcat-trainer/webapp/frontend/src/styles/_admin.scss
index ff6001726..9f5e81fc6 100644
--- a/medcat-trainer/webapp/frontend/src/styles/_admin.scss
+++ b/medcat-trainer/webapp/frontend/src/styles/_admin.scss
@@ -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;
}
diff --git a/medcat-trainer/webapp/frontend/src/views/ProjectAdmin.vue b/medcat-trainer/webapp/frontend/src/views/ProjectAdmin.vue
index 7ae148c57..df41543a4 100644
--- a/medcat-trainer/webapp/frontend/src/views/ProjectAdmin.vue
+++ b/medcat-trainer/webapp/frontend/src/views/ProjectAdmin.vue
@@ -141,7 +141,22 @@
-
+
+
@@ -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,
@@ -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()
@@ -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
@@ -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) {
@@ -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
@@ -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) {
@@ -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)`
@@ -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 {