Skip to content

Criar researcher.models.AffiliationMixin e article.models.ArticleAffiliation#1339

Merged
robertatakenaka merged 5 commits intomainfrom
copilot/create-affiliation-mixin
Feb 17, 2026
Merged

Criar researcher.models.AffiliationMixin e article.models.ArticleAffiliation#1339
robertatakenaka merged 5 commits intomainfrom
copilot/create-affiliation-mixin

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 17, 2026

O que esse PR faz?

Implementa AffiliationMixin e ArticleAffiliation para gerenciar afiliações com suporte dual a dados estruturados (Organization) e não estruturados (campos raw_*).

researcher.models.AffiliationMixin

  • Herda RawOrganizationMixin, adiciona FK para Organization
  • Métodos get(), create(), create_or_update() com lookup prioritário (organization → raw_text → raw_institution_name)
  • Constante RAW_ORGANIZATION_FIELDS para manutenibilidade

article.models.ArticleAffiliation

  • Herda AffiliationMixin + CommonControlField
  • ParentalKey para Article (cascade delete)
  • Sobrescreve métodos para requerir article nos lookups
  • Wagtail panels e indexes para performance
# Criação com organização estruturada
affiliation = ArticleAffiliation.create(
    user=user,
    article=article,
    organization=usp_org
)

# Ou com dados não estruturados
affiliation = ArticleAffiliation.create(
    user=user,
    article=article,
    raw_institution_name="University of Example",
    raw_country_code="USA"
)

# Update-or-create com lookup automático
affiliation = ArticleAffiliation.create_or_update(
    user=user,
    article=article,
    organization=usp_org,
    raw_text="Updated info"
)

Onde a revisão poderia começar?

  1. researcher/models.py linha 81 - AffiliationMixin (mixin abstrato base)
  2. article/models.py linha 2269 - ArticleAffiliation (implementação concreta)
  3. article/migrations/0046_articleaffiliation.py - schema migration

Como este poderia ser testado manualmente?

# Via Docker
make django_migrate
make django_shell

# No shell Django
from article.models import Article, ArticleAffiliation
from organization.models import Organization

article = Article.objects.first()
org = Organization.objects.first()

# Testar create
aff = ArticleAffiliation.create(user=request.user, article=article, organization=org)

# Testar create_or_update
aff2 = ArticleAffiliation.create_or_update(
    user=request.user, 
    article=article, 
    organization=org,
    raw_text="Updated"
)
assert aff.id == aff2.id  # Deve ser o mesmo objeto

Testes unitários: pytest researcher/tests.py::AffiliationMixinTest article/tests.py::ArticleAffiliationTest

Algum cenário de contexto que queira dar?

Substitui padrão legado onde afiliações usavam apenas Institution (deprecated) por modelo híbrido que suporta Organization estruturado + campos raw para dados não normalizados. Permite migração gradual sem perda de dados.

AffiliationMixin é abstrato e reutilizável - pode ser base para futuras classes de afiliação em outros contextos (autores, revisores, etc).

Screenshots

N/A - mudanças backend apenas

Quais são tickets relevantes?

Issue vinculada automaticamente pelo GitHub.

Referências

  • core/models.py - RawOrganizationMixin (classe pai)
  • organization/models.py - Organization (FK target)
  • institution/models.py - BaseInstitution (padrão legado sendo substituído)
Original prompt

This section details on the original issue you should resolve

<issue_title>Criar researcher.models.AffiliationMixin e article.models.ArticleAffiliation</issue_title>
<issue_description>### Descrição da tarefa

Criar researcher.models.AffiliationMixin

Subtarefas

  • Criar researcher.models.AffiliationMixin, herança de RawOrganization (core/models)
  • Adicione o campo organization (FK de Organization)
  • adicione os métodos get, create, create_or_update em AffiliationMixin
  • Crie article.models.ArticleAffiliation, herança de AffiliationMixin, CommonControlField
  • Adicione a article.models.ArticleAffiliation o campo article (ParentalKey de Article)
  • adicione os métodos get, create, create_or_update em AffiliationMixin

</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 4 commits February 17, 2026 15:59
…rticle.models

Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com>
Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com>
Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com>
Co-authored-by: robertatakenaka <505143+robertatakenaka@users.noreply.github.com>
Copilot AI changed the title [WIP] Create researcher.models.AffiliationMixin and article.models.ArticleAffiliation Criar researcher.models.AffiliationMixin e article.models.ArticleAffiliation Feb 17, 2026
@robertatakenaka robertatakenaka marked this pull request as ready for review February 17, 2026 18:42
Copilot AI review requested due to automatic review settings February 17, 2026 18:42
@robertatakenaka robertatakenaka merged commit bfa38d2 into main Feb 17, 2026
3 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a dual-mode affiliation system for articles, supporting both structured organization references and unstructured raw data fields. It introduces AffiliationMixin as an abstract base class in the researcher module and ArticleAffiliation as a concrete implementation in the article module.

