Coverage for paperless/models.py: 99%

104 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 09:07 +0000

1from django.core.validators import FileExtensionValidator 

2from django.core.validators import MinValueValidator 

3from django.db import models 

4from django.utils.translation import gettext_lazy as _ 

5 

6DEFAULT_SINGLETON_INSTANCE_ID = 1 

7 

8 

9class AbstractSingletonModel(models.Model): 

10 class Meta: 

11 abstract = True 

12 

13 def save(self, *args, **kwargs): 

14 """ 

15 Always save as the first and only model 

16 """ 

17 self.pk = DEFAULT_SINGLETON_INSTANCE_ID 

18 super().save(*args, **kwargs) 

19 

20 

21class OutputTypeChoices(models.TextChoices): 

22 """ 

23 Matches to --output-type 

24 """ 

25 

26 PDF = ("pdf", _("pdf")) 

27 PDF_A = ("pdfa", _("pdfa")) 

28 PDF_A1 = ("pdfa-1", _("pdfa-1")) 

29 PDF_A2 = ("pdfa-2", _("pdfa-2")) 

30 PDF_A3 = ("pdfa-3", _("pdfa-3")) 

31 

32 

33class ModeChoices(models.TextChoices): 

34 """ 

35 Matches to --skip-text, --redo-ocr, --force-ocr 

36 and our own custom setting 

37 """ 

38 

39 AUTO = ("auto", _("auto")) 

40 FORCE = ("force", _("force")) 

41 REDO = ("redo", _("redo")) 

42 OFF = ("off", _("off")) 

43 

44 

45class ArchiveFileGenerationChoices(models.TextChoices): 

46 """ 

47 Settings to control creation of an archive PDF file 

48 """ 

49 

50 AUTO = ("auto", _("auto")) 

51 ALWAYS = ("always", _("always")) 

52 NEVER = ("never", _("never")) 

53 

54 

55class CleanChoices(models.TextChoices): 

56 """ 

57 Matches to --clean, --clean-final 

58 """ 

59 

60 CLEAN = ("clean", _("clean")) 

61 FINAL = ("clean-final", _("clean-final")) 

62 NONE = ("none", _("none")) 

63 

64 

65class ColorConvertChoices(models.TextChoices): 

66 """ 

67 Refer to the Ghostscript documentation for valid options 

68 """ 

69 

70 UNCHANGED = ("LeaveColorUnchanged", _("LeaveColorUnchanged")) 

71 RGB = ("RGB", _("RGB")) 

72 INDEPENDENT = ("UseDeviceIndependentColor", _("UseDeviceIndependentColor")) 

73 GRAY = ("Gray", _("Gray")) 

74 CMYK = ("CMYK", _("CMYK")) 

75 

76 

77class RemoteOCREngine(models.TextChoices): 

78 """ 

79 Matches to PAPERLESS_REMOTE_OCR_ENGINE 

80 """ 

81 

82 AZURE_AI = ("azureai", _("Azure AI Document Intelligence")) 

83 

84 

85class RemoteOCRMode(models.TextChoices): 

86 """ 

87 Matches to PAPERLESS_REMOTE_OCR_MODE 

88 """ 

89 

90 ALWAYS = ("always", _("All supported documents")) 

91 WORKFLOW_ONLY = ("workflow_only", _("Only when a workflow enables it")) 

92 

93 

94class LLMEmbeddingBackend(models.TextChoices): 

95 OPENAI_LIKE = ("openai-like", _("OpenAI-compatible")) 

96 HUGGINGFACE = ("huggingface", _("Huggingface")) 

97 OLLAMA = ("ollama", _("Ollama")) 

98 

99 

100class LLMBackend(models.TextChoices): 

101 """ 

102 Matches to --llm-backend 

103 """ 

104 

105 OPENAI_LIKE = ("openai-like", _("OpenAI-compatible")) 

106 OLLAMA = ("ollama", _("Ollama")) 

107 

108 

109class ApplicationConfiguration(AbstractSingletonModel): 

110 """ 

111 Settings which are common across more than 1 parser 

112 """ 

113 

114 output_type = models.CharField( 

115 verbose_name=_("Sets the output PDF type"), 

116 null=True, 

117 blank=True, 

118 max_length=8, 

119 choices=OutputTypeChoices.choices, 

120 ) 

121 

122 """ 

123 Settings for the Tesseract based OCR parser 

124 """ 

125 

126 pages = models.PositiveSmallIntegerField( 

127 verbose_name=_("Do OCR from page 1 to this value"), 

128 null=True, 

129 validators=[MinValueValidator(1)], 

130 ) 

131 

132 language = models.CharField( 

133 verbose_name=_("Do OCR using these languages"), 

134 null=True, 

135 blank=True, 

136 max_length=32, 

137 ) 

138 

139 mode = models.CharField( 

140 verbose_name=_("Sets the OCR mode"), 

141 null=True, 

142 blank=True, 

143 max_length=16, 

144 choices=ModeChoices.choices, 

145 ) 

146 

