From e5c552996353f4dbe36ff60baba4e1ca04bab71c Mon Sep 17 00:00:00 2001 From: Edward Oliveira Date: Sat, 18 Apr 2026 16:06:52 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20introduce=20FileMetadata=20model=20and?= =?UTF-8?q?=20MetadataFileField=20(RFC=20=C2=A71=E2=80=93=C2=A76.2,=20?= =?UTF-8?q?=C2=A711.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the foundational infrastructure for the file-serving redesign described in docs/rfc/files-metafields.md. What changed: sapl/base/models.py - Add FileMetadata model (db_table=base_file_metadata): stable uuid, storage_name, original_filename, file_size_bytes, content_hash, version, backfilled_at. The uuid is the stable public identifier that survives file replacements; storage_name is the storage-backend key (relative path or S3 object key). sapl/base/fields.py (new) - MetadataFileField: FileField subclass that injects a companion FK (_metadata → base.FileMetadata) via contribute_to_class and manages the FileMetadata row lifecycle in pre_save (create on first upload, update-in-place on replacement keeping uuid stable, nullify+delete on clear, no-op on re-save). sapl/utils.py - fabrica_validador_de_tipos_de_arquivo: also set __qualname__ = nome so Django 2.2's migration serializer (which checks __qualname__ for '') can resolve the function as sapl.utils.. 14 models across 5 apps swapped FileField → MetadataFileField: norma: NormaJuridica.texto_integral, AnexoNormaJuridica.anexo_arquivo materia: MateriaLegislativa.texto_original, DocumentoAcessorio.arquivo, Proposicao.texto_original sessao: SessaoPlenaria.upload_{pauta,ata,anexo}, AbstractOrador.upload_anexo (→ Orador, OradorExpediente, OradorOrdemDia), JustificativaAusencia comissoes: Reuniao.upload_{pauta,ata,anexo}, DocumentoAcessorio.arquivo audiencia: AudienciaPublica.upload_{pauta,ata,anexo}, AnexoAudienciaPublica.arquivo protocoloadm: DocumentoAdministrativo.texto_integral, DocumentoAcessorioAdministrativo.arquivo 6 migrations adding _metadata FK columns (AddField, pure SQL; no existing rows touched). sapl/api/views_base.py - Remove FileMetadata from the auto-built API set after build_class to prevent UUID enumeration and storage_name leakage (RFC §11.3). Not yet implemented (follow-up commits): - serve_file / serve_model_file views (RFC §6.4, §9) - MetadataFileField.url() returning semantic aliases (RFC §9) - MetadataOverwriteStorage with UUID-based paths (RFC §6.3) - backfill_file_metadata management command (RFC §8) - SaplSerializerMixin integration for API canonical URLs (RFC §10) - nginx /media/ internal + ConditionalGetMiddleware (RFC §6.4) Co-Authored-By: Claude Sonnet 4.6 --- sapl/api/views_base.py | 9 +- .../migrations/0021_metadata_filefield.py | 63 +++++++++ sapl/audiencia/models.py | 9 +- sapl/base/fields.py | 120 ++++++++++++++++++ sapl/base/migrations/0061_file_metadata.py | 32 +++++ sapl/base/models.py | 63 +++++++++ .../migrations/0031_metadata_filefield.py | 58 +++++++++ sapl/comissoes/models.py | 9 +- .../migrations/0088_metadata_filefield.py | 68 ++++++++++ sapl/materia/models.py | 7 +- .../migrations/0046_metadata_filefield.py | 53 ++++++++ sapl/norma/models.py | 5 +- .../migrations/0046_metadata_filefield.py | 47 +++++++ sapl/protocoloadm/models.py | 5 +- .../migrations/0070_metadata_filefield.py | 88 +++++++++++++ sapl/sessao/models.py | 11 +- sapl/utils.py | 6 +- 17 files changed, 631 insertions(+), 22 deletions(-) create mode 100644 sapl/audiencia/migrations/0021_metadata_filefield.py create mode 100644 sapl/base/fields.py create mode 100644 sapl/base/migrations/0061_file_metadata.py create mode 100644 sapl/comissoes/migrations/0031_metadata_filefield.py create mode 100644 sapl/materia/migrations/0088_metadata_filefield.py create mode 100644 sapl/norma/migrations/0046_metadata_filefield.py create mode 100644 sapl/protocoloadm/migrations/0046_metadata_filefield.py create mode 100644 sapl/sessao/migrations/0070_metadata_filefield.py diff --git a/sapl/api/views_base.py b/sapl/api/views_base.py index 04dbd3fc9..fdea5e23b 100644 --- a/sapl/api/views_base.py +++ b/sapl/api/views_base.py @@ -10,7 +10,7 @@ from rest_framework.response import Response from drfautoapi.drfautoapi import ApiViewSetConstrutor, customize from sapl.api.forms import AutoresPossiveisFilterSet from sapl.api.serializers import ChoiceSerializer -from sapl.base.models import Autor, TipoAutor +from sapl.base.models import Autor, FileMetadata, TipoAutor from sapl.utils import models_with_gr_for_model, SaplGenericRelation @@ -23,6 +23,13 @@ ApiViewSetConstrutor.build_class( ] ) +# FileMetadata is an infrastructure model, not a domain model. +# Exposing it would allow UUID enumeration (collapsing access control to zero +# for public files) and leak storage_name (internal filesystem/S3 paths). +# See RFC §11.3. +del ApiViewSetConstrutor._built_sets[ + apps.get_app_config('base')][FileMetadata] + @customize(ContentType) class _ContentTypeSet: diff --git a/sapl/audiencia/migrations/0021_metadata_filefield.py b/sapl/audiencia/migrations/0021_metadata_filefield.py new file mode 100644 index 000000000..f3e4b653f --- /dev/null +++ b/sapl/audiencia/migrations/0021_metadata_filefield.py @@ -0,0 +1,63 @@ +# Generated by Django 2.2.28 on 2026-04-18 19:03 + +from django.db import migrations, models +import django.db.models.deletion +import sapl.audiencia.models +import sapl.base.fields +import sapl.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0061_file_metadata'), + ('audiencia', '0020_auto_20251201_1450'), + ] + + operations = [ + migrations.AddField( + model_name='anexoaudienciapublica', + name='arquivo_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='audienciapublica', + name='upload_anexo_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='audienciapublica', + name='upload_ata_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='audienciapublica', + name='upload_pauta_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AlterField( + model_name='anexoaudienciapublica', + name='arquivo', + field=sapl.base.fields.MetadataFileField(max_length=300, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.utils.texto_upload_path, verbose_name='Arquivo'), + ), + migrations.AlterField( + model_name='audienciapublica', + name='ano', + field=models.PositiveSmallIntegerField(choices=[(2027, 2027), (2026, 2026), (2025, 2025), (2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano'), + ), + migrations.AlterField( + model_name='audienciapublica', + name='upload_anexo', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.audiencia.models.anexo_upload_path, verbose_name='Anexo da Audiência Pública'), + ), + migrations.AlterField( + model_name='audienciapublica', + name='upload_ata', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.audiencia.models.ata_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Ata da Audiência Pública'), + ), + migrations.AlterField( + model_name='audienciapublica', + name='upload_pauta', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.audiencia.models.pauta_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Pauta da Audiência Pública'), + ), + ] diff --git a/sapl/audiencia/models.py b/sapl/audiencia/models.py index 6b74cffd0..1180eb4f7 100755 --- a/sapl/audiencia/models.py +++ b/sapl/audiencia/models.py @@ -5,6 +5,7 @@ from model_utils import Choices from sapl.materia.models import MateriaLegislativa from sapl.parlamentares.models import (CargoMesa, Parlamentar) +from sapl.base.fields import MetadataFileField from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericRelation, restringe_tipos_de_arquivo_txt, texto_upload_path, OverwriteStorage) @@ -98,7 +99,7 @@ class AudienciaPublica(models.Model): url_video = models.URLField( max_length=150, blank=True, verbose_name=_('URL Arquivo Vídeo (Formatos MP4 / FLV / WebM)')) - upload_pauta = models.FileField( + upload_pauta = MetadataFileField( max_length=300, blank=True, null=True, @@ -106,7 +107,7 @@ class AudienciaPublica(models.Model): storage=OverwriteStorage(), verbose_name=_('Pauta da Audiência Pública'), validators=[restringe_tipos_de_arquivo_txt]) - upload_ata = models.FileField( + upload_ata = MetadataFileField( max_length=300, blank=True, null=True, @@ -114,7 +115,7 @@ class AudienciaPublica(models.Model): verbose_name=_('Ata da Audiência Pública'), storage=OverwriteStorage(), validators=[restringe_tipos_de_arquivo_txt]) - upload_anexo = models.FileField( + upload_anexo = MetadataFileField( max_length=300, blank=True, null=True, @@ -177,7 +178,7 @@ class AudienciaPublica(models.Model): class AnexoAudienciaPublica(models.Model): audiencia = models.ForeignKey(AudienciaPublica, on_delete=models.PROTECT) - arquivo = models.FileField( + arquivo = MetadataFileField( max_length=300, upload_to=texto_upload_path, storage=OverwriteStorage(), diff --git a/sapl/base/fields.py b/sapl/base/fields.py new file mode 100644 index 000000000..c521cef5c --- /dev/null +++ b/sapl/base/fields.py @@ -0,0 +1,120 @@ +import hashlib +from pathlib import Path + +from django.core.files.storage import default_storage +from django.db import models + + +def _compute_size_and_hash(field_file): + """ + Read the file content once to compute size and SHA-256 digest. + field_file must be open-able via field_file.open(). + Returns (size_in_bytes, hex_digest). + """ + h = hashlib.sha256() + size = 0 + with field_file.open('rb') as fh: + for chunk in iter(lambda: fh.read(65536), b''): + h.update(chunk) + size += len(chunk) + return size, h.hexdigest() + + +class MetadataFileField(models.FileField): + """ + Drop-in replacement for models.FileField. + + In addition to normal FileField behaviour, this field: + 1. Injects a companion ForeignKey '_metadata' pointing to + base.FileMetadata on the owning model class at class-definition time. + 2. In pre_save, creates or updates the FileMetadata row that tracks the + stable uuid, storage_name, original_filename, size, and hash for the + uploaded file. + + Four lifecycle scenarios handled in pre_save: + Case 1 — first upload : create a new FileMetadata row; set the FK. + Case 2 — replacement : delete the old physical file; update the existing + FileMetadata row in-place so the uuid (and therefore + /documentos//) never changes. + Case 3 — field cleared : nullify the FK; delete the FileMetadata row; + physical file cleanup is deferred to the + clean_orphan_files management command. + Case 4 — no-op re-save : nothing touched. + """ + + def contribute_to_class(self, cls, name): + super().contribute_to_class(cls, name) + # Inject companion FK: e.g. texto_original → texto_original_metadata_id + fk = models.ForeignKey( + 'base.FileMetadata', + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='+', + verbose_name='File metadata', + ) + cls.add_to_class(f'{name}_metadata', fk) + + def pre_save(self, instance, add): + from sapl.base.models import FileMetadata + + meta_attr = f'{self.attname}_metadata' + file_before = getattr(instance, self.attname) + meta_before = getattr(instance, meta_attr, None) + + # Capture intent BEFORE super() — storage.save() sets _committed=True, + # erasing the distinction between "new upload" and "already committed". + has_new_upload = bool(file_before) and not getattr(file_before, '_committed', True) + is_clearing = not file_before and meta_before is not None + + # Capture browser-supplied filename before storage renames it to the UUID path. + if has_new_upload and hasattr(file_before, 'file'): + original_filename = Path(file_before.file.name).name + else: + original_filename = '' + + file = super().pre_save(instance, add) + # file.name is now the full storage path produced by upload_to, + # e.g. "sapl/public/normajuridica/2025/9395/.pdf" + + if is_clearing: + # Case 3: ClearableFileInput submitted with clear=True. + # Nullify FK on the in-memory instance immediately so the subsequent + # model.save() writes NULL — do NOT rely on SET_NULL cascade, which + # only fires in the DB. Physical file left for offline cleanup. + setattr(instance, f'{meta_attr}_id', None) + meta_before.delete() + + elif file and has_new_upload: + storage_name = file.name + size, digest = _compute_size_and_hash(file) + + if meta_before is None: + # Case 1: first upload — create a new FileMetadata row. + meta = FileMetadata( + storage_name=storage_name, + original_filename=original_filename, + file_size_bytes=size, + content_hash=digest, + ) + meta.save() + setattr(instance, f'{meta_attr}_id', meta.pk) + else: + # Case 2: replacement — reuse the existing row so the stable uuid + # (and /documentos//) never changes. Delete old physical file + # only after the new one is confirmed saved (super() already returned). + try: + default_storage.delete(meta_before.storage_name) + except OSError: + pass # already gone — proceed + meta_before.version += 1 + meta_before.storage_name = storage_name + meta_before.original_filename = original_filename + meta_before.file_size_bytes = size + meta_before.content_hash = digest + meta_before.save(update_fields=[ + 'version', 'storage_name', 'original_filename', + 'file_size_bytes', 'content_hash', + ]) + + return file diff --git a/sapl/base/migrations/0061_file_metadata.py b/sapl/base/migrations/0061_file_metadata.py new file mode 100644 index 000000000..c4187f38a --- /dev/null +++ b/sapl/base/migrations/0061_file_metadata.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2.28 on 2026-04-18 18:56 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0060_auto_20240812_1628'), + ] + + operations = [ + migrations.CreateModel( + name='FileMetadata', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='UUID')), + ('storage_name', models.CharField(max_length=512, verbose_name='Storage name')), + ('original_filename', models.CharField(max_length=512, verbose_name='Original filename')), + ('file_size_bytes', models.BigIntegerField(blank=True, null=True, verbose_name='File size (bytes)')), + ('content_hash', models.CharField(blank=True, default='', max_length=64, verbose_name='SHA-256 hash')), + ('version', models.PositiveIntegerField(default=1, verbose_name='Version')), + ('backfilled_at', models.DateTimeField(blank=True, null=True, verbose_name='Backfilled at')), + ], + options={ + 'verbose_name': 'File Metadata', + 'verbose_name_plural': 'File Metadata', + 'db_table': 'base_file_metadata', + }, + ), + ] diff --git a/sapl/base/models.py b/sapl/base/models.py index 692c55138..f6475c56b 100644 --- a/sapl/base/models.py +++ b/sapl/base/models.py @@ -1,3 +1,5 @@ +from uuid import uuid4 + from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields.jsonb import JSONField @@ -482,3 +484,64 @@ class Metadata(models.Model): def __str__(self): return f'Metadata de {self.content_object}' + + +class FileMetadata(models.Model): + """ + Central registry for every uploaded file across all SAPL apps. + + uuid — stable public identifier; never changes even on file replacement. + Bookmarked /documentos// URLs survive re-uploads. + storage_name — key used by the storage backend to locate the physical file. + For existing (pre-migration) files this is the original relative + path; for new files it is '{uuid}{ext}' under the same upload_to + directory. serve_file resolves uuid → storage_name transparently. + original_filename — user-visible name from the upload; used in Content-Disposition. + version — increments on every file replacement so CDN ETags stay accurate. + file_size_bytes / content_hash — populated lazily by the backfill management command; + new uploads populate them synchronously in MetadataFileField.pre_save. + backfilled_at — set by the management command so operators can track backfill progress. + """ + + uuid = models.UUIDField( + default=uuid4, + editable=False, + unique=True, + verbose_name=_('UUID'), + ) + storage_name = models.CharField( + max_length=512, + verbose_name=_('Storage name'), + ) + original_filename = models.CharField( + max_length=512, + verbose_name=_('Original filename'), + ) + file_size_bytes = models.BigIntegerField( + null=True, + blank=True, + verbose_name=_('File size (bytes)'), + ) + content_hash = models.CharField( + max_length=64, + blank=True, + default='', + verbose_name=_('SHA-256 hash'), + ) + version = models.PositiveIntegerField( + default=1, + verbose_name=_('Version'), + ) + backfilled_at = models.DateTimeField( + null=True, + blank=True, + verbose_name=_('Backfilled at'), + ) + + class Meta: + db_table = 'base_file_metadata' + verbose_name = _('File Metadata') + verbose_name_plural = _('File Metadata') + + def __str__(self): + return f'{self.original_filename} (v{self.version})' diff --git a/sapl/comissoes/migrations/0031_metadata_filefield.py b/sapl/comissoes/migrations/0031_metadata_filefield.py new file mode 100644 index 000000000..935840871 --- /dev/null +++ b/sapl/comissoes/migrations/0031_metadata_filefield.py @@ -0,0 +1,58 @@ +# Generated by Django 2.2.28 on 2026-04-18 19:03 + +from django.db import migrations, models +import django.db.models.deletion +import sapl.base.fields +import sapl.comissoes.models +import sapl.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0061_file_metadata'), + ('comissoes', '0030_auto_20231007_2149'), + ] + + operations = [ + migrations.AddField( + model_name='documentoacessorio', + name='arquivo_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='reuniao', + name='upload_anexo_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='reuniao', + name='upload_ata_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='reuniao', + name='upload_pauta_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AlterField( + model_name='documentoacessorio', + name='arquivo', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.comissoes.models.anexo_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Integral'), + ), + migrations.AlterField( + model_name='reuniao', + name='upload_anexo', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.comissoes.models.anexo_upload_path, verbose_name='Anexo da Reunião'), + ), + migrations.AlterField( + model_name='reuniao', + name='upload_ata', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.comissoes.models.ata_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Ata da Reunião'), + ), + migrations.AlterField( + model_name='reuniao', + name='upload_pauta', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.comissoes.models.pauta_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Pauta da Reunião'), + ), + ] diff --git a/sapl/comissoes/models.py b/sapl/comissoes/models.py index 2d9a323d3..adc36f98b 100644 --- a/sapl/comissoes/models.py +++ b/sapl/comissoes/models.py @@ -4,6 +4,7 @@ from model_utils import Choices from sapl.base.models import Autor from sapl.parlamentares.models import Parlamentar +from sapl.base.fields import MetadataFileField from sapl.utils import (YES_NO_CHOICES, SaplGenericRelation, restringe_tipos_de_arquivo_txt, texto_upload_path, OverwriteStorage) @@ -234,21 +235,21 @@ class Reuniao(models.Model): url_video = models.URLField( max_length=150, blank=True, verbose_name=_('URL do Arquivo de Vídeo (Formatos MP4 / FLV / WebM)')) - upload_pauta = models.FileField( + upload_pauta = MetadataFileField( max_length=300, blank=True, null=True, upload_to=pauta_upload_path, verbose_name=_('Pauta da Reunião'), storage=OverwriteStorage(), validators=[restringe_tipos_de_arquivo_txt]) - upload_ata = models.FileField( + upload_ata = MetadataFileField( max_length=300, blank=True, null=True, upload_to=ata_upload_path, verbose_name=_('Ata da Reunião'), storage=OverwriteStorage(), validators=[restringe_tipos_de_arquivo_txt]) - upload_anexo = models.FileField( + upload_anexo = MetadataFileField( max_length=300, blank=True, null=True, upload_to=anexo_upload_path, @@ -319,7 +320,7 @@ class DocumentoAcessorio(models.Model): max_length=200, verbose_name=_('Autor')) ementa = models.TextField(blank=True, verbose_name=_('Ementa')) indexacao = models.TextField(blank=True) - arquivo = models.FileField( + arquivo = MetadataFileField( max_length=300, blank=True, null=True, diff --git a/sapl/materia/migrations/0088_metadata_filefield.py b/sapl/materia/migrations/0088_metadata_filefield.py new file mode 100644 index 000000000..36efc5616 --- /dev/null +++ b/sapl/materia/migrations/0088_metadata_filefield.py @@ -0,0 +1,68 @@ +# Generated by Django 2.2.28 on 2026-04-18 19:03 + +from django.db import migrations, models +import django.db.models.deletion +import sapl.base.fields +import sapl.materia.models +import sapl.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0061_file_metadata'), + ('materia', '0087_update_viewdb_materiaemtramitacao'), + ] + + operations = [ + migrations.AddField( + model_name='documentoacessorio', + name='arquivo_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='materialegislativa', + name='texto_original_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='proposicao', + name='texto_original_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AlterField( + model_name='documentoacessorio', + name='arquivo', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.materia.models.anexo_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Integral'), + ), + migrations.AlterField( + model_name='materialegislativa', + name='ano', + field=models.PositiveSmallIntegerField(choices=[(2027, 2027), (2026, 2026), (2025, 2025), (2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano'), + ), + migrations.AlterField( + model_name='materialegislativa', + name='ano_origem_externa', + field=models.PositiveSmallIntegerField(blank=True, choices=[(2027, 2027), (2026, 2026), (2025, 2025), (2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], null=True, verbose_name='Ano'), + ), + migrations.AlterField( + model_name='materialegislativa', + name='texto_original', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.materia.models.materia_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Original'), + ), + migrations.AlterField( + model_name='numeracao', + name='ano_materia', + field=models.PositiveSmallIntegerField(choices=[(2027, 2027), (2026, 2026), (2025, 2025), (2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano'), + ), + migrations.AlterField( + model_name='proposicao', + name='ano', + field=models.PositiveSmallIntegerField(blank=True, choices=[(2027, 2027), (2026, 2026), (2025, 2025), (2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], default=None, null=True, verbose_name='Ano'), + ), + migrations.AlterField( + model_name='proposicao', + name='texto_original', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.materia.models.materia_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Original'), + ), + ] diff --git a/sapl/materia/models.py b/sapl/materia/models.py index bdeb402e3..807c69c6d 100644 --- a/sapl/materia/models.py +++ b/sapl/materia/models.py @@ -19,6 +19,7 @@ from sapl.parlamentares.models import Legislatura from sapl.compilacao.models import (PerfilEstruturalTextoArticulado, TextoArticulado) from sapl.parlamentares.models import Parlamentar +from sapl.base.fields import MetadataFileField from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericForeignKey, SaplGenericRelation, restringe_tipos_de_arquivo_txt, texto_upload_path, get_settings_auth_user_model, @@ -264,7 +265,7 @@ class MateriaLegislativa(models.Model): through_fields=( 'materia_principal', 'materia_anexada')) - texto_original = models.FileField( + texto_original = MetadataFileField( max_length=300, blank=True, null=True, @@ -652,7 +653,7 @@ class DocumentoAcessorio(models.Model): max_length=200, blank=True, verbose_name=_('Autor')) ementa = models.TextField(blank=True, verbose_name=_('Ementa')) indexacao = models.TextField(blank=True, verbose_name=_('Indexação')) - arquivo = models.FileField( + arquivo = MetadataFileField( blank=True, null=True, max_length=300, @@ -976,7 +977,7 @@ class Proposicao(models.Model): verbose_name=_('Status Proposição') ) - texto_original = models.FileField( + texto_original = MetadataFileField( max_length=300, upload_to=materia_upload_path, blank=True, diff --git a/sapl/norma/migrations/0046_metadata_filefield.py b/sapl/norma/migrations/0046_metadata_filefield.py new file mode 100644 index 000000000..bc1751820 --- /dev/null +++ b/sapl/norma/migrations/0046_metadata_filefield.py @@ -0,0 +1,53 @@ +# Generated by Django 2.2.28 on 2026-04-18 18:59 + +from django.db import migrations, models +import django.db.models.deletion +import sapl.base.fields +import sapl.norma.models +import sapl.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0061_file_metadata'), + ('norma', '0045_auto_20240711_1405'), + ] + + operations = [ + migrations.AddField( + model_name='anexonormajuridica', + name='anexo_arquivo_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='normajuridica', + name='texto_integral_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AlterField( + model_name='anexonormajuridica', + name='anexo_arquivo', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.norma.models.norma_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Arquivo Anexo'), + ), + migrations.AlterField( + model_name='anexonormajuridica', + name='ano', + field=models.PositiveSmallIntegerField(choices=[(2027, 2027), (2026, 2026), (2025, 2025), (2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano'), + ), + migrations.AlterField( + model_name='normaestatisticas', + name='ano', + field=models.PositiveSmallIntegerField(choices=[(2027, 2027), (2026, 2026), (2025, 2025), (2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], default=sapl.norma.models.get_ano_atual, verbose_name='Ano'), + ), + migrations.AlterField( + model_name='normajuridica', + name='ano', + field=models.PositiveSmallIntegerField(choices=[(2027, 2027), (2026, 2026), (2025, 2025), (2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano'), + ), + migrations.AlterField( + model_name='normajuridica', + name='texto_integral', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.norma.models.norma_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Texto Original'), + ), + ] diff --git a/sapl/norma/models.py b/sapl/norma/models.py index 668f93347..424a8eee2 100644 --- a/sapl/norma/models.py +++ b/sapl/norma/models.py @@ -8,6 +8,7 @@ from model_utils import Choices from sapl.base.models import Autor from sapl.compilacao.models import TextoArticulado from sapl.materia.models import MateriaLegislativa, Orgao +from sapl.base.fields import MetadataFileField from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, restringe_tipos_de_arquivo_txt, texto_upload_path, @@ -133,7 +134,7 @@ class NormaJuridica(models.Model): ('F', 'federal', _('Federal')), ) - texto_integral = models.FileField( + texto_integral = MetadataFileField( max_length=300, blank=True, null=True, @@ -487,7 +488,7 @@ class AnexoNormaJuridica(models.Model): verbose_name=_('Assunto do Anexo'), max_length=250 ) - anexo_arquivo = models.FileField( + anexo_arquivo = MetadataFileField( max_length=300, blank=True, null=True, diff --git a/sapl/protocoloadm/migrations/0046_metadata_filefield.py b/sapl/protocoloadm/migrations/0046_metadata_filefield.py new file mode 100644 index 000000000..846062411 --- /dev/null +++ b/sapl/protocoloadm/migrations/0046_metadata_filefield.py @@ -0,0 +1,47 @@ +# Generated by Django 2.2.28 on 2026-04-18 19:03 + +from django.db import migrations, models +import django.db.models.deletion +import sapl.base.fields +import sapl.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0061_file_metadata'), + ('protocoloadm', '0045_auto_20240711_1405'), + ] + + operations = [ + migrations.AddField( + model_name='documentoacessorioadministrativo', + name='arquivo_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='documentoadministrativo', + name='texto_integral_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AlterField( + model_name='documentoacessorioadministrativo', + name='arquivo', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.utils.texto_upload_path, verbose_name='Arquivo'), + ), + migrations.AlterField( + model_name='documentoadministrativo', + name='ano', + field=models.PositiveSmallIntegerField(choices=[(2027, 2027), (2026, 2026), (2025, 2025), (2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano'), + ), + migrations.AlterField( + model_name='documentoadministrativo', + name='texto_integral', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.utils.texto_upload_path, verbose_name='Texto Integral'), + ), + migrations.AlterField( + model_name='protocolo', + name='ano', + field=models.PositiveSmallIntegerField(choices=[(2027, 2027), (2026, 2026), (2025, 2025), (2024, 2024), (2023, 2023), (2022, 2022), (2021, 2021), (2020, 2020), (2019, 2019), (2018, 2018), (2017, 2017), (2016, 2016), (2015, 2015), (2014, 2014), (2013, 2013), (2012, 2012), (2011, 2011), (2010, 2010), (2009, 2009), (2008, 2008), (2007, 2007), (2006, 2006), (2005, 2005), (2004, 2004), (2003, 2003), (2002, 2002), (2001, 2001), (2000, 2000), (1999, 1999), (1998, 1998), (1997, 1997), (1996, 1996), (1995, 1995), (1994, 1994), (1993, 1993), (1992, 1992), (1991, 1991), (1990, 1990), (1989, 1989), (1988, 1988), (1987, 1987), (1986, 1986), (1985, 1985), (1984, 1984), (1983, 1983), (1982, 1982), (1981, 1981), (1980, 1980), (1979, 1979), (1978, 1978), (1977, 1977), (1976, 1976), (1975, 1975), (1974, 1974), (1973, 1973), (1972, 1972), (1971, 1971), (1970, 1970), (1969, 1969), (1968, 1968), (1967, 1967), (1966, 1966), (1965, 1965), (1964, 1964), (1963, 1963), (1962, 1962), (1961, 1961), (1960, 1960), (1959, 1959), (1958, 1958), (1957, 1957), (1956, 1956), (1955, 1955), (1954, 1954), (1953, 1953), (1952, 1952), (1951, 1951), (1950, 1950), (1949, 1949), (1948, 1948), (1947, 1947), (1946, 1946), (1945, 1945), (1944, 1944), (1943, 1943), (1942, 1942), (1941, 1941), (1940, 1940), (1939, 1939), (1938, 1938), (1937, 1937), (1936, 1936), (1935, 1935), (1934, 1934), (1933, 1933), (1932, 1932), (1931, 1931), (1930, 1930), (1929, 1929), (1928, 1928), (1927, 1927), (1926, 1926), (1925, 1925), (1924, 1924), (1923, 1923), (1922, 1922), (1921, 1921), (1920, 1920), (1919, 1919), (1918, 1918), (1917, 1917), (1916, 1916), (1915, 1915), (1914, 1914), (1913, 1913), (1912, 1912), (1911, 1911), (1910, 1910), (1909, 1909), (1908, 1908), (1907, 1907), (1906, 1906), (1905, 1905), (1904, 1904), (1903, 1903), (1902, 1902), (1901, 1901), (1900, 1900), (1899, 1899), (1898, 1898), (1897, 1897), (1896, 1896), (1895, 1895), (1894, 1894), (1893, 1893), (1892, 1892), (1891, 1891), (1890, 1890)], verbose_name='Ano do Protocolo'), + ), + ] diff --git a/sapl/protocoloadm/models.py b/sapl/protocoloadm/models.py index 3cdf32735..0831e5d6f 100644 --- a/sapl/protocoloadm/models.py +++ b/sapl/protocoloadm/models.py @@ -9,6 +9,7 @@ from model_utils import Choices from sapl.base.models import Autor, AppConfig as SaplAppConfig from sapl.materia.models import TipoMateriaLegislativa, UnidadeTramitacao,\ MateriaLegislativa +from sapl.base.fields import MetadataFileField from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, texto_upload_path, get_settings_auth_user_model, OverwriteStorage) @@ -192,7 +193,7 @@ class DocumentoAdministrativo(models.Model): verbose_name=_('Número Externo')) observacao = models.TextField( blank=True, verbose_name=_('Observação')) - texto_integral = models.FileField( + texto_integral = MetadataFileField( max_length=300, blank=True, null=True, @@ -349,7 +350,7 @@ class DocumentoAcessorioAdministrativo(models.Model): on_delete=models.PROTECT, verbose_name=_('Tipo')) nome = models.CharField(max_length=30, verbose_name=_('Nome')) - arquivo = models.FileField( + arquivo = MetadataFileField( max_length=300, blank=True, null=True, diff --git a/sapl/sessao/migrations/0070_metadata_filefield.py b/sapl/sessao/migrations/0070_metadata_filefield.py new file mode 100644 index 000000000..03bd91235 --- /dev/null +++ b/sapl/sessao/migrations/0070_metadata_filefield.py @@ -0,0 +1,88 @@ +# Generated by Django 2.2.28 on 2026-04-18 19:03 + +from django.db import migrations, models +import django.db.models.deletion +import sapl.base.fields +import sapl.sessao.models +import sapl.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0061_file_metadata'), + ('sessao', '0069_auto_20220919_1705'), + ] + + operations = [ + migrations.AddField( + model_name='justificativaausencia', + name='upload_anexo_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='orador', + name='upload_anexo_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='oradorexpediente', + name='upload_anexo_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='oradorordemdia', + name='upload_anexo_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='sessaoplenaria', + name='upload_anexo_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='sessaoplenaria', + name='upload_ata_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AddField( + model_name='sessaoplenaria', + name='upload_pauta_metadata', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='base.FileMetadata', verbose_name='File metadata'), + ), + migrations.AlterField( + model_name='justificativaausencia', + name='upload_anexo', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.anexo_upload_path, verbose_name='Anexo de Justificativa'), + ), + migrations.AlterField( + model_name='orador', + name='upload_anexo', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.anexo_upload_path, verbose_name='Anexo do Orador'), + ), + migrations.AlterField( + model_name='oradorexpediente', + name='upload_anexo', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.anexo_upload_path, verbose_name='Anexo do Orador'), + ), + migrations.AlterField( + model_name='oradorordemdia', + name='upload_anexo', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.anexo_upload_path, verbose_name='Anexo do Orador'), + ), + migrations.AlterField( + model_name='sessaoplenaria', + name='upload_anexo', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.anexo_upload_path, verbose_name='Anexo da Sessão'), + ), + migrations.AlterField( + model_name='sessaoplenaria', + name='upload_ata', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.ata_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Ata da Sessão'), + ), + migrations.AlterField( + model_name='sessaoplenaria', + name='upload_pauta', + field=sapl.base.fields.MetadataFileField(blank=True, max_length=300, null=True, storage=sapl.utils.OverwriteStorage(), upload_to=sapl.sessao.models.pauta_upload_path, validators=[sapl.utils.restringe_tipos_de_arquivo_txt], verbose_name='Pauta da Sessão'), + ), + ] diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index 613068afe..c3846d17f 100644 --- a/sapl/sessao/models.py +++ b/sapl/sessao/models.py @@ -13,6 +13,7 @@ from sapl.materia.models import Tramitacao from sapl.parlamentares.models import (CargoMesa, Legislatura, Parlamentar, Partido, SessaoLegislativa) from sapl.protocoloadm.models import DocumentoAdministrativo +from sapl.base.fields import MetadataFileField from sapl.utils import (YES_NO_CHOICES, SaplGenericRelation, get_settings_auth_user_model, restringe_tipos_de_arquivo_txt, texto_upload_path, @@ -187,7 +188,7 @@ class SessaoPlenaria(models.Model): url_video = models.URLField( max_length=150, blank=True, verbose_name=_('URL Arquivo Vídeo (Formatos MP4 / FLV / WebM)')) - upload_pauta = models.FileField( + upload_pauta = MetadataFileField( max_length=300, blank=True, null=True, @@ -195,7 +196,7 @@ class SessaoPlenaria(models.Model): verbose_name=_('Pauta da Sessão'), storage=OverwriteStorage(), validators=[restringe_tipos_de_arquivo_txt]) - upload_ata = models.FileField( + upload_ata = MetadataFileField( max_length=300, blank=True, null=True, @@ -203,7 +204,7 @@ class SessaoPlenaria(models.Model): storage=OverwriteStorage(), verbose_name=_('Ata da Sessão'), validators=[restringe_tipos_de_arquivo_txt]) - upload_anexo = models.FileField( + upload_anexo = MetadataFileField( max_length=300, blank=True, null=True, @@ -507,7 +508,7 @@ class AbstractOrador(models.Model): # Oradores max_length=150, blank=True, verbose_name=_('URL Vídeo')) observacao = models.CharField( max_length=150, blank=True, verbose_name=_('Observação')) - upload_anexo = models.FileField( + upload_anexo = MetadataFileField( max_length=300, blank=True, null=True, @@ -867,7 +868,7 @@ class JustificativaAusencia(models.Model): materias_da_ordem_do_dia = models.ManyToManyField( OrdemDia, blank=True, verbose_name=_('Matérias do Ordem do Dia')) - upload_anexo = models.FileField( + upload_anexo = MetadataFileField( max_length=300, blank=True, null=True, diff --git a/sapl/utils.py b/sapl/utils.py index ee97094aa..72fd56fde 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -660,8 +660,12 @@ def fabrica_validador_de_tipos_de_arquivo(lista, nome): except FileNotFoundError: raise ValidationError(_('Arquivo não encontrado')) - # o nome é importante para as migrations + # Django 2.2+ migration serializer uses __qualname__ (not just __name__) to + # locate the function in its module. Setting both ensures the serializer can + # resolve the function as sapl.utils. rather than hitting the + # "" branch and raising ValueError. restringe_tipos_de_arquivo.__name__ = nome + restringe_tipos_de_arquivo.__qualname__ = nome return restringe_tipos_de_arquivo