summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÉtienne Loks <etienne.loks@iggdrasil.net>2023-04-24 11:21:01 +0200
committerÉtienne Loks <etienne.loks@iggdrasil.net>2023-04-24 11:21:15 +0200
commitf2cd1c1326d5fbfbd39a7b2a30a278dd66883c3c (patch)
treee661aafe6d8a1d0afc8352791865ecb915a3d8db
parent0ff92ef98dbfbfc0e2d89dff18f7bb2dc3950b8c (diff)
downloadIshtar-f2cd1c1326d5fbfbd39a7b2a30a278dd66883c3c.tar.bz2
Ishtar-f2cd1c1326d5fbfbd39a7b2a30a278dd66883c3c.zip
Document -> Town/Area: sheet
-rw-r--r--archaeological_files/models.py4
-rw-r--r--ishtar_common/models_common.py680
-rw-r--r--ishtar_common/templates/ishtar/sheet_document.html2
-rw-r--r--ishtar_common/templates/ishtar/sheet_town.html127
-rw-r--r--ishtar_common/templates/ishtar/sheet_town_pdf.html14
-rw-r--r--ishtar_common/templates/ishtar/sheet_town_window.html3
-rw-r--r--ishtar_common/urls.py5
-rw-r--r--ishtar_common/views.py2
8 files changed, 518 insertions, 319 deletions
diff --git a/archaeological_files/models.py b/archaeological_files/models.py
index d1caaea49..f6f667c4a 100644
--- a/archaeological_files/models.py
+++ b/archaeological_files/models.py
@@ -39,6 +39,7 @@ from ishtar_common.utils import (
)
from ishtar_common.models import (
+ Department,
GeneralType,
BaseHistorizedItem,
OwnPerms,
@@ -47,6 +48,7 @@ from ishtar_common.models import (
Town,
Dashboard,
DashboardFormItem,
+ HistoricalRecords,
ValueGetter,
MainItem,
OperationType,
@@ -61,8 +63,6 @@ from ishtar_common.models import (
HierarchicalType,
)
-from ishtar_common.models_common import HistoricalRecords, Department, MainItem
-
from archaeological_operations.models import (
get_values_town_related,
ClosedItem,
diff --git a/ishtar_common/models_common.py b/ishtar_common/models_common.py
index bc4a0549b..cacb5664e 100644
--- a/ishtar_common/models_common.py
+++ b/ishtar_common/models_common.py
@@ -2056,7 +2056,11 @@ class DocumentItem:
except AttributeError:
actions = []
- if not hasattr(self, "SLUG"):
+ if not hasattr(self, "SLUG") or not hasattr(self, "can_do"):
+ return actions
+
+ if not hasattr(self, "can_do"):
+ print(f"**WARNING** can_do not implemented for {self.__class__}")
return actions
can_add_doc = self.can_do(request, "add_document")
@@ -3092,12 +3096,343 @@ class GeographicItem(models.Model):
return lst
+class SerializeItem:
+ SERIALIZE_EXCLUDE = ["search_vector"]
+ SERIALIZE_PROPERTIES = [
+ "external_id",
+ "multi_polygon_geojson",
+ "point_2d_geojson",
+ "images_number",
+ "json_sections",
+ ]
+ SERIALIZE_CALL = {}
+ SERIALIZE_DATES = []
+ SERIALIZATION_FILES = []
+ SERIALIZE_STRING = []
+
+ def full_serialize(self, search_model=None, recursion=False) -> dict:
+ """
+ API serialization
+ :return: data dict
+ """
+ full_result = {}
+ serialize_fields = []
+
+ exclude = []
+ if search_model:
+ exclude = [sf.key for sf in search_model.sheet_filters.distinct().all()]
+
+ no_geodata = False
+ for prop in ("main_geodata", "geodata", "geodata_list"):
+ if prop in self.SERIALIZE_EXCLUDE or prop in exclude:
+ no_geodata = True
+ break
+
+ for field in self._meta.get_fields():
+ field_name = field.name
+ if field_name in self.SERIALIZE_EXCLUDE or field_name in exclude:
+ continue
+ if field.many_to_one or field.one_to_one:
+ try:
+ value = getattr(self, field_name)
+ except (MultipleObjectsReturned, ObjectDoesNotExist):
+ value = None
+ if value:
+ if (
+ field_name not in self.SERIALIZE_STRING
+ and hasattr(value, "full_serialize")
+ and not recursion
+ ):
+ # print(field.name, self.__class__, self)
+ if field_name == "main_geodata" and no_geodata:
+ continue
+ value = value.full_serialize(search_model, recursion=True)
+ elif field_name in self.SERIALIZATION_FILES:
+ try:
+ value = {"url": value.url}
+ except ValueError:
+ value = None
+ else:
+ value = str(value)
+ else:
+ value = None
+ full_result[field_name] = value
+ if field_name == "main_geodata":
+ full_result["geodata_list"] = [value]
+ elif field.many_to_many:
+ values = getattr(self, field_name)
+ if values.count():
+ first_value = values.all()[0]
+ if (
+ field_name not in self.SERIALIZE_STRING
+ and hasattr(first_value, "full_serialize")
+ and not recursion
+ ):
+ # print(field.name, self.__class__, self)
+ values = [
+ v.full_serialize(search_model, recursion=True)
+ for v in values.all()
+ ]
+ else:
+ if first_value in self.SERIALIZATION_FILES:
+ values = []
+ for v in values:
+ try:
+ values.append({"url": v.url})
+ except ValueError:
+ pass
+ else:
+ values = [str(v) for v in values.all()]
+ else:
+ values = []
+ full_result[field_name] = values
+ else:
+ if field_name in self.SERIALIZATION_FILES:
+ value = getattr(self, field_name)
+ try:
+ value = {"url": value.url}
+ except ValueError:
+ value = None
+ full_result[field.name] = value
+ else:
+ serialize_fields.append(field_name)
+
+ result = json.loads(serialize("json", [self], fields=serialize_fields))
+ full_result.update(result[0]["fields"])
+ for prop in self.SERIALIZE_PROPERTIES:
+ if prop in self.SERIALIZE_EXCLUDE or prop in exclude:
+ continue
+ if hasattr(self, prop) and prop not in full_result:
+ full_result[prop] = getattr(self, prop)
+ if "point_2d_geojson" in full_result:
+ full_result["point_2d"] = True
+ if "multi_polygon_geojson" in full_result:
+ full_result["multi_polygon"] = True
+ for prop in self.SERIALIZE_DATES:
+ if prop in self.SERIALIZE_EXCLUDE or prop in exclude:
+ continue
+ dt = getattr(self, prop) or ""
+ if dt:
+ dt = human_date(dt)
+ full_result[prop] = dt
+ for k in self.SERIALIZE_CALL:
+ if k in self.SERIALIZE_EXCLUDE or k in exclude:
+ continue
+ full_result[k] = getattr(self, self.SERIALIZE_CALL[k])()
+ full_result["SLUG"] = self.SLUG
+ full_result["pk"] = f"external_{self.pk}"
+ full_result["id"] = f"external_{self.id}"
+ return full_result
+
+ def get_associated_main_item_list(self, attr, model) -> list:
+ items = getattr(self, attr)
+ if not items.count():
+ return []
+ lst = []
+ table_cols = model.TABLE_COLS
+ if callable(table_cols):
+ table_cols = table_cols()
+ for colname in table_cols:
+ if colname in model.COL_LABELS:
+ lst.append(str(model.COL_LABELS[colname]))
+ else:
+ lst.append(model._meta.get_field(colname).verbose_name)
+ lst = [lst]
+ for values in items.values_list(*table_cols):
+ lst.append(["-" if v is None else v for v in values])
+ return lst
+
+
+class ShortMenuItem:
+ """
+ Item available in the short menu
+ """
+
+ UP_MODEL_QUERY = {}
+
+ @classmethod
+ def get_short_menu_class(cls, pk):
+ return ""
+
+ @property
+ def short_class_name(self):
+ return ""
+
+
+class MainItem(ShortMenuItem, SerializeItem):
+ """
+ Item with quick actions available from tables
+ Extra actions are available from sheets
+ Manage cascade updated, has_changed and no_post_process
+ """
+
+ QUICK_ACTIONS = []
+ SLUG = ""
+ DOWN_MODEL_UPDATE = []
+ INITIAL_VALUES = [] # list of field checkable if changed on save
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._initial_values = {}
+ for field_name in self.INITIAL_VALUES:
+ self._initial_values[field_name] = getattr(self, field_name)
+
+ def has_changed(self):
+ """
+ Check which field have a changed value between INITIAL_VALUES
+ :return: list of changed fields
+ """
+ changed = []
+ for field_name in self._initial_values:
+ value = getattr(self, field_name)
+ if self._initial_values[field_name] != value:
+ changed.append(field_name)
+ self._initial_values[field_name] = value
+ return changed
+
+ def cascade_update(self, changed=True):
+ if not changed:
+ return
+ for down_model in self.DOWN_MODEL_UPDATE:
+ if not settings.USE_BACKGROUND_TASK:
+ rel = getattr(self, down_model)
+ if hasattr(rel.model, "need_update"):
+ rel.update(need_update=True)
+ continue
+ for item in getattr(self, down_model).all():
+ item.cached_label_changed()
+ if hasattr(item, "main_geodata"):
+ item.post_save_geo()
+
+ def no_post_process(self):
+ self.skip_history_when_saving = True
+ self._cached_label_checked = True
+ self._post_saved_geo = True
+ self._external_id_changed = False
+ self._search_updated = True
+ self._no_move = True
+
+ @classmethod
+ def app_label(cls):
+ return cls._meta.app_label
+
+ @classmethod
+ def model_name(cls):
+ return cls._meta.model_name
+
+ @classmethod
+ def class_verbose_name(cls):
+ return cls._meta.verbose_name
+
+ @classmethod
+ def get_columns(cls, table_cols_attr="TABLE_COLS", dict_col_labels=True):
+ """
+ :param table_cols_attr: "TABLE_COLS" if not specified
+ :param dict_col_labels: (default: True) if set to False return list matching
+ with table_cols list
+ :return: (table_cols, table_col_labels)
+ """
+ return get_columns_from_class(cls, table_cols_attr=table_cols_attr,
+ dict_col_labels=dict_col_labels)
+
+ def get_search_url(self):
+ if self.SLUG:
+ try:
+ return reverse(self.SLUG + "_search")
+ except NoReverseMatch:
+ pass
+
+ @classmethod
+ def get_quick_actions(cls, user, session=None, obj=None):
+ """
+ Get a list of (url, title, icon, target) actions for an user
+ """
+ qas = []
+ for action in cls.QUICK_ACTIONS:
+ if not action.is_available(user, session=session, obj=obj):
+ continue
+ qas.append(
+ [
+ action.base_url,
+ mark_safe(action.text),
+ mark_safe(action.rendered_icon),
+ action.target or "",
+ action.is_popup,
+ ]
+ )
+ return qas
+
+ @classmethod
+ def get_quick_action_by_url(cls, url):
+ for action in cls.QUICK_ACTIONS:
+ if action.url == url:
+ return action
+
+ def regenerate_external_id(self):
+ if not hasattr(self, "external_id"):
+ return
+ self.skip_history_when_saving = True
+ self._no_move = True
+ self.external_id = ""
+ self.auto_external_id = True
+ self.save()
+
+ def cached_label_changed(self):
+ self.no_post_process()
+ self._cached_label_checked = False
+ cached_label_changed(self.__class__, instance=self, created=False)
+
+ def post_save_geo(self):
+ self.no_post_process()
+ self._post_saved_geo = False
+ post_save_geo(self.__class__, instance=self, created=False)
+
+ def external_id_changed(self):
+ self.no_post_process()
+ self._external_id_changed = False
+ external_id_changed(self.__class__, instance=self, created=False)
+
+ def can_do(self, request, action_name):
+ """
+ Check permission availability for the current object.
+ :param request: request object
+ :param action_name: action name eg: "change_find"
+ :return: boolean
+ """
+ # overload with OwnPerm when _own_ is relevant
+ if not getattr(request.user, "ishtaruser", None):
+ return False
+ user = request.user
+ return user.ishtaruser.has_right(action_name, request.session)\
+
+ def get_extra_actions(self, request):
+ if not hasattr(self, "SLUG"):
+ return []
+
+ actions = []
+ if request.user.is_superuser and hasattr(self, "auto_external_id"):
+ actions += [
+ (
+ reverse("regenerate-external-id")
+ + "?{}={}".format(self.SLUG, self.pk),
+ _("Regenerate ID"),
+ "fa fa-key",
+ _("regen."),
+ "btn-info",
+ True,
+ 200,
+ )
+ ]
+
+ return actions
+
+
class TownManager(models.Manager):
def get_by_natural_key(self, numero_insee, year):
return self.get(numero_insee=numero_insee, year=year)
-class Town(GeographicItem, Imported, DocumentItem, models.Model):
+class Town(GeographicItem, Imported, DocumentItem, MainItem, models.Model):
SLUG = "town"
name = models.CharField(_("Name"), max_length=100)
surface = models.IntegerField(_("Surface (m2)"), blank=True, null=True)
@@ -3163,6 +3498,20 @@ class Town(GeographicItem, Imported, DocumentItem, models.Model):
_("Name"), "Code commune (numéro INSEE)", _("Cached name")
)
+ @property
+ def surface_ha(self):
+ if not self.surface:
+ return 0
+ return self.surface / 10000.0
+
+ def get_filename(self):
+ if self.numero_insee:
+ return f"{self.numero_insee} - {slugify(self.name)}"
+ return slugify(self.name)
+
+ def associated_filename(self):
+ return self.get_filename()
+
def get_values(self, prefix="", **kwargs):
return {
prefix or "label": str(self),
@@ -3289,6 +3638,18 @@ class Town(GeographicItem, Imported, DocumentItem, models.Model):
cached_label += " ({})".format(self.year)
return cached_label
+ def get_extra_actions(self, request):
+ """
+ For sheet template
+ """
+ # url, base_text, icon, extra_text, extra css class, is a quick action
+ actions = super().get_extra_actions(request)
+ profile = get_current_profile()
+ can_add_geo = profile.mapping and self.can_do(request, "add_geovectordata")
+ if can_add_geo:
+ actions.append(self.get_add_geo_action())
+ return actions
+
def post_save_town(sender, **kwargs):
cached_label_changed(sender, **kwargs)
@@ -4249,318 +4610,3 @@ class SearchVectorConfig:
if not isinstance(value, list):
return [value]
return value
-
-
-class ShortMenuItem:
- """
- Item available in the short menu
- """
-
- UP_MODEL_QUERY = {}
-
- @classmethod
- def get_short_menu_class(cls, pk):
- return ""
-
- @property
- def short_class_name(self):
- return ""
-
-
-class SerializeItem:
- SERIALIZE_EXCLUDE = ["search_vector"]
- SERIALIZE_PROPERTIES = [
- "external_id",
- "multi_polygon_geojson",
- "point_2d_geojson",
- "images_number",
- "json_sections",
- ]
- SERIALIZE_CALL = {}
- SERIALIZE_DATES = []
- SERIALIZATION_FILES = []
- SERIALIZE_STRING = []
-
- def full_serialize(self, search_model=None, recursion=False) -> dict:
- """
- API serialization
- :return: data dict
- """
- full_result = {}
- serialize_fields = []
-
- exclude = []
- if search_model:
- exclude = [sf.key for sf in search_model.sheet_filters.distinct().all()]
-
- no_geodata = False
- for prop in ("main_geodata", "geodata", "geodata_list"):
- if prop in self.SERIALIZE_EXCLUDE or prop in exclude:
- no_geodata = True
- break
-
- for field in self._meta.get_fields():
- field_name = field.name
- if field_name in self.SERIALIZE_EXCLUDE or field_name in exclude:
- continue
- if field.many_to_one or field.one_to_one:
- try:
- value = getattr(self, field_name)
- except (MultipleObjectsReturned, ObjectDoesNotExist):
- value = None
- if value:
- if (
- field_name not in self.SERIALIZE_STRING
- and hasattr(value, "full_serialize")
- and not recursion
- ):
- # print(field.name, self.__class__, self)
- if field_name == "main_geodata" and no_geodata:
- continue
- value = value.full_serialize(search_model, recursion=True)
- elif field_name in self.SERIALIZATION_FILES:
- try:
- value = {"url": value.url}
- except ValueError:
- value = None
- else:
- value = str(value)
- else:
- value = None
- full_result[field_name] = value
- if field_name == "main_geodata":
- full_result["geodata_list"] = [value]
- elif field.many_to_many:
- values = getattr(self, field_name)
- if values.count():
- first_value = values.all()[0]
- if (
- field_name not in self.SERIALIZE_STRING
- and hasattr(first_value, "full_serialize")
- and not recursion
- ):
- # print(field.name, self.__class__, self)
- values = [
- v.full_serialize(search_model, recursion=True)
- for v in values.all()
- ]
- else:
- if first_value in self.SERIALIZATION_FILES:
- values = []
- for v in values:
- try:
- values.append({"url": v.url})
- except ValueError:
- pass
- else:
- values = [str(v) for v in values.all()]
- else:
- values = []
- full_result[field_name] = values
- else:
- if field_name in self.SERIALIZATION_FILES:
- value = getattr(self, field_name)
- try:
- value = {"url": value.url}
- except ValueError:
- value = None
- full_result[field.name] = value
- else:
- serialize_fields.append(field_name)
-
- result = json.loads(serialize("json", [self], fields=serialize_fields))
- full_result.update(result[0]["fields"])
- for prop in self.SERIALIZE_PROPERTIES:
- if prop in self.SERIALIZE_EXCLUDE or prop in exclude:
- continue
- if hasattr(self, prop) and prop not in full_result:
- full_result[prop] = getattr(self, prop)
- if "point_2d_geojson" in full_result:
- full_result["point_2d"] = True
- if "multi_polygon_geojson" in full_result:
- full_result["multi_polygon"] = True
- for prop in self.SERIALIZE_DATES:
- if prop in self.SERIALIZE_EXCLUDE or prop in exclude:
- continue
- dt = getattr(self, prop) or ""
- if dt:
- dt = human_date(dt)
- full_result[prop] = dt
- for k in self.SERIALIZE_CALL:
- if k in self.SERIALIZE_EXCLUDE or k in exclude:
- continue
- full_result[k] = getattr(self, self.SERIALIZE_CALL[k])()
- full_result["SLUG"] = self.SLUG
- full_result["pk"] = f"external_{self.pk}"
- full_result["id"] = f"external_{self.id}"
- return full_result
-
- def get_associated_main_item_list(self, attr, model) -> list:
- items = getattr(self, attr)
- if not items.count():
- return []
- lst = []
- table_cols = model.TABLE_COLS
- if callable(table_cols):
- table_cols = table_cols()
- for colname in table_cols:
- if colname in model.COL_LABELS:
- lst.append(str(model.COL_LABELS[colname]))
- else:
- lst.append(model._meta.get_field(colname).verbose_name)
- lst = [lst]
- for values in items.values_list(*table_cols):
- lst.append(["-" if v is None else v for v in values])
- return lst
-
-
-class MainItem(ShortMenuItem, SerializeItem):
- """
- Item with quick actions available from tables
- Extra actions are available from sheets
- Manage cascade updated, has_changed and no_post_process
- """
-
- QUICK_ACTIONS = []
- SLUG = ""
- DOWN_MODEL_UPDATE = []
- INITIAL_VALUES = [] # list of field checkable if changed on save
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self._initial_values = {}
- for field_name in self.INITIAL_VALUES:
- self._initial_values[field_name] = getattr(self, field_name)
-
- def has_changed(self):
- """
- Check which field have a changed value between INITIAL_VALUES
- :return: list of changed fields
- """
- changed = []
- for field_name in self._initial_values:
- value = getattr(self, field_name)
- if self._initial_values[field_name] != value:
- changed.append(field_name)
- self._initial_values[field_name] = value
- return changed
-
- def cascade_update(self, changed=True):
- if not changed:
- return
- for down_model in self.DOWN_MODEL_UPDATE:
- if not settings.USE_BACKGROUND_TASK:
- rel = getattr(self, down_model)
- if hasattr(rel.model, "need_update"):
- rel.update(need_update=True)
- continue
- for item in getattr(self, down_model).all():
- item.cached_label_changed()
- if hasattr(item, "main_geodata"):
- item.post_save_geo()
-
- def no_post_process(self):
- self.skip_history_when_saving = True
- self._cached_label_checked = True
- self._post_saved_geo = True
- self._external_id_changed = False
- self._search_updated = True
- self._no_move = True
-
- @classmethod
- def app_label(cls):
- return cls._meta.app_label
-
- @classmethod
- def model_name(cls):
- return cls._meta.model_name
-
- @classmethod
- def class_verbose_name(cls):
- return cls._meta.verbose_name
-
- @classmethod
- def get_columns(cls, table_cols_attr="TABLE_COLS", dict_col_labels=True):
- """
- :param table_cols_attr: "TABLE_COLS" if not specified
- :param dict_col_labels: (default: True) if set to False return list matching
- with table_cols list
- :return: (table_cols, table_col_labels)
- """
- return get_columns_from_class(cls, table_cols_attr=table_cols_attr,
- dict_col_labels=dict_col_labels)
-
- def get_search_url(self):
- if self.SLUG:
- return reverse(self.SLUG + "_search")
-
- @classmethod
- def get_quick_actions(cls, user, session=None, obj=None):
- """
- Get a list of (url, title, icon, target) actions for an user
- """
- qas = []
- for action in cls.QUICK_ACTIONS:
- if not action.is_available(user, session=session, obj=obj):
- continue
- qas.append(
- [
- action.base_url,
- mark_safe(action.text),
- mark_safe(action.rendered_icon),
- action.target or "",
- action.is_popup,
- ]
- )
- return qas
-
- @classmethod
- def get_quick_action_by_url(cls, url):
- for action in cls.QUICK_ACTIONS:
- if action.url == url:
- return action
-
- def regenerate_external_id(self):
- if not hasattr(self, "external_id"):
- return
- self.skip_history_when_saving = True
- self._no_move = True
- self.external_id = ""
- self.auto_external_id = True
- self.save()
-
- def cached_label_changed(self):
- self.no_post_process()
- self._cached_label_checked = False
- cached_label_changed(self.__class__, instance=self, created=False)
-
- def post_save_geo(self):
- self.no_post_process()
- self._post_saved_geo = False
- post_save_geo(self.__class__, instance=self, created=False)
-
- def external_id_changed(self):
- self.no_post_process()
- self._external_id_changed = False
- external_id_changed(self.__class__, instance=self, created=False)
-
- def get_extra_actions(self, request):
- if not hasattr(self, "SLUG"):
- return []
-
- actions = []
- if request.user.is_superuser and hasattr(self, "auto_external_id"):
- actions += [
- (
- reverse("regenerate-external-id")
- + "?{}={}".format(self.SLUG, self.pk),
- _("Regenerate ID"),
- "fa fa-key",
- _("regen."),
- "btn-info",
- True,
- 200,
- )
- ]
-
- return actions
diff --git a/ishtar_common/templates/ishtar/sheet_document.html b/ishtar_common/templates/ishtar/sheet_document.html
index 388c2ca26..e4ce47af5 100644
--- a/ishtar_common/templates/ishtar/sheet_document.html
+++ b/ishtar_common/templates/ishtar/sheet_document.html
@@ -128,6 +128,8 @@
{% field_flex_full "Treatment files" item.treatment_files|add_links %}
{% field_flex_full "Warehouses" item.warehouses|add_links %}
{% field_flex_full "Containers" item.containers|add_links %}
+{% field_flex_full "Towns" item.towns|add_links %}
+{% field_flex_full "Areas" item.areas|add_links %}
{% endif %}
{{ item.coins_tag|default:""|safe }}
{% endblock %}
diff --git a/ishtar_common/templates/ishtar/sheet_town.html b/ishtar_common/templates/ishtar/sheet_town.html
new file mode 100644
index 000000000..425effd8b
--- /dev/null
+++ b/ishtar_common/templates/ishtar/sheet_town.html
@@ -0,0 +1,127 @@
+{% extends "ishtar/sheet.html" %}
+{% load i18n ishtar_helpers window_tables window_header window_field from_dict %}
+
+{% block head_title %}<strong>{% trans "Town" %}</strong> - {{item.name}}{% if item.numero_insee %} ({{item.numero_insee}}){% endif %}{% endblock %}
+
+{% block toolbar %}
+{% window_nav item window_id 'show-town' %}
+{% endblock %}
+
+{% block content %}
+
+{# trick to set to null non existing variable #}
+{% with permission_view_document=permission_view_document %}
+{% with permission_view_own_document=permission_view_own_document %}
+{% with permission_change_own_geovectordata=permission_change_own_geovectordata %}
+{% with permission_change_geovectordata=permission_change_geovectordata %}
+
+{% with permission_change_geo=permission_change_own_geovectordata|or_:permission_change_geovectordata %}
+
+{% with perm_documents=permission_view_own_document|or_:permission_view_document %}
+{% with has_documents=item|safe_or:"documents.count|documents_list" %}
+{% with display_documents=perm_documents|and_:has_documents %}
+
+{% if output != "ODT" and output != "PDF" %}
+<ul class="nav nav-tabs" id="{{window_id}}-tabs" role="tablist">
+ <li class="nav-item">
+ <a class="nav-link active" id="{{window_id}}-general-tab"
+ data-toggle="tab" href="#{{window_id}}-general" role="tab"
+ aria-controls="{{window_id}}-general" aria-selected="false">
+ {% trans "General" %}
+ </a>
+ </li>
+ {% if not is_external and SHOW_GEO %}
+ <li class="nav-item">
+ <a class="nav-link" id="{{window_id}}-geodata-tab"
+ data-toggle="tab" href="#{{window_id}}-geodata" role="tab"
+ aria-controls="{{window_id}}-geodata" aria-selected="false">
+ {% trans "Geographic data" %}
+ </a>
+ </li>
+ {% endif %}
+</ul>
+{% endif %}
+
+<div class="tab-content" id="{{window_id}}-tab-content">
+ <div class="tab-pane fade show active" id="{{window_id}}-general"
+ role="tabpanel" aria-labelledby="{{window_id}}-general-tab">
+
+ <div class="clearfix">
+ <div class="card float-left col-12 col-md-6 col-lg-4">
+ {% include "ishtar/blocks/window_image.html" %}
+ <div class="card-body">
+ <div class="row">
+ <div class="col main">
+ {% if item.numero_insee %}
+ <strong>{{ item.numero_insee }}</strong
+ >{% endif %}
+ </div>
+ </div>
+ <div class="card-text">
+ <p class='window-refs' title="{% trans 'Name' %}">{{item.name}}</p>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ {% if item.surface %}
+ <dl class="col-12 col-md-6 col-lg-3 flex-wrap">
+ <dt>{%trans "Surface"%}</dt>
+ <dd>
+ {{ item.surface }} m<sup>2</sup> ({{ item.surface_ha }} ha)
+ </dd>
+ </dl>
+ {% endif %}
+ {% with has_image=item.images.count %}
+ {% if not has_image %}
+ </div>
+ </div>
+ {% endif %}
+ {% if has_image %}
+ </div> {# <div> #}
+ {% endif %}
+ {% endwith %}
+ <hr class="clearfix">
+ <h3>{% trans "Geographic localisation" %}</h3>
+ <div class="row">
+ {% with geo_item=item %}
+ {% include "ishtar/blocks/sheet_map.html"%}
+ {% endwith %}
+ </div>
+
+ {% if display_documents and item.documents.count %}
+ {% trans "Documents" as town_docs %}
+ {% dynamic_table_document town_docs 'documents' 'towns' item.pk '' output %}
+ {% endif %}
+
+ </div>
+ {% if not is_external and SHOW_GEO %}
+ <div class="tab-pane fade" id="{{window_id}}-geodata"
+ role="tabpanel" aria-labelledby="{{window_id}}-geodata-tab">
+ {% with geo_item=item %}{% include "ishtar/blocks/sheet_geographic.html" %}{% endwith %}
+ </div>
+ {% endif %}
+
+</div>
+
+
+<script type="text/javascript">
+$(document).ready( function () {
+ datatable_options = {
+ "dom": 'ltip',
+ };
+ $.extend(datatable_options, datatables_static_default);
+ if (datatables_i18n) datatable_options['language'] = datatables_i18n;
+ $('.datatables').each(
+ function(){
+ var dt_id = "#" + $(this).attr('id');
+ if (! $.fn.DataTable.isDataTable(dt_id) ) {
+ $(dt_id).DataTable(datatable_options);
+ }
+ });
+} );
+</script>
+
+{% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %} {% endwith %}
+
+{% endblock %} \ No newline at end of file
diff --git a/ishtar_common/templates/ishtar/sheet_town_pdf.html b/ishtar_common/templates/ishtar/sheet_town_pdf.html
new file mode 100644
index 000000000..72928f86b
--- /dev/null
+++ b/ishtar_common/templates/ishtar/sheet_town_pdf.html
@@ -0,0 +1,14 @@
+{% extends "ishtar/sheet_town.html" %}
+{% block header %}
+{% endblock %}
+{% block main_head %}
+{{ block.super }}
+<div id="pdfheader">
+Ishtar &ndash; {{APP_NAME}} &ndash; {{item}}
+</div>
+{% endblock %}
+{%block head_sheet%}{%endblock%}
+{%block main_foot%}
+</body>
+</html>
+{%endblock%}
diff --git a/ishtar_common/templates/ishtar/sheet_town_window.html b/ishtar_common/templates/ishtar/sheet_town_window.html
new file mode 100644
index 000000000..045b6a163
--- /dev/null
+++ b/ishtar_common/templates/ishtar/sheet_town_window.html
@@ -0,0 +1,3 @@
+{% extends "ishtar/sheet_town.html" %}
+{% block main_head %}{%endblock%}
+{% block main_foot %}{%endblock%}
diff --git a/ishtar_common/urls.py b/ishtar_common/urls.py
index 0fe9c1716..ebd6df0f0 100644
--- a/ishtar_common/urls.py
+++ b/ishtar_common/urls.py
@@ -378,6 +378,11 @@ urlpatterns += [
views.department_by_state,
name="department-by-state",
),
+ url(
+ r"show-town(?:/(?P<pk>.+))?/(?P<type>.+)?$",
+ views.show_town,
+ name="show-town",
+ ),
url(r"autocomplete-town/?$", views.autocomplete_town, name="autocomplete-town"),
url(
r"autocomplete-advanced-town/(?P<department_id>[0-9]+[ABab]?)?$",
diff --git a/ishtar_common/views.py b/ishtar_common/views.py
index b469df12d..7af07d6ad 100644
--- a/ishtar_common/views.py
+++ b/ishtar_common/views.py
@@ -1029,6 +1029,8 @@ get_person_for_account = get_item(
get_ishtaruser = get_item(models.IshtarUser, "get_ishtaruser", "ishtaruser")
+show_town = show_item(models.Town, "town")
+
def action(request, action_slug, obj_id=None, *args, **kwargs):
"""