Exemple #1
0
    def handle(self, *args, **options):
        from_text = options['from_text']
        to_text = options['to_text']

        for revision in PageRevision.objects.filter(
                content_json__contains=from_text):
            revision.content_json = revision.content_json.replace(
                from_text, to_text)
            revision.save(update_fields=['content_json'])

        for page_class in get_page_models():
            self.stdout.write("scanning %s" % page_class._meta.verbose_name)

            child_relation_names = [
                rel.get_accessor_name()
                for rel in get_all_child_relations(page_class)
            ]

            # Find all pages of this exact type; exclude subclasses, as they will
            # appear in the get_page_models() list in their own right, and this
            # ensures that replacement happens only once
            for page in page_class.objects.exact_type(page_class):
                replace_in_model(page, from_text, to_text)
                for child_rel in child_relation_names:
                    for child in getattr(page, child_rel).all():
                        replace_in_model(child, from_text, to_text)
Exemple #2
0
    def __new__(cls, name, bases, attrs):
        try:
            parents = [b for b in bases if issubclass(b, ClusterForm)]
        except NameError:
            # We are defining ClusterForm itself.
            parents = None

        # grab any formfield_callback that happens to be defined in attrs -
        # so that we can pass it on to child formsets - before ModelFormMetaclass deletes it.
        # BAD METACLASS NO BISCUIT.
        formfield_callback = attrs.get('formfield_callback')

        new_class = super(ClusterFormMetaclass, cls).__new__(cls, name, bases, attrs)
        if not parents:
            return new_class

        # ModelFormMetaclass will have set up new_class._meta as a ModelFormOptions instance;
        # replace that with ClusterFormOptions so that we can access _meta.formsets
        opts = new_class._meta = ClusterFormOptions(getattr(new_class, 'Meta', None))
        if opts.model:
            formsets = {}
            for rel in get_all_child_relations(opts.model):
                # to build a childformset class from this relation, we need to specify:
                # - the base model (opts.model)
                # - the child model (rel.field.model)
                # - the fk_name from the child model to the base (rel.field.name)

                rel_name = rel.get_accessor_name()

                # apply 'formsets' and 'exclude_formsets' rules from meta
                if opts.formsets is not None and rel_name not in opts.formsets:
                    continue
                if opts.exclude_formsets and rel_name in opts.exclude_formsets:
                    continue

                try:
                    widgets = opts.widgets.get(rel_name)
                except AttributeError:  # thrown if opts.widgets is None
                    widgets = None

                kwargs = {
                    'extra': cls.extra_form_count,
                    'formfield_callback': formfield_callback,
                    'fk_name': rel.field.name,
                    'widgets': widgets
                }

                # see if opts.formsets looks like a dict; if so, allow the value
                # to override kwargs
                try:
                    kwargs.update(opts.formsets.get(rel_name))
                except AttributeError:
                    pass

                formset = childformset_factory(opts.model, rel.field.model, **kwargs)
                formsets[rel_name] = formset

            new_class.formsets = formsets

        return new_class
Exemple #3
0
    def copy(self):
        """ Copy this form and its fields. """

        exclude_fields = ['id', 'slug']
        specific_self = self.specific
        specific_dict = {}

        for field in specific_self._meta.get_fields():

            # ignore explicitly excluded fields
            if field.name in exclude_fields:
                continue  # pragma: no cover

            # ignore reverse relations
            if field.auto_created:
                continue  # pragma: no cover

            # ignore m2m relations - they will be copied as child objects
            # if modelcluster supports them at all (as it does for tags)
            if field.many_to_many:
                continue  # pragma: no cover

            # ignore parent links (baseform_ptr)
            if isinstance(field, models.OneToOneField) and field.rel.parent_link:
                continue  # pragma: no cover

            specific_dict[field.name] = getattr(specific_self, field.name)

        # new instance from prepared dict values, in case the instance class implements multiple levels inheritance
        form_copy = self.specific_class(**specific_dict)

        # a dict that maps child objects to their new ids
        # used to remap child object ids in revisions
        child_object_id_map = defaultdict(dict)

        # create the slug - temp as will be changed from the copy form
        form_copy.slug = uuid.uuid4()

        form_copy.save()

        # copy child objects
        for child_relation in get_all_child_relations(specific_self):
            accessor_name = child_relation.get_accessor_name()
            parental_key_name = child_relation.field.attname
            child_objects = getattr(specific_self, accessor_name, None)

            if child_objects:
                for child_object in child_objects.all():
                    old_pk = child_object.pk
                    child_object.pk = None
                    setattr(child_object, parental_key_name, form_copy.id)
                    child_object.save()

                    # add mapping to new primary key (so we can apply this change to revisions)
                    child_object_id_map[accessor_name][old_pk] = child_object.pk

            else:  # we should never get here as there is always a FormField child class
                pass  # pragma: no cover

        return form_copy
