Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#34653] Relate and Unrelate Vocabulary Concepts to other concepts #100

Merged
merged 23 commits into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
93ec46e
feat: add Concept model unit test
nevinsm Oct 28, 2024
3a1a60a
feat: Add unit tests for Concept model relationships
nevinsm Oct 28, 2024
aa8e2f6
feat: fix concept relationships
nevinsm Oct 28, 2024
df564b0
feat: add new alternate terms to concept
nevinsm Oct 28, 2024
f93c79d
feat: Add tests for broader, narrower, and related concept relationships
nevinsm Oct 28, 2024
21d0275
fix: correct unauthorized user relationship updating to pass related_…
nevinsm Oct 28, 2024
d7cdd90
chore: phpfmt updates
nevinsm Oct 28, 2024
ec2549c
fix: correct api route model binding
nevinsm Oct 28, 2024
c5a9573
feat: add relateConcept method to ConceptService
nevinsm Oct 28, 2024
fe54725
feat: Add relationship management functionality
nevinsm Oct 28, 2024
0494147
feat: Improve search functionality and data loading in Relationship m…
nevinsm Oct 28, 2024
14bfbc2
feat: Add searchConcepts method to ConceptService
nevinsm Oct 28, 2024
7b5f40c
feat: Add search endpoint to ConceptController
nevinsm Oct 28, 2024
6ae7959
feat: add comprehensive concept search test
nevinsm Oct 28, 2024
fcf604f
feat: update search endpoint testing, change conceptservice to use ne…
nevinsm Oct 28, 2024
8c3f29c
fix: update preferred_term property in Concept/Default.vue
nevinsm Oct 28, 2024
532a113
feat: Update relationships display in Default.vue
nevinsm Oct 28, 2024
d36129f
feat: Update show view to allow vue to handle relationship rendering
nevinsm Oct 28, 2024
331d2b2
feat: Add relationship removal functionality
nevinsm Oct 28, 2024
855c9b9
feat: Add ability to remove concept relationships
nevinsm Oct 28, 2024
5d8c185
feat: Add test coverage for remove concept relationships
nevinsm Oct 28, 2024
b03a5ff
feat: add request validation to relationship removal
nevinsm Oct 29, 2024
dada331
feat: add request validation and testing to relateConcepts method
nevinsm Oct 29, 2024
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
93 changes: 89 additions & 4 deletions app/Http/Controllers/API/ConceptController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ConceptController extends Controller
*/
public function __construct()
{
$this->middleware('auth:sanctum')->except(['index', 'show', 'reconcile']);
$this->middleware('auth:sanctum')->except(['index', 'show', 'reconcile', 'search']);
$this->authorizeResource(Concept::class);
}

Expand Down Expand Up @@ -174,7 +174,12 @@ public function relateConcepts(Request $request, Concept $concept)
if ($request->user()->cannot('update', $concept)) {
abort(403);
}


$request->validate([
'relation_type' => 'required|in:broader,narrower,related',
'related_id' => 'required|exists:concepts,id',
]);

$relation_type = $request->input('relation_type');
$related_id = $request->input('related_id');

Expand All @@ -190,7 +195,47 @@ public function relateConcepts(Request $request, Concept $concept)
break;
}

return $concept;
return $concept->loadMissing(['broader', 'narrower', 'related']);
}

/**
* Remove a relationship between concepts.
*
* @param \Illuminate\Http\Request $request
* @param \App\Concept $concept
* @return \Illuminate\Http\Response
*/
public function removeRelationship(Request $request, Concept $concept)
{
if ($request->user()->cannot('update', $concept)) {
abort(403);
}

$request->validate([
'relation_type' => 'required|in:broader,narrower,related',
'related_id' => 'required|exists:concepts,id',
]);

$relation_type = $request->input('relation_type');
$related_id = $request->input('related_id');

switch ($relation_type) {
case "broader":
$concept->broader()->detach($related_id);
break;
case "narrower":
$concept->narrower()->detach($related_id);
break;
case "related":
// Remove both directions for related relationships
$concept->related()->detach($related_id);
$concept->belongsToMany("App\Models\Concept", "concept_relationships", "related_concept_id", "concept_id")
->wherePivot("relationship_type", "related")
->detach($related_id);
break;
}

return $concept->loadMissing(['broader', 'narrower', 'related']);
}

