mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			dependabot
			...
			dependabot
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					349d9acd47 | ||
| 
						 | 
					33f61767c6 | 
@@ -26,7 +26,7 @@ dependencies = [
 | 
			
		||||
  # WARNING: django does not use semver.
 | 
			
		||||
  #          Only patch versions are guaranteed to not introduce breaking changes.
 | 
			
		||||
  "django~=5.2.5",
 | 
			
		||||
  "django-allauth[mfa,socialaccount]~=65.4.0",
 | 
			
		||||
  "django-allauth[mfa,socialaccount]~=65.12.1",
 | 
			
		||||
  "django-auditlog~=3.2.1",
 | 
			
		||||
  "django-cachalot~=2.8.0",
 | 
			
		||||
  "django-celery-results~=2.6.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -672,33 +672,11 @@
 | 
			
		||||
          <context context-type="linenumber">4</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8461842260159597706" datatype="html">
 | 
			
		||||
        <source>Show</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">8</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">37</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">52</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="5724363929304709833" datatype="html">
 | 
			
		||||
        <source>lines</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">17</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8838884664569764142" datatype="html">
 | 
			
		||||
        <source>Auto refresh</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">21</context>
 | 
			
		||||
          <context context-type="linenumber">8</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
			
		||||
@@ -709,11 +687,11 @@
 | 
			
		||||
        <source>Loading...</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">38</context>
 | 
			
		||||
          <context context-type="linenumber">24</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">53</context>
 | 
			
		||||
          <context context-type="linenumber">36</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
 | 
			
		||||
@@ -7281,25 +7259,25 @@
 | 
			
		||||
        <source>Print failed.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">1460</context>
 | 
			
		||||
          <context context-type="linenumber">1455</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6457245677384603573" datatype="html">
 | 
			
		||||
        <source>Error loading document for printing.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">1472</context>
 | 
			
		||||
          <context context-type="linenumber">1463</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="6085793215710522488" datatype="html">
 | 
			
		||||
        <source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">1537</context>
 | 
			
		||||
          <context context-type="linenumber">1528</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">1541</context>
 | 
			
		||||
          <context context-type="linenumber">1532</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="4958946940233632319" datatype="html">
 | 
			
		||||
@@ -7903,6 +7881,17 @@
 | 
			
		||||
          <context context-type="linenumber">45</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="8461842260159597706" datatype="html">
 | 
			
		||||
        <source>Show</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">37</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">52</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="5146398958364876914" datatype="html">
 | 
			
		||||
        <source>Sort</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,7 @@
 | 
			
		||||
    "@angular/cli": "~20.3.3",
 | 
			
		||||
    "@angular/compiler-cli": "~20.3.2",
 | 
			
		||||
    "@codecov/webpack-plugin": "^1.9.1",
 | 
			
		||||
    "@playwright/test": "^1.56.1",
 | 
			
		||||
    "@playwright/test": "^1.55.1",
 | 
			
		||||
    "@types/jest": "^30.0.0",
 | 
			
		||||
    "@types/node": "^24.6.1",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^8.45.0",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								src-ui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										26
									
								
								src-ui/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -130,8 +130,8 @@ importers:
 | 
			
		||||
        specifier: ^1.9.1
 | 
			
		||||
        version: 1.9.1(webpack@5.102.0)
 | 
			
		||||
      '@playwright/test':
 | 
			
		||||
        specifier: ^1.56.1
 | 
			
		||||
        version: 1.56.1
 | 
			
		||||
        specifier: ^1.55.1
 | 
			
		||||
        version: 1.55.1
 | 
			
		||||
      '@types/jest':
 | 
			
		||||
        specifier: ^30.0.0
 | 
			
		||||
        version: 30.0.0
 | 
			
		||||
@@ -2571,8 +2571,8 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
 | 
			
		||||
    engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
 | 
			
		||||
 | 
			
		||||
  '@playwright/test@1.56.1':
 | 
			
		||||
    resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==}
 | 
			
		||||
  '@playwright/test@1.55.1':
 | 
			
		||||
    resolution: {integrity: sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==}
 | 
			
		||||
    engines: {node: '>=18'}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
 | 
			
		||||
@@ -5711,13 +5711,13 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
 | 
			
		||||
    engines: {node: '>=8'}
 | 
			
		||||
 | 
			
		||||
  playwright-core@1.56.1:
 | 
			
		||||
    resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==}
 | 
			
		||||
  playwright-core@1.55.1:
 | 
			
		||||
    resolution: {integrity: sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==}
 | 
			
		||||
    engines: {node: '>=18'}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
 | 
			
		||||
  playwright@1.56.1:
 | 
			
		||||
    resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==}
 | 
			
		||||
  playwright@1.55.1:
 | 
			
		||||
    resolution: {integrity: sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==}
 | 
			
		||||
    engines: {node: '>=18'}
 | 
			
		||||
    hasBin: true
 | 
			
		||||
 | 
			
		||||
@@ -9632,9 +9632,9 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  '@pkgr/core@0.2.9': {}
 | 
			
		||||
 | 
			
		||||
  '@playwright/test@1.56.1':
 | 
			
		||||
  '@playwright/test@1.55.1':
 | 
			
		||||
    dependencies:
 | 
			
		||||
      playwright: 1.56.1
 | 
			
		||||
      playwright: 1.55.1
 | 
			
		||||
 | 
			
		||||
  '@popperjs/core@2.11.8': {}
 | 
			
		||||
 | 
			
		||||