Exemple #4
0
def get_api_data(obj, fields):
    # Find any child relations (pages only)
    child_relations = {}
    if isinstance(obj, Page):
        child_relations = {
            child_relation.field.rel.related_name: child_relation.model
            for child_relation in get_all_child_relations(type(obj))
        }

    # Loop through fields
    for field_name in fields:
        # Check child relations
        if field_name in child_relations and hasattr(child_relations[field_name], 'api_fields'):
            yield field_name, [
                dict(get_api_data(child_object, child_relations[field_name].api_fields))
                for child_object in getattr(obj, field_name).all()
            ]
            continue

        # Check django fields
        try:
            field = obj._meta.get_field_by_name(field_name)[0]
            yield field_name, field._get_val_from_obj(obj)
            continue
        except models.fields.FieldDoesNotExist:
            pass

        # Check attributes
        if hasattr(obj, field_name):
            value = getattr(obj, field_name)
            yield field_name, force_text(value, strings_only=True)
            continue
Exemple #5
0
def get_api_data(obj, fields):
    # Find any child relations (pages only)
    child_relations = {}
    if isinstance(obj, Page):
        child_relations = {
            child_relation.field.rel.related_name: child_relation.model
            for child_relation in get_all_child_relations(type(obj))
        }

    # Loop through fields
    for field_name in fields:
        # Check child relations
        if field_name in child_relations and hasattr(
                child_relations[field_name], 'api_fields'):
            yield field_name, [
                dict(
                    get_api_data(child_object,
                                 child_relations[field_name].api_fields))
                for child_object in getattr(obj, field_name).all()
            ]
            continue

        # Check django fields
        try:
            field = obj._meta.get_field_by_name(field_name)[0]
            yield field_name, field._get_val_from_obj(obj)
            continue
        except models.fields.FieldDoesNotExist:
            pass

        # Check attributes
        if hasattr(obj, field_name):
            value = getattr(obj, field_name)
            yield field_name, force_text(value, strings_only=True)
            continue
Exemple #6
0
def get_api_data(obj, fields):
    # Find any child relations (pages only)
    child_relations = {}
    if isinstance(obj, Page):
        child_relations = {
            child_relation.field.rel.related_name:
            get_related_model(child_relation)
            for child_relation in get_all_child_relations(type(obj))
        }

    # Loop through fields
    for field_name in fields:
        # Check child relations
        if field_name in child_relations and hasattr(
                child_relations[field_name], 'api_fields'):
            yield field_name, [
                dict(
                    get_api_data(child_object,
                                 child_relations[field_name].api_fields))
                for child_object in getattr(obj, field_name).all()
            ]
            continue

        # Check django fields
        try:
            field = obj._meta.get_field(field_name)

            if field.rel and isinstance(field.rel, models.ManyToOneRel):
                # Foreign key
                val = field._get_val_from_obj(obj)

                if val:
                    yield field_name, OrderedDict([
                        ('id', field._get_val_from_obj(obj)),
                        ('meta',
                         OrderedDict([
                             ('type', field.rel.to._meta.app_label + '.' +
                              field.rel.to.__name__),
                             ('detail_url', ObjectDetailURL(field.rel.to,
                                                            val)),
                         ])),
                    ])
                else:
                    yield field_name, None
            else:
                yield field_name, field._get_val_from_obj(obj)

            continue
        except models.fields.FieldDoesNotExist:
            pass

        # Check attributes
        if hasattr(obj, field_name):
            value = getattr(obj, field_name)
            yield field_name, force_text(value, strings_only=True)
            continue
Exemple #7
0
    def build_relational_field(self, field_name, relation_info):
        # Find all relation fields that point to child class and make them use
        # the ChildRelationField class.
        if relation_info.to_many:
            model = getattr(self.Meta, 'model')
            child_relations = {
                child_relation.field.remote_field.related_name: child_relation.related_model
                for child_relation in get_all_child_relations(model)
            }

            if field_name in child_relations and field_name in self.child_serializer_classes:
                return ChildRelationField, {'serializer_class': self.child_serializer_classes[field_name]}

        return super().build_relational_field(field_name, relation_info)
Exemple #8
0
    def build_relational_field(self, field_name, relation_info):
        # Find all relation fields that point to child class and make them use
        # the ChildRelationField class.
        if relation_info.to_many:
            model = getattr(self.Meta, 'model')
            child_relations = {
                child_relation.field.rel.related_name: child_relation.related_model
                for child_relation in get_all_child_relations(model)
            }

            if field_name in child_relations and field_name in self.child_serializer_classes:
                return ChildRelationField, {'serializer_class': self.child_serializer_classes[field_name]}

        return super(PageSerializer, self).build_relational_field(field_name, relation_info)
Exemple #9
0
    def build_relational_field(self, field_name, relation_info):
        # Find all relation fields that point to child class and make them use
        # the ChildRelationField class.
        if relation_info.to_many:
            model = getattr(self.Meta, 'model')
            child_relations = {
                child_relation.field.rel.related_name: get_related_model(child_relation)
                for child_relation in get_all_child_relations(model)
            }

            if field_name in child_relations and hasattr(child_relations[field_name], 'api_fields'):
                return ChildRelationField, {'child_fields': child_relations[field_name].api_fields}

        return super(BaseSerializer, self).build_relational_field(field_name, relation_info)
