diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts
index edbd261f6..dd34724a6 100644
--- a/src-ui/src/app/app.module.ts
+++ b/src-ui/src/app/app.module.ts
@@ -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
index 000000000..6a36f999f
--- /dev/null
+++ b/src-ui/src/app/components/document-comment/document-comment.component.html
@@ -0,0 +1,25 @@
+
\ 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
index 000000000..778556485
--- /dev/null
+++ b/src-ui/src/app/components/document-comment/document-comment.component.scss
@@ -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
index 000000000..1f6389b1d
--- /dev/null
+++ b/src-ui/src/app/components/document-comment/document-comment.component.spec.ts
@@ -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;
+
+ 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
index 000000000..57b052d84
--- /dev/null
+++ b/src-ui/src/app/components/document-comment/document-comment.component.ts
@@ -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 {
+ 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
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html
index 764a587e0..ebf286895 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.html
+++ b/src-ui/src/app/components/document-detail/document-detail.component.html
@@ -169,6 +169,13 @@
+
+ Comments
+
+
+
+
+
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts
index 203a56f04..d0f4ecded 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.ts
+++ b/src-ui/src/app/components/document-detail/document-detail.component.ts
@@ -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
isDirty$: Observable
unsubscribeNotifier: Subject = 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
index 000000000..1b60e6c5c
--- /dev/null
+++ b/src-ui/src/app/data/paperless-document-comment.ts
@@ -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
index 000000000..27dda6427
--- /dev/null
+++ b/src-ui/src/app/data/paperless-environment.ts
@@ -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
index 000000000..9324cab43
--- /dev/null
+++ b/src-ui/src/app/data/user-type.ts
@@ -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
index 000000000..112144b90
--- /dev/null
+++ b/src-ui/src/app/services/rest/document-comment.service.spec.ts
@@ -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
index 000000000..b5739d65e
--- /dev/null
+++ b/src-ui/src/app/services/rest/document-comment.service.ts
@@ -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 {
+
+ constructor(http: HttpClient) {
+ super(http, 'documents')
+ }
+
+
+ getComments(id: number): Observable {
+ return this.http.get(this.getResourceUrl(id, "comments"))
+ }
+
+ addComment(id: number, comment): Observable{
+ return this.http.post(this.getResourceUrl(id, 'comments'), {"payload": comment})
+ }
+
+ deleteComment(documentId: number, commentId: number): Observable{
+ let httpParams = new HttpParams();
+ httpParams = httpParams.set("commentId", commentId.toString());
+ return this.http.delete(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
index 000000000..941a180b9
--- /dev/null
+++ b/src-ui/src/app/services/rest/environment.service.spec.ts
@@ -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
index 000000000..86ac146ea
--- /dev/null
+++ b/src-ui/src/app/services/rest/environment.service.ts
@@ -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 {
+ let httpParams = new HttpParams();
+ httpParams = httpParams.set('name', environment);
+
+ return this.http.get(`${this.baseUrl}environment/`, {params: httpParams})
+ }
+}
\ No newline at end of file
diff --git a/src/documents/management/commands/document_exporter.py b/src/documents/management/commands/document_exporter.py
index 526d59368..da00d10f5 100644
--- a/src/documents/management/commands/document_exporter.py
+++ b/src/documents/management/commands/document_exporter.py
@@ -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
index 000000000..8ae779e66
--- /dev/null
+++ b/src/documents/migrations/1023_add_comments.py
@@ -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
diff --git a/src/documents/models.py b/src/documents/models.py
index f6df273ad..cb64946c3 100644
--- a/src/documents/models.py
+++ b/src/documents/models.py
@@ -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
diff --git a/src/documents/views.py b/src/documents/views.py
index b261f37fd..4633642e6 100644
--- a/src/documents/views.py
+++ b/src/documents/views.py
@@ -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)
+ });
diff --git a/src/paperless/settings.py b/src/paperless/settings.py
index 2ce99ac0e..e42cf4359 100644
--- a/src/paperless/settings.py
+++ b/src/paperless/settings.py
@@ -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
diff --git a/src/paperless/urls.py b/src/paperless/urls.py
index 46309e1e6..6ba1ee263 100644
--- a/src/paperless/urls.py
+++ b/src/paperless/urls.py
@@ -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,