Skip to content

Commit

Permalink
Merge pull request #893 from ubyssey/peter/888-duplicate-head
Browse files Browse the repository at this point in the history
Peter/888 duplicate head
  • Loading branch information
psiemens authored Oct 31, 2018
2 parents 612b84f + 50a8548 commit 677418e
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 35 deletions.
17 changes: 15 additions & 2 deletions dispatch/api/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,28 @@
from rest_framework.exceptions import ValidationError

class JSONField(Field):

def to_internal_value(self, data):
return data

def to_representation(self, value):
return value

class PrimaryKeyField(Field):
class NullBooleanField(Field):
"""Forces a Django NullBooleanField to always return False for null values."""

def get_attribute(self, instance):
"""Overrides the default get_attribute method to convert None values to False."""

attr = super(NullBooleanField, self).get_attribute(instance)
return True if attr else False

def to_internal_value(self, data):
return True if data else None

def to_representation(self, value):
return True if value else False

class PrimaryKeyField(Field):
def __init__(self, serializer, *args, **kwargs):
super(PrimaryKeyField, self).__init__(*args, **kwargs)

Expand Down
6 changes: 5 additions & 1 deletion dispatch/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from dispatch.api.validators import (
FilenameValidator, ImageGalleryValidator, PasswordValidator,
SlugValidator, AuthorValidator, TemplateValidator)
from dispatch.api.fields import JSONField, PrimaryKeyField, ForeignKeyField
from dispatch.api.fields import JSONField, NullBooleanField, PrimaryKeyField, ForeignKeyField

class PersonSerializer(DispatchModelSerializer):
"""Serializes the Person model."""
Expand Down Expand Up @@ -608,6 +608,8 @@ class ArticleSerializer(DispatchModelSerializer, DispatchPublishableSerializer):
id = serializers.ReadOnlyField(source='parent_id')
slug = serializers.SlugField(validators=[SlugValidator()])

is_published = NullBooleanField(read_only=True)

section = SectionSerializer(read_only=True)
section_id = serializers.IntegerField(write_only=True)

Expand Down Expand Up @@ -770,6 +772,8 @@ class PageSerializer(DispatchModelSerializer, DispatchPublishableSerializer):
id = serializers.ReadOnlyField(source='parent_id')
slug = serializers.SlugField(validators=[SlugValidator()])

is_published = NullBooleanField(read_only=True)

featured_image = ImageAttachmentSerializer(required=False, allow_null=True)
featured_video = VideoAttachmentSerializer(required=False, allow_null=True)

Expand Down
93 changes: 93 additions & 0 deletions dispatch/migrations/0022_publishable_constraints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-10-30 18:59
from __future__ import unicode_literals

from django.db import migrations, models

from dispatch.models import Article, Page

def fix_latest_head(model, item):
latest = model.objects. \
filter(parent=item.parent). \
order_by('-id')[0]

model.objects. \
filter(parent=item.parent). \
update(head=None)

latest.head = True
latest.save(revision=False)

duplicates = model.objects. \
filter(slug=item.slug). \
exclude(parent=item.parent). \
delete()

def fix_latest_published(model, item):
latest_items = model.objects. \
filter(parent_id=item.parent_id, is_published=True). \
order_by('-id')

if not latest_items:
return

model.objects. \
filter(parent=item.parent). \
update(is_published=None)

latest = latest_items[0]
latest.is_published = True
latest.save(revision=False)

def fix_items(model):
seen = set()
for item in model.objects.order_by('-id'):
if item.slug in seen:
continue

fix_latest_head(model, item)
fix_latest_published(model, item)

seen.add(item.slug)

def remove_duplicates(apps, schema_editor):
fix_items(Article)
fix_items(Page)

class Migration(migrations.Migration):

dependencies = [
('dispatch', '0021_podcast_fields'),
]

operations = [
migrations.AlterField(
model_name='article',
name='head',
field=models.NullBooleanField(db_index=True, default=False),
),
migrations.AlterField(
model_name='article',
name='is_published',
field=models.NullBooleanField(db_index=True, default=False),
),
migrations.AlterField(
model_name='page',
name='head',
field=models.NullBooleanField(db_index=True, default=False),
),
migrations.AlterField(
model_name='page',
name='is_published',
field=models.NullBooleanField(db_index=True, default=False),
),
migrations.RunPython(remove_duplicates),
migrations.AlterUniqueTogether(
name='article',
unique_together=set([('parent', 'slug', 'is_published'), ('slug', 'head'), ('parent', 'slug', 'head')]),
),
migrations.AlterUniqueTogether(
name='page',
unique_together=set([('parent', 'slug', 'is_published'), ('slug', 'head'), ('parent', 'slug', 'head')]),
),
]
67 changes: 35 additions & 32 deletions dispatch/modules/content/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

from django.db.models import (
Model, DateTimeField, CharField, TextField, PositiveIntegerField,
ImageField, FileField, BooleanField, UUIDField, ForeignKey,
ManyToManyField, SlugField, SET_NULL, CASCADE, F)
ImageField, FileField, BooleanField, NullBooleanField, UUIDField,
ForeignKey, ManyToManyField, SlugField, SET_NULL, CASCADE, F)
from django.conf import settings
from django.core.validators import MaxValueValidator
from django.utils import timezone
Expand Down Expand Up @@ -70,10 +70,10 @@ class Publishable(Model):