Changes:

  • Created AffiliationMixin in researcher/models.py - abstract mixin combining raw organization fields with Organization FK, providing get(), create(), and create_or_update() methods
  • Created ArticleAffiliation in article/models.py - concrete model inheriting from AffiliationMixin with ParentalKey to Article, overriding methods to enforce article parameter requirement
  • Added migration 0046_articleaffiliation.py to create the ArticleAffiliation table with proper indexes

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
researcher/models.py Adds AffiliationMixin abstract base class with organization FK and methods for managing affiliations with dual structured/unstructured data support
article/models.py Implements ArticleAffiliation concrete model with article ParentalKey, overridden methods requiring article parameter, Wagtail panels, and database indexes
article/migrations/0046_articleaffiliation.py Creates ArticleAffiliation table with all inherited fields and indexes for article and organization lookups
researcher/tests.py Comprehensive test suite for AffiliationMixin functionality via ArticleAffiliation, covering create, get, create_or_update, and string representation
article/tests.py Tests specific to ArticleAffiliation including ParentalKey cascade deletion and article parameter validation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +39 to +41
options={
'abstract': False,
},
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration explicitly sets 'abstract': False in the options. This is unusual for Django migrations and is typically not necessary. Django automatically treats concrete models as non-abstract. This explicit setting might indicate confusion about model inheritance or could be a remnant from copy-pasting. Consider removing this option to follow standard Django migration patterns.

Suggested change
options={
'abstract': False,
},

Copilot uses AI. Check for mistakes.
Comment on lines +2375 to +2425
@classmethod
def create_or_update(cls, user, article, organization=None, **kwargs):
"""
Create a new article affiliation or update an existing one.

Lookup strategy (in priority order):
1. If organization is provided, lookup by article + organization
2. Otherwise, lookup by article + raw_text if provided
3. Otherwise, lookup by article + raw_institution_name if provided

Args:
user: User creating/updating the instance
article: Article instance
organization: Organization instance (optional, used for lookup)
**kwargs: Additional field values

Returns:
ArticleAffiliation instance (created or updated)
"""
if not article:
raise ValueError("ArticleAffiliation.create_or_update requires article parameter")

try:
# Build lookup parameters
lookup_params = {"article": article}
if organization:
lookup_params["organization"] = organization
elif 'raw_text' in kwargs and kwargs['raw_text']:
lookup_params["raw_text"] = kwargs['raw_text']
elif 'raw_institution_name' in kwargs and kwargs['raw_institution_name']:
lookup_params["raw_institution_name"] = kwargs['raw_institution_name']

obj = cls.get(**lookup_params)

# Update fields
if organization:
obj.organization = organization

# Update raw organization fields (using parent class constant)
for field in cls.RAW_ORGANIZATION_FIELDS:
if field in kwargs:
setattr(obj, field, kwargs[field])

if user:
obj.updated_by = user

obj.save()
return obj

except cls.DoesNotExist:
return cls.create(user=user, article=article, organization=organization, **kwargs)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The create_or_update method doesn't update other kwargs fields beyond raw organization fields, similar to the create method. Looking at the parent class implementation (researcher/models.py lines 219-222), it updates other fields from kwargs. This override should maintain that capability to avoid silently ignoring field updates.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +4
# Generated manually for ArticleAffiliation model
# Manual generation was necessary because Django and dependencies are not installed
# in the CI environment. This migration creates the ArticleAffiliation table with
# all required fields inherited from AffiliationMixin and CommonControlField.
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration is marked as "Generated manually" but includes a comment that "Django and dependencies are not installed in the CI environment". This is unusual and suggests potential issues with the development/CI setup. Manually generated migrations can lead to inconsistencies with the actual model definitions. Consider regenerating this migration in a proper Django environment to ensure it matches the model definition exactly.

