From 00bc860069a9ae301f0862c9fee76139174d5e93 Mon Sep 17 00:00:00 2001 From: Dana Lambert Date: Mon, 2 Dec 2019 17:27:24 +1300 Subject: [PATCH 1/4] [Issue #4276] Updating django-autocomplete-light to version 3.5.0 --- geonode/base/admin.py | 55 ++++---- geonode/base/autocomplete_light_registry.py | 97 --------------- geonode/base/fields.py | 45 +------ geonode/base/forms.py | 117 +++++++++--------- geonode/base/models.py | 20 ++- geonode/base/templatetags/base_tags.py | 2 +- geonode/base/urls.py | 59 +++++++++ geonode/base/views.py | 90 +++++++++++++- geonode/base/widgets.py | 61 ++++----- geonode/documents/admin.py | 10 +- .../documents/autocomplete_light_registry.py | 39 ------ geonode/documents/forms.py | 3 - .../documents/document_metadata_advanced.html | 9 ++ .../templates/layouts/doc_panels.html | 3 + geonode/documents/urls.py | 3 + geonode/documents/views.py | 74 +++++------ geonode/groups/autocomplete_light_registry.py | 34 ----- geonode/groups/models.py | 3 +- geonode/groups/urls.py | 4 + geonode/groups/views.py | 29 +++++ geonode/layers/admin.py | 26 +++- geonode/layers/autocomplete_light_registry.py | 61 --------- geonode/layers/forms.py | 3 - .../layers/layer_metadata_advanced.html | 16 ++- geonode/layers/templates/layouts/panels.html | 3 + geonode/layers/urls.py | 3 + geonode/layers/views.py | 81 ++++++------ geonode/maps/admin.py | 14 ++- geonode/maps/autocomplete_light_registry.py | 39 ------ geonode/maps/forms.py | 4 - .../maps/templates/layouts/map_panels.html | 3 + .../templates/maps/map_metadata_advanced.html | 9 ++ geonode/maps/urls.py | 3 + geonode/maps/views.py | 72 ++++++----- geonode/people/admin.py | 2 +- geonode/people/autocomplete_light_registry.py | 39 ------ geonode/people/urls.py | 3 + geonode/people/views.py | 16 +++ geonode/services/admin.py | 2 +- geonode/settings.py | 6 +- geonode/static/geonode/css/base.css | 65 +++++++++- .../static/geonode/js/search/autocomplete.js | 100 +++++++++++++++ geonode/static/geonode/js/search/search.js | 59 ++++----- geonode/static/geonode/less/base.less | 57 +++++++++ geonode/templates/500.html | 22 ++-- geonode/templates/admin/base_site.html | 3 +- geonode/templates/base.html | 74 +++++------ geonode/templates/index.html | 4 +- geonode/templates/metadata_form_js.html | 25 ++-- geonode/templates/search/_region_filter.html | 16 +-- .../search/_search_user_content.html | 12 +- geonode/templates/search/_text_filter.html | 12 +- geonode/templates/search/search_scripts.html | 40 ++++-- geonode/urls.py | 9 +- requirements.txt | 2 +- 55 files changed, 909 insertions(+), 753 deletions(-) delete mode 100644 geonode/base/autocomplete_light_registry.py create mode 100644 geonode/base/urls.py delete mode 100644 geonode/documents/autocomplete_light_registry.py delete mode 100644 geonode/groups/autocomplete_light_registry.py delete mode 100644 geonode/layers/autocomplete_light_registry.py delete mode 100644 geonode/maps/autocomplete_light_registry.py delete mode 100644 geonode/people/autocomplete_light_registry.py create mode 100644 geonode/static/geonode/js/search/autocomplete.js diff --git a/geonode/base/admin.py b/geonode/base/admin.py index b85ab433ede..62445d5ad4c 100755 --- a/geonode/base/admin.py +++ b/geonode/base/admin.py @@ -29,14 +29,14 @@ except ImportError: from io import StringIO -from autocomplete_light.forms import ModelForm -from autocomplete_light.forms import modelform_factory -from autocomplete_light.contrib.taggit_field import TaggitField, TaggitWidget +from dal import autocomplete +from taggit.forms import TagField +from django import forms from treebeard.admin import TreeAdmin from treebeard.forms import movenodeform_factory -from modeltranslation.admin import TranslationAdmin +from modeltranslation.admin import TabbedTranslationAdmin from geonode.base.models import ( TopicCategory, @@ -55,6 +55,8 @@ ) from django.http import HttpResponseRedirect +from geonode.base.widgets import TaggitSelect2Custom + def metadata_batch_edit(modeladmin, request, queryset): ids = ','.join([str(element.pk) for element in queryset]) @@ -76,17 +78,7 @@ def set_batch_permissions(modeladmin, request, queryset): set_batch_permissions.short_description = 'Set permissions' -class MediaTranslationAdmin(TranslationAdmin): - class Media: - js = ( - 'modeltranslation/js/tabbed_translation_fields.js', - ) - css = { - 'screen': ('modeltranslation/css/tabbed_translation_fields.css',), - } - - -class BackupAdminForm(ModelForm): +class BackupAdminForm(forms.ModelForm): class Meta: model = Backup @@ -140,7 +132,7 @@ def restore(self, request, queryset): if request.POST.get("post"): for siteObj in queryset: self.message_user(request, "Executed Restore: " + siteObj.name) - out = StringIO() + out = StringIO.StringIO() if siteObj.location: call_command( 'restore', force_exec=True, backup_file=str( @@ -166,7 +158,7 @@ def restore(self, request, queryset): restore.short_description = "Run the Restore" -class BackupAdmin(MediaTranslationAdmin): +class BackupAdmin(TabbedTranslationAdmin): list_display = ('id', 'name', 'date', 'location') list_display_links = ('name',) date_hierarchy = 'date' @@ -175,13 +167,13 @@ class BackupAdmin(MediaTranslationAdmin): actions = [run, restore] -class LicenseAdmin(MediaTranslationAdmin): +class LicenseAdmin(TabbedTranslationAdmin): model = License list_display = ('id', 'name') list_display_links = ('name',) -class TopicCategoryAdmin(MediaTranslationAdmin): +class TopicCategoryAdmin(TabbedTranslationAdmin): model = TopicCategory list_display_links = ('identifier',) list_display = ( @@ -208,7 +200,7 @@ def has_delete_permission(self, request, obj=None): return False -class RegionAdmin(MediaTranslationAdmin): +class RegionAdmin(TabbedTranslationAdmin): model = Region list_display_links = ('name',) list_display = ('code', 'name', 'parent') @@ -216,7 +208,7 @@ class RegionAdmin(MediaTranslationAdmin): group_fieldsets = True -class SpatialRepresentationTypeAdmin(MediaTranslationAdmin): +class SpatialRepresentationTypeAdmin(TabbedTranslationAdmin): model = SpatialRepresentationType list_display_links = ('identifier',) list_display = ('identifier', 'description', 'gn_description', 'is_choice') @@ -230,7 +222,7 @@ def has_delete_permission(self, request, obj=None): return False -class RestrictionCodeTypeAdmin(MediaTranslationAdmin): +class RestrictionCodeTypeAdmin(TabbedTranslationAdmin): model = RestrictionCodeType list_display_links = ('identifier',) list_display = ('identifier', 'description', 'gn_description', 'is_choice') @@ -249,7 +241,7 @@ class ContactRoleAdmin(admin.ModelAdmin): list_display_links = ('id',) list_display = ('id', 'contact', 'resource', 'role') list_editable = ('contact', 'resource', 'role') - form = modelform_factory(ContactRole, fields='__all__') + form = forms.modelform_factory(ContactRole, fields='__all__') class LinkAdmin(admin.ModelAdmin): @@ -258,7 +250,7 @@ class LinkAdmin(admin.ModelAdmin): list_display = ('id', 'resource', 'extension', 'link_type', 'name', 'mime') list_filter = ('resource', 'extension', 'link_type', 'mime') search_fields = ('name', 'resource__title',) - form = modelform_factory(Link, fields='__all__') + form = forms.modelform_factory(Link, fields='__all__') class HierarchicalKeywordAdmin(TreeAdmin): @@ -301,12 +293,9 @@ class CuratedThumbnailAdmin(admin.ModelAdmin): admin.site.register(CuratedThumbnail, CuratedThumbnailAdmin) -class ResourceBaseAdminForm(ModelForm): - # We need to specify autocomplete='TagAutocomplete' or admin views like - # /admin/maps/map/2/ raise exceptions during form rendering. - # But if we specify it up front, TaggitField.__init__ throws an exception - # which prevents app startup. Therefore, we defer setting the widget until - # after that's done. - keywords = TaggitField(required=False) - keywords.widget = TaggitWidget( - autocomplete='HierarchicalKeywordAutocomplete') +class ResourceBaseAdminForm(autocomplete.FutureModelForm): + + keywords = TagField(widget=TaggitSelect2Custom('autocomplete_hierachical_keyword')) + + class Meta: + pass diff --git a/geonode/base/autocomplete_light_registry.py b/geonode/base/autocomplete_light_registry.py deleted file mode 100644 index 080604280fd..00000000000 --- a/geonode/base/autocomplete_light_registry.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- coding: utf-8 -*- -######################################################################### -# -# Copyright (C) 2016 OSGeo -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -######################################################################### - -import logging - -from autocomplete_light.registry import register -from autocomplete_light.autocomplete.shortcuts import AutocompleteModelBase, AutocompleteModelTemplate - -from guardian.shortcuts import get_objects_for_user -from django.conf import settings -from django.db.models import Q -from geonode.security.utils import get_visible_resources - -from .models import ResourceBase, Region, HierarchicalKeyword, ThesaurusKeywordLabel - -logger = logging.getLogger(__name__) - - -class ResourceBaseAutocomplete(AutocompleteModelTemplate): - choice_template = 'autocomplete_response.html' - model = ResourceBase - - def choices_for_request(self): - request = self.request - permitted = get_objects_for_user( - request.user, - 'base.view_resourcebase') - self.choices = self.choices.filter(id__in=permitted) - - self.choices = get_visible_resources( - self.choices, - request.user if request else None, - admin_approval_required=settings.ADMIN_MODERATE_UPLOADS, - unpublished_not_visible=settings.RESOURCE_PUBLISHING, - private_groups_not_visibile=settings.GROUP_PRIVATE_RESOURCES) - - return super(ResourceBaseAutocomplete, self).choices_for_request() - - -register(Region, - search_fields=['name'], - autocomplete_js_attributes={'placeholder': 'Region/Country ..', },) - -register(ResourceBaseAutocomplete, - search_fields=['title'], - order_by=['title'], - limit_choices=100, - autocomplete_js_attributes={'placeholder': 'Resource name..', },) - -register(HierarchicalKeyword, - search_fields=['name', 'slug'], - autocomplete_js_attributes={'placeholder': - 'A space or comma-separated list of keywords', },) - - -class ThesaurusKeywordLabelAutocomplete(AutocompleteModelBase): - - search_fields = ['label'] - - model = ThesaurusKeywordLabel - - def choices_for_request(self): - - lang = 'en' # TODO: use user's language - self.choices = self.choices.filter(lang=lang) - return super(ThesaurusKeywordLabelAutocomplete, self).choices_for_request() - - -if hasattr(settings, 'THESAURUS') and settings.THESAURUS: - thesaurus = settings.THESAURUS - tname = thesaurus['name'] - ac_name = 'thesaurus_' + tname - - logger.debug('Registering thesaurus autocomplete for {}: {}'.format(tname, ac_name)) - - register( - ThesaurusKeywordLabelAutocomplete, - name=ac_name, - choices=ThesaurusKeywordLabel.objects.filter(Q(keyword__thesaurus__identifier=tname)) - ) diff --git a/geonode/base/fields.py b/geonode/base/fields.py index c978f2e9eb2..825f914bbcc 100644 --- a/geonode/base/fields.py +++ b/geonode/base/fields.py @@ -18,46 +18,13 @@ # ######################################################################### -import logging -import traceback - from django import forms -from django.conf import settings - -from geonode.base.models import Thesaurus - -from .widgets import MultiThesaurusWidget - -logger = logging.getLogger(__name__) - - -class MultiThesauriField(forms.MultiValueField): - - widget = MultiThesaurusWidget() - def __init__(self, *args, **kwargs): - super(MultiThesauriField, self).__init__(*args, **kwargs) - self.require_all_fields = kwargs.pop('require_all_fields', True) - if hasattr(settings, 'THESAURUS') and settings.THESAURUS: - el = settings.THESAURUS - choices_list = [] - thesaurus_name = el['name'] - try: - t = Thesaurus.objects.get(identifier=thesaurus_name) - for tk in t.thesaurus.all(): - tkl = tk.keyword.filter(lang='en') - choices_list.append((tkl[0].id, tkl[0].label)) - self.fields += (forms.MultipleChoiceField(choices=tuple(choices_list)), ) - except BaseException: - tb = traceback.format_exc() - logger.exception(tb) +class MultiThesauriField(forms.ModelMultipleChoiceField): - for f in self.fields: - f.error_messages.setdefault('incomplete', - self.error_messages['incomplete']) - if self.require_all_fields: - # Set 'required' to False on the individual fields, because the - # required validation will be handled by MultiValueField, not - # by those individual fields. - f.required = False + def label_from_instance(self, obj): + # Note: Not using .get() because filter()[0] is used in original + # code. The hard-coded language is currently used throughout + # geonode. + return obj.keyword.filter(lang='en').first().label diff --git a/geonode/base/forms.py b/geonode/base/forms.py index 46923a45c49..9416530013e 100644 --- a/geonode/base/forms.py +++ b/geonode/base/forms.py @@ -19,16 +19,20 @@ ######################################################################### import logging -import traceback from .fields import MultiThesauriField -from autocomplete_light.widgets import ChoiceWidget -from autocomplete_light.contrib.taggit_field import TaggitField, TaggitWidget +from dal import autocomplete +from taggit.forms import TagField + +import six from django import forms from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group from django.core import validators +from django.db.models import Prefetch, Q from django.forms import models from django.forms import ModelForm from django.forms.fields import ChoiceField @@ -36,7 +40,6 @@ from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ -from django.db.models import Q from django.utils.encoding import ( force_text, @@ -46,10 +49,11 @@ from modeltranslation.forms import TranslationModelForm from geonode.base.models import HierarchicalKeyword, TopicCategory, Region, License, CuratedThumbnail +from geonode.base.models import ThesaurusKeyword, ThesaurusKeywordLabel +from geonode.documents.models import Document from geonode.people.models import Profile from geonode.base.enumerations import ALL_LANGUAGES -from django.contrib.auth.models import Group -from django.contrib.auth import get_user_model +from geonode.base.widgets import TaggitSelect2Custom logger = logging.getLogger(__name__) @@ -111,28 +115,30 @@ def label_from_instance(self, obj): '
' + obj.gn_description + '' -class TreeWidget(TaggitWidget): - input_type = 'text' - - def render(self, name, value, attrs=None): - if isinstance(value, str): - vals = value - elif value: - vals = ','.join([i.tag.name for i in value]) - else: - vals = "" - output = ["""
-
""" % (vals)] - output.append( - '') - output.append( - '') - output.append('
') - - return mark_safe('\n'.join(output)) +# NOTE: This is commented as it needs updating to work with select2 and autocomlete light. +# +# class TreeWidget(autocomplete.TaggitSelect2): +# input_type = 'text' + +# def render(self, name, value, attrs=None): +# if isinstance(value, basestring): +# vals = value +# elif value: +# vals = ','.join([i.tag.name for i in value]) +# else: +# vals = "" +# output = ["""
+#
""" % (vals)] +# output.append( +# '') +# output.append( +# '') +# output.append('
') + +# return mark_safe(u'\n'.join(output)) class RegionsMultipleChoiceField(forms.MultipleChoiceField): @@ -211,7 +217,7 @@ def render_options(self, selected_choices): # Normalize to strings. def _region_id_from_choice(choice): if isinstance(choice, int) or \ - (isinstance(choice, str) and choice.isdigit()): + (isinstance(choice, six.string_types) and choice.isdigit()): return int(choice) else: return choice.id @@ -292,30 +298,24 @@ def clean(self): return cleaned_data -class TKeywordForm(forms.Form): +class TKeywordForm(forms.ModelForm): + prefix = 'tkeywords' + + class Meta: + model = Document + fields = ['tkeywords'] + tkeywords = MultiThesauriField( + queryset=ThesaurusKeyword.objects.prefetch_related( + Prefetch('keyword', queryset=ThesaurusKeywordLabel.objects.filter(lang='en')) + ), + widget=autocomplete.ModelSelect2Multiple( + url='thesaurus_autocomplete', + ), label=_("Keywords from Thesaurus"), required=False, - help_text=_("List of keywords from Thesaurus")) - - def __init__(self, *args, **kwargs): - super(TKeywordForm, self).__init__(*args, **kwargs) - initial_arguments = kwargs.get('initial', None) - if initial_arguments and 'tkeywords' in initial_arguments and \ - isinstance(initial_arguments['tkeywords'], str): - initial_arguments['tkeywords'] = initial_arguments['tkeywords'].split(',') - self.data = initial_arguments - - def clean(self): - cleaned_data = None - if self.data: - try: - cleaned_data = [{key: self.data.get(key)} for key, value in self.data.items( - ) if 'tkeywords' in key.lower() and 'autocomplete' not in key.lower()] - except BaseException: - tb = traceback.format_exc() - logger.exception(tb) - return cleaned_data + help_text=_("List of keywords from Thesaurus",), + ) class ResourceBaseDateTimePicker(DateTimePicker): @@ -336,9 +336,8 @@ class ResourceBaseForm(TranslationModelForm): empty_label="Owner", label=_("Owner"), required=False, - queryset=Profile.objects.exclude( - username='AnonymousUser'), - widget=ChoiceWidget('ProfileAutocomplete')) + queryset=Profile.objects.exclude(username='AnonymousUser'), + widget=autocomplete.ModelSelect2(url='autocomplete_profile')) date = forms.DateTimeField( label=_("Date"), @@ -367,7 +366,7 @@ class ResourceBaseForm(TranslationModelForm): required=False, queryset=Profile.objects.exclude( username='AnonymousUser'), - widget=ChoiceWidget('ProfileAutocomplete')) + widget=autocomplete.ModelSelect2(url='autocomplete_profile')) metadata_author = forms.ModelChoiceField( empty_label=_("Person outside GeoNode (fill form)"), @@ -375,14 +374,15 @@ class ResourceBaseForm(TranslationModelForm): required=False, queryset=Profile.objects.exclude( username='AnonymousUser'), - widget=ChoiceWidget('ProfileAutocomplete')) + widget=autocomplete.ModelSelect2(url='autocomplete_profile')) - keywords = TaggitField( + keywords = TagField( label=_("Free-text Keywords"), required=False, help_text=_("A space or comma-separated list of keywords. Use the widget to select from Hierarchical tree."), - widget=TreeWidget( - autocomplete='HierarchicalKeywordAutocomplete')) + # widget=TreeWidget(url='autocomplete_hierachical_keyword'), #Needs updating to work with select2 + widget=TaggitSelect2Custom(url='autocomplete_hierachical_keyword') + ) """ regions = TreeNodeMultipleChoiceField( @@ -396,6 +396,7 @@ class ResourceBaseForm(TranslationModelForm): required=False, choices=get_tree_data(), widget=RegionsSelect) + regions.widget.attrs = {"size": 20} def __init__(self, *args, **kwargs): diff --git a/geonode/base/models.py b/geonode/base/models.py index e3495826d06..443b2077518 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -63,7 +63,7 @@ from geonode.security.models import PermissionLevelMixin from taggit.managers import TaggableManager, _TaggableManager from taggit.models import TagBase, ItemBase -from treebeard.mp_tree import MP_Node +from treebeard.mp_tree import MP_Node, MP_NodeQuerySet, MP_NodeManager from geonode.people.enumerations import ROLE_VALUES @@ -316,9 +316,26 @@ class Meta: verbose_name_plural = 'Licenses' +class HierarchicalKeywordQuerySet(MP_NodeQuerySet): + """QuerySet to automatically create a root node if `depth` not given.""" + + def create(self, **kwargs): + if 'depth' not in kwargs: + return self.model.add_root(**kwargs) + return super(HierarchicalKeywordQuerySet, self).create(**kwargs) + + +class HierarchicalKeywordManager(MP_NodeManager): + + def get_queryset(self): + return HierarchicalKeywordQuerySet(self.model).order_by('path') + + class HierarchicalKeyword(TagBase, MP_Node): node_order_by = ['name'] + objects = HierarchicalKeywordManager() + @classmethod def dump_bulk_tree(cls, parent=None, keep_ids=True): """Dumps a tree branch to a python data structure.""" @@ -379,7 +396,6 @@ def tags_for(cls, model, instance=None): class _HierarchicalTagManager(_TaggableManager): - def add(self, *tags): str_tags = set([ t diff --git a/geonode/base/templatetags/base_tags.py b/geonode/base/templatetags/base_tags.py index 9f359cf4aee..4280f5fa82c 100644 --- a/geonode/base/templatetags/base_tags.py +++ b/geonode/base/templatetags/base_tags.py @@ -294,7 +294,7 @@ def get_current_path(context): def get_context_resourcetype(context): c_path = get_current_path(context) resource_types = ['layers', 'maps', 'documents', 'search', 'people', - 'groups'] + 'groups/categories', 'groups'] for resource_type in resource_types: if "/{0}/".format(resource_type) in c_path: return resource_type diff --git a/geonode/base/urls.py b/geonode/base/urls.py new file mode 100644 index 00000000000..01499a0258e --- /dev/null +++ b/geonode/base/urls.py @@ -0,0 +1,59 @@ + +######################################################################### +# +# Copyright (C) 2019 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + + +from django.conf.urls import url +from django.conf import settings + +from .views import ( + ResourceBaseAutocomplete, RegionAutocomplete, + HierarchicalKeywordAutocomplete, ThesaurusKeywordLabelAutocomplete) + + +urlpatterns = [ + url( + r'^autocomplete_response/$', + ResourceBaseAutocomplete.as_view(), + name='autocomplete_base', + ), + + url( + r'^autocomplete_region/$', + RegionAutocomplete.as_view(), + name='autocomplete_region', + ), + + url( + r'^autocomplete_hierachical_keyword/$', + HierarchicalKeywordAutocomplete.as_view(), + name='autocomplete_hierachical_keyword', + ), +] + +# Only register the url for thesuarus if it is enabled in settings +if hasattr(settings, 'THESAURUS') and settings.THESAURUS: + + urlpatterns.append( + url( + r'^thesaurus_autocomplete/$', + ThesaurusKeywordLabelAutocomplete.as_view(), + name='thesaurus_autocomplete', + ), + ) diff --git a/geonode/base/views.py b/geonode/base/views.py index 1b4fadcae63..88e78a99082 100644 --- a/geonode/base/views.py +++ b/geonode/base/views.py @@ -24,12 +24,17 @@ from django.http import HttpResponse from django.http import HttpResponseRedirect from django.core.exceptions import PermissionDenied +from django.conf import settings + +from guardian.shortcuts import get_objects_for_user +from dal import views, autocomplete from geonode.documents.models import Document from geonode.layers.models import Layer from geonode.maps.models import Map -from geonode.base.models import ResourceBase +from geonode.base.models import ResourceBase, Region, HierarchicalKeyword, ThesaurusKeywordLabel from geonode.utils import resolve_object +from geonode.security.utils import get_visible_resources from .forms import BatchEditForm from .forms import CuratedThumbnailForm @@ -131,3 +136,86 @@ def thumbnail_upload( 'resource': res, 'form': form }) + + +class SimpleSelect2View(autocomplete.Select2QuerySetView): + """ Generic select2 view for autocompletes + Params: + model: model to perform the autocomplete query on + filter_arg: property to filter with ie. name__icontains + """ + + def __init__(self, *args, **kwargs): + super(views.BaseQuerySetView, self).__init__(*args, **kwargs) + if not hasattr(self, 'filter_arg'): + raise AttributeError("SimpleSelect2View missing required 'filter_arg' argument") + + def get_queryset(self): + qs = super(views.BaseQuerySetView, self).get_queryset() + + if self.q: + qs = qs.filter(**{self.filter_arg: self.q}) + return qs + + +class ResourceBaseAutocomplete(autocomplete.Select2QuerySetView): + """ Base resource autocomplete - searches all the resources by title + returns any visible resources in this queryset for autocomplete + """ + + def get_queryset(self): + request = self.request + + permitted = get_objects_for_user(request.user, 'base.view_resourcebase') + qs = ResourceBase.objects.all().filter(id__in=permitted) + + if self.q: + qs = qs.filter(title__icontains=self.q).order_by('title') + + return get_visible_resources( + qs, + request.user if request else None, + admin_approval_required=settings.ADMIN_MODERATE_UPLOADS, + unpublished_not_visible=settings.RESOURCE_PUBLISHING, + private_groups_not_visibile=settings.GROUP_PRIVATE_RESOURCES)[:100] + + +class RegionAutocomplete(SimpleSelect2View): + + model = Region + filter_arg = 'name__icontains' + + +class HierarchicalKeywordAutocomplete(SimpleSelect2View): + + model = HierarchicalKeyword + filter_arg = 'slug__icontains' + + +class ThesaurusKeywordLabelAutocomplete(autocomplete.Select2QuerySetView): + + def get_queryset(self): + thesaurus = settings.THESAURUS + tname = thesaurus['name'] + lang = 'en' + + # Filters thesaurus results based on thesaurus name and language + qs = ThesaurusKeywordLabel.objects.all().filter( + keyword__thesaurus__identifier=tname, + lang=lang + ) + + if self.q: + qs = qs.filter(label__icontains=self.q) + + return qs + + # Overides the get results method to return custom json to frontend + def get_results(self, context): + return [ + { + 'id': self.get_result_value(result.keyword), + 'text': self.get_result_label(result), + 'selected_text': self.get_selected_result_label(result), + } for result in context['object_list'] + ] diff --git a/geonode/base/widgets.py b/geonode/base/widgets.py index 3e321c273d8..2bdd8dd7a0b 100644 --- a/geonode/base/widgets.py +++ b/geonode/base/widgets.py @@ -1,37 +1,24 @@ -# -*- coding: utf-8 -*- -######################################################################### -# -# Copyright (C) 2016 OSGeo -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -######################################################################### - -from django.conf import settings -from autocomplete_light.widgets import MultipleChoiceWidget - - -class MultiThesaurusWidget(MultipleChoiceWidget): - - def __init__(self, attrs=None): - if hasattr(settings, 'THESAURUS') and settings.THESAURUS: - el = settings.THESAURUS - widget_name = el['name'] - cleaned_name = el['name'].replace("-", " ").replace("_", " ").title() - super(MultiThesaurusWidget, self).__init__( - 'thesaurus_' + widget_name, - attrs={'placeholder': '%s - Start typing for suggestions' % cleaned_name}, - extra_context={'thesauri_title': cleaned_name}) - else: - super(MultiThesaurusWidget, self).__init__([], attrs) + +from dal_select2_taggit.widgets import TaggitSelect2 + + +class TaggitSelect2Custom(TaggitSelect2): + """Overriding Select2 tag widget for taggit's TagField. + Fixes error in tests where 'value' is None. + """ + + def value_from_datadict(self, data, files, name): + """Handle multi-word tag. + + Insure there's a comma when there's only a single multi-word tag, + or tag "Multi word" would end up as "Multi" and "word". + """ + + try: + value = super(TaggitSelect2, self).value_from_datadict(data, files, name) + + if value and ',' not in value: + value = '%s,' % value + return value + except TypeError: + return "" diff --git a/geonode/documents/admin.py b/geonode/documents/admin.py index 794eba72d8c..8817d214aed 100644 --- a/geonode/documents/admin.py +++ b/geonode/documents/admin.py @@ -19,14 +19,16 @@ ######################################################################### from django.contrib import admin + +from modeltranslation.admin import TabbedTranslationAdmin + from geonode.documents.models import Document -from geonode.base.admin import MediaTranslationAdmin, ResourceBaseAdminForm +from geonode.base.admin import ResourceBaseAdminForm from geonode.base.admin import metadata_batch_edit class DocumentAdminForm(ResourceBaseAdminForm): - - class Meta: + class Meta(ResourceBaseAdminForm.Meta): model = Document fields = '__all__' # exclude = ( @@ -34,7 +36,7 @@ class Meta: # ) -class DocumentAdmin(MediaTranslationAdmin): +class DocumentAdmin(TabbedTranslationAdmin): list_display = ('id', 'title', 'date', diff --git a/geonode/documents/autocomplete_light_registry.py b/geonode/documents/autocomplete_light_registry.py deleted file mode 100644 index c439fe493d8..00000000000 --- a/geonode/documents/autocomplete_light_registry.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -######################################################################### -# -# Copyright (C) 2016 OSGeo -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -######################################################################### - -from autocomplete_light.registry import register -from autocomplete_light.autocomplete.shortcuts import AutocompleteModelTemplate -from .models import Document - - -class DocumentAutocomplete(AutocompleteModelTemplate): - choice_template = 'autocomplete_response.html' - - -register( - Document, - DocumentAutocomplete, - search_fields=['title'], - order_by=['title'], - limit_choices=100, - autocomplete_js_attributes={ - 'placeholder': 'Document name..', - }, -) diff --git a/geonode/documents/forms.py b/geonode/documents/forms.py index d4a8e7703db..5328739da67 100644 --- a/geonode/documents/forms.py +++ b/geonode/documents/forms.py @@ -22,7 +22,6 @@ import json import os import re -from autocomplete_light.registry import autodiscover from django import forms from django.utils.translation import ugettext as _ @@ -39,8 +38,6 @@ from geonode.maps.models import Map from geonode.layers.models import Layer -autodiscover() # flake8: noqa - class DocumentFormMixin(object): diff --git a/geonode/documents/templates/documents/document_metadata_advanced.html b/geonode/documents/templates/documents/document_metadata_advanced.html index b2ad2110eae..9347e36b358 100644 --- a/geonode/documents/templates/documents/document_metadata_advanced.html +++ b/geonode/documents/templates/documents/document_metadata_advanced.html @@ -12,6 +12,9 @@ {{ block.super }} + +{{ document_form.media }} + {% csrf_token %} +
{{ document_form|as_bootstrap }} + {% if THESAURI_FILTERS %} + {{ tkeywords_form }} + {% endif %}
diff --git a/geonode/documents/templates/layouts/doc_panels.html b/geonode/documents/templates/layouts/doc_panels.html index 8147dfe9cfe..a499984a12e 100644 --- a/geonode/documents/templates/layouts/doc_panels.html +++ b/geonode/documents/templates/layouts/doc_panels.html @@ -269,10 +269,13 @@
{{ document_form.keywords }} + {{ document_form.media }}
{% if THESAURI_FILTERS %}
{{ tkeywords_form }} + + {{ tkeywords_form.media }}
{% endif %}
diff --git a/geonode/documents/urls.py b/geonode/documents/urls.py index 6c160892441..39a423a4666 100644 --- a/geonode/documents/urls.py +++ b/geonode/documents/urls.py @@ -23,6 +23,7 @@ from django.views.generic import TemplateView from .views import DocumentUploadView, DocumentUpdateView +from .views import DocumentAutocomplete from . import views from geonode.monitoring import register_url_event @@ -63,4 +64,6 @@ name='document_metadata_advanced'), url(r'^(?P[^/]*)/thumb_upload$', views.document_thumb_upload, name='document_thumb_upload'), + url(r'^autocomplete/$', + DocumentAutocomplete.as_view(), name='autocomplete_document'), ] diff --git a/geonode/documents/views.py b/geonode/documents/views.py index d4663ec96a4..44f39636de6 100644 --- a/geonode/documents/views.py +++ b/geonode/documents/views.py @@ -24,7 +24,7 @@ import traceback from itertools import chain -from guardian.shortcuts import get_perms +from guardian.shortcuts import get_perms, get_objects_for_user from django.shortcuts import render, get_object_or_404 from django.http import HttpResponse, HttpResponseRedirect, Http404 @@ -57,6 +57,9 @@ from geonode.base.views import batch_modify from geonode.monitoring import register_event from geonode.monitoring.models import EventType +from geonode.security.utils import get_visible_resources + +from dal import autocomplete logger = logging.getLogger("geonode.documents.views") @@ -374,9 +377,7 @@ def document_metadata( category_form = CategoryForm(request.POST, prefix="category_choice_field", initial=int( request.POST["category_choice_field"]) if "category_choice_field" in request.POST and request.POST["category_choice_field"] else None) - tkeywords_form = TKeywordForm( - prefix="tkeywords", - initial={'tkeywords': request.POST.getlist('tkeywords-tkeywords')}) + tkeywords_form = TKeywordForm(request.POST) else: document_form = DocumentForm(instance=document, prefix="resource") category_form = CategoryForm( @@ -406,9 +407,7 @@ def document_metadata( tb = traceback.format_exc() logger.error(tb) - tkeywords_form = TKeywordForm( - prefix="tkeywords", - initial={'tkeywords': tkeywords_list}) + tkeywords_form = TKeywordForm(instance=document) if request.method == "POST" and document_form.is_valid( ) and category_form.is_valid(): @@ -484,37 +483,22 @@ def document_metadata( try: # Keywords from THESAURUS management - tkeywords_to_add = [] - tkeywords_cleaned = tkeywords_form.clean() - if tkeywords_cleaned and len(tkeywords_cleaned) > 0: - tkeywords_ids = [] - for i, val in enumerate(tkeywords_cleaned): - try: - for key, value in tkeywords_cleaned[i].items(): - if 'tkeywords' in key.lower() and 'autocomplete' not in key.lower(): - tkeywords_ids.extend(map(int, value)) - break - except BaseException: - pass - - if hasattr(settings, 'THESAURUS') and settings.THESAURUS: - el = settings.THESAURUS - thesaurus_name = el['name'] - try: - t = Thesaurus.objects.get( - identifier=thesaurus_name) - for tk in t.thesaurus.all(): - tkl = tk.keyword.filter(pk__in=tkeywords_ids) - if len(tkl) > 0: - tkeywords_to_add.append(tkl[0].keyword_id) - document.tkeywords.clear() - document.tkeywords.add(*tkeywords_to_add) - except BaseException: - tb = traceback.format_exc() - logger.error(tb) + # Rewritten to work with updated autocomplete + if not tkeywords_form.is_valid(): + return HttpResponse(json.dumps({'message': "Invalid thesaurus keywords"}, status_code=400)) + + tkeywords_data = tkeywords_form.cleaned_data['tkeywords'] + + thesaurus_setting = getattr(settings, 'THESAURUS', None) + if thesaurus_setting: + tkeywords_data = tkeywords_data.filter( + thesaurus__identifier=thesaurus_setting['name'] + ) + document.tkeywords = tkeywords_data except BaseException: tb = traceback.format_exc() logger.error(tb) + return HttpResponse(json.dumps({'message': message})) # - POST Request Ends here - @@ -738,3 +722,23 @@ def document_metadata_detail( @login_required def document_batch_metadata(request, ids): return batch_modify(request, ids, 'Document') + + +class DocumentAutocomplete(autocomplete.Select2QuerySetView): + + def get_queryset(self): + request = self.request + permitted = get_objects_for_user( + request.user, + 'base.view_resourcebase') + qs = Document.objects.all().filter(id__in=permitted) + + if self.q: + qs = qs.filter(title__icontains=self.q) + + return get_visible_resources( + qs, + request.user if request else None, + admin_approval_required=settings.ADMIN_MODERATE_UPLOADS, + unpublished_not_visible=settings.RESOURCE_PUBLISHING, + private_groups_not_visibile=settings.GROUP_PRIVATE_RESOURCES) diff --git a/geonode/groups/autocomplete_light_registry.py b/geonode/groups/autocomplete_light_registry.py deleted file mode 100644 index 440d1aa5498..00000000000 --- a/geonode/groups/autocomplete_light_registry.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -######################################################################### -# -# Copyright (C) 2016 OSGeo -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -######################################################################### - -from autocomplete_light.registry import register -from autocomplete_light.autocomplete.shortcuts import AutocompleteModelTemplate -from .models import GroupProfile - - -class GroupProfileAutocomplete(AutocompleteModelTemplate): - choice_template = 'autocomplete_response.html' - - -register( - GroupProfile, - GroupProfileAutocomplete, - search_fields=['title'], -) diff --git a/geonode/groups/models.py b/geonode/groups/models.py index f9fee71d9f9..3444567db75 100644 --- a/geonode/groups/models.py +++ b/geonode/groups/models.py @@ -45,7 +45,8 @@ class Meta: verbose_name_plural = _('Group Categories') def __unicode__(self): - return 'Category: {}'.format(self.name.encode('utf-8')) + # return 'Category: {}'.format(self.name.encode('utf-8')) + return self.name.encode('utf-8') def get_absolute_url(self): return reverse('group_category_detail', args=(self.slug,)) diff --git a/geonode/groups/urls.py b/geonode/groups/urls.py index 8a35b26d055..bcbe428bab7 100644 --- a/geonode/groups/urls.py +++ b/geonode/groups/urls.py @@ -56,4 +56,8 @@ views.group_join, name='group_join'), url(r'^group/(?P[-\w]+)/activity/$', GroupActivityView.as_view(), name='group_activity'), + url(r'^autocomplete/$', + views.GroupProfileAutocomplete.as_view(), name='autocomplete_groups'), + url(r'^autocomplete_category/$', + views.GroupCategoryAutocomplete.as_view(), name='autocomplete_category'), ] diff --git a/geonode/groups/views.py b/geonode/groups/views.py index 801c2460b4f..27efc783ed0 100644 --- a/geonode/groups/views.py +++ b/geonode/groups/views.py @@ -38,8 +38,12 @@ from django.views.generic import ListView, CreateView from django.views.generic.edit import UpdateView from django.views.generic.detail import DetailView +from django.db.models import Q from geonode.decorators import view_decorator, superuser_only +from geonode.base.views import SimpleSelect2View + +from dal import autocomplete from . import forms from . import models @@ -298,3 +302,28 @@ def getKey(action): action_list.extend(context['action_list_comments']) context['action_list'] = sorted(action_list, key=getKey, reverse=True) return context + + +class GroupProfileAutocomplete(autocomplete.Select2QuerySetView): + def get_queryset(self): + request = self.request + user = request.user + qs = models.GroupProfile.objects.all() + + if self.q: + qs = qs.filter(title__icontains=self.q) + + if not user.is_authenticated() or user.is_anonymous: + qs = qs.exclude(access='private') + elif not user.is_superuser: + groups_member_of = user.group_list_all() + qs = qs.filter( + Q(self__in=groups_member_of) | + ~Q(groupprofile__access='private')) + + return qs + + +class GroupCategoryAutocomplete(SimpleSelect2View): + model = models.GroupCategory + filter_arg = 'name__icontains' diff --git a/geonode/layers/admin.py b/geonode/layers/admin.py index 286906736c4..4f7c4bc0e33 100644 --- a/geonode/layers/admin.py +++ b/geonode/layers/admin.py @@ -19,12 +19,20 @@ ######################################################################### from django.contrib import admin +from django.db.models import Prefetch -from geonode.base.admin import MediaTranslationAdmin, ResourceBaseAdminForm +from modeltranslation.admin import TabbedTranslationAdmin + +from geonode.base.admin import ResourceBaseAdminForm from geonode.base.admin import metadata_batch_edit, set_batch_permissions from geonode.layers.models import Layer, Attribute, Style from geonode.layers.models import LayerFile, UploadSession +from geonode.base.fields import MultiThesauriField +from geonode.base.models import ThesaurusKeyword, ThesaurusKeywordLabel + +from dal import autocomplete + class AttributeInline(admin.TabularInline): model = Attribute @@ -32,12 +40,24 @@ class AttributeInline(admin.TabularInline): class LayerAdminForm(ResourceBaseAdminForm): - class Meta: + class Meta(ResourceBaseAdminForm.Meta): model = Layer fields = '__all__' + tkeywords = MultiThesauriField( + queryset=ThesaurusKeyword.objects.prefetch_related( + Prefetch('keyword', queryset=ThesaurusKeywordLabel.objects.filter(lang='en')) + ), + widget=autocomplete.ModelSelect2Multiple( + url='thesaurus_autocomplete', + ), + label=("Keywords from Thesaurus"), + required=False, + help_text=("List of keywords from Thesaurus",), + ) + -class LayerAdmin(MediaTranslationAdmin): +class LayerAdmin(TabbedTranslationAdmin): list_display = ( 'id', 'alternate', diff --git a/geonode/layers/autocomplete_light_registry.py b/geonode/layers/autocomplete_light_registry.py deleted file mode 100644 index 0f847336e97..00000000000 --- a/geonode/layers/autocomplete_light_registry.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -######################################################################### -# -# Copyright (C) 2016 OSGeo -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -######################################################################### - -from autocomplete_light.registry import register -from autocomplete_light.autocomplete.shortcuts import AutocompleteModelTemplate -from .models import Layer -from guardian.shortcuts import get_objects_for_user - -from django.conf import settings -from geonode.security.utils import get_visible_resources - - -class LayerAutocomplete(AutocompleteModelTemplate): - choice_template = 'autocomplete_response.html' - - def choices_for_request(self): - request = self.request - permitted = get_objects_for_user( - request.user, - 'base.view_resourcebase') - self.choices = self.choices.filter(id__in=permitted) - - self.choices = get_visible_resources( - self.choices, - request.user if request else None, - admin_approval_required=settings.ADMIN_MODERATE_UPLOADS, - unpublished_not_visible=settings.RESOURCE_PUBLISHING, - private_groups_not_visibile=settings.GROUP_PRIVATE_RESOURCES) - - return super(LayerAutocomplete, self).choices_for_request() - - -register( - Layer, - LayerAutocomplete, - search_fields=[ - 'title', - '^alternate'], - order_by=['title'], - limit_choices=100, - autocomplete_js_attributes={ - 'placeholder': 'Layer name..', - }, -) diff --git a/geonode/layers/forms.py b/geonode/layers/forms.py index a34eb0445c8..c1d7e72b01d 100644 --- a/geonode/layers/forms.py +++ b/geonode/layers/forms.py @@ -22,7 +22,6 @@ import os import tempfile import zipfile -from autocomplete_light.registry import autodiscover from django import forms @@ -33,8 +32,6 @@ from geonode.utils import unzip_file from geonode.layers.models import Layer, Attribute -autodiscover() # flake8: noqa - class JSONField(forms.CharField): diff --git a/geonode/layers/templates/layers/layer_metadata_advanced.html b/geonode/layers/templates/layers/layer_metadata_advanced.html index 82f8bd2131b..2fe751ec8fe 100644 --- a/geonode/layers/templates/layers/layer_metadata_advanced.html +++ b/geonode/layers/templates/layers/layer_metadata_advanced.html @@ -12,6 +12,9 @@ {{ block.super }} + +{{ layer_form.media }} +