diff options
-rw-r--r-- | archaeological_finds/forms_treatments.py | 71 | ||||
-rw-r--r-- | archaeological_finds/migrations/0127_find_treatments_container.py | 102 | ||||
-rw-r--r-- | archaeological_finds/models.py | 2 | ||||
-rw-r--r-- | archaeological_finds/models_finds.py | 51 | ||||
-rw-r--r-- | archaeological_finds/models_treatments.py | 195 | ||||
-rw-r--r-- | archaeological_finds/templates/ishtar/sheet_find.html | 12 | ||||
-rw-r--r-- | archaeological_finds/tests.py | 49 | ||||
-rw-r--r-- | archaeological_warehouse/migrations/0102_auto_20200408_1823.py | 1 | ||||
-rw-r--r-- | archaeological_warehouse/models.py | 7 |
9 files changed, 393 insertions, 97 deletions
diff --git a/archaeological_finds/forms_treatments.py b/archaeological_finds/forms_treatments.py index f7d260f42..50469828b 100644 --- a/archaeological_finds/forms_treatments.py +++ b/archaeological_finds/forms_treatments.py @@ -426,7 +426,7 @@ class QAFindTreatmentForm(IshtarForm): label=_("Year"), initial=lambda: datetime.datetime.now().year, validators=[validators.MinValueValidator(1000), validators.MaxValueValidator(2100)], required=False) - start_date = DateField(label=_("Precise date"), required=False) + start_date = DateField(label=_("Precise date"), required=False, initial=lambda: datetime.datetime.today()) person = forms.IntegerField( label=_("Responsible"), widget=widgets.JQueryAutoComplete( @@ -451,7 +451,9 @@ class QAFindTreatmentForm(IshtarForm): tt_change_ref_loca = list( models.TreatmentType.objects.filter( - available=True, change_reference_location=True).all()) + available=True, change_reference_location=True, + change_current_location=False + ).all()) self.treatment_type_ref_choices = "".join( "<option value='{}'>{}</option>".format(tt.pk, str(tt)) @@ -460,26 +462,34 @@ class QAFindTreatmentForm(IshtarForm): tt_change_current_loca = list( models.TreatmentType.objects.filter( - available=True, change_current_location=True).all()) + available=True, change_current_location=True, + change_reference_location=False + ).all()) self.treatment_type_current_choices = "".join( "<option value='{}'>{}</option>".format(tt.pk, str(tt)) for tt in tt_change_current_loca ) + tt_change_both_loca = list( + models.TreatmentType.objects.filter( + available=True, change_current_location=True, + change_reference_location=True + ).all()) + self.treatment_type_all_choices = "".join( "<option value='{}'>{}</option>".format(tt.pk, str(tt)) - for tt in set(tt_change_current_loca + tt_change_ref_loca) + for tt in tt_change_both_loca ) - super(QAFindTreatmentForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if not self.user: return # treatment type is dynamic put all for check self.fields['treatment_type'].choices = [ (tt.pk, str(tt)) for tt in set( - tt_change_ref_loca + tt_change_current_loca) + tt_change_ref_loca + tt_change_current_loca + tt_change_both_loca) ] q = Person.objects.filter(ishtaruser__pk=self.user.pk) @@ -505,14 +515,16 @@ class QAFindTreatmentForm(IshtarForm): def save(self, items, user): container = Container.objects.get(pk=self.cleaned_data['container']) container_to_change = self.cleaned_data.get('container_to_change', '') + container_attrs = [] + if container_to_change in ('reference', 'current-and-reference'): + container_attrs.append('container_ref') + if container_to_change in ('current', 'current-and-reference'): + container_attrs.append('container') + if self.cleaned_data['create_treatment']: treat_type = models.TreatmentType.objects.get( pk=self.cleaned_data['treatment_type']) - treat_state, __ = models.TreatmentState.objects.get_or_create( - txt_idx='completed', - defaults={ - 'label': _("Completed"), 'executed': True, - 'available': True}) + treat_state = models.TreatmentState.get_completed_state() t = models.Treatment.objects.create( container=container, year=self.cleaned_data['year'], @@ -524,30 +536,19 @@ class QAFindTreatmentForm(IshtarForm): treatment_state=treat_state ) t.treatment_types.add(treat_type) - new_items = t.save(items=items, return_new=True) - if new_items: - items = new_items - container_attrs = [] - if container_to_change in ('reference', 'current-and-reference'): - container_attrs.append('container_ref') - if container_to_change in ('current', 'current-and-reference'): - container_attrs.append('container') - #collection = None - #if self.cleaned_data.get("collection"): - # collection = container.location_id - for find in items: - changed = False - #if collection and find.collection_id != collection: - # find.collection_id = collection - # changed = True - for container_attr in container_attrs: - if getattr(find, container_attr) == container: - continue - setattr(find, container_attr, container) - changed = True - if changed: - find.history_modifier = user - find.save() + t.save(items=items) + t.move_finds_to_new_container(container_attrs=container_attrs) + else: + for find in items: + changed = False + for container_attr in container_attrs: + if getattr(find, container_attr) == container: + continue + setattr(find, container_attr, container) + changed = True + if changed: + find.history_modifier = user + find.save() # administrative act treatment diff --git a/archaeological_finds/migrations/0127_find_treatments_container.py b/archaeological_finds/migrations/0127_find_treatments_container.py new file mode 100644 index 000000000..0667d34f8 --- /dev/null +++ b/archaeological_finds/migrations/0127_find_treatments_container.py @@ -0,0 +1,102 @@ +# Generated by Django 2.2.24 on 2024-09-02 16:43 + +from django.db import migrations, models +import django.db.models.deletion + +SQL = """ +ALTER TABLE archaeological_finds_find_treatments ADD COLUMN full_location TEXT; +ALTER TABLE archaeological_finds_find_treatments ADD COLUMN location_type VARCHAR(1); +""" + + +class Migration(migrations.Migration): + + dependencies = [ + ('archaeological_finds', '0126_fix_treatment_file_exhibition'), + ] + + operations = [ + migrations.RunSQL(SQL), + migrations.AddField( + model_name='findtreatments', + name='full_location', + field=models.TextField(blank=True, default='', verbose_name='Full location') + ), + migrations.AddField( + model_name='findtreatments', + name='location_type', + field=models.CharField(choices=[['C', 'Current'], ['R', 'Reference'], ['B', 'Both']], default='C', + max_length=1, + verbose_name='Location type') + ), + migrations.AlterModelOptions( + name='batchtype', + options={'ordering': ('order', 'label'), 'verbose_name': 'Batch type', 'verbose_name_plural': 'Batch types'}, + ), + migrations.AlterModelOptions( + name='checkedtype', + options={'ordering': ('order', 'label'), 'verbose_name': 'Checked type', 'verbose_name_plural': 'Checked types'}, + ), + migrations.AlterModelOptions( + name='materialtypequalitytype', + options={'ordering': ('order', 'label'), 'verbose_name': 'Material type quality type', 'verbose_name_plural': 'Material type quality types'}, + ), + migrations.AlterModelOptions( + name='objecttypequalitytype', + options={'ordering': ('order', 'label'), 'verbose_name': 'Object type quality type', 'verbose_name_plural': 'Object type quality types'}, + ), + migrations.AlterField( + model_name='findbasket', + name='items', + field=models.ManyToManyField(blank=True, related_name='basket', to='archaeological_finds.Find', verbose_name='Associated finds'), + ), + migrations.AlterField( + model_name='historicaltreatmentfile', + name='external_id', + field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Reference'), + ), + migrations.AlterField( + model_name='treatmentfile', + name='external_id', + field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Reference'), + ), + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.CreateModel( + name='FindTreatment', + fields=[ + ('id', + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('full_location', models.TextField(blank=True, default='', verbose_name='Full location')), + ('location_type', + models.CharField(choices=[['C', 'Current'], ['R', 'Reference'], ['B', 'Both']], default='C', + max_length=1, verbose_name='Location type')), + ], + options={ + 'verbose_name': 'Find - Treatment', + 'verbose_name_plural': 'Find - Treatments', + 'db_table': 'archaeological_finds_find_treatments', + }, + ), + migrations.AlterField( + model_name='find', + name='treatments', + field=models.ManyToManyField(blank=True, help_text='Related treatments when no new find is created', + related_name='finds', through='archaeological_finds.FindTreatment', + to='archaeological_finds.Treatment', verbose_name='Treatments'), + ), + migrations.AddField( + model_name='findtreatment', + name='find', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='archaeological_finds.Find', + verbose_name='Find'), + ), + migrations.AddField( + model_name='findtreatment', + name='treatment', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, + to='archaeological_finds.Treatment'), + ), + ] + ), + ] diff --git a/archaeological_finds/models.py b/archaeological_finds/models.py index 052e493a4..ede0ff2ba 100644 --- a/archaeological_finds/models.py +++ b/archaeological_finds/models.py @@ -12,6 +12,7 @@ from archaeological_finds.models_finds import ( Find, FindBasket, FindInsideContainer, + FindTreatment, FirstBaseFindView, FunctionalArea, IntegrityType, @@ -61,6 +62,7 @@ __all__ = [ "FindDownstreamTreatments", "FindInsideContainer", "FindNonModifTreatments", + "FindTreatment", "FindTreatments", "FindUpstreamTreatments", "FunctionalArea", diff --git a/archaeological_finds/models_finds.py b/archaeological_finds/models_finds.py index 68c988517..55219d452 100644 --- a/archaeological_finds/models_finds.py +++ b/archaeological_finds/models_finds.py @@ -2260,6 +2260,7 @@ class Find( related_name="finds", blank=True, help_text=_("Related treatments when no new find is created"), + through="FindTreatment" ) cached_periods = models.TextField( _("Cached periods label"), @@ -3553,6 +3554,56 @@ for attr in Find.HISTORICAL_M2M: m2m_changed.connect(m2m_historization_changed, sender=getattr(Find, attr).through) +LOCATION_TYPE = [ + ["C", _("Current")], + ["R", _("Reference")], + ["B", _("Both")], +] + + +class FindTreatment(models.Model): + """ + Record all new location for a find. + """ + find = models.ForeignKey( + Find, + verbose_name=_("Find"), + on_delete=models.CASCADE, + ) + treatment = models.ForeignKey( + "archaeological_finds.Treatment", + blank=True, null=True, + on_delete=models.CASCADE + ) + full_location = models.TextField(_("Full location"), default="", blank=True) + location_type = models.CharField(_("Location type"), max_length=1, choices=LOCATION_TYPE, + default="C") + + class Meta: + verbose_name = _("Find - Treatment") + verbose_name_plural = _("Find - Treatments") + db_table = 'archaeological_finds_find_treatments' + + def generate_full_location(self): + if getattr(self, "_full_location_set", False) or self.full_location or ( + not self.treatment.is_current_container_changer and + not self.treatment.is_reference_container_changer): + return + if self.treatment.is_current_container_changer: + if self.treatment.is_reference_container_changer: + self.location_type = "B" + else: + self.location_type = "C" + elif self.treatment.is_reference_container_changer: + self.location_type = "R" + if self.treatment.container: + self.full_location = self.treatment.container.generate_full_location() + elif self.treatment.is_loan: + self.full_location = self.container_ref.generate_full_location() + self._full_location_set = True + self.save() + + class Property(LightHistorizedItem): find = models.ForeignKey(Find, verbose_name=_("Find"), on_delete=models.CASCADE) administrative_act = models.ForeignKey( diff --git a/archaeological_finds/models_treatments.py b/archaeological_finds/models_treatments.py index 168c87e2f..cbca04c9a 100644 --- a/archaeological_finds/models_treatments.py +++ b/archaeological_finds/models_treatments.py @@ -22,13 +22,14 @@ import datetime from django.conf import settings from django.contrib.gis.db import models from django.contrib.postgres.indexes import GinIndex +from django.db import transaction from django.db.models import Max, Q from django.db.models.signals import post_save, post_delete, pre_delete, m2m_changed from django.template.defaultfilters import slugify from django.urls import reverse from ishtar_common.utils import ugettext_lazy as _, pgettext_lazy, SheetItem -from archaeological_finds.models_finds import Find, FindBasket, TreatmentType +from archaeological_finds.models_finds import Find, FindBasket, TreatmentType, FindTreatment from archaeological_operations.models import ClosedItem, Operation from archaeological_context_records.models import Dating from archaeological_warehouse.models import Warehouse, Container @@ -81,6 +82,14 @@ class TreatmentState(GeneralType): return None return q.all()[0].pk + @classmethod + def get_completed_state(cls): + treat_state, __ = cls.objects.get_or_create( + txt_idx='completed', + defaults={ + 'label': _("Completed"), 'executed': True, + 'available': True}) + return treat_state post_save.connect(post_save_cache, sender=TreatmentState) post_delete.connect(post_save_cache, sender=TreatmentState) @@ -560,6 +569,50 @@ class Treatment( for item in new_items: b.items.add(item) + @property + def associated_filename(self): + return "-".join( + str(slugify(getattr(self, attr))) for attr in ("year", "index", "label") + ) + + def get_find_treatment_list(self): + return FindTreatment.objects.filter(treatment=self).all() + + def clean_cache(self): + self._saved_container_attributes, self._is_loan_return, self._is_new_find_creator = None, None, None + self._is_current_container_changer, self._is_reference_container_changer = None, None + + @property + def is_loan_return(self): + if getattr(self, "_is_loan_return", None) is None: + self._is_loan_return = any( + 1 for tp in self.treatment_types.all() if tp.restore_reference_location + ) + return self._is_loan_return + + @property + def is_new_find_creator(self): + if getattr(self, "_is_new_find_creator", None) is None: + self._is_new_find_creator = any( + 1 for tp in self.treatment_types.all() if tp.create_new_find + ) + return self._is_new_find_creator + + @property + def is_current_container_changer(self): + if getattr(self, "_is_current_container_changer", None) is None: + self._is_current_container_changer = self.treatment_types.filter( + change_current_location=True + ).exists() + return self._is_current_container_changer + + @property + def is_reference_container_changer(self): + if getattr(self, "_is_reference_container_changer", None) is None: + self._is_reference_container_changer = self.treatment_types.filter( + change_reference_location=True).exists() + return self._is_reference_container_changer + def save(self, *args, **kwargs): items, user, extra_args_for_new, resulting_find = [], None, [], None upstream_items, upstream_item, resulting_finds = [], None, None @@ -583,7 +636,9 @@ class Treatment( if "return_new" in kwargs: return_new = kwargs.pop("return_new") self.pre_save() - super(Treatment, self).save(*args, **kwargs) + super().save(*args, **kwargs) + # reinit cached values + self.clean_cache() to_be_executed = not self.executed and self.treatment_state.executed updated = [] @@ -620,7 +675,7 @@ class Treatment( self.save() return - create_new_find = bool([tp for tp in treatment_types if tp.create_new_find]) + create_new_find = self.is_new_find_creator new_items = [] for item in items: @@ -642,71 +697,105 @@ class Treatment( basket.items.remove(item) basket.items.add(new) + if to_be_executed or self.executed: + self.verify_find_container_history() + if not to_be_executed: if return_new: return new_items return - if create_new_find: - q = Find.objects.filter(upstream_treatment=self) - else: - q = Find.objects.filter(treatments=self) - # manage loan return - for tp in treatment_types: - if tp.restore_reference_location: - for find in q.all(): - if find.container_ref: - find.container = find.container_ref - if find.pk in updated: - # don't record twice history - find.skip_history_when_saving = True - find.save() - self.executed = True - self.save() - break - + if self.loan_return(updated=updated): + self.executed = True + self.save() # manage containers - if not self.container: - if return_new: - return new_items - return + elif self.container and self.move_finds_to_new_container(updated=updated): + self.executed = True + self.save() + if return_new: + return new_items + return - container_attrs = [] - for tp in treatment_types: - if tp.change_current_location: - if "container" in container_attrs: - continue + @property + def saved_container_attributes(self): + """ + Return container attribute to change depending on treatment types + """ + if getattr(self, "_saved_container_attributes", None) is None: + container_attrs = [] + if self.is_current_container_changer: container_attrs.append("container") - if tp.change_reference_location: - if "container_ref" in container_attrs: - continue + if self.is_reference_container_changer: container_attrs.append("container_ref") + self._saved_container_attributes = container_attrs + return self._saved_container_attributes + + def verify_find_container_history(self): + container_attrs = self.saved_container_attributes + if (not self.container or not container_attrs) and not self.is_loan_return: + return False + q = self._get_finds_query_for_treatments() + if not q.exists(): + return False + for find in q.all(): + q2 = FindTreatment.objects.filter(find=find, treatment=self) + for find_treatment in q2.all(): + find_treatment.generate_full_location() + def _get_finds_query_for_treatments(self): + if self.is_new_find_creator: + return Find.objects.filter(upstream_treatment=self) + return Find.objects.filter(treatments=self) + + def loan_return(self, updated=None): + """ + Manage loan return - change find location using treatment info + :param treatment_types: if not provided re-fetched from database + :param updated: list of already updated finds + :return: True if container changed + """ + if not updated: + updated = [] + if not self.is_loan_return: + return False + q = self._get_finds_query_for_treatments() + for find in q.all(): + if find.container_ref and find.container != find.container_ref: + find.container = find.container_ref + if find.pk in updated: + # don't record twice history + find.skip_history_when_saving = True + else: + updated.append(find.pk) + find.save() + return True + + def move_finds_to_new_container(self, container_attrs=None, updated=None): + """ + Change find location using treatment info + :param updated: list of already updated finds + to update + :return: True if container changed + """ + if not updated: + updated = [] if not container_attrs: - # non consistent treatment - if return_new: - return new_items - return + container_attrs = self.saved_container_attributes + if not container_attrs: + return False + q = self._get_finds_query_for_treatments() for find in q.all(): for container_attr in container_attrs: - if getattr(find, container_attr) != self.container: - setattr(find, container_attr, self.container) - if find.pk in updated: - # don't record twice history - find.skip_history_when_saving = True - find.save() - self.executed = True - self.save() - if return_new: - return new_items - - @property - def associated_filename(self): - return "-".join( - str(slugify(getattr(self, attr))) for attr in ("year", "index", "label") - ) + setattr(find, container_attr, self.container) + if find.pk in updated: + # don't record twice history + find.skip_history_when_saving = True + else: + updated.append(find.pk) + find.save() + return True post_save.connect(cached_label_changed, sender=Treatment) diff --git a/archaeological_finds/templates/ishtar/sheet_find.html b/archaeological_finds/templates/ishtar/sheet_find.html index 27d106fbd..465e2e1d5 100644 --- a/archaeological_finds/templates/ishtar/sheet_find.html +++ b/archaeological_finds/templates/ishtar/sheet_find.html @@ -382,12 +382,11 @@ <tr> <th> </th> <th>{% trans "Year - index" %}</th> - <th>{% trans "Label" %}</th> - <th>{% trans "Type" %}</th> + <th>{% trans "Label/type" %}</th> <th>{% trans "State" %}</th> <th>{% trans "Related finds (max. 15 displayed)" %}</th> <th>{% trans "Doer" %}</th> - <th>{% trans "Container" %}</th> + {% if can_view_container %}<th>{% trans "Container" %}</th>{% endif %} <th>{% trans "Start date" %}</th> <th>{% trans "End date" %}</th> </tr> @@ -400,13 +399,12 @@ <i class="fa fa-info-circle" aria-hidden="true"></i> </a> </td> - <td class='string'>{{ treatment.year }} - {{treatment.index}}</td> - <td class='string'>{{ treatment.label|default_if_none:"-" }}</td> - <td class='string'>{{ treatment.treatment_types_lbl }}</td> + <td class='string'>{{ treatment.year|unlocalize }}-{{treatment.index|unlocalize}}</td> + <td class='string'>{% if treatment.label %}{{ treatment.label}} ({{treatment.treatment_types_lbl}}){% else %}{{treatment.treatment_types_lbl}}{% endif %}</td> <td class='string'>{{ treatment.treatment_state|default_if_none:"-" }}</td> <td class='item-list'>{% for it in items %}<span>{{it}} {{it|link_to_window:request}}</span>{% endfor %}</td> <td class='string'>{{ treatment.person|default_if_none:"-" }}</td> - <td class='string'>{{ treatment.container|default_if_none:"-" }}</td> + {% if can_view_container %}<td class='string'>{% for find_treatment in treatment.get_find_treatment_list %}{% if find_treatment.find.pk == item.pk %}{{ find_treatment.full_location|default_if_none:"-" }}{% endif %}{% endfor %}</td>{% endif %} <td class='string'>{{ treatment.start_date|default_if_none:"-" }}</td> <td class='string'>{{ treatment.end_date|default_if_none:"-" }}</td> </tr> diff --git a/archaeological_finds/tests.py b/archaeological_finds/tests.py index 346aa0b1e..3c38583ae 100644 --- a/archaeological_finds/tests.py +++ b/archaeological_finds/tests.py @@ -2204,7 +2204,20 @@ class FindQATest(FindInit, TestCase): ) packaging = models.TreatmentType.objects.get(txt_idx="packaging") packaging.change_reference_location = True + packaging.change_current_location = False packaging.save() + packaging2, __ = models.TreatmentType.objects.get_or_create( + txt_idx="packaging-2", defaults={"virtual": False} + ) + packaging2.change_reference_location = True + packaging2.change_current_location = True + packaging2.save() + packaging3, __ = models.TreatmentType.objects.get_or_create( + txt_idx="packaging-3", defaults={"virtual": False} + ) + packaging3.change_reference_location = False + packaging3.change_current_location = True + packaging3.save() data_check_lst = [ ( @@ -2242,9 +2255,32 @@ class FindQATest(FindInit, TestCase): {"container_ref": container2, "container": None}, 1, ), + ( + { + "qa-packaging-container": container2.pk, + "qa-packaging-container_to_change": "current-and-reference", + "qa-packaging-create_treatment": True, + "qa-packaging-year": 2019, + "qa-packaging-treatment_type": packaging2.pk, + }, + {"container_ref": container2, "container": container2}, + 1, + ), + ( + { + "qa-packaging-container": container2.pk, + "qa-packaging-container_to_change": "current", + "qa-packaging-create_treatment": True, + "qa-packaging-year": 2019, + "qa-packaging-treatment_type": packaging3.pk, + }, + {"container_ref": None, "container": container2}, + 1, + ), ] - for data, check, nb_treat in data_check_lst: + for idx, lst in enumerate(data_check_lst): + data, check, nb_treat = lst # reinit find_0.container, find_0.container_ref = None, None find_0.skip_history_when_saving = True @@ -2266,6 +2302,17 @@ class FindQATest(FindInit, TestCase): final_nb_treat = models.Treatment.objects.count() self.assertEqual(init_nb_treat + nb_treat, final_nb_treat) + if not final_nb_treat: + self.assertEqual(models.FindTreatment.objects.filter(find=find_0).count(), 0) + continue + + treatment = models.Treatment.objects.order_by("-pk")[0] + q = models.FindTreatment.objects.filter(treatment=treatment) + self.assertEqual(q.filter(find=find_0).count(), 1) + ft = q.all()[0] + self.assertTrue(ft.full_location) + self.assertTrue(ft.location_type) + class FindHistoryTest(FindInit, TestCase): fixtures = FIND_FIXTURES diff --git a/archaeological_warehouse/migrations/0102_auto_20200408_1823.py b/archaeological_warehouse/migrations/0102_auto_20200408_1823.py index f2f969ce0..d2bbf95e3 100644 --- a/archaeological_warehouse/migrations/0102_auto_20200408_1823.py +++ b/archaeological_warehouse/migrations/0102_auto_20200408_1823.py @@ -16,6 +16,7 @@ class Migration(migrations.Migration): ('ishtar_common', '0203_auto_20200407_1142'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('archaeological_warehouse', '0101_squashed'), + ('archaeological_finds', '0105_auto_20200407_1414'), ] operations = [ diff --git a/archaeological_warehouse/models.py b/archaeological_warehouse/models.py index dd6714a41..364926173 100644 --- a/archaeological_warehouse/models.py +++ b/archaeological_warehouse/models.py @@ -1265,7 +1265,7 @@ class Container( @property def name(self): - return "{} - {}".format(self.container_type.name, self.reference) + return "{} - {}".format(self.container_type.label, self.reference) @property def short_label(self): @@ -1316,6 +1316,11 @@ class Container( cached_label = " - ".join(items) return cached_label + def generate_full_location(self): + full_location = [location.short_label for location in self.get_localisations()] + full_location.append(self.short_label) + return " / ".join(full_location) + def _generate_cached_weight(self): return self.weight if self.weight else self.calculated_weight |