mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge branch 'dev' into fix/issue-603
This commit is contained in:
		
							
								
								
									
										48
									
								
								docs/api.rst
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								docs/api.rst
									
									
									
									
									
								
							| @@ -284,3 +284,51 @@ The endpoint supports the following optional form fields: | |||||||
| The endpoint will immediately return "OK" if the document consumption process | The endpoint will immediately return "OK" if the document consumption process | ||||||
| was started successfully. No additional status information about the consumption | was started successfully. No additional status information about the consumption | ||||||
| process itself is available, since that happens in a different process. | process itself is available, since that happens in a different process. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | API Versioning | ||||||
|  | ############## | ||||||
|  |  | ||||||
|  | The REST API is versioned since Paperless-ng 1.3.0. | ||||||
|  |  | ||||||
|  | * Versioning ensures that changes to the API don't break older clients. | ||||||
|  | * Clients specify the specific version of the API they wish to use with every request and Paperless will handle the request using the specified API version. | ||||||
|  | * Even if the underlying data model changes, older API versions will always serve compatible data. | ||||||
|  | * If no version is specified, Paperless will serve version 1 to ensure compatibility with older clients that do not request a specific API version. | ||||||
|  |  | ||||||
|  | API versions are specified by submitting an additional HTTP ``Accept`` header with every request: | ||||||
|  |  | ||||||
|  | .. code:: | ||||||
|  |  | ||||||
|  |     Accept: application/json; version=6 | ||||||
|  |  | ||||||
|  | If an invalid version is specified, Paperless 1.3.0 will respond with "406 Not Acceptable" and an error message in the body. | ||||||
|  | Earlier versions of Paperless will serve API version 1 regardless of whether a version is specified via the ``Accept`` header. | ||||||
|  |  | ||||||
|  | If a client wishes to verify whether it is compatible with any given server, the following procedure should be performed: | ||||||
|  |  | ||||||
|  | 1.  Perform an *authenticated* request against any API endpoint. If the server is on version 1.3.0 or newer, the server will | ||||||
|  |     add two custom headers to the response: | ||||||
|  |  | ||||||
|  |     .. code:: | ||||||
|  |  | ||||||
|  |         X-Api-Version: 2 | ||||||
|  |         X-Version: 1.3.0 | ||||||
|  |  | ||||||
|  | 2.  Determine whether the client is compatible with this server based on the presence/absence of these headers and their values if present. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | API Changelog | ||||||
|  | ============= | ||||||
|  |  | ||||||
|  | Version 1 | ||||||
|  | --------- | ||||||
|  |  | ||||||
|  | Initial API version. | ||||||
|  |  | ||||||
|  | Version 2 | ||||||
|  | --------- | ||||||
|  |  | ||||||
|  | * Added field ``Tag.color``. This read/write string field contains a hex color such as ``#a6cee3``. | ||||||
|  | * Added read-only field ``Tag.text_color``. This field contains the text color to use for a specific tag, which is either black or white depending on the brightness of ``Tag.color``. | ||||||
|  | * Removed field ``Tag.colour``. | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ Changelog | |||||||
| paperless-ng 1.2.1 | paperless-ng 1.2.1 | ||||||
| ################## | ################## | ||||||
|  |  | ||||||
| * `Rodrigo Avelino <https://github.com/rodavelino>`_ translated Paperless into Portuguese (Brazil). | * `Rodrigo Avelino <https://github.com/rodavelino>`_ translated Paperless into Portuguese (Brazil)! | ||||||
|  |  | ||||||
| * The date input fields now respect the currently selected date format. | * The date input fields now respect the currently selected date format. | ||||||
|  |  | ||||||
| @@ -16,6 +16,8 @@ paperless-ng 1.2.1 | |||||||
|  |  | ||||||
| * When using regular expression matching, the regular expression is now validated before saving the tag/correspondent/type. | * When using regular expression matching, the regular expression is now validated before saving the tag/correspondent/type. | ||||||
|  |  | ||||||
|  | * Regression fix: Dates on the front end did not respect date locale settings in some cases. | ||||||
|  |  | ||||||
| paperless-ng 1.2.0 | paperless-ng 1.2.0 | ||||||
| ################## | ################## | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										20
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -2035,6 +2035,11 @@ | |||||||
|         "to-fast-properties": "^2.0.0" |         "to-fast-properties": "^2.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "@ctrl/tinycolor": { | ||||||
|  |       "version": "3.4.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz", | ||||||
|  |       "integrity": "sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ==" | ||||||
|  |     }, | ||||||
|     "@istanbuljs/schema": { |     "@istanbuljs/schema": { | ||||||
|       "version": "0.1.2", |       "version": "0.1.2", | ||||||
|       "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", |       "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", | ||||||
| @@ -7895,6 +7900,11 @@ | |||||||
|         "object-visit": "^1.0.0" |         "object-visit": "^1.0.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "material-colors": { | ||||||
|  |       "version": "1.2.6", | ||||||
|  |       "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", | ||||||
|  |       "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" | ||||||
|  |     }, | ||||||
|     "md5.js": { |     "md5.js": { | ||||||
|       "version": "1.3.5", |       "version": "1.3.5", | ||||||
|       "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", |       "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", | ||||||
| @@ -8333,6 +8343,16 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "ngx-color": { | ||||||
|  |       "version": "6.2.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-6.2.0.tgz", | ||||||
|  |       "integrity": "sha512-n04tcMnCpOgmI24egST94YwHmnSoAxK8O1T2t3nGrTwWbvw5XBRJvImNFnoNrriBXzc4Gx4hFehH5MU8CZxp1w==", | ||||||
|  |       "requires": { | ||||||
|  |         "@ctrl/tinycolor": "^3.1.6", | ||||||
|  |         "material-colors": "^1.2.6", | ||||||
|  |         "tslib": "^2.0.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "ngx-cookie-service": { |     "ngx-cookie-service": { | ||||||
|       "version": "10.1.1", |       "version": "10.1.1", | ||||||
|       "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-10.1.1.tgz", |       "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-10.1.1.tgz", | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ | |||||||
|     "file-saver": "^2.0.5", |     "file-saver": "^2.0.5", | ||||||
|     "ng-bootstrap": "^1.6.3", |     "ng-bootstrap": "^1.6.3", | ||||||
|     "ng2-pdf-viewer": "^6.3.2", |     "ng2-pdf-viewer": "^6.3.2", | ||||||
|  |     "ngx-color": "^6.2.0", | ||||||
|     "ngx-cookie-service": "^10.1.1", |     "ngx-cookie-service": "^10.1.1", | ||||||
|     "ngx-file-drop": "^10.0.0", |     "ngx-file-drop": "^10.0.0", | ||||||
|     "ngx-infinite-scroll": "^9.1.0", |     "ngx-infinite-scroll": "^9.1.0", | ||||||
|   | |||||||
| @@ -63,6 +63,8 @@ import { DateComponent } from './components/common/input/date/date.component'; | |||||||
| import { ISODateTimeAdapter } from './utils/ngb-iso-date-time-adapter'; | import { ISODateTimeAdapter } from './utils/ngb-iso-date-time-adapter'; | ||||||
| import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'; | import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'; | ||||||
| import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'; | import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'; | ||||||
|  | import { ColorSliderModule } from 'ngx-color/slider'; | ||||||
|  | import { ColorComponent } from './components/common/input/color/color.component'; | ||||||
|  |  | ||||||
| import localeFr from '@angular/common/locales/fr'; | import localeFr from '@angular/common/locales/fr'; | ||||||
| import localeNl from '@angular/common/locales/nl'; | import localeNl from '@angular/common/locales/nl'; | ||||||
| @@ -125,7 +127,8 @@ registerLocaleData(localeEnGb) | |||||||
|     NumberComponent, |     NumberComponent, | ||||||
|     SafePipe, |     SafePipe, | ||||||
|     CustomDatePipe, |     CustomDatePipe, | ||||||
|     DateComponent |     DateComponent, | ||||||
|  |     ColorComponent | ||||||
|   ], |   ], | ||||||
|   imports: [ |   imports: [ | ||||||
|     BrowserModule, |     BrowserModule, | ||||||
| @@ -137,7 +140,8 @@ registerLocaleData(localeEnGb) | |||||||
|     NgxFileDropModule, |     NgxFileDropModule, | ||||||
|     InfiniteScrollModule, |     InfiniteScrollModule, | ||||||
|     PdfViewerModule, |     PdfViewerModule, | ||||||
|     NgSelectModule |     NgSelectModule, | ||||||
|  |     ColorSliderModule | ||||||
|   ], |   ], | ||||||
|   providers: [ |   providers: [ | ||||||
|     DatePipe, |     DatePipe, | ||||||
|   | |||||||
| @@ -0,0 +1,33 @@ | |||||||
|  | <div class="form-group"> | ||||||
|  |   <label [for]="inputId">{{title}}</label> | ||||||
|  |  | ||||||
|  |   <div class="input-group" [class.is-invalid]="error"> | ||||||
|  |       <div class="input-group-prepend"> | ||||||
|  |         <span class="input-group-text" [style.background-color]="value">   </span> | ||||||
|  |       </div> | ||||||
|  |        | ||||||
|  |       <ng-template #popContent> | ||||||
|  |         <div style="min-width: 200px;" class="pb-3"> | ||||||
|  |           <color-slider [color]="value" (onChangeComplete)="colorChanged($event.color.hex)"></color-slider> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |       </ng-template> | ||||||
|  |  | ||||||
|  |       <input class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow"> | ||||||
|  |  | ||||||
|  |       <div class="input-group-append"> | ||||||
|  |         <button class="btn btn-outline-secondary" type="button" (click)="randomize()"> | ||||||
|  |           <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dice-5" viewBox="0 0 16 16"> | ||||||
|  |             <path d="M13 1a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10zM3 0a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V3a3 3 0 0 0-3-3H3z"/> | ||||||
|  |             <path d="M5.5 4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm-8 0a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm4-4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/> | ||||||
|  |           </svg> | ||||||
|  |         </button> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     </div> | ||||||
|  |     <small *ngIf="hint" class="form-text text-muted">{{hint}}</small> | ||||||
|  |     <div class="invalid-feedback"> | ||||||
|  |       {{error}} | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
| @@ -0,0 +1,25 @@ | |||||||
|  | import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||||
|  |  | ||||||
|  | import { ColorComponent } from './color.component'; | ||||||
|  |  | ||||||
|  | describe('ColorComponent', () => { | ||||||
|  |   let component: ColorComponent; | ||||||
|  |   let fixture: ComponentFixture<ColorComponent>; | ||||||
|  |  | ||||||
|  |   beforeEach(async () => { | ||||||
|  |     await TestBed.configureTestingModule({ | ||||||
|  |       declarations: [ ColorComponent ] | ||||||
|  |     }) | ||||||
|  |     .compileComponents(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   beforeEach(() => { | ||||||
|  |     fixture = TestBed.createComponent(ColorComponent); | ||||||
|  |     component = fixture.componentInstance; | ||||||
|  |     fixture.detectChanges(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should create', () => { | ||||||
|  |     expect(component).toBeTruthy(); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | import { Component, forwardRef } from '@angular/core'; | ||||||
|  | import { NG_VALUE_ACCESSOR } from '@angular/forms'; | ||||||
|  | import { randomColor } from 'src/app/utils/color'; | ||||||
|  | import { AbstractInputComponent } from '../abstract-input'; | ||||||
|  |  | ||||||
|  | @Component({ | ||||||
|  |   providers: [{ | ||||||
|  |     provide: NG_VALUE_ACCESSOR, | ||||||
|  |     useExisting: forwardRef(() => ColorComponent), | ||||||
|  |     multi: true | ||||||
|  |   }], | ||||||
|  |   selector: 'app-input-color', | ||||||
|  |   templateUrl: './color.component.html', | ||||||
|  |   styleUrls: ['./color.component.scss'] | ||||||
|  | }) | ||||||
|  | export class ColorComponent extends AbstractInputComponent<string> { | ||||||
|  |  | ||||||
|  |   constructor() { | ||||||
|  |     super() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   randomize() { | ||||||
|  |     this.colorChanged(randomColor()) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   colorChanged(value) { | ||||||
|  |     this.value = value | ||||||
|  |     this.onChange(value) | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| <div class="form-group"> | <div class="form-group"> | ||||||
|   <label [for]="inputId">{{title}}</label> |   <label [for]="inputId">{{title}}</label> | ||||||
|   <div class="input-group"> |   <div class="input-group" [class.is-invalid]="error"> | ||||||
|     <input [class.is-invalid]="error" class="form-control" [placeholder]="placeholder" [id]="inputId" (dateSelect)="onChange(value)" (change)="onChange(value)" |     <input class="form-control" [class.is-invalid]="error"  [placeholder]="placeholder" [id]="inputId" (dateSelect)="onChange(value)" (change)="onChange(value)" | ||||||
|            name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel"> |            name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel"> | ||||||
|     <div class="input-group-append"> |     <div class="input-group-append"> | ||||||
|       <button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button"> |       <button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button"> | ||||||
| @@ -10,6 +10,7 @@ | |||||||
|         </svg> |         </svg> | ||||||
|       </button> |       </button> | ||||||
|     </div> |     </div> | ||||||
|     <div class="invalid-feedback" *ngIf="error" i18n>Invalid date.</div> |  | ||||||
|   </div> |   </div> | ||||||
|  |   <div class="invalid-feedback" i18n>Invalid date.</div> | ||||||
|  |   <small *ngIf="hint" class="form-text text-muted">{{hint}}</small> | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -1,2 +1,2 @@ | |||||||
| <span *ngIf="!clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</span> | <span *ngIf="!clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</span> | ||||||
| <a [routerLink]="" [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</a> | <a [routerLink]="" [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a> | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { Component, Input, OnInit } from '@angular/core'; | import { Component, Input, OnInit } from '@angular/core'; | ||||||
| import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag'; | import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-tag', |   selector: 'app-tag', | ||||||
| @@ -22,8 +22,4 @@ export class TagComponent implements OnInit { | |||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getColour() { |  | ||||||
|     return TAG_COLOURS.find(c => c.id == this.tag.colour) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,15 +8,7 @@ | |||||||
|     <div class="modal-body"> |     <div class="modal-body"> | ||||||
|       <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> |       <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> | ||||||
|  |  | ||||||
|  |       <app-input-color i18n-title title="Color" formControlName="color" [error]="error?.color"></app-input-color> | ||||||
|       <div class="form-group paperless-input-select"> |  | ||||||
|         <label for="colour" i18n>Color</label> |  | ||||||
|         <ng-select name="colour" formControlName="colour" [items]="getColours()" bindValue="id" bindLabel="name" [clearable]="false"> |  | ||||||
|           <ng-template ng-option-tmp ng-label-tmp let-item="item"> |  | ||||||
|             <span class="badge" [style.background]="item.value" [style.color]="item.textColor">{{item.name}}</span> |  | ||||||
|           </ng-template> |  | ||||||
|         </ng-select> |  | ||||||
|       </div> |  | ||||||
|  |  | ||||||
|       <app-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check> |       <app-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check> | ||||||
|       <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select> |       <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select> | ||||||
|   | |||||||
| @@ -2,9 +2,10 @@ import { Component } from '@angular/core'; | |||||||
| import { FormControl, FormGroup } from '@angular/forms'; | import { FormControl, FormGroup } from '@angular/forms'; | ||||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; | ||||||
| import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'; | import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'; | ||||||
| import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag'; | import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||||
| import { TagService } from 'src/app/services/rest/tag.service'; | import { TagService } from 'src/app/services/rest/tag.service'; | ||||||
| import { ToastService } from 'src/app/services/toast.service'; | import { ToastService } from 'src/app/services/toast.service'; | ||||||
|  | import { randomColor } from 'src/app/utils/color'; | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-tag-edit-dialog', |   selector: 'app-tag-edit-dialog', | ||||||
| @@ -13,7 +14,7 @@ import { ToastService } from 'src/app/services/toast.service'; | |||||||
| }) | }) | ||||||
| export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | ||||||
|  |  | ||||||
|   constructor(service: TagService, activeModal: NgbActiveModal, toastService: ToastService) {  |   constructor(service: TagService, activeModal: NgbActiveModal, toastService: ToastService) { | ||||||
|     super(service, activeModal, toastService) |     super(service, activeModal, toastService) | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -28,7 +29,7 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | |||||||
|   getForm(): FormGroup { |   getForm(): FormGroup { | ||||||
|     return new FormGroup({ |     return new FormGroup({ | ||||||
|       name: new FormControl(''), |       name: new FormControl(''), | ||||||
|       colour: new FormControl(1), |       color: new FormControl(randomColor()), | ||||||
|       is_inbox_tag: new FormControl(false), |       is_inbox_tag: new FormControl(false), | ||||||
|       matching_algorithm: new FormControl(1), |       matching_algorithm: new FormControl(1), | ||||||
|       match: new FormControl(""), |       match: new FormControl(""), | ||||||
| @@ -36,12 +37,4 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | |||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getColours() { |  | ||||||
|     return TAG_COLOURS |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   getColor(id: number) { |  | ||||||
|     return TAG_COLOURS.find(c => c.id == id) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -26,8 +26,8 @@ | |||||||
|   <tbody> |   <tbody> | ||||||
|     <tr *ngFor="let tag of data"> |     <tr *ngFor="let tag of data"> | ||||||
|       <td scope="row">{{ tag.name }}</td> |       <td scope="row">{{ tag.name }}</td> | ||||||
|       <td scope="row"><span class="badge" [style.color]="getColor(tag.colour).textColor" |       <td scope="row"><span class="badge" [style.color]="tag.text_color" | ||||||
|           [style.background-color]="getColor(tag.colour).value">{{ getColor(tag.colour).name }}</span></td> |           [style.background-color]="tag.color">{{tag.color}}</span></td> | ||||||
|       <td scope="row">{{ getMatching(tag) }}</td> |       <td scope="row">{{ getMatching(tag) }}</td> | ||||||
|       <td scope="row">{{ tag.document_count }}</td> |       <td scope="row">{{ tag.document_count }}</td> | ||||||
|       <td scope="row"> |       <td scope="row"> | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { Component } from '@angular/core'; | import { Component } from '@angular/core'; | ||||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | ||||||
| import { FILTER_HAS_TAG } from 'src/app/data/filter-rule-type'; | import { FILTER_HAS_TAG } from 'src/app/data/filter-rule-type'; | ||||||
| import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag'; | import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service'; | import { DocumentListViewService } from 'src/app/services/document-list-view.service'; | ||||||
| import { TagService } from 'src/app/services/rest/tag.service'; | import { TagService } from 'src/app/services/rest/tag.service'; | ||||||
| import { ToastService } from 'src/app/services/toast.service'; | import { ToastService } from 'src/app/services/toast.service'; | ||||||
| @@ -22,10 +22,6 @@ export class TagListComponent extends GenericListComponent<PaperlessTag> { | |||||||
|     super(tagService, modalService, TagEditDialogComponent, toastService) |     super(tagService, modalService, TagEditDialogComponent, toastService) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getColor(id) { |  | ||||||
|     return TAG_COLOURS.find(c => c.id == id) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   getDeleteMessage(object: PaperlessTag) { |   getDeleteMessage(object: PaperlessTag) { | ||||||
|     return $localize`Do you really want to delete the tag "${object.name}"?` |     return $localize`Do you really want to delete the tag "${object.name}"?` | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,26 +1,10 @@ | |||||||
| import { MatchingModel } from './matching-model'; | import { MatchingModel } from "./matching-model"; | ||||||
| import { ObjectWithId } from './object-with-id'; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| export const TAG_COLOURS = [ |  | ||||||
|     {id: 1, value: "#a6cee3", name: $localize`Light blue`, textColor: "#000000"}, |  | ||||||
|     {id: 2, value: "#1f78b4", name: $localize`Blue`, textColor: "#ffffff"}, |  | ||||||
|     {id: 3, value: "#b2df8a", name: $localize`Light green`, textColor: "#000000"}, |  | ||||||
|     {id: 4, value: "#33a02c", name: $localize`Green`, textColor: "#ffffff"}, |  | ||||||
|     {id: 5, value: "#fb9a99", name: $localize`Light red`, textColor: "#000000"}, |  | ||||||
|     {id: 6, value: "#e31a1c", name: $localize`Red `, textColor: "#ffffff"}, |  | ||||||
|     {id: 7, value: "#fdbf6f", name: $localize`Light orange`, textColor: "#000000"}, |  | ||||||
|     {id: 8, value: "#ff7f00", name: $localize`Orange`, textColor: "#000000"}, |  | ||||||
|     {id: 9, value: "#cab2d6", name: $localize`Light violet`, textColor: "#000000"}, |  | ||||||
|     {id: 10, value: "#6a3d9a", name: $localize`Violet`, textColor: "#ffffff"}, |  | ||||||
|     {id: 11, value: "#b15928", name: $localize`Brown`, textColor: "#ffffff"}, |  | ||||||
|     {id: 12, value: "#000000", name: $localize`Black`, textColor: "#ffffff"}, |  | ||||||
|     {id: 13, value: "#cccccc", name: $localize`Light grey`, textColor: "#000000"} |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| export interface PaperlessTag extends MatchingModel { | export interface PaperlessTag extends MatchingModel { | ||||||
|  |  | ||||||
|     colour?: number |     color?: string | ||||||
|  |  | ||||||
|  |     text_color?: string | ||||||
|  |  | ||||||
|     is_inbox_tag?: boolean |     is_inbox_tag?: boolean | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,17 +13,20 @@ const FORMAT_TO_ISO_FORMAT = { | |||||||
| }) | }) | ||||||
| export class CustomDatePipe extends DatePipe implements PipeTransform { | export class CustomDatePipe extends DatePipe implements PipeTransform { | ||||||
|  |  | ||||||
|  |   private defaultLocale: string | ||||||
|  |  | ||||||
|   constructor(@Inject(LOCALE_ID) locale: string, private settings: SettingsService) { |   constructor(@Inject(LOCALE_ID) locale: string, private settings: SettingsService) { | ||||||
|     super(locale) |     super(locale) | ||||||
|  |     this.defaultLocale = locale | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   transform(value: any, format?: string, timezone?: string, locale?: string): string | null { |   transform(value: any, format?: string, timezone?: string, locale?: string): string | null { | ||||||
|     let l = locale || this.settings.get(SETTINGS_KEYS.DATE_LOCALE) |     let l = locale || this.settings.get(SETTINGS_KEYS.DATE_LOCALE) || this.defaultLocale | ||||||
|     let f = format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT) |     let f = format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT) | ||||||
|     if (l == "iso-8601") { |     if (l == "iso-8601") { | ||||||
|       return super.transform(value, FORMAT_TO_ISO_FORMAT[f], timezone) |       return super.transform(value, FORMAT_TO_ISO_FORMAT[f], timezone) | ||||||
|     } else { |     } else { | ||||||
|       return super.transform(value, format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT), timezone, locale) |       return super.transform(value, format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT), timezone, l) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								src-ui/src/app/utils/color.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src-ui/src/app/utils/color.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  |  | ||||||
|  |   function componentToHex(c) { | ||||||
|  |     var hex = c.toString(16); | ||||||
|  |     return hex.length == 1 ? "0" + hex : hex; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   export function randomColor() { | ||||||
|  |     let r = Math.floor(Math.random() * 150) + 50 | ||||||
|  |     let g = Math.floor(Math.random() * 150) + 50 | ||||||
|  |     let b = Math.floor(Math.random() * 150) + 50 | ||||||
|  |     return `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}` | ||||||
|  |   } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| export const environment = { | export const environment = { | ||||||
|   production: true, |   production: true, | ||||||
|   apiBaseUrl: "/api/", |   apiBaseUrl: "/api/", | ||||||
|   apiVersion: "1", |   apiVersion: "2", | ||||||
|   appTitle: "Paperless-ng", |   appTitle: "Paperless-ng", | ||||||
|   version: "1.2.1", |   version: "1.2.1", | ||||||
|   webSocketHost: window.location.host, |   webSocketHost: window.location.host, | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
| export const environment = { | export const environment = { | ||||||
|   production: false, |   production: false, | ||||||
|   apiBaseUrl: "http://localhost:8000/api/", |   apiBaseUrl: "http://localhost:8000/api/", | ||||||
|   apiVersion: "1", |   apiVersion: "2", | ||||||
|   appTitle: "Paperless-ng", |   appTitle: "Paperless-ng", | ||||||
|   version: "DEVELOPMENT", |   version: "DEVELOPMENT", | ||||||
|   webSocketHost: "localhost:8000", |   webSocketHost: "localhost:8000", | ||||||
|   | |||||||
| @@ -1729,7 +1729,7 @@ | |||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit datatype="html" id="90917e1a0a7bb59e9d11bdde9183e9391963e17b"> |       <trans-unit datatype="html" id="90917e1a0a7bb59e9d11bdde9183e9391963e17b"> | ||||||
|         <source>{VAR_PLURAL, plural, =1 {One more document} other {<x id="INTERPOLATION"/> more documents}}</source> |         <source>{VAR_PLURAL, plural, =1 {One more document} other {<x id="INTERPOLATION"/> more documents}}</source> | ||||||
|         <target>{VAR_PLURAL, plural, =1 {Mais um documento} other {Mais <x id="INTERPOLATION"/> documentos}</target> |         <target>{VAR_PLURAL, plural, =1 {Mais um documento} other {Mais <x id="INTERPOLATION"/> documentos}}</target> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.html</context> |           <context context-type="sourcefile">src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.html</context> | ||||||
|           <context context-type="linenumber">25</context> |           <context context-type="linenumber">25</context> | ||||||
|   | |||||||
| @@ -19,12 +19,12 @@ class TagAdmin(admin.ModelAdmin): | |||||||
|  |  | ||||||
|     list_display = ( |     list_display = ( | ||||||
|         "name", |         "name", | ||||||
|         "colour", |         "color", | ||||||
|         "match", |         "match", | ||||||
|         "matching_algorithm" |         "matching_algorithm" | ||||||
|     ) |     ) | ||||||
|     list_filter = ("colour", "matching_algorithm") |     list_filter = ("color", "matching_algorithm") | ||||||
|     list_editable = ("colour", "match", "matching_algorithm") |     list_editable = ("color", "match", "matching_algorithm") | ||||||
|  |  | ||||||
|  |  | ||||||
| class DocumentTypeAdmin(admin.ModelAdmin): | class DocumentTypeAdmin(admin.ModelAdmin): | ||||||
|   | |||||||
							
								
								
									
										70
									
								
								src/documents/migrations/1013_migrate_tag_colour.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/documents/migrations/1013_migrate_tag_colour.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | # Generated by Django 3.1.4 on 2020-12-02 21:43 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  | COLOURS_OLD = { | ||||||
|  |     1: "#a6cee3", | ||||||
|  |     2: "#1f78b4", | ||||||
|  |     3: "#b2df8a", | ||||||
|  |     4: "#33a02c", | ||||||
|  |     5: "#fb9a99", | ||||||
|  |     6: "#e31a1c", | ||||||
|  |     7: "#fdbf6f", | ||||||
|  |     8: "#ff7f00", | ||||||
|  |     9: "#cab2d6", | ||||||
|  |     10: "#6a3d9a", | ||||||
|  |     11: "#b15928", | ||||||
|  |     12: "#000000", | ||||||
|  |     13: "#cccccc", | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def forward(apps, schema_editor): | ||||||
|  |     Tag = apps.get_model('documents', 'Tag') | ||||||
|  |  | ||||||
|  |     for tag in Tag.objects.all(): | ||||||
|  |         colour_old_id = tag.colour_old | ||||||
|  |         rgb = COLOURS_OLD[colour_old_id] | ||||||
|  |         tag.color = rgb | ||||||
|  |         tag.save() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def reverse(apps, schema_editor): | ||||||
|  |     Tag = apps.get_model('documents', 'Tag') | ||||||
|  |  | ||||||
|  |     def _get_colour_id(rdb): | ||||||
|  |         for idx, rdbx in COLOURS_OLD.items(): | ||||||
|  |             if rdbx == rdb: | ||||||
|  |                 return idx | ||||||
|  |         # Return colour 1 if we can't match anything | ||||||
|  |         return 1 | ||||||
|  |  | ||||||
|  |     for tag in Tag.objects.all(): | ||||||
|  |         colour_id = _get_colour_id(tag.color) | ||||||
|  |         tag.colour_old = colour_id | ||||||
|  |         tag.save() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('documents', '1012_fix_archive_files'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.RenameField( | ||||||
|  |             model_name='tag', | ||||||
|  |             old_name='colour', | ||||||
|  |             new_name='colour_old', | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='tag', | ||||||
|  |             name='color', | ||||||
|  |             field=models.CharField(default='#a6cee3', max_length=7, verbose_name='color'), | ||||||
|  |         ), | ||||||
|  |         migrations.RunPython(forward, reverse), | ||||||
|  |         migrations.RemoveField( | ||||||
|  |             model_name='tag', | ||||||
|  |             name='colour_old', | ||||||
|  |         ) | ||||||
|  |     ] | ||||||
| @@ -77,25 +77,11 @@ class Correspondent(MatchingModel): | |||||||
|  |  | ||||||
| class Tag(MatchingModel): | class Tag(MatchingModel): | ||||||
|  |  | ||||||
|     COLOURS = ( |     color = models.CharField( | ||||||
|         (1, "#a6cee3"), |  | ||||||
|         (2, "#1f78b4"), |  | ||||||
|         (3, "#b2df8a"), |  | ||||||
|         (4, "#33a02c"), |  | ||||||
|         (5, "#fb9a99"), |  | ||||||
|         (6, "#e31a1c"), |  | ||||||
|         (7, "#fdbf6f"), |  | ||||||
|         (8, "#ff7f00"), |  | ||||||
|         (9, "#cab2d6"), |  | ||||||
|         (10, "#6a3d9a"), |  | ||||||
|         (11, "#b15928"), |  | ||||||
|         (12, "#000000"), |  | ||||||
|         (13, "#cccccc") |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     colour = models.PositiveIntegerField( |  | ||||||
|         _("color"), |         _("color"), | ||||||
|         choices=COLOURS, default=1) |         max_length=7, | ||||||
|  |         default="#a6cee3" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     is_inbox_tag = models.BooleanField( |     is_inbox_tag = models.BooleanField( | ||||||
|         _("is inbox tag"), |         _("is inbox tag"), | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import re | import re | ||||||
|  |  | ||||||
| import magic | import magic | ||||||
|  | import math | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| from rest_framework import serializers | from rest_framework import serializers | ||||||
| from rest_framework.fields import SerializerMethodField | from rest_framework.fields import SerializerMethodField | ||||||
| @@ -88,7 +89,40 @@ class DocumentTypeSerializer(MatchingModelSerializer): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class TagSerializer(MatchingModelSerializer): | class ColorField(serializers.Field): | ||||||
|  |  | ||||||
|  |     COLOURS = ( | ||||||
|  |         (1, "#a6cee3"), | ||||||
|  |         (2, "#1f78b4"), | ||||||
|  |         (3, "#b2df8a"), | ||||||
|  |         (4, "#33a02c"), | ||||||
|  |         (5, "#fb9a99"), | ||||||
|  |         (6, "#e31a1c"), | ||||||
|  |         (7, "#fdbf6f"), | ||||||
|  |         (8, "#ff7f00"), | ||||||
|  |         (9, "#cab2d6"), | ||||||
|  |         (10, "#6a3d9a"), | ||||||
|  |         (11, "#b15928"), | ||||||
|  |         (12, "#000000"), | ||||||
|  |         (13, "#cccccc") | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     def to_internal_value(self, data): | ||||||
|  |         for id, color in self.COLOURS: | ||||||
|  |             if id == data: | ||||||
|  |                 return color | ||||||
|  |         raise serializers.ValidationError() | ||||||
|  |  | ||||||
|  |     def to_representation(self, value): | ||||||
|  |         for id, color in self.COLOURS: | ||||||
|  |             if color == value: | ||||||
|  |                 return id | ||||||
|  |         return 1 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TagSerializerVersion1(MatchingModelSerializer): | ||||||
|  |  | ||||||
|  |     colour = ColorField(source='color', default="#a6cee3") | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = Tag |         model = Tag | ||||||
| @@ -105,6 +139,45 @@ class TagSerializer(MatchingModelSerializer): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TagSerializer(MatchingModelSerializer): | ||||||
|  |  | ||||||
|  |     def get_text_color(self, obj): | ||||||
|  |         try: | ||||||
|  |             h = obj.color.lstrip('#') | ||||||
|  |             rgb = tuple(int(h[i:i + 2], 16)/256 for i in (0, 2, 4)) | ||||||
|  |             luminance = math.sqrt( | ||||||
|  |                 0.299 * math.pow(rgb[0], 2) + | ||||||
|  |                 0.587 * math.pow(rgb[1], 2) + | ||||||
|  |                 0.114 * math.pow(rgb[2], 2) | ||||||
|  |             ) | ||||||
|  |             return "#ffffff" if luminance < 0.53 else "#000000" | ||||||
|  |         except ValueError: | ||||||
|  |             return "#000000" | ||||||
|  |  | ||||||
|  |     text_color = serializers.SerializerMethodField() | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         model = Tag | ||||||
|  |         fields = ( | ||||||
|  |             "id", | ||||||
|  |             "slug", | ||||||
|  |             "name", | ||||||
|  |             "color", | ||||||
|  |             "text_color", | ||||||
|  |             "match", | ||||||
|  |             "matching_algorithm", | ||||||
|  |             "is_insensitive", | ||||||
|  |             "is_inbox_tag", | ||||||
|  |             "document_count" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def validate_color(self, color): | ||||||
|  |         regex = r"#[0-9a-fA-F]{6}" | ||||||
|  |         if not re.match(regex, color): | ||||||
|  |             raise serializers.ValidationError(_("Invalid color.")) | ||||||
|  |         return color | ||||||
|  |  | ||||||
|  |  | ||||||
| class CorrespondentField(serializers.PrimaryKeyRelatedField): | class CorrespondentField(serializers.PrimaryKeyRelatedField): | ||||||
|     def get_queryset(self): |     def get_queryset(self): | ||||||
|         return Correspondent.objects.all() |         return Correspondent.objects.all() | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ | |||||||
|   <link rel="icon" type="image/x-icon" href="favicon.ico"> |   <link rel="icon" type="image/x-icon" href="favicon.ico"> | ||||||
|   <link rel="manifest" href="{% static webmanifest %}"> |   <link rel="manifest" href="{% static webmanifest %}"> | ||||||
| 	<link rel="stylesheet" href="{% static styles_css %}"> | 	<link rel="stylesheet" href="{% static styles_css %}"> | ||||||
| 	<link rel="apple-touch-icon" href="apple-touch-icon.png"> | 	<link rel="apple-touch-icon" href="{% static apple_touch_icon %}"> | ||||||
| </head> | </head> | ||||||
| <body> | <body> | ||||||
|   <app-root>{% translate "Paperless-ng is loading..." %}</app-root> |   <app-root>{% translate "Paperless-ng is loading..." %}</app-root> | ||||||
|   | |||||||
| @@ -807,6 +807,69 @@ class TestDocumentApi(DirectoriesMixin, APITestCase): | |||||||
|             }, format='json') |             }, format='json') | ||||||
|             self.assertEqual(response.status_code, 201, endpoint) |             self.assertEqual(response.status_code, 201, endpoint) | ||||||
|  |  | ||||||
|  |     def test_tag_color_default(self): | ||||||
|  |         response = self.client.post("/api/tags/", { | ||||||
|  |             "name": "tag" | ||||||
|  |         }, format="json") | ||||||
|  |         self.assertEqual(response.status_code, 201) | ||||||
|  |         self.assertEqual(Tag.objects.get(id=response.data['id']).color, "#a6cee3") | ||||||
|  |         self.assertEqual(self.client.get(f"/api/tags/{response.data['id']}/", format="json").data['colour'], 1) | ||||||
|  |  | ||||||
|  |     def test_tag_color(self): | ||||||
|  |         response = self.client.post("/api/tags/", { | ||||||
|  |             "name": "tag", | ||||||
|  |             "colour": 3 | ||||||
|  |         }, format="json") | ||||||
|  |         self.assertEqual(response.status_code, 201) | ||||||
|  |         self.assertEqual(Tag.objects.get(id=response.data['id']).color, "#b2df8a") | ||||||
|  |         self.assertEqual(self.client.get(f"/api/tags/{response.data['id']}/", format="json").data['colour'], 3) | ||||||
|  |  | ||||||
|  |     def test_tag_color_invalid(self): | ||||||
|  |         response = self.client.post("/api/tags/", { | ||||||
|  |             "name": "tag", | ||||||
|  |             "colour": 34 | ||||||
|  |         }, format="json") | ||||||
|  |         self.assertEqual(response.status_code, 400) | ||||||
|  |  | ||||||
|  |     def test_tag_color_custom(self): | ||||||
|  |         tag = Tag.objects.create(name="test", color="#abcdef") | ||||||
|  |         self.assertEqual(self.client.get(f"/api/tags/{tag.id}/", format="json").data['colour'], 1) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestDocumentApiV2(DirectoriesMixin, APITestCase): | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super(TestDocumentApiV2, self).setUp() | ||||||
|  |  | ||||||
|  |         self.user = User.objects.create_superuser(username="temp_admin") | ||||||
|  |  | ||||||
|  |         self.client.force_login(user=self.user) | ||||||
|  |         self.client.defaults['HTTP_ACCEPT'] = 'application/json; version=2' | ||||||
|  |  | ||||||
|  |     def test_tag_validate_color(self): | ||||||
|  |         self.assertEqual(self.client.post("/api/tags/", {"name": "test", "color": "#12fFaA"}, format="json").status_code, 201) | ||||||
|  |  | ||||||
|  |         self.assertEqual(self.client.post("/api/tags/", {"name": "test1", "color": "abcdef"}, format="json").status_code, 400) | ||||||
|  |         self.assertEqual(self.client.post("/api/tags/", {"name": "test2", "color": "#abcdfg"}, format="json").status_code, 400) | ||||||
|  |         self.assertEqual(self.client.post("/api/tags/", {"name": "test3", "color": "#asd"}, format="json").status_code, 400) | ||||||
|  |         self.assertEqual(self.client.post("/api/tags/", {"name": "test4", "color": "#12121212"}, format="json").status_code, 400) | ||||||
|  |  | ||||||
|  |     def test_tag_text_color(self): | ||||||
|  |         t = Tag.objects.create(name="tag1", color="#000000") | ||||||
|  |         self.assertEqual(self.client.get(f"/api/tags/{t.id}/", format="json").data['text_color'], "#ffffff") | ||||||
|  |  | ||||||
|  |         t.color = "#ffffff" | ||||||
|  |         t.save() | ||||||
|  |         self.assertEqual(self.client.get(f"/api/tags/{t.id}/", format="json").data['text_color'], "#000000") | ||||||
|  |  | ||||||
|  |         t.color = "asdf" | ||||||
|  |         t.save() | ||||||
|  |         self.assertEqual(self.client.get(f"/api/tags/{t.id}/", format="json").data['text_color'], "#000000") | ||||||
|  |  | ||||||
|  |         t.color = "123" | ||||||
|  |         t.save() | ||||||
|  |         self.assertEqual(self.client.get(f"/api/tags/{t.id}/", format="json").data['text_color'], "#000000") | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestBulkEdit(DirectoriesMixin, APITestCase): | class TestBulkEdit(DirectoriesMixin, APITestCase): | ||||||
|  |  | ||||||
|   | |||||||
| @@ -50,6 +50,7 @@ from .parsers import get_parser_class_for_mime_type | |||||||
| from .serialisers import ( | from .serialisers import ( | ||||||
|     CorrespondentSerializer, |     CorrespondentSerializer, | ||||||
|     DocumentSerializer, |     DocumentSerializer, | ||||||
|  |     TagSerializerVersion1, | ||||||
|     TagSerializer, |     TagSerializer, | ||||||
|     DocumentTypeSerializer, |     DocumentTypeSerializer, | ||||||
|     PostDocumentSerializer, |     PostDocumentSerializer, | ||||||
| @@ -89,6 +90,7 @@ class IndexView(TemplateView): | |||||||
|         context['polyfills_js'] = f"frontend/{self.get_language()}/polyfills.js"  # NOQA: E501 |         context['polyfills_js'] = f"frontend/{self.get_language()}/polyfills.js"  # NOQA: E501 | ||||||
|         context['main_js'] = f"frontend/{self.get_language()}/main.js" |         context['main_js'] = f"frontend/{self.get_language()}/main.js" | ||||||
|         context['webmanifest'] = f"frontend/{self.get_language()}/manifest.webmanifest"  # NOQA: E501 |         context['webmanifest'] = f"frontend/{self.get_language()}/manifest.webmanifest"  # NOQA: E501 | ||||||
|  |         context['apple_touch_icon'] = f"frontend/{self.get_language()}/apple-touch-icon.png"  # NOQA: E501 | ||||||
|         return context |         return context | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -118,7 +120,12 @@ class TagViewSet(ModelViewSet): | |||||||
|     queryset = Tag.objects.annotate( |     queryset = Tag.objects.annotate( | ||||||
|         document_count=Count('documents')).order_by(Lower('name')) |         document_count=Count('documents')).order_by(Lower('name')) | ||||||
|  |  | ||||||
|     serializer_class = TagSerializer |     def get_serializer_class(self): | ||||||
|  |         if int(self.request.version) == 1: | ||||||
|  |             return TagSerializerVersion1 | ||||||
|  |         else: | ||||||
|  |             return TagSerializer | ||||||
|  |  | ||||||
|     pagination_class = StandardPagination |     pagination_class = StandardPagination | ||||||
|     permission_classes = (IsAuthenticated,) |     permission_classes = (IsAuthenticated,) | ||||||
|     filter_backends = (DjangoFilterBackend, OrderingFilter) |     filter_backends = (DjangoFilterBackend, OrderingFilter) | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								src/paperless/middleware.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/paperless/middleware.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | from django.conf import settings | ||||||
|  |  | ||||||
|  | from paperless import version | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ApiVersionMiddleware: | ||||||
|  |  | ||||||
|  |     def __init__(self, get_response): | ||||||
|  |         self.get_response = get_response | ||||||
|  |  | ||||||
|  |     def __call__(self, request): | ||||||
|  |         response = self.get_response(request) | ||||||
|  |         if request.user.is_authenticated: | ||||||
|  |             versions = settings.REST_FRAMEWORK['ALLOWED_VERSIONS'] | ||||||
|  |             response['X-Api-Version'] = versions[len(versions)-1] | ||||||
|  |             response['X-Version'] = ".".join([str(_) for _ in version.__version__]) | ||||||
|  |  | ||||||
|  |         return response | ||||||
| @@ -114,7 +114,9 @@ REST_FRAMEWORK = { | |||||||
|         'rest_framework.authentication.TokenAuthentication' |         'rest_framework.authentication.TokenAuthentication' | ||||||
|     ], |     ], | ||||||
|     'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', |     'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', | ||||||
|     'DEFAULT_VERSION': 'v1', |     'DEFAULT_VERSION': '1', | ||||||
|  |     # Make sure these are ordered and that the most recent version appears | ||||||
|  |     # last | ||||||
|     'ALLOWED_VERSIONS': ['1', '2'] |     'ALLOWED_VERSIONS': ['1', '2'] | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -131,6 +133,7 @@ MIDDLEWARE = [ | |||||||
|     'django.middleware.locale.LocaleMiddleware', |     'django.middleware.locale.LocaleMiddleware', | ||||||
|     'django.middleware.common.CommonMiddleware', |     'django.middleware.common.CommonMiddleware', | ||||||
|     'django.middleware.csrf.CsrfViewMiddleware', |     'django.middleware.csrf.CsrfViewMiddleware', | ||||||
|  |     'paperless.middleware.ApiVersionMiddleware', | ||||||
|     'django.contrib.auth.middleware.AuthenticationMiddleware', |     'django.contrib.auth.middleware.AuthenticationMiddleware', | ||||||
|     'django.contrib.messages.middleware.MessageMiddleware', |     'django.contrib.messages.middleware.MessageMiddleware', | ||||||
|     'django.middleware.clickjacking.XFrameOptionsMiddleware', |     'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 jonaswinkler
					jonaswinkler