147 archive_file_generation = models.CharField( 

148 verbose_name=_("Controls archive file generation"), 

149 null=True, 

150 blank=True, 

151 max_length=8, 

152 choices=ArchiveFileGenerationChoices.choices, 

153 ) 

154 

155 image_dpi = models.PositiveSmallIntegerField( 

156 verbose_name=_("Sets image DPI fallback value"), 

157 null=True, 

158 validators=[MinValueValidator(1)], 

159 ) 

160 

161 # Can't call it clean, that's a model method 

162 unpaper_clean = models.CharField( 

163 verbose_name=_("Controls the unpaper cleaning"), 

164 null=True, 

165 blank=True, 

166 max_length=16, 

167 choices=CleanChoices.choices, 

168 ) 

169 

170 deskew = models.BooleanField(verbose_name=_("Enables deskew"), null=True) 

171 

172 rotate_pages = models.BooleanField( 

173 verbose_name=_("Enables page rotation"), 

174 null=True, 

175 ) 

176 

177 rotate_pages_threshold = models.FloatField( 

178 verbose_name=_("Sets the threshold for rotation of pages"), 

179 null=True, 

180 validators=[MinValueValidator(0.0)], 

181 ) 

182 

183 max_image_pixels = models.FloatField( 

184 verbose_name=_("Sets the maximum image size for decompression"), 

185 null=True, 

186 validators=[MinValueValidator(0.0)], 

187 ) 

188 

189 color_conversion_strategy = models.CharField( 

190 verbose_name=_("Sets the Ghostscript color conversion strategy"), 

191 blank=True, 

192 null=True, 

193 max_length=32, 

194 choices=ColorConvertChoices.choices, 

195 ) 

196 

197 user_args = models.JSONField( 

198 verbose_name=_("Adds additional user arguments for OCRMyPDF"), 

199 null=True, 

200 ) 

201 

202 """ 

203 Settings for the Paperless application 

204 """ 

205 

206 app_title = models.CharField( 

207 verbose_name=_("Application title"), 

208 null=True, 

209 blank=True, 

210 max_length=48, 

211 ) 

212 

213 app_logo = models.FileField( 

214 verbose_name=_("Application logo"), 

215 null=True, 

216 blank=True, 

217 validators=[ 

218 FileExtensionValidator(allowed_extensions=["jpg", "png", "gif", "svg"]), 

219 ], 

220 upload_to="logo/", 

221 ) 

222 

223 """ 

224 Settings for the barcode scanner 

225 """ 

226 

227 # PAPERLESS_CONSUMER_ENABLE_BARCODES 

228 barcodes_enabled = models.BooleanField( 

229 verbose_name=_("Enables barcode scanning"), 

230 null=True, 

231 ) 

232 

233 # PAPERLESS_CONSUMER_BARCODE_TIFF_SUPPORT 

234 barcode_enable_tiff_support = models.BooleanField( 

235 verbose_name=_("Enables barcode TIFF support"), 

236 null=True, 

237 ) 

238 

239 # PAPERLESS_CONSUMER_BARCODE_STRING 

240 barcode_string = models.CharField( 

241 verbose_name=_("Sets the barcode string"), 

242 null=True, 

243 blank=True, 

244 max_length=32, 

245 ) 

246 

247 # PAPERLESS_CONSUMER_BARCODE_RETAIN_SPLIT_PAGES 

248 barcode_retain_split_pages = models.BooleanField( 

249 verbose_name=_("Retains split pages"), 

250 null=True, 

251 ) 

252 

253 # PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE 

254 barcode_enable_asn = models.BooleanField( 

255 verbose_name=_("Enables ASN barcode"), 

256 null=True, 

257 ) 

258 

259 # PAPERLESS_CONSUMER_ASN_BARCODE_PREFIX 

260 barcode_asn_prefix = models.CharField( 

261 verbose_name=_("Sets the ASN barcode prefix"), 

262 null=True, 

263 blank=True, 

264 max_length=32, 

265 ) 

266 

267 # PAPERLESS_CONSUMER_BARCODE_UPSCALE 

268 barcode_upscale = models.FloatField( 

269 verbose_name=_("Sets the barcode upscale factor"), 

270 null=True, 

271 validators=[MinValueValidator(1.0)], 

272 ) 

273 

274 # PAPERLESS_CONSUMER_BARCODE_DPI 

275 barcode_dpi = models.PositiveSmallIntegerField( 

276 verbose_name=_("Sets the barcode DPI"), 

277 null=True, 

278 validators=[MinValueValidator(1)], 

279 ) 

280 

281 # PAPERLESS_CONSUMER_BARCODE_MAX_PAGES 

282 barcode_max_pages = models.PositiveSmallIntegerField( 

283 verbose_name=_("Sets the maximum pages for barcode"), 

284 null=True, 

285 validators=[MinValueValidator(1)], 

286 ) 

287 

288 # PAPERLESS_CONSUMER_ENABLE_TAG_BARCODE 

289 barcode_enable_tag = models.BooleanField( 

290 verbose_name=_("Enables tag barcode"), 

291 null=True, 

292 ) 

293 