Exemple #10
0
    def copy(self,
             recursive=False,
             to=None,
             update_attrs=None,
             copy_revisions=True):
        # Make a copy
        page_copy = Page.objects.get(id=self.id).specific
        page_copy.pk = None
        page_copy.id = None
        page_copy.depth = None
        page_copy.numchild = 0
        page_copy.path = None

        if update_attrs:
            for field, value in update_attrs.items():
                setattr(page_copy, field, value)

        if to:
            page_copy = to.add_child(instance=page_copy)
        else:
            page_copy = self.add_sibling(instance=page_copy)

        # Copy child objects
        specific_self = self.specific
        for child_relation in get_all_child_relations(specific_self):
            parental_key_name = child_relation.field.attname
            child_objects = getattr(specific_self,
                                    child_relation.get_accessor_name(), None)

            if child_objects:
                for child_object in child_objects.all():
                    child_object.pk = None
                    setattr(child_object, parental_key_name, page_copy.id)
                    child_object.save()

        # Copy revisions
        if copy_revisions:
            for revision in self.revisions.all():
                revision.pk = None
                revision.submitted_for_moderation = False
                revision.approved_go_live_at = None
                revision.page = page_copy
                revision.save()

        # Copy child pages
        if recursive:
            for child_page in self.get_children():
                child_page.specific.copy(recursive=True, to=page_copy)

        return page_copy
Exemple #11
0
    def build_relational_field(self, field_name, relation_info):
        # Find all relation fields that point to child class and make them use
        # the ChildRelationField class.
        if relation_info.to_many:
            model = getattr(self.Meta, 'model')
            child_relations = {
                child_relation.field.rel.related_name: get_related_model(child_relation)
                for child_relation in get_all_child_relations(model)
            }

            if field_name in child_relations and hasattr(child_relations[field_name], 'api_fields'):
                return ChildRelationField, {'child_fields': child_relations[field_name].api_fields}

        return super(BaseSerializer, self).build_relational_field(field_name, relation_info)
Exemple #12
0
def get_api_data(obj, fields):
    # Find any child relations (pages only)
    child_relations = {}
    if isinstance(obj, Page):
        child_relations = {
            child_relation.field.rel.related_name: child_relation.model
            for child_relation in get_all_child_relations(type(obj))
        }

    # Loop through fields
    for field_name in fields:
        # Check child relations
        if field_name in child_relations and hasattr(child_relations[field_name], 'api_fields'):
            yield field_name, [
                dict(get_api_data(child_object, child_relations[field_name].api_fields))
                for child_object in getattr(obj, field_name).all()
            ]
            continue

        # Check django fields
        try:
            field = obj._meta.get_field_by_name(field_name)[0]

            if field.rel and isinstance(field.rel, models.ManyToOneRel):
                # Foreign key
                val = field._get_val_from_obj(obj)

                if val:
                    yield field_name, OrderedDict([
                        ('id', field._get_val_from_obj(obj)),
                        ('meta', OrderedDict([
                             ('type', field.rel.to._meta.app_label + '.' + field.rel.to.__name__),
                             ('detail_url', ObjectDetailURL(field.rel.to, val)),
                        ])),
                    ])
                else:
                    yield field_name, None
            else:
                yield field_name, field._get_val_from_obj(obj)

            continue
        except models.fields.FieldDoesNotExist:
            pass

        # Check attributes
        if hasattr(obj, field_name):
            value = getattr(obj, field_name)
            yield field_name, force_text(value, strings_only=True)
            continue
Exemple #13
0
    def handle(self, from_text, to_text, **options):
        for revision in PageRevision.objects.filter(content_json__contains=from_text):
            revision.content_json = revision.content_json.replace(from_text, to_text)
            revision.save(update_fields=['content_json'])

        for content_type in get_page_types():
            self.stdout.write("scanning %s" % content_type.name)
            page_class = content_type.model_class()

            child_relation_names = [rel.get_accessor_name() for rel in get_all_child_relations(page_class)]

            for page in page_class.objects.all():
                replace_in_model(page, from_text, to_text)
                for child_rel in child_relation_names:
                    for child in getattr(page, child_rel).all():
                        replace_in_model(child, from_text, to_text)
