Consistent naming to Share Link Bundle

This commit is contained in:
shamoon
2025-11-05 17:24:23 -08:00
parent c5ec073a5b
commit 931e2321b3
24 changed files with 283 additions and 271 deletions

View File

@@ -12,8 +12,8 @@ from documents.models import Note
from documents.models import PaperlessTask
from documents.models import SavedView
from documents.models import SavedViewFilterRule
from documents.models import ShareBundle
from documents.models import ShareLink
from documents.models import ShareLinkBundle
from documents.models import StoragePath
from documents.models import Tag
from documents.tasks import update_document_parent_tags
@@ -186,7 +186,7 @@ class ShareLinksAdmin(GuardedModelAdmin):
return super().get_queryset(request).select_related("document__correspondent")
class ShareBundleAdmin(GuardedModelAdmin):
class ShareLinkBundleAdmin(GuardedModelAdmin):
list_display = ("created", "status", "expiration", "owner", "slug")
list_filter = ("status", "created", "expiration", "owner")
search_fields = ("slug",)
@@ -233,7 +233,7 @@ admin.site.register(StoragePath, StoragePathAdmin)
admin.site.register(PaperlessTask, TaskAdmin)
admin.site.register(Note, NotesAdmin)
admin.site.register(ShareLink, ShareLinksAdmin)
admin.site.register(ShareBundle, ShareBundleAdmin)
admin.site.register(ShareLinkBundle, ShareLinkBundleAdmin)
admin.site.register(CustomField, CustomFieldsAdmin)
admin.site.register(CustomFieldInstance, CustomFieldInstancesAdmin)

View File

