supports lookup by domains

This commit is contained in:
2025-12-09 22:01:06 -08:00
parent c3108ff68c
commit 234578b646
8 changed files with 110 additions and 42 deletions

View File

@@ -45,6 +45,7 @@ def register_admin_routes(app):
"uid": doc.id,
"user_email": user_data.get("user_email", ""),
"case_email": user_data.get("case_email", ""),
"case_domain_email": user_data.get("case_domain_email", ""),
"enabled": bool(user_data.get("enabled", False)),
"is_admin": bool(user_data.get("is_admin", False))
})
@@ -78,6 +79,7 @@ def register_admin_routes(app):
"uid": uid,
"user_email": user_data.get("user_email", ""),
"case_email": user_data.get("case_email", ""),
"case_domain_email": user_data.get("case_domain_email", ""),
"enabled": bool(user_data.get("enabled", False)),
"is_admin": bool(user_data.get("is_admin", False))
}
@@ -129,7 +131,8 @@ def register_admin_routes(app):
# Only update fields that can be changed, excluding is_admin
update_data = {
"enabled": data.get("enabled", False),
"case_email": data.get("case_email", "")
"case_email": data.get("case_email", ""),
"case_domain_email": data.get("case_domain_email", "")
}
# Never allow changing is_admin field during updates - admin status can only be set during creation
user_ref.update(update_data)
@@ -172,6 +175,7 @@ def register_admin_routes(app):
user_ref.set({
"user_email": user_email,
"case_email": request.form.get("case_email", ""),
"case_domain_email": request.form.get("case_domain_email", ""),
"enabled": bool(request.form.get("enabled", False)),
"is_admin": bool(request.form.get("is_admin", False))
})

66
app.py
View File

@@ -34,7 +34,7 @@ def projects_for(profile, case_email_match, per_page, offset):
Filter projects based on user profile and case_email query string argument.
Args:
profile (dict): User profile containing 'enabled', 'is_admin', and 'case_email' fields
profile (dict): User profile containing 'enabled', 'is_admin', 'case_email', and 'case_domain_email' fields
case_email_match (str): Case email from query string argument, or None
Returns:
@@ -50,40 +50,52 @@ def projects_for(profile, case_email_match, per_page, offset):
cnt = 0
if is_admin:
if case_email_match:
projects_ref = db.collection("projects").where("viewing_emails", "array_contains", case_email_match.lower())
# Get filtered document IDs using client-side filtering with partial match
z = db.collection("projects").select(["viewing_emails", "matter_description", "id"])
# Get all matching documents with their IDs and descriptions
matching_docs = [(x.id, x.to_dict().get('matter_description', '')) for x in z.stream()
if any(case_email_match.lower() in email.lower() for email in x.to_dict().get('viewing_emails', []))]
count = len(matching_docs)
case_email_match_lower = case_email_match.lower().strip()
# Sort by matter_description
matching_docs.sort(key=lambda x: x[1].lower())
# Extract just the IDs after sorting
filtered_ids = [doc_id for doc_id, _ in matching_docs]
# Apply client-side pagination
filtered_ids = filtered_ids[offset:offset + per_page]
print(f"Filtered document IDs (partial match, sorted, paginated): {filtered_ids}")
projects_ref = db.collection("projects")
# Check if case_email_match is a valid email address (contains @)
if '@' in case_email_match_lower and not case_email_match_lower.startswith('@'):
# If it's a complete email address, filter by exact match in viewing_emails
projects_ref = db.collection("projects").where("viewing_emails", "array_contains", case_email_match_lower)
cnt = int(projects_ref.count().get()[0][0].value)
projects = []
for doc_id in filtered_ids:
doc = projects_ref.document(doc_id).get()
if doc.exists:
for doc in projects_ref.order_by("matter_description").limit(per_page).offset(offset).stream():
projects.append(doc.to_dict())
return (projects, cnt)
else:
# If no @ sign, treat as domain search
# Also handle cases like "@gmail.com" by extracting the domain
domain_search = case_email_match_lower
if domain_search.startswith('@'):
domain_search = domain_search[1:] # Remove the @ sign
return (projects, count)
# Filter by domain match in viewing_emails
projects_ref = db.collection("projects").where("viewing_domains", "array_contains", domain_search)
print("HERE domain", domain_search)
cnt = int(projects_ref.count().get()[0][0].value)
projects = []
for doc in projects_ref.order_by("matter_description").limit(per_page).offset(offset).stream():
projects.append(doc.to_dict())
return (projects, cnt)
else:
projects_ref = db.collection("projects")
else:
if not profile.get("case_email"):
# For non-admin users, check if they have domain email or specific case email
case_domain_email = profile.get("case_domain_email", "")
case_email = profile.get("case_email", "")
if case_domain_email:
# Use exact match on viewing_domains field
domain_lower = case_domain_email.lower()
projects_ref = db.collection("projects").where("viewing_domains", "array_contains", domain_lower)
elif case_email:
# Use the original logic for specific case email match
projects_ref = db.collection("projects").where("viewing_emails", "array_contains", case_email.lower())
else:
return ([], 0)
projects_ref = db.collection("projects").where("viewing_emails", "array_contains", profile.get("case_email").to_lower())
cnt = int(projects_ref.count().get()[0][0].value)
projects = []
for doc in projects_ref.order_by("matter_description").limit(per_page).offset(offset).stream():
@@ -127,7 +139,7 @@ def index():
if not uid:
return redirect(url_for("login"))
profile = get_user_profile(uid)
if profile.get("enabled") and profile.get("case_email"):
if profile.get("enabled") and (profile.get("case_email") or profile.get("case_domain_email")):
return redirect(url_for("dashboard"))
return redirect(url_for("welcome"))
@@ -197,7 +209,7 @@ def dashboard(page=1):
case_email_match = None
if is_admin and request.args.get('case_email'):
case_email_match = request.args.get('case_email')
if not is_admin and not profile.get('case_email'):
if not is_admin and (not profile.get('case_email') and not profile.get('case_domain_email')):
return redirect(url_for("welcome"))
paginated_rows, total_projects = projects_for(profile, case_email_match, per_page, offset)