Exemple #14
0
    def copy(self, recursive=False, to=None, update_attrs=None, copy_revisions=True):
        # Make a copy
        page_copy = Page.objects.get(id=self.id).specific
        page_copy.pk = None
        page_copy.id = None
        page_copy.depth = None
        page_copy.numchild = 0
        page_copy.path = None

        if update_attrs:
            for field, value in update_attrs.items():
                setattr(page_copy, field, value)

        if to:
            page_copy = to.add_child(instance=page_copy)
        else:
            page_copy = self.add_sibling(instance=page_copy)

        # Copy child objects
        specific_self = self.specific
        for child_relation in get_all_child_relations(specific_self):
            parental_key_name = child_relation.field.attname
            child_objects = getattr(specific_self, child_relation.get_accessor_name(), None)

            if child_objects:
                for child_object in child_objects.all():
                    child_object.pk = None
                    setattr(child_object, parental_key_name, page_copy.id)
                    child_object.save()

        # Copy revisions
        if copy_revisions:
            for revision in self.revisions.all():
                revision.pk = None
                revision.submitted_for_moderation = False
                revision.approved_go_live_at = None
                revision.page = page_copy
                revision.save()

        # Copy child pages
        if recursive:
            for child_page in self.get_children():
                child_page.specific.copy(recursive=True, to=page_copy)

        return page_copy
Exemple #15
0
    def handle(self, from_text, to_text, **options):
        for revision in PageRevision.objects.filter(content_json__contains=from_text):
            revision.content_json = revision.content_json.replace(from_text, to_text)
            revision.save(update_fields=['content_json'])

        for page_class in get_page_models():
            self.stdout.write("scanning %s" % page_class._meta.verbose_name)

            child_relation_names = [rel.get_accessor_name() for rel in get_all_child_relations(page_class)]

            # Find all pages of this exact type; exclude subclasses, as they will
            # appear in the get_page_models() list in their own right, and this
            # ensures that replacement happens only once
            for page in page_class.objects.exact_type(page_class):
                replace_in_model(page, from_text, to_text)
                for child_rel in child_relation_names:
                    for child in getattr(page, child_rel).all():
                        replace_in_model(child, from_text, to_text)
def serializable_data(page):
    obj = get_serializable_data_for_fields(page)

    for rel in get_all_child_relations(page):
        rel_name = rel.get_accessor_name()
        children = getattr(page, rel_name).all()

        if hasattr(rel.related_model, 'serializable_data'):
            obj[rel_name] = [child.serializable_data() for child in children]
        else:
            obj[rel_name] = [
                get_serializable_data_for_fields(child) for child in children
            ]

    for field in get_all_child_m2m_relations(page):
        children = getattr(page, field.name).all()
        obj[field.name] = [child.pk for child in children]

    return obj
def update_page_references(model, pages_by_original_id):
    for field in model._meta.get_fields():
        if isinstance(field, models.ForeignKey) and issubclass(
                field.related_model, Page):
            linked_page_id = getattr(model, field.attname)
            try:
                # see if the linked page is one of the ones we're importing
                linked_page = pages_by_original_id[linked_page_id]
            except KeyError:
                # any references to pages outside of the import should be left unchanged
                continue

            # update fk to the linked page's new ID
            setattr(model, field.attname, linked_page.id)

    # update references within inline child models, including the ParentalKey pointing back
    # to the page
    for rel in get_all_child_relations(model):
        for child in getattr(model, rel.name).all():
            # reset the child model's PK so that it will be inserted as a new record
            # rather than updating an existing one
            child.pk = None
            # update page references on the child model, including the ParentalKey
            update_page_references(child, pages_by_original_id)
from django.db.utils import IntegrityError
from django.test import TestCase

from modelcluster.models import get_all_child_relations

from tests.models import Album, Band, BandMember

# Get child relations
band_child_rels_by_model = {
    rel.related_model: rel
    for rel in get_all_child_relations(Band)
}
band_members_rel = band_child_rels_by_model[BandMember]
band_albums_rel = band_child_rels_by_model[Album]


