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)
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
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
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
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
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
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)
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)
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)
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
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)
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
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)
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
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))
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
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']) )
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']))
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