class TouristicEvent(AddPropertyMixin, PublishableMixin, MapEntityMixin, StructureRelated, PicturesMixin, TimeStampedModelMixin, NoDeleteMixin): """ A touristic event (conference, workshop, etc.) in the park """ description_teaser = models.TextField(verbose_name=_("Description teaser"), blank=True, help_text=_("A brief summary"), db_column='chapeau') description = models.TextField(verbose_name=_("Description"), blank=True, db_column='description', help_text=_("Complete description")) themes = models.ManyToManyField(Theme, related_name="touristic_events", db_table="t_r_evenement_touristique_theme", blank=True, verbose_name=_("Themes"), help_text=_("Main theme(s)")) geom = models.PointField(verbose_name=_("Location"), srid=settings.SRID) begin_date = models.DateField(blank=True, null=True, verbose_name=_("Begin date"), db_column='date_debut') end_date = models.DateField(blank=True, null=True, verbose_name=_("End date"), db_column='date_fin') duration = models.CharField(verbose_name=_("Duration"), max_length=64, blank=True, db_column='duree', help_text=_("3 days, season, ...")) meeting_point = models.CharField(verbose_name=_("Meeting point"), max_length=256, blank=True, db_column='point_rdv', help_text=_("Where exactly ?")) meeting_time = models.TimeField(verbose_name=_("Meeting time"), blank=True, null=True, db_column='heure_rdv', help_text=_("11:00, 23:30")) contact = models.TextField(verbose_name=_("Contact"), blank=True, db_column='contact') email = models.EmailField(verbose_name=_("Email"), max_length=256, db_column='email', blank=True, null=True) website = models.URLField(verbose_name=_("Website"), max_length=256, db_column='website', blank=True, null=True) organizer = models.CharField(verbose_name=_("Organizer"), max_length=256, blank=True, db_column='organisateur') speaker = models.CharField(verbose_name=_("Speaker"), max_length=256, blank=True, db_column='intervenant') type = models.ForeignKey(TouristicEventType, verbose_name=_("Type"), blank=True, null=True, db_column='type') accessibility = models.CharField(verbose_name=_("Accessibility"), max_length=256, blank=True, db_column='accessibilite') participant_number = models.CharField( verbose_name=_("Number of participants"), max_length=256, blank=True, db_column='nb_places') booking = models.TextField(verbose_name=_("Booking"), blank=True, db_column='reservation') target_audience = models.CharField(verbose_name=_("Target audience"), max_length=128, blank=True, null=True, db_column='public_vise') practical_info = models.TextField( verbose_name=_("Practical info"), blank=True, db_column='infos_pratiques', help_text=_("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=_("External id"), max_length=1024, blank=True, null=True, db_column='id_externe') approved = models.BooleanField(verbose_name=_("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 = _("Touristic event") verbose_name_plural = _("Touristic events") ordering = ['-begin_date'] def __str__(self): return self.name @property def type1(self): return [self.type] if self.type else [] @property def type2(self): return [] @property def districts_display(self): return ', '.join([str(d) for d in self.districts]) @property def dates_display(self): if not self.begin_date and not self.end_date: return "" elif not self.end_date: return _("starting from {begin}").format( begin=date_format(self.begin_date, 'SHORT_DATE_FORMAT')) elif not self.begin_date: return _("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 _("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([str(portal) for portal in self.portal.all()]) @property def source_display(self): return ', '.join([str(source) for source in self.source.all()]) @property def themes_display(self): return ','.join([str(source) for source in self.themes.all()]) @property def rando_url(self): category_slug = _('touristic-event') return '{}/{}/'.format(category_slug, self.slug) @property def meta_description(self): return plain_text(self.description_teaser or self.description)[:500]
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=timezone.get_default_timezone().zone, choices=[(x, x) for x in pytz.all_timezones]) # organization = models.ForeignKey(...) 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')) 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) slug = AutoSlugField(populate_from=get_translated_name, unique=True) reservable_days_in_advance = models.PositiveSmallIntegerField( verbose_name=_('Reservable days in advance'), null=True, blank=True) class Meta: verbose_name = _("unit") verbose_name_plural = _("units") permissions = ( ('can_approve_reservation', _('Can approve reservation')), ('can_view_reservation_access_code', _('Can view reservation access code')), ) ordering = ('name', ) 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, self.periods, begin, end) def get_tz(self): return pytz.timezone(self.time_zone) def get_reservable_before(self): return create_reservable_before_datetime( self.reservable_days_in_advance) def is_admin(self, user): # Currently all staff members are allowed to administrate # all units. Might be more finegrained in the future. return user.is_staff
class Contributor(models.Model): zip_code = models.CharField(max_length=5) email = models.EmailField(null=True) name = models.CharField(max_length=100) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True)
class Place(models.Model): name = models.TextField() place_id = models.TextField(primary_key=True) lat = models.FloatField() lng = models.FloatField() user_rating = models.FloatField() num_ratings = models.FloatField() address = models.TextField() area = models.ForeignKey(to='Area', null=True, blank=True, on_delete=models.SET_NULL) email_contact = models.EmailField(null=True, blank=True) place_url = models.URLField(null=True, blank=True, max_length=1000) image_url = models.URLField(null=True, blank=True, max_length=1000) image_attribution = models.TextField(null=True, blank=True) gift_card_url = models.URLField(null=True, blank=True, max_length=1000) takeout_url = models.URLField(null=True, blank=True, max_length=1000) donation_url = models.URLField(null=True, blank=True, max_length=1000) geom = models.PointField(srid=4326, null=True, blank=True) place_types = models.TextField(null=True, blank=True) @classmethod def dump_names_for_site(cls, out_fl): all_places = cls.objects.all() output = [] for place in all_places: info = ( """{{key: "{place_id}", address: "{address}", name: "{name}"}},""" .format(name=place.name, address=place.get_short_address(), place_id=place.place_id)) output.append(info) with open(out_fl, 'w') as fl: fl.writelines(output) @classmethod def dump_places_missing_photos(cls, out_fl): missing_photo = cls.objects.filter(image_url=None) names = ['%s\n' % place.place_id for place in missing_photo] with open(out_fl, 'w') as fl: fl.writelines(names) @classmethod def dump_places_missing_website(cls, out_fl): missing_photo = cls.objects.filter(place_url=None) names = ['%s\n' % place.place_id for place in missing_photo] with open(out_fl, 'w') as fl: fl.writelines(names) def get_image_url(self): return self.image_url or "http://TODO/placeholder" def get_short_address(self): return self.address.split(', CA')[0] def to_json(self): return { 'name': self.name, 'address': self.get_short_address(), 'giftCardURL': self.gift_card_url, 'takeoutURL': self.takeout_url, 'donationURL': self.donation_url, 'placeURL': self.place_url, 'emailContact': self.email_contact, 'imageURL': self.get_image_url(), 'placeID': self.place_id, 'area': self.area.key if self.area else None } def to_typeahead_json(self): return { 'name': self.name, 'address': self.get_short_address(), 'key': self.place_id, 'image_attribution': self.image_attribution } def __str__(self): return '%s (%s)' % (self.name, self.address) def save(self, *args, **kwargs): from places.helper import check_link_against_blacklist if self.gift_card_url and not check_link_against_blacklist( self.gift_card_url): raise Exception("Bad Link Saved") if (self.lat and self.lng): self.geom = Point([float(x) for x in (self.lng, self.lat)], srid=4326) super(self.__class__, self).save(*args, **kwargs)
class CitacionsEspecie(models.Model): id = models.AutoField(primary_key=True) especie = models.CharField(max_length=255, blank=True, null=True) # idspinvasora = models.ForeignKey(Especieinvasora, null=True) # FOREIGN #idspinvasora = models.CharField(max_length=255, blank=True, null=True) idspinvasora = models.ForeignKey(Especieinvasora, models.DO_NOTHING, db_column='idspinvasora', blank=True, null=True) data = models.CharField(max_length=100, blank=True, null=True) comarca = models.CharField(max_length=100, blank=True, null=True) municipi = models.CharField(max_length=255, blank=True, null=True) localitat = models.CharField(max_length=255, blank=True, null=True) finca = models.CharField(max_length=255, blank=True, null=True) paratge = models.CharField(max_length=255, blank=True, null=True) utmx = models.FloatField(blank=True, null=True) utmy = models.FloatField(blank=True, null=True) utmz = models.FloatField(blank=True, null=True) utm_10 = models.CharField(max_length=4, blank=True, null=True) #ej DG89 utm_1 = models.CharField(max_length=6, blank=True, null=True) #ej DG8391 geom = models.TextField(blank=True, null=True) geom_4326 = models.TextField(blank=True, null=True) #geom_4326 = models.GeometryField(blank=True, null=True) # se usara el de arriba para mostrar los datos en el mapa propietari_nom = models.CharField(max_length=255, blank=True, null=True) adreca = models.CharField(max_length=255, blank=True, null=True) poblacio = models.CharField(max_length=255, blank=True, null=True) telefon = models.CharField(max_length=255, blank=True, null=True) qual_terreny = models.CharField(max_length=255, blank=True, null=True) espai_natural_protegit = models.CharField(max_length=255, blank=True, null=True) espai_nom = models.CharField(max_length=255, blank=True, null=True) superficie_ocupada = models.CharField(max_length=100, blank=True, null=True) presencia = models.CharField(max_length=255, blank=True, null=True) estat_invasio = models.CharField(max_length=255, blank=True, null=True) # foreign de estatus? observacions = models.CharField(max_length=4000, blank=True, null=True) grup = models.CharField(max_length=255, blank=True, null=True) # imatges = models.FileField(upload_to='imatges',blank=True,null=True) # imatge_panoramica = models.ForeignKey('Imatges', models.DO_NOTHING, related_name='citacio_imatge_panoramica', blank=True, null=True) # imatge_detall_espcie = models.ForeignKey('Imatges', models.DO_NOTHING, related_name='citacio_imatge_detall_especie', blank=True, null=True) # imatge_detall_localitat_1 = models.ForeignKey('Imatges', models.DO_NOTHING, related_name='citacio_imatge_detall_localitat_1',blank=True, null=True) # imatge_detall_localitat_2 = models.ForeignKey('Imatges', models.DO_NOTHING,related_name='citacio_imatge_detall_localitat_2', blank=True,null=True) contacte = models.EmailField(blank=True, null=True) NIP = models.CharField(max_length=255, blank=True, null=True) validat = models.CharField(max_length=255, blank=True, null=True) usuari = models.CharField(max_length=255, blank=True, null=True) # usuario que ha creado el formulario data_creacio = models.CharField(max_length=100, blank=True, null=True) data_modificacio = models.CharField(max_length=100, blank=True, null=True) class Meta: managed = True db_table = 'citacions_especie'
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.db.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 ( BigIntegerRangeField, DateRangeField, DateTimeRangeField, IntegerRangeField, ) 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() date_range = DateRangeField() datetime_range = DateTimeRangeField() except ImportError: # Skip PostgreSQL-related fields pass try: from django.contrib.postgres.fields.ranges import FloatRangeField if settings.USING_POSTGRES: float_range = FloatRangeField() except ImportError: # Django version greater or equal than 3.1 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 App(models.Model): name = models.CharField(max_length=200, null=True, blank=True) title = models.CharField(max_length=200, null=True, blank=True) description = models.TextField(null=True, blank=True) short_description = models.TextField(null=True, blank=True) app_url = models.URLField(null=True, blank=True) author = models.CharField(max_length=200, null=True, blank=True) author_website = models.URLField(null=True, blank=True) license = models.CharField(max_length=200, null=True, blank=True) tags = TaggableManager() date_installed = models.DateTimeField( 'Date Installed', auto_now_add=True, null=True) installed_by = models.ForeignKey( geonode_settings.AUTH_USER_MODEL, null=True, blank=True) single_instance = models.BooleanField( default=False, null=False, blank=False) category = models.ManyToManyField(AppType, related_name='apps') status = models.CharField( max_length=100, blank=False, null=False, default='Alpha') owner_url = models.URLField(null=True, blank=True) help_url = models.URLField(null=True, blank=True) app_img_url = models.TextField(max_length=1000, blank=True, null=True) rating = models.IntegerField(default=0, null=True, blank=True) contact_name = models.CharField(max_length=200, null=True, blank=True) contact_email = models.EmailField(null=True, blank=True) version = models.CharField(max_length=10) store = models.ForeignKey(AppStore, null=True) order = models.IntegerField(null=True, default=0) default_config = JSONField(default={}) class meta(object): ordering = ['order'] def __str__(self): return self.title @property def settings_url(self): try: return reverse("%s_settings" % self.name) except BaseException as e: logger.error(e.message) return None @property def urls(self): admin_urls = logged_in_urls = anonymous_urls = None try: app_module = __import__(self.name) if hasattr(app_module, 'urls_dict'): urls_dict = getattr(app_module, 'urls_dict') if 'admin' in list(urls_dict.keys()): admin_urls = urls_dict['admin'] else: admin_urls = None if 'logged_in' in list(urls_dict.keys()): logged_in_urls = urls_dict['logged_in'] else: logged_in_urls = None if 'anonymous' in list(urls_dict.keys()): anonymous_urls = urls_dict['anonymous'] else: anonymous_urls = None except ImportError as e: logger.error(e.message) return (admin_urls, logged_in_urls, anonymous_urls) @property def open_url(self): from django.core.urlresolvers import reverse open_url = reverse('app_manager_base_url') + self.name try: app_module = __import__(self.name) if hasattr(app_module, 'OPEN_URL_NAME'): open_url = reverse(getattr(app_module, 'OPEN_URL_NAME')) except ImportError as e: logger.error(e.message) return open_url @property def create_new_url(self): from django.core.urlresolvers import reverse create_new_url = reverse('{}.new'.format(self.name)) try: app_module = __import__(self.name) if hasattr(app_module, 'CREATE_NEW_URL_NAME'): create_new_url = reverse( getattr(app_module, 'CREATE_NEW_URL_NAME')) except ImportError as e: logger.error(e.message) return create_new_url @property def admin_urls(self): return self.urls[0] @property def logged_in_urls(self): return self.urls[1] @property def anonymous_urls(self): return self.urls[2] @property def new_url(self): try: return reverse("%s.new" % self.name) except BaseException as e: logger.error(e.message) return None def set_active(self, active=True): app = CartoviewApp.objects.get(self.name, None) if app: app.active = active app.commit() CartoviewApp.save() return app @property def config(self): return CartoviewApp.objects.get(self.name, None)
class User(AbstractUser): email = models.EmailField( verbose_name='email address', max_length=255, unique=True, ) has_profile = models.BooleanField(default=False) middle_name = models.CharField(blank=True, max_length=255, unique=False) bio = models.TextField(blank=True, default='') contact_email = models.EmailField(blank=True, default='', verbose_name=_('Contact email')) phone = models.CharField(blank=True, default='', max_length=255) address = models.CharField(blank=True, default='', max_length=255) city = models.CharField(blank=True, default='', max_length=255) state = models.CharField(blank=True, default='', max_length=255) postal_code = models.CharField(blank=True, default='', max_length=255) country = CountryField(blank=True) url = models.URLField(blank=True, default='', max_length=255) geom = models.PointField(blank=True, null=True) roles = models.ManyToManyField( Role, blank=True, ) related_individuals = models.ManyToManyField( 'self', through='mdi.EntitiesEntities') related_organizations = models.ManyToManyField( 'mdi.Organization', through='mdi.EntitiesEntities', through_fields=['from_ind', 'to_org']) languages = models.ManyToManyField( 'mdi.Language', blank=True, ) services = models.ManyToManyField( 'mdi.Service', blank=True, ) community_skills = models.TextField( blank=True, default='') # Only applies to Community Builders. field_of_study = models.CharField( blank=True, default='', max_length=254) # Only applies to Researchers. Much still TBD. affiliation = models.TextField( blank=True, default='') # Only applies to Researchers. Much still TBD. affiliation_url = models.URLField(blank=True, default='', max_length=255) projects = models.TextField( blank=True, default='') # Only applies to Researchers. Much still TBD. challenges = models.ManyToManyField( 'mdi.Challenge', blank=True, ) socialnetworks = models.ManyToManyField(SocialNetwork, blank=True, through='UserSocialNetwork') notes = models.TextField(blank=True, default='') source = models.ForeignKey('mdi.Source', on_delete=models.CASCADE, default=5) # created_at: would normally add this but django-registration gives us date_joined updated_at = models.DateTimeField(auto_now=True) USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] @classmethod def get_email_field_name(cls): return 'email' class Meta: ordering = [ 'last_name', ] db_table = 'auth_user'
class Status(CreatedUpdatedModel): TARGET_API_SIGMAX = 'sigmax' TARGET_API_CHOICES = ((TARGET_API_SIGMAX, 'Sigmax (City Control)'), ) _signal = models.ForeignKey('signals.Signal', related_name='statuses', on_delete=models.CASCADE) text = models.CharField(max_length=10000, null=True, blank=True) # TODO, rename field to `email` it's not a `User` it's a `email`... user = models.EmailField(null=True, blank=True) target_api = models.CharField(max_length=250, null=True, blank=True, choices=TARGET_API_CHOICES) state = models.CharField(max_length=20, blank=True, choices=workflow.STATUS_CHOICES, default=workflow.GEMELD, help_text='Melding status') # TODO, do we need this field or can we remove it? extern = models.BooleanField( default=False, help_text='Wel of niet status extern weergeven') extra_properties = models.JSONField(null=True, blank=True) # SIG-2620 Flag to determine if a status can send an email send_email = models.BooleanField(default=False) history_log = GenericRelation('history.Log', object_id_field='object_pk') class Meta: permissions = ( ('push_to_sigmax', 'Doorsturen van een melding (THOR)'), # SIG-2192 ) verbose_name_plural = 'Statuses' get_latest_by = 'datetime' ordering = ('created_at', ) def __str__(self): return str(self.text) # TODO: Maybe migrate user to created_by, for now made this work-around @property def created_by(self): return self.user @created_by.setter def created_by(self, created_by): self.user = created_by def clean(self): """Validate instance. Most important validation is the state transition. :raises: ValidationError :returns: """ errors = {} if self._signal.status: # We already have a status so let's check if the new status can be set current_state = self._signal.status.state current_state_display = self._signal.status.get_state_display() else: # No status has been set yet so we default to LEEG current_state = workflow.LEEG current_state_display = workflow.LEEG logger.warning('Signal #{} has status set to None'.format( self._signal.pk)) new_state = self.state new_state_display = self.get_state_display() # Validating state transition. if new_state not in workflow.ALLOWED_STATUS_CHANGES[current_state]: error_msg = 'Invalid state transition from `{from_state}` to `{to_state}`.'.format( from_state=current_state_display, to_state=new_state_display) errors['state'] = ValidationError(error_msg, code='invalid') # Validating state "TE_VERZENDEN". if new_state == workflow.TE_VERZENDEN and not self.target_api: error_msg = 'This field is required when changing `state` to `{new_state}`.'.format( new_state=new_state_display) errors['target_api'] = ValidationError(error_msg, code='required') if new_state != workflow.TE_VERZENDEN and self.target_api: error_msg = 'This field can only be set when changing `state` to `{state}`.'.format( state=workflow.TE_VERZENDEN) errors['target_api'] = ValidationError(error_msg, code='invalid') # Validating text field required. if new_state in [workflow.AFGEHANDELD, workflow.HEROPEND ] and not self.text: error_msg = 'This field is required when changing `state` to `{new_state}`.'.format( new_state=new_state_display) errors['text'] = ValidationError(error_msg, code='required') if errors: raise ValidationError(errors)
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) data_source = models.CharField(max_length=128, blank=True, default='', verbose_name=_('External data source')) data_source_hours = models.CharField(max_length=128, blank=True, default='', verbose_name=_('External data source for opening hours')) 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())) def has_imported_data(self): return self.data_source != '' def has_imported_hours(self): return self.data_source_hours != '' def is_editable(self): """ Whether unit is editable by normal admin users or not """ return not (self.has_imported_data() or self.has_imported_hours())
class AuthUser(AbstractBaseUser, PermissionsMixin): account = models.ForeignKey(Account, null=True, blank=True, related_name='users') email = models.EmailField( _('email address'), unique=True, help_text=_('Required.'), error_messages={ 'unique': _('The given email address has already been registered.') }) first_name = models.CharField(_('first name'), max_length=30, blank=True) last_name = models.CharField(_('last name'), max_length=30, blank=True) metadata = models.TextField(blank=True) is_account_admin = models.BooleanField( _('account admin status'), default=False, help_text=_('Designates whether the user is as account admin.')) is_staff = models.BooleanField( _('staff status'), default=False, help_text=_( 'Designates whether the user can log into the admin site.')) is_active = models.BooleanField( _('active'), default=True, help_text=_('Designates whether this user should be treated as ' 'active. Unselect this instead of deleting accounts.')) created_at = models.DateTimeField(_('created at'), editable=False) updated_at = models.DateTimeField(_('updated at'), editable=False) objects = AuthUserManager() USERNAME_FIELD = 'email' class Meta: verbose_name = _('user') verbose_name_plural = _('users') def get_full_name(self): full_name = '%s %s' % (self.first_name, self.last_name) return full_name.strip() def get_short_name(self): return self.first_name def get_token(self): try: token = Token.objects.get(user=self) except Token.DoesNotExist: token = '' return str(token) def email_user(self, subject, message, from_email=None, **kwargs): send_mail(subject, message, from_email, [self.email], **kwargs) def save(self, *args, **kwargs): now = datetime.now(timezone.utc) if not self.id: self.created_at = now self.updated_at = now if self.account is not None and len( AuthUser.objects.filter(account=self.account, is_account_admin=True)) == 0: self.is_account_admin = True # an account needs an account admin, so make it the first user to enroll else: self.updated_at = now super(AuthUser, self).save(*args, **kwargs)
class Person(models.Model): name = models.CharField(max_length=130) email = models.EmailField(blank=True) job_title = models.CharField(max_length=30, blank=True) bio = models.TextField(blank=True)
class Reservation(ModifiableModel): CREATED = 'created' CANCELLED = 'cancelled' CONFIRMED = 'confirmed' DENIED = 'denied' REQUESTED = 'requested' STATE_CHOICES = ( (CREATED, _('created')), (CANCELLED, _('cancelled')), (CONFIRMED, _('confirmed')), (DENIED, _('denied')), (REQUESTED, _('requested')), ) resource = models.ForeignKey('Resource', verbose_name=_('Resource'), db_index=True, related_name='reservations', on_delete=models.PROTECT) begin = models.DateTimeField(verbose_name=_('Begin time')) end = models.DateTimeField(verbose_name=_('End time')) duration = pgfields.DateTimeRangeField( verbose_name=_('Length of reservation'), null=True, blank=True, db_index=True) comments = models.TextField(null=True, blank=True, verbose_name=_('Comments')) user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('User'), null=True, blank=True, db_index=True, on_delete=models.PROTECT) state = models.CharField(max_length=16, choices=STATE_CHOICES, verbose_name=_('State'), default=CONFIRMED) approver = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('Approver'), related_name='approved_reservations', null=True, blank=True, on_delete=models.SET_NULL) # access-related fields access_code = models.CharField(verbose_name=_('Access code'), max_length=32, null=True, blank=True) # EXTRA FIELDS START HERE event_subject = models.CharField(max_length=200, verbose_name=_('Event subject'), blank=True) event_description = models.TextField(verbose_name=_('Event description'), blank=True) number_of_participants = models.PositiveSmallIntegerField( verbose_name=_('Number of participants'), blank=True, null=True) participants = models.TextField(verbose_name=_('Participants'), blank=True) host_name = models.CharField(verbose_name=_('Host name'), max_length=100, blank=True) # extra detail fields for manually confirmed reservations reserver_name = models.CharField(verbose_name=_('Reserver name'), max_length=100, blank=True) reserver_id = models.CharField( verbose_name=_('Reserver ID (business or person)'), max_length=30, blank=True) reserver_email_address = models.EmailField( verbose_name=_('Reserver email address'), blank=True) reserver_phone_number = models.CharField( verbose_name=_('Reserver phone number'), max_length=30, blank=True) reserver_address_street = models.CharField( verbose_name=_('Reserver address street'), max_length=100, blank=True) reserver_address_zip = models.CharField( verbose_name=_('Reserver address zip'), max_length=30, blank=True) reserver_address_city = models.CharField( verbose_name=_('Reserver address city'), max_length=100, blank=True) company = models.CharField(verbose_name=_('Company'), max_length=100, blank=True) billing_address_street = models.CharField( verbose_name=_('Billing address street'), max_length=100, blank=True) billing_address_zip = models.CharField( verbose_name=_('Billing address zip'), max_length=30, blank=True) billing_address_city = models.CharField( verbose_name=_('Billing address city'), max_length=100, blank=True) # If the reservation was imported from another system, you can store the original ID in the field below. origin_id = models.CharField(verbose_name=_('Original ID'), max_length=50, editable=False, null=True) objects = ReservationQuerySet.as_manager() class Meta: verbose_name = _("reservation") verbose_name_plural = _("reservations") ordering = ('id', ) def _save_dt(self, attr, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ save_dt(self, attr, dt, self.resource.unit.time_zone) def _get_dt(self, attr, tz): return get_dt(self, attr, tz) @property def begin_tz(self): return self.begin @begin_tz.setter def begin_tz(self, dt): self._save_dt('begin', dt) def get_begin_tz(self, tz): return self._get_dt("begin", tz) @property def end_tz(self): return self.end @end_tz.setter def end_tz(self, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ self._save_dt('end', dt) def get_end_tz(self, tz): return self._get_dt("end", tz) def is_active(self): return self.end >= timezone.now() and self.state not in ( Reservation.CANCELLED, Reservation.DENIED) def is_own(self, user): if not (user and user.is_authenticated): return False return user == self.user def need_manual_confirmation(self): return self.resource.need_manual_confirmation def are_extra_fields_visible(self, user): # the following logic is used also implemented in ReservationQuerySet # so if this is changed that probably needs to be changed as well if self.is_own(user): return True return self.resource.can_view_reservation_extra_fields(user) def can_view_access_code(self, user): if self.is_own(user): return True return self.resource.can_view_access_codes(user) def set_state(self, new_state, user): # Make sure it is a known state assert new_state in (Reservation.REQUESTED, Reservation.CONFIRMED, Reservation.DENIED, Reservation.CANCELLED) old_state = self.state if new_state == old_state: if old_state == Reservation.CONFIRMED: reservation_modified.send(sender=self.__class__, instance=self, user=user) return if new_state == Reservation.CONFIRMED: self.approver = user reservation_confirmed.send(sender=self.__class__, instance=self, user=user) elif old_state == Reservation.CONFIRMED: self.approver = None # Notifications if new_state == Reservation.REQUESTED: self.send_reservation_requested_mail() self.send_reservation_requested_mail_to_officials() elif new_state == Reservation.CONFIRMED: if self.need_manual_confirmation(): self.send_reservation_confirmed_mail() elif self.resource.is_access_code_enabled(): self.send_reservation_created_with_access_code_mail() elif new_state == Reservation.DENIED: self.send_reservation_denied_mail() elif new_state == Reservation.CANCELLED: if user != self.user: self.send_reservation_cancelled_mail() reservation_cancelled.send(sender=self.__class__, instance=self, user=user) self.state = new_state self.save() def can_modify(self, user): if not user: return False # reservations that need manual confirmation and are confirmed cannot be # modified or cancelled without reservation approve permission cannot_approve = not self.resource.can_approve_reservations(user) if self.need_manual_confirmation( ) and self.state == Reservation.CONFIRMED and cannot_approve: return False return self.user == user or self.resource.can_modify_reservations(user) def can_add_comment(self, user): if self.is_own(user): return True return self.resource.can_access_reservation_comments(user) def can_view_field(self, user, field): if field not in RESERVATION_EXTRA_FIELDS: return True if self.is_own(user): return True return self.resource.can_view_reservation_extra_fields(user) def can_view_catering_orders(self, user): if self.is_own(user): return True return self.resource.can_view_catering_orders(user) def format_time(self): tz = self.resource.unit.get_tz() begin = self.begin.astimezone(tz) end = self.end.astimezone(tz) return format_dt_range(translation.get_language(), begin, end) def __str__(self): if self.state != Reservation.CONFIRMED: state_str = ' (%s)' % self.state else: state_str = '' return "%s: %s%s" % (self.format_time(), self.resource, state_str) def clean(self, **kwargs): """ Check restrictions that are common to all reservations. If this reservation isn't yet saved and it will modify an existing reservation, the original reservation need to be provided in kwargs as 'original_reservation', so that it can be excluded when checking if the resource is available. """ if self.end <= self.begin: raise ValidationError( _("You must end the reservation after it has begun")) # Check that begin and end times are on valid time slots. opening_hours = self.resource.get_opening_hours( self.begin.date(), self.end.date()) for dt in (self.begin, self.end): days = opening_hours.get(dt.date(), []) day = next((day for day in days if day['opens'] is not None and day['opens'] <= dt <= day['closes']), None) if day and not is_valid_time_slot(dt, self.resource.min_period, day['opens']): raise ValidationError( _("Begin and end time must match time slots")) original_reservation = self if self.pk else kwargs.get( 'original_reservation', None) if self.resource.check_reservation_collision(self.begin, self.end, original_reservation): raise ValidationError( _("The resource is already reserved for some of the period")) if (self.end - self.begin) < self.resource.min_period: raise ValidationError( _("The minimum reservation length is %(min_period)s") % {'min_period': humanize_duration(self.min_period)}) if self.access_code: validate_access_code(self.access_code, self.resource.access_code_type) def get_notification_context(self, language_code, user=None): if not user: user = self.user with translation.override(language_code): reserver_name = self.reserver_name if not reserver_name and self.user and self.user.get_display_name( ): reserver_name = self.user.get_display_name() context = { 'resource': self.resource.name, 'begin': localize_datetime(self.begin), 'end': localize_datetime(self.end), 'begin_dt': self.begin, 'end_dt': self.end, 'time_range': self.format_time(), 'number_of_participants': self.number_of_participants, 'host_name': self.host_name, 'reserver_name': reserver_name, 'event_subject': self.event_subject, } if self.resource.unit: context['unit'] = self.resource.unit.name if self.can_view_access_code(user) and self.access_code: context['access_code'] = self.access_code if self.resource.reservation_confirmed_notification_extra: context[ 'extra_content'] = self.resource.reservation_confirmed_notification_extra return context def send_reservation_mail(self, notification_type, user=None): """ Stuff common to all reservation related mails. If user isn't given use self.user. """ if user: email_address = user.email else: if not (self.reserver_email_address or self.user): return email_address = self.reserver_email_address or self.user.email user = self.user language = user.get_preferred_language() if user else DEFAULT_LANG context = self.get_notification_context(language) try: rendered_notification = render_notification_template( notification_type, context, language) except NotificationTemplateException as e: logger.error(e, exc_info=True, extra={'user': user.uuid}) return send_respa_mail(email_address, rendered_notification['subject'], rendered_notification['body']) def send_reservation_requested_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_REQUESTED) def send_reservation_requested_mail_to_officials(self): notify_users = self.resource.get_users_with_perm( 'can_approve_reservation') if len(notify_users) > 100: raise Exception("Refusing to notify more than 100 users (%s)" % self) for user in notify_users: self.send_reservation_mail( NotificationType.RESERVATION_REQUESTED_OFFICIAL, user=user) def send_reservation_denied_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_DENIED) def send_reservation_confirmed_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_CONFIRMED) def send_reservation_cancelled_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_CANCELLED) def send_reservation_created_with_access_code_mail(self): self.send_reservation_mail( NotificationType.RESERVATION_CREATED_WITH_ACCESS_CODE) def save(self, *args, **kwargs): self.duration = DateTimeTZRange(self.begin, self.end, '[)') access_code_type = self.resource.access_code_type if not self.resource.is_access_code_enabled(): self.access_code = '' elif not self.access_code: self.access_code = generate_access_code(access_code_type) return super().save(*args, **kwargs)
class InformationDesk(models.Model): name = models.CharField(verbose_name=_("Title"), max_length=256, db_column='nom') type = models.ForeignKey(InformationDeskType, verbose_name=_("Type"), related_name='desks', db_column='type') description = models.TextField(verbose_name=_("Description"), blank=True, db_column='description', help_text=_("Brief description")) phone = models.CharField(verbose_name=_("Phone"), max_length=32, blank=True, null=True, db_column='telephone') email = models.EmailField(verbose_name=_("Email"), max_length=256, db_column='email', blank=True, null=True) website = models.URLField(verbose_name=_("Website"), max_length=256, db_column='website', blank=True, null=True) photo = models.FileField(verbose_name=_("Photo"), upload_to=settings.UPLOAD_DIR, db_column='photo', max_length=512, blank=True, null=True) street = models.CharField(verbose_name=_("Street"), max_length=256, blank=True, null=True, db_column='rue') postal_code = models.CharField(verbose_name=_("Postal code"), max_length=8, blank=True, null=True, db_column='code') municipality = models.CharField(verbose_name=_("Municipality"), blank=True, null=True, max_length=256, db_column='commune') geom = models.PointField(verbose_name=_("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 = _("Information desk") verbose_name_plural = _("Information desks") ordering = ['name'] def __str__(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 (IOError, InvalidImageFormatError): logger.warning( _("Image %s invalid or missing from disk.") % self.photo) return None @property def resized_picture(self): if not self.photo: return None thumbnailer = get_thumbnailer(self.photo) try: return thumbnailer.get_thumbnail(aliases.get('medium')) except (IOError, 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 DownloadRequest(models.Model): email_address = models.EmailField() items = models.ManyToManyField(BoundedItem, blank=True) #stored as array of string JSON, only used once so that's okay in this case, change if upgraded to Postgres 9.4+ #FIXME: remove, no longer used external_items = ArrayField(models.TextField(), null=True, blank=True) date = models.DateTimeField(auto_now_add=True) active = models.BooleanField(default=False) image_width = models.IntegerField(default=2000) wfs_format = models.TextField(null=True, blank=True) def create_zip_file_and_notify(self, **kwargs): i = self if not i.active: return zip_file_path = "/media/zip/place_data_for_%s_%d.zip" % ( i.email_address, i.id) zip_file = zipfile.ZipFile( "%s/%s" % (settings.BASE_DIR, zip_file_path), "w") for item in i.items.all(): location = "{%s}" if not item.Location[0] == '{' else item.Location location = json.loads(location) if item.DataType == "Book": pdf_file_path = "%s/media/pdf/%s.pdf" % (settings.BASE_DIR, item.Name) zip_file.write(pdf_file_path, "place_data/%s.pdf" % (item.LayerId)) zip_file.writestr("place_data/%s.xml" % (item.LayerId), item.FgdcText.encode('utf-8')) elif "wfs" in location: wfs_path = location["wfs"][0] data_file_path = "%s/temp/%s_%d.json" % (settings.BASE_DIR, item.LayerId, i.id) data_url = "http://%s/external_wfs/%s?request=GetFeature&typeName=%s:%s&outputFormat=%s&srsName=EPSG:4326" % ( settings.ALLOWED_HOSTS[0], wfs_path.replace("http://", ""), item.WorkspaceName, item.Name, i.wfs_format) urlretrieve(data_url, data_file_path) extension = None if i.wfs_format == "shape-zip": extension = "zip" elif i.wfs_format == "GML2" or i.wfs_format == "GML3": extension = "gml" elif i.wfs_format == "KML": extension = "kml" else: extension = "json" zip_file.write(data_file_path, "place_data/%s.%s" % (item.LayerId, extension)) os.remove(data_file_path) zip_file.writestr("place_data/%s.xml" % (item.LayerId), item.FgdcText.encode('utf-8')) elif "wms" in location: wms_path = location["wms"][0] image_file_path = "%s/temp/%s_%d.tiff" % (settings.BASE_DIR, item.Name, i.id) image_url = "%s/reflect?format=image/geotiff&transparent=true&width=%d&layers=%s" % ( wms_path, i.image_width, item.Name) urlretrieve(image_url, image_file_path) zip_file.write(image_file_path, "place_data/%s.tif" % (item.LayerId)) os.remove(image_file_path) zip_file.writestr("place_data/%s.xml" % (item.LayerId), item.FgdcText.encode('utf-8')) zip_file.close() # send mail with zip link mail_server = smtplib.SMTP('cisunix.unh.edu') message = MIMEText( "Your download request is located at http://%s%s. It will be deleted after 24 hours." % (settings.ALLOWED_HOSTS[0], zip_file_path)) message["Subject"] = "PLACE Data ready for download" message["To"] = i.email_address message["From"] = "*****@*****.**" mail_server.sendmail(message["From"], [i.email_address], message.as_string()) i.active = False i.save()
class Person(models.Model): first_name = models.CharField(max_length=255) last_name = models.CharField(max_length=255) mail = models.EmailField()
class Profile(models.Model): email = models.EmailField()
class Project(models.Model): owner = models.ForeignKey(User, null=True, blank=True, related_name="owner") editors = models.ManyToManyField(User, related_name="editors") nom = models.CharField(u"Nom de l'ÉcoQuartier", max_length=255) # mise_a_jour = models.DateField(auto_now_add=True) # statut = models.ForeignKey(Statut, null=True) # zonage_insee = models.ForeignKey(ZonageINSEE, null=True, verbose_name="Zonage INSEE") # commune = models.ForeignKey( Commune, null=True, verbose_name="Commune principale", help_text=u"Sur quelle commune est situé l'ÉcoQuartier") # communes = models.ManyToManyField(Commune, related_name="other_communes") # population = models.IntegerField(default=0) # description = models.TextField("Description du projet", help_text="10 lignes maximum") # contexte_commune = models.ForeignKey(ContexteCommune, null=True) # littorale = models.BooleanField(default=False) # montagne = models.BooleanField(default=False) # autres_communes = models.TextField() adresse = models.TextField() # systeme_projection = models.CharField(max_length=255) # coordonnees_geographiques = models.GeometryCollectionField(blank=True, null=True) # site = models.TextField( "Caractéristiques initiales du site", help_text= u"Préciser en 5 lignes maximum les caractéristiques initiales du site : par exemple, terrains agricoles, site militaire, friches industrielles, quartier d’habitat social..." ) # contexte_site = models.TextField("Contexte du site", help_text=context_site_help) # type_operations = models.ManyToManyField( TypeOperation, verbose_name="Type d'opérations (plusieurs choix possibles)", help_text="Plusieurs choix possibles") # type_operation_autre = models.TextField() # vocations = models.ManyToManyField(Vocation) # vocation_autre = models.TextField() # superficieha = models.FloatField( u"Superficie de l'opération", help_text=u"Quelle est la superficie de l'EcoQuartier ? (ha)", null=True) # surface_nonbatie = models.FloatField( u"Surface non bâtie publique", help_text= u"Toute surface non bâtie appartenant au domaine public, notamment voirie, espaces verts, espaces publics", null=True) # habitants = models.IntegerField(u"Nombre d'habitants prévus", default=0) # logements = models.IntegerField(u"Nombre de logements", help_text=logements_help, default=0) # shon_logementsm = models.IntegerField( u"SHON logement", help_text=u"Surface hors œuvre net des logements", default=0) # logements_sociau = models.IntegerField(u"Nombre de logements sociaux", help_text=u"", null=True, blank=True) # logements_sociaux_detail = models.TextField(u"Logements sociaux détail") equipements_publics = models.TextField( u"Détail équipements publics", help_text=u"Précisions sur les équipements publics considérés") # shon_equipementsm = models.IntegerField(u"Surface équipements publics", null=True, blank=True) # commerces_services = models.TextField( u"Détail commerces et services", help_text= u"Préciser le type de commerces et services programmés dans l'opération" ) # shon_commercesm = models.IntegerField( u"Surface de plancher des commerces et services", null=True, blank=True) bureaux_activites = models.TextField( u"Détail bureaux et activités", help_text=u"Préciser le type d'activités prévues dans l'opération") shon_bureauxm = models.IntegerField( u"Surface de plancher bureaux et activités", null=True, blank=True) programme_detail = models.TextField() # densite_brute = models.IntegerField(null=True, blank=True) # densite_brute_logements = models.IntegerField(null=True, blank=True) # densite_logements = models.IntegerField(null=True, blank=True) # projet_social = models.TextField() # economie_circulaire = models.TextField() # charte = models.FileField(upload_to='charte/%Y/%m/%d/', null=True, blank=True, verbose_name="Charte ÉcoQuartier") charte_date = models.DateField(null=True, blank=True) demarches = models.ManyToManyField( Demarche, verbose_name= u"Engagement dans d'autres démarches de développement durable") demarches_autres = models.CharField(u"Autres démarches", max_length=255, null=True, blank=True) echelle = models.ForeignKey(Echelle, null=True, blank=True) def is_economie_circulaire(self): return True if self.economie_circulaire != '' else False is_economie_circulaire.boolean = True is_economie_circulaire.short_description = u'économie circulaire' attenuation_changement_climatique = models.TextField() # def is_attenuation_changement_climatique(self): return True if self.attenuation_changement_climatique != '' else False is_attenuation_changement_climatique.boolean = True is_attenuation_changement_climatique.short_description = u'atténuation du changement climatique' label_demarche = models.TextField() # def is_label_demarche(self): return True if self.label_demarche != '' else False is_label_demarche.boolean = True is_label_demarche.short_description = u'label démarche' participation_2009 = models.BooleanField(default=False) # participation_2011 = models.BooleanField(default=False) # nomine = models.BooleanField(default=False) # laureat = models.BooleanField(default=False) # resultats_palmares = models.TextField() # candidat_label = models.BooleanField(default=False) # annee_candidature = models.IntegerField(null=True, blank=True) # label_ecoquartier = models.ForeignKey(LabelEcoQuartier, null=True, verbose_name=u"État d'avancement") # @property def state(self): if self.label_ecoquartier is None: return 'none' if self.label_ecoquartier.id == 3: return 'labeled' elif self.label_ecoquartier.id == 2: return 'engaged' else: return 'charte' annee_label = models.IntegerField(null=True, blank=True) # procedure = models.ForeignKey(Procedure, null=True) # procedure_detail = models.TextField() # aspects_fonciers = models.TextField() # etudes_prealables = models.TextField() # concertation = models.TextField() # collectivite_ou_epci_porteur = models.CharField(max_length=500) # maitrise_ouvrage = models.TextField() # maitrise_oeuvre = models.TextField() # partenariats = models.TextField() # opacrations_marquantes = models.TextField( u"Opérations marquantes du projet", help_text= u"<i>Si au sein de l’opération d’aménagement, une ou plusieurs constructions ou espaces publics majeurs méritent d’être remarquées, vous pouvez nous indiquer ici tous les éléments nécessaires (nom du bâtiment ou de l’espace public, fonction, maîtrise d’ouvrage et maîtrise d’œuvre, particularités de l’opération, autres éléments ou photographies en votre possession...)</i>" ) # engagement = models.IntegerField( "Date d'engagement de l'opération", help_text= u"L’année d’engagement de l’opération : année de la première délibération concernant l’opération", null=True, blank=True) # creation = models.IntegerField( u"Date de création de la ZAC", help_text=u"L’année de création de la ZAC (si ZAC)", null=True, blank=True) # realisation = models.IntegerField( u"Date de réalisation de la ZAC", help_text="Année de réalisation de la ZAC (si ZAC)", null=True, blank=True) # autorisation = models.IntegerField( u"Date d'autorisation d'aménager", help_text=u"Année d’autorisation d’aménager (si permis d’aménager)", null=True, blank=True) # permis = models.IntegerField( u"Date du permis de construire", help_text= u"année du permis de construire (si opération se limite à un permis ou un permis groupé)", null=True, blank=True) # debut = models.IntegerField(u"Date du début des travaux", help_text=u"année du début des travaux", null=True, blank=True) # livraison = models.IntegerField( u"Date de livraison des premiers bâtiments", help_text=u"Année de livraison des premiers bâtiments", null=True, blank=True) # achevement = models.IntegerField( u"Date d'achèvement de l'opération", help_text=u"Année d’achèvement de l’opération", null=True, blank=True) # complementaire = models.IntegerField( u"Date complémentaire", help_text=u"Autre date importante non citée ci-dessus", null=True, blank=True) # coats = models.TextField() # sources = models.TextField() # sources_details = models.TextField() # contact = models.TextField() # project_manager_lastname = models.CharField("Nom", max_length=255, null=True, blank=True) project_manager_firstname = models.CharField("Prénom", max_length=255, null=True, blank=True) project_manager_mail = models.EmailField("Mail", max_length=255, null=True, blank=True) project_manager_structure = models.CharField("Organisme de rattachement", max_length=255, null=True, blank=True) project_developer_lastname = models.CharField("Nom", max_length=255, null=True, blank=True) project_developer_firstname = models.CharField("Prénom", max_length=255, null=True, blank=True) project_developer_mail = models.EmailField("Mail", max_length=255, null=True, blank=True) project_developer_structure = models.CharField("Organisme de rattachement", max_length=255, null=True, blank=True) plusieurs_tranches = models.BooleanField( u"L'opération comporte plusieurs tranches", default=False) sites_enlien = models.TextField() # documents = models.TextField() # eau = models.TextField() # dechets = models.TextField() # biodiversite = models.TextField() # mobilite = models.TextField() # sobriete_energetique_et_energie_renouvelable = models.TextField() # densite_et_formes_urbaines = models.TextField() # ecoconstruction = models.TextField() # autres = models.TextField() # demarches_et_processus = models.TextField() # cadre_de_vie_et_usages = models.TextField() # def is_eau(self): return True if self.eau != '' else False is_eau.boolean = True is_eau.short_description = 'eau' def is_dechets(self): return True if self.dechets != '' else False is_dechets.boolean = True is_dechets.short_description = 'dechets' def is_biodiversite(self): return True if self.biodiversite != '' else False is_biodiversite.boolean = True is_biodiversite.short_description = 'biodiversite' def is_mobilite(self): return True if self.mobilite != '' else False is_mobilite.boolean = True is_mobilite.short_description = 'mobilite' def is_sobriete_energetique_et_energie_renouvelable(self): return True if self.sobriete_energetique_et_energie_renouvelable != '' else False is_sobriete_energetique_et_energie_renouvelable.boolean = True is_sobriete_energetique_et_energie_renouvelable.short_description = 'sobriete energetique et energie renouvelable' def is_densite_et_formes_urbaines(self): return True if self.densite_et_formes_urbaines != '' else False is_densite_et_formes_urbaines.boolean = True is_densite_et_formes_urbaines.short_description = 'densite et formes urbaines' def is_ecoconstruction(self): return True if self.ecoconstruction != '' else False is_ecoconstruction.boolean = True is_ecoconstruction.short_description = 'ecoconstruction' def is_demarches_et_processus(self): return True if self.demarches_et_processus != '' else False is_demarches_et_processus.boolean = True is_demarches_et_processus.short_description = 'demarches et processus' def is_cadre_de_vie_et_usages(self): return True if self.cadre_de_vie_et_usages != '' else False is_cadre_de_vie_et_usages.boolean = True is_cadre_de_vie_et_usages.short_description = 'cadre de vie et usages' tags = models.ManyToManyField(Tag, verbose_name="Points forts du projet") commentaires_demarche_et_processus = models.TextField() ambition_1 = models.TextField() ambition_2 = models.TextField() ambition_3 = models.TextField() ambition_4 = models.TextField() ambition_5 = models.TextField() commentaires_cadre_de_vie_et_usages = models.TextField() ambition_6 = models.TextField() ambition_7 = models.TextField() ambition_8 = models.TextField() ambition_9 = models.TextField() ambition_10 = models.TextField() commentaires_developpement_territorial = models.TextField() ambition_11 = models.TextField() ambition_12 = models.TextField() ambition_13 = models.TextField() ambition_14 = models.TextField() ambition_15 = models.TextField() commentaires_environnement_et_climat = models.TextField() ambition_16 = models.TextField() ambition_17 = models.TextField() ambition_18 = models.TextField() ambition_19 = models.TextField() ambition_20 = models.TextField() synthese_demarche_et_processus = models.TextField() engagement_1 = models.TextField( "Engagement 1 (2000 signes maximum sans compter les espaces)", help_text=engagement_1_help) engagement_2 = models.TextField() engagement_3 = models.TextField() engagement_4 = models.TextField() engagement_5 = models.TextField() synthese_cadre_de_vie_et_usages = models.TextField() engagement_6 = models.TextField() engagement_7 = models.TextField() engagement_8 = models.TextField() engagement_9 = models.TextField(help_text=engagement_9_help) engagement_10 = models.TextField() synthese_developpement_territorial = models.TextField() engagement_11 = models.TextField() engagement_12 = models.TextField() engagement_13 = models.TextField() engagement_14 = models.TextField() engagement_15 = models.TextField() synthese_environnement_et_climat = models.TextField() engagement_16 = models.TextField() engagement_17 = models.TextField() engagement_18 = models.TextField() engagement_19 = models.TextField() engagement_20 = models.TextField() indicateur_i1 = models.TextField() valeur_i1 = models.TextField() indicateur_i2 = models.TextField() valeur_i2 = models.TextField() indicateur_i3 = models.TextField() valeur_i3 = models.TextField() indicateur_i4 = models.TextField() valeur_i4 = models.TextField() indicateur_i5 = models.TextField() valeur_i5 = models.TextField() indicateur_i6 = models.TextField() valeur_i6 = models.TextField() indicateur_i7 = models.TextField() valeur_i7 = models.TextField() indicateur_i8 = models.TextField() valeur_i8 = models.TextField() indicateur_i9 = models.TextField() valeur_i9 = models.TextField() indicateur_i10 = models.TextField() valeur_i10 = models.TextField() indicateur_i11 = models.TextField() valeur_i11 = models.TextField() indicateur_i12 = models.TextField() valeur_i12 = models.TextField() indicateur_i13 = models.TextField() valeur_i13 = models.TextField() indicateur_i14 = models.TextField() valeur_i14 = models.TextField() indicateur_i15 = models.TextField() valeur_i15 = models.TextField() indicateur_i16 = models.TextField() valeur_i16 = models.TextField() indicateur_i17 = models.TextField() valeur_i17 = models.TextField() indicateur_i18 = models.TextField() valeur_i18 = models.TextField() indicateur_i19 = models.TextField() valeur_i19 = models.TextField() indicateur_i20 = models.TextField() valeur_i20 = models.TextField() indicateur_i21 = models.TextField() valeur_i21 = models.TextField() indicateur_i22 = models.TextField() valeur_i22 = models.TextField() indicateur_i23 = models.TextField() valeur_i23 = models.TextField() indicateur_i24 = models.TextField() valeur_i24 = models.TextField() indicateur_i25 = models.TextField() valeur_i25 = models.TextField() indicateur_i26 = models.TextField() valeur_i26 = models.TextField() indicateur_i27 = models.TextField() valeur_i27 = models.TextField() indicateur_i28 = models.TextField() valeur_i28 = models.TextField() indicateur_i29 = models.TextField() valeur_i29 = models.TextField() indicateur_i30 = models.TextField() valeur_i30 = models.TextField() indicateur_i31 = models.TextField() valeur_i31 = models.TextField() indicateur_i32 = models.TextField() valeur_i32 = models.TextField() indicateur_i33 = models.TextField() valeur_i33 = models.TextField() indicateur_i34 = models.TextField() valeur_i34 = models.TextField() indicateur_i35 = models.TextField() valeur_i35 = models.TextField() indicateur_i36 = models.TextField() valeur_i36 = models.TextField() indicateur_i37 = models.TextField() valeur_i37 = models.TextField() indicateur_i38 = models.TextField() valeur_i38 = models.TextField() indicateur_i39 = models.TextField() valeur_i39 = models.TextField() indicateur_i40 = models.TextField() valeur_i40 = models.TextField() indicateur_i41 = models.TextField() valeur_i41 = models.TextField() indicateur_i42 = models.TextField() valeur_i42 = models.TextField() indicateur_i43 = models.TextField() valeur_i43 = models.TextField() indicateur_i44 = models.TextField() valeur_i44 = models.TextField() indicateur_i45 = models.TextField() valeur_i45 = models.TextField() indicateur_i46 = models.TextField() valeur_i46 = models.TextField() indicateur_i47 = models.TextField() valeur_i47 = models.TextField() indicateur_i48 = models.TextField() valeur_i48 = models.TextField() indicateur_i49 = models.TextField() valeur_i49 = models.TextField() indicateur_i50 = models.TextField() valeur_i50 = models.TextField() commentaires = models.TextField() objects = models.GeoManager() plan_situation_1_5000 = models.FileField(upload_to="upload", null=True, blank=True) plan_masse_1_1000 = models.FileField(upload_to="upload", null=True, blank=True) plan_masse_1_500 = models.FileField(upload_to="upload", null=True, blank=True) plan_detaille = models.FileField(upload_to="upload", null=True, blank=True) MAITRISE_OUVRAGE_STRUCTURE_CHOICES = ( ('RC', u'régie communale'), ('SM', u'SEM'), ('SP', u'SPLA'), ('AP', u'Aménageur privé'), ) maitrise_ouvrage_structure = models.CharField( max_length=2, choices=MAITRISE_OUVRAGE_STRUCTURE_CHOICES, null=True, blank=True) maitrise_ouvrage_nom = models.CharField(max_length=255, null=True, blank=True) partenaires = models.ManyToManyField(Partenaire, through='PartenaireDetail') @property def commune_label(self): return self.commune.label @property def short_description(self): return Truncator(self.description).words(50, html=False, truncate=' ...') @property def feature(self): return self.photos[0] if len(self.photos) > 0 else None @property def photos(self): return [ photo.photo.url for photo in self.projectphoto_set.exclude(photo__isnull=True) ] @property def engagement_1_completed(self): fields = [ 'site', 'contexte_site', 'superficieha', 'surface_nonbatie', 'engagement', 'creation', 'realisation', 'autorisation', 'permis', 'debut', 'livraison', 'achevement', 'complementaire', 'programme_detail', 'etudes_prealables', 'opacrations_marquantes', 'habitants', 'logements', 'shon_logementsm', 'logements_sociau', 'equipements_publics', 'shon_equipementsm', 'commerces_services', 'shon_commercesm', 'bureaux_activites', 'shon_bureauxm', 'engagement_1', ] return self.completed(fields) @property def engagement_2_completed(self): fields = [ 'procedure', 'procedure_detail', 'concertation', 'collectivite_ou_epci_porteur', 'maitrise_ouvrage', 'maitrise_oeuvre', 'partenariats', 'engagement_2' ] return self.completed(fields) @property def engagement_3_completed(self): fields = ['coats', 'engagement_3'] return self.completed(fields) @property def engagement_4_completed(self): fields = ['engagement_4'] return self.completed(fields) @property def engagement_5_completed(self): fields = ['engagement_5'] return self.completed(fields) @property def dimension_1_completed(self): values = [ self.engagement_1_completed, self.engagement_2_completed, self.engagement_3_completed, self.engagement_4_completed, self.engagement_5_completed ] return sum(values) @property def engagement_6_completed(self): fields = [ 'aspects_fonciers', 'densite_brute', 'densite_brute_logements', 'densite_logements', 'surface_nonbatie', 'engagement_6' ] return self.completed(fields) @property def engagement_7_completed(self): fields = [ 'habitants', 'logements', 'shon_logementsm', 'logements_sociau', 'engagement_7' ] return self.completed(fields) @property def engagement_8_completed(self): fields = ['coats', 'engagement_8'] return self.completed(fields) @property def engagement_9_completed(self): fields = ['opacrations_marquantes', 'engagement_9'] return self.completed(fields) @property def engagement_10_completed(self): fields = ['contexte_site', 'engagement_10'] return self.completed(fields) @property def dimension_2_completed(self): values = [ self.engagement_6_completed, self.engagement_7_completed, self.engagement_8_completed, self.engagement_9_completed, self.engagement_10_completed, ] return sum(values) @property def engagement_11_completed(self): fields = [ 'equipements_publics', 'shon_equipementsm', 'commerces_services', 'shon_commercesm', 'bureaux_activites', 'shon_bureauxm', 'engagement_11' ] return self.completed(fields) @property def engagement_12_completed(self): fields = [ 'equipements_publics', 'shon_equipementsm', 'commerces_services', 'shon_commercesm', 'bureaux_activites', 'shon_bureauxm', 'engagement_12' ] return self.completed(fields) @property def engagement_13_completed(self): fields = ['engagement_13'] return self.completed(fields) @property def engagement_14_completed(self): fields = ['engagement_14'] return self.completed(fields) @property def engagement_15_completed(self): fields = ['engagement_15'] return self.completed(fields) @property def dimension_3_completed(self): values = [ self.engagement_11_completed, self.engagement_12_completed, self.engagement_13_completed, self.engagement_14_completed, self.engagement_15_completed, ] return sum(values) @property def engagement_16_completed(self): fields = ['engagement_16'] return self.completed(fields) @property def engagement_17_completed(self): fields = ['engagement_17'] return self.completed(fields) @property def engagement_18_completed(self): fields = ['engagement_18'] return self.completed(fields) @property def engagement_19_completed(self): fields = ['engagement_19'] return self.completed(fields) @property def engagement_20_completed(self): fields = ['engagement_20'] return self.completed(fields) @property def dimension_4_completed(self): values = [ self.engagement_16_completed, self.engagement_17_completed, self.engagement_18_completed, self.engagement_19_completed, self.engagement_20_completed ] return sum(values) @property def dimensions_completed(self): values = [ self.dimension_1_completed, self.dimension_2_completed, self.dimension_3_completed, self.dimension_4_completed ] return all(values) def completed(self, fields): values = [ getattr(self, field) != '' and getattr(self, field) is not None for field in fields ] if all(values): return True return False @models.permalink def get_absolute_url(self): return ('detail', (), {'pk': self.pk}) @property def url(self): return reverse('detail', kwargs={'pk': self.id}) def __unicode__(self): if self.commune.departement: return "%s (%s, %s)" % (self.nom, self.commune, self.commune.departement.region) else: return "%s (%s)" % (self.nom, self.commune) def save(self, *args, **kwargs): # update charte_date when a charte is added if self.pk is not None: orig = Project.objects.get(pk=self.pk) if orig.charte != self.charte: self.charte_date = date.today() else: if self.charte: self.charte_date = date.today() # update label_ecoquartier ("état d'avancement") if self.charte_date: self.label_ecoquartier = LabelEcoQuartier.objects.get(id=5) if self.annee_candidature: self.label_ecoquartier = LabelEcoQuartier.objects.get(id=2) if self.annee_label: self.label_ecoquartier = LabelEcoQuartier.objects.get(id=3) super(Project, self).save(*args, **kwargs)
class DepartmentUser(MPTTModel): """Represents a Department user. Maps to an object managed by Active Directory. """ ACTIVE_FILTER = { "active": True, "email__isnull": False, "cost_centre__isnull": False, "contractor": False } # The following choices are intended to match options in Alesco. ACCOUNT_TYPE_CHOICES = ( (3, 'Agency contract'), (0, 'Department fixed-term contract'), (1, 'Other'), (2, 'Permanent'), (4, 'Resigned'), (9, 'Role-based account'), (8, 'Seasonal'), (5, 'Shared account'), (6, 'Vendor'), (7, 'Volunteer'), ) POSITION_TYPE_CHOICES = ( (0, 'Full time'), (1, 'Part time'), (2, 'Casual'), (3, 'Other'), ) # These fields are populated from Active Directory. date_created = models.DateTimeField(auto_now_add=True) date_updated = models.DateTimeField(auto_now=True) cost_centre = models.ForeignKey("organisation.CostCentre", on_delete=models.PROTECT, null=True) cost_centres_secondary = models.ManyToManyField( "organisation.CostCentre", related_name="cost_centres_secondary", blank=True, help_text='NOTE: this provides security group access (e.g. T drives).') org_unit = models.ForeignKey( "organisation.OrgUnit", on_delete=models.PROTECT, null=True, blank=True, verbose_name='organisational unit', help_text="""The organisational unit that represents the user's""" """ primary physical location (also set their distribution group).""") org_units_secondary = models.ManyToManyField( "organisation.OrgUnit", related_name="org_units_secondary", blank=True, help_text='NOTE: this provides email distribution group access.') extra_data = JSONField(null=True, blank=True) ad_guid = models.CharField(max_length=48, unique=True, editable=False) ad_dn = models.CharField(max_length=512, unique=True, editable=False) ad_data = JSONField(null=True, blank=True, editable=False) org_data = JSONField(null=True, blank=True, editable=False) employee_id = models.CharField( max_length=128, null=True, unique=True, blank=True, verbose_name='Employee ID', help_text="HR Employee ID, use 'n/a' if a contractor") email = models.EmailField(unique=True, editable=False) username = models.CharField(max_length=128, editable=False, unique=True, help_text='Pre-Windows 2000 login username.') name = models.CharField(max_length=128, help_text='Format: Surname, Given name') given_name = models.CharField( max_length=128, null=True, help_text='Legal first name (matches birth certificate/password/etc.)') surname = models.CharField( max_length=128, null=True, help_text='Legal surname (matches birth certificate/password/etc.)') name_update_reference = models.CharField( max_length=512, null=True, blank=True, verbose_name='update reference', help_text='Reference for name/CC change request') preferred_name = models.CharField( max_length=256, null=True, blank=True, help_text='Employee-editable preferred name.') title = models.CharField( max_length=128, null=True, help_text='Occupation position title (should match Alesco)') position_type = models.PositiveSmallIntegerField( choices=POSITION_TYPE_CHOICES, null=True, blank=True, default=0, help_text= 'Employee position working arrangement (should match Alesco status)') parent = TreeForeignKey('self', on_delete=models.PROTECT, null=True, blank=True, related_name='children', editable=True, verbose_name='Reports to', help_text='Person that this employee reports to') expiry_date = models.DateTimeField( null=True, editable=False, help_text='Date that the AD account is set to expire.') date_ad_updated = models.DateTimeField( null=True, editable=False, verbose_name='Date AD updated', help_text='The date when the AD account was last updated.') telephone = models.CharField(max_length=128, null=True, blank=True) mobile_phone = models.CharField(max_length=128, null=True, blank=True) extension = models.CharField(max_length=128, null=True, blank=True, verbose_name='VoIP extension') home_phone = models.CharField(max_length=128, null=True, blank=True) other_phone = models.CharField(max_length=128, null=True, blank=True) active = models.BooleanField( default=True, editable=False, help_text='Account is active within Active Directory.') ad_deleted = models.BooleanField( default=False, editable=False, help_text='Account has been deleted in Active Directory.') in_sync = models.BooleanField( default=False, editable=False, help_text='CMS data has been synchronised from AD data.') vip = models.BooleanField( default=False, help_text= "An individual who carries out a critical role for the department") executive = models.BooleanField( default=False, help_text="An individual who is an executive") contractor = models.BooleanField( default=False, help_text= "An individual who is an external contractor (does not include agency contract staff)" ) photo = models.ImageField(blank=True, upload_to=get_photo_path) photo_ad = models.ImageField(blank=True, editable=False, upload_to=get_photo_ad_path) sso_roles = models.TextField( null=True, editable=False, help_text="Groups/roles separated by semicolon") notes = models.TextField(null=True, blank=True, help_text="Officer secondary roles, etc.") working_hours = models.TextField( default="N/A", null=True, blank=True, help_text="Description of normal working hours") secondary_locations = models.ManyToManyField("organisation.Location", blank=True) populate_primary_group = models.BooleanField( default=True, help_text="If unchecked, user will not be added to primary group email" ) account_type = models.PositiveSmallIntegerField( choices=ACCOUNT_TYPE_CHOICES, null=True, blank=True, help_text='Employee account status (should match Alesco status)') alesco_data = JSONField(null=True, blank=True, help_text='Readonly data from Alesco') security_clearance = models.BooleanField( default=False, verbose_name='security clearance granted', help_text='''Security clearance approved by CC Manager (confidentiality agreement, referee check, police clearance, etc.''') o365_licence = models.NullBooleanField( default=None, editable=False, help_text='Account consumes an Office 365 licence.') shared_account = models.BooleanField( default=False, editable=False, help_text='Automatically set from account type.') class MPTTMeta: order_insertion_by = ['name'] class Meta: ordering = ('name', ) def __init__(self, *args, **kwargs): super(DepartmentUser, self).__init__(*args, **kwargs) # Store the pre-save values of some fields on object init. self.__original_given_name = self.given_name self.__original_surname = self.surname self.__original_employee_id = self.employee_id self.__original_cost_centre = self.cost_centre self.__original_name = self.name self.__original_org_unit = self.org_unit def __str__(self): return self.email def save(self, *args, **kwargs): """Override the save method with additional business logic. """ if self.employee_id and self.employee_id.lower() == "n/a": self.employee_id = None if self.employee_id: self.employee_id = "{0:06d}".format(int(self.employee_id)) self.in_sync = True if self.date_ad_updated else False # If the CC is set but not the OrgUnit, use the CC's OrgUnit. if self.cost_centre and not self.org_unit: self.org_unit = self.cost_centre.org_position if self.cost_centre and self.org_unit: self.org_data = self.org_data or {} self.org_data["units"] = list( self.org_unit.get_ancestors(include_self=True).values( "id", "name", "acronym", "unit_type", "costcentre__code", "costcentre__name", "location__name")) self.org_data["unit"] = self.org_data["units"][-1] if self.org_unit.location: self.org_data["location"] = self.org_unit.location.as_dict() if self.org_unit.secondary_location: self.org_data[ "secondary_location"] = self.org_unit.secondary_location.as_dict( ) for unit in self.org_data["units"]: unit["unit_type"] = self.org_unit.TYPE_CHOICES_DICT[ unit["unit_type"]] self.org_data["cost_centre"] = { "name": self.org_unit.name, "code": self.cost_centre.code, "cost_centre_manager": str(self.cost_centre.manager), "business_manager": str(self.cost_centre.business_manager), "admin": str(self.cost_centre.admin), "tech_contact": str(self.cost_centre.tech_contact), } if self.cost_centres_secondary.exists(): self.org_data['cost_centres_secondary'] = [{ 'name': i.name, 'code': i.code, } for i in self.cost_centres_secondary.all()] if self.org_units_secondary: self.org_data['org_units_secondary'] = [{ 'name': i.name, 'acronym': i.name, 'unit_type': i.get_unit_type_display(), } for i in self.org_units_secondary.all()] try: self.update_photo_ad() except: # Don't bomb out of saving for update_photo_ad errors. pass if self.account_type in [5, 9]: # Shared/role-based account types. self.shared_account = True super(DepartmentUser, self).save(*args, **kwargs) def update_photo_ad(self): # Update self.photo_ad to a 240x240 thumbnail >10 kb in size. if not self.photo: if self.photo_ad: self.photo_ad.delete() return from PIL import Image from six import BytesIO from django.core.files.base import ContentFile if hasattr(self.photo.file, 'content_type'): PHOTO_TYPE = self.photo.file.content_type if PHOTO_TYPE == 'image/jpeg': PIL_TYPE = 'jpeg' elif PHOTO_TYPE == 'image/png': PIL_TYPE = 'png' else: return else: PIL_TYPE = 'jpeg' # good defaults to get ~10kb JPEG images PHOTO_AD_SIZE = (240, 240) PIL_QUALITY = 75 # remote file size limit PHOTO_AD_FILESIZE = 10000 image = Image.open(BytesIO(self.photo.read())) image.thumbnail(PHOTO_AD_SIZE, Image.LANCZOS) # in case we miss 10kb, drop the quality and recompress for i in range(12): temp_buffer = BytesIO() image.save(temp_buffer, PIL_TYPE, quality=PIL_QUALITY, optimize=True) length = temp_buffer.tell() if length <= PHOTO_AD_FILESIZE: break if PIL_TYPE == 'png': PIL_TYPE = 'jpeg' else: PIL_QUALITY -= 5 temp_buffer.seek(0) self.photo_ad.save(os.path.basename(self.photo.name), ContentFile(temp_buffer.read()), save=False) def org_data_pretty(self): if not self.org_data: return self.org_data return format_html(json2html.convert(json=self.org_data)) def ad_data_pretty(self): if not self.ad_data: return self.ad_data return format_html(json2html.convert(json=self.ad_data)) def alesco_data_pretty(self): if not self.alesco_data: return self.alesco_data # Manually generate HTML table output, to guarantee field order. t = '''<table border="1"> <tr><th>FIRST_NAME</th><td>{FIRST_NAME}</td></tr> <tr><th>SECOND_NAME</th><td>{SECOND_NAME}</td></tr> <tr><th>SURNAME</th><td>{SURNAME}</td></tr> <tr><th>EMPLOYEE_NO</th><td>{EMPLOYEE_NO}</td></tr> <tr><th>PAYPOINT</th><td>{PAYPOINT}</td></tr> <tr><th>PAYPOINT_DESC</th><td>{PAYPOINT_DESC}</td></tr> <tr><th>MANAGER_POS#</th><td>{MANAGER_POS#}</td></tr> <tr><th>MANAGER_NAME</th><td>{MANAGER_NAME}</td></tr> <tr><th>JOB_NO</th><td>{JOB_NO}</td></tr> <tr><th>FIRST_COMMENCE</th><td>{FIRST_COMMENCE}</td></tr> <tr><th>OCCUP_TERM_DATE</th><td>{OCCUP_TERM_DATE}</td></tr> <tr><th>POSITION_NO</th><td>{POSITION_NO}</td></tr> <tr><th>OCCUP_POS_TITLE</th><td>{OCCUP_POS_TITLE}</td></tr> <tr><th>LOC_DESC</th><td>{LOC_DESC}</td></tr> <tr><th>CLEVEL1_ID</th><td>{CLEVEL1_ID}</td></tr> <tr><th>CLEVEL2_DESC</th><td>{CLEVEL2_DESC}</td></tr> <tr><th>CLEVEL3_DESC</th><td>{CLEVEL3_DESC}</td></tr> <tr><th>EMP_STAT_DESC</th><td>{EMP_STAT_DESC}</td></tr> <tr><th>GEO_LOCATION_DESC</th><td>{GEO_LOCATION_DESC}</td></tr> </table>''' t = t.format(**self.alesco_data) return mark_safe(t) @property def password_age_days(self): if self.ad_data and 'pwdLastSet' in self.ad_data: try: td = datetime.now() - convert_ad_timestamp( self.ad_data['pwdLastSet']) return td.days except: pass return None
class Report(MapEntityMixin, PicturesMixin, TimeStampedModelMixin): """ User reports, mainly submitted via *Geotrek-rando*. """ email = models.EmailField(verbose_name=_("Email")) comment = models.TextField(blank=True, default="", verbose_name=_("Comment")) activity = models.ForeignKey('ReportActivity', on_delete=models.CASCADE, null=True, blank=True, verbose_name=_("Activity")) category = models.ForeignKey('ReportCategory', on_delete=models.CASCADE, null=True, blank=True, verbose_name=_("Category")) problem_magnitude = models.ForeignKey('ReportProblemMagnitude', null=True, blank=True, on_delete=models.CASCADE, verbose_name=_("Problem magnitude")) status = models.ForeignKey('ReportStatus', on_delete=models.CASCADE, null=True, blank=True, default=status_default, verbose_name=_("Status")) geom = models.PointField(null=True, blank=True, default=None, verbose_name=_("Location"), srid=settings.SRID) related_trek = models.ForeignKey(Trek, null=True, blank=True, on_delete=models.CASCADE, verbose_name=_('Related trek')) class Meta: verbose_name = _("Report") verbose_name_plural = _("Reports") ordering = ['-date_insert'] def __str__(self): if self.email: return self.email return "Anonymous report" @property def email_display(self): return '<a data-pk="%s" href="%s" title="%s" >%s</a>' % (self.pk, self.get_detail_url(), self, self) @property def full_url(self): try: return '{}{}'.format( settings.ALLOWED_HOSTS[0], self.get_detail_url() ) except KeyError: # Do not display url if there is no ALLOWED_HOSTS return "" @classmethod def get_create_label(cls): return _("Add a new feedback") @property def geom_wgs84(self): return self.geom.transform(4326, clone=True) @property def comment_text(self): return html.unescape(self.comment)
class AreaSoltura(models.Model): processo = models.IntegerField(null=True, blank=True) nome = models.CharField('Nome da propriedade', max_length=255, null=True, blank=True) endereco = models.CharField('Endereço', max_length=400, null=True, blank=True) uf = models.CharField('Unidade da Federação', max_length=2, null=True, blank=True) municipio = models.CharField('Município', max_length=255, null=True, blank=True) proprietario = models.CharField('Nome do proprietário', max_length=255, null=True, blank=True) cpf = models.CharField('CPF', null=True, blank=True, max_length=11) telefone = models.CharField(max_length=15, null=True, blank=True) email = models.EmailField(null=True, blank=True) area = models.FloatField('Área da Propriedade (ha)', null=True, blank=True) arl_app = models.FloatField('Área de reserva legal e proteção permanente', null=True, blank=True) bioma = models.CharField('Bioma', max_length=255, null=True, blank=True) fitofisionomia = models.CharField(max_length=255, null=True, blank=True) taxon = models.CharField(max_length=255, null=True, blank=True) conservacao = models.NullBooleanField() conectividade = models.NullBooleanField() uc = models.NullBooleanField() agua = models.NullBooleanField() atividade = models.CharField('Atividade Econômica', max_length=255, null=True, blank=True) documento = models.NullBooleanField() mapa = models.NullBooleanField() carta = models.NullBooleanField() reabilitador = models.NullBooleanField() viveiros = models.PositiveSmallIntegerField('Número de viveiros', null=True, blank=True) distancia = models.FloatField('Distância até o CETAS mais próximo', null=True, blank=True) tempo = models.CharField('Tempo de viagem ao CETAS mais próximo', max_length=5, null=True, blank=True) vistoria = models.DateField(null=True, blank=True) usuario = models.ForeignKey(User, related_name='area_soltura') data_criacao = models.DateTimeField('Data de Criação', auto_now_add=True) geom = models.PolygonField(srid=4674) objects = models.GeoManager() def __str__(self): return '%s' % self.processo class Meta: verbose_name = 'Área de Soltura de Animais Silvestres' verbose_name_plural = 'Áreas de Soltura de Animais Silvestres'
class AbstractObservation(models.Model): originates_in_vespawatch = models.BooleanField( default=True, help_text= "The observation was first created in VespaWatch, not iNaturalist") taxon = models.ForeignKey(Taxon, on_delete=models.PROTECT, blank=True, null=True) observation_time = models.DateTimeField(verbose_name=_("Observation date"), validators=[no_future]) comments = models.TextField( verbose_name=_("Comments"), blank=True, help_text= _("Comments are public: use them to describe your observation and help verification." )) latitude = models.FloatField( validators=[MinValueValidator(-90), MaxValueValidator(90)], verbose_name=_("Latitude")) longitude = models.FloatField( validators=[MinValueValidator(-180), MaxValueValidator(180)], verbose_name=_("Longitude")) inaturalist_id = models.BigIntegerField(verbose_name=_("iNaturalist ID"), blank=True, null=True) inaturalist_species = models.CharField( verbose_name=_("iNaturalist species"), max_length=100, blank=True, null=True) # TODO: check if this is still in use or useful inat_vv_confirmed = models.BooleanField( blank=True, null=True) # The community ID of iNaturalist says it's Vespa Velutina # Observer info observer_name = models.CharField(verbose_name=_("Name"), max_length=255, blank=True, null=True) observer_email = models.EmailField(verbose_name=_("Email address"), blank=True, null=True) observer_phone = models.CharField(verbose_name=_("Telephone number"), max_length=20, blank=True, null=True) created_at = models.DateTimeField(default=timezone.now) # Managers objects = models.Manager() # The default manager. from_inat_objects = InatCreatedObservationsManager() from_vespawatch_objects = VespawatchCreatedObservationsManager() new_vespawatch_objects = VespawatchNewlyCreatedObservationsManager() class Meta: abstract = True # We got some duplicates and don't exactly know why, this is an attempt to block them without being too # aggresive and introduce bugs (hence the limited number of fields). unique_together = [ 'taxon', 'observation_time', 'latitude', 'longitude', 'comments', 'inaturalist_id' ] @property def vernacular_names_in_all_languages(self): """Returns a dict such as: {'en': XXXX, 'nl': YYYY}""" vn = {} for lang in settings.LANGUAGES: code = lang[0] vn[code] = getattr(self.taxon, f'vernacular_name_{code}') return vn @property def display_vernacular_name(self): if self.taxon: return _(self.taxon.vernacular_name) else: return '' @property def display_scientific_name(self): if self.taxon: return self.taxon.name else: return self.inaturalist_species or _('Unknown') @property def can_be_edited_in_admin(self): if self.originates_in_vespawatch: if self.exists_in_inaturalist: return False else: return True else: # Comes from iNaturalist: we can never delete return False @property def can_be_edited_or_deleted(self): """Return True if this observation can be edited in Vespa-Watch (admin, ...)""" return self.originates_in_vespawatch # We can't edit obs that comes from iNaturalist (they're never pushed). @property def taxon_can_be_locally_changed(self): if self.originates_in_vespawatch and self.exists_in_inaturalist: return False # Because we rely on community: info is always pulled and never pushed return True @property def exists_in_inaturalist(self): return self.inaturalist_id is not None @property def inaturalist_obs_url(self): if self.exists_in_inaturalist: return f'https://www.inaturalist.org/observations/{self.inaturalist_id}' return None def has_warnings(self): return len(self.warnings.all()) > 0 has_warnings.boolean = True def _params_for_inat(self): """(Create/update): Common ground for the pushed data to iNaturalist. taxon_id is not part of it because we rely on iNaturalist to correct the identification, if necessary. All the rest is pushed. """ vespawatch_evidence_value = 'nest' if self.__class__ == Nest else 'individual' ofv = [{ 'observation_field_id': settings.VESPAWATCH_ID_OBS_FIELD_ID, 'value': self.pk }, { 'observation_field_id': settings.VESPAWATCH_EVIDENCE_OBS_FIELD_ID, 'value': vespawatch_evidence_value }] if vespawatch_evidence_value == 'individual' and self.behaviour: ofv.append( { 'observation_field_id': settings.VESPAWATCH_BEHAVIOUR_OBS_FIELD_ID, 'value': self.get_behaviour_display() } ) # TODO: get_behaviour_display(): what will happen to push if we translate the values for the UI return { 'observed_on_string': self.observation_time.isoformat(), 'time_zone': 'Brussels', 'description': self.comments, 'latitude': self.latitude, 'longitude': self.longitude, 'observation_field_values_attributes': [{ 'observation_field_id': settings.VESPAWATCH_ID_OBS_FIELD_ID, 'value': self.pk }, { 'observation_field_id': settings.VESPAWATCH_EVIDENCE_OBS_FIELD_ID, 'value': vespawatch_evidence_value }] } def flag_warning(self, text): if text in [x.text for x in self.warnings.all()]: return # warning already set if self.__class__.__name__ == 'Nest': warning = NestObservationWarning(text=text, datetime=now(), observation=self) warning.save() elif self.__class__.__name__ == 'Individual': warning = IndividualObservationWarning(text=text, datetime=now(), observation=self) warning.save() def flag_based_on_inat_data(self, inat_observation_data): """ The observation was no longer found on iNaturalist with our general filters. Check why, and flag this observation """ # Project is vespawatch? if not settings.VESPAWATCH_PROJECT_ID in inat_observation_data[ 'project_ids']: self.flag_warning('not in vespawatch project') # Taxon known in VW? returned_taxon_id = '' if 'community_taxon_id' in inat_observation_data and inat_observation_data[ 'community_taxon_id']: returned_taxon_id = inat_observation_data['community_taxon_id'] elif 'taxon' in inat_observation_data: if 'id' in inat_observation_data['taxon']: returned_taxon_id = inat_observation_data['taxon']['id'] if returned_taxon_id not in [ y for x in Taxon.objects.all() for y in x.inaturalist_pull_taxon_ids ]: self.flag_warning('unknown taxon') def update_from_inat_data(self, inat_observation_data): # Check the vespawatch_evidence # ------ # If the observation is a nest but the vespawatch evidence is not nest => flag the nest if 'ofvs' in inat_observation_data: vw_evidence_list = [ x['value'] for x in inat_observation_data['ofvs'] if x['field_id'] == settings.VESPAWATCH_EVIDENCE_OBS_FIELD_ID ] if len(vw_evidence_list) > 0: vw_evidence = vw_evidence_list[0] if self.__class__.__name__ == 'Nest': if vw_evidence != 'nest': self.flag_warning('individual at inaturalist') # If the observation is an individual but the vespawatch evidence is a nest and the observation originates in vespawatch => delete the individual and create a nest elif self.__class__.__name__ == 'Individual': if vw_evidence == 'nest': if self.originates_in_vespawatch: self.flag_warning('nest at inaturalist') else: create_observation_from_inat_data( inat_observation_data) self.delete() return # Update taxon data and set inat_vv_confirmed (use inat_data_confirms_vv() ) self.inat_vv_confirmed = inat_data_confirms_vv(inat_observation_data) # Update photos # ------------- # When we pull again and the API returns additional images, those are not added. This is done # because we insert a UUID in the filename when we pull it. The result of that is that we cannot # compare that image with the image url that we retrieve from iNaturalist. So to prevent adding # the same image again and again with subsequent pulls, we only add images when the observation # has none. if len(self.pictures.all()) == 0: for photo in inat_observation_data['photos']: self.assign_picture_from_url(photo['url']) # Update location self.latitude = inat_observation_data['geojson']['coordinates'][1] self.longitude = inat_observation_data['geojson']['coordinates'][0] # Update time # ------------- observation_time = dateparser.parse( inat_observation_data['observed_on_string'], settings={'TIMEZONE': inat_observation_data['observed_time_zone']}) if observation_time is None: # Sometimes, dateparser doesn't understand the string but we have the bits and pieces in # inaturalist_data['observed_on_details'] details = inat_observation_data['observed_on_details'] observation_time = datetime( year=details['year'], month=details['month'], day=details['day'], hour=details['hour'] ) # in the observed cases, we had nothing more precise than the hour # Sometimes, the time is naive (even when specifying it to dateparser), because (for the detected cases, at least) # The time is 00:00:00. In that case we make it aware to avoid Django warnings (in the local time zone since all # observations occur in Belgium if is_naive(observation_time): # Some dates (apparently) observation_time = make_aware(observation_time) self.observation_time = observation_time self.comments = inat_observation_data['description'] or '' # Update taxon # ------------- try: self.inaturalist_species = '' taxon = get_taxon_from_inat_taxon_id( inat_observation_data['taxon']['id']) self.taxon = taxon except Taxon.DoesNotExist: self.taxon = None self.inaturalist_species = inat_observation_data['taxon'][ 'name'] if 'name' in inat_observation_data['taxon'] else '' self.save() def create_at_inaturalist(self, access_token, user_agent): """Creates a new observation at iNaturalist for this observation It will update the current object so self.inaturalist_id is properly set. On the other side, it will also set the vespawatch_id observation field so the observation can be found from the iNaturalist record. :param access_token: as returned by pyinaturalist.rest_api.get_access_token( """ params_only_for_create = { 'taxon_id': self.taxon.inaturalist_push_taxon_id } # TODO: with the new sync, does it still makes sense to separate the create/update parameters? params = { 'observation': { **params_only_for_create, **self._params_for_inat() } } r = create_observations(params=params, access_token=access_token, user_agent=user_agent) self.inaturalist_id = r[0]['id'] self.save() self.push_attached_pictures_at_inaturalist(access_token=access_token, user_agent=user_agent) def get_photo_filename(self, photo_url): # TODO: Find a cleaner solution to this # It seems the iNaturalist only returns small thumbnails such as # 'https://static.inaturalist.org/photos/1960816/square.jpg?1444437211' # We can circumvent the issue by hacking the URL... photo_url = photo_url.replace('square.jpg', 'large.jpg') photo_url = photo_url.replace('square.jpeg', 'large.jpeg') photo_filename = photo_url[photo_url.rfind("/") + 1:].split('?', 1)[0] return photo_filename def assign_picture_from_url(self, photo_url): photo_filename = self.get_photo_filename(photo_url) if photo_filename not in [x.image.name for x in self.pictures.all()]: if self.__class__ == Nest: photo_obj = NestPicture() else: photo_obj = IndividualPicture() photo_content = ContentFile(requests.get(photo_url).content) photo_obj.observation = self photo_obj.image.save(photo_filename, photo_content) photo_obj.save() def push_attached_pictures_at_inaturalist(self, access_token, user_agent): if self.inaturalist_id: for picture in self.pictures.all(): add_photo_to_observation(observation_id=self.inaturalist_id, file_object=picture.image.read(), access_token=access_token, user_agent=user_agent) def get_taxon_name(self): if self.taxon: return self.taxon.name else: return '' @property def formatted_observation_date(self): # We need to be aware of the timezone, hence the defaultfilter trick return defaultfilters.date(self.observation_time, 'Y-m-d') @property def observation_time_iso(self): return self.observation_time.isoformat() def save(self, *args, **kwargs): # Let's make sure model.clean() is called on each save(), for validation self.full_clean() return super(AbstractObservation, self).save(*args, **kwargs) def delete(self, *args, **kwargs): if self.originates_in_vespawatch and self.exists_in_inaturalist: InatObsToDelete.objects.create(inaturalist_id=self.inaturalist_id) return super(AbstractObservation, self).delete(*args, **kwargs)
class Event(MPTTModel, BaseModel, SchemalessFieldMixin, ReplacedByMixin): jsonld_type = "Event/LinkedEvent" objects = BaseTreeQuerySet.as_manager() """ 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' UMBRELLA = 'umbrella' SUPER_EVENT_TYPES = ( (SuperEventType.RECURRING, _('Recurring')), (SuperEventType.UMBRELLA, _('Umbrella event')), ) # 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) provider_contact_info = models.CharField( verbose_name=_("Provider's contact info"), max_length=255, null=True, blank=True) publisher = models.ForeignKey('django_orghierarchy.Organization', verbose_name=_('Publisher'), db_index=True, on_delete=models.PROTECT, related_name='published_events', null=True) # 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) audience_min_age = models.SmallIntegerField( verbose_name=_('Minimum recommended age'), blank=True, null=True, db_index=True) audience_max_age = models.SmallIntegerField( verbose_name=_('Maximum recommended age'), blank=True, null=True, db_index=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, db_index=True, default=None, choices=SUPER_EVENT_TYPES) in_language = models.ManyToManyField(Language, verbose_name=_('In language'), related_name='events', blank=True) images = models.ManyToManyField(Image, related_name='events', blank=True) deleted = models.BooleanField(default=False, db_index=True) replaced_by = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='aliases', null=True, blank=True) # Custom fields not from schema.org keywords = models.ManyToManyField(Keyword, related_name='events') audience = models.ManyToManyField(Keyword, related_name='audience_events', blank=True) # Tavastia Events pin = models.CharField(blank=False, max_length=64, default='0000') accessible = models.BooleanField(default=False, null=False, blank=False) provider_email = models.EmailField(null=True, blank=True, default='*****@*****.**') multi_day = models.BooleanField(default=False, null=False) class Meta: verbose_name = _('event') verbose_name_plural = _('events') class MPTTMeta: parent_attr = 'super_event' def save(self, *args, **kwargs): if self._has_circular_replacement(): raise Exception( "Trying to replace this event with an event that is replaced by this event" "Please refrain from creating circular replacements and" "remove one of the replacements.") if self.replaced_by and not self.deleted: self.deleted = True logger.warning( "Event replaced without soft deleting. Soft deleting automatically", extra={'event': self}) # needed to cache location event numbers old_location = None # needed for notifications old_publication_status = None old_deleted = None created = True if self.id: try: event = Event.objects.get(id=self.id) created = False old_location = event.location old_publication_status = event.publication_status old_deleted = event.deleted 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.' ) }) if (self.keywords.filter(deprecated=True) or self.audience.filter(deprecated=True)) and (not self.deleted): raise ValidationError({ 'keywords': _("Trying to save event with deprecated keywords " + str(self.keywords.filter(deprecated=True).values('id')) + " or " + str(self.audience.filter(deprecated=True).values('id')) + ". Please use up-to-date keywords.") }) 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) call_command('update_n_events') # send notifications if old_publication_status == PublicationStatus.DRAFT and self.publication_status == PublicationStatus.PUBLIC: self.send_published_notification() if old_deleted is False and self.deleted is True: self.send_deleted_notification() if created and self.publication_status == PublicationStatus.DRAFT: self.send_draft_posted_notification() def __str__(self): name = '' languages = [lang[0] for lang in settings.LANGUAGES] for lang in languages: lang = lang.replace( '-', '_') # to handle complex codes like e.g. zh-hans s = getattr(self, 'name_%s' % lang, 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) def _send_notification(self, notification_type, recipient_list, request=None): if len(recipient_list) == 0: logger.warning("No recipients for notification type '%s'" % notification_type, extra={'event': self}) return context = {'event': self} try: rendered_notification = render_notification_template( notification_type, context) except NotificationTemplateException as e: logger.error(e, exc_info=True, extra={'request': request}) return try: send_mail(rendered_notification['subject'], rendered_notification['body'], 'noreply@%s' % Site.objects.get_current().domain, recipient_list, html_message=rendered_notification['html_body']) except SMTPException as e: logger.error(e, exc_info=True, extra={ 'request': request, 'event': self }) def _get_author_emails(self): author_emails = [] for user in (self.created_by, self.last_modified_by): if user and user.email: author_emails.append(user.email) return author_emails def send_deleted_notification(self, request=None): recipient_list = self._get_author_emails() self._send_notification(NotificationType.UNPUBLISHED_EVENT_DELETED, recipient_list, request) def send_published_notification(self, request=None): recipient_list = self._get_author_emails() self._send_notification(NotificationType.EVENT_PUBLISHED, recipient_list, request) def send_draft_posted_notification(self, request=None): recipient_list = [] for admin in self.publisher.admin_users.all(): if admin.email: recipient_list.append(admin.email) self._send_notification(NotificationType.DRAFT_POSTED, recipient_list, request) # Tavastia Events # Filter soft deleted events from events sub events def filter_deleted(self): return self.sub_events.filter(deleted=False)
class Reservation(ModifiableModel): CREATED = 'created' CANCELLED = 'cancelled' CONFIRMED = 'confirmed' DENIED = 'denied' REQUESTED = 'requested' WAITING_FOR_PAYMENT = 'waiting_for_payment' STATE_CHOICES = ( (CREATED, _('created')), (CANCELLED, _('cancelled')), (CONFIRMED, _('confirmed')), (DENIED, _('denied')), (REQUESTED, _('requested')), (WAITING_FOR_PAYMENT, _('waiting for payment')), ) TYPE_NORMAL = 'normal' TYPE_BLOCKED = 'blocked' TYPE_CHOICES = ( (TYPE_NORMAL, _('Normal reservation')), (TYPE_BLOCKED, _('Resource blocked')), ) resource = models.ForeignKey('Resource', verbose_name=_('Resource'), db_index=True, related_name='reservations', on_delete=models.PROTECT) begin = models.DateTimeField(verbose_name=_('Begin time')) end = models.DateTimeField(verbose_name=_('End time')) duration = pgfields.DateTimeRangeField( verbose_name=_('Length of reservation'), null=True, blank=True, db_index=True) comments = models.TextField(null=True, blank=True, verbose_name=_('Comments')) user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('User'), null=True, blank=True, db_index=True, on_delete=models.PROTECT) preferred_language = models.CharField(choices=settings.LANGUAGES, verbose_name='Preferred Language', null=True, default=settings.LANGUAGES[0][0], max_length=8) state = models.CharField(max_length=32, choices=STATE_CHOICES, verbose_name=_('State'), default=CREATED) approver = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('Approver'), related_name='approved_reservations', null=True, blank=True, on_delete=models.SET_NULL) staff_event = models.BooleanField(verbose_name=_('Is staff event'), default=False) type = models.CharField(blank=False, verbose_name=_('Type'), max_length=32, choices=TYPE_CHOICES, default=TYPE_NORMAL) has_arrived = models.BooleanField(verbose_name=_('Has arrived'), default=False) # access-related fields access_code = models.CharField(verbose_name=_('Access code'), max_length=32, null=True, blank=True) # EXTRA FIELDS START HERE event_subject = models.CharField(max_length=200, verbose_name=_('Event subject'), blank=True) event_description = models.TextField(verbose_name=_('Event description'), blank=True) number_of_participants = models.PositiveSmallIntegerField( verbose_name=_('Number of participants'), blank=True, null=True, default=1) participants = models.TextField(verbose_name=_('Participants'), blank=True) host_name = models.CharField(verbose_name=_('Host name'), max_length=100, blank=True) require_assistance = models.BooleanField( verbose_name=_('Require assistance'), default=False) require_workstation = models.BooleanField( verbose_name=_('Require workstation'), default=False) home_municipality = models.ForeignKey('ReservationHomeMunicipalityField', verbose_name=_('Home municipality'), null=True, blank=True, on_delete=models.SET_NULL) # extra detail fields for manually confirmed reservations reserver_name = models.CharField(verbose_name=_('Reserver name'), max_length=100, blank=True) reserver_id = models.CharField( verbose_name=_('Reserver ID (business or person)'), max_length=30, blank=True) reserver_email_address = models.EmailField( verbose_name=_('Reserver email address'), blank=True) reserver_phone_number = models.CharField( verbose_name=_('Reserver phone number'), max_length=30, blank=True) reserver_address_street = models.CharField( verbose_name=_('Reserver address street'), max_length=100, blank=True) reserver_address_zip = models.CharField( verbose_name=_('Reserver address zip'), max_length=30, blank=True) reserver_address_city = models.CharField( verbose_name=_('Reserver address city'), max_length=100, blank=True) reservation_extra_questions = models.TextField( verbose_name=_('Reservation extra questions'), blank=True) company = models.CharField(verbose_name=_('Company'), max_length=100, blank=True) billing_first_name = models.CharField(verbose_name=_('Billing first name'), max_length=100, blank=True) billing_last_name = models.CharField(verbose_name=_('Billing last name'), max_length=100, blank=True) billing_email_address = models.EmailField( verbose_name=_('Billing email address'), blank=True) billing_phone_number = models.CharField( verbose_name=_('Billing phone number'), max_length=30, blank=True) billing_address_street = models.CharField( verbose_name=_('Billing address street'), max_length=100, blank=True) billing_address_zip = models.CharField( verbose_name=_('Billing address zip'), max_length=30, blank=True) billing_address_city = models.CharField( verbose_name=_('Billing address city'), max_length=100, blank=True) # If the reservation was imported from another system, you can store the original ID in the field below. origin_id = models.CharField(verbose_name=_('Original ID'), max_length=50, editable=False, null=True) reminder = models.ForeignKey('ReservationReminder', verbose_name=_('Reservation Reminder'), db_index=True, related_name='ReservationReminders', on_delete=models.SET_NULL, null=True, blank=True) timmi_id = models.PositiveIntegerField(verbose_name=_('Timmi ID'), null=True, blank=True) timmi_receipt = models.TextField(verbose_name=_('Timmi receipt'), null=True, blank=True, max_length=2000) objects = ReservationQuerySet.as_manager() class Meta: verbose_name = _("reservation") verbose_name_plural = _("reservations") ordering = ('id', ) def _save_dt(self, attr, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ save_dt(self, attr, dt, self.resource.unit.time_zone) def _get_dt(self, attr, tz): return get_dt(self, attr, tz) @property def begin_tz(self): return self.begin @begin_tz.setter def begin_tz(self, dt): self._save_dt('begin', dt) def get_begin_tz(self, tz): return self._get_dt("begin", tz) @property def end_tz(self): return self.end @end_tz.setter def end_tz(self, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ self._save_dt('end', dt) def get_end_tz(self, tz): return self._get_dt("end", tz) def is_active(self): print( self.end + self.resource.cooldown >= timezone.now() and self.state not in (Reservation.CANCELLED, Reservation.DENIED)) return self.end + self.resource.cooldown >= timezone.now( ) and self.state not in (Reservation.CANCELLED, Reservation.DENIED) def is_own(self, user): if not (user and user.is_authenticated): return False return user == self.user def need_manual_confirmation(self): return self.resource.need_manual_confirmation def are_extra_fields_visible(self, user): # the following logic is used also implemented in ReservationQuerySet # so if this is changed that probably needs to be changed as well if self.is_own(user): return True return self.resource.can_view_reservation_extra_fields(user) def can_view_access_code(self, user): if self.is_own(user): return True return self.resource.can_view_reservation_access_code(user) def set_state(self, new_state, user): # Make sure it is a known state assert new_state in (Reservation.REQUESTED, Reservation.CONFIRMED, Reservation.DENIED, Reservation.CANCELLED, Reservation.WAITING_FOR_PAYMENT) old_state = self.state if new_state == old_state: if old_state == Reservation.CONFIRMED: reservation_modified.send(sender=self.__class__, instance=self, user=user) return if new_state == Reservation.CONFIRMED: self.approver = user if user and user.is_authenticated else None if user and user.is_authenticated or self.resource.authentication == 'unauthenticated': reservation_confirmed.send(sender=self.__class__, instance=self, user=user) elif old_state == Reservation.CONFIRMED: self.approver = None user_is_staff = self.user is not None and self.user.is_staff # Notifications if new_state == Reservation.REQUESTED: if not user_is_staff: self.send_reservation_requested_mail() self.notify_staff_about_reservation( NotificationType.RESERVATION_REQUESTED_OFFICIAL) else: if self.reserver_email_address != self.user.email: self.send_reservation_requested_mail( action_by_official=True) elif new_state == Reservation.CONFIRMED: if self.need_manual_confirmation(): self.send_reservation_confirmed_mail() elif self.access_code: if not user_is_staff: self.send_reservation_created_with_access_code_mail() self.notify_staff_about_reservation( NotificationType. RESERVATION_CREATED_WITH_ACCESS_CODE_OFFICIAL) else: if self.reserver_email_address != self.user.email: self.send_reservation_created_with_access_code_mail( action_by_official=True) else: if not user_is_staff: self.send_reservation_created_mail() self.notify_staff_about_reservation( NotificationType.RESERVATION_CREATED_OFFICIAL) else: if self.reserver_email_address != self.user.email: self.send_reservation_created_mail( action_by_official=True) self.notify_staff_about_reservation( NotificationType.RESERVATION_CREATED_OFFICIAL) elif new_state == Reservation.DENIED: self.send_reservation_denied_mail() elif new_state == Reservation.CANCELLED: if self.user: order = self.get_order() if order: if order.state == order.CANCELLED: self.send_reservation_cancelled_mail() else: if user.is_staff and (user.email != self.user.email ): # Assume staff cancelled it self.send_reservation_cancelled_mail( action_by_official=True) else: self.send_reservation_cancelled_mail() self.notify_staff_about_reservation( NotificationType.RESERVATION_CANCELLED_OFFICIAL) reservation_cancelled.send(sender=self.__class__, instance=self, user=user) self.state = new_state self.save() def can_modify(self, user): if not user: return False if self.state == Reservation.WAITING_FOR_PAYMENT: return False if self.get_order(): return self.resource.can_modify_paid_reservations(user) # reservations that need manual confirmation and are confirmed cannot be # modified or cancelled without reservation approve permission cannot_approve = not self.resource.can_approve_reservations(user) if self.need_manual_confirmation( ) and self.state == Reservation.CONFIRMED and cannot_approve: return False return self.user == user or self.resource.can_modify_reservations(user) def can_add_comment(self, user): if self.is_own(user): return True return self.resource.can_access_reservation_comments(user) def can_view_field(self, user, field): if field not in RESERVATION_EXTRA_FIELDS: return True if self.is_own(user): return True return self.resource.can_view_reservation_extra_fields(user) def can_view_catering_orders(self, user): if self.is_own(user): return True return self.resource.can_view_reservation_catering_orders(user) def can_add_product_order(self, user): return self.is_own(user) def can_view_product_orders(self, user): if self.is_own(user): return True return self.resource.can_view_reservation_product_orders(user) def get_order(self): return getattr(self, 'order', None) def format_time(self): tz = self.resource.unit.get_tz() begin = self.begin.astimezone(tz) end = self.end.astimezone(tz) return format_dt_range(translation.get_language(), begin, end) def create_reminder(self): r_date = self.begin - datetime.timedelta( hours=int(self.resource.unit.sms_reminder_delay)) reminder = ReservationReminder() reminder.reservation = self reminder.reminder_date = r_date reminder.save() self.reminder = reminder def modify_reminder(self): if not self.reminder: return r_date = self.begin - datetime.timedelta( hours=int(self.resource.unit.sms_reminder_delay)) self.reminder.reminder_date = r_date self.reminder.save() def __str__(self): if self.state != Reservation.CONFIRMED: state_str = ' (%s)' % self.state else: state_str = '' return "%s: %s%s" % (self.format_time(), self.resource, state_str) def clean(self, **kwargs): """ Check restrictions that are common to all reservations. If this reservation isn't yet saved and it will modify an existing reservation, the original reservation need to be provided in kwargs as 'original_reservation', so that it can be excluded when checking if the resource is available. """ if 'user' in kwargs: user = kwargs['user'] else: user = self.user user_is_admin = user and self.resource.is_admin(user) if self.end <= self.begin: raise ValidationError( _("You must end the reservation after it has begun")) # Check that begin and end times are on valid time slots. opening_hours = self.resource.get_opening_hours( self.begin.date(), self.end.date()) for dt in (self.begin, self.end): days = opening_hours.get(dt.date(), []) day = next((day for day in days if day['opens'] is not None and day['opens'] <= dt <= day['closes']), None) if day and not is_valid_time_slot(dt, self.resource.slot_size, day['opens']): raise ValidationError( _("Begin and end time must match time slots"), code='invalid_time_slot') # Check if Unit has disallow_overlapping_reservations value of True if (self.resource.unit.disallow_overlapping_reservations and not self.resource.can_create_overlapping_reservations(user)): if self.resource.unit.disallow_overlapping_reservations_per_user: reservations_for_same_unit = Reservation.objects.filter( user=user, resource__unit=self.resource.unit) else: reservations_for_same_unit = Reservation.objects.filter( resource__unit=self.resource.unit) valid_reservations_for_same_unit = reservations_for_same_unit.exclude( state=Reservation.CANCELLED) user_has_conflicting_reservations = valid_reservations_for_same_unit.filter( Q(begin__gt=self.begin, begin__lt=self.end) | Q(begin__lt=self.begin, end__gt=self.begin) | Q(begin__gte=self.begin, end__lte=self.end)) if user_has_conflicting_reservations: raise ValidationError(_( 'This unit does not allow overlapping reservations for its resources' ), code='conflicting_reservation') original_reservation = self if self.pk else kwargs.get( 'original_reservation', None) if self.resource.check_reservation_collision(self.begin, self.end, original_reservation): raise ValidationError( _("The resource is already reserved for some of the period")) if not user_is_admin: if (self.end - self.begin) < self.resource.min_period: raise ValidationError( _("The minimum reservation length is %(min_period)s") % { 'min_period': humanize_duration( self.resource.min_period) }) else: if not (self.end - self.begin ) % self.resource.slot_size == datetime.timedelta(0): raise ValidationError( _("The minimum reservation length is %(slot_size)s") % {'slot_size': humanize_duration(self.resource.slot_size)}) if self.access_code: validate_access_code(self.access_code, self.resource.access_code_type) if self.resource.people_capacity: if (self.number_of_participants > self.resource.people_capacity): raise ValidationError( _("This resource has people capacity limit of %s" % self.resource.people_capacity)) def get_notification_context(self, language_code, user=None, notification_type=None, extra_context={}): if not user: user = self.user with translation.override(language_code): reserver_home_municipality = self.home_municipality_id for municipality in self.resource.get_included_home_municipality_names( ): if municipality['id'] == self.home_municipality_id: reserver_home_municipality = municipality['name'].get( language_code, None) break reserver_name = self.reserver_name reserver_email_address = self.reserver_email_address if not reserver_name and self.user and self.user.get_display_name( ): reserver_name = self.user.get_display_name() if not reserver_email_address and user and user.email: reserver_email_address = user.email context = { 'resource': self.resource.name, 'begin': localize_datetime(self.begin), 'end': localize_datetime(self.end), 'begin_dt': self.begin, 'end_dt': self.end, 'time_range': self.format_time(), 'reserver_name': reserver_name, 'reserver_email_address': reserver_email_address, 'require_assistance': self.require_assistance, 'require_workstation': self.require_workstation, 'extra_question': self.reservation_extra_questions, 'home_municipality_id': reserver_home_municipality } directly_included_fields = ('number_of_participants', 'host_name', 'event_subject', 'event_description', 'reserver_phone_number', 'billing_first_name', 'billing_last_name', 'billing_email_address', 'billing_phone_number', 'billing_address_street', 'billing_address_zip', 'billing_address_city') for field in directly_included_fields: context[field] = getattr(self, field) if self.resource.unit: context['unit'] = self.resource.unit.name context[ 'unit_address'] = self.resource.unit.address_postal_full context['unit_id'] = self.resource.unit.id context[ 'unit_map_service_id'] = self.resource.unit.map_service_id if self.can_view_access_code(user) and self.access_code: context['access_code'] = self.access_code if self.user and self.user.is_staff: context['staff_name'] = self.user.get_display_name() if notification_type in [ NotificationType.RESERVATION_CONFIRMED, NotificationType.RESERVATION_CREATED ]: if self.resource.reservation_confirmed_notification_extra: context[ 'extra_content'] = self.resource.reservation_confirmed_notification_extra elif notification_type == NotificationType.RESERVATION_REQUESTED: if self.resource.reservation_requested_notification_extra: context[ 'extra_content'] = self.resource.reservation_requested_notification_extra # Get last main and ground plan images. Normally there shouldn't be more than one of each # of those images. images = self.resource.images.filter( type__in=('main', 'ground_plan')).order_by('-sort_order') main_image = next((i for i in images if i.type == 'main'), None) ground_plan_image = next( (i for i in images if i.type == 'ground_plan'), None) if main_image: main_image_url = main_image.get_full_url() if main_image_url: context['resource_main_image_url'] = main_image_url if ground_plan_image: ground_plan_image_url = ground_plan_image.get_full_url() if ground_plan_image_url: context[ 'resource_ground_plan_image_url'] = ground_plan_image_url order = getattr(self, 'order', None) if order: context['order'] = order.get_notification_context( language_code) all_products = [] # Iterate through each order/product in order_lines. # Each order/product is appended to a list that is then set as the value of context['order']. for item in context["order"]["order_lines"]: product = {} product_fields = ('id', 'created_at', 'reservation_name', 'name', 'quantity', 'price', 'unit_price', 'unit_price_num', 'tax_percentage', 'price_type', 'price_period', 'order_number', 'decimal_hours', 'pretax_price', 'pretax_price_num', 'tax_price', 'tax_price_num') ''' product_values These keys are used in the email template to display order/payment information. id - id of this order created_at - creation date of the parent order reservation_name - name of resource name - name of this product quantity - quantity of products, total price of product / single unit price of product price - single unit price of this product unit_price - total price of this product, string e.g. 75,00 unit_price_num - total price of this product, float e.g. 75.00 tax_percentage - tax percentage of this product price_type - price type of product, per period / fixed price_period - price period of product if type=per period, e.g. 00:30:00 for 30min order_number - id of parent order pretax_price - price amount without tax, string e.g. 6,05 if total price is 7,5 with 24% vat pretax_price_num - price amount without tax, float e.g. 6.05 tax_price - tax amount, string e.g. 1,45 if total price is 7,5 with 24% vat tax_price_num - tax amount, float e.g. 1.45 ''' product_values = { 'id': item["product"]["id"], 'created_at': self.created_at.astimezone( self.resource.unit.get_tz()).strftime( '%d.%m.%Y %H:%M:%S'), 'reservation_name': context["resource"], 'name': item["product"]["name"], 'quantity': float(item["unit_price"].replace(',', '.')) / float(item["product"]["price"].replace(',', '.')), 'price': item["product"]["price"], 'unit_price': item["unit_price"], 'unit_price_num': float(item["unit_price"].replace(',', '.')), 'tax_percentage': item["product"]["tax_percentage"], 'price_type': item["product"]["price_type"], 'price_period': item["product"]["price_period"], 'order_number': context["order"]["id"], 'pretax_price': item["product"]["pretax_price"], 'pretax_price_num': float(item["product"]["pretax_price"].replace( ',', '.')), 'tax_price': item["product"]["tax_price"], 'tax_price_num': float(item["product"]["tax_price"].replace(',', '.')) } for field in product_fields: if field == 'decimal_hours': # price_period is None if price_type is 'fixed' if item["product"]["price_period"] is not None: # list of integers based on price_period string values, e.g. string '01:30:00' --> list [1,30,0] price_unit_time = [ int(x) for x in item["product"] ["price_period"].split(':') ] # calculate decimal time from list integers e.g. based on previous values, ((1*60) + 30) / 60 = 1.5 decimal_hours = ((price_unit_time[0] * 60) + price_unit_time[1]) / 60 product[field] = decimal_hours else: # price_type is 'fixed' product[field] = 1 else: product[field] = product_values[field] all_products.append(product) context['order_details'] = all_products if extra_context: context.update({'bulk_email_context': {**extra_context}}) return context def send_reservation_mail(self, notification_type, user=None, attachments=None, action_by_official=False, staff_email=None, extra_context={}, is_reminder=False): # Check if resource's unit has a template group and if that group contains a notification template with correct notification type. if self.resource.unit.notification_template_group_id: try: # Check if template group contains a notification template with correct notification type. unit_template_group = NotificationTemplateGroup.objects.get( id=self.resource.unit.notification_template_group_id) notification_template = unit_template_group.templates.get( type=notification_type) except (NotificationTemplateGroup.DoesNotExist, NotificationTemplate.DoesNotExist): # Otherwise search all notification templates for the default template of this type. try: notification_template = NotificationTemplate.objects.get( type=notification_type, groups=None, is_default_template=True) except NotificationTemplate.DoesNotExist: return except NotificationTemplate.MultipleObjectsReturned: # Multiple templates found with same type, using default template instead. logger.error( f"Template group: {unit_template_group.name} contains multiple templates of type: {notification_type}." ) try: notification_template = NotificationTemplate.objects.get( type=notification_type, groups=None, is_default_template=True) except NotificationTemplate.DoesNotExist: return # Otherwise search all notification templates for the default template of this type. else: try: notification_template = NotificationTemplate.objects.get( type=notification_type, groups=None, is_default_template=True) except NotificationTemplate.DoesNotExist: return if self.resource.unit.sms_reminder: # only allow certain notification types as reminders e.g. exclude reservation_access_code_created allowed_reminder_notification_types = ( NotificationType.RESERVATION_CONFIRMED, NotificationType.RESERVATION_CREATED, NotificationType.RESERVATION_CREATED_BY_OFFICIAL, NotificationType.RESERVATION_CREATED_WITH_ACCESS_CODE, NotificationType. RESERVATION_CREATED_WITH_ACCESS_CODE_BY_OFFICIAL, ) if self.reminder and notification_type in allowed_reminder_notification_types: self.reminder.notification_type = self.reminder.notification_type if self.reminder.notification_type else notification_type self.reminder.user = self.reminder.user if self.reminder.user else user self.reminder.action_by_official = self.reminder.action_by_official if self.reminder.action_by_official else action_by_official self.reminder.save() """ Stuff common to all reservation related mails. If user isn't given use self.user. """ if getattr(self, 'order', None) and self.billing_email_address: email_address = self.billing_email_address elif user: email_address = user.email else: if not (self.reserver_email_address or self.user): return if action_by_official: email_address = self.reserver_email_address else: email_address = self.reserver_email_address or self.user.email user = self.user language = DEFAULT_LANG # use reservation's preferred_language if it exists if getattr(self, 'preferred_language', None): language = self.preferred_language # if user is defined and user.is_staff, use default lang if user and user.is_staff: language = DEFAULT_LANG context = self.get_notification_context( language, notification_type=notification_type, extra_context=extra_context) try: if staff_email: language = DEFAULT_LANG rendered_notification = notification_template.render( context, language) except NotificationTemplateException as e: print('NotifcationTemplateException: %s' % e) logger.error(e, exc_info=True, extra={'user': user.uuid}) return if is_reminder: print("Sending SMS notification :: (%s) %s || LOCALE: %s" % (self.reserver_phone_number, rendered_notification['subject'], language)) ret = send_respa_mail( email_address='%s@%s' % (self.reserver_phone_number, settings.GSM_NOTIFICATION_ADDRESS), subject=rendered_notification['subject'], body=rendered_notification['short_message'], ) print(ret[1]) return if staff_email: print("Sending automated mail :: (%s) %s || LOCALE: %s" % (staff_email, rendered_notification['subject'], language)) ret = send_respa_mail(staff_email, rendered_notification['subject'], rendered_notification['body'], rendered_notification['html_body'], attachments) print(ret[1]) else: print("Sending automated mail :: (%s) %s || LOCALE: %s" % (email_address, rendered_notification['subject'], language)) ret = send_respa_mail(email_address, rendered_notification['subject'], rendered_notification['body'], rendered_notification['html_body'], attachments) print(ret[1]) def notify_staff_about_reservation(self, notification): if self.resource.resource_staff_emails: for email in self.resource.resource_staff_emails: self.send_reservation_mail(notification, staff_email=email) else: notify_users = self.resource.get_users_with_perm( 'can_approve_reservation') if len(notify_users) > 100: raise Exception("Refusing to notify more than 100 users (%s)" % self) for user in notify_users: self.send_reservation_mail(notification, user=user) def send_reservation_requested_mail(self, action_by_official=False): notification = NotificationType.RESERVATION_REQUESTED_BY_OFFICIAL if action_by_official else NotificationType.RESERVATION_REQUESTED self.send_reservation_mail(notification) def send_reservation_modified_mail(self, action_by_official=False): notification = NotificationType.RESERVATION_MODIFIED_BY_OFFICIAL if action_by_official else NotificationType.RESERVATION_MODIFIED self.send_reservation_mail(notification, action_by_official=action_by_official) def send_reservation_denied_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_DENIED) def send_reservation_confirmed_mail(self): reservations = [self] ical_file = build_reservations_ical_file(reservations) attachment = ('reservation.ics', ical_file, 'text/calendar') self.send_reservation_mail(NotificationType.RESERVATION_CONFIRMED, attachments=[attachment]) def send_reservation_cancelled_mail(self, action_by_official=False): notification = NotificationType.RESERVATION_CANCELLED_BY_OFFICIAL if action_by_official else NotificationType.RESERVATION_CANCELLED self.send_reservation_mail(notification, action_by_official=action_by_official) def send_reservation_created_mail(self, action_by_official=False): reservations = [self] ical_file = build_reservations_ical_file(reservations) attachment = 'reservation.ics', ical_file, 'text/calendar' notification = NotificationType.RESERVATION_CREATED_BY_OFFICIAL if action_by_official else NotificationType.RESERVATION_CREATED self.send_reservation_mail(notification, attachments=[attachment], action_by_official=action_by_official) def send_reservation_created_with_access_code_mail(self, action_by_official=False ): reservations = [self] ical_file = build_reservations_ical_file(reservations) attachment = 'reservation.ics', ical_file, 'text/calendar' notification = NotificationType.RESERVATION_CREATED_WITH_ACCESS_CODE_OFFICIAL_BY_OFFICIAL if action_by_official else NotificationType.RESERVATION_CREATED_WITH_ACCESS_CODE self.send_reservation_mail(notification, attachments=[attachment], action_by_official=action_by_official) def send_access_code_created_mail(self): self.send_reservation_mail( NotificationType.RESERVATION_ACCESS_CODE_CREATED) def save(self, *args, **kwargs): self.duration = DateTimeTZRange(self.begin, self.end, '[)') if not self.access_code: access_code_type = self.resource.access_code_type if self.resource.is_access_code_enabled( ) and self.resource.generate_access_codes: self.access_code = generate_access_code(access_code_type) return super().save(*args, **kwargs)
class Layer(BaseDate): """ Layer Model A layer represent a categorization of nodes. Layers might have geographical boundaries and might be managed by certain organizations. """ name = models.CharField(_('name'), max_length=50, unique=True) slug = models.SlugField(max_length=50, db_index=True, unique=True) description = models.CharField( _('description'), max_length=250, blank=True, null=True, help_text=_('short description of this layer')) text = models.TextField( _('extended text'), blank=True, null=True, help_text=_( 'extended description, specific instructions, links, ecc.')) # record management is_published = models.BooleanField(_('published'), default=True) is_external = models.BooleanField(_('is it external?'), default=False) # geographic related fields area = models.GeometryField( _('area'), help_text= _('If a polygon is used nodes of this layer will have to be contained in it.\ If a point is used nodes of this layer can be located anywhere. Lines are not allowed.' )) # organizational organization = models.CharField( _('organization'), max_length=255, blank=True, help_text=_('Organization which is responsible to manage this layer')) website = models.URLField(_('Website'), blank=True, null=True) email = models.EmailField( _('email'), blank=True, help_text= _("""possibly an email address that delivers messages to all the active participants; if you don't have such an email you can add specific users in the "mantainers" field""" )) mantainers = models.ManyToManyField( settings.AUTH_USER_MODEL, verbose_name=_('mantainers'), blank=True, help_text= _('you can specify the users who are mantaining this layer so they will receive emails from the system' )) # settings nodes_minimum_distance = models.IntegerField( default=NODES_MINIMUM_DISTANCE, help_text= _('minimum distance between nodes in meters, 0 means there is no minimum distance' )) new_nodes_allowed = models.BooleanField( _('new nodes allowed'), default=True, help_text=_('indicates whether users can add new nodes to this layer')) data = DictionaryField(_('extra data'), schema=HSTORE_SCHEMA, null=True, editable=False) # default manager objects = LayerManager() # this is needed to check if the is_published is changing # explained here: # http://stackoverflow.com/questions/1355150/django-when-saving-how-can-you-check-if-a-field-has-changed _current_is_published = None class Meta: db_table = 'layers_layer' app_label = 'layers' def __unicode__(self): return self.name def __init__(self, *args, **kwargs): """ Fill _current_is_published """ super(Layer, self).__init__(*args, **kwargs) # set current is_published, but only if it is an existing layer if self.pk: self._current_is_published = self.is_published def save(self, *args, **kwargs): """ intercepts changes to is_published and fires layer_is_published_changed signal """ super(Layer, self).save(*args, **kwargs) # if is_published of an existing layer changes if self.pk and self.is_published != self._current_is_published: # send django signal layer_is_published_changed.send( sender=self.__class__, instance=self, old_is_published=self._current_is_published, new_is_published=self.is_published) # unpublish nodes self.update_nodes_published() # update _current_is_published self._current_is_published = self.is_published def clean(self): """ Ensure area is either a Point or a Polygon """ if not isinstance(self.area, (Polygon, Point)): raise ValidationError('area can be only of type Polygon or Point') @property def center(self): # if area is point just return that if isinstance(self.area, Point) or self.area is None: return self.area # otherwise return point_on_surface or centroid try: # point_on_surface guarantees that the point is within the geometry return self.area.point_on_surface except GEOSException: # fall back on centroid which may not be within the geometry # for example, a horseshoe shaped polygon return self.area.centroid def update_nodes_published(self): """ publish or unpublish nodes of current layer """ if self.pk: self.node_set.all().update(is_published=self.is_published) if 'grappelli' in settings.INSTALLED_APPS: @staticmethod def autocomplete_search_fields(): return ('name__icontains', 'slug__icontains')
class Reporter(CreatedUpdatedModel): """ Privacy sensitive information on reporter. This information will be anonymized after X time """ _signal = models.ForeignKey('signals.Signal', related_name='reporters', null=False, on_delete=models.CASCADE) email = models.EmailField(blank=True, null=True) phone = models.CharField(max_length=17, blank=True, null=True) email_anonymized = models.BooleanField(default=False) phone_anonymized = models.BooleanField(default=False) sharing_allowed = models.BooleanField(default=False) class Meta: permissions = (('sia_can_view_contact_details', 'Inzien van contactgegevens melder (in melding)'), ) @property def is_anonymized(self): """ Checks if an anonymous reporter is anonymized? """ return self.is_anonymous and (self.email_anonymized or self.phone_anonymized) @property def is_anonymous(self): """ Checks if a reporter is anonymous """ return not self.email and not self.phone def anonymize(self, always_call_save=False): call_save = False if not self.email_anonymized and self.email: self.email_anonymized = True call_save = True if not self.phone_anonymized and self.phone: self.phone_anonymized = True call_save = True if call_save or always_call_save: self.save() def save(self, *args, **kwargs): """ Make sure that the email and phone are set to none while saving the Reporter """ if self.email_anonymized: self.email = None if self.phone_anonymized: self.phone = None super().save(*args, **kwargs)
class Place(MPTTModel, BaseModel, SchemalessFieldMixin, ImageMixin, ReplacedByMixin): objects = BaseTreeQuerySet.as_manager() geo_objects = objects publisher = models.ForeignKey('django_orghierarchy.Organization', on_delete=models.CASCADE, 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', on_delete=models.CASCADE, 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', on_delete=models.SET_NULL, related_name='aliases', null=True, blank=True) divisions = models.ManyToManyField(AdministrativeDivision, verbose_name=_('Divisions'), related_name='places', blank=True) 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._has_circular_replacement(): raise Exception( "Trying to replace this place with a place that is replaced by this place" "Please refrain from creating circular replacements and" "remove one of the replacements." "We don't want homeless events.") if self.replaced_by and not self.deleted: self.deleted = True logger.warning( "Place replaced without soft deleting. Soft deleting automatically", extra={'place': self}) # 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.set( AdministrativeDivision.objects.filter( type__type__in=('district', 'sub_district', 'neighborhood', 'muni'), geometry__boundary__contains=self.position)) else: self.divisions.clear()
class Campus(models.Model): provider = models.ForeignKey( 'ford3.provider', on_delete=models.CASCADE) name = models.CharField( blank=False, null=False, unique=False, help_text='The name of the campus', max_length=255) location = models.PointField( blank=True, null=True, help_text='The spatial point position of the campus') photo = models.FileField( blank=False, null=True, help_text='Representative photo of campus', upload_to='campus/photo' ) telephone = models.CharField( blank=False, null=True, unique=False, help_text="The campus' switchboard", max_length=16) email = models.EmailField( blank=False, null=True, unique=False, help_text="The campus' email", max_length=255) max_students_per_year = models.PositiveIntegerField( blank=False, null=True, unique=False, help_text="Maximum number of students") physical_address_line_1 = models.CharField( blank=False, null=True, unique=False, help_text="The campus' physical address details", max_length=255) physical_address_line_2 = models.CharField( blank=False, null=True, unique=False, help_text="The campus' physical address details", max_length=255) physical_address_city = models.CharField( blank=False, null=True, unique=False, help_text="The campus' physical address city", max_length=255) physical_address_postal_code = models.CharField( blank=False, null=True, unique=False, help_text="The campus' physical address post code", max_length=255) postal_address_differs = models.BooleanField( blank=False, null=True, default=False, help_text="Is the postal address different from the physical address?") postal_address_line_1 = models.CharField( blank=False, null=True, unique=False, help_text="The campus' postal address", max_length=255) postal_address_line_2 = models.CharField( blank=False, null=True, unique=False, help_text="The campus' postal address", max_length=255) postal_address_city = models.CharField( blank=False, null=True, unique=False, help_text="The campus' postal address city", max_length=255) postal_address_postal_code = models.CharField( blank=False, null=True, unique=False, help_text="The campus' postal adress code", max_length=255) created_at = models.DateTimeField( auto_now_add=True) edited_at = models.DateTimeField( auto_now=True) created_by = models.ForeignKey( 'ford3.User', null=True, on_delete=models.PROTECT, related_name='campus_created_by' ) edited_by = models.ForeignKey( 'ford3.User', null=True, on_delete=models.PROTECT, related_name='campus_edited_by' ) deleted_by = models.ForeignKey( 'ford3.User', null=True, on_delete=models.PROTECT, related_name='campus_deleted_by' ) deleted = models.BooleanField( default=False, help_text="Campus has been deleted") completion_rate = models.PositiveIntegerField( blank=True, null=True, default=0, help_text="How much of the campus' details has been completed?" ) COMPLETION_RULES = completion_rules objects = models.Manager() active_objects = ActiveCampusManager() def save(self, *args, **kwargs): if self.id is None: if len(self.name) == 0: raise ValidationError({'campus': 'Name is required.'}) if Campus.objects.filter( provider_id=self.provider.id, name__iexact=self.name, deleted=False).exists(): raise ValidationError({'campus': 'Name is already taken.'}) super().save(*args, **kwargs) def save_location_data(self, cleaned_data): x_value = cleaned_data['location_value_x'] y_value = cleaned_data['location_value_y'] geometry_point = GEOSGeometry(Point( x_value, y_value)) self.location = geometry_point self.save() def soft_delete(self): self.soft_delete_all_qualifications() self.deleted = True self.save() @property def events(self): event_query = CampusEvent.active_objects.filter( campus__id=self.id).order_by('id').values( 'id', 'name', 'date_start', 'date_end', 'http_link') return list(event_query) @property def qualifications(self): queryset = self.qualification_set \ .filter(deleted=False) \ .values( 'id', 'saqa_qualification__id', 'saqa_qualification__name', 'saqa_qualification__saqa_id', 'saqa_qualification__accredited', 'edited_at', 'published', 'ready_to_publish', 'completion_rate') \ .order_by('id') return list(queryset) @property def saqa_ids(self): return [ str(s['saqa_qualification__id']) for s in self.qualifications] @property def physical_address(self): if self.physical_address_line_1 is None \ and self.physical_address_line_2 is None \ and self.physical_address_city is None \ and self.physical_address_postal_code is None: return None return f''' {self.physical_address_line_1} {self.physical_address_line_2} {self.physical_address_city} {self.physical_address_postal_code} ''' @property def postal_address(self): if self.postal_address_line_1 is None \ and self.postal_address_line_2 is None \ and self.postal_address_city is None \ and self.postal_address_postal_code is None: return None return f''' {self.postal_address_line_1} {self.postal_address_line_2} {self.postal_address_city} {self.postal_address_postal_code} ''' @property def qualifications_completion_rate(self): try: return int(sum([ qualification['completion_rate'] for qualification in self.qualifications ]) / len(self.qualifications)) except ZeroDivisionError: return 0 def save_postal_data(self, form_data): postal_address_differs = form_data.get( 'postal_address_differs', '') physical_address_line_1 = form_data.get( 'physical_address_line_1', '') physical_address_line_2 = form_data.get( 'physical_address_line_2', '') physical_address_city = form_data.get( 'physical_address_city', '') physical_address_postal_code = form_data.get( 'physical_address_postal_code', '') if not postal_address_differs: postal_address_line_1 = physical_address_line_1 postal_address_line_2 = physical_address_line_2 postal_address_city = physical_address_city postal_address_postal_code = physical_address_postal_code else: postal_address_line_1 = form_data.get( 'postal_address_line_1', '') postal_address_line_2 = form_data.get( 'postal_address_line_2', '') postal_address_city = form_data.get( 'postal_address_city', '') postal_address_postal_code = form_data.get( 'postal_address_postal_code', '') setattr(self, 'physical_address_line_1', physical_address_line_1) setattr(self, 'physical_address_line_2', physical_address_line_2) setattr(self, 'physical_address_city', physical_address_city) setattr(self, 'physical_address_postal_code', physical_address_postal_code) setattr(self, 'postal_address_line_1', postal_address_line_1) setattr(self, 'postal_address_line_2', postal_address_line_2) setattr(self, 'postal_address_city', postal_address_city) setattr(self, 'postal_address_postal_code', postal_address_postal_code) setattr(self, 'postal_address_differs', postal_address_differs) self.save() def save_form_data(self, form_data): for key, value in form_data.items(): setattr(self, key, value) self.save() def save_qualifications(self, form_data, created_by): if len(form_data['saqa_ids']) == 0: return ids = [ sid for sid in form_data['saqa_ids'].split(' ') if sid not in self.saqa_ids ] for saqa_id in ids: qualif = self.qualification_set.create( created_by=created_by, edited_by=created_by ) qualif.set_saqa_qualification(saqa_id) qualif.save() def delete_qualifications(self, form_data): # ids missing in form_data must be deleted ids = [ sid for sid in self.saqa_ids if sid not in form_data['saqa_ids'].split(' ') ] for saqa_id in ids: qualif = self.qualification_set.filter( saqa_qualification__id=saqa_id, campus=self) qualif.update(deleted=True) def soft_delete_all_qualifications(self): for qualification in self.qualification_set.all(): qualification.soft_delete() def __str__(self): return self.name
class Reservation(ModifiableModel): CREATED = 'created' CANCELLED = 'cancelled' CONFIRMED = 'confirmed' DENIED = 'denied' REQUESTED = 'requested' WAITING_FOR_PAYMENT = 'waiting_for_payment' STATE_CHOICES = ( (CREATED, _('created')), (CANCELLED, _('cancelled')), (CONFIRMED, _('confirmed')), (DENIED, _('denied')), (REQUESTED, _('requested')), (WAITING_FOR_PAYMENT, _('waiting for payment')), ) TYPE_NORMAL = 'normal' TYPE_BLOCKED = 'blocked' TYPE_CHOICES = ( (TYPE_NORMAL, _('Normal reservation')), (TYPE_BLOCKED, _('Resource blocked')), ) resource = models.ForeignKey('Resource', verbose_name=_('Resource'), db_index=True, related_name='reservations', on_delete=models.PROTECT) begin = models.DateTimeField(verbose_name=_('Begin time')) end = models.DateTimeField(verbose_name=_('End time')) duration = pgfields.DateTimeRangeField( verbose_name=_('Length of reservation'), null=True, blank=True, db_index=True) comments = models.TextField(null=True, blank=True, verbose_name=_('Comments')) user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('User'), null=True, blank=True, db_index=True, on_delete=models.PROTECT) state = models.CharField(max_length=32, choices=STATE_CHOICES, verbose_name=_('State'), default=CREATED) approver = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('Approver'), related_name='approved_reservations', null=True, blank=True, on_delete=models.SET_NULL) staff_event = models.BooleanField(verbose_name=_('Is staff event'), default=False) type = models.CharField(blank=False, verbose_name=_('Type'), max_length=32, choices=TYPE_CHOICES, default=TYPE_NORMAL) # access-related fields access_code = models.CharField(verbose_name=_('Access code'), max_length=32, null=True, blank=True) # EXTRA FIELDS START HERE event_subject = models.CharField(max_length=200, verbose_name=_('Event subject'), blank=True) event_description = models.TextField(verbose_name=_('Event description'), blank=True) number_of_participants = models.PositiveSmallIntegerField( verbose_name=_('Number of participants'), blank=True, null=True) participants = models.TextField(verbose_name=_('Participants'), blank=True) host_name = models.CharField(verbose_name=_('Host name'), max_length=100, blank=True) reservation_extra_questions = models.TextField( verbose_name=_('Reservation extra questions'), blank=True) reserver_name = models.CharField(verbose_name=_('Reserver name'), max_length=100, blank=True) reserver_id = models.CharField( verbose_name=_('Reserver ID (business or person)'), max_length=30, blank=True) reserver_email_address = models.EmailField( verbose_name=_('Reserver email address'), blank=True) reserver_phone_number = models.CharField( verbose_name=_('Reserver phone number'), max_length=30, blank=True) reserver_address_street = models.CharField( verbose_name=_('Reserver address street'), max_length=100, blank=True) reserver_address_zip = models.CharField( verbose_name=_('Reserver address zip'), max_length=30, blank=True) reserver_address_city = models.CharField( verbose_name=_('Reserver address city'), max_length=100, blank=True) company = models.CharField(verbose_name=_('Company'), max_length=100, blank=True) billing_first_name = models.CharField(verbose_name=_('Billing first name'), max_length=100, blank=True) billing_last_name = models.CharField(verbose_name=_('Billing last name'), max_length=100, blank=True) billing_email_address = models.EmailField( verbose_name=_('Billing email address'), blank=True) billing_phone_number = models.CharField( verbose_name=_('Billing phone number'), max_length=30, blank=True) billing_address_street = models.CharField( verbose_name=_('Billing address street'), max_length=100, blank=True) billing_address_zip = models.CharField( verbose_name=_('Billing address zip'), max_length=30, blank=True) billing_address_city = models.CharField( verbose_name=_('Billing address city'), max_length=100, blank=True) # If the reservation was imported from another system, you can store the original ID in the field below. origin_id = models.CharField(verbose_name=_('Original ID'), max_length=50, editable=False, null=True) objects = ReservationQuerySet.as_manager() class Meta: verbose_name = _("reservation") verbose_name_plural = _("reservations") ordering = ('id', ) def _save_dt(self, attr, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ save_dt(self, attr, dt, self.resource.unit.time_zone) def _get_dt(self, attr, tz): return get_dt(self, attr, tz) @property def begin_tz(self): return self.begin @begin_tz.setter def begin_tz(self, dt): self._save_dt('begin', dt) def get_begin_tz(self, tz): return self._get_dt("begin", tz) @property def end_tz(self): return self.end @end_tz.setter def end_tz(self, dt): """ Any DateTime object is converted to UTC time zone aware DateTime before save If there is no time zone on the object, resource's time zone will be assumed through its unit's time zone """ self._save_dt('end', dt) def get_end_tz(self, tz): return self._get_dt("end", tz) def is_active(self): return self.end >= timezone.now() and self.state not in ( Reservation.CANCELLED, Reservation.DENIED) def is_own(self, user): if not (user and user.is_authenticated): return False return user == self.user def need_manual_confirmation(self): return self.resource.need_manual_confirmation def are_extra_fields_visible(self, user): # the following logic is used also implemented in ReservationQuerySet # so if this is changed that probably needs to be changed as well if self.is_own(user): return True return self.resource.can_view_reservation_extra_fields(user) def can_view_access_code(self, user): if self.is_own(user): return True return self.resource.can_view_reservation_access_code(user) def set_state(self, new_state, user): # Make sure it is a known state assert new_state in (Reservation.REQUESTED, Reservation.CONFIRMED, Reservation.DENIED, Reservation.CANCELLED, Reservation.WAITING_FOR_PAYMENT) old_state = self.state if new_state == old_state: if old_state == Reservation.CONFIRMED: reservation_modified.send(sender=self.__class__, instance=self, user=user) return if new_state == Reservation.CONFIRMED: self.approver = user reservation_confirmed.send(sender=self.__class__, instance=self, user=user) elif old_state == Reservation.CONFIRMED: self.approver = None user_is_staff = self.user is not None and self.user.is_staff # Notifications if new_state == Reservation.REQUESTED: self.send_reservation_requested_mail() self.send_reservation_requested_mail_to_officials() elif new_state == Reservation.CONFIRMED: if self.need_manual_confirmation(): self.send_reservation_confirmed_mail() elif self.access_code: self.send_reservation_created_with_access_code_mail() else: if not user_is_staff: # notifications are not sent from staff created reservations to avoid spam self.send_reservation_created_mail() elif new_state == Reservation.DENIED: self.send_reservation_denied_mail() elif new_state == Reservation.CANCELLED: order = self.get_order() if order: if order.state == order.CANCELLED: self.send_reservation_cancelled_mail() else: if user != self.user: self.send_reservation_cancelled_mail() reservation_cancelled.send(sender=self.__class__, instance=self, user=user) self.state = new_state self.save() def can_modify(self, user): if not user: return False if self.state == Reservation.WAITING_FOR_PAYMENT: return False if self.get_order(): return self.resource.can_modify_paid_reservations(user) # reservations that need manual confirmation and are confirmed cannot be # modified or cancelled without reservation approve permission cannot_approve = not self.resource.can_approve_reservations(user) if self.need_manual_confirmation( ) and self.state == Reservation.CONFIRMED and cannot_approve: return False return self.user == user or self.resource.can_modify_reservations(user) def can_add_comment(self, user): if self.is_own(user): return True return self.resource.can_access_reservation_comments(user) def can_view_field(self, user, field): if field not in RESERVATION_EXTRA_FIELDS: return True if self.is_own(user): return True return self.resource.can_view_reservation_extra_fields(user) def can_view_catering_orders(self, user): if self.is_own(user): return True return self.resource.can_view_reservation_catering_orders(user) def can_add_product_order(self, user): return self.is_own(user) def can_view_product_orders(self, user): if self.is_own(user): return True return self.resource.can_view_reservation_product_orders(user) def get_order(self): return getattr(self, 'order', None) def format_time(self): tz = self.resource.unit.get_tz() begin = self.begin.astimezone(tz) end = self.end.astimezone(tz) return format_dt_range(translation.get_language(), begin, end) def __str__(self): if self.state != Reservation.CONFIRMED: state_str = ' (%s)' % self.state else: state_str = '' return "%s: %s%s" % (self.format_time(), self.resource, state_str) def clean(self, **kwargs): """ Check restrictions that are common to all reservations. If this reservation isn't yet saved and it will modify an existing reservation, the original reservation need to be provided in kwargs as 'original_reservation', so that it can be excluded when checking if the resource is available. """ if 'user' in kwargs: user = kwargs['user'] else: user = self.user user_is_admin = user and self.resource.is_admin(user) if self.end <= self.begin: raise ValidationError( _("You must end the reservation after it has begun")) # Check that begin and end times are on valid time slots. opening_hours = self.resource.get_opening_hours( self.begin.date(), self.end.date()) for dt in (self.begin, self.end): days = opening_hours.get(dt.date(), []) day = next((day for day in days if day['opens'] is not None and day['opens'] <= dt <= day['closes']), None) if day and not is_valid_time_slot(dt, self.resource.slot_size, day['opens']): raise ValidationError( _("Begin and end time must match time slots"), code='invalid_time_slot') # Check if Unit has disallow_overlapping_reservations value of True if (self.resource.unit.disallow_overlapping_reservations and not self.resource.can_create_overlapping_reservations(user)): reservations_for_same_unit = Reservation.objects.filter( user=user, resource__unit=self.resource.unit) valid_reservations_for_same_unit = reservations_for_same_unit.exclude( state=Reservation.CANCELLED) user_has_conflicting_reservations = valid_reservations_for_same_unit.filter( Q(begin__gt=self.begin, begin__lt=self.end) | Q(begin__lt=self.begin, end__gt=self.begin) | Q(begin__gte=self.begin, end__lte=self.end)) if user_has_conflicting_reservations: raise ValidationError(_( 'This unit does not allow overlapping reservations for its resources' ), code='conflicting_reservation') original_reservation = self if self.pk else kwargs.get( 'original_reservation', None) if self.resource.check_reservation_collision(self.begin, self.end, original_reservation): raise ValidationError( _("The resource is already reserved for some of the period")) if not user_is_admin: if (self.end - self.begin) < self.resource.min_period: raise ValidationError( _("The minimum reservation length is %(min_period)s") % { 'min_period': humanize_duration( self.resource.min_period) }) else: if not (self.end - self.begin ) % self.resource.slot_size == datetime.timedelta(0): raise ValidationError( _("The minimum reservation length is %(slot_size)s") % {'slot_size': humanize_duration(self.resource.slot_size)}) if self.access_code: validate_access_code(self.access_code, self.resource.access_code_type) def get_notification_context(self, language_code, user=None, notification_type=None): if not user: user = self.user with translation.override(language_code): reserver_name = self.reserver_name reserver_email_address = self.reserver_email_address if not reserver_name and self.user and self.user.get_display_name( ): reserver_name = self.user.get_display_name() if not reserver_email_address and user and user.email: reserver_email_address = user.email context = { 'resource': self.resource.name, 'begin': localize_datetime(self.begin), 'end': localize_datetime(self.end), 'begin_dt': self.begin, 'end_dt': self.end, 'time_range': self.format_time(), 'reserver_name': reserver_name, 'reserver_email_address': reserver_email_address, } directly_included_fields = ( 'number_of_participants', 'host_name', 'event_subject', 'event_description', 'reserver_phone_number', 'billing_first_name', 'billing_last_name', 'billing_email_address', 'billing_phone_number', 'billing_address_street', 'billing_address_zip', 'billing_address_city', ) for field in directly_included_fields: context[field] = getattr(self, field) if self.resource.unit: context['unit'] = self.resource.unit.name context['unit_id'] = self.resource.unit.id if self.can_view_access_code(user) and self.access_code: context['access_code'] = self.access_code if notification_type == NotificationType.RESERVATION_CONFIRMED: if self.resource.reservation_confirmed_notification_extra: context[ 'extra_content'] = self.resource.reservation_confirmed_notification_extra elif notification_type == NotificationType.RESERVATION_REQUESTED: if self.resource.reservation_requested_notification_extra: context[ 'extra_content'] = self.resource.reservation_requested_notification_extra # Get last main and ground plan images. Normally there shouldn't be more than one of each # of those images. images = self.resource.images.filter( type__in=('main', 'ground_plan')).order_by('-sort_order') main_image = next((i for i in images if i.type == 'main'), None) ground_plan_image = next( (i for i in images if i.type == 'ground_plan'), None) if main_image: main_image_url = main_image.get_full_url() if main_image_url: context['resource_main_image_url'] = main_image_url if ground_plan_image: ground_plan_image_url = ground_plan_image.get_full_url() if ground_plan_image_url: context[ 'resource_ground_plan_image_url'] = ground_plan_image_url order = getattr(self, 'order', None) if order: context['order'] = order.get_notification_context( language_code) return context def send_reservation_mail(self, notification_type, user=None, attachments=None): """ Stuff common to all reservation related mails. If user isn't given use self.user. """ try: notification_template = NotificationTemplate.objects.get( type=notification_type) except NotificationTemplate.DoesNotExist: return if user: email_address = user.email else: if not (self.reserver_email_address or self.user): return email_address = self.reserver_email_address or self.user.email user = self.user language = user.get_preferred_language() if user else DEFAULT_LANG context = self.get_notification_context( language, notification_type=notification_type) try: rendered_notification = notification_template.render( context, language) except NotificationTemplateException as e: logger.error(e, exc_info=True, extra={'user': user.uuid}) return send_respa_mail(email_address, rendered_notification['subject'], rendered_notification['body'], rendered_notification['html_body'], attachments) def send_reservation_requested_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_REQUESTED) def send_reservation_requested_mail_to_officials(self): notify_users = self.resource.get_users_with_perm( 'can_approve_reservation') if len(notify_users) > 100: raise Exception("Refusing to notify more than 100 users (%s)" % self) for user in notify_users: self.send_reservation_mail( NotificationType.RESERVATION_REQUESTED_OFFICIAL, user=user) def send_reservation_denied_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_DENIED) def send_reservation_confirmed_mail(self): reservations = [self] ical_file = build_reservations_ical_file(reservations) attachment = ('reservation.ics', ical_file, 'text/calendar') self.send_reservation_mail(NotificationType.RESERVATION_CONFIRMED, attachments=[attachment]) def send_reservation_cancelled_mail(self): self.send_reservation_mail(NotificationType.RESERVATION_CANCELLED) def send_reservation_created_mail(self): reservations = [self] ical_file = build_reservations_ical_file(reservations) attachment = 'reservation.ics', ical_file, 'text/calendar' self.send_reservation_mail(NotificationType.RESERVATION_CREATED, attachments=[attachment]) def send_reservation_created_with_access_code_mail(self): reservations = [self] ical_file = build_reservations_ical_file(reservations) attachment = 'reservation.ics', ical_file, 'text/calendar' self.send_reservation_mail( NotificationType.RESERVATION_CREATED_WITH_ACCESS_CODE, attachments=[attachment]) def send_access_code_created_mail(self): self.send_reservation_mail( NotificationType.RESERVATION_ACCESS_CODE_CREATED) def save(self, *args, **kwargs): self.duration = DateTimeTZRange(self.begin, self.end, '[)') if not self.access_code: access_code_type = self.resource.access_code_type if self.resource.is_access_code_enabled( ) and self.resource.generate_access_codes: self.access_code = generate_access_code(access_code_type) return super().save(*args, **kwargs)
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=_("Description teaser"), blank=True, help_text=_("A brief summary"), db_column='chapeau') description = models.TextField(verbose_name=_("Description"), blank=True, db_column='description', help_text=_("Complete description")) themes = models.ManyToManyField(Theme, related_name="touristiccontents", db_table="t_r_contenu_touristique_theme", blank=True, verbose_name=_("Themes"), help_text=_("Main theme(s)")) geom = models.GeometryField(verbose_name=_("Location"), srid=settings.SRID) category = models.ForeignKey(TouristicContentCategory, related_name='contents', verbose_name=_("Category"), db_column='categorie') contact = models.TextField(verbose_name=_("Contact"), blank=True, db_column='contact', help_text=_("Address, phone, etc.")) email = models.EmailField(verbose_name=_("Email"), max_length=256, db_column='email', blank=True, null=True) website = models.URLField(verbose_name=_("Website"), max_length=256, db_column='website', blank=True, null=True) practical_info = models.TextField(verbose_name=_("Practical info"), blank=True, db_column='infos_pratiques', help_text=_("Anything worth to know")) type1 = models.ManyToManyField(TouristicContentType1, related_name='contents1', verbose_name=_("Type 1"), db_table="t_r_contenu_touristique_type1", blank=True) type2 = models.ManyToManyField(TouristicContentType2, related_name='contents2', verbose_name=_("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=_("External id"), max_length=1024, blank=True, null=True, db_column='id_externe') reservation_system = models.ForeignKey( ReservationSystem, verbose_name=_("Reservation system"), blank=True, null=True) reservation_id = models.CharField(verbose_name=_("Reservation ID"), max_length=1024, blank=True, db_column='id_reservation') approved = models.BooleanField(verbose_name=_("Approved"), default=False, db_column='labellise') objects = NoDeleteMixin.get_manager_cls(models.GeoManager)() class Meta: db_table = 't_t_contenu_touristique' verbose_name = _("Touristic content") verbose_name_plural = _("Touristic contents") def __str__(self): return self.name @property def districts_display(self): return ', '.join([str(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([str(n) for n in self.type1.all()]) @property def type2_display(self): return ', '.join([str(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([str(portal) for portal in self.portal.all()]) @property def source_display(self): return ','.join([str(source) for source in self.source.all()]) @property def themes_display(self): return ','.join([str(source) for source in self.themes.all()]) @property def extent(self): return self.geom.buffer(10).transform(settings.API_SRID, clone=True).extent @property def rando_url(self): category_slug = _('touristic-content') return '{}/{}/'.format(category_slug, self.slug) @property def meta_description(self): return plain_text(self.description_teaser or self.description)[:500]