]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
add comment function
authortim-vogel <timvogel@gmx.net>
Sun, 7 Aug 2022 19:41:30 +0000 (12:41 -0700)
committerMichael Shamoon <4887959+shamoon@users.noreply.github.com>
Wed, 24 Aug 2022 02:19:21 +0000 (19:19 -0700)
20 files changed:
src-ui/src/app/app.module.ts
src-ui/src/app/components/document-comment/document-comment.component.html [new file with mode: 0644]
src-ui/src/app/components/document-comment/document-comment.component.scss [new file with mode: 0644]
src-ui/src/app/components/document-comment/document-comment.component.spec.ts [new file with mode: 0644]
src-ui/src/app/components/document-comment/document-comment.component.ts [new file with mode: 0644]
src-ui/src/app/components/document-detail/document-detail.component.html
src-ui/src/app/components/document-detail/document-detail.component.ts
src-ui/src/app/data/paperless-document-comment.ts [new file with mode: 0644]
src-ui/src/app/data/paperless-environment.ts [new file with mode: 0644]
src-ui/src/app/data/user-type.ts [new file with mode: 0644]
src-ui/src/app/services/rest/document-comment.service.spec.ts [new file with mode: 0644]
src-ui/src/app/services/rest/document-comment.service.ts [new file with mode: 0644]
src-ui/src/app/services/rest/environment.service.spec.ts [new file with mode: 0644]
src-ui/src/app/services/rest/environment.service.ts [new file with mode: 0644]
src/documents/management/commands/document_exporter.py
src/documents/migrations/1023_add_comments.py [new file with mode: 0644]
src/documents/models.py
src/documents/views.py
src/paperless/settings.py
src/paperless/urls.py

index edbd261f6f517c2ddab0ad4666d96a89d25fef04..dd34724a65692bd7ea1fd1969a85aefdf94c8410 100644 (file)
@@ -67,6 +67,7 @@ import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'
 import { ColorSliderModule } from 'ngx-color/slider'
 import { ColorComponent } from './components/common/input/color/color.component'
 import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
+import { DocumentCommentComponent } from './components/document-comment/document-comment.component';
 import { DirtyDocGuard } from './guards/dirty-doc.guard'
 
 import localeBe from '@angular/common/locales/be'