@@ -13208,11 +13208,11 @@ snapshots:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      find-up: 4.1.0
 | 
			
		||||
 | 
			
		||||
  playwright-core@1.56.1: {}
 | 
			
		||||
  playwright-core@1.55.1: {}
 | 
			
		||||
 | 
			
		||||
  playwright@1.56.1:
 | 
			
		||||
  playwright@1.55.1:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      playwright-core: 1.56.1
 | 
			
		||||
      playwright-core: 1.55.1
 | 
			
		||||
    optionalDependencies:
 | 
			
		||||
      fsevents: 2.3.2
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,23 +3,9 @@
 | 
			
		||||
  i18n-title
 | 
			
		||||
  info="Review the log files for the application and for email checking."
 | 
			
		||||
  i18n-info>
 | 
			
		||||
  <div class="input-group input-group-sm align-items-center">
 | 
			
		||||
    <div class="input-group input-group-sm me-3">
 | 
			
		||||
      <span class="input-group-text text-muted" i18n>Show</span>
 | 
			
		||||
      <input
 | 
			
		||||
        class="form-control"
 | 
			
		||||
        type="number"
 | 
			
		||||
        min="100"
 | 
			
		||||
        step="100"
 | 
			
		||||
        [(ngModel)]="limit"
 | 
			
		||||
        (ngModelChange)="onLimitChange($event)"
 | 
			
		||||
        style="width: 100px;">
 | 
			
		||||
      <span class="input-group-text text-muted" i18n>lines</span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="form-check form-switch mt-1">
 | 
			
		||||
      <input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
 | 
			
		||||
      <label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
 | 
			
		||||
    </div>
 | 
			
		||||
  <div class="form-check form-switch">
 | 
			
		||||
    <input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
 | 
			
		||||
    <label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
 | 
			
		||||
  </div>
 | 
			
		||||
</pngx-page-header>
 | 
			
		||||
 | 
			
		||||
@@ -43,19 +29,14 @@
 | 
			
		||||
 | 
			
		||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
 | 
			
		||||
 | 
			
		||||
<cdk-virtual-scroll-viewport
 | 
			
		||||
  itemSize="20"
 | 
			
		||||
  class="bg-dark p-3 text-light font-monospace log-container"
 | 
			
		||||
  #logContainer>
 | 
			
		||||
<div class="bg-dark p-3 text-light font-monospace log-container" #logContainer>
 | 
			
		||||
  @if (loading && logFiles.length) {
 | 
			
		||||
    <div>
 | 
			
		||||
      <div class="spinner-border spinner-border-sm me-2" role="status"></div>
 | 
			
		||||
      <ng-container i18n>Loading...</ng-container>
 | 
			
		||||
    </div>
 | 
			
		||||
  }
 | 
			
		||||
  <p *cdkVirtualFor="let log of logs"
 | 
			
		||||
     class="m-0 p-0"
 | 
			
		||||
     [ngClass]="'log-entry-' + log.level">
 | 
			
		||||
    {{log.message}}
 | 
			
		||||
  </p>
 | 
			
		||||
</cdk-virtual-scroll-viewport>
 | 
			
		||||
  @for (log of logs; track $index) {
 | 
			
		||||
    <p class="m-0 p-0 log-entry-{{getLogLevel(log)}}">{{log}}</p>
 | 
			
		||||
  }
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@
 | 
			
		||||
