Пример #1
0
def test_urlencode():
    """Our urlencode is Unicode-safe."""
    items = [('q', u'Fran\xe7ais')]
    eq_('q=Fran%C3%A7ais', urlencode(items))

    items = [('q', u'は「着')]
    eq_('q=%E3%81%AF%E3%80%8C%E7%9D%80', urlencode(items))
Пример #2
0
def test_urlencode_int():
    """urlencode() should not choke on integers."""
    items = [('q', 't'), ('a', 1)]
    eq_('q=t&a=1', urlencode(items))
Пример #3
0
def search(request):
    """Performs search or displays the search form"""

    # Form must be nested inside request for fixtures to be used properly
    class SearchForm(forms.Form):
        """Django form for handling display and validation"""

        def clean(self):
            """Clean up data and set defaults"""

            cleaned_data = self.cleaned_data

            if ('a' not in cleaned_data or
                not cleaned_data['a']) and cleaned_data['q'] == '':
                raise ValidationError('Basic search requires a query string.')

            # Validate created and updated dates
            date_fields = (('created', 'created_date'),
                           ('updated', 'updated_date'))
            for field_option, field_date in date_fields:
                if cleaned_data[field_date] != '':
                    try:
                        created_timestamp = time.mktime(
                            time.strptime(cleaned_data[field_date],
                                          '%m/%d/%Y'))
                        cleaned_data[field_date] = int(created_timestamp)
                    except (ValueError, OverflowError):
                        cleaned_data[field_option] = None
                else:
                    cleaned_data[field_option] = None

            # Validate all integer fields
            if not cleaned_data.get('num_votes'):
                cleaned_data['num_votes'] = 0

            # Set defaults for MultipleChoiceFields and convert to ints.
            # Ticket #12398 adds TypedMultipleChoiceField which would replace
            # MultipleChoiceField + map(int, ...) and use coerce instead.
            if cleaned_data.get('category'):
                try:
                    cleaned_data['category'] = map(int,
                                                   cleaned_data['category'])
                except ValueError:
                    cleaned_data['category'] = None
            try:
                cleaned_data['forum'] = map(int, cleaned_data.get('forum'))
            except ValueError:
                cleaned_data['forum'] = None

            try:
                cleaned_data['thread_type'] = map(
                    int, cleaned_data.get('thread_type'))
            except ValueError:
                cleaned_data['thread_type'] = None

            return cleaned_data

        class NoValidateMultipleChoiceField(forms.MultipleChoiceField):
            def valid_value(self, value):
                return True

        # Common fields
        q = forms.CharField(required=False)

        w = forms.TypedChoiceField(
            widget=forms.HiddenInput, required=False, coerce=int,
            empty_value=constants.WHERE_BASIC,
            choices=((constants.WHERE_SUPPORT, None),
                     (constants.WHERE_WIKI, None),
                     (constants.WHERE_BASIC, None),
                     (constants.WHERE_DISCUSSION, None)))

        a = forms.IntegerField(widget=forms.HiddenInput, required=False)

        # KB fields
        tag_widget = forms.TextInput(attrs={'placeholder': _('tag1, tag2'),
                                            'class': 'auto-fill'})
        tags = forms.CharField(label=_('Tags'), required=False,
                               widget=tag_widget)

        language = forms.ChoiceField(
            label=_('Language'), required=False,
            choices=[(LOCALES[k].external, LOCALES[k].native) for
                     k in settings.SUMO_LANGUAGES])

        categories = [(cat.categId, cat.name) for
                      cat in Category.objects.all()]
        category = NoValidateMultipleChoiceField(
            widget=forms.CheckboxSelectMultiple,
            label=_('Category'), choices=categories, required=False)

        # Support questions and discussion forums fields
        created = forms.TypedChoiceField(
            label=_('Created'), coerce=int, empty_value=0,
            choices=constants.DATE_LIST, required=False)
        created_date = forms.CharField(required=False)

        updated = forms.TypedChoiceField(
            label=_('Last updated'), coerce=int, empty_value=0,
            choices=constants.DATE_LIST, required=False)
        updated_date = forms.CharField(required=False)

        user_widget = forms.TextInput(attrs={'placeholder': _('username'),
                                             'class': 'auto-fill'})

        # Discussion forums fields
        author = forms.CharField(required=False, widget=user_widget)

        sortby = forms.TypedChoiceField(
            label=_('Sort results by'), coerce=int, empty_value=0,
            choices=constants.SORTBY_FORUMS, required=False)

        thread_type = NoValidateMultipleChoiceField(
            label=_('Thread type'), choices=constants.DISCUSSION_STATUS_LIST,
            required=False,
            widget=forms.CheckboxSelectMultiple)

        forums = [(f.id, f.name) for f in DiscussionForum.objects.all()]
        forum = NoValidateMultipleChoiceField(label=_('Search in forum'),
                                              choices=forums, required=False)

        # Support questions fields
        asked_by = forms.CharField(required=False, widget=user_widget)
        answered_by = forms.CharField(required=False, widget=user_widget)

        sortby_questions = forms.TypedChoiceField(
            label=_('Sort results by'), coerce=int, empty_value=0,
            choices=constants.SORTBY_QUESTIONS, required=False)

        is_locked = forms.TypedChoiceField(
            label=_('Locked'), coerce=int, empty_value=0,
            choices=constants.TERNARY_LIST, required=False,
            widget=forms.RadioSelect)

        is_solved = forms.TypedChoiceField(
            label=_('Solved'), coerce=int, empty_value=0,
            choices=constants.TERNARY_LIST, required=False,
            widget=forms.RadioSelect)

        has_answers = forms.TypedChoiceField(
            label=_('Has answers'), coerce=int, empty_value=0,
            choices=constants.TERNARY_LIST, required=False,
            widget=forms.RadioSelect)

        has_helpful = forms.TypedChoiceField(
            label=_('Has helpful answers'), coerce=int, empty_value=0,
            choices=constants.TERNARY_LIST, required=False,
            widget=forms.RadioSelect)

        num_voted = forms.TypedChoiceField(
            label=_('Votes'), coerce=int, empty_value=0,
            choices=constants.NUMBER_LIST, required=False)
        num_votes = forms.IntegerField(required=False)

        q_tags = forms.CharField(label=_('Tags'), required=False,
                                 widget=tag_widget)

    # JSON-specific variables
    is_json = (request.GET.get('format') == 'json')
    callback = request.GET.get('callback', '').strip()
    mimetype = 'application/x-javascript' if callback else 'application/json'

    # Search "Expires" header format
    expires_fmt = '%A, %d %B %Y %H:%M:%S GMT'

    # Check callback is valid
    if is_json and callback and not jsonp_is_valid(callback):
        return HttpResponse(
            json.dumps({'error': _('Invalid callback function.')}),
            mimetype=mimetype, status=400)

    language = locale_or_default(request.GET.get('language', request.locale))
    r = request.GET.copy()
    a = request.GET.get('a', '0')

    # Search default values
    try:
        category = map(int, r.getlist('category')) or \
                   settings.SEARCH_DEFAULT_CATEGORIES
    except ValueError:
        category = settings.SEARCH_DEFAULT_CATEGORIES
    r.setlist('category', [x for x in category if x > 0])
    exclude_category = [abs(x) for x in category if x < 0]
    # Basic form
    if a == '0':
        r['w'] = r.get('w', constants.WHERE_BASIC)
    # Advanced form
    if a == '2':
        r['language'] = language
        r['a'] = '1'

    search_form = SearchForm(r)

    if not search_form.is_valid() or a == '2':
        if is_json:
            return HttpResponse(
                json.dumps({'error': _('Invalid search data.')}),
                mimetype=mimetype,
                status=400)

        search_ = jingo.render(request, 'search/form.html',
                            {'advanced': a, 'request': request,
                             'search_form': search_form})
        search_['Cache-Control'] = 'max-age=%s' % \
                                   (settings.SEARCH_CACHE_PERIOD * 60)
        search_['Expires'] = (datetime.utcnow() +
                              timedelta(
                                minutes=settings.SEARCH_CACHE_PERIOD)) \
                              .strftime(expires_fmt)
        return search_

    cleaned = search_form.cleaned_data
    search_locale = (sphinx_locale(language),)

    try:
        page = int(request.GET.get('page', 1))
        page = max(page, 1)
    except ValueError:
        page = 1
    offset = (page - 1) * settings.SEARCH_RESULTS_PER_PAGE

    # get language name for display in template
    lang = language.lower()
    if settings.LANGUAGES.get(lang):
        lang_name = settings.LANGUAGES[lang]
    else:
        lang_name = ''

    documents = []
    filters_w = []
    filters_q = []
    filters_f = []

    # wiki filters
    # Category filter
    if cleaned['category']:
        filters_w.append({
            'filter': 'category',
            'value': cleaned['category'],
        })

    if exclude_category:
        filters_w.append({
            'filter': 'category',
            'value': exclude_category,
            'exclude': True,
        })

    # Locale filter
    filters_w.append({
        'filter': 'locale',
        'value': search_locale,
    })

    # Tags filter
    tags = [crc32(t.strip()) for t in cleaned['tags'].split()]
    if tags:
        for t in tags:
            filters_w.append({
                'filter': 'tag',
                'value': (t,),
                })
    # End of wiki filters

    # Support questions specific filters
    if cleaned['w'] & constants.WHERE_SUPPORT:

        # Solved is set by default if using basic search
        if a == '0' and not cleaned['is_solved']:
            cleaned['is_solved'] = constants.TERNARY_YES

        # These filters are ternary, they can be either YES, NO, or OFF
        toggle_filters = ('is_locked', 'is_solved', 'has_answers',
                          'has_helpful')
        for filter_name in toggle_filters:
            if cleaned[filter_name] == constants.TERNARY_YES:
                filters_q.append({
                    'filter': filter_name,
                    'value': (True,),
                })
            if cleaned[filter_name] == constants.TERNARY_NO:
                filters_q.append({
                    'filter': filter_name,
                    'value': (False,),
                })

        if cleaned['asked_by']:
            filters_q.append({
                'filter': 'question_creator',
                'value': (crc32(cleaned['asked_by']),),
            })

        if cleaned['answered_by']:
            filters_q.append({
                'filter': 'answer_creator',
                'value': (crc32(cleaned['answered_by']),),
            })

        q_tags = [crc32(t.strip()) for t in cleaned['q_tags'].split()]
        if q_tags:
            for t in q_tags:
                filters_q.append({
                    'filter': 'tag',
                    'value': (t,),
                    })

    # Discussion forum specific filters
    if cleaned['w'] & constants.WHERE_DISCUSSION:
        if cleaned['author']:
            filters_f.append({
                'filter': 'author_ord',
                'value': (crc32(cleaned['author']),),
            })

        if cleaned['thread_type']:
            if constants.DISCUSSION_STICKY in cleaned['thread_type']:
                filters_f.append({
                    'filter': 'is_sticky',
                    'value': (1,),
                })

            if constants.DISCUSSION_LOCKED in cleaned['thread_type']:
                filters_f.append({
                    'filter': 'is_locked',
                    'value': (1,),
                })

        if cleaned['forum']:
            filters_f.append({
                'filter': 'forum_id',
                'value': cleaned['forum'],
            })

    # Filters common to support and discussion forums
    # Created filter
    unix_now = int(time.time())
    interval_filters = (
        ('created', cleaned['created'], cleaned['created_date']),
        ('updated', cleaned['updated'], cleaned['updated_date']),
        ('question_votes', cleaned['num_voted'], cleaned['num_votes']))
    for filter_name, filter_option, filter_date in interval_filters:
        if filter_option == constants.INTERVAL_BEFORE:
            before = {
                'range': True,
                'filter': filter_name,
                'min': 0,
                'max': max(filter_date, 0),
            }
            if filter_name != 'question_votes':
                filters_f.append(before)
            filters_q.append(before)
        elif filter_option == constants.INTERVAL_AFTER:
            after = {
                'range': True,
                'filter': filter_name,
                'min': min(filter_date, unix_now),
                'max': unix_now,
            }
            if filter_name != 'question_votes':
                filters_f.append(after)
            filters_q.append(after)

    sortby = int(request.GET.get('sortby', 0))
    try:
        if cleaned['w'] & constants.WHERE_WIKI:
            wc = WikiClient()  # Wiki SearchClient instance
            # Execute the query and append to documents
            documents += wc.query(cleaned['q'], filters_w)

        if cleaned['w'] & constants.WHERE_SUPPORT:
            qc = QuestionsClient()  # Support question SearchClient instance

            # Sort results by
            try:
                qc.set_sort_mode(constants.SORT_QUESTIONS[sortby][0],
                                 constants.SORT_QUESTIONS[sortby][1])
            except IndexError:
                pass

            documents += qc.query(cleaned['q'], filters_q)

        if cleaned['w'] & constants.WHERE_DISCUSSION:
            dc = DiscussionClient()  # Discussion forums SearchClient instance

            # Sort results by
            try:
                dc.set_groupsort(constants.GROUPSORT[sortby])
            except IndexError:
                pass

            documents += dc.query(cleaned['q'], filters_f)

    except SearchError:
        if is_json:
            return HttpResponse(json.dumps({'error':
                                             _('Search Unavailable')}),
                                mimetype=mimetype, status=503)

        return jingo.render(request, 'search/down.html', {}, status=503)

    pages = paginate(request, documents, settings.SEARCH_RESULTS_PER_PAGE)

    results = []
    for i in range(offset, offset + settings.SEARCH_RESULTS_PER_PAGE):
        try:
            if documents[i]['attrs'].get('category', False) != False:
                wiki_page = WikiPage.objects.get(pk=documents[i]['id'])

                excerpt = wc.excerpt(wiki_page.content, cleaned['q'])
                summary = jinja2.Markup(excerpt)

                result = {'search_summary': summary,
                          'url': wiki_page.get_url(),
                          'title': wiki_page.name, }
                results.append(result)
            elif documents[i]['attrs'].get('question_creator', False) != False:
                question = Question.objects.get(
                    pk=documents[i]['attrs']['question_id'])

                excerpt = qc.excerpt(question.content, cleaned['q'])
                summary = jinja2.Markup(excerpt)

                result = {'search_summary': summary,
                          'url': question.get_absolute_url(),
                          'title': question.title, }
                results.append(result)
            else:
                thread = Thread.objects.get(
                    pk=documents[i]['attrs']['thread_id'])
                post = Post.objects.get(pk=documents[i]['id'])

                excerpt = dc.excerpt(post.content, cleaned['q'])
                summary = jinja2.Markup(excerpt)

                result = {'search_summary': summary,
                          'url': thread.get_absolute_url(),
                          'title': thread.title, }
                results.append(result)
        except IndexError:
            break
        except (WikiPage.DoesNotExist, Question.DoesNotExist,
                Thread.DoesNotExist):
            continue

    items = [(k, v) for k in search_form.fields for
             v in r.getlist(k) if v and k != 'a']
    items.append(('a', '2'))

    refine_query = u'?%s' % urlencode(items)

    if is_json:
        data = {}
        data['results'] = results
        data['total'] = len(results)
        data['query'] = cleaned['q']
        if not results:
            data['message'] = _('No pages matched the search criteria')
        json_data = json.dumps(data)
        if callback:
            json_data = callback + '(' + json_data + ');'

        return HttpResponse(json_data, mimetype=mimetype)

    results_ = jingo.render(request, 'search/results.html',
        {'num_results': len(documents), 'results': results, 'q': cleaned['q'],
         'pages': pages, 'w': cleaned['w'], 'refine_query': refine_query,
         'search_form': search_form, 'lang_name': lang_name, })
    results_['Cache-Control'] = 'max-age=%s' % \
                                (settings.SEARCH_CACHE_PERIOD * 60)
    results_['Expires'] = (datetime.utcnow() +
                           timedelta(minutes=settings.SEARCH_CACHE_PERIOD)) \
                           .strftime(expires_fmt)
    return results_