mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-07-25 05:14:55 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f60a2af841 | ||
|
|
33cd48a17f | ||
|
|
d76dbf5366 | ||
|
|
b66182cd7b | ||
|
|
3fe6562c57 | ||
|
|
737d568f80 | ||
|
|
b992f9fc05 | ||
|
|
ed714b8a5a | ||
|
|
e0ab6a2e75 | ||
|
|
6d249b4932 | ||
|
|
90ea4f27c4 | ||
|
|
371c4f57d3 |
@@ -86,7 +86,7 @@ jobs:
|
|||||||
# ---- Prepare Release ----
|
# ---- Prepare Release ----
|
||||||
- name: Generate requirements file
|
- name: Generate requirements file
|
||||||
run: |
|
run: |
|
||||||
uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt
|
uv export --quiet --no-dev --all-extras --emit-index-url --format requirements-txt --output-file requirements.txt
|
||||||
- name: Compile messages
|
- name: Compile messages
|
||||||
env:
|
env:
|
||||||
PAPERLESS_SECRET_KEY: "ci-release-not-a-real-secret"
|
PAPERLESS_SECRET_KEY: "ci-release-not-a-real-secret"
|
||||||
|
|||||||
@@ -1,5 +1,64 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 3.0.2
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: fix broken migration in 3.0.1 [@shamoon](https://github.com/shamoon) ([#13242](https://github.com/paperless-ngx/paperless-ngx/pull/13242))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
- Fix: fix broken migration in 3.0.1 [@shamoon](https://github.com/shamoon) ([#13242](https://github.com/paperless-ngx/paperless-ngx/pull/13242))
|
||||||
|
|
||||||
|
## paperless-ngx 3.0.1
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: selection\_data re-derives the filtered document set 5 times over [@stumpylog](https://github.com/stumpylog) ([#13229](https://github.com/paperless-ngx/paperless-ngx/pull/13229))
|
||||||
|
- Fix: exclude source document from its own RAG similarity results [@stumpylog](https://github.com/stumpylog) ([#13233](https://github.com/paperless-ngx/paperless-ngx/pull/13233))
|
||||||
|
- Fix: split mailrule maximum\_age clamp into its own migration [@stumpylog](https://github.com/stumpylog) ([#13231](https://github.com/paperless-ngx/paperless-ngx/pull/13231))
|
||||||
|
- Chore: mark yes in confirm button for translation [@shamoon](https://github.com/shamoon) ([#13225](https://github.com/paperless-ngx/paperless-ngx/pull/13225))
|
||||||
|
- Fix: correct URL for W001 check [@shamoon](https://github.com/shamoon) ([#13220](https://github.com/paperless-ngx/paperless-ngx/pull/13220))
|
||||||
|
- Fix: ensure create dialog uses correct attribute type [@shamoon](https://github.com/shamoon) ([#13221](https://github.com/paperless-ngx/paperless-ngx/pull/13221))
|
||||||
|
- Fix: correct database engine from postgres to postgresql [@OberstVonGatow](https://github.com/OberstVonGatow) ([#13213](https://github.com/paperless-ngx/paperless-ngx/pull/13213))
|
||||||
|
- Fix: remove unnecessary .distinct() dominating document list queries [@stumpylog](https://github.com/stumpylog) ([#13205](https://github.com/paperless-ngx/paperless-ngx/pull/13205))
|
||||||
|
- Fix: also wrap non-breaking words in chat messages, handle whitespace [@shamoon](https://github.com/shamoon) ([#13211](https://github.com/paperless-ngx/paperless-ngx/pull/13211))
|
||||||
|
- Fix: tag/custom-field document\_count scales badly with tag count [@stumpylog](https://github.com/stumpylog) ([#13203](https://github.com/paperless-ngx/paperless-ngx/pull/13203))
|
||||||
|
- Fix: batch document user\_can\_change checks to avoid per-row N+1 [@stumpylog](https://github.com/stumpylog) ([#13204](https://github.com/paperless-ngx/paperless-ngx/pull/13204))
|
||||||
|
- Fix: fix app title restoration [@shamoon](https://github.com/shamoon) ([#13208](https://github.com/paperless-ngx/paperless-ngx/pull/13208))
|
||||||
|
- Fix: handle long wrapping titles in AI chat document list [@shamoon](https://github.com/shamoon) ([#13206](https://github.com/paperless-ngx/paperless-ngx/pull/13206))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Chore(deps-dev): Bump postcss from 8.5.6 to 8.5.22 in /src/paperless\_mail/templates in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13236](https://github.com/paperless-ngx/paperless-ngx/pull/13236))
|
||||||
|
- Chore(deps): Bump pyasn1 from 0.6.3 to 0.6.4 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13237](https://github.com/paperless-ngx/paperless-ngx/pull/13237))
|
||||||
|
- Chore(deps): Bump the uv group across 1 directory with 8 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13189](https://github.com/paperless-ngx/paperless-ngx/pull/13189))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>17 changes</summary>
|
||||||
|
|
||||||
|
- Chore(deps-dev): Bump postcss from 8.5.6 to 8.5.22 in /src/paperless\_mail/templates in the npm\_and\_yarn group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13236](https://github.com/paperless-ngx/paperless-ngx/pull/13236))
|
||||||
|
- Fix: selection\_data re-derives the filtered document set 5 times over [@stumpylog](https://github.com/stumpylog) ([#13229](https://github.com/paperless-ngx/paperless-ngx/pull/13229))
|
||||||
|
- Chore: reword "Settings > Default permissions" description for clarity [@NotaInutilis](https://github.com/NotaInutilis) ([#13232](https://github.com/paperless-ngx/paperless-ngx/pull/13232))
|
||||||
|
- Chore(deps): Bump pyasn1 from 0.6.3 to 0.6.4 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#13237](https://github.com/paperless-ngx/paperless-ngx/pull/13237))
|
||||||
|
- Chore(deps): Bump the uv group across 1 directory with 8 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#13189](https://github.com/paperless-ngx/paperless-ngx/pull/13189))
|
||||||
|
- Fix: exclude source document from its own RAG similarity results [@stumpylog](https://github.com/stumpylog) ([#13233](https://github.com/paperless-ngx/paperless-ngx/pull/13233))
|
||||||
|
- Fix: split mailrule maximum\_age clamp into its own migration [@stumpylog](https://github.com/stumpylog) ([#13231](https://github.com/paperless-ngx/paperless-ngx/pull/13231))
|
||||||
|
- Chore: mark yes in confirm button for translation [@shamoon](https://github.com/shamoon) ([#13225](https://github.com/paperless-ngx/paperless-ngx/pull/13225))
|
||||||
|
- Fix: correct URL for W001 check [@shamoon](https://github.com/shamoon) ([#13220](https://github.com/paperless-ngx/paperless-ngx/pull/13220))
|
||||||
|
- Fix: ensure create dialog uses correct attribute type [@shamoon](https://github.com/shamoon) ([#13221](https://github.com/paperless-ngx/paperless-ngx/pull/13221))
|
||||||
|
- Fix: correct database engine from postgres to postgresql [@OberstVonGatow](https://github.com/OberstVonGatow) ([#13213](https://github.com/paperless-ngx/paperless-ngx/pull/13213))
|
||||||
|
- Fix: remove unnecessary .distinct() dominating document list queries [@stumpylog](https://github.com/stumpylog) ([#13205](https://github.com/paperless-ngx/paperless-ngx/pull/13205))
|
||||||
|
- Fix: also wrap non-breaking words in chat messages, handle whitespace [@shamoon](https://github.com/shamoon) ([#13211](https://github.com/paperless-ngx/paperless-ngx/pull/13211))
|
||||||
|
- Fix: tag/custom-field document\_count scales badly with tag count [@stumpylog](https://github.com/stumpylog) ([#13203](https://github.com/paperless-ngx/paperless-ngx/pull/13203))
|
||||||
|
- Fix: batch document user\_can\_change checks to avoid per-row N+1 [@stumpylog](https://github.com/stumpylog) ([#13204](https://github.com/paperless-ngx/paperless-ngx/pull/13204))
|
||||||
|
- Fix: fix app title restoration [@shamoon](https://github.com/shamoon) ([#13208](https://github.com/paperless-ngx/paperless-ngx/pull/13208))
|
||||||
|
- Fix: handle long wrapping titles in AI chat document list [@shamoon](https://github.com/shamoon) ([#13206](https://github.com/paperless-ngx/paperless-ngx/pull/13206))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## paperless-ngx 3.0.0
|
## paperless-ngx 3.0.0
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|||||||
+61
-3
@@ -133,7 +133,7 @@ PAPERLESS_DB_OPTIONS="sslmode=require,sslrootcert=/certs/ca.pem,pool.max_size=10
|
|||||||
|
|
||||||
## OCR and Archive File Generation Settings
|
## OCR and Archive File Generation Settings
|
||||||
|
|
||||||
The settings that control OCR behaviour and archive file generation have been redesigned. The old settings that coupled these two concerns together are **removed** — old values are not silently honoured; a startup warning is logged if any removed variable is still set in your environment.
|
The settings that control OCR behaviour and archive file generation have been redesigned. The old settings that coupled these two concerns together are **removed** - old values are not silently honoured; a startup warning is logged if any removed variable is still set in your environment.
|
||||||
|
|
||||||
### Removed settings
|
### Removed settings
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ Remove any `PAPERLESS_OCR_SKIP_ARCHIVE_FILE` variable from your environment. If
|
|||||||
# v2: skip OCR when text present, always archive
|
# v2: skip OCR when text present, always archive
|
||||||
PAPERLESS_OCR_MODE=skip
|
PAPERLESS_OCR_MODE=skip
|
||||||
# v3: equivalent (auto is the new default)
|
# v3: equivalent (auto is the new default)
|
||||||
# No change needed — auto is the default
|
# No change needed - auto is the default
|
||||||
|
|
||||||
# v2: skip OCR when text present, skip archive too
|
# v2: skip OCR when text present, skip archive too
|
||||||
PAPERLESS_OCR_MODE=skip_noarchive
|
PAPERLESS_OCR_MODE=skip_noarchive
|
||||||
@@ -189,7 +189,7 @@ PAPERLESS_ARCHIVE_FILE_GENERATION=auto
|
|||||||
|
|
||||||
If you use the **remote OCR parser** (Azure AI), note that it always produces a
|
If you use the **remote OCR parser** (Azure AI), note that it always produces a
|
||||||
searchable PDF and stores it as the archive copy. `ARCHIVE_FILE_GENERATION=never`
|
searchable PDF and stores it as the archive copy. `ARCHIVE_FILE_GENERATION=never`
|
||||||
has no effect for documents handled by the remote parser — the archive is produced
|
has no effect for documents handled by the remote parser - the archive is produced
|
||||||
unconditionally by the remote engine.
|
unconditionally by the remote engine.
|
||||||
|
|
||||||
## Search Index (Whoosh -> Tantivy)
|
## Search Index (Whoosh -> Tantivy)
|
||||||
@@ -327,6 +327,64 @@ behind a reverse proxy may need to set
|
|||||||
[`PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](configuration.md#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER),
|
[`PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](configuration.md#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER),
|
||||||
or both, to avoid `403 Forbidden` errors on login.
|
or both, to avoid `403 Forbidden` errors on login.
|
||||||
|
|
||||||
|
## Minimum CPU Requirements (NumPy Baseline)
|
||||||
|
|
||||||
|
Starting with NumPy 2.4.0, official `manylinux` x86_64 wheels are compiled with a minimum
|
||||||
|
CPU baseline of `x86-64-v2`, which requires SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT, and
|
||||||
|
CMPXCHG16B. This is a [deliberate upstream change](https://github.com/numpy/numpy/issues/27851)
|
||||||
|
citing that these instructions have been present in over 99.7% of CPUs since 2008
|
||||||
|
([Intel](<https://en.wikipedia.org/wiki/Nehalem_(microarchitecture)>)) or 2011
|
||||||
|
([AMD](<https://en.wikipedia.org/wiki/Bulldozer_(microarchitecture)>)).
|
||||||
|
|
||||||
|
NumPy is a dependency of the document classifier (via scikit-learn), so any CPU that
|
||||||
|
predates SSE4.2 support will crash with `SIGILL` (illegal instruction) when the classifier
|
||||||
|
is loaded or trained, regardless of whether AI features are enabled.
|
||||||
|
|
||||||
|
This differs from NumPy's optional SIMD dispatch (e.g. AVX2, AVX512), which is detected and
|
||||||
|
selected safely at runtime - the `x86-64-v2` requirement above is a hard floor baked into the
|
||||||
|
wheel, with no runtime fallback.
|
||||||
|
|
||||||
|
### Affected hardware
|
||||||
|
|
||||||
|
CPUs older than roughly 2008 (Intel) or 2011 (AMD) that lack SSE4.2 support. This is more
|
||||||
|
likely to affect low-power or embedded hardware - e.g. early Atom, Celeron, or pre-Bulldozer
|
||||||
|
AMD chips - than typical desktop or server hardware from the last decade.
|
||||||
|
|
||||||
|
Check for SSE4.2 support with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -o -m1 sse4_2 /proc/cpuinfo
|
||||||
|
```
|
||||||
|
|
||||||
|
If this prints nothing, your CPU is affected.
|
||||||
|
|
||||||
|
### Symptoms
|
||||||
|
|
||||||
|
The Celery worker (and potentially the web server) repeatedly crashes and restarts with a
|
||||||
|
`SIGILL` error, typically visible in `dmesg`/`journalctl` as a `trap invalid opcode` inside
|
||||||
|
`_multiarray_umath...so`. Because the classifier is trained on a periodic schedule
|
||||||
|
(hourly, by default), affected instances see intermittent, hard-to-reproduce document
|
||||||
|
consumption failures whenever that scheduled task runs and takes down the worker process
|
||||||
|
mid-task.
|
||||||
|
|
||||||
|
### Action Required (for affected hardware only)
|
||||||
|
|
||||||
|
There is no way to make the classifier itself work on such CPUs - it requires an unofficial
|
||||||
|
NumPy build with `cpu-baseline=none`, which is not something we can ship. The practical
|
||||||
|
path forward is to stop the classifier from ever loading or training, which avoids
|
||||||
|
importing NumPy at all:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PAPERLESS_TRAIN_TASK_CRON=disable
|
||||||
|
```
|
||||||
|
|
||||||
|
This disables the periodic classifier training task (see
|
||||||
|
[`PAPERLESS_TRAIN_TASK_CRON`](configuration.md#PAPERLESS_TRAIN_TASK_CRON)). Automatic
|
||||||
|
matching based on the classifier (suggested correspondents, document types, tags, and
|
||||||
|
storage paths from trained rules) will no longer be available, but rule-based matching is
|
||||||
|
unaffected, and document consumption itself will no longer be at risk of crashing the
|
||||||
|
worker.
|
||||||
|
|
||||||
## Database Migrations
|
## Database Migrations
|
||||||
|
|
||||||
Some integer fields have been changed to smaller types to reduce database size. If you have any `MailRule` records with a `maximum_age` greater than 32767, they will be clamped to 32767 during the migration to avoid errors during migration.
|
Some integer fields have been changed to smaller types to reduce database size. If you have any `MailRule` records with a `maximum_age` greater than 32767, they will be clamped to 32767 during the migration to avoid errors during migration.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "3.0.1"
|
version = "3.0.2"
|
||||||
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "paperless-ngx-ui",
|
"name": "paperless-ngx-ui",
|
||||||
"version": "3.0.1",
|
"version": "3.0.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const environment = {
|
|||||||
apiVersion: '10', // match src/paperless/settings.py
|
apiVersion: '10', // match src/paperless/settings.py
|
||||||
appTitle: DEFAULT_APP_TITLE,
|
appTitle: DEFAULT_APP_TITLE,
|
||||||
tag: 'prod',
|
tag: 'prod',
|
||||||
version: '3.0.1',
|
version: '3.0.2',
|
||||||
webSocketHost: window.location.host,
|
webSocketHost: window.location.host,
|
||||||
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||||
webSocketBaseUrl: base_url.pathname + 'ws/',
|
webSocketBaseUrl: base_url.pathname + 'ws/',
|
||||||
|
|||||||
@@ -475,7 +475,13 @@ class TantivyBackend:
|
|||||||
note_texts: list[str] = []
|
note_texts: list[str] = []
|
||||||
for note in document.notes.all():
|
for note in document.notes.all():
|
||||||
num_notes += 1
|
num_notes += 1
|
||||||
doc.add_json("notes", {"note": note.note, "user": note.user.username})
|
doc.add_json(
|
||||||
|
"notes",
|
||||||
|
{
|
||||||
|
"note": note.note,
|
||||||
|
"user": note.user.username if note.user else None,
|
||||||
|
},
|
||||||
|
)
|
||||||
note_texts.append(note.note)
|
note_texts.append(note.note)
|
||||||
if note_texts:
|
if note_texts:
|
||||||
doc.add_text("notes_text", " ".join(note_texts))
|
doc.add_text("notes_text", " ".join(note_texts))
|
||||||
|
|||||||
@@ -1264,7 +1264,17 @@ def task_failure_handler(
|
|||||||
"error_message": str(exception) if exception else "Unknown error",
|
"error_message": str(exception) if exception else "Unknown error",
|
||||||
}
|
}
|
||||||
if traceback:
|
if traceback:
|
||||||
tb_str = "".join(_tb.format_tb(traceback))
|
# billiard/celery pass a pre-formatted string instead of a real
|
||||||
|
# traceback object when the worker process itself died (e.g.
|
||||||
|
# WorkerLostError from a SIGILL) since there's no live traceback
|
||||||
|
# to walk in that case.
|
||||||
|
tb_str = (
|
||||||
|
traceback
|
||||||
|
if isinstance(traceback, str)
|
||||||
|
else "".join(
|
||||||
|
_tb.format_tb(traceback),
|
||||||
|
)
|
||||||
|
)
|
||||||
result_data["traceback"] = tb_str[:5000]
|
result_data["traceback"] = tb_str[:5000]
|
||||||
|
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
|||||||
@@ -844,6 +844,23 @@ class TestFieldHandling:
|
|||||||
f"Expected 1, got {len(ids)}. Note content should be searchable via notes.note: prefix."
|
f"Expected 1, got {len(ids)}. Note content should be searchable via notes.note: prefix."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_notes_without_user_are_indexed(self, backend: TantivyBackend) -> None:
|
||||||
|
"""Notes whose user was deleted (SET_NULL) must not break indexing."""
|
||||||
|
doc = Document.objects.create(
|
||||||
|
title="Doc with orphaned note",
|
||||||
|
content="test",
|
||||||
|
checksum="NT2",
|
||||||
|
pk=81,
|
||||||
|
)
|
||||||
|
Note.objects.create(document=doc, note="Orphaned note", user=None)
|
||||||
|
|
||||||
|
backend.add_or_update(doc)
|
||||||
|
|
||||||
|
ids = backend.search_ids("notes.note:orphaned", user=None)
|
||||||
|
assert len(ids) == 1, (
|
||||||
|
f"Expected 1, got {len(ids)}. Notes without a user should still be indexed."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestHighlightHits:
|
class TestHighlightHits:
|
||||||
"""Test highlight_hits returns proper HTML strings, not raw Snippet objects."""
|
"""Test highlight_hits returns proper HTML strings, not raw Snippet objects."""
|
||||||
|
|||||||
@@ -272,10 +272,7 @@ class MailDocumentParser:
|
|||||||
logger.debug("Building formatted text from email")
|
logger.debug("Building formatted text from email")
|
||||||
self._text = build_formatted_text(mail)
|
self._text = build_formatted_text(mail)
|
||||||
|
|
||||||
if is_naive(mail.date):
|
self._date = mail.date
|
||||||
self._date = make_aware(mail.date)
|
|
||||||
else:
|
|
||||||
self._date = mail.date
|
|
||||||
|
|
||||||
logger.debug("Creating a PDF from the email")
|
logger.debug("Creating a PDF from the email")
|
||||||
if self._mailrule_id:
|
if self._mailrule_id:
|
||||||
@@ -502,6 +499,9 @@ class MailDocumentParser:
|
|||||||
f"Could not parse {filepath}: {err}",
|
f"Could not parse {filepath}: {err}",
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
if is_naive(parsed.date):
|
||||||
|
parsed.date = make_aware(parsed.date)
|
||||||
|
|
||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
def tika_parse(self, html: str) -> str:
|
def tika_parse(self, html: str) -> str:
|
||||||
|
|||||||
@@ -421,6 +421,11 @@ class TikaDocumentParser:
|
|||||||
logger.info("Converting %s to PDF as %s", document_path, pdf_path)
|
logger.info("Converting %s to PDF as %s", document_path, pdf_path)
|
||||||
|
|
||||||
with self._gotenberg_client.libre_office.to_pdf() as route:
|
with self._gotenberg_client.libre_office.to_pdf() as route:
|
||||||
|
# Preserve document fields as authored. updateIndexes (Gotenberg's
|
||||||
|
# default) triggers a refresh() that rewrites dynamic fields like
|
||||||
|
# auto-dates to the current date.
|
||||||
|
route.update_indexes(update_indexes=False)
|
||||||
|
|
||||||
# Set the output format of the resulting PDF.
|
# Set the output format of the resulting PDF.
|
||||||
# OutputTypeConfig reads the database-stored ApplicationConfiguration
|
# OutputTypeConfig reads the database-stored ApplicationConfiguration
|
||||||
# first, then falls back to the PAPERLESS_OCR_OUTPUT_TYPE env var.
|
# first, then falls back to the PAPERLESS_OCR_OUTPUT_TYPE env var.
|
||||||
|
|||||||
@@ -145,6 +145,41 @@ class TestEmailFileParsing:
|
|||||||
assert parsed_msg.text == "This is just a simple Text Mail.\n"
|
assert parsed_msg.text == "This is just a simple Text Mail.\n"
|
||||||
assert parsed_msg.to == ("some@one.de",)
|
assert parsed_msg.to == ("some@one.de",)
|
||||||
|
|
||||||
|
def test_parse_file_to_message_makes_naive_date_aware(
|
||||||
|
self,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
mail_parser: MailDocumentParser,
|
||||||
|
simple_txt_email_file: Path,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- An email whose parsed date is naive (no tzinfo)
|
||||||
|
WHEN:
|
||||||
|
- The .eml file is parsed into a MailMessage
|
||||||
|
THEN:
|
||||||
|
- The resulting message date is made timezone-aware
|
||||||
|
"""
|
||||||
|
mock_message = mocker.Mock(
|
||||||
|
from_values="mail@someserver.de",
|
||||||
|
date=datetime.datetime(2022, 10, 12, 21, 40, 43),
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
"paperless.parsers.mail.MailMessage.from_bytes",
|
||||||
|
return_value=mock_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
parsed_msg = mail_parser.parse_file_to_message(simple_txt_email_file)
|
||||||
|
|
||||||
|
assert timezone.is_aware(parsed_msg.date)
|
||||||
|
assert parsed_msg.date.replace(tzinfo=None) == datetime.datetime(
|
||||||
|
2022,
|
||||||
|
10,
|
||||||
|
12,
|
||||||
|
21,
|
||||||
|
40,
|
||||||
|
43,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestEmailMetadataExtraction:
|
class TestEmailMetadataExtraction:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -223,4 +223,11 @@ class TestTikaParser:
|
|||||||
|
|
||||||
assert form_field_found
|
assert form_field_found
|
||||||
|
|
||||||
|
# Field updates must be disabled so LibreOffice preserves document
|
||||||
|
# fields (e.g. auto-date fields) as authored rather than refreshing
|
||||||
|
# them to the current date.
|
||||||
|
assert any(
|
||||||
|
b'name="updateIndexes"' in part and b"false" in part for part in parts
|
||||||
|
)
|
||||||
|
|
||||||
httpx_mock.reset()
|
httpx_mock.reset()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
__version__: Final[tuple[int, int, int]] = (3, 0, 1)
|
__version__: Final[tuple[int, int, int]] = (3, 0, 2)
|
||||||
# Version string like X.Y.Z
|
# Version string like X.Y.Z
|
||||||
__full_version_str__: Final[str] = ".".join(map(str, __version__))
|
__full_version_str__: Final[str] = ".".join(map(str, __version__))
|
||||||
# Version string like X.Y
|
# Version string like X.Y
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
# Split out of 0002_optimize_integer_field_sizes.py into its own migration
|
|
||||||
# (own transaction). Running this UPDATE in the same transaction as the
|
|
||||||
# later ALTER TABLE on paperless_mail_mailrule can fail on PostgreSQL with
|
|
||||||
# "cannot ALTER TABLE because it has pending trigger events", since the
|
|
||||||
# update queues FK trigger events against that table that are still pending
|
|
||||||
# when the subsequent AlterField tries to rewrite it. Committing the clamp
|
|
||||||
# here first avoids that.
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def clamp_mailrule_maximum_age(apps, schema_editor):
|
|
||||||
# Clamp the maximum_age field of MailRule because of PositiveIntegerField --> PositiveSmallIntegerField
|
|
||||||
MailRule = apps.get_model("paperless_mail", "MailRule")
|
|
||||||
MailRule.objects.filter(maximum_age__gt=32767).update(maximum_age=32767)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("paperless_mail", "0001_squashed"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(
|
|
||||||
clamp_mailrule_maximum_age,
|
|
||||||
migrations.RunPython.noop,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -4,9 +4,19 @@ from django.db import migrations
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
def clamp_mailrule_maximum_age(apps, schema_editor):
|
||||||
|
# Clamp the maximum_age field of MailRule because of PositiveIntegerField --> PositiveSmallIntegerField
|
||||||
|
MailRule = apps.get_model("paperless_mail", "MailRule")
|
||||||
|
MailRule.objects.filter(maximum_age__gt=32767).update(maximum_age=32767)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
# The data update must commit before PostgreSQL rewrites the table, otherwise
|
||||||
|
# pending foreign-key trigger events can block the subsequent ALTER TABLE.
|
||||||
|
atomic = False
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("paperless_mail", "0001_1_clamp_mailrule_maximum_age"),
|
("paperless_mail", "0001_squashed"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -112,6 +122,10 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="consumption scope",
|
verbose_name="consumption scope",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
clamp_mailrule_maximum_age,
|
||||||
|
migrations.RunPython.noop,
|
||||||
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="mailrule",
|
model_name="mailrule",
|
||||||
name="maximum_age",
|
name="maximum_age",
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import importlib
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def test_optimize_integer_fields_preserves_released_migration_history():
|
||||||
|
migration_module = importlib.import_module(
|
||||||
|
"paperless_mail.migrations.0002_optimize_integer_field_sizes",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert migration_module.Migration.dependencies == [
|
||||||
|
("paperless_mail", "0001_squashed"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_mailrule_maximum_age_is_clamped_before_non_atomic_alter():
|
||||||
|
migration_module = importlib.import_module(
|
||||||
|
"paperless_mail.migrations.0002_optimize_integer_field_sizes",
|
||||||
|
)
|
||||||
|
migration = migration_module.Migration
|
||||||
|
|
||||||
|
clamp_index = next(
|
||||||
|
index
|
||||||
|
for index, operation in enumerate(migration.operations)
|
||||||
|
if isinstance(operation, migrations.RunPython)
|
||||||
|
and operation.code is migration_module.clamp_mailrule_maximum_age
|
||||||
|
)
|
||||||
|
alter_index = next(
|
||||||
|
index
|
||||||
|
for index, operation in enumerate(migration.operations)
|
||||||
|
if isinstance(operation, migrations.AlterField)
|
||||||
|
and operation.model_name == "mailrule"
|
||||||
|
and operation.name == "maximum_age"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert migration.atomic is False
|
||||||
|
assert clamp_index < alter_index
|
||||||
@@ -2880,7 +2880,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paperless-ngx"
|
name = "paperless-ngx"
|
||||||
version = "3.0.1"
|
version = "3.0.2"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
|
|||||||
Reference in New Issue
Block a user