Skip to content

Commit

Permalink
Merge pull request #100 from snac-cooperative/nsm/34653-concept-relat…
Browse files Browse the repository at this point in the history
…ionships

[#34653] Relate and Unrelate Vocabulary Concepts to other concepts
  • Loading branch information
jglass-st authored Nov 10, 2024
2 parents 376a4be + dada331 commit 064398c
Show file tree
Hide file tree
Showing 11 changed files with 698 additions and 54 deletions.
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

0 comments on commit 064398c

Please sign in to comment.