@@ -173,6 +174,7 @@ function initializeApp(settings: SettingsService) {
     DateComponent,
     ColorComponent,
     DocumentAsnComponent,
+    DocumentCommentComponent,
     TasksComponent,
   ],
   imports: [
diff --git a/src-ui/src/app/components/document-comment/document-comment.component.html b/src-ui/src/app/components/document-comment/document-comment.component.html
new file mode 100644 (file)
index 0000000..6a36f99
--- /dev/null
@@ -0,0 +1,25 @@
+<div *ngIf="comments">
+    <form [formGroup]='commentForm'>
+        <div class="form-group">
+            <textarea class="form-control" id="newcomment" rows="5" formControlName='newcomment'></textarea>
+        </div>
+        <button type="button" class="btn btn-primary" i18n [disabled]="networkActive" (click)="addComment()">add comment</button>&nbsp;
+    </form>
+    <hr>
+    <div *ngFor="let comment of comments; trackBy: byId" [disableRipple]="true" class="card border-bg-primary bg-primary mb-3 comment-card" [attr.comment-id]="comment.id">
+        <div class="d-flex card-header comment-card-header text-white justify-content-between">
+            <span>{{comment?.user?.firstname}} {{comment?.user?.lastname}} ({{comment?.user?.username}}) - {{ comment?.created | customDate}}</span>
+            <span>
+                <a class="text-white" (click)="deleteComment($event)">
+                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
+                        <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
+                        <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
+                    </svg>
+                </a>
+            </span>
+        </div>
+        <div class="card-body bg-white text-dark comment-card-body card-text">
+            {{comment.comment}}
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/src-ui/src/app/components/document-comment/document-comment.component.scss b/src-ui/src/app/components/document-comment/document-comment.component.scss
new file mode 100644 (file)
index 0000000..7785564
--- /dev/null
@@ -0,0 +1,22 @@
+.comment-card-body {
+    padding-top: .8rem !important;
+    padding-bottom: .8rem !important;
+    max-height: 10rem;
+    overflow: scroll;
+    white-space: pre-wrap;
+}
+
+.comment-card-header a {
+    border: none;
+    background: none;
+    padding: 5px;
+    border-radius: 50%;
+}
+
+.comment-card-header a:hover {
+    background: #FFF;
+}
+
+.comment-card-header a:hover svg {
+    fill: var(--primary);
+} 
\ No newline at end of file
diff --git a/src-ui/src/app/components/document-comment/document-comment.component.spec.ts b/src-ui/src/app/components/document-comment/document-comment.component.spec.ts
new file mode 100644 (file)
index 0000000..1f6389b
--- /dev/null
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DocumentCommentComponent } from './document-comment.component';
+
+describe('DocumentCommentComponent', () => {
+  let component: DocumentCommentComponent;
+  let fixture: ComponentFixture<DocumentCommentComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ DocumentCommentComponent ]
+    })
+    .compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(DocumentCommentComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
\ No newline at end of file
diff --git a/src-ui/src/app/components/document-comment/document-comment.component.ts b/src-ui/src/app/components/document-comment/document-comment.component.ts
new file mode 100644 (file)
index 0000000..57b052d
--- /dev/null
@@ -0,0 +1,63 @@
+import { Component, OnInit } from '@angular/core';
+import { DocumentDetailComponent } from 'src/app/components/document-detail/document-detail.component';
+import { DocumentCommentService } from 'src/app/services/rest/document-comment.service';
+import { PaperlessDocumentComment } from 'src/app/data/paperless-document-comment';
+
+import { take } from 'rxjs/operators';
+import { FormControl, FormGroup } from '@angular/forms';
+
+@Component({
+  selector: 'app-document-comment',
+  templateUrl: './document-comment.component.html',
+  styleUrls: ['./document-comment.component.scss']
+})
+export class DocumentCommentComponent implements OnInit {
+
+  comments:PaperlessDocumentComment[];
+  networkActive = false;
+  documentId: number;
+  commentForm: FormGroup = new FormGroup({
+    newcomment: new FormControl('')
+  })
+
+  constructor(
+    private documentDetailComponent: DocumentDetailComponent,
+    private documentCommentService: DocumentCommentService,
+  ) { }
+
+  byId(index, item: PaperlessDocumentComment) {
+    return item.id;
+  }
+
+  async ngOnInit(): Promise<any> {
+    try {
+      this.documentId = this.documentDetailComponent.documentId;
+      this.comments= await this.documentCommentService.getComments(this.documentId).pipe(take(1)).toPromise();
+    } catch(err){
+      this.comments = [];
+    }
+  }
+
+  addComment(){
+    this.networkActive = true
+    this.documentCommentService.addComment(this.documentId, this.commentForm.get("newcomment").value).subscribe(result => {
+      this.comments = result;
+      this.commentForm.get("newcomment").reset();
+      this.networkActive = false;
+    }, error => {
+      this.networkActive = false;
+    });
+  }
+
+  deleteComment(event){
+    let parent = event.target.parentElement.closest('div[comment-id]');
+    if(parent){
+      this.documentCommentService.deleteComment(this.documentId, parseInt(parent.getAttribute("comment-id"))).subscribe(result => {
+        this.comments = result;
+        this.networkActive = false;
+      }, error => {
+        this.networkActive = false;
+      });
+    }
+  }
+}
\ No newline at end of file
index 764a587e0f2715ab9b9cfa7b4dc66912bdab4e9c..ebf28689561ba85b5ffd428f16d19978f4b6867d 100644 (file)
                         </div>
                     </ng-template>
                 </li>
+                <li [ngbNavItem]="5" *ngIf="isCommentsEnabled">
+                    <a ngbNavLink i18n>Comments</a>
+                    <ng-template ngbNavContent>
+                        <app-document-comment #commentComponent></app-document-comment>
+                    </ng-template>
+
+                </li>
             </ul>
 
             <div [ngbNavOutlet]="nav" class="mt-2"></div>
index 203a56f0447ec9e3cc1871b1b1fc0984e3e0cdca..d0f4ecded0fdf0605d1bdb762a4fa473ebbb72d3 100644 (file)
@@ -35,6 +35,7 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service'
 import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
 import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
 import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
+import { EnvironmentService } from 'src/app/services/rest/environment.service'
 
 @Component({
   selector: 'app-document-detail',
@@ -83,6 +84,8 @@ export class DocumentDetailComponent
   previewCurrentPage: number = 1
   previewNumPages: number = 1
 
+  isCommentsEnabled:boolean = false
+
   store: BehaviorSubject<any>
   isDirty$: Observable<boolean>
   unsubscribeNotifier: Subject<any> = new Subject()
@@ -118,7 +121,8 @@ export class DocumentDetailComponent
     private documentTitlePipe: DocumentTitlePipe,
     private toastService: ToastService,
     private settings: SettingsService,
-    private storagePathService: StoragePathService
+    private storagePathService: StoragePathService,
+    private environment: EnvironmentService
   ) {}
 
   titleKeyUp(event) {
@@ -274,6 +278,13 @@ export class DocumentDetailComponent
           this.suggestions = null
         },
       })
