Browse Source

feat: introduce FileMetadata model and MetadataFileField (RFC §1–§6.2, §11.3)

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
      (<fieldname>_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 '<locals>')
      can resolve the function as sapl.utils.<name>.

  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 <fieldname>_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 <noreply@anthropic.com>
file-metafields
Edward Ribeiro 2 weeks ago
parent
commit
e5c5529963
  1. 9
      sapl/api/views_base.py
  2. 63
      sapl/audiencia/migrations/0021_metadata_filefield.py
  3. 9
      sapl/audiencia/models.py
  4. 120
      sapl/base/fields.py
  5. 32
      sapl/base/migrations/0061_file_metadata.py
  6. 63
      sapl/base/models.py
  7. 58
      sapl/comissoes/migrations/0031_metadata_filefield.py
  8. 9
      sapl/comissoes/models.py
  9. 68
      sapl/materia/migrations/0088_metadata_filefield.py
  10. 7
      sapl/materia/models.py
  11. 53
      sapl/norma/migrations/0046_metadata_filefield.py
  12. 5
      sapl/norma/models.py
  13. 47
      sapl/protocoloadm/migrations/0046_metadata_filefield.py
  14. 5
      sapl/protocoloadm/models.py
  15. 88
      sapl/sessao/migrations/0070_metadata_filefield.py
  16. 11
      sapl/sessao/models.py
  17. 6
      sapl/utils.py

9
sapl/api/views_base.py

@ -10,7 +10,7 @@ from rest_framework.response import Response
from drfautoapi.drfautoapi import ApiViewSetConstrutor, customize from drfautoapi.drfautoapi import ApiViewSetConstrutor, customize
from sapl.api.forms import AutoresPossiveisFilterSet from sapl.api.forms import AutoresPossiveisFilterSet
from sapl.api.serializers import ChoiceSerializer 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 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) @customize(ContentType)
class _ContentTypeSet: class _ContentTypeSet:

63
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'),
),
]

9
sapl/audiencia/models.py

@ -5,6 +5,7 @@ from model_utils import Choices
from sapl.materia.models import MateriaLegislativa from sapl.materia.models import MateriaLegislativa
from sapl.parlamentares.models import (CargoMesa, Parlamentar) from sapl.parlamentares.models import (CargoMesa, Parlamentar)
from sapl.base.fields import MetadataFileField
from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericRelation, from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericRelation,
restringe_tipos_de_arquivo_txt, texto_upload_path, restringe_tipos_de_arquivo_txt, texto_upload_path,
OverwriteStorage) OverwriteStorage)
@ -98,7 +99,7 @@ class AudienciaPublica(models.Model):
url_video = models.URLField( url_video = models.URLField(
max_length=150, blank=True, max_length=150, blank=True,
verbose_name=_('URL Arquivo Vídeo (Formatos MP4 / FLV / WebM)')) verbose_name=_('URL Arquivo Vídeo (Formatos MP4 / FLV / WebM)'))
upload_pauta = models.FileField( upload_pauta = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,
@ -106,7 +107,7 @@ class AudienciaPublica(models.Model):
storage=OverwriteStorage(), storage=OverwriteStorage(),
verbose_name=_('Pauta da Audiência Pública'), verbose_name=_('Pauta da Audiência Pública'),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
upload_ata = models.FileField( upload_ata = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,
@ -114,7 +115,7 @@ class AudienciaPublica(models.Model):
verbose_name=_('Ata da Audiência Pública'), verbose_name=_('Ata da Audiência Pública'),
storage=OverwriteStorage(), storage=OverwriteStorage(),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
upload_anexo = models.FileField( upload_anexo = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,
@ -177,7 +178,7 @@ class AudienciaPublica(models.Model):
class AnexoAudienciaPublica(models.Model): class AnexoAudienciaPublica(models.Model):
audiencia = models.ForeignKey(AudienciaPublica, audiencia = models.ForeignKey(AudienciaPublica,
on_delete=models.PROTECT) on_delete=models.PROTECT)
arquivo = models.FileField( arquivo = MetadataFileField(
max_length=300, max_length=300,
upload_to=texto_upload_path, upload_to=texto_upload_path,
storage=OverwriteStorage(), storage=OverwriteStorage(),

120
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 '<fieldname>_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/<uuid>/) 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/<uuid>.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/<uuid>/) 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

32
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',
},
),
]

63
sapl/base/models.py

@ -1,3 +1,5 @@
from uuid import uuid4
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields.jsonb import JSONField from django.contrib.postgres.fields.jsonb import JSONField
@ -482,3 +484,64 @@ class Metadata(models.Model):
def __str__(self): def __str__(self):
return f'Metadata de {self.content_object}' 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/<uuid>/ 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})'

