We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Can't save term translation after updating the term's text in DB
I've changed the term's text to `Review checker`, since that's the correct capitalization in English (changed from the original)
https://pontoon.mozilla.org/a/terminology/term/666/change/

To my surprise, the term showed up as untranslated for Italian. The other problem is that I cannot save the translation: https://pontoon.mozilla.org/it/terminology/all-resources/?status=missing&string=301053

Error in log is 

```
Internal Server Error: /translations/create/
    response = get_response(request)
  File "/app/.heroku/python/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    return func(request, *args, **kwargs)
  File "/app/pontoon/base/utils.py", line 126, in wrap
    return f(request, *args, **kwargs)
  File "/app/.heroku/python/lib/python3.9/site-packages/django/contrib/auth/decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "/app/.heroku/python/lib/python3.9/contextlib.py", line 79, in inner
  File "/app/pontoon/translations/views.py", line 117, in create_translation
    translation.save(failed_checks=failed_checks)
  File "/app/pontoon/base/models.py", line 3429, in save
    self.entity.reset_term_translation(self.locale)
  File "/app/pontoon/base/models.py", line 2865, in reset_term_translation
    term = self.term
pontoon.base.models.Entity.term.RelatedObjectDoesNotExist: Entity has no term.
```
Can't save term translation after updating the term's text in DB
I've changed the term's text to `Review checker`, since that's the correct capitalization in English (changed from the original)
https://pontoon.mozilla.org/a/terminology/term/666/change/

To my surprise, the term showed up as untranslated for Italian. The other problem is that I cannot save the translation: https://pontoon.mozilla.org/it/terminology/all-resources/?status=missing&string=301053

Error in log is 

