class Meta: model = models.ReviewRequest fields = [ 'title', 'due_date', 'target_registration_state', 'registration_date', 'concepts', 'cascade_registration' ] widgets = { 'title': forms.Textarea(attrs={"rows": "1"}), 'target_registration_state': forms.Select, 'due_date': BootstrapDateTimePicker(options={"format": "YYYY-MM-DD"}), 'registration_date': BootstrapDateTimePicker(options={"format": "YYYY-MM-DD"}), 'concepts': ConceptAutocompleteSelectMultiple(), 'cascade_registration': forms.RadioSelect(), } help_texts = { 'target_registration_state': "The state for endorsement for metadata in this review", 'due_date': "Date this review needs to be actioned by", 'registration_date': "Date the metadata will be endorsed at", 'title': "A short title for this review", 'concepts': "List of metadata for review", 'cascade_registration': "Include related items when registering metadata. When enabled, see the full list of metadata under the \"impact\" tab.", }
class EditStatusForm(ModelForm): class Meta: model = MDR.Status fields = ['registrationDate', 'until_date', 'state', 'changeDetails'] registrationDate = forms.DateField( required=False, label=_("Registration date"), widget=BootstrapDateTimePicker(options={"format": "YYYY-MM-DD"}), ) until_date = forms.DateField( required=False, label=_("Expiration date"), widget=BootstrapDateTimePicker(options={"format": "YYYY-MM-DD"}), ) state = forms.ChoiceField( choices=MDR.STATES, widget=forms.RadioSelect, ) changeDetails = forms.CharField( max_length=512, required=True, label=_("Why is the status being changed for these items?"), widget=forms.Textarea )
def __init__(self, *args, **kwargs): # TODO: Have tis throw a 'no user' error first_load = kwargs.pop('first_load', None) super().__init__(*args, **kwargs) for f in self.fields: if f == "workgroup": self.fields[f].widget = widgets.WorkgroupAutocompleteSelect() self.fields[f].widget.choices = self.fields[f].choices if not self.user.is_superuser: self.fields[ 'workgroup'].queryset = self.user.profile.editable_workgroups elif hasattr(self.fields[f], 'queryset') and type( self.fields[f].queryset) == ConceptQuerySet: if hasattr(self.fields[f].queryset, 'visible'): if f in [ m2m.name for m2m in self._meta.model._meta.many_to_many ]: field_widget = widgets.ConceptAutocompleteSelectMultiple else: field_widget = widgets.ConceptAutocompleteSelect self.fields[f].queryset = self.fields[f].queryset.all( ).visible(self.user) self.fields[f].widget = field_widget( model=self.fields[f].queryset.model) self.fields[f].widget.choices = self.fields[f].choices elif type(self.fields[f]) == forms.fields.DateField: self.fields[f].widget = BootstrapDateTimePicker( options={"format": "YYYY-MM-DD"}) elif type(self.fields[f]) == forms.fields.DateTimeField: self.fields[f].widget = BootstrapDateTimePicker( options={"format": "YYYY-MM-DD"})
class Meta: model = VersionPublicationRecord exclude = ['content_type', 'object_id'] widgets = { 'public_user_publication_date': BootstrapDateTimePicker(options={"format": "YYYY-MM-DD HH:MM"}), 'authenticated_user_publication_date': BootstrapDateTimePicker(options={"format": "YYYY-MM-DD HH:MM"}), }
def get_aristotle_widgets(model, ordering_field=None): _widgets = {} for f in model._meta.fields: foreign_model = model._meta.get_field(f.name).related_model widget = None if foreign_model and issubclass(foreign_model, _concept): widget = widgets.ConceptAutocompleteSelect( model=foreign_model, attrs={"style": "max-width:250px"}) elif foreign_model: widget = forms.Select(attrs={"class": "form-control"}) if isinstance(model._meta.get_field(f.name), DateField): widget = BootstrapDateTimePicker( options=datePickerOptions, attrs={"style": "min-width:150px"}) if ordering_field is not None and f.name == ordering_field: widget = forms.HiddenInput() if widget is not None: _widgets[f.name] = widget for f in model._meta.many_to_many: foreign_model = model._meta.get_field(f.name).related_model if foreign_model and issubclass(foreign_model, _concept): _widgets.update({ f.name: widgets.ConceptAutocompleteSelectMultiple(model=foreign_model) }) return _widgets
def get_aristotle_widgets(model, ordering_field=None): _widgets = {} for f in model._meta.fields: foreign_model = model._meta.get_field(f.name).related_model if foreign_model and issubclass(foreign_model, _concept): _widgets.update({ f.name: widgets.ConceptAutocompleteSelect( model=foreign_model ) }) if isinstance(model._meta.get_field(f.name), DateField): _widgets.update({ f.name: BootstrapDateTimePicker(options=datePickerOptions) }) if ordering_field is not None and f.name == ordering_field: _widgets.update({ ordering_field: forms.HiddenInput() }) for f in model._meta.many_to_many: foreign_model = model._meta.get_field(f.name).related_model if foreign_model and issubclass(foreign_model, _concept): _widgets.update({ f.name: widgets.ConceptAutocompleteSelectMultiple( model=foreign_model ) }) return _widgets
class ChangeStatusGenericForm(RegistrationAuthorityMixin, UserAwareForm): state = forms.ChoiceField( choices=BLANK_CHOICE_DASH + MDR.STATES, widget=forms.Select(attrs={"class": "form-control"})) registrationDate = forms.DateField( required=False, label=_("Registration date"), help_text="Date the registration state will be active from.", widget=BootstrapDateTimePicker(options={"format": "YYYY-MM-DD"}), initial=timezone.now()) cascadeRegistration = forms.ChoiceField(initial=0, choices=CASCADE_OPTIONS, label=_("Cascade registration"), help_text=CASCADE_HELP_TEXT, widget=forms.RadioSelect()) changeDetails = forms.CharField( max_length=512, required=False, label=_("Administrative Note"), help_text= "The administrative note is a publishable statement describing the reasons for registration.", widget=forms.Textarea) registrationAuthorities = forms.ChoiceField( label="Registration Authorities", choices=MDR.RegistrationAuthority.objects.none(), widget=forms.Select()) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.set_registration_authority_field( field_name="registrationAuthorities", qs=self.user.profile.registrarAuthorities.filter(active=0))
class ChangeStatusGenericForm(RegistrationAuthorityMixin, UserAwareForm): state = forms.ChoiceField(choices=MDR.STATES, widget=forms.RadioSelect) registrationDate = forms.DateField( required=False, label=_("Registration date"), widget=BootstrapDateTimePicker(options={"format": "YYYY-MM-DD"}), initial=timezone.now() ) cascadeRegistration = forms.ChoiceField( initial=0, choices=[(0, _('No')), (1, _('Yes'))], label=_("Do you want to request a status change for associated items") ) changeDetails = forms.CharField( max_length=512, required=True, label=_("Why is the status being changed for these items?"), widget=forms.Textarea ) registrationAuthorities = forms.ChoiceField( label="Registration Authorities", choices=MDR.RegistrationAuthority.objects.none(), widget=forms.RadioSelect ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.set_registration_authority_field( field_name="registrationAuthorities", qs=self.user.profile.registrarAuthorities.filter(active=0) )
class Meta: model = PublicationRecord exclude = ('user', 'concept') widgets = { 'publication_date': BootstrapDateTimePicker(options={"format": "YYYY-MM-DD"}), 'visibility': RadioSelect() }
class Meta: model = MDR.SupersedeRelationship fields = [ 'older_item', 'registration_authority', 'message', 'date_effective' ] widgets = { 'date_effective': BootstrapDateTimePicker(options={"format": "YYYY-MM-DD"}), }
class RequestReviewForm(ChangeStatusGenericForm): due_date = forms.DateField( required=False, label=_("Due date"), widget=BootstrapDateTimePicker(options={"format": "YYYY-MM-DD"}), initial=timezone.now()) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.set_registration_authority_field( field_name='registrationAuthorities') def clean_registrationAuthorities(self): value = self.cleaned_data['registrationAuthorities'] return MDR.RegistrationAuthority.objects.get(id=int(value))
class RequestReviewEndorseForm(RequestReviewAcceptForm): registration_state = forms.ChoiceField( widget=forms.RadioSelect(), choices=MDR.STATES, label=_("Registration State"), help_text="The state for endorsement for metadata in this review", ) registration_date = forms.DateField( widget=BootstrapDateTimePicker(options={"format": "YYYY-MM-DD"}), label=_("Registration Date"), ) cascade_registration = forms.ChoiceField( initial=0, choices=[(0, _('No')), (1, _('Yes'))], label=_("Do you want to request a status change for associated items")) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['close_review'].initial = 0
def one_to_many_formset_factory(model_to_add, model_to_add_field, ordering_field, extra_excludes=[]): # creates a one to many formset # model_to_add is weak entity class, model_to_add_field is the foriegn key field name _widgets = {} exclude_fields = [model_to_add_field, ordering_field] exclude_fields += extra_excludes for f in model_to_add._meta.fields: foreign_model = model_to_add._meta.get_field(f.name).related_model if foreign_model and issubclass(foreign_model, _concept): _widgets.update({ f.name: widgets.ConceptAutocompleteSelect(model=foreign_model) }) if isinstance(model_to_add._meta.get_field(f.name), DateField): _widgets.update( {f.name: BootstrapDateTimePicker(options=datePickerOptions)}) for f in model_to_add._meta.many_to_many: foreign_model = model_to_add._meta.get_field(f.name).related_model if foreign_model and issubclass(foreign_model, _concept): _widgets.update({ f.name: widgets.ConceptAutocompleteSelectMultiple(model=foreign_model) }) return modelformset_factory( model_to_add, formset=HiddenOrderModelFormSet, can_order=True, # we assign this back to the ordering field can_delete=True, exclude=exclude_fields, # fields='__all__', extra=1, widgets=_widgets)
class PermissionSearchForm(TokenSearchForm): """ We need to make a new form as permissions to view objects are a bit finicky. This form allows us to perform the base query then restrict it to just those of interest. """ # Use short names to reduce URL length mq=forms.ChoiceField( required=False, initial=QUICK_DATES.anytime, choices=QUICK_DATES, widget=BootstrapDropdownIntelligentDate ) mds = forms.DateField( required=False, label="Modified after date", widget=BootstrapDateTimePicker(options=datePickerOptions) ) mde = forms.DateField( required=False, label="Modified before date", widget=BootstrapDateTimePicker(options=datePickerOptions) ) cq=forms.ChoiceField( required=False, initial=QUICK_DATES.anytime, choices=QUICK_DATES, widget=BootstrapDropdownIntelligentDate ) cds = forms.DateField( required=False, label="Created after date", widget=BootstrapDateTimePicker(options=datePickerOptions) ) cde = forms.DateField( required=False, label="Created before date", widget=BootstrapDateTimePicker(options=datePickerOptions) ) ra = forms.MultipleChoiceField( required=False, label=_("Registration authority"), choices=[], widget=BootstrapDropdownSelectMultiple ) sort = forms.ChoiceField( required=False, initial=SORT_OPTIONS.natural, choices=SORT_OPTIONS, widget=BootstrapDropdownSelect ) from aristotle_mdr.search_indexes import BASE_RESTRICTION res = forms.ChoiceField( required=False, initial=None, choices=BASE_RESTRICTION.items(), label="Item visibility state" ) state = forms.MultipleChoiceField( required=False, label=_("Registration status"), choices=MDR.STATES + [(-99, _('Unregistered'))], # Allow unregistered as a selection widget=BootstrapDropdownSelectMultiple ) public_only = forms.BooleanField( required=False, label="Only show public items" ) title_only = forms.BooleanField( required=False, label="Search titles only" ) myWorkgroups_only = forms.BooleanField( required=False, label="Only show items in my workgroups" ) category = forms.ChoiceField( choices=SEARCH_CATEGORIES, required=False, label=_('Categories'), widget=BootstrapDropdownSelect ) models = forms.MultipleChoiceField( choices=[], # model_choices(), required=False, label=_('Item type'), widget=BootstrapDropdownSelectMultiple ) rpp = forms.IntegerField( required=False, label='Results per page' ) # Hidden Workgroup field that is not rendered in the template, # label is required for faceting display wg = forms.IntegerField(required=False, label="Workgroup") # Hidden Stewardship Organisation field that is not rendered in the template, # label is required for faceting display sa = forms.IntegerField(required=False, label="Stewardship Organisation") # Filters that are to be applied filters = ["models", "mq", "cq", "cds", "cde", "mds", "mde", "state", "ra", "res", "wg", "sa"] def __init__(self, *args, **kwargs): if 'searchqueryset' not in kwargs.keys() or kwargs['searchqueryset'] is None: kwargs['searchqueryset'] = get_permission_sqs() if not issubclass(type(kwargs['searchqueryset']), PermissionSearchQuerySet): raise ImproperlyConfigured("Aristotle Search Queryset connection must be a subclass of PermissionSearchQuerySet") super().__init__(*args, **kwargs) # Populate choice of Registration Authorities ordered by active state and name # Inactive last self.fields['ra'].choices = [(ra.id, ra.name) for ra in MDR.RegistrationAuthority.objects.filter(active__in=[0, 1]).order_by('active', 'name')] # List of models that you can search for self.default_models = [ m[0] for m in model_choices() if m[0].split('.', 1)[0] in allowable_search_models() ] # Set choices for models self.fields['models'].choices = [ m for m in model_choices() if m[0].split('.', 1)[0] in allowable_search_models() ] def get_models(self): """Return an alphabetical list of model classes in the index.""" search_models = [] if self.is_valid(): app_labels = self.default_models if self.cleaned_data['models']: app_labels = self.cleaned_data['models'] for model in app_labels: search_models.append(apps.get_model(*model.split('.'))) return search_models @property def applied_filters(self): """ Returns the filters appearing in the URL that are applied """ if not hasattr(self, 'cleaned_data'): return [] return [f for f in self.filters if self.cleaned_data.get(f, False)] def search(self, repeat_search=False): # First, store the SearchQuerySet received from other processing. sqs = super().search() # If we got an empty search queryset, no need for further processing if isinstance(sqs, EmptyPermissionSearchQuerySet): return sqs if not self.token_models and self.get_models(): sqs = sqs.models(*self.get_models()) self.repeat_search = repeat_search # Is there no filter and no query -> no search was performed has_filter = self.kwargs or self.token_models or self.applied_filters if not has_filter and not self.query_text: return self.no_query_found() if self.applied_filters and not self.query_text: # Set flag when filtering with no query (used in template) self.filter_search = True # Get filter data from query states = self.cleaned_data.get('state', None) ras = self.cleaned_data.get('ra', None) restriction = self.cleaned_data['res'] search_category = self.cleaned_data['category'] workgroup = self.cleaned_data.get('wg', None) stewardship_organisation = self.cleaned_data.get('sa', None) # Apply the filters sqs = sqs.apply_registration_status_filters(states, ras) if restriction: sqs = sqs.filter(restriction=restriction) if search_category and search_category != SEARCH_CATEGORIES.all: sqs = sqs.filter(category=search_category) sqs = self.apply_date_filtering(sqs) sqs = sqs.apply_permission_checks( user=self.request.user, public_only=self.cleaned_data['public_only'], user_workgroups_only=self.cleaned_data['myWorkgroups_only'] ) if workgroup is not None: # We don't want to filter on a non-existent field # Must filter exactly sqs = sqs.filter(workgroup__exact=workgroup) if stewardship_organisation is not None: # Apply the stewardship organisation filter sqs = sqs.filter(stewardship_organisation=stewardship_organisation) # f for facets extra_facets_details = {} facets_opts = self.request.GET.getlist('f', []) for _facet in facets_opts: _facet, value = _facet.split("::", 1) # Force exact as otherwise we don't match when there are spaces. sqs = sqs.filter(**{"%s__exact" % _facet: value}) facets_details = extra_facets_details.get(_facet, {'applied': []}) facets_details['applied'] = list(set(facets_details['applied'] + [value])) extra_facets_details[_facet] = facets_details self.has_spelling_suggestions = False if not self.repeat_search: if sqs.count() < 5: self.check_spelling(sqs) if sqs.count() == 0: if sqs.count() == 0 and self.has_spelling_suggestions: self.auto_correct_spell_search = True self.cleaned_data['q'] = self.suggested_query elif has_filter and self.cleaned_data['q']: # If there are 0 results with a search term, and filters applied # lets be nice and remove the filters and try again. # There will be a big message on the search page that says what we did. for f in self.filters: self.cleaned_data[f] = None self.auto_broaden_search = True # Re run the query with the updated details sqs = self.search(repeat_search=True) # Only apply sorting on the first pass through sqs = self.apply_sorting(sqs) # Don't applying sorting on the facet as ElasticSearch2 doesn't like this. filters_to_facets = { 'ra': 'registrationAuthorities', 'models': 'facet_model_ct', 'state': 'statuses', } # Add filters that are also facets to Search Query Set for _filter, facet in filters_to_facets.items(): if _filter not in self.applied_filters: sqs = sqs.facet(facet) logged_in_facets = { 'wg': 'workgroup', 'res': 'restriction' } # If user is logged in, add permisssioned facets to Search Query Set if self.request.user.is_active: for _filter, facet in logged_in_facets.items(): if _filter not in self.applied_filters: # Don't do this: sqs = sqs.facet(facet, sort='count') sqs = sqs.facet(facet) # For facets that will always appear on the sidebar, but are not part of the previous lists additional_hardcoded_facets = ['stewardship_organisation'] for facet in additional_hardcoded_facets: sqs = sqs.facet(facet) # Generate details about facets from ``concepts`` registered (as facetable) with a Haystack search index that conforms # to Aristotle permissions. Excludes facets that have been previously been added. extra_facets = [] from aristotle_mdr.search_indexes import registered_indexes for model_index in registered_indexes: for name, field in model_index.fields.items(): if field.faceted: if name not in (list(filters_to_facets.values()) + list(logged_in_facets.values()) + additional_hardcoded_facets): extra_facets.append(name) x = extra_facets_details.get(name, {}) x.update({ 'title': getattr(field, 'title', name), 'display': getattr(field, 'display', None), 'allow_search': getattr(field, 'allow_search', False), }) extra_facets_details[name]= x # Don't do this: sqs = sqs.facet(facet, sort='count') # Why Sam, why? sqs = sqs.facet(name) # Generate facet content self.facets = sqs.facet_counts() # Populate the extra facet fields if 'fields' in self.facets: self.extra_facet_fields = [ (k, {'values': sorted(v, key=lambda x: -x[1])[:10], 'details': extra_facets_details[k]}) for k, v in self.facets['fields'].items() if k in extra_facets ] self.extra_facet_fields = [ (k, { 'values': [ f for f in sorted(v, key=lambda x: -x[1]) if f[0] not in extra_facets_details.get(k, {}).get('applied', []) ][:10], 'details': extra_facets_details[k] }) for k, v in self.facets['fields'].items() if k in extra_facets ] # Cut down to only the top 10 results for each facet, order by number of results for facet, counts in self.facets['fields'].items(): self.facets['fields'][facet] = sorted(counts, key=lambda x: -x[1])[:10] # Perform id to object lookup from django.contrib.contenttypes.models import ContentType model_types = { 'stewardship_organisation': MDR.StewardOrganisation, 'registrationAuthorities': MDR.RegistrationAuthority, 'workgroup': MDR.Workgroup, 'facet_model_ct': ContentType, } for facet in self.facets['fields'].keys(): if facet in model_types.keys(): id_to_item = {} # Facet is for a model that must be looked up from the database item_type = model_types.get(facet) ids = [] for id, count in self.facets['fields'][facet]: if id is not None: ids.append(id) id_to_instance = item_type.objects.in_bulk(ids) for id, count in self.facets['fields'][facet]: if id is None: id_to_item[id] = (None, count) # TODO: eradicate -99 from code elif (id == -99): id_to_item[id] = (None, count) else: id_to_item[id] = (id_to_instance[int(id)], count) self.facets['fields'][facet] = id_to_item return sqs def check_spelling(self, sqs): if self.query_text: original_query = self.cleaned_data.get('q', "") from urllib.parse import quote_plus suggestions = [] has_suggestions = False suggested_query = [] # lets assume the words are ordered in importance # So we suggest words in order optimal_query = original_query for token in self.cleaned_data.get('q', "").split(" "): if token: # remove blanks suggestion = self.searchqueryset.spelling_suggestion(token) if suggestion: test_query = optimal_query.replace(token, suggestion) # Haystack can *over correct* so we'll do a quick search with the # suggested spelling to compare words against try: self.searchqueryset.auto_query(test_query)[0] suggested_query.append(suggestion) has_suggestions = True optimal_query = test_query except: suggestion = None else: suggested_query.append(token) suggestions.append((token, suggestion)) if optimal_query != original_query: if optimal_query == original_query.lower(): # If the suggested query is the same query but in lowercase, don't suggest it self.has_spelling_suggestions = False else: self.spelling_suggestions = suggestions self.has_spelling_suggestions = has_suggestions self.original_query = self.cleaned_data.get('q') self.suggested_query = quote_plus(' '.join(suggested_query), safe="") def apply_date_filtering(self, sqs): modify_quick_date = self.cleaned_data['mq'] create_quick_date = self.cleaned_data['cq'] create_date_start = self.cleaned_data['cds'] create_date_end = self.cleaned_data['cde'] modify_date_start = self.cleaned_data['mds'] modify_date_end = self.cleaned_data['mde'] """ Modified filtering is really hard to do formal testing for as the modified dates are altered on save, so its impossible to alter the modified dates to check the search is working. However, this is the exact same process as creation date (which we can alter), so if creation filtering is working, modified filtering should work too. """ if modify_quick_date and modify_quick_date is not QUICK_DATES.anytime: # pragma: no cover delta = time_delta(modify_quick_date) if delta is not None: sqs = sqs.filter(modifed__gte=delta) elif modify_date_start or modify_date_end: # pragma: no cover if modify_date_start: sqs = sqs.filter(modifed__gte=modify_date_start) if modify_date_end: sqs = sqs.filter(modifed__lte=modify_date_end) if create_quick_date and create_quick_date is not QUICK_DATES.anytime: delta = time_delta(create_quick_date) if delta is not None: sqs = sqs.filter(created__gte=delta) elif create_date_start or create_date_end: if create_date_start: sqs = sqs.filter(created__gte=create_date_start) if create_date_end: sqs = sqs.filter(created__lte=create_date_end) return sqs def apply_sorting(self, sqs): # pragma: no cover, no security issues, standard Haystack methods, so already tested. sort_order = self.cleaned_data['sort'] # Ordering is evaluated from left to right if sort_order == SORT_OPTIONS.modified_ascending: sqs = sqs.order_by('modified', 'name_sortable') elif sort_order == SORT_OPTIONS.modified_descending: sqs = sqs.order_by('-modified', 'name_sortable') elif sort_order == SORT_OPTIONS.created_ascending: sqs = sqs.order_by('created', 'name_sortable') elif sort_order == SORT_OPTIONS.created_descending: sqs = sqs.order_by('-created', 'name_sortable') elif sort_order == SORT_OPTIONS.alphabetical: sqs = sqs.order_by('name_sortable') elif sort_order == SORT_OPTIONS.state: sqs = sqs.order_by('-highest_state', 'name_sortable') return sqs
# Type choices presented when creating a custom field type_choices = Choices(('int', 'Integer'), ('str', 'Text'), ('html', 'Rich Text'), ('date', 'Date'), ('enum', 'Choice')) # Form field used when creating a custom value of the specified type type_field_mapping = { 'int': { 'field': forms.IntegerField, }, 'str': { 'field': forms.CharField, 'args': { 'widget': forms.widgets.Textarea } }, 'date': { 'field': forms.DateField, 'args': { 'widget': BootstrapDateTimePicker(options={'format': 'YYYY-MM-DD'}) } }, 'html': { 'field': RichTextFormField }, 'enum': { 'field': forms.ChoiceField } }
class PermissionSearchForm(TokenSearchForm): """ We need to make a new form as permissions to view objects are a bit finicky. This form allows us to perform the base query then restrict it to just those of interest. TODO: This might not scale well, so it may need to be looked at in production. """ mq=forms.ChoiceField( required=False, initial=QUICK_DATES.anytime, choices=QUICK_DATES, widget=BootstrapDropdownIntelligentDate ) mds = forms.DateField( required=False, label="Modified after date", widget=BootstrapDateTimePicker(options=datePickerOptions) ) mde = forms.DateField( required=False, label="Modified before date", widget=BootstrapDateTimePicker(options=datePickerOptions) ) cq=forms.ChoiceField( required=False, initial=QUICK_DATES.anytime, choices=QUICK_DATES, widget=BootstrapDropdownIntelligentDate ) cds = forms.DateField( required=False, label="Created after date", widget=BootstrapDateTimePicker(options=datePickerOptions) ) cde = forms.DateField( required=False, label="Created before date", widget=BootstrapDateTimePicker(options=datePickerOptions) ) # Use short singular names # ras = [(ra.id, ra.name) for ra in MDR.RegistrationAuthority.objects.all()] ra = forms.MultipleChoiceField( required=False, label=_("Registration authority"), choices=[], widget=BootstrapDropdownSelectMultiple ) sort = forms.ChoiceField( required=False, initial=SORT_OPTIONS.natural, choices=SORT_OPTIONS, widget=BootstrapDropdownSelect ) from aristotle_mdr.search_indexes import BASE_RESTRICTION res = forms.ChoiceField( required=False, initial=None, choices=BASE_RESTRICTION.items(), label="Item visibility state" ) state = forms.MultipleChoiceField( required=False, label=_("Registration status"), choices=MDR.STATES + [(-99, _('Unregistered'))], # Allow unregistered as a selection widget=BootstrapDropdownSelectMultiple ) public_only = forms.BooleanField( required=False, label="Only show public items" ) myWorkgroups_only = forms.BooleanField( required=False, label="Only show items in my workgroups" ) models = forms.MultipleChoiceField( choices=[], # model_choices(), required=False, label=_('Item type'), widget=BootstrapDropdownSelectMultiple ) rpp = forms.IntegerField( required=False, label='Results per page' ) # F for facet! # searchqueryset = PermissionSearchQuerySet def __init__(self, *args, **kwargs): if 'searchqueryset' not in kwargs.keys() or kwargs['searchqueryset'] is None: kwargs['searchqueryset'] = get_permission_sqs() if not issubclass(type(kwargs['searchqueryset']), PermissionSearchQuerySet): raise ImproperlyConfigured("Aristotle Search Queryset connection must be a subclass of PermissionSearchQuerySet") super().__init__(*args, **kwargs) # Show visible workgroups ordered by active state and name # Inactive last self.fields['ra'].choices = [(ra.id, ra.name) for ra in MDR.RegistrationAuthority.objects.filter(active__in=[0, 1]).order_by('active', 'name')] self.fields['models'].choices = [ m for m in model_choices() if m[0].split('.', 1)[0] in fetch_metadata_apps() + ['aristotle_mdr_help'] ] def get_models(self): """Return an alphabetical list of model classes in the index.""" search_models = [] if self.is_valid() and self.cleaned_data['models']: for model in self.cleaned_data['models']: search_models.append(apps.get_model(*model.split('.'))) return search_models filters = "models mq cq cds cde mds mde state ra res".split() @property def applied_filters(self): if not hasattr(self, 'cleaned_data'): return [] return [f for f in self.filters if self.cleaned_data.get(f, False)] def search(self, repeat_search=False): # First, store the SearchQuerySet received from other processing. sqs = super().search() if not self.token_models and self.get_models(): sqs = sqs.models(*self.get_models()) self.repeat_search = repeat_search has_filter = self.kwargs or self.token_models or self.applied_filters if not has_filter and not self.query_text: return self.no_query_found() if self.applied_filters and not self.query_text: # and not self.kwargs: # If there is a filter, but no query then we'll force some results. sqs = self.searchqueryset.order_by('-modified') self.filter_search = True self.attempted_filter_search = True states = self.cleaned_data.get('state', None) ras = self.cleaned_data.get('ra', None) restriction = self.cleaned_data['res'] sqs = sqs.apply_registration_status_filters(states, ras) if restriction: sqs = sqs.filter(restriction=restriction) sqs = self.apply_date_filtering(sqs) sqs = sqs.apply_permission_checks( user=self.request.user, public_only=self.cleaned_data['public_only'], user_workgroups_only=self.cleaned_data['myWorkgroups_only'] ) extra_facets_details = {} facets_opts = self.request.GET.getlist('f', []) for _facet in facets_opts: _facet, value = _facet.split("::", 1) # Force exact as otherwise we don't match when there are spaces. sqs = sqs.filter(**{"%s__exact" % _facet: value}) facets_details = extra_facets_details.get(_facet, {'applied': []}) facets_details['applied'] = list(set(facets_details['applied'] + [value])) extra_facets_details[_facet] = facets_details self.has_spelling_suggestions = False if not self.repeat_search: if sqs.count() < 5: self.check_spelling(sqs) if sqs.count() == 0: if sqs.count() == 0 and self.has_spelling_suggestions: self.auto_correct_spell_search = True self.cleaned_data['q'] = self.suggested_query elif has_filter and self.cleaned_data['q']: # If there are 0 results with a search term, and filters applied # lets be nice and remove the filters and try again. # There will be a big message on the search page that says what we did. for f in self.filters: self.cleaned_data[f] = None self.auto_broaden_search = True # Re run the query with the updated details sqs = self.search(repeat_search=True) # Only apply sorting on the first pass through sqs = self.apply_sorting(sqs) # Don't applying sorting on the facet as ElasticSearch2 doesn't like this. filters_to_facets = { 'ra': 'registrationAuthorities', 'models': 'facet_model_ct', 'state': 'statuses', } for _filter, facet in filters_to_facets.items(): if _filter not in self.applied_filters: # Don't do this: sqs = sqs.facet(facet, sort='count') sqs = sqs.facet(facet) logged_in_facets = { 'wg': 'workgroup', 'res': 'restriction' } if self.request.user.is_active: for _filter, facet in logged_in_facets.items(): if _filter not in self.applied_filters: # Don't do this: sqs = sqs.facet(facet, sort='count') sqs = sqs.facet(facet) extra_facets = [] from aristotle_mdr.search_indexes import registered_indexes for model_index in registered_indexes: for name, field in model_index.fields.items(): if field.faceted: if name not in (list(filters_to_facets.values()) + list(logged_in_facets.values())): extra_facets.append(name) x = extra_facets_details.get(name, {}) x.update(**{ 'title': getattr(field, 'title', name), 'display': getattr(field, 'display', None), 'allow_search': getattr(field, 'allow_search', False), }) extra_facets_details[name]= x # Don't do this: sqs = sqs.facet(facet, sort='count') # Why Sam, why? sqs = sqs.facet(name) self.facets = sqs.facet_counts() if 'fields' in self.facets: self.extra_facet_fields = [ (k, {'values': sorted(v, key=lambda x: -x[1])[:10], 'details': extra_facets_details[k]}) for k, v in self.facets['fields'].items() if k in extra_facets ] self.extra_facet_fields = [ (k, { 'values': [ f for f in sorted(v, key=lambda x: -x[1]) if f[0] not in extra_facets_details.get(k, {}).get('applied', []) ][:10], 'details': extra_facets_details[k] }) for k, v in self.facets['fields'].items() if k in extra_facets ] for facet, counts in self.facets['fields'].items(): # Return the 5 top results for each facet in order of number of results. self.facets['fields'][facet] = sorted(counts, key=lambda x: -x[1])[:10] return sqs def check_spelling(self, sqs): if self.query_text: original_query = self.cleaned_data.get('q', "") from urllib.parse import quote_plus suggestions = [] has_suggestions = False suggested_query = [] # lets assume the words are ordered in importance # So we suggest words in order optimal_query = original_query for token in self.cleaned_data.get('q', "").split(" "): if token: # remove blanks suggestion = self.searchqueryset.spelling_suggestion(token) if suggestion: test_query = optimal_query.replace(token, suggestion) # Haystack can *over correct* so we'll do a quick search with the # suggested spelling to compare words against try: self.searchqueryset.auto_query(test_query)[0] suggested_query.append(suggestion) has_suggestions = True optimal_query = test_query except: suggestion = None else: suggested_query.append(token) suggestions.append((token, suggestion)) if optimal_query != original_query: self.spelling_suggestions = suggestions self.has_spelling_suggestions = has_suggestions self.original_query = self.cleaned_data.get('q') self.suggested_query = quote_plus(' '.join(suggested_query), safe="") def apply_date_filtering(self, sqs): modify_quick_date = self.cleaned_data['mq'] create_quick_date = self.cleaned_data['cq'] create_date_start = self.cleaned_data['cds'] create_date_end = self.cleaned_data['cde'] modify_date_start = self.cleaned_data['mds'] modify_date_end = self.cleaned_data['mde'] """ Modified filtering is really hard to do formal testing for as the modified dates are altered on save, so its impossible to alter the modified dates to check the search is working. However, this is the exact same process as creation date (which we can alter), so if creation filtering is working, modified filtering should work too. """ if modify_quick_date and modify_quick_date is not QUICK_DATES.anytime: # pragma: no cover delta = time_delta(modify_quick_date) if delta is not None: sqs = sqs.filter(modifed__gte=delta) elif modify_date_start or modify_date_end: # pragma: no cover if modify_date_start: sqs = sqs.filter(modifed__gte=modify_date_start) if modify_date_end: sqs = sqs.filter(modifed__lte=modify_date_end) if create_quick_date and create_quick_date is not QUICK_DATES.anytime: delta = time_delta(create_quick_date) if delta is not None: sqs = sqs.filter(created__gte=delta) elif create_date_start or create_date_end: if create_date_start: sqs = sqs.filter(created__gte=create_date_start) if create_date_end: sqs = sqs.filter(created__lte=create_date_end) return sqs def apply_sorting(self, sqs): # pragma: no cover, no security issues, standard Haystack methods, so already tested. sort_order = self.cleaned_data['sort'] if sort_order == SORT_OPTIONS.modified_ascending: sqs = sqs.order_by('-modified', 'name_sortable') elif sort_order == SORT_OPTIONS.modified_descending: sqs = sqs.order_by('modified', '-name_sortable') elif sort_order == SORT_OPTIONS.created_ascending: sqs = sqs.order_by('-created', 'name_sortable') elif sort_order == SORT_OPTIONS.created_descending: sqs = sqs.order_by('created', '-name_sortable') elif sort_order == SORT_OPTIONS.alphabetical: sqs = sqs.order_by('name_sortable') elif sort_order == SORT_OPTIONS.state: sqs = sqs.order_by('-highest_state', 'name_sortable') return sqs
def __init__(self, *args, **kwargs): from comet.managers import FrameworkDimensionQuerySet super().__init__(*args, **kwargs) if 'aristotle_mdr_backwards' not in fetch_aristotle_settings().get( 'CONTENT_EXTENSIONS', []): bc_fields = self._meta.model.backwards_compatible_fields for fname in bc_fields: if fname in self.fields: del self.fields[fname] for f in self.fields: # Add workgroup if f == "workgroup": self.fields[f].widget = widgets.WorkgroupAutocompleteSelect() self.fields[f].widget.choices = self.fields[f].choices if not self.user.is_superuser: self.fields[ 'workgroup'].queryset = self.user.profile.editable_workgroups # Add foreign keys and m2m key widgets elif hasattr(self.fields[f], 'queryset') and type( self.fields[f].queryset) == ConceptQuerySet: if hasattr(self.fields[f].queryset, 'visible'): if f in [ m2m.name for m2m in self._meta.model._meta.many_to_many ]: field_widget = widgets.ConceptAutocompleteSelectMultiple else: field_widget = widgets.ConceptAutocompleteSelect self.fields[f].queryset = self.fields[f].queryset.all( ).visible(self.user) self.fields[f].widget = field_widget( model=self.fields[f].queryset.model) self.fields[f].widget.choices = self.fields[f].choices elif hasattr(self.fields[f], 'queryset') and type( self.fields[f].queryset) == FrameworkDimensionQuerySet: if f in [ m2m.name for m2m in self._meta.model._meta.many_to_many ]: field_widget = widgets.FrameworkDimensionAutocompleteSelectMultiple self.fields[f].widget = field_widget( model=self.fields[f].queryset.model) self.fields[f].queryset = self.fields[f].queryset.all() # Add date field elif type(self.fields[f]) == forms.fields.DateField: self.fields[f].widget = BootstrapDateTimePicker( options={"format": "YYYY-MM-DD"}) elif type(self.fields[f]) == forms.fields.DateTimeField: self.fields[f].widget = BootstrapDateTimePicker( options={"format": "YYYY-MM-DD"}) # Add the name suggestion button aristotle_settings = fetch_aristotle_settings() self.fields['name'].widget = NameSuggestInput( name_suggest_fields=self._meta.model.name_suggest_fields, separator=aristotle_settings['SEPARATORS'].get( self._meta.model.__name__, '-'))