58
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'),
),
]

9
sapl/comissoes/models.py

@ -4,6 +4,7 @@ from model_utils import Choices
from sapl.base.models import Autor from sapl.base.models import Autor
from sapl.parlamentares.models import Parlamentar from sapl.parlamentares.models import Parlamentar
from sapl.base.fields import MetadataFileField
from sapl.utils import (YES_NO_CHOICES, SaplGenericRelation, from sapl.utils import (YES_NO_CHOICES, SaplGenericRelation,
restringe_tipos_de_arquivo_txt, texto_upload_path, restringe_tipos_de_arquivo_txt, texto_upload_path,
OverwriteStorage) OverwriteStorage)
@ -234,21 +235,21 @@ class Reuniao(models.Model):
url_video = models.URLField( url_video = models.URLField(
max_length=150, blank=True, max_length=150, blank=True,
verbose_name=_('URL do Arquivo de Vídeo (Formatos MP4 / FLV / WebM)')) verbose_name=_('URL do Arquivo de Vídeo (Formatos MP4 / FLV / WebM)'))
upload_pauta = models.FileField( upload_pauta = MetadataFileField(
max_length=300, max_length=300,
blank=True, null=True, blank=True, null=True,
upload_to=pauta_upload_path, upload_to=pauta_upload_path,
verbose_name=_('Pauta da Reunião'), verbose_name=_('Pauta da Reunião'),
storage=OverwriteStorage(), storage=OverwriteStorage(),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
upload_ata = models.FileField( upload_ata = MetadataFileField(
max_length=300, max_length=300,
blank=True, null=True, blank=True, null=True,
upload_to=ata_upload_path, upload_to=ata_upload_path,
verbose_name=_('Ata da Reunião'), verbose_name=_('Ata da Reunião'),
storage=OverwriteStorage(), storage=OverwriteStorage(),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
upload_anexo = models.FileField( upload_anexo = MetadataFileField(
max_length=300, max_length=300,
blank=True, null=True, blank=True, null=True,
upload_to=anexo_upload_path, upload_to=anexo_upload_path,
@ -319,7 +320,7 @@ class DocumentoAcessorio(models.Model):
max_length=200, verbose_name=_('Autor')) max_length=200, verbose_name=_('Autor'))
ementa = models.TextField(blank=True, verbose_name=_('Ementa')) ementa = models.TextField(blank=True, verbose_name=_('Ementa'))
indexacao = models.TextField(blank=True) indexacao = models.TextField(blank=True)
arquivo = models.FileField( arquivo = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,

68
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'),
),
]

7
sapl/materia/models.py

@ -19,6 +19,7 @@ from sapl.parlamentares.models import Legislatura
from sapl.compilacao.models import (PerfilEstruturalTextoArticulado, from sapl.compilacao.models import (PerfilEstruturalTextoArticulado,
TextoArticulado) TextoArticulado)
from sapl.parlamentares.models import Parlamentar from sapl.parlamentares.models import Parlamentar
from sapl.base.fields import MetadataFileField
from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericForeignKey, from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, SaplGenericForeignKey,
SaplGenericRelation, restringe_tipos_de_arquivo_txt, SaplGenericRelation, restringe_tipos_de_arquivo_txt,
texto_upload_path, get_settings_auth_user_model, texto_upload_path, get_settings_auth_user_model,
@ -264,7 +265,7 @@ class MateriaLegislativa(models.Model):
through_fields=( through_fields=(
'materia_principal', 'materia_principal',
'materia_anexada')) 'materia_anexada'))
texto_original = models.FileField( texto_original = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,
@ -652,7 +653,7 @@ class DocumentoAcessorio(models.Model):
max_length=200, blank=True, verbose_name=_('Autor')) max_length=200, blank=True, verbose_name=_('Autor'))
ementa = models.TextField(blank=True, verbose_name=_('Ementa')) ementa = models.TextField(blank=True, verbose_name=_('Ementa'))
indexacao = models.TextField(blank=True, verbose_name=_('Indexação')) indexacao = models.TextField(blank=True, verbose_name=_('Indexação'))
arquivo = models.FileField( arquivo = MetadataFileField(
blank=True, blank=True,
null=True, null=True,
max_length=300, max_length=300,
@ -976,7 +977,7 @@ class Proposicao(models.Model):
verbose_name=_('Status Proposição') verbose_name=_('Status Proposição')
) )
texto_original = models.FileField( texto_original = MetadataFileField(
max_length=300, max_length=300,
upload_to=materia_upload_path, upload_to=materia_upload_path,
blank=True, blank=True,

53
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'),
),
]

