include archived
This commit is contained in:
27
app.py
27
app.py
@@ -29,13 +29,14 @@ def login_required(view):
|
|||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
return wrapped
|
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.
|
Filter projects based on user profile and case_email query string argument.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
profile (dict): User profile containing 'enabled', 'is_admin', 'case_email', and 'case_domain_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
|
case_email_match (str): Case email from query string argument, or None
|
||||||
|
include_archived (bool): When False (default), archived projects are excluded
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: List of project dictionaries that match the filtering criteria
|
list: List of project dictionaries that match the filtering criteria
|
||||||
@@ -45,6 +46,10 @@ def projects_for(profile, case_email_match, per_page, offset):
|
|||||||
if not profile.get("enabled"):
|
if not profile.get("enabled"):
|
||||||
return ([], 0)
|
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
|
# Query Firestore for projects where case_email is in viewing_emails array
|
||||||
try:
|
try:
|
||||||
cnt = 0
|
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 @)
|
# 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 '@' 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
|
# 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)
|
cnt = int(projects_ref.count().get()[0][0].value)
|
||||||
projects = []
|
projects = []
|
||||||
for doc in projects_ref.order_by("matter_description").limit(per_page).offset(offset).stream():
|
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
|
domain_search = domain_search[1:] # Remove the @ sign
|
||||||
|
|
||||||
# Filter by domain match in viewing_emails
|
# 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)
|
print("HERE domain", domain_search)
|
||||||
cnt = int(projects_ref.count().get()[0][0].value)
|
cnt = int(projects_ref.count().get()[0][0].value)
|
||||||
|
|
||||||
@@ -79,7 +84,7 @@ def projects_for(profile, case_email_match, per_page, offset):
|
|||||||
return (projects, cnt)
|
return (projects, cnt)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
projects_ref = db.collection("projects").where("is_archived", "==", False)
|
projects_ref = not_archived(db.collection("projects"))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# For non-admin users, check if they have domain email or specific case email
|
# For non-admin users, check if they have domain email or specific case email
|
||||||
@@ -89,10 +94,10 @@ def projects_for(profile, case_email_match, per_page, offset):
|
|||||||
if case_domain_email:
|
if case_domain_email:
|
||||||
# Use exact match on viewing_domains field
|
# Use exact match on viewing_domains field
|
||||||
domain_lower = case_domain_email.lower()
|
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:
|
elif case_email:
|
||||||
# Use the original logic for specific case email match
|
# 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:
|
else:
|
||||||
return ([], 0)
|
return ([], 0)
|
||||||
|
|
||||||
@@ -288,7 +293,8 @@ def dashboard(page=1):
|
|||||||
case_email_match = request.args.get('case_email')
|
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')):
|
if not is_admin and (not profile.get('case_email') and not profile.get('case_domain_email')):
|
||||||
return redirect(url_for("welcome"))
|
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
|
# Calculate pagination
|
||||||
total_pages = (total_projects + per_page - 1) // per_page # Ceiling division
|
total_pages = (total_projects + per_page - 1) // per_page # Ceiling division
|
||||||
@@ -304,6 +310,7 @@ def dashboard(page=1):
|
|||||||
total_pages=total_pages,
|
total_pages=total_pages,
|
||||||
total_projects=total_projects,
|
total_projects=total_projects,
|
||||||
per_page=per_page,
|
per_page=per_page,
|
||||||
|
include_archived=include_archived,
|
||||||
is_admin=is_admin)
|
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')):
|
if not is_admin and (not profile.get('case_email') and not profile.get('case_domain_email')):
|
||||||
return redirect(url_for("welcome"))
|
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
|
# Get all projects without pagination
|
||||||
try:
|
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
|
# Filter projects where case_email is in viewing_emails array
|
||||||
# Order by matter_description to maintain consistent ordering
|
# Order by matter_description to maintain consistent ordering
|
||||||
print(f"Retrieved {cnt} projects from Firestore for XLS export")
|
print(f"Retrieved {cnt} projects from Firestore for XLS export")
|
||||||
|
|||||||
48
sync.py
48
sync.py
@@ -441,16 +441,47 @@ def get_oldest_unsynced_projects(db, fraction: float = 0.2) -> List[int]:
|
|||||||
return []
|
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():
|
def main():
|
||||||
"""Main function to fetch and sync projects"""
|
"""Main function to fetch and sync projects"""
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Sync Filevine projects to Firestore')
|
parser = argparse.ArgumentParser(description='Sync Filevine projects to Firestore')
|
||||||
parser.add_argument('--mode', choices=['full', 'last_n', 'oldest_percent', 'hybrid', 'single'],
|
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')
|
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('--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('--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('--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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.mode == 'single' and not args.project_id:
|
if args.mode == 'single' and not args.project_id:
|
||||||
@@ -460,11 +491,14 @@ def main():
|
|||||||
try:
|
try:
|
||||||
client = FilevineClient()
|
client = FilevineClient()
|
||||||
client.get_bearer_token()
|
client.get_bearer_token()
|
||||||
from app import db
|
|
||||||
|
|
||||||
recent_successes = 0
|
recent_successes = 0
|
||||||
oldest_successes = 0
|
oldest_successes = 0
|
||||||
total_failures = 0
|
total_failures = 0
|
||||||
|
documents = []
|
||||||
|
|
||||||
|
if args.mode != 'dump_json':
|
||||||
|
from app import db
|
||||||
|
|
||||||
if args.mode == 'full':
|
if args.mode == 'full':
|
||||||
print("[MODE] Full sync - fetching all projects")
|
print("[MODE] Full sync - fetching all projects")
|
||||||
@@ -561,7 +595,13 @@ def main():
|
|||||||
total_failures = len(detailed_rows) - len(project_ids_synced)
|
total_failures = len(detailed_rows) - len(project_ids_synced)
|
||||||
record_sync_stats(db, recent_successes, oldest_successes, total_failures)
|
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:
|
except Exception as e:
|
||||||
print(f"Error during sync: {e}")
|
print(f"Error during sync: {e}")
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
{% if total_pages > 1 %}
|
{% 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 %}
|
||||||
<div class="flex justify-center items-center mt-6 space-x-2">
|
<div class="flex justify-center items-center mt-6 space-x-2">
|
||||||
{% if current_page > 1 %}
|
{% if current_page > 1 %}
|
||||||
<a href="{{ url_for(request.endpoint, page=current_page - 1, per_page=per_page) }}" class="px-3 py-2 text-sm text-slate-600 bg-white border border-slate-300 rounded-md hover:bg-slate-50">
|
<a href="{{ url_for(request.endpoint, page=current_page - 1, **page_args) }}" class="px-3 py-2 text-sm text-slate-600 bg-white border border-slate-300 rounded-md hover:bg-slate-50">
|
||||||
Previous
|
Previous
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -12,7 +15,7 @@
|
|||||||
{% if page == current_page %}
|
{% if page == current_page %}
|
||||||
<span class="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-md">{{ page }}</span>
|
<span class="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-md">{{ page }}</span>
|
||||||
{% elif page == 1 or page == total_pages or (page >= current_page - 2 and page <= current_page + 2) %}
|
{% elif page == 1 or page == total_pages or (page >= current_page - 2 and page <= current_page + 2) %}
|
||||||
<a href="{{ url_for(request.endpoint, page=page, per_page=per_page) }}" class="px-3 py-2 text-sm text-slate-600 bg-white border border-slate-300 rounded-md hover:bg-slate-50">
|
<a href="{{ url_for(request.endpoint, page=page, **page_args) }}" class="px-3 py-2 text-sm text-slate-600 bg-white border border-slate-300 rounded-md hover:bg-slate-50">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
</a>
|
</a>
|
||||||
{% elif page == current_page - 3 or page == current_page + 3 %}
|
{% elif page == current_page - 3 or page == current_page + 3 %}
|
||||||
@@ -21,7 +24,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if current_page < total_pages %}
|
{% if current_page < total_pages %}
|
||||||
<a href="{{ url_for(request.endpoint, page=current_page + 1, per_page=per_page) }}" class="px-3 py-2 text-sm text-slate-600 bg-white border border-slate-300 rounded-md hover:bg-slate-50">
|
<a href="{{ url_for(request.endpoint, page=current_page + 1, **page_args) }}" class="px-3 py-2 text-sm text-slate-600 bg-white border border-slate-300 rounded-md hover:bg-slate-50">
|
||||||
Next
|
Next
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<div class="mb-4 flex w-[400px]">
|
<div class="mb-4 flex w-[400px]">
|
||||||
<label for="simulateCaseEmail" class=" text-sm font-medium text-slate-700 mb-1">Simulate case email:</label>
|
<label for="simulateCaseEmail" class=" text-sm font-medium text-slate-700 mb-1">Simulate case email:</label>
|
||||||
<input type="text" id="simulateCaseEmail" x-model="case_email_sim"
|
<input type="text" id="simulateCaseEmail" x-model="case_email_sim"
|
||||||
@keyup.debounce.1000ms="window.location.href=`/dashboard/1?case_email=${encodeURIComponent($data.case_email_sim)}`"
|
@keyup.debounce.1000ms="window.location.href=`/dashboard/1?per_page=${$data.perPage}&include_archived=${$data.includeArchived ? '1' : '0'}&case_email=${encodeURIComponent($data.case_email_sim)}`"
|
||||||
class="w-full px-3 py-2 border w-64 border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
class="w-full px-3 py-2 border w-64 border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
placeholder="Enter case email to simulate">
|
placeholder="Enter case email to simulate">
|
||||||
</div>
|
</div>
|
||||||
@@ -35,9 +35,19 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
|
<!-- Include Archived Toggle -->
|
||||||
|
<div class="mb-4 flex items-center">
|
||||||
|
<label class="flex items-center cursor-pointer text-sm font-medium text-slate-700">
|
||||||
|
<input type="checkbox" x-model="includeArchived"
|
||||||
|
@change="window.location.href = `/dashboard/1?per_page=${$data.perPage}&include_archived=${$data.includeArchived ? '1' : '0'}${$data.case_email_sim ? '&case_email=' + encodeURIComponent($data.case_email_sim) : ''}`"
|
||||||
|
class="mr-2 h-4 w-4 text-blue-600 border-slate-300 rounded focus:ring-blue-500">
|
||||||
|
Include archived
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Export Button -->
|
<!-- Export Button -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<a href="{{ url_for('dashboard_export_xls') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
<a :href="`{{ url_for('dashboard_export_xls') }}?include_archived=${$data.includeArchived ? '1' : '0'}${$data.case_email_sim ? '&case_email=' + encodeURIComponent($data.case_email_sim) : ''}`" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||||
<svg class="mr-2 -ml-1 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="mr-2 -ml-1 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -48,7 +58,7 @@
|
|||||||
<!-- Per Page Dropdown -->
|
<!-- Per Page Dropdown -->
|
||||||
<div class="mb-4 flex items-center">
|
<div class="mb-4 flex items-center">
|
||||||
<label for="perPage" class="mr-2 text-sm font-medium text-slate-700">Items per page:</label>
|
<label for="perPage" class="mr-2 text-sm font-medium text-slate-700">Items per page:</label>
|
||||||
<select id="perPage" x-model="perPage" @change="window.location.href = `/dashboard/1?per_page=${$data.perPage}${$data.case_email_sim ? '&case_email=' + encodeURIComponent($data.case_email_sim) : ''}`"
|
<select id="perPage" x-model="perPage" @change="window.location.href = `/dashboard/1?per_page=${$data.perPage}&include_archived=${$data.includeArchived ? '1' : '0'}${$data.case_email_sim ? '&case_email=' + encodeURIComponent($data.case_email_sim) : ''}`"
|
||||||
class="px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
class="px-3 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
<option value="10">10</option>
|
<option value="10">10</option>
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
@@ -571,6 +581,7 @@
|
|||||||
showColumnModal: false,
|
showColumnModal: false,
|
||||||
case_email_sim: '',
|
case_email_sim: '',
|
||||||
perPage: 25,
|
perPage: 25,
|
||||||
|
includeArchived: false,
|
||||||
columns: [
|
columns: [
|
||||||
'Matter Num',
|
'Matter Num',
|
||||||
'Matter Description',
|
'Matter Description',
|
||||||
@@ -639,6 +650,7 @@
|
|||||||
if (perPage) {
|
if (perPage) {
|
||||||
this.perPage = parseInt(perPage);
|
this.perPage = parseInt(perPage);
|
||||||
}
|
}
|
||||||
|
this.includeArchived = urlParams.get('include_archived') === '1';
|
||||||
},
|
},
|
||||||
|
|
||||||
isColumnVisible(columnName) {
|
isColumnVisible(columnName) {
|
||||||
|
|||||||
Reference in New Issue
Block a user