class GeometryMixin(models.Model): geometry = MultiPolygonField(verbose_name=_('Country border geometry'), null=True, blank=True) geometry_simplified = MultiPolygonField(verbose_name=_('Simplified Geometry'), null=True, blank=True) class Meta: abstract = True @classmethod def to_multipolygon(cls, geos_geom: [MultiPolygon, Polygon]) -> MultiPolygon: return MultiPolygon(geos_geom) if isinstance(geos_geom, Polygon) else geos_geom @classmethod def optimize_geometry(cls, geometry: [GEOSGeometry]) -> [MultiPolygon]: if geometry is None: return geometry # magic numbers tolerance = 0.03 tolerance_divider = 4 max_attempts = 5 for _i in range(max_attempts): geometry_simplified = geometry.simplify(tolerance=tolerance) if not geometry_simplified.empty: return cls.to_multipolygon(geometry_simplified) tolerance = tolerance / tolerance_divider return geometry def save(self, *args, **kwargs): self.geometry_simplified = self.optimize_geometry(self.geometry) super().save(*args, **kwargs)
class Province(models.Model): name = models.CharField(max_length=50) code = models.IntegerField(null=True, blank=True) boundary = MultiPolygonField(null=True, blank=True) def __str__(self): return self.name
class MultiAreaSource(models.Model): """A multi-polygon geometry demarcating the area of a Project or Real Estate boundaries (where applicable)""" # IDENTIFICATION name = models.CharField(max_length=255) # LINKS asset = models.ForeignKey('portfolio.ProjectAsset', null=True, blank=True, on_delete=models.CASCADE) # ATTRIBUTES location = MultiPolygonField() # # BOOKKEEPING FIELDS # creation_date = models.DateTimeField(auto_now_add=True) last_change_date = models.DateTimeField(auto_now=True) def __str__(self): return self.name def get_absolute_url(self): return reverse('portfolio:MultiAreaSource_edit', kwargs={'pk': self.pk}) class Meta: verbose_name = "Multi Area Source" verbose_name_plural = "Multi Area Sources"
class Shape(Model): # Should this just be called Border? """ A Shape is a region polygon associated with a State from a start_date to an end_date. This should accomodate States with discontiguous territories and existences. A Shape may optionally have Events attached to its start_date and end_date. """ state = ForeignKey(State, on_delete=CASCADE) # TODO: Figure out the right way to store the shapefile here. PolygonField feels like the right approach, but is it? # Progress! It will probably involve LayerMapping (https://docs.djangoproject.com/en/2.0/ref/contrib/gis/tutorial/#importing-spatial-data) # TODO: Add this field once this app is dockerized and Postgres + PostGIS are working shape = MultiPolygonField(blank=True, null=True) source = TextField( help_text= 'Citation for where you found this map. Guide: http://rmit.libguides.com/harvardvisual/maps.' ) start_date = DateField(help_text='When this border takes effect.') start_event = ForeignKey( 'Event', on_delete=SET_NULL, null=True, blank=True, related_name='new_borders', help_text= "If this field is set, this event's date overwrites the start_date") end_date = DateField(help_text='When this border ceases to exist.') end_event = ForeignKey( 'Event', on_delete=SET_NULL, null=True, blank=True, related_name='prior_borders', help_text= "If this field is set, this event's date overwrites the end_date") def get_bordering_shapes(self, date=None): """ TODO: This should return the list of shapes that border it @date. If date is None, it should return all shapes thar border it throughout its existence. I imagine this will require a custom PostGIS query. Or maybe just the __touches GeoDjango query. """ Shape.objects.filter( start_date__lte=date, end_date__gte=date, shape__touches=self.shape) if date else Shape.objects.filter( shape__touches=self.shape) def clean(self): """ Sets the start/end_date to the date of the associated events? This will need to be called again if the event's date ever changes. """ if self.start_event: self.start_date = self.start_event.date if self.end_event: self.end_date = self.end_event.date def __str__(self): return f'{self.state}: {self.start_date} to {self.end_date}'
class CategoryDbca(models.Model): ''' This model is used for defining the categories ''' wkb_geometry = MultiPolygonField(srid=4326, blank=True, null=True) category_name = models.CharField(max_length=20, blank=True, null=True) class Meta: app_label = 'disturbance'
class RegionGeomMA(models.Model): # note: these are only the fields we care about gid = models.IntegerField(primary_key=True) town = models.TextField() geom = MultiPolygonField(geography=True) class Meta: db_table = "official_region_geom_ma" managed = False
class District(models.Model): name = models.CharField(max_length=50) code = models.IntegerField(null=True, blank=True) province = models.ForeignKey('Province', related_name='district', on_delete=models.CASCADE) boundary = MultiPolygonField(null=True, blank=True) def __str__(self): return self.name
class WaCoast(models.Model): ''' This model is used for validating if the apiary site is in the valid area ''' wkb_geometry = MultiPolygonField(srid=4326, blank=True, null=True) type = models.CharField(max_length=30, blank=True, null=True) source = models.CharField(max_length=50, blank=True, null=True) smoothed = models.BooleanField(default=False) class Meta: app_label = 'disturbance'
class RegionDbca(models.Model): wkb_geometry = MultiPolygonField(srid=4326, blank=True, null=True) region_name = models.CharField(max_length=200, blank=True, null=True) office = models.CharField(max_length=200, blank=True, null=True) object_id = models.PositiveIntegerField(blank=True, null=True) class Meta: ordering = [ 'object_id', ] app_label = 'disturbance'
class UsStates(Model): state = CharField(max_length=2) name = CharField(max_length=24) fips = CharField(max_length=2) lon = FloatField() lat = FloatField() geom = MultiPolygonField(srid=4326) # Returns the string representation of the model. def __str__(self): # __unicode__ on Python 2 return self.name
class Locality(models.Model): name = models.CharField(max_length=128, db_column='name', help_text=_('Name of locality'), blank=False) description = models.TextField(blank=True, db_column='description', verbose_name=_('description'), help_text=_('Description of the locality')) locality_type = models.ForeignKey('LocalityType', on_delete=models.PROTECT, db_column='locality_type_id', verbose_name=_('locality type'), help_text=_('Type of locality'), blank=False, null=False) geometry = MultiPolygonField(blank=True, db_column='geometry', verbose_name=_('geometry'), help_text=_('Geometry of locality'), spatial_index=True) metadata = JSONField(db_column='metadata', verbose_name=_('metadata'), help_text=_('Metadata associated to locality'), default=empty_JSON, blank=True, null=True) is_part_of = models.ManyToManyField("self", symmetrical=False, blank=True) class Meta: verbose_name = _('Locality') verbose_name_plural = _('Localities') ordering = ['-name'] def clean(self, *args, **kwargs): try: self.locality_type.validate_metadata(self.metadata) except ValidationError as error: raise ValidationError({'metadata': error}) super().clean(*args, **kwargs) def validate_point(self, point): if not self.geometry.contains(point): msg = _("Point is not contained within the locality's geometry") raise ValidationError(msg) def __str__(self): return '{locality_type}: {name}'.format( locality_type=self.locality_type, name=self.name)
class CensusBlocksResults(models.Model): """ Stores geometries and results from the Census blocks shapefile. """ uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) geom = MultiPolygonField(srid=4326, blank=True, null=True) job = models.ForeignKey(AnalysisJob, related_name='census_block_results', on_delete=models.CASCADE, null=True, blank=True) overall_score = models.FloatField(blank=True, null=True)
class CommunitySpace(models.Model): province = models.ForeignKey('Province', related_name='community_spaces', on_delete=models.CASCADE) district = models.ForeignKey('District', related_name='community_spaces', on_delete=models.CASCADE) municipality = models.ForeignKey('Municipality', related_name='community_spaces', on_delete=models.CASCADE) ward = models.CharField(max_length=100, blank=True, null=True) cid = models.CharField(max_length=100, null=True, blank=True) title = models.CharField(max_length=1000) description = models.TextField(blank=True, null=True) current_land_use = models.CharField(max_length=300, blank=True, null=True) coordinates_elevation = models.CharField(max_length=200, null=True, blank=True) usable_area = models.FloatField(default=Decimal('0.0000'), null=True, blank=True) total_area = models.FloatField(default=Decimal('0.0000'), null=True, blank=True) elevation = models.CharField(max_length=1000, null=True, blank=True) location = PointField(geography=True, srid=4326, blank=True, null=True) capacity = models.FloatField(blank=True, null=True, default=Decimal('0.0000')) type = models.CharField(max_length=100, null=True, blank=True) address = models.CharField(max_length=200, blank=True, null=True) main_community_space = models.ForeignKey(MainCommunitySpace, on_delete=models.CASCADE, related_name="community_spaces", null=True, blank=True) polygons = MultiPolygonField(null=True, blank=True) def __str__(self): return self.title @property def latitude(self): if self.location: return self.location.y @property def longitude(self): if self.location: return self.location.x
class EPCI(models.Model): class TypeEPCI(models.TextChoices): CA = "CA", "Communauté d'agglomération" CC = "CC", "Communauté de communes" CU = "CU", "Communauté urbaine" METROPOLE = "ME", "Métropole" TYPE_CA = "CA" TYPE_CC = "CC" TYPE_CU = "CU" TYPE_METROPOLE = "ME" code = models.CharField("Code SIREN", max_length=10, editable=False, unique=True) type = models.CharField("Type d'EPCI", max_length=2, choices=TypeEPCI.choices) nom = models.CharField("Nom de l'EPCI", max_length=300) population = models.PositiveIntegerField("Population", null=True) geometry = MultiPolygonField("Géométrie", geography=True, srid=4326, null=True, spatial_index=True) def __str__(self): return f"{self.nom} ({self.code})" def as_dict(self): """Sérialise l'instance (compatible JSON)""" return { "code": self.code, "nom": self.nom, "type": self.type, "population": self.population, "communes": {c.code: c.nom for c in self.communes.all()}, } class Meta: verbose_name = "EPCI" verbose_name_plural = "EPCI" ordering = ("code", "nom")
class RegionGIS(models.Model): wkb_geometry = MultiPolygonField(srid=4326, blank=True, null=True) region_name = models.CharField(max_length=200, blank=True, null=True) office = models.CharField(max_length=200, blank=True, null=True) object_id = models.PositiveIntegerField(blank=True, null=True) class Meta: ordering = [ 'object_id', ] app_label = 'wildlifecompliance' def __str__(self): return "{}: {}".format(self.id, self.region_name)
class TaskElement(models.Model): tasks = models.ManyToManyField(Task, related_name='task_element') title = models.CharField(max_length=200) building_nr = models.IntegerField(null=True, blank=True) info1 = models.CharField(max_length=200) info2 = models.CharField(max_length=200) info3 = models.CharField(max_length=200) element_name = models.CharField(max_length=200) element_geom = MultiPolygonField(null=True, blank=True) json = JSONField(blank=True) is_imported = models.BooleanField(default=False) def __str__(self): return self.element_name
def get_queryset(self): code_postal = get_object_or_404(CodePostal, code=self.kwargs["code"]) geom = code_postal.communes.aggregate(geom=Union( Cast("geometry", output_field=MultiPolygonField( geography=False))))["geom"] if geom: return queryset_elus_proches( self.request.user.person, geom).filter(commune__geometry__dwithin=( geom, D(km=5), )) return EluMunicipal.objects.none()
class Departement(TypeNomMixin, models.Model): code = models.CharField("Code INSEE", max_length=3, editable=False, unique=True) nom = models.CharField("Nom du département", max_length=200, editable=False) chef_lieu = models.ForeignKey( "Commune", verbose_name="Chef-lieu", on_delete=models.PROTECT, related_name="+", related_query_name="chef_lieu_de", ) region = models.ForeignKey( "Region", verbose_name="Région", on_delete=models.PROTECT, related_name="departements", related_query_name="departement", ) population = models.PositiveIntegerField("Population", null=True) geometry = MultiPolygonField("Géométrie", geography=True, srid=4326, null=True, spatial_index=True) def __str__(self): return f"{self.nom} ({self.code})" def as_dict(self): return { "code": self.code, "nom": self.nom, "population": self.population, "chefLieu": { "nom": self.chef_lieu.nom_complet, "code": self.chef_lieu.code, }, } class Meta: verbose_name = "Département" ordering = ("code", )
class Municipality(models.Model): name = models.CharField(max_length=50) province = models.ForeignKey('Province', related_name='municipality', on_delete=models.CASCADE, blank=True, null=True) district = models.ForeignKey('District', related_name='municipality', on_delete=models.CASCADE) hlcit_code = models.CharField(max_length=100, blank=True, null=True) boundary = MultiPolygonField(null=True, blank=True) def __str__(self): return self.name
class Taxlot(models.Model): class Meta: verbose_name = 'Taxlot' verbose_name_plural = 'Taxlots' app_label = 'landmapper' shape_leng = models.FloatField(null=True, blank=True) shape_area = models.FloatField(null=True, blank=True) geometry = MultiPolygonField( srid=3857, null=True, blank=True, verbose_name="Grid Cell Geometry" ) objects = GeoManager() @property def area_in_sq_miles(self): true_area = self.geometry.transform(2163, clone=True).area return sq_meters_to_sq_miles(true_area) @property def formatted_area(self): return int((self.area_in_sq_miles * 10) + .5) / 10. def serialize_attributes(self): attributes = [] attributes.append({'title': 'Area', 'data': '%.1f sq miles' % (self.area_in_sq_miles)}) # attributes.append({'title': 'Description', 'data': self.description}) return { 'event': 'click', 'attributes': attributes } # @classmethod # def fill_color(self): # return '776BAEFD' @classmethod def outline_color(self): return '776BAEFD' class Options: verbose_name = 'Taxlot' icon_url = None export_png = False manipulators = [] optional_manipulators = [] form = None form_template = None show_template = None
class TaskConflict(models.Model): tasks = models.ManyToManyField(Task, related_name='task_conflict') element = models.OneToOneField(TaskElement, related_name='conflict', on_delete=models.CASCADE) title = models.CharField(max_length=200, default='No title') building_nr = models.IntegerField(null=True, blank=True) info1 = models.CharField(max_length=200) info2 = models.CharField(max_length=200) info3 = models.CharField(max_length=200) conflict_name = models.CharField(max_length=200) conflict_geom = MultiPolygonField(null=True, blank=True) json = JSONField(blank=True) is_fixed = models.BooleanField(default=False) def __str__(self): return self.conflict_name
class CollectiviteDepartementale(TypeNomMixin, models.Model): TYPE_CONSEIL_DEPARTEMENTAL = "D" TYPE_CONSEIL_METROPOLE = "M" TYPE_CHOICES = ( (TYPE_CONSEIL_DEPARTEMENTAL, "Conseil départemental"), (TYPE_CONSEIL_METROPOLE, "Conseil de métropole"), ) code = models.CharField("Code INSEE", max_length=4, unique=True) type = models.CharField("Type de collectivité départementale", max_length=1, choices=TYPE_CHOICES) actif = models.BooleanField("En cours d'existence", default=True) nom = models.CharField("Nom", max_length=200) departement = models.ForeignKey( "Departement", on_delete=models.PROTECT, verbose_name="Circonscription administrative correspondante", ) population = models.PositiveIntegerField("Population", null=True) geometry = MultiPolygonField("Géométrie", geography=True, srid=4326, null=True) class Meta: verbose_name = "Collectivité à compétences départementales" verbose_name_plural = "Collectivités à compétences départementales" ordering = ("code", ) def __str__(self): return f"{self.nom} ({self.code})" def as_dict(self): return { "code": self.code, "nom": self.nom_complet, "population": self.population, "type": self.type, }
class CollectiviteDepartementale(TypeNomMixin, models.Model): TYPE_CONSEIL_DEPARTEMENTAL = "D" TYPE_STATUT_PARTICULIER = "S" TYPE_CHOICES = ( (TYPE_CONSEIL_DEPARTEMENTAL, "Conseil départemental"), (TYPE_STATUT_PARTICULIER, "Collectivité à statut particulier"), ) code = models.CharField("Code INSEE", max_length=4, unique=True) type = models.CharField("Type de collectivité départementale", max_length=1, choices=TYPE_CHOICES) actif = models.BooleanField("En cours d'existence", default=True) nom = models.CharField("Nom", max_length=200) region = models.ForeignKey("Region", on_delete=models.PROTECT, verbose_name="Région", null=True) population = models.PositiveIntegerField("Population", null=True) geometry = MultiPolygonField("Géométrie", geography=True, srid=4326, null=True, spatial_index=True) class Meta: verbose_name = "Collectivité à compétences départementales" verbose_name_plural = "Collectivités à compétences départementales" ordering = ("code", ) def __str__(self): return f"{self.nom} ({self.code})" def as_dict(self): return { "code": self.code, "nom": self.nom, "population": self.population, "type": self.type, }
class Region(Described): """ Regions are geographical boundaries which will come in 2 different levels: - Country - Province They will only know their boundaries data. They will also be MANAGED by certain staff members that can be assigned to them. Remarks in particular for each region. """ # Filtering data (by region). boundaries = MultiPolygonField(verbose_name=_('Boundaries')) # Managers. managers = models.ForeignKey('User', related_name='managed_%(class)s_records', blank=True, on_delete=models.PROTECT) class Meta: abstract = True
class CirconscriptionLegislative(models.Model): code = models.CharField( verbose_name="Numéro de la circonscription", max_length=10, blank=False, editable=False, null=False, ) departement = models.ForeignKey( Departement, null=True, on_delete=models.CASCADE, ) geometry = MultiPolygonField("Géométrie", geography=True, srid=4326, null=True) def __str__(self): code_dep, num = self.code.split("-") num = int(num) if num == 1: ordinal = "1ère" else: ordinal = f"{num}ème" if self.departement: return f"{ordinal} {self.departement.nom_avec_charniere}" elif code_dep == "99": return f"{ordinal} des Français de l'Étranger" elif code_dep == "977": return f"{ordinal} de Saint-Barthélémy et Saint-Martin" else: return f"{ordinal} {NOMS_COM[code_dep].nom_avec_charniere}" class Meta: verbose_name = "Circonscription législative" verbose_name_plural = "Circonscriptions législatives" ordering = ("code", )
class WorldBorder(Model): # Regular Django fields corresponding to the attributes in the # world borders shapefile. name = CharField(max_length=50) area = IntegerField() pop2005 = IntegerField('Population 2005') fips = CharField('FIPS Code', max_length=2) iso2 = CharField('2 Digit ISO', max_length=2) iso3 = CharField('3 Digit ISO', max_length=3) un = IntegerField('United Nations Code') region = IntegerField('Region Code') subregion = IntegerField('Sub-Region Code') lon = FloatField() lat = FloatField() # GeoDjango-specific: a geometry field (MultiPolygonField), and # overriding the default manager with a GeoManager instance. mpoly = MultiPolygonField() # Returns the string representation of the model. def __str__(self): # __unicode__ on Python 2 return self.name
class Region(BaseModel): name = models.CharField(max_length=50) slug = models.SlugField() geom = MultiPolygonField(srid=4326) @classmethod def get_for_point(cls, pt): return cls.objects.get(geom__contains=pt) def save(self, *args, **kwargs): if not self.slug or self.slug == "": self.slug = slugify(self.name) super().save(*args, **kwargs) def get_absolute_url(self): return reverse("region-detail", kwargs={"slug": self.slug}) def __str__(self): return self.name class Meta: ordering = ("name",)
class Region(TypeNomMixin, models.Model): code = models.CharField("Code INSEE", max_length=3, editable=False, unique=True) nom = models.CharField("Nom de la région", max_length=200, editable=False) chef_lieu = models.ForeignKey( "Commune", verbose_name="Chef-lieu", on_delete=models.PROTECT, related_name="+", related_query_name="chef_lieu_de", ) population = models.PositiveIntegerField("Population", null=True) geometry = MultiPolygonField("Géométrie", geography=True, srid=4326, null=True) def __str__(self): return self.nom def as_dict(self): return { "code": self.code, "nom": self.nom, "population": self.population, "chefLieu": { "nom": self.chef_lieu.nom_complet, "code": self.chef_lieu.code, }, } class Meta: verbose_name = "Région" ordering = ("nom", ) # personne ne connait les codes de région
class Region(RegionInterface, models.Model): title = models.CharField(max_length=128) polygon = MultiPolygonField(geography=True) parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True) modified = models.DateTimeField(auto_now=True) wikidata_id = ExternalIdField(max_length=20, link='https://www.wikidata.org/wiki/{id}', null=True) osm_id = models.PositiveIntegerField(unique=True) osm_data = JSONField(default=dict) is_enabled = models.BooleanField(default=True) objects = RegionManager() class Meta: verbose_name = 'Region' verbose_name_plural = 'Regions' def __str__(self): return '{} ({})'.format(self.title, self.id) def __init__(self, *args, **kwargs): super(Region, self).__init__(*args, **kwargs) @property @cacheable def polygon_bounds(self) -> List: return self.polygon.extent @property def _strip_polygon(self): precision = 0.01 + 0.004 * (self.polygon.area / 10.0) return self.polygon.simplify(precision, preserve_topology=True) @property @cacheable def polygon_strip(self) -> List: simplify = self._strip_polygon return encode_geometry(simplify, min_points=10) @property @cacheable def polygon_gmap(self) -> List: precision = 0.005 + 0.001 * (self.polygon.area / 100.0) simplify = self.polygon.simplify(precision, preserve_topology=True) return encode_geometry(simplify) @property @cacheable def polygon_center(self) -> List: # http://lists.osgeo.org/pipermail/postgis-users/2007-February/014612.html def calc_polygon(strip, force): def calc_part(result, subpolygon): for point in subpolygon.coords[0]: result['count'] += 1 result['lat'] += point[0] result['lng'] += point[1] return result result = {'lat': 0.0, 'lng': 0.0, 'count': 0} if isinstance(strip, MultiPolygon): for part in strip: if force or part.num_points > 10: result = calc_part(result, part) else: result = calc_part(result, strip) return result result = calc_polygon(self._strip_polygon, force=False) if result['count'] == 0: result = calc_polygon(self._strip_polygon, force=True) return [ result['lat'] / result['count'], result['lng'] / result['count'] ] def infobox_status(self, lang: str) -> Dict: fields = ('name', 'wiki', 'capital', 'coat_of_arms', 'flag') trans = self.load_translation(lang) result = {field: field in trans.infobox for field in fields} result['capital'] = result.get('capital') and isinstance( trans.infobox['capital'], dict) return result @property @cacheable def polygon_infobox(self) -> Dict: def get_marker(infobox): by_capital = infobox.get('capital', {}) if 'lat' in by_capital and 'lon' in by_capital: return {'lat': by_capital['lat'], 'lon': by_capital['lon']} else: center = self.polygon_center return {'lat': center[1], 'lon': center[0]} result = {} for trans in self.translations.all(): infobox = trans.infobox infobox.pop('geonamesID', None) if isinstance(infobox.get('capital'), dict): del (infobox['capital']['id']) infobox['marker'] = get_marker(infobox) result[trans.language_code] = infobox return result @classmethod def caches(cls) -> List[str]: result = [] for name in dir(cls): method = getattr(cls, name) if isinstance( method, property) and method.fget.__name__ == 'cache_wrapper': result.append(name) return result def json(self, lang: str) -> Dict: translation = self.load_translation(lang) result = { 'id': str(self.id), 'name': translation.name, 'items': self.items(lang) } result['items_exists'] = len(result['items']) > 0 return result def items(self, lang: str) -> List[Dict]: child_query = Region.objects.filter(parent_id=OuterRef('pk')) return [{ 'id': str(x.id), 'name': x.lang, 'items_exists': x.items_exists } for x in self.region_set.filter( translations__language_code=lang).annotate( items_exists=Exists(child_query), lang=F( 'translations__name')).order_by('lang').distinct()] def fetch_polygon(self) -> None: def content(): cache = os.path.join(settings.GEOJSON_DIR, '{}.geojson'.format(self.osm_id)) if not os.path.exists(cache): url = settings.OSM_URL.format(id=self.osm_id, key=settings.OSM_KEY, level=self.osm_data['level']) fetch_logger.info(f'Download from {url}') response = requests.get(url) if response.status_code != 200: raise Exception('Bad request') zipfile = ZipFile(BytesIO(response.content)) zip_names = zipfile.namelist() if len(zip_names) != 1: raise Exception('Too many geometries') filename = zip_names.pop() with open(cache, 'wb') as c: c.write(zipfile.open(filename).read()) with open(cache, 'r') as c: return json.loads(c.read()) def update_self(properties, geometry, type): def extract_data(properties): result = {'level': int(properties['admin_level'])} fields = ['boundary', 'ISO3166-1:alpha3', 'timezone'] for field in fields: result[field] = properties['alltags'].get(field, None) return result self.title = properties['name'] self.polygon = GEOSGeometry(json.dumps(geometry)) self.wikidata_id = properties.get('wikidata') self.osm_id = properties['id'] self.osm_data = extract_data(properties) fetch_logger.info(f'Update polygon: {self.id}') feature = content()['features'][0] update_self(**feature) self.save() for lang in settings.ALLOWED_LANGUAGES: trans = self.load_translation(lang) trans.master = self trans.name = self.title trans.save() def fetch_items_list(self) -> Dict: params = { 'caller': 'boundaries-4.3.14', 'database': 'planet3', 'parent': self.osm_id } headers = { 'Referer': 'https://wambachers-osm.website/boundaries/', 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36', 'X-Requested-With': 'XMLHttpRequest', 'Cookie': 'JSESSIONID=node01ln7zb8ljtnbacctakke8yb6s49.node0; osm_boundaries_map=1%7C40.65536999999962%7C45.25161549584642%7C10%7C0BT%7Copen; osm_boundaries_base=4%7Cfalse%7Cjson%7Czip%7Cnull%7Cfalse%7Clevels%7Cland%7C4.3%7Ctrue%7C3', } fetch_logger.info(f'Get items: {self.id}') response = requests.post( 'https://wambachers-osm.website/boundaries/getJsTree6', params=params, headers=headers) items = response.json() fetch_logger.info(f'Count items for {self.id}: {len(items)}') return items def fetch_items(self) -> None: for item in self.fetch_items_list(): if item['data']['admin_level'] >= 8: continue region, _ = Region.objects.get_or_create( osm_id=item['id'], defaults={ 'parent': self, 'osm_data': { 'level': item['data']['admin_level'] } }) region.fetch_polygon() region.fetch_infobox() def fetch_infobox(self) -> None: fetch_logger.info(f'Get infobox: {self.wikidata_id}') if self.wikidata_id is None or self.wikidata_id == '': rows = {lang: {} for lang in settings.ALLOWED_LANGUAGES} else: wikidata_id = None if self.parent is None else self.parent.wikidata_id rows = query_by_wikidata_id(country_id=wikidata_id, item_id=self.wikidata_id) for lang, infobox in rows.items(): fetch_logger.info(f'Update infobox: {lang}') if 'name' not in infobox: infobox['name'] = self.title trans = self.load_translation(lang) trans.master = self trans.infobox = infobox trans.name = infobox['name'] trans.save() @property def translation(self): return self.load_translation(get_language()) def load_translation(self, lang): result = self.translations.filter(language_code=lang).first() if result is None: result = RegionTranslation.objects.create(language_code=lang, master=self, name='(empty)') return result
class Segment(BaseSegment, models.Model): GA_STATUS_NOT_MEMBER = "N" GA_STATUS_MEMBER = "m" GA_STATUS_MANAGER = "M" GA_STATUS_REFERENT = "R" GA_STATUS_CHOICES = ( (GA_STATUS_NOT_MEMBER, "Non membres de GA"), (GA_STATUS_MEMBER, "Membres de GA"), (GA_STATUS_MANAGER, "Animateur·ices et gestionnaires de GA"), (GA_STATUS_REFERENT, "Animateur·ices de GA"), ) name = models.CharField("Nom", max_length=255) tags = models.ManyToManyField("people.PersonTag", blank=True) is_2022 = models.BooleanField("Inscrits NSP", null=True, blank=True) is_insoumise = models.BooleanField("Inscrits LFI", null=True, blank=True, default=True) newsletters = ChoiceArrayField( models.CharField(choices=Person.NEWSLETTERS_CHOICES, max_length=255), default=default_newsletters, help_text="Inclure les personnes abonnées aux newsletters suivantes.", blank=True, ) supportgroup_status = models.CharField( "Limiter aux membres de groupes ayant ce statut", max_length=1, choices=GA_STATUS_CHOICES, blank=True, ) supportgroup_subtypes = models.ManyToManyField( "groups.SupportGroupSubtype", verbose_name="Limiter aux membres de groupes d'un de ces sous-types", blank=True, ) events = models.ManyToManyField( "events.Event", verbose_name="Limiter aux participant⋅e⋅s à un des événements", blank=True, ) events_subtypes = models.ManyToManyField( "events.EventSubtype", verbose_name="Limiter aux participant⋅e⋅s à un événements de ce type", blank=True, ) events_start_date = models.DateTimeField( "Limiter aux participant⋅e⋅s à des événements commençant après cette date", blank=True, null=True, ) events_end_date = models.DateTimeField( "Limiter aux participant⋅e⋅s à des événements terminant avant cette date", blank=True, null=True, ) events_organizer = models.BooleanField( "Limiter aux organisateurices (sans effet si pas d'autres filtres événements)", blank=True, default=False, ) draw_status = models.BooleanField( "Limiter aux gens dont l'inscription au tirage au sort est", null=True, blank=True, default=None, ) forms = models.ManyToManyField( "people.PersonForm", verbose_name="A répondu à au moins un de ces formulaires", blank=True, related_name="+", ) polls = models.ManyToManyField( "polls.Poll", verbose_name="A participé à au moins une de ces consultations", blank=True, related_name="+", ) countries = CountryField("Limiter aux pays", multiple=True, blank=True) departements = ChoiceArrayField( models.CharField(choices=data.departements_choices, max_length=3), verbose_name= "Limiter aux départements (calcul à partir du code postal)", default=list, blank=True, ) area = MultiPolygonField("Limiter à un territoire définit manuellement", blank=True, null=True) campaigns = models.ManyToManyField( "nuntius.Campaign", related_name="+", verbose_name="Limiter aux personnes ayant reçu une des campagnes", blank=True, ) last_open = models.IntegerField( "Limiter aux personnes ayant ouvert un email envoyé au court de derniers jours", help_text="Indiquer le nombre de jours", blank=True, null=True, ) last_click = models.IntegerField( "Limiter aux personnes ayant cliqué dans un email envoyé au court des derniers jours", help_text="Indiquer le nombre de jours", blank=True, null=True, ) FEEDBACK_OPEN = 1 FEEDBACK_CLICKED = 2 FEEDBACK_NOT_OPEN = 3 FEEDBACK_NOT_CLICKED = 4 FEEDBACK_OPEN_NOT_CLICKED = 5 FEEDBACK_CHOICES = ( (FEEDBACK_OPEN, "Personnes ayant ouvert"), (FEEDBACK_CLICKED, "Personnes ayant cliqué"), (FEEDBACK_NOT_OPEN, "Personnes n'ayant pas ouvert"), (FEEDBACK_NOT_CLICKED, "Personnes n'ayant pas cliqué"), (FEEDBACK_OPEN_NOT_CLICKED, "Personnes ayant ouvert mais pas cliqué"), ) campaigns_feedback = models.PositiveSmallIntegerField( "Limiter en fonction de la réaction à ces campagnes", blank=True, null=True, choices=FEEDBACK_CHOICES, help_text= "Aucun effet si aucune campagne n'est sélectionnée dans le champ précédent", ) registration_date = models.DateTimeField( "Limiter aux membres inscrit⋅e⋅s après cette date", blank=True, null=True) registration_duration = models.IntegerField( "Limiter aux membres inscrit⋅e⋅s depuis au moins un certain nombre d'heures", help_text="Indiquer le nombre d'heures", blank=True, null=True, ) last_login = models.DateTimeField( "Limiter aux membres s'étant connecté⋅e pour la dernière fois après cette date", blank=True, null=True, ) gender = models.CharField("Genre", max_length=1, blank=True, choices=Person.GENDER_CHOICES) born_after = models.DateField("Personnes nées après le", blank=True, null=True, help_text=DATE_HELP_TEXT) born_before = models.DateField("Personnes nées avant le", blank=True, null=True, help_text=DATE_HELP_TEXT) donation_after = models.DateField( "A fait au moins un don (don mensuel inclus) après le", blank=True, null=True, help_text=DATE_HELP_TEXT, ) donation_not_after = models.DateField( "N'a pas fait de don (don mensuel inclus) depuis le", blank=True, null=True, help_text=DATE_HELP_TEXT, ) donation_total_min = AmountField( "Montant total des dons supérieur ou égal à", blank=True, null=True) donation_total_max = AmountField( "Montant total des dons inférieur ou égal à", blank=True, null=True) donation_total_range = DateRangeField( "Pour le filtre de montant total, prendre uniquement en compte les dons entre ces deux dates", blank=True, null=True, help_text= "Écrire sous la forme JJ/MM/AAAA. La date de début est incluse, pas la date de fin.", ) subscription = models.BooleanField("A une souscription mensuelle active", blank=True, null=True) ELUS_NON = "N" ELUS_MEMBRE_RESEAU = "M" ELUS_REFERENCE = "R" ELUS_CHOICES = ( ("", "Peu importe"), (ELUS_MEMBRE_RESEAU, "Uniquement les membres du réseau des élus"), ( ELUS_REFERENCE, "Tous les élus, membres ou non du réseau, sauf les exclus du réseau", ), ) elu = models.CharField("Est un élu", max_length=1, choices=ELUS_CHOICES, blank=True) elu_municipal = models.BooleanField("Avec un mandat municipal", default=True) elu_departemental = models.BooleanField("Avec un mandat départemental", default=True) elu_regional = models.BooleanField("Avec un mandat régional", default=True) elu_consulaire = models.BooleanField("Avec un mandat consulaire", default=True) exclude_segments = models.ManyToManyField( "self", symmetrical=False, related_name="+", verbose_name="Exclure les personnes membres des segments suivants", blank=True, ) add_segments = models.ManyToManyField( "self", symmetrical=False, related_name="+", verbose_name="Ajouter les personnes membres des segments suivants", blank=True, ) def get_subscribers_q(self): # ne pas inclure les rôles inactifs dans les envois de mail q = ~Q(role__is_active=False) # permettre de créer des segments capables d'inclure des personnes inscrites à aucune des newsletters if self.newsletters: q &= Q(newsletters__overlap=self.newsletters) if self.is_insoumise is not None: q = q & Q(is_insoumise=self.is_insoumise) if self.is_2022 is not None: q = q & Q(is_2022=self.is_2022) if self.tags.all().count() > 0: q = q & Q(tags__in=self.tags.all()) if self.supportgroup_status: if self.supportgroup_status == self.GA_STATUS_NOT_MEMBER: supportgroup_q = ~Q(memberships__supportgroup__published=True) elif self.supportgroup_status == self.GA_STATUS_MEMBER: supportgroup_q = Q(memberships__supportgroup__published=True) elif self.supportgroup_status == self.GA_STATUS_REFERENT: supportgroup_q = Q( memberships__supportgroup__published=True, memberships__membership_type__gte=Membership. MEMBERSHIP_TYPE_REFERENT, ) else: # ==> self.supportgroup_status == self.GA_STATUS_MANAGER supportgroup_q = Q( memberships__supportgroup__published=True, memberships__membership_type__gte=Membership. MEMBERSHIP_TYPE_MANAGER, ) if self.supportgroup_subtypes.all().count() > 0: supportgroup_q = supportgroup_q & Q( memberships__supportgroup__subtypes__in=self. supportgroup_subtypes.all()) q = q & supportgroup_q events_filter = {} if self.events.all().count() > 0: events_filter["in"] = self.events.all() if self.events_subtypes.all().count() > 0: events_filter["subtype__in"] = self.events_subtypes.all() if self.events_start_date is not None: events_filter["start_time__gt"] = self.events_start_date if self.events_end_date is not None: events_filter["end_time__lt"] = self.events_end_date if events_filter: prefix = "organized_events" if self.events_organizer else "rsvps__event" q = q & Q( **{f"{prefix}__{k}": v for k, v in events_filter.items()}) if not self.events_organizer: q = q & Q(rsvps__status__in=[ RSVP.STATUS_CONFIRMED, RSVP.STATUS_AWAITING_PAYMENT, ]) if self.draw_status is not None: q = q & Q(draw_participation=self.draw_status) if self.forms.all().count() > 0: q = q & Q(form_submissions__form__in=self.forms.all()) if self.polls.all().count() > 0: q = q & Q(poll_choices__poll__in=self.polls.all()) if self.campaigns.all().count() > 0: if self.campaigns_feedback == self.FEEDBACK_OPEN: campaign__kwargs = {"campaignsentevent__open_count__gt": 0} elif self.campaigns_feedback == self.FEEDBACK_CLICKED: campaign__kwargs = {"campaignsentevent__click_count__gt": 0} elif self.campaigns_feedback == self.FEEDBACK_NOT_OPEN: campaign__kwargs = {"campaignsentevent__open_count": 0} elif self.campaigns_feedback == self.FEEDBACK_NOT_CLICKED: campaign__kwargs = {"campaignsentevent__click_count": 0} elif self.campaigns_feedback == self.FEEDBACK_OPEN_NOT_CLICKED: campaign__kwargs = { "campaignsentevent__open_count__gt": 1, "campaignsentevent__click_count": 0, } else: campaign__kwargs = {} q = q & Q( campaignsentevent__result__in=[ CampaignSentStatusType.UNKNOWN, CampaignSentStatusType.OK, ], campaignsentevent__campaign__in=self.campaigns.all(), **campaign__kwargs, ) if self.last_open is not None: q = q & Q( campaignsentevent__open_count__gt=0, campaignsentevent__datetime__gt=now() - timedelta(days=self.last_open), ) if self.last_click is not None: q = q & Q( campaignsentevent__click_count__gt=0, campaignsentevent__datetime__gt=now() - timedelta(days=self.last_click), ) if len(self.countries) > 0: q = q & Q(location_country__in=self.countries) if len(self.departements) > 0: q = q & Q(data.filtre_departements(*self.departements)) if self.area is not None: q = q & Q(coordinates__intersects=self.area) if self.registration_date is not None: q = q & Q(created__gt=self.registration_date) if self.registration_duration: q = q & Q(created__lt=now() - timedelta(hours=self.registration_duration)) if self.last_login is not None: q = q & Q(role__last_login__gt=self.last_login) if self.gender: q = q & Q(gender=self.gender) if self.born_after is not None: q = q & Q(date_of_birth__gt=self.born_after) if self.born_before is not None: q = q & Q(date_of_birth__lt=self.born_before) if self.donation_after is not None: q = q & Q(payments__created__gt=self.donation_after, **DONATION_FILTER) if self.donation_not_after is not None: q = q & ~Q(payments__created__gt=self.donation_not_after, **DONATION_FILTER) if self.donation_total_min or self.donation_total_max: donation_range = ( { "payments__created__gt": self.donation_total_range.lower, "payments__created__lt": self.donation_total_range.upper, } if self.donation_total_range else {}) annotated_qs = Person.objects.annotate(donation_total=Sum( "payments__price", filter=Q(**DONATION_FILTER, **donation_range))) if self.donation_total_min: annotated_qs = annotated_qs.filter( donation_total__gte=self.donation_total_min) if self.donation_total_max: annotated_qs = annotated_qs.filter( donation_total__lte=self.donation_total_max) q = q & Q(id__in=annotated_qs.values_list("id")) if self.subscription is not None: if self.subscription: q = q & Q(subscriptions__status=Subscription.STATUS_ACTIVE) else: q = q & ~Q(subscriptions__status=Subscription.STATUS_ACTIVE) if self.elu: if self.elu == Segment.ELUS_MEMBRE_RESEAU: q &= Q(membre_reseau_elus=Person.MEMBRE_RESEAU_OUI) elif self.elu == Segment.ELUS_REFERENCE: q &= ~Q(membre_reseau_elus=Person.MEMBRE_RESEAU_EXCLUS) q_mandats = Q() for t in [ "elu_municipal", "elu_departemental", "elu_regional", "elu_consulaire", ]: if getattr(self, t): q_mandats |= Q(**{t: True}) q &= q_mandats return q def _get_own_filters_queryset(self): qs = Person.objects.all() if self.elu: qs = qs.annotate_elus() return qs.filter( self.get_subscribers_q()).filter(emails___bounced=False) def get_subscribers_queryset(self): qs = self._get_own_filters_queryset() for s in self.add_segments.all(): qs = Person.objects.filter( Q(pk__in=qs) | Q(pk__in=s.get_subscribers_queryset())) for s in self.exclude_segments.all(): qs = qs.exclude(pk__in=s.get_subscribers_queryset()) return qs.order_by("id").distinct("id") def get_subscribers_count(self): return (self._get_own_filters_queryset().order_by("id").distinct( "id").count() + sum(s.get_subscribers_count() for s in self.add_segments.all()) - sum(s.get_subscribers_count() for s in self.exclude_segments.all())) def is_subscriber(self, person): return self.get_subscribers_queryset().filter(pk=person.pk).exists() get_subscribers_count.short_description = "Personnes" get_subscribers_count.help_text = "Estimation du nombre d'inscrits" def __str__(self): return self.name