5
sapl/norma/models.py

@ -8,6 +8,7 @@ from model_utils import Choices
from sapl.base.models import Autor from sapl.base.models import Autor
from sapl.compilacao.models import TextoArticulado from sapl.compilacao.models import TextoArticulado
from sapl.materia.models import MateriaLegislativa, Orgao from sapl.materia.models import MateriaLegislativa, Orgao
from sapl.base.fields import MetadataFileField
from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES,
restringe_tipos_de_arquivo_txt, restringe_tipos_de_arquivo_txt,
texto_upload_path, texto_upload_path,
@ -133,7 +134,7 @@ class NormaJuridica(models.Model):
('F', 'federal', _('Federal')), ('F', 'federal', _('Federal')),
) )
texto_integral = models.FileField( texto_integral = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,
@ -487,7 +488,7 @@ class AnexoNormaJuridica(models.Model):
verbose_name=_('Assunto do Anexo'), verbose_name=_('Assunto do Anexo'),
max_length=250 max_length=250
) )
anexo_arquivo = models.FileField( anexo_arquivo = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,

47
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'),
),
]

5
sapl/protocoloadm/models.py

@ -9,6 +9,7 @@ from model_utils import Choices
from sapl.base.models import Autor, AppConfig as SaplAppConfig from sapl.base.models import Autor, AppConfig as SaplAppConfig
from sapl.materia.models import TipoMateriaLegislativa, UnidadeTramitacao,\ from sapl.materia.models import TipoMateriaLegislativa, UnidadeTramitacao,\
MateriaLegislativa MateriaLegislativa
from sapl.base.fields import MetadataFileField
from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, texto_upload_path, from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES, texto_upload_path,
get_settings_auth_user_model, get_settings_auth_user_model,
OverwriteStorage) OverwriteStorage)
@ -192,7 +193,7 @@ class DocumentoAdministrativo(models.Model):
verbose_name=_('Número Externo')) verbose_name=_('Número Externo'))
observacao = models.TextField( observacao = models.TextField(
blank=True, verbose_name=_('Observação')) blank=True, verbose_name=_('Observação'))
texto_integral = models.FileField( texto_integral = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,
@ -349,7 +350,7 @@ class DocumentoAcessorioAdministrativo(models.Model):
on_delete=models.PROTECT, on_delete=models.PROTECT,
verbose_name=_('Tipo')) verbose_name=_('Tipo'))
nome = models.CharField(max_length=30, verbose_name=_('Nome')) nome = models.CharField(max_length=30, verbose_name=_('Nome'))
arquivo = models.FileField( arquivo = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,

88
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'),
),
]

