release(main): 同步 dev 最新候选改动
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
This commit is contained in:
@@ -10,12 +10,9 @@ from app.models.desktop_notification import (
|
||||
DesktopNotificationDelivery,
|
||||
DesktopNotificationSubscription,
|
||||
)
|
||||
from app.models.distribution import Distribution, DistributionStatus, DistributionTargetType
|
||||
from app.models.document import Document
|
||||
from app.models.document_version import DocumentVersion
|
||||
from app.models.study import Study
|
||||
from app.models.notification import Notification
|
||||
from app.models.study_member import StudyMember
|
||||
from app.schemas.notification import NotificationItem
|
||||
from app.models.user import User
|
||||
|
||||
CLAIM_LEASE = timedelta(minutes=5)
|
||||
|
||||
@@ -47,46 +44,28 @@ async def set_subscription(
|
||||
|
||||
|
||||
def _eligible_query(user_id: uuid.UUID, enabled_at: datetime, lease_cutoff: datetime):
|
||||
role_target_exists = exists(
|
||||
active_membership = exists(
|
||||
select(StudyMember.id).where(
|
||||
StudyMember.study_id == Document.trial_id,
|
||||
StudyMember.study_id == Notification.study_id,
|
||||
StudyMember.user_id == user_id,
|
||||
StudyMember.is_active.is_(True),
|
||||
StudyMember.role_in_study == Distribution.target_id,
|
||||
)
|
||||
)
|
||||
target_matches = or_(
|
||||
and_(
|
||||
Distribution.target_type == DistributionTargetType.USER,
|
||||
Distribution.target_id == str(user_id),
|
||||
),
|
||||
and_(
|
||||
Distribution.target_type == DistributionTargetType.ROLE,
|
||||
role_target_exists,
|
||||
),
|
||||
)
|
||||
return (
|
||||
select(
|
||||
Distribution,
|
||||
Document,
|
||||
DocumentVersion,
|
||||
Study,
|
||||
DesktopNotificationDelivery,
|
||||
)
|
||||
.join(Document, Distribution.document_id == Document.id)
|
||||
.join(DocumentVersion, Distribution.version_id == DocumentVersion.id)
|
||||
.join(Study, Document.trial_id == Study.id)
|
||||
select(Notification, DesktopNotificationDelivery)
|
||||
.outerjoin(
|
||||
DesktopNotificationDelivery,
|
||||
and_(
|
||||
DesktopNotificationDelivery.distribution_id == Distribution.id,
|
||||
DesktopNotificationDelivery.notification_id == Notification.id,
|
||||
DesktopNotificationDelivery.user_id == user_id,
|
||||
),
|
||||
)
|
||||
.where(
|
||||
Distribution.status == DistributionStatus.ACTIVE,
|
||||
Distribution.created_at >= enabled_at,
|
||||
target_matches,
|
||||
Notification.recipient_id == user_id,
|
||||
Notification.created_at >= enabled_at,
|
||||
Notification.resolved_at.is_(None),
|
||||
Notification.read_at.is_(None),
|
||||
active_membership,
|
||||
or_(
|
||||
DesktopNotificationDelivery.id.is_(None),
|
||||
and_(
|
||||
@@ -98,8 +77,8 @@ def _eligible_query(user_id: uuid.UUID, enabled_at: datetime, lease_cutoff: date
|
||||
),
|
||||
),
|
||||
)
|
||||
.order_by(Distribution.created_at.asc())
|
||||
.with_for_update(of=Distribution, skip_locked=True)
|
||||
.order_by(Notification.created_at.asc())
|
||||
.with_for_update(of=Notification, skip_locked=True)
|
||||
)
|
||||
|
||||
|
||||
@@ -107,45 +86,46 @@ async def claim_notifications(
|
||||
db: AsyncSession,
|
||||
user_id: uuid.UUID,
|
||||
limit: int,
|
||||
) -> tuple[uuid.UUID | None, datetime | None, list[NotificationItem]]:
|
||||
) -> tuple[uuid.UUID | None, datetime | None, list[Notification]]:
|
||||
subscription = await get_subscription(db, user_id)
|
||||
if not subscription or not subscription.enabled or not subscription.enabled_at:
|
||||
return None, None, []
|
||||
|
||||
# Reconcile every active project before selecting desktop deliveries so permission
|
||||
# changes fail closed and time-based reminders do not depend on opening the web Feed.
|
||||
user = await db.get(User, user_id)
|
||||
if user is None or not user.is_active:
|
||||
return None, None, []
|
||||
study_ids = (await db.scalars(
|
||||
select(StudyMember.study_id).where(
|
||||
StudyMember.user_id == user_id,
|
||||
StudyMember.is_active.is_(True),
|
||||
)
|
||||
)).all()
|
||||
from app.services.project_reminder_service import sync_project_reminders
|
||||
|
||||
for study_id in set(study_ids):
|
||||
await sync_project_reminders(db, study_id, user)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
token = uuid.uuid4()
|
||||
rows = (
|
||||
await db.execute(
|
||||
_eligible_query(user_id, subscription.enabled_at, now - CLAIM_LEASE).limit(max(1, min(limit, 50)))
|
||||
_eligible_query(user_id, subscription.enabled_at, now - CLAIM_LEASE)
|
||||
.limit(max(1, min(limit, 50)))
|
||||
)
|
||||
).all()
|
||||
items: list[NotificationItem] = []
|
||||
for distribution, document, version, study, delivery in rows:
|
||||
items: list[Notification] = []
|
||||
for notification, delivery in rows:
|
||||
if delivery is None:
|
||||
delivery = DesktopNotificationDelivery(
|
||||
user_id=user_id,
|
||||
distribution_id=distribution.id,
|
||||
notification_id=notification.id,
|
||||
)
|
||||
db.add(delivery)
|
||||
delivery.claim_token = token
|
||||
delivery.claimed_at = now
|
||||
items.append(
|
||||
NotificationItem(
|
||||
id=distribution.id,
|
||||
document_id=document.id,
|
||||
version_id=version.id,
|
||||
document_title=document.title,
|
||||
document_no=document.doc_no,
|
||||
version_no=version.version_no,
|
||||
change_summary=version.change_summary,
|
||||
effective_at=version.effective_at,
|
||||
created_at=distribution.created_at,
|
||||
study_id=study.id,
|
||||
study_name=study.name,
|
||||
delivered_at=delivery.delivered_at,
|
||||
read_at=delivery.read_at,
|
||||
)
|
||||
)
|
||||
items.append(notification)
|
||||
await db.commit()
|
||||
if not items:
|
||||
return None, None, []
|
||||
@@ -164,7 +144,7 @@ async def acknowledge_notifications(
|
||||
.where(
|
||||
DesktopNotificationDelivery.user_id == user_id,
|
||||
DesktopNotificationDelivery.claim_token == claim_token,
|
||||
DesktopNotificationDelivery.distribution_id.in_(delivered_ids),
|
||||
DesktopNotificationDelivery.notification_id.in_(delivered_ids),
|
||||
)
|
||||
.values(delivered_at=datetime.now(timezone.utc))
|
||||
)
|
||||
@@ -174,21 +154,17 @@ async def acknowledge_notifications(
|
||||
async def mark_notification_read(
|
||||
db: AsyncSession,
|
||||
user_id: uuid.UUID,
|
||||
distribution_id: uuid.UUID,
|
||||
notification_id: uuid.UUID,
|
||||
) -> None:
|
||||
delivery = (
|
||||
await db.execute(
|
||||
select(DesktopNotificationDelivery).where(
|
||||
DesktopNotificationDelivery.user_id == user_id,
|
||||
DesktopNotificationDelivery.distribution_id == distribution_id,
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if delivery is None:
|
||||
delivery = DesktopNotificationDelivery(
|
||||
user_id=user_id,
|
||||
distribution_id=distribution_id,
|
||||
)
|
||||
db.add(delivery)
|
||||
delivery.read_at = datetime.now(timezone.utc)
|
||||
item = await db.scalar(select(Notification).where(
|
||||
Notification.id == notification_id,
|
||||
Notification.recipient_id == user_id,
|
||||
Notification.resolved_at.is_(None),
|
||||
))
|
||||
if item is None:
|
||||
return
|
||||
now = datetime.now(timezone.utc)
|
||||
item.read_at = item.read_at or now
|
||||
if not item.requires_action:
|
||||
item.resolved_at = item.resolved_at or now
|
||||
await db.commit()
|
||||
|
||||
Reference in New Issue
Block a user