WIP: add asn prefix

This commit is contained in:
Jakob Englisch 2025-01-05 18:58:29 +01:00
parent aef68f0b41
commit d974bc542e
16 changed files with 179 additions and 12 deletions

View File

@ -12,6 +12,14 @@
</div>
<div class="position-relative" [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error">
@if (prefix) {
<ng-select class="form-control"
[items]="prefix"
bindLabel="name"
bindValue="id"
[(ngModel)]="prefixSelect"
></ng-select>
}
<input #inputField type="number" class="form-control" [step]="step" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
@if (showAdd) {
<button class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="disabled">+1</button>

View File

@ -2,11 +2,13 @@ import { Component, forwardRef, Input } from '@angular/core'
import {
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
ReactiveFormsModule
} from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DocumentService } from 'src/app/services/rest/document.service'
import { AbstractInputComponent } from '../abstract-input'
import { NgSelectModule } from '@ng-select/ng-select'
import { AsnPrefix } from "../../../../data/asn-prefix";
@Component({
providers: [
@ -19,12 +21,18 @@ import { AbstractInputComponent } from '../abstract-input'
selector: 'pngx-input-number',
templateUrl: './number.component.html',
styleUrls: ['./number.component.scss'],
imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule],
imports: [FormsModule, ReactiveFormsModule, NgxBootstrapIconsModule, NgSelectModule],
})
export class NumberComponent extends AbstractInputComponent<number> {
@Input()
showAdd: boolean = true
@Input()
prefix: AsnPrefix[]
@Input()
prefixSelect: number
@Input()
step: number = 1

View File

@ -106,7 +106,7 @@
<ng-template ngbNavContent>
<div>
<pngx-input-text #inputTitle i18n-title title="Title" formControlName="title" [horizontal]="true" (keyup)="titleKeyUp($event)" [error]="error?.title"></pngx-input-text>
<pngx-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" [horizontal]="true" formControlName='archive_serial_number'></pngx-input-number>
<pngx-input-number [prefix]="asnPrefix" [prefixSelect]="archive_serial_number_prefix" i18n-title title="Archive serial number" [error]="error?.archive_serial_number" [horizontal]="true" formControlName='archive_serial_number'></pngx-input-number>
<pngx-input-date i18n-title title="Date created" formControlName="created_date" [suggestions]="suggestions?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
[error]="error?.created_date"></pngx-input-date>
<pngx-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.Correspondent)"

View File

@ -67,6 +67,7 @@ import {
PermissionsService,
PermissionType,
} from 'src/app/services/permissions.service'
import { AsnPrefixService } from 'src/app/services/rest/asn-prefix.service'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
@ -102,6 +103,7 @@ import { DocumentHistoryComponent } from '../document-history/document-history.c
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
import { AsnPrefix } from "../../data/asn-prefix";
enum DocumentDetailNavIDs {
Details = 1,
@ -171,8 +173,7 @@ enum ZoomSetting {
})
export class DocumentDetailComponent
extends ComponentWithPermissions
implements OnInit, OnDestroy, DirtyComponent
{
implements OnInit, OnDestroy, DirtyComponent {
@ViewChild('inputTitle')
titleInput: TextComponent
@ -200,6 +201,7 @@ export class DocumentDetailComponent
tiffURL: string
tiffError: string
asnPrefix: AsnPrefix[]
correspondents: Correspondent[]
documentTypes: DocumentType[]
storagePaths: StoragePath[]
@ -211,6 +213,7 @@ export class DocumentDetailComponent
correspondent: new FormControl(),
document_type: new FormControl(),
storage_path: new FormControl(),
archive_serial_number_prefix: new FormControl(),
archive_serial_number: new FormControl(),
tags: new FormControl([]),
permissions_form: new FormControl(null),
@ -258,6 +261,7 @@ export class DocumentDetailComponent
constructor(
private documentsService: DocumentService,
private route: ActivatedRoute,
private asnPrefixService: AsnPrefixService,
private correspondentService: CorrespondentService,
private documentTypeService: DocumentTypeService,
private router: Router,
@ -347,6 +351,17 @@ export class DocumentDetailComponent
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe((result) => (this.correspondents = result.results))
}
if (
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.AsnPrefix
)
) {
this.asnPrefixService
.listAll()
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe((result) => (this.asnPrefix = result.results))
}
if (
this.permissionsService.currentUserCan(
PermissionAction.View,
@ -403,9 +418,8 @@ export class DocumentDetailComponent
this.previewText = res.toString()
},
error: (err) => {
this.previewText = $localize`An error occurred loading content: ${
err.message ?? err.toString()
}`
this.previewText = $localize`An error occurred loading content: ${err.message ?? err.toString()
}`
},
})
this.thumbUrl = this.documentsService.getThumbUrl(documentId)
@ -449,7 +463,7 @@ export class DocumentDetailComponent
this.documentForm.get('permissions_form').value['owner']
openDocument['permissions'] =
this.documentForm.get('permissions_form').value[
'set_permissions'
'set_permissions'
]
delete openDocument['permissions_form']
}
@ -494,6 +508,7 @@ export class DocumentDetailComponent
correspondent: doc.correspondent,
document_type: doc.document_type,
storage_path: doc.storage_path,
archive_serial_number_prefix: doc.archive_serial_number_prefix,
archive_serial_number: doc.archive_serial_number,
tags: [...doc.tags],
permissions_form: {
@ -793,7 +808,7 @@ export class DocumentDetailComponent
save(close: boolean = false) {
this.networkActive = true
;(document.activeElement as HTMLElement)?.dispatchEvent(new Event('change'))
; (document.activeElement as HTMLElement)?.dispatchEvent(new Event('change'))
this.documentsService
.update(this.document)
.pipe(first())
@ -1046,7 +1061,7 @@ export class DocumentDetailComponent
this.previewZoomScale = ZoomSetting.PageWidth
this.previewZoomSetting =
Object.values(ZoomSetting)[
Math.min(Object.values(ZoomSetting).length - 1, currentIndex + 1)
Math.min(Object.values(ZoomSetting).length - 1, currentIndex + 1)
]
}

View File

@ -0,0 +1,9 @@
import {ObjectWithId} from "./object-with-id";
export interface AsnPrefix extends ObjectWithId {
name?: string
slug?: string
document_count?: number
}

View File

@ -1,4 +1,5 @@
import { Observable } from 'rxjs'
import { AsnPrefix } from './asn-prefix'
import { Correspondent } from './correspondent'
import { CustomFieldInstance } from './custom-field-instance'
import { DocumentNote } from './document-note'
@ -118,6 +119,10 @@ export interface SearchHit {
}
export interface Document extends ObjectWithPermissions {
archive_serial_number_prefix$?: Observable<AsnPrefix>
archive_serial_number_prefix?: number
correspondent$?: Observable<Correspondent>
correspondent?: number

View File

@ -12,6 +12,7 @@ export enum PermissionAction {
export enum PermissionType {
Document = '%s_document',
Tag = '%s_tag',
AsnPrefix = '%s_asnprefix',
Correspondent = '%s_correspondent',
DocumentType = '%s_documenttype',
StoragePath = '%s_storagepath',

View File

@ -0,0 +1,13 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { AsnPrefix } from 'src/app/data/asn-prefix'
import { AbstractNameFilterService } from './abstract-name-filter-service'
@Injectable({
providedIn: 'root',
})
export class AsnPrefixService extends AbstractNameFilterService<AsnPrefix> {
constructor(http: HttpClient) {
super(http, 'asn_prefix')
}
}

View File

@ -2,6 +2,7 @@ from django.conf import settings
from django.contrib import admin
from guardian.admin import GuardedModelAdmin
from documents.models import ArchiveSerialNumberPrefix
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import CustomFieldInstance
@ -38,6 +39,10 @@ class DocumentTypeAdmin(GuardedModelAdmin):
list_filter = ("matching_algorithm",)
list_editable = ("match", "matching_algorithm")
class ArchiveSerialNumberPrefixAdmin(GuardedModelAdmin):
list_display = ("name", "owner")
list_filter = ("name",)
#list_editable = ("name",)
class DocumentAdmin(GuardedModelAdmin):
search_fields = ("correspondent__name", "title", "content", "tags__name")
@ -60,6 +65,7 @@ class DocumentAdmin(GuardedModelAdmin):
list_filter = (
("mime_type"),
("archive_serial_number_prefix", admin.EmptyFieldListFilter),
("archive_serial_number", admin.EmptyFieldListFilter),
("archive_filename", admin.EmptyFieldListFilter),
)
@ -195,6 +201,7 @@ class CustomFieldInstancesAdmin(GuardedModelAdmin):
admin.site.register(Correspondent, CorrespondentAdmin)
admin.site.register(Tag, TagAdmin)
admin.site.register(DocumentType, DocumentTypeAdmin)
admin.site.register(ArchiveSerialNumberPrefix, ArchiveSerialNumberPrefixAdmin)
admin.site.register(Document, DocumentAdmin)
admin.site.register(SavedView, SavedViewAdmin)
admin.site.register(StoragePath, StoragePathAdmin)

View File

@ -28,6 +28,7 @@ from rest_framework import serializers
from rest_framework.filters import OrderingFilter
from rest_framework_guardian.filters import ObjectPermissionsFilter
from documents.models import ArchiveSerialNumberPrefix
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import CustomFieldInstance
@ -65,6 +66,14 @@ class TagFilterSet(FilterSet):
}
class ArchiveSerialNumberPrefixFilterSet(FilterSet):
class Meta:
model = ArchiveSerialNumberPrefix
fields = {
"id": ID_KWARGS,
"name": CHAR_KWARGS,
}
class DocumentTypeFilterSet(FilterSet):
class Meta:
model = DocumentType

View File

@ -245,6 +245,7 @@ class DelayedQuery:
"title": "title",
"correspondent__name": "correspondent",
"document_type__name": "type",
"archive_serial_number_prefix": "asn_prefix",
"archive_serial_number": "asn",
"num_notes": "num_notes",
"owner": "owner",

View File

@ -0,0 +1,32 @@
# Generated by Django 5.1.4 on 2025-01-05 16:53
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('documents', '1060_alter_customfieldinstance_value_select'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ArchiveSerialNumberPrefix',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=6, verbose_name='name')),
('owner', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='owner')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='document',
name='archive_serial_number_prefix',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documents', to='documents.archiveserialnumberprefix', verbose_name='archive serial number prefix'),
),
]

View File

@ -95,6 +95,13 @@ class MatchingModel(ModelWithOwner):
return self.name
class ArchiveSerialNumberPrefix(ModelWithOwner):
name = models.CharField(_("name"), max_length=6)
def __str__(self):
return self.name
class Correspondent(MatchingModel):
class Meta(MatchingModel.Meta):
verbose_name = _("correspondent")
@ -272,6 +279,15 @@ class Document(SoftDeleteModel, ModelWithOwner):
help_text=_("The original name of the file when it was uploaded"),
)
archive_serial_number_prefix = models.ForeignKey(
ArchiveSerialNumberPrefix,
blank=True,
null=True,
related_name="documents",
on_delete=models.SET_NULL,
verbose_name=_("archive serial number prefix"),
)
ARCHIVE_SERIAL_NUMBER_MIN: Final[int] = 0
ARCHIVE_SERIAL_NUMBER_MAX: Final[int] = 0xFF_FF_FF_FF

View File

@ -34,6 +34,7 @@ if settings.AUDIT_LOG_ENABLED:
from documents import bulk_edit
from documents.data_models import DocumentSource
from documents.models import ArchiveSerialNumberPrefix
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import CustomFieldInstance
@ -342,6 +343,22 @@ class OwnedObjectListSerializer(serializers.ListSerializer):
return super().to_representation(documents)
class ArchiveSerialNumberPrefixSerializer(MatchingModelSerializer, OwnedObjectSerializer):
class Meta:
model = ArchiveSerialNumberPrefix
fields = (
"id",
"slug",
"name",
"document_count",
"owner",
"permissions",
"user_can_change",
"set_permissions",
)
class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer):
last_correspondence = serializers.DateTimeField(read_only=True, required=False)
@ -362,7 +379,6 @@ class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer):
"set_permissions",
)
class DocumentTypeSerializer(MatchingModelSerializer, OwnedObjectSerializer):
class Meta:
model = DocumentType
@ -474,6 +490,9 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
raise serializers.ValidationError(_("Invalid color."))
return color
class ArchiveSerialNumberPrefixField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
return ArchiveSerialNumberPrefix.objects.all()
class CorrespondentField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
@ -778,6 +797,7 @@ class DocumentSerializer(
NestedUpdateMixin,
DynamicFieldsModelSerializer,
):
archive_serial_number_prefix = ArchiveSerialNumberPrefixField(allow_null=True)
correspondent = CorrespondentField(allow_null=True)
tags = TagsField(many=True)
document_type = DocumentTypeField(allow_null=True)
@ -832,6 +852,7 @@ class DocumentSerializer(
and len(str(attrs["archive_serial_number"])) > 0
and Document.deleted_objects.filter(
archive_serial_number=attrs["archive_serial_number"],
#archive_serial_number_prefix__prefix=attrs["archive_serial_number_prefix"], # TODO: asn type empty is allowed
).exists()
):
raise serializers.ValidationError(
@ -934,6 +955,7 @@ class DocumentSerializer(
"modified",
"added",
"deleted_at",
"archive_serial_number_prefix",
"archive_serial_number",
"original_file_name",
"archived_file_name",

View File

@ -93,6 +93,7 @@ from documents.conditionals import thumbnail_last_modified
from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides
from documents.data_models import DocumentSource
from documents.filters import ArchiveSerialNumberPrefixFilterSet
from documents.filters import CorrespondentFilterSet
from documents.filters import CustomFieldFilterSet
from documents.filters import DocumentFilterSet
@ -107,6 +108,7 @@ from documents.matching import match_correspondents
from documents.matching import match_document_types
from documents.matching import match_storage_paths
from documents.matching import match_tags
from documents.models import ArchiveSerialNumberPrefix
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import Document
@ -130,6 +132,7 @@ from documents.permissions import get_objects_for_user_owner_aware
from documents.permissions import has_perms_owner_aware
from documents.permissions import set_permissions_for_object
from documents.serialisers import AcknowledgeTasksViewSerializer
from documents.serialisers import ArchiveSerialNumberPrefixSerializer
from documents.serialisers import BulkDownloadSerializer
from documents.serialisers import BulkEditObjectsSerializer
from documents.serialisers import BulkEditSerializer
@ -258,6 +261,22 @@ class PermissionsAwareDocumentCountMixin(PassUserMixin):
)
class ArchiveSerialNumberPrefixViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin):
model = ArchiveSerialNumberPrefix
queryset = ArchiveSerialNumberPrefix.objects.select_related("owner").order_by(Lower("name"))
serializer_class = ArchiveSerialNumberPrefixSerializer
pagination_class = StandardPagination
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (
DjangoFilterBackend,
OrderingFilter,
ObjectOwnedOrGrantedPermissionsFilter,
)
filterset_class = ArchiveSerialNumberPrefixFilterSet
ordering_fields = ("name")
class CorrespondentViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin):
model = Correspondent

View File

@ -17,6 +17,7 @@ from django.views.static import serve
from rest_framework.authtoken import views
from rest_framework.routers import DefaultRouter
from documents.views import ArchiveSerialNumberPrefixViewSet
from documents.views import BulkDownloadView
from documents.views import BulkEditObjectsView
from documents.views import BulkEditView
@ -59,6 +60,7 @@ from paperless_mail.views import MailRuleViewSet
from paperless_mail.views import OauthCallbackView
api_router = DefaultRouter()
api_router.register(r"asn_prefix", ArchiveSerialNumberPrefixViewSet)
api_router.register(r"correspondents", CorrespondentViewSet)
api_router.register(r"document_types", DocumentTypeViewSet)
api_router.register(r"documents", UnifiedSearchViewSet)