11
sapl/sessao/models.py

@ -13,6 +13,7 @@ from sapl.materia.models import Tramitacao
from sapl.parlamentares.models import (CargoMesa, Legislatura, Parlamentar, from sapl.parlamentares.models import (CargoMesa, Legislatura, Parlamentar,
Partido, SessaoLegislativa) Partido, SessaoLegislativa)
from sapl.protocoloadm.models import DocumentoAdministrativo from sapl.protocoloadm.models import DocumentoAdministrativo
from sapl.base.fields import MetadataFileField
from sapl.utils import (YES_NO_CHOICES, SaplGenericRelation, from sapl.utils import (YES_NO_CHOICES, SaplGenericRelation,
get_settings_auth_user_model, get_settings_auth_user_model,
restringe_tipos_de_arquivo_txt, texto_upload_path, restringe_tipos_de_arquivo_txt, texto_upload_path,
@ -187,7 +188,7 @@ class SessaoPlenaria(models.Model):
url_video = models.URLField( url_video = models.URLField(
max_length=150, blank=True, max_length=150, blank=True,
verbose_name=_('URL Arquivo Vídeo (Formatos MP4 / FLV / WebM)')) verbose_name=_('URL Arquivo Vídeo (Formatos MP4 / FLV / WebM)'))
upload_pauta = models.FileField( upload_pauta = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,
@ -195,7 +196,7 @@ class SessaoPlenaria(models.Model):
verbose_name=_('Pauta da Sessão'), verbose_name=_('Pauta da Sessão'),
storage=OverwriteStorage(), storage=OverwriteStorage(),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
upload_ata = models.FileField( upload_ata = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,
@ -203,7 +204,7 @@ class SessaoPlenaria(models.Model):
storage=OverwriteStorage(), storage=OverwriteStorage(),
verbose_name=_('Ata da Sessão'), verbose_name=_('Ata da Sessão'),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
upload_anexo = models.FileField( upload_anexo = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,
@ -507,7 +508,7 @@ class AbstractOrador(models.Model): # Oradores
max_length=150, blank=True, verbose_name=_('URL Vídeo')) max_length=150, blank=True, verbose_name=_('URL Vídeo'))
observacao = models.CharField( observacao = models.CharField(
max_length=150, blank=True, verbose_name=_('Observação')) max_length=150, blank=True, verbose_name=_('Observação'))
upload_anexo = models.FileField( upload_anexo = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,
@ -867,7 +868,7 @@ class JustificativaAusencia(models.Model):
materias_da_ordem_do_dia = models.ManyToManyField( materias_da_ordem_do_dia = models.ManyToManyField(
OrdemDia, blank=True, verbose_name=_('Matérias do Ordem do Dia')) OrdemDia, blank=True, verbose_name=_('Matérias do Ordem do Dia'))
upload_anexo = models.FileField( upload_anexo = MetadataFileField(
max_length=300, max_length=300,
blank=True, blank=True,
null=True, null=True,

6
sapl/utils.py

@ -660,8 +660,12 @@ def fabrica_validador_de_tipos_de_arquivo(lista, nome):
except FileNotFoundError: except FileNotFoundError:
raise ValidationError(_('Arquivo não encontrado')) 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.<nome> rather than hitting the
# "<locals>" branch and raising ValueError.
restringe_tipos_de_arquivo.__name__ = nome restringe_tipos_de_arquivo.__name__ = nome
restringe_tipos_de_arquivo.__qualname__ = nome
return restringe_tipos_de_arquivo return restringe_tipos_de_arquivo

Loading…
Cancel
Save