Models
This guide explains the Django models that implement the four ZineCore2 profiles: ZineCore2, AgentCore2, HoldingCore2, and RepoCore2.
Base Models
All models inherit from shared base classes in the core app.
TimestampedModel
Provides automatic timestamps for all models:
# core/models.py
class TimestampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
Used by: All profile models (Zine, Agent, Holding, Repository)
BaseVocabulary
Provides common fields for controlled vocabularies:
# core/models.py
class BaseVocabulary(TimestampedModel):
code = models.CharField(max_length=100, unique=True)
label = models.CharField(max_length=255)
definition = models.TextField(blank=True)
uri = models.URLField()
class Meta:
abstract = True
ordering = ['code']
def __str__(self):
return f"{self.code}: {self.label}"
Used by: All vocabulary models (Subject, Genre, AgentRole, etc.)
Profile Models
Zine Model (ZineCore2)
Implements the ZineCore2 bibliographic profile.
# catalog/models.py
from django.contrib.postgres.fields import ArrayField
from core.models import TimestampedModel
class Zine(TimestampedModel):
# External identifier
zine_id = models.CharField(
max_length=255,
unique=True,
db_index=True,
help_text="External unique identifier (e.g., zine_mutate_3_1st)"
)
# Required fields
title = models.CharField(max_length=500)
creator = ArrayField(
models.CharField(max_length=255),
help_text="Agent IDs from AgentCore2"
)
subject = ArrayField(
models.CharField(max_length=100),
help_text="Subject codes from subjects vocabulary"
)
genre = ArrayField(
models.CharField(max_length=100),
help_text="Genre codes from genres vocabulary"
)
date = ArrayField(
models.CharField(max_length=100),
help_text="Publication dates (ISO 8601)"
)
language = ArrayField(
models.CharField(max_length=10),
help_text="Language codes (ISO 639-1)"
)
rights = ArrayField(
models.CharField(max_length=100),
help_text="Rights statement codes"
)
# Optional fields
series_title = ArrayField(
models.CharField(max_length=500),
blank=True,
default=list,
help_text="Series name(s)"
)
issue_designation = models.CharField(
max_length=100,
blank=True,
help_text="Issue number/label"
)
edition_statement = ArrayField(
models.CharField(max_length=255),
blank=True,
default=list,
help_text="Edition/version descriptions"
)
alternative_title = ArrayField(
models.CharField(max_length=500),
blank=True,
default=list,
help_text="Subtitles, variant titles"
)
contributor = ArrayField(
models.CharField(max_length=255),
blank=True,
default=list,
help_text="Contributor agent IDs"
)
abstract = models.TextField(
blank=True,
help_text="Summary description"
)
table_of_contents = models.TextField(
blank=True,
help_text="Text table of contents"
)
public_notes = ArrayField(
models.TextField(),
blank=True,
default=list,
help_text="Public-facing notes"
)
publisher = ArrayField(
models.CharField(max_length=255),
blank=True,
default=list,
help_text="Publisher agent IDs"
)
physical_dimensions = models.CharField(
max_length=100,
blank=True,
help_text="Height × width (e.g., 8.5 × 5.5 inches)"
)
number_of_pages = models.CharField(
max_length=50,
blank=True,
help_text="Page count"
)
format = ArrayField(
models.CharField(max_length=100),
blank=True,
default=list,
help_text="Production method (e.g., Photocopy, Risograph)"
)
binding_features = ArrayField(
models.CharField(max_length=255),
blank=True,
default=list,
help_text="Binding type & physical features"
)
place_of_publication = ArrayField(
models.CharField(max_length=255),
blank=True,
default=list,
help_text="Places where published"
)
coverage = ArrayField(
models.CharField(max_length=255),
blank=True,
default=list,
help_text="Spatial/temporal coverage of content"
)
source = ArrayField(
models.CharField(max_length=255),
blank=True,
default=list,
help_text="Acquisition/source information"
)
relation = ArrayField(
models.CharField(max_length=500),
blank=True,
default=list,
help_text="Related works"
)
identifier = ArrayField(
models.CharField(max_length=255),
blank=True,
default=list,
help_text="Local, union, URI identifiers"
)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['zine_id']),
models.Index(fields=['title']),
]
def __str__(self):
return self.title
Key Features:
- ArrayField for all repeatable elements
- Separate zine_id for external API use
- Internal id for database relationships
- All Dublin Core required fields present
Agent Model (AgentCore2)
Implements the AgentCore2 authority profile.
# agents/models.py
class Agent(TimestampedModel):
agent_id = models.CharField(
max_length=255,
unique=True,
db_index=True,
help_text="External unique identifier"
)
# Required fields
kind = models.CharField(
max_length=50,
help_text="Person, Collective, or Organization"
)
display_name = models.CharField(
max_length=500,
help_text="Public name for display"
)
public = models.BooleanField(
default=True,
help_text="Whether agent details may be shown publicly"
)
# Optional fields
legal_name = models.CharField(
max_length=500,
blank=True,
help_text="Legal or full name (may be private)"
)
alternative_names = ArrayField(
models.CharField(max_length=500),
blank=True,
default=list,
help_text="Pseudonyms, former names, variants"
)
sort_name = models.CharField(
max_length=500,
blank=True,
help_text="Name for alphabetical sorting"
)
pronouns = ArrayField(
models.CharField(max_length=50),
blank=True,
default=list,
help_text="Preferred pronouns"
)
biography = models.TextField(
blank=True,
help_text="Biographical statement"
)
scope_note = ArrayField(
models.TextField(),
blank=True,
default=list,
help_text="Usage notes or preferred citation"
)
roles = ArrayField(
models.CharField(max_length=100),
blank=True,
default=list,
help_text="Common roles (creator, editor, etc.)"
)
orcid = models.URLField(
blank=True,
help_text="ORCID identifier URL"
)
wikidata = models.CharField(
max_length=50,
blank=True,
help_text="Wikidata Q-ID"
)
other_identifiers = ArrayField(
models.CharField(max_length=255),
blank=True,
default=list,
help_text="VIAF, ISNI, local authority IDs"
)
active_dates = ArrayField(
models.CharField(max_length=100),
blank=True,
default=list,
help_text="Date ranges when agent was active"
)
location = ArrayField(
models.CharField(max_length=255),
blank=True,
default=list,
help_text="Geographic location(s)"
)
website = models.URLField(
blank=True,
help_text="Personal or organizational website"
)
email = models.EmailField(
blank=True,
help_text="Contact email (may be private)"
)
social_media = ArrayField(
models.URLField(),
blank=True,
default=list,
help_text="Social media profiles"
)
class Meta:
ordering = ['display_name']
indexes = [
models.Index(fields=['agent_id']),
models.Index(fields=['display_name']),
]
def __str__(self):
return self.display_name
Privacy Features:
- public field controls visibility
- legal_name can be kept private
- email not exposed via API by default
Holding Model (HoldingCore2)
Implements the HoldingCore2 holdings profile.
# holdings/models.py
class Holding(TimestampedModel):
holding_id = models.CharField(
max_length=255,
unique=True,
db_index=True
)
# Required foreign keys
zine = models.ForeignKey(
'catalog.Zine',
on_delete=models.CASCADE,
related_name='holdings',
help_text="Which zine this holding describes"
)
repository = models.ForeignKey(
'repositories.Repository',
on_delete=models.CASCADE,
related_name='holdings',
help_text="Which repository holds this copy"
)
# Optional fields
call_number = models.CharField(
max_length=255,
blank=True,
help_text="Local call number or shelfmark"
)
location = models.CharField(
max_length=500,
blank=True,
help_text="Shelf location or sub-collection"
)
access_status = models.CharField(
max_length=100,
blank=True,
help_text="Access/circulation status code"
)
condition = models.TextField(
blank=True,
help_text="Physical condition note"
)
copy_count = models.PositiveIntegerField(
default=1,
help_text="Number of copies this record represents"
)
barcode = models.CharField(
max_length=255,
blank=True,
help_text="ILS barcode or item identifier"
)
digital_available = models.BooleanField(
default=False,
help_text="Digital version available?"
)
digital_url = models.URLField(
blank=True,
help_text="URL to digital version"
)
distro_status = models.CharField(
max_length=100,
blank=True,
help_text="Duplication/digitization permissions code"
)
notes = ArrayField(
models.TextField(),
blank=True,
default=list,
help_text="Additional holding-specific notes"
)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['holding_id']),
models.Index(fields=['zine', 'repository']),
]
def __str__(self):
return f"{self.zine.title} @ {self.repository.repository_name}"
Relationships:
- zine ForeignKey to Zine model
- repository ForeignKey to Repository model
- Enables queries like "all holdings for this zine" or "all holdings at this repository"
Repository Model (RepoCore2)
Implements the RepoCore2 institutional profile.
# repositories/models.py
class Repository(TimestampedModel):
repo_id = models.CharField(
max_length=255,
unique=True,
db_index=True
)
# Required fields
repository_name = models.CharField(
max_length=500,
help_text="Official name of repository"
)
repository_kind = models.CharField(
max_length=100,
help_text="Type: Library, Archive, Distro, etc."
)
country = models.CharField(
max_length=2,
help_text="ISO 3166-1 alpha-2 country code"
)
# Optional fields
alternative_names = ArrayField(
models.CharField(max_length=500),
blank=True,
default=list,
help_text="Former names, abbreviations"
)
description = models.TextField(
blank=True,
help_text="Description of repository"
)
scope_note = models.TextField(
blank=True,
help_text="Collection scope and specializations"
)
marc_org_code = models.CharField(
max_length=20,
blank=True,
help_text="MARC Organization Code"
)
isil = models.CharField(
max_length=50,
blank=True,
help_text="ISO 15511 ISIL identifier"
)
ror = models.URLField(
blank=True,
help_text="Research Organization Registry ID"
)
city = models.CharField(max_length=255, blank=True)
region = models.CharField(max_length=255, blank=True)
postal_code = models.CharField(max_length=20, blank=True)
website = models.URLField(blank=True)
email = models.EmailField(blank=True)
phone = models.CharField(max_length=50, blank=True)
social_media = ArrayField(
models.URLField(),
blank=True,
default=list
)
holdings_count = models.PositiveIntegerField(
null=True,
blank=True,
help_text="Approximate number of zine holdings"
)
established = models.CharField(
max_length=10,
blank=True,
help_text="Year established"
)
status = models.CharField(
max_length=50,
default='active',
help_text="active, inactive, or defunct"
)
class Meta:
ordering = ['repository_name']
indexes = [
models.Index(fields=['repo_id']),
models.Index(fields=['repository_name']),
]
def __str__(self):
return self.repository_name
Vocabulary Models
All vocabulary models inherit from BaseVocabulary:
# catalog/models.py
class Subject(BaseVocabulary):
class Meta:
verbose_name_plural = "subjects"
class Genre(BaseVocabulary):
class Meta:
verbose_name_plural = "genres"
# agents/models.py
class AgentKind(BaseVocabulary):
pass
class AgentRole(BaseVocabulary):
pass
# And so on for all 8 vocabularies...
Shared structure from BaseVocabulary:
- code (unique identifier)
- label (display name)
- definition (term definition)
- uri (full URI for term)
- created_at, updated_at (timestamps)
Model Managers
Custom managers for common queries:
class ZineManager(models.Manager):
def by_creator(self, agent_id):
return self.filter(creator__contains=[agent_id])
def by_subject(self, subject_code):
return self.filter(subject__contains=[subject_code])
class Zine(TimestampedModel):
objects = ZineManager()
# ... fields
Usage:
# Get all zines by an agent
zines = Zine.objects.by_creator('agent_judith_arcana')
# Get all feminist zines
zines = Zine.objects.by_subject('feminism')
Validation
Models include field-level validation:
class Agent(TimestampedModel):
kind = models.CharField(
max_length=50,
choices=[
('Person', 'Person'),
('Collective', 'Collective'),
('Organization', 'Organization'),
]
)
JSON Schema validation happens at the serializer level, not in models.
Next Steps
- Serializers — How read/write serializers work
- External Identifiers — Dual identifier pattern
- Database Schema — Table structure