2020-12-06 19:03:45 +01:00
|
|
|
import datetime
|
2016-02-27 20:18:50 +00:00
|
|
|
import logging
|
2016-01-01 16:13:59 +00:00
|
|
|
import os
|
2016-01-28 07:23:11 +00:00
|
|
|
import re
|
2020-11-11 14:21:33 +01:00
|
|
|
from collections import OrderedDict
|
2022-06-10 06:56:28 -07:00
|
|
|
from typing import Optional
|
2016-03-24 19:18:33 +00:00
|
|
|
|
2018-09-09 21:03:37 +01:00
|
|
|
import dateutil.parser
|
2022-03-11 10:55:51 -08:00
|
|
|
import pathvalidate
|
2016-01-01 16:13:59 +00:00
|
|
|
from django.conf import settings
|
2020-12-12 15:46:56 +01:00
|
|
|
from django.contrib.auth.models import User
|
2015-12-20 19:23:33 +00:00
|
|
|
from django.db import models
|
2015-12-26 13:20:52 +00:00
|
|
|
from django.utils import timezone
|
2020-12-30 21:48:34 +01:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
2020-11-30 00:40:04 +01:00
|
|
|
from documents.parsers import get_default_file_extension
|
|
|
|
|
|
2018-09-09 21:03:37 +01:00
|
|
|
|
2016-03-28 11:11:15 +01:00
|
|
|
class MatchingModel(models.Model):
|
2016-01-28 07:23:11 +00:00
|
|
|
|
2020-10-28 11:45:11 +01:00
|
|
|
MATCH_ANY = 1
|
|
|
|
|
MATCH_ALL = 2
|
|
|
|
|
MATCH_LITERAL = 3
|
|
|
|
|
MATCH_REGEX = 4
|
|
|
|
|
MATCH_FUZZY = 5
|
|
|
|
|
MATCH_AUTO = 6
|
|
|
|
|
|
|
|
|
|
MATCHING_ALGORITHMS = (
|
2021-01-02 00:45:23 +01:00
|
|
|
(MATCH_ANY, _("Any word")),
|
|
|
|
|
(MATCH_ALL, _("All words")),
|
|
|
|
|
(MATCH_LITERAL, _("Exact match")),
|
|
|
|
|
(MATCH_REGEX, _("Regular expression")),
|
|
|
|
|
(MATCH_FUZZY, _("Fuzzy word")),
|
|
|
|
|
(MATCH_AUTO, _("Automatic")),
|
2020-10-28 11:45:11 +01:00
|
|
|
)
|
|
|
|
|
|
2022-02-27 15:26:41 +01:00
|
|
|
name = models.CharField(_("name"), max_length=128, unique=True)
|
2020-12-30 21:48:34 +01:00
|
|
|
|
2022-02-27 15:26:41 +01:00
|
|
|
match = models.CharField(_("match"), max_length=256, blank=True)
|
2016-03-28 11:11:15 +01:00
|
|
|
|
2020-10-28 11:45:11 +01:00
|
|
|
matching_algorithm = models.PositiveIntegerField(
|
2022-03-11 10:55:51 -08:00
|
|
|
_("matching algorithm"),
|
|
|
|
|
choices=MATCHING_ALGORITHMS,
|
|
|
|
|
default=MATCH_ANY,
|
2018-09-25 16:09:33 +02:00
|
|
|
)
|
2016-10-05 23:43:55 +02:00
|
|
|
|
2022-02-27 15:26:41 +01:00
|
|
|
is_insensitive = models.BooleanField(_("is insensitive"), default=True)
|
2020-10-28 11:45:11 +01:00
|
|
|
|
2018-05-27 23:21:36 +01:00
|
|
|
class Meta:
|
2016-03-28 11:11:15 +01:00
|
|
|
abstract = True
|
2020-12-16 21:08:03 +01:00
|
|
|
ordering = ("name",)
|
2016-03-28 11:11:15 +01:00
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Correspondent(MatchingModel):
|
2020-12-16 22:33:03 +01:00
|
|
|
class Meta:
|
|
|
|
|
ordering = ("name",)
|
2020-12-30 21:48:34 +01:00
|
|
|
verbose_name = _("correspondent")
|
|
|
|
|
verbose_name_plural = _("correspondents")
|
2020-12-16 22:33:03 +01:00
|
|
|
|
2016-03-28 11:11:15 +01:00
|
|
|
|
|
|
|
|
class Tag(MatchingModel):
|
|
|
|
|
|
2022-02-27 15:26:41 +01:00
|
|
|
color = models.CharField(_("color"), max_length=7, default="#a6cee3")
|
2016-01-23 04:40:35 +00:00
|
|
|
|
2018-07-06 13:25:02 +02:00
|
|
|
is_inbox_tag = models.BooleanField(
|
2020-12-30 21:48:34 +01:00
|
|
|
_("is inbox tag"),
|
2018-07-06 13:25:02 +02:00
|
|
|
default=False,
|
2022-02-27 15:26:41 +01:00
|
|
|
help_text=_(
|
|
|
|
|
"Marks this tag as an inbox tag: All newly consumed "
|
2022-03-11 10:55:51 -08:00
|
|
|
"documents will be tagged with inbox tags.",
|
2022-02-27 15:26:41 +01:00
|
|
|
),
|
2018-09-25 16:09:33 +02:00
|
|
|
)
|
2018-07-06 13:25:02 +02:00
|
|
|
|
2020-12-30 21:48:34 +01:00
|
|
|
class Meta:
|
|
|
|
|
verbose_name = _("tag")
|
|
|
|
|
verbose_name_plural = _("tags")
|
|
|
|
|
|
2016-01-23 04:40:35 +00:00
|
|
|
|
2018-08-24 13:45:15 +02:00
|
|
|
class DocumentType(MatchingModel):
|
2020-12-30 21:48:34 +01:00
|
|
|
class Meta:
|
|
|
|
|
verbose_name = _("document type")
|
|
|
|
|
verbose_name_plural = _("document types")
|
|
|
|
|
|
2018-08-24 13:45:15 +02:00
|
|
|
|
2022-05-19 23:42:25 +02:00
|
|
|
class StoragePath(MatchingModel):
|
|
|
|
|
path = models.CharField(
|
|
|
|
|
_("path"),
|
|
|
|
|
max_length=512,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
ordering = ("name",)
|
|
|
|
|
verbose_name = _("storage path")
|
|
|
|
|
verbose_name_plural = _("storage paths")
|
|
|
|
|
|
|
|
|
|
|
2015-12-20 19:23:33 +00:00
|
|
|
class Document(models.Model):
|
|
|
|
|
|
2018-02-04 13:13:24 +00:00
|
|
|
STORAGE_TYPE_UNENCRYPTED = "unencrypted"
|
|
|
|
|
STORAGE_TYPE_GPG = "gpg"
|
|
|
|
|
STORAGE_TYPES = (
|
2020-12-30 21:48:34 +01:00
|
|
|
(STORAGE_TYPE_UNENCRYPTED, _("Unencrypted")),
|
2022-02-27 15:26:41 +01:00
|
|
|
(STORAGE_TYPE_GPG, _("Encrypted with GNU Privacy Guard")),
|
2018-02-04 13:13:24 +00:00
|
|
|
)
|
|
|
|
|
|
2016-03-04 09:14:50 +00:00
|
|
|
correspondent = models.ForeignKey(
|
2017-07-15 19:06:52 +01:00
|
|
|
Correspondent,
|
|
|
|
|
blank=True,
|
|
|
|
|
null=True,
|
|
|
|
|
related_name="documents",
|
2020-12-30 21:48:34 +01:00
|
|
|
on_delete=models.SET_NULL,
|
2022-02-27 15:26:41 +01:00
|
|
|
verbose_name=_("correspondent"),
|
2017-07-15 19:06:52 +01:00
|
|
|
)
|
2017-03-11 16:37:30 +00:00
|
|
|
|
2022-05-19 23:42:25 +02:00
|
|
|
storage_path = models.ForeignKey(
|
|
|
|
|
StoragePath,
|
|
|
|
|
blank=True,
|
|
|
|
|
null=True,
|
|
|
|
|
related_name="documents",
|
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
|
verbose_name=_("storage path"),
|
|
|
|
|
)
|
|
|
|
|
|
2022-02-27 15:26:41 +01:00
|
|
|
title = models.CharField(_("title"), max_length=128, blank=True, db_index=True)
|
2017-03-11 16:37:30 +00:00
|
|
|
|
2018-08-24 13:45:15 +02:00
|
|
|
document_type = models.ForeignKey(
|
|
|
|
|
DocumentType,
|
|
|
|
|
blank=True,
|
|
|
|
|
null=True,
|
|
|
|
|
related_name="documents",
|
2020-12-30 21:48:34 +01:00
|
|
|
on_delete=models.SET_NULL,
|
2022-02-27 15:26:41 +01:00
|
|
|
verbose_name=_("document type"),
|
2018-08-24 13:45:15 +02:00
|
|
|
)
|
|
|
|
|
|
2017-03-11 16:37:30 +00:00
|
|
|
content = models.TextField(
|
2020-12-30 21:48:34 +01:00
|
|
|
_("content"),
|
2017-03-11 16:37:30 +00:00
|
|
|
blank=True,
|
2022-02-27 15:26:41 +01:00
|
|
|
help_text=_(
|
|
|
|
|
"The raw, text-only data of the document. This field is "
|
2022-03-11 10:55:51 -08:00
|
|
|
"primarily used for searching.",
|
2022-02-27 15:26:41 +01:00
|
|
|
),
|
2017-03-11 16:37:30 +00:00
|
|
|
)
|
|
|
|
|
|
2022-02-27 15:26:41 +01:00
|
|
|
mime_type = models.CharField(_("mime type"), max_length=256, editable=False)
|
2017-03-11 16:37:30 +00:00
|
|
|
|
2016-02-08 23:46:16 +00:00
|
|
|
tags = models.ManyToManyField(
|
2022-03-11 10:55:51 -08:00
|
|
|
Tag,
|
|
|
|
|
related_name="documents",
|
|
|
|
|
blank=True,
|
|
|
|
|
verbose_name=_("tags"),
|
2020-12-30 21:48:34 +01:00
|
|
|
)
|
2016-04-03 16:34:09 +01:00
|
|
|
|
|
|
|
|
checksum = models.CharField(
|
2020-12-30 21:48:34 +01:00
|
|
|
_("checksum"),
|
2016-04-03 16:34:09 +01:00
|
|
|
max_length=32,
|
|
|
|
|
editable=False,
|
|
|
|
|
unique=True,
|
2022-02-27 15:26:41 +01:00
|
|
|
help_text=_("The checksum of the original document."),
|
2020-11-29 12:31:26 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
archive_checksum = models.CharField(
|
2020-12-30 21:48:34 +01:00
|
|
|
_("archive checksum"),
|
2020-11-29 12:31:26 +01:00
|
|
|
max_length=32,
|
|
|
|
|
editable=False,
|
|
|
|
|
blank=True,
|
|
|
|
|
null=True,
|
2022-02-27 15:26:41 +01:00
|
|
|
help_text=_("The checksum of the archived document."),
|
2016-04-03 16:34:09 +01:00
|
|
|
)
|
|
|
|
|
|
2022-02-27 15:26:41 +01:00
|
|
|
created = models.DateTimeField(_("created"), default=timezone.now, db_index=True)
|
2020-12-07 21:51:00 +01:00
|
|
|
|
2016-04-03 16:34:09 +01:00
|
|
|
modified = models.DateTimeField(
|
2022-03-11 10:55:51 -08:00
|
|
|
_("modified"),
|
|
|
|
|
auto_now=True,
|
|
|
|
|
editable=False,
|
|
|
|
|
db_index=True,
|
2022-02-27 15:26:41 +01:00
|
|
|
)
|
2018-06-17 16:32:51 +01:00
|
|
|
|
2018-02-04 13:13:24 +00:00
|
|
|
storage_type = models.CharField(
|
2020-12-30 21:48:34 +01:00
|
|
|
_("storage type"),
|
2018-02-04 13:13:24 +00:00
|
|
|
max_length=11,
|
|
|
|
|
choices=STORAGE_TYPES,
|
2018-05-27 23:17:21 +01:00
|
|
|
default=STORAGE_TYPE_UNENCRYPTED,
|
2022-02-27 15:26:41 +01:00
|
|
|
editable=False,
|
2018-02-04 13:13:24 +00:00
|
|
|
)
|
2015-12-26 13:20:52 +00:00
|
|
|
|
2018-04-26 11:58:05 +02:00
|
|
|
added = models.DateTimeField(
|
2022-03-11 10:55:51 -08:00
|
|
|
_("added"),
|
|
|
|
|
default=timezone.now,
|
|
|
|
|
editable=False,
|
|
|
|
|
db_index=True,
|
2022-02-27 15:26:41 +01:00
|
|
|
)
|
2015-12-26 13:20:52 +00:00
|
|
|
|
2020-11-08 13:00:45 +01:00
|
|
|
filename = models.FilePathField(
|
2020-12-30 21:48:34 +01:00
|
|
|
_("filename"),
|
2020-11-11 14:21:33 +01:00
|
|
|
max_length=1024,
|
2020-11-08 13:00:45 +01:00
|
|
|
editable=False,
|
|
|
|
|
default=None,
|
2021-02-09 19:46:19 +01:00
|
|
|
unique=True,
|
2020-11-08 13:00:45 +01:00
|
|
|
null=True,
|
2022-02-27 15:26:41 +01:00
|
|
|
help_text=_("Current filename in storage"),
|
2020-11-08 13:00:45 +01:00
|
|
|
)
|
|
|
|
|
|
2021-02-09 19:46:19 +01:00
|
|
|
archive_filename = models.FilePathField(
|
|
|
|
|
_("archive filename"),
|
|
|
|
|
max_length=1024,
|
|
|
|
|
editable=False,
|
|
|
|
|
default=None,
|
|
|
|
|
unique=True,
|
|
|
|
|
null=True,
|
2022-02-27 15:26:41 +01:00
|
|
|
help_text=_("Current archive filename in storage"),
|
2020-11-08 13:00:45 +01:00
|
|
|
)
|
|
|
|
|
|
2018-07-06 13:25:02 +02:00
|
|
|
archive_serial_number = models.IntegerField(
|
2020-12-30 21:48:34 +01:00
|
|
|
_("archive serial number"),
|
2018-07-06 13:25:02 +02:00
|
|
|
blank=True,
|
|
|
|
|
null=True,
|
|
|
|
|
unique=True,
|
|
|
|
|
db_index=True,
|
2022-02-27 15:26:41 +01:00
|
|
|
help_text=_(
|
2022-03-11 10:55:51 -08:00
|
|
|
"The position of this document in your physical document " "archive.",
|
2022-02-27 15:26:41 +01:00
|
|
|
),
|
2018-09-25 16:09:33 +02:00
|
|
|
)
|
2018-07-06 13:25:02 +02:00
|
|
|
|
2018-05-27 23:21:36 +01:00
|
|
|
class Meta:
|
2020-12-16 18:40:19 +01:00
|
|
|
ordering = ("-created",)
|
2020-12-31 15:59:12 +01:00
|
|
|
verbose_name = _("document")
|
|
|
|
|
verbose_name_plural = _("documents")
|
2015-12-26 13:20:52 +00:00
|
|
|
|
2022-06-10 06:56:28 -07:00
|
|
|
def __str__(self) -> str:
|
2022-05-13 09:10:36 -07:00
|
|
|
|
|
|
|
|
# Convert UTC database time to local time
|
|
|
|
|
created = datetime.date.isoformat(timezone.localdate(self.created))
|
2022-05-01 08:54:41 +02:00
|
|
|
|
2022-06-01 08:03:38 -07:00
|
|
|
res = f"{created}"
|
|
|
|
|
|
|
|
|
|
if self.correspondent:
|
|
|
|
|
res += f" {self.correspondent}"
|
|
|
|
|
if self.title:
|
|
|
|
|
res += f" {self.title}"
|
|
|
|
|
return res
|
2016-01-01 16:13:59 +00:00
|
|
|
|
2020-11-08 13:00:45 +01:00
|
|
|
@property
|
2022-06-10 06:56:28 -07:00
|
|
|
def source_path(self) -> str:
|
2020-11-11 14:21:33 +01:00
|
|
|
if self.filename:
|
|
|
|
|
fname = str(self.filename)
|
|
|
|
|
else:
|
2022-05-06 09:04:08 -07:00
|
|
|
fname = f"{self.pk:07}{self.file_type}"
|
2020-11-11 14:21:33 +01:00
|
|
|
if self.storage_type == self.STORAGE_TYPE_GPG:
|
2020-12-15 13:47:43 +01:00
|
|
|
fname += ".gpg" # pragma: no cover
|
2020-11-08 13:00:45 +01:00
|
|
|
|
2022-02-27 15:26:41 +01:00
|
|
|
return os.path.join(settings.ORIGINALS_DIR, fname)
|
2016-01-01 16:13:59 +00:00
|
|
|
|
|
|
|
|
@property
|
2016-01-29 23:18:03 +00:00
|
|
|
def source_file(self):
|
|
|
|
|
return open(self.source_path, "rb")
|
2016-01-14 19:47:57 +00:00
|
|
|
|
2021-02-09 19:46:19 +01:00
|
|
|
@property
|
2022-06-10 06:56:28 -07:00
|
|
|
def has_archive_version(self) -> bool:
|
2021-02-09 19:46:19 +01:00
|
|
|
return self.archive_filename is not None
|
|
|
|
|
|
2020-11-25 14:47:01 +01:00
|
|
|
@property
|
2022-06-10 06:56:28 -07:00
|
|
|
def archive_path(self) -> Optional[str]:
|
2021-02-09 19:46:19 +01:00
|
|
|
if self.has_archive_version:
|
2022-02-27 15:26:41 +01:00
|
|
|
return os.path.join(settings.ARCHIVE_DIR, str(self.archive_filename))
|
2020-11-30 21:38:21 +01:00
|
|
|
else:
|
2021-02-09 19:46:19 +01:00
|
|
|
return None
|
2020-11-25 14:47:01 +01:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def archive_file(self):
|
|
|
|
|
return open(self.archive_path, "rb")
|
|
|
|
|
|
2022-06-10 06:56:28 -07:00
|
|
|
def get_public_filename(self, archive=False, counter=0, suffix=None) -> str:
|
2020-12-06 19:03:45 +01:00
|
|
|
result = str(self)
|
2020-11-20 13:31:03 +01:00
|
|
|
|
2020-12-06 19:03:45 +01:00
|
|
|
if counter:
|
|
|
|
|
result += f"_{counter:02}"
|
|
|
|
|
|
|
|
|
|
if suffix:
|
|
|
|
|
result += suffix
|
|
|
|
|
|
|
|
|
|
if archive:
|
|
|
|
|
result += ".pdf"
|
|
|
|
|
else:
|
|
|
|
|
result += self.file_type
|
|
|
|
|
|
|
|
|
|
return pathvalidate.sanitize_filename(result, replacement_text="-")
|
2020-11-25 18:01:29 +01:00
|
|
|
|
2020-11-20 13:31:03 +01:00
|
|
|
@property
|
|
|
|
|
def file_type(self):
|
2020-11-30 00:40:04 +01:00
|
|
|
return get_default_file_extension(self.mime_type)
|
2016-02-15 22:38:18 +00:00
|
|
|
|
2016-03-05 01:57:49 +00:00
|
|
|
@property
|
2022-06-10 06:56:28 -07:00
|
|
|
def thumbnail_path(self) -> str:
|
|
|
|
|
png_file_name = f"{self.pk:07}.png"
|
|
|
|
|
webp_file_name = f"{self.pk:07}.webp"
|
2018-02-04 13:13:24 +00:00
|
|
|
if self.storage_type == self.STORAGE_TYPE_GPG:
|
2022-06-10 06:56:28 -07:00
|
|
|
png_file_name += ".gpg"
|
|
|
|
|
webp_file_name += ".gpg"
|
2018-02-04 13:13:24 +00:00
|
|
|
|
2022-06-10 07:59:22 -07:00
|
|
|
# This property is used to both generate the file path
|
|
|
|
|
# and locate the file itself
|
|
|
|
|
# Hence why this looks a little weird
|
|
|
|
|
|
|
|
|
|
webp_file_path = os.path.join(settings.THUMBNAIL_DIR, webp_file_name)
|
2022-06-10 08:56:25 -07:00
|
|
|
png_file_path = os.path.join(settings.THUMBNAIL_DIR, png_file_name)
|
2022-06-10 07:59:22 -07:00
|
|
|
|
|
|
|
|
# 1. Assume the thumbnail is WebP
|
2022-06-11 08:38:49 -07:00
|
|
|
if os.path.exists(png_file_path):
|
|
|
|
|
thumb = png_file_path
|
2022-06-10 07:59:22 -07:00
|
|
|
else:
|
|
|
|
|
thumb = webp_file_path
|
2022-06-11 08:38:49 -07:00
|
|
|
|
2022-06-10 08:56:25 -07:00
|
|
|
return os.path.normpath(thumb)
|
2016-03-05 01:57:49 +00:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def thumbnail_file(self):
|
|
|
|
|
return open(self.thumbnail_path, "rb")
|
|
|
|
|
|
2016-02-27 20:18:50 +00:00
|
|
|
|
|
|
|
|
class Log(models.Model):
|
|
|
|
|
|
|
|
|
|
LEVELS = (
|
2021-01-02 00:45:23 +01:00
|
|
|
(logging.DEBUG, _("debug")),
|
|
|
|
|
(logging.INFO, _("information")),
|
|
|
|
|
(logging.WARNING, _("warning")),
|
|
|
|
|
(logging.ERROR, _("error")),
|
|
|
|
|
(logging.CRITICAL, _("critical")),
|
2016-02-27 20:18:50 +00:00
|
|
|
)
|
|
|
|
|
|
2022-02-27 15:26:41 +01:00
|
|
|
group = models.UUIDField(_("group"), blank=True, null=True)
|
2020-12-30 21:48:34 +01:00
|
|
|
|
|
|
|
|
message = models.TextField(_("message"))
|
|
|
|
|
|
|
|
|
|
level = models.PositiveIntegerField(
|
2022-03-11 10:55:51 -08:00
|
|
|
_("level"),
|
|
|
|
|
choices=LEVELS,
|
|
|
|
|
default=logging.INFO,
|
2022-02-27 15:26:41 +01:00
|
|
|
)
|
2020-12-30 21:48:34 +01:00
|
|
|
|
|
|
|
|
created = models.DateTimeField(_("created"), auto_now_add=True)
|
2016-02-28 00:41:03 +00:00
|
|
|
|
2018-05-27 23:21:36 +01:00
|
|
|
class Meta:
|
2020-11-02 01:24:56 +01:00
|
|
|
ordering = ("-created",)
|
2020-12-30 21:48:34 +01:00
|
|
|
verbose_name = _("log")
|
|
|
|
|
verbose_name_plural = _("logs")
|
2016-02-27 20:18:50 +00:00
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.message
|
2016-03-06 17:26:07 +00:00
|
|
|
|
2020-11-21 12:12:19 +01:00
|
|
|
|
2020-12-12 15:46:56 +01:00
|
|
|
class SavedView(models.Model):
|
2020-12-16 13:49:48 +01:00
|
|
|
class Meta:
|
|
|
|
|
|
2020-12-16 21:08:03 +01:00
|
|
|
ordering = ("name",)
|
2020-12-30 21:48:34 +01:00
|
|
|
verbose_name = _("saved view")
|
|
|
|
|
verbose_name_plural = _("saved views")
|
2020-12-16 13:49:48 +01:00
|
|
|
|
2022-02-27 15:26:41 +01:00
|
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_("user"))
|
|
|
|
|
name = models.CharField(_("name"), max_length=128)
|
2020-12-12 15:46:56 +01:00
|
|
|
|
2020-12-30 21:48:34 +01:00
|
|
|
show_on_dashboard = models.BooleanField(
|
|
|
|
|
_("show on dashboard"),
|
|
|
|
|
)
|
|
|
|
|
show_in_sidebar = models.BooleanField(
|
|
|
|
|
_("show in sidebar"),
|
|
|
|
|
)
|
2020-12-12 15:46:56 +01:00
|
|
|
|
2020-12-30 21:48:34 +01:00
|
|
|
sort_field = models.CharField(
|
2022-03-11 10:55:51 -08:00
|
|
|
_("sort field"),
|
|
|
|
|
max_length=128,
|
|
|
|
|
null=True,
|
|
|
|
|
blank=True,
|
2021-03-17 22:25:22 +01:00
|
|
|
)
|
2022-02-27 15:26:41 +01:00
|
|
|
sort_reverse = models.BooleanField(_("sort reverse"), default=False)
|
2020-12-12 15:46:56 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class SavedViewFilterRule(models.Model):
|
|
|
|
|
RULE_TYPES = [
|
2020-12-30 21:48:34 +01:00
|
|
|
(0, _("title contains")),
|
|
|
|
|
(1, _("content contains")),
|
|
|
|
|
(2, _("ASN is")),
|
|
|
|
|
(3, _("correspondent is")),
|
|
|
|
|
(4, _("document type is")),
|
|
|
|
|
(5, _("is in inbox")),
|
|
|
|
|
(6, _("has tag")),
|
|
|
|
|
(7, _("has any tag")),
|
|
|
|
|
(8, _("created before")),
|
|
|
|
|
(9, _("created after")),
|
|
|
|
|
(10, _("created year is")),
|
|
|
|
|
(11, _("created month is")),
|
|
|
|
|
(12, _("created day is")),
|
|
|
|
|
(13, _("added before")),
|
|
|
|
|
(14, _("added after")),
|
|
|
|
|
(15, _("modified before")),
|
|
|
|
|
(16, _("modified after")),
|
|
|
|
|
(17, _("does not have tag")),
|
2021-02-28 16:19:41 +01:00
|
|
|
(18, _("does not have ASN")),
|
|
|
|
|
(19, _("title or content contains")),
|
2021-03-17 22:25:22 +01:00
|
|
|
(20, _("fulltext query")),
|
2022-02-14 22:23:31 -08:00
|
|
|
(21, _("more like this")),
|
2022-02-27 15:26:41 +01:00
|
|
|
(22, _("has tags in")),
|
2020-12-12 15:46:56 +01:00
|
|
|
]
|
|
|
|
|
|
2020-12-15 12:06:24 +01:00
|
|
|
saved_view = models.ForeignKey(
|
|
|
|
|
SavedView,
|
|
|
|
|
on_delete=models.CASCADE,
|
2020-12-30 21:48:34 +01:00
|
|
|
related_name="filter_rules",
|
2022-02-27 15:26:41 +01:00
|
|
|
verbose_name=_("saved view"),
|
2020-12-15 12:06:24 +01:00
|
|
|
)
|
2020-12-12 15:46:56 +01:00
|
|
|
|
2022-02-27 15:26:41 +01:00
|
|
|
rule_type = models.PositiveIntegerField(_("rule type"), choices=RULE_TYPES)
|
2020-12-30 21:48:34 +01:00
|
|
|
|
2022-04-01 15:54:45 -07:00
|
|
|
value = models.CharField(_("value"), max_length=255, blank=True, null=True)
|
2020-12-12 15:46:56 +01:00
|
|
|
|
2020-12-30 21:48:34 +01:00
|
|
|
class Meta:
|
|
|
|
|
verbose_name = _("filter rule")
|
|
|
|
|
verbose_name_plural = _("filter rules")
|
2020-12-12 15:46:56 +01:00
|
|
|
|
|
|
|
|
|
2020-11-20 16:18:59 +01:00
|
|
|
# TODO: why is this in the models file?
|
2022-04-12 19:52:56 -07:00
|
|
|
# TODO: how about, what is this and where is it documented?
|
|
|
|
|
# It appears to parsing JSON from an environment variable to get a title and date from
|
|
|
|
|
# the filename, if possible, as a higher priority than either document filename or
|
|
|
|
|
# content parsing
|
2018-04-22 16:28:03 +01:00
|
|
|
class FileInfo:
|
2016-03-24 19:18:33 +00:00
|
|
|
|
2022-02-27 15:26:41 +01:00
|
|
|
REGEXES = OrderedDict(
|
|
|
|
|
[
|
|
|
|
|
(
|
|
|
|
|
"created-title",
|
|
|
|
|
re.compile(
|
2022-04-12 19:52:56 -07:00
|
|
|
r"^(?P<created>\d{8}(\d{6})?Z) - " r"(?P<title>.*)$",
|
2022-02-27 15:26:41 +01:00
|
|
|
flags=re.IGNORECASE,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
("title", re.compile(r"(?P<title>.*)$", flags=re.IGNORECASE)),
|
2022-03-11 10:55:51 -08:00
|
|
|
],
|
2022-02-27 15:26:41 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def __init__(
|
2022-03-11 10:55:51 -08:00
|
|
|
self,
|
|
|
|
|
created=None,
|
|
|
|
|
correspondent=None,
|
|
|
|
|
title=None,
|
|
|
|
|
tags=(),
|
|
|
|
|
extension=None,
|
2022-02-27 15:26:41 +01:00
|
|
|
):
|
2016-03-24 19:18:33 +00:00
|
|
|
|
|
|
|
|
self.created = created
|
|
|
|
|
self.title = title
|
|
|
|
|
self.extension = extension
|
|
|
|
|
self.correspondent = correspondent
|
|
|
|
|
self.tags = tags
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def _get_created(cls, created):
|
2018-04-22 16:27:43 +01:00
|
|
|
try:
|
2022-05-06 09:04:08 -07:00
|
|
|
return dateutil.parser.parse(f"{created[:-1]:0<14}Z")
|
2018-04-22 16:27:43 +01:00
|
|
|
except ValueError:
|
|
|
|
|
return None
|
2016-03-24 19:18:33 +00:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def _get_title(cls, title):
|
|
|
|
|
return title
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def _mangle_property(cls, properties, name):
|
|
|
|
|
if name in properties:
|
2022-05-06 09:04:08 -07:00
|
|
|
properties[name] = getattr(cls, f"_get_{name}")(properties[name])
|
2016-03-24 19:18:33 +00:00
|
|
|
|
|
|
|
|
@classmethod
|
2022-04-12 19:52:56 -07:00
|
|
|
def from_filename(cls, filename) -> "FileInfo":
|
2019-09-08 17:00:02 +02:00
|
|
|
# Mutate filename in-place before parsing its components
|
|
|
|
|
# by applying at most one of the configured transformations.
|
2019-05-18 19:25:50 +02:00
|
|
|
for (pattern, repl) in settings.FILENAME_PARSE_TRANSFORMS:
|
|
|
|
|
(filename, count) = pattern.subn(repl, filename)
|
|
|
|
|
if count:
|
|
|
|
|
break
|
|
|
|
|
|
2020-11-20 16:18:59 +01:00
|
|
|
# do this after the transforms so that the transforms can do whatever
|
|
|
|
|
# with the file extension.
|
|
|
|
|
filename_no_ext = os.path.splitext(filename)[0]
|
|
|
|
|
|
|
|
|
|
if filename_no_ext == filename and filename.startswith("."):
|
|
|
|
|
# This is a very special case where there is no text before the
|
|
|
|
|
# file type.
|
|
|
|
|
# TODO: this should be handled better. The ext is not removed
|
|
|
|
|
# because usually, files like '.pdf' are just hidden files
|
|
|
|
|
# with the name pdf, but in our case, its more likely that
|
|
|
|
|
# there's just no name to begin with.
|
|
|
|
|
filename = ""
|
|
|
|
|
# This isn't too bad either, since we'll just not match anything
|
|
|
|
|
# and return an empty title. TODO: actually, this is kinda bad.
|
|
|
|
|
else:
|
|
|
|
|
filename = filename_no_ext
|
|
|
|
|
|
2019-09-08 17:00:02 +02:00
|
|
|
# Parse filename components.
|
2016-03-24 19:18:33 +00:00
|
|
|
for regex in cls.REGEXES.values():
|
2019-05-18 19:25:50 +02:00
|
|
|
m = regex.match(filename)
|
2016-03-24 19:18:33 +00:00
|
|
|
if m:
|
|
|
|
|
properties = m.groupdict()
|
|
|
|
|
cls._mangle_property(properties, "created")
|
|
|
|
|
cls._mangle_property(properties, "title")
|
|
|
|
|
return cls(**properties)
|
2022-05-06 22:10:35 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# Extending User Model Using a One-To-One Link
|
2022-05-07 08:11:10 -07:00
|
|
|
class UiSettings(models.Model):
|
2022-05-06 22:10:35 -07:00
|
|
|
|
|
|
|
|
user = models.OneToOneField(
|
|
|
|
|
User,
|
|
|
|
|
on_delete=models.CASCADE,
|
2022-05-07 08:11:10 -07:00
|
|
|
related_name="ui_settings",
|
2022-05-06 22:10:35 -07:00
|
|
|
)
|
|
|
|
|
settings = models.JSONField(null=True)
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
return self.user.username
|