@@ -38,8 +38,8 @@ from documents.models import CustomFieldInstance
from documents.models import Document
from documents.models import DocumentType
from documents.models import PaperlessTask
from documents.models import ShareBundle
from documents.models import ShareLink
from documents.models import ShareLinkBundle
from documents.models import StoragePath
from documents.models import Tag
@@ -797,11 +797,11 @@ class ShareLinkFilterSet(FilterSet):
}
class ShareBundleFilterSet(FilterSet):
class ShareLinkBundleFilterSet(FilterSet):
documents = Filter(method="filter_documents")
class Meta:
model = ShareBundle
model = ShareLinkBundle
fields = {
"created": DATETIME_KWARGS,
"expiration": DATETIME_KWARGS,

View File

@@ -11,7 +11,7 @@ from django.db import migrations
from django.db import models
def grant_sharebundle_permissions(apps, schema_editor):
def grant_share_link_bundle_permissions(apps, schema_editor):
# Ensure newly introduced permissions are created for all apps
for app_config in apps.get_app_configs():
app_config.models_module = True
@@ -22,27 +22,27 @@ def grant_sharebundle_permissions(apps, schema_editor):
if add_document_perm is None:
return
sharebundle_permissions = Permission.objects.filter(
codename__contains="sharebundle",
share_bundle_permissions = Permission.objects.filter(
codename__contains="sharelinkbundle",
)
users = User.objects.filter(user_permissions=add_document_perm).distinct()
for user in users:
user.user_permissions.add(*sharebundle_permissions)
user.user_permissions.add(*share_bundle_permissions)
groups = Group.objects.filter(permissions=add_document_perm).distinct()
for group in groups:
group.permissions.add(*sharebundle_permissions)
group.permissions.add(*share_bundle_permissions)
def revoke_sharebundle_permissions(apps, schema_editor):
sharebundle_permissions = Permission.objects.filter(
codename__contains="sharebundle",
def revoke_share_link_bundle_permissions(apps, schema_editor):
share_bundle_permissions = Permission.objects.filter(
codename__contains="sharelinkbundle",
)
for user in User.objects.all():
user.user_permissions.remove(*sharebundle_permissions)
user.user_permissions.remove(*share_bundle_permissions)
for group in Group.objects.all():
group.permissions.remove(*sharebundle_permissions)
group.permissions.remove(*share_bundle_permissions)
class Migration(migrations.Migration):
@@ -53,7 +53,7 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name="ShareBundle",
name="ShareLinkBundle",
fields=[
(
"id",
@@ -150,7 +150,7 @@ class Migration(migrations.Migration):
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="share_bundles",
related_name="share_link_bundles",
to=settings.AUTH_USER_MODEL,
verbose_name="owner",
),
@@ -170,21 +170,21 @@ class Migration(migrations.Migration):
],
options={
"ordering": ("-created",),
"verbose_name": "share bundle",
"verbose_name_plural": "share bundles",
"verbose_name": "share link bundle",
"verbose_name_plural": "share link bundles",
},
),
migrations.AddField(
model_name="sharebundle",
model_name="sharelinkbundle",
name="documents",
field=models.ManyToManyField(
related_name="share_bundles",
related_name="share_link_bundles",
to="documents.document",
verbose_name="documents",
),
),
migrations.RunPython(
grant_sharebundle_permissions,
reverse_code=revoke_sharebundle_permissions,
grant_share_link_bundle_permissions,
reverse_code=revoke_share_link_bundle_permissions,
),
]

View File

@@ -777,7 +777,7 @@ class ShareLink(SoftDeleteModel):
return f"Share Link for {self.document.title}"
class ShareBundle(SoftDeleteModel):
class ShareLinkBundle(SoftDeleteModel):
class Status(models.TextChoices):
PENDING = ("pending", _("Pending"))
PROCESSING = ("processing", _("Processing"))
@@ -786,8 +786,8 @@ class ShareBundle(SoftDeleteModel):
class Meta:
ordering = ("-created",)
verbose_name = _("share bundle")
verbose_name_plural = _("share bundles")
verbose_name = _("share link bundle")
verbose_name_plural = _("share link bundles")
created = models.DateTimeField(
_("created"),
@@ -816,7 +816,7 @@ class ShareBundle(SoftDeleteModel):
User,
blank=True,
null=True,
related_name="share_bundles",
related_name="share_link_bundles",
on_delete=models.SET_NULL,
verbose_name=_("owner"),
)
@@ -858,12 +858,12 @@ class ShareBundle(SoftDeleteModel):
documents = models.ManyToManyField(
"documents.Document",
related_name="share_bundles",
related_name="share_link_bundles",
verbose_name=_("documents"),
)
def __str__(self):
return _("Share bundle %(slug)s") % {"slug": self.slug}
return _("Share link bundle %(slug)s") % {"slug": self.slug}
@property
def absolute_file_path(self) -> Path | None:

View File

@@ -58,8 +58,8 @@ from documents.models import Note
from documents.models import PaperlessTask
from documents.models import SavedView
from documents.models import SavedViewFilterRule
from documents.models import ShareBundle
from documents.models import ShareLink
from documents.models import ShareLinkBundle
from documents.models import StoragePath
from documents.models import Tag
from documents.models import UiSettings
@@ -2130,7 +2130,7 @@ class ShareLinkSerializer(OwnedObjectSerializer):
return super().create(validated_data)
class ShareBundleSerializer(OwnedObjectSerializer):
class ShareLinkBundleSerializer(OwnedObjectSerializer):
document_ids = serializers.ListField(
child=serializers.IntegerField(min_value=1),
allow_empty=False,
@@ -2149,7 +2149,7 @@ class ShareBundleSerializer(OwnedObjectSerializer):
document_count = SerializerMethodField()
class Meta:
model = ShareBundle
model = ShareLinkBundle
fields = (
"id",
"created",
@@ -2198,7 +2198,7 @@ class ShareBundleSerializer(OwnedObjectSerializer):
else:
validated_data["expiration"] = None
share_bundle = super().create(validated_data)
share_link_bundle = super().create(validated_data)
if documents is None:
documents = list(
@@ -2224,12 +2224,12 @@ class ShareBundleSerializer(OwnedObjectSerializer):
)
ordered_documents = [documents_by_id[doc_id] for doc_id in document_ids]
share_bundle.documents.set(ordered_documents)
share_bundle.document_total = len(ordered_documents)
share_link_bundle.documents.set(ordered_documents)
share_link_bundle.document_total = len(ordered_documents)
return share_bundle
return share_link_bundle
def get_document_count(self, obj: ShareBundle) -> int:
def get_document_count(self, obj: ShareLinkBundle) -> int:
count = getattr(obj, "document_total", None)
if count is not None:
return count

View File

@@ -43,8 +43,8 @@ from documents.models import CustomFieldInstance
from documents.models import Document
from documents.models import DocumentType
from documents.models import PaperlessTask
from documents.models import ShareBundle
from documents.models import ShareLink
from documents.models import ShareLinkBundle
from documents.models import StoragePath
from documents.models import Tag
from documents.models import Workflow
@@ -572,17 +572,19 @@ def update_document_parent_tags(tag: Tag, new_parent: Tag) -> None:
@shared_task
def build_share_bundle(bundle_id: int):
def build_share_link_bundle(bundle_id: int):
try:
bundle = (
ShareBundle.objects.filter(pk=bundle_id).prefetch_related("documents").get()
ShareLinkBundle.objects.filter(pk=bundle_id)
.prefetch_related("documents")
.get()
)
except ShareBundle.DoesNotExist:
logger.warning("Share bundle %s no longer exists.", bundle_id)
except ShareLinkBundle.DoesNotExist:
logger.warning("Share link bundle %s no longer exists.", bundle_id)
return
bundle.remove_file()
bundle.status = ShareBundle.Status.PROCESSING
bundle.status = ShareLinkBundle.Status.PROCESSING
bundle.last_error = ""
bundle.size_bytes = None
bundle.built_at = None
@@ -617,7 +619,7 @@ def build_share_bundle(bundle_id: int):
for document in documents:
strategy.add_document(document)
output_dir = settings.SHARE_BUNDLE_DIR
output_dir = settings.SHARE_LINK_BUNDLE_DIR
output_dir.mkdir(parents=True, exist_ok=True)
final_path = (output_dir / f"{bundle.slug}.zip").resolve()
if final_path.exists():
@@ -629,7 +631,7 @@ def build_share_bundle(bundle_id: int):
except ValueError:
bundle.file_path = str(final_path)
bundle.size_bytes = final_path.stat().st_size
bundle.status = ShareBundle.Status.READY
bundle.status = ShareLinkBundle.Status.READY
bundle.built_at = timezone.now()
bundle.last_error = ""
bundle.save(
@@ -641,10 +643,14 @@ def build_share_bundle(bundle_id: int):
"last_error",
],
)
logger.info("Built share bundle %s", bundle.pk)
logger.info("Built share link bundle %s", bundle.pk)
except Exception as exc:
logger.exception("Failed to build share bundle %s: %s", bundle_id, exc)
bundle.status = ShareBundle.Status.FAILED
logger.exception(
"Failed to build share link bundle %s: %s",
bundle_id,
exc,
)
bundle.status = ShareLinkBundle.Status.FAILED
bundle.last_error = str(exc)
bundle.save(update_fields=["status", "last_error"])
try:
@@ -661,9 +667,9 @@ def build_share_bundle(bundle_id: int):
@shared_task
def cleanup_expired_share_bundles():
def cleanup_expired_share_link_bundles():
now = timezone.now()
expired_qs = ShareBundle.objects.filter(
expired_qs = ShareLinkBundle.objects.filter(
deleted_at__isnull=True,
expiration__isnull=False,
expiration__lt=now,
@@ -675,9 +681,9 @@ def cleanup_expired_share_bundles():
bundle.hard_delete()
except Exception as exc:
logger.warning(
"Failed to delete expired share bundle %s: %s",
"Failed to delete expired share link bundle %s: %s",
bundle.pk,
exc,
)
if count:
logger.info("Deleted %s expired share bundle(s)", count)
logger.info("Deleted %s expired share link bundle(s)", count)

View File

@@ -119,7 +119,7 @@ from documents.filters import DocumentTypeFilterSet
from documents.filters import ObjectOwnedOrGrantedPermissionsFilter
from documents.filters import ObjectOwnedPermissionsFilter
from documents.filters import PaperlessTaskFilterSet
from documents.filters import ShareBundleFilterSet
from documents.filters import ShareLinkBundleFilterSet
from documents.filters import ShareLinkFilterSet
from documents.filters import StoragePathFilterSet
from documents.filters import TagFilterSet
@@ -136,8 +136,8 @@ from documents.models import DocumentType
from documents.models import Note
from documents.models import PaperlessTask
from documents.models import SavedView
from documents.models import ShareBundle
from documents.models import ShareLink
from documents.models import ShareLinkBundle
from documents.models import StoragePath
from documents.models import Tag
from documents.models import UiSettings
@@ -171,7 +171,7 @@ from documents.serialisers import PostDocumentSerializer
from documents.serialisers import RunTaskViewSerializer
from documents.serialisers import SavedViewSerializer
from documents.serialisers import SearchResultSerializer
from documents.serialisers import ShareBundleSerializer
from documents.serialisers import ShareLinkBundleSerializer
from documents.serialisers import ShareLinkSerializer
from documents.serialisers import StoragePathSerializer
from documents.serialisers import StoragePathTestSerializer
@@ -184,7 +184,7 @@ from documents.serialisers import WorkflowActionSerializer
from documents.serialisers import WorkflowSerializer
from documents.serialisers import WorkflowTriggerSerializer
from documents.signals import document_updated
from documents.tasks import build_share_bundle
from documents.tasks import build_share_link_bundle
from documents.tasks import consume_file
from documents.tasks import empty_trash
from documents.tasks import index_optimize
@@ -2621,12 +2621,12 @@ class ShareLinkViewSet(ModelViewSet, PassUserMixin):
ordering_fields = ("created", "expiration", "document")
class ShareBundleViewSet(ModelViewSet, PassUserMixin):
model = ShareBundle
class ShareLinkBundleViewSet(ModelViewSet, PassUserMixin):
model = ShareLinkBundle
queryset = ShareBundle.objects.all()
queryset = ShareLinkBundle.objects.all()
serializer_class = ShareBundleSerializer
serializer_class = ShareLinkBundleSerializer
pagination_class = StandardPagination
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (
@@ -2634,7 +2634,7 @@ class ShareBundleViewSet(ModelViewSet, PassUserMixin):
OrderingFilter,
ObjectOwnedOrGrantedPermissionsFilter,
)
filterset_class = ShareBundleFilterSet
filterset_class = ShareLinkBundleFilterSet
ordering_fields = ("created", "expiration", "status")
def get_queryset(self):
@@ -2684,7 +2684,7 @@ class ShareBundleViewSet(ModelViewSet, PassUserMixin):
documents=ordered_documents,
)
bundle.remove_file()
bundle.status = ShareBundle.Status.PENDING
bundle.status = ShareLinkBundle.Status.PENDING
bundle.last_error = ""
bundle.size_bytes = None
bundle.built_at = None
@@ -2698,7 +2698,7 @@ class ShareBundleViewSet(ModelViewSet, PassUserMixin):
"file_path",
],
)
build_share_bundle.delay(bundle.pk)
build_share_link_bundle.delay(bundle.pk)
bundle.document_total = len(ordered_documents)
response_serializer = self.get_serializer(bundle)
headers = self.get_success_headers(response_serializer.data)
@@ -2711,13 +2711,13 @@ class ShareBundleViewSet(ModelViewSet, PassUserMixin):
@action(detail=True, methods=["post"])
def rebuild(self, request, pk=None):
bundle = self.get_object()
if bundle.status == ShareBundle.Status.PROCESSING:
if bundle.status == ShareLinkBundle.Status.PROCESSING:
return Response(
{"detail": _("Bundle is already being processed.")},
status=status.HTTP_400_BAD_REQUEST,
)
bundle.remove_file()
bundle.status = ShareBundle.Status.PENDING
bundle.status = ShareLinkBundle.Status.PENDING
bundle.last_error = ""
bundle.size_bytes = None
bundle.built_at = None
@@ -2731,7 +2731,7 @@ class ShareBundleViewSet(ModelViewSet, PassUserMixin):
"file_path",
],
)
build_share_bundle.delay(bundle.pk)
build_share_link_bundle.delay(bundle.pk)
bundle.document_total = (
getattr(bundle, "document_total", None) or bundle.documents.count()
)
@@ -2757,35 +2757,32 @@ class SharedLinkView(View):
disposition="inline",
)
share_bundle = ShareBundle.objects.filter(slug=slug).first()
if share_bundle is None:
bundle = ShareLinkBundle.objects.filter(slug=slug).first()
if bundle is None:
return HttpResponseRedirect("/accounts/login/?sharelink_notfound=1")
if (
share_bundle.expiration is not None
and share_bundle.expiration < timezone.now()
):
if bundle.expiration is not None and bundle.expiration < timezone.now():
return HttpResponseRedirect("/accounts/login/?sharelink_expired=1")
if share_bundle.status in {
ShareBundle.Status.PENDING,
ShareBundle.Status.PROCESSING,
if bundle.status in {
ShareLinkBundle.Status.PENDING,
ShareLinkBundle.Status.PROCESSING,
}:
return HttpResponse(
_(
"The shared bundle is still being prepared. Please try again later.",
"The share link bundle is still being prepared. Please try again later.",
),
status=status.HTTP_202_ACCEPTED,
)
if share_bundle.status == ShareBundle.Status.FAILED:
share_bundle.remove_file()
share_bundle.status = ShareBundle.Status.PENDING
share_bundle.last_error = ""
share_bundle.size_bytes = None
share_bundle.built_at = None
share_bundle.file_path = ""
share_bundle.save(
if bundle.status == ShareLinkBundle.Status.FAILED:
bundle.remove_file()
bundle.status = ShareLinkBundle.Status.PENDING
bundle.last_error = ""
bundle.size_bytes = None
bundle.built_at = None
bundle.file_path = ""
bundle.save(
update_fields=[
"status",
"last_error",
@@ -2794,22 +2791,22 @@ class SharedLinkView(View):
"file_path",
],
)
build_share_bundle.delay(share_bundle.pk)
build_share_link_bundle.delay(bundle.pk)
return HttpResponse(
_(
"The shared bundle is temporarily unavailable. A rebuild has been scheduled. Please try again later.",
"The share link bundle is temporarily unavailable. A rebuild has been scheduled. Please try again later.",
),
status=status.HTTP_503_SERVICE_UNAVAILABLE,
)
file_path = share_bundle.absolute_file_path
file_path = bundle.absolute_file_path
if file_path is None or not file_path.exists():
share_bundle.status = ShareBundle.Status.PENDING
share_bundle.last_error = ""
share_bundle.size_bytes = None
share_bundle.built_at = None
share_bundle.file_path = ""
share_bundle.save(
bundle.status = ShareLinkBundle.Status.PENDING
bundle.last_error = ""
bundle.size_bytes = None
bundle.built_at = None
bundle.file_path = ""
bundle.save(
update_fields=[
"status",
"last_error",
@@ -2818,16 +2815,16 @@ class SharedLinkView(View):
"file_path",
],
)
build_share_bundle.delay(share_bundle.pk)
build_share_link_bundle.delay(bundle.pk)
return HttpResponse(
_(
"The shared bundle is being prepared. Please try again later.",
"The share link bundle is being prepared. Please try again later.",
),
status=status.HTTP_202_ACCEPTED,
)
response = FileResponse(file_path.open("rb"), content_type="application/zip")
short_slug = share_bundle.slug[:12]
short_slug = bundle.slug[:12]
download_name = f"paperless-share-{short_slug}.zip"
filename_normalized = (
normalize("NFKD", download_name)

View File

@@ -231,11 +231,11 @@ def _parse_beat_schedule() -> dict:
},
},
{
"name": "Cleanup expired share bundles",
"env_key": "PAPERLESS_SHARE_BUNDLE_CLEANUP_CRON",
"name": "Cleanup expired share link bundles",
"env_key": "PAPERLESS_SHARE_LINK_BUNDLE_CLEANUP_CRON",
# Default daily at 02:00
"env_default": "0 2 * * *",
"task": "documents.tasks.cleanup_expired_share_bundles",
"task": "documents.tasks.cleanup_expired_share_link_bundles",
"options": {
# 1 hour before default schedule sends again
"expires": 23.0 * 60.0 * 60.0,
@@ -279,7 +279,7 @@ MEDIA_ROOT = __get_path("PAPERLESS_MEDIA_ROOT", BASE_DIR.parent / "media")
ORIGINALS_DIR = MEDIA_ROOT / "documents" / "originals"
ARCHIVE_DIR = MEDIA_ROOT / "documents" / "archive"
THUMBNAIL_DIR = MEDIA_ROOT / "documents" / "thumbnails"
SHARE_BUNDLE_DIR = MEDIA_ROOT / "documents" / "share_bundles"
SHARE_LINK_BUNDLE_DIR = MEDIA_ROOT / "documents" / "share_link_bundles"
DATA_DIR = __get_path("PAPERLESS_DATA_DIR", BASE_DIR.parent / "data")

View File

@@ -29,8 +29,8 @@ from documents.views import RemoteVersionView
from documents.views import SavedViewViewSet
from documents.views import SearchAutoCompleteView
from documents.views import SelectionDataView
from documents.views import ShareBundleViewSet
from documents.views import SharedLinkView
from documents.views import ShareLinkBundleViewSet
from documents.views import ShareLinkViewSet
from documents.views import StatisticsView
from documents.views import StoragePathViewSet
@@ -73,7 +73,7 @@ api_router.register(r"users", UserViewSet, basename="users")
api_router.register(r"groups", GroupViewSet, basename="groups")
api_router.register(r"mail_accounts", MailAccountViewSet)
api_router.register(r"mail_rules", MailRuleViewSet)
api_router.register(r"share_bundles", ShareBundleViewSet)
api_router.register(r"share_link_bundles", ShareLinkBundleViewSet)
api_router.register(r"share_links", ShareLinkViewSet)
api_router.register(r"workflow_triggers", WorkflowTriggerViewSet)
api_router.register(r"workflow_actions", WorkflowActionViewSet)