/**
Expand All @@ -205,7 +250,7 @@ public function deprecate(Request $request, Concept $concept)
if ($request->user()->cannot('update', $concept)) {
abort(403);
}

$to = $request->input('to');
if ($to) {
$replaceConcept = Concept::findOrFail($to);
Expand Down Expand Up @@ -233,6 +278,46 @@ public function destroy(Concept $concept)
return response('Deleted ' . $concept->id, 204);
}

/**
* Search concepts by term.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
*/
public function search(Request $request)
{
$request->validate([
'term' => 'required|string|min:2',
'all_terms' => 'boolean',
'category' => 'nullable|string',
'per_page' => 'nullable|integer|min:1|max:100',
]);

$query = Concept::with(['terms', 'conceptCategories'])
->select('concepts.*')
->join('terms', 'concepts.id', '=', 'terms.concept_id')
->leftJoin('concept_categories', 'concepts.id', '=', 'concept_categories.concept_id')
->leftJoin('vocabulary', 'concept_categories.category_id', '=', 'vocabulary.id')
->where('terms.text', 'ILIKE', '%' . $request->term . '%')
->where('concepts.deprecated', false);

if (!$request->boolean('all_terms', false)) {
$query->where('terms.preferred', true);
}

if ($request->filled('category')) {
$query->where('vocabulary.value', 'ILIKE', $request->category);
}

$query->distinct();

$perPage = $request->input('per_page', 15);

return ConceptResource::collection(
$query->paginate($perPage)
);
}

/**
* Reconcile Concept for OpenRefine
*
Expand Down
25 changes: 9 additions & 16 deletions app/Models/Concept.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,6 @@ public function broader()
->wherePivot("relationship_type", "broader");
}

// This version requires insert of the two relationships (both ways, narrower and broader)
//public function narrower() {
//return $this->belongsToMany("App\Models\Concept", "concept_relationships", "concept_id", "related_concept_id")
//->withPivot("relationship_type")
//->wherePivot("relationship_type", "narrower");
//}

// This version requires insert only one relationships, the other relationship is inferred from concept_id or related_concept_id
// coming from other direction
public function narrower()
Expand All @@ -70,7 +63,7 @@ public function narrower()

public function related()
{
return $this->belongsToMany("App\Models\Concept", "concept_relationships", "related_concept_id", "concept_id")
return $this->belongsToMany("App\Models\Concept", "concept_relationships", "concept_id", "related_concept_id")
->withPivot("relationship_type")
->wherePivot("relationship_type", "related");
}
Expand All @@ -82,18 +75,18 @@ public function addBroader($conceptId)

public function addRelated($conceptId)
{
return $this->related()->attach([$conceptId => ["relationship_type" => "related"]]);
}
// Add both directions of the relationship
$this->belongsToMany("App\Models\Concept", "concept_relationships", "concept_id", "related_concept_id")
->attach([$conceptId => ["relationship_type" => "related"]]);

$this->belongsToMany("App\Models\Concept", "concept_relationships", "related_concept_id", "concept_id")
->attach([$conceptId => ["relationship_type" => "related"]]);

// This version requires insert of the two relationships (both ways, narrower and broader)
// public function addNarrower($concept) {
// // TODO : check it works for new narrower enum
// return $this->narrower()->attach([$concept => ["relationship_type" => "narrower"]]);
// }
return true;
}

public function addNarrower($conceptId)
{
// TODO : check it works for new narrower enum
return $this->narrower()->attach([$conceptId => ["relationship_type" => "broader"]]);
}

Expand Down
40 changes: 40 additions & 0 deletions resources/js/api/ConceptService.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,44 @@ export default {
return [error, null];
}
},

async searchConcepts(searchTerm, perPage = 10) {
try {
const { data } = await apiClient.get('/search', {
params: {
term: searchTerm,
per_page: perPage,
},
});
return [null, data];
} catch (error) {
return [error, null];
}
},

async relateConcept(conceptId, relationshipData) {
try {
const { data } = await apiClient.put(`/${conceptId}/relate_concept`, {
relation_type: relationshipData.type,
related_id: relationshipData.relatedId,
});
return [null, data];
} catch (error) {
return [error, null];
}
},

async removeRelationship(conceptId, relationshipData) {
try {
const { data } = await apiClient.delete(`/${conceptId}/relate_concept`, {
data: { // Using data property for DELETE request body
relation_type: relationshipData.type,
related_id: relationshipData.relatedId,
}
});
return [null, data];
} catch (error) {
return [error, null];
}
},
};
3 changes: 2 additions & 1 deletion resources/js/components/Concept/Default.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import MixinDirty from './mixins/Dirty';
import MixinEditMode from './mixins/EditMode';
import MixinSource from './mixins/Source';
import MixinTerm from './mixins/Term';
import MixinRelationship from './mixins/Relationship';

export default {
mixins: [MixinCategory, MixinDirty, MixinEditMode, MixinSource, MixinTerm],
mixins: [MixinCategory, MixinDirty, MixinEditMode, MixinSource, MixinTerm, MixinRelationship],
components: {
BModal,
BButton,
Expand Down
Loading
Loading