class TestCopyChildRelations(TestCase):
    def setUp(self):
        self.beatles = Band(name='The Beatles',
                            members=[
                                BandMember(name='John Lennon'),
                                BandMember(name='Paul McCartney'),
                            ])

    def test_copy_child_relations_between_unsaved_objects(self):
        # This test clones the Beatles into a new band. We haven't saved them in either the old record
        # or the new one.

        # Clone the beatle
        beatles_clone = Band(name='The Beatles 2020 comeback')
    def _handle_task(self, objective, task):
        """
        Attempt to convert a task into a corresponding operation.May fail if we do not yet have
        the object data for this object, in which case it will be added to postponed_tasks
        """

        # It's possible that we've already found a resolution for this task in the process of
        # solving another objective; for example, "ensure page 123 exists" and "ensure page 123
        # is fully updated" might both be solved by creating page 123. If so, we re-use the
        # same operation that we built previously; this ensures that when we establish an order
        # for the operations to happen in, we'll recognise the duplicate and won't run it twice.
        try:
            operation = self.task_resolutions[task]
            self.resolutions[objective] = operation
            return
        except KeyError:
            pass

        model, source_id, action = task
        try:
            object_data = self.object_data_by_source[(model, source_id)]
        except KeyError:
            # need to postpone this until we have the object data
            self.postponed_tasks.add((objective, task))
            self.missing_object_data.add((model, source_id))
            return

        # retrieve the specific model for this object
        specific_model = self._model_for_path(object_data['model'])

        if issubclass(specific_model, Page):
            if action == 'create':
                if source_id == self.root_page_source_pk:
                    # this is the root page of the import; ignore the parent ID in the source
                    # record and import at the requested destination instead
                    operation = CreatePage(specific_model, object_data,
                                           self.destination_parent_id)
                else:
                    operation = CreatePage(specific_model, object_data)
            else:  # action == 'update'
                destination_id = self.destination_ids_by_source[(model,
                                                                 source_id)]
                obj = specific_model.objects.get(pk=destination_id)
                operation = UpdatePage(obj, object_data)
        else:
            # non-page model
            if action == 'create':
                operation = CreateModel(specific_model, object_data)
            else:  # action == 'update'
                destination_id = self.destination_ids_by_source[(model,
                                                                 source_id)]
                obj = specific_model.objects.get(pk=destination_id)
                operation = UpdateModel(obj, object_data)

        if issubclass(specific_model, ClusterableModel):
            # Process child object relations for this item
            # and add objectives to ensure that they're all updated to their newest versions
            for rel in get_all_child_relations(specific_model):
                related_base_model = get_base_model(rel.related_model)
                child_uids = set()

                for child_obj_data in object_data['fields'][rel.name]:
                    # Add child object data to the object_data_by_source lookup
                    self._add_object_data_to_lookup(child_obj_data)

                    # Add an objective for handling the child object. Regardless of whether
                    # this is a 'create' or 'update' task, we want the child objects to be at
                    # their most up-to-date versions, so set the objective type to 'updated'
                    self._add_objective(
                        (related_base_model, child_obj_data['pk'], 'updated'))

                    # look up the child object's UID
                    uid = self.uids_by_source[(related_base_model,
                                               child_obj_data['pk'])]
                    child_uids.add(uid)

                if action == 'update':
                    # delete any child objects on the existing object if they can't be mapped back
                    # to one of the uids in the new set
                    matched_destination_ids = IDMapping.objects.filter(
                        uid__in=child_uids,
                        content_type=ContentType.objects.get_for_model(
                            related_base_model)).values_list('local_id',
                                                             flat=True)
                    for child in getattr(obj, rel.name).all():
                        if str(child.pk) not in matched_destination_ids:
                            self.operations.add(DeleteModel(child))

        self.operations.add(operation)
        self.resolutions[objective] = operation
        self.task_resolutions[task] = operation

        for objective in operation.dependencies:
            self._add_objective(objective)
    def _handle_task(self, task):
        """
        Attempt to convert a task into a corresponding operation.May fail if we do not yet have
        the object data for this object, in which case it will be added to postponed_tasks
        """

        # It's possible that over the course of planning the import, we will encounter multiple
        # tasks relating to the same object. For example, a page may be part of the selected
        # subtree to be imported, and, separately, be referenced from another page - both of
        # these will trigger an 'update' or 'create' task for that page (according to whether
        # it already exists or not).

        # Given that the only defined task types are 'update' and 'create', and the choice between
        # these depends ONLY on whether the object previously existed at the destination or not,
        # we can be confident that all of the tasks we encounter for a given object will be the
        # same type.

        # Therefore, if we find an existing entry for this task in task_resolutions, we know that
        # we've already handled this task and updated the ImportPlanner state accordingly
        # (including `task_resolutions`, `resolutions` and `operations`), and should quit now
        # rather than create duplicate database operations.
        if task in self.task_resolutions:
            return

        action, model, source_id = task
        try:
            object_data = self.object_data_by_source[(model, source_id)]
        except KeyError:
            # Cannot complete this task during this pass; request the missing object data,
            # unless we've already tried that
            if (model, source_id) in self.really_missing_object_data:
                # object data apparently doesn't exist on the source site either, so give up on
                # this object entirely
                if action == 'create':
                    self.failed_creations.add((model, source_id))

            else:
                # need to postpone this until we have the object data
                self.postponed_tasks.add(task)
                self.missing_object_data.add((model, source_id))

            return

        # retrieve the specific model for this object
        specific_model = get_model_for_path(object_data['model'])

        if issubclass(specific_model, MP_Node):
            if object_data['parent_id'] is None:
                # This is the root node; populate destination_ids_by_source so that we use the
                # existing root node for any references to it, rather than creating a new one
                destination_id = specific_model.get_first_root_node().pk
                self.context.destination_ids_by_source[(
                    model, source_id)] = destination_id

                # No operation to be performed for this task
                operation = None
            elif action == 'create':
                if issubclass(specific_model,
                              Page) and source_id == self.root_page_source_pk:
                    # this is the root page of the import; ignore the parent ID in the source
                    # record and import at the requested destination instead
                    operation = CreateTreeModel(specific_model, object_data,
                                                self.destination_parent_id)
                else:
                    operation = CreateTreeModel(specific_model, object_data)
            else:  # action == 'update'
                destination_id = self.context.destination_ids_by_source[(
                    model, source_id)]
                obj = specific_model.objects.get(pk=destination_id)
                operation = UpdateModel(obj, object_data)
        else:
            # non-tree model
            if action == 'create':
                operation = CreateModel(specific_model, object_data)
            else:  # action == 'update'
                destination_id = self.context.destination_ids_by_source[(
                    model, source_id)]
                obj = specific_model.objects.get(pk=destination_id)
                operation = UpdateModel(obj, object_data)

        if issubclass(specific_model, ClusterableModel):
            # Process child object relations for this item
            # and add objectives to ensure that they're all updated to their newest versions
            for rel in get_all_child_relations(specific_model):
                related_base_model = get_base_model(rel.related_model)
                child_uids = set()

                for child_obj_pk in object_data['fields'][rel.name]:

                    # Add an objective for handling the child object. Regardless of whether
                    # this is a 'create' or 'update' task, we want the child objects to be at
                    # their most up-to-date versions, so set the objective to 'must update'

                    self._add_objective(
                        Objective(related_base_model,
                                  child_obj_pk,
                                  self.context,
                                  must_update=True))

        if operation is not None:
            self.operations.add(operation)

        if action == 'create':
            # For 'create' actions, record this operation in `resolutions`, so that any operations
            # that identify this object as a dependency know that this operation has to happen
            # first.

            # (Alternatively, the operation can be None, and that's fine too: it means that we've
            # been able to populate destination_ids_by_source with no further action, and so the
            # dependent operation has nothing to wait for.)

            # For 'update' actions, this doesn't matter, since we can happily fill in the
            # destination ID wherever it's being referenced, regardless of whether that object has
            # completed its update or not; in this case, we would have already set the resolution
            # to None during _handle_objective.
            self.resolutions[(model, source_id)] = operation

        self.task_resolutions[task] = operation

        if operation is not None:
            for model, source_id, is_hard_dep in operation.dependencies:
                self._add_objective(
                    Objective(model,
                              source_id,
                              self.context,
                              must_update=(model._meta.label_lower
                                           in UPDATE_RELATED_MODELS)))

            for instance in operation.deletions(self.context):
                self.operations.add(DeleteModel(instance))