preview_id = UUIDField(default=uuid.uuid4)
revision_id = PositiveIntegerField(default=0, db_index=True)
head = BooleanField(default=False, db_index=True)
head = NullBooleanField(default=None, db_index=True, null=True)

is_published = BooleanField(default=False, db_index=True)
is_active = BooleanField(default=True)
is_published = NullBooleanField(default=None, db_index=True)
is_active = NullBooleanField(default=True)

published_version = PositiveIntegerField(null=True)
latest_version = PositiveIntegerField(null=True)
Expand Down Expand Up @@ -143,7 +143,7 @@ def is_parent(self):

def publish(self):
# Unpublish last published version
type(self).objects.filter(parent=self.parent, is_published=True).update(is_published=False, published_at=None)
type(self).objects.filter(parent=self.parent, is_published=True).update(is_published=None, published_at=None)
self.is_published = True
if self.published_at is None:
self.published_at = timezone.now()
Expand All @@ -156,8 +156,8 @@ def publish(self):
return self

def unpublish(self):
type(self).objects.filter(parent=self.parent, is_published=True).update(is_published=False, published_at=None)
self.is_published = False
type(self).objects.filter(parent=self.parent, is_published=True).update(is_published=None, published_at=None)
self.is_published = None

# Unset published version for all articles
type(self).objects.filter(parent=self.parent).update(published_version=None)
Expand All @@ -184,19 +184,17 @@ def save(self, revision=True, *args, **kwargs):

if not self.is_parent():
# If this is a revision, delete the old head of the list.
type(self).objects.filter(parent=self.parent, head=True).update(head=False)
type(self).objects \
.filter(parent=self.parent, head=True) \
.update(head=None)

# Clear the instance id to force Django to save a new instance.
# Both fields (pk, id) required for this to work -- something to do with model inheritance
self.pk = None
self.id = None

# New version is unpublished by default
self.is_published = False

# Raise integrity error if instance with given slug already exists.
if type(self).objects.filter(slug=self.slug).exclude(parent=self.parent).exists():
raise IntegrityError("%s with slug '%s' already exists." % (type(self).__name__, self.slug))
self.is_published = None

# Set created_at to current time, but only for first version
if not self.created_at:
Expand All @@ -206,14 +204,6 @@ def save(self, revision=True, *args, **kwargs):
if revision:
self.updated_at = timezone.now()

# Check that there is only one 'head'
if self.is_conflicting_head():
raise IntegrityError("%s with head=True already exists." % (type(self).__name__,))

# Check that there is only one version with this revision_id
if self.is_conflicting_revision_id():
raise IntegrityError("%s with revision_id=%s already exists." % (self.revision_id, type(self).__name__))

super(Publishable, self).save(*args, **kwargs)

# Update the parent foreign key
Expand All @@ -223,7 +213,10 @@ def save(self, revision=True, *args, **kwargs):

if revision:
# Set latest version for all articles
type(self).objects.filter(parent=self.parent).update(latest_version=self.revision_id)
type(self).objects \
.filter(parent=self.parent) \
.update(latest_version=self.revision_id)

self.latest_version = self.revision_id

return self
Expand All @@ -232,14 +225,9 @@ def save(self, revision=True, *args, **kwargs):
def delete(self, *args, **kwargs):
if self.parent == self:
return super(Publishable, self).delete(*args, **kwargs)

return self.parent.delete()

def is_conflicting_head(self):
return self.head is True and type(self).objects.filter(parent=self.parent, head=True).exclude(id=self.id).exists()

def is_conflicting_revision_id(self):
return type(self).objects.filter(parent=self.parent, id=self.id).count() > 1

def save_featured_image(self, data):
"""
Handles saving the featured image.
Expand Down Expand Up @@ -321,7 +309,9 @@ def get_previous_revision(self):
if self.parent == self:
return self
try:
revision = type(self).objects.filter(parent=self.parent).order_by('-pk')[1]
revision = type(self).objects \
.filter(parent=self.parent) \
.order_by('-pk')[1]
return revision
except:
return self
Expand All @@ -330,7 +320,6 @@ class Meta:
abstract = True

class Article(Publishable, AuthorMixin):

parent = ForeignKey('Article', related_name='article_parent', blank=True, null=True)

headline = CharField(max_length=255)
Expand All @@ -356,6 +345,13 @@ class Article(Publishable, AuthorMixin):

reading_time = CharField(max_length=100, choices=READING_CHOICES, default='anytime')

class Meta:
unique_together = (
('slug', 'head'),
('parent', 'slug', 'head'),
('parent', 'slug', 'is_published'),
)

AuthorModel = Author

@property
Expand Down Expand Up @@ -433,14 +429,21 @@ def get_published_articles(self):
return Article.objects.filter(subsection=self, is_published=True).order_by('-published_at')

def get_absolute_url(self):
""" Returns the subsection URL. """
"""Returns the subsection URL."""
return "%s%s/" % (settings.BASE_URL, self.slug)

class Page(Publishable):
parent = ForeignKey('Page', related_name='page_parent', blank=True, null=True)
parent_page = ForeignKey('Page', related_name='parent_page_fk', null=True)
title = CharField(max_length=255)

class Meta:
unique_together = (
('slug', 'head'),
('parent', 'slug', 'head'),
('parent', 'slug', 'is_published'),
)

def get_author_string(self):
return None

Expand Down

0 comments on commit 677418e

Please sign in to comment.