```
Internal Server Error: /translations/create/
    response = get_response(request)
  File "/app/.heroku/python/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    return func(request, *args, **kwargs)
  File "/app/pontoon/base/utils.py", line 126, in wrap
    return f(request, *args, **kwargs)
  File "/app/.heroku/python/lib/python3.9/site-packages/django/contrib/auth/decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "/app/.heroku/python/lib/python3.9/contextlib.py", line 79, in inner
  File "/app/pontoon/translations/views.py", line 117, in create_translation
    translation.save(failed_checks=failed_checks)
  File "/app/pontoon/base/models.py", line 3429, in save
    self.entity.reset_term_translation(self.locale)
  File "/app/pontoon/base/models.py", line 2865, in reset_term_translation
    term = self.term
pontoon.base.models.Entity.term.RelatedObjectDoesNotExist: Entity has no term.
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `pontoon/terminology/models.py`
Content:
```
1 import re
2 
3 from django.db import models
4 
5 from pontoon.base.models import Entity, ProjectLocale, Resource, TranslatedResource
6 
7 
8 def update_terminology_project_stats():
9     resource = Resource.objects.get(project__slug="terminology")
10     project = resource.project
11     total_strings = Entity.objects.filter(resource=resource, obsolete=False).count()
12     resource.total_strings = total_strings
13     resource.save(update_fields=["total_strings"])
14 
15     translated_resources = list(TranslatedResource.objects.filter(resource=resource))
16 
17     for translated_resource in translated_resources:
18         translated_resource.calculate_stats(save=False)
19 
20     TranslatedResource.objects.bulk_update(
21         translated_resources,
22         [
23             "total_strings",
24             "approved_strings",
25             "pretranslated_strings",
26             "strings_with_errors",
27             "strings_with_warnings",
28             "unreviewed_strings",
29         ],
30     )
31 
32     project.aggregate_stats()
33 
34     for locale in project.locales.all():
35         locale.aggregate_stats()
36 
37     for projectlocale in ProjectLocale.objects.filter(project=project):
38         projectlocale.aggregate_stats()
39 
40 
41 class TermQuerySet(models.QuerySet):
42     def for_string(self, string):
43         terms = []
44         available_terms = self.exclude(definition="").exclude(forbidden=True)
45 
46         for term in available_terms:
47             term_text = r"\b" + re.escape(term.text)
48             flags = 0 if term.case_sensitive else re.IGNORECASE
49 
50             if re.search(term_text, string, flags):
51                 terms.append(term)
52 
53         return terms
54 
55     def delete(self, *args, **kwargs):
56         """
57         Before deleting Terms, obsolete their Entities
58         """
59         for term in self:
60             term.obsolete_entity()
61         update_terminology_project_stats()
62 
63         super().delete(*args, **kwargs)
64 
65 
66 class Term(models.Model):
67     text = models.CharField(max_length=255)
68     entity = models.OneToOneField("base.Entity", models.SET_NULL, null=True, blank=True)
69 
70     class PartOfSpeech(models.TextChoices):
71         ADJECTIVE = "adjective", "Adjective"
72         ADVERB = "adverb", "Adverb"
73         NOUN = "noun", "Noun"
74         VERB = "verb", "Verb"
75 
76     part_of_speech = models.CharField(max_length=50, choices=PartOfSpeech.choices)
77 
78     definition = models.TextField(blank=True)
79     usage = models.TextField(blank=True)
80     notes = models.TextField(blank=True)
81 
82     class Status(models.TextChoices):
83         APPROVED = "approved", "Approved"
84         NEW = "new", "New"
85         OBSOLETE = "obsolete", "Obsolete"
86         REVIEW = "review", "Review"
87 
88     status = models.CharField(
89         max_length=20, choices=Status.choices, null=True, blank=True
90     )
91 
92     case_sensitive = models.BooleanField(default=False)
93     do_not_translate = models.BooleanField(default=False)
94     forbidden = models.BooleanField(default=False)
95 
96     created_at = models.DateTimeField(auto_now_add=True)
97     created_by = models.ForeignKey(
98         "auth.User", models.SET_NULL, related_name="terms", null=True, blank=True
99     )
100 
101     objects = TermQuerySet.as_manager()
102 
103     def translation(self, locale):
104         """
105         Get locale translation of the term.
106         """
107         if self.do_not_translate:
108             return self.text
109         else:
110             try:
111                 return self.translations.get(locale=locale).text
112             except (AttributeError, TermTranslation.DoesNotExist):
113                 return None
114 
115     @property
116     def localizable(self):
117         """
118         Check if the term is localizable.
119         """
120         if self.do_not_translate:
121             return False
122 
123         if self.forbidden:
124             return False
125 
126         if self.definition == "":
127             return False
128 
129         return True
130 
131     def entity_comment(self):
132         """
133         Generate entity comment from the term.
134         """
135         comment = "{}. {}.".format(
136             self.part_of_speech.capitalize(),
137             self.definition.capitalize().rstrip("."),
138         )
139 
140         if self.usage:
141             comment += " E.g. {}.".format(self.usage.capitalize().rstrip("."))
142 
143         return comment
144 
145     def create_entity(self):
146         """
147         An Entity must be created (or deobsoleted) for a Term according to the
148         following rules:
149         - Entity.string contains content of Term.text.
150         - Entity.comment contains joint content of several fields:
151           Term.part_of_speech. Term.definition. E.g.: Term.usage.
152         """
153         resource = Resource.objects.get(project__slug="terminology")
154 
155         entity, created = Entity.objects.get_or_create(
156             string=self.text,
157             comment=self.entity_comment(),
158             resource=resource,
159         )
160 
161         # Using update() to avoid circular Term.save() call
162         Term.objects.filter(pk=self.pk).update(entity_id=entity.id)
163 
164         if not created:
165             entity.obsolete = False
166             entity.save(update_fields=["obsolete"])
167 
168         # Make sure Term entities are ordered alphabetically
169         entities = list(
170             Entity.objects.filter(resource=resource, obsolete=False).order_by("string")
171         )
172         for index, e in enumerate(entities):
173             e.order = index
174         Entity.objects.bulk_update(entities, ["order"])
175 
176     def obsolete_entity(self):
177         entity = self.entity
178 
179         # Ignore if term doesn't have entity assigned
180         if entity is None:
181             return
182 
183         entity.obsolete = True
184         entity.save(update_fields=["obsolete"])
185 
186     def handle_term_update(self):
187         """
188         Before updating an existing Term, update its Entity if neccessary
189         """
190         term = self
191         old_term = Term.objects.get(pk=term.pk)
192 
193         # Ignore changes to non-localizable terms that stay non-localizable
194         if not old_term.localizable and not term.localizable:
195             return
196 
197         # If localizable term becomes non-localizable, obsolete its Entity
198         if old_term.localizable and not term.localizable:
199             old_term.obsolete_entity()
200 
201         # If non-localizable term becomes localizable, create a corresponding Entity
202         elif not old_term.localizable and term.localizable:
203             term.create_entity()
204 
205         # If relevant changes are made to the localizable term that stays localizable
206         else:
207             # If Term.text changes, a new Entity instance gets created and the previous one becomes obsolete.
208             if old_term.text != term.text:
209                 old_term.obsolete_entity()
210                 term.create_entity()
211 
212             # If Term.part_of_speech, Term.definition or Term.usage change, Entity.comment gets updated.
213             elif (
214                 old_term.part_of_speech != term.part_of_speech
215                 or old_term.definition != term.definition
216                 or old_term.usage != term.usage
217             ):
218                 entity = term.entity
219 
220                 # Ignore if term doesn't have entity assigned
221                 if entity is None:
222                     return
223 
224                 entity.comment = term.entity_comment()
225                 entity.save(update_fields=["comment"])
226 
227                 return
228 
229         update_terminology_project_stats()
230 
231     def handle_term_create(self):
232         """
233         After creating a new localizable Term, create its Entity
234         """
235         self.create_entity()
236         update_terminology_project_stats()
237 
238     def save(self, *args, **kwargs):
239         created = self.pk is None
240 
241         if not created:
242             self.handle_term_update()
243 
244         super().save(*args, **kwargs)
245 
246         if created and self.localizable:
247             self.handle_term_create()
248 
249     def delete(self, *args, **kwargs):
250         """
251         Before deleting a Term, obsolete its Entity
252         """
253         self.obsolete_entity()
254         update_terminology_project_stats()
255 
256         super().delete(*args, **kwargs)
257 
258     def __str__(self):
259         return self.text
260 
261 
262 class TermTranslation(models.Model):
263     term = models.ForeignKey(Term, models.CASCADE, related_name="translations")
264     locale = models.ForeignKey("base.Locale", models.CASCADE, related_name="terms")
265 
266     text = models.CharField(max_length=255)
267 
268     def __str__(self):
269         return self.text
270
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/pontoon/terminology/models.py b/pontoon/terminology/models.py
--- a/pontoon/terminology/models.py
+++ b/pontoon/terminology/models.py
@@ -160,10 +160,12 @@
 
         # Using update() to avoid circular Term.save() call
         Term.objects.filter(pk=self.pk).update(entity_id=entity.id)
