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

Search - Adds the ability to search within an LCV and create an Alias without a context #16

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
30 changes: 30 additions & 0 deletions app/uid/forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django import forms
from .models import Provider, LCVTerm, UIDRequestToken # Import Neo4j models directly
from uuid import uuid4
from .models import Alias # Import Neo4j models directly
#from .models import LastGeneratedUID

#class LastGeneratedUIDForm(forms.ModelForm):
Expand Down Expand Up @@ -70,3 +71,32 @@ class Meta:
model = LCVTerm
#fields = ['uid', 'term', 'echelon_level']
fields = ['provider_name', 'term', 'echelon', 'structure'] # UID is self Generated

# Search Forms
class SearchForm(forms.Form):
search_term = forms.CharField(max_length=255, required=True, label="Search Term")
search_type = forms.ChoiceField(choices=[
('general', 'General Search'),
('alias', 'Search by Alias'),
('definition', 'Search by Definition'),
('context', 'Search by Context'),
], required=True, label="Search Type"
)
context = forms.CharField(label='Context', required=False, max_length=255)

class AliasForm(forms.Form):
alias = forms.CharField(max_length=255, required=True) # The alias name
context = forms.CharField(max_length=255, required=False) # Context as a string (the term's name)

def save(self):
# Create and save Alias
alias = Alias(alias=self.cleaned_data['alias'], context=self.cleaned_data.get('context'))
alias.save()

# Optionally, if context is provided, link to the NeoTerm
if alias.context:
term = NeoTerm.nodes.get_or_none(name=alias.context)
if term:
alias.link_to_term(term) # Link this alias to the found NeoTerm

return alias
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anyway to make this work with NeoAlias?

50 changes: 49 additions & 1 deletion app/uid/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
from typing import List
from uuid import uuid4

from collections import defaultdict
from core.models import NeoTerm

logger = logging.getLogger(__name__)

GLOBAL_PROVIDER_OWNER_UID = "0xFFFFFFFF"
Expand All @@ -23,6 +26,51 @@ def check_neo4j_connection():
time.sleep(1) # Wait before retrying
return False

# Alias class incase you create and alias with no context
class Alias(StructuredNode):
alias = StringProperty(unique_index=True) # The alias name
context = StringProperty(required=False, default=None) # Optional context
points_to = RelationshipTo('NeoTerm', 'POINTS_TO') # The relationship to NeoTerm
context_error = StringProperty(required=False) # Optional field to store error message

def __str__(self):
return self.alias

def link_to_term(self, neo_term):
"""Link this alias to a NeoTerm."""
if isinstance(neo_term, NeoTerm):
self.points_to.connect(neo_term)

def save(self, *args, **kwargs):
"""Override the save method to automatically link the alias to a NeoTerm if context is provided."""
context_error = None # Initialize an error variable

# Call the parent class save method
super(Alias, self).save(*args, **kwargs)

if self.context:
# Get or create the NeoTerm based on the context
term, created = NeoTerm.get_or_create(uid=self.context)
if term:
# Set relationships for the NeoTerm, including the alias
term.set_relationships(definition_node, context_node, self)
else:
context_error = f"No matching NeoTerm found for context: {self.context}"
else:
# If no context is provided, link to a default NeoTerm (first available NeoTerm)
term = NeoTerm.nodes.first() # You can change this to a specific fallback logic
if term:
self.link_to_term(term)
else:
context_error = "No NeoTerm available to link."

# If an error was encountered, raise it so it can be caught in the view or returned to the form
if context_error:
self.context_error = context_error # Store the error message in the instance
self.save()

return context_error # Return the error message, if any

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the way you wrote this class. Is there anyway you can make NeoAlias work in a similar way?

# Generated Logs to track instance, time of generation, uid, provider and lcv terms
class GeneratedUIDLog(models.Model):
uid = models.CharField(max_length=255, default="UNKNOWN")
Expand Down Expand Up @@ -512,4 +560,4 @@ def report_all_term_uids():