+    
+    this.environment.get("PAPERLESS_COMMENTS_ENABLED").subscribe(result => {
+      this.isCommentsEnabled = (result.value.toString().toLowerCase() === "true"?true:false);
+    }, error => {
+      this.isCommentsEnabled = false;
+    })
+
     this.title = this.documentTitlePipe.transform(doc.title)
     this.documentForm.patchValue(doc)
   }
diff --git a/src-ui/src/app/data/paperless-document-comment.ts b/src-ui/src/app/data/paperless-document-comment.ts
new file mode 100644 (file)
index 0000000..1b60e6c
--- /dev/null
@@ -0,0 +1,8 @@
+import { ObjectWithId } from './object-with-id'
+import { CommentUser } from './user-type'
+
+export interface PaperlessDocumentComment extends ObjectWithId {
+    created?: Date
+    comment?: string
+    user?: CommentUser
+} 
\ No newline at end of file
diff --git a/src-ui/src/app/data/paperless-environment.ts b/src-ui/src/app/data/paperless-environment.ts
new file mode 100644 (file)
index 0000000..27dda64
--- /dev/null
@@ -0,0 +1,3 @@
+export interface PaperlessEnvironment {
+    value?: string;
+}
\ No newline at end of file
diff --git a/src-ui/src/app/data/user-type.ts b/src-ui/src/app/data/user-type.ts
new file mode 100644 (file)
index 0000000..9324cab
--- /dev/null
@@ -0,0 +1,7 @@
+import { ObjectWithId } from './object-with-id'
+
+export interface CommentUser extends ObjectWithId {
+    username: string
+    firstname: string
+    lastname: string
+} 
\ No newline at end of file
diff --git a/src-ui/src/app/services/rest/document-comment.service.spec.ts b/src-ui/src/app/services/rest/document-comment.service.spec.ts
new file mode 100644 (file)
index 0000000..112144b
--- /dev/null
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { DocumentCommentService } from './document-comment.service';
+
+describe('DocumentCommentService', () => {
+  let service: DocumentCommentService;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+    service = TestBed.inject(DocumentCommentService);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+});
\ No newline at end of file
diff --git a/src-ui/src/app/services/rest/document-comment.service.ts b/src-ui/src/app/services/rest/document-comment.service.ts
new file mode 100644 (file)
index 0000000..b5739d6
--- /dev/null
@@ -0,0 +1,31 @@
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { PaperlessDocumentComment } from 'src/app/data/paperless-document-comment';
+import { AbstractPaperlessService } from './abstract-paperless-service';
+import { Observable } from 'rxjs';
+import { PaperlessDocumentCommentFrame } from 'src/app/data/paperless-document-comment-frame';
+
+@Injectable({
+    providedIn: 'root'
+})
+export class DocumentCommentService extends AbstractPaperlessService<PaperlessDocumentComment> {
+
+  constructor(http: HttpClient) {
+    super(http, 'documents')
+  }
+
+
+  getComments(id: number): Observable<PaperlessDocumentComment> {
+    return this.http.get<PaperlessDocumentComment[]>(this.getResourceUrl(id, "comments"))
+  }
+
+  addComment(id: number, comment): Observable<PaperlessDocumentComment[]>{
+    return this.http.post<PaperlessDocumentComment[]>(this.getResourceUrl(id, 'comments'), {"payload": comment})
+  }
+
+  deleteComment(documentId: number, commentId: number): Observable<PaperlessDocumentComment[]>{
+    let httpParams = new HttpParams();
+    httpParams = httpParams.set("commentId", commentId.toString());
+    return this.http.delete<PaperlessDocumentComment[]>(this.getResourceUrl(documentId, 'comments'), {params: httpParams});
+  }
+} 
\ No newline at end of file
diff --git a/src-ui/src/app/services/rest/environment.service.spec.ts b/src-ui/src/app/services/rest/environment.service.spec.ts
new file mode 100644 (file)
index 0000000..941a180
--- /dev/null
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { EnvironmentService } from './environment.service';
+
+describe('EnvironmentService', () => {
+  let service: EnvironmentService;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+    service = TestBed.inject(EnvironmentService);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+});
\ No newline at end of file
diff --git a/src-ui/src/app/services/rest/environment.service.ts b/src-ui/src/app/services/rest/environment.service.ts
new file mode 100644 (file)
index 0000000..86ac146
--- /dev/null
@@ -0,0 +1,22 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { PaperlessEnvironment } from 'src/app/data/paperless-environment';
+import { environment } from 'src/environments/environment'
+
+@Injectable({
+  providedIn: 'root'
+})
+export class EnvironmentService {
+
+  protected baseUrl: string = environment.apiBaseUrl
+
+  constructor(protected http: HttpClient) { }
+
+  get(environment: string): Observable<PaperlessEnvironment> {
+    let httpParams = new HttpParams();
+    httpParams = httpParams.set('name', environment);
+
+    return this.http.get<PaperlessEnvironment>(`${this.baseUrl}environment/`, {params: httpParams})
+  }
+}
\ No newline at end of file
index 526d59368a6cebb109b44503ed177354c307e18a..da00d10f54b6013ff7ce93348aea9c6ed66ddeaf 100644 (file)
@@ -12,6 +12,7 @@ from django.core import serializers
 from django.core.management.base import BaseCommand
 from django.core.management.base import CommandError
 from django.db import transaction