+        entity.term = self
 
         if not created:
             entity.obsolete = False
-            entity.save(update_fields=["obsolete"])
+
+        entity.save()
 
         # Make sure Term entities are ordered alphabetically
         entities = list(

{"golden_diff": "diff --git a/pontoon/terminology/models.py b/pontoon/terminology/models.py\n--- a/pontoon/terminology/models.py\n+++ b/pontoon/terminology/models.py\n@@ -160,10 +160,12 @@\n \n         # Using update() to avoid circular Term.save() call\n         Term.objects.filter(pk=self.pk).update(entity_id=entity.id)\n+        entity.term = self\n \n         if not created:\n             entity.obsolete = False\n-            entity.save(update_fields=[\"obsolete\"])\n+\n+        entity.save()\n \n         # Make sure Term entities are ordered alphabetically\n         entities = list(\n", "issue": "Can't save term translation after updating the term's text in DB\nI've changed the term's text to `Review checker`, since that's the correct capitalization in English (changed from the original)\r\nhttps://pontoon.mozilla.org/a/terminology/term/666/change/\r\n\r\nTo my surprise, the term showed up as untranslated for Italian. The other problem is that I cannot save the translation: https://pontoon.mozilla.org/it/terminology/all-resources/?status=missing&string=301053\r\n\r\nError in log is \r\n\r\n```\r\nInternal Server Error: /translations/create/\r\n    response = get_response(request)\r\n  File \"/app/.heroku/python/lib/python3.9/site-packages/django/core/handlers/base.py\", line 181, in _get_response\r\n    return func(request, *args, **kwargs)\r\n  File \"/app/pontoon/base/utils.py\", line 126, in wrap\r\n    return f(request, *args, **kwargs)\r\n  File \"/app/.heroku/python/lib/python3.9/site-packages/django/contrib/auth/decorators.py\", line 21, in _wrapped_view\r\n    return view_func(request, *args, **kwargs)\r\n  File \"/app/.heroku/python/lib/python3.9/contextlib.py\", line 79, in inner\r\n  File \"/app/pontoon/translations/views.py\", line 117, in create_translation\r\n    translation.save(failed_checks=failed_checks)\r\n  File \"/app/pontoon/base/models.py\", line 3429, in save\r\n    self.entity.reset_term_translation(self.locale)\r\n  File \"/app/pontoon/base/models.py\", line 2865, in reset_term_translation\r\n    term = self.term\r\npontoon.base.models.Entity.term.RelatedObjectDoesNotExist: Entity has no term.\r\n```\nCan't save term translation after updating the term's text in DB\nI've changed the term's text to `Review checker`, since that's the correct capitalization in English (changed from the original)\r\nhttps://pontoon.mozilla.org/a/terminology/term/666/change/\r\n\r\nTo my surprise, the term showed up as untranslated for Italian. The other problem is that I cannot save the translation: https://pontoon.mozilla.org/it/terminology/all-resources/?status=missing&string=301053\r\n\r\nError in log is \r\n\r\n```\r\nInternal Server Error: /translations/create/\r\n    response = get_response(request)\r\n  File \"/app/.heroku/python/lib/python3.9/site-packages/django/core/handlers/base.py\", line 181, in _get_response\r\n    return func(request, *args, **kwargs)\r\n  File \"/app/pontoon/base/utils.py\", line 126, in wrap\r\n    return f(request, *args, **kwargs)\r\n  File \"/app/.heroku/python/lib/python3.9/site-packages/django/contrib/auth/decorators.py\", line 21, in _wrapped_view\r\n    return view_func(request, *args, **kwargs)\r\n  File \"/app/.heroku/python/lib/python3.9/contextlib.py\", line 79, in inner\r\n  File \"/app/pontoon/translations/views.py\", line 117, in create_translation\r\n    translation.save(failed_checks=failed_checks)\r\n  File \"/app/pontoon/base/models.py\", line 3429, in save\r\n    self.entity.reset_term_translation(self.locale)\r\n  File \"/app/pontoon/base/models.py\", line 2865, in reset_term_translation\r\n    term = self.term\r\npontoon.base.models.Entity.term.RelatedObjectDoesNotExist: Entity has no term.\r\n```\n", "before_files": [{"content": "import re\n\nfrom django.db import models\n\nfrom pontoon.base.models import Entity, ProjectLocale, Resource, TranslatedResource\n\n\ndef update_terminology_project_stats():\n    resource = Resource.objects.get(project__slug=\"terminology\")\n    project = resource.project\n    total_strings = Entity.objects.filter(resource=resource, obsolete=False).count()\n    resource.total_strings = total_strings\n    resource.save(update_fields=[\"total_strings\"])\n\n    translated_resources = list(TranslatedResource.objects.filter(resource=resource))\n\n    for translated_resource in translated_resources:\n        translated_resource.calculate_stats(save=False)\n\n    TranslatedResource.objects.bulk_update(\n        translated_resources,\n        [\n            \"total_strings\",\n            \"approved_strings\",\n            \"pretranslated_strings\",\n            \"strings_with_errors\",\n            \"strings_with_warnings\",\n            \"unreviewed_strings\",\n        ],\n    )\n\n    project.aggregate_stats()\n\n    for locale in project.locales.all():\n        locale.aggregate_stats()\n\n    for projectlocale in ProjectLocale.objects.filter(project=project):\n        projectlocale.aggregate_stats()\n\n\nclass TermQuerySet(models.QuerySet):\n    def for_string(self, string):\n        terms = []\n        available_terms = self.exclude(definition=\"\").exclude(forbidden=True)\n\n        for term in available_terms:\n            term_text = r\"\\b\" + re.escape(term.text)\n            flags = 0 if term.case_sensitive else re.IGNORECASE\n\n            if re.search(term_text, string, flags):\n                terms.append(term)\n\n        return terms\n\n    def delete(self, *args, **kwargs):\n        \"\"\"\n        Before deleting Terms, obsolete their Entities\n        \"\"\"\n        for term in self:\n            term.obsolete_entity()\n        update_terminology_project_stats()\n\n        super().delete(*args, **kwargs)\n\n\nclass Term(models.Model):\n    text = models.CharField(max_length=255)\n    entity = models.OneToOneField(\"base.Entity\", models.SET_NULL, null=True, blank=True)\n\n    class PartOfSpeech(models.TextChoices):\n        ADJECTIVE = \"adjective\", \"Adjective\"\n        ADVERB = \"adverb\", \"Adverb\"\n        NOUN = \"noun\", \"Noun\"\n        VERB = \"verb\", \"Verb\"\n\n    part_of_speech = models.CharField(max_length=50, choices=PartOfSpeech.choices)\n\n    definition = models.TextField(blank=True)\n    usage = models.TextField(blank=True)\n    notes = models.TextField(blank=True)\n\n    class Status(models.TextChoices):\n        APPROVED = \"approved\", \"Approved\"\n        NEW = \"new\", \"New\"\n        OBSOLETE = \"obsolete\", \"Obsolete\"\n        REVIEW = \"review\", \"Review\"\n\n    status = models.CharField(\n        max_length=20, choices=Status.choices, null=True, blank=True\n    )\n\n    case_sensitive = models.BooleanField(default=False)\n    do_not_translate = models.BooleanField(default=False)\n    forbidden = models.BooleanField(default=False)\n\n    created_at = models.DateTimeField(auto_now_add=True)\n    created_by = models.ForeignKey(\n        \"auth.User\", models.SET_NULL, related_name=\"terms\", null=True, blank=True\n    )\n\n    objects = TermQuerySet.as_manager()\n\n    def translation(self, locale):\n        \"\"\"\n        Get locale translation of the term.\n        \"\"\"\n        if self.do_not_translate:\n            return self.text\n        else:\n            try:\n                return self.translations.get(locale=locale).text\n            except (AttributeError, TermTranslation.DoesNotExist):\n                return None\n\n    @property\n    def localizable(self):\n        \"\"\"\n        Check if the term is localizable.\n        \"\"\"\n        if self.do_not_translate:\n            return False\n\n        if self.forbidden:\n            return False\n\n        if self.definition == \"\":\n            return False\n\n        return True\n\n    def entity_comment(self):\n        \"\"\"\n        Generate entity comment from the term.\n        \"\"\"\n        comment = \"{}. {}.\".format(\n            self.part_of_speech.capitalize(),\n            self.definition.capitalize().rstrip(\".\"),\n        )\n\n        if self.usage:\n            comment += \" E.g. {}.\".format(self.usage.capitalize().rstrip(\".\"))\n\n        return comment\n\n    def create_entity(self):\n        \"\"\"\n        An Entity must be created (or deobsoleted) for a Term according to the\n        following rules:\n        - Entity.string contains content of Term.text.\n        - Entity.comment contains joint content of several fields:\n          Term.part_of_speech. Term.definition. E.g.: Term.usage.\n        \"\"\"\n        resource = Resource.objects.get(project__slug=\"terminology\")\n\n        entity, created = Entity.objects.get_or_create(\n            string=self.text,\n            comment=self.entity_comment(),\n            resource=resource,\n        )\n\n        # Using update() to avoid circular Term.save() call\n        Term.objects.filter(pk=self.pk).update(entity_id=entity.id)\n\n        if not created:\n            entity.obsolete = False\n            entity.save(update_fields=[\"obsolete\"])\n\n        # Make sure Term entities are ordered alphabetically\n        entities = list(\n            Entity.objects.filter(resource=resource, obsolete=False).order_by(\"string\")\n        )\n        for index, e in enumerate(entities):\n            e.order = index\n        Entity.objects.bulk_update(entities, [\"order\"])\n\n    def obsolete_entity(self):\n        entity = self.entity\n\n        # Ignore if term doesn't have entity assigned\n        if entity is None:\n            return\n\n        entity.obsolete = True\n        entity.save(update_fields=[\"obsolete\"])\n\n    def handle_term_update(self):\n        \"\"\"\n        Before updating an existing Term, update its Entity if neccessary\n        \"\"\"\n        term = self\n        old_term = Term.objects.get(pk=term.pk)\n\n        # Ignore changes to non-localizable terms that stay non-localizable\n        if not old_term.localizable and not term.localizable:\n            return\n\n        # If localizable term becomes non-localizable, obsolete its Entity\n        if old_term.localizable and not term.localizable:\n            old_term.obsolete_entity()\n\n        # If non-localizable term becomes localizable, create a corresponding Entity\n        elif not old_term.localizable and term.localizable:\n            term.create_entity()\n\n        # If relevant changes are made to the localizable term that stays localizable\n        else:\n            # If Term.text changes, a new Entity instance gets created and the previous one becomes obsolete.\n            if old_term.text != term.text:\n                old_term.obsolete_entity()\n                term.create_entity()\n\n            # If Term.part_of_speech, Term.definition or Term.usage change, Entity.comment gets updated.\n            elif (\n                old_term.part_of_speech != term.part_of_speech\n                or old_term.definition != term.definition\n                or old_term.usage != term.usage\n            ):\n                entity = term.entity\n\n                # Ignore if term doesn't have entity assigned\n                if entity is None:\n                    return\n\n                entity.comment = term.entity_comment()\n                entity.save(update_fields=[\"comment\"])\n\n                return\n\n        update_terminology_project_stats()\n\n    def handle_term_create(self):\n        \"\"\"\n        After creating a new localizable Term, create its Entity\n        \"\"\"\n        self.create_entity()\n        update_terminology_project_stats()\n\n    def save(self, *args, **kwargs):\n        created = self.pk is None\n\n        if not created:\n            self.handle_term_update()\n\n        super().save(*args, **kwargs)\n\n        if created and self.localizable:\n            self.handle_term_create()\n\n    def delete(self, *args, **kwargs):\n        \"\"\"\n        Before deleting a Term, obsolete its Entity\n        \"\"\"\n        self.obsolete_entity()\n        update_terminology_project_stats()\n\n        super().delete(*args, **kwargs)\n\n    def __str__(self):\n        return self.text\n\n\nclass TermTranslation(models.Model):\n    term = models.ForeignKey(Term, models.CASCADE, related_name=\"translations\")\n    locale = models.ForeignKey(\"base.Locale\", models.CASCADE, related_name=\"terms\")\n\n    text = models.CharField(max_length=255)\n\n    def __str__(self):\n        return self.text\n", "path": "pontoon/terminology/models.py"}], "after_files": [{"content": "import re\n\nfrom django.db import models\n\nfrom pontoon.base.models import Entity, ProjectLocale, Resource, TranslatedResource\n\n\ndef update_terminology_project_stats():\n    resource = Resource.objects.get(project__slug=\"terminology\")\n    project = resource.project\n    total_strings = Entity.objects.filter(resource=resource, obsolete=False).count()\n    resource.total_strings = total_strings\n    resource.save(update_fields=[\"total_strings\"])\n\n    translated_resources = list(TranslatedResource.objects.filter(resource=resource))\n\n    for translated_resource in translated_resources:\n        translated_resource.calculate_stats(save=False)\n\n    TranslatedResource.objects.bulk_update(\n        translated_resources,\n        [\n            \"total_strings\",\n            \"approved_strings\",\n            \"pretranslated_strings\",\n            \"strings_with_errors\",\n            \"strings_with_warnings\",\n            \"unreviewed_strings\",\n        ],\n    )\n\n    project.aggregate_stats()\n\n    for locale in project.locales.all():\n        locale.aggregate_stats()\n\n    for projectlocale in ProjectLocale.objects.filter(project=project):\n        projectlocale.aggregate_stats()\n\n\nclass TermQuerySet(models.QuerySet):\n    def for_string(self, string):\n        terms = []\n        available_terms = self.exclude(definition=\"\").exclude(forbidden=True)\n\n        for term in available_terms:\n            term_text = r\"\\b\" + re.escape(term.text)\n            flags = 0 if term.case_sensitive else re.IGNORECASE\n\n            if re.search(term_text, string, flags):\n                terms.append(term)\n\n        return terms\n\n    def delete(self, *args, **kwargs):\n        \"\"\"\n        Before deleting Terms, obsolete their Entities\n        \"\"\"\n        for term in self:\n            term.obsolete_entity()\n        update_terminology_project_stats()\n\n        super().delete(*args, **kwargs)\n\n\nclass Term(models.Model):\n    text = models.CharField(max_length=255)\n    entity = models.OneToOneField(\"base.Entity\", models.SET_NULL, null=True, blank=True)\n\n    class PartOfSpeech(models.TextChoices):\n        ADJECTIVE = \"adjective\", \"Adjective\"\n        ADVERB = \"adverb\", \"Adverb\"\n        NOUN = \"noun\", \"Noun\"\n        VERB = \"verb\", \"Verb\"\n\n    part_of_speech = models.CharField(max_length=50, choices=PartOfSpeech.choices)\n\n    definition = models.TextField(blank=True)\n    usage = models.TextField(blank=True)\n    notes = models.TextField(blank=True)\n\n    class Status(models.TextChoices):\n        APPROVED = \"approved\", \"Approved\"\n        NEW = \"new\", \"New\"\n        OBSOLETE = \"obsolete\", \"Obsolete\"\n        REVIEW = \"review\", \"Review\"\n\n    status = models.CharField(\n        max_length=20, choices=Status.choices, null=True, blank=True\n    )\n\n    case_sensitive = models.BooleanField(default=False)\n    do_not_translate = models.BooleanField(default=False)\n    forbidden = models.BooleanField(default=False)\n\n    created_at = models.DateTimeField(auto_now_add=True)\n    created_by = models.ForeignKey(\n        \"auth.User\", models.SET_NULL, related_name=\"terms\", null=True, blank=True\n    )\n\n    objects = TermQuerySet.as_manager()\n\n    def translation(self, locale):\n        \"\"\"\n        Get locale translation of the term.\n        \"\"\"\n        if self.do_not_translate:\n            return self.text\n        else:\n            try:\n                return self.translations.get(locale=locale).text\n            except (AttributeError, TermTranslation.DoesNotExist):\n                return None\n\n    @property\n    def localizable(self):\n        \"\"\"\n        Check if the term is localizable.\n        \"\"\"\n        if self.do_not_translate:\n            return False\n\n        if self.forbidden:\n            return False\n\n        if self.definition == \"\":\n            return False\n\n        return True\n\n    def entity_comment(self):\n        \"\"\"\n        Generate entity comment from the term.\n        \"\"\"\n        comment = \"{}. {}.\".format(\n            self.part_of_speech.capitalize(),\n            self.definition.capitalize().rstrip(\".\"),\n        )\n\n        if self.usage:\n            comment += \" E.g. {}.\".format(self.usage.capitalize().rstrip(\".\"))\n\n        return comment\n\n    def create_entity(self):\n        \"\"\"\n        An Entity must be created (or deobsoleted) for a Term according to the\n        following rules:\n        - Entity.string contains content of Term.text.\n        - Entity.comment contains joint content of several fields:\n          Term.part_of_speech. Term.definition. E.g.: Term.usage.\n        \"\"\"\n        resource = Resource.objects.get(project__slug=\"terminology\")\n\n        entity, created = Entity.objects.get_or_create(\n            string=self.text,\n            comment=self.entity_comment(),\n            resource=resource,\n        )\n\n        # Using update() to avoid circular Term.save() call\n        Term.objects.filter(pk=self.pk).update(entity_id=entity.id)\n        entity.term = self\n\n        if not created:\n            entity.obsolete = False\n\n        entity.save()\n\n        # Make sure Term entities are ordered alphabetically\n        entities = list(\n            Entity.objects.filter(resource=resource, obsolete=False).order_by(\"string\")\n        )\n        for index, e in enumerate(entities):\n            e.order = index\n        Entity.objects.bulk_update(entities, [\"order\"])\n\n    def obsolete_entity(self):\n        entity = self.entity\n\n        # Ignore if term doesn't have entity assigned\n        if entity is None:\n            return\n\n        entity.obsolete = True\n        entity.save(update_fields=[\"obsolete\"])\n\n    def handle_term_update(self):\n        \"\"\"\n        Before updating an existing Term, update its Entity if neccessary\n        \"\"\"\n        term = self\n        old_term = Term.objects.get(pk=term.pk)\n\n        # Ignore changes to non-localizable terms that stay non-localizable\n        if not old_term.localizable and not term.localizable:\n            return\n\n        # If localizable term becomes non-localizable, obsolete its Entity\n        if old_term.localizable and not term.localizable:\n            old_term.obsolete_entity()\n\n        # If non-localizable term becomes localizable, create a corresponding Entity\n        elif not old_term.localizable and term.localizable:\n            term.create_entity()\n\n        # If relevant changes are made to the localizable term that stays localizable\n        else:\n            # If Term.text changes, a new Entity instance gets created and the previous one becomes obsolete.\n            if old_term.text != term.text:\n                old_term.obsolete_entity()\n                term.create_entity()\n\n            # If Term.part_of_speech, Term.definition or Term.usage change, Entity.comment gets updated.\n            elif (\n                old_term.part_of_speech != term.part_of_speech\n                or old_term.definition != term.definition\n                or old_term.usage != term.usage\n            ):\n                entity = term.entity\n\n                # Ignore if term doesn't have entity assigned\n                if entity is None:\n                    return\n\n                entity.comment = term.entity_comment()\n                entity.save(update_fields=[\"comment\"])\n\n                return\n\n        update_terminology_project_stats()\n\n    def handle_term_create(self):\n        \"\"\"\n        After creating a new localizable Term, create its Entity\n        \"\"\"\n        self.create_entity()\n        update_terminology_project_stats()\n\n    def save(self, *args, **kwargs):\n        created = self.pk is None\n\n        if not created:\n            self.handle_term_update()\n\n        super().save(*args, **kwargs)\n\n        if created and self.localizable:\n            self.handle_term_create()\n\n    def delete(self, *args, **kwargs):\n        \"\"\"\n        Before deleting a Term, obsolete its Entity\n        \"\"\"\n        self.obsolete_entity()\n        update_terminology_project_stats()\n\n        super().delete(*args, **kwargs)\n\n    def __str__(self):\n        return self.text\n\n\nclass TermTranslation(models.Model):\n    term = models.ForeignKey(Term, models.CASCADE, related_name=\"translations\")\n    locale = models.ForeignKey(\"base.Locale\", models.CASCADE, related_name=\"terms\")\n\n    text = models.CharField(max_length=255)\n\n    def __str__(self):\n        return self.text\n", "path": "pontoon/terminology/models.py"}]}