mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge pull request #591 from paperless-ngx/feature-version-checker
Feature: Update checker
This commit is contained in:
		| @@ -764,3 +764,26 @@ PAPERLESS_OCR_LANGUAGES=<list> | |||||||
|         PAPERLESS_OCR_LANGUAGE=tur |         PAPERLESS_OCR_LANGUAGE=tur | ||||||
|  |  | ||||||
|     Defaults to none, which does not install any additional languages. |     Defaults to none, which does not install any additional languages. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | .. _configuration-update-checking: | ||||||
|  |  | ||||||
|  | Update Checking | ||||||
|  | ############### | ||||||
|  |  | ||||||
|  | PAPERLESS_ENABLE_UPDATE_CHECK=<bool> | ||||||
|  |     Enable (or disable) the automatic check for available updates. This feature is disabled | ||||||
|  |     by default but if it is not explicitly set Paperless-ngx will show a message about this. | ||||||
|  |  | ||||||
|  |     If enabled, the feature works by pinging the the Github API for the latest release e.g. | ||||||
|  |     https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest | ||||||
|  |     to determine whether a new version is available. | ||||||
|  |  | ||||||
|  |     Actual updating of the app must still be performed manually. | ||||||
|  |  | ||||||
|  |     Note that for users of thirdy-party containers e.g. linuxserver.io this notification | ||||||
|  |     may be 'ahead' of a new release from the third-party maintainers. | ||||||
|  |  | ||||||
|  |     In either case, no tracking data is collected by the app in any way. | ||||||
|  |  | ||||||
|  |     Defaults to none, which disables the feature. | ||||||
|   | |||||||
| @@ -67,6 +67,7 @@ | |||||||
| #PAPERLESS_FILENAME_PARSE_TRANSFORMS=[] | #PAPERLESS_FILENAME_PARSE_TRANSFORMS=[] | ||||||
| #PAPERLESS_THUMBNAIL_FONT_NAME= | #PAPERLESS_THUMBNAIL_FONT_NAME= | ||||||
| #PAPERLESS_IGNORE_DATES= | #PAPERLESS_IGNORE_DATES= | ||||||
|  | #PAPERLESS_ENABLE_UPDATE_CHECK= | ||||||
|  |  | ||||||
| # Tika settings | # Tika settings | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ | |||||||
|   </a> |   </a> | ||||||
|   <div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"> |   <div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"> | ||||||
|     <form (ngSubmit)="search()" class="form-inline flex-grow-1"> |     <form (ngSubmit)="search()" class="form-inline flex-grow-1"> | ||||||
|       <svg width="1em" height="1em"> |       <svg width="1em" height="1em" fill="currentColor"> | ||||||
|         <use xlink:href="assets/bootstrap-icons.svg#search"/> |         <use xlink:href="assets/bootstrap-icons.svg#search"/> | ||||||
|       </svg> |       </svg> | ||||||
|       <input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search" |       <input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search" | ||||||
| @@ -25,7 +25,7 @@ | |||||||
|         <span *ngIf="displayName" class="navbar-text small me-2 text-light d-none d-sm-inline"> |         <span *ngIf="displayName" class="navbar-text small me-2 text-light d-none d-sm-inline"> | ||||||
|           {{displayName}} |           {{displayName}} | ||||||
|         </span> |         </span> | ||||||
|         <svg width="1.3em" height="1.3em"> |         <svg width="1.3em" height="1.3em" fill="currentColor"> | ||||||
|           <use xlink:href="assets/bootstrap-icons.svg#person-circle"/> |           <use xlink:href="assets/bootstrap-icons.svg#person-circle"/> | ||||||
|         </svg> |         </svg> | ||||||
|       </button> |       </button> | ||||||
| @@ -170,21 +170,46 @@ | |||||||
|           <li class="nav-item"> |           <li class="nav-item"> | ||||||
|             <div class="d-flex w-100 flex-wrap"> |             <div class="d-flex w-100 flex-wrap"> | ||||||
|               <a class="nav-link pe-2 pb-1" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx"> |               <a class="nav-link pe-2 pb-1" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx"> | ||||||
|                 <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="sidebaricon bi bi-github" viewBox="0 0 16 16"> |                 <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="sidebaricon" viewBox="0 0 16 16"> | ||||||
|                   <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/> |                   <use xlink:href="assets/bootstrap-icons.svg#github" /> | ||||||
|                 </svg> <ng-container i18n>GitHub</ng-container> |                 </svg> <ng-container i18n>GitHub</ng-container> | ||||||
|               </a> |               </a> | ||||||
|               <a class="nav-link-additional small text-muted ms-3" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/discussions/categories/feature-requests" title="Suggest an idea"> |               <a class="nav-link-additional small text-muted ms-3" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/discussions/categories/feature-requests" title="Suggest an idea"> | ||||||
|                 <svg xmlns="http://www.w3.org/2000/svg" width="1.3em" height="1.3em" fill="currentColor" class="bi bi-lightbulb pe-1" viewBox="0 0 16 16"> |                 <svg xmlns="http://www.w3.org/2000/svg" width="1.1em" height="1.1em" fill="currentColor" class="me-1" viewBox="0 0 16 16"> | ||||||
|                   <path d="M2 6a6 6 0 1 1 10.174 4.31c-.203.196-.359.4-.453.619l-.762 1.769A.5.5 0 0 1 10.5 13a.5.5 0 0 1 0 1 .5.5 0 0 1 0 1l-.224.447a1 1 0 0 1-.894.553H6.618a1 1 0 0 1-.894-.553L5.5 15a.5.5 0 0 1 0-1 .5.5 0 0 1 0-1 .5.5 0 0 1-.46-.302l-.761-1.77a1.964 1.964 0 0 0-.453-.618A5.984 5.984 0 0 1 2 6zm6-5a5 5 0 0 0-3.479 8.592c.263.254.514.564.676.941L5.83 12h4.342l.632-1.467c.162-.377.413-.687.676-.941A5 5 0 0 0 8 1z"/> |                   <use xlink:href="assets/bootstrap-icons.svg#lightbulb" /> | ||||||
|                 </svg> |                 </svg> | ||||||
|                 <ng-container i18n>Suggest an idea</ng-container> |                 <ng-container i18n>Suggest an idea</ng-container> | ||||||
|               </a> |               </a> | ||||||
|             </div> |             </div> | ||||||
|           </li> |           </li> | ||||||
|           <li class="nav-item mt-2"> |           <li class="nav-item mt-2"> | ||||||
|             <div class="px-3 py-2 text-muted small"> |             <div class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap"> | ||||||
|               {{versionString}} |               <div class="me-3">{{ versionString }}</div> | ||||||
|  |               <div *ngIf="appRemoteVersion" class="version-check"> | ||||||
|  |                 <ng-template #updateAvailablePopContent> | ||||||
|  |                   <span class="small">Paperless-ngx v{{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span> | ||||||
|  |                 </ng-template> | ||||||
|  |                 <ng-template #updateCheckingNotEnabledPopContent> | ||||||
|  |                   <span class="small"><ng-container i18n>Checking for updates is disabled.</ng-container><br/><ng-container i18n>Click for more information.</ng-container></span> | ||||||
|  |                 </ng-template> | ||||||
|  |                 <ng-container *ngIf="appRemoteVersion.feature_is_set; else updateCheckNotSet"> | ||||||
|  |                   <a *ngIf="appRemoteVersion.update_available" class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases" | ||||||
|  |                   [ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> | ||||||
|  |                     <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> | ||||||
|  |                       <use xlink:href="assets/bootstrap-icons.svg#info-circle" /> | ||||||
|  |                     </svg> | ||||||
|  |                     <ng-container *ngIf="appRemoteVersion?.update_available" i18n>Update available</ng-container> | ||||||
|  |                   </a> | ||||||
|  |                 </ng-container> | ||||||
|  |                 <ng-template #updateCheckNotSet> | ||||||
|  |                   <a class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://paperless-ngx.readthedocs.io/en/latest/configuration.html#update-checking" | ||||||
|  |                   [ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body"> | ||||||
|  |                     <svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16"> | ||||||
|  |                       <use xlink:href="assets/bootstrap-icons.svg#info-circle" /> | ||||||
|  |                     </svg> | ||||||
|  |                   </a> | ||||||
|  |                 </ng-template> | ||||||
|  |               </div> | ||||||
|             </div> |             </div> | ||||||
|           </li> |           </li> | ||||||
|         </ul> |         </ul> | ||||||
|   | |||||||
| @@ -176,3 +176,22 @@ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .version-check { | ||||||
|  |   animation: pulse 2s ease-in-out 0s 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @keyframes pulse { | ||||||
|  |   0% { | ||||||
|  |     opacity: 0; | ||||||
|  |   } | ||||||
|  |   25% { | ||||||
|  |     opacity: 100%; | ||||||
|  |   } | ||||||
|  |   75% { | ||||||
|  |     opacity: 0; | ||||||
|  |   } | ||||||
|  |   100% { | ||||||
|  |     opacity: 100%; | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -18,6 +18,10 @@ import { DocumentDetailComponent } from '../document-detail/document-detail.comp | |||||||
| import { Meta } from '@angular/platform-browser' | import { Meta } from '@angular/platform-browser' | ||||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service' | import { DocumentListViewService } from 'src/app/services/document-list-view.service' | ||||||
| import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type' | import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type' | ||||||
|  | import { | ||||||
|  |   RemoteVersionService, | ||||||
|  |   AppRemoteVersion, | ||||||
|  | } from 'src/app/services/rest/remote-version.service' | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-app-frame', |   selector: 'app-app-frame', | ||||||
| @@ -32,10 +36,18 @@ export class AppFrameComponent { | |||||||
|     private searchService: SearchService, |     private searchService: SearchService, | ||||||
|     public savedViewService: SavedViewService, |     public savedViewService: SavedViewService, | ||||||
|     private list: DocumentListViewService, |     private list: DocumentListViewService, | ||||||
|     private meta: Meta |     private meta: Meta, | ||||||
|   ) {} |     private remoteVersionService: RemoteVersionService | ||||||
|  |   ) { | ||||||
|  |     this.remoteVersionService | ||||||
|  |       .checkForUpdates() | ||||||
|  |       .subscribe((appRemoteVersion: AppRemoteVersion) => { | ||||||
|  |         this.appRemoteVersion = appRemoteVersion | ||||||
|  |       }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   versionString = `${environment.appTitle} ${environment.version}` |   versionString = `${environment.appTitle} ${environment.version}` | ||||||
|  |   appRemoteVersion | ||||||
|  |  | ||||||
|   isMenuCollapsed: boolean = true |   isMenuCollapsed: boolean = true | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								src-ui/src/app/services/rest/remote-version.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src-ui/src/app/services/rest/remote-version.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import { HttpClient } from '@angular/common/http' | ||||||
|  | import { Injectable } from '@angular/core' | ||||||
|  | import { map, Observable } from 'rxjs' | ||||||
|  | import { environment } from 'src/environments/environment' | ||||||
|  |  | ||||||
|  | export interface AppRemoteVersion { | ||||||
|  |   version: string | ||||||
|  |   update_available: boolean | ||||||
|  |   feature_is_set: boolean | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Injectable({ | ||||||
|  |   providedIn: 'root', | ||||||
|  | }) | ||||||
|  | export class RemoteVersionService { | ||||||
|  |   constructor(private http: HttpClient) {} | ||||||
|  |  | ||||||
|  |   public checkForUpdates(): Observable<AppRemoteVersion> { | ||||||
|  |     return this.http.get<AppRemoteVersion>( | ||||||
|  |       `${environment.apiBaseUrl}remote_version/` | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | } | ||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| Before Width: | Height: | Size: 579 KiB After Width: | Height: | Size: 861 KiB | 
| @@ -271,6 +271,7 @@ table.table { | |||||||
|   .popover-body { |   .popover-body { | ||||||
|     background-color: var(--ngx-bg-alt); |     background-color: var(--ngx-bg-alt); | ||||||
|     border-color: var(--bs-border-color); |     border-color: var(--bs-border-color); | ||||||
|  |     color: var(--bs-body-color); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
|  | import json | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
| import tempfile | import tempfile | ||||||
|  | import urllib | ||||||
| import uuid | import uuid | ||||||
| import zipfile | import zipfile | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| @@ -24,6 +26,8 @@ from django.views.decorators.cache import cache_control | |||||||
| from django.views.generic import TemplateView | from django.views.generic import TemplateView | ||||||
| from django_filters.rest_framework import DjangoFilterBackend | from django_filters.rest_framework import DjangoFilterBackend | ||||||
| from django_q.tasks import async_task | from django_q.tasks import async_task | ||||||
|  | from packaging import version as packaging_version | ||||||
|  | from paperless import version | ||||||
| from paperless.db import GnuPG | from paperless.db import GnuPG | ||||||
| from paperless.views import StandardPagination | from paperless.views import StandardPagination | ||||||
| from rest_framework import parsers | from rest_framework import parsers | ||||||
| @@ -666,3 +670,40 @@ class BulkDownloadView(GenericAPIView): | |||||||
|             ) |             ) | ||||||
|  |  | ||||||
|             return response |             return response | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RemoteVersionView(GenericAPIView): | ||||||
|  |     def get(self, request, format=None): | ||||||
|  |         remote_version = "0.0.0" | ||||||
|  |         is_greater_than_current = False | ||||||
|  |         # TODO: this can likely be removed when frontend settings are saved to DB | ||||||
|  |         feature_is_set = settings.ENABLE_UPDATE_CHECK != "default" | ||||||
|  |         if feature_is_set and settings.ENABLE_UPDATE_CHECK: | ||||||
|  |             try: | ||||||
|  |                 with urllib.request.urlopen( | ||||||
|  |                     "https://api.github.com/repos/" | ||||||
|  |                     + "paperless-ngx/paperless-ngx/releases/latest", | ||||||
|  |                 ) as response: | ||||||
|  |                     remote = response.read().decode("utf-8") | ||||||
|  |                 try: | ||||||
|  |                     remote_json = json.loads(remote) | ||||||
|  |                     remote_version = remote_json["tag_name"].replace("ngx-", "") | ||||||
|  |                 except ValueError: | ||||||
|  |                     logger.debug("An error occured parsing remote version json") | ||||||
|  |             except urllib.error.URLError: | ||||||
|  |                 logger.debug("An error occured checking for available updates") | ||||||
|  |  | ||||||
|  |             current_version = ".".join([str(_) for _ in version.__version__[:3]]) | ||||||
|  |             is_greater_than_current = packaging_version.parse( | ||||||
|  |                 remote_version, | ||||||
|  |             ) > packaging_version.parse( | ||||||
|  |                 current_version, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         return Response( | ||||||
|  |             { | ||||||
|  |                 "version": remote_version, | ||||||
|  |                 "update_available": is_greater_than_current, | ||||||
|  |                 "feature_is_set": feature_is_set, | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|   | |||||||
| @@ -566,3 +566,7 @@ if os.getenv("PAPERLESS_IGNORE_DATES", ""): | |||||||
|         d = dateparser.parse(s) |         d = dateparser.parse(s) | ||||||
|         if d: |         if d: | ||||||
|             IGNORE_DATES.add(d.date()) |             IGNORE_DATES.add(d.date()) | ||||||
|  |  | ||||||
|  | ENABLE_UPDATE_CHECK = os.getenv("PAPERLESS_ENABLE_UPDATE_CHECK", "default") | ||||||
|  | if ENABLE_UPDATE_CHECK != "default": | ||||||
|  |     ENABLE_UPDATE_CHECK = __get_boolean("PAPERLESS_ENABLE_UPDATE_CHECK") | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ from documents.views import DocumentTypeViewSet | |||||||
| from documents.views import IndexView | from documents.views import IndexView | ||||||
| from documents.views import LogViewSet | from documents.views import LogViewSet | ||||||
| from documents.views import PostDocumentView | from documents.views import PostDocumentView | ||||||
|  | from documents.views import RemoteVersionView | ||||||
| from documents.views import SavedViewViewSet | from documents.views import SavedViewViewSet | ||||||
| from documents.views import SearchAutoCompleteView | from documents.views import SearchAutoCompleteView | ||||||
| from documents.views import SelectionDataView | from documents.views import SelectionDataView | ||||||
| @@ -72,6 +73,11 @@ urlpatterns = [ | |||||||
|                     BulkDownloadView.as_view(), |                     BulkDownloadView.as_view(), | ||||||
|                     name="bulk_download", |                     name="bulk_download", | ||||||
|                 ), |                 ), | ||||||
|  |                 re_path( | ||||||
|  |                     r"^remote_version/", | ||||||
|  |                     RemoteVersionView.as_view(), | ||||||
|  |                     name="remoteversion", | ||||||
|  |                 ), | ||||||
|                 path("token/", views.obtain_auth_token), |                 path("token/", views.obtain_auth_token), | ||||||
|             ] |             ] | ||||||
|             + api_router.urls, |             + api_router.urls, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Felix E
					Felix E