Exemple #21
0
def get_translatable_fields(model):
    if hasattr(model, 'translatable_fields'):
        return model.translatable_fields

    translatable_fields = []

    for field in model._meta.get_fields():
        # Ignore automatically generated IDs
        if isinstance(field, models.AutoField):
            continue

        # Ignore non-editable fields
        if not field.editable:
            continue

        # Ignore many to many fields (not supported yet)
        # TODO: Add support for these
        if isinstance(field, models.ManyToManyField):
            continue

        # Ignore fields defined by MP_Node mixin
        if issubclass(model,
                      MP_Node) and field.name in ['path', 'depth', 'numchild']:
            continue

        # Ignore some editable fields defined on Page
        if issubclass(model, Page) and field.name in [
                'go_live_at', 'expire_at', 'first_published_at',
                'content_type', 'owner'
        ]:
            continue

        # URL, Email and choices fields are an exception to the rule below.
        # Text fields are translatable, but these are synchronised.
        if isinstance(field,
                      (models.URLField, models.EmailField)) or isinstance(
                          field, models.CharField) and field.choices:
            translatable_fields.append(SynchronizedField(field.name))

        # Translatable text fields should be translatable
        elif isinstance(
                field,
            (StreamField, RichTextField, models.TextField, models.CharField)):
            translatable_fields.append(TranslatableField(field.name))

        # Foreign keys to translatable models should be translated. Others should be synchronised
        elif isinstance(field, models.ForeignKey):
            # Ignore if this is a link to a parent model
            if isinstance(field, ParentalKey):
                continue

            # Ignore parent links
            if isinstance(
                    field,
                    models.OneToOneField) and field.remote_field.parent_link:
                continue

            # All FKs to translatable models should be translatable.
            # With the exception of pages that are special because we can localize them at runtime easily.
            # TODO: Perhaps we need a special type for pages where it links to the translation if availabe,
            # but falls back to the source if it isn't translated yet?
            if issubclass(field.related_model,
                          TranslatableMixin) and not issubclass(
                              field.related_model, Page):
                translatable_fields.append(TranslatableField(field.name))
            else:
                translatable_fields.append(SynchronizedField(field.name))

        # Fields that support extracting segments are translatable
        elif hasattr(field, "get_translatable_segments"):
            translatable_fields.append(TranslatableField(field.name))

        else:
            # Everything else is synchronised
            translatable_fields.append(SynchronizedField(field.name))

    # Add child relations for clusterable models
    if issubclass(model, ClusterableModel):
        for child_relation in get_all_child_relations(model):
            if issubclass(child_relation.related_model, TranslatableMixin):
                translatable_fields.append(
                    TranslatableField(child_relation.name))
            else:
                translatable_fields.append(
                    SynchronizedField(child_relation.name))

    return translatable_fields