View File

@@ -69,7 +69,8 @@ class ProjectModel:
project_name: str = "",
project_url: str = "",
property_contacts: Dict[str, Any] = None,
viewing_emails: List[str] = None
viewing_emails: List[str] = None,
viewing_domains: List[str] = None
):
self.client = client
@@ -127,6 +128,7 @@ class ProjectModel:
self.project_url = project_url
self.property_contacts = property_contacts or {}
self.viewing_emails = viewing_emails or []
self.viewing_domains = viewing_domains or []
def to_dict(self) -> Dict[str, Any]:
"""Convert the ProjectModel to a dictionary for Firestore storage."""
@@ -185,7 +187,8 @@ class ProjectModel:
"ProjectName": self.project_name,
"ProjectUrl": self.project_url,
"property_contacts": self.property_contacts,
"viewing_emails": self.viewing_emails
"viewing_emails": self.viewing_emails,
"viewing_domains": self.viewing_domains
}
@classmethod
@@ -246,5 +249,6 @@ class ProjectModel:
project_name=data.get("ProjectName", ""),
project_url=data.get("ProjectUrl", ""),
property_contacts=data.get("property_contacts", {}),
viewing_emails=data.get("viewing_emails", [])
viewing_emails=data.get("viewing_emails", []),
viewing_domains=data.get("viewing_domains", [])
)

25
sync.py
View File

@@ -29,6 +29,28 @@ def convert_to_pacific_time(date_str):
if not date_str:
return ''
def extract_domains_from_emails(emails: List[str]) -> List[str]:
"""Extract unique domains from a list of email addresses.
Args:
emails (List[str]): List of email addresses
Returns:
List[str]: List of unique domains extracted from the emails
"""
if not emails:
return []
domains = set()
for email in emails:
if email and '@' in email:
# Extract domain part after @
domain = email.split('@')[1].lower()
domains.add(domain)
return sorted(list(domains))
try:
# Parse the UTC datetime
utc_time = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
@@ -241,7 +263,8 @@ def process_project(index: int, total: int, project_data: dict, client: Filevine
project_name=p.get("projectName") or detail.get("projectName"),
project_url=p.get("projectUrl") or detail.get("projectUrl"),
#property_contacts=property_contacts
viewing_emails = valid_property_managers
viewing_emails = valid_property_managers,
viewing_domains = extract_domains_from_emails(valid_property_managers)
)
# Store the results in Firestore
from app import db # Import db from app

View File

@@ -40,6 +40,14 @@
<p class="mt-1 text-sm text-slate-500">The email address used for project access.</p>
</div>
<div>
<label for="case_domain_email" class="block text-sm font-medium text-slate-700">Case Domain Email</label>
<input type="text" id="case_domain_email" name="case_domain_email"
value=""
class="mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<p class="mt-1 text-sm text-slate-500">All cases with property contacts in this domain will be viewable to the user</p>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button type="button" onclick="window.location.href='/admin/users'"
class="px-4 py-2 text-sm font-medium text-slate-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors">

View File

@@ -42,7 +42,15 @@
<input type="email" id="case_email" name="case_email"
value="{{ user.case_email }}"
class="mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<p class="mt-1 text-sm text-slate-500">The email address used for project access.</p>
<p class="mt-1 text-sm text-slate-500">All cases with this email will be viewable by the user</p>
</div>
<div>
<label for="case_domain_email" class="block text-sm font-medium text-slate-700">Case Domain Email</label>
<input type="text" id="case_domain_email" name="case_domain_email"
value="{{ user.case_domain_email }}"
class="mt-1 block w-full px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
<p class="mt-1 text-sm text-slate-500">All cases with property contacts in this domain will be viewable to the user</p>
</div>
<div class="flex justify-end space-x-3 pt-4">
@@ -66,7 +74,8 @@ document.getElementById('userForm').addEventListener('submit', function(e) {
const userData = {
uid: '{{ user.uid }}',
enabled: formData.get('enabled') === 'on',
case_email: formData.get('case_email')
case_email: formData.get('case_email'),
case_domain_email: formData.get('case_domain_email')
};
fetch('/admin/users/update', {

View File

@@ -1,7 +1,12 @@
{% extends 'base.html' %}
{% block content %}
<div class="h-full flex flex-col" x-data="columnConfig()">
{% if case_email %}
<h1 class="text-xl font-semibold mb-4">Projects for {{ case_email }}</h1>
{% else %}
<h1 class="text-xl font-semibold mb-4">All projects</h1>
{% endif %}
<div class="flex justify-between">
{% set profile = get_user_profile(session.uid) %}

View File

@@ -19,18 +19,21 @@ def get_user_profile(uid: str):
"enabled": False,
"is_admin": False,
"user_email": user_email,
"case_email": user_email
"case_email": user_email,
"case_domain_email": ""
}, merge=True)
return {
"enabled": False,
"is_admin": False,
"user_email": user_email,
"case_email": user_email
"case_email": user_email,
"case_domain_email": ""
}
data = snap.to_dict() or {}
return {
"enabled": bool(data.get("enabled", False)),
"is_admin": bool(data.get("is_admin", False)),
"user_email": data.get("user_email"),
"case_email": data.get("case_email")
"case_email": data.get("case_email"),
"case_domain_email": data.get("case_domain_email", "")
}