+from documents.models import Comment
 from documents.models import Correspondent
 from documents.models import Document
 from documents.models import DocumentType
@@ -126,6 +127,9 @@ class Command(BaseCommand):
                 serializers.serialize("json", DocumentType.objects.all()),
             )
 
+            manifest += json.loads(
+                serializers.serialize("json", Comment.objects.all())),
+
             documents = Document.objects.order_by("id")
             document_map = {d.pk: d for d in documents}
             document_manifest = json.loads(serializers.serialize("json", documents))
diff --git a/src/documents/migrations/1023_add_comments.py b/src/documents/migrations/1023_add_comments.py
new file mode 100644 (file)
index 0000000..8ae779e
--- /dev/null
@@ -0,0 +1,19 @@
+from django.db import migrations, models
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('documents', '1016_auto_20210317_1351'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Comment',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('comment', models.TextField()),
+                ('created', models.DateTimeField(auto_now_add=True)),
+                ('document_id', models.PositiveIntegerField()),
+                ('user_id', models.PositiveIntegerField())
+            ],
+        )
+    ] 
\ No newline at end of file
index f6df273adb016244ecb2a12f1b9db0b9cb509410..cb64946c3b90d84b9d834bc24811cb7668bc7613 100644 (file)
@@ -537,3 +537,40 @@ class PaperlessTask(models.Model):
         blank=True,
     )
     acknowledged = models.BooleanField(default=False)
+
+class Comment(models.Model):
+    comment = models.TextField(
+        _("content"),
+        blank=True,
+        help_text=_("Comment for the document")
+    )
+
+    created = models.DateTimeField(
+        _("created"),
+        default=timezone.now, db_index=True)
+
+    document = models.ForeignKey(
+        Document,
+        blank=True,
+        null=True,
+        related_name="documents",
+        on_delete=models.CASCADE,
+        verbose_name=_("document")
+    )
+
+    user = models.ForeignKey(
+        User,
+        blank=True,
+        null=True,
+        related_name="users",
+        on_delete=models.SET_NULL,
+        verbose_name=_("user")
+    )
+
+    class Meta:
+        ordering = ("created",)
+        verbose_name = _("comment")
+        verbose_name_plural = _("comments")
+
+    def __str__(self):
+        return self.content
\ No newline at end of file
index b261f37fdfd3f61f4f0c7a6a2bcb3fefa31dc79d..4633642e6829cda8c26997e09cba13fb70feba56 100644 (file)
@@ -21,6 +21,8 @@ from django.db.models.functions import Lower
 from django.http import Http404
 from django.http import HttpResponse
 from django.http import HttpResponseBadRequest
+from django.http import HttpResponseNotAllowed
+from django.http import HttpResponseNotFound
 from django.utils.decorators import method_decorator
 from django.utils.translation import get_language
 from django.views.decorators.cache import cache_control
@@ -62,6 +64,7 @@ from .matching import match_correspondents
 from .matching import match_document_types
 from .matching import match_storage_paths
 from .matching import match_tags
+from .models import Comment
 from .models import Correspondent
 from .models import Document
 from .models import DocumentType
