From 234578b646f3ca9485be73862f3e0514ae64d9c7 Mon Sep 17 00:00:00 2001 From: Bryce Date: Tue, 9 Dec 2025 22:01:06 -0800 Subject: [PATCH] supports lookup by domains --- admin.py | 6 ++- app.py | 70 +++++++++++++++++++------------- models/project_model.py | 10 +++-- sync.py | 25 +++++++++++- templates/admin_user_create.html | 10 ++++- templates/admin_user_edit.html | 15 +++++-- templates/dashboard.html | 7 +++- utils.py | 9 ++-- 8 files changed, 110 insertions(+), 42 deletions(-) diff --git a/admin.py b/admin.py index b72d615..9c8341c 100644 --- a/admin.py +++ b/admin.py @@ -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)) }) diff --git a/app.py b/app.py index 7b256cc..ed4f12a 100644 --- a/app.py +++ b/app.py @@ -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") - projects = [] - for doc_id in filtered_ids: - doc = projects_ref.document(doc_id).get() - if doc.exists: + # 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 in projects_ref.order_by("matter_description").limit(per_page).offset(offset).stream(): projects.append(doc.to_dict()) - - return (projects, count) + 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 + + # 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) diff --git a/models/project_model.py b/models/project_model.py index 768679e..80a47aa 100644 --- a/models/project_model.py +++ b/models/project_model.py @@ -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", []) ) diff --git a/sync.py b/sync.py index b40bbbe..402d378 100644 --- a/sync.py +++ b/sync.py @@ -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 diff --git a/templates/admin_user_create.html b/templates/admin_user_create.html index d99b2a5..41fd3a9 100644 --- a/templates/admin_user_create.html +++ b/templates/admin_user_create.html @@ -40,6 +40,14 @@

The email address used for project access.

+
+ + +

All cases with property contacts in this domain will be viewable to the user

+
+
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin_user_edit.html b/templates/admin_user_edit.html index 3a4ef16..55e1fe7 100644 --- a/templates/admin_user_edit.html +++ b/templates/admin_user_edit.html @@ -42,7 +42,15 @@ -

The email address used for project access.

+

All cases with this email will be viewable by the user

+ + +
+ + +

All cases with property contacts in this domain will be viewable to the user

@@ -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', { @@ -88,4 +97,4 @@ document.getElementById('userForm').addEventListener('submit', function(e) { }); }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/dashboard.html b/templates/dashboard.html index 5502f20..07c1c1d 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,7 +1,12 @@ {% extends 'base.html' %} {% block content %}
+ {% if case_email %}

Projects for {{ case_email }}

+ {% else %} +

All projects

+ + {% endif %}
{% set profile = get_user_profile(session.uid) %} @@ -660,4 +665,4 @@
--> -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/utils.py b/utils.py index f8291a3..e0fb3ab 100644 --- a/utils.py +++ b/utils.py @@ -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", "") } \ No newline at end of file