Suggested change
# Generated manually for ArticleAffiliation model
# Manual generation was necessary because Django and dependencies are not installed
# in the CI environment. This migration creates the ArticleAffiliation table with
# all required fields inherited from AffiliationMixin and CommonControlField.
# Migration for the ArticleAffiliation model.
# This migration creates the ArticleAffiliation table and its associated fields
# in line with the current ArticleAffiliation model definition.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +49
index=models.Index(fields=['article'], name='article_art_article_idx'),
),
migrations.AddIndex(
model_name='articleaffiliation',
index=models.Index(fields=['organization'], name='article_art_organiz_idx'),
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Index names appear to be truncated or auto-generated (e.g., 'article_art_article_idx', 'article_art_organiz_idx'). These names are not very descriptive. Following Django conventions, consider using more meaningful names like 'article_affiliation_article_idx' or let Django auto-generate the full names. The abbreviated names could cause confusion when debugging or maintaining indexes.

Suggested change
index=models.Index(fields=['article'], name='article_art_article_idx'),
),
migrations.AddIndex(
model_name='articleaffiliation',
index=models.Index(fields=['organization'], name='article_art_organiz_idx'),
index=models.Index(fields=['article'], name='article_affiliation_article_idx'),
),
migrations.AddIndex(
model_name='articleaffiliation',
index=models.Index(fields=['organization'], name='article_affiliation_organization_idx'),

Copilot uses AI. Check for mistakes.
return obj

@classmethod
def create_or_update(cls, user, article, organization=None, **kwargs):
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user parameter in ArticleAffiliation.create_or_update is required (non-optional), but in the parent class AffiliationMixin.create_or_update it's optional (user=None). This inconsistency in the method signature could be confusing and may cause issues if someone tries to use the methods polymorphically. Consider making the user parameter consistently required or optional across both classes.

Suggested change
def create_or_update(cls, user, article, organization=None, **kwargs):
def create_or_update(cls, user=None, article=None, organization=None, **kwargs):

Copilot uses AI. Check for mistakes.
Comment on lines +2397 to +2407
try:
# Build lookup parameters
lookup_params = {"article": article}
if organization:
lookup_params["organization"] = organization
elif 'raw_text' in kwargs and kwargs['raw_text']:
lookup_params["raw_text"] = kwargs['raw_text']
elif 'raw_institution_name' in kwargs and kwargs['raw_institution_name']:
lookup_params["raw_institution_name"] = kwargs['raw_institution_name']

obj = cls.get(**lookup_params)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the lookup logic for create_or_update, when only raw_text or raw_institution_name is provided without organization, the lookup doesn't include the article parameter. This could match an affiliation from a different article. The lookup should always include article to ensure affiliations are scoped correctly to their parent article. Consider adding article to all lookup_params to prevent cross-article matches.

Copilot uses AI. Check for mistakes.
Comment on lines +165 to +168
# Set any additional fields from kwargs (excluding raw fields)
for key, value in kwargs.items():
if hasattr(obj, key) and key not in cls.RAW_ORGANIZATION_FIELDS:
setattr(obj, key, value)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The create method sets additional fields from kwargs without filtering, which could allow arbitrary field assignments. This logic first sets raw organization fields, then processes other fields but excludes raw fields. However, this could still allow setting unexpected fields like 'id' or other protected fields. Consider explicitly whitelisting allowed fields or documenting which fields can be passed via kwargs.

Copilot uses AI. Check for mistakes.
Comment on lines +2342 to +2373
@classmethod
def create(cls, user, article, organization=None, **kwargs):
"""
Create a new article affiliation.

Args:
user: User creating the instance
article: Article instance
organization: Organization instance (optional)
**kwargs: Additional field values including raw fields

Returns:
New ArticleAffiliation instance
"""
if not article:
raise ValueError("ArticleAffiliation.create requires article parameter")

obj = cls()
obj.article = article
if organization:
obj.organization = organization

# Set raw organization fields if provided (using parent class constant)
for field in cls.RAW_ORGANIZATION_FIELDS:
if field in kwargs:
setattr(obj, field, kwargs[field])

if user:
obj.creator = user

obj.save()
return obj
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The create method in ArticleAffiliation doesn't handle or update other kwargs fields beyond raw organization fields. Unlike the parent class AffiliationMixin.create which has logic to set additional fields from kwargs (lines 165-168 in researcher/models.py), this override omits that functionality. This inconsistency could lead to unexpected behavior where fields passed in kwargs are silently ignored.

Copilot uses AI. Check for mistakes.
Comment on lines +2343 to +2348
def create(cls, user, article, organization=None, **kwargs):
"""
Create a new article affiliation.

Args:
user: User creating the instance
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user parameter in ArticleAffiliation.create is required (non-optional), but in the parent class AffiliationMixin.create it's optional (user=None). This inconsistency in the method signature could be confusing and may cause issues if someone tries to use the methods polymorphically. Consider making the user parameter consistently required or optional across both classes.

Suggested change
def create(cls, user, article, organization=None, **kwargs):
"""
Create a new article affiliation.
Args:
user: User creating the instance
def create(cls, user=None, article=None, organization=None, **kwargs):
"""
Create a new article affiliation.
Args:
user: User creating the instance (optional)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Criar researcher.models.AffiliationMixin e article.models.ArticleAffiliation

3 participants