Exemple #22
0
    def _copy_page(self,
                   page,
                   to=None,
                   update_attrs=None,
                   exclude_fields=None,
                   _mpnode_attrs=None):
        specific_page = page.specific
        exclude_fields = (specific_page.default_exclude_fields_in_copy +
                          specific_page.exclude_fields_in_copy +
                          (exclude_fields or []))
        if self.keep_live:
            base_update_attrs = {
                "alias_of": None,
            }
        else:
            base_update_attrs = {
                "live": False,
                "has_unpublished_changes": True,
                "live_revision": None,
                "first_published_at": None,
                "last_published_at": None,
                "alias_of": None,
            }

        if self.user:
            base_update_attrs["owner"] = self.user

        # When we're not copying for translation, we should give the translation_key a new value
        if self.reset_translation_key:
            base_update_attrs["translation_key"] = uuid.uuid4()

        if update_attrs:
            base_update_attrs.update(update_attrs)

        page_copy, child_object_map = _copy(specific_page,
                                            exclude_fields=exclude_fields,
                                            update_attrs=base_update_attrs)
        # Save copied child objects and run process_child_object on them if we need to
        for (child_relation, old_pk), child_object in child_object_map.items():

            if self.process_child_object:
                self.process_child_object(specific_page, page_copy,
                                          child_relation, child_object)

            if self.reset_translation_key and isinstance(
                    child_object, TranslatableMixin):
                child_object.translation_key = self.generate_translation_key(
                    child_object.translation_key)

        # Save the new page
        if _mpnode_attrs:
            # We've got a tree position already reserved. Perform a quick save
            page_copy.path = _mpnode_attrs[0]
            page_copy.depth = _mpnode_attrs[1]
            page_copy.save(clean=False)

        else:
            if to:
                page_copy = to.add_child(instance=page_copy)
            else:
                page_copy = page.add_sibling(instance=page_copy)

            _mpnode_attrs = (page_copy.path, page_copy.depth)

        _copy_m2m_relations(
            specific_page,
            page_copy,
            exclude_fields=exclude_fields,
            update_attrs=base_update_attrs,
        )

        # Copy revisions
        if self.copy_revisions:
            for revision in page.revisions.all():
                revision.pk = None
                revision.submitted_for_moderation = False
                revision.approved_go_live_at = None
                revision.page = page_copy

                # Update ID fields in content
                revision_content = revision.content
                revision_content["pk"] = page_copy.pk

                for child_relation in get_all_child_relations(specific_page):
                    accessor_name = child_relation.get_accessor_name()
                    try:
                        child_objects = revision_content[accessor_name]
                    except KeyError:
                        # KeyErrors are possible if the revision was created
                        # before this child relation was added to the database
                        continue

                    for child_object in child_objects:
                        child_object[child_relation.field.name] = page_copy.pk
                        # Remap primary key to copied versions
                        # If the primary key is not recognised (eg, the child object has been deleted from the database)
                        # set the primary key to None
                        copied_child_object = child_object_map.get(
                            (child_relation, child_object["pk"]))
                        child_object["pk"] = (copied_child_object.pk
                                              if copied_child_object else None)
                        if (self.reset_translation_key
                                and "translation_key" in child_object):
                            child_object[
                                "translation_key"] = self.generate_translation_key(
                                    child_object["translation_key"])

                revision.content = revision_content

                # Save
                revision.save()

        # Create a new revision
        # This code serves a few purposes:
        # * It makes sure update_attrs gets applied to the latest revision
        # * It bumps the last_revision_created_at value so the new page gets ordered as if it was just created
        # * It sets the user of the new revision so it's possible to see who copied the page by looking at its history
        latest_revision = page_copy.get_latest_revision_as_page()

        if update_attrs:
            for field, value in update_attrs.items():
                setattr(latest_revision, field, value)

        latest_revision_as_page_revision = latest_revision.save_revision(
            user=self.user, changed=False, clean=False)
        if self.keep_live:
            page_copy.live_revision = latest_revision_as_page_revision
            page_copy.last_published_at = latest_revision_as_page_revision.created_at
            page_copy.first_published_at = latest_revision_as_page_revision.created_at
            page_copy.save(clean=False)

        if page_copy.live:
            page_published.send(
                sender=page_copy.specific_class,
                instance=page_copy,
                revision=latest_revision_as_page_revision,
            )

        # Log
        if self.log_action:
            parent = specific_page.get_parent()
            log(
                instance=page_copy,
                action=self.log_action,
                user=self.user,
                data={
                    "page": {
                        "id": page_copy.id,
                        "title": page_copy.get_admin_display_title(),
                        "locale": {
                            "id": page_copy.locale_id,
                            "language_code": page_copy.locale.language_code,
                        },
                    },
                    "source": {
                        "id":
                        parent.id,
                        "title":
                        parent.specific_deferred.get_admin_display_title(),
                    } if parent else None,
                    "destination": {
                        "id": to.id,
                        "title":
                        to.specific_deferred.get_admin_display_title(),
                    } if to else None,
                    "keep_live": page_copy.live and self.keep_live,
                    "source_locale": {
                        "id": page.locale_id,
                        "language_code": page.locale.language_code,
                    },
                },
            )
            if page_copy.live and self.keep_live:
                # Log the publish if the use chose to keep the copied page live
                log(
                    instance=page_copy,
                    action="wagtail.publish",
                    user=self.user,
                    revision=latest_revision_as_page_revision,
                )
        logger.info('Page copied: "%s" id=%d from=%d', page_copy.title,
                    page_copy.id, page.id)

        # Copy child pages
        from wagtail.models import Page

        if self.recursive:
            numchild = 0

            for child_page in page.get_children().specific():
                newdepth = _mpnode_attrs[1] + 1
                child_mpnode_attrs = (
                    Page._get_path(_mpnode_attrs[0], newdepth, numchild),
                    newdepth,
                )
                numchild += 1
                self._copy_page(child_page,
                                to=page_copy,
                                _mpnode_attrs=child_mpnode_attrs)

            if numchild > 0:
                page_copy.numchild = numchild
                page_copy.save(clean=False, update_fields=["numchild"])

        return page_copy
 def test_get_all_child_relations(self):
     self.assertEqual(
         set([rel.name for rel in get_all_child_relations(Restaurant)]),
         set(['tagged_items', 'reviews', 'menu_items'])
     )
