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 | ||||
| was started successfully. No additional status information about the consumption | ||||
| 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 | ||||
| ################## | ||||
|  | ||||
| * `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. | ||||
|  | ||||
| @@ -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. | ||||
|  | ||||
| * Regression fix: Dates on the front end did not respect date locale settings in some cases. | ||||
|  | ||||
| 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" | ||||
|       } | ||||
|     }, | ||||
|     "@ctrl/tinycolor": { | ||||
|       "version": "3.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz", | ||||
|       "integrity": "sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ==" | ||||
|     }, | ||||
|     "@istanbuljs/schema": { | ||||
|       "version": "0.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", | ||||
| @@ -7895,6 +7900,11 @@ | ||||
|         "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": { | ||||
|       "version": "1.3.5", | ||||
|       "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": { | ||||
|       "version": "10.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-10.1.1.tgz", | ||||
|   | ||||
| @@ -26,6 +26,7 @@ | ||||
|     "file-saver": "^2.0.5", | ||||
|     "ng-bootstrap": "^1.6.3", | ||||
|     "ng2-pdf-viewer": "^6.3.2", | ||||
|     "ngx-color": "^6.2.0", | ||||
|     "ngx-cookie-service": "^10.1.1", | ||||
|     "ngx-file-drop": "^10.0.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 { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'; | ||||
| 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 localeNl from '@angular/common/locales/nl'; | ||||
| @@ -125,7 +127,8 @@ registerLocaleData(localeEnGb) | ||||
|     NumberComponent, | ||||
|     SafePipe, | ||||
|     CustomDatePipe, | ||||
|     DateComponent | ||||
|     DateComponent, | ||||
|     ColorComponent | ||||
|   ], | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
| @@ -137,7 +140,8 @@ registerLocaleData(localeEnGb) | ||||
|     NgxFileDropModule, | ||||
|     InfiniteScrollModule, | ||||
|     PdfViewerModule, | ||||
|     NgSelectModule | ||||
|     NgSelectModule, | ||||
|     ColorSliderModule | ||||
|   ], | ||||
|   providers: [ | ||||
|     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"> | ||||
|   <label [for]="inputId">{{title}}</label> | ||||
|   <div class="input-group"> | ||||
|     <input [class.is-invalid]="error" class="form-control" [placeholder]="placeholder" [id]="inputId" (dateSelect)="onChange(value)" (change)="onChange(value)" | ||||
|   <div class="input-group" [class.is-invalid]="error"> | ||||
|     <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"> | ||||
|     <div class="input-group-append"> | ||||
|       <button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button"> | ||||
| @@ -10,6 +10,7 @@ | ||||
|         </svg> | ||||
|       </button> | ||||
|     </div> | ||||
|     <div class="invalid-feedback" *ngIf="error" i18n>Invalid date.</div> | ||||
|   </div> | ||||
|   <div class="invalid-feedback" i18n>Invalid date.</div> | ||||
|   <small *ngIf="hint" class="form-text text-muted">{{hint}}</small> | ||||
| </div> | ||||
|   | ||||
| @@ -1,2 +1,2 @@ | ||||
| <span *ngIf="!clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</span> | ||||
| <a [routerLink]="" [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</a> | ||||
| <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]="tag.color" [style.color]="tag.text_color">{{tag.name}}</a> | ||||
| @@ -1,5 +1,5 @@ | ||||
| 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({ | ||||
|   selector: 'app-tag', | ||||
| @@ -22,8 +22,4 @@ export class TagComponent implements OnInit { | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
|   getColour() { | ||||
|     return TAG_COLOURS.find(c => c.id == this.tag.colour) | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -8,15 +8,7 @@ | ||||
|     <div class="modal-body"> | ||||
|       <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> | ||||
|  | ||||
|  | ||||
|       <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-color i18n-title title="Color" formControlName="color" [error]="error?.color"></app-input-color> | ||||
|  | ||||
|       <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> | ||||
|   | ||||
| @@ -2,9 +2,10 @@ import { Component } from '@angular/core'; | ||||
| import { FormControl, FormGroup } from '@angular/forms'; | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| 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 { ToastService } from 'src/app/services/toast.service'; | ||||
| import { randomColor } from 'src/app/utils/color'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-tag-edit-dialog', | ||||
| @@ -13,7 +14,7 @@ import { ToastService } from 'src/app/services/toast.service'; | ||||
| }) | ||||
| export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | ||||
|  | ||||
|   constructor(service: TagService, activeModal: NgbActiveModal, toastService: ToastService) {  | ||||
|   constructor(service: TagService, activeModal: NgbActiveModal, toastService: ToastService) { | ||||
|     super(service, activeModal, toastService) | ||||
|   } | ||||
|  | ||||
| @@ -28,7 +29,7 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | ||||
|   getForm(): FormGroup { | ||||
|     return new FormGroup({ | ||||
|       name: new FormControl(''), | ||||
|       colour: new FormControl(1), | ||||
|       color: new FormControl(randomColor()), | ||||
|       is_inbox_tag: new FormControl(false), | ||||
|       matching_algorithm: new FormControl(1), | ||||
|       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> | ||||
|     <tr *ngFor="let tag of data"> | ||||
|       <td scope="row">{{ tag.name }}</td> | ||||
|       <td scope="row"><span class="badge" [style.color]="getColor(tag.colour).textColor" | ||||
|           [style.background-color]="getColor(tag.colour).value">{{ getColor(tag.colour).name }}</span></td> | ||||
|       <td scope="row"><span class="badge" [style.color]="tag.text_color" | ||||
|           [style.background-color]="tag.color">{{tag.color}}</span></td> | ||||
|       <td scope="row">{{ getMatching(tag) }}</td> | ||||
|       <td scope="row">{{ tag.document_count }}</td> | ||||
|       <td scope="row"> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { Component } from '@angular/core'; | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| 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 { TagService } from 'src/app/services/rest/tag.service'; | ||||
| import { ToastService } from 'src/app/services/toast.service'; | ||||
| @@ -22,10 +22,6 @@ export class TagListComponent extends GenericListComponent<PaperlessTag> { | ||||
|     super(tagService, modalService, TagEditDialogComponent, toastService) | ||||
|   } | ||||
|  | ||||
|   getColor(id) { | ||||
|     return TAG_COLOURS.find(c => c.id == id) | ||||
|   } | ||||
|  | ||||
|   getDeleteMessage(object: PaperlessTag) { | ||||
|     return $localize`Do you really want to delete the tag "${object.name}"?` | ||||
|   } | ||||
|   | ||||
| @@ -1,26 +1,10 @@ | ||||
| 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"} | ||||
| ] | ||||
| import { MatchingModel } from "./matching-model"; | ||||
|  | ||||
| export interface PaperlessTag extends MatchingModel { | ||||
|  | ||||
|     colour?: number | ||||
|     color?: string | ||||
|  | ||||
|     text_color?: string | ||||
|  | ||||
|     is_inbox_tag?: boolean | ||||
|  | ||||
|   | ||||
| @@ -13,17 +13,20 @@ const FORMAT_TO_ISO_FORMAT = { | ||||
| }) | ||||
| export class CustomDatePipe extends DatePipe implements PipeTransform { | ||||
|  | ||||
|   private defaultLocale: string | ||||
|  | ||||
|   constructor(@Inject(LOCALE_ID) locale: string, private settings: SettingsService) { | ||||
|     super(locale) | ||||
|     this.defaultLocale = locale | ||||
|   } | ||||
|  | ||||
|   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) | ||||
|     if (l == "iso-8601") { | ||||
|       return super.transform(value, FORMAT_TO_ISO_FORMAT[f], timezone) | ||||
|     } 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 = { | ||||
|   production: true, | ||||
|   apiBaseUrl: "/api/", | ||||
|   apiVersion: "1", | ||||
|   apiVersion: "2", | ||||
|   appTitle: "Paperless-ng", | ||||
|   version: "1.2.1", | ||||
|   webSocketHost: window.location.host, | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| export const environment = { | ||||
|   production: false, | ||||
|   apiBaseUrl: "http://localhost:8000/api/", | ||||
|   apiVersion: "1", | ||||
|   apiVersion: "2", | ||||
|   appTitle: "Paperless-ng", | ||||
|   version: "DEVELOPMENT", | ||||
|   webSocketHost: "localhost:8000", | ||||
|   | ||||
| @@ -1729,7 +1729,7 @@ | ||||
|       </trans-unit> | ||||
|       <trans-unit datatype="html" id="90917e1a0a7bb59e9d11bdde9183e9391963e17b"> | ||||
|         <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 context-type="sourcefile">src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.html</context> | ||||
|           <context context-type="linenumber">25</context> | ||||
|   | ||||
| @@ -19,12 +19,12 @@ class TagAdmin(admin.ModelAdmin): | ||||
|  | ||||
|     list_display = ( | ||||
|         "name", | ||||
|         "colour", | ||||
|         "color", | ||||
|         "match", | ||||
|         "matching_algorithm" | ||||
|     ) | ||||
|     list_filter = ("colour", "matching_algorithm") | ||||
|     list_editable = ("colour", "match", "matching_algorithm") | ||||
|     list_filter = ("color", "matching_algorithm") | ||||
|     list_editable = ("color", "match", "matching_algorithm") | ||||
|  | ||||
|  | ||||
| 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): | ||||
|  | ||||
|     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") | ||||
|     ) | ||||
|  | ||||
|     colour = models.PositiveIntegerField( | ||||
|     color = models.CharField( | ||||
|         _("color"), | ||||
|         choices=COLOURS, default=1) | ||||
|         max_length=7, | ||||
|         default="#a6cee3" | ||||
|     ) | ||||
|  | ||||
|     is_inbox_tag = models.BooleanField( | ||||
|         _("is inbox tag"), | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import re | ||||
|  | ||||
| import magic | ||||
| import math | ||||
| from django.utils.text import slugify | ||||
| from rest_framework import serializers | ||||
| 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: | ||||
|         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): | ||||
|     def get_queryset(self): | ||||
|         return Correspondent.objects.all() | ||||
|   | ||||
| @@ -15,7 +15,7 @@ | ||||
|   <link rel="icon" type="image/x-icon" href="favicon.ico"> | ||||
|   <link rel="manifest" href="{% static webmanifest %}"> | ||||
| 	<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> | ||||
| <body> | ||||
|   <app-root>{% translate "Paperless-ng is loading..." %}</app-root> | ||||
|   | ||||
| @@ -807,6 +807,69 @@ class TestDocumentApi(DirectoriesMixin, APITestCase): | ||||
|             }, format='json') | ||||
|             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): | ||||
|  | ||||
|   | ||||
| @@ -50,6 +50,7 @@ from .parsers import get_parser_class_for_mime_type | ||||
| from .serialisers import ( | ||||
|     CorrespondentSerializer, | ||||
|     DocumentSerializer, | ||||
|     TagSerializerVersion1, | ||||
|     TagSerializer, | ||||
|     DocumentTypeSerializer, | ||||
|     PostDocumentSerializer, | ||||
| @@ -89,6 +90,7 @@ class IndexView(TemplateView): | ||||
|         context['polyfills_js'] = f"frontend/{self.get_language()}/polyfills.js"  # NOQA: E501 | ||||
|         context['main_js'] = f"frontend/{self.get_language()}/main.js" | ||||
|         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 | ||||
|  | ||||
|  | ||||
| @@ -118,7 +120,12 @@ class TagViewSet(ModelViewSet): | ||||
|     queryset = Tag.objects.annotate( | ||||
|         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 | ||||
|     permission_classes = (IsAuthenticated,) | ||||
|     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' | ||||
|     ], | ||||
|     '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'] | ||||
| } | ||||
|  | ||||
| @@ -131,6 +133,7 @@ MIDDLEWARE = [ | ||||
|     'django.middleware.locale.LocaleMiddleware', | ||||
|     'django.middleware.common.CommonMiddleware', | ||||
|     'django.middleware.csrf.CsrfViewMiddleware', | ||||
|     'paperless.middleware.ApiVersionMiddleware', | ||||
|     'django.contrib.auth.middleware.AuthenticationMiddleware', | ||||
|     'django.contrib.messages.middleware.MessageMiddleware', | ||||
|     'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 jonaswinkler
					jonaswinkler