# # Find collisions (where length > 1)
# collisions = {key: value for key, value in uid_dict.items() if len(value) > 1}
# return collisions
# return collisions
122 changes: 122 additions & 0 deletions app/uid/templates/search.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Search</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f9f9f9;
}
.container {
width: 80%;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
color: #333;
}
.form-group {
margin-bottom: 20px;
}
label {
font-size: 16px;
margin-bottom: 8px;
display: block;
}
input[type="text"], select, button {
padding: 10px;
font-size: 16px;
width: 100%;
max-width: 400px;
margin-top: 5px;
}
button {
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #45a049;
}
.results-list {
margin-top: 30px;
}
.result-item {
background-color: #fff;
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 10px;
border-radius: 5px;
}
.error-message {
color: red;
font-size: 16px;
}
</style>
</head>
<body>
<div class="container">
<h1>Search CCV/LCV Definitions</h1>

<form method="POST">
{% csrf_token %}
<div class="form-group">
<label for="search_term">Search Term:</label>
<input type="text" name="search_term" id="search_term" placeholder="Enter search term" required>
</div>

<div class="form-group">
<label for="search_type">Search By:</label>
<select name="search_type" id="search_type">
<option value="alias">Alias</option>
<option value="definition">Definition</option>
<option value="context">Context</option>
<option value="general">General</option>
</select>
</div>

<div class="form-group">
<button type="submit">Search</button>
</div>
</form>

<!-- Display search results -->
{% if results_data %}
<div class="results-list">
<h3>Search Results</h3>
<table>
<thead>
<tr>
<th>UID</th>
<th>Alias</th>
<th>Definition</th>
<th>Context</th>
</tr>
</thead>
<tbody>
{% for record in results_data %}
<tr>
<td>{{ record.0 }}</td> <!-- UID -->
<td>{{ record.1 }}</td> <!-- Alias -->
<td>{{ record.2 }}</td> <!-- Definition -->
<td>{{ record.3|default:"No context" }}</td> <!-- Context (with default fallback) -->
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}

{% if error %}
<p class="error-message">{{ error }}</p>
{% endif %}
</div>
</body>
</html>
3 changes: 3 additions & 0 deletions app/uid/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

path('api/log', report_generated_uids, name='uid-generated'),
path('api/generate', api_generate_uid, name='uid-generated'),

path('search/', views.search, name='search'), # For the search endpoint
path('create_alias/', views.create_alias, name='create_alias'),

# path('api/uid-repo/', UIDRepoViewSet.as_view({'get': 'list'}), name='uid-repo'),
# path('api/uid/all', UIDTermViewSet.as_view({'get': 'list'}), name='uid-all'),
Expand Down
133 changes: 133 additions & 0 deletions app/uid/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,65 @@
from rest_framework.response import Response
from django.views.decorators.csrf import csrf_exempt

from django.contrib import messages
import os
from .forms import SearchForm
import requests
import urllib.parse
from .models import Alias
from .forms import AliasForm

# Cypher Queries
SEARCH_BY_ALIAS = """
WITH toLower($search_term) as search_term
MATCH (a:NeoAlias)
WHERE toLower(a.alias) CONTAINS search_term
MATCH (a)-[:POINTS_TO]->(term:NeoTerm)
OPTIONAL MATCH (term)-[:POINTS_TO]->(def:NeoDefinition)
OPTIONAL MATCH (ctx:NeoContext)-[:IS_A]->(term)
RETURN term.uid as LCVID, a.alias as Alias, def.definition as Definition, ctx.context as Context
LIMIT 100
"""

SEARCH_BY_DEFINITION = """
WITH toLower($search_term) as search_term
MATCH (def:NeoDefinition)
WHERE toLower(def.definition) CONTAINS search_term
MATCH (term:NeoTerm)-[:POINTS_TO]->(def)
OPTIONAL MATCH (a:NeoAlias)-[:POINTS_TO]->(term)
OPTIONAL MATCH (ctx:NeoContext)-[:IS_A]->(term)
RETURN term.uid as LCVID, a.alias as Alias, def.definition as Definition, ctx.context as Context
LIMIT 100
"""

