Architecture

Models

Django models implementing the four ZineCore2 profiles

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

Understanding models? Continue to Serializers to see how models are transformed for the API.
Copyright ©2026 ZineCore2 Contributors,