class Grootstedelijkgebied(models.Model): """ model for data from shp files layer.fields: ['NAAM'] """ id = models.SlugField(max_length=100, primary_key=True) naam = models.CharField(max_length=100) gsg_type = models.CharField(max_length=5, null=True) geometrie = geo.MultiPolygonField(null=True, srid=28992) date_modified = models.DateTimeField(auto_now=True) objects = geo.Manager() class Meta: verbose_name = "Grootstedelijkgebied" verbose_name_plural = "Grootstedelijke gebieden" def __str__(self): return "{}".format(self.naam)
class MapFeature(models.Model): unique_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) accuracy = models.FloatField(default=0) altitude = models.FloatField(default=0) direction = models.FloatField(default=0) first_seen_at = models.DateTimeField(null=True, blank=True) mf_key = models.CharField(max_length=100, null=True, blank=True) last_seen_at = models.DateTimeField(null=True, blank=True) layer = models.CharField(max_length=50) value = models.CharField(max_length=100) geometry_type = models.CharField(max_length=50, default='Point') geometry_point = models.PointField(null=True, blank=True) detection_keys = ArrayField(ArrayField(models.CharField(max_length=50)), null=True, blank=True) image_keys = ArrayField(ArrayField(models.CharField(max_length=50)), null=True, blank=True) user_keys = ArrayField(ArrayField(models.CharField(max_length=50)), null=True, blank=True) objects = models.Manager() vector_tiles = CustomMapFeatureMVTManager( geo_col='geometry_point', select_columns=['mf_key', 'unique_id'], is_show_id=False, source_layer='mtp-map-features' )
class Category(models.Model): "Model definition of a Category." guid = models.UUIDField(_('GUID'), primary_key=False, null=False, default=uuid_lib.uuid4, editable=False) name = models.CharField(_('Name of Category'), max_length=255, help_text=_('Enter Category name.')) image = models.ImageField( _('Image file'), null=True, blank=True, upload_to=os.path.join(MEDIA_ROOT, 'images/trail_category'), help_text=_( 'An image of the trail section. ' 'Most browsers support dragging the image directly on to the ' '"Choose File" button above.')) slug = models.SlugField(null=True, blank=True) objects = models.Manager() class Meta: ordering = ['name'] app_label = 'trail' verbose_name_plural = 'Trail Categories' def _str__(self): return self.__unicode__() def __unicode__(self): return '%s' % (self.name)
class Feed(models.Model): title = models.TextField() unit = models.CharField(max_length=10, null=True, blank=True) rss_url = models.URLField() slug = models.SlugField() last_modified = models.DateTimeField(null=True, blank=True) # this one is in UTC ptype = models.CharField(max_length=1, choices=FEED_TYPE_CHOICES) provider = models.CharField(max_length=128, choices=PROVIDER_CHOICES) def _set_importer_params(self, value): self._importer_params = simplejson.dumps(value) def _get_importer_params(self): return simplejson.loads(self._importer_params) importer_params = property(_get_importer_params, _set_importer_params) objects = models.Manager() events = EventsManager() news = NewsManager() tags = models.ManyToManyField(Tag, blank=True) def __unicode__(self): return self.title def get_absolute_url(self): if self.ptype == 'n': return reverse('news:item-list', args=[self.slug]) else: return reverse('events:item-list', args=[self.slug]) class Meta: ordering = ('title', )
class PointBased(gis_models.Model): id = gis_models.AutoField(primary_key=True, editable=False) name = gis_models.CharField(max_length=200) type = gis_models.CharField(max_length=200, null=True, blank=True) subnational = gis_models.ForeignKey(SubNational, null=True, blank=True, on_delete=gis_models.SET_NULL) center_longlat = gis_models.PointField(null=True, blank=True) comment = gis_models.TextField() data_source = gis_models.CharField(max_length=100, null=True, blank=True) objects = gis_models.Manager() geolocation = GenericRelation(Geolocation, related_query_name='point_based') @property def is_capital(self): return hasattr(self, 'capital_of') def __unicode__(self): return self.name class Meta: verbose_name_plural = "cities"
class SubNational(gis_models.Model): id = gis_models.AutoField(primary_key=True, editable=False) name = gis_models.CharField(unique=True, max_length=100) iso_3166_2 = gis_models.CharField(null=True, blank=True, max_length=2) code_local = gis_models.CharField(null=True, blank=True, max_length=100) postcode = gis_models.CharField(null=True, blank=True, max_length=100) country = gis_models.ForeignKey(Country, null=True, blank=True, on_delete=gis_models.SET_NULL) center_longlat = gis_models.PointField(null=True, blank=True) polygons = gis_models.GeometryField(null=True, blank=True) wikipedia = gis_models.CharField(null=True, blank=True, max_length=150) language = gis_models.CharField(max_length=2, null=True) data_source = gis_models.CharField(max_length=100, null=True, blank=True) objects = gis_models.Manager() geolocation = GenericRelation(Geolocation, related_query_name='sub_national') def __unicode__(self): return self.name class Meta: verbose_name_plural = "admin1 regions"
class GebiedsgerichtwerkenPraktijkgebieden(models.Model): """ model for data from shp files layer.fields: ['NAAM'] """ naam = models.CharField(max_length=100, unique=True) date_modified = models.DateTimeField(auto_now=True) geometrie = geo.MultiPolygonField(null=True, srid=28992) objects = geo.Manager() class Meta: verbose_name = "Gebiedsgerichtwerken praktijkgebieden" verbose_name_plural = "Gebiedsgerichtwerken praktijkgebieden" ordering = ('naam', ) def __str__(self): return "{}".format(self.naam)
class ExportFormat(TimeStampedModelMixin): """ Model for a ExportFormat. """ id = models.AutoField(primary_key=True, editable=False) uid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False, db_index=True) name = models.CharField(max_length=100) slug = LowerCaseCharField(max_length=20, unique=True, default='') description = models.CharField(max_length=255) cmd = models.TextField(max_length=1000) objects = models.Manager() class Meta: # pragma: no cover managed = True db_table = 'export_formats' def __str__(self): return '{0}'.format(self.name) def __unicode__(self, ): return '{0}'.format(self.slug)
class Feed(models.Model): title = models.TextField(help_text=_("Feed title")) entity = models.ForeignKey(Entity, null=True, blank=True, help_text=_('Place which this feed belongs to')) rss_url = models.URLField(help_text=_("URL of feed"), verbose_name=_("URL")) slug = models.SlugField(help_text=_("Slug of feed, e.g. oucs-news")) # this one is in UTC last_modified = models.DateTimeField(null=True, blank=True) language = models.CharField(max_length=10, choices=settings.LANGUAGES, null=True) # Provider type ptype = models.CharField(max_length=1, choices=FEED_TYPE_CHOICES) provider = models.CharField(max_length=128, choices=PROVIDER_CHOICES) objects = models.Manager() events = EventsManager() news = NewsManager() tags = models.ManyToManyField(Tag, blank=True) def __unicode__(self): return self.title def get_absolute_url(self): if self.ptype == 'n': return reverse('news:item-list', args=[self.slug]) else: return reverse('events:item-list', args=[self.slug]) class Meta: ordering = ('title', )
class Hostels(models.Model): location = models.PointField(null=True) objects = models.Manager() address = models.CharField(max_length=100, null=True) hostel_name = models.CharField(max_length=50, null=True) hostel_room_size = models.IntegerField(null=True) hostel_floor_no = models.IntegerField(null=True) hostel_room_type = models.BooleanField(default=True) hostel_attached_bathroom = models.BooleanField(default=True) hostel_mess_facility = models.BooleanField(default=True) hostel_other_facilities1 = models.BooleanField(default=True) hostel_other_facilities2 = models.BooleanField(default=True) hostel_other_facilities3 = models.BooleanField(default=True) hostel_other_facilities4 = models.BooleanField(default=True) hostel_owner_name = models.CharField(max_length=100, null=True) hostel_owner_number = models.PositiveIntegerField( null=True, default=0, validators=[MaxValueValidator(9999999999)]) hostel_owner_mail = models.CharField(max_length=100, null=True) hostel_price = models.DecimalField(null=True, max_digits=10, decimal_places=2, default=Decimal('0.00')) hostel_description = models.CharField(max_length=300, null=True) images1 = models.ImageField(upload_to="images", null=True) images2 = models.ImageField(upload_to="images", null=True) images3 = models.ImageField(upload_to="images", null=True) rating = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00')) comments_count = models.IntegerField(default=0) def __unicode__(self): return self.name class Meta: verbose_name_plural = "Hostels"
class Country(models.Model): code = models.CharField(max_length=2, primary_key=True) name = models.CharField(max_length=200, unique=True, db_index=True) languages = models.ManyToManyField(Language, related_name="countries") currency = models.ForeignKey(Currency, related_name="countries") # is the website available in this country? available = models.BooleanField(default=False) deleted = models.BooleanField(default=False) objects = ActiveCountryManager() objects_deleted_inc = models.Manager() class Meta: ordering = ['name'] def __unicode__(self): return self.name def search_locality(self, locality_name): if len(locality_name) == 0: return [] q = Q(country_id=self.code) q &= (Q(name__iexact=locality_name) | Q(alternatenames__name__iexact=locality_name)) return Locality.objects.filter(q).distinct()
class Map(NamedModel): """ Workspace """ ANONYMOUS = 1 EDITORS = 2 OWNER = 3 PUBLIC = 1 OPEN = 2 PRIVATE = 3 EDIT_STATUS = ( (ANONYMOUS, _('Everyone can edit')), (EDITORS, _('Only editors can edit')), (OWNER, _('Only owner can edit')), ) SHARE_STATUS = ( (PUBLIC, _('Public: everyone')), (OPEN, _('Hidden: anyone with link')), (PRIVATE, _('Private: editors only')), ) slug = models.SlugField(db_index=True) description = models.TextField(blank=True, null=True, verbose_name=_("description")) center = models.PointField(geography=True, verbose_name=_("center")) zoom = models.IntegerField(default=7, verbose_name=_("zoom")) epsg = models.TextField(default=settings.EPSG, blank=True, null=True, verbose_name=_("EPSG code")) proj = models.TextField(default=settings.PROJ, blank=True, null=True, verbose_name=_("Proj")) resolutions = models.TextField(default=settings.RESOLUTIONS, blank=True, null=True, verbose_name=_("Proj")) origin = models.TextField(default=settings.ORIGIN, blank=True, null=True, verbose_name=_("Proj")) locate = models.BooleanField(default=False, verbose_name=_("locate"), help_text=_("Locate user on load?")) licence = models.ForeignKey(Licence, help_text=_("Choose the map licence."), verbose_name=_('licence'), on_delete=models.SET_DEFAULT, default=get_default_licence) modified_at = models.DateTimeField(auto_now=True) owner = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name="owned_maps", verbose_name=_("owner"), on_delete=models.PROTECT) editors = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, verbose_name=_("editors")) edit_status = models.SmallIntegerField(choices=EDIT_STATUS, default=EDITORS, verbose_name=_("edit status")) share_status = models.SmallIntegerField(choices=SHARE_STATUS, default=PUBLIC, verbose_name=_("share status")) settings = DictField(blank=True, null=True, verbose_name=_("settings")) objects = models.Manager() public = PublicManager() def get_absolute_url(self): return reverse("map", kwargs={ 'slug': self.slug or "map", 'pk': self.pk }) def get_anonymous_edit_url(self): signer = Signer() signature = signer.sign(self.pk) return reverse('map_anonymous_edit_url', kwargs={'signature': signature}) def is_anonymous_owner(self, request): if self.owner: # edit cookies are only valid while map hasn't owner return False key, value = self.signed_cookie_elements try: has_anonymous_cookie = int(request.get_signed_cookie( key, False)) == value except ValueError: has_anonymous_cookie = False return has_anonymous_cookie def can_edit(self, user=None, request=None): """ Define if a user can edit or not the instance, according to his account or the request. """ can = False if request and not self.owner: if (getattr(settings, "UMAP_ALLOW_ANONYMOUS", False) and self.is_anonymous_owner(request)): can = True if self.edit_status == self.ANONYMOUS: can = True elif not user.is_authenticated: pass elif user == self.owner: can = True elif self.edit_status == self.EDITORS and user in self.editors.all(): can = True return can def can_view(self, request): if self.owner is None: can = True elif self.share_status in [self.PUBLIC, self.OPEN]: can = True elif request.user == self.owner: can = True else: can = not (self.share_status == self.PRIVATE and request.user not in self.editors.all()) return can @property def signed_cookie_elements(self): return ('anonymous_owner|%s' % self.pk, self.pk) def get_tilelayer(self): return self.tilelayer or TileLayer.get_default() def clone(self, **kwargs): new = self.__class__.objects.get(pk=self.pk) new.pk = None new.name = u"%s %s" % (_("Clone of"), self.name) if "owner" in kwargs: # can be None in case of anonymous cloning new.owner = kwargs["owner"] new.save() for editor in self.editors.all(): new.editors.add(editor) for datalayer in self.datalayer_set.all(): datalayer.clone(map_inst=new) return new class Meta: verbose_name = 'Workspace' verbose_name_plural = 'Workspaces'
class Questionnaire(models.Model): """ The model representing a Questionnaire instance. This is the common denominator for all version (:class:`QuestionnaireVersion`) of a Questionnaire. """ data = JSONField() created = models.DateTimeField() updated = models.DateTimeField() uuid = models.CharField(max_length=64, default=uuid4) code = models.CharField(max_length=64, default='') geom = models.GeometryField(null=True, blank=True) is_deleted = models.BooleanField(default=False) status = models.IntegerField(choices=STATUSES) version = models.IntegerField() members = models.ManyToManyField(settings.AUTH_USER_MODEL, through='QuestionnaireMembership') configuration = models.ForeignKey('configuration.Configuration', on_delete=models.PROTECT) links = models.ManyToManyField('self', through='QuestionnaireLink', symmetrical=False, related_name='linked_to+') flags = models.ManyToManyField('Flag') objects = models.Manager() with_status = StatusQuerySet.as_manager() class Meta: ordering = ['-updated'] permissions = ( ("review_questionnaire", "Can review questionnaire"), ("publish_questionnaire", "Can publish questionnaire"), ("assign_questionnaire", "Can assign questionnaire (for review/publish)"), ("view_questionnaire", "Can view questionnaire"), ("edit_questionnaire", "Can edit questionnaire"), ("change_compiler", "Can change compiler of questionnaire"), ("flag_unccd_questionnaire", "Can flag UNCCD questionnaire"), ("unflag_unccd_questionnaire", "Can unflag UNCCD questionnaire"), ) unique_together = (('code', 'version'), ) def _get_url_from_configured_app(self, url_name: str) -> str: """ Try to resolve the proper code for the object, using it as namespace. If some day, the configurations code is not the exact same string as the application name, a 'mapping' dict is required. """ with contextlib.suppress(NoReverseMatch): return reverse('{app_name}:{url_name}'.format( app_name=self.configuration.code, url_name=url_name), kwargs={'identifier': self.code}) return None def get_absolute_url(self): """ Detail view url of the questionnaire. Important: don't use type hints as djangorestframework as of now throws errors (https://github.com/tomchristie/django-rest-framework/pull/4076) """ return self._get_url_from_configured_app('questionnaire_details') def get_edit_url(self) -> str: """ Edit view url of the questionnaire """ return self._get_url_from_configured_app('questionnaire_edit') def get_perma_url(self) -> str: """ Detail view url of this specific version, matched by its ID. 'Pseudo-Permalink', not the URL that shows the current version of the questionnaire. """ with contextlib.suppress(NoReverseMatch): return reverse( f'{self.configuration.code}:questionnaire_permalink', kwargs={'pk': self.pk}) return '' def update_data(self, data, updated, configuration_code): """ Helper function to just update the data of the questionnaire without creating a new instance. Args: ``data`` (dict): The data dictionary ``updated`` (timestamp): The timestamp of the update ``configuration_code`` (str): The configuration code. Returns: ``Questionnaire`` """ self.data = data self.updated = updated self.save() # Unblock all questionnaires with this code, as all questionnaires with # this code are blocked for editing. Lock.objects.filter(questionnaire_code=self.code).update( is_finished=True) try: self.update_geometry(configuration_code=configuration_code) except: pass # Update the users attached to the questionnaire self.update_users_from_data(configuration_code) return self @staticmethod def create_new(configuration_code, data, user, previous_version=None, status=1, created=None, updated=None, old_data=None, languages=None): """ Create and return a new Questionnaire. Args: ``configuration_code`` (str): The code of the configuration. An active configuration with the given code needs to exist. The configuration is linked to the questionnaire. ``data`` (dict): The questionnaire data. Kwargs: ``previous_version`` (questionnaire.models.Questionnaire): The previous version of the questionnaire. ``status`` (int): The status of the questionnaire to be created. Defaults to 1 (draft) if not set. ``created`` (datetime): A specific datetime object to be set as created timestamp. Defaults to ``now`` if not set. ``updated`` (datetime): A specific datetime object to be set as updated timestamp. Defaults to ``now`` if not set. ``old_data`` (dict): The data dictionary containing the old data of the questionnaire. ``languages`` (list): An optional list of languages in which a newly created questionnaire is available. Should only be used when importing data. Returns: ``questionnaire.models.Questionnaire``. The created Questionnaire. Raises: ``ValidationError`` """ if updated is None: updated = timezone.now() if created is None: created = timezone.now() if previous_version: created = previous_version.created roles, permissions = previous_version.get_roles_permissions(user) code = previous_version.code version = previous_version.version uuid = previous_version.uuid # Unblock all other questionnaires with same code Lock.objects.filter(questionnaire_code=code).update( is_finished=True) if 'edit_questionnaire' not in permissions: raise ValidationError( 'You do not have permission to edit the questionnaire.') if previous_version.status == settings.QUESTIONNAIRE_PUBLIC: # Edit of a public questionnaire: Create new version # with the same code version_count = Questionnaire.objects.filter(code=code).count() version = version_count + 1 languages = previous_version.translations elif previous_version.status == settings.QUESTIONNAIRE_DRAFT: # Edit of a draft questionnaire: Only update the data previous_version.update_data(data, updated, configuration_code) previous_version.add_translation_language(original=False) return previous_version elif previous_version.status == settings.QUESTIONNAIRE_SUBMITTED: # Edit of a submitted questionnaire: Only update the data # User must be reviewer! if 'review_questionnaire' not in permissions: raise ValidationError( 'You do not have permission to edit the ' 'questionnaire.') previous_version.update_data(data, updated, configuration_code) previous_version.add_translation_language(original=False) return previous_version elif previous_version.status == settings.QUESTIONNAIRE_REVIEWED: # Edit of a reviewed questionnaire: Only update the data # User must be publisher! if 'publish_questionnaire' not in permissions: raise ValidationError( 'You do not have permission to edit the ' 'questionnaire.') previous_version.update_data(data, updated, configuration_code) previous_version.add_translation_language(original=False) return previous_version else: raise ValidationError( 'The questionnaire cannot be updated because of its status' ' "{}"'.format(previous_version.status)) else: code = '' # Will be generated later version = 1 uuid = uuid4() if status not in [s[0] for s in STATUSES]: raise ValidationError('"{}" is not a valid status'.format(status)) configuration = Configuration.latest_by_code(configuration_code) if configuration is None: raise ValidationError( 'No active configuration found for code "{}"'.format( configuration_code)) questionnaire = Questionnaire.objects.create( data=data, uuid=uuid, code=code, version=version, status=status, created=created, updated=updated, configuration=configuration) if not previous_version: # Generate and set a new code for the questionnaire from configuration.utils import create_new_code code = create_new_code(questionnaire, configuration_code) questionnaire.code = code if questionnaire.status != settings.QUESTIONNAIRE_PUBLIC: questionnaire.save() create_questionnaire.send(sender=settings.NOTIFICATIONS_CREATE, questionnaire=questionnaire, user=user) questionnaire.update_geometry(configuration_code=configuration_code) if not languages: questionnaire.add_translation_language(original=True) else: for i, language in enumerate(languages): original = i == 0 questionnaire.add_translation_language(original=original, language=language) if previous_version: # Copy all the functional user roles from the old version user_roles = [ settings.QUESTIONNAIRE_COMPILER, settings.QUESTIONNAIRE_EDITOR, settings.QUESTIONNAIRE_REVIEWER, settings.QUESTIONNAIRE_PUBLISHER ] for role in user_roles: for old_user in previous_version.get_users_by_role(role): questionnaire.add_user(old_user, role) # Also copy any flags questionnaire.flags.add(*previous_version.flags.all()) else: questionnaire.add_user(user, settings.QUESTIONNAIRE_COMPILER) questionnaire.update_users_from_data(configuration_code) return questionnaire def add_translation_language(self, original=False, language=None): """ Add a language as a translation of the questionnaire. Add it only once. Args: original: bool. Whether the language is the original language or not language: string. Manually set the language of the translation. If not provided, it is looked up using get_language(). Returns: """ if language is None: language = get_language() if language not in self.translations: QuestionnaireTranslation.objects.create(questionnaire=self, language=language, original_language=original) # Delete cached translation property try: delattr(self, 'translations') except AttributeError: pass def get_id(self): return self.id def get_roles_permissions(self, current_user): """ Return the roles and permissions of a given user for the current questionnaire. The following rules apply: * Compilers and editors can edit questionnaires if the status is either draft or public. * Compilers can submit and assign questionnaires if the status is draft. * Reviewers can edit and review questionnaires if the status is submitted. * Publishers can edit and review questionnaires if the status is reviewed. * Secretariat members can assign questionnaires if the status is submitted (assign reviewers) or published (assign publishers). Permissions to be returned are: * ``edit_questionnaire`` * ``submit_questionnaire`` * ``review_questionnaire`` * ``publish_questionnaire`` * ``assign_questionnaire`` Args: ``current_user`` (User): The user. Returns: ``namedtuple``. A named tuple with - roles: A list of roles of the user for this questionnaire object as tuple (role_code, role_name) - permissions. A list of permissions of the user for this questionnaire object. """ roles = [] permissions = [] RolesPermissions = collections.namedtuple('RolesPermissions', ['roles', 'permissions']) if not isinstance(current_user, get_user_model()): return RolesPermissions(roles=roles, permissions=permissions) # Permissions based on role of current user in questionnaire permission_groups = { settings.QUESTIONNAIRE_COMPILER: [{ 'status': [settings.QUESTIONNAIRE_DRAFT, settings.QUESTIONNAIRE_PUBLIC], 'permissions': ['edit_questionnaire', 'delete_questionnaire'] }, { 'status': [settings.QUESTIONNAIRE_DRAFT], 'permissions': ['submit_questionnaire', 'assign_questionnaire'] }], settings.QUESTIONNAIRE_EDITOR: [{ 'status': [settings.QUESTIONNAIRE_DRAFT, settings.QUESTIONNAIRE_PUBLIC], 'permissions': ['edit_questionnaire'] }], settings.QUESTIONNAIRE_REVIEWER: [{ 'status': [settings.QUESTIONNAIRE_SUBMITTED], 'permissions': ['edit_questionnaire', 'review_questionnaire'] }], settings.QUESTIONNAIRE_PUBLISHER: [{ 'status': [settings.QUESTIONNAIRE_REVIEWED], 'permissions': ['edit_questionnaire', 'publish_questionnaire'] }] } for member_role, user in self.get_users(user=current_user): permission_group = permission_groups.get(member_role) if not permission_group: continue add_role = False for access_level in permission_group: if self.status in access_level['status']: permissions.extend(access_level['permissions']) add_role = True if add_role is True: roles.append( (member_role, dict(QUESTIONNAIRE_ROLES).get(member_role))) # General permissions of user user_permissions = current_user.get_all_permissions() if ('questionnaire.review_questionnaire' in user_permissions and self.status in [settings.QUESTIONNAIRE_SUBMITTED]): permissions.extend(['review_questionnaire', 'edit_questionnaire']) role = settings.QUESTIONNAIRE_REVIEWER roles.append((role, dict(QUESTIONNAIRE_ROLES).get(role))) if ('questionnaire.publish_questionnaire' in user_permissions and self.status in [settings.QUESTIONNAIRE_REVIEWED]): permissions.extend(['publish_questionnaire', 'edit_questionnaire']) role = settings.QUESTIONNAIRE_PUBLISHER roles.append((role, dict(QUESTIONNAIRE_ROLES).get(role))) if 'questionnaire.assign_questionnaire' in user_permissions: # Piggybacking on the "assign_questionnaire" permission to identify # WOCAT secretariat users as this permission is only available for # this role. permissions.extend([ 'edit_questionnaire', 'delete_questionnaire', 'submit_questionnaire', 'review_questionnaire', 'publish_questionnaire', 'change_compiler' ]) if self.status in [ settings.QUESTIONNAIRE_SUBMITTED, settings.QUESTIONNAIRE_REVIEWED ]: permissions.extend(['assign_questionnaire']) role = settings.QUESTIONNAIRE_SECRETARIAT roles.append((role, dict(QUESTIONNAIRE_ROLES).get(role))) # UNCCD Flagging questionnaire_country = self.get_question_data('qg_location', 'country') if len(questionnaire_country) == 1 \ and self.status in [settings.QUESTIONNAIRE_PUBLIC]: for country in current_user.get_unccd_countries(): if country.keyword == questionnaire_country[0]: role = settings.ACCOUNTS_UNCCD_ROLE_NAME roles.append((role, dict(QUESTIONNAIRE_ROLES).get(role))) try: self.flags.get(flag=settings.QUESTIONNAIRE_FLAG_UNCCD) # Flag already exists permissions.extend(['unflag_unccd_questionnaire']) except Flag.DoesNotExist: # No flag yet permissions.extend(['flag_unccd_questionnaire']) # Remove duplicates permissions = list(set(permissions)) roles = list(set(roles)) # Do the translation of the role names translated_roles = [] for role_keyword, role_name in roles: translated_roles.append((role_keyword, _(role_name))) return RolesPermissions(roles=translated_roles, permissions=permissions) def get_question_data(self, qg_keyword, q_keyword): """ Get the raw question data by keyword. This does not translate the values or return the labelled choices. For this, use QuestionnaireConfiguration. Args: qg_keyword: (str): The keyword of the questiongroup. q_keyword: (str): The keyword of the question. Returns: (list). A list of (raw) values. """ data = [] if self.data: for q_data in self.data.get(qg_keyword, []): for key, value in q_data.items(): if key == q_keyword: data.append(value) return data @property def configuration_object(self): return get_configuration(code=self.configuration.code, edition=self.configuration.edition) def get_name(self, locale='') -> str: """ Return the name of the questionnaire, based on the configuration. """ names = self.configuration_object.get_questionnaire_name( self.data) or {} name = names.get(locale or get_language()) if name: # omit additional query return name original_lang = self.questionnairetranslation_set.filter( original_language=True).first() if original_lang and names.get(original_lang.language): return names[original_lang.language] return '' def get_countries(self) -> []: """ Return list of translated country names. """ codes = self.get_question_data('qg_location', 'country') if codes: values = Value.objects.filter(keyword__in=codes) if values.exists(): return [ value.get_translation(keyword='label') for value in values ] return [] def get_history_versions(self, user: User or None) -> list: item = collections.namedtuple('Item', 'id, name, url, updated, status') # Function get_query_status_filter requires a request object, which we # will fake here. pseudo_request = collections.namedtuple('MockRequest', 'user') from questionnaire.utils import get_query_status_filter status_filter = get_query_status_filter(pseudo_request(user)) # Inactive versions (previously active) are always visible status_filter |= Q(status=settings.QUESTIONNAIRE_INACTIVE) history = Questionnaire.with_status.not_deleted().filter( status_filter, code=self.code, ).order_by('created') return [ item(questionnaire.id, questionnaire.get_name(), questionnaire.get_perma_url(), questionnaire.updated, questionnaire.status_property) for questionnaire in history ] def update_geometry(self, configuration_code, force_update=False): """ Update the geometry of a questionnaire based on the GeoJSON found in the data json. Args: configuration_code: Returns: - """ def get_geometry_from_string(geometry_string): """ Extract and convert the geometry from a (GeoJSON) string. Args: geometry_string: The geometry as (GeoJSON) string. Returns: A GeometryCollection or None. """ if geometry_string is None: return None try: geometry_json = json.loads(geometry_string) except json.decoder.JSONDecodeError: return None geoms = [] for feature in geometry_json.get('features', []): try: feature_geom = GEOSGeometry( json.dumps(feature.get('geometry'))) except ValueError: continue except GDALException: continue geoms.append(feature_geom) if geoms: return GeometryCollection(tuple(geoms)) else: return None geometry_value = self.configuration_object.get_questionnaire_geometry( self.data) geometry = get_geometry_from_string(geometry_value) geometry_changed = self.geom != geometry try: self.geom = geometry self.save() except ValidationError: return if self.geom is None or (not force_update and not geometry_changed): # If there is no geometry or if it did not change, there is no need # to create the static map image (again) return # Create static map width = 1000 height = 800 marker_diameter = 24 marker_color = '#0036FF' m = StaticMap(width, height) for point in iter(self.geom): m.add_marker( CircleMarker((point.x, point.y), marker_color, marker_diameter)) bbox = None questionnaire_country = self.get_question_data('qg_location', 'country') if len(questionnaire_country) == 1: country_iso3 = questionnaire_country[0].replace('country_', '') country_iso2 = settings.CONFIGURATION_COUNTRY_ISO_MAPPING.get( country_iso3) if country_iso2: r = requests.get( 'http://api.geonames.org/countryInfoJSON?username=wocat_webdev&country={}' .format(country_iso2)) geonames_country = r.json().get('geonames') if len(geonames_country) == 1: ctry = geonames_country[0] poly_coords = [[ctry.get('west'), ctry.get('north')], [ctry.get('west'), ctry.get('south')], [ctry.get('east'), ctry.get('south')], [ctry.get('east'), ctry.get('north')], [ctry.get('west'), ctry.get('north')]] bbox = Polygon(poly_coords, None, None) if bbox: m.add_polygon(bbox) image = m.render() else: # No bbox found, guess zoom level image = m.render(zoom=6) map_folder = get_upload_folder_path(str(self.uuid), subfolder='maps') if not os.path.exists(map_folder): os.makedirs(map_folder) filename = '{}_{}.jpg'.format(self.uuid, self.version) image.save(os.path.join(map_folder, filename)) def add_flag(self, flag): """ Add a Flag object to the questionnaire. Add it only once. Args: flag: (Flag): The flag to be added. """ if flag not in self.flags.all(): self.flags.add(flag) def get_user(self, user, role): """ Get and return a user of the Questionnaire by role. Args: user: (User) The user. role: (str): The role of the user. Returns: User or None. """ membership = self.questionnairemembership_set.filter( user=user, role=role).first() if membership: return membership.user return None def get_users(self, **kwargs): """ Helper function to return the users of a questionnaire along with their role in this membership. Returns: ``list``. A list of tuples where each entry contains the following elements: - [0]: ``string``. The role of the membership. - [1]: ``accounts.models.User``. The user object. """ users = [] for membership in self.questionnairemembership_set.filter(**kwargs): users.append((membership.role, membership.user)) return users def get_users_by_role(self, role): """ Return all users of a questionnaire based on their role in the membership. Args: ``role`` (str): The role of the membership used as a filter. Returns: ``list``. A list of users. """ users = [] for user_role, user in self.get_users(): if user_role == role: users.append(user) return users def get_users_by_roles(self, roles: list) -> list: for role, user in self.get_users(): if role in roles: yield user @staticmethod def get_wocat_mailbox_user(): """ Return the WOCAT Mailbox User with the ID set in the settings. """ try: return User.objects.get(pk=settings.WOCAT_MAILBOX_USER_ID) except User.DoesNotExist: return None def get_users_for_next_publish_step(self): if self.status in settings.QUESTIONNAIRE_WORKFLOW_STEPS: role = settings.QUESTIONNAIRE_PUBLICATION_ROLES[self.status] return getattr(self, 'get_{role}s'.format(role=role))() return [] def get_reviewers(self): return get_user_model().objects.filter( groups__permissions__codename='review_questionnaire') def get_publishers(self): return get_user_model().objects.filter( groups__permissions__codename='publish_questionnaire') def add_user(self, user, role): """ Add a user. Users are only added if the membership does not yet exist. Args: ``user`` (User): The user. ``role`` (str): The role of the user. """ if self.get_user(user, role) is None: QuestionnaireMembership.objects.create(questionnaire=self, user=user, role=role) def remove_user(self, user, role): """ Remove a user. Args: ``user`` (User): The user. ``role`` (str): The role of the user. """ user = self.get_user(user, role) QuestionnaireMembership.objects.filter(questionnaire=self, user=user, role=role).delete() def update_users_from_data(self, configuration_code): """ Based on the data dictionary, update the user links in the database. This usually happens after the form of the questionnaire was submitted. Args: ``configuration_code`` (str): The code of the configuration of the questionnaire which triggered the update. ``compiler`` (accounts.models.User): A user figuring as the compiler of the questionnaire. """ user_fields = self.configuration_object.get_user_fields() # Collect the users appearing in the data dictionary. submitted_users = [] for user_questiongroup in user_fields: for user_data in self.data.get(user_questiongroup[0], []): user_id = user_data.get(user_questiongroup[1]) if not bool(user_id): continue submitted_users.append((user_questiongroup[3], user_id)) # Get the users which were attached before modifying the # questionnaire. Collect only those which can be changed through # the data dictionary (no functional user roles) previous_users = [] for user_role, user in self.get_users(): if user_role not in [ settings.QUESTIONNAIRE_COMPILER, settings.QUESTIONNAIRE_EDITOR, settings.QUESTIONNAIRE_REVIEWER, settings.QUESTIONNAIRE_PUBLISHER ]: previous_users.append((user_role, user)) # Check which users are new (in submitted_users but not in # previous_users) and add them to the questionnaire. previous_users_found = [] for submitted_user in submitted_users: user_found = False for previous_user in previous_users: if submitted_user[0] != previous_user[0]: continue if str(submitted_user[1]) != str(previous_user[1].id): continue user_found = True previous_users_found.append(previous_user) if user_found is False: user = User.objects.get(pk=submitted_user[1]) self.add_user(user, submitted_user[0]) # Check for users which were removed (in previous_users but not # found when looking through submitted_users) and remove them # from the questionnaire for removed_user in list( set(previous_users) - set(previous_users_found)): self.remove_user(removed_user[1], removed_user[0]) def get_metadata(self): """ Return some metadata about the Questionnaire. Returns: ``dict``. A dict containing the following metadata: * ``created`` (timestamp) * ``updated`` (timestamp) * ``compilers`` (list): A list of dictionaries containing information about the compilers. Each entry contains the following data: * ``id`` * ``name`` * ``editors`` (list): A list of dictionaries containing information about the editors. The format is the same as for ``compilers`` * ``code`` (string) * ``configurations`` (list) * ``translations`` (list) """ return dict(self._get_metadata()) def _get_metadata(self): # Access the property first, then the model field. for key in settings.QUESTIONNAIRE_METADATA_KEYS: yield key, getattr(self, '{}_property'.format(key), getattr(self, key)) def add_link(self, questionnaire, symm=True): """ Add a link to another Questionnaire. This actually creates two entries in the link table to make the reference symmetrical. https://charlesleifer.com/blog/self-referencing-many-many-through/ Args: ``questionnaire`` (questionnaire.models.Questionnaire): The questionnaire to link to. Kwargs: ``symm`` (bool): Whether or not to add the symmetrical link (to avoid recursion). Returns: ``questionnaire.models.Questionnaire``. The updated questionnaire. """ link, created = QuestionnaireLink.objects.get_or_create( from_questionnaire=self, from_status=self.status, to_questionnaire=questionnaire, to_status=questionnaire.status) if symm: # avoid recursion by passing `symm=False` questionnaire.add_link(self, symm=False) return questionnaire def remove_link(self, questionnaire, symm=True): """ Remove a link to another Questionnaire. This actually removes both links (also the symmetrical one). https://charlesleifer.com/blog/self-referencing-many-many-through/ Args: ``questionnaire`` (questionnaire.models.Questionnaire): The questionnaire to remove. Kwargs: ``symm`` (bool): Whether or not to remove the symmetrical link (to avoid recursion). """ QuestionnaireLink.objects.filter( from_questionnaire=self, to_questionnaire=questionnaire).delete() if symm: # avoid recursion by passing `symm=False` questionnaire.remove_link(self, symm=False) def __str__(self): return json.dumps(self.data) @classmethod def has_questionnaires_for_code(cls, code: str) -> bool: return cls.objects.filter(code=code).exists() @classmethod def lock_questionnaire(cls, code: str, user: settings.AUTH_USER_MODEL): """ If the questionnaire is not locked, or locked by given user: lock the questionnaire for this user - else raise an error. """ qs_locks = Lock.with_status.is_blocked(code, for_user=user) if qs_locks.exists(): raise QuestionnaireLockedException(qs_locks.first().user) else: Lock.objects.create(questionnaire_code=code, user=user) def can_edit(self, user: settings.AUTH_USER_MODEL) -> bool: has_questionnaires = self.has_questionnaires_for_code(self.code) qs_locks = Lock.with_status.is_blocked(self.code, for_user=user) return has_questionnaires and not qs_locks.exists() def unlock_questionnaire(self): Lock.objects.filter(questionnaire_code=self.code).update( is_finished=True) def get_blocked_message(self, user: settings.AUTH_USER_MODEL) -> tuple: """ Get status and message for blocked status (blocked or can be edited). """ locks = Lock.with_status.is_blocked(code=self.code, for_user=user) if not locks.exists(): return SUCCESS, _(u"This questionnaire can be edited.") else: return WARNING, _(u"This questionnaire is " u"locked for editing by {user}.".format( user=locks.first().user.get_display_name())) # Properties for the get_metadata function. def _get_role_list(self, role): members = [] for member in self.members.filter(questionnairemembership__role=role): members.append({ 'id': member.id, 'name': str(member), }) return members @cached_property def editors(self): return self._get_role_list(settings.QUESTIONNAIRE_EDITOR) @cached_property def compilers(self): return self._get_role_list(settings.QUESTIONNAIRE_COMPILER) @cached_property def reviewers(self): return self._get_role_list(settings.QUESTIONNAIRE_REVIEWER) @cached_property def status_property(self): status = next((x for x in STATUSES if x[0] == self.status), (None, '')) status_code = next((x for x in STATUSES_CODES if x[0] == self.status), (None, '')) return status_code[1], status[1] @cached_property def translations(self): return list( self.questionnairetranslation_set.values_list('language', flat=True)) @cached_property def original_locale(self): translation = self.questionnairetranslation_set.filter( original_language=True).first() if translation: return translation.language else: return None @cached_property def links_property(self): """ Collect all info about linked questionnaire and structure it according to language. This follows a often used pattern of questionnaire data. Returns: list """ links = [] current_language = get_language() for link in self.links.filter(status=settings.QUESTIONNAIRE_PUBLIC): link_configuration = get_configuration( code=link.configuration.code, edition=link.configuration.edition) name_data = link_configuration.get_questionnaire_name(link.data) try: original_language = link.questionnairetranslation_set.first( ).language except AttributeError: original_language = settings.LANGUAGES[0][0] # 'en' names = {} urls = {} for code, language in settings.LANGUAGES: activate(code) names[code] = name_data.get(code, name_data.get(original_language)) urls[code] = link.get_absolute_url() if code == original_language: names['default'] = names[code] urls['default'] = urls[code] links.append({ 'code': link.code, 'configuration': link_configuration.keyword, 'name': names, 'url': urls, }) activate(current_language) return links @cached_property def flags_property(self): flags = [] for flag in self.flags.all(): flags.append({ 'flag': flag.flag, 'name': flag.get_flag_display(), 'helptext': flag.get_helptext(), }) return flags @cached_property def has_release(self): """ Check if any version of this questionnaire was published. Returns: boolean """ return self.status == settings.QUESTIONNAIRE_PUBLIC or self._meta.model.with_status.not_deleted( ).filter(code=self.code, status=settings.QUESTIONNAIRE_PUBLIC).exists()
class Inmueble(models.Model): creador = models.ForeignKey('auth.User') email = models.EmailField(null=True, blank=False) info = models.TextField(null=True, blank=True) fecha_creacion = models.DateTimeField(auto_now_add=True) fecha_publicacion = models.DateTimeField(blank=True, null=True) titulo = models.CharField(max_length=60, blank=False) descripcion = models.CharField(max_length=20, null=True, blank=True) direccion = models.CharField(max_length=150, blank=False) calle = models.CharField(max_length=150, blank=False) altura = models.CharField(max_length=6, blank=False) pais = models.ForeignKey(Pais, on_delete=models.CASCADE) ciudad = models.ForeignKey(Ciudad, on_delete=models.CASCADE) barrio = models.ForeignKey(Barrio, on_delete=models.CASCADE) metros_terreno = models.CharField(max_length=5, null=True, blank=True) metros_cubiertos = models.CharField(max_length=5, null=True, blank=True) antiguedad = models.CharField(max_length=2, blank=False) banos = models.CharField(max_length=5, null=True, blank=True) habitaciones = models.CharField(max_length=5, null=True, blank=True) ambientes = models.CharField(max_length=2, blank=False) plantas = models.CharField(max_length=2, blank=False) pileta = models.CharField(max_length=2, blank=False) cochera = models.CharField(max_length=2, blank=False) patio = models.CharField(max_length=2, blank=False) precio = models.CharField(max_length=10, blank=False) location = models.PointField(u"longitude/latitude", geography=True, blank=True, null=True) gis = models.GeoManager() objects = models.Manager() slug = AutoSlugField(populate_from='titulo', max_length=255) uuid = ext_fields.UUIDField(auto=True) def publish(self): self.fecha_publicacion = timezone.now() self.save() def __str__(self): return self.titulo def save(self, **kwargs): if not self.location: address = u'%s %s' % (self.ciudad.nombre, self.direccion) address = address.encode('utf-8') geocoder = GoogleV3() try: _, latlon = geocoder.geocode(address) except (URLError, GeocoderQueryError, ValueError): pass else: point = "POINT(%s %s)" % (latlon[1], latlon[0]) self.location = geos.fromstr(point) super(Inmueble, self).save() class Meta: verbose_name_plural = "Inmuebles"
class Layer(models.Model): class Meta(object): verbose_name = "Couche de données" verbose_name_plural = "Couches de données" # Managers # ======== objects = models.Manager() vector = VectorLayerManager() raster = RasterLayerManager() # Champs atributaires # =================== name = models.SlugField( verbose_name="Nom de la couche", max_length=100, editable=False, primary_key=True, ) resource = models.ForeignKey( to='Resource', verbose_name="Ressource", null=True, blank=True, on_delete=models.CASCADE, ) TYPE_CHOICES = ( ('raster', 'raster'), ('vector', 'vector'), ) type = models.CharField( verbose_name="Type", max_length=6, null=True, blank=True, choices=TYPE_CHOICES, ) bbox = models.PolygonField( verbose_name="Rectangle englobant", null=True, blank=True, srid=4171, ) def __str__(self): return self.resource.__str__() # Propriétés # ========== @property def layername(self): return self.mra_info['name'] @property def geometry_type(self): return { 'POLYGON': 'Polygone', 'POINT': 'Point', 'LINESTRING': 'Ligne', 'RASTER': 'Raster', }.get(self.mra_info['type'], None) @property def is_enabled(self): return self.mra_info['enabled'] @property def title(self): return self.mra_info['title'] @property def abstract(self): return self.mra_info['abstract'] @property def styles(self): return self.mra_info['styles']['styles'] @property def id(self): return self.name @property def filename(self): if self.type == 'vector': # Peut-être quelque chose à retourner ici ? return None if self.type == 'raster': x = str(self.resource.ckan_id) _filename = os.path.join(CKAN_STORAGE_PATH, x[:3], x[3:6], self.resource.filename.split('/')[-1]) if os.path.isfile(_filename): filename = os.path.join(MAPSERV_STORAGE_PATH, x[:3], x[3:6], self.resource.filename.split('/')[-1]) else: filename = os.path.join(MAPSERV_STORAGE_PATH, x[:3], x[3:6], x[6:]) return filename # Méthodes héritées # ================= def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) organisation = self.resource.dataset.organisation ws_name = organisation.slug try: l = MRAHandler.get_layer(self.name) except MraBaseError: return # Récupération des informations de couche vecteur # =============================================== if self.type == 'vector': try: ft = MRAHandler.get_featuretype(ws_name, 'public', self.name) except MraBaseError: return if not l or not ft: return ll = ft['featureType']['latLonBoundingBox'] bbox = [[ll['miny'], ll['minx']], [ll['maxy'], ll['maxx']]] attributes = [ item['name'] for item in ft['featureType']['attributes'] ] default_style_name = l['defaultStyle']['name'] styles = [{ 'name': 'default', 'text': 'Style par défaut', 'url': l['defaultStyle']['href'].replace('json', 'sld'), 'sld': MRAHandler.get_style(l['defaultStyle']['name']) }] if l.get('styles'): for style in l.get('styles')['style']: styles.append({ 'name': style['name'], 'text': style['name'], 'url': style['href'].replace('json', 'sld'), 'sld': MRAHandler.get_style(style['name']) }) # Récupération des informations de couche raster # ============================================== elif self.type == 'raster': try: c = MRAHandler.get_coverage(ws_name, self.name, self.name) except MraBaseError: return if not l or not c: return ll = c['coverage']['latLonBoundingBox'] bbox = [[ll['miny'], ll['minx']], [ll['maxy'], ll['maxx']]] attributes = [] default_style_name = None styles = [] # Puis.. self.mra_info = { 'name': l['name'], 'title': l['title'], 'type': l['type'], 'enabled': l['enabled'], 'abstract': l['abstract'], 'bbox': bbox, 'attributes': attributes, 'styles': { 'default': default_style_name, 'styles': styles } } def save(self, *args, synchronize=False, **kwargs): # Synchronisation avec le service OGC en fonction du type de données if self.type == 'vector': self.save_vector_layer() elif self.type == 'raster': self.save_raster_layer() # Puis sauvegarde super().save(*args, **kwargs) self.handle_enable_ows_status() self.handle_layergroup() if synchronize: self.synchronize() def delete(self, *args, current_user=None, **kwargs): with_user = current_user # On supprime la ressource CKAN if with_user: username = with_user.username apikey = CkanHandler.get_user(username)['apikey'] with CkanUserHandler(apikey=apikey) as ckan_user: ckan_user.delete_resource(self.name) else: CkanHandler.delete_resource(self.name) # On supprime les ressources MRA try: MRAHandler.del_layer(self.name) ws_name = self.resource.dataset.organisation.slug if self.type == 'vector': MRAHandler.del_featuretype(ws_name, 'public', self.name) if self.type == 'raster': MRAHandler.del_coverage(ws_name, self.name, self.name) # MRAHandler.del_coveragestore(ws_name, self.name) except Exception as e: logger.error(e) pass # On supprime la table de données PostGIS try: drop_table(self.name) except Exception as e: logger.error(e) pass # Puis on supprime l'instance super().delete(*args, **kwargs) # Autres méthodes # =============== def save_raster_layer(self, *args, **kwargs): """Synchronizer la couche de données matricielle avec le service OGC via MRA.""" organisation = self.resource.dataset.organisation ws_name = organisation.slug cs_name = self.name if self.pk: try: Layer.objects.get(pk=self.pk) except Layer.DoesNotExist: pass else: # On vérifie si l'organisation du jeu de données a changée, # auquel cas il est nécessaire de supprimer les objets MRA # afin de les recréer dans le bon workspace (c-à-d Mapfile). previous_layer = MRAHandler.get_layer(self.name) regex = '/workspaces/(?P<ws_name>[a-z_\-]+)/coveragestores/' matched = re.search(regex, previous_layer['resource']['href']) if matched: previous_ws_name = matched.group('ws_name') if not ws_name == previous_ws_name: MRAHandler.del_layer(self.name) MRAHandler.del_coverage(previous_ws_name, cs_name, self.name) MRAHandler.get_or_create_workspace(organisation) MRAHandler.get_or_create_coveragestore(ws_name, cs_name, filename=self.filename) MRAHandler.get_or_create_coverage(ws_name, cs_name, self.name, enabled=True, title=self.resource.title, abstract=self.resource.description) def save_vector_layer(self, *args, **kwargs): """Synchronizer la couche de données vectorielle avec le service OGC via MRA.""" organisation = self.resource.dataset.organisation ws_name = organisation.slug ds_name = 'public' if self.pk: try: Layer.objects.get(pk=self.pk) except Layer.DoesNotExist: pass else: # On vérifie si l'organisation du jeu de données a changée, # auquel cas il est nécessaire de supprimer les objets MRA # afin de les recréer dans le bon workspace (c-à-d Mapfile). previous_layer = MRAHandler.get_layer(self.name) regex = '/workspaces/(?P<ws_name>[a-z_\-]+)/datastores/' matched = re.search(regex, previous_layer['resource']['href']) if matched: previous_ws_name = matched.group('ws_name') if not ws_name == previous_ws_name: MRAHandler.del_layer(self.name) MRAHandler.del_featuretype(previous_ws_name, ds_name, self.name) MRAHandler.get_or_create_workspace(organisation) MRAHandler.get_or_create_datastore(ws_name, ds_name) MRAHandler.get_or_create_featuretype( ws_name, ds_name, self.name, enabled=True, title=self.resource.title, abstract=self.resource.description) def synchronize(self, with_user=None): """Synchronizer le jeu de données avec l'instance de CKAN.""" # 'with_user' n'est pas utiliser dans ce contexte # Définition des propriétés de la « ressource » # ============================================= id = self.name name = 'Aperçu cartographique'.format(name=self.resource.title) description = self.resource.description organisation = self.resource.dataset.organisation base_url = OWS_URL_PATTERN.format( organisation=organisation.slug).replace('?', '') getlegendgraphic = ( '{base_url}?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetLegendGraphic' '&LAYER={layer}&FORMAT=image/png').format(base_url=base_url, layer=id) api = { 'url': base_url, 'typename': id, 'getlegendgraphic': getlegendgraphic } try: DEFAULT_SRID = settings.DEFAULTS_VALUES['SRID'] except Exception: DEFAULT_SRID = 4326 else: SupportedCrs = apps.get_model(app_label='idgo_admin', model_name='SupportedCrs') try: SupportedCrs.objects.get(auth_name='EPSG', auth_code=DEFAULT_SRID) except SupportedCrs.DoesNotExist: DEFAULT_SRID = 4326 if self.type == 'vector': if self.resource.format_type.extension.lower() in ('json', 'geojson'): outputformat = 'shapezip' # Il faudrait être sûr que le format existe avec le même nom ! elif self.resource.format_type.extension.lower() in ('zip', 'tar'): outputformat = 'geojson' # Il faudrait être sûr que le format existe avec le même nom ! api[outputformat] = ( '{base_url}?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature' '&TYPENAME={typename}&OUTPUTFORMAT={outputformat}&CRSNAME=EPSG:{srid}' ).format(base_url=base_url, typename=id, outputformat=outputformat, srid=str(DEFAULT_SRID)) CkanHandler.update_resource(str(self.resource.ckan_id), api=json.dumps(api)) CkanHandler.push_resource_view(title=name, description=description, resource_id=str(self.resource.ckan_id), view_type='geo_view') def handle_enable_ows_status(self): """Gérer le statut d'activation de la couche de données SIG.""" ws_name = self.resource.dataset.organisation.slug if self.resource.ogc_services: MRAHandler.enable_layer(ws_name, self.name) # TODO: Comment on gère les ressources CKAN service ??? else: MRAHandler.disable_layer(ws_name, self.name) # TODO: Comment on gère les ressources CKAN service ??? def handle_layergroup(self): dataset = self.resource.dataset layers = list( itertools.chain.from_iterable([ qs for qs in [ resource.get_layers() for resource in dataset.get_resources() ] ])) # TODO remplacer par `layers = dataset.get_layers()` MRAHandler.create_or_update_layergroup( dataset.organisation.slug, { 'name': dataset.slug, 'title': dataset.title, 'abstract': dataset.description, 'layers': [layer.name for layer in layers] })
class Poi(models.Model): "Misto - bod v mape" author = models.ForeignKey(User, verbose_name="Autor", on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True, verbose_name="Posledni zmena") nazev = models.CharField(max_length=255, verbose_name=u"název", help_text=u"Přesný název místa.") # Relationships znacka = models.ForeignKey( Znacka, limit_choices_to={ 'status__show_TU': 'True', 'vrstva__status__show_TU': 'True' }, verbose_name=u"značka", help_text="Zde vyberte ikonu, která se zobrazí na mapě.", related_name="pois", on_delete=models.CASCADE) status = models.ForeignKey( Status, default=2, help_text="Status místa; určuje, kde všude se místo zobrazí.", on_delete=models.CASCADE) vlastnosti = models.ManyToManyField( 'Vlastnost', blank=True, null=True, limit_choices_to={'status__show_TU': 'True'}, help_text= "Určete, jaké má místo vlastnosti. Postupujte podle manuálu.<br/>") # "dulezitost" - modifikator minimalniho zoomu, ve kterem se misto zobrazuje. dulezitost = models.SmallIntegerField( default=0, verbose_name=u"důležitost", help_text= u"""Modifikátor minimalniho zoomu, ve kterém se místo zobrazuje (20+ bude vidět vždy).<br/> Cíl je mít výběr základních objektů viditelných ve velkých měřítcích a zabránit přetížení mapy značkami v přehledce.<br/> Lze použít pro placenou reklamu! ("Váš podnik bude vidět hned po otevření mapy")""" ) # Geographical intepretation geom = models.GeometryField( verbose_name=u"poloha", srid=4326, help_text= u"""Vložení bodu: Klikněte na tužku s plusem a umístěte bod na mapu.""" ) #Kreslení linie: Klikněte na ikonu linie a klikáním do mapy určete lomenou čáru. Kreslení ukončíte dvouklikem.<br/> #Kreslení oblasti: Klikněte na ikonu oblasti a klikáním do mapy definujte oblast. Kreslení ukončíte dvouklikem.<br/> #Úprava vložených objektů: Klikněte na první ikonu a potom klikněte na objekt v mapě. Tažením přesouváte body, body uprostřed úseků slouží k vkládání nových bodů do úseku.""") objects = models.Manager() # Own content (facultative) desc = models.TextField( null=True, blank=True, verbose_name=u"popis", help_text=u"Text, který se zobrazí na mapě po kliknutí na ikonu.") desc_extra = models.TextField( null=True, blank=True, verbose_name=u"podrobný popis", help_text="Text, který rozšiřuje informace výše.") url = models.URLField(null=True, blank=True, help_text=u"Odkaz na webovou stránku místa.") address = models.CharField(max_length=255, null=True, blank=True, verbose_name=u"adresa", help_text=u"Adresa místa (ulice, číslo domu)") remark = models.TextField( null=True, blank=True, verbose_name=u"interní poznámka", help_text=u"Interní informace o objektu, které se nebudou zobrazovat.") # navzdory nazvu jde o fotku v plnem rozliseni foto_thumb = models.ImageField( null=True, blank=True, upload_to='foto', storage=SlugifyFileSystemStorage(), verbose_name=u"fotka", help_text=u"Nahrajte fotku v plné velikosti.", ) # zde se ulozi slugy vsech vlastnosti, aby se pri renederovani kml # nemusel delat db dotaz pro kazde Poi na jeho vlastnosti vlastnosti_cache = models.CharField(max_length=255, null=True, blank=True) sit_geom = models.GeometryField(verbose_name=u"SIT poloha", srid=4326, blank=True, null=True, help_text=u"Původní poloha podle SITu") viditelne = ViditelneManager() class Meta: permissions = [ ("can_only_own_data_only", "Can only edit his own data"), ] verbose_name = "místo" verbose_name_plural = "místa" def __str__(self): return self.nazev def save_vlastnosti_cache(self): self.vlastnosti_cache = u",".join( [v.slug for v in self.vlastnosti.filter(status__show=True)]) self.save() def get_absolute_url(self): return "/misto/%i/" % self.id def save(self, *args, **kwargs): self.created_at = datetime.datetime.now() super(Poi, self).save(*args, **kwargs)
class NHDLake(models.Model): # FYI: We add an hstore field (called changed_on) in a migration that tracks # when each field was updated (which is triggered with a trigger -- also # defined in a migration) reachcode = models.CharField(max_length=32, primary_key=True) title = models.CharField(max_length=255, blank=True) permanent_id = models.CharField(max_length=64) fdate = models.DateField() ftype = models.IntegerField() fcode = models.IntegerField() shape_length = models.FloatField() shape_area = models.FloatField() resolution = models.IntegerField() gnis_id = models.CharField(max_length=32) gnis_name = models.CharField(max_length=255) area_sq_km = models.FloatField() elevation = models.FloatField() parent = models.ForeignKey('self', null=True, db_column="parent", blank=True) aol_page = models.IntegerField(null=True, blank=True) body = models.TextField() fishing_zone = models.ForeignKey('FishingZone', null=True) huc6 = models.ForeignKey('HUC6', null=True, blank=True) county_set = models.ManyToManyField('County', through="LakeCounty") plants = models.ManyToManyField('Plant', through="LakePlant") # denormalized fields # unfortunately, for performance reasons, we need to cache whether a lake # has plant data, has mussels, was in the original AOL, has docs, has # photos etc has_mussels = models.BooleanField(default=False, blank=True) has_plants = models.BooleanField(default=False, blank=True) has_docs = models.BooleanField(default=False, blank=True) has_photos = models.BooleanField(default=False, blank=True) # some lakes in the NHD are not in oregon, so we cache this value so we # don't have to join on the lake_county table is_in_oregon = models.BooleanField(default=False, blank=True) objects = NHDLakeManager() unfiltered = models.Manager() class Meta: db_table = 'nhd' def __str__(self): return self.title or self.gnis_name or self.pk @classmethod def update_cached_fields(cls): """ Important lakes are lakes with plant data, mussel data, photos docs, etc. We cache these booleans (has_mussels, has_plants, etc) on the model itself for performance reasons, so this class method needs to be called to refresh thos booleans. It also refreshes the is_in_oregon flag """ cursor = connection.cursor() cursor.execute(""" SELECT reachcode, MAX(has_plants) AS has_plants, MAX(has_docs) AS has_docs, MAX(has_photos) AS has_photos, MAX(has_aol_page) AS has_aol_page, MAX(has_mussels) AS has_mussels FROM ( -- get all reachcodes for lakes with plants SELECT reachcode, 1 AS has_plants, 0 AS has_docs, 0 AS has_photos, 0 AS has_aol_page, 0 AS has_mussels FROM "lake_plant" GROUP BY reachcode HAVING COUNT(*) >= 1 UNION -- get all reachcodes for lakes with documents SELECT reachcode, 0 AS has_plants, 1 AS has_docs, 0 AS has_photos, 0 AS has_aol_page, 0 AS has_mussels FROM "document" GROUP BY reachcode HAVING COUNT(*) >= 1 UNION -- get all reachcodes for lakes with photos SELECT reachcode, 0 AS has_plants, 0 AS has_docs, 1 AS has_photos, 0 AS has_aol_page, 0 AS has_mussels FROM "photo" GROUP BY reachcode HAVING COUNT(*) >= 1 UNION -- get all original AOL lakes SELECT reachcode, 0 AS has_plants, 0 AS has_docs, 0 AS has_photos, 1 AS has_aol_page, 0 AS has_mussels FROM "nhd" WHERE aol_page IS NOT NULL UNION SELECT DISTINCT reachcode, 0 AS has_plants, 0 AS has_docs, 0 AS has_photos, 0 AS has_aol_page, 1 AS has_mussels FROM mussels.observation INNER JOIN "lake_geom" ON (ST_BUFFER(ST_TRANSFORM(observation.the_geom, 3644), %s) && lake_geom.the_geom) ) k GROUP BY reachcode """, [DISTANCE_FROM_ITEM]) # noqa for row in dictfetchall(cursor): NHDLake.unfiltered.filter(reachcode=row['reachcode']).update( has_plants=row['has_plants'], has_docs=row['has_docs'], has_photos=row['has_photos'], has_mussels=row['has_mussels'] ) # now update the is_in_oregon flag cursor.execute(""" WITH foo AS (SELECT COUNT(*) AS the_count, reachcode FROM lake_county GROUP BY reachcode) UPDATE nhd SET is_in_oregon = foo.the_count > 0 FROM foo WHERE foo.reachcode = nhd.reachcode """) @property def area(self): """Returns the number of acres this lake is""" if not hasattr(self, "_area"): cursor = connections['default'].cursor() # 43560 is the number of square feet in an arre cursor.execute("SELECT ST_AREA(the_geom)/43560 FROM lake_geom WHERE reachcode = %s", (self.reachcode,)) self._area = cursor.fetchone()[0] return self._area @property def perimeter(self): """Returns the number of acres this lake is""" if not hasattr(self, "_perimeter"): cursor = connections['default'].cursor() # 5280 is the number of feet in a mile cursor.execute("SELECT ST_PERIMETER(the_geom)/5280 FROM lake_geom WHERE reachcode = %s", (self.reachcode,)) self._perimeter = cursor.fetchone()[0] return self._perimeter @property def bounding_box(self): if not hasattr(self, "_bbox"): lakes = LakeGeom.objects.raw("""SELECT reachcode, Box2D(ST_Envelope(st_expand(the_geom,1000))) as coords from lake_geom WHERE reachcode = %s""", (self.pk,)) # noqa lake = list(lakes)[0] self._bbox = re.sub(r'[^0-9.-]', " ", lake.coords).split() return self._bbox @property def counties(self): """Return a nice comma separated list of the counties this lake belongs to""" if not hasattr(self, "_counties"): self._counties = ", ".join(c.name for c in self.county_set.all()) return self._counties @counties.setter def counties(self, value): """ We need a setter since the raw query we perform in the manager class generates the comma separated list of counties in the query itself """ self._counties = value @property def watershed_tile_url(self): """ Returns the URL to the watershed tile thumbnail from the arcgis server for this lake """ # get the bounding box of the huc6 geom for the lake. The magic 300 # here is from the original AOL results = HUC6.objects.raw(""" SELECT Box2D(st_envelope(st_expand(the_geom, 300))) AS bbox, huc6.huc6_id FROM huc6 WHERE huc6.huc6_id = %s """, (self.huc6_id,)) try: bbox = list(results)[0].bbox except IndexError: # this lake does not have a watershed return None return self._bbox_thumbnail_url(bbox) @property def basin_tile_url(self): """ Return the URL to the lakebasin tile thumbnail from the arcgis server """ # the magic 1000 here is from the original AOL too results = LakeGeom.objects.raw(""" SELECT Box2D(st_envelope(st_expand(the_geom,1000))) as bbox, reachcode FROM lake_geom where reachcode = %s """, (self.pk,)) bbox = results[0].bbox return self._bbox_thumbnail_url(bbox) def _bbox_thumbnail_url(self, bbox): """ Take a boundingbox string from postgis, for example: BOX(727773.25 1372170,829042.75 1430280.75) and build the URL to a tile of that bounding box in the arcgis server """ # extract out the numbers from the bbox, and comma separate them bbox = re.sub(r'[^0-9.-]', " ", bbox).split() bbox = ",".join(bbox) path = "export?bbox=%s&bboxSR=&layers=&layerdefs=&size=&imageSR=&format=jpg&transparent=false&dpi=&time=&layerTimeOptions=&f=image" return settings.TILE_URL + (path % bbox) @property def mussels(self): """ This queries the mussel DB for any mussels that are within a certain distance of this lake. It returns a comma separated string of the status of the mussels """ if not hasattr(self, "_mussels"): cursor = connection.cursor() cursor.execute(""" SELECT DISTINCT specie.name as species, date_checked, agency.name as agency FROM mussels.observation INNER JOIN mussels.specie USING(specie_id) INNER JOIN mussels.agency USING(agency_id) WHERE ST_BUFFER(ST_TRANSFORM(the_geom, 3644), %s) && (SELECT the_geom FROM lake_geom WHERE reachcode = %s) ORDER BY date_checked DESC """, (DISTANCE_FROM_ITEM, self.pk)) results = [] for row in cursor: results.append({ "species": row[0], "date_checked": row[1], "source": row[2] }) self._mussels = results return self._mussels
class PublicBody(models.Model): name = models.CharField(_("Name"), max_length=255) other_names = models.TextField(_("Other names"), default="", blank=True) slug = models.SlugField(_("Slug"), max_length=255) description = models.TextField(_("Description"), blank=True) url = models.URLField(_("URL"), null=True, blank=True, max_length=500) parent = models.ForeignKey('PublicBody', null=True, blank=True, default=None, on_delete=models.SET_NULL, related_name="children") root = models.ForeignKey('PublicBody', null=True, blank=True, default=None, on_delete=models.SET_NULL, related_name="descendants") depth = models.SmallIntegerField(default=0) classification = models.ForeignKey(Classification, null=True, blank=True, on_delete=models.SET_NULL) email = models.EmailField(_("Email"), blank=True, default='') fax = models.CharField(max_length=50, blank=True) contact = models.TextField(_("Contact"), blank=True) address = models.TextField(_("Address"), blank=True) website_dump = models.TextField(_("Website Dump"), null=True, blank=True) request_note = models.TextField(_("request note"), blank=True) file_index = models.CharField(_("file index"), max_length=1024, blank=True) org_chart = models.CharField(_("organisational chart"), max_length=1024, blank=True) _created_by = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("Created by"), blank=True, null=True, related_name='public_body_creators', on_delete=models.SET_NULL) _updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("Updated by"), blank=True, null=True, related_name='public_body_updaters', on_delete=models.SET_NULL) created_at = models.DateTimeField(_("Created at"), default=timezone.now) updated_at = models.DateTimeField(_("Updated at"), default=timezone.now) confirmed = models.BooleanField(_("confirmed"), default=True) number_of_requests = models.IntegerField(_("Number of requests"), default=0) site = models.ForeignKey(Site, verbose_name=_("Site"), null=True, on_delete=models.SET_NULL, default=settings.SITE_ID) wikidata_item = models.CharField(max_length=50, blank=True) jurisdiction = models.ForeignKey(Jurisdiction, verbose_name=_('Jurisdiction'), blank=True, null=True, on_delete=models.SET_NULL) geo = models.PointField(null=True, blank=True, geography=True) regions = models.ManyToManyField(GeoRegion, blank=True) laws = models.ManyToManyField( FoiLaw, verbose_name=_("Freedom of Information Laws")) tags = TaggableManager(through=TaggedPublicBody, blank=True) categories = TaggableManager(through=CategorizedPublicBody, verbose_name=_("categories"), blank=True) non_filtered_objects = models.Manager() objects = PublicBodyManager() published = objects class Meta: ordering = ('name', ) verbose_name = _("Public Body") verbose_name_plural = _("Public Bodies") serializable_fields = ('id', 'name', 'slug', 'request_note_html', 'description', 'url', 'email', 'contact', 'address', 'domain', 'number_of_requests') def __str__(self): return self.name @property def created_by(self): return self._created_by @property def updated_by(self): return self._updated_by @property def domain(self): if self.url and self.url.count('/') > 1: return self.url.split("/")[2] return None @property def all_names(self): names = [self.name, self.other_names] if self.jurisdiction: names.extend([self.jurisdiction.name, self.jurisdiction.slug]) return ' '.join(names) @property def request_note_html(self): return markdown(self.request_note) @property def tag_list(self): return edit_string_for_tags(self.tags.all()) @property def default_law(self): # FIXME: Materialize this? return self.get_applicable_law() def get_applicable_law(self, law_type=None): return get_applicable_law(pb=self, law_type=law_type) def get_absolute_url(self): return reverse('publicbody-show', kwargs={"slug": self.slug}) def get_absolute_short_url(self): return reverse('publicbody-publicbody_shortlink', kwargs={'obj_id': self.pk}) def get_absolute_domain_url(self): return "%s%s" % (settings.SITE_URL, self.get_absolute_url()) def get_absolute_domain_short_url(self): return "%s%s" % (settings.SITE_URL, self.get_absolute_short_url()) def get_mediator(self): law = self.default_law if law is None: return None return law.mediator def get_label(self): return mark_safe( '%(name)s - <a href="%(url)s" target="_blank" ' 'class="info-link">%(detail)s</a>' % { "name": escape(self.name), "url": self.get_absolute_url(), "detail": _("More Info") }) def as_data(self, request=None): from .api_views import PublicBodyListSerializer if request is None: ctx = get_fake_api_context() else: ctx = {'request': request} return PublicBodyListSerializer(self, context=ctx).data @property def children_count(self): return len(PublicBody.objects.filter(parent=self)) @classmethod def export_csv(cls, queryset): fields = ("id", "name", "email", "fax", "contact", "address", "url", ('classification', lambda x: x.classification.name if x.classification else None), "jurisdiction__slug", ("categories", lambda x: edit_string_for_tags(x.categories.all())), "other_names", "website_dump", "description", "request_note", "parent__name", ('regions', lambda obj: ','.join(str(x.id) for x in obj.regions.all()))) return export_csv(queryset, fields)
class Subsidiary(WithGalleryMixin, models.Model): """Pobočka""" class Meta: verbose_name = _("Pobočka organizace") verbose_name_plural = _("Pobočky organizací") address = Address() company = ChainedForeignKey( "Company", related_name="subsidiaries", null=False, blank=False, ) city = models.ForeignKey( City, verbose_name=_("Soutěžní město"), help_text=_( "Váš tým se zařadí do žebříčků za nejbližší soutěžní město."), null=False, blank=False, on_delete=models.CASCADE, ) active = models.BooleanField( verbose_name=_("Aktivní"), default=True, null=False, ) box_addressee_name = models.CharField( verbose_name=_("Jméno adresáta krabice pro pobočku"), help_text= _("Jmené osoby, která převezme krabici s tričky a zajistí jeich rozdělení na této pobočce. Nemusí se účastnit soutěže." ), max_length=30, null=True, blank=True, ) box_addressee_telephone = models.CharField( verbose_name=_("Telefon adresáta krabice pro pobočku"), max_length=30, null=True, blank=True, ) box_addressee_email = models.EmailField( verbose_name=_("Email adresáta krabice pro pobočku"), null=True, blank=True, ) gallery = models.ForeignKey( "photologue.Gallery", verbose_name=_("Galerie fotek"), null=True, blank=True, on_delete=models.CASCADE, ) icon = models.ForeignKey( "photologue.Photo", verbose_name=_("Ikona"), null=True, blank=True, on_delete=models.SET_NULL, ) objects = models.Manager() active_objects = ActiveManager() def __str__(self): return "%s - %s" % (get_address_string(self.address), self.city) def name(self): return get_address_string(self.address) def get_recipient_string(self): """makes recipient from address_recipient and company name""" if self.address_recipient: if (self.address_recipient.lower().strip() == self.company.name.lower().strip()): return self.address_recipient else: return "%s (%s)" % (self.address_recipient, self.company.name) else: return self.company.name def clean(self): Address.clean(self.address, self, Subsidiary) if (self.box_addressee_name or self.box_addressee_email or self.box_addressee_telephone): if not self.box_addressee_name: raise ValidationError({ "box_addressee_name": _("Pokud vyplňujete adresáta krabice, vyplňte prosím i jeho jméno" ) }) if not self.box_addressee_email: raise ValidationError({ "box_addressee_email": _("Pokud vyplňujete adresáta krabice, vyplňte prosím i jeho e-mail" ) }) if not self.box_addressee_telephone: raise ValidationError({ "box_addressee_telephone": _("Pokud vyplňujete adresáta krabice, vyplňte prosím i jeho telefon" ) })
class BusStation(models.Model): name = models.CharField(max_length=20) location = models.PointField(srid=4326,null=True,blank=True) address = models.CharField(max_length=50) bus_station_id=models.CharField(max_length=10) objects=models.Manager()
class AbstractObservation(models.Model): originates_in_vespawatch = models.BooleanField( default=True, help_text= "The observation was first created in VespaWatch, not iNaturalist") taxon = models.ForeignKey(Taxon, on_delete=models.PROTECT, blank=True, null=True) observation_time = models.DateTimeField(verbose_name=_("Observation date"), validators=[no_future]) comments = models.TextField( verbose_name=_("Comments"), blank=True, help_text= _("Comments are public: use them to describe your observation and help verification." )) latitude = models.FloatField( validators=[MinValueValidator(-90), MaxValueValidator(90)], verbose_name=_("Latitude")) longitude = models.FloatField( validators=[MinValueValidator(-180), MaxValueValidator(180)], verbose_name=_("Longitude")) inaturalist_id = models.BigIntegerField(verbose_name=_("iNaturalist ID"), blank=True, null=True) inaturalist_species = models.CharField( verbose_name=_("iNaturalist species"), max_length=100, blank=True, null=True) # TODO: check if this is still in use or useful inat_vv_confirmed = models.BooleanField( blank=True, null=True) # The community ID of iNaturalist says it's Vespa Velutina # Observer info observer_name = models.CharField(verbose_name=_("Name"), max_length=255, blank=True, null=True) observer_email = models.EmailField(verbose_name=_("Email address"), blank=True, null=True) observer_phone = models.CharField(verbose_name=_("Telephone number"), max_length=20, blank=True, null=True) created_at = models.DateTimeField(default=timezone.now) # Managers objects = models.Manager() # The default manager. from_inat_objects = InatCreatedObservationsManager() from_vespawatch_objects = VespawatchCreatedObservationsManager() new_vespawatch_objects = VespawatchNewlyCreatedObservationsManager() class Meta: abstract = True # We got some duplicates and don't exactly know why, this is an attempt to block them without being too # aggresive and introduce bugs (hence the limited number of fields). unique_together = [ 'taxon', 'observation_time', 'latitude', 'longitude', 'comments', 'inaturalist_id' ] @property def vernacular_names_in_all_languages(self): """Returns a dict such as: {'en': XXXX, 'nl': YYYY}""" vn = {} for lang in settings.LANGUAGES: code = lang[0] vn[code] = getattr(self.taxon, f'vernacular_name_{code}') return vn @property def display_vernacular_name(self): if self.taxon: return _(self.taxon.vernacular_name) else: return '' @property def display_scientific_name(self): if self.taxon: return self.taxon.name else: return self.inaturalist_species or _('Unknown') @property def can_be_edited_in_admin(self): if self.originates_in_vespawatch: if self.exists_in_inaturalist: return False else: return True else: # Comes from iNaturalist: we can never delete return False @property def can_be_edited_or_deleted(self): """Return True if this observation can be edited in Vespa-Watch (admin, ...)""" return self.originates_in_vespawatch # We can't edit obs that comes from iNaturalist (they're never pushed). @property def taxon_can_be_locally_changed(self): if self.originates_in_vespawatch and self.exists_in_inaturalist: return False # Because we rely on community: info is always pulled and never pushed return True @property def exists_in_inaturalist(self): return self.inaturalist_id is not None @property def inaturalist_obs_url(self): if self.exists_in_inaturalist: return f'https://www.inaturalist.org/observations/{self.inaturalist_id}' return None def has_warnings(self): return len(self.warnings.all()) > 0 has_warnings.boolean = True def _params_for_inat(self): """(Create/update): Common ground for the pushed data to iNaturalist. taxon_id is not part of it because we rely on iNaturalist to correct the identification, if necessary. All the rest is pushed. """ vespawatch_evidence_value = 'nest' if self.__class__ == Nest else 'individual' ofv = [{ 'observation_field_id': settings.VESPAWATCH_ID_OBS_FIELD_ID, 'value': self.pk }, { 'observation_field_id': settings.VESPAWATCH_EVIDENCE_OBS_FIELD_ID, 'value': vespawatch_evidence_value }] if vespawatch_evidence_value == 'individual' and self.behaviour: ofv.append( { 'observation_field_id': settings.VESPAWATCH_BEHAVIOUR_OBS_FIELD_ID, 'value': self.get_behaviour_display() } ) # TODO: get_behaviour_display(): what will happen to push if we translate the values for the UI return { 'observed_on_string': self.observation_time.isoformat(), 'time_zone': 'Brussels', 'description': self.comments, 'latitude': self.latitude, 'longitude': self.longitude, 'observation_field_values_attributes': [{ 'observation_field_id': settings.VESPAWATCH_ID_OBS_FIELD_ID, 'value': self.pk }, { 'observation_field_id': settings.VESPAWATCH_EVIDENCE_OBS_FIELD_ID, 'value': vespawatch_evidence_value }] } def flag_warning(self, text): if text in [x.text for x in self.warnings.all()]: return # warning already set if self.__class__.__name__ == 'Nest': warning = NestObservationWarning(text=text, datetime=now(), observation=self) warning.save() elif self.__class__.__name__ == 'Individual': warning = IndividualObservationWarning(text=text, datetime=now(), observation=self) warning.save() def flag_based_on_inat_data(self, inat_observation_data): """ The observation was no longer found on iNaturalist with our general filters. Check why, and flag this observation """ # Project is vespawatch? if not settings.VESPAWATCH_PROJECT_ID in inat_observation_data[ 'project_ids']: self.flag_warning('not in vespawatch project') # Taxon known in VW? returned_taxon_id = '' if 'community_taxon_id' in inat_observation_data and inat_observation_data[ 'community_taxon_id']: returned_taxon_id = inat_observation_data['community_taxon_id'] elif 'taxon' in inat_observation_data: if 'id' in inat_observation_data['taxon']: returned_taxon_id = inat_observation_data['taxon']['id'] if returned_taxon_id not in [ y for x in Taxon.objects.all() for y in x.inaturalist_pull_taxon_ids ]: self.flag_warning('unknown taxon') def update_from_inat_data(self, inat_observation_data): # Check the vespawatch_evidence # ------ # If the observation is a nest but the vespawatch evidence is not nest => flag the nest if 'ofvs' in inat_observation_data: vw_evidence_list = [ x['value'] for x in inat_observation_data['ofvs'] if x['field_id'] == settings.VESPAWATCH_EVIDENCE_OBS_FIELD_ID ] if len(vw_evidence_list) > 0: vw_evidence = vw_evidence_list[0] if self.__class__.__name__ == 'Nest': if vw_evidence != 'nest': self.flag_warning('individual at inaturalist') # If the observation is an individual but the vespawatch evidence is a nest and the observation originates in vespawatch => delete the individual and create a nest elif self.__class__.__name__ == 'Individual': if vw_evidence == 'nest': if self.originates_in_vespawatch: self.flag_warning('nest at inaturalist') else: create_observation_from_inat_data( inat_observation_data) self.delete() return # Update taxon data and set inat_vv_confirmed (use inat_data_confirms_vv() ) self.inat_vv_confirmed = inat_data_confirms_vv(inat_observation_data) # Update photos # ------------- # When we pull again and the API returns additional images, those are not added. This is done # because we insert a UUID in the filename when we pull it. The result of that is that we cannot # compare that image with the image url that we retrieve from iNaturalist. So to prevent adding # the same image again and again with subsequent pulls, we only add images when the observation # has none. if len(self.pictures.all()) == 0: for photo in inat_observation_data['photos']: self.assign_picture_from_url(photo['url']) # Update location self.latitude = inat_observation_data['geojson']['coordinates'][1] self.longitude = inat_observation_data['geojson']['coordinates'][0] # Update time # ------------- observation_time = dateparser.parse( inat_observation_data['observed_on_string'], settings={'TIMEZONE': inat_observation_data['observed_time_zone']}) if observation_time is None: # Sometimes, dateparser doesn't understand the string but we have the bits and pieces in # inaturalist_data['observed_on_details'] details = inat_observation_data['observed_on_details'] observation_time = datetime( year=details['year'], month=details['month'], day=details['day'], hour=details['hour'] ) # in the observed cases, we had nothing more precise than the hour # Sometimes, the time is naive (even when specifying it to dateparser), because (for the detected cases, at least) # The time is 00:00:00. In that case we make it aware to avoid Django warnings (in the local time zone since all # observations occur in Belgium if is_naive(observation_time): # Some dates (apparently) observation_time = make_aware(observation_time) self.observation_time = observation_time self.comments = inat_observation_data['description'] or '' # Update taxon # ------------- try: self.inaturalist_species = '' taxon = get_taxon_from_inat_taxon_id( inat_observation_data['taxon']['id']) self.taxon = taxon except Taxon.DoesNotExist: self.taxon = None self.inaturalist_species = inat_observation_data['taxon'][ 'name'] if 'name' in inat_observation_data['taxon'] else '' self.save() def create_at_inaturalist(self, access_token, user_agent): """Creates a new observation at iNaturalist for this observation It will update the current object so self.inaturalist_id is properly set. On the other side, it will also set the vespawatch_id observation field so the observation can be found from the iNaturalist record. :param access_token: as returned by pyinaturalist.rest_api.get_access_token( """ params_only_for_create = { 'taxon_id': self.taxon.inaturalist_push_taxon_id } # TODO: with the new sync, does it still makes sense to separate the create/update parameters? params = { 'observation': { **params_only_for_create, **self._params_for_inat() } } r = create_observations(params=params, access_token=access_token, user_agent=user_agent) self.inaturalist_id = r[0]['id'] self.save() self.push_attached_pictures_at_inaturalist(access_token=access_token, user_agent=user_agent) def get_photo_filename(self, photo_url): # TODO: Find a cleaner solution to this # It seems the iNaturalist only returns small thumbnails such as # 'https://static.inaturalist.org/photos/1960816/square.jpg?1444437211' # We can circumvent the issue by hacking the URL... photo_url = photo_url.replace('square.jpg', 'large.jpg') photo_url = photo_url.replace('square.jpeg', 'large.jpeg') photo_filename = photo_url[photo_url.rfind("/") + 1:].split('?', 1)[0] return photo_filename def assign_picture_from_url(self, photo_url): photo_filename = self.get_photo_filename(photo_url) if photo_filename not in [x.image.name for x in self.pictures.all()]: if self.__class__ == Nest: photo_obj = NestPicture() else: photo_obj = IndividualPicture() photo_content = ContentFile(requests.get(photo_url).content) photo_obj.observation = self photo_obj.image.save(photo_filename, photo_content) photo_obj.save() def push_attached_pictures_at_inaturalist(self, access_token, user_agent): if self.inaturalist_id: for picture in self.pictures.all(): add_photo_to_observation(observation_id=self.inaturalist_id, file_object=picture.image.read(), access_token=access_token, user_agent=user_agent) def get_taxon_name(self): if self.taxon: return self.taxon.name else: return '' @property def formatted_observation_date(self): # We need to be aware of the timezone, hence the defaultfilter trick return defaultfilters.date(self.observation_time, 'Y-m-d') @property def observation_time_iso(self): return self.observation_time.isoformat() def save(self, *args, **kwargs): # Let's make sure model.clean() is called on each save(), for validation self.full_clean() return super(AbstractObservation, self).save(*args, **kwargs) def delete(self, *args, **kwargs): if self.originates_in_vespawatch and self.exists_in_inaturalist: InatObsToDelete.objects.create(inaturalist_id=self.inaturalist_id) return super(AbstractObservation, self).delete(*args, **kwargs)
class Sequence(models.Model): unique_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) user = models.ForeignKey(UserModel, on_delete=models.CASCADE) camera_make = models.ForeignKey(CameraMake, on_delete=models.CASCADE, null=True, blank=True) captured_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(null=True, blank=True) seq_key = models.CharField( max_length=100, null=True, blank=True, verbose_name="Mapillary Sequence Key", ) pano = models.BooleanField(default=False) user_key = models.CharField( max_length=100, null=True, blank=True, verbose_name="Mapillary User Key", ) username = models.CharField( max_length=100, null=True, blank=True, verbose_name="Mapillary Username", ) geometry_coordinates = models.LineStringField(null=True, blank=True, srid=4326) geometry_coordinates_ary = ArrayField(ArrayField( models.FloatField(default=1)), null=True, blank=True) coordinates_cas = ArrayField(models.FloatField(default=0), null=True, blank=True) coordinates_image = ArrayField(models.CharField(default='', max_length=100), null=True, blank=True) is_uploaded = models.BooleanField(default=False) is_private = models.BooleanField(default=False) updated_at = models.DateTimeField(default=datetime.now, blank=True) name = models.CharField(max_length=100, default='') description = models.TextField(default='') transport_type = models.ForeignKey(TransType, on_delete=models.CASCADE, null=True) tag = models.ManyToManyField(Tag) image_count = models.IntegerField(default=0) is_mapillary = models.BooleanField(default=True) is_published = models.BooleanField(default=True) is_image_download = models.BooleanField(default=False) is_map_feature = models.BooleanField(default=False) google_street_view = models.BooleanField(default=False) strava = models.CharField(max_length=50, null=True, blank=True) distance = models.FloatField(null=True, blank=True) objects = models.Manager() vector_tiles = CustomSequenceMVTManager( geo_col='geometry_coordinates', select_columns=['seq_key', 'unique_id'], is_show_id=False, source_layer='mtp-sequences') def get_absolute_url(self): from django.urls import reverse return reverse('sequence.sequence_detail', kwargs={'unique_id': str(self.unique_id)}) def get_image_count(self): if self.coordinates_image is not None: return len(self.coordinates_image) else: return 0 def get_tag_str(self): tags = [] if self.tag.all().count() == 0: return '' for tag in self.tag.all(): if tag and tag.is_actived: tags.append(tag.name) if len(tags) > 0: return ', '.join(tags) else: return '' def get_tags(self): tags = [] if self.tag.all().count() == 0: return '' for tag in self.tag.all(): if tag and tag.is_actived: tags.append(tag.name) return tags def get_short_description(self): description = self.description if len(description) > 100: return description[0:100] + '...' else: return description def get_first_image_key(self): if len(self.coordinates_image) > 0: return self.coordinates_image[0] else: return '' def get_mapillary_username(self): return self.username def get_like_count(self): liked_guidebook = SequenceLike.objects.filter(sequence=self) if not liked_guidebook: return 0 else: return liked_guidebook.count() def get_distance(self): all_distance = 0 if self.geometry_coordinates_ary is None: return all_distance if len(self.geometry_coordinates_ary) > 0: first_point = self.geometry_coordinates_ary[0] for i in range(len(self.geometry_coordinates_ary) - 1): if i == 0: continue second_point = self.geometry_coordinates_ary[i] d = distance(first_point, second_point) first_point = second_point all_distance += d all_distance = "%.3f" % all_distance return all_distance def get_cover_image(self): image_keys = self.coordinates_image if image_keys is None: return None if len(image_keys) > 0: return image_keys[0] else: return None def get_first_point_lat(self): if self.geometry_coordinates_ary is None: return None lat = self.geometry_coordinates_ary[0][1] return lat def get_first_point_lng(self): if self.geometry_coordinates_ary is None: return None lng = self.geometry_coordinates_ary[0][0] return lng
def get_manager(schema): m = models.Manager().db_manager(schema) # print('Created Manager On DB: {} {}'.format(schema, m.uuid)) return m
class Image(models.Model): unique_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) user = models.ForeignKey(UserModel, on_delete=models.CASCADE) camera_make = models.ForeignKey(CameraMake, on_delete=models.CASCADE, null=True, blank=True) camera_model = models.ForeignKey(CameraModel, on_delete=models.CASCADE, null=True, blank=True) cas = models.FloatField(default=0) captured_at = models.DateTimeField(null=True, blank=True) sequence = models.ForeignKey(Sequence, on_delete=models.CASCADE, null=True, blank=True) seq_key = models.CharField(max_length=100, default='') image_key = models.CharField(max_length=100) pano = models.BooleanField(default=False) user_key = models.CharField(max_length=100, default='') username = models.CharField(max_length=100, default='') organization_key = models.CharField(max_length=255, null=True) is_uploaded = models.BooleanField(default=False) is_private = models.BooleanField(default=False) is_mapillary = models.BooleanField(default=True) lat = models.FloatField(default=0) lng = models.FloatField(default=0) ele = models.FloatField(default=0) type = models.CharField(max_length=50, default='Point') point = models.PointField(null=True, blank=True) mapillary_image = models.ImageField(upload_to=image_directory_path, null=True, blank=True) image_label = models.ManyToManyField(LabelType, through='ImageLabel') map_feature_keys = ArrayField(ArrayField(models.CharField(max_length=50)), null=True, blank=True) map_feature_values = ArrayField(ArrayField( models.CharField(max_length=50)), null=True, blank=True) objects = models.Manager() vector_tiles = CustomImageMVTManager( geo_col='point', select_columns=['image_key', 'unique_id'], is_show_id=False, source_layer='mtp-images') def get_sequence_by_key(self): if self.seq_key != '': sequence = Sequence.objects.get(seq_key=self.seq_key) if sequence is None or not sequence: return None return sequence return None
class KadastraalObject(models.Model): id = models.CharField(max_length=60, primary_key=True) aanduiding = models.CharField(max_length=17) kadastrale_gemeente = models.ForeignKey(KadastraleGemeente, related_name="kadastrale_objecten", on_delete=models.CASCADE) sectie = models.ForeignKey(KadastraleSectie, related_name="kadastrale_objecten", on_delete=models.CASCADE) date_modified = models.DateTimeField(auto_now=True) perceelnummer = models.IntegerField() indexletter = models.CharField(max_length=1) indexnummer = models.IntegerField() soort_grootte = models.ForeignKey(SoortGrootte, null=True, on_delete=models.CASCADE) grootte = models.IntegerField(null=True) koopsom = models.IntegerField(null=True) koopsom_valuta_code = models.CharField(max_length=50, null=True) koopjaar = models.CharField(max_length=15, null=True) meer_objecten = models.NullBooleanField(default=None) cultuurcode_onbebouwd = models.ForeignKey(CultuurCodeOnbebouwd, null=True, on_delete=models.CASCADE) cultuurcode_bebouwd = models.ForeignKey(CultuurCodeBebouwd, null=True, on_delete=models.CASCADE) register9_tekst = models.TextField() status_code = models.CharField(max_length=50) toestandsdatum = models.DateField(null=True) voorlopige_kadastrale_grens = models.NullBooleanField(default=None) in_onderzoek = models.TextField(null=True) poly_geom = geo.MultiPolygonField(srid=SRID_RD, null=True) point_geom = geo.PointField(srid=SRID_RD, null=True) voornaamste_gerechtigde = models.ForeignKey(Eigenaar, null=True, on_delete=models.CASCADE) verblijfsobjecten = models.ManyToManyField( bag.Verblijfsobject, through='KadastraalObjectVerblijfsobjectRelatie', related_name="kadastrale_objecten") g_percelen = models.ManyToManyField('KadastraalObject', through=APerceelGPerceelRelatie, through_fields=('a_perceel', 'g_perceel'), related_name="a_percelen") buurten = models.ManyToManyField(bag.Buurt, through='EigendomBuurt', through_fields=('kadastraal_object', 'buurt'), related_name="eigendommen") wijken = models.ManyToManyField(bag.Buurtcombinatie, through='EigendomWijk', through_fields=('kadastraal_object', 'buurt_combi'), related_name="eigendommen") ggws = models.ManyToManyField(bag.Gebiedsgerichtwerken, through='EigendomGGW', through_fields=('kadastraal_object', 'ggw'), related_name="eigendommen") stadsdelen = models.ManyToManyField(bag.Stadsdeel, through='EigendomStadsdeel', through_fields=('kadastraal_object', 'stadsdeel'), related_name="eigendommen") objects = geo.Manager() class Meta: managed = False ordering = ('kadastrale_gemeente__id', 'sectie', 'perceelnummer', '-indexletter', 'indexnummer') def __str__(self): return self.get_aanduiding_spaties() def get_aanduiding_spaties(self): return kadaster.get_aanduiding_spaties(self.kadastrale_gemeente.id, self.sectie.sectie, self.perceelnummer, self.indexletter, self.indexnummer)
class NonStandardManager(models.Model): name = models.CharField(max_length=30) manager = models.Manager()
class Hearing(StringIdBaseModel, TranslatableModel): open_at = models.DateTimeField(verbose_name=_('opening time'), default=timezone.now) close_at = models.DateTimeField(verbose_name=_('closing time'), default=timezone.now) force_closed = models.BooleanField(verbose_name=_('force hearing closed'), default=False) translations = TranslatedFields( title=models.CharField(verbose_name=_('title'), max_length=255), borough=models.CharField(verbose_name=_('borough'), blank=True, default='', max_length=200), ) servicemap_url = models.CharField(verbose_name=_('service map URL'), default='', max_length=255, blank=True) geojson = GeoJSONField(blank=True, null=True, verbose_name=_('area')) geometry = models.GeometryField(blank=True, null=True, verbose_name=_('area geometry')) organization = models.ForeignKey(Organization, verbose_name=_('organization'), related_name="hearings", blank=True, null=True, on_delete=models.PROTECT) labels = models.ManyToManyField("Label", verbose_name=_('labels'), blank=True) followers = models.ManyToManyField( settings.AUTH_USER_MODEL, verbose_name=_('followers'), help_text=_('users who follow this hearing'), related_name='followed_hearings', blank=True, editable=False) slug = AutoSlugField( verbose_name=_('slug'), populate_from='title', editable=True, unique=True, blank=True, help_text=_( 'You may leave this empty to automatically generate a slug')) n_comments = models.IntegerField(verbose_name=_('number of comments'), blank=True, default=0, editable=False) contact_persons = models.ManyToManyField(ContactPerson, verbose_name=_('contact persons'), related_name='hearings') project_phase = models.ForeignKey(ProjectPhase, verbose_name=_('project phase'), related_name='hearings', on_delete=models.PROTECT, null=True, blank=True) objects = BaseModelManager.from_queryset(HearingQueryset)() original_manager = models.Manager() class Meta: verbose_name = _('hearing') verbose_name_plural = _('hearings') def __str__(self): return (self.title or self.id) @property def closed(self): return self.force_closed or not (self.open_at <= now() <= self.close_at) def check_commenting(self, request): if self.closed: raise ValidationError( _("%s is closed and does not allow comments anymore") % self, code="hearing_closed") def check_voting(self, request): if self.closed: raise ValidationError( _("%s is closed and does not allow voting anymore") % self, code="hearing_closed") @property def preview_code(self): if not self.pk: return None return get_hmac_b64_encoded(self.pk) @property def preview_url(self): if not (self.preview_code and hasattr(settings, 'DEMOCRACY_UI_BASE_URL')): return None url = urljoin(settings.DEMOCRACY_UI_BASE_URL, '/hearing/%s/?preview=%s' % (self.pk, self.preview_code)) return url def save(self, *args, **kwargs): slug_field = self._meta.get_field('slug') # we need to manually use autoslug utils here with ModelManager, because automatic slug populating # uses our default manager, which can lead to a slug collision between this and a deleted hearing self.slug = generate_unique_slug(slug_field, self, self.slug, Hearing.original_manager) self.geometry = get_geometry_from_geojson(self.geojson) super().save(*args, **kwargs) def recache_n_comments(self): new_n_comments = (self.sections.all().aggregate( Sum('n_comments')).get('n_comments__sum') or 0) if new_n_comments != self.n_comments: self.n_comments = new_n_comments self.save(update_fields=("n_comments", )) def get_main_section(self): try: return self.sections.get(type__identifier=InitialSectionType.MAIN) except ObjectDoesNotExist: return None def is_visible_for(self, user): if self.published and self.open_at < now(): return True if not user.is_authenticated: return False if user.is_superuser: return True user_organization = user.get_default_organization() if not (user_organization and self.organization): return False return self.organization in user.admin_organizations.all() def soft_delete(self, using=None): # we want deleted hearings to give way to new ones, the original slug from a deleted hearing # is now free to use self.slug += '-deleted' self.save() super().soft_delete(using=using)
class Station(Gpoint): owner = models.ForeignKey( Lentity, related_name="owned_stations", on_delete=models.CASCADE, verbose_name=pgettext_lazy("Entity that owns the station", "Owner"), ) start_date = models.DateField( null=True, blank=True, verbose_name=pgettext_lazy("Station start date", "Start date"), ) end_date = models.DateField( null=True, blank=True, verbose_name=pgettext_lazy("Station end date", "End date"), ) overseer = models.CharField(max_length=30, blank=True, verbose_name=_("Overseer")) # The following two fields are only useful when USERS_CAN_ADD_CONTENT # is set. creator = models.ForeignKey( User, null=True, blank=True, related_name="created_stations", on_delete=models.CASCADE, verbose_name=pgettext_lazy( "User who has full permissions on station", "Administrator" ), ) maintainers = models.ManyToManyField( User, blank=True, related_name="maintaining_stations", verbose_name=_("Maintainers"), ) objects = models.Manager() on_site = CurrentSiteManager() f_dependencies = ["Gpoint"] class Meta: verbose_name = _("Station") verbose_name_plural = _("Stations") @property def last_update_naive(self): def get_last_update_naive(): result = self.last_update if result is not None: result = result.replace(tzinfo=None) return result return cache.get_or_set( f"station_last_update_naive_{self.id}", get_last_update_naive ) @property def last_update(self): def get_last_update(): from .timeseries import Timeseries timeseries = Timeseries.objects.filter(timeseries_group__gentity_id=self.id) result = None for t in timeseries: t_end_date = t.end_date if t_end_date is None: continue if result is None or t_end_date > result: result = t_end_date return result return cache.get_or_set(f"station_last_update_{self.id}", get_last_update)
class Feature(models.Model): STATUS_CHOICES = ( ("draft", "Brouillon"), ("pending", "En attente de publication"), ("published", "Publié"), ("archived", "Archivé"), ) feature_id = models.UUIDField( "Identifiant", primary_key=True, editable=False, default=uuid.uuid4) title = models.CharField("Titre", max_length=128, null=True, blank=True) description = models.TextField("Description", blank=True, null=True) status = models.CharField( "Statut", choices=STATUS_CHOICES, max_length=50, default="draft") created_on = models.DateTimeField("Date de création", null=True, blank=True) updated_on = models.DateTimeField("Date de maj", null=True, blank=True) archived_on = models.DateField( "Date d'archivage automatique", null=True, blank=True) deletion_on = models.DateField( "Date de suppression automatique", null=True, blank=True) creator = models.ForeignKey( to=settings.AUTH_USER_MODEL, verbose_name="Créateur", on_delete=models.SET_NULL, null=True, blank=True) project = models.ForeignKey("geocontrib.Project", on_delete=models.CASCADE) feature_type = models.ForeignKey("geocontrib.FeatureType", on_delete=models.CASCADE) geom = models.GeometryField("Géométrie", srid=4326) feature_data = JSONField(blank=True, null=True) objects = models.Manager() handy = AvailableFeaturesManager() class Meta: verbose_name = "Signalement" verbose_name_plural = "Signalements" def clean(self): if self.feature_data and not isinstance(self.feature_data, dict): raise ValidationError('Format de donnée invalide') def save(self, *args, **kwargs): if self._state.adding is True: self.created_on = timezone.now() self.updated_on = timezone.now() super().save(*args, **kwargs) def __str__(self): return str(self.title) def get_absolute_url(self): return reverse('geocontrib:feature_update', kwargs={ 'slug': self.project.slug, 'feature_type_slug': self.feature_type.slug, 'feature_id': self.feature_id}) def get_view_url(self): return reverse('geocontrib:feature_detail', kwargs={ 'slug': self.project.slug, 'feature_type_slug': self.feature_type.slug, 'feature_id': self.feature_id}) @property def custom_fields_as_list(self): CustomField = apps.get_model(app_label='geocontrib', model_name="CustomField") custom_fields = CustomField.objects.filter(feature_type=self.feature_type) res = [] if custom_fields.exists(): for row in custom_fields.order_by('position').values('name', 'label', 'field_type'): value = '' if isinstance(self.feature_data, dict): value = self.feature_data.get(row['name']) res.append({ 'label': row['label'], 'field_type': row['field_type'], 'value': value }) return res @property def display_creator(self): res = "Utilisateur supprimé" if self.creator: res = self.creator.get_full_name() or self.creator.username return res
class Shop(models.Model): objects = models.Manager() name = models.CharField(max_length=100) location = models.PointField() #represents a pair of longitude and latitude coordinates. address = models.CharField(max_length=100) city = models.CharField(max_length=50)