294 # PAPERLESS_CONSUMER_TAG_BARCODE_MAPPING 

295 barcode_tag_mapping = models.JSONField( 

296 verbose_name=_("Sets the tag barcode mapping"), 

297 null=True, 

298 ) 

299 

300 # PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT 

301 barcode_tag_split = models.BooleanField( 

302 verbose_name=_("Enables splitting on tag barcodes"), 

303 null=True, 

304 ) 

305 

306 # PAPERLESS_CONSUMER_STORE_BARCODE_VALUES 

307 barcode_store_values = models.BooleanField( 

308 verbose_name=_("Stores the values of detected barcodes"), 

309 null=True, 

310 ) 

311 

312 """ 

313 Settings for the remote OCR parser 

314 """ 

315 

316 # PAPERLESS_REMOTE_OCR_ENGINE 

317 remote_ocr_engine = models.CharField( 

318 verbose_name=_("Sets the remote OCR engine"), 

319 blank=True, 

320 null=True, 

321 max_length=32, 

322 choices=RemoteOCREngine.choices, 

323 ) 

324 

325 # PAPERLESS_REMOTE_OCR_API_KEY 

326 remote_ocr_api_key = models.CharField( 

327 verbose_name=_("Sets the remote OCR API key"), 

328 blank=True, 

329 null=True, 

330 max_length=1024, 

331 ) 

332 

333 # PAPERLESS_REMOTE_OCR_ENDPOINT 

334 remote_ocr_endpoint = models.CharField( 

335 verbose_name=_("Sets the remote OCR endpoint"), 

336 blank=True, 

337 null=True, 

338 max_length=256, 

339 ) 

340 

341 # PAPERLESS_REMOTE_OCR_MODE 

342 remote_ocr_mode = models.CharField( 

343 verbose_name=_("Sets which documents are sent to the remote OCR engine"), 

344 blank=True, 

345 null=True, 

346 max_length=32, 

347 choices=RemoteOCRMode.choices, 

348 ) 

349 

350 """ 

351 AI related settings 

352 """ 

353 

354 ai_enabled = models.BooleanField( 

355 verbose_name=_("Enables AI features"), 

356 null=True, 

357 ) 

358 

359 llm_embedding_backend = models.CharField( 

360 verbose_name=_("Sets the LLM embedding backend"), 

361 blank=True, 

362 null=True, 

363 max_length=128, 

364 choices=LLMEmbeddingBackend.choices, 

365 ) 

366 

367 llm_embedding_model = models.CharField( 

368 verbose_name=_("Sets the LLM embedding model"), 

369 blank=True, 

370 null=True, 

371 max_length=128, 

372 ) 

373 

374 llm_embedding_api_key = models.CharField( 

375 verbose_name=_("Sets the LLM embedding API key"), 

376 blank=True, 

377 null=True, 

378 max_length=1024, 

379 ) 

380 

381 llm_embedding_endpoint = models.CharField( 

382 verbose_name=_("Sets the LLM embedding endpoint, optional"), 

383 blank=True, 

384 null=True, 

385 max_length=256, 

386 ) 

387 

388 llm_embedding_chunk_size = models.PositiveSmallIntegerField( 

389 verbose_name=_("Sets the LLM embedding chunk size"), 

390 null=True, 

391 validators=[MinValueValidator(1)], 

392 ) 

393 

394 llm_context_size = models.PositiveIntegerField( 

395 verbose_name=_("Sets the LLM context size"), 

396 null=True, 

397 validators=[MinValueValidator(1)], 

398 ) 

399 

400 llm_backend = models.CharField( 

401 verbose_name=_("Sets the LLM backend"), 

402 blank=True, 

403 null=True, 

404 max_length=128, 

405 choices=LLMBackend.choices, 

406 ) 

407 

408 llm_model = models.CharField( 

409 verbose_name=_("Sets the LLM model"), 

410 blank=True, 

411 null=True, 

412 max_length=128, 

413 ) 

414 

415 llm_api_key = models.CharField( 

416 verbose_name=_("Sets the LLM API key"), 

417 blank=True, 

418 null=True, 

419 max_length=1024, 

420 ) 

421 

422 llm_endpoint = models.CharField( 

423 verbose_name=_("Sets the LLM endpoint, optional"), 

424 blank=True, 

425 null=True, 

426 max_length=256, 

427 ) 

428 

429 llm_output_language = models.CharField( 

430 verbose_name=_("Sets the LLM output language"), 

431 blank=True, 

432 null=True, 

433 max_length=32, 

434 ) 

435 

436 llm_request_timeout = models.PositiveSmallIntegerField( 

437 verbose_name=_("Sets the LLM timeout in seconds"), 

438 null=True, 

439 validators=[MinValueValidator(1)], 

440 ) 

441 

442 class Meta: 

443 verbose_name = _("paperless application settings") 

444 permissions = [ 

445 ("view_global_statistics", "Can view global object counts"), 

446 ("view_system_monitoring", "Can view system status information"), 

447 ] 

448 

449 def __str__(self) -> str: # pragma: no cover 

450 return "ApplicationConfiguration"