@@ -379,6 +382,61 @@ class DocumentViewSet(
         except (FileNotFoundError, Document.DoesNotExist):
             raise Http404()
 
+    def getComments(self, doc):
+        return [
+            {
+                "id":c.id, 
+                "comment":c.comment, 
+                "created":c.created, 
+                "user":{ 
+                    "id":c.user.id, 
+                    "username": c.user.username,
+                    "firstname":c.user.first_name, 
+                    "lastname":c.user.last_name
+                }
+            } for c in Comment.objects.filter(document=doc).order_by('-created')
+        ];
+
+    @action(methods=['get', 'post', 'delete'], detail=True)
+    def comments(self, request, pk=None):
+        if settings.PAPERLESS_COMMENTS_ENABLED != True:
+            return HttpResponseNotAllowed("comment function is disabled")
+
+        try:
+            doc = Document.objects.get(pk=pk)
+        except Document.DoesNotExist:
+            raise Http404()
+
+        currentUser = request.user;
+
+        if request.method == 'GET': 
+            try:
+                return Response(self.getComments(doc));
+            except Exception as e:
+                return Response({"error": str(e)});
+        elif request.method == 'POST':
+            try:
+                c = Comment.objects.create(
+                    document = doc,
+                    comment=request.data["payload"],
+                    user=currentUser
+                );
+                c.save();
+
+                return Response(self.getComments(doc));
+            except Exception as e:
+                return Response({
+                    "error": str(e)
+                });
+        elif request.method == 'DELETE':
+            comment = Comment.objects.get(id=int(request.GET.get("commentId")));
+            comment.delete();
+            return Response(self.getComments(doc));
+
+        return Response({
+            "error": "error"
+        });
+
 
 class SearchResultSerializer(DocumentSerializer):
     def to_representation(self, instance):
@@ -835,3 +893,32 @@ class AcknowledgeTasksView(GenericAPIView):
             return Response({"result": result})
         except Exception:
             return HttpResponseBadRequest()
+
+class EnvironmentView(APIView):
+
+    permission_classes = (IsAuthenticated,)
+
+    def get(self, request, format=None):
+        if 'name' in request.query_params:
+            name = request.query_params['name']
+        else:
+            return HttpResponseBadRequest("name required")
+
+        if(name not in settings.PAPERLESS_FRONTEND_ALLOWED_ENVIRONMENTS and settings.PAPERLESS_DISABLED_FRONTEND_ENVIRONMENT_CHECK == False):
+            return HttpResponseNotAllowed("environment not allowed to request")
+
+        value = None
+        try:
+            value = getattr(settings, name)
+        except:
+            try:
+                value = os.getenv(name)
+            except:
+                value = None
+
+        if value == None: 
+            return HttpResponseNotFound("environment not found")    
+
+        return Response({
+            "value": str(value)
+        }); 
index 2ce99ac0ec2be1a34e4214dafab57352cac0b193..e42cf4359342d0b2f49a2ff16a3dcf6c96342fd2 100644 (file)
@@ -566,6 +566,14 @@ CONVERT_MEMORY_LIMIT = os.getenv("PAPERLESS_CONVERT_MEMORY_LIMIT")
 
 GS_BINARY = os.getenv("PAPERLESS_GS_BINARY", "gs")
 
+# Comment settings
+PAPERLESS_COMMENTS_ENABLED = __get_boolean("PAPERLESS_COMMENTS_ENABLED", "NO")
+
+# allowed environments for frontend
+PAPERLESS_DISABLED_FRONTEND_ENVIRONMENT_CHECK = __get_boolean("PAPERLESS_DISABLED_FRONTEND_ENVIRONMENT_CHECK", "NO")
+PAPERLESS_FRONTEND_ALLOWED_ENVIRONMENTS = [
+    "PAPERLESS_COMMENTS_ENABLED"
+]
 
 # Pre-2.x versions of Paperless stored your documents locally with GPG
 # encryption, but that is no longer the default.  This behaviour is still
index 46309e1e65043dba40524953d47b5b7687eb8e43..6ba1ee2634e6a8fd4d33fd46118e0ae95613b943 100644 (file)
@@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
 from django.views.decorators.csrf import csrf_exempt
 from django.views.generic import RedirectView
 from documents.views import AcknowledgeTasksView
+from documents.views import EnvironmentView
 from documents.views import BulkDownloadView
 from documents.views import BulkEditView
 from documents.views import CorrespondentViewSet
@@ -94,6 +95,7 @@ urlpatterns = [
                     AcknowledgeTasksView.as_view(),
                     name="acknowledge_tasks",
                 ),
+                re_path(r"^environment/", EnvironmentView.as_view()),
                 path("token/", views.obtain_auth_token),
             ]
             + api_router.urls,