diff --git a/app.py b/app.py index 587e3d9..b86bc18 100644 --- a/app.py +++ b/app.py @@ -29,22 +29,27 @@ def login_required(view): return view(*args, **kwargs) return wrapped -def projects_for(profile, case_email_match, per_page, offset): +def projects_for(profile, case_email_match, per_page, offset, include_archived=False): """ Filter projects based on user profile and case_email query string argument. - + Args: 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 - + include_archived (bool): When False (default), archived projects are excluded + Returns: list: List of project dictionaries that match the filtering criteria """ is_admin = profile.get("is_admin", False) - + if not profile.get("enabled"): return ([], 0) - + + def not_archived(ref): + """Exclude archived projects unless include_archived is set.""" + return ref if include_archived else ref.where("is_archived", "==", False) + # Query Firestore for projects where case_email is in viewing_emails array try: cnt = 0 @@ -55,7 +60,7 @@ def projects_for(profile, case_email_match, per_page, offset): # 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).where("is_archived", "==", False) + projects_ref = not_archived(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(): @@ -69,7 +74,7 @@ def projects_for(profile, case_email_match, per_page, offset): 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).where("is_archived", "==", False) + projects_ref = not_archived(db.collection("projects").where("viewing_domains", "array_contains", domain_search)) print("HERE domain", domain_search) cnt = int(projects_ref.count().get()[0][0].value) @@ -78,10 +83,10 @@ def projects_for(profile, case_email_match, per_page, offset): projects.append(doc.to_dict()) return (projects, cnt) - else: - projects_ref = db.collection("projects").where("is_archived", "==", False) + else: + projects_ref = not_archived(db.collection("projects")) - else: + else: # 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", "") @@ -89,10 +94,10 @@ def projects_for(profile, case_email_match, per_page, offset): 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).where("is_archived", "==", False) + projects_ref = not_archived(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()).where("is_archived", "==", False) + projects_ref = not_archived(db.collection("projects").where("viewing_emails", "array_contains", case_email.lower())) else: return ([], 0) @@ -288,7 +293,8 @@ def dashboard(page=1): case_email_match = request.args.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) + include_archived = request.args.get('include_archived') == '1' + paginated_rows, total_projects = projects_for(profile, case_email_match, per_page, offset, include_archived) # Calculate pagination total_pages = (total_projects + per_page - 1) // per_page # Ceiling division @@ -304,6 +310,7 @@ def dashboard(page=1): total_pages=total_pages, total_projects=total_projects, per_page=per_page, + include_archived=include_archived, is_admin=is_admin) @@ -323,9 +330,13 @@ def dashboard_export_xls(): if not is_admin and (not profile.get('case_email') and not profile.get('case_domain_email')): return redirect(url_for("welcome")) + if is_admin and request.args.get('case_email'): + case_email = request.args.get('case_email') + include_archived = request.args.get('include_archived') == '1' + # Get all projects without pagination try: - all_rows, cnt = projects_for(profile, case_email, 10000, 0) + all_rows, cnt = projects_for(profile, case_email, 10000, 0, include_archived) # Filter projects where case_email is in viewing_emails array # Order by matter_description to maintain consistent ordering print(f"Retrieved {cnt} projects from Firestore for XLS export") diff --git a/sync.py b/sync.py index f959d16..00fa275 100644 --- a/sync.py +++ b/sync.py @@ -441,16 +441,47 @@ def get_oldest_unsynced_projects(db, fraction: float = 0.2) -> List[int]: return [] +import json + + +def dump_cases_to_json(projects: List[dict], client: FilevineClient, output_dir: str = "sample-cases", limit: int = 10) -> None: + """Fetch project details and dump them as individual JSON files instead of Firestore. + + Args: + projects: List of project data dictionaries from Filevine + client: FilevineClient instance + output_dir: Directory to write JSON files to + limit: Maximum number of cases to dump + """ + os.makedirs(output_dir, exist_ok=True) + projects = projects[:limit] + print(f"[DUMP] Processing {len(projects)} projects to {output_dir}/") + + detailed_rows = process_projects_parallel(projects, client, max_workers=10) + written = 0 + for row in detailed_rows: + if row.get('ProjectId'): + row['is_archived'] = (row.get('phase_name') == 'Archived') + filepath = os.path.join(output_dir, f"{row['ProjectId']}.json") + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(row, f, indent=2, default=str) + written += 1 + print(f"[DUMP] Wrote {filepath}") + + print(f"[DUMP] Complete - {written} cases written to {output_dir}/") + + def main(): """Main function to fetch and sync projects""" import argparse parser = argparse.ArgumentParser(description='Sync Filevine projects to Firestore') - parser.add_argument('--mode', choices=['full', 'last_n', 'oldest_percent', 'hybrid', 'single'], - default='hybrid', help='Sync mode: full=all projects, last_n=recently active, oldest_percent=oldest by last_synced_at, hybrid=last_n+oldest_percent, single=one project') + parser.add_argument('--mode', choices=['full', 'last_n', 'oldest_percent', 'hybrid', 'single', 'dump_json'], + default='hybrid', help='Sync mode: full=all projects, last_n=recently active, oldest_percent=oldest by last_synced_at, hybrid=last_n+oldest_percent, single=one project, dump_json=export to JSON files') parser.add_argument('--days', type=int, default=14, help='Number of days for last_n mode (default: 14)') parser.add_argument('--percent', type=float, default=20.0, help='Percentage for oldest_percent mode (default: 20)') parser.add_argument('--project-id', type=int, help='Project ID for single mode (required when mode=single)') + parser.add_argument('--limit', type=int, default=10, help='Number of cases to dump (default: 10)') args = parser.parse_args() if args.mode == 'single' and not args.project_id: @@ -460,11 +491,14 @@ def main(): try: client = FilevineClient() client.get_bearer_token() - from app import db recent_successes = 0 oldest_successes = 0 total_failures = 0 + documents = [] + + if args.mode != 'dump_json': + from app import db if args.mode == 'full': print("[MODE] Full sync - fetching all projects") @@ -561,7 +595,13 @@ def main(): total_failures = len(detailed_rows) - len(project_ids_synced) record_sync_stats(db, recent_successes, oldest_successes, total_failures) - print(f"[SYNC] Complete - {len(documents)} projects saved to Firestore") + elif args.mode == 'dump_json': + print(f"[MODE] Dump JSON - fetching {args.limit} cases") + projects = client.list_all_projects() + dump_cases_to_json(projects, client, limit=args.limit) + + if args.mode != 'dump_json': + print(f"[SYNC] Complete - {len(documents)} projects saved to Firestore") except Exception as e: print(f"Error during sync: {e}") diff --git a/templates/_pagination.html b/templates/_pagination.html index c8610eb..8a3162c 100644 --- a/templates/_pagination.html +++ b/templates/_pagination.html @@ -1,27 +1,30 @@ {% if total_pages > 1 %} +{% set page_args = {'per_page': per_page} %} +{% if include_archived %}{% set _ = page_args.update({'include_archived': '1'}) %}{% endif %} +{% if case_email %}{% set _ = page_args.update({'case_email': case_email}) %}{% endif %}