根据另一个 QuerySet 中的匹配字段过滤 QuerySet,但仅限于另一个 QuerySet 中的关联字段

发布于 2025-01-13 06:03:21 字数 10595 浏览 2 评论 0原文

我正在尝试根据两个相关模型的两个查询集(之前都已过滤)创建一个过滤器。第一个返回 ID 和概念(名称)以及关联值(定义)的列表。

dqs:ID + 概念 + 定义 + 关联语言(四个中的任何一个)

例如:

<QuerySet [<Definition: Definition (en) for #39: CBD, central business district: definition text>, <Definition: Definition (en) for #184: migration: definition text...>

第二个也有 ID 和概念,它们在第一个列表中可能有也可能没有匹配的值。

tqs:ID1 + 概念1 + 术语(语言1); ID1 + 概念 1 + 术语(语言 2)...ID2 + 概念 1 + 术语(语言 1)等。

例如:

<QuerySet [<Translation: “other language term entry” (fr) for #1: abiotic component>, <Translation: “abiotic component” (en) for #1: abiotic component>, <Translation: “yet another language term entry” (es) for #1: abiotic component>...>

ID 和概念(用英语(源语言)表示)不是特定于语言的。术语和定义是特定于语言的。因此,每个概念+ID 都与四种不同语言的术语条目和定义相关联。

我需要过滤 tqs,以便它仅显示 dqs 中具有匹配 ID 的条目,并且仅显示与定义搜索查询匹配的语言(否则它将显示与匹配 ID 关联的所有四个条目)。

下面有更多信息。

def 的以下部分描述了在术语表中进行与定义中的字符串匹配的搜索(与特定条目相关)时会发生什么情况。

if data['definitions_show_partial_matches']:
    if case_filter:
        dqs = dqs.filter(text__contains=search_string)
    else:
        dqs = dqs.filter(text__icontains=search_string)

data['definitions_show_partial_matches'] 是一个 BooleanField 形式 - 用户在复选框中指示搜索是否还包括定义文本。 case_filter 指示搜索是否应区分大小写。

如果字符串出现在与多种语言关联的多个定义中,则过滤后 dqs 中可能会出现不止一种语言。

我有一个用于匹配 ID 的临时解决方案:

ids = []
for d in dqs:
    ids.append(d.concept.id)
tqs = tqs.filter(concept__in=ids)

但是,这可能不是很有效。我不知道如何直接在另一个查询集中应用concept__in,而不先将ID 提取到列表中。我想过枚举 for 循环,但这显然也不是一个好主意。

所有这一切之后的代码可以有效地创建一个具有截断定义和最大结果数量的结果列表。

dqs 仅显示匹配的定义。因此,我需要对 tqs 应用过滤器,以便与匹配 ID 关联的条目不会以所有四种语言显示。

我还需要建立一个检查,以便如果匹配以英语进行,我需要使用相关词汇表模型中的“source_language”字段,而不是“other_languages”,反之亦然。为了展示这些以及概念字段如何与术语表相关以及我如何解决相关问题,这里是模板中的一些代码(“current_language”是视图中传递的变量):

{% with used_languages=concept.glossary.other_languages.all|dictsort:"iso_code" %}
  {% if used_languages %}
    <div class="language_link_list">
        {% if current_language.name != "English" %}
            <a href="{% url "concept_detail_for_language" pk=concept.pk lang=concept.glossary.source_language.pk %}" class="language_link">{{ concept.glossary.source_language.name }}</a>
        {% endif %}
...
        {% for lang in used_languages %}
            {% if lang.pk != current_language.pk %}
                <a href="{% url "concept_detail_for_language" pk=concept.pk lang=lang.pk %}" class="language_link">{{ lang.name }}</a>
            {% endif %}
        {% endfor %}
    </div>
    <br>
  {% endif %}
{% endwith %}

但这允许检查值一个已知的值并逐个元素应用它,而不是单个过滤器命令,我的问题是该值可以根据另一个查询集中条目的位置和/或该查询集中相关字段的值而改变查询集。

我觉得它不应该很复杂 - 我想到了以下内容:

  • 使用“更新” - 但不只有一个值可以与
  • 使用“注释”进行比较 - 但定义已经有一种关联的语言,所以它不应该有必要吗?
  • 以某种方式使用 Q() 或 prefetch_lated 表达式?

我已经阅读了很多示例,但不知怎的,它们都与我想做的事情不太相关。

谢谢!

编辑:以下是相关模型的相关位。它改编自 GNU GPL 下的一个在线项目。我已经删除了一些评论和名称,但如果公开的话应该不成问题。

class Translation(models.Model, ConceptLangUrlMixin):
    concept = models.ForeignKey(Concept, on_delete=models.CASCADE, verbose_name=_("concept"))
    language = models.ForeignKey(Language, on_delete=models.PROTECT, verbose_name=_("language"))
    translation_text = models.CharField(max_length=100, verbose_name=_("translation text"))
    administrative_status = models.ForeignKey(AdministrativeStatus, null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("administrative status"))
    administrative_status_reason = models.ForeignKey(AdministrativeStatusReason, null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("administrative status reason"))
    note = models.TextField(blank=True)

    class Meta:
        verbose_name = _("translation")
        verbose_name_plural = _("translations")
        ordering = ['concept', 'language']

    def __str__(self):
        trans_data = {
            'term': self.translation_text,
            'iso_code': self.language_id,
            'concept': self.concept
        }
        return _("“%(term)s” (%(iso_code)s) for %(concept)s") % trans_data

    def save(self, *args, **kwargs):
        update_repr_cache = kwargs.pop("update_repr_cache", True)
        super(Translation, self).save(*args, **kwargs)
        if update_repr_cache:
            self.concept.update_repr_cache()

    def cmp_key(self):
        # used to sort terms according to their perceived worth
        keys = {
                "preferredTerm-admn-sts": 0,
                "": 1,
                None: 1,
                "admittedTerm-admn-sts": 2,
                "supersededTerm-admn-sts": 3,
                "deprecatedTerm-admn-sts": 4,
        }
        # We return a tuple with the "quality" key, and the text to allow
        # alphabetic sorting for all adminitted terms, for example.
        return (keys.get(self.administrative_status_id, 1), self.translation_text.lower())

class Definition(models.Model, ConceptLangUrlMixin):
    concept = models.ForeignKey(Concept, on_delete=models.CASCADE, verbose_name=_("concept"))
    language = models.ForeignKey(Language, on_delete=models.PROTECT, verbose_name=_("language"))
    text = models.TextField(verbose_name=_("definition text"))
    source = models.URLField(blank=True, verbose_name=_("source"))
    history = HistoricalRecords()

    class Meta:
        verbose_name = _("definition")
        verbose_name_plural = _("definitions")
        unique_together = ("concept", "language")

    def __str__(self):
        text = self.text
        concept = force_text(self.concept)
        if len(concept) > 40 and len(text) > 150:
            boundary = concept.find(",", 0, 35)
            if boundary < 0:
                concept = concept[:35] + "…"
            else:
                concept = concept[:boundary]

        trans_data = {
            'iso_code': self.language_id,
            'concept': concept,
            'text': text
        }
        return _("Definition (%(iso_code)s) for %(concept)s: %(text)s") % trans_data

class Concept(models.Model):
    glossary = models.ForeignKey(Glossary, on_delete=models.CASCADE, verbose_name=_("glossary"))
    subject_field = models.ForeignKey('self', related_name='concepts_in_subject_field', null=True, blank=True, on_delete=models.PROTECT, verbose_name=_("subject field"))
    broader_concept = models.ForeignKey('self', related_name='narrower_concepts', null=True, blank=True, on_delete=models.PROTECT, verbose_name=_("broader concept"))
    related_concepts = models.ManyToManyField('self', blank=True, verbose_name=_("related concepts"))
    # This keeps a readable version cached in this table so that no joining
    # with Translation is required for a human readable form.
    repr_cache = models.CharField(max_length=200, editable=False, null=True, blank=True, verbose_name=_("representation"))

    class Meta:
        verbose_name = _("concept")
        verbose_name_plural = _("concepts")
        ordering = ['id']

    def __str__(self):
        if self.repr_cache:
            return self.repr_cache
        return _("Concept #%(concept_id)d") % {'concept_id': self.id}

    def update_repr_cache(self):
        if not self.id:
            return
        src_translations = self.translation_set.filter(
                language_id=self.glossary.source_language_id,
        )
        repr_ = self.repr_from(src_translations)
        if repr_:
            self.repr_cache = repr_
            self.save(update_fields=['repr_cache'])

    def repr_from(self, translations):
        translations = sorted(translations, key=lambda t: t.cmp_key())
        repr_ = ', '.join(t.translation_text for t in translations[:4])[:200]
        repr_ = "#%d: %s" % (self.id, repr_)
        return repr_

    def prev_concept(self):
        """The previous concept in the same glossary."""
        return Concept.objects.filter(
                glossary=self.glossary,
                id__lt=self.id,
        ).only("id", "repr_cache").order_by('-id').first()

    def next_concept(self):
        """The next concept in the same glossary."""
        return Concept.objects.filter(
                glossary=self.glossary,
                id__gt=self.id,
        ).only("id", "repr_cache").order_by('id').first()

    def other_concepts(self):
        """Some surrounding concepts in the glossary."""
        previous_concepts = Concept.objects.filter(
                glossary=self.glossary,
                id__lt=self.pk,
        ).only("id", "repr_cache").order_by('-id')[:5]
        next_concepts = Concept.objects.filter(
                glossary=self.glossary,
                id__gte=self.pk,
        ).only("id", "repr_cache").order_by('id')[:6]

        return itertools.chain(reversed(previous_concepts), next_concepts)

    def get_absolute_url(self):
        return reverse('concept_detail', kwargs={'pk': self.pk})


class Glossary(models.Model):
    name = models.CharField(max_length=50, unique=True, verbose_name=_("name"))
    description = models.TextField(verbose_name=_("description"))
    source_language = models.ForeignKey(Language, related_name='+', on_delete=models.PROTECT, default='en', verbose_name=_("source language"))
    other_languages = models.ManyToManyField(Language, blank=True, related_name='+', verbose_name=_("other languages"))
    subject_fields = models.ManyToManyField('Concept', related_name='glossary_subject_fields', blank=True, verbose_name=_("subject fields"))

    class Meta:
        verbose_name = _("glossary")
        verbose_name_plural = _("glossaries")
        permissions = (
            ('owner', 'Owner of this glossary'),
        )

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('glossary_detail', kwargs={'pk': self.pk})

编辑:根据 Reddit 上的建议,我尝试过这个:

tqs = tqs.filter(language__in=dqs.values_list('language', flat=True)).filter(concept__id__in=dqs.values_list('concept__id', flat=True))

它看起来好多了,但不太管用。它似乎忽略了之前应用于 dqs 的 case 过滤器。当我搜索“GIS”并且关闭区分大小写时,搜索结果包含不包含字符串“gis”或“GIS”的定义。这是相关代码,但使用最新的过滤器来指示正确的缩进:

   if data['definitions_show_partial_matches']:
        if case_filter:
            dqs = dqs.filter(text__contains=search_string)
        else:
            dqs = dqs.filter(text__icontains=search_string)
        tqs = tqs.filter(language__in=dqs.values_list('language', flat=True)).filter(concept__id__in=dqs.values_list('concept__id', flat=True))

I am trying to create a filter based off two query sets of two related models (both already previously filtered). The first returns a list of IDs and concepts (names) with associated values (definitions).

dqs: ID + concept + definitions + associated language (any one of four)

For example:

<QuerySet [<Definition: Definition (en) for #39: CBD, central business district: definition text>, <Definition: Definition (en) for #184: migration: definition text...>

The second also has IDs and concepts which may or may not have matching values in the first list.

tqs: ID1 + concept1 + terms (lang 1); ID1 + concept1 + terms (lang 2)...ID2 + concept1 + terms (lang 1) etc.

For example:

<QuerySet [<Translation: “other language term entry” (fr) for #1: abiotic component>, <Translation: “abiotic component” (en) for #1: abiotic component>, <Translation: “yet another language term entry” (es) for #1: abiotic component>...>

IDs and concepts (expressed in English, the source language) are not language specific. Terms and definitions are language specific. So each concept+ID is associated with term entries and definitions in four different languages.

I need to filter tqs so that it only displays entries that have matching IDs in dqs, and only in the language in which the definition search query was matched (otherwise it will show all four entries associated with the matching ID).

A bit more information below.

The following part of the def describes what happens when a search is made in the glossary matching a string in the definition (which is related to a specific entry).

if data['definitions_show_partial_matches']:
    if case_filter:
        dqs = dqs.filter(text__contains=search_string)
    else:
        dqs = dqs.filter(text__icontains=search_string)

data['definitions_show_partial_matches'] is a BooleanField form - the user indicates in a checkbox whether or not the search also includes the definition text. case_filter indicates whether the search should be case sensitive.

It is possible for more than one language to appear in dqs after filtering, if the string appears in multiple definitions associated with multiple languages.

I have a makeshift solution for matching the IDs:

ids = []
for d in dqs:
    ids.append(d.concept.id)
tqs = tqs.filter(concept__in=ids)

However, this is probably not very efficient. I don't know how to apply concept__in directly in another query set without first extracting the IDs to a list. I've thought of enumerating the for loop, but this is apparently also not a very good idea.

All this is followed by code that efficiently creates a results list with truncated definitions and a maximum number of results.

dqs only displays the matching definitions. Therefore, I need to apply a filter to tqs so that entries associated with matching IDs are not displayed in all four languages.

I also need to build in a check so that if the match took place in English, I need to use the "source_language" field in the related Glossary model, instead of "other_languages", and vice versa. To show how these as well as the concept field are related to Glossary and how I solved a related issue, here is some code from a template ("current_language" is a passed variable from the view):

{% with used_languages=concept.glossary.other_languages.all|dictsort:"iso_code" %}
  {% if used_languages %}
    <div class="language_link_list">
        {% if current_language.name != "English" %}
            <a href="{% url "concept_detail_for_language" pk=concept.pk lang=concept.glossary.source_language.pk %}" class="language_link">{{ concept.glossary.source_language.name }}</a>
        {% endif %}
...
        {% for lang in used_languages %}
            {% if lang.pk != current_language.pk %}
                <a href="{% url "concept_detail_for_language" pk=concept.pk lang=lang.pk %}" class="language_link">{{ lang.name }}</a>
            {% endif %}
        {% endfor %}
    </div>
    <br>
  {% endif %}
{% endwith %}

But this allows the checking of values against an already known value and applying it element by element, instead of a single filter command, where my problem is that the value can change depending on the position of an entry in another query set, and/or the value of a related field in that query set.

I feel like it should not be complicated - I've thought of the following:

  • using "update" - but there is not just one value to compare against
  • using "annotate" - but a definition already has an associated language, so it should not be necessary?
  • using Q() or prefetch_related expressions somehow?

I've been reading through a lot of examples but somehow none of them are quite related to what I'm trying to do.

Thanks!

EDIT: Here are the relevant bits from the relevant models. It's an adaptation from an online project under the GNU GPL. I've removed some comments as well as the name but it shouldn't be a problem if its public.

class Translation(models.Model, ConceptLangUrlMixin):
    concept = models.ForeignKey(Concept, on_delete=models.CASCADE, verbose_name=_("concept"))
    language = models.ForeignKey(Language, on_delete=models.PROTECT, verbose_name=_("language"))
    translation_text = models.CharField(max_length=100, verbose_name=_("translation text"))
    administrative_status = models.ForeignKey(AdministrativeStatus, null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("administrative status"))
    administrative_status_reason = models.ForeignKey(AdministrativeStatusReason, null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("administrative status reason"))
    note = models.TextField(blank=True)

    class Meta:
        verbose_name = _("translation")
        verbose_name_plural = _("translations")
        ordering = ['concept', 'language']

    def __str__(self):
        trans_data = {
            'term': self.translation_text,
            'iso_code': self.language_id,
            'concept': self.concept
        }
        return _("“%(term)s” (%(iso_code)s) for %(concept)s") % trans_data

    def save(self, *args, **kwargs):
        update_repr_cache = kwargs.pop("update_repr_cache", True)
        super(Translation, self).save(*args, **kwargs)
        if update_repr_cache:
            self.concept.update_repr_cache()

    def cmp_key(self):
        # used to sort terms according to their perceived worth
        keys = {
                "preferredTerm-admn-sts": 0,
                "": 1,
                None: 1,
                "admittedTerm-admn-sts": 2,
                "supersededTerm-admn-sts": 3,
                "deprecatedTerm-admn-sts": 4,
        }
        # We return a tuple with the "quality" key, and the text to allow
        # alphabetic sorting for all adminitted terms, for example.
        return (keys.get(self.administrative_status_id, 1), self.translation_text.lower())

class Definition(models.Model, ConceptLangUrlMixin):
    concept = models.ForeignKey(Concept, on_delete=models.CASCADE, verbose_name=_("concept"))
    language = models.ForeignKey(Language, on_delete=models.PROTECT, verbose_name=_("language"))
    text = models.TextField(verbose_name=_("definition text"))
    source = models.URLField(blank=True, verbose_name=_("source"))
    history = HistoricalRecords()

    class Meta:
        verbose_name = _("definition")
        verbose_name_plural = _("definitions")
        unique_together = ("concept", "language")

    def __str__(self):
        text = self.text
        concept = force_text(self.concept)
        if len(concept) > 40 and len(text) > 150:
            boundary = concept.find(",", 0, 35)
            if boundary < 0:
                concept = concept[:35] + "…"
            else:
                concept = concept[:boundary]

        trans_data = {
            'iso_code': self.language_id,
            'concept': concept,
            'text': text
        }
        return _("Definition (%(iso_code)s) for %(concept)s: %(text)s") % trans_data

class Concept(models.Model):
    glossary = models.ForeignKey(Glossary, on_delete=models.CASCADE, verbose_name=_("glossary"))
    subject_field = models.ForeignKey('self', related_name='concepts_in_subject_field', null=True, blank=True, on_delete=models.PROTECT, verbose_name=_("subject field"))
    broader_concept = models.ForeignKey('self', related_name='narrower_concepts', null=True, blank=True, on_delete=models.PROTECT, verbose_name=_("broader concept"))
    related_concepts = models.ManyToManyField('self', blank=True, verbose_name=_("related concepts"))
    # This keeps a readable version cached in this table so that no joining
    # with Translation is required for a human readable form.
    repr_cache = models.CharField(max_length=200, editable=False, null=True, blank=True, verbose_name=_("representation"))

    class Meta:
        verbose_name = _("concept")
        verbose_name_plural = _("concepts")
        ordering = ['id']

    def __str__(self):
        if self.repr_cache:
            return self.repr_cache
        return _("Concept #%(concept_id)d") % {'concept_id': self.id}

    def update_repr_cache(self):
        if not self.id:
            return
        src_translations = self.translation_set.filter(
                language_id=self.glossary.source_language_id,
        )
        repr_ = self.repr_from(src_translations)
        if repr_:
            self.repr_cache = repr_
            self.save(update_fields=['repr_cache'])

    def repr_from(self, translations):
        translations = sorted(translations, key=lambda t: t.cmp_key())
        repr_ = ', '.join(t.translation_text for t in translations[:4])[:200]
        repr_ = "#%d: %s" % (self.id, repr_)
        return repr_

    def prev_concept(self):
        """The previous concept in the same glossary."""
        return Concept.objects.filter(
                glossary=self.glossary,
                id__lt=self.id,
        ).only("id", "repr_cache").order_by('-id').first()

    def next_concept(self):
        """The next concept in the same glossary."""
        return Concept.objects.filter(
                glossary=self.glossary,
                id__gt=self.id,
        ).only("id", "repr_cache").order_by('id').first()

    def other_concepts(self):
        """Some surrounding concepts in the glossary."""
        previous_concepts = Concept.objects.filter(
                glossary=self.glossary,
                id__lt=self.pk,
        ).only("id", "repr_cache").order_by('-id')[:5]
        next_concepts = Concept.objects.filter(
                glossary=self.glossary,
                id__gte=self.pk,
        ).only("id", "repr_cache").order_by('id')[:6]

        return itertools.chain(reversed(previous_concepts), next_concepts)

    def get_absolute_url(self):
        return reverse('concept_detail', kwargs={'pk': self.pk})


class Glossary(models.Model):
    name = models.CharField(max_length=50, unique=True, verbose_name=_("name"))
    description = models.TextField(verbose_name=_("description"))
    source_language = models.ForeignKey(Language, related_name='+', on_delete=models.PROTECT, default='en', verbose_name=_("source language"))
    other_languages = models.ManyToManyField(Language, blank=True, related_name='+', verbose_name=_("other languages"))
    subject_fields = models.ManyToManyField('Concept', related_name='glossary_subject_fields', blank=True, verbose_name=_("subject fields"))

    class Meta:
        verbose_name = _("glossary")
        verbose_name_plural = _("glossaries")
        permissions = (
            ('owner', 'Owner of this glossary'),
        )

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('glossary_detail', kwargs={'pk': self.pk})

EDIT: Based on a suggestion on Reddit I've tried this:

tqs = tqs.filter(language__in=dqs.values_list('language', flat=True)).filter(concept__id__in=dqs.values_list('concept__id', flat=True))

It looks much better but doesn't quite work. It seems to ignore the case filter applied to dqs just before. When I search for "GIS" and case sensitivity is turned off, search results include definitions that do not contains either the strings "gis" or "GIS". Here is the relevant code but with the newest filter to indicate correct indentation:

   if data['definitions_show_partial_matches']:
        if case_filter:
            dqs = dqs.filter(text__contains=search_string)
        else:
            dqs = dqs.filter(text__icontains=search_string)
        tqs = tqs.filter(language__in=dqs.values_list('language', flat=True)).filter(concept__id__in=dqs.values_list('concept__id', flat=True))

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文