mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-26 04:33:20 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca7c31878e | ||
|
|
c11190bb4d | ||
|
|
57d04f23cd | ||
|
|
595a3ffa03 | ||
|
|
35d40b51cb | ||
|
|
78025df405 | ||
|
|
e24db7023a | ||
|
|
f5ddc14588 |
@@ -10,16 +10,12 @@
|
||||
tourAnchor="tour.intro">
|
||||
@if (!hasCustomBranding) {
|
||||
<pngx-logo extra_classes="navbar-official-logo px-1" height="2.4rem"></pngx-logo>
|
||||
<svg class="brand-mark brand-mark-slim d-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" width="1.5em" height="1.5em" fill="currentColor">
|
||||
<path d="M341,949.1c-6.9-20.3-20.7-61.2-21.9-61-199.6-88.9-182.5-229.8-134.3-347.5,30,137.2,268.8,148.9,146.2,336-.9,2.2,10,27.8,19.5,51.3,22.7-51.9,58.6-115.5,55.8-120.8C178,398.7,724.9,299,807.1,18.5c83,251.5,53.1,659.8-377.4,814.9-2,1.4-63.5,148.6-66.9,150.2-.2-2.1-33.2,2.9-30.1-8.7,1.6-7,4.8-16.2,8.2-25.6h0v-.2h.1ZM323.1,846.2c48.3-71.9-12.7-120.8-56.9-152.2,81.2,107.4,66.4,120.8,56.9,152.2h0Z"/>
|
||||
</svg>
|
||||
<svg pngx-brand-mark class="brand-mark brand-mark-slim d-none" width="1.5em" height="1.5em"></svg>
|
||||
} @else {
|
||||
@if (customAppLogo) {
|
||||
<img class="brand-logo" [src]="customAppLogo" alt="" />
|
||||
} @else {
|
||||
<svg class="brand-mark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" width="1.5em" height="1.5em" fill="currentColor">
|
||||
<path d="M341,949.1c-6.9-20.3-20.7-61.2-21.9-61-199.6-88.9-182.5-229.8-134.3-347.5,30,137.2,268.8,148.9,146.2,336-.9,2.2,10,27.8,19.5,51.3,22.7-51.9,58.6-115.5,55.8-120.8C178,398.7,724.9,299,807.1,18.5c83,251.5,53.1,659.8-377.4,814.9-2,1.4-63.5,148.6-66.9,150.2-.2-2.1-33.2,2.9-30.1-8.7,1.6-7,4.8-16.2,8.2-25.6h0v-.2h.1ZM323.1,846.2c48.3-71.9-12.7-120.8-56.9-152.2,81.2,107.4,66.4,120.8,56.9,152.2h0Z"/>
|
||||
</svg>
|
||||
<svg pngx-brand-mark class="brand-mark" width="1.5em" height="1.5em"></svg>
|
||||
}
|
||||
<div class="brand-copy ms-2 text-truncate" [class.d-md-none]="slimSidebarEnabled">
|
||||
<span class="brand-title text-truncate">{{ appTitle }}</span>
|
||||
|
||||
@@ -417,7 +417,7 @@ main {
|
||||
|
||||
:host ::ng-deep .navbar-official-logo {
|
||||
.leaf {
|
||||
fill: color-mix(in srgb, var(--pngx-primary-text-contrast) 70%, var(--bs-primary)) !important;
|
||||
fill: color-mix(in srgb, var(--pngx-primary-text-contrast) 85%, var(--bs-primary)) !important;
|
||||
}
|
||||
|
||||
.text {
|
||||
|
||||
@@ -45,6 +45,7 @@ import { TasksService } from 'src/app/services/tasks.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { ChatComponent } from '../chat/chat/chat.component'
|
||||
import { BrandMarkComponent } from '../common/logo/brand-mark/brand-mark.component'
|
||||
import { LogoComponent } from '../common/logo/logo.component'
|
||||
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
|
||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
||||
@@ -61,6 +62,7 @@ const SCROLL_THRESHOLD = 16
|
||||
imports: [
|
||||
GlobalSearchComponent,
|
||||
LogoComponent,
|
||||
BrandMarkComponent,
|
||||
DocumentTitlePipe,
|
||||
IfPermissionsDirective,
|
||||
ToastsDropdownComponent,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg:path d="M341,949.1c-6.9-20.3-20.7-61.2-21.9-61-199.6-88.9-182.5-229.8-134.3-347.5,30,137.2,268.8,148.9,146.2,336-.9,2.2,10,27.8,19.5,51.3,22.7-51.9,58.6-115.5,55.8-120.8C178,398.7,724.9,299,807.1,18.5c83,251.5,53.1,659.8-377.4,814.9-2,1.4-63.5,148.6-66.9,150.2-.2-2.1-33.2,2.9-30.1-8.7,1.6-7,4.8-16.2,8.2-25.6h0v-.2h.1ZM323.1,846.2c48.3-71.9-12.7-120.8-56.9-152.2,81.2,107.4,66.4,120.8,56.9,152.2h0Z"/>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,16 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
/**
|
||||
* The paperless-ngx leaf mark, applied as an attribute on an <svg>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'svg[pngx-brand-mark]',
|
||||
templateUrl: './brand-mark.component.html',
|
||||
host: {
|
||||
'[attr.xmlns]': "'http://www.w3.org/2000/svg'",
|
||||
'[attr.viewBox]': "'0 0 1000 1000'",
|
||||
'[attr.fill]': "'currentColor'",
|
||||
'[attr.aria-hidden]': "'true'",
|
||||
},
|
||||
})
|
||||
export class BrandMarkComponent {}
|
||||
+16
-8
@@ -1,10 +1,18 @@
|
||||
<ngb-alert class="pe-3" type="primary" [dismissible]="true" (closed)="dismiss.emit(true)">
|
||||
<h4 class="alert-heading"><ng-container i18n>Paperless-ngx is running!</ng-container> 🎉</h4>
|
||||
<p i18n>You're ready to start uploading documents! Explore the various features of this web app on your own, or start a quick tour using the button below.</p>
|
||||
<p i18n>More detail on how to use and configure Paperless-ngx is always available in the <a href="https://docs.paperless-ngx.com" target="_blank">documentation</a>.</p>
|
||||
<hr>
|
||||
<div class="d-flex align-items-end">
|
||||
<p class="lead fs-6 m-0"><em i18n>Thanks for being a part of the Paperless-ngx community!</em></p>
|
||||
<button class="btn btn-primary ms-auto flex-shrink-0" (click)="tourService.start()"><ng-container i18n>Start the tour</ng-container> →</button>
|
||||
<ngb-alert class="welcome-widget bg-light shadow-sm pe-3 position-relative overflow-hidden" type="primary" [dismissible]="true" (closed)="dismiss.emit(true)">
|
||||
<svg pngx-brand-mark class="welcome-watermark d-none d-md-block position-absolute top-50 translate-middle-y text-primary pe-none" width="26rem" height="26rem"></svg>
|
||||
<div class="d-flex align-items-start gap-3 gap-md-4 position-relative">
|
||||
<svg pngx-brand-mark class="d-none d-sm-block flex-shrink-0 text-primary" width="3rem" height="3rem"></svg>
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="alert-heading text-primary fw-semibold"><ng-container i18n>Paperless-ngx is running!</ng-container> 🎉</h4>
|
||||
<div class="welcome-copy text-dark">
|
||||
<p i18n>You're ready to start uploading documents! Explore the various features of this web app on your own, or start a quick tour using the button below.</p>
|
||||
<p class="mb-0" i18n>More detail on how to use and configure Paperless-ngx is always available in the <a href="https://docs.paperless-ngx.com" target="_blank">documentation</a>.</p>
|
||||
</div>
|
||||
<hr class="opacity-100">
|
||||
<div class="d-flex flex-column flex-sm-row align-items-sm-center gap-3">
|
||||
<p class="m-0 text-secondary"><em i18n>Thanks for being a part of the Paperless-ngx community!</em></p>
|
||||
<button class="btn btn-primary ms-sm-auto flex-shrink-0 align-self-start align-self-sm-auto" (click)="tourService.start()"><ng-container i18n>Start the tour</ng-container> →</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ngb-alert>
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
.welcome-widget {
|
||||
border-color: color-mix(in srgb, var(--bs-primary) 50%, transparent);
|
||||
|
||||
hr {
|
||||
border-top-color: color-mix(in srgb, var(--bs-primary) 25%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-copy {
|
||||
max-width: 68ch;
|
||||
}
|
||||
|
||||
.welcome-watermark {
|
||||
right: -6rem;
|
||||
opacity: .07;
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,12 +1,13 @@
|
||||
import { Component, EventEmitter, Output, inject } from '@angular/core'
|
||||
import { NgbAlertModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import { BrandMarkComponent } from '../../../common/logo/brand-mark/brand-mark.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-welcome-widget',
|
||||
templateUrl: './welcome-widget.component.html',
|
||||
styleUrls: ['./welcome-widget.component.scss'],
|
||||
imports: [NgbAlertModule],
|
||||
imports: [NgbAlertModule, BrandMarkComponent],
|
||||
})
|
||||
export class WelcomeWidgetComponent {
|
||||
readonly tourService = inject(TourService)
|
||||
|
||||
@@ -1133,7 +1133,8 @@ class DocumentViewSet(
|
||||
"custom_fields",
|
||||
queryset=CustomFieldInstance.objects.select_related("field"),
|
||||
),
|
||||
"notes",
|
||||
# NotesSerializer nests the author, this avoids query per note
|
||||
Prefetch("notes", queryset=Note.objects.select_related("user")),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-24 21:43+0000\n"
|
||||
"POT-Creation-Date: 2026-08-25 21:36+0000\n"
|
||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
@@ -1580,7 +1580,7 @@ msgid "workflow runs"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:523 documents/serialisers.py:875
|
||||
#: documents/serialisers.py:2827 documents/views.py:311 documents/views.py:2612
|
||||
#: documents/serialisers.py:2827 documents/views.py:311 documents/views.py:2613
|
||||
#: paperless_mail/serialisers.py:155
|
||||
msgid "Insufficient permissions."
|
||||
msgstr ""
|
||||
@@ -1621,7 +1621,7 @@ msgstr ""
|
||||
msgid "Duplicate document identifiers are not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2913 documents/views.py:4606
|
||||
#: documents/serialisers.py:2913 documents/views.py:4607
|
||||
#, python-format
|
||||
msgid "Documents not found: %(ids)s"
|
||||
msgstr ""
|
||||
@@ -1889,36 +1889,36 @@ msgstr ""
|
||||
msgid "Unable to parse URI {value}"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:304 documents/views.py:2609
|
||||
#: documents/views.py:304 documents/views.py:2610
|
||||
msgid "Invalid more_like_id"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1586
|
||||
#: documents/views.py:1587
|
||||
msgid "Invalid AI configuration."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1597
|
||||
#: documents/views.py:1598
|
||||
msgid "AI backend request timed out."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:2434 documents/views.py:2755
|
||||
#: documents/views.py:2435 documents/views.py:2756
|
||||
msgid "Specify only one of text, title_search, query, or more_like_id."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4619
|
||||
#: documents/views.py:4620
|
||||
#, python-format
|
||||
msgid "Insufficient permissions to share document %(id)s."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4665
|
||||
#: documents/views.py:4666
|
||||
msgid "Bundle is already being processed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4726
|
||||
#: documents/views.py:4727
|
||||
msgid "The share link bundle is still being prepared. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4736
|
||||
#: documents/views.py:4737
|
||||
msgid "The share link bundle is unavailable."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -253,6 +253,81 @@ class TestAPIMailAccounts(DirectoriesMixin, APITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["success"], True)
|
||||
|
||||
def test_mail_account_test_existing_no_global_perms(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing account without an owner
|
||||
- User without any mail account permissions
|
||||
WHEN:
|
||||
- API call is made to test the account by id
|
||||
THEN:
|
||||
- API returns forbidden
|
||||
"""
|
||||
account = MailAccountFactory(
|
||||
username="admin",
|
||||
password="secret",
|
||||
imap_server="server.example.com",
|
||||
imap_port=443,
|
||||
owner=None,
|
||||
)
|
||||
user = User.objects.create_user(username="no_perms")
|
||||
self.client.force_authenticate(user=user)
|
||||
|
||||
response = self.client.post(
|
||||
f"{self.ENDPOINT}test/",
|
||||
json.dumps(
|
||||
{
|
||||
"id": account.pk,
|
||||
"imap_server": "server.example.com",
|
||||
"imap_port": 443,
|
||||
"imap_security": MailAccount.ImapSecurity.SSL,
|
||||
"username": "admin",
|
||||
"password": "******",
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.content.decode(), "Insufficient permissions")
|
||||
|
||||
def test_mail_account_test_existing_object_perms_only(self) -> None:
|
||||
"""
|
||||
GIVEN:
|
||||
- Existing account owned by another user
|
||||
- User with an object level grant but no global change permission
|
||||
WHEN:
|
||||
- API call is made to test the account by id
|
||||
THEN:
|
||||
- API returns forbidden
|
||||
"""
|
||||
owner = User.objects.create_user(username="account_owner")
|
||||
account = MailAccountFactory(
|
||||
username="admin",
|
||||
password="secret",
|
||||
imap_server="server.example.com",
|
||||
imap_port=443,
|
||||
owner=owner,
|
||||
)
|
||||
user = User.objects.create_user(username="object_perms_only")
|
||||
assign_perm("change_mailaccount", user, account)
|
||||
self.client.force_authenticate(user=user)
|
||||
|
||||
response = self.client.post(
|
||||
f"{self.ENDPOINT}test/",
|
||||
json.dumps(
|
||||
{
|
||||
"id": account.pk,
|
||||
"imap_server": "server.example.com",
|
||||
"imap_port": 443,
|
||||
"imap_security": MailAccount.ImapSecurity.SSL,
|
||||
"username": "admin",
|
||||
"password": "******",
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_mail_account_test_existing_nonexistent_id_forbidden(self) -> None:
|
||||
response = self.client.post(
|
||||
f"{self.ENDPOINT}test/",
|
||||
|
||||
@@ -2195,7 +2195,9 @@ class TestMailAccountTestView(APITestCase):
|
||||
password="testpassword",
|
||||
)
|
||||
self.user.user_permissions.add(
|
||||
*Permission.objects.filter(codename__in=["add_mailaccount"]),
|
||||
*Permission.objects.filter(
|
||||
codename__in=["add_mailaccount", "change_mailaccount"],
|
||||
),
|
||||
)
|
||||
self.user.save()
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
@@ -106,7 +106,9 @@ class MailAccountViewSet(PassUserMixin, ModelViewSet[MailAccount]):
|
||||
except (TypeError, ValueError, MailAccount.DoesNotExist):
|
||||
return HttpResponseForbidden("Insufficient permissions")
|
||||
|
||||
if not has_perms_owner_aware(
|
||||
if not request.user.has_perms(
|
||||
["paperless_mail.change_mailaccount"],
|
||||
) or not has_perms_owner_aware(
|
||||
request.user,
|
||||
"change_mailaccount",
|
||||
existing_account,
|
||||
|
||||
Reference in New Issue
Block a user