class Person(models.Model): gender = models.CharField(max_length=1, choices=GENDER_CH) happy = models.BooleanField(default=True) unhappy = models.BooleanField(default=False) bipolar = models.BooleanField(default=False) name = models.CharField(max_length=30) nickname = models.SlugField(max_length=36) age = models.IntegerField() bio = models.TextField() birthday = models.DateField() birth_time = models.TimeField() appointment = models.DateTimeField() blog = models.URLField() occupation = models.CharField(max_length=10, choices=OCCUPATION_CHOCIES) try: uuid = models.UUIDField(primary_key=False) except AttributeError: # New at Django 1.9 pass try: name_hash = models.BinaryField(max_length=16) except AttributeError: # We can't test the binary field if it is not supported # (django < 1,6) pass try: from django.contrib.postgres.fields import ArrayField acquaintances = ArrayField(models.IntegerField()) except ImportError: # New at Django 1.9 pass try: from django.contrib.postgres.fields import JSONField data = JSONField() except ImportError: # New at Django 1.9 pass try: from django.contrib.postgres.fields import HStoreField hstore_data = HStoreField() except ImportError: # New at Django 1.8 pass # backward compatibility with Django 1.1 try: wanted_games_qtd = models.BigIntegerField() except AttributeError: wanted_games_qtd = models.IntegerField() try: from django.contrib.postgres.fields.citext import CICharField, CIEmailField, CITextField ci_char = CICharField(max_length=30) ci_email = CIEmailField() ci_text = CITextField() except ImportError: # New at Django 1.11 pass try: duration_of_sleep = models.DurationField() except AttributeError: pass if MOMMY_GIS: geom = models.GeometryField() point = models.PointField() line_string = models.LineStringField() polygon = models.PolygonField() multi_point = models.MultiPointField() multi_line_string = models.MultiLineStringField() multi_polygon = models.MultiPolygonField() geom_collection = models.GeometryCollectionField()
class LegFile(TimestampedModelMixin, models.Model): key = models.IntegerField(primary_key=True) id = models.CharField(max_length=100, null=True) contact = models.CharField(max_length=1000, default="No contact") controlling_body = models.CharField(max_length=1000) date_scraped = models.DateTimeField(auto_now_add=True, null=True, blank=True) last_scraped = models.DateTimeField(auto_now=True) final_date = models.DateField(null=True) intro_date = models.DateField(default=datetime.datetime.now) sponsors = models.ManyToManyField(CouncilMember, related_name='legislation') status = models.CharField(max_length=1000) title = models.TextField() type = models.CharField(max_length=1000) url = models.URLField() version = models.CharField(max_length=100) is_routine = models.BooleanField(default=True, blank=True) class Meta: ordering = ['-key'] def __unicode__(self): return "%s %s: %s%s" % (self.type, self.id, self.title[:100], '...' if len(self.title) > 100 else '') @models.permalink def get_absolute_url(self): return ('legislation_detail', [str(self.pk)]) @property def last_action_date(self): """ Gets the date of the latest action """ actions = self.actions.all() if len(actions) == 0: return None return max([action.date_taken for action in actions]) @property def timeline(self): """ Gets a timeline object that represents the legfile actions grouped by ``date_taken``. """ from collections import defaultdict class LegActionTimeline(defaultdict): def __init__(self): super(LegActionTimeline, self).__init__(list) def __iter__(self): return iter(sorted(self.keys())) timeline = LegActionTimeline() for action in self.actions.all().order_by('date_taken'): print action.date_taken timeline[action.date_taken].append(action) return timeline def all_text(self): if not hasattr(self, '_all_text'): att_text = [att.fulltext for att in self.attachments.all()] self._all_text = ' '.join([self.title] + att_text) return self._all_text def unique_words(self): """ Gets all the white-space separated words in the file. A word is anything that starts and ends with word characters and has no internal white space. """ # Get rid of any punctuation on the outside of words. only_words = re.sub(r'(\s\W+|\W+\s|\W+$)', ' ', self.title) # Pick out and return the unique values by spliting on whitespace, and # lowercasing everything. unique_words = set(word.lower() for word in only_words.split()) return unique_words def addresses(self): addresses = ebdata.nlp.addresses.parse_addresses(self.all_text()) return addresses def topics(self): return settings.TOPIC_CLASSIFIER(self.title) def get_status_label(self): if self.status in [ 'Adopted', 'Approved', 'Direct Introduction', 'Passed' ]: return 'label-success' elif self.status in ['Failed to Pass', 'Vetoed']: return 'label-important' else: return 'label-inverse' def mentioned_legfiles(self): """ Gets a generator for any files (specifically, bills) mentioned in the file. """ # Find all the strings that match the characteristic regular expression # for a bill id. id_matches = re.findall(r'\s(\d{6}(-A+)?)', self.title) # The id matches may each have two groups (the second of which will # contain only the A's). We only care about the first. mentioned_legfile_ids = set(groups[0] for groups in id_matches) for mentioned_legfile_id in mentioned_legfile_ids: # It's possible that no legfile in our database may match the id # we've parsed out. When this is the case, there's nothing we can # do about it, so just fail "silently" (with a log message). try: mentioned_legfile = LegFile.objects.get( id=mentioned_legfile_id) yield mentioned_legfile except LegFile.DoesNotExist: # TODO: Use a log message. print 'LegFile %r, referenced from key %s, does not exist!!!' % ( mentioned_legfile_id, self.pk) def update(self, attribs, commit=True, **save_kwargs): for attr, val in attribs.items(): setattr(self, attr, val) if commit: return self.save(**save_kwargs) def save(self, update_words=True, update_mentions=True, update_locations=True, update_topics=True, *args, **kwargs): """ Calls the default ``Models.save()`` method, and creates or updates metadata for the legislative file as well. """ super(LegFile, self).save(*args, **kwargs) metadata = LegFileMetaData.objects.get_or_create(legfile=self)[0] if update_words: # Add the unique words to the metadata metadata.words.clear() unique_words = self.unique_words() for word in unique_words: md_word = MetaData_Word.objects.get_or_create(value=word)[0] metadata.words.add(md_word) if update_locations: # Add the unique locations to the metadata metadata.locations.clear() locations = self.addresses() for location in locations: try: md_location = MetaData_Location.objects.get_or_create( address=location[0])[0] except MetaData_Location.CouldNotBeGeocoded: continue metadata.locations.add(md_location) if update_mentions: # Add the mentioned files to the metadata metadata.mentioned_legfiles.clear() for mentioned_legfile in self.mentioned_legfiles(): metadata.mentioned_legfiles.add(mentioned_legfile) if update_topics: # Add topics to the metadata metadata.topics.clear() for topic in self.topics(): t = MetaData_Topic.objects.get_or_create(topic=topic)[0] metadata.topics.add(t) metadata.save() def get_data_source(self): return PhillyLegistarSiteWrapper() def refresh(self, stale_time=datetime.timedelta(days=1), force=False): """ Update the file if it has not been updated in a while. The "while" is dictated the `stale_time` parameter, a `timedelta`. If `force` is True, then the refresh will happen immediately, regardless of the time it was last updated. """ pass
class TouristicEvent(AddPropertyMixin, PublishableMixin, MapEntityMixin, StructureRelated, PicturesMixin, TimeStampedModelMixin, NoDeleteMixin): """ A touristic event (conference, workshop, etc.) in the park """ description_teaser = models.TextField( verbose_name=_(u"Description teaser"), blank=True, help_text=_(u"A brief summary"), db_column='chapeau') description = models.TextField(verbose_name=_(u"Description"), blank=True, db_column='description', help_text=_(u"Complete description")) themes = models.ManyToManyField(Theme, related_name="touristic_events", db_table="t_r_evenement_touristique_theme", blank=True, verbose_name=_(u"Themes"), help_text=_(u"Main theme(s)")) geom = models.PointField(verbose_name=_(u"Location"), srid=settings.SRID) begin_date = models.DateField(blank=True, null=True, verbose_name=_(u"Begin date"), db_column='date_debut') end_date = models.DateField(blank=True, null=True, verbose_name=_(u"End date"), db_column='date_fin') duration = models.CharField(verbose_name=_(u"Duration"), max_length=64, blank=True, db_column='duree', help_text=_(u"3 days, season, ...")) meeting_point = models.CharField(verbose_name=_(u"Meeting point"), max_length=256, blank=True, db_column='point_rdv', help_text=_(u"Where exactly ?")) meeting_time = models.TimeField(verbose_name=_(u"Meeting time"), blank=True, null=True, db_column='heure_rdv', help_text=_(u"11:00, 23:30")) contact = models.TextField(verbose_name=_(u"Contact"), blank=True, db_column='contact') email = models.EmailField(verbose_name=_(u"Email"), max_length=256, db_column='email', blank=True, null=True) website = models.URLField(verbose_name=_(u"Website"), max_length=256, db_column='website', blank=True, null=True) organizer = models.CharField(verbose_name=_(u"Organizer"), max_length=256, blank=True, db_column='organisateur') speaker = models.CharField(verbose_name=_(u"Speaker"), max_length=256, blank=True, db_column='intervenant') type = models.ForeignKey(TouristicEventType, verbose_name=_(u"Type"), blank=True, null=True, db_column='type') accessibility = models.CharField(verbose_name=_(u"Accessibility"), max_length=256, blank=True, db_column='accessibilite') participant_number = models.CharField( verbose_name=_(u"Number of participants"), max_length=256, blank=True, db_column='nb_places') booking = models.TextField(verbose_name=_(u"Booking"), blank=True, db_column='reservation') target_audience = models.CharField(verbose_name=_(u"Target audience"), max_length=128, blank=True, null=True, db_column='public_vise') practical_info = models.TextField( verbose_name=_(u"Practical info"), blank=True, db_column='infos_pratiques', help_text=_(u"Recommandations / To plan / Advices")) source = models.ManyToManyField( 'common.RecordSource', blank=True, related_name='touristicevents', verbose_name=_("Source"), db_table='t_r_evenement_touristique_source') portal = models.ManyToManyField( 'common.TargetPortal', blank=True, related_name='touristicevents', verbose_name=_("Portal"), db_table='t_r_evenement_touristique_portal') eid = models.CharField(verbose_name=_(u"External id"), max_length=128, blank=True, null=True, db_column='id_externe') approved = models.BooleanField(verbose_name=_(u"Approved"), default=False, db_column='labellise') objects = NoDeleteMixin.get_manager_cls(models.GeoManager)() category_id_prefix = 'E' class Meta: db_table = 't_t_evenement_touristique' verbose_name = _(u"Touristic event") verbose_name_plural = _(u"Touristic events") ordering = ['-begin_date'] def __unicode__(self): return self.name @models.permalink def get_document_public_url(self): """ Override ``geotrek.common.mixins.PublishableMixin`` """ return ('tourism:touristicevent_document_public', [], { 'lang': get_language(), 'pk': self.pk, 'slug': self.slug }) @property def type1(self): return [self.type] if self.type else [] @property def type2(self): return [] @property def districts_display(self): return ', '.join([unicode(d) for d in self.districts]) @property def dates_display(self): if not self.begin_date and not self.end_date: return u"" elif not self.end_date: return _(u"starting from {begin}").format( begin=date_format(self.begin_date, 'SHORT_DATE_FORMAT')) elif not self.begin_date: return _(u"up to {end}").format( end=date_format(self.end_date, 'SHORT_DATE_FORMAT')) elif self.begin_date == self.end_date: return date_format(self.begin_date, 'SHORT_DATE_FORMAT') else: return _(u"from {begin} to {end}").format( begin=date_format(self.begin_date, 'SHORT_DATE_FORMAT'), end=date_format(self.end_date, 'SHORT_DATE_FORMAT')) @property def prefixed_category_id(self): return self.category_id_prefix def distance(self, to_cls): return settings.TOURISM_INTERSECTION_MARGIN @property def portal_display(self): return ', '.join([unicode(portal) for portal in self.portal.all()]) @property def source_display(self): return ', '.join([unicode(source) for source in self.source.all()]) @property def themes_display(self): return ','.join([unicode(source) for source in self.themes.all()])
class User(AbstractBaseUser): # main name = models.CharField(max_length=60, blank=True, null=True) email = models.EmailField(max_length=60, unique=True) #verbose_name="email" username = models.CharField(_('username'), max_length=150, unique=True, validators=[UnicodeUsernameValidator()]) image = models.ImageField(default="default-profile.png", upload_to="profile_pics", blank=True) # contact info phones = models.JSONField(default=list, null=True, blank=True) website = models.URLField(blank=True, null=True) about = models.CharField(max_length=280, blank=True, null=True) location = models.PointField(blank=True, null=True) address = models.CharField(max_length=200, blank=True, null=True) # additional info genre = models.ManyToManyField(Genre, related_name='users') job = models.ManyToManyField(Job, blank=True, related_name="users") instrument = models.ManyToManyField(Instrument, blank=True, related_name="users") # budget = models.JSONField(default=list, validators=[validate_budget]) budget_from = models.BigIntegerField(null=True) budget_to = models.BigIntegerField(null=True) # Social Media instagram = models.CharField(max_length=60, blank=True, null=True) twitter = models.CharField(max_length=60, blank=True, null=True) facebook = models.CharField(max_length=60, blank=True, null=True) tiktok = models.CharField(max_length=60, blank=True, null=True) youtube = models.CharField(max_length=60, blank=True, null=True) soundcloud = models.CharField(max_length=60, blank=True, null=True) spotify = models.CharField(max_length=60, blank=True, null=True) tidal = models.CharField(max_length=60, blank=True, null=True) deezer = models.CharField(max_length=60, blank=True, null=True) # REQUIRED date_joined = models.DateTimeField(auto_now_add=True) last_login = models.DateTimeField(auto_now=True) is_admin = models.BooleanField(default=False) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) #Rating field ratings = models.ManyToManyField('self', through='Rating', symmetrical=False, related_name='rated_by') objects = Manager() USERNAME_FIELD = "email" REQUIRED_FIELDS = [ "username", ] # class Meta: # ordering = ['field_name'] # def __str__(self): # return f"{self.field_name} {self.field_name}" def has_perm(self, perm, obj=None): return self.is_admin def has_module_perms(self, app_label): return True def save(self, *args, **kwargs): super().save(*args, **kwargs)
class Layer(models.Model): """ A layer object that can be added to any map. """ name = models.CharField( max_length=200, help_text='Name that will be displayed within GeoQ') type = models.CharField(choices=SERVICE_TYPES, max_length=75) url = models.CharField( help_text= 'URL of service. If WMS or ESRI, can be any valid URL. Otherwise, the URL will require a local proxy', max_length=500) layer = models.CharField( max_length=800, null=True, blank=True, help_text= 'Layer names can sometimes be comma-separated, and are not needed for data layers (KML, GeoRSS, GeoJSON...)' ) image_format = models.CharField( null=True, blank=True, choices=IMAGE_FORMATS, max_length=75, help_text= 'The MIME type of the image format to use for tiles on WMS layers (image/png, image/jpeg image/gif...). Double check that the server exposes this exactly - some servers push png instead of image/png.' ) styles = models.CharField( null=True, blank=True, max_length=200, help_text= 'The name of a style to use for this layer (only useful for WMS layers if the server exposes it.)' ) transparent = models.BooleanField( default=True, help_text= 'If WMS or overlay, should the tiles be transparent where possible?') refreshrate = models.PositiveIntegerField( blank=True, null=True, verbose_name="Layer Refresh Rate", help_text= 'Layer refresh rate in seconds for vector/data layers (will not refresh WMS layers)' ) description = models.TextField( max_length=800, null=True, blank=True, help_text= 'Text to show in layer chooser, please be descriptive - this will soon be searchable' ) attribution = models.CharField( max_length=200, null=True, blank=True, help_text= "Attribution from layers to the map display (will show in bottom of map when layer is visible)." ) token = models.CharField( max_length=400, null=True, blank=True, help_text= 'Authentication token, if required (usually only for secure layer servers)' ) ## Advanced layer options objects = models.GeoManager() extent = models.PolygonField(null=True, blank=True, help_text='Extent of the layer.') layer_parsing_function = models.CharField( max_length=100, blank=True, null=True, help_text= 'Advanced - The javascript function used to parse a data service (GeoJSON, GeoRSS, KML), needs to be an internally known parser. Contact an admin if you need data parsed in a new way.' ) enable_identify = models.BooleanField( default=False, help_text= 'Advanced - Allow user to click map to query layer for details. The map server must support queries for this layer.' ) info_format = models.CharField( max_length=75, null=True, blank=True, choices=INFO_FORMATS, help_text='Advanced - what format the server returns for an WMS-I query' ) root_field = models.CharField( max_length=100, null=True, blank=True, help_text= 'Advanced - For WMS-I (queryable) layers, the root field returned by server. Leave blank for default (will usually be "FIELDS" in returned XML).' ) fields_to_show = models.CharField( max_length=200, null=True, blank=True, help_text= 'Fields to show when someone uses the identify tool to click on the layer. Leave blank for all.' ) downloadableLink = models.URLField( max_length=400, null=True, blank=True, help_text= 'URL of link to supporting tool (such as a KML document that will be shown as a download button)' ) layer_params = JSONField( null=True, blank=True, help_text= 'JSON key/value pairs to be sent to the web service. ex: {"crs":"urn:ogc:def:crs:EPSG::4326"}' ) dynamic_params = JSONField( null=True, blank=True, help_text= 'URL Variables that may be modified by the analyst. ex: "date"') spatial_reference = models.CharField( max_length=32, blank=True, null=True, default="EPSG:4326", help_text= 'The spatial reference of the service. Should be in ESPG:XXXX format.' ) constraints = models.TextField( null=True, blank=True, help_text='Constrain layer data displayed to certain feature types') disabled = models.BooleanField( default=False, blank=True, help_text="If unchecked, Don't show this layer when listing all layers" ) layer_info_link = models.URLField( null=True, blank=True, help_text='URL of info about the service, or a help doc or something', max_length=500) created_at = models.DateTimeField(auto_now_add=True, null=True) updated_at = models.DateTimeField(auto_now_add=True, null=True) ## Primarily for http://trac.osgeo.org/openlayers/wiki/OpenLayersOptimization additional_domains = models.TextField( null=True, blank=True, help_text= 'Semicolon seperated list of additional domains for the layer. Only used if you want to cycle through domains for load-balancing' ) def __unicode__(self): return '{0}'.format(self.name) def get_layer_urls(self): """ Returns a list of urls for the layer. """ urls = [] if getattr(self, 'additional_domains'): map(urls.append, (domain for domain in self.additional_domains.split(";") if domain)) return urls def get_absolute_url(self): return reverse('layer-update', args=[self.id]) def get_layer_params(self): """ Returns the layer_params attribute, which should be json """ return self.layer_params def layer_json(self): return { "id": self.id, "name": self.name, "format": self.image_format, "type": self.type, "url": self.url, "subdomains": self.get_layer_urls(), "layer": self.layer, "transparent": self.transparent, "layerParams": self.layer_params, "dynamicParams": self.dynamic_params, "refreshrate": self.refreshrate, "token": self.token, "attribution": self.attribution, "spatialReference": self.spatial_reference, "layerParsingFunction": self.layer_parsing_function, "enableIdentify": self.enable_identify, "rootField": self.root_field, "infoFormat": self.info_format, "fieldsToShow": self.fields_to_show, "description": self.description, "downloadableLink": self.downloadableLink, "layer_info_link": self.layer_info_link, "styles": self.styles, } class Meta: ordering = ["name"]
class Topology(BaseDate): name = models.CharField(_('name'), max_length=75, unique=True) format = models.CharField(_('format'), choices=PARSERS, max_length=128, help_text=_('Select topology format')) url = models.URLField(_('url'), help_text=_('URL where topology will be retrieved')) class Meta: app_label = 'links' verbose_name_plural = _('topologies') def __unicode__(self): return self.name _parser = None @property def parser(self): if not self._parser: self._parser = import_by_path(self.format) return self._parser @property def is_layer2(self): return self.format == 'netdiff.BatmanParser' @property def latest(self): return self.parser(self.url) def diff(self): """ shortcut to netdiff.diff """ latest = self.latest current = NetJsonParser(self.json()) return diff(current, latest) def json(self): """ returns a dict that represents a NetJSON NetworkGraph object """ nodes = [] links = [] for link in self.link_set.all(): if self.is_layer2: source = link.interface_a.mac destination = link.interface_b.mac else: source = str(link.interface_a.ip_set.first().address) destination = str(link.interface_b.ip_set.first().address) nodes.append({'id': source}) nodes.append({'id': destination}) links.append( OrderedDict((('source', source), ('target', destination), ('weight', link.metric_value)))) return OrderedDict( (('type', 'NetworkGraph'), ('protocol', self.parser.protocol), ('version', self.parser.version), ('metric', self.parser.metric), ('nodes', nodes), ('links', links))) def update(self): """ Updates topology Links are not deleted straightaway but set as "disconnected" """ from .link import Link # avoid circular dependency diff = self.diff() status = { 'added': 'active', 'removed': 'disconnected', 'changed': 'active' } for section in ['added', 'removed', 'changed']: # section might be empty if not diff[section]: continue for link_dict in diff[section]['links']: try: link = Link.get_or_create(source=link_dict['source'], target=link_dict['target'], weight=link_dict['weight'], topology=self) except LinkDataNotFound as e: msg = 'Exception while updating {0}'.format( self.__repr__()) logger.exception(msg) print('{0}\n{1}\n'.format(msg, e)) continue link.ensure(status=status[section], weight=link_dict['weight'])
class DataResource(Displayable): """Represents a file that has been uploaded to Geoanalytics for representation""" original_file = models.FileField(upload_to='geographica_resources', null=True, blank=True) resource_file = models.FileField(upload_to='geographica_resources', null=True, blank=True) resource_url = models.URLField(null=True, blank=True) metadata_url = models.URLField(null=True, blank=True) metadata_xml = models.TextField(null=True, blank=True) driver_config = DictionaryField(null=True, blank=True) metadata_properties = DictionaryField(null=True, blank=True) last_change = models.DateTimeField(null=True, blank=True, auto_now=True) last_refresh = models.DateTimeField( null=True, blank=True ) # updates happen only to geocms that were not uploaded by the user. next_refresh = models.DateTimeField( null=True, blank=True, db_index=True) # will be populated every time the update manager runs refresh_every = TimedeltaField(null=True, blank=True) md5sum = models.CharField(max_length=64, blank=True, null=True) # the unique md5 sum of the data bounding_box = models.PolygonField(null=True, srid=4326, blank=True) import_log = models.TextField(null=True, blank=True) associated_pages = models.ManyToManyField("pages.Page", blank=True, null=True, related_name='data_resources') driver = models.CharField( default='terrapyn.geocms.drivers.spatialite', max_length=255, null=False, blank=False, choices=getattr(settings, 'INSTALLED_DATARESOURCE_DRIVERS', ( ('terrapyn.geocms.drivers.spatialite', 'Spatialite (universal vector)'), ('terrapyn.geocms.drivers.shapefile', 'Shapefile'), ('terrapyn.geocms.drivers.geotiff', 'GeoTIFF'), ('terrapyn.geocms.drivers.postgis', 'PostGIS'), ('terrapyn.geocms.drivers.kmz', 'Google Earth KMZ'), ('terrapyn.geocms.drivers.ogr', 'OGR DataSource'), ))) big = models.BooleanField( default=False, help_text='Set this to be true if the dataset is more than 100MB' ) # causes certain drivers to optimize for datasets larger than memory def get_absolute_url(self): return reverse('resource-page', kwargs={'slug': self.slug}) def get_admin_url(self): return reverse("admin:geocms_dataresource_change", args=(self.id, )) @property def srs(self): if not self.metadata.native_srs: self.driver_instance.compute_spatial_metadata() srs = osr.SpatialReference() srs.ImportFromProj4(self.metadata.native_srs.encode('ascii')) return srs @property def driver_instance(self): if not hasattr(self, '_driver_instance'): self._driver_instance = get_driver(self.driver)(self) return self._driver_instance def __unicode__(self): return self.title class Meta: permissions = ( ('view_dataresource', "View data resource"), # to add beyond the default )
class Profile(BaseModel): """ """ active = models.BooleanField(default=True) app_admin = models.BooleanField(default=False) is_contact = models.BooleanField(default=False) notify = models.BooleanField(default=True) published = models.BooleanField(default=False) dashboard_override = models.BooleanField( 'Override Default Dashboard Settings', default=False) dashboard_choices = MultiSelectField('Dashboard Choices', choices=DASHBOARD_CHOICES, null=True, blank=True) editor = models.CharField(max_length=8, choices=EDITOR_CHOICES, null=True, blank=True) user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True) icon_size = models.CharField(max_length=255, blank=True, null=True, choices=ICON_CHOICES) icon_color = models.CharField(max_length=255, blank=True, null=True, choices=COLOR_CHOICES) page_size = models.PositiveIntegerField(blank=True, null=True) preferred_username = models.CharField('Preferred Username', max_length=150, blank=True, null=True) rate = models.DecimalField('Hourly Rate (United States Dollar - USD)', blank=True, null=True, max_digits=12, decimal_places=2) unit = models.DecimalField("Unit", default=1.0, blank=True, null=True, max_digits=12, decimal_places=2) avatar_url = models.URLField("Avatar URL", blank=True, null=True) bio = models.TextField(blank=True, null=True) address = models.TextField(blank=True, null=True) preferred_payment_method = models.CharField('Preferred Payment Method', max_length=255, blank=True, null=True, choices=PAYMENT_CHOICES) def __str__(self): if self.user: return self.user.username else: return '-'.join([self._meta.verbose_name, str(self.pk)]) def get_avatar_url(self): if self.avatar_url is not None: return self.avatar_url else: return gravatar_url(self.user.email) def get_username(self): if self.preferred_username is not None: return self.preferred_username elif self.user: return self.user.username else: return '-'.join([self._meta.verbose_name, str(self.pk)])
class Time(BaseModel): """ Date, Client, Project, Project Code, Task, Notes, Hours, Billable?, Invoiced?, First Name, Last Name, Department, Employee?, Billable Rate, Billable Amount, Cost Rate, Cost Amount, Currency, External Reference URL """ billable = models.BooleanField(default=True) employee = models.BooleanField(default=True) invoiced = models.BooleanField(default=False) client = models.ForeignKey( Client, blank=True, null=True, limit_choices_to={'active': True}, ) project = models.ForeignKey( Project, blank=True, null=True, limit_choices_to={'active': True}, ) task = models.ForeignKey( Task, blank=True, null=True, limit_choices_to={'active': True}, ) user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, limit_choices_to={'profile__active': True}) estimate = models.ForeignKey(Estimate, blank=True, null=True, on_delete=models.SET_NULL) invoice = models.ForeignKey(Invoice, blank=True, null=True, on_delete=models.SET_NULL, limit_choices_to={'last_payment_date': None}) date = models.DateField(default=timezone.now) hours = models.DecimalField("Hours", default=1.0, blank=True, null=True, max_digits=12, decimal_places=2) first_name = models.CharField(max_length=300, blank=True, null=True) last_name = models.CharField(max_length=300, blank=True, null=True) department = models.CharField(max_length=300, blank=True, null=True) cost_rate = models.DecimalField(blank=True, null=True, max_digits=12, decimal_places=2) cost_amount = models.DecimalField(blank=True, null=True, max_digits=12, decimal_places=2) currency = models.CharField(max_length=300, blank=True, null=True) external_reference_url = models.URLField(blank=True, null=True) project_code = models.IntegerField(blank=True, null=True) log = models.TextField(blank=True, null=True) def __str__(self): return '-'.join([self._meta.verbose_name, str(self.pk)]) # https://docs.djangoproject.com/en/1.9/ref/models/instances/#get-absolute-url def get_absolute_url(self, hostname): return '%s/%s' % (hostname, reverse('time_view', args=[str(self.id)]))
class Event(MPTTModel, BaseModel, SchemalessFieldMixin): jsonld_type = "Event/LinkedEvent" """ eventStatus enumeration is based on http://schema.org/EventStatusType """ class Status: SCHEDULED = 1 CANCELLED = 2 POSTPONED = 3 RESCHEDULED = 4 # Properties from schema.org/Event STATUSES = ( (Status.SCHEDULED, "EventScheduled"), (Status.CANCELLED, "EventCancelled"), (Status.POSTPONED, "EventPostponed"), (Status.RESCHEDULED, "EventRescheduled"), ) class SuperEventType: RECURRING = 'recurring' SUPER_EVENT_TYPES = ((SuperEventType.RECURRING, _('Recurring')), # Other types include e.g. a festival ) # Properties from schema.org/Thing info_url = models.URLField(verbose_name=_('Event home page'), blank=True, null=True, max_length=1000) description = models.TextField(verbose_name=_('Description'), blank=True, null=True) short_description = models.TextField(verbose_name=_('Short description'), blank=True, null=True) # Properties from schema.org/CreativeWork date_published = models.DateTimeField(verbose_name=_('Date published'), null=True, blank=True) # headline and secondary_headline are for cases where # the original event data contains a title and a subtitle - in that # case the name field is combined from these. # # secondary_headline is mapped to schema.org alternative_headline # and is used for subtitles, that is for # secondary, complementary headlines, not "alternative" headlines headline = models.CharField(verbose_name=_('Headline'), max_length=255, null=True, db_index=True) secondary_headline = models.CharField(verbose_name=_('Secondary headline'), max_length=255, null=True, db_index=True) provider = models.CharField(verbose_name=_('Provider'), max_length=512, null=True) publisher = models.ForeignKey('django_orghierarchy.Organization', verbose_name=_('Publisher'), db_index=True, on_delete=models.PROTECT, related_name='published_events') # Status of the event itself event_status = models.SmallIntegerField(verbose_name=_('Event status'), choices=STATUSES, default=Status.SCHEDULED) # Whether or not this data about the event is ready to be viewed by the general public. # DRAFT means the data is considered incomplete or is otherwise undergoing refinement -- # or just waiting to be published for other reasons. publication_status = models.SmallIntegerField( verbose_name=_('Event data publication status'), choices=PUBLICATION_STATUSES, default=PublicationStatus.PUBLIC) location = models.ForeignKey(Place, related_name='events', null=True, blank=True, on_delete=models.PROTECT) location_extra_info = models.CharField( verbose_name=_('Location extra info'), max_length=400, null=True, blank=True) start_time = models.DateTimeField(verbose_name=_('Start time'), null=True, db_index=True, blank=True) end_time = models.DateTimeField(verbose_name=_('End time'), null=True, db_index=True, blank=True) has_start_time = models.BooleanField(default=True) has_end_time = models.BooleanField(default=True) super_event = TreeForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, related_name='sub_events') super_event_type = models.CharField(max_length=255, blank=True, null=True, default=None, choices=SUPER_EVENT_TYPES) in_language = models.ManyToManyField(Language, verbose_name=_('In language'), related_name='events', blank=True) deleted = models.BooleanField(default=False, db_index=True) # Custom fields not from schema.org keywords = models.ManyToManyField(Keyword, related_name='events') audience = models.ManyToManyField(Keyword, related_name='audience_events', blank=True) class Meta: verbose_name = _('event') verbose_name_plural = _('events') class MPTTMeta: parent_attr = 'super_event' def save(self, *args, **kwargs): # needed to cache location event numbers old_location = None if self.id: try: old_location = Event.objects.get(id=self.id).location except Event.DoesNotExist: pass # drafts may not have times set, so check that first start = getattr(self, 'start_time', None) end = getattr(self, 'end_time', None) if start and end: if start > end: raise ValidationError({ 'end_time': _('The event end time cannot be earlier than the start time.' ) }) super(Event, self).save(*args, **kwargs) # needed to cache location event numbers if not old_location and self.location: Place.objects.filter(id=self.location.id).update( n_events_changed=True) if old_location and not self.location: # drafts (or imported events) may not always have location set Place.objects.filter(id=old_location.id).update( n_events_changed=True) if old_location and self.location and old_location != self.location: Place.objects.filter(id__in=(old_location.id, self.location.id)).update( n_events_changed=True) def __str__(self): name = '' for lang in settings.LANGUAGES: s = getattr(self, 'name_%s' % lang[0], None) if s: name = s break val = [name, '(%s)' % self.id] dcount = self.get_descendant_count() if dcount > 0: val.append(u" (%d children)" % dcount) else: val.append(str(self.start_time)) return u" ".join(val) def is_admin(self, user): if user.is_superuser: return True else: return user.is_admin(self.publisher) def can_be_edited_by(self, user): """Check if current event can be edited by the given user""" if user.is_superuser: return True return user.can_edit_event(self.publisher, self.publication_status) def soft_delete(self, using=None): self.deleted = True self.save(update_fields=("deleted", ), using=using, force_update=True) def undelete(self, using=None): self.deleted = False self.save(update_fields=("deleted", ), using=using, force_update=True)
class AdvertisingCampaign(models.Model): name = models.CharField(max_length=128) account = models.ForeignKey('accounts.Account') venue_account = models.ForeignKey('accounts.VenueAccount', blank=True, null=True, on_delete=models.SET_NULL) all_of_canada = models.BooleanField() regions = models.ManyToManyField(Region) budget = MoneyField(max_digits=10, decimal_places=2, default_currency='CAD') ammount_spent = MoneyField(max_digits=18, decimal_places=10, default_currency='CAD') enough_money = models.BooleanField(default=False) started = models.DateTimeField(auto_now=True, auto_now_add=True) ended = models.DateTimeField(auto_now=False, auto_now_add=False, null=True, blank=True) active_from = models.DateTimeField('active to', null=True, blank=True, auto_now=False, auto_now_add=False) active_to = models.DateTimeField('active to', null=True, blank=True, auto_now=False, auto_now_add=False) website = models.URLField() free = models.BooleanField(default=False) objects = money_manager(models.Manager()) admin = AdminAdvertisingCampaignManager() with_unused_money = AdvertisingCampaignWithUnsusedMoney() active = ActiveAdvertisingCampaign() expired = ExpiredAdvertisingCampaign() def save(self, *args, **kwargs): if self.enough_money and self.ammount_spent >= self.budget and self.budget > Money( 0, CAD): inform_user_that_money_was_spent(self) if self.ammount_spent >= self.budget: self.enough_money = False else: self.enough_money = True super(AdvertisingCampaign, self).save(*args, **kwargs) return self def __unicode__(self): return self.name def ammount_remaining(self): return self.budget - self.ammount_spent def regions_representation(self): return ", ".join(self.regions.all().values_list("name", flat=True)) def is_active(self): now = datetime.datetime.now() return (self.enough_money or self.free) and ( not self.active_from or self.active_from < now) and (not self.active_to or self.active_to > now) def is_finished(self): return self.active_to and self.active_to < datetime.datetime.now() def is_future(self): return self.active_from and self.active_from > datetime.datetime.now()
class Place(MPTTModel, BaseModel, SchemalessFieldMixin): publisher = models.ForeignKey('django_orghierarchy.Organization', verbose_name=_('Publisher'), db_index=True) info_url = models.URLField(verbose_name=_('Place home page'), null=True, blank=True, max_length=1000) description = models.TextField(verbose_name=_('Description'), null=True, blank=True) parent = TreeForeignKey('self', null=True, blank=True, related_name='children') position = models.PointField(srid=settings.PROJECTION_SRID, null=True, blank=True) email = models.EmailField(verbose_name=_('E-mail'), null=True, blank=True) telephone = models.CharField(verbose_name=_('Telephone'), max_length=128, null=True, blank=True) contact_type = models.CharField(verbose_name=_('Contact type'), max_length=255, null=True, blank=True) street_address = models.CharField(verbose_name=_('Street address'), max_length=255, null=True, blank=True) address_locality = models.CharField(verbose_name=_('Address locality'), max_length=255, null=True, blank=True) address_region = models.CharField(verbose_name=_('Address region'), max_length=255, null=True, blank=True) postal_code = models.CharField(verbose_name=_('Postal code'), max_length=128, null=True, blank=True) post_office_box_num = models.CharField(verbose_name=_('PO BOX'), max_length=128, null=True, blank=True) address_country = models.CharField(verbose_name=_('Country'), max_length=2, null=True, blank=True) deleted = models.BooleanField(verbose_name=_('Deleted'), default=False) replaced_by = models.ForeignKey('Place', related_name='aliases', null=True) divisions = models.ManyToManyField(AdministrativeDivision, verbose_name=_('Divisions'), related_name='places', blank=True) geo_objects = models.GeoManager() n_events = models.IntegerField( verbose_name=_('event count'), help_text=_('number of events in this location'), default=0, editable=False, db_index=True) n_events_changed = models.BooleanField(default=False, db_index=True) class Meta: verbose_name = _('place') verbose_name_plural = _('places') unique_together = (('data_source', 'origin_id'), ) def __unicode__(self): values = filter( lambda x: x, [self.street_address, self.postal_code, self.address_locality]) return u', '.join(values) @transaction.atomic def save(self, *args, **kwargs): if self.replaced_by and self.replaced_by.replaced_by == self: raise Exception( "Trying to replace the location replacing this location by this location." "Please refrain from creating circular replacements and" "remove either one of the replacements." "We don't want homeless events.") # needed to remap events to replaced location old_replaced_by = None if self.id: try: old_replaced_by = Place.objects.get(id=self.id).replaced_by except Place.DoesNotExist: pass super().save(*args, **kwargs) # needed to remap events to replaced location if not old_replaced_by == self.replaced_by: Event.objects.filter(location=self).update( location=self.replaced_by) # Update doesn't call save so we update event numbers manually. # Not all of the below are necessarily present. ids_to_update = [ event.id for event in (self, self.replaced_by, old_replaced_by) if event ] Place.objects.filter(id__in=ids_to_update).update( n_events_changed=True) if self.position: self.divisions = AdministrativeDivision.objects.filter( type__type__in=('district', 'sub_district', 'neighborhood', 'muni'), geometry__boundary__contains=self.position) else: self.divisions.clear()
class Image(models.Model): jsonld_type = 'ImageObject' # Properties from schema.org/Thing name = models.CharField(verbose_name=_('Name'), max_length=255, db_index=True, default='') data_source = models.ForeignKey(DataSource, related_name='provided_%(class)s_data', db_index=True, null=True) publisher = models.ForeignKey('django_orghierarchy.Organization', verbose_name=_('Publisher'), db_index=True, null=True, blank=True, related_name='Published_images') created_time = models.DateTimeField(auto_now_add=True) last_modified_time = models.DateTimeField(auto_now=True, db_index=True) created_by = models.ForeignKey(User, null=True, blank=True, related_name='EventImage_created_by') last_modified_by = models.ForeignKey( User, related_name='EventImage_last_modified_by', null=True, blank=True) image = models.ImageField(upload_to='images', null=True, blank=True) url = models.URLField(verbose_name=_('Image'), max_length=400, null=True, blank=True) cropping = ImageRatioField('image', '800x800', verbose_name=_('Cropping')) license = models.ForeignKey(License, verbose_name=_('License'), related_name='images', default='cc_by') photographer_name = models.CharField(verbose_name=_('Photographer name'), max_length=255, null=True, blank=True) def save(self, *args, **kwargs): if not self.publisher: try: self.publisher = self.created_by.get_default_organization() except AttributeError: pass # ensure that either image or url is provided if not self.url and not self.image: raise ValidationError(_('You must provide either image or url.')) if self.url and self.image: raise ValidationError( _('You can only provide image or url, not both.')) self.last_modified_time = BaseModel.now() super(Image, self).save(*args, **kwargs) def is_user_editable(self): return self.data_source.user_editable def is_user_edited(self): return bool(self.data_source.user_editable and self.last_modified_by) def can_be_edited_by(self, user): """Check if current event can be edited by the given user""" if user.is_superuser: return True return user.is_admin(self.publisher)
class RasterLayer(models.Model, ValueCountMixin): """ Source data model for raster layers """ CONTINUOUS = 'co' CATEGORICAL = 'ca' MASK = 'ma' RANK_ORDERED = 'ro' DATATYPES = ( (CONTINUOUS, 'Continuous'), (CATEGORICAL, 'Categorical'), (MASK, 'Mask'), (RANK_ORDERED, 'Rank Ordered'), ) name = models.CharField(max_length=100, blank=True, null=True) description = models.TextField(blank=True, null=True) datatype = models.CharField(max_length=2, choices=DATATYPES, default='co') rasterfile = models.FileField(upload_to='rasters', null=True, blank=True) source_url = models.URLField( default='', blank=True, max_length=2500, help_text='External url to get the raster file from. If a value is set,' 'the rasterfile field will be ignored.') nodata = models.CharField( max_length=100, null=True, blank=True, help_text= 'Leave blank to keep the internal band nodata values. If a nodata ' 'value is specified here, it will be used for all bands of this raster.' ) srid = models.IntegerField( null=True, blank=True, help_text='Leave blank to use the internal raster srid. If a srid is ' 'specified here, it will be used for all calculations.') max_zoom = models.IntegerField( null=True, blank=True, help_text='Leave blank to automatically determine the max zoom level ' 'from the raster scale. Otherwise the raster parsed up to ' 'the zoom level specified here.') build_pyramid = models.BooleanField( default=True, help_text='Should the tile pyramid be built? If unchecked, tiles will ' 'only be generated at the max zoom level.') next_higher = models.BooleanField( default=True, help_text= 'Compared to the scale of the rasterlayer, use the next-higher ' 'zoomlevel as max zoom? If unchecked, the next-lower zoom level ' 'is used. This flag is ignored if the max_zoom is manually ' 'specified.') store_reprojected = models.BooleanField( default=True, help_text='Should the reprojected raster be stored? If unchecked, the ' 'reprojected version of the raster is not stored.') legend = models.ForeignKey(Legend, blank=True, null=True) modified = models.DateTimeField(auto_now=True) def __str__(self): return '{} {} (type: {})'.format(self.id, self.name, self.datatype) @property def discrete(self): """ Returns true for discrete rasters. """ return self.datatype in (self.CATEGORICAL, self.MASK, self.RANK_ORDERED) _bbox = None _bbox_srid = None def extent(self, srid=WEB_MERCATOR_SRID): """ Returns bbox for layer. """ if not self._bbox or self._bbox_srid != srid: # Get bbox for raster in original coordinates meta = self.metadata xmin = meta.uperleftx ymax = meta.uperlefty xmax = xmin + meta.width * meta.scalex ymin = ymax + meta.height * meta.scaley # Create Polygon box geom = OGRGeometry(Envelope((xmin, ymin, xmax, ymax)).wkt) # Set original srs if meta.srs_wkt: geom.srs = SpatialReference(meta.srs_wkt) else: geom.srid = meta.srid # Transform to requested srid geom.transform(srid) # Calculate value range for bbox coords = geom.coords[0] xvals = [x[0] for x in coords] yvals = [x[1] for x in coords] # Set bbox self._bbox = (min(xvals), min(yvals), max(xvals), max(yvals)) self._bbox_srid = srid return self._bbox def index_range(self, zoom): """ Compute the index range for this rasterlayer at a given zoom leve. """ return self.rastertile_set.filter(tilez=zoom).aggregate( Min('tilex'), Max('tilex'), Min('tiley'), Max('tiley'))
class WMTSBasemap(Basemap): url = models.URLField(max_length=500, help_text="Capabilities xml url") layer = models.CharField(max_length=100) tile_matrix_set = models.CharField(max_length=100)
class InformationDesk(models.Model): name = models.CharField(verbose_name=_(u"Title"), max_length=256, db_column='nom') description = models.TextField(verbose_name=_(u"Description"), blank=True, db_column='description', help_text=_(u"Brief description")) phone = models.CharField(verbose_name=_(u"Phone"), max_length=32, blank=True, null=True, db_column='telephone') email = models.EmailField(verbose_name=_(u"Email"), max_length=256, db_column='email', blank=True, null=True) website = models.URLField(verbose_name=_(u"Website"), max_length=256, db_column='website', blank=True, null=True) photo = models.FileField(verbose_name=_(u"Photo"), upload_to=settings.UPLOAD_DIR, db_column='photo', max_length=512, blank=True, null=True) street = models.CharField(verbose_name=_(u"Street"), max_length=256, blank=True, null=True, db_column='rue') postal_code = models.CharField(verbose_name=_(u"Postal code"), max_length=8, blank=True, null=True, db_column='code') municipality = models.CharField(verbose_name=_(u"Municipality"), blank=True, null=True, max_length=256, db_column='commune') geom = models.PointField(verbose_name=_(u"Emplacement"), db_column='geom', blank=True, null=True, srid=settings.SRID, spatial_index=False) class Meta: db_table = 'o_b_renseignement' verbose_name = _(u"Information desk") verbose_name_plural = _(u"Information desks") ordering = ['name'] def __unicode__(self): return self.name def __json__(self): return { 'name': self.name, 'description': self.description, 'phone': self.phone, 'email': self.email, 'website': self.website, 'photo_url': self.photo_url, 'street': self.street, 'postal_code': self.postal_code, 'municipality': self.municipality, 'latitude': self.latitude, 'longitude': self.longitude, } @property def description_strip(self): nobr = re.compile(r'(\s*<br.*?>)+\s*', re.I) newlines = nobr.sub("\n", self.description) return smart_plain_text(newlines) @property def latitude(self): if self.geom: api_geom = self.geom.transform(settings.API_SRID, clone=True) return api_geom.y return None @property def longitude(self): if self.geom: api_geom = self.geom.transform(settings.API_SRID, clone=True) return api_geom.x return None @property def photo_url(self): if not self.photo: return None thumbnailer = get_thumbnailer(self.photo) try: thumb_detail = thumbnailer.get_thumbnail(aliases.get('thumbnail')) thumb_url = os.path.join(settings.MEDIA_URL, thumb_detail.name) except InvalidImageFormatError: thumb_url = None logger.error( _("Image %s invalid or missing from disk.") % self.photo) return thumb_url
class VectorTileBasemap(Basemap): url = models.URLField(max_length=1000, help_text="Vector tile url") api_key = models.CharField(max_length=200)
class Unit(ModifiableModel, AutoIdentifiedModel): id = models.CharField(primary_key=True, max_length=50) name = models.CharField(verbose_name=_('Name'), max_length=200) description = models.TextField(verbose_name=_('Description'), null=True, blank=True) location = models.PointField(verbose_name=_('Location'), null=True, srid=settings.DEFAULT_SRID) time_zone = models.CharField(verbose_name=_('Time zone'), max_length=50, default=_get_default_timezone) manager_email = models.EmailField(verbose_name=_('Manager email'), max_length=100, null=True, blank=True) street_address = models.CharField(verbose_name=_('Street address'), max_length=100, null=True) address_zip = models.CharField(verbose_name=_('Postal code'), max_length=10, null=True, blank=True) phone = models.CharField(verbose_name=_('Phone number'), max_length=30, null=True, blank=True) email = models.EmailField(verbose_name=_('Email'), max_length=100, null=True, blank=True) www_url = models.URLField(verbose_name=_('WWW link'), max_length=400, null=True, blank=True) address_postal_full = models.CharField(verbose_name=_('Full postal address'), max_length=100, null=True, blank=True) municipality = models.ForeignKey(Municipality, null=True, blank=True, verbose_name=_('Municipality'), on_delete=models.SET_NULL) picture_url = models.URLField(verbose_name=_('Picture URL'), max_length=200, null=True, blank=True) picture_caption = models.CharField(verbose_name=_('Picture caption'), max_length=200, null=True, blank=True) reservable_max_days_in_advance = models.PositiveSmallIntegerField(verbose_name=_('Reservable max. days in advance'), null=True, blank=True) reservable_min_days_in_advance = models.PositiveSmallIntegerField(verbose_name=_('Reservable min. days in advance'), null=True, blank=True) objects = UnitQuerySet.as_manager() class Meta: verbose_name = _("unit") verbose_name_plural = _("units") permissions = UNIT_PERMISSIONS ordering = ('name',) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Set the time zone choices here in order to avoid spawning # spurious migrations. self._meta.get_field('time_zone').choices = _get_timezone_choices() def __str__(self): return "%s (%s)" % (get_translated(self, 'name'), self.id) def get_opening_hours(self, begin=None, end=None): """ :rtype : dict[str, list[dict[str, datetime.datetime]]] :type begin: datetime.date :type end: datetime.date """ return get_opening_hours(self.time_zone, list(self.periods.all()), begin, end) def update_opening_hours(self): for res in self.resources.all(): res.update_opening_hours() def get_tz(self): return pytz.timezone(self.time_zone) def get_reservable_before(self): return create_datetime_days_from_now(self.reservable_max_days_in_advance) def get_reservable_after(self): return create_datetime_days_from_now(self.reservable_min_days_in_advance) def is_admin(self, user): return is_authenticated_user(user) and ( is_general_admin(user) or user.unit_authorizations.to_unit(self).admin_level().exists() or (user.unit_group_authorizations .to_unit(self).admin_level().exists())) def is_manager(self, user): return self.is_admin(user) or (is_authenticated_user(user) and ( user.unit_authorizations.to_unit(self).manager_level().exists()))
class Resource(models.Model): """Modèle de classe d'une ressource de données.""" class Meta(object): verbose_name = 'Ressource' verbose_name_plural = 'Ressources' # Managers # ======== objects = models.Manager() default = DefaultResourceManager() # Champs atributaires # =================== ckan_id = models.UUIDField( verbose_name='Ckan UUID', default=uuid.uuid4, editable=False, unique=True, ) title = models.TextField(verbose_name='Title', ) description = models.TextField( verbose_name='Description', blank=True, null=True, ) ftp_file = models.FileField( verbose_name='Fichier déposé sur sFTP', blank=True, null=True, upload_to=_ftp_file_upload_to, max_length=255, ) referenced_url = models.URLField( verbose_name='Référencer une URL', max_length=2000, blank=True, null=True, ) dl_url = models.URLField( verbose_name='Télécharger depuis une URL', max_length=2000, blank=True, null=True, ) up_file = models.FileField( verbose_name='Téléverser un ou plusieurs fichiers', blank=True, null=True, upload_to=_up_file_upload_to, max_length=255, ) LANG_CHOICES = ( ('french', 'Français'), ('english', 'Anglais'), ('italian', 'Italien'), ('german', 'Allemand'), ('other', 'Autre'), ) lang = models.CharField( verbose_name='Langue', choices=LANG_CHOICES, default='french', max_length=10, ) format_type = models.ForeignKey( to='ResourceFormats', verbose_name='Format', blank=False, null=True, ) LEVEL_CHOICES = ( ('public', 'Tous les utilisateurs'), ('registered', 'Utilisateurs authentifiés'), ('only_allowed_users', 'Utilisateurs authentifiés avec droits spécifiques'), ('same_organization', 'Utilisateurs de cette organisation uniquement'), ('any_organization', 'Organisations spécifiées'), ('only_idgo_partners', 'Tous les %s' % IDGO_USER_PARTNER_LABEL_PLURAL), ) restricted_level = models.CharField( verbose_name="Restriction d'accès", choices=LEVEL_CHOICES, default='public', max_length=20, blank=True, null=True, ) profiles_allowed = models.ManyToManyField( to='Profile', verbose_name='Utilisateurs autorisés', blank=True, ) organisations_allowed = models.ManyToManyField( to='Organisation', verbose_name='Organisations autorisées', blank=True, ) dataset = models.ForeignKey( to='Dataset', verbose_name='Jeu de données', on_delete=models.SET_NULL, blank=True, null=True, ) bbox = models.PolygonField( verbose_name='Rectangle englobant', blank=True, null=True, srid=4171, ) geo_restriction = models.BooleanField( verbose_name='Restriction géographique', default=False, ) extractable = models.BooleanField( verbose_name='Extractible', default=True, ) ogc_services = models.BooleanField( verbose_name='Services OGC', default=True, ) created_on = models.DateTimeField( verbose_name='Date de création de la resource', blank=True, null=True, default=timezone.now, ) last_update = models.DateTimeField( verbose_name='Date de dernière modification de la resource', blank=True, null=True, ) TYPE_CHOICES = ( ('raw', 'Données brutes'), ('annexe', 'Documentation associée'), ('service', 'Service'), ) data_type = models.CharField( verbose_name='Type de la ressource', choices=TYPE_CHOICES, max_length=10, default='raw', ) synchronisation = models.BooleanField( verbose_name='Synchronisation de données distante', default=False, ) EXTRA_FREQUENCY_CHOICES = ( # ('5mn', 'Toutes les 5 minutes'), # ('15mn', 'Toutes les 15 minutes'), # ('20mn', 'Toutes les 20 minutes'), # ('30mn', 'Toutes les 30 minutes'), ) FREQUENCY_CHOICES = ( ('1hour', 'Toutes les heures'), ('3hours', 'Toutes les trois heures'), ('6hours', 'Toutes les six heures'), ('daily', 'Quotidienne (tous les jours à minuit)'), ('weekly', 'Hebdomadaire (tous les lundi)'), ('bimonthly', 'Bimensuelle (1er et 15 de chaque mois)'), ('monthly', 'Mensuelle (1er de chaque mois)'), ('quarterly', 'Trimestrielle (1er des mois de janvier, avril, juillet, octobre)'), ('biannual', 'Semestrielle (1er janvier et 1er juillet)'), ('annual', 'Annuelle (1er janvier)'), ('never', 'Jamais'), ) sync_frequency = models.CharField( verbose_name='Fréquence de synchronisation', max_length=20, blank=True, null=True, choices=FREQUENCY_CHOICES + EXTRA_FREQUENCY_CHOICES, default='never', ) crs = models.ForeignKey( to='SupportedCrs', verbose_name='CRS', on_delete=models.SET_NULL, blank=True, null=True, ) def __str__(self): return self.title # Propriétés # ========== _encoding = 'utf-8' @property def encoding(self): return self._encoding @encoding.setter def encoding(self, value): if value: self._encoding = value @property def filename(self): if self.ftp_file: return self.ftp_file.name if self.up_file: return self.up_file.name return '{}.{}'.format(slugify(self.title), self.format.lower()) @property def ckan_url(self): return urljoin( CKAN_URL, 'dataset/{}/resource/{}/'.format(self.dataset.slug, self.ckan_id)) @property def api_location(self): kwargs = { 'dataset_name': self.dataset.slug, 'resource_id': self.ckan_id } return reverse('api:resource_show', kwargs=kwargs) @property def title_overflow(self): return three_suspension_points(self.title) @property def anonymous_access(self): return self.restricted_level == 'public' @property def is_datagis(self): return self.get_layers() and True or False # Méthodes héritées # ================= def save(self, *args, current_user=None, synchronize=False, file_extras=None, skip_download=False, update_m2m=False, update_dataset=True, **kwargs): if update_m2m: return super().save(*args, **kwargs) if 'update_fields' in kwargs: return super().save(*args, **kwargs) # Version précédante de la ressource (avant modification) previous, created = self.pk \ and (Resource.objects.get(pk=self.pk), False) or (None, True) if previous: # crs est immuable sauf si le jeu de données change (Cf. plus bas) self.crs = previous.crs # Quelques valeur par défaut à la création de l'instance if created: self.geo_restriction = False self.ogc_services = True self.extractable = True # La restriction au territoire de compétence désactive toujours les services OGC if self.geo_restriction: self.ogc_services = False self.last_update = timezone.now() if created: super().save(*args, **kwargs) kwargs['force_insert'] = False # Quelques contrôles sur les fichiers de données téléversée ou à télécharger filename = False content_type = None file_must_be_deleted = False # permet d'indiquer si les fichiers doivent être supprimés à la fin de la chaine de traitement publish_raw_resource = True # permet d'indiquer si les ressources brutes sont publiées dans CKAN if self.ftp_file and not skip_download: filename = self.ftp_file.file.name # Si la taille de fichier dépasse la limite autorisée, # on traite les données en fonction du type détecté if self.ftp_file.size > DATA_TRANSMISSION_SIZE_LIMITATION: logger.info("This is a big file: %s." % self.ftp_file.size) publish_raw_resource = False # IMPORTANT s0 = str(self.ckan_id) s1, s2, s3 = s0[:3], s0[3:6], s0[6:] dir = os.path.join(CKAN_STORAGE_PATH, s1, s2) os.makedirs(dir, mode=0o777, exist_ok=True) logger.info("cp %s %s" % (filename, os.path.join(dir, s3))) shutil.copyfile(filename, os.path.join(dir, s3)) src = os.path.join(dir, s3) dst = os.path.join(dir, filename.split('/')[-1]) logger.info("ln -s %s %s" % (dst, src)) try: os.symlink(src, dst) except (FileNotFoundError, FileExistsError) as e: logger.exception(e) logger.warning("Error was ignored.") pass elif (self.up_file and file_extras): # GDAL/OGR ne semble pas prendre de fichier en mémoire.. # ..à vérifier mais si c'est possible comment indiquer le vsi en préfixe du filename ? filename = self.up_file.path self.save(update_fields=('up_file', )) file_must_be_deleted = True elif self.dl_url and not skip_download: try: directory, filename, content_type = download( self.dl_url, settings.MEDIA_ROOT, max_size=DOWNLOAD_SIZE_LIMIT) except SizeLimitExceededError as e: logger.exception(e) l = len(str(e.max_size)) if l > 6: m = '{0} mo'.format(Decimal(int(e.max_size) / 1024 / 1024)) elif l > 3: m = '{0} ko'.format(Decimal(int(e.max_size) / 1024)) else: m = '{0} octets'.format(int(e.max_size)) raise ValidationError(('La taille du fichier dépasse ' 'la limite autorisée : {0}.').format(m), code='dl_url') except Exception as e: logger.exception(e) if e.__class__.__name__ == 'HTTPError': if e.response.status_code == 404: msg = ('La ressource distante ne semble pas exister. ' "Assurez-vous que l'URL soit correcte.") if e.response.status_code == 403: msg = ("Vous n'avez pas l'autorisation pour " 'accéder à la ressource.') if e.response.status_code == 401: msg = ('Une authentification est nécessaire ' 'pour accéder à la ressource.') else: msg = 'Le téléchargement du fichier a échoué.' raise ValidationError(msg, code='dl_url') file_must_be_deleted = True # Synchronisation avec CKAN # ========================= # La synchronisation doit s'effectuer avant la publication des # éventuelles couches de données SIG car dans le cas des données # de type « raster », nous utilisons le filestore de CKAN. if synchronize: if publish_raw_resource: self.synchronize(content_type=content_type, file_extras=file_extras, filename=filename, with_user=current_user) else: url = reduce(urljoin, [ CKAN_URL, 'dataset/', str(self.dataset.ckan_id) + '/', 'resource/', str(self.ckan_id) + '/', 'download/', Path(self.ftp_file.name).name ]) self.synchronize(url=url, with_user=current_user) # Détection des données SIG # ========================= if filename: # On vérifie s'il s'agit de données SIG, uniquement pour # les extensions de fichier autorisées.. extension = self.format_type.extension.lower() if self.format_type.is_gis_format: # Si c'est le cas, on monte les données dans la base PostGIS dédiée # et on déclare la couche au service OGC:WxS de l'organisation. # Mais d'abord, on vérifie si la ressource contient # déjà des « Layers », auquel cas il faudra vérifier si # la table de données a changée. existing_layers = {} if not created: existing_layers = dict( (re.sub('^(\w+)_[a-z0-9]{7}$', '\g<1>', layer.name), layer.name) for layer in self.get_layers()) try: # C'est carrément moche mais c'est pour aller vite. # Il faudrait factoriser tout ce bazar et créer # un décorateur pour gérer le rool-back sur CKAN. try: gdalogr_obj = get_gdalogr_object(filename, extension) except NotDataGISError: tables = [] pass else: try: self.format_type = ResourceFormats.objects.get( extension=extension, ckan_format=gdalogr_obj.format) # except ResourceFormats.MultipleObjectsReturned: # pass except Exception as e: logger.exception(e) logger.warning("Error was ignored.") pass # ========================== # Jeu de données vectorielle # ========================== if gdalogr_obj.__class__.__name__ == 'OgrOpener': # On convertit les données vers PostGIS try: tables = ogr2postgis( gdalogr_obj, update=existing_layers, epsg=self.crs and self.crs.auth_code or None, encoding=self.encoding) except NotOGRError as e: logger.exception(e) file_must_be_deleted and remove_file(filename) msg = ( "Le fichier reçu n'est pas reconnu " 'comme étant un jeu de données SIG correct.' ) raise ValidationError(msg, code='__all__') except DataDecodingError as e: logger.exception(e) file_must_be_deleted and remove_file(filename) msg = ( 'Impossible de décoder correctement les ' "données. Merci d'indiquer l'encodage " 'ci-dessous.') raise ValidationError(msg, code='encoding') except WrongDataError as e: logger.exception(e) file_must_be_deleted and remove_file(filename) msg = ( 'Votre ressource contient des données SIG que ' 'nous ne parvenons pas à lire correctement. ' 'Un ou plusieurs objets sont erronés.') raise ValidationError(msg) except NotFoundSrsError as e: logger.exception(e) file_must_be_deleted and remove_file(filename) msg = ( 'Votre ressource semble contenir des données SIG ' 'mais nous ne parvenons pas à détecter le système ' 'de coordonnées. Merci de sélectionner le code du ' 'CRS dans la liste ci-dessous.') raise ValidationError(msg, code='crs') except NotSupportedSrsError as e: logger.exception(e) file_must_be_deleted and remove_file(filename) msg = ( 'Votre ressource semble contenir des données SIG ' 'mais le système de coordonnées de celles-ci ' "n'est pas supporté par l'application.") raise ValidationError(msg, code='__all__') except ExceedsMaximumLayerNumberFixedError as e: logger.exception(e) file_must_be_deleted and remove_file(filename) raise ValidationError(e.__str__(), code='__all__') else: # Ensuite, pour tous les jeux de données SIG trouvés, # on crée le service ows à travers la création de `Layer` try: Layer = apps.get_model( app_label='idgo_admin', model_name='Layer') for table in tables: try: Layer.objects.get(name=table['id'], resource=self) except Layer.DoesNotExist: save_opts = { 'synchronize': synchronize } bbox = transform( table['bbox'], table['epsg']) Layer.vector.create( bbox=bbox, name=table['id'], resource=self, save_opts=save_opts) except Exception as e: logger.exception(e) file_must_be_deleted and remove_file( filename) for table in tables: drop_table(table['id']) raise e # ========================== # Jeu de données matricielle # ========================== if gdalogr_obj.__class__.__name__ == 'GdalOpener': coverage = gdalogr_obj.get_coverage() try: tables = [ gdalinfo( coverage, update=existing_layers, epsg=self.crs and self.crs.auth_code or None) ] except NotFoundSrsError as e: logger.exception(e) file_must_be_deleted and remove_file(filename) msg = ( 'Votre ressource semble contenir des données SIG ' 'mais nous ne parvenons pas à détecter le système ' 'de coordonnées. Merci de sélectionner le code du ' 'CRS dans la liste ci-dessous.') raise ValidationError(msg, code='crs') except NotSupportedSrsError as e: logger.exception(e) file_must_be_deleted and remove_file(filename) msg = ( 'Votre ressource semble contenir des données SIG ' 'mais le système de coordonnées de celles-ci ' "n'est pas supporté par l'application.") raise ValidationError(msg, code='__all__') # Super Crado Code s0 = str(self.ckan_id) s1, s2, s3 = s0[:3], s0[3:6], s0[6:] dir = os.path.join(CKAN_STORAGE_PATH, s1, s2) src = os.path.join(dir, s3) dst = os.path.join(dir, filename.split('/')[-1]) try: os.symlink(src, dst) except FileExistsError as e: logger.exception(e) except FileNotFoundError as e: logger.exception(e) else: logger.debug( 'Created a symbolic link {dst} pointing to {src}.' .format(dst=dst, src=src)) try: Layer = apps.get_model(app_label='idgo_admin', model_name='Layer') for table in tables: try: Layer.objects.get(name=table['id'], resource=self) except Layer.DoesNotExist: Layer.raster.create(bbox=table['bbox'], name=table['id'], resource=self) except Exception as e: logger.exception(e) file_must_be_deleted and remove_file(filename) raise e except Exception as e: logger.exception(e) if created: if current_user: username = current_user.username apikey = CkanHandler.get_user(username)['apikey'] with CkanUserHandler(apikey) as ckan: ckan.delete_resource(str(self.ckan_id)) else: CkanHandler.delete_resource(str(self.ckan_id)) for layer in self.get_layers(): layer.delete(current_user=current_user) # Puis on « raise » l'erreur raise e # On met à jour les champs de la ressource SupportedCrs = apps.get_model(app_label='idgo_admin', model_name='SupportedCrs') crs = [ SupportedCrs.objects.get(auth_name='EPSG', auth_code=table['epsg']) for table in tables ] # On prend la première valeur (c'est moche) self.crs = crs and crs[0] or None # Si les données changent.. if existing_layers and \ set(previous.get_layers()) != set(self.get_layers()): # on supprime les anciens `layers`.. for layer in previous.get_layers(): layer.delete() #### if self.get_layers(): extent = self.get_layers().aggregate( models.Extent('bbox')).get('bbox__extent') if extent: xmin, ymin = extent[0], extent[1] xmax, ymax = extent[2], extent[3] setattr(self, 'bbox', bounds_to_wkt(xmin, ymin, xmax, ymax)) else: # Si la ressource n'est pas de type SIG, on passe les trois arguments # qui concernent exclusivement ces dernières à « False ». self.geo_restriction = False self.ogc_services = False self.extractable = False super().save(*args, **kwargs) # Puis dans tous les cas.. # on met à jour le statut des couches du service cartographique.. if not created: self.update_enable_layers_status() # on supprime les données téléversées ou téléchargées.. if file_must_be_deleted: remove_file(filename) # [Crado] on met à jour la ressource CKAN if synchronize: try: CkanHandler.update_resource(str(self.ckan_id), extracting_service=str( self.extractable)) except Exception as e: logger.exception(e) logger.warning("Error was ignored.") for layer in self.get_layers(): layer.save(synchronize=synchronize) if update_dataset: self.dataset.date_modification = timezone.now().date() self.dataset.save(current_user=None, synchronize=True, update_fields=['date_modification']) def delete(self, *args, current_user=None, synchronize_dataset=True, **kwargs): with_user = current_user for layer in self.get_layers(): layer.delete(current_user=current_user) # On supprime la ressource CKAN ckan_id = str(self.ckan_id) if with_user: username = with_user.username apikey = CkanHandler.get_user(username)['apikey'] with CkanUserHandler(apikey=apikey) as ckan_user: ckan_user.delete_resource(ckan_id) else: CkanHandler.delete_resource(ckan_id) # On supprime l'instance super().delete(*args, **kwargs) # Ce n'est vraiment pas une bonne idée de synchroniser ici le dataset : self.dataset.date_modification = timezone.now().date() self.dataset.save(current_user=current_user, synchronize=synchronize_dataset, update_fields=['date_modification']) # Autres méthodes # =============== def synchronize(self, url=None, filename=None, content_type=None, file_extras=None, with_user=None): """Synchronizer le jeu de données avec l'instance de CKAN.""" # Identifiant de la resource CKAN : id = str(self.ckan_id) ckan_resource = {} try: ckan_resource = CkanHandler.get_resource(id) except Exception as e: logger.warning(e) # Définition des propriétés du « package » : data = { 'crs': self.crs and self.crs.description or '', 'name': self.title, 'description': self.description, 'data_type': self.data_type, 'extracting_service': str(self.extractable or False), # I <3 CKAN 'format': self.format_type and self.format_type.ckan_format, 'view_type': self.format_type and self.format_type.ckan_view, 'id': id, 'lang': self.lang, 'restricted_by_jurisdiction': str(self.geo_restriction), 'url': url and url or '', 'api': ckan_resource.get('api', '{}'), } # (0) Aucune restriction if self.restricted_level == 'public': restricted = json.dumps({'level': 'public'}) # (1) Uniquement pour un utilisateur connecté elif self.restricted_level == 'registered': restricted = json.dumps({'level': 'registered'}) # (2) Seulement les utilisateurs indiquées elif self.restricted_level == 'only_allowed_users': restricted = json.dumps({ 'allowed_users': ','.join( self.profiles_allowed.exists() and [p.user.username for p in self.profiles_allowed.all()] or []), 'level': 'only_allowed_users' }) # (3) Les utilisateurs de cette organisation elif self.restricted_level == 'same_organization': restricted = json.dumps({ 'allowed_users': ','.join( get_all_users_for_organisations( self.organisations_allowed.all())), 'level': 'only_allowed_users' }) # (3) Les utilisateurs des organisations indiquées elif self.restricted_level == 'any_organization': restricted = json.dumps({ 'allowed_users': ','.join( get_all_users_for_organisations( self.organisations_allowed.all())), 'level': 'only_allowed_users' }) # (4) Les utilisateurs partenaires IDGO elif self.restricted_level == 'only_idgo_partners': restricted = json.dumps({ 'allowed_groups': ['idgo-partner'], 'level': 'only_group_member' }) data['restricted'] = restricted if self.referenced_url: data['url'] = self.referenced_url if self.dl_url and filename: downloaded_file = File(open(filename, 'rb')) data['upload'] = downloaded_file data['size'] = downloaded_file.size data['mimetype'] = content_type if self.up_file and file_extras: data['upload'] = self.up_file.file data['size'] = file_extras.get('size') data['mimetype'] = file_extras.get('mimetype') if self.ftp_file: if not url and filename: data['upload'] = self.ftp_file.file data['size'] = self.ftp_file.size if url or filename: if self.format_type and ( type(self.format_type.mimetype) is list and len(self.format_type.mimetype)): data['mimetype'] = self.format_type.mimetype[0] else: data['mimetype'] = 'text/plain' # data['force_url_type'] = 'upload' # NON PREVU PAR CKAN API if self.data_type == 'raw': if self.ftp_file or self.dl_url or self.up_file: data['resource_type'] = 'file.upload' elif self.referenced_url: data['resource_type'] = 'file' if self.data_type == 'annexe': data['resource_type'] = 'documentation' if self.data_type == 'service': data['resource_type'] = 'api' ckan_package = CkanHandler.get_package(str(self.dataset.ckan_id)) if with_user: username = with_user.username apikey = CkanHandler.get_user(username)['apikey'] with CkanUserHandler(apikey=apikey) as ckan: ckan.publish_resource(ckan_package, **data) else: return CkanHandler.publish_resource(ckan_package, **data) def get_layers(self, **kwargs): Layer = apps.get_model(app_label='idgo_admin', model_name='Layer') return Layer.objects.filter(resource=self, **kwargs) def update_enable_layers_status(self): for layer in self.get_layers(): layer.handle_enable_ows_status() def is_profile_authorized(self, user): Profile = apps.get_model(app_label='idgo_admin', model_name='Profile') if not user.pk: raise IntegrityError("User does not exists.") if self.restricted_level == 'only_allowed_users' and self.profiles_allowed.exists( ): return user in [p.user for p in self.profiles_allowed.all()] elif self.restricted_level in ( 'same_organization', 'any_organization') and self.organisations_allowed.exists(): return user in [ p.user for p in Profile.objects.filter( organisation__in=self.organisations_allowed.all(), organisation__is_active=True) ] elif self.restricted_level == 'only_idgo_partners': return user in [ p.user for p in Profile.objects.filter(crige_membership=True) ] return True
class Erp(models.Model): HISTORY_MAX_LATEST_ITEMS = 25 # Fix me : move to settings SOURCE_ACCESLIBRE = "acceslibre" SOURCE_ADMIN = "admin" SOURCE_API = "api" SOURCE_API_ENTREPRISE = "entreprise_api" SOURCE_CCONFORME = "cconforme" SOURCE_GENDARMERIE = "gendarmerie" SOURCE_NESTENN = "nestenn" SOURCE_ODS = "opendatasoft" SOURCE_PUBLIC = "public" SOURCE_PUBLIC_ERP = "public_erp" SOURCE_SERVICE_PUBLIC = "service_public" SOURCE_SIRENE = "sirene" SOURCE_TH = "tourisme-handicap" SOURCE_VACCINATION = "centres-vaccination" SOURCE_CHOICES = ( (SOURCE_ACCESLIBRE, "Base de données Acceslibre"), (SOURCE_ADMIN, "Back-office"), (SOURCE_API, "API"), (SOURCE_API_ENTREPRISE, "API Entreprise (publique)"), (SOURCE_CCONFORME, "cconforme"), (SOURCE_GENDARMERIE, "Gendarmerie"), (SOURCE_NESTENN, "Nestenn"), (SOURCE_ODS, "API OpenDataSoft"), (SOURCE_PUBLIC, "Saisie manuelle publique"), (SOURCE_PUBLIC_ERP, "API des établissements publics"), (SOURCE_SERVICE_PUBLIC, "Service Public"), (SOURCE_SIRENE, "API Sirene INSEE"), (SOURCE_TH, "Tourisme & Handicap"), (SOURCE_VACCINATION, "Centres de vaccination"), ) USER_ROLE_ADMIN = "admin" USER_ROLE_GESTIONNAIRE = "gestionnaire" USER_ROLE_PUBLIC = "public" USER_ROLE_SYSTEM = "system" USER_ROLES = ( (USER_ROLE_ADMIN, "Administration"), (USER_ROLE_GESTIONNAIRE, "Gestionnaire"), (USER_ROLE_PUBLIC, "Utilisateur public"), (USER_ROLE_SYSTEM, "Système"), ) class Meta: ordering = ("nom", ) verbose_name = "Établissement" verbose_name_plural = "Établissements" indexes = [ models.Index(fields=["source", "source_id"]), models.Index(fields=["slug"]), models.Index(fields=["commune"]), models.Index(fields=["commune", "activite_id"]), models.Index(fields=["user_type"]), GinIndex(name="nom_trgm", fields=["nom"], opclasses=["gin_trgm_ops"]), GinIndex(fields=["search_vector"]), GinIndex(fields=["metadata"], name="gin_metadata"), ] objects = managers.ErpQuerySet.as_manager() uuid = models.UUIDField(default=uuid.uuid4, unique=True) source = models.CharField( max_length=100, null=True, verbose_name="Source", default=SOURCE_PUBLIC, choices=SOURCE_CHOICES, help_text="Nom de la source de données dont est issu cet ERP", ) source_id = models.CharField( max_length=255, null=True, verbose_name="Source ID", help_text="Identifiant de l'ERP dans la source initiale de données", ) user = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, verbose_name="Contributeur", on_delete=models.SET_NULL, ) user_type = models.CharField( max_length=50, choices=USER_ROLES, verbose_name="Profil de contributeur", default=USER_ROLE_SYSTEM, ) commune_ext = models.ForeignKey( Commune, null=True, blank=True, verbose_name="Commune (relation)", help_text="La commune de cet établissement", on_delete=models.SET_NULL, ) nom = models.CharField(max_length=255, help_text="Nom de l'établissement ou de l'enseigne") slug = AutoSlugField( default="", unique=True, populate_from="nom", help_text="Identifiant d'URL (slug)", max_length=255, ) activite = models.ForeignKey( Activite, null=True, blank=True, verbose_name="Activité", help_text= "Domaine d'activité de l'ERP. Attention, la recherche se fait sur les lettres accentuées", on_delete=models.SET_NULL, ) published = models.BooleanField( default=True, verbose_name="Publié", help_text= "Statut de publication de cet ERP: si la case est décochée, l'ERP ne sera pas listé publiquement", ) geom = models.PointField( null=True, blank=True, verbose_name="Localisation", help_text= "Géolocalisation (carte rafraîchie une fois l'enregistrement sauvegardé)", ) siret = models.CharField( max_length=14, null=True, blank=True, verbose_name="SIRET", help_text="Numéro SIRET si l'ERP est une entreprise", ) # contact telephone = models.CharField( max_length=20, null=True, blank=True, verbose_name="Téléphone", help_text="Numéro de téléphone de l'ERP", ) site_internet = models.URLField( max_length=255, null=True, blank=True, help_text="Adresse du site internet de l'ERP", ) contact_email = models.EmailField( max_length=255, null=True, blank=True, verbose_name="Courriel", help_text="Adresse email permettant de contacter l'ERP", ) contact_url = models.URLField( max_length=255, null=True, blank=True, verbose_name="Lien vers outil de contact", help_text= "Lien hypertexte permettant de contacter l'établissement (formulaire, chatbot, etc.)", ) # adresse numero = models.CharField( max_length=255, null=True, blank=True, verbose_name="Numéro", help_text= "Numéro dans la voie, incluant le complément (BIS, TER, etc.)", ) voie = models.CharField(max_length=255, null=True, blank=True, help_text="Voie") lieu_dit = models.CharField(max_length=255, null=True, blank=True, help_text="Lieu dit") code_postal = models.CharField(max_length=5, help_text="Code postal") commune = models.CharField(max_length=255, help_text="Nom de la commune") code_insee = models.CharField( max_length=5, null=True, blank=True, verbose_name="Code INSEE", help_text="Code INSEE de la commune", ) # Metadata # Notes: # - DO NOT store Python datetimes or attempt to pass some; JSON doesn't # have a native datetime type, so while we could encode dates, we couldn't # reliably decode them # - For updating nested values, you have to retrieve the whole object, # update the target nested values so the metadata object is mutated, then # save the instance. See tests for illustration. # XXX: we might want to provide convenient getter and setters targetting # given nested keys later at some point. metadata = models.JSONField(default=dict) # datetimes created_at = models.DateTimeField(auto_now_add=True, verbose_name="Date de création") updated_at = models.DateTimeField(auto_now=True, verbose_name="Dernière modification") # search vector search_vector = SearchVectorField("Search vector", null=True) def __str__(self): return f"ERP #{self.id} ({self.nom}, {self.commune})" def get_activite_icon(self): default = "amenity_public_building" if self.activite and self.activite.icon: return self.activite.icon return default def get_activite_vector_icon(self): default = "building" if self.activite and self.activite.vector_icon: return self.activite.vector_icon return default def get_history(self, exclude_changes_from=None): "Combines erp and related accessibilite histories." erp_history = _get_history( self.get_versions(), exclude_fields=( "uuid", "source", "source_id", "search_vector", ), exclude_changes_from=exclude_changes_from, ) accessibilite_history = self.accessibilite.get_history( exclude_changes_from=exclude_changes_from) global_history = erp_history + accessibilite_history global_history.sort(key=lambda x: x["date"], reverse=True) return global_history def get_versions(self): # take the last n revisions qs = (Version.objects.get_for_object(self).select_related( "revision__user").order_by("-revision__date_created") [:self.HISTORY_MAX_LATEST_ITEMS + 1]) # make it a list, so it's reversable versions = list(qs) # reorder the slice by date_created ASC versions.reverse() return versions def editable_by(self, user): if not user.is_active: return False # admins can do whatever they want if user.is_superuser: return True # intrapreneurs can update any erps if "intrapreneurs" in list(user.groups.values_list("name")): return True # users can take over erps with no owner if not self.user: return True # check ownership if user.id != self.user.id: return False return True def get_absolute_uri(self): return f"{settings.SITE_ROOT_URL}{self.get_absolute_url()}" def get_absolute_url(self): if self.commune_ext: commune_slug = self.commune_ext.slug else: commune_slug = slugify(f"{self.departement}-{self.commune}") if self.activite is None: return reverse( "commune_erp", kwargs=dict(commune=commune_slug, erp_slug=self.slug), ) else: return reverse( "commune_activite_erp", kwargs=dict( commune=commune_slug, activite_slug=self.activite.slug, erp_slug=self.slug, ), ) def get_admin_url(self): return (reverse("admin:erp_erp_change", kwargs={"object_id": self.pk}) if self.pk else None) def get_global_timestamps(self): (created_at, updated_at) = (self.created_at, self.updated_at) if self.has_accessibilite(): (a_created_at, a_updated_at) = ( self.accessibilite.created_at, self.accessibilite.updated_at, ) (created_at, updated_at) = ( a_created_at if a_created_at > created_at else created_at, a_updated_at if a_updated_at > updated_at else updated_at, ) return { "created_at": created_at, "updated_at": updated_at, } def has_accessibilite(self): return hasattr(self, "accessibilite") and self.accessibilite is not None def is_online(self): return self.published and self.has_accessibilite( ) and self.geom is not None def is_subscribed_by(self, user): return ErpSubscription.objects.filter(user=user, erp=self).count() == 1 @property def adresse(self): pieces = filter( lambda x: x is not None, [ self.numero, self.voie, self.lieu_dit, self.code_postal, self.commune_ext.nom if self.commune_ext else self.commune, ], ) return " ".join(pieces).strip().replace(" ", " ") @property def short_adresse(self): pieces = filter( lambda x: x is not None, [ self.numero, self.voie, self.lieu_dit, ], ) return " ".join(pieces).strip().replace(" ", " ") @property def departement(self): return self.code_postal[:2] @classmethod def update_coordinates(cls): counter = 0 erp_updates = 0 for e in cls.objects.filter(commune_ext__isnull=False): if not e.commune_ext.in_contour(Point(e.geom.x, e.geom.y)): print(f"Erp concerné : {e.nom}; {e.code_postal}; {e.commune}") counter += 1 try: coordinates = geocoder.geocode( e.short_adresse, citycode=e.commune_ext.code_insee) except Exception as error: print(error) else: if coordinates: e.geom = Point(coordinates["geom"][0], coordinates["geom"][1]) e.save() erp_updates += 1 else: print("No Coordinates") print(f"{erp_updates} erps mis à jour sur {counter}") @classmethod def fix_import_service_public(cls): qs = cls.objects.filter(numero__isnull=False, voie__isnull=False) for erp in qs: if all(not char.isdigit() for char in erp.numero): erp.voie = f"{erp.numero} {erp.voie}" erp.numero = None erp.save() @classmethod def export_doublons(cls): filename = "doublons.csv" start_date = datetime.date(2022, 1, 19) qs = cls.objects.filter(accessibilite__isnull=False) if os.path.exists(filename): os.remove(filename) csv = open(filename, "w") doublons = list(e["erp_list"] for e in qs.annotate( voie_lower=Lower("voie"), commune_lower=Lower("commune")).values( "numero", "voie_lower", "code_postal", "commune_lower" ).annotate(erp_count=Count("pk"), erp_list=ArrayAgg( "pk")).order_by("-erp_count").filter(erp_count__gt=1)) csv.write( f"created_at;nom;numero;voie;code_postal;commune;activite;{Accessibilite.export_data_comma_headers()}\n" ) counter_doublons = 0 for e in doublons: if any(erp.created_at.date() >= start_date for erp in cls.objects.filter(pk__in=e)): for id in e: counter_doublons += 1 erp = cls.objects.get(pk=id) csv.write( f"{erp.created_at.date()};{erp.nom};{erp.numero or ''};{erp.voie};{erp.code_postal};{erp.commune};{erp.activite};{erp.accessibilite.export_data_comma()};\n" ) csv.close() print(f"{counter_doublons} erps exportés dans {filename}") def clean(self): # Fix me : move to form (abstract) # Code postal if self.code_postal and len(self.code_postal) != 5: raise ValidationError( {"code_postal": "Le code postal doit faire 5 caractères"}) # Voie OU lieu-dit sont requis if self.voie is None and self.lieu_dit is None: error = "Veuillez entrer une voie ou un lieu-dit" raise ValidationError({"voie": error, "lieu_dit": error}) # Commune if self.commune and self.code_postal: matches = Commune.objects.filter( nom__unaccent__iexact=self.commune, code_postaux__contains=[self.code_postal], ) if len(matches) == 0: matches = Commune.objects.filter( code_postaux__contains=[self.code_postal]) if len(matches) == 0: matches = Commune.objects.filter(code_insee=self.code_insee) if len(matches) == 0: matches = Commune.objects.filter( nom__unaccent__iexact=self.commune, ) if len(matches) == 0: raise ValidationError({ "commune": f"Commune {self.commune} introuvable, veuillez vérifier votre saisie." }) else: self.commune_ext = matches[0] # SIRET if self.siret: siret = sirene.validate_siret(self.siret) if siret is None: raise ValidationError( {"siret": "Ce numéro SIRET est invalide."}) self.siret = siret def vote(self, user, action, comment=None): votes = Vote.objects.filter(erp=self, user=user) if votes.count() > 0: vote = votes.first() # check for vote cancellation if (action == "UP" and vote.value == 1) or (action == "DOWN" and vote.value == -1 and not comment): vote.delete() return None else: vote = Vote(erp=self, user=user) vote.value = 1 if action == "UP" else -1 vote.comment = comment if action == "DOWN" else None vote.save() return vote def save(self, *args, **kwargs): search_vector = SearchVector( Value(self.nom, output_field=models.TextField()), weight="A", config=FULLTEXT_CONFIG, ) if self.activite is not None: search_vector = search_vector + SearchVector( Value( self.activite.nom, output_field=models.TextField(), ), weight="A", config=FULLTEXT_CONFIG, ) if self.activite.mots_cles is not None: search_vector = search_vector + SearchVector( Value( " ".join(self.activite.mots_cles), output_field=models.TextField(), ), weight="B", config=FULLTEXT_CONFIG, ) self.search_vector = search_vector super().save(*args, **kwargs)
class BoundarySet(models.Model): """ A set of boundaries, corresponding to one or more shapefiles. """ slug = models.SlugField(max_length=200, primary_key=True, editable=False, help_text=ugettext_lazy("The boundary set's unique identifier, used as a path component in URLs.")) name = models.CharField(max_length=100, unique=True, help_text=ugettext_lazy('The plural name of the boundary set.')) singular = models.CharField(max_length=100, help_text=ugettext_lazy('A generic singular name for a boundary in the set.')) authority = models.CharField(max_length=256, help_text=ugettext_lazy('The entity responsible for publishing the data.')) domain = models.CharField(max_length=256, help_text=ugettext_lazy("The geographic area covered by the boundary set.")) last_updated = models.DateField( help_text=ugettext_lazy('The most recent date on which the data was updated.')) source_url = models.URLField(blank=True, help_text=ugettext_lazy('A URL to the source of the data.')) notes = models.TextField(blank=True, help_text=ugettext_lazy('Free-form text notes, often used to describe changes that were made to the original source data.')) licence_url = models.URLField(blank=True, help_text=ugettext_lazy('A URL to the licence under which the data is made available.')) extent = JSONField(blank=True, null=True, help_text=ugettext_lazy("The set's boundaries' bounding box as a list like [xmin, ymin, xmax, ymax] in EPSG:4326.")) start_date = models.DateField(blank=True, null=True, help_text=ugettext_lazy("The date from which the set's boundaries are in effect.")) end_date = models.DateField(blank=True, null=True, help_text=ugettext_lazy("The date until which the set's boundaries are in effect.")) extra = JSONField(default={}, help_text=ugettext_lazy("Any additional metadata.")) name_plural = property(lambda s: s.name) name_singular = property(lambda s: s.singular) api_fields = ('name_plural', 'name_singular', 'authority', 'domain', 'source_url', 'notes', 'licence_url', 'last_updated', 'extent', 'extra', 'start_date', 'end_date') api_fields_doc_from = {'name_plural': 'name', 'name_singular': 'singular'} class Meta: ordering = ('name',) verbose_name = ugettext_lazy('boundary set') verbose_name_plural = ugettext_lazy('boundary sets') def __str__(self): return self.name def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) return super(BoundarySet, self).save(*args, **kwargs) def as_dict(self): r = { 'related': { 'boundaries_url': urlresolvers.reverse('boundaries_boundary_list', kwargs={'set_slug': self.slug}), }, } for field in self.api_fields: r[field] = getattr(self, field) if not isinstance(r[field], (string_types, int, list, tuple, dict)) and r[field] is not None: r[field] = text_type(r[field]) return r @staticmethod def get_dicts(sets): return [ { 'url': urlresolvers.reverse('boundaries_set_detail', kwargs={'slug': s.slug}), 'related': { 'boundaries_url': urlresolvers.reverse('boundaries_boundary_list', kwargs={'set_slug': s.slug}), }, 'name': s.name, 'domain': s.domain, } for s in sets ] def extend(self, extent): if self.extent[0] is None or extent[0] < self.extent[0]: self.extent[0] = extent[0] if self.extent[1] is None or extent[1] < self.extent[1]: self.extent[1] = extent[1] if self.extent[2] is None or extent[2] > self.extent[2]: self.extent[2] = extent[2] if self.extent[3] is None or extent[3] > self.extent[3]: self.extent[3] = extent[3]
class Accessibilite(models.Model): HISTORY_MAX_LATEST_ITEMS = 25 class Meta: verbose_name = "Accessibilité" verbose_name_plural = "Accessibilité" erp = models.OneToOneField( Erp, on_delete=models.CASCADE, null=True, blank=True, verbose_name="Établissement", help_text="ERP", ) ################################### # Transports en commun # ################################### # Station de transport en commun transport_station_presence = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("transport_station_presence"), verbose_name="Desserte par transports en commun", ) transport_information = models.TextField( max_length=1000, null=True, blank=True, verbose_name="Informations transports", ) ################################### # Stationnement # ################################### # Stationnement dans l'ERP stationnement_presence = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("stationnement_presence"), verbose_name="Stationnement dans l'ERP", ) stationnement_pmr = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("stationnement_pmr"), verbose_name="Stationnements PMR dans l'ERP", ) # Stationnement à proximité stationnement_ext_presence = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("stationnement_ext_presence"), verbose_name="Stationnement à proximité de l'ERP", ) stationnement_ext_pmr = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("stationnement_ext_pmr"), verbose_name="Stationnements PMR à proximité de l'ERP", ) ################################### # Espace et Cheminement extérieur # ################################### cheminement_ext_presence = models.BooleanField( null=True, blank=True, choices=schema.NULLABLE_OR_NA_BOOLEAN_CHOICES, verbose_name="Espace extérieur", ) # Cheminement de plain-pied – oui / non / inconnu cheminement_ext_plain_pied = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("cheminement_ext_plain_pied"), verbose_name="Cheminement de plain-pied", ) # Terrain meuble ou accidenté cheminement_ext_terrain_accidente = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("cheminement_ext_terrain_accidente"), verbose_name="Terrain meuble ou accidenté", ) # Nombre de marches – nombre entre 0 et >10 cheminement_ext_nombre_marches = models.PositiveSmallIntegerField( null=True, blank=True, verbose_name="Nombre de marches", ) # Sens des marches de l'escalier cheminement_ext_sens_marches = models.CharField( max_length=20, null=True, blank=True, verbose_name="Sens de circulation de l'escalier", choices=schema.ESCALIER_SENS, ) # Repérage des marches ou de l’escalier – oui / non / inconnu / sans objet cheminement_ext_reperage_marches = models.BooleanField( null=True, blank=True, choices=schema.NULLABLE_OR_NA_BOOLEAN_CHOICES, verbose_name="Repérage des marches ou de l’escalier", ) # Main courante - oui / non / inconnu / sans objet cheminement_ext_main_courante = models.BooleanField( null=True, blank=True, choices=schema.NULLABLE_OR_NA_BOOLEAN_CHOICES, verbose_name="Main courante", ) # Rampe – oui / non / inconnu / sans objet cheminement_ext_rampe = models.CharField( max_length=20, null=True, blank=True, choices=schema.RAMPE_CHOICES, verbose_name="Rampe", ) # Ascenseur / élévateur : oui / non / inconnu / sans objet cheminement_ext_ascenseur = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("cheminement_ext_ascenseur"), verbose_name="Ascenseur/élévateur", ) # Pente - oui / non / inconnu cheminement_ext_pente_presence = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("cheminement_ext_pente_presence"), verbose_name="Pente présence", ) # Pente - Aucune, légère, importante, inconnu cheminement_ext_pente_degre_difficulte = models.CharField( max_length=15, null=True, blank=True, choices=schema.PENTE_CHOICES, verbose_name="Difficulté de la pente", ) # Pente - Aucune, légère, importante, inconnu cheminement_ext_pente_longueur = models.CharField( max_length=15, null=True, blank=True, choices=schema.PENTE_LENGTH_CHOICES, verbose_name="Longueur de la pente", ) # Dévers - Aucun, léger, important, inconnu cheminement_ext_devers = models.CharField( max_length=15, null=True, blank=True, verbose_name="Dévers", choices=schema.DEVERS_CHOICES, ) # Bande de guidage – oui / non / inconnu cheminement_ext_bande_guidage = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("cheminement_ext_bande_guidage"), verbose_name="Bande de guidage", ) # Rétrécissement du cheminement – oui / non / inconnu cheminement_ext_retrecissement = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("cheminement_ext_retrecissement"), verbose_name="Rétrécissement du cheminement", ) ########## # Entrée # ########## # Entrée facilement repérable – oui / non / inconnu entree_reperage = models.BooleanField( null=True, blank=True, choices=schema.NULLABLE_OR_NA_BOOLEAN_CHOICES, verbose_name="Entrée facilement repérable", ) # Présence d'une porte (oui / non) entree_porte_presence = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("entree_porte_presence"), verbose_name="Y a-t-il une porte ?", ) # Manoeuvre de la porte (porte battante / porte coulissante / tourniquet / porte tambour / inconnu ou sans objet) entree_porte_manoeuvre = models.CharField( max_length=255, null=True, blank=True, choices=schema.PORTE_MANOEUVRE_CHOICES, verbose_name="Manœuvre de la porte", ) # Type de porte (manuelle / automatique / inconnu) entree_porte_type = models.CharField( max_length=255, null=True, blank=True, choices=schema.PORTE_TYPE_CHOICES, verbose_name="Type de porte", ) # Entrée vitrée entree_vitree = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("entree_vitree"), verbose_name="Entrée vitrée", ) entree_vitree_vitrophanie = models.BooleanField( null=True, blank=True, choices=schema.NULLABLE_OR_NA_BOOLEAN_CHOICES, verbose_name="Vitrophanie", ) # Entrée de plain-pied entree_plain_pied = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("entree_plain_pied"), verbose_name="Entrée de plain-pied", ) # Nombre de marches entree_marches = models.PositiveSmallIntegerField( null=True, blank=True, verbose_name="Marches d'escalier", ) # Sens des marches de l'escalier entree_marches_sens = models.CharField( max_length=20, null=True, blank=True, verbose_name="Sens de circulation de l'escalier", choices=schema.ESCALIER_SENS, ) # Repérage des marches ou de l'escalier entree_marches_reperage = models.BooleanField( null=True, blank=True, choices=schema.NULLABLE_OR_NA_BOOLEAN_CHOICES, verbose_name="Repérage de l'escalier", ) # Main courante entree_marches_main_courante = models.BooleanField( null=True, blank=True, choices=schema.NULLABLE_OR_NA_BOOLEAN_CHOICES, verbose_name="Main courante", ) # Rampe entree_marches_rampe = models.CharField( max_length=20, null=True, blank=True, verbose_name="Rampe", choices=schema.RAMPE_CHOICES, ) # Système de guidage sonore – oui / non / inconnu entree_balise_sonore = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("entree_balise_sonore"), verbose_name="Présence d'une balise sonore", ) # Dispositif d’appel entree_dispositif_appel = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("entree_dispositif_appel"), verbose_name="Dispositif d'appel", ) entree_dispositif_appel_type = ArrayField( models.CharField(max_length=255, blank=True, choices=schema.DISPOSITIFS_APPEL_CHOICES), verbose_name="Dispositifs d'appel disponibles", default=list, null=True, blank=True, ) entree_aide_humaine = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("entree_aide_humaine"), verbose_name="Aide humaine", ) entree_ascenseur = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("entree_ascenseur"), verbose_name="Ascenseur/élévateur", ) # Largeur minimale entree_largeur_mini = models.PositiveSmallIntegerField( null=True, blank=True, verbose_name="Largeur minimale", ) # Entrée spécifique PMR entree_pmr = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("entree_pmr"), verbose_name="Entrée spécifique PMR", ) # Informations sur l’entrée spécifique entree_pmr_informations = models.TextField( max_length=500, null=True, blank=True, verbose_name="Infos entrée spécifique PMR", ) ########### # Accueil # ########### # Visibilité directe de la zone d'accueil depuis l’entrée accueil_visibilite = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("accueil_visibilite"), verbose_name="Visibilité directe de la zone d'accueil depuis l'entrée", ) # Personnel d’accueil accueil_personnels = models.CharField( max_length=255, null=True, blank=True, choices=schema.PERSONNELS_CHOICES, verbose_name="Personnel d'accueil", ) # Équipements pour personnes sourdes ou malentendantes accueil_equipements_malentendants_presence = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices( "accueil_equipements_malentendants_presence"), verbose_name="Présence d'équipement(s) sourds/malentendants", ) # Équipements pour personnes sourdes ou malentendantes accueil_equipements_malentendants = ArrayField( models.CharField(max_length=255, blank=True, choices=schema.EQUIPEMENT_MALENTENDANT_CHOICES), verbose_name="Équipement(s) sourd/malentendant", default=list, null=True, blank=True, ) # Cheminement de plain pied entre l’entrée et l’accueil accueil_cheminement_plain_pied = models.BooleanField( null=True, blank=True, choices=schema.NULLABLE_OR_NA_BOOLEAN_CHOICES, verbose_name="Cheminement de plain pied", ) # Présence de marches entre l’entrée et l’accueil – nombre entre 0 et >10 accueil_cheminement_nombre_marches = models.PositiveSmallIntegerField( null=True, blank=True, verbose_name="Nombre de marches", ) # Sens des marches de l'escalier accueil_cheminement_sens_marches = models.CharField( max_length=20, null=True, blank=True, verbose_name="Sens de circulation de l'escalier", choices=schema.ESCALIER_SENS, ) # Repérage des marches ou de l’escalier accueil_cheminement_reperage_marches = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices( "accueil_cheminement_reperage_marches"), verbose_name="Repérage des marches ou de l’escalier", ) # Main courante accueil_cheminement_main_courante = models.BooleanField( null=True, blank=True, choices=schema.NULLABLE_OR_NA_BOOLEAN_CHOICES, verbose_name="Main courante", ) # Rampe – aucune / fixe / amovible / inconnu accueil_cheminement_rampe = models.CharField( max_length=20, null=True, blank=True, choices=schema.RAMPE_CHOICES, verbose_name="Rampe", ) # Ascenseur / élévateur accueil_cheminement_ascenseur = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("accueil_cheminement_ascenseur"), verbose_name="Ascenseur/élévateur", ) # Rétrécissement du cheminement accueil_retrecissement = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("accueil_retrecissement"), verbose_name="Rétrécissement du cheminement", ) ############## # Sanitaires # ############## sanitaires_presence = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("sanitaires_presence"), verbose_name="Sanitaires", ) sanitaires_adaptes = models.PositiveSmallIntegerField( null=True, blank=True, verbose_name="Nombre de sanitaires adaptés", ) ########## # labels # ########## labels = ArrayField( models.CharField(max_length=255, blank=True, choices=schema.LABEL_CHOICES), verbose_name="Marques ou labels", default=list, null=True, blank=True, ) labels_familles_handicap = ArrayField( models.CharField(max_length=255, blank=True, choices=schema.HANDICAP_CHOICES), verbose_name="Famille(s) de handicap concernées(s)", default=list, null=True, blank=True, ) labels_autre = models.CharField( max_length=255, null=True, blank=True, verbose_name="Autre label", ) ##################### # Commentaire libre # ##################### commentaire = models.TextField( max_length=1000, null=True, blank=True, verbose_name="Commentaire libre", ) ########################## # Registre # ########################## registre_url = models.URLField( max_length=255, null=True, blank=True, verbose_name="URL du registre", ) ########################## # Conformité # ########################## conformite = models.BooleanField( null=True, blank=True, verbose_name="Conformité", choices=schema.get_field_choices("conformite"), ) # Datetimes created_at = models.DateTimeField(auto_now_add=True, verbose_name="Date de création") updated_at = models.DateTimeField(auto_now=True, verbose_name="Dernière modification") def __str__(self): if self.erp: return f'Accessibilité de l\'établissement "{self.erp.nom}" ({self.erp.code_postal})' else: return "Caractéristiques d'accessibilité de cet ERP" def get_history(self, exclude_changes_from=None): return _get_history(self.get_versions(), exclude_changes_from=exclude_changes_from) def get_versions(self): # take the last n revisions qs = (Version.objects.get_for_object(self).select_related( "revision__user").order_by("-revision__date_created") [:self.HISTORY_MAX_LATEST_ITEMS + 1]) # make it a list, so it's reversable versions = list(qs) # reorder the slice by date_created ASC versions.reverse() return versions def to_debug(self): cleaned = dict([(k, v) for (k, v) in model_to_dict(self).copy().items() if v is not None and v != "" and v != []]) return json.dumps(cleaned, indent=2) def has_cheminement_ext(self): fields = schema.get_section_fields(schema.SECTION_CHEMINEMENT_EXT) return any(getattr(f) is not None for f in fields) def has_data(self): # count the number of filled fields to provide more validation for field_name in schema.get_a11y_fields(): if hasattr(self, field_name): field_value = getattr(self, field_name) if field_value not in [None, "", []]: return True return False @staticmethod def export_data_comma_headers(): return ";".join([ str(field_name) for field_name in schema.get_a11y_fields() if field_name not in ( "commentaire", "transport_information", "entree_pmr_informations", ) ]) def export_data_comma(self): # count the number of filled fields to provide more validation fields = [ getattr(self, field_name) for field_name in schema.get_a11y_fields() if field_name not in ("commentaire", "transport_information", "entree_pmr_informations") ] fl = list() for f in fields: if f is None or (isinstance(f, list) and len(f) == 0): fl.append("") else: fl.append(str(f).replace("\n", " ").replace(";", " ")) return ";".join(fl)
class Resource(ModifiableModel, AutoIdentifiedModel): AUTHENTICATION_TYPES = (('none', _('None')), ('weak', _('Weak')), ('strong', _('Strong'))) ACCESS_CODE_TYPE_NONE = 'none' ACCESS_CODE_TYPE_PIN4 = 'pin4' ACCESS_CODE_TYPE_PIN6 = 'pin6' ACCESS_CODE_TYPES = ( (ACCESS_CODE_TYPE_NONE, _('None')), (ACCESS_CODE_TYPE_PIN4, _('4-digit PIN code')), (ACCESS_CODE_TYPE_PIN6, _('6-digit PIN code')), ) PRICE_TYPE_HOURLY = 'hourly' PRICE_TYPE_DAILY = 'daily' PRICE_TYPE_WEEKLY = 'weekly' PRICE_TYPE_FIXED = 'fixed' PRICE_TYPE_CHOICES = ( (PRICE_TYPE_HOURLY, _('Hourly')), (PRICE_TYPE_DAILY, _('Daily')), (PRICE_TYPE_WEEKLY, _('Weekly')), (PRICE_TYPE_FIXED, _('Fixed')), ) id = models.CharField(primary_key=True, max_length=100) public = models.BooleanField(default=True, verbose_name=_('Public')) unit = models.ForeignKey('Unit', verbose_name=_('Unit'), db_index=True, null=True, blank=True, related_name="resources", on_delete=models.PROTECT) type = models.ForeignKey(ResourceType, verbose_name=_('Resource type'), db_index=True, on_delete=models.PROTECT) purposes = models.ManyToManyField(Purpose, verbose_name=_('Purposes')) name = models.CharField(verbose_name=_('Name'), max_length=200) description = models.TextField(verbose_name=_('Description'), null=True, blank=True) need_manual_confirmation = models.BooleanField( verbose_name=_('Need manual confirmation'), default=False) authentication = models.CharField(blank=False, verbose_name=_('Authentication'), max_length=20, choices=AUTHENTICATION_TYPES) people_capacity = models.PositiveIntegerField( verbose_name=_('People capacity'), null=True, blank=True) area = models.PositiveIntegerField(verbose_name=_('Area (m2)'), null=True, blank=True) # if not set, location is inherited from unit location = models.PointField(verbose_name=_('Location'), null=True, blank=True, srid=settings.DEFAULT_SRID) min_period = models.DurationField( verbose_name=_('Minimum reservation time'), default=datetime.timedelta(minutes=30)) max_period = models.DurationField( verbose_name=_('Maximum reservation time'), null=True, blank=True) slot_size = models.DurationField( verbose_name=_('Slot size for reservation time'), default=datetime.timedelta(minutes=30)) equipment = EquipmentField(Equipment, through='ResourceEquipment', verbose_name=_('Equipment')) max_reservations_per_user = models.PositiveIntegerField( verbose_name=_('Maximum number of active reservations per user'), null=True, blank=True) reservable = models.BooleanField(verbose_name=_('Reservable'), default=False) reservation_info = models.TextField(verbose_name=_('Reservation info'), null=True, blank=True) responsible_contact_info = models.TextField( verbose_name=_('Responsible contact info'), blank=True) generic_terms = models.ForeignKey( TermsOfUse, verbose_name=_('Generic terms'), null=True, blank=True, on_delete=models.SET_NULL, related_name='resources_where_generic_terms') payment_terms = models.ForeignKey( TermsOfUse, verbose_name=_('Payment terms'), null=True, blank=True, on_delete=models.SET_NULL, related_name='resources_where_payment_terms') specific_terms = models.TextField(verbose_name=_('Specific terms'), blank=True) reservation_requested_notification_extra = models.TextField(verbose_name=_( 'Extra content to "reservation requested" notification'), blank=True) reservation_confirmed_notification_extra = models.TextField(verbose_name=_( 'Extra content to "reservation confirmed" notification'), blank=True) min_price = models.DecimalField( verbose_name=_('Min price'), max_digits=8, decimal_places=2, blank=True, null=True, validators=[MinValueValidator(Decimal('0.00'))]) max_price = models.DecimalField( verbose_name=_('Max price'), max_digits=8, decimal_places=2, blank=True, null=True, validators=[MinValueValidator(Decimal('0.00'))]) price_type = models.CharField(max_length=32, verbose_name=_('price type'), choices=PRICE_TYPE_CHOICES, default=PRICE_TYPE_HOURLY) access_code_type = models.CharField(verbose_name=_('Access code type'), max_length=20, choices=ACCESS_CODE_TYPES, default=ACCESS_CODE_TYPE_NONE) # Access codes can be generated either by the general Respa code or # the Kulkunen app. Kulkunen will set the `generate_access_codes` # attribute by itself if special access code considerations are # needed. generate_access_codes = models.BooleanField( verbose_name=_('Generate access codes'), default=True, editable=False, help_text=_('Should access codes generated by the general system')) reservable_max_days_in_advance = models.PositiveSmallIntegerField( verbose_name=_('Reservable max. days in advance'), null=True, blank=True) reservable_min_days_in_advance = models.PositiveSmallIntegerField( verbose_name=_('Reservable min. days in advance'), null=True, blank=True) reservation_metadata_set = models.ForeignKey( 'resources.ReservationMetadataSet', verbose_name=_('Reservation metadata set'), null=True, blank=True, on_delete=models.SET_NULL) external_reservation_url = models.URLField( verbose_name=_('External reservation URL'), help_text= _('A link to an external reservation system if this resource is managed elsewhere' ), null=True, blank=True) reservation_extra_questions = models.TextField( verbose_name=_('Reservation extra questions'), blank=True) objects = ResourceQuerySet.as_manager() class Meta: verbose_name = _("resource") verbose_name_plural = _("resources") ordering = ( 'unit', 'name', ) def __str__(self): return "%s (%s)/%s" % (get_translated(self, 'name'), self.id, self.unit) @cached_property def main_image(self): resource_image = next( (image for image in self.images.all() if image.type == 'main'), None) return resource_image.image if resource_image else None def validate_reservation_period(self, reservation, user, data=None): """ Check that given reservation if valid for given user. Reservation may be provided as Reservation or as a data dict. When providing the data dict from a serializer, reservation argument must be present to indicate the reservation being edited, or None if we are creating a new reservation. If the reservation is not valid raises a ValidationError. Staff members have no restrictions at least for now. Normal users cannot make multi day reservations or reservations outside opening hours. :type reservation: Reservation :type user: User :type data: dict[str, Object] """ # no restrictions for staff if self.is_admin(user): return tz = self.unit.get_tz() # check if data from serializer is present: if data: begin = data['begin'] end = data['end'] else: # if data is not provided, the reservation object has the desired data: begin = reservation.begin end = reservation.end if begin.tzinfo: begin = begin.astimezone(tz) else: begin = tz.localize(begin) if end.tzinfo: end = end.astimezone(tz) else: end = tz.localize(end) if begin.date() != end.date(): raise ValidationError(_("You cannot make a multi day reservation")) if not self.can_ignore_opening_hours(user): opening_hours = self.get_opening_hours(begin.date(), end.date()) days = opening_hours.get(begin.date(), None) if days is None or not any(day['opens'] and begin >= day['opens'] and end <= day['closes'] for day in days): raise ValidationError( _("You must start and end the reservation during opening hours" )) if not self.can_ignore_max_period(user) and ( self.max_period and (end - begin) > self.max_period): raise ValidationError( _("The maximum reservation length is %(max_period)s") % {'max_period': humanize_duration(self.max_period)}) def validate_max_reservations_per_user(self, user): """ Check maximum number of active reservations per user per resource. If the user has too many reservations raises ValidationError. Staff members have no reservation limits. :type user: User """ if self.can_ignore_max_reservations_per_user(user): return max_count = self.max_reservations_per_user if max_count is not None: reservation_count = self.reservations.filter( user=user).active().count() if reservation_count >= max_count: raise ValidationError( _("Maximum number of active reservations for this resource exceeded." )) def check_reservation_collision(self, begin, end, reservation): overlapping = self.reservations.filter(end__gt=begin, begin__lt=end).active() if reservation: overlapping = overlapping.exclude(pk=reservation.pk) return overlapping.exists() def get_available_hours(self, start=None, end=None, duration=None, reservation=None, during_closing=False): """ Returns hours that the resource is not reserved for a given date range If include_closed=True, will also return hours when the resource is closed, if it is not reserved. This is so that admins can book resources during closing hours. Returns the available hours as a list of dicts. The optional reservation argument is for disregarding a given reservation during checking, if we wish to move an existing reservation. The optional duration argument specifies minimum length for periods to be returned. :rtype: list[dict[str, datetime.datetime]] :type start: datetime.datetime :type end: datetime.datetime :type duration: datetime.timedelta :type reservation: Reservation :type during_closing: bool """ today = arrow.get(timezone.now()) if start is None: start = today.floor('day').naive if end is None: end = today.replace(days=+1).floor('day').naive if not start.tzinfo and not end.tzinfo: """ Only try to localize naive dates """ tz = timezone.get_current_timezone() start = tz.localize(start) end = tz.localize(end) if not during_closing: """ Check open hours only """ open_hours = self.get_opening_hours(start, end) hours_list = [] for date, open_during_date in open_hours.items(): for period in open_during_date: if period['opens']: # if the start or end straddle opening hours opens = period[ 'opens'] if period['opens'] > start else start closes = period[ 'closes'] if period['closes'] < end else end # include_closed to prevent recursion, opening hours need not be rechecked hours_list.extend( self.get_available_hours(start=opens, end=closes, duration=duration, reservation=reservation, during_closing=True)) return hours_list reservations = self.reservations.filter( end__gte=start, begin__lte=end).order_by('begin') hours_list = [({'starts': start})] first_checked = False for res in reservations: # skip the reservation that is being edited if res == reservation: continue # check if the reservation spans the beginning if not first_checked: first_checked = True if res.begin < start: if res.end > end: return [] hours_list[0]['starts'] = res.end # proceed to the next reservation continue if duration: if res.begin - hours_list[-1]['starts'] < duration: # the free period is too short, discard this period hours_list[-1]['starts'] = res.end continue hours_list[-1]['ends'] = timezone.localtime(res.begin) # check if the reservation spans the end if res.end > end: return hours_list hours_list.append({'starts': timezone.localtime(res.end)}) # after the last reservation, we must check if the remaining free period is too short if duration: if end - hours_list[-1]['starts'] < duration: hours_list.pop() return hours_list # otherwise add the remaining free period hours_list[-1]['ends'] = end return hours_list def get_opening_hours(self, begin=None, end=None, opening_hours_cache=None): """ :rtype : dict[str, datetime.datetime] :type begin: datetime.date :type end: datetime.date """ tz = pytz.timezone(self.unit.time_zone) begin, end = determine_hours_time_range(begin, end, tz) if opening_hours_cache is None: hours_objs = self.opening_hours.filter( open_between__overlap=(begin, end, '[)')) else: hours_objs = opening_hours_cache opening_hours = dict() for h in hours_objs: opens = h.open_between.lower.astimezone(tz) closes = h.open_between.upper.astimezone(tz) date = opens.date() hours_item = OrderedDict(opens=opens, closes=closes) date_item = opening_hours.setdefault(date, []) date_item.append(hours_item) # Set the dates when the resource is closed. date = begin.date() end = end.date() while date < end: if date not in opening_hours: opening_hours[date] = [OrderedDict(opens=None, closes=None)] date += datetime.timedelta(days=1) return opening_hours def update_opening_hours(self): hours = self.opening_hours.order_by('open_between') existing_hours = {} for h in hours: assert h.open_between.lower not in existing_hours existing_hours[h.open_between.lower] = h.open_between.upper unit_periods = list(self.unit.periods.all()) resource_periods = list(self.periods.all()) # Periods set for the resource always carry a higher priority. If # nothing is defined for the resource for a given day, use the # periods configured for the unit. for period in unit_periods: period.priority = 0 for period in resource_periods: period.priority = 1 earliest_date = None latest_date = None all_periods = unit_periods + resource_periods for period in all_periods: if earliest_date is None or period.start < earliest_date: earliest_date = period.start if latest_date is None or period.end > latest_date: latest_date = period.end # Assume we delete everything, but remove items from the delete # list if the hours are identical. to_delete = existing_hours to_add = {} if all_periods: hours = get_opening_hours(self.unit.time_zone, all_periods, earliest_date, latest_date) for hours_items in hours.values(): for h in hours_items: if not h['opens'] or not h['closes']: continue if h['opens'] in to_delete and h['closes'] == to_delete[ h['opens']]: del to_delete[h['opens']] continue to_add[h['opens']] = h['closes'] if to_delete: ret = ResourceDailyOpeningHours.objects.filter( open_between__in=[(opens, closes, '[)') for opens, closes in to_delete.items()], resource=self).delete() assert ret[0] == len(to_delete) add_objs = [ ResourceDailyOpeningHours(resource=self, open_between=(opens, closes, '[)')) for opens, closes in to_add.items() ] if add_objs: ResourceDailyOpeningHours.objects.bulk_create(add_objs) def is_admin(self, user): """ Check if the given user is an administrator of this resource. :type user: users.models.User :rtype: bool """ # UserFilterBackend and ReservationFilterSet in resources.api.reservation assume the same behaviour, # so if this is changed those need to be changed as well. if not self.unit: return is_general_admin(user) return self.unit.is_admin(user) def is_manager(self, user): """ Check if the given user is a manager of this resource. :type user: users.models.User :rtype: bool """ if not self.unit: return False return self.unit.is_manager(user) def is_viewer(self, user): """ Check if the given user is a viewer of this resource. :type user: users.models.User :rtype: bool """ if not self.unit: return False return self.unit.is_viewer(user) def _has_perm(self, user, perm, allow_admin=True): if not is_authenticated_user(user): return False if (self.is_admin(user) and allow_admin) or user.is_superuser: return True return self._has_role_perm(user, perm) or self._has_explicit_perm( user, perm, allow_admin) def _has_explicit_perm(self, user, perm, allow_admin=True): if hasattr(self, '_permission_checker'): checker = self._permission_checker else: checker = ObjectPermissionChecker(user) # Permissions can be given per-unit if checker.has_perm('unit:%s' % perm, self.unit): return True # ... or through Resource Groups resource_group_perms = [ checker.has_perm('group:%s' % perm, rg) for rg in self.groups.all() ] return any(resource_group_perms) def _has_role_perm(self, user, perm): allowed_roles = UNIT_ROLE_PERMISSIONS.get(perm) is_allowed = False if (UnitAuthorizationLevel.admin in allowed_roles or UnitGroupAuthorizationLevel.admin in allowed_roles) and not is_allowed: is_allowed = self.is_admin(user) if UnitAuthorizationLevel.manager in allowed_roles and not is_allowed: is_allowed = self.is_manager(user) if UnitAuthorizationLevel.viewer in allowed_roles and not is_allowed: is_allowed = self.is_viewer(user) return is_allowed def get_users_with_perm(self, perm): users = { u for u in get_users_with_perms(self.unit) if u.has_perm('unit:%s' % perm, self.unit) } for rg in self.groups.all(): users |= { u for u in get_users_with_perms(rg) if u.has_perm('group:%s' % perm, rg) } return users def can_make_reservations(self, user): return self.reservable or self._has_perm(user, 'can_make_reservations') def can_modify_reservations(self, user): return self._has_perm(user, 'can_modify_reservations') def can_comment_reservations(self, user): return self._has_perm(user, 'can_comment_reservations') def can_ignore_opening_hours(self, user): return self._has_perm(user, 'can_ignore_opening_hours') def can_view_reservation_extra_fields(self, user): return self._has_perm(user, 'can_view_reservation_extra_fields') def can_view_reservation_user(self, user): return self._has_perm(user, 'can_view_reservation_user') def can_access_reservation_comments(self, user): return self._has_perm(user, 'can_access_reservation_comments') def can_view_reservation_catering_orders(self, user): return self._has_perm(user, 'can_view_reservation_catering_orders') def can_modify_reservation_catering_orders(self, user): return self._has_perm(user, 'can_modify_reservation_catering_orders') def can_view_reservation_product_orders(self, user): return self._has_perm(user, 'can_view_reservation_product_orders', allow_admin=False) def can_modify_paid_reservations(self, user): return self._has_perm(user, 'can_modify_paid_reservations', allow_admin=False) def can_approve_reservations(self, user): return self._has_perm(user, 'can_approve_reservation', allow_admin=False) def can_view_reservation_access_code(self, user): return self._has_perm(user, 'can_view_reservation_access_code') def can_bypass_payment(self, user): return self._has_perm(user, 'can_bypass_payment') def can_create_staff_event(self, user): return self._has_perm(user, 'can_create_staff_event') def can_create_special_type_reservation(self, user): return self._has_perm(user, 'can_create_special_type_reservation') def can_bypass_manual_confirmation(self, user): return self._has_perm(user, 'can_bypass_manual_confirmation') def can_create_reservations_for_other_users(self, user): return self._has_perm(user, 'can_create_reservations_for_other_users') def can_create_overlapping_reservations(self, user): return self._has_perm(user, 'can_create_overlapping_reservations') def can_ignore_max_reservations_per_user(self, user): return self._has_perm(user, 'can_ignore_max_reservations_per_user') def can_ignore_max_period(self, user): return self._has_perm(user, 'can_ignore_max_period') def is_access_code_enabled(self): return self.access_code_type != Resource.ACCESS_CODE_TYPE_NONE def get_reservable_max_days_in_advance(self): return self.reservable_max_days_in_advance or self.unit.reservable_max_days_in_advance def get_reservable_before(self): return create_datetime_days_from_now( self.get_reservable_max_days_in_advance()) def get_reservable_min_days_in_advance(self): return self.reservable_min_days_in_advance or self.unit.reservable_min_days_in_advance def get_reservable_after(self): return create_datetime_days_from_now( self.get_reservable_min_days_in_advance()) def has_rent(self): return self.products.current().rents().exists() def get_supported_reservation_extra_field_names(self, cache=None): if not self.reservation_metadata_set_id: return [] if cache: metadata_set = cache[self.reservation_metadata_set_id] else: metadata_set = self.reservation_metadata_set return [x.field_name for x in metadata_set.supported_fields.all()] def get_required_reservation_extra_field_names(self, cache=None): if not self.reservation_metadata_set: return [] if cache: metadata_set = cache[self.reservation_metadata_set_id] else: metadata_set = self.reservation_metadata_set return [x.field_name for x in metadata_set.required_fields.all()] def clean(self): if self.min_price is not None and self.max_price is not None and self.min_price > self.max_price: raise ValidationError({ 'min_price': _('This value cannot be greater than max price') }) if self.min_period % self.slot_size != datetime.timedelta(0): raise ValidationError({ 'min_period': _('This value must be a multiple of slot_size') }) if self.need_manual_confirmation and self.products.current().exists(): raise ValidationError({ 'need_manual_confirmation': _('This cannot be enabled because the resource has product(s).' ) })
class Person(models.Model): gender = models.CharField(max_length=1, choices=GENDER_CHOICES) # Jards Macalé is an amazing brazilian musician! =] enjoy_jards_macale = models.BooleanField(default=True) like_metal_music = models.BooleanField(default=False) name = models.CharField(max_length=30) nickname = models.SlugField(max_length=36) age = models.IntegerField() bio = models.TextField() birthday = models.DateField() birth_time = models.TimeField() appointment = models.DateTimeField() blog = models.URLField() occupation = models.CharField(max_length=10, choices=OCCUPATION_CHOICES) uuid = models.UUIDField(primary_key=False) name_hash = models.BinaryField(max_length=16) days_since_last_login = models.BigIntegerField() duration_of_sleep = models.DurationField() email = models.EmailField() id_document = models.CharField(unique=True, max_length=10) try: from django.models import JSONField data = JSONField() except ImportError: # Skip JSONField-related fields pass try: from django.contrib.postgres.fields import ArrayField, HStoreField from django.contrib.postgres.fields import JSONField as PostgresJSONField from django.contrib.postgres.fields.citext import ( CICharField, CIEmailField, CITextField, ) from django.contrib.postgres.fields.ranges import ( IntegerRangeField, BigIntegerRangeField, FloatRangeField, DateRangeField, DateTimeRangeField, ) if settings.USING_POSTGRES: acquaintances = ArrayField(models.IntegerField()) postgres_data = PostgresJSONField() hstore_data = HStoreField() ci_char = CICharField(max_length=30) ci_email = CIEmailField() ci_text = CITextField() int_range = IntegerRangeField() bigint_range = BigIntegerRangeField() float_range = FloatRangeField() date_range = DateRangeField() datetime_range = DateTimeRangeField() except ImportError: # Skip PostgreSQL-related fields pass try: from django.contrib.postgres.fields.ranges import DecimalRangeField if settings.USING_POSTGRES: decimal_range = DecimalRangeField() except ImportError: # Django version lower than 2.2 pass if BAKER_GIS: geom = models.GeometryField() point = models.PointField() line_string = models.LineStringField() polygon = models.PolygonField() multi_point = models.MultiPointField() multi_line_string = models.MultiLineStringField() multi_polygon = models.MultiPolygonField() geom_collection = models.GeometryCollectionField()
class GeoeventsSource(models.Model): name = models.CharField(max_length=200) url = models.URLField( help_text='URL of service location. Requires JSONP support', max_length=500)
class Profile(ModelWithSlugMixin, TimeStampedModel): name = models.CharField(max_length=128, blank=True, help_text=_('The full name of the person or team')) slug = models.CharField( max_length=128, unique=True, blank=True, help_text= _('A short name that will be used in URLs for projects owned by this profile' )) email = models.EmailField( blank=True, help_text=_('Contact email address of the profile holder')) description = models.TextField(blank=True, default='') avatar_url = models.URLField(blank=True, null=True) # projects (reverse, Project) # User-profile specific auth = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='profile', null=True, blank=True, on_delete=models.CASCADE) affiliation = models.CharField(max_length=256, blank=True, default='') teams = models.ManyToManyField('Profile', related_name='members', blank=True, limit_choices_to={'auth__isnull': True}) # Team-profile specific # members (reverse, Profile) # Feature flags/versions class Versions: AMETHYST = 1 BISTRE = 2 PROJECT_EDITOR_VERSION_CHOICES = ( (Versions.AMETHYST, "Amethyst"), (Versions.BISTRE, "Bistre"), ) project_editor_version = models.PositiveIntegerField( choices=PROJECT_EDITOR_VERSION_CHOICES, default=Versions.BISTRE) objects = ProfileManager() def __str__(self): return self.slug if self.auth is None else self.auth.username def natural_key(self): return (self.slug, ) def get_slug_basis(self): return self.name def get_all_slugs(self): return set([p['slug'] for p in Profile.objects.all().values('slug')]) def slug_exists(self, slug): return Profile.objects.filter(slug__iexact=slug).exists() def is_user_profile(self): return self.auth is not None def is_owned_by(self, user): return (user.id == self.auth_id) def has_member(self, user): members = list(self.members.all()) if user.id in [profile.auth_id for profile in members]: return True else: return any(profile.has_member(user) for profile in members) def is_synced_with_auth(self, auth=None): auth = auth or self.auth if self.email != auth.email: return False return True def save(self, **kwargs): super(Profile, self).save(**kwargs) if self.auth and not self.is_synced_with_auth(): self.auth.username = self.slug self.auth.email = self.email self.auth.save() def authorizes(self, user): """ Test whether a given authenticated user is allowed to perform actions on behalf of this profile. """ if user.is_superuser: return True if self.is_owned_by(user): return True if self.has_member(user): return True return False
class TouristicContent(AddPropertyMixin, PublishableMixin, MapEntityMixin, StructureRelated, TimeStampedModelMixin, PicturesMixin, NoDeleteMixin): """ A generic touristic content (accomodation, museum, etc.) in the park """ description_teaser = models.TextField( verbose_name=_(u"Description teaser"), blank=True, help_text=_(u"A brief summary"), db_column='chapeau') description = models.TextField(verbose_name=_(u"Description"), blank=True, db_column='description', help_text=_(u"Complete description")) themes = models.ManyToManyField(Theme, related_name="touristiccontents", db_table="t_r_contenu_touristique_theme", blank=True, verbose_name=_(u"Themes"), help_text=_(u"Main theme(s)")) geom = models.GeometryField(verbose_name=_(u"Location"), srid=settings.SRID) category = models.ForeignKey(TouristicContentCategory, related_name='contents', verbose_name=_(u"Category"), db_column='categorie') contact = models.TextField(verbose_name=_(u"Contact"), blank=True, db_column='contact', help_text=_(u"Address, phone, etc.")) email = models.EmailField(verbose_name=_(u"Email"), max_length=256, db_column='email', blank=True, null=True) website = models.URLField(verbose_name=_(u"Website"), max_length=256, db_column='website', blank=True, null=True) practical_info = models.TextField(verbose_name=_(u"Practical info"), blank=True, db_column='infos_pratiques', help_text=_(u"Anything worth to know")) type1 = models.ManyToManyField(TouristicContentType, related_name='contents1', verbose_name=_(u"Type 1"), db_table="t_r_contenu_touristique_type1", blank=True) type2 = models.ManyToManyField(TouristicContentType, related_name='contents2', verbose_name=_(u"Type 2"), db_table="t_r_contenu_touristique_type2", blank=True) source = models.ManyToManyField('common.RecordSource', blank=True, related_name='touristiccontents', verbose_name=_("Source"), db_table='t_r_contenu_touristique_source') portal = models.ManyToManyField('common.TargetPortal', blank=True, related_name='touristiccontents', verbose_name=_("Portal"), db_table='t_r_contenu_touristique_portal') eid = models.CharField(verbose_name=_(u"External id"), max_length=128, blank=True, null=True, db_column='id_externe') reservation_system = models.ForeignKey( ReservationSystem, verbose_name=_(u"Reservation system"), blank=True, null=True) reservation_id = models.CharField(verbose_name=_(u"Reservation ID"), max_length=128, blank=True, db_column='id_reservation') approved = models.BooleanField(verbose_name=_(u"Approved"), default=False, db_column='labellise') objects = NoDeleteMixin.get_manager_cls(models.GeoManager)() class Meta: db_table = 't_t_contenu_touristique' verbose_name = _(u"Touristic content") verbose_name_plural = _(u"Touristic contents") def __unicode__(self): return self.name @models.permalink def get_document_public_url(self): """ Override ``geotrek.common.mixins.PublishableMixin`` """ return ('tourism:touristiccontent_document_public', [], { 'lang': get_language(), 'pk': self.pk, 'slug': self.slug }) @property def districts_display(self): return ', '.join([unicode(d) for d in self.districts]) @property def type1_label(self): return self.category.type1_label @property def type2_label(self): return self.category.type2_label @property def type1_display(self): return ', '.join([unicode(n) for n in self.type1.all()]) @property def type2_display(self): return ', '.join([unicode(n) for n in self.type2.all()]) @property def prefixed_category_id(self): return self.category.prefixed_id def distance(self, to_cls): return settings.TOURISM_INTERSECTION_MARGIN @property def type(self): """Fake type to simulate POI for mobile app v1""" return self.category @property def min_elevation(self): return 0 @property def max_elevation(self): return 0 @property def portal_display(self): return ', '.join([unicode(portal) for portal in self.portal.all()]) @property def source_display(self): return ','.join([unicode(source) for source in self.source.all()]) @property def themes_display(self): return ','.join([unicode(source) for source in self.themes.all()]) @property def extent(self): return self.geom.buffer(10).transform(settings.API_SRID, clone=True).extent
class Project(ModelWithSlugMixin, CloneableModelMixin, TimeStampedModel): STATUS_CHOICES = ( ('not-started', _('Not Started')), ('active', _('Active')), ('complete', _('Complete')), ) LINK_TYPE_CHOICES = ( ('event', _('Event')), ('section', _('Section')), ('external', _('External URL')), ) LAYOUT_CHOICES = ( ('generic', _('Default (classic)')), ('shareabouts', _('Shareabouts Map')), ) title = models.TextField(blank=True) slug = models.CharField(max_length=128, blank=True) public = models.BooleanField(default=False, blank=True) status = models.CharField( help_text=_("A string representing the project's status"), choices=STATUS_CHOICES, default='not-started', max_length=32, blank=True) location = models.TextField(help_text=_( "The general location of the project, e.g. \"Philadelphia, PA\", \"Clifton Heights, Louisville, KY\", \"4th St. Corridor, Brooklyn, NY\", etc." ), default='', blank=True) contact = models.TextField( help_text=_("The contact information for the project"), default='', blank=True) owner = models.ForeignKey('Profile', related_name='projects') details = JSONField(blank=True, default=dict) theme = models.ForeignKey('Theme', related_name='projects', null=True, blank=True, on_delete=models.SET_NULL) layout = models.CharField(max_length=20, choices=LAYOUT_CHOICES, default='generic') cover_img_url = models.URLField(_('Cover Image URL'), blank=True, max_length=2048) logo_img_url = models.URLField(_('Logo Image URL'), blank=True, max_length=2048) template = models.ForeignKey( 'Project', help_text=_("The project, if any, that this one is based off of"), null=True, blank=True, on_delete=models.SET_NULL) geometry = models.GeometryField(null=True, blank=True) expires_at = models.DateTimeField(null=True, blank=True) payment_type = models.CharField(max_length=20, blank=True) customer = models.OneToOneField('moonclerk.Customer', blank=True, null=True, related_name='project') payments = GenericRelation('moonclerk.Payment', content_type_field='item_type', object_id_field='item_id') # NOTE: These may belong in a separate model, but are on the project for # now. I think the model would be called a Highlight. happening_now_description = models.TextField(blank=True) happening_now_link_type = models.CharField(max_length=16, choices=LINK_TYPE_CHOICES, blank=True) happening_now_link_url = models.CharField(max_length=2048, blank=True) get_involved_description = models.TextField(blank=True) get_involved_link_type = models.CharField(max_length=16, choices=LINK_TYPE_CHOICES, blank=True) get_involved_link_url = models.CharField(max_length=2048, blank=True) # Project activity last_opened_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, related_name='+') last_opened_at = models.DateTimeField(null=True, blank=True) last_saved_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, related_name='+') last_saved_at = models.DateTimeField(null=True, blank=True) objects = ProjectManager() class Meta: unique_together = [('owner', 'slug')] def __str__(self): return self.title def get_summary(self): for section in self.sections.all(): if section.type == 'text': return section.details.get('content', '') def mark_opened_by(self, user, opened_at=None): # TODO: This could just be done in the cache. self.last_opened_at = opened_at or now() self.last_opened_by = user if (user and user.is_authenticated()) else None self.save() def mark_closed(self): self.mark_opened_by(None) def is_opened_by(self, user): two_minutes = timedelta(minutes=2) return self.last_opened_by == user and ( now() - self.last_opened_at) < two_minutes def get_opened_by(self): two_minutes = timedelta(minutes=2) if self.last_opened_at and (now() - self.last_opened_at) < two_minutes: return self.last_opened_by else: return None def natural_key(self): return self.owner.natural_key() + (self.slug, ) def get_slug_basis(self): """ Get the string off that will be slugified to construct the slug. """ return self.title def get_all_slugs(self): """ Generate the set of all mututally unique slugs with respect to this model. """ return [p.slug for p in self.owner.projects.all()] def slug_exists(self, slug): return self.owner.projects.filter(slug__iexact=slug).exists() def clone(self, *args, **kwargs): new_inst = super(Project, self).clone(*args, **kwargs) for e in self.events.all(): e.clone(project=new_inst) for s in self.sections.all(): s.clone(project=new_inst) return new_inst def owned_by(self, obj): UserAuth = auth.get_user_model() if isinstance(obj, UserAuth): try: obj = obj.profile except Profile.DoesNotExist: return False return (self.owner == obj) def editable_by(self, obj): UserAuth = auth.get_user_model() if hasattr(obj, 'is_authenticated') and not obj.is_authenticated(): return False if isinstance(obj, UserAuth): try: obj = obj.profile except Profile.DoesNotExist: return False if obj.auth.is_superuser: return True return self.owned_by(obj) or (obj in self.owner.members.all()) def reset_trial_period(self): if hasattr(settings, 'TRIAL_DURATION'): duration = settings.TRIAL_DURATION if not isinstance(duration, timedelta): duration = timedelta(seconds=duration) self.expires_at = now() + duration def save(self, *args, **kwargs): if self.pk is None: # Creating... if self.expires_at is None: self.reset_trial_period() return super(Project, self).save(*args, **kwargs)
class InformationDesk(models.Model): name = models.CharField(verbose_name=_(u"Title"), max_length=256, db_column='nom') type = models.ForeignKey(InformationDeskType, verbose_name=_(u"Type"), related_name='desks', db_column='type') description = models.TextField(verbose_name=_(u"Description"), blank=True, db_column='description', help_text=_(u"Brief description")) phone = models.CharField(verbose_name=_(u"Phone"), max_length=32, blank=True, null=True, db_column='telephone') email = models.EmailField(verbose_name=_(u"Email"), max_length=256, db_column='email', blank=True, null=True) website = models.URLField(verbose_name=_(u"Website"), max_length=256, db_column='website', blank=True, null=True) photo = models.FileField(verbose_name=_(u"Photo"), upload_to=settings.UPLOAD_DIR, db_column='photo', max_length=512, blank=True, null=True) street = models.CharField(verbose_name=_(u"Street"), max_length=256, blank=True, null=True, db_column='rue') postal_code = models.CharField(verbose_name=_(u"Postal code"), max_length=8, blank=True, null=True, db_column='code') municipality = models.CharField(verbose_name=_(u"Municipality"), blank=True, null=True, max_length=256, db_column='commune') geom = models.PointField(verbose_name=_(u"Emplacement"), db_column='geom', blank=True, null=True, srid=settings.SRID, spatial_index=False) objects = models.GeoManager() class Meta: db_table = 't_b_renseignement' verbose_name = _(u"Information desk") verbose_name_plural = _(u"Information desks") ordering = ['name'] def __unicode__(self): return self.name @property def description_strip(self): """Used in trek public template. """ nobr = re.compile(r'(\s*<br.*?>)+\s*', re.I) newlines = nobr.sub("\n", self.description) return smart_plain_text(newlines) @property def serializable_type(self): return { 'id': self.type.id, 'label': self.type.label, 'pictogram': self.type.pictogram.url, } @property def latitude(self): if self.geom: api_geom = self.geom.transform(settings.API_SRID, clone=True) return api_geom.y return None @property def longitude(self): if self.geom: api_geom = self.geom.transform(settings.API_SRID, clone=True) return api_geom.x return None @property def thumbnail(self): if not self.photo: return None thumbnailer = get_thumbnailer(self.photo) try: return thumbnailer.get_thumbnail(aliases.get('thumbnail')) except InvalidImageFormatError: logger.warning( _("Image %s invalid or missing from disk.") % self.photo) return None @property def photo_url(self): thumbnail = self.thumbnail if not thumbnail: return None return os.path.join(settings.MEDIA_URL, thumbnail.name)
class TrafficSignReal( DecimalValueFromDeviceTypeMixin, SourceControlModel, SoftDeleteModel, UserControlModel, ): id = models.UUIDField(primary_key=True, unique=True, editable=False, default=uuid.uuid4) traffic_sign_plan = models.ForeignKey( TrafficSignPlan, verbose_name=_("Traffic Sign Plan"), on_delete=models.PROTECT, blank=True, null=True, ) location = models.PointField(_("Location (3D)"), dim=3, srid=settings.SRID) height = models.IntegerField(_("Height"), blank=True, null=True) direction = models.IntegerField(_("Direction"), default=0) device_type = models.ForeignKey( TrafficControlDeviceType, verbose_name=_("Device type"), on_delete=models.PROTECT, limit_choices_to=Q( Q(target_model=None) | Q(target_model=DeviceTypeTargetModel.TRAFFIC_SIGN)), blank=False, null=True, ) value = models.DecimalField( _("Traffic Sign Code value"), max_digits=10, decimal_places=2, blank=True, null=True, ) legacy_code = models.CharField(_("Legacy Traffic Sign Code"), max_length=32, blank=True, null=True) txt = models.CharField(_("Txt"), max_length=254, blank=True, null=True) mount_real = models.ForeignKey( MountReal, verbose_name=_("Mount Real"), on_delete=models.PROTECT, blank=True, null=True, ) mount_type = models.ForeignKey( MountType, verbose_name=_("Mount type"), blank=True, null=True, on_delete=models.SET_NULL, ) installation_date = models.DateField(_("Installation date"), blank=True, null=True) installation_status = EnumField( InstallationStatus, verbose_name=_("Installation status"), max_length=10, blank=True, null=True, ) installation_id = models.CharField(_("Installation id"), max_length=254, blank=True, null=True) installation_details = models.CharField(_("Installation details"), max_length=254, blank=True, null=True) permit_decision_id = models.CharField(_("Permit decision id"), max_length=254, blank=True, null=True) validity_period_start = models.DateField(_("Validity period start"), blank=True, null=True) validity_period_end = models.DateField(_("Validity period end"), blank=True, null=True) condition = EnumIntegerField( Condition, verbose_name=_("Condition"), blank=True, null=True, ) coverage_area = models.ForeignKey( CoverageArea, verbose_name=_("Coverage area"), blank=True, null=True, on_delete=models.PROTECT, ) scanned_at = models.DateTimeField(_("Scanned at"), blank=True, null=True) size = EnumField( Size, verbose_name=_("Size"), max_length=1, blank=True, null=True, ) reflection_class = EnumField( Reflection, verbose_name=_("Reflection"), max_length=2, blank=True, null=True, ) surface_class = EnumField( Surface, verbose_name=_("Surface"), max_length=6, blank=True, null=True, ) seasonal_validity_period_start = models.DateField( _("Seasonal validity period start"), blank=True, null=True) seasonal_validity_period_end = models.DateField( _("Seasonal validity period end"), blank=True, null=True) owner = models.ForeignKey( "traffic_control.Owner", verbose_name=_("Owner"), blank=False, null=False, on_delete=models.PROTECT, ) manufacturer = models.CharField(_("Manufacturer"), max_length=254, blank=True, null=True) rfid = models.CharField(_("RFID"), max_length=254, blank=True, null=True) lifecycle = EnumIntegerField(Lifecycle, verbose_name=_("Lifecycle"), default=Lifecycle.ACTIVE) road_name = models.CharField(_("Road name"), max_length=254, blank=True, null=True) lane_number = EnumField(LaneNumber, verbose_name=_("Lane number"), default=LaneNumber.MAIN_1, blank=True) lane_type = EnumField( LaneType, verbose_name=_("Lane type"), default=LaneType.MAIN, blank=True, ) location_specifier = EnumIntegerField( LocationSpecifier, verbose_name=_("Location specifier"), default=LocationSpecifier.RIGHT, blank=True, null=True, ) operation = models.CharField(_("Operation"), max_length=64, blank=True, null=True) attachment_url = models.URLField(_("Attachment url"), max_length=500, blank=True, null=True) objects = TrafficSignRealQuerySet.as_manager() class Meta: db_table = "traffic_sign_real" verbose_name = _("Traffic Sign Real") verbose_name_plural = _("Traffic Sign Reals") unique_together = ["source_name", "source_id"] def __str__(self): return f"{self.id} {self.device_type}" def save(self, *args, **kwargs): if self.device_type and not self.device_type.validate_relation( DeviceTypeTargetModel.TRAFFIC_SIGN): raise ValidationError( f'Device type "{self.device_type}" is not allowed for traffic signs' ) if not self.device_type: self.device_type = ( TrafficControlDeviceType.objects.for_target_model( DeviceTypeTargetModel.TRAFFIC_SIGN).filter( legacy_code=self.legacy_code).order_by("code").first()) super().save(*args, **kwargs) def has_additional_signs(self): return self.additional_signs.active().exists() @transaction.atomic def soft_delete(self, user): super().soft_delete(user) self.additional_signs.soft_delete(user)