feat(ui): show file owner, add claim ownership on file summary, detail, and annotations pages
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/0ce1d83e-500c-473f-bbb6-ccf510ba953a
This commit is contained in:
+44
-12
@@ -19,6 +19,43 @@ router = APIRouter()
|
|||||||
_FILE_NOT_FOUND = "File not found"
|
_FILE_NOT_FOUND = "File not found"
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_owner_context(request: Request, file_record, db: Session) -> dict:
|
||||||
|
"""Return owner display info and the current user's effective role.
|
||||||
|
|
||||||
|
Returns a dict with:
|
||||||
|
- ``current_user_role``: one of "owner" / "editor" / "viewer" / None
|
||||||
|
- ``owner_display``: human-readable owner string (display_name or user_id)
|
||||||
|
- ``multi_user_enabled``: whether multi-user mode is active
|
||||||
|
"""
|
||||||
|
from app.config import settings
|
||||||
|
from app.models import UserProfile
|
||||||
|
from app.utils.user_scope import get_current_owner_id, get_file_role
|
||||||
|
|
||||||
|
multi_user_enabled = settings.multi_user_enabled
|
||||||
|
|
||||||
|
current_owner_id = get_current_owner_id(request)
|
||||||
|
user_session = request.session.get("user")
|
||||||
|
is_admin = isinstance(user_session, dict) and bool(user_session.get("is_admin"))
|
||||||
|
|
||||||
|
if is_admin:
|
||||||
|
current_user_role: str | None = "owner"
|
||||||
|
else:
|
||||||
|
current_user_role = get_file_role(file_record, current_owner_id, db)
|
||||||
|
|
||||||
|
# Build a human-readable owner label
|
||||||
|
if file_record.owner_id:
|
||||||
|
profile = db.query(UserProfile).filter(UserProfile.user_id == file_record.owner_id).first()
|
||||||
|
owner_display: str | None = profile.display_name if profile and profile.display_name else file_record.owner_id
|
||||||
|
else:
|
||||||
|
owner_display = None # No owner (unowned)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"current_user_role": current_user_role,
|
||||||
|
"owner_display": owner_display,
|
||||||
|
"multi_user_enabled": multi_user_enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/files")
|
@router.get("/files")
|
||||||
@require_login
|
@require_login
|
||||||
def files_page(
|
def files_page(
|
||||||
@@ -268,6 +305,7 @@ def file_summary_page(request: Request, file_id: int, db: Session = Depends(get_
|
|||||||
step_summary = None
|
step_summary = None
|
||||||
|
|
||||||
pipeline_info = _resolve_pipeline(db, file_record)
|
pipeline_info = _resolve_pipeline(db, file_record)
|
||||||
|
owner_ctx = _resolve_owner_context(request, file_record, db)
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"file_summary.html",
|
"file_summary.html",
|
||||||
@@ -279,6 +317,7 @@ def file_summary_page(request: Request, file_id: int, db: Session = Depends(get_
|
|||||||
"processed_file_exists": processed_file_exists,
|
"processed_file_exists": processed_file_exists,
|
||||||
"step_summary": step_summary,
|
"step_summary": step_summary,
|
||||||
"pipeline_info": pipeline_info,
|
"pipeline_info": pipeline_info,
|
||||||
|
**owner_ctx,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -353,6 +392,7 @@ def file_view_page(request: Request, file_id: int, db: Session = Depends(get_db)
|
|||||||
|
|
||||||
# Resolve the pipeline assigned to this file (explicit or system default)
|
# Resolve the pipeline assigned to this file (explicit or system default)
|
||||||
pipeline_info = _resolve_pipeline(db, file_record)
|
pipeline_info = _resolve_pipeline(db, file_record)
|
||||||
|
owner_ctx = _resolve_owner_context(request, file_record, db)
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"file_view.html",
|
"file_view.html",
|
||||||
@@ -364,6 +404,7 @@ def file_view_page(request: Request, file_id: int, db: Session = Depends(get_db)
|
|||||||
"processed_file_exists": processed_file_exists,
|
"processed_file_exists": processed_file_exists,
|
||||||
"step_summary": step_summary,
|
"step_summary": step_summary,
|
||||||
"pipeline_info": pipeline_info,
|
"pipeline_info": pipeline_info,
|
||||||
|
**owner_ctx,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -497,17 +538,8 @@ def file_annotations_page(request: Request, file_id: int, db: Session = Depends(
|
|||||||
mime = file_record.mime_type or ""
|
mime = file_record.mime_type or ""
|
||||||
is_pdf = mime == "application/pdf" or (file_record.original_filename or "").lower().endswith(".pdf")
|
is_pdf = mime == "application/pdf" or (file_record.original_filename or "").lower().endswith(".pdf")
|
||||||
|
|
||||||
# Determine the current user's role on this file
|
# Determine the current user's role on this file (and owner display info)
|
||||||
from app.utils.user_scope import get_current_owner_id, get_file_role
|
owner_ctx = _resolve_owner_context(request, file_record, db)
|
||||||
|
|
||||||
current_owner_id = get_current_owner_id(request)
|
|
||||||
user_session = request.session.get("user")
|
|
||||||
is_admin = isinstance(user_session, dict) and bool(user_session.get("is_admin"))
|
|
||||||
if is_admin:
|
|
||||||
current_user_role: str | None = "owner"
|
|
||||||
else:
|
|
||||||
current_user_role = get_file_role(file_record, current_owner_id, db)
|
|
||||||
# None means no access — the template will not show owner-only UI
|
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"file_annotations.html",
|
"file_annotations.html",
|
||||||
@@ -517,7 +549,7 @@ def file_annotations_page(request: Request, file_id: int, db: Session = Depends(
|
|||||||
"original_file_exists": original_file_exists,
|
"original_file_exists": original_file_exists,
|
||||||
"processed_file_exists": processed_file_exists,
|
"processed_file_exists": processed_file_exists,
|
||||||
"is_pdf": is_pdf,
|
"is_pdf": is_pdf,
|
||||||
"current_user_role": current_user_role,
|
**owner_ctx,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* claim.js — Claim-ownership UI helper for unowned documents.
|
||||||
|
*
|
||||||
|
* Usage: call initClaimOwnership(fileId, i18n) after DOMContentLoaded.
|
||||||
|
* The i18n object must contain:
|
||||||
|
* confirm, success, failed
|
||||||
|
*/
|
||||||
|
function initClaimOwnership(fileId, i18n) {
|
||||||
|
var btn = document.getElementById('claim-btn');
|
||||||
|
var msg = document.getElementById('claim-msg');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
if (!confirm(i18n.confirm)) return;
|
||||||
|
btn.disabled = true;
|
||||||
|
fetch('/api/files/' + fileId + '/claim', { method: 'POST' })
|
||||||
|
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d }; }); })
|
||||||
|
.then(function (result) {
|
||||||
|
if (result.ok || (result.data && result.data.status === 'already_owned')) {
|
||||||
|
if (msg) {
|
||||||
|
msg.textContent = i18n.success;
|
||||||
|
msg.style.color = '#059669';
|
||||||
|
msg.style.display = msg.tagName === 'SPAN' ? 'inline' : 'block';
|
||||||
|
}
|
||||||
|
setTimeout(function () { location.reload(); }, 1200);
|
||||||
|
} else {
|
||||||
|
if (msg) {
|
||||||
|
msg.textContent = (result.data && result.data.detail) || i18n.failed;
|
||||||
|
msg.style.color = '#dc2626';
|
||||||
|
msg.style.display = msg.tagName === 'SPAN' ? 'inline' : 'block';
|
||||||
|
}
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
if (msg) {
|
||||||
|
msg.textContent = i18n.failed;
|
||||||
|
msg.style.color = '#dc2626';
|
||||||
|
msg.style.display = msg.tagName === 'SPAN' ? 'inline' : 'block';
|
||||||
|
}
|
||||||
|
btn.disabled = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -586,6 +586,23 @@
|
|||||||
Comments & Annotations
|
Comments & Annotations
|
||||||
</div>
|
</div>
|
||||||
<div class="annotations-subtitle">{{ file.original_filename }}</div>
|
<div class="annotations-subtitle">{{ file.original_filename }}</div>
|
||||||
|
{% if multi_user_enabled %}
|
||||||
|
<div class="annotations-subtitle" style="margin-top:0.25rem;">
|
||||||
|
<i class="fas fa-user" aria-hidden="true" style="margin-right:0.25rem;"></i>
|
||||||
|
{{ _("file.owner_label") }}: <strong>{{ owner_display or _("file.owner_unowned") }}</strong>
|
||||||
|
{% if file.owner_id is none %}
|
||||||
|
—
|
||||||
|
<button
|
||||||
|
id="claim-btn"
|
||||||
|
aria-label="{{ _('file.claim_ownership') }}"
|
||||||
|
style="background:#10b981;color:#fff;border:none;border-radius:0.375rem;padding:0.25rem 0.75rem;font-size:0.8rem;font-weight:600;cursor:pointer;"
|
||||||
|
>
|
||||||
|
<i class="fas fa-user-check" aria-hidden="true"></i> {{ _("file.claim_ownership") }}
|
||||||
|
</button>
|
||||||
|
<span id="claim-msg" style="font-size:0.8rem;margin-left:0.5rem;display:none;" role="alert"></span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -912,5 +929,17 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if multi_user_enabled and file and file.owner_id is none %}
|
||||||
|
<script src="{{ url_for('static', path='js/claim.js') }}" defer></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
initClaimOwnership({{ file.id | tojson }}, {
|
||||||
|
confirm: {{ _("file.claim_ownership_confirm") | tojson }},
|
||||||
|
success: {{ _("file.claim_ownership_success") | tojson }},
|
||||||
|
failed: {{ _("file.claim_ownership_failed") | tojson }}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,12 @@
|
|||||||
{% if file.document_title %}
|
{% if file.document_title %}
|
||||||
<div class="info-row"><span class="info-key">Document Title</span><span class="info-val">{{ file.document_title }}</span></div>
|
<div class="info-row"><span class="info-key">Document Title</span><span class="info-val">{{ file.document_title }}</span></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if multi_user_enabled %}
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-key">{{ _("file.owner_label") }}</span>
|
||||||
|
<span class="info-val">{{ owner_display or _("file.owner_unowned") }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Quick actions ── -->
|
<!-- ── Quick actions ── -->
|
||||||
@@ -184,7 +190,18 @@
|
|||||||
<a href="/files/{{ file.id }}/detail" class="action-btn btn-secondary">
|
<a href="/files/{{ file.id }}/detail" class="action-btn btn-secondary">
|
||||||
<i class="fas fa-eye" aria-hidden="true"></i> View Detail
|
<i class="fas fa-eye" aria-hidden="true"></i> View Detail
|
||||||
</a>
|
</a>
|
||||||
|
{% if multi_user_enabled and file.owner_id is none %}
|
||||||
|
<button
|
||||||
|
class="action-btn btn-primary"
|
||||||
|
id="claim-btn"
|
||||||
|
aria-label="{{ _('file.claim_ownership') }}"
|
||||||
|
style="background:#10b981;"
|
||||||
|
>
|
||||||
|
<i class="fas fa-user-check" aria-hidden="true"></i> {{ _("file.claim_ownership") }}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<p id="claim-msg" style="margin-top:0.5rem;font-size:0.875rem;display:none;" role="alert"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -193,4 +210,17 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if multi_user_enabled and file and file.owner_id is none %}
|
||||||
|
<script src="{{ url_for('static', path='js/claim.js') }}" defer></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
initClaimOwnership({{ file.id | tojson }}, {
|
||||||
|
confirm: {{ _("file.claim_ownership_confirm") | tojson }},
|
||||||
|
success: {{ _("file.claim_ownership_success") | tojson }},
|
||||||
|
failed: {{ _("file.claim_ownership_failed") | tojson }}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -316,6 +316,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% if multi_user_enabled %}
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-key">{{ _("file.owner_label") }}</span>
|
||||||
|
<span class="info-val">{{ owner_display or _("file.owner_unowned") }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
@@ -344,7 +350,18 @@
|
|||||||
<a href="/shared-links?file_id={{ file.id }}" class="action-btn btn-secondary">
|
<a href="/shared-links?file_id={{ file.id }}" class="action-btn btn-secondary">
|
||||||
<i class="fas fa-share-alt" aria-hidden="true"></i> Share
|
<i class="fas fa-share-alt" aria-hidden="true"></i> Share
|
||||||
</a>
|
</a>
|
||||||
|
{% if multi_user_enabled and file.owner_id is none %}
|
||||||
|
<button
|
||||||
|
class="action-btn btn-primary"
|
||||||
|
id="claim-btn"
|
||||||
|
aria-label="{{ _('file.claim_ownership') }}"
|
||||||
|
style="background:#10b981;border:none;cursor:pointer;"
|
||||||
|
>
|
||||||
|
<i class="fas fa-user-check" aria-hidden="true"></i> {{ _("file.claim_ownership") }}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<p id="claim-msg" style="margin-top:0.5rem;font-size:0.875rem;display:none;" role="alert"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -937,6 +954,16 @@
|
|||||||
pdfInit('/api/files/{{ file.id }}/preview?version={{ pv }}');
|
pdfInit('/api/files/{{ file.id }}/preview?version={{ pv }}');
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if multi_user_enabled and file and file.owner_id is none %}
|
||||||
|
initClaimOwnership({{ file.id | tojson }}, {
|
||||||
|
confirm: {{ _("file.claim_ownership_confirm") | tojson }},
|
||||||
|
success: {{ _("file.claim_ownership_success") | tojson }},
|
||||||
|
failed: {{ _("file.claim_ownership_failed") | tojson }}
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
{% if multi_user_enabled and file and file.owner_id is none %}
|
||||||
|
<script src="{{ url_for('static', path='js/claim.js') }}" defer></script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1757,6 +1757,12 @@
|
|||||||
"sharing.role_viewer": "Viewer",
|
"sharing.role_viewer": "Viewer",
|
||||||
"sharing.user_id_label": "User ID or email",
|
"sharing.user_id_label": "User ID or email",
|
||||||
"sharing.user_id_placeholder": "e.g. alice@example.com",
|
"sharing.user_id_placeholder": "e.g. alice@example.com",
|
||||||
|
"file.owner_label": "Owner",
|
||||||
|
"file.owner_unowned": "Unowned",
|
||||||
|
"file.claim_ownership": "Claim Ownership",
|
||||||
|
"file.claim_ownership_confirm": "Claim this document as yours? You will become the owner and can manage sharing.",
|
||||||
|
"file.claim_ownership_success": "You are now the owner of this document.",
|
||||||
|
"file.claim_ownership_failed": "Could not claim ownership. The document may already have an owner.",
|
||||||
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
"similarity.backfill_auto": "The background task will compute them automatically every 5 minutes, or you can",
|
||||||
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
"similarity.files_missing_text": "file(s) have OCR text but no embedding yet.",
|
||||||
"similarity.find_pairs_btn": "Find Pairs",
|
"similarity.find_pairs_btn": "Find Pairs",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Target: Bring coverage from 8.77% to 70%+
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
@@ -1981,3 +1982,128 @@ class TestPipelineInfoInViews:
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"Standard" in response.content
|
assert b"Standard" in response.content
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Owner display and claim ownership tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class TestOwnerDisplayAndClaim:
|
||||||
|
"""Tests that owner info and claim button appear correctly on file views."""
|
||||||
|
|
||||||
|
def _make_file(self, db_session, owner_id=None) -> FileRecord:
|
||||||
|
file_rec = FileRecord(
|
||||||
|
filehash=uuid.uuid4().hex,
|
||||||
|
original_filename="doc.pdf",
|
||||||
|
local_filename="/tmp/doc.pdf",
|
||||||
|
file_size=512,
|
||||||
|
mime_type="application/pdf",
|
||||||
|
owner_id=owner_id,
|
||||||
|
)
|
||||||
|
db_session.add(file_rec)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(file_rec)
|
||||||
|
return file_rec
|
||||||
|
|
||||||
|
# ── /files/{id} (file_summary.html) ──────────────────────────────────
|
||||||
|
|
||||||
|
def test_summary_shows_owner_when_multi_user_enabled(self, client, db_session):
|
||||||
|
"""Owner ID is rendered in file summary when multi-user mode is on."""
|
||||||
|
file_rec = self._make_file(db_session, owner_id="alice@example.com")
|
||||||
|
with patch("app.config.settings.multi_user_enabled", True):
|
||||||
|
response = client.get(f"/files/{file_rec.id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"alice@example.com" in response.content
|
||||||
|
|
||||||
|
def test_summary_shows_unowned_label_for_unowned_file(self, client, db_session):
|
||||||
|
"""'Unowned' label is rendered in file summary for files without an owner."""
|
||||||
|
file_rec = self._make_file(db_session, owner_id=None)
|
||||||
|
with patch("app.config.settings.multi_user_enabled", True):
|
||||||
|
response = client.get(f"/files/{file_rec.id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"Unowned" in response.content
|
||||||
|
|
||||||
|
def test_summary_shows_claim_button_for_unowned_file(self, client, db_session):
|
||||||
|
"""Claim Ownership button appears on file summary for an unowned file."""
|
||||||
|
file_rec = self._make_file(db_session, owner_id=None)
|
||||||
|
with patch("app.config.settings.multi_user_enabled", True):
|
||||||
|
response = client.get(f"/files/{file_rec.id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"Claim Ownership" in response.content
|
||||||
|
|
||||||
|
def test_summary_no_claim_button_when_owned(self, client, db_session):
|
||||||
|
"""No Claim Ownership button when the file already has an owner."""
|
||||||
|
file_rec = self._make_file(db_session, owner_id="bob@example.com")
|
||||||
|
with patch("app.config.settings.multi_user_enabled", True):
|
||||||
|
response = client.get(f"/files/{file_rec.id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"Claim Ownership" not in response.content
|
||||||
|
|
||||||
|
def test_summary_no_owner_row_in_single_user_mode(self, client, db_session):
|
||||||
|
"""Owner row is hidden in single-user mode."""
|
||||||
|
file_rec = self._make_file(db_session, owner_id=None)
|
||||||
|
with patch("app.config.settings.multi_user_enabled", False):
|
||||||
|
response = client.get(f"/files/{file_rec.id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
# Claim button and Unowned label should not appear in single-user mode
|
||||||
|
assert b"Claim Ownership" not in response.content
|
||||||
|
|
||||||
|
# ── /files/{id}/detail (file_view.html) ──────────────────────────────
|
||||||
|
|
||||||
|
def test_detail_shows_owner_when_multi_user_enabled(self, client, db_session):
|
||||||
|
"""Owner ID is rendered in file detail view when multi-user mode is on."""
|
||||||
|
file_rec = self._make_file(db_session, owner_id="charlie@example.com")
|
||||||
|
with patch("app.config.settings.multi_user_enabled", True):
|
||||||
|
response = client.get(f"/files/{file_rec.id}/detail")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"charlie@example.com" in response.content
|
||||||
|
|
||||||
|
def test_detail_shows_claim_button_for_unowned_file(self, client, db_session):
|
||||||
|
"""Claim Ownership button appears in file detail view for an unowned file."""
|
||||||
|
file_rec = self._make_file(db_session, owner_id=None)
|
||||||
|
with patch("app.config.settings.multi_user_enabled", True):
|
||||||
|
response = client.get(f"/files/{file_rec.id}/detail")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"Claim Ownership" in response.content
|
||||||
|
|
||||||
|
# ── /files/{id}/annotations (file_annotations.html) ──────────────────
|
||||||
|
|
||||||
|
def test_annotations_shows_owner_info(self, client, db_session):
|
||||||
|
"""Owner info is rendered on the annotations page in multi-user mode."""
|
||||||
|
file_rec = self._make_file(db_session, owner_id="dave@example.com")
|
||||||
|
with patch("app.config.settings.multi_user_enabled", True):
|
||||||
|
response = client.get(f"/files/{file_rec.id}/annotations")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"dave@example.com" in response.content
|
||||||
|
|
||||||
|
def test_annotations_shows_claim_button_for_unowned_file(self, client, db_session):
|
||||||
|
"""Claim Ownership button appears on annotations page for unowned file."""
|
||||||
|
file_rec = self._make_file(db_session, owner_id=None)
|
||||||
|
with patch("app.config.settings.multi_user_enabled", True):
|
||||||
|
response = client.get(f"/files/{file_rec.id}/annotations")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"Claim Ownership" in response.content
|
||||||
|
|
||||||
|
def test_annotations_no_claim_button_when_owned(self, client, db_session):
|
||||||
|
"""No Claim Ownership button on annotations page when file has an owner."""
|
||||||
|
file_rec = self._make_file(db_session, owner_id="eve@example.com")
|
||||||
|
with patch("app.config.settings.multi_user_enabled", True):
|
||||||
|
response = client.get(f"/files/{file_rec.id}/annotations")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"Claim Ownership" not in response.content
|
||||||
|
|
||||||
|
def test_display_name_used_when_profile_exists(self, client, db_session):
|
||||||
|
"""UserProfile.display_name overrides raw user_id in the owner display."""
|
||||||
|
from app.models import UserProfile
|
||||||
|
|
||||||
|
file_rec = self._make_file(db_session, owner_id="frank@example.com")
|
||||||
|
profile = UserProfile(user_id="frank@example.com", display_name="Frank Lastname")
|
||||||
|
db_session.add(profile)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
with patch("app.config.settings.multi_user_enabled", True):
|
||||||
|
response = client.get(f"/files/{file_rec.id}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"Frank Lastname" in response.content
|
||||||
|
|||||||
Reference in New Issue
Block a user