.log-container {
 | 
			
		||||
  overflow-y: scroll;
 | 
			
		||||
  height: calc(100vh - 200px);
 | 
			
		||||
  top: 0;
 | 
			
		||||
  top: 70px;
 | 
			
		||||
 | 
			
		||||
  p {
 | 
			
		||||
    white-space: pre-wrap;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,3 @@
 | 
			
		||||
import {
 | 
			
		||||
  CdkVirtualScrollViewport,
 | 
			
		||||
  ScrollingModule,
 | 
			
		||||
} from '@angular/cdk/scrolling'
 | 
			
		||||
import { CommonModule } from '@angular/common'
 | 
			
		||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
 | 
			
		||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
 | 
			
		||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
 | 
			
		||||
@@ -43,9 +38,6 @@ describe('LogsComponent', () => {
 | 
			
		||||
        NgxBootstrapIconsModule.pick(allIcons),
 | 
			
		||||
        LogsComponent,
 | 
			
		||||
        PageHeaderComponent,
 | 
			
		||||
        CommonModule,
 | 
			
		||||
        CdkVirtualScrollViewport,
 | 
			
		||||
        ScrollingModule,
 | 
			
		||||
      ],
 | 
			
		||||
      providers: [
 | 
			
		||||
        provideHttpClient(withInterceptorsFromDi()),
 | 
			
		||||
@@ -62,12 +54,13 @@ describe('LogsComponent', () => {
 | 
			
		||||
    fixture = TestBed.createComponent(LogsComponent)
 | 
			
		||||
    component = fixture.componentInstance
 | 
			
		||||
    reloadSpy = jest.spyOn(component, 'reloadLogs')
 | 
			
		||||
    window.HTMLElement.prototype.scroll = function () {} // mock scroll
 | 
			
		||||
    jest.useFakeTimers()
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should display logs with first log initially', () => {
 | 
			
		||||
    expect(logSpy).toHaveBeenCalledWith('paperless', 5000)
 | 
			
		||||
    expect(logSpy).toHaveBeenCalledWith('paperless')
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    expect(fixture.debugElement.nativeElement.textContent).toContain(
 | 
			
		||||
      paperless_logs[0]
 | 
			
		||||
@@ -78,7 +71,7 @@ describe('LogsComponent', () => {
 | 
			
		||||
    fixture.debugElement
 | 
			
		||||
      .queryAll(By.directive(NgbNavLink))[1]
 | 
			
		||||
      .nativeElement.dispatchEvent(new MouseEvent('click'))
 | 
			
		||||
    expect(logSpy).toHaveBeenCalledWith('mail', 5000)
 | 
			
		||||
    expect(logSpy).toHaveBeenCalledWith('mail')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should handle error with no logs', () => {
 | 
			
		||||
@@ -90,10 +83,6 @@ describe('LogsComponent', () => {
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should auto refresh, allow toggle', () => {
 | 
			
		||||
    jest
 | 
			
		||||
      .spyOn(CdkVirtualScrollViewport.prototype, 'scrollToIndex')
 | 
			
		||||
      .mockImplementation(() => undefined)
 | 
			
		||||
 | 
			
		||||
    jest.advanceTimersByTime(6000)
 | 
			
		||||
    expect(reloadSpy).toHaveBeenCalledTimes(2)
 | 
			
		||||
 | 
			
		||||
@@ -101,13 +90,4 @@ describe('LogsComponent', () => {
 | 
			
		||||
    jest.advanceTimersByTime(6000)
 | 
			
		||||
    expect(reloadSpy).toHaveBeenCalledTimes(2)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should debounce limit changes before reloading logs', () => {
 | 
			
		||||
    const initialCalls = reloadSpy.mock.calls.length
 | 
			
		||||
    component.onLimitChange(6000)
 | 
			
		||||
    jest.advanceTimersByTime(299)
 | 
			
		||||
    expect(reloadSpy).toHaveBeenCalledTimes(initialCalls)
 | 
			
		||||
    jest.advanceTimersByTime(1)
 | 
			
		||||
    expect(reloadSpy).toHaveBeenCalledTimes(initialCalls + 1)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,7 @@
 | 
			
		||||
import {
 | 
			
		||||
  CdkVirtualScrollViewport,
 | 
			
		||||
  ScrollingModule,
 | 
			
		||||
} from '@angular/cdk/scrolling'
 | 
			
		||||
import { CommonModule } from '@angular/common'
 | 
			
		||||
import {
 | 
			
		||||
  ChangeDetectorRef,
 | 
			
		||||
  Component,
 | 
			
		||||
  ElementRef,
 | 
			
		||||
  OnDestroy,
 | 
			
		||||
  OnInit,
 | 
			
		||||
  ViewChild,
 | 
			
		||||
@@ -13,7 +9,7 @@ import {
 | 
			
		||||
} from '@angular/core'
 | 
			
		||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
 | 
			
		||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { Subject, debounceTime, filter, takeUntil, timer } from 'rxjs'
 | 
			
		||||
import { filter, takeUntil, timer } from 'rxjs'
 | 
			
		||||
import { LogService } from 'src/app/services/rest/log.service'
 | 
			
		||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
 | 
			
		||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
 | 
			
		||||
@@ -25,11 +21,8 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
 | 
			
		||||
  imports: [
 | 
			
		||||
    PageHeaderComponent,
 | 
			
		||||
    NgbNavModule,
 | 
			
		||||
    CommonModule,
 | 
			
		||||
    FormsModule,
 | 
			
		||||
    ReactiveFormsModule,
 | 
			
		||||
    CdkVirtualScrollViewport,
 | 
			
		||||
    ScrollingModule,
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
export class LogsComponent
 | 
			
		||||
@@ -39,7 +32,7 @@ export class LogsComponent
 | 
			
		||||
  private logService = inject(LogService)
 | 
			
		||||
  private changedetectorRef = inject(ChangeDetectorRef)
 | 
			
		||||
 | 
			
		||||
  public logs: Array<{ message: string; level: number }> = []
 | 
			
		||||
  public logs: string[] = []
 | 
			
		||||
 | 
			
		||||
  public logFiles: string[] = []
 | 
			
		||||
 | 
			
		||||
@@ -47,17 +40,9 @@ export class LogsComponent
 | 
			
		||||
 | 
			
		||||
  public autoRefreshEnabled: boolean = true
 | 
			
		||||
 | 
			
		||||
  public limit: number = 5000
 | 
			
		||||
 | 
			
		||||
  private readonly limitChange$ = new Subject<number>()
 | 
			
		||||
 | 
			
		||||
  @ViewChild('logContainer') logContainer: CdkVirtualScrollViewport
 | 
			
		||||
  @ViewChild('logContainer') logContainer: ElementRef
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.limitChange$
 | 
			
		||||
      .pipe(debounceTime(300), takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
      .subscribe(() => this.reloadLogs())
 | 
			
		||||
 | 
			
		||||
    this.logService
 | 
			
		||||
      .list()
 | 
			
		||||
      .pipe(takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
@@ -83,33 +68,16 @@ export class LogsComponent
 | 
			
		||||
    super.ngOnDestroy()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onLimitChange(limit: number): void {
 | 
			
		||||
    this.limitChange$.next(limit)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  reloadLogs() {
 | 
			
		||||
    this.loading = true
 | 
			
		||||
    this.logService
 | 
			
		||||
      .get(this.activeLog, this.limit)
 | 
			
		||||
      .get(this.activeLog)
 | 
			
		||||
      .pipe(takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
      .subscribe({
 | 
			
		||||
        next: (result) => {
 | 
			
		||||
          this.logs = result
 | 
			
		||||
          this.loading = false
 | 
			
		||||
          const parsed = this.parseLogsWithLevel(result)
 | 
			
		||||
          const hasChanges =
 | 
			
		||||
            parsed.length !== this.logs.length ||
 | 
			
		||||
            parsed.some((log, idx) => {
 | 
			
		||||
              const current = this.logs[idx]
 | 
			
		||||
              return (
 | 
			
		||||
                !current ||
 | 
			
		||||
                current.message !== log.message ||
 | 
			
		||||
                current.level !== log.level
 | 
			
		||||
              )
 | 
			
		||||
            })
 | 
			
		||||
          if (hasChanges) {
 | 
			
		||||
            this.logs = parsed
 | 
			
		||||
            this.scrollToBottom()
 | 
			
		||||
          }
 | 
			
		||||
          this.scrollToBottom()
 | 
			
		||||
        },
 | 
			
		||||
        error: () => {
 | 
			
		||||
          this.logs = []
 | 
			
		||||
@@ -132,19 +100,12 @@ export class LogsComponent
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private parseLogsWithLevel(
 | 
			
		||||
    logs: string[]
 | 
			
		||||
  ): Array<{ message: string; level: number }> {
 | 
			
		||||
    return logs.map((log) => ({
 | 
			
		||||
      message: log,
 | 
			
		||||
      level: this.getLogLevel(log),
 | 
			
		||||
    }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  scrollToBottom(): void {
 | 
			
		||||
    this.changedetectorRef.detectChanges()
 | 
			
		||||
    if (this.logContainer) {
 | 
			
		||||
      this.logContainer.scrollToIndex(this.logs.length - 1)
 | 
			
		||||
    }
 | 
			
		||||
    this.logContainer?.nativeElement.scroll({
 | 
			
		||||
      top: this.logContainer.nativeElement.scrollHeight,
 | 
			
		||||
      left: 0,
 | 
			
		||||
      behavior: 'auto',
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1489,8 +1489,6 @@ describe('DocumentDetailComponent', () => {
 | 
			
		||||
      mockContentWindow.onafterprint(new Event('afterprint'))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    tick(500)
 | 
			
		||||
 | 
			
		||||
    expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
 | 
			
		||||
    expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
 | 
			
		||||
 | 
			
		||||
@@ -1514,97 +1512,65 @@ describe('DocumentDetailComponent', () => {
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const iframePrintErrorCases: Array<{
 | 
			
		||||
    description: string
 | 
			
		||||
    thrownError: Error
 | 
			
		||||
    expectToast: boolean
 | 
			
		||||
  }> = [
 | 
			
		||||
    {
 | 
			
		||||
      description: 'should show error toast if printing throws inside iframe',
 | 
			
		||||
      thrownError: new Error('focus failed'),
 | 
			
		||||
      expectToast: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      description:
 | 
			
		||||
        'should suppress toast if cross-origin afterprint error occurs',
 | 
			
		||||
      thrownError: new DOMException(
 | 
			
		||||
        'Accessing onafterprint triggered a cross-origin violation',
 | 
			
		||||
        'SecurityError'
 | 
			
		||||
      ),
 | 
			
		||||
      expectToast: false,
 | 
			
		||||
    },
 | 
			
		||||
  ]
 | 
			
		||||
  it('should show error toast if printing throws inside iframe', fakeAsync(() => {
 | 
			
		||||
    initNormally()
 | 
			
		||||
 | 
			
		||||
  iframePrintErrorCases.forEach(({ description, thrownError, expectToast }) => {
 | 
			
		||||
    it(
 | 
			
		||||
      description,
 | 
			
		||||
      fakeAsync(() => {
 | 
			
		||||
        initNormally()
 | 
			
		||||
    const appendChildSpy = jest
 | 
			
		||||
      .spyOn(document.body, 'appendChild')
 | 
			
		||||
      .mockImplementation((node: Node) => node)
 | 
			
		||||
    const removeChildSpy = jest
 | 
			
		||||
      .spyOn(document.body, 'removeChild')
 | 
			
		||||
      .mockImplementation((node: Node) => node)
 | 
			
		||||
    const createObjectURLSpy = jest
 | 
			
		||||
      .spyOn(URL, 'createObjectURL')
 | 
			
		||||
      .mockReturnValue('blob:mock-url')
 | 
			
		||||
    const revokeObjectURLSpy = jest
 | 
			
		||||
      .spyOn(URL, 'revokeObjectURL')
 | 
			
		||||
      .mockImplementation(() => {})
 | 
			
		||||
 | 
			
		||||
        const appendChildSpy = jest
 | 
			
		||||
          .spyOn(document.body, 'appendChild')
 | 
			
		||||
          .mockImplementation((node: Node) => node)
 | 
			
		||||
        const removeChildSpy = jest
 | 
			
		||||
          .spyOn(document.body, 'removeChild')
 | 
			
		||||
          .mockImplementation((node: Node) => node)
 | 
			
		||||
        const createObjectURLSpy = jest
 | 
			
		||||
          .spyOn(URL, 'createObjectURL')
 | 
			
		||||
          .mockReturnValue('blob:mock-url')
 | 
			
		||||
        const revokeObjectURLSpy = jest
 | 
			
		||||
          .spyOn(URL, 'revokeObjectURL')
 | 
			
		||||
          .mockImplementation(() => {})
 | 
			
		||||
    const toastSpy = jest.spyOn(toastService, 'showError')
 | 
			
		||||
 | 
			
		||||
        const toastSpy = jest.spyOn(toastService, 'showError')
 | 
			
		||||
    const mockContentWindow = {
 | 
			
		||||
      focus: jest.fn().mockImplementation(() => {
 | 
			
		||||
        throw new Error('focus failed')
 | 
			
		||||
      }),
 | 
			
		||||
      print: jest.fn(),
 | 
			
		||||
      onafterprint: null,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        const mockContentWindow = {
 | 
			
		||||
          focus: jest.fn().mockImplementation(() => {
 | 
			
		||||
            throw thrownError
 | 
			
		||||
          }),
 | 
			
		||||
          print: jest.fn(),
 | 
			
		||||
          onafterprint: null,
 | 
			
		||||
        }
 | 
			
		||||
    const mockIframe: any = {
 | 
			
		||||
      style: {},
 | 
			
		||||
      src: '',
 | 
			
		||||
      onload: null,
 | 
			
		||||
      contentWindow: mockContentWindow,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        const mockIframe: any = {
 | 
			
		||||
          style: {},
 | 
			
		||||
          src: '',
 | 
			
		||||
          onload: null,
 | 
			
		||||
          contentWindow: mockContentWindow,
 | 
			
		||||
        }
 | 
			
		||||
    const createElementSpy = jest
 | 
			
		||||
      .spyOn(document, 'createElement')
 | 
			
		||||
      .mockReturnValue(mockIframe as any)
 | 
			
		||||
 | 
			
		||||
        const createElementSpy = jest
 | 
			
		||||
          .spyOn(document, 'createElement')
 | 
			
		||||
          .mockReturnValue(mockIframe as any)
 | 
			
		||||
    const blob = new Blob(['test'], { type: 'application/pdf' })
 | 
			
		||||
    component.printDocument()
 | 
			
		||||
 | 
			
		||||
        const blob = new Blob(['test'], { type: 'application/pdf' })
 | 
			
		||||
        component.printDocument()
 | 
			
		||||
 | 
			
		||||
        const req = httpTestingController.expectOne(
 | 
			
		||||
          `${environment.apiBaseUrl}documents/${doc.id}/download/`
 | 
			
		||||
        )
 | 
			
		||||
        req.flush(blob)
 | 
			
		||||
 | 
			
		||||
        tick()
 | 
			
		||||
 | 
			
		||||
        if (mockIframe.onload) {
 | 
			
		||||
          mockIframe.onload(new Event('load'))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        tick(200)
 | 
			
		||||
 | 
			
		||||
        if (expectToast) {
 | 
			
		||||
          expect(toastSpy).toHaveBeenCalled()
 | 
			
		||||
        } else {
 | 
			
		||||
          expect(toastSpy).not.toHaveBeenCalled()
 | 
			
		||||
        }
 | 
			
		||||
        expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
 | 
			
		||||
        expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
 | 
			
		||||
 | 
			
		||||
        createElementSpy.mockRestore()
 | 
			
		||||
        appendChildSpy.mockRestore()
 | 
			
		||||
        removeChildSpy.mockRestore()
 | 
			
		||||
        createObjectURLSpy.mockRestore()
 | 
			
		||||
        revokeObjectURLSpy.mockRestore()
 | 
			
		||||
      })
 | 
			
		||||
    const req = httpTestingController.expectOne(
 | 
			
		||||
      `${environment.apiBaseUrl}documents/${doc.id}/download/`
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
    req.flush(blob)
 | 
			
		||||
 | 
			
		||||
    tick()
 | 
			
		||||
 | 
			
		||||
    if (mockIframe.onload) {
 | 
			
		||||
      mockIframe.onload(new Event('load'))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    expect(toastSpy).toHaveBeenCalled()
 | 
			
		||||
    expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
 | 
			
		||||
    expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
 | 
			
		||||
 | 
			
		||||
    createElementSpy.mockRestore()
 | 
			
		||||
    appendChildSpy.mockRestore()
 | 
			
		||||
    removeChildSpy.mockRestore()
 | 
			
		||||
    createObjectURLSpy.mockRestore()
 | 
			
		||||
    revokeObjectURLSpy.mockRestore()
 | 
			
		||||
  }))
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
 | 
			
		||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
 | 
			
		||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
 | 
			
		||||
import { DeviceDetectorService } from 'ngx-device-detector'
 | 
			
		||||
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs'
 | 
			
		||||
import { BehaviorSubject, Observable, of, Subject } from 'rxjs'
 | 
			
		||||
import {
 | 
			
		||||
  catchError,
 | 
			
		||||
  debounceTime,
 | 
			
		||||
@@ -1452,18 +1452,9 @@ export class DocumentDetailComponent
 | 
			
		||||
                URL.revokeObjectURL(blobUrl)
 | 
			
		||||
              }
 | 
			
		||||
            } catch (err) {
 | 
			
		||||
              // FF throws cross-origin error on onafterprint
 | 
			
		||||
              const isCrossOriginAfterPrintError =
 | 
			
		||||
                err instanceof DOMException &&
 | 
			
		||||
                err.message.includes('onafterprint')
 | 
			
		||||
              if (!isCrossOriginAfterPrintError) {
 | 
			
		||||
                this.toastService.showError($localize`Print failed.`, err)
 | 
			
		||||
              }
 | 
			
		||||
              timer(100).subscribe(() => {
 | 
			
		||||
                // delay to avoid FF print failure
 | 
			
		||||
                document.body.removeChild(iframe)
 | 
			
		||||
                URL.revokeObjectURL(blobUrl)
 | 
			
		||||
              })
 | 
			
		||||
              this.toastService.showError($localize`Print failed.`, err)
 | 
			
		||||
              document.body.removeChild(iframe)
 | 
			
		||||
              URL.revokeObjectURL(blobUrl)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@
 | 
			
		||||
              </td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <ng-template #errorPopover>
 | 
			
		||||
                  <pre class="small">
 | 
			
		||||
                  <pre class="small text-light">
 | 
			
		||||
                    {{ mail.error }}
 | 
			
		||||
                  </pre>
 | 
			
		||||
                </ng-template>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,5 @@
 | 
			
		||||
::ng-deep .popover {
 | 
			
		||||
    max-width: 350px;
 | 
			
		||||
    max-height: 600px;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
 | 
			
		||||
    pre {
 | 
			
		||||
        white-space: pre-wrap;
 | 
			
		||||
 
 | 
			
		||||
@@ -73,14 +73,9 @@ describe('TagListComponent', () => {
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should omit matching children from top level when their parent is present', () => {
 | 
			
		||||
  it('should filter out child tags if name filter is empty, otherwise show all', () => {
 | 
			
		||||
    const tags = [
 | 
			
		||||
      {
 | 
			
		||||
        id: 1,
 | 
			
		||||
        name: 'Tag1',
 | 
			
		||||
        parent: null,
 | 
			
		||||
        children: [{ id: 2, name: 'Tag2', parent: 1 }],
 | 
			
		||||
      },
 | 
			
		||||
      { id: 1, name: 'Tag1', parent: null },
 | 
			
		||||
      { id: 2, name: 'Tag2', parent: 1 },
 | 
			
		||||
      { id: 3, name: 'Tag3', parent: null },
 | 
			
		||||
    ]
 | 
			
		||||
@@ -91,13 +86,7 @@ describe('TagListComponent', () => {
 | 
			
		||||
 | 
			
		||||
    component['_nameFilter'] = 'Tag2' // Simulate non-empty name filter
 | 
			
		||||
    const filteredWithName = component.filterData(tags as any)
 | 
			
		||||
    expect(filteredWithName.length).toBe(2)
 | 
			
		||||
    expect(filteredWithName.find((t) => t.id === 2)).toBeUndefined()
 | 
			
		||||
    expect(
 | 
			
		||||
      filteredWithName
 | 
			
		||||
        .find((t) => t.id === 1)
 | 
			
		||||
        ?.children?.some((c) => c.id === 2)
 | 
			
		||||
    ).toBe(true)
 | 
			
		||||
    expect(filteredWithName.length).toBe(3)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should request only parent tags when no name filter is applied', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -69,13 +69,9 @@ export class TagListComponent extends ManagementListComponent<Tag> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  filterData(data: Tag[]) {
 | 
			
		||||
    if (!this.nameFilter?.length) {
 | 
			
		||||
      return data.filter((tag) => !tag.parent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // When filtering by name, exclude children if their parent is also present
 | 
			
		||||
    const availableIds = new Set(data.map((tag) => tag.id))
 | 
			
		||||
    return data.filter((tag) => !tag.parent || !availableIds.has(tag.parent))
 | 
			
		||||
    return this.nameFilter?.length
 | 
			
		||||
      ? [...data]
 | 
			
		||||
      : data.filter((tag) => !tag.parent)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected override getSelectableIDs(tags: Tag[]): number[] {
 | 
			
		||||
 
 | 
			
		||||
@@ -49,14 +49,4 @@ describe('LogService', () => {
 | 
			
		||||
    )
 | 
			
		||||
    expect(req.request.method).toEqual('GET')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should pass limit param on logs get when provided', () => {
 | 
			
		||||
    const id: string = 'mail'
 | 
			
		||||
    const limit: number = 100
 | 
			
		||||
    subscription = service.get(id, limit).subscribe()
 | 
			
		||||
    const req = httpTestingController.expectOne(
 | 
			
		||||
      `${environment.apiBaseUrl}${endpoint}/${id}/?limit=${limit}`
 | 
			
		||||
    )
 | 
			
		||||
    expect(req.request.method).toEqual('GET')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { HttpClient, HttpParams } from '@angular/common/http'
 | 
			
		||||
import { HttpClient } from '@angular/common/http'
 | 
			
		||||
import { Injectable, inject } from '@angular/core'
 | 
			
		||||
import { Observable } from 'rxjs'
 | 
			
		||||
import { environment } from 'src/environments/environment'
 | 
			
		||||
@@ -13,13 +13,7 @@ export class LogService {
 | 
			
		||||
    return this.http.get<string[]>(`${environment.apiBaseUrl}logs/`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get(id: string, limit?: number): Observable<string[]> {
 | 
			
		||||
    let params = new HttpParams()
 | 
			
		||||
    if (limit !== undefined) {
 | 
			
		||||
      params = params.set('limit', limit.toString())
 | 
			
		||||
    }
 | 
			
		||||
    return this.http.get<string[]>(`${environment.apiBaseUrl}logs/${id}/`, {
 | 
			
		||||
      params,
 | 
			
		||||
    })
 | 
			
		||||
  get(id: string): Observable<string[]> {
 | 
			
		||||
    return this.http.get<string[]>(`${environment.apiBaseUrl}logs/${id}/`)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2250,23 +2250,6 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
 | 
			
		||||
        self.assertEqual(response.status_code, status.HTTP_200_OK)
 | 
			
		||||
        self.assertListEqual(response.data, ["test", "test2"])
 | 
			
		||||
 | 
			
		||||
    def test_get_log_with_limit(self):
 | 
			
		||||
        log_data = "test1\ntest2\ntest3\n"
 | 
			
		||||
        with (Path(settings.LOGGING_DIR) / "paperless.log").open("w") as f:
 | 
			
		||||
            f.write(log_data)
 | 
			
		||||
        response = self.client.get("/api/logs/paperless/", {"limit": 2})
 | 
			
		||||
        self.assertEqual(response.status_code, status.HTTP_200_OK)
 | 
			
		||||
        self.assertListEqual(response.data, ["test2", "test3"])
 | 
			
		||||
 | 
			
		||||
    def test_get_log_with_invalid_limit(self):
 | 
			
		||||
        log_data = "test1\ntest2\n"
 | 
			
		||||
        with (Path(settings.LOGGING_DIR) / "paperless.log").open("w") as f:
 | 
			
		||||
            f.write(log_data)
 | 
			
		||||
        response = self.client.get("/api/logs/paperless/", {"limit": "abc"})
 | 
			
		||||
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 | 
			
		||||
        response = self.client.get("/api/logs/paperless/", {"limit": -5})
 | 
			
		||||
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 | 
			
		||||
 | 
			
		||||
    def test_invalid_regex_other_algorithm(self):
 | 
			
		||||
        for endpoint in ["correspondents", "tags", "document_types"]:
 | 
			
		||||
            response = self.client.post(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from pytest_httpx import HTTPXMock
 | 
			
		||||
from rest_framework import status
 | 
			
		||||
from rest_framework.test import APIClient
 | 
			
		||||
@@ -9,9 +8,6 @@ from paperless import version
 | 
			
		||||
class TestApiRemoteVersion:
 | 
			
		||||
    ENDPOINT = "/api/remote_version/"
 | 
			
		||||
 | 
			
		||||
    def setup_method(self):
 | 
			
		||||
        cache.clear()
 | 
			
		||||
 | 
			
		||||
    def test_remote_version_enabled_no_update_prefix(
 | 
			
		||||
        self,
 | 
			
		||||
        rest_api_client: APIClient,
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ import re
 | 
			
		||||
import tempfile
 | 
			
		||||
import zipfile
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
from collections import deque
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from time import mktime
 | 
			
		||||
@@ -51,7 +50,6 @@ from django.utils.timezone import make_aware
 | 
			
		||||
from django.utils.translation import get_language
 | 
			
		||||
from django.views import View
 | 
			
		||||
from django.views.decorators.cache import cache_control
 | 
			
		||||
from django.views.decorators.cache import cache_page
 | 
			
		||||
from django.views.decorators.http import condition
 | 
			
		||||
from django.views.decorators.http import last_modified
 | 
			
		||||
from django.views.generic import TemplateView
 | 
			
		||||
@@ -71,7 +69,6 @@ from rest_framework import parsers
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
from rest_framework.decorators import action
 | 
			
		||||
from rest_framework.exceptions import NotFound
 | 
			
		||||
from rest_framework.exceptions import ValidationError
 | 
			
		||||
from rest_framework.filters import OrderingFilter
 | 
			
		||||
from rest_framework.filters import SearchFilter
 | 
			
		||||
from rest_framework.generics import GenericAPIView
 | 
			
		||||
@@ -1365,13 +1362,6 @@ class UnifiedSearchViewSet(DocumentViewSet):
 | 
			
		||||
                type=OpenApiTypes.STR,
 | 
			
		||||
                location=OpenApiParameter.PATH,
 | 
			
		||||
            ),
 | 
			
		||||
            OpenApiParameter(
 | 
			
		||||
                name="limit",
 | 
			
		||||
                type=OpenApiTypes.INT,
 | 
			
		||||
                location=OpenApiParameter.QUERY,
 | 
			
		||||
                description="Return only the last N entries from the log file",
 | 
			
		||||
                required=False,
 | 
			
		||||
            ),
 | 
			
		||||
        ],
 | 
			
		||||
        responses={
 | 
			
		||||
            (200, "application/json"): serializers.ListSerializer(
 | 
			
		||||
@@ -1403,22 +1393,8 @@ class LogViewSet(ViewSet):
 | 
			
		||||
        if not log_file.is_file():
 | 
			
		||||
            raise Http404
 | 
			
		||||
 | 
			
		||||
        limit_param = request.query_params.get("limit")
 | 
			
		||||
        if limit_param is not None:
 | 
			
		||||
            try:
 | 
			
		||||
                limit = int(limit_param)
 | 
			
		||||
            except (TypeError, ValueError):
 | 
			
		||||
                raise ValidationError({"limit": "Must be a positive integer"})
 | 
			
		||||
            if limit < 1:
 | 
			
		||||
                raise ValidationError({"limit": "Must be a positive integer"})
 | 
			
		||||
        else:
 | 
			
		||||
            limit = None
 | 
			
		||||
 | 
			
		||||
        with log_file.open() as f:
 | 
			
		||||
            if limit is None:
 | 
			
		||||
                lines = [line.rstrip() for line in f.readlines()]
 | 
			
		||||
            else:
 | 
			
		||||
                lines = [line.rstrip() for line in deque(f, maxlen=limit)]
 | 
			
		||||
            lines = [line.rstrip() for line in f.readlines()]
 | 
			
		||||
 | 
			
		||||
        return Response(lines)
 | 
			
		||||
 | 
			
		||||
@@ -2426,7 +2402,6 @@ class UiSettingsView(GenericAPIView):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@method_decorator(cache_page(60 * 15), name="dispatch")
 | 
			
		||||
@extend_schema_view(
 | 
			
		||||
    get=extend_schema(
 | 
			
		||||
        description="Get the current version of the Paperless-NGX server",
 | 
			
		||||
 
 | 
			
		||||
@@ -54,8 +54,8 @@ class TestCustomAccountAdapter(TestCase):
 | 
			
		||||
            # False because request host is not in allowed hosts
 | 
			
		||||
            self.assertFalse(adapter.is_safe_url(url))
 | 
			
		||||
 | 
			
		||||
    @mock.patch("allauth.core.ratelimit._consume_rate", return_value=True)
 | 
			
		||||
    def test_pre_authenticate(self, mock_consume_rate):
 | 
			
		||||
    @mock.patch("allauth.core.internal.ratelimit.consume", return_value=True)
 | 
			
		||||
    def test_pre_authenticate(self, mock_consume):
 | 
			
		||||
        adapter = get_adapter()
 | 
			
		||||
        request = HttpRequest()
 | 
			
		||||
        request.get_host = mock.Mock(return_value="example.com")
 | 
			
		||||
 
 | 
			
		||||
@@ -53,15 +53,6 @@ class TestUrlCanary:
 | 
			
		||||
    Verify certain URLs are still available so testing is valid still
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    # Wikimedia rejects requests without a browser-like User-Agent header and returns 403.
 | 
			
		||||
    _WIKIMEDIA_HEADERS = {
 | 
			
		||||
        "User-Agent": (
 | 
			
		||||
            "Mozilla/5.0 (X11; Linux x86_64) "
 | 
			
		||||
            "AppleWebKit/537.36 (KHTML, like Gecko) "
 | 
			
		||||
            "Chrome/123.0.0.0 Safari/537.36"
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def test_online_image_exception_on_not_available(self):
 | 
			
		||||
        """
 | 
			
		||||
        GIVEN:
 | 
			
		||||
@@ -79,7 +70,6 @@ class TestUrlCanary:
 | 
			
		||||
        with pytest.raises(httpx.HTTPStatusError) as exec_info:
 | 
			
		||||
            resp = httpx.get(
 | 
			
		||||
                "https://upload.wikimedia.org/wikipedia/en/f/f7/nonexistent.png",
 | 
			
		||||
                headers=self._WIKIMEDIA_HEADERS,
 | 
			
		||||
            )
 | 
			
		||||
            resp.raise_for_status()
 | 
			
		||||
 | 
			
		||||
@@ -100,10 +90,7 @@ class TestUrlCanary:
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        # Now check the URL used in samples/sample.html
 | 
			
		||||
        resp = httpx.get(
 | 
			
		||||
            "https://upload.wikimedia.org/wikipedia/en/f/f7/RickRoll.png",
 | 
			
		||||
            headers=self._WIKIMEDIA_HEADERS,
 | 
			
		||||
        )
 | 
			
		||||
        resp = httpx.get("https://upload.wikimedia.org/wikipedia/en/f/f7/RickRoll.png")
 | 
			
		||||
        resp.raise_for_status()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										21
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							@@ -689,13 +689,13 @@ wheels = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "django-allauth"
 | 
			
		||||
version = "65.4.1"
 | 
			
		||||
version = "65.12.1"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
dependencies = [
 | 
			
		||||
    { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
			
		||||
    { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/e7/b3232c27da9f43e3db72d16addd90891ee233fa058ddd0588bafcded2ea7/django_allauth-65.4.1.tar.gz", hash = "sha256:60b32aef7dbbcc213319aa4fd8f570e985266ea1162ae6ef7a26a24efca85c8c", size = 1558220, upload-time = "2025-02-07T09:35:18.359Z" }
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/52/94/75d7f8c59e061d1b66a6d917b287817fe02d2671c9e6376a4ddfb3954989/django_allauth-65.12.1.tar.gz", hash = "sha256:662666ff2d5c71766f66b1629ac7345c30796813221184e13e11ed7460940c6a", size = 1967971, upload-time = "2025-10-16T16:39:58.342Z" }
 | 
			
		||||
 | 
			
		||||
[package.optional-dependencies]
 | 
			
		||||
mfa = [
 | 
			
		||||
@@ -703,9 +703,9 @@ mfa = [
 | 
			
		||||
    { name = "qrcode", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
			
		||||
]
 | 
			
		||||
socialaccount = [
 | 
			
		||||
    { name = "oauthlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
			
		||||
    { name = "pyjwt", extra = ["crypto"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
			
		||||
    { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
			
		||||
    { name = "requests-oauthlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@@ -2262,7 +2262,7 @@ requires-dist = [
 | 
			
		||||
    { name = "concurrent-log-handler", specifier = "~=0.9.25" },
 | 
			
		||||
    { name = "dateparser", specifier = "~=1.2" },
 | 
			
		||||
    { name = "django", specifier = "~=5.2.5" },
 | 
			
		||||
    { name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.4.0" },
 | 
			
		||||
    { name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.12.1" },
 | 
			
		||||
    { name = "django-auditlog", specifier = "~=3.2.1" },
 | 
			
		||||
    { name = "django-cachalot", specifier = "~=2.8.0" },
 | 
			
		||||
    { name = "django-celery-results", specifier = "~=2.6.0" },
 | 
			
		||||
@@ -3379,19 +3379,6 @@ wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "requests-oauthlib"
 | 
			
		||||
version = "2.0.0"
 | 
			
		||||
source = { registry = "https://pypi.org/simple" }
 | 
			
		||||
dependencies = [
 | 
			
		||||
    { name = "oauthlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
			
		||||
    { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
			
		||||
]
 | 
			
		||||
sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" }
 | 
			
		||||
wheels = [
 | 
			
		||||
    { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rich"
 | 
			
		||||
version = "14.1.0"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user