Rotate
This commit is contained in:
parent
d6d0071175
commit
c9dd407cbe
@ -116,9 +116,11 @@ import { ConfirmButtonComponent } from './components/common/confirm-button/confi
|
|||||||
import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
|
import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
|
||||||
import { SystemStatusDialogComponent } from './components/common/system-status-dialog/system-status-dialog.component'
|
import { SystemStatusDialogComponent } from './components/common/system-status-dialog/system-status-dialog.component'
|
||||||
import { NgxFilesizeModule } from 'ngx-filesize'
|
import { NgxFilesizeModule } from 'ngx-filesize'
|
||||||
|
import { RotateConfirmDialogComponent } from './components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||||
import {
|
import {
|
||||||
airplane,
|
airplane,
|
||||||
archive,
|
archive,
|
||||||
|
arrowClockwise,
|
||||||
arrowCounterclockwise,
|
arrowCounterclockwise,
|
||||||
arrowDown,
|
arrowDown,
|
||||||
arrowLeft,
|
arrowLeft,
|
||||||
@ -127,6 +129,7 @@ import {
|
|||||||
arrowRightShort,
|
arrowRightShort,
|
||||||
arrowUpRight,
|
arrowUpRight,
|
||||||
asterisk,
|
asterisk,
|
||||||
|
bodyText,
|
||||||
boxArrowUp,
|
boxArrowUp,
|
||||||
boxArrowUpRight,
|
boxArrowUpRight,
|
||||||
boxes,
|
boxes,
|
||||||
@ -209,6 +212,7 @@ import {
|
|||||||
const icons = {
|
const icons = {
|
||||||
airplane,
|
airplane,
|
||||||
archive,
|
archive,
|
||||||
|
arrowClockwise,
|
||||||
arrowCounterclockwise,
|
arrowCounterclockwise,
|
||||||
arrowDown,
|
arrowDown,
|
||||||
arrowLeft,
|
arrowLeft,
|
||||||
@ -217,6 +221,7 @@ const icons = {
|
|||||||
arrowRightShort,
|
arrowRightShort,
|
||||||
arrowUpRight,
|
arrowUpRight,
|
||||||
asterisk,
|
asterisk,
|
||||||
|
bodyText,
|
||||||
boxArrowUp,
|
boxArrowUp,
|
||||||
boxArrowUpRight,
|
boxArrowUpRight,
|
||||||
boxes,
|
boxes,
|
||||||
@ -458,6 +463,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
ConfirmButtonComponent,
|
ConfirmButtonComponent,
|
||||||
MonetaryComponent,
|
MonetaryComponent,
|
||||||
SystemStatusDialogComponent,
|
SystemStatusDialogComponent,
|
||||||
|
RotateConfirmDialogComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3 d-flex justify-content-end">
|
||||||
|
<button class="btn btn-secondary mt-auto" (click)="rotate(false)">
|
||||||
|
<i-bs name="arrow-counterclockwise"></i-bs>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 d-flex align-items-center">
|
||||||
|
@if (documentID) {
|
||||||
|
<img class="w-50 m-auto" [ngStyle]="{'transform': 'rotate('+rotation+'deg)'}" [src]="documentService.getThumbUrl(documentID)" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="col-3 d-flex">
|
||||||
|
<button class="btn btn-secondary mt-auto" (click)="rotate()">
|
||||||
|
<i-bs name="arrow-clockwise"></i-bs>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col">
|
||||||
|
@if (messageBold) {
|
||||||
|
<p><b>{{messageBold}}</b></p>
|
||||||
|
}
|
||||||
|
@if (message) {
|
||||||
|
<p class="mb-0" [innerHTML]="message | safeHtml"></p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
||||||
|
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
||||||
|
<span>
|
||||||
|
{{btnCaption}}
|
||||||
|
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
|
||||||
|
</span>
|
||||||
|
@if (!confirmButtonEnabled) {
|
||||||
|
<ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
@if (alternativeBtnCaption) {
|
||||||
|
<button type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled">
|
||||||
|
{{alternativeBtnCaption}}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
@ -0,0 +1,3 @@
|
|||||||
|
img {
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { RotateConfirmDialogComponent } from './rotate-confirm-dialog.component'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
|
describe('RotateConfirmDialogComponent', () => {
|
||||||
|
let component: RotateConfirmDialogComponent
|
||||||
|
let fixture: ComponentFixture<RotateConfirmDialogComponent>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [RotateConfirmDialogComponent, SafeHtmlPipe],
|
||||||
|
providers: [NgbActiveModal, SafeHtmlPipe],
|
||||||
|
imports: [
|
||||||
|
HttpClientTestingModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(RotateConfirmDialogComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support rotating the image', () => {
|
||||||
|
component.documentID = 1
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.rotate()
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(component.degrees).toBe(90)
|
||||||
|
expect(fixture.nativeElement.querySelector('img').style.transform).toBe(
|
||||||
|
'rotate(90deg)'
|
||||||
|
)
|
||||||
|
component.rotate()
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(fixture.nativeElement.querySelector('img').style.transform).toBe(
|
||||||
|
'rotate(180deg)'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should normalize degrees', () => {
|
||||||
|
expect(component.degrees).toBe(0)
|
||||||
|
component.rotate()
|
||||||
|
expect(component.degrees).toBe(90)
|
||||||
|
component.rotate()
|
||||||
|
expect(component.degrees).toBe(180)
|
||||||
|
component.rotate()
|
||||||
|
expect(component.degrees).toBe(270)
|
||||||
|
component.rotate()
|
||||||
|
expect(component.degrees).toBe(0)
|
||||||
|
component.rotate()
|
||||||
|
expect(component.degrees).toBe(90)
|
||||||
|
component.rotate(false)
|
||||||
|
expect(component.degrees).toBe(0)
|
||||||
|
component.rotate(false)
|
||||||
|
expect(component.degrees).toBe(270)
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,33 @@
|
|||||||
|
import { Component } from '@angular/core'
|
||||||
|
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-rotate-confirm-dialog',
|
||||||
|
templateUrl: './rotate-confirm-dialog.component.html',
|
||||||
|
styleUrl: './rotate-confirm-dialog.component.scss',
|
||||||
|
})
|
||||||
|
export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
|
||||||
|
public documentID: number
|
||||||
|
|
||||||
|
// animation is better if we dont normalize yet
|
||||||
|
public rotation: number = 0
|
||||||
|
|
||||||
|
public get degrees(): number {
|
||||||
|
let degrees = this.rotation % 360
|
||||||
|
if (degrees < 0) degrees += 360
|
||||||
|
return degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
activeModal: NgbActiveModal,
|
||||||
|
public documentService: DocumentService
|
||||||
|
) {
|
||||||
|
super(activeModal)
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate(clockwise: boolean = true) {
|
||||||
|
this.rotation += clockwise ? 90 : -90
|
||||||
|
}
|
||||||
|
}
|
@ -88,7 +88,12 @@
|
|||||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
|
||||||
<button ngbDropdownItem (click)="redoOcrSelected()" [disabled]="!userCanEditAll" i18n>Redo OCR</button>
|
<button ngbDropdownItem (click)="redoOcrSelected()" [disabled]="!userCanEditAll">
|
||||||
|
<i-bs name="body-text"></i-bs> <ng-container i18n>Redo OCR</ng-container>
|
||||||
|
</button>
|
||||||
|
<button ngbDropdownItem (click)="rotateSelected()" [disabled]="!userCanEditAll">
|
||||||
|
<i-bs name="arrow-clockwise"></i-bs> <ng-container i18n>Rotate</ng-container>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -113,22 +118,16 @@
|
|||||||
<div class="form-group ps-3 mb-2">
|
<div class="form-group ps-3 mb-2">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" />
|
<input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" />
|
||||||
<label class="form-check-label" for="downloadFileType_archive" i18n>
|
<label class="form-check-label" for="downloadFileType_archive" i18n>Archived files</label>
|
||||||
Archived files
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" />
|
<input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" />
|
||||||
<label class="form-check-label" for="downloadFileType_originals" i18n>
|
<label class="form-check-label" for="downloadFileType_originals" i18n>Original files</label>
|
||||||
Original files
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" />
|
<input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" />
|
||||||
<label class="form-check-label" for="downloadUseFormatting" i18n>
|
<label class="form-check-label" for="downloadUseFormatting" i18n>Use formatted filename</label>
|
||||||
Use formatted filename
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,6 +52,8 @@ import { StoragePath } from 'src/app/data/storage-path'
|
|||||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
|
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
|
||||||
|
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||||
|
|
||||||
const selectionData: SelectionData = {
|
const selectionData: SelectionData = {
|
||||||
selected_tags: [
|
selected_tags: [
|
||||||
@ -97,6 +99,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
PermissionsGroupComponent,
|
PermissionsGroupComponent,
|
||||||
PermissionsUserComponent,
|
PermissionsUserComponent,
|
||||||
SwitchComponent,
|
SwitchComponent,
|
||||||
|
RotateConfirmDialogComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
PermissionsService,
|
PermissionsService,
|
||||||
@ -818,6 +821,42 @@ describe('BulkEditorComponent', () => {
|
|||||||
) // listAllFilteredIds
|
) // listAllFilteredIds
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support rotate', () => {
|
||||||
|
let modal: NgbModalRef
|
||||||
|
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||||
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'documents', 'get')
|
||||||
|
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||||
|
jest
|
||||||
|
.spyOn(documentListViewService, 'selected', 'get')
|
||||||
|
.mockReturnValue(new Set([3, 4]))
|
||||||
|
jest
|
||||||
|
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||||
|
.mockReturnValue(true)
|
||||||
|
component.showConfirmationDialogs = true
|
||||||
|
fixture.detectChanges()
|
||||||
|
component.rotateSelected()
|
||||||
|
expect(modal).not.toBeUndefined()
|
||||||
|
modal.componentInstance.rotate()
|
||||||
|
modal.componentInstance.confirm()
|
||||||
|
let req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||||
|
)
|
||||||
|
req.flush(true)
|
||||||
|
expect(req.request.body).toEqual({
|
||||||
|
documents: [3, 4],
|
||||||
|
method: 'rotate',
|
||||||
|
parameters: { degrees: 90 },
|
||||||
|
})
|
||||||
|
httpTestingController.match(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||||
|
) // list reload
|
||||||
|
httpTestingController.match(
|
||||||
|
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||||
|
) // listAllFilteredIds
|
||||||
|
})
|
||||||
|
|
||||||
it('should support bulk download with archive, originals or both and file formatting', () => {
|
it('should support bulk download with archive, originals or both and file formatting', () => {
|
||||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||||
jest
|
jest
|
||||||
|
@ -39,6 +39,7 @@ import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
|||||||
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
|
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-bulk-editor',
|
selector: 'pngx-bulk-editor',
|
||||||
@ -641,4 +642,25 @@ export class BulkEditorComponent
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rotateSelected() {
|
||||||
|
let modal = this.modalService.open(RotateConfirmDialogComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
})
|
||||||
|
const rotateDialog = modal.componentInstance as RotateConfirmDialogComponent
|
||||||
|
rotateDialog.title = $localize`Rotate confirm`
|
||||||
|
rotateDialog.messageBold = $localize`This operation will permanently rotate ${this.list.selected.size} selected document(s).`
|
||||||
|
rotateDialog.message = $localize`This will alter the original copy.`
|
||||||
|
rotateDialog.btnClass = 'btn-danger'
|
||||||
|
rotateDialog.btnCaption = $localize`Proceed`
|
||||||
|
rotateDialog.documentID = Array.from(this.list.selected)[0]
|
||||||
|
rotateDialog.confirmClicked
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => {
|
||||||
|
rotateDialog.buttonsEnabled = false
|
||||||
|
this.executeBulkOperation(modal, 'rotate', {
|
||||||
|
degrees: rotateDialog.degrees,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
import hashlib
|
||||||
import itertools
|
import itertools
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
@ -9,6 +12,9 @@ from documents.models import StoragePath
|
|||||||
from documents.permissions import set_permissions_for_object
|
from documents.permissions import set_permissions_for_object
|
||||||
from documents.tasks import bulk_update_documents
|
from documents.tasks import bulk_update_documents
|
||||||
from documents.tasks import update_document_archive_file
|
from documents.tasks import update_document_archive_file
|
||||||
|
from paperless import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger("paperless.bulk_edit")
|
||||||
|
|
||||||
|
|
||||||
def set_correspondent(doc_ids, correspondent):
|
def set_correspondent(doc_ids, correspondent):
|
||||||
@ -146,3 +152,30 @@ def set_permissions(doc_ids, set_permissions, owner=None, merge=False):
|
|||||||
bulk_update_documents.delay(document_ids=affected_docs)
|
bulk_update_documents.delay(document_ids=affected_docs)
|
||||||
|
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
def rotate(doc_ids: list[int], degrees: int):
|
||||||
|
qs = Document.objects.filter(id__in=doc_ids)
|
||||||
|
affected_docs = []
|
||||||
|
import pikepdf
|
||||||
|
|
||||||
|
for doc in qs:
|
||||||
|
try:
|
||||||
|
with pikepdf.open(doc.source_path, allow_overwriting_input=True) as pdf:
|
||||||
|
for page in pdf.pages:
|
||||||
|
page.rotate(degrees, relative=True)
|
||||||
|
pdf.save()
|
||||||
|
doc.checksum = hashlib.md5(doc.source_file.read()).hexdigest()
|
||||||
|
doc.save()
|
||||||
|
update_document_archive_file.delay(
|
||||||
|
document_id=doc.id,
|
||||||
|
)
|
||||||
|
logger.info(f"Rotated document {doc.id} ({path}) by {degrees} degrees")
|
||||||
|
affected_docs.append(doc.id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error rotating document {doc.id}", e)
|
||||||
|
|
||||||
|
if len(affected_docs) > 0:
|
||||||
|
bulk_update_documents.delay(document_ids=affected_docs)
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
@ -869,6 +869,7 @@ class BulkEditSerializer(DocumentListSerializer, SetPermissionsMixin):
|
|||||||
"delete",
|
"delete",
|
||||||
"redo_ocr",
|
"redo_ocr",
|
||||||
"set_permissions",
|
"set_permissions",
|
||||||
|
"rotate",
|
||||||
],
|
],
|
||||||
label="Method",
|
label="Method",
|
||||||
write_only=True,
|
write_only=True,
|
||||||
@ -906,6 +907,8 @@ class BulkEditSerializer(DocumentListSerializer, SetPermissionsMixin):
|
|||||||
return bulk_edit.redo_ocr
|
return bulk_edit.redo_ocr
|
||||||
elif method == "set_permissions":
|
elif method == "set_permissions":
|
||||||
return bulk_edit.set_permissions
|
return bulk_edit.set_permissions
|
||||||
|
elif method == "rotate":
|
||||||
|
return bulk_edit.rotate
|
||||||
else:
|
else:
|
||||||
raise serializers.ValidationError("Unsupported method.")
|
raise serializers.ValidationError("Unsupported method.")
|
||||||
|
|
||||||
@ -984,6 +987,16 @@ class BulkEditSerializer(DocumentListSerializer, SetPermissionsMixin):
|
|||||||
if "merge" not in parameters:
|
if "merge" not in parameters:
|
||||||
parameters["merge"] = False
|
parameters["merge"] = False
|
||||||
|
|
||||||
|
def _validate_parameters_rotate(self, parameters):
|
||||||
|
try:
|
||||||
|
if (
|
||||||
|
"degrees" not in parameters
|
||||||
|
or not float(parameters["degrees"]).is_integer()
|
||||||
|
):
|
||||||
|
raise serializers.ValidationError("invalid rotation degrees")
|
||||||
|
except ValueError:
|
||||||
|
raise serializers.ValidationError("invalid rotation degrees")
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
method = attrs["method"]
|
method = attrs["method"]
|
||||||
parameters = attrs["parameters"]
|
parameters = attrs["parameters"]
|
||||||
@ -1000,6 +1013,8 @@ class BulkEditSerializer(DocumentListSerializer, SetPermissionsMixin):
|
|||||||
self._validate_storage_path(parameters)
|
self._validate_storage_path(parameters)
|
||||||
elif method == bulk_edit.set_permissions:
|
elif method == bulk_edit.set_permissions:
|
||||||
self._validate_parameters_set_permissions(parameters)
|
self._validate_parameters_set_permissions(parameters)
|
||||||
|
elif method == bulk_edit.rotate:
|
||||||
|
self._validate_parameters_rotate(parameters)
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@ -781,3 +781,58 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
m.assert_called_once()
|
m.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.rotate")
|
||||||
|
def test_rotate(self, m):
|
||||||
|
m.return_value = "OK"
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id, self.doc3.id],
|
||||||
|
"method": "rotate",
|
||||||
|
"parameters": {"degrees": 90},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
m.assert_called_once()
|
||||||
|
args, kwargs = m.call_args
|
||||||
|
self.assertCountEqual(args[0], [self.doc2.id, self.doc3.id])
|
||||||
|
self.assertEqual(kwargs["degrees"], 90)
|
||||||
|
|
||||||
|
@mock.patch("documents.serialisers.bulk_edit.rotate")
|
||||||
|
def test_rotate_invalid_params(self, m):
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id, self.doc3.id],
|
||||||
|
"method": "rotate",
|
||||||
|
"parameters": {"degrees": "foo"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id, self.doc3.id],
|
||||||
|
"method": "rotate",
|
||||||
|
"parameters": {"degrees": 90.5},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
m.assert_not_called()
|
||||||
|
@ -891,7 +891,8 @@ class BulkEditView(GenericAPIView, PassUserMixin):
|
|||||||
document_objs = Document.objects.filter(pk__in=documents)
|
document_objs = Document.objects.filter(pk__in=documents)
|
||||||
has_perms = (
|
has_perms = (
|
||||||
all((doc.owner == user or doc.owner is None) for doc in document_objs)
|
all((doc.owner == user or doc.owner is None) for doc in document_objs)
|
||||||
if method == bulk_edit.set_permissions
|
if method
|
||||||
|
in [bulk_edit.set_permissions, bulk_edit.delete, bulk_edit.rotate]
|
||||||
else all(
|
else all(
|
||||||
has_perms_owner_aware(user, "change_document", doc)
|
has_perms_owner_aware(user, "change_document", doc)
|
||||||
for doc in document_objs
|
for doc in document_objs
|
||||||
|
Loading…
x
Reference in New Issue
Block a user