class AidViewEvent(models.Model): aid = models.ForeignKey( 'aids.Aid', verbose_name=_('Aid'), on_delete=models.PROTECT) targeted_audiences = ChoiceArrayField( verbose_name=_('Targeted audiences'), null=True, blank=True, base_field=models.CharField( max_length=32, choices=Aid.AUDIENCES)) querystring = models.TextField( _('Querystring')) source = models.CharField( 'Source', max_length=256, default='') date_created = models.DateTimeField( _('Date created'), default=timezone.now) class Meta: verbose_name = _('Aid View Event') verbose_name_plural = _('Aid View Events')
class AidSearchEvent(models.Model): targeted_audiences = ChoiceArrayField( verbose_name=_('Targeted audiences'), null=True, blank=True, base_field=models.CharField( max_length=32, choices=Aid.AUDIENCES)) perimeter = models.ForeignKey( 'geofr.Perimeter', verbose_name=_('Perimeter'), on_delete=models.PROTECT, null=True, blank=True) themes = models.ManyToManyField( 'categories.Theme', verbose_name=_('Themes'), related_name='aid_search_events', blank=True) categories = models.ManyToManyField( 'categories.Category', verbose_name=_('Categories'), related_name='aid_search_events', blank=True) text = models.CharField( _('Text search'), max_length=256, blank=True, default='') querystring = models.TextField( _('Querystring')) results_count = models.PositiveIntegerField( _('Results count'), default=0) source = models.CharField( 'Source', max_length=256, blank=True, default='') date_created = models.DateTimeField( _('Date created'), default=timezone.now) class Meta: verbose_name = _('Aid Search Event') verbose_name_plural = _('Aid Search Events') def save(self, *args, **kwargs): self.clean_fields() return super().save(*args, **kwargs) def clean_fields(self): self.text = self.text[:256] if self.text else '' self.source = self.source[:256] if self.source else ''
class Project(TimeStampedModel): uuid = models.UUIDField( _('UUID'), default=uuid.uuid4, editable=False, unique=True ) name = models.CharField(_('name'), max_length=255) description = models.TextField(_('description'), blank=True) code = models.SlugField( _('code'), blank=True, allow_unicode=True, unique=True ) organization = models.ForeignKey( 'organizations.Organization', blank=True, null=True, related_name='projects', related_query_name='project', verbose_name=_('organization'), on_delete=models.SET_NULL ) email = models.EmailField(_('email'), blank=True) phone_number = PhoneNumberField(_('phone number'), blank=True) avatar = models.ImageField( _('avatar'), blank=True, null=True, upload_to='organizations/avatars' ) website = models.URLField(_('website'), blank=True) countries = ChoiceArrayField( CountryField(), verbose_name=_('countries'), blank=True, default=list ) tags = ArrayField( models.CharField(max_length=50), verbose_name=_('tags'), blank=True, default=list ) #: Project facilitators. #: Only users with facilitator status can be project facilitators. facilitators = models.ManyToManyField( settings.AUTH_USER_MODEL, limit_choices_to={'is_facilitator': True}, related_name='facilitates_projects', related_query_name='facilitates_project', verbose_name=_('facilitators'), blank=True ) creator = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('creator'), blank=True, related_name='created_projects', related_query_name='created_project', on_delete=models.CASCADE ) extras = JSONField(_('extras'), blank=True, default=dict) class Meta: verbose_name = _('Project') verbose_name_plural = _('Projects') ordering = ['-created_at'] def __str__(self): return self.name def save(self, *args, **kwargs): if not self.code: self.code = slugify(self.name[:50]) super().save(*args, **kwargs) def get_absolute_url(self): """Obtain project absolute url.""" return reverse('projects:project-detail', kwargs={'pk': self.pk}) # Derive project name abbreviation @property def abbreviation(self): parts = [] words = self.name.split(' ') for word in words: parts.append(str(word[0])) return ''.join(parts[:2])
class Aid(xwf_models.WorkflowEnabled, models.Model): """Represents a single Aid.""" TYPES = Choices( ('grant', _('Grant')), ('loan', _('Loan')), ('recoverable_advance', _('Recoverable advance')), ('interest_subsidy', _('Interest subsidy')), ('guidance', _('Guidance')), ('networking', _('Networking')), ('valorisation', _('Valorisation')), ) FINANCIAL_AIDS = ('grant', 'loan', 'recoverable_advance', 'interest_subsidy') TECHNICAL_AIDS = ('guidance', 'networking', 'valorisation') PERIMETERS = Choices( ('europe', _('Europe')), ('france', _('France')), ('region', _('Region')), ('department', _('Department')), ('commune', _('Commune')), ('mainland', _('Mainland')), ('overseas', _('Overseas')), ('other', _('Other')), ) STEPS = Choices( ('preop', _('Preoperational')), ('op', _('Operational')), ('postop', _('Postoperation')), ) AUDIANCES = Choices( ('commune', _('Commune')), ('department', _('Department')), ('region', _('Region')), ('epci', _('Audiance EPCI')), ('lessor', _('Audiance lessor')), ('association', _('Association')), ('private_person', _('Individual')), ('researcher', _('Research')), ('private_sector', _('Private sector')), ) DESTINATIONS = Choices( ('service', _('Service (AMO, survey)')), ('works', _('Works')), ('supply', _('Supply')), ) RECURRENCE = Choices( ('oneoff', _('One off')), ('ongoing', _('Ongoing')), ('recurring', _('Recurring')), ) objects = ExistingAidsManager() all_aids = AidQuerySet.as_manager() slug = models.SlugField( _('Slug'), help_text=_('Let it empty so it will be autopopulated.'), blank=True) name = models.CharField( _('Name'), max_length=256, null=False, blank=False) author = models.ForeignKey( 'accounts.User', on_delete=models.PROTECT, verbose_name=_('Author'), help_text=_('Who is submitting the aid?')) backers = models.ManyToManyField( 'backers.Backer', related_name='aids', verbose_name=_('Backers'), help_text=_('On a national level if appropriate')) description = models.TextField( _('Short description'), blank=False) eligibility = models.TextField( _('Eligibility'), blank=True) perimeter = models.ForeignKey( 'geofr.Perimeter', verbose_name=_('Perimeter'), on_delete=models.PROTECT, null=True, blank=True, help_text=_('What is the aid broadcasting perimeter?')) mobilization_steps = ChoiceArrayField( verbose_name=_('Mobilization step'), null=True, blank=True, base_field=models.CharField( max_length=32, choices=STEPS, default=STEPS.preop)) url = models.URLField( _('URL'), blank=True) application_url = models.URLField( _('Application url'), blank=True) targeted_audiances = ChoiceArrayField( verbose_name=_('Targeted audiances'), null=True, blank=True, base_field=models.CharField( max_length=32, choices=AUDIANCES)) aid_types = ChoiceArrayField( verbose_name=_('Aid types'), null=True, blank=True, base_field=models.CharField( max_length=32, choices=TYPES), help_text=_('Specify the help type or types.')) destinations = ChoiceArrayField( verbose_name=_('Destinations'), null=True, blank=True, base_field=models.CharField( max_length=32, choices=DESTINATIONS)) start_date = models.DateField( _('Start date'), null=True, blank=True, help_text=_('When is the application opening?')) predeposit_date = models.DateField( _('Predeposit date'), null=True, blank=True, help_text=_('When is the pre-deposit date, if applicable?')) submission_deadline = models.DateField( _('Submission deadline'), null=True, blank=True, help_text=_('When is the submission deadline?')) subvention_rate = models.DecimalField( _('Subvention rate (in %)'), max_digits=6, decimal_places=2, null=True, blank=True, help_text=_('If this is a subvention aid, specify the rate.')) contact_email = models.EmailField( _('Contact email'), blank=True) contact_phone = models.CharField( _('Contact phone number'), max_length=20, blank=True) contact_detail = models.CharField( _('Contact detail'), max_length=256, blank=True) recurrence = models.CharField( _('Recurrence'), help_text=_('Is this a one-off aid, is it recurring or ongoing?'), max_length=16, choices=RECURRENCE, blank=True) status = xwf_models.StateField( AidWorkflow, verbose_name=_('Status')) date_created = models.DateTimeField( _('Date created'), default=timezone.now) date_updated = models.DateTimeField( _('Date updated'), auto_now=True) # Third-party data import related fields is_imported = models.BooleanField( _('Is imported?'), default=False) import_uniqueid = models.CharField( _('Unique identifier for imported data'), max_length=20, blank=True) # This field is used to index searchable text content search_vector = SearchVectorField( _('Search vector'), null=True) # This is where we store tags tags = ArrayField( models.CharField(max_length=50, blank=True), verbose_name=_('Tags'), default=list, size=16, blank=True) _tags_m2m = models.ManyToManyField( 'tags.Tag', related_name='aids', verbose_name=_('Tags')) class Meta: verbose_name = _('Aid') verbose_name_plural = _('Aids') indexes = [ GinIndex(fields=['search_vector']), ] def set_slug(self): """Set the object's slug. Lots of aids have duplicate name, so we prefix the slug with random characters.""" if not self.id: full_title = '{}-{}'.format(str(uuid4())[:4], self.name) self.slug = slugify(full_title)[:50] def set_search_vector(self): """Update the full text cache field.""" # Note: we use `SearchVector(Value(self.field))` instead of # `SearchVector('field')` because the latter only works for updates, # not when inserting new records. self.search_vector = \ SearchVector(Value(self.name), weight='A', config='french') + \ SearchVector( Value(self.eligibility), weight='D', config='french') + \ SearchVector( Value(self.description), weight='B', config='french') + \ SearchVector( Value(' '.join(self.tags)), weight='A', config='french') def save(self, *args, **kwargs): self.set_slug() self.set_search_vector() return super().save(*args, **kwargs) def __str__(self): return self.name def get_absolute_url(self): return reverse('aid_detail_view', args=[self.slug]) def get_admin_url(self): return reverse('admin:aids_aid_change', args=[self.id]) def is_draft(self): return self.status == AidWorkflow.states.draft def is_under_review(self): return self.status == AidWorkflow.states.reviewable def is_published(self): return self.status == AidWorkflow.states.published def is_financial(self): """Does this aid have financial parts?""" return bool(set(self.aid_types) & set(self.FINANCIAL_AIDS)) def is_technical(self): """Does this aid have technical parts?""" return bool(set(self.aid_types) & set(self.TECHNICAL_AIDS)) def has_appreaching_deadline(self): if not self.submission_deadline: return False delta = self.submission_deadline - timezone.now().date() return delta.days <= settings.APPROACHING_DEADLINE_DELTA
class Aid(xwf_models.WorkflowEnabled, models.Model): """Represents a single Aid.""" TYPES = Choices( ('grant', _('Grant')), ('loan', _('Loan')), ('recoverable_advance', _('Recoverable advance')), ('technical', _('Technical engineering')), ('financial', _('Financial engineering')), ('legal', _('Legal engineering')), ('other', _('Other')), ) FINANCIAL_AIDS = ('grant', 'loan', 'recoverable_advance', 'other') TECHNICAL_AIDS = ('technical', 'financial', 'legal') PERIMETERS = Choices( ('europe', _('Europe')), ('france', _('France')), ('region', _('Region')), ('department', _('Department')), ('commune', _('Commune')), ('mainland', _('Mainland')), ('overseas', _('Overseas')), ('other', _('Other')), ) STEPS = Choices( ('preop', _('Preoperational')), ('op', _('Operational')), ('postop', _('Postoperation')), ) AUDIANCES = Choices( ('commune', _('Communes')), ('epci', _('Audiance EPCI')), ('unions', ('Intermunicipal unions')), ('department', _('Departments')), ('region', _('Regions')), ('association', _('Associations')), ('private_sector', _('Private sector')), ('public_org', _('Public organizations')), ('lessor', _('Audiance lessors')), ('researcher', _('Research')), ('private_person', _('Individuals')), ('farmer', _('Farmers')), ('other', _('Other')), ) DESTINATIONS = Choices( ('service', _('Service (AMO, survey)')), ('works', _('Works')), ('supply', _('Supply')), ) RECURRENCE = Choices( ('oneoff', _('One off')), ('ongoing', _('Ongoing')), ('recurring', _('Recurring')), ) IMPORT_LICENCES = Choices( ('unknown', _('Unknown')), ('openlicence20', _('Open licence 2.0')), ) objects = ExistingAidsManager() all_aids = AidQuerySet.as_manager() amendments = AmendmentManager() slug = models.SlugField( _('Slug'), help_text=_('Let it empty so it will be autopopulated.'), blank=True) name = models.CharField( _('Name'), max_length=180, help_text=_('A good title describes the purpose of the help and ' 'should speak to the recipient.'), null=False, blank=False) author = models.ForeignKey('accounts.User', on_delete=models.PROTECT, verbose_name=_('Author'), help_text=_('Who is submitting the aid?'), null=True) categories = models.ManyToManyField('categories.Category', verbose_name=_('Categories'), related_name='aids', blank=True) financers = models.ManyToManyField('backers.Backer', related_name='financed_aids', verbose_name=_('Financers')) financer_suggestion = models.CharField(_('Financer suggestion'), max_length=256, blank=True) instructors = models.ManyToManyField('backers.Backer', blank=True, related_name='instructed_aids', verbose_name=_('Instructors')) instructor_suggestion = models.CharField(_('Instructor suggestion'), max_length=256, blank=True) description = models.TextField( _('Full description of the aid and its objectives'), blank=False) eligibility = models.TextField(_('Eligibility'), blank=True) perimeter = models.ForeignKey( 'geofr.Perimeter', verbose_name=_('Perimeter'), on_delete=models.PROTECT, null=True, blank=True, help_text=_('What is the aid broadcasting perimeter?')) mobilization_steps = ChoiceArrayField(verbose_name=_('Mobilization step'), null=True, blank=True, base_field=models.CharField( max_length=32, choices=STEPS, default=STEPS.preop)) origin_url = models.URLField(_('Origin URL'), blank=True) application_url = models.URLField(_('Application url'), blank=True) targeted_audiances = ChoiceArrayField( verbose_name=_('Targeted audiances'), null=True, blank=True, base_field=models.CharField(max_length=32, choices=AUDIANCES)) aid_types = ChoiceArrayField(verbose_name=_('Aid types'), null=True, blank=True, base_field=models.CharField(max_length=32, choices=TYPES), help_text=_('Specify the aid type or types.')) destinations = ChoiceArrayField(verbose_name=_('Destinations'), null=True, blank=True, base_field=models.CharField( max_length=32, choices=DESTINATIONS)) start_date = models.DateField( _('Start date'), null=True, blank=True, help_text=_('When is the application opening?')) predeposit_date = models.DateField( _('Predeposit date'), null=True, blank=True, help_text=_('When is the pre-deposit date, if applicable?')) submission_deadline = models.DateField( _('Submission deadline'), null=True, blank=True, help_text=_('When is the submission deadline?')) subvention_rate = PercentRangeField( _('Subvention rate, min. and max. (in round %)'), null=True, blank=True, help_text=_('If fixed rate, only fill the max. rate.')) subvention_comment = models.CharField( _('Subvention rate, optional comment'), max_length=256, blank=True) contact = models.TextField(_('Contact'), blank=True) contact_email = models.EmailField(_('Contact email'), blank=True) contact_phone = models.CharField(_('Contact phone number'), max_length=35, blank=True) contact_detail = models.CharField(_('Contact detail'), max_length=256, blank=True) recurrence = models.CharField( _('Recurrence'), help_text=_('Is this a one-off aid, is it recurring or ongoing?'), max_length=16, choices=RECURRENCE, blank=True) is_call_for_project = models.BooleanField( _('Call for project / Call for expressions of interest'), null=True) status = xwf_models.StateField(AidWorkflow, verbose_name=_('Status')) date_created = models.DateTimeField(_('Date created'), default=timezone.now) date_updated = models.DateTimeField(_('Date updated'), auto_now=True) date_published = models.DateTimeField(_('First publication date'), null=True, blank=True) # Third-party data import related fields is_imported = models.BooleanField(_('Is imported?'), default=False) # Even if this field is a CharField, we make it nullable with `null=True` # because null values are not taken into account by postgresql when # enforcing the `unique` constraint, which is very handy for us. import_uniqueid = models.CharField( _('Unique identifier for imported data'), max_length=200, unique=True, null=True, blank=True) import_data_url = models.URLField(_('Origin url of the imported data'), null=True, blank=True) import_share_licence = models.CharField( _('Under which license was this aid shared?'), max_length=50, choices=IMPORT_LICENCES, blank=True) import_last_access = models.DateField(_('Date of the latest access'), null=True, blank=True) # This field is used to index searchable text content search_vector = SearchVectorField(_('Search vector'), null=True) # This is where we store tags tags = ArrayField(models.CharField(max_length=50, blank=True), verbose_name=_('Tags'), default=list, size=30, blank=True) _tags_m2m = models.ManyToManyField('tags.Tag', related_name='aids', verbose_name=_('Tags')) # Those fields handle the "aid amendment" feature # Users, including anonymous, can suggest amendments to existing aids. # We store a suggested edit as a clone of the original aid, with the # following field as True. is_amendment = models.BooleanField(_('Is amendment'), default=False) amended_aid = models.ForeignKey('aids.Aid', verbose_name=_('Amended aid'), on_delete=models.CASCADE, null=True) amendment_author_name = models.CharField(_('Amendment author'), max_length=256, blank=True) amendment_author_email = models.EmailField(_('Amendment author email'), null=True, blank=True) amendment_author_org = models.CharField(_('Amendment author organization'), max_length=255, blank=True) amendment_comment = models.TextField(_('Amendment comment'), blank=True) class Meta: verbose_name = _('Aid') verbose_name_plural = _('Aids') indexes = [ GinIndex(fields=['search_vector']), ] def set_slug(self): """Set the object's slug. Lots of aids have duplicate name, so we prefix the slug with random characters.""" if not self.id: full_title = '{}-{}'.format(str(uuid4())[:4], self.name) self.slug = slugify(full_title)[:50] def set_publication_date(self): """Set the object's publication date. We set the first publication date once and for all when the aid is first published. """ if self.is_published() and self.date_published is None: self.date_published = timezone.now() def set_search_vector(self, financers=None, instructors=None): """Update the full text cache field.""" # Note: we use `SearchVector(Value(self.field))` instead of # `SearchVector('field')` because the latter only works for updates, # not when inserting new records. # # Note 2: we have to pass the financers parameter instead of using # `self.financers.all()` because that last expression would not work # during an object creation. search_vector = \ SearchVector( Value(self.name, output_field=models.CharField()), weight='A', config='french') + \ SearchVector( Value(self.eligibility, output_field=models.CharField()), weight='D', config='french') + \ SearchVector( Value(self.description, output_field=models.CharField()), weight='B', config='french') + \ SearchVector( Value(' '.join(self.tags), output_field=models.CharField()), weight='A', config='french') if financers: search_vector += SearchVector(Value( ' '.join(str(backer) for backer in financers), output_field=models.CharField()), weight='D', config='french') if instructors: search_vector += SearchVector(Value( ' '.join(str(backer) for backer in instructors), output_field=models.CharField()), weight='D', config='french') self.search_vector = search_vector def populate_tags(self): """Populates the `_tags_m2m` field. cache `_tags_m2m` field with values from the `tags` field. Tag that do not exist will be created. """ all_tag_names = self.tags existing_tag_objects = Tag.objects.filter(name__in=all_tag_names) existing_tag_names = [tag.name for tag in existing_tag_objects] missing_tag_names = list(set(all_tag_names) - set(existing_tag_names)) new_tags = [Tag(name=tag) for tag in missing_tag_names] new_tag_objects = Tag.objects.bulk_create(new_tags) all_tag_objects = list(existing_tag_objects) + list(new_tag_objects) self._tags_m2m.set(all_tag_objects, clear=True) def save(self, *args, **kwargs): self.set_slug() self.set_publication_date() return super().save(*args, **kwargs) def __str__(self): return self.name def get_absolute_url(self): return reverse('aid_detail_view', args=[self.slug]) def get_admin_url(self): return reverse('admin:aids_aid_change', args=[self.id]) def is_draft(self): return self.status == AidWorkflow.states.draft def is_under_review(self): return self.status == AidWorkflow.states.reviewable def is_published(self): return self.status == AidWorkflow.states.published def is_financial(self): """Does this aid have financial parts?""" aid_types = self.aid_types or [] return bool(set(aid_types) & set(self.FINANCIAL_AIDS)) def is_technical(self): """Does this aid have technical parts?""" aid_types = self.aid_types or [] return bool(set(aid_types) & set(self.TECHNICAL_AIDS)) def is_ongoing(self): return self.recurrence == self.RECURRENCE.ongoing def has_approaching_deadline(self): if not self.submission_deadline: return False delta = self.submission_deadline - timezone.now().date() return delta.days <= settings.APPROACHING_DEADLINE_DELTA def has_expired(self): if not self.submission_deadline: return False today = timezone.now().date() return self.submission_deadline < today def is_live(self): """True if the aid must be displayed on the site.""" return self.is_published() and not self.has_expired()
class Organization(models.Model): ORGANIZATION_TYPE = Choices(*AUDIENCES_ALL) name = models.CharField( 'Nom', max_length=256, db_index=True) slug = models.SlugField( "Fragment d'URL", help_text='Laisser vide pour autoremplir.', blank=True) organization_type = ChoiceArrayField( verbose_name="Type de structure", null=True, blank=True, base_field=models.CharField( max_length=32, choices=ORGANIZATION_TYPE)) address = models.CharField( 'Adresse postale', max_length=900, null=True, blank=True) city_name = models.CharField( 'Nom de la ville', max_length=256, null=True, blank=True) zip_code = models.PositiveIntegerField( 'Code postal', null=True, blank=True) siren_code = models.BigIntegerField( 'Code SIREN', null=True, blank=True) siret_code = models.BigIntegerField( 'Code SIRET', null=True, blank=True) ape_code = models.CharField( 'Code APE', max_length=5, null=True, blank=True) inhabitants_number = models.PositiveIntegerField( "Nombre d'habitants", null=True, blank=True) voters_number = models.PositiveIntegerField( 'Nombre de votants', null=True, blank=True) corporates_number = models.PositiveIntegerField( "Nombre d'entreprises", null=True, blank=True) associations_number = models.PositiveIntegerField( "Nombre d'associations", null=True, blank=True) municipal_roads = models.PositiveIntegerField( "Routes communales (kms)", null=True, blank=True) departmental_roads = models.PositiveIntegerField( "Routes départementales (kms)", null=True, blank=True) tram_roads = models.PositiveIntegerField( "Tramways (kms)", null=True, blank=True) lamppost_number = models.PositiveIntegerField( "Nombre de lampadaires", null=True, blank=True) library_number = models.PositiveIntegerField( "Nombre de bibliothèques", null=True, blank=True) medialibrary_number = models.PositiveIntegerField( "Nombre de mediathèques", null=True, blank=True) theater_number = models.PositiveIntegerField( "Nombre de théâtres", null=True, blank=True) museum_number = models.PositiveIntegerField( "Nombre de musées", null=True, blank=True) kindergarten_number = models.PositiveIntegerField( "Nombre d'écoles maternelles", null=True, blank=True) primary_school_number = models.PositiveIntegerField( "Nombre d'écoles primaires", null=True, blank=True) middle_school_number = models.PositiveIntegerField( "Nombre de collèges", null=True, blank=True) high_school_number = models.PositiveIntegerField( "Nombre de lycées", null=True, blank=True) university_number = models.PositiveIntegerField( "Nombre d'universités", null=True, blank=True) gymnasium_number = models.PositiveIntegerField( "Nombre de gymnases et salles de sport", null=True, blank=True) sports_ground_number = models.PositiveIntegerField( "Nombre de stades et structures extérieures", null=True, blank=True) swimming_pool_number = models.PositiveIntegerField( "Nombre de piscines", null=True, blank=True) place_of_worship_number = models.PositiveIntegerField( "Nombre de lieux de cultes", null=True, blank=True) cemetery_number = models.PositiveIntegerField( "Nombre de cimetières", null=True, blank=True) beneficiaries = models.ManyToManyField( 'accounts.User', verbose_name='Bénéficiaires', blank=True) projects = models.ManyToManyField( 'projects.Project', verbose_name='Projets', blank=True) perimeter = models.ForeignKey( 'geofr.Perimeter', verbose_name="Périmètre de la structure", on_delete=models.PROTECT, help_text="Sur quel périmètre la structure intervient-elle ?", null=True, blank=True) date_created = models.DateTimeField( 'Date de création', default=timezone.now) date_updated = models.DateTimeField( 'Date de mise à jour', auto_now=True) class Meta: verbose_name = 'Structure' verbose_name_plural = 'Structures' def __str__(self): return self.name def set_slug(self): """Set the object's slug if it is missing.""" if not self.slug: self.slug = slugify(self.name)[:50] def save(self, *args, **kwargs): self.set_slug() return super().save(*args, **kwargs)
class Survey(TimeStampedModel): """ Survey model class Defines set of unique questions, that respondent encounters when taking it, with the purpose to understand data flow hierarchies, data topics, data sets, data sharing entities, data storage mechanisms and data access permissions. If the survey has :attr:`~login_required` set to ``True`` only logged in users should be allowed to respond to the survey. If the survey has :attr:`~invitation_required` set to ``True`` only users only invited users should be allowed to respond to the survey. Currently this only registered users can be invitees by being pre-added to the respondent list but this will change in future. """ YES_NO_CHOICES = ((True, _('Yes')), (False, _('No'))) #: Global unique identifier for a survey. uuid = models.UUIDField(_('UUID'), default=uuid.uuid4, editable=False, unique=True) #: Project under which a survey belongs to. project = models.ForeignKey('projects.project', related_name='surveys', related_query_name='survey', verbose_name=_('project'), on_delete=models.CASCADE) #: User who created(or owning) a survey creator = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('creator'), blank=True, related_name='created_surveys', related_query_name='created_survey', on_delete=models.CASCADE) #: Internal used survey name. name = models.CharField(_('name'), max_length=255) #: Human readable, brief details about survey. description = models.TextField(_('description')) #: Human readable, survey alternative name for respondents. display_name = models.CharField( _('alternative name'), help_text=_('Use this optional field to provide the survey name as ' 'Respondents will see it.'), max_length=255, blank=True) #: Accompanies survey research questions. research_question = models.CharField( _('research question'), help_text=_('Every Data Compass survey must have a specific research ' 'question. What is yours?'), max_length=255) #: Survey languages for translations. languages = ChoiceArrayField( models.CharField( max_length=5, choices=settings.LANGUAGES, ), help_text=_('By default, all surveys have an English version. ' 'If your survey will be in other languages, select or ' 'add them here. You will provide translations later.'), verbose_name=_('languages'), blank=True, default=list) #: A slug for the survey. code = models.SlugField(_('code'), blank=True, allow_unicode=True) #: Flag whether respondents should not be linked with system hierachy levels. dont_link_hierarchy_levels = models.BooleanField( _('do not link respondents with system hierarchy levels'), help_text= _('This can be easier in some contexts, but will limit aggregate or comparative analyses.' ), blank=True, default=False, choices=YES_NO_CHOICES) #: Flag whether respondents can add their own hierarchy level. allow_respondent_hierarchy_levels = models.BooleanField( _('allow respondent hierarchy levels'), help_text=_("Respondents will select from the List you provided " "for each level. If they can't find theirs, they can " "add their own?"), blank=True, default=False, choices=YES_NO_CHOICES) #: Flag whether respondents can add their own roles. allow_respondent_roles = models.BooleanField( _('allow respondent roles'), help_text=_( 'If Yes, respondents will be able to add their own roles.'), blank=True, default=False, choices=YES_NO_CHOICES) #: Flag whether survey respondents must login. login_required = models.BooleanField( _('login required'), help_text=_("If no, they won't be able to save and return to their " "responses, or view previous responses."), blank=True, default=True, choices=YES_NO_CHOICES) #: Flag whether survey respondents must be envited. invitation_required = models.BooleanField( _('invitation required'), help_text=_("If no, anyone with the survey link can respond to it."), blank=True, default=True, choices=YES_NO_CHOICES) #: Flag wether respondent can see others responses. respondent_can_aggregate = models.BooleanField( _('respondent can aggregate'), help_text=_("'Yes', will update their networ visual with all users' " "responses in realtime. 'No' will not."), blank=True, default=True, choices=YES_NO_CHOICES) #: Flag wether respondent can invite others. respondent_can_invite = models.BooleanField( _('respondent can suggest others'), help_text=_('If Yes, the survey will include question collecting ' 'email address. Respondents are responsible for ' 'ensuring consent.'), blank=True, default=True, choices=YES_NO_CHOICES) #: Flag whether respondents can add their own topics. allow_respondent_topics = models.BooleanField( _('allow respondent topics'), help_text=_( 'If Yes, respondents will be able to add their own topics.'), blank=True, default=False, choices=YES_NO_CHOICES) #: Number of topics respondent have to complete for a survey respondent_topic_number = models.PositiveSmallIntegerField( _('respondent topic number'), help_text=_('Up to 10 topics are allowed'), blank=False, default=10, validators=[MinValueValidator(1), MaxValueValidator(10)]) #: Flag whether respondents can add their own datasets. allow_respondent_datasets = models.BooleanField( _('allow respondent datasets'), help_text= _('If Yes, respondents will be able to add their own datasets. This is not recommended.' ), blank=True, default=False, choices=YES_NO_CHOICES) #: Flag whether respondents can add their own entities. allow_respondent_entities = models.BooleanField( _('allow respondent entities'), help_text=_( 'If Yes, respondents will be able to add their own entities.'), blank=True, default=False, choices=YES_NO_CHOICES) #: Flag whether respondents can add their own storages. allow_respondent_storages = models.BooleanField( _('allow respondent storages'), help_text=_( 'If Yes, respondents will be able to add their own storages.'), blank=True, default=False, choices=YES_NO_CHOICES) #: Human readable, header(introductory information) of survey. introduction_text = models.TextField( _('introduction text'), help_text= _('What text do you want to appear when a respondent begins the survey?' ), default='') #: Human readable, footer(closing information) of survey. closing_text = models.TextField( _('closing text'), help_text=_( 'What text do you want to appear when a respondent ends the survey?' ), default='') #: Flag is survey is published. is_active = models.BooleanField(_('is active'), help_text=_('Is published'), blank=True, default=False) #: Gender used in various parts of the survey. # #: ``post_save`` signal is used to automatially add primary genders on new #: instances with no pre-assigned genders. #: Users (`facilitators`) could be allowed to add or remove primary genders #: on a survey but shouldn't be allowed to modify attributes #: of an individual primary gender. genders = models.ManyToManyField( 'users.Gender', verbose_name=_('genders'), related_name='surveys', related_query_name='survey', blank=True, ) #: Flag whether survey collect respondent(s) email address. allow_collect_email = models.BooleanField(_('collect email address'), default=True) #: Flag whether survey collect respondent(s) name. allow_collect_name = models.BooleanField(_('collect name'), default=True) #: Flag whether survey collect respondent(s) gender. allow_collect_gender = models.BooleanField(_('collect gender'), default=True) #: Extra survey fields. extras = JSONField(_('extras'), blank=True, default=dict) #: Default manager. objects = SurveyManager() class Meta: verbose_name = _('Survey') verbose_name_plural = _('Surveys') ordering = ['-created_at'] def __str__(self): """Returns string representation of survey""" return self.display_name def save(self, *args, **kwargs): """Save the survey.""" if not self.code: self.code = slugify(self.name[:50]) if not self.display_name: self.display_name = self.name super().save(*args, **kwargs) def get_or_create_respondent(self, user=None, email=None): """ Get or create respondent. Returns: (respondent, created). If a new respondent was created in the process the second value of the tuple (created) will be ``True``. """ # if login/user is not required if not self.login_required: if user and user.is_authenticated: return self.respondents.get_or_create(user=user, survey=self) elif email: return self.respondents.get_or_create(email=email, survey=self) respondent = self.respondents.create(survey=self) return (respondent, True) # user is required if not user.is_authenticated: raise PermissionDenied(_('A user is required')) return self.respondents.get_or_create(user=user, survey=self) def available_for_respondent(self, respondent): """ Checks if the survey is available for respondent. Args: respondent: :class:`.Respondent` object Returns: bool: True if survey can be taken by respondent otherwise False. """ user = None if respondent: user = respondent.user if user is None and (self.login_required or self.invitation_required): return False if self.invitation_required and not self.respondents.filter( user=user).exists(): return False # Make sure user is not AnonymousUser if user.is_authenticated: return True return False def get_absolute_url(self): """Obtain survey absolute url.""" return reverse('surveys:survey-detail', kwargs={'pk': self.pk}) # Derive survey name abbreviation @property def abbreviation(self): parts = [] words = self.name.split(' ') for word in words: parts.append(str(word[0])) return ''.join(parts[:2])
class Aid(xwf_models.WorkflowEnabled, models.Model): """Represents a single Aid.""" TYPES = Choices(*TYPES_ALL) STEPS = Choices( ('preop', 'Réflexion / conception'), ('op', 'Mise en œuvre / réalisation'), ('postop', 'Usage / valorisation'), ) AUDIENCES = Choices(*AUDIENCES_ALL) DESTINATIONS = Choices( ('supply', 'Dépenses de fonctionnement'), ('investment', "Dépenses d'investissement"), ) RECURRENCES = Choices( ('oneoff', 'Ponctuelle'), ('ongoing', 'Permanente'), ('recurring', 'Récurrente'), ) objects = ExistingAidsManager() all_aids = AidQuerySet.as_manager() deleted_aids = DeletedAidsManager() amendments = AmendmentManager() slug = models.SlugField("Fragment d'URL", help_text='Laisser vide pour autoremplir.', blank=True) name = models.CharField( 'Nom', max_length=180, help_text= "Le titre doit commencer par un verbe à l’infinitif pour que l'objectif de l'aide soit explicite vis-à-vis de ses bénéficiaires.", # noqa null=False, blank=False) name_initial = models.CharField( 'Nom initial', max_length=180, help_text= "Comment cette aide s’intitule-t-elle au sein de votre structure ? Exemple : AAP Mob’Biodiv", # noqa null=True, blank=True) short_title = models.CharField( 'Titre court', max_length=64, help_text='Un titre plus concis, pour affichage spécifique.', blank=True) author = models.ForeignKey('accounts.User', verbose_name='Auteur', on_delete=models.PROTECT, related_name='aids', help_text='Qui renseigne cette aide ?', null=True) categories = models.ManyToManyField('categories.Category', verbose_name='Sous-thématiques', related_name='aids', blank=True) financers = models.ManyToManyField('backers.Backer', verbose_name="Porteurs d'aides", through=AidFinancer, related_name='financed_aids') financer_suggestion = models.CharField("Porteurs d'aides suggérés", max_length=256, blank=True) instructors = models.ManyToManyField('backers.Backer', verbose_name='Instructeurs', through=AidInstructor, related_name='instructed_aids', blank=True) instructor_suggestion = models.CharField('Instructeurs suggérés', max_length=256, blank=True) description = models.TextField( "Description complète de l'aide et de ses objectifs", blank=False) project_examples = models.TextField('Exemples de projets réalisables', default='', blank=True) projects = models.ManyToManyField('projects.Project', through='AidProject', verbose_name='Projets', blank=True) eligibility = models.TextField('Éligibilité', blank=True) perimeter = models.ForeignKey( 'geofr.Perimeter', verbose_name='Périmètre', on_delete=models.PROTECT, help_text="Sur quel périmètre l'aide est-elle diffusée ?", null=True, blank=True) perimeter_suggestion = models.CharField('Périmètre suggéré', max_length=256, null=True, blank=True) mobilization_steps = ChoiceArrayField( verbose_name= "État d'avancement du projet pour bénéficier du dispositif", null=True, blank=True, base_field=models.CharField(max_length=32, choices=STEPS, default=STEPS.preop)) origin_url = models.URLField("URL d'origine", max_length=500, blank=True) application_url = models.URLField("Lien vers une démarche en ligne", max_length=500, blank=True) targeted_audiences = ChoiceArrayField( verbose_name="Bénéficiaires de l'aide", null=True, blank=True, base_field=models.CharField(max_length=32, choices=AUDIENCES)) aid_types = ChoiceArrayField( verbose_name="Types d'aide", null=True, blank=True, base_field=models.CharField(max_length=32, choices=TYPES), help_text="Précisez le ou les types de l'aide.") is_generic = models.BooleanField( 'Aide générique ?', help_text='Cette aide est-elle générique ?', default=False) generic_aid = models.ForeignKey( 'aids.Aid', verbose_name='Aide générique', on_delete=models.CASCADE, related_name='local_aids', limit_choices_to={'is_generic': True}, help_text='Aide générique associée à une aide locale.', null=True, blank=True) local_characteristics = models.TextField('Spécificités locales', blank=True) destinations = ChoiceArrayField( verbose_name='Types de dépenses / actions couvertes', null=True, blank=True, base_field=models.CharField(max_length=32, choices=DESTINATIONS)) start_date = models.DateField( "Date d'ouverture", help_text="À quelle date l'aide est-elle ouverte aux candidatures ?", null=True, blank=True) predeposit_date = models.DateField( 'Date de pré-dépôt', help_text= "Quelle est la date de pré-dépôt des dossiers, si applicable ?", null=True, blank=True) submission_deadline = models.DateField( 'Date de clôture', help_text="Quelle est la date de clôture de dépôt des dossiers ?", null=True, blank=True) subvention_rate = PercentRangeField( "Taux de subvention, min. et max. (en %, nombre entier)", help_text="Si le taux est fixe, remplissez uniquement le taux max.", null=True, blank=True) subvention_comment = models.CharField( "Taux de subvention (commentaire optionnel)", max_length=100, blank=True) recoverable_advance_amount = models.PositiveIntegerField( 'Montant de l\'avance récupérable', null=True, blank=True) loan_amount = models.PositiveIntegerField('Montant du prêt', null=True, blank=True) other_financial_aid_comment = models.CharField( 'Autre aide financière (commentaire optionnel)', max_length=100, blank=True) contact = models.TextField('Contact', blank=True) contact_email = models.EmailField('Adresse e-mail de contact', blank=True) contact_phone = models.CharField('Numéro de téléphone', max_length=35, blank=True) contact_detail = models.CharField('Contact (détail)', max_length=256, blank=True) recurrence = models.CharField( 'Récurrence', help_text="L'aide est-elle ponctuelle, permanente, ou récurrente ?", max_length=16, choices=RECURRENCES, blank=True) is_call_for_project = models.BooleanField( "Appel à projet / Manifestation d'intérêt", null=True) programs = models.ManyToManyField('programs.Program', verbose_name='Programmes', related_name='aids', blank=True) status = xwf_models.StateField(AidWorkflow, verbose_name='Statut') # Eligibility eligibility_test = models.ForeignKey('eligibility.EligibilityTest', verbose_name="Test d'éligibilité", on_delete=models.PROTECT, related_name='aids', null=True, blank=True) # Dates date_created = models.DateTimeField('Date de création', default=timezone.now) date_updated = models.DateTimeField('Date de mise à jour', auto_now=True) date_published = models.DateTimeField('Première date de publication', null=True, blank=True) # Specific to France Relance features in_france_relance = models.BooleanField( 'France Relance ?', help_text='Cette aide est-elle éligible au programme France Relance ?', default=False) # Disable send_publication_email's task author_notification = models.BooleanField( "Envoyer un email à l'auteur de l'aide ?", help_text="Un email doit-il être envoyé à l'auteur de cette aide \ au moment de sa publication ?", default=True) # Third-party data import related fields is_imported = models.BooleanField('Importé ?', default=False) import_data_source = models.ForeignKey('dataproviders.DataSource', verbose_name='Source de données', on_delete=models.PROTECT, related_name='aids', null=True) # Even if this field is a CharField, we make it nullable with `null=True` # because null values are not taken into account by postgresql when # enforcing the `unique` constraint, which is very handy for us. import_uniqueid = models.CharField("Identifiant d'import unique", max_length=200, unique=True, null=True, blank=True) import_data_url = models.URLField("URL d'origine de la donnée importée", null=True, blank=True) import_share_licence = models.CharField( "Sous quelle licence cette aide a-t-elle été partagée ?", max_length=50, choices=IMPORT_LICENCES, blank=True) import_last_access = models.DateField('Date du dernier accès', null=True, blank=True) import_raw_object = models.JSONField('Donnée JSON brute', editable=False, null=True) # This field is used to index searchable text content search_vector_unaccented = SearchVectorField('Search vector unaccented', null=True) # Those fields handle the "aid amendment" feature # Users, including anonymous, can suggest amendments to existing aids. # We store a suggested edit as a clone of the original aid, with the # following field as True. is_amendment = models.BooleanField('Est un amendement', default=False) amended_aid = models.ForeignKey('aids.Aid', verbose_name='Aide amendée', on_delete=models.CASCADE, null=True) amendment_author_name = models.CharField("Auteur de l'amendement", max_length=256, blank=True) amendment_author_email = models.EmailField( "E-mail de l'auteur de l'amendement", null=True, blank=True) amendment_author_org = models.CharField( "Structure de l'auteur de l'amendement", max_length=255, blank=True) amendment_comment = models.TextField('Commentaire', blank=True) class Meta: verbose_name = 'Aide' verbose_name_plural = 'Aides' indexes = [ GinIndex(fields=['search_vector_unaccented']), ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # We store here the current status as we need to check if it # has change - check what we do when saving the Aid instance. self.original_status = self.status def set_slug(self): """Set the object's slug. Lots of aids have duplicate name, so we prefix the slug with random characters.""" if not self.id: full_title = '{}-{}'.format(str(uuid4())[:4], self.name) self.slug = slugify(full_title)[:50] def set_publication_date(self): """Set the object's publication date. We set the first publication date once and for all when the aid is first published. """ if self.is_published() and self.date_published is None: self.date_published = timezone.now() def set_search_vector_unaccented(self, financers=None, instructors=None): """Update the full text unaccented cache field.""" # Note: we use `SearchVector(Value(self.field))` instead of # `SearchVector('field')` because the latter only works for updates, # not when inserting new records. # # Note 2: we have to pass the financers parameter instead of using # `self.financers.all()` because that last expression would not work # during an object creation. search_vector_unaccented = \ SearchVector( Value(self.name, output_field=models.CharField()), weight='A', config='french_unaccent') + \ SearchVector( Value(self.name_initial, output_field=models.CharField()), weight='A', config='french_unaccent') + \ SearchVector( Value(self.description, output_field=models.CharField()), weight='B', config='french_unaccent') + \ SearchVector( Value(self.project_examples, output_field=models.CharField()), weight='B', config='french_unaccent') + \ SearchVector( Value(self.eligibility, output_field=models.CharField()), weight='D', config='french_unaccent') if financers: search_vector_unaccented += SearchVector(Value( ' '.join(str(backer) for backer in financers), output_field=models.CharField()), weight='D', config='french_unaccent') if instructors: search_vector_unaccented += SearchVector(Value( ' '.join(str(backer) for backer in instructors), output_field=models.CharField()), weight='D', config='french_unaccent') self.search_vector_unaccented = search_vector_unaccented def save(self, *args, **kwargs): self.set_slug() self.set_publication_date() is_new = not self.id # There's no ID => newly created aid is_being_published = self.is_published() and self.status_has_changed() if not is_new and is_being_published and self.author_notification and not self.is_imported: send_publication_email.delay(aid_id=self.id) return super().save(*args, **kwargs) def __str__(self): return self.name def get_absolute_url(self): return reverse('aid_detail_view', args=[self.slug]) def get_admin_url(self): return reverse('admin:aids_aid_change', args=[self.id]) def get_sorted_local_aids(self): return self.local_aids.live() \ .select_related('perimeter') \ .order_by('perimeter__name') def is_draft(self): return self.status == AidWorkflow.states.draft def is_under_review(self): return self.status == AidWorkflow.states.reviewable def is_published(self): return self.status == AidWorkflow.states.published def status_has_changed(self): return self.original_status != self.status def is_financial(self): """Does this aid have financial parts?""" aid_types = self.aid_types or [] return bool(set(aid_types) & set(FINANCIAL_AIDS_LIST)) def is_grant(self): """Does this aid is a grant?""" aid_types = self.aid_types or [] return bool(set(aid_types) & set((('grant', 'Subvention')))) def is_loan(self): """Does this aid is a loan?""" aid_types = self.aid_types or [] return bool(set(aid_types) & set((('loan', 'Prêt')))) def is_technical(self): """Does this aid have technical parts?""" aid_types = self.aid_types or [] return bool(set(aid_types) & set(TECHNICAL_AIDS_LIST)) def is_ongoing(self): return self.recurrence == self.RECURRENCES.ongoing def has_calendar(self): """Does the aid has valid calendar data?.""" if self.is_ongoing(): return False return any( (self.start_date, self.predeposit_date, self.submission_deadline)) def has_approaching_deadline(self): if self.is_ongoing() or not self.submission_deadline: return False delta = self.submission_deadline - timezone.now().date() return delta.days >= 0 \ and delta.days <= settings.APPROACHING_DEADLINE_DELTA def days_before_deadline(self): if not self.submission_deadline or self.is_ongoing(): return None today = timezone.now().date() deadline_delta = self.submission_deadline - today return deadline_delta.days def is_coming_soon(self): if not self.start_date: return False today = timezone.now().date() return self.start_date > today def has_expired(self): if not self.submission_deadline: return False today = timezone.now().date() return self.submission_deadline < today def is_live(self): """True if the aid must be displayed on the site.""" return self.is_published() and not self.has_expired() def has_projects(self): return self.projects is not None def get_live_status_display(self): status = 'Affichée' if self.is_live() else 'Non affichée' return status def has_eligibility_test(self): return self.eligibility_test is not None def is_local(self): return self.generic_aid is not None def is_corporate_aid(self): return (self.targeted_audiences and self.AUDIENCES.private_sector in self.targeted_audiences) def clone_m2m(self, source_aid): """ Clones the many-to-many fields for the the given source aid. """ m2m_fields = self._meta.many_to_many projects_field = self._meta.get_field('projects') for field in m2m_fields: if field != projects_field: for item in field.value_from_object(source_aid): getattr(self, field.attname).add(item) self.save()
class SearchPage(models.Model): """A single search result page with additional data. A customized search page is a pre-filtered search page with it's own url, configurable titles, descriptions, etc. and built for navigation and seo purpose. """ objects = SearchPageQuerySet.as_manager() title = models.CharField(_('Title'), max_length=180, help_text=_('The main displayed title.')) short_title = models.CharField( _('Short title'), max_length=180, blank=True, default='', help_text=_('A shorter, more concise title.')) slug = models.SlugField(_('Slug'), help_text=_( 'This part is used in the url. ' 'DON\'t change this for existing pages. ' 'MUST be lowercase for minisites.')) content = models.TextField(_('Page content'), help_text=_('Full description of the page. ' 'Will be displayed above results.')) more_content = models.TextField( _('Additional page content'), blank=True, help_text=_('Hidden content, revealed with a `See more` button')) tab_title = models.CharField("Titre de l'onglet principal", blank=True, default='Accueil', max_length=180) search_querystring = models.TextField( _('Querystring'), help_text=_('The search paramaters url')) administrator = models.ForeignKey('accounts.User', on_delete=models.PROTECT, verbose_name='Administrateur', related_name='search_pages', null=True, blank=True) highlighted_aids = models.ManyToManyField( 'aids.Aid', verbose_name=_('Highlighted aids'), related_name='highlighted_in_search_pages', blank=True) excluded_aids = models.ManyToManyField( 'aids.Aid', verbose_name=_('Excluded aids'), related_name='excluded_from_search_pages', blank=True) # SEO meta_title = models.CharField( _('Meta title'), max_length=180, blank=True, default='', help_text=_('This will be displayed in SERPs. ' 'Keep it under 60 characters. ' 'Leave empty and we will reuse the page title.')) meta_description = models.TextField(_('Meta description'), blank=True, default='', max_length=256, help_text=_( 'This will be displayed in SERPs. ' 'Keep it under 120 characters.')) meta_image = models.FileField( _('Meta image'), null=True, blank=True, upload_to=meta_upload_to, help_text=_('Make sure the file is at least 1024px long.')) # custom_colors color_1 = models.CharField(_('Color 1'), max_length=10, blank=True, help_text=_('Main background color')) color_2 = models.CharField(_('Color 2'), max_length=10, blank=True, help_text=_('Search form background color')) color_3 = models.CharField(_('Color 3'), max_length=10, blank=True, help_text=_('Buttons and title borders color')) color_4 = models.CharField(_('Color 4'), max_length=10, blank=True, help_text=_('Link colors')) color_5 = models.CharField(_('Color 5'), max_length=10, blank=True, help_text=_('Footer background color')) logo = models.FileField( _('Logo image'), null=True, blank=True, upload_to=logo_upload_to, help_text=_('Make sure the file is not too heavy. Prefer svg files.')) logo_link = models.URLField( _('Logo link'), null=True, blank=True, help_text=_('The url for the partner\'s logo link')) # Search form customization fields show_categories_field = models.BooleanField( 'Montrer le champ « thématiques » ?', default=True) available_categories = models.ManyToManyField('categories.Category', verbose_name=_('Categories'), related_name='search_pages', blank=True) show_audience_field = models.BooleanField( 'Montrer le champ « structure » ?', default=True) available_audiences = ChoiceArrayField( verbose_name=_('Targeted audiences'), null=True, blank=True, base_field=models.CharField(max_length=32, choices=AUDIENCES_GROUPED)) show_perimeter_field = models.BooleanField( 'Montrer le champ « territoire » ?', default=True) show_backers_field = models.BooleanField('Montrer le champ « porteur » ?', default=False) show_mobilization_step_field = models.BooleanField( 'Montrer le champ « avancement du projet » ?', default=False) show_text_field = models.BooleanField( 'Montrer le champ « recherche textuelle » ?', default=False) show_aid_type_field = models.BooleanField( "Montrer le champ « nature de l'aide » ?", default=False) date_created = models.DateTimeField(_('Date created'), default=timezone.now) date_updated = models.DateTimeField(_('Date updated'), auto_now=True) class Meta: verbose_name = "page personnalisée" verbose_name_plural = "pages personnalisées" def __str__(self): return self.title def get_absolute_url(self): return reverse('search_page', args=[self.slug]) def get_base_querystring_data(self): # Sometime, the admin person enters a prefix "?" character # and we don't want it here. querystring = self.search_querystring.strip('?') data = QueryDict(querystring) return data def get_base_queryset(self, all_aids=False): """Return the list of aids based on the initial search querysting.""" from aids.forms import AidSearchForm data = self.get_base_querystring_data() form = AidSearchForm(data) if all_aids: qs = form.filter_queryset( qs=Aid.objects.all(), apply_generic_aid_filter=False).distinct() else: qs = form.filter_queryset( apply_generic_aid_filter=False).distinct() # Annotate aids contained in the highlighted_aids field # This field will be helpful to order the queryset # source: https://stackoverflow.com/a/44048355 highlighted_aids_id_list = self.highlighted_aids.values_list( 'id', flat=True) # noqa qs = qs.annotate(is_highlighted_aid=Count( Case(When(id__in=highlighted_aids_id_list, then=1), output_field=IntegerField()))) # Simpler approach, but error-prone (aid could be highlighted in another SearchPage) # noqa # qs = qs.annotate(is_highlighted_aid=Count('highlighted_in_search_pages')) # noqa # Also exlude aids contained in the excluded_aids field excluded_aids_id_list = self.excluded_aids.values_list('id', flat=True) qs = qs.exclude(id__in=excluded_aids_id_list) return qs def get_aids_per_status(self): all_aids_per_status = self.get_base_queryset(all_aids=True) \ .values('status') \ .annotate(count=Count('id', distinct=True)) return {s['status']: s['count'] for s in list(all_aids_per_status)}
class SearchPage(models.Model): """A single search result page with additional data. A customized search page is a pre-filtered search page with it's own url, configurable titles, descriptions, etc. and built for navigation and seo purpose. """ title = models.CharField(_('Title'), max_length=180, help_text=_('The main displayed title.')) short_title = models.CharField( _('Short title'), max_length=180, blank=True, default='', help_text=_('A shorter, more concise title.')) slug = models.SlugField(_('Slug'), help_text=_( 'This part is used in the url. ' 'DON\'t change this for existing pages. ' 'MUST be lowercase for minisites.')) content = models.TextField(_('Page content'), help_text=_('Full description of the page. ' 'Will be displayed above results.')) more_content = models.TextField( _('Additional page content'), blank=True, help_text=_('Hidden content, revealed with a `See more` button')) search_querystring = models.TextField( _('Querystring'), help_text=_('The search paramaters url')) excluded_aids = models.ManyToManyField( 'aids.Aid', verbose_name=_('Excluded aids'), related_name='excluded_from_search_pages', blank=True) # SEO meta_title = models.CharField( _('Meta title'), max_length=180, blank=True, default='', help_text=_('This will be displayed in SERPs. ' 'Keep it under 60 characters. ' 'Leave empty and we will reuse the page title.')) meta_description = models.TextField(_('Meta description'), blank=True, default='', max_length=256, help_text=_( 'This will be displayed in SERPs. ' 'Keep it under 120 characters.')) meta_image = models.FileField( _('Meta image'), null=True, blank=True, upload_to=meta_upload_to, help_text=_('Make sure the file is at least 1024px long.')) # custom_colors color_1 = models.CharField(_('Color 1'), max_length=10, blank=True, help_text=_('Main background color')) color_2 = models.CharField(_('Color 2'), max_length=10, blank=True, help_text=_('Search form background color')) color_3 = models.CharField(_('Color 3'), max_length=10, blank=True, help_text=_('Buttons and title borders color')) color_4 = models.CharField(_('Color 4'), max_length=10, blank=True, help_text=_('Link colors')) color_5 = models.CharField(_('Color 5'), max_length=10, blank=True, help_text=_('Footer background color')) logo = models.FileField( _('Logo image'), null=True, blank=True, upload_to=logo_upload_to, help_text=_('Make sure the file is not too heavy. Prefer svg files.')) logo_link = models.URLField( _('Logo link'), null=True, blank=True, help_text=_('The url for the partner\'s logo link')) # Search form customization fields show_categories_field = models.BooleanField(_('Show categories field?'), default=True) available_categories = models.ManyToManyField('categories.Category', verbose_name=_('Categories'), related_name='search_pages', blank=True) show_audience_field = models.BooleanField(_('Show audience field?'), default=True) available_audiences = ChoiceArrayField( verbose_name=_('Targeted audiences'), null=True, blank=True, base_field=models.CharField(max_length=32, choices=AUDIENCES)) show_perimeter_field = models.BooleanField(_('Show perimeter field?'), default=True) show_mobilization_step_field = models.BooleanField( _('Show mobilization step filter?'), default=False) show_aid_type_field = models.BooleanField(_('Show aid type filter?'), default=False) date_created = models.DateTimeField(_('Date created'), default=timezone.now) date_updated = models.DateTimeField(_('Date updated'), auto_now=True) class Meta: verbose_name = _('Search page') verbose_name_plural = _('Search pages') def __str__(self): return self.title def get_absolute_url(self): return reverse('search_page', args=[self.slug]) def get_base_queryset(self, all_aids=False): """Return the list of aids based on the initial search querysting.""" from aids.forms import AidSearchForm # Sometime, the admin person enters a prefix "?" character # and we don't want it here. querystring = self.search_querystring.strip('?') data = QueryDict(querystring) form = AidSearchForm(data) if all_aids: qs = form.filter_queryset(Aid.objects.all()).distinct() else: qs = form.filter_queryset().distinct() # Also exlude aids contained in the excluded_aids field qs = qs.exclude(id__in=self.excluded_aids.values_list('id', flat=True)) return qs def get_aids_per_status(self): all_aids_per_status = self.get_base_queryset(all_aids=True) \ .values('status') \ .annotate(count=Count('id', distinct=True)) return {s['status']: s['count'] for s in list(all_aids_per_status)}