SEARCH_BY_CONTEXT = """
WITH toLower($search_term) as search_term
MATCH (ctx:NeoContext)
WHERE toLower(ctx.context) CONTAINS search_term
MATCH (ctx)-[:IS_A]->(term:NeoTerm)
OPTIONAL MATCH (term)-[:POINTS_TO]->(def:NeoDefinition)
OPTIONAL MATCH (a:NeoAlias)-[:POINTS_TO]->(term)
RETURN term.uid as LCVID, a.alias as Alias, def.definition as Definition, ctx.context as Context
LIMIT 100
"""

GENERAL_GRAPH_SEARCH = """
WITH toLower($search_term) as search_term
MATCH (n)
WHERE (n:NeoAlias OR n:NeoDefinition OR n:NeoContext)
AND (
(n:NeoAlias AND toLower(n.alias) CONTAINS search_term) OR
(n:NeoDefinition AND toLower(n.definition) CONTAINS search_term) OR
(n:NeoContext AND toLower(n.context) CONTAINS search_term)
)
WITH n
CALL {
WITH n
MATCH path = (n)-[*1..2]-(connected)
RETURN path
}
RETURN * LIMIT 100
"""

# Set up logging to capture errors and important information
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -82,6 +141,80 @@ def generate_uid_node(request: HttpRequest):
#except Provider.DoesNotExist:
# return JsonResponse({'error': 'Provider not found'}, status=404)

# Django view for search functionality
def search(request):
results = []
if request.method == 'POST':
form = SearchForm(request.POST)
if form.is_valid():
search_term = form.cleaned_data['search_term']
search_type = form.cleaned_data['search_type']

# Log form data for debugging
logger.info(f"Search form data: search_term={search_term}, search_type={search_type}")

# Determine which query to use based on search type
if search_type == 'alias':
query = SEARCH_BY_ALIAS
elif search_type == 'definition':
query = SEARCH_BY_DEFINITION
elif search_type == 'context':
query = SEARCH_BY_CONTEXT
else:
query = GENERAL_GRAPH_SEARCH # For 'general' search

# Log the query and params being sent to Neo4j
logger.info(f"Executing query: {query} with params: {{'search_term': {search_term}}}")

# Execute the query
results_data = execute_neo4j_query(query, {"search_term": search_term})

if results_data:
logger.info(f"Raw results data: {results_data}")
results = [
{
"LCVID": record[0], # Assuming record[0] is 'LCVID'
"Alias": record[1], # Assuming record[1] is 'Alias'
"Definition": record[2], # Assuming record[2] is 'Definition'
"Context": record[3] # Assuming record[3] is 'Context'
}
for record in results_data # Iterating over each record in results_data
]
else:
logger.info("No results found.")
results = [{'error': 'No results found or error querying Neo4j.'}]

else:
form = SearchForm()

return render(request, 'search.html', {'form': form, 'results': results})

def create_alias(request):
if request.method == 'POST':
form = AliasForm(request.POST)
if form.is_valid():
alias = form.save() # This will create and save the Alias to Neo4j

# Check if there was an error with the context linking
context_error = getattr(alias, 'context_error', None)

if context_error:
# If there was a context error, show a message
messages.error(request, f"Error: {context_error}")
else:
# If the alias was created successfully, show a success message
messages.success(request, f"Warning: No Context provided but Alias '{alias.alias}' has been successfully created.")

# Reset the form for another entry if needed
form = AliasForm()

else:
messages.error(request, "There was an error with the form. Please try again.")
else:
form = AliasForm()

return render(request, 'create_alias.html', {'form': form})

# Provider and LCVTerm (Otherwise alternative Parent and child) Now with collision detection on both.
def create_provider(request):
if request.method == 'POST':
Expand Down