class Page(MPTTModel, TranslatableModel): available_from = models.DateTimeField(null=True, blank=True, verbose_name=_('available from')) available_to = models.DateTimeField(null=True, blank=True, verbose_name=_('available to')) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, related_name="+", on_delete=models.SET_NULL, verbose_name=_('created by') ) modified_by = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, related_name="+", on_delete=models.SET_NULL, verbose_name=_('modified by') ) created_on = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_('created on')) modified_on = models.DateTimeField(auto_now=True, editable=False, verbose_name=_('modified on')) identifier = InternalIdentifierField( unique=True, help_text=_('This identifier can be used in templates to create URLs'), editable=True ) visible_in_menu = models.BooleanField(verbose_name=_("visible in menu"), default=False) parent = TreeForeignKey( "self", blank=True, null=True, related_name="children", verbose_name=_("parent")) list_children_on_page = models.BooleanField(verbose_name=_("list children on page"), default=False) translations = TranslatedFields( title=models.CharField(max_length=256, verbose_name=_('title')), url=models.CharField( max_length=100, verbose_name=_('URL'), unique=True, default=None, blank=True, null=True ), content=models.TextField(verbose_name=_('content')), ) objects = TreeManager.from_queryset(PageQuerySet)() class Meta: ordering = ('-id',) verbose_name = _('page') verbose_name_plural = _('pages') def is_visible(self, dt=None): if not dt: dt = now() return ( (self.available_from and self.available_from <= dt) and (self.available_to is None or self.available_to >= dt) ) def get_html(self): return markdown.markdown(self.content) def __str__(self): return force_text(self.safe_translation_getter("title", any_language=True, default=_("Untitled")))
class BaseTermManager(RebuildTreeMixin, TreeManager.from_queryset(BaseTermQuerySet)): """ ENG: Customized model manager for our Term model. RUS: Адаптированная модель менеджера для модели Терминов. """ '''
def as_manager(cls): # Address the circular dependency between `Queryset` and `Manager`. from mptt.managers import TreeManager manager = TreeManager.from_queryset(cls)() manager._built_with_as_manager = True return manager
class Comment(MPTTModel): author = models.ForeignKey(User, verbose_name=_('Author'), related_name='comments', on_delete=models.CASCADE) content_type_id = models.IntegerField(_('Content type')) object_id = models.IntegerField(_('Object id')) parent = TreeForeignKey( 'self', verbose_name=_('Parent comment'), null=True, blank=True, related_name='children', db_index=True, on_delete=models.CASCADE, ) created_at = models.DateTimeField(_('Date of creation'), auto_now_add=True, db_index=True) modified_at = models.DateTimeField(_('Date of modification'), auto_now=True) content = models.TextField(_('Content'), max_length=settings.COMMENT_CONTENT_MAX_LENGTH) is_deleted = models.BooleanField(_('Is deleted'), default=False, db_index=True) is_edited = models.BooleanField(_('Is edited'), default=False) objects = TreeManager.from_queryset(CommentQuerySet)() class Meta: verbose_name = _('Comment') verbose_name_plural = _('Comments') index_together = [ ['content_type_id', 'object_id', 'is_deleted'], ['content_type_id', 'object_id'], ['tree_id', 'lft'], ] class MPTTMeta: order_insertion_by = ['created_at'] def __str__(self): return f'{self.pk}'
class CustomTreeManager(TreeManager.from_queryset(CustomTreeQuerySet)): pass
class Page(MPTTModel, TranslatableModel): shop = models.ForeignKey("shuup.Shop", verbose_name=_('shop')) available_from = models.DateTimeField( null=True, blank=True, verbose_name=_('available from'), help_text= _("Set an available from date to restrict the page to be available only after a certain date and time. " "This is useful for pages describing sales campaigns or other time-sensitive pages." )) available_to = models.DateTimeField( null=True, blank=True, verbose_name=_('available to'), help_text= _("Set an available to date to restrict the page to be available only after a certain date and time. " "This is useful for pages describing sales campaigns or other time-sensitive pages." )) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name="+", on_delete=models.SET_NULL, verbose_name=_('created by')) modified_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name="+", on_delete=models.SET_NULL, verbose_name=_('modified by')) created_on = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_('created on')) modified_on = models.DateTimeField(auto_now=True, editable=False, verbose_name=_('modified on')) identifier = InternalIdentifierField( unique=False, help_text=_('This identifier can be used in templates to create URLs'), editable=True) visible_in_menu = models.BooleanField( verbose_name=_("visible in menu"), default=False, help_text= _("Check this if this page should have a link in the top menu of the store front." )) parent = TreeForeignKey( "self", blank=True, null=True, related_name="children", verbose_name=_("parent"), help_text= _("Set this to a parent page if this page should be subcategorized under another page." )) list_children_on_page = models.BooleanField( verbose_name=_("list children on page"), default=False, help_text=_("Check this if this page should list its children pages.")) page_type = EnumIntegerField(PageType, default=PageType.NORMAL, db_index=True, verbose_name=_("page type")) deleted = models.BooleanField(default=False, verbose_name=_("deleted")) translations = TranslatedFields( title=models.CharField( max_length=256, verbose_name=_('title'), help_text= _("The page title. This is shown anywhere links to your page are shown." )), url=models.CharField( max_length=100, verbose_name=_('URL'), default=None, blank=True, null=True, help_text= _("The page url. Choose a descriptive url so that search engines can rank your page higher. " "Often the best url is simply the page title with spaces replaced with dashes." )), content=models.TextField( verbose_name=_('content'), help_text= _("The page content. This is the text that is displayed when customers click on your page link." )), ) objects = TreeManager.from_queryset(PageQuerySet)() class Meta: ordering = ('-id', ) verbose_name = _('page') verbose_name_plural = _('pages') unique_together = ("shop", "identifier") def delete(self, using=None): raise NotImplementedError("Not implemented: Use `soft_delete()`") def soft_delete(self, user=None): if not self.deleted: self.deleted = True self.add_log_entry("Deleted.", kind=LogEntryKind.DELETION, user=user) # Bypassing local `save()` on purpose. super(Page, self).save(update_fields=("deleted", )) def make_gdpr_old_version(self): self.identifier = slugify("{}-{}".format(self.identifier, self.pk)) self.save(update_fields=["identifier"]) page_translation_model = self._meta.model._parler_meta.root_model for page_translation in page_translation_model.objects.filter( master_id=self.pk): page_translation.url = "{}-{}".format(page_translation.url, self.pk) page_translation.save(update_fields=["url"]) def clean(self): url = getattr(self, "url", None) if url: page_translation = self._meta.model._parler_meta.root_model shop_pages = Page.objects.for_shop(self.shop).values_list( "id", flat=True) url_checker = page_translation.objects.filter( url=url, master_id__in=shop_pages) if self.pk: url_checker = url_checker.exclude(master_id=self.pk) if url_checker.exists(): raise ValidationError(_("URL already exists."), code="invalid_url") if self.pk: original_page = Page.objects.get(id=self.pk) if original_page.page_type == PageType.GDPR_CONSENT_DOCUMENT: # prevent changing content when page type is GDPR_CONSENT_DOCUMENT content = getattr(self, "content", None) if original_page.content != content or original_page.page_type != self.page_type: msg = _( "This page is protected against changes because it is a GDPR consent document." ) raise ValidationError(msg, code="gdpr-protected") def is_visible(self, dt=None): if not dt: dt = now() return ((self.available_from and self.available_from <= dt) and (self.available_to is None or self.available_to >= dt)) def get_html(self): return self.content def __str__(self): return force_text( self.safe_translation_getter("title", any_language=True, default=_("Untitled")))
for menu in MenuItem.objects.all(): cache.delete('menu-%s' % menu.slug) cache.delete('menu-tree-%s' % menu.slug) class MenuUnCacheQuerySet(TreeQuerySet): def delete(self, *args, **kwargs): delete_cache() super(MenuUnCacheQuerySet, self).delete(*args, **kwargs) def update(self, *args, **kwargs): delete_cache() super(MenuUnCacheQuerySet, self).update(*args, **kwargs) MenuItemManager = TreeManager.from_queryset(MenuUnCacheQuerySet) class MenuItem(MPTTModel): parent = TreeForeignKey('self', null=True, blank=True, related_name='children') label = models.CharField( _('label'), max_length=255, help_text="The display name on the web site.", ) slug = models.SlugField( _('slug'),
class Page(MPTTModel, TranslatableModel): shop = models.ForeignKey("shuup.Shop", verbose_name=_('shop')) supplier = models.ForeignKey("shuup.Supplier", null=True, blank=True, verbose_name=_('supplier')) available_from = models.DateTimeField( default=now, null=True, blank=True, db_index=True, verbose_name=_('available from'), help_text=_( "Set an available from date to restrict the page to be available only after a certain date and time. " "This is useful for pages describing sales campaigns or other time-sensitive pages." ) ) available_to = models.DateTimeField( null=True, blank=True, db_index=True, verbose_name=_('available to'), help_text=_( "Set an available to date to restrict the page to be available only after a certain date and time. " "This is useful for pages describing sales campaigns or other time-sensitive pages." ) ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, related_name="+", on_delete=models.SET_NULL, verbose_name=_('created by') ) modified_by = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, related_name="+", on_delete=models.SET_NULL, verbose_name=_('modified by') ) created_on = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_('created on')) modified_on = models.DateTimeField(auto_now=True, editable=False, verbose_name=_('modified on')) identifier = InternalIdentifierField( unique=False, help_text=_('This identifier can be used in templates to create URLs'), editable=True ) visible_in_menu = models.BooleanField(verbose_name=_("visible in menu"), default=False, help_text=_( "Check this if this page should have a link in the top menu of the store front." )) parent = TreeForeignKey( "self", blank=True, null=True, related_name="children", verbose_name=_("parent"), help_text=_( "Set this to a parent page if this page should be subcategorized under another page." )) list_children_on_page = models.BooleanField(verbose_name=_("list children on page"), default=False, help_text=_( "Check this if this page should list its children pages." )) show_child_timestamps = models.BooleanField(verbose_name=_("show child page timestamps"), default=True, help_text=_( "Check this if you want to show timestamps on the child pages. Please note, that this " "requires the children to be listed on the page as well." )) deleted = models.BooleanField(default=False, verbose_name=_("deleted")) translations = TranslatedFields( title=models.CharField(max_length=256, verbose_name=_('title'), help_text=_( "The page title. This is shown anywhere links to your page are shown." )), url=models.CharField( max_length=100, verbose_name=_('URL'), default=None, blank=True, null=True, help_text=_( "The page url. Choose a descriptive url so that search engines can rank your page higher. " "Often the best url is simply the page title with spaces replaced with dashes." ) ), content=models.TextField(verbose_name=_('content'), help_text=_( "The page content. This is the text that is displayed when customers click on your page link." "You can leave this empty and add all page content through placeholder editor in shop front." "To edit the style of the page you can use the Snippet plugin which is in shop front editor." )) ) template_name = models.TextField( max_length=500, verbose_name=_("Template path"), default=settings.SHUUP_SIMPLE_CMS_DEFAULT_TEMPLATE ) render_title = models.BooleanField(verbose_name=_("render title"), default=True, help_text=_( "Check this if this page should have a visible title" )) objects = TreeManager.from_queryset(PageQuerySet)() class Meta: ordering = ('-id',) verbose_name = _('page') verbose_name_plural = _('pages') unique_together = ("shop", "identifier") def delete(self, using=None): raise NotImplementedError("Not implemented: Use `soft_delete()`") def soft_delete(self, user=None): if not self.deleted: self.deleted = True self.add_log_entry("Deleted.", kind=LogEntryKind.DELETION, user=user) # Bypassing local `save()` on purpose. super(Page, self).save(update_fields=("deleted",)) def clean(self): url = getattr(self, "url", None) if url: page_translation = self._meta.model._parler_meta.root_model shop_pages = Page.objects.for_shop(self.shop).values_list("id", flat=True) url_checker = page_translation.objects.filter(url=url, master_id__in=shop_pages) if self.pk: url_checker = url_checker.exclude(master_id=self.pk) if url_checker.exists(): raise ValidationError(_("URL already exists."), code="invalid_url") def is_visible(self, dt=None): if not dt: dt = now() return ( (self.available_from and self.available_from <= dt) and (self.available_to is None or self.available_to >= dt) ) def save(self, *args, **kwargs): with reversion.create_revision(): super(Page, self).save(*args, **kwargs) def get_html(self): return self.content @classmethod def create_initial_revision(cls, page): from reversion.models import Version if not Version.objects.get_for_object(page).exists(): with reversion.create_revision(): page.save() def __str__(self): return force_text(self.safe_translation_getter("title", any_language=True, default=_("Untitled")))
for menu in MenuItem.objects.all(): cache.delete('menu-%s' % menu.slug) cache.delete('menu-tree-%s' % menu.slug) class MenuUnCacheQuerySet(TreeQuerySet): def delete(self, *args, **kwargs): delete_cache() super(MenuUnCacheQuerySet, self).delete(*args, **kwargs) def update(self, *args, **kwargs): delete_cache() super(MenuUnCacheQuerySet, self).update(*args, **kwargs) MenuItemManager = TreeManager.from_queryset(MenuUnCacheQuerySet) class MenuItem(MPTTModel): parent = TreeForeignKey('self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) label = models.CharField( _('label'), max_length=255, help_text="The display name on the web site.", ) slug = models.SlugField( _('slug'), unique=True, max_length=255,
class Page(MPTTModel, TranslatableModel): shop = models.ForeignKey("wshop.Shop", verbose_name=_('shop')) available_from = models.DateTimeField(null=True, blank=True, verbose_name=_('available from'), help_text=_( "Set an available from date to restrict the page to be available only after a certain date and time. " "This is useful for pages describing sales campaigns or other time-sensitive pages." )) available_to = models.DateTimeField(null=True, blank=True, verbose_name=_('available to'), help_text=_( "Set an available to date to restrict the page to be available only after a certain date and time. " "This is useful for pages describing sales campaigns or other time-sensitive pages." )) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, related_name="+", on_delete=models.SET_NULL, verbose_name=_('created by') ) modified_by = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, related_name="+", on_delete=models.SET_NULL, verbose_name=_('modified by') ) created_on = models.DateTimeField(auto_now_add=True, editable=False, verbose_name=_('created on')) modified_on = models.DateTimeField(auto_now=True, editable=False, verbose_name=_('modified on')) identifier = InternalIdentifierField( unique=False, help_text=_('This identifier can be used in templates to create URLs'), editable=True ) visible_in_menu = models.BooleanField(verbose_name=_("visible in menu"), default=False, help_text=_( "Check this if this page should have a link in the top menu of the store front." )) parent = TreeForeignKey( "self", blank=True, null=True, related_name="children", verbose_name=_("parent"), help_text=_( "Set this to a parent page if this page should be subcategorized under another page." )) list_children_on_page = models.BooleanField(verbose_name=_("list children on page"), default=False, help_text=_( "Check this if this page should list its children pages." )) translations = TranslatedFields( title=models.CharField(max_length=256, verbose_name=_('title'), help_text=_( "The page title. This is shown anywhere links to your page are shown." )), url=models.CharField( max_length=100, verbose_name=_('URL'), default=None, blank=True, null=True, help_text=_( "The page url. Choose a descriptive url so that search engines can rank your page higher. " "Often the best url is simply the page title with spaces replaced with dashes." ) ), content=models.TextField(verbose_name=_('content'), help_text=_( "The page content. This is the text that is displayed when customers click on your page link." )), ) objects = TreeManager.from_queryset(PageQuerySet)() class Meta: ordering = ('-id',) verbose_name = _('page') verbose_name_plural = _('pages') unique_together = ("shop", "identifier") def clean(self): url = getattr(self, "url", None) if url: page_translation = self._meta.model._parler_meta.root_model shop_pages = Page.objects.for_shop(self.shop).values_list("id", flat=True) url_checker = page_translation.objects.filter(url=url, master_id__in=shop_pages) if self.pk: url_checker = url_checker.exclude(master_id=self.pk) if url_checker.exists(): raise ValidationError(_("URL already exists."), code="invalid_url") def is_visible(self, dt=None): if not dt: dt = now() return ( (self.available_from and self.available_from <= dt) and (self.available_to is None or self.available_to >= dt) ) def get_html(self): return self.content def __str__(self): return force_text(self.safe_translation_getter("title", any_language=True, default=_("Untitled")))
class CustomContentNodeTreeManager( TreeManager.from_queryset(CustomTreeQuerySet)): # Added 7-31-2018. We can remove this once we are certain we have eliminated all cases # where root nodes are getting prepended rather than appended to the tree list. def _create_tree_space(self, target_tree_id, num_trees=1): """ Creates space for a new tree by incrementing all tree ids greater than ``target_tree_id``. """ if target_tree_id == -1: raise Exception( "ERROR: Calling _create_tree_space with -1! Something is attempting to sort all MPTT trees root nodes!" ) return super(CustomContentNodeTreeManager, self)._create_tree_space(target_tree_id, num_trees) def _get_next_tree_id(self, *args, **kwargs): from contentcuration.models import MPTTTreeIDManager new_id = MPTTTreeIDManager.objects.create().id return new_id @contextlib.contextmanager def _attempt_lock(self, tree_ids, shared_tree_ids=None): """ Internal method to allow the lock_mptt method to do retries in case of deadlocks """ shared_tree_ids = shared_tree_ids or [] start = time.time() with transaction.atomic(): # Issue a separate lock on each tree_id # in a predictable order. # This will mean that every process acquires locks in the same order # and should help to minimize deadlocks for tree_id in tree_ids: advisory_lock(TREE_LOCK, key2=tree_id, shared=tree_id in shared_tree_ids) yield log_lock_time_spent(time.time() - start) @contextlib.contextmanager def lock_mptt(self, *tree_ids, **kwargs): tree_ids = sorted((t for t in set(tree_ids) if t is not None)) shared_tree_ids = kwargs.pop('shared_tree_ids', []) # If this is not inside the context of a delay context manager # or updates are not disabled set a lock on the tree_ids. if (not self.model._mptt_is_tracking and self.model._mptt_updates_enabled and tree_ids): try: with self._attempt_lock(tree_ids, shared_tree_ids=shared_tree_ids): yield except OperationalError as e: if "deadlock detected" in e.args[0]: logging.error( "Deadlock detected while trying to lock ContentNode trees for mptt operations, retrying" ) with self._attempt_lock(tree_ids, shared_tree_ids=shared_tree_ids): yield else: raise else: # Otherwise just let it carry on! yield def partial_rebuild(self, tree_id): with self.lock_mptt(tree_id): return super(CustomContentNodeTreeManager, self).partial_rebuild(tree_id) def _move_child_to_new_tree(self, node, target, position): from contentcuration.models import PrerequisiteContentRelationship super(CustomContentNodeTreeManager, self)._move_child_to_new_tree(node, target, position) PrerequisiteContentRelationship.objects.filter( Q(prerequisite_id=node.id) | Q(target_node_id=node.id)).delete() def _mptt_refresh(self, *nodes): """ This is based off the MPTT model method mptt_refresh except that handles an arbitrary list of nodes to get the updated values in a single DB query. """ ids = [node.id for node in nodes if node.id] # Don't bother doing a query if no nodes # were passed in if not ids: return opts = self.model._mptt_meta # Look up all the mptt field values # and the id so we can marry them up to the # passed in nodes. values_lookup = { # Create a lookup dict to cross reference # with the passed in nodes. c["id"]: c for c in self.filter(id__in=ids).values( "id", opts.left_attr, opts.right_attr, opts.level_attr, opts.tree_id_attr, ) } for node in nodes: # Set the values on each of the nodes if node.id and node.id in values_lookup: values = values_lookup[node.id] for k, v in values.items(): setattr(node, k, v) def move_node(self, node, target, position="last-child"): """ Vendored from mptt - by default mptt moves then saves This is updated to call the save with the skip_lock kwarg to prevent a second atomic transaction and tree locking context being opened. Moves ``node`` relative to a given ``target`` node as specified by ``position`` (when appropriate), by examining both nodes and calling the appropriate method to perform the move. A ``target`` of ``None`` indicates that ``node`` should be turned into a root node. Valid values for ``position`` are ``'first-child'``, ``'last-child'``, ``'left'`` or ``'right'``. ``node`` will be modified to reflect its new tree state in the database. This method explicitly checks for ``node`` being made a sibling of a root node, as this is a special case due to our use of tree ids to order root nodes. NOTE: This is a low-level method; it does NOT respect ``MPTTMeta.order_insertion_by``. In most cases you should just move the node yourself by setting node.parent. """ old_parent = node.parent with self.lock_mptt(node.tree_id, target.tree_id): # Call _mptt_refresh to ensure that the mptt fields on # these nodes are up to date once we have acquired a lock # on the associated trees. This means that the mptt data # will remain fresh until the lock is released at the end # of the context manager. self._mptt_refresh(node, target) # N.B. this only calls save if we are running inside a # delay MPTT updates context self._move_node(node, target, position=position) node.save(skip_lock=True) node_moved.send( sender=node.__class__, instance=node, target=target, position=position, ) # when moving to a new tree, like trash, we'll blanket reset the modified for the # new root and the old root nodes if old_parent.tree_id != target.tree_id: for size_cache in [ ResourceSizeCache(target.get_root()), ResourceSizeCache(old_parent.get_root()) ]: size_cache.reset_modified(None) def get_source_attributes(self, source): """ These attributes will be copied when the node is copied and also when a copy is synced with its source """ return { "content_id": source.content_id, "kind_id": source.kind_id, "title": source.title, "description": source.description, "language_id": source.language_id, "license_id": source.license_id, "license_description": source.license_description, "thumbnail_encoding": source.thumbnail_encoding, "extra_fields": source.extra_fields, "copyright_holder": source.copyright_holder, "author": source.author, "provider": source.provider, "role_visibility": source.role_visibility, } def _clone_node(self, source, parent_id, source_channel_id, can_edit_source_channel, pk, mods): copy = { "id": pk or uuid.uuid4().hex, "node_id": uuid.uuid4().hex, "aggregator": source.aggregator, "cloned_source": source, "source_channel_id": source_channel_id, "source_node_id": source.node_id, "original_channel_id": source.original_channel_id, "original_source_node_id": source.original_source_node_id, "freeze_authoring_data": not can_edit_source_channel or source.freeze_authoring_data, "changed": True, "published": False, "parent_id": parent_id, "complete": source.complete, } copy.update(self.get_source_attributes(source)) if isinstance(mods, dict): copy.update(mods) # There might be some legacy nodes that don't have these, so ensure they are added if (copy["original_channel_id"] is None or copy["original_source_node_id"] is None): original_node = source.get_original_node() if copy["original_channel_id"] is None: original_channel = original_node.get_channel() copy["original_channel_id"] = (original_channel.id if original_channel else None) if copy["original_source_node_id"] is None: copy["original_source_node_id"] = original_node.node_id return copy def _recurse_to_create_tree( self, source, parent_id, source_channel_id, nodes_by_parent, source_copy_id_map, can_edit_source_channel, pk, mods, ): copy = self._clone_node( source, parent_id, source_channel_id, can_edit_source_channel, pk, mods, ) if source.kind_id == content_kinds.TOPIC and source.id in nodes_by_parent: children = sorted(nodes_by_parent[source.id], key=lambda x: x.lft) copy["children"] = list( map( lambda x: self._recurse_to_create_tree( x, copy["id"], source_channel_id, nodes_by_parent, source_copy_id_map, can_edit_source_channel, None, None, ), children, )) source_copy_id_map[source.id] = copy["id"] return copy def _all_nodes_to_copy(self, node, excluded_descendants): nodes_to_copy = node.get_descendants(include_self=True) if excluded_descendants: excluded_descendants = self.filter( node_id__in=excluded_descendants.keys()).get_descendants( include_self=True) nodes_to_copy = nodes_to_copy.difference(excluded_descendants) return nodes_to_copy def copy_node(self, node, target=None, position="last-child", pk=None, mods=None, excluded_descendants=None, can_edit_source_channel=None, batch_size=None, progress_tracker=None): """ :type progress_tracker: contentcuration.utils.celery.ProgressTracker|None """ if batch_size is None: batch_size = BATCH_SIZE source_channel_id = node.get_channel_id() total_nodes = self._all_nodes_to_copy(node, excluded_descendants).count() if progress_tracker: progress_tracker.set_total(total_nodes) return self._copy( node, target, position, source_channel_id, pk, mods, excluded_descendants, can_edit_source_channel, batch_size, progress_tracker=progress_tracker, ) def _copy( self, node, target, position, source_channel_id, pk, mods, excluded_descendants, can_edit_source_channel, batch_size, progress_tracker=None, ): """ :type progress_tracker: contentcuration.utils.celery.ProgressTracker|None """ if node.rght - node.lft < batch_size: copied_nodes = self._deep_copy( node, target, position, source_channel_id, pk, mods, excluded_descendants, can_edit_source_channel, ) if progress_tracker: progress_tracker.increment(len(copied_nodes)) return copied_nodes else: node_copy = self._shallow_copy( node, target, position, source_channel_id, pk, mods, can_edit_source_channel, ) if progress_tracker: progress_tracker.increment() children = node.get_children().order_by("lft") if excluded_descendants: children = children.exclude( node_id__in=excluded_descendants.keys()) for child in children: self._copy( child, node_copy, "last-child", source_channel_id, None, None, excluded_descendants, can_edit_source_channel, batch_size, progress_tracker=progress_tracker, ) return [node_copy] def _copy_tags(self, source_copy_id_map): from contentcuration.models import ContentTag node_tags_mappings = list( self.model.tags.through.objects.filter( contentnode_id__in=source_copy_id_map.keys())) tags_to_copy = ContentTag.objects.filter( tagged_content__in=source_copy_id_map.keys(), channel__isnull=False) # Get a lookup of all existing null channel tags so we don't duplicate existing_tags_lookup = { t["tag_name"]: t["id"] for t in ContentTag.objects.filter( tag_name__in=tags_to_copy.values_list("tag_name", flat=True), channel__isnull=True, ).values("tag_name", "id") } tags_to_copy = list(tags_to_copy) tags_to_create = [] tag_id_map = {} for tag in tags_to_copy: if tag.tag_name in existing_tags_lookup: tag_id_map[tag.id] = existing_tags_lookup.get(tag.tag_name) else: new_tag = ContentTag(tag_name=tag.tag_name) tag_id_map[tag.id] = new_tag.id tags_to_create.append(new_tag) ContentTag.objects.bulk_create(tags_to_create) mappings_to_create = [ self.model.tags.through( contenttag_id=tag_id_map.get(mapping.contenttag_id, mapping.contenttag_id), contentnode_id=source_copy_id_map.get(mapping.contentnode_id), ) for mapping in node_tags_mappings ] self.model.tags.through.objects.bulk_create(mappings_to_create) def _copy_assessment_items(self, source_copy_id_map): from contentcuration.models import File from contentcuration.models import AssessmentItem node_assessmentitems = list( AssessmentItem.objects.filter( contentnode_id__in=source_copy_id_map.keys())) node_assessmentitem_files = list( File.objects.filter(assessment_item__in=node_assessmentitems)) assessmentitem_old_id_lookup = {} for assessmentitem in node_assessmentitems: old_id = assessmentitem.id assessmentitem.id = None assessmentitem.contentnode_id = source_copy_id_map[ assessmentitem.contentnode_id] assessmentitem_old_id_lookup[assessmentitem.contentnode_id + ":" + assessmentitem.assessment_id] = old_id node_assessmentitems = AssessmentItem.objects.bulk_create( node_assessmentitems) assessmentitem_new_id_lookup = {} for assessmentitem in node_assessmentitems: old_id = assessmentitem_old_id_lookup[assessmentitem.contentnode_id + ":" + assessmentitem.assessment_id] assessmentitem_new_id_lookup[old_id] = assessmentitem.id for file in node_assessmentitem_files: file.id = None file.assessment_item_id = assessmentitem_new_id_lookup[ file.assessment_item_id] File.objects.bulk_create(node_assessmentitem_files) def _copy_files(self, source_copy_id_map): from contentcuration.models import File node_files = list( File.objects.filter(contentnode_id__in=source_copy_id_map.keys())) for file in node_files: file.id = None file.contentnode_id = source_copy_id_map[file.contentnode_id] File.objects.bulk_create(node_files) def _copy_associated_objects(self, source_copy_id_map): self._copy_files(source_copy_id_map) self._copy_assessment_items(source_copy_id_map) self._copy_tags(source_copy_id_map) def _shallow_copy( self, node, target, position, source_channel_id, pk, mods, can_edit_source_channel, ): data = self._clone_node( node, None, source_channel_id, can_edit_source_channel, pk, mods, ) with self.lock_mptt(target.tree_id if target else None): node_copy = self.model(**data) if target: self._mptt_refresh(target) self.insert_node(node_copy, target, position=position, save=False) node_copy.save(force_insert=True) self._copy_associated_objects({node.id: node_copy.id}) return node_copy def _deep_copy( self, node, target, position, source_channel_id, pk, mods, excluded_descendants, can_edit_source_channel, ): # lock mptt source tree with shared advisory lock with self.lock_mptt(node.tree_id, shared_tree_ids=[node.tree_id]): nodes_to_copy = list( self._all_nodes_to_copy(node, excluded_descendants)) nodes_by_parent = {} for copy_node in nodes_to_copy: if copy_node.parent_id not in nodes_by_parent: nodes_by_parent[copy_node.parent_id] = [] nodes_by_parent[copy_node.parent_id].append(copy_node) source_copy_id_map = {} parent_id = None # If the position is *-child then parent is target # but if it is not - then our parent is the same as the target's parent if target: if position in ["last-child", "first-child"]: parent_id = target.id else: parent_id = target.parent_id data = self._recurse_to_create_tree( node, parent_id, source_channel_id, nodes_by_parent, source_copy_id_map, can_edit_source_channel, pk, mods, ) with self.lock_mptt(target.tree_id if target else None): if target: self._mptt_refresh(target) nodes_to_create = self.build_tree_nodes(data, target=target, position=position) new_nodes = self.bulk_create(nodes_to_create) if target: self.filter(pk=target.pk).update(changed=True) self._copy_associated_objects(source_copy_id_map) return new_nodes def build_tree_nodes(self, data, target=None, position="last-child"): """ vendored from: https://github.com/django-mptt/django-mptt/blob/fe2b9cc8cfd8f4b764d294747dba2758147712eb/mptt/managers.py#L614 """ opts = self.model._mptt_meta if target: tree_id = target.tree_id if position in ("left", "right"): level = getattr(target, opts.level_attr) if position == "left": cursor = getattr(target, opts.left_attr) else: cursor = getattr(target, opts.right_attr) + 1 else: level = getattr(target, opts.level_attr) + 1 if position == "first-child": cursor = getattr(target, opts.left_attr) + 1 else: cursor = getattr(target, opts.right_attr) else: tree_id = self._get_next_tree_id() cursor = 1 level = 0 stack = [] def treeify(data, cursor=1, level=0): data = dict(data) children = data.pop("children", []) node = self.model(**data) stack.append(node) setattr(node, opts.tree_id_attr, tree_id) setattr(node, opts.level_attr, level) setattr(node, opts.left_attr, cursor) for child in children: cursor = treeify(child, cursor=cursor + 1, level=level + 1) cursor += 1 setattr(node, opts.right_attr, cursor) return cursor treeify(data, cursor=cursor, level=level) if target: self._create_space(2 * len(stack), cursor - 1, tree_id) return stack
# # Copyright (C) 2016-2020 TU Muenchen and contributors of ANEXIA Internetdienstleistungs GmbH # SPDX-License-Identifier: AGPL-3.0-or-later # from eric.core.models import BaseManager from eric.projects.models.querysets import ProjectQuerySet, ProjectRoleUserAssignmentQuerySet, RoleQuerySet, \ ResourceQuerySet, UserStorageLimitQuerySet, ElementLockQuerySet from mptt.managers import TreeManager # create managers for all our important objects ProjectManager = TreeManager.from_queryset(ProjectQuerySet) ProjectRoleUserAssignmentManager = BaseManager.from_queryset( ProjectRoleUserAssignmentQuerySet) ResourceManager = BaseManager.from_queryset(ResourceQuerySet) RoleManager = BaseManager.from_queryset(RoleQuerySet) UserStorageLimitManager = BaseManager.from_queryset(UserStorageLimitQuerySet) ElementLockManager = BaseManager.from_queryset(ElementLockQuerySet)
def user_access_filter(self, user): # personal folders filter = Q(owned_by=user) # folders owned by orgs in which the user a member orgs = user.get_orgs() if orgs: filter |= Q(organization__in=orgs) return filter def accessible_to(self, user): return self.filter(self.user_access_filter(user)) FolderManager = TreeManager.from_queryset(FolderQuerySet) class Folder(MPTTModel): name = models.CharField(max_length=255, null=False, blank=False) parent = TreeForeignKey('self', null=True, blank=True, related_name='children') creation_timestamp = models.DateTimeField(auto_now_add=True) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, related_name='folders_created',) # this may be null if this is the shared folder for a org owned_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, related_name='folders',) # this will be set if this is inside a shared folder organization = models.ForeignKey(Organization, null=True, blank=True, related_name='folders') # true if this is the apex shared folder (not subfolder) for a org
class ModeratedTreeManager(TreeManager.from_queryset(ModeratedTreeQuerySet)): pass