mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-13 03:27:13 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e717817c6 | ||
|
|
3e0fa73771 |
No files matched your search
@@ -31,9 +31,19 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
#### 🤖 Use of AI tools
|
#### 🤖 Use of AI tools
|
||||||
|
|
||||||
If this report was generated, in whole or in part, by an AI tool or agent you **must** clearly say so in the description below. Failing to clearly disclose this is a violation of our [Code of Conduct](https://github.com/paperless-ngx/paperless-ngx/blob/main/CODE_OF_CONDUCT.md).
|
If this report was generated, in whole or in part, by an AI tool or agent you **must** say so using the field below. Failing to clearly disclose this is a violation of our [Code of Conduct](https://github.com/paperless-ngx/paperless-ngx/blob/main/CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
Reports written by an AI agent must describe the _observed behavior only_: what happened, how to reproduce it, logs etc. Do **not** include code analysis, speculation about the root cause, or suggest code fixes.
|
Reports written by an AI agent must describe the _observed behavior only_: what happened, how to reproduce it, logs etc. Do **not** include code analysis, speculation about the root cause, or suggest code fixes.
|
||||||
|
- type: dropdown
|
||||||
|
id: ai-usage
|
||||||
|
attributes:
|
||||||
|
label: AI usage
|
||||||
|
description: Was any part of this report written by an AI tool or agent?
|
||||||
|
options:
|
||||||
|
- No AI tools were used
|
||||||
|
- Yes, AI tools helped write this report
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ jobs:
|
|||||||
const haystack = `${issue.title}\n${issue.body ?? ''}`;
|
const haystack = `${issue.title}\n${issue.body ?? ''}`;
|
||||||
const honeypot = haystack.toUpperCase().includes('ASLOP-PR-VERIFY');
|
const honeypot = haystack.toUpperCase().includes('ASLOP-PR-VERIFY');
|
||||||
|
|
||||||
|
// The form's required "AI usage" dropdown renders as a `### AI usage` section. An agent
|
||||||
|
// that follows the template trips the honeypot even when it discloses, so only treat
|
||||||
|
// the honeypot as undisclosed AI use when the reporter did not answer "Yes" here.
|
||||||
|
const aiUsage = (issue.body ?? '').match(/^###\s+AI usage\s*\n+\s*(.+?)\s*$/m)?.[1] ?? '';
|
||||||
|
const disclosedAI = aiUsage.toLowerCase().startsWith('yes');
|
||||||
|
|
||||||
// Issues opened through the form always get the template's default labels. GitHub
|
// Issues opened through the form always get the template's default labels. GitHub
|
||||||
// applies them a second or two *after* creation, so re-read them instead of trusting
|
// applies them a second or two *after* creation, so re-read them instead of trusting
|
||||||
// the webhook payload, and retry before concluding that there are none.
|
// the webhook payload, and retry before concluding that there are none.
|
||||||
@@ -55,19 +61,29 @@ jobs:
|
|||||||
if (labels.some((label) => templateLabels.includes(label))) break;
|
if (labels.some((label) => templateLabels.includes(label))) break;
|
||||||
}
|
}
|
||||||
const bypassedTemplate = !labels.some((label) => templateLabels.includes(label));
|
const bypassedTemplate = !labels.some((label) => templateLabels.includes(label));
|
||||||
core.info(`Labels: [${labels.join(', ')}], honeypot: ${honeypot}, bypassed template: ${bypassedTemplate}`);
|
core.info(
|
||||||
|
`Labels: [${labels.join(', ')}], honeypot: ${honeypot}, AI usage: "${aiUsage}", ` +
|
||||||
|
`bypassed template: ${bypassedTemplate}`
|
||||||
|
);
|
||||||
|
|
||||||
if (!honeypot && !bypassedTemplate) {
|
if (!bypassedTemplate && (disclosedAI || !honeypot)) {
|
||||||
core.info('No slop signals found');
|
if (disclosedAI) {
|
||||||
|
core.info('AI use disclosed, labeling only');
|
||||||
|
await github.rest.issues.addLabels({ ...common, labels: ['ai'] });
|
||||||
|
} else {
|
||||||
|
core.info('No slop signals found');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The honeypot is only ever tripped deliberately, so that message can name the cause.
|
// Reaching here with the honeypot means AI usage was not disclosed in the form, so that
|
||||||
|
// message can name the cause.
|
||||||
// A missing template label only tells us the issue did not come from the form, which
|
// A missing template label only tells us the issue did not come from the form, which
|
||||||
// is reason enough to close it, but not proof of how it was written.
|
// is reason enough to close it, but not proof of how it was written.
|
||||||
const body = honeypot
|
const body = honeypot
|
||||||
? 'This issue was automatically closed because it contains a marker that is only visible to ' +
|
? 'This issue was automatically closed because it contains a marker that is only visible to ' +
|
||||||
'automated tools, which indicates it was generated by an AI agent without being disclosed as such.\n\n' +
|
'automated tools, which indicates it was generated by an AI agent, but the "AI usage" field does not ' +
|
||||||
|
'disclose this.\n\n' +
|
||||||
`Please see our [contributing guidelines](${contributing}) and [Code of Conduct](${codeOfConduct}). ` +
|
`Please see our [contributing guidelines](${contributing}) and [Code of Conduct](${codeOfConduct}). ` +
|
||||||
'You are welcome to open a new issue that describes the problem you observed in your own words.'
|
'You are welcome to open a new issue that describes the problem you observed in your own words.'
|
||||||
: 'This issue was automatically closed because it was not opened using our bug report form. ' +
|
: 'This issue was automatically closed because it was not opened using our bug report form. ' +
|
||||||
|
|||||||
@@ -514,8 +514,11 @@ class Command(CryptMixin, PaperlessCommand):
|
|||||||
else:
|
else:
|
||||||
base_name = document.get_public_filename(counter=filename_counter)
|
base_name = document.get_public_filename(counter=filename_counter)
|
||||||
|
|
||||||
if base_name not in self.exported_files:
|
# The thumbnail and archive names are derived from the stem, so
|
||||||
self.exported_files.add(base_name)
|
# uniqueness must hold without the original's extension
|
||||||
|
unique_key = str(Path(base_name).with_suffix(""))
|
||||||
|
if unique_key not in self.exported_files:
|
||||||
|
self.exported_files.add(unique_key)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
filename_counter += 1
|
filename_counter += 1
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from datetime import date
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -44,14 +45,18 @@ from documents.models import Workflow
|
|||||||
from documents.models import WorkflowAction
|
from documents.models import WorkflowAction
|
||||||
from documents.models import WorkflowTrigger
|
from documents.models import WorkflowTrigger
|
||||||
from documents.sanity_checker import check_sanity
|
from documents.sanity_checker import check_sanity
|
||||||
|
from documents.settings import EXPORTER_ARCHIVE_NAME
|
||||||
from documents.settings import EXPORTER_FILE_NAME
|
from documents.settings import EXPORTER_FILE_NAME
|
||||||
from documents.settings import EXPORTER_SHARE_LINK_BUNDLE_NAME
|
from documents.settings import EXPORTER_SHARE_LINK_BUNDLE_NAME
|
||||||
|
from documents.settings import EXPORTER_THUMBNAIL_NAME
|
||||||
from documents.tests.utils import SampleDirMixin
|
from documents.tests.utils import SampleDirMixin
|
||||||
from paperless_mail.models import MailAccount
|
from paperless_mail.models import MailAccount
|
||||||
from paperless_testing.assertions import FileSystemAssertsMixin
|
from paperless_testing.assertions import FileSystemAssertsMixin
|
||||||
from paperless_testing.dirs import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.dirs import PaperlessDirs
|
||||||
from paperless_testing.dirs import paperless_environment
|
from paperless_testing.dirs import paperless_environment
|
||||||
from paperless_testing.factories import DocumentBarcodeFactory
|
from paperless_testing.factories import DocumentBarcodeFactory
|
||||||
|
from paperless_testing.factories import DocumentFactory
|
||||||
from paperless_testing.permissions import grant_object
|
from paperless_testing.permissions import grant_object
|
||||||
|
|
||||||
|
|
||||||
@@ -1462,3 +1467,73 @@ class TestCryptExportImport(
|
|||||||
("No passphrase was given, sensitive fields will be in plaintext"),
|
("No passphrase was given, sensitive fields will be in plaintext"),
|
||||||
stdout.read(),
|
stdout.read(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.management
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestExportDerivedFileNameCollisions:
|
||||||
|
def test_same_stem_different_extension_get_distinct_derived_files(
|
||||||
|
self,
|
||||||
|
paperless_dirs: PaperlessDirs,
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Two documents with the same title and created date
|
||||||
|
- One original is a PDF, the other a PNG, both with archive and thumbnail
|
||||||
|
WHEN:
|
||||||
|
- The exporter runs
|
||||||
|
THEN:
|
||||||
|
- Each document is exported under a distinct original, thumbnail and archive name
|
||||||
|
- Each exported file holds its own document's content
|
||||||
|
"""
|
||||||
|
for pk, (filename, mime_type) in enumerate(
|
||||||
|
[("collide.pdf", "application/pdf"), ("collide.png", "image/png")],
|
||||||
|
start=1,
|
||||||
|
):
|
||||||
|
(paperless_dirs.originals_dir / filename).write_bytes(
|
||||||
|
f"original {pk}".encode(),
|
||||||
|
)
|
||||||
|
(paperless_dirs.archive_dir / f"{pk:07}.pdf").write_bytes(
|
||||||
|
f"archive {pk}".encode(),
|
||||||
|
)
|
||||||
|
(paperless_dirs.thumbnail_dir / f"{pk:07}.webp").write_bytes(
|
||||||
|
f"thumbnail {pk}".encode(),
|
||||||
|
)
|
||||||
|
DocumentFactory(
|
||||||
|
pk=pk,
|
||||||
|
title="collide",
|
||||||
|
created=date(2026, 10, 7),
|
||||||
|
filename=filename,
|
||||||
|
archive_filename=f"{pk:07}.pdf",
|
||||||
|
mime_type=mime_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
target = tmp_path / "export"
|
||||||
|
target.mkdir()
|
||||||
|
call_command(
|
||||||
|
"document_exporter",
|
||||||
|
str(target),
|
||||||
|
"--no-progress-bar",
|
||||||
|
skip_checks=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
manifest = json.loads((target / "manifest.json").read_text())
|
||||||
|
records = {r["pk"]: r for r in manifest if r["model"] == "documents.document"}
|
||||||
|
|
||||||
|
for key in (
|
||||||
|
EXPORTER_FILE_NAME,
|
||||||
|
EXPORTER_THUMBNAIL_NAME,
|
||||||
|
EXPORTER_ARCHIVE_NAME,
|
||||||
|
):
|
||||||
|
assert records[1][key] != records[2][key]
|
||||||
|
|
||||||
|
for pk in (1, 2):
|
||||||
|
record = records[pk]
|
||||||
|
assert (target / record[EXPORTER_FILE_NAME]).read_text() == f"original {pk}"
|
||||||
|
assert (
|
||||||
|
target / record[EXPORTER_THUMBNAIL_NAME]
|
||||||
|
).read_text() == f"thumbnail {pk}"
|
||||||
|
assert (
|
||||||
|
target / record[EXPORTER_ARCHIVE_NAME]
|
||||||
|
).read_text() == f"archive {pk}"
|
||||||
Reference in new issue
Block a user