Exemple #24
0
 def test_get_all_child_relations(self):
     self.assertEqual(
         set([rel.name for rel in get_all_child_relations(Restaurant)]),
         set(['tagged_items', 'reviews', 'menu_items']))
Exemple #25
0
def get_translatable_fields(model):
    """
    Derives a list of translatable fields from the given model class.

    Arguments:
        model (Model class): The model class to derive translatable fields from.

    Returns:
        list[TranslatableField or SynchronizedField]: A list of TranslatableField and SynchronizedFields that were
            derived from the model.

    """
    if hasattr(model, 'translatable_fields'):
        return model.translatable_fields

    translatable_fields = []

    for field in model._meta.get_fields():
        # Ignore automatically generated IDs
        if isinstance(field, models.AutoField):
            continue

        # Ignore non-editable fields
        if not field.editable:
            continue

        # Ignore many to many fields (not supported yet)
        # TODO: Add support for these
        if isinstance(field, models.ManyToManyField):
            continue

        # Ignore fields defined by MP_Node mixin
        if issubclass(model, MP_Node) and field.name in ['path', 'depth', 'numchild']:
            continue

        # Ignore some editable fields defined on Page
        if issubclass(model, Page) and field.name in ['go_live_at', 'expire_at', 'first_published_at', 'content_type', 'owner']:
            continue

        # URL, Email and choices fields are an exception to the rule below.
        # Text fields are translatable, but these are synchronised.
        if isinstance(field, (models.URLField, models.EmailField)) or isinstance(field, models.CharField) and field.choices:
            translatable_fields.append(SynchronizedField(field.name))

        # Translatable text fields should be translatable
        elif isinstance(field, (StreamField, RichTextField, models.TextField, models.CharField)):
            translatable_fields.append(TranslatableField(field.name))

        # Foreign keys to translatable models should be translated. Others should be synchronised
        elif isinstance(field, models.ForeignKey):
            # Ignore if this is a link to a parent model
            if isinstance(field, ParentalKey):
                continue

            # Ignore parent links
            if isinstance(field, models.OneToOneField) and field.remote_field.parent_link:
                continue

            # All FKs to translatable models should be translatable.
            # With the exception of pages that are special because we can localize them at runtime easily.
            # TODO: Perhaps we need a special type for pages where it links to the translation if availabe,
            # but falls back to the source if it isn't translated yet?
            # Note: This exact same decision was made for page chooser blocks in segments/extract.py
            if issubclass(field.related_model, TranslatableMixin) and not issubclass(field.related_model, Page):
                translatable_fields.append(TranslatableField(field.name))
            else:
                translatable_fields.append(SynchronizedField(field.name))

        # Fields that support extracting segments are translatable
        elif hasattr(field, "get_translatable_segments"):
            translatable_fields.append(TranslatableField(field.name))

        else:
            # Everything else is synchronised
            translatable_fields.append(SynchronizedField(field.name))

    # Add child relations for clusterable models
    if issubclass(model, ClusterableModel):
        for child_relation in get_all_child_relations(model):
            # Ignore comments
            if issubclass(model, Page) and child_relation.name == 'comments':
                continue

            if issubclass(child_relation.related_model, TranslatableMixin):
                translatable_fields.append(TranslatableField(child_relation.name))
            else:
                translatable_fields.append(SynchronizedField(child_relation.name))

    # Combine with any overrides defined on the model
    override_translatable_fields = getattr(model, 'override_translatable_fields', [])

    if override_translatable_fields:
        override_translatable_fields = {
            field.field_name: field
            for field in override_translatable_fields
        }

        combined_translatable_fields = []
        for field in translatable_fields:
            if field.field_name in override_translatable_fields:
                combined_translatable_fields.append(override_translatable_fields.pop(field.field_name))
            else:
                combined_translatable_fields.append(field)

        if override_translatable_fields:
            combined_translatable_fields.extend(override_translatable_fields.values())

        return combined_translatable_fields

    else:
        return translatable_fields