Compare commits
6 Commits
main
...
wip/guiche
Author | SHA1 | Date |
---|---|---|
Frédéric Péters | cb4ccd0579 | |
Frédéric Péters | 80fcdb2f67 | |
Frédéric Péters | 3eb37ec764 | |
Frédéric Péters | 28a2dab203 | |
Frédéric Péters | c0031f62d2 | |
Thomas NOËL | d4f0d28e6e |
|
@ -4,6 +4,8 @@ Configure search services in settings.COMBO_SEARCH_SERVICES:
|
|||
'user': {
|
||||
'label': 'Search a user',
|
||||
'url': 'https://.../api/user/?q=%(q)s',
|
||||
# 'cache_duration': 60, # in seconds, default is 0
|
||||
# 'signature': True, # boolean, default is False
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
import django.apps
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .engines import engines
|
||||
|
||||
|
||||
class AppConfig(django.apps.AppConfig):
|
||||
name = 'combo.apps.search'
|
||||
verbose_name = _('Search')
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2014-2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Engines(object):
|
||||
"""Singleton object that serves as a registry of classes providing search
|
||||
engines."""
|
||||
|
||||
def __init__(self):
|
||||
self.engines = {}
|
||||
|
||||
def register(self, key, **options):
|
||||
self.engines[key] = options
|
||||
|
||||
def get(self, key):
|
||||
if key in settings.COMBO_SEARCH_SERVICES:
|
||||
return settings.COMBO_SEARCH_SERVICES[key]
|
||||
return self.engines.get(key)
|
||||
|
||||
def get_engines(self):
|
||||
data = {}
|
||||
for key in settings.COMBO_SEARCH_SERVICES:
|
||||
data[key] = settings.COMBO_SEARCH_SERVICES[key]
|
||||
for key in self.engines:
|
||||
data[key] = self.engines[key]
|
||||
return data
|
||||
|
||||
engines = Engines() # singleton object
|
|
@ -0,0 +1,34 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2014-2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django import forms
|
||||
|
||||
from combo.utils.forms import MultiSortWidget
|
||||
|
||||
from . import engines
|
||||
from .models import SearchCell
|
||||
|
||||
|
||||
class SearchCellForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = SearchCell
|
||||
fields = ('_search_services',)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SearchCellForm, self).__init__(*args, **kwargs)
|
||||
options = [(x, engines.get_engines()[x]['label']) for x in engines.get_engines().keys()]
|
||||
self.fields['_search_services'].widget = MultiSortWidget(choices=options,
|
||||
with_checkboxes=True)
|
|
@ -30,7 +30,7 @@ class Command(UpdateIndexCommand):
|
|||
dest='skip_external_links_collection')
|
||||
|
||||
def handle(self, **options):
|
||||
if SearchCell.objects.filter(_search_service='_text').count() == 0:
|
||||
if not any(SearchCell.get_cells_by_search_service('_text')):
|
||||
# do not index site if there's no matching search cell
|
||||
return
|
||||
if not options.get('skip_external_links_collection', False):
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
import jsonfield.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('search', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='searchcell',
|
||||
name='_search_services',
|
||||
field=jsonfield.fields.JSONField(default=dict,
|
||||
verbose_name='Search Services',
|
||||
blank=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def create_search_services(apps, schema_editor):
|
||||
SearchCell = apps.get_model('search', 'SearchCell')
|
||||
for searchcell in SearchCell.objects.all():
|
||||
if searchcell._search_service:
|
||||
searchcell._search_services = {'data': [searchcell._search_service]}
|
||||
else:
|
||||
searchcell._search_services = {'data': []}
|
||||
searchcell.save()
|
||||
|
||||
|
||||
def back_to_search_service(apps, schema_editor):
|
||||
SearchCell = apps.get_model('search', 'SearchCell')
|
||||
for searchcell in SearchCell.objects.all():
|
||||
if searchcell._search_services.get('data'):
|
||||
searchcell._search_service = searchcell._search_services.get('data')[0]
|
||||
else:
|
||||
searchcell._search_service = ''
|
||||
searchcell.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('search', '0002_auto_20180720_1511'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_search_services, back_to_search_service),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('search', '0003_create_search_services'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='searchcell',
|
||||
name='_search_service',
|
||||
),
|
||||
]
|
|
@ -15,43 +15,40 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django import template
|
||||
from django.http import HttpResponse
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.forms import models as model_forms, Select
|
||||
from django.utils.http import quote
|
||||
|
||||
from jsonfield import JSONField
|
||||
|
||||
from combo.utils import requests
|
||||
from combo.data.models import CellBase
|
||||
from combo.data.library import register_cell_class
|
||||
from combo.utils import NothingInCacheException, get_templated_url
|
||||
from combo.utils import get_templated_url
|
||||
|
||||
from . import engines
|
||||
|
||||
|
||||
@register_cell_class
|
||||
class SearchCell(CellBase):
|
||||
template_name = 'combo/search-cell.html'
|
||||
|
||||
_search_service = models.CharField(verbose_name=_('Search Service'), max_length=64)
|
||||
_search_services = JSONField(_('Search Services'), default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Search')
|
||||
|
||||
def is_visible(self, user=None):
|
||||
if not self.search_service:
|
||||
if not self.search_services:
|
||||
return False
|
||||
return super(SearchCell, self).is_visible(user=user)
|
||||
|
||||
def get_default_form_class(self):
|
||||
search_services = [(None, _('Not configured'))]
|
||||
search_services.append(('_text', _('Page Contents')))
|
||||
search_services.extend([(code, service['label'])
|
||||
for code, service in settings.COMBO_SEARCH_SERVICES.items()])
|
||||
widgets = {'_search_service': Select(choices=search_services)}
|
||||
return model_forms.modelform_factory(self.__class__,
|
||||
fields=['_search_service'],
|
||||
widgets=widgets)
|
||||
from .forms import SearchCellForm
|
||||
return SearchCellForm
|
||||
|
||||
@property
|
||||
def varname(self):
|
||||
|
@ -61,10 +58,24 @@ class SearchCell(CellBase):
|
|||
return ''
|
||||
|
||||
@property
|
||||
def search_service(self):
|
||||
if self._search_service == '_text':
|
||||
return {'url': reverse('api-search') + '?q=%(q)s', 'label': _('Page Contents')}
|
||||
return settings.COMBO_SEARCH_SERVICES.get(self._search_service) or {}
|
||||
def search_services(self):
|
||||
services = []
|
||||
for service_slug in self._search_services.get('data') or []:
|
||||
service = engines.get(service_slug)
|
||||
if service and service.get('url'):
|
||||
service['slug'] = service_slug
|
||||
services.append(service)
|
||||
return services
|
||||
|
||||
@property
|
||||
def has_multiple_search_services(self):
|
||||
return len(self._search_services.get('data') or []) > 1
|
||||
|
||||
@classmethod
|
||||
def get_cells_by_search_service(cls, search_service):
|
||||
for cell in cls.objects.all():
|
||||
if search_service in (cell._search_services.get('data') or []):
|
||||
yield cell
|
||||
|
||||
def modify_global_context(self, context, request):
|
||||
# if self.varname is in the query string (of the page),
|
||||
|
@ -94,25 +105,39 @@ class SearchCell(CellBase):
|
|||
return extra_context
|
||||
|
||||
@classmethod
|
||||
def ajax_results_view(cls, request, cell_pk):
|
||||
def ajax_results_view(cls, request, cell_pk, service_slug):
|
||||
cell = cls.objects.get(pk=cell_pk)
|
||||
if not cell.is_visible(request.user) or not cell.page.is_visible(request.user):
|
||||
raise PermissionDenied
|
||||
|
||||
query = request.GET.get('q')
|
||||
if query and cell.search_service.get('url'):
|
||||
url = cell.search_service.get('url')
|
||||
url = get_templated_url(url)
|
||||
url = url % {'q': quote(query.encode('utf-8'))}
|
||||
if url.startswith('/'):
|
||||
url = request.build_absolute_uri(url)
|
||||
results = requests.get(url, cache_duration=0).json()
|
||||
else:
|
||||
results = {'err': 0, 'data': []}
|
||||
def render_response(service={}, results={'err': 0, 'data': []}):
|
||||
template_names = ['combo/search-cell-results.html']
|
||||
if cell.slug:
|
||||
template_names.insert(0, 'combo/cells/%s/search-cell-results.html' % cell.slug)
|
||||
tmpl = template.loader.select_template(template_names)
|
||||
context = {'cell': cell, 'results': results, 'search_service': service}
|
||||
return HttpResponse(tmpl.render(context, request), content_type='text/html')
|
||||
|
||||
template_names = ['combo/search-cell-results.html']
|
||||
if cell.slug:
|
||||
template_names.insert(0, 'combo/cells/%s/search-cell-results.html' % cell.slug)
|
||||
tmpl = template.loader.select_template(template_names)
|
||||
context= {'cell': cell, 'results': results}
|
||||
return HttpResponse(tmpl.render(context, request), content_type='text/html')
|
||||
for service in cell.search_services:
|
||||
if service.get('slug') == service_slug:
|
||||
break
|
||||
else:
|
||||
return render_response()
|
||||
|
||||
query = request.GET.get('q')
|
||||
if not query:
|
||||
return render_response(service)
|
||||
|
||||
url = get_templated_url(service['url'], context={'q': query, 'search_service': service})
|
||||
url = url % {'q': quote(query.encode('utf-8'))} # if url contains %(q)s
|
||||
if url.startswith('/'):
|
||||
url = request.build_absolute_uri(url)
|
||||
|
||||
if not url:
|
||||
return render_response(service)
|
||||
|
||||
kwargs = {}
|
||||
kwargs['cache_duration'] = service.get('cache_duration', 0)
|
||||
kwargs['remote_service'] = 'auto' if service.get('signature') else None
|
||||
results = requests.get(url, **kwargs).json()
|
||||
return render_response(service, results)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
{% if results.data %}
|
||||
{% if cell.has_multiple_search_services %}<p class="search-service-label">{{ search_service.label }}</p>{% endif %}
|
||||
<div class="links-list">
|
||||
<ul>
|
||||
{% for item in results.data %}
|
||||
|
@ -7,3 +9,4 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -2,30 +2,36 @@
|
|||
{% block cell-content %}
|
||||
|
||||
<form id="combo-search-form-{{ cell.pk }}" class="combo-search-form">
|
||||
<input type="text" name="q" autocomplete="off" id="combo-search-input-{{ cell.pk }}" class="combo-search-input"
|
||||
data-autocomplete-json="{% url 'combo-search-ajax-results' cell_pk=cell.pk %}{% if initial_query_string %}?{{ initial_query_string }}{% endif %}" />{# initial_query_string pass some context to ajax call #}
|
||||
<input type="text" name="q" autocomplete="off" id="combo-search-input-{{ cell.pk }}" class="combo-search-input" />
|
||||
<button class="submit-button">{% trans "Search" %}</button>
|
||||
</form>
|
||||
|
||||
<div id="combo-search-results-{{ cell.pk }}" class="combo-search-results"></div>
|
||||
{% for search_service in cell.search_services %}
|
||||
<div id="combo-search-results-{{ cell.pk }}-{{ forloop.counter }}" class="combo-search-results combo-search-results-{{ search_service.slug }}"></div>
|
||||
{% endfor %}
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
var combo_search_timeout_{{ cell.pk }};
|
||||
var combo_search_form_{{ cell.pk }} = $('#combo-search-form-{{ cell.pk }}');
|
||||
var combo_search_input_{{ cell.pk }} = $('#combo-search-input-{{ cell.pk }}');
|
||||
var combo_search_results_{{ cell.pk }} = $('#combo-search-results-{{ cell.pk }}');
|
||||
var xhr = null;
|
||||
{% for search_service in cell.search_services %}
|
||||
var combo_search_results_{{ cell.pk }}_{{ forloop.counter }} = $('#combo-search-results-{{ cell.pk }}-{{ forloop.counter }}');
|
||||
var xhr_{{ forloop.counter }} = null;
|
||||
var url_{{ forloop.counter }} = '{% url 'combo-search-ajax-results' cell_pk=cell.pk service_slug=search_service.slug %}{% if initial_query_string %}?{{ intial_query_string }}{% endif %}';
|
||||
{% endfor %}
|
||||
|
||||
function combo_search_update_{{ cell.pk }}() {
|
||||
if (xhr) xhr.abort();
|
||||
xhr = $.get(combo_search_input_{{ cell.pk }}.data('autocomplete-json'),
|
||||
{% for search_service in cell.search_services %}
|
||||
if (xhr_{{ forloop.counter }}) xhr_{{ forloop.counter }}.abort();
|
||||
xhr_{{ forloop.counter }} = $.get(url_{{ forloop.counter }},
|
||||
{'q': combo_search_input_{{ cell.pk }}.val()},
|
||||
function (response) {
|
||||
xhr = null;
|
||||
combo_search_results_{{ cell.pk }}.html(response);
|
||||
xhr_{{ forloop.counter }} = null;
|
||||
combo_search_results_{{ cell.pk }}_{{ forloop.counter }}.html(response);
|
||||
}
|
||||
);
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
combo_search_input_{{ cell.pk }}.on('paste keyup', function() {
|
||||
|
|
|
@ -19,6 +19,6 @@ from django.conf.urls import url
|
|||
from .models import SearchCell
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^ajax/search/(?P<cell_pk>\w+)/$', SearchCell.ajax_results_view,
|
||||
url(r'^ajax/search/(?P<cell_pk>\w+)/(?P<service_slug>[\w-]+)/$', SearchCell.ajax_results_view,
|
||||
name='combo-search-ajax-results'),
|
||||
]
|
||||
|
|
|
@ -15,12 +15,21 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import django.apps
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class AppConfig(django.apps.AppConfig):
|
||||
name = 'combo.apps.wcs'
|
||||
verbose_name = _('Forms')
|
||||
|
||||
def ready(self):
|
||||
from combo.apps.search import engines
|
||||
engines.register('tracking-code',
|
||||
url=reverse('wcs-tracking-code-search') + '?q=%(q)s',
|
||||
label=_('Tracking Code')
|
||||
)
|
||||
|
||||
def get_before_urls(self):
|
||||
from . import urls
|
||||
return urls.urlpatterns
|
||||
|
|
|
@ -20,10 +20,13 @@ from django.utils.datastructures import MultiValueDict
|
|||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from combo.utils.forms import MultiSortWidget
|
||||
|
||||
from .models import (WcsFormCell, WcsCategoryCell, WcsFormsOfCategoryCell,
|
||||
WcsCurrentFormsCell)
|
||||
from .utils import get_wcs_options, get_wcs_services
|
||||
|
||||
|
||||
class WcsFormCellForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = WcsFormCell
|
||||
|
@ -46,45 +49,6 @@ class WcsCategoryCellForm(forms.ModelForm):
|
|||
self.fields['category_reference'].widget = forms.Select(choices=references)
|
||||
|
||||
|
||||
|
||||
class MultiSortWidget(forms.SelectMultiple):
|
||||
def render(self, name, value, attrs=None, choices=()):
|
||||
# reorder choices to get them in the current value order
|
||||
self_choices = self.choices[:]
|
||||
choices_dict = dict(self_choices)
|
||||
if value:
|
||||
for option in reversed(value.get('data')):
|
||||
if not option in choices_dict:
|
||||
continue
|
||||
option_tuple = (option, choices_dict[option])
|
||||
self.choices.remove(option_tuple)
|
||||
self.choices.insert(0, option_tuple)
|
||||
|
||||
# render the <select multiple>
|
||||
if django.VERSION < (1, 11, 0):
|
||||
rendered = super(MultiSortWidget, self).render(name, value,
|
||||
attrs=attrs, choices=choices)
|
||||
else:
|
||||
rendered = super(MultiSortWidget, self).render(name, value,
|
||||
attrs=attrs)
|
||||
|
||||
# include it in a <div> that will be turned into an appropriate widget
|
||||
# in javascript
|
||||
id_ = 'wid-%s' % name
|
||||
return mark_safe('''<div class="multisort" id="%s">%s</div>
|
||||
<script type="text/javascript">multisort($("#%s"));</script>
|
||||
''' % (id_, rendered, id_))
|
||||
|
||||
def render_options(self, choices, value):
|
||||
value = value.get('data') or []
|
||||
return super(MultiSortWidget, self).render_options(choices, value)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
if isinstance(data, MultiValueDict):
|
||||
return {'data': data.getlist(name)}
|
||||
return data.get(name, None)
|
||||
|
||||
|
||||
class WcsFormsOfCategoryCellForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = WcsFormsOfCategoryCell
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.12 on 2018-08-10 08:05
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import combo.apps.wcs.models
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('data', '0035_page_related_cells'),
|
||||
('wcs', '0015_wcscurrentformscell_categories'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BackofficeSubmissionCell',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('placeholder', models.CharField(max_length=20)),
|
||||
('order', models.PositiveIntegerField()),
|
||||
('slug', models.SlugField(blank=True, verbose_name='Slug')),
|
||||
('extra_css_class', models.CharField(blank=True, max_length=100, verbose_name='Extra classes for CSS styling')),
|
||||
('public', models.BooleanField(default=True, verbose_name='Public')),
|
||||
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
|
||||
('last_update_timestamp', models.DateTimeField(auto_now=True)),
|
||||
('wcs_site', models.CharField(blank=True, max_length=50, verbose_name='Site')),
|
||||
('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='Groups')),
|
||||
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Page')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Backoffice Submission',
|
||||
},
|
||||
bases=(models.Model, combo.apps.wcs.models.WcsBlurpMixin),
|
||||
),
|
||||
]
|
|
@ -503,3 +503,28 @@ class TrackingCodeInputCell(CellBase):
|
|||
self.wcs_site = list(get_wcs_services().keys())[0]
|
||||
extra_context['url'] = get_wcs_services().get(self.wcs_site).get('url')
|
||||
return extra_context
|
||||
|
||||
|
||||
@register_cell_class
|
||||
class BackofficeSubmissionCell(WcsDataBaseCell):
|
||||
api_url = '/api/formdefs/?backoffice-submission=on'
|
||||
variable_name = 'all_formdefs'
|
||||
template_name = 'combo/wcs/backoffice_submission.html'
|
||||
cache_duration = 600
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Backoffice Submission')
|
||||
|
||||
def get_concerned_user(self, context):
|
||||
# always return connected user; the selected user will be used in the
|
||||
# query string when creating a new formdata.
|
||||
return getattr(context.get('request'), 'user', None)
|
||||
|
||||
def get_cell_extra_context(self, context):
|
||||
context = super(BackofficeSubmissionCell, self).get_cell_extra_context(context)
|
||||
# add a fake category where it's missing
|
||||
for site_formdefs in context['all_formdefs'].values():
|
||||
for formdef in site_formdefs['data']:
|
||||
if not 'category' in formdef:
|
||||
formdef['category'] = _('Misc')
|
||||
return context
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{% load i18n %}
|
||||
{% block cell-content %}
|
||||
<h2>{% trans "New Form" %}</h2>
|
||||
{% for site_formdefs in all_formdefs.values %}
|
||||
<div class="links-list">
|
||||
{% if all_formdefs.keys|length > 1 %}<h3>{{ site_formdefs.title }}</h3>{% endif %}
|
||||
<ul>
|
||||
{% regroup site_formdefs.data|dictsort:"category" by category as categories_formdefs %}
|
||||
{% for category_formdefs in categories_formdefs %}
|
||||
<li><h4>{{ category_formdefs.grouper }}</h4></li>
|
||||
{% for formdef in category_formdefs.list|dictsort:"title" %}
|
||||
<li><a href="{{formdef.backoffice_submission_url}}?NameID={{name_id}}">{{formdef.title}}</a></li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
|
@ -16,8 +16,9 @@
|
|||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .views import TrackingCodeView
|
||||
from .views import TrackingCodeView, tracking_code_search
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^tracking-code/$', TrackingCodeView.as_view(), name='wcs-tracking-code'),
|
||||
url(r'^api/search/tracking-code/$', tracking_code_search, name='wcs-tracking-code-search'),
|
||||
]
|
||||
|
|
|
@ -14,9 +14,11 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect, HttpResponseBadRequest
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import JsonResponse, HttpResponseRedirect, HttpResponseBadRequest
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
@ -37,14 +39,10 @@ class TrackingCodeView(View):
|
|||
# sites in a skeleton.
|
||||
return super(TrackingCodeView, self).dispatch(*args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
try:
|
||||
cell = TrackingCodeInputCell.objects.get(id=request.POST['cell'])
|
||||
except (ValueError, TrackingCodeInputCell.DoesNotExist):
|
||||
return HttpResponseBadRequest('Invalid cell id')
|
||||
code = request.POST['code']
|
||||
if cell.wcs_site:
|
||||
wcs_sites = [get_wcs_services().get(cell.wcs_site)]
|
||||
@classmethod
|
||||
def search(self, code, wcs_site=None):
|
||||
if wcs_site:
|
||||
wcs_sites = [get_wcs_services().get(wcs_site)]
|
||||
else:
|
||||
wcs_sites = get_wcs_services().values()
|
||||
|
||||
|
@ -52,12 +50,24 @@ class TrackingCodeView(View):
|
|||
response = requests.get('/api/code/' + code,
|
||||
remote_service=wcs_site, log_errors=False)
|
||||
if response.status_code == 200 and response.json().get('err') == 0:
|
||||
url = response.json().get('load_url')
|
||||
return HttpResponseRedirect(url)
|
||||
return response.json().get('load_url')
|
||||
|
||||
return None
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
try:
|
||||
cell = TrackingCodeInputCell.objects.get(id=request.POST['cell'])
|
||||
except (ValueError, TrackingCodeInputCell.DoesNotExist):
|
||||
return HttpResponseBadRequest('Invalid cell id')
|
||||
code = request.POST['code']
|
||||
|
||||
url = self.search(code, wcs_site=cell.wcs_site)
|
||||
if url:
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
next_url = request.POST.get('url') or '/'
|
||||
next_netloc = urlparse.urlparse(next_url).netloc
|
||||
if not (next_netloc and next_netloc != urlparse.urlparse(request.build_absolute_uri()).netloc):
|
||||
if not (next_netloc and next_netloc != urlparse.urlparse(self.request.build_absolute_uri()).netloc):
|
||||
messages.error(self.request,
|
||||
_(u'The tracking code could not been found.'))
|
||||
else:
|
||||
|
@ -68,3 +78,16 @@ class TrackingCodeView(View):
|
|||
next_url += 'unknown-tracking-code'
|
||||
|
||||
return HttpResponseRedirect(next_url)
|
||||
|
||||
|
||||
def tracking_code_search(request):
|
||||
hits = []
|
||||
query = request.GET.get('q') or ''
|
||||
if re.match(r'^[BCDFGHJKLMNPQRSTVWXZ]{8}$', query):
|
||||
url = TrackingCodeView.search(query)
|
||||
if url:
|
||||
hits.append({
|
||||
'text': _('Use tracking code %s') % query,
|
||||
'url': url,
|
||||
})
|
||||
return JsonResponse({'data': hits})
|
||||
|
|
|
@ -15,7 +15,18 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class DataConfig(AppConfig):
|
||||
name = 'combo.data'
|
||||
verbose_name = 'data'
|
||||
|
||||
def ready(self):
|
||||
# register built-in search engine for page contents
|
||||
from combo.apps.search import engines
|
||||
engines.register('_text',
|
||||
url=reverse('api-search') + '?q=%(q)s',
|
||||
label=_('Page Contents')
|
||||
)
|
||||
|
|
|
@ -1043,6 +1043,7 @@ class JsonCellBase(CellBase):
|
|||
# },
|
||||
# ...
|
||||
# ]
|
||||
first_data_key = 'json'
|
||||
|
||||
_json_content = None
|
||||
|
||||
|
@ -1063,7 +1064,9 @@ class JsonCellBase(CellBase):
|
|||
context[varname] = context['request'].GET[varname]
|
||||
self._json_content = None
|
||||
|
||||
data_urls = [{'key': 'json', 'url': self.url, 'cache_duration': self.cache_duration,
|
||||
context['concerned_user'] = self.get_concerned_user(context)
|
||||
|
||||
data_urls = [{'key': self.first_data_key, 'url': self.url, 'cache_duration': self.cache_duration,
|
||||
'log_errors': self.log_errors, 'timeout': self.timeout}]
|
||||
data_urls.extend(self.additional_data or [])
|
||||
|
||||
|
@ -1128,7 +1131,7 @@ class JsonCellBase(CellBase):
|
|||
|
||||
# keep cache of first response as it may be used to find the
|
||||
# appropriate template.
|
||||
self._json_content = extra_context['json']
|
||||
self._json_content = extra_context[self.first_data_key]
|
||||
|
||||
return extra_context
|
||||
|
||||
|
|
|
@ -27,24 +27,54 @@ function multisort(element)
|
|||
|
||||
var $ul = $('<ul class="multisort"></ul>');
|
||||
|
||||
var checkboxes = $(element).data('checkboxes');
|
||||
|
||||
$(element).find('option').each(function(i, x) {
|
||||
if (category_value && $(x).val().indexOf(category_value + ':') != 0) {
|
||||
return;
|
||||
}
|
||||
$('<li data-value="' + $(x).val() + '"><span class="handle">⣿</span>' + $(x).text() + '</li>').appendTo($ul);
|
||||
var checkbox = '';
|
||||
if (checkboxes) {
|
||||
if ($(x).attr('selected')) {
|
||||
checkbox = '<input type="checkbox" checked/>'
|
||||
} else {
|
||||
checkbox = '<input type="checkbox"/>'
|
||||
}
|
||||
}
|
||||
$('<li data-value="' + $(x).val() + '"><span class="handle">⣿</span>'+ checkbox + $(x).text() + '</li>').appendTo($ul);
|
||||
});
|
||||
$ul.appendTo(element);
|
||||
|
||||
function multisort_sync() {
|
||||
var $select = $(element).find('select');
|
||||
var options = Array();
|
||||
$ul.find('li').each(function(i, x) {
|
||||
var selected = true;
|
||||
if (checkboxes && $(x).find('input[type=checkbox]:checked').length == 0) {
|
||||
selected = false;
|
||||
}
|
||||
var value = $(x).data('value');
|
||||
var $option = $select.find('[value="' + value + '"]');
|
||||
if (selected) {
|
||||
$option.prop('selected', 'selected');
|
||||
} else {
|
||||
$option.prop('selected', null);
|
||||
}
|
||||
$option.detach();
|
||||
options.push($option);
|
||||
});
|
||||
while (options.length) {
|
||||
$select.prepend(options.pop());
|
||||
}
|
||||
}
|
||||
|
||||
$ul.find('input[type=checkbox]').on('change', function() {
|
||||
multisort_sync();
|
||||
});
|
||||
$ul.sortable({
|
||||
handle: '.handle',
|
||||
update: function(event, ui) {
|
||||
var options = Array();
|
||||
var select = $(element).find('select');
|
||||
$ul.find('li').each(function(i, x) {
|
||||
options.push($(element).find("option[value='" + $(x).data('value') + "']").attr('selected', 'selected').detach());
|
||||
});
|
||||
while (options.length) {
|
||||
select.prepend(options.pop());
|
||||
}
|
||||
multisort_sync();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2014-2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import django.apps
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class AppConfig(django.apps.AppConfig):
|
||||
name = 'combo.profile'
|
||||
verbose_name = _('Profile')
|
||||
|
||||
default_app_config = 'combo.profile.AppConfig'
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.12 on 2018-08-10 13:52
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('data', '0035_page_related_cells'),
|
||||
('profile', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProfileCell',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('placeholder', models.CharField(max_length=20)),
|
||||
('order', models.PositiveIntegerField()),
|
||||
('slug', models.SlugField(blank=True, verbose_name='Slug')),
|
||||
('extra_css_class', models.CharField(blank=True, max_length=100, verbose_name='Extra classes for CSS styling')),
|
||||
('public', models.BooleanField(default=True, verbose_name='Public')),
|
||||
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
|
||||
('last_update_timestamp', models.DateTimeField(auto_now=True)),
|
||||
('groups', models.ManyToManyField(blank=True, to='auth.Group', verbose_name='Groups')),
|
||||
('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data.Page')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Profile',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -14,10 +14,47 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from collections import OrderedDict
|
||||
import copy
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.dateparse import parse_date
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from combo.data.models import JsonCellBase
|
||||
from combo.data.library import register_cell_class
|
||||
|
||||
|
||||
class Profile(models.Model):
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL)
|
||||
initial_login_view_timestamp = models.DateTimeField(null=True)
|
||||
|
||||
|
||||
@register_cell_class
|
||||
class ProfileCell(JsonCellBase):
|
||||
template_name = 'combo/profile.html'
|
||||
first_data_key = 'profile'
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Profile')
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
idp = settings.KNOWN_SERVICES.get('authentic').values()[0]
|
||||
return '{%% load combo %%}%sapi/users/{{ concerned_user|name_id }}/' % idp.get('url')
|
||||
|
||||
def get_cell_extra_context(self, context):
|
||||
extra_context = super(ProfileCell, self).get_cell_extra_context(context)
|
||||
extra_context['profile_fields'] = OrderedDict()
|
||||
if extra_context.get('profile') is not None:
|
||||
for attribute in settings.USER_PROFILE_CONFIG.get('fields'):
|
||||
extra_context['profile_fields'][attribute['name']] = copy.copy(attribute)
|
||||
value = extra_context['profile'].get(attribute['name'])
|
||||
if value:
|
||||
if attribute['kind'] in ('birthdate', 'date'):
|
||||
value = parse_date(value)
|
||||
extra_context['profile_fields'][attribute['name']]['value'] = value
|
||||
else:
|
||||
extra_context['error'] = 'unknown user'
|
||||
return extra_context
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
{% load i18n %}
|
||||
{% block cell-content %}
|
||||
<div class="profile">
|
||||
<h2>{% trans "Profile" %}</h2>
|
||||
{% for key, details in profile_fields.items %}
|
||||
{% if details.value and details.user_visible %}
|
||||
<p><span class="label">{{ details.label }}</span> <span class="value">{{ details.value }}</span></p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if error == 'unknown user' %}
|
||||
<p>{% trans 'Unknown User' %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -19,8 +19,10 @@ from __future__ import absolute_import
|
|||
import datetime
|
||||
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.core import signing
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.template import VariableDoesNotExist
|
||||
from django.template.base import TOKEN_BLOCK, TOKEN_VAR
|
||||
from django.template.defaultfilters import stringfilter
|
||||
from django.utils import dateparse
|
||||
|
@ -30,6 +32,9 @@ from combo.public.menu import get_menu_context
|
|||
from combo.utils import NothingInCacheException, flatten_context
|
||||
from combo.apps.dashboard.models import DashboardCell, Tile
|
||||
|
||||
if 'mellon' in settings.INSTALLED_APPS:
|
||||
from mellon.models import UserSAMLIdentifier
|
||||
|
||||
register = template.Library()
|
||||
|
||||
def skeleton_text(context, placeholder_name, content=''):
|
||||
|
@ -229,3 +234,12 @@ def as_list(obj):
|
|||
@register.filter
|
||||
def signed(obj):
|
||||
return signing.dumps(obj)
|
||||
|
||||
@register.filter
|
||||
def name_id(user):
|
||||
saml_id = UserSAMLIdentifier.objects.filter(user=user).last()
|
||||
if saml_id:
|
||||
return saml_id.name_id
|
||||
# it is important to raise this so get_templated_url is aborted and no call
|
||||
# is tried with a missing user argument.
|
||||
raise VariableDoesNotExist('name_id')
|
||||
|
|
|
@ -491,7 +491,7 @@ def menu_badges(request):
|
|||
|
||||
|
||||
def api_search(request):
|
||||
for cell in SearchCell.objects.filter(_search_service='_text'):
|
||||
for cell in SearchCell.get_cells_by_search_service('_text'):
|
||||
if not cell.is_visible(request.user):
|
||||
continue
|
||||
break
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2014-2018 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import django
|
||||
from django import forms
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
class MultiSortWidget(forms.SelectMultiple):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'with_checkboxes' in kwargs:
|
||||
self.with_checkboxes = kwargs.pop('with_checkboxes')
|
||||
else:
|
||||
self.with_checkboxes = False
|
||||
super(MultiSortWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
def render(self, name, value, attrs=None, choices=()):
|
||||
# reorder choices to get them in the current value order
|
||||
self_choices = self.choices[:]
|
||||
choices_dict = dict(self_choices)
|
||||
if value:
|
||||
for option in reversed(value.get('data')):
|
||||
if option not in choices_dict:
|
||||
continue
|
||||
option_tuple = (option, choices_dict[option])
|
||||
self.choices.remove(option_tuple)
|
||||
self.choices.insert(0, option_tuple)
|
||||
|
||||
# render the <select multiple>
|
||||
if django.VERSION < (1, 11, 0):
|
||||
rendered = super(MultiSortWidget, self).render(name, value,
|
||||
attrs=attrs, choices=choices)
|
||||
else:
|
||||
rendered = super(MultiSortWidget, self).render(name, value,
|
||||
attrs=attrs)
|
||||
|
||||
# include it in a <div> that will be turned into an appropriate widget
|
||||
# in javascript
|
||||
id_ = 'wid-%s' % name
|
||||
if self.with_checkboxes:
|
||||
attrs = 'data-checkboxes="true"'
|
||||
else:
|
||||
attrs = ''
|
||||
return mark_safe('''<div class="multisort" %s id="%s">%s</div>
|
||||
<script type="text/javascript">multisort($("#%s"));</script>
|
||||
''' % (attrs, id_, rendered, id_))
|
||||
|
||||
def render_options(self, choices, value):
|
||||
value = value.get('data') or []
|
||||
return super(MultiSortWidget, self).render_options(choices, value)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
if isinstance(data, MultiValueDict):
|
||||
return {'data': data.getlist(name)}
|
||||
return data.get(name, None)
|
|
@ -184,6 +184,13 @@ div.cell {
|
|||
}
|
||||
}
|
||||
|
||||
div.profile {
|
||||
span.value {
|
||||
display: block;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1586px) {
|
||||
div#page-content div.cubesbarchart {
|
||||
width: 49.5%;
|
||||
|
|
|
@ -62,3 +62,20 @@ FAMILY_SERVICE = {'root': '/'}
|
|||
|
||||
BOOKING_CALENDAR_CELL_ENABLED = True
|
||||
NEWSLETTERS_CELL_ENABLED = True
|
||||
|
||||
USER_PROFILE_CONFIG = {
|
||||
'fields': [
|
||||
{
|
||||
'name': 'first_name',
|
||||
'kind': 'string',
|
||||
'label': 'First Name',
|
||||
'user_visible': True,
|
||||
},
|
||||
{
|
||||
'name': 'birthdate',
|
||||
'kind': 'birthdate',
|
||||
'label': 'Birth Date',
|
||||
'user_visible': True,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import datetime
|
||||
import json
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from django.test import override_settings
|
||||
|
||||
from combo.data.models import Page
|
||||
from combo.profile.models import ProfileCell
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@override_settings(
|
||||
KNOWN_SERVICES={'authentic': {'idp': {'title': 'IdP', 'url': 'http://example.org/'}}})
|
||||
@mock.patch('combo.public.templatetags.combo.UserSAMLIdentifier')
|
||||
@mock.patch('combo.utils.requests.get')
|
||||
def test_profile_cell(requests_get, user_saml, app, admin_user):
|
||||
page = Page()
|
||||
page.save()
|
||||
|
||||
cell = ProfileCell(page=page, order=0)
|
||||
cell.save()
|
||||
|
||||
data = {'first_name': 'Foo', 'birthdate': '2018-08-10'}
|
||||
requests_get.return_value = mock.Mock(
|
||||
content=json.dumps(data),
|
||||
json=lambda: data,
|
||||
status_code=200)
|
||||
|
||||
def filter_mock(user=None):
|
||||
assert user is admin_user
|
||||
return mock.Mock(last=lambda: mock.Mock(name_id='123456'))
|
||||
|
||||
mocked_objects = mock.Mock()
|
||||
mocked_objects.filter = mock.Mock(side_effect=filter_mock)
|
||||
user_saml.objects = mocked_objects
|
||||
|
||||
context = cell.get_cell_extra_context({'synchronous': True, 'selected_user': admin_user})
|
||||
assert context['profile_fields']['first_name']['value'] == 'Foo'
|
||||
assert context['profile_fields']['birthdate']['value'] == datetime.date(2018, 8, 10)
|
||||
assert requests_get.call_args[0][0] == 'http://example.org/api/users/123456/'
|
||||
|
||||
def filter_mock_missing(user=None):
|
||||
return mock.Mock(last=lambda: None)
|
||||
|
||||
mocked_objects.filter = mock.Mock(side_effect=filter_mock_missing)
|
||||
|
||||
context = cell.get_cell_extra_context({'synchronous': True, 'selected_user': admin_user})
|
||||
assert context['error'] == 'unknown user'
|
||||
assert requests_get.call_count == 1 # no new call was made
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
import os
|
||||
import pytest
|
||||
import re
|
||||
import mock
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -15,6 +16,9 @@ from combo.apps.search.models import SearchCell
|
|||
from combo.data.models import Page, JsonCell, TextCell, MenuCell, LinkCell
|
||||
from combo.data.search_indexes import PageIndex
|
||||
|
||||
from .test_manager import admin_user, login
|
||||
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
|
@ -40,7 +44,7 @@ class SearchServices(object):
|
|||
settings.COMBO_SEARCH_SERVICES = self.search_services
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
delattr(settings, 'COMBO_SEARCH_SERVICES')
|
||||
settings.COMBO_SEARCH_SERVICES = {}
|
||||
|
||||
def test_search_cell(app):
|
||||
with SearchServices(SEARCH_SERVICES):
|
||||
|
@ -48,7 +52,7 @@ def test_search_cell(app):
|
|||
page.save()
|
||||
|
||||
cell = SearchCell(page=page, placeholder='content', order=0)
|
||||
cell._search_service = 'search1'
|
||||
cell._search_services = {'data': ['search1']}
|
||||
cell.save()
|
||||
|
||||
resp = cell.render({})
|
||||
|
@ -66,35 +70,35 @@ def test_search_cell(app):
|
|||
mock_json = mock.Mock()
|
||||
mock_json.json.return_value = response
|
||||
requests_get.return_value = mock_json
|
||||
resp = app.get('/ajax/search/%s/?q=foo' % cell.pk, status=200)
|
||||
resp = app.get('/ajax/search/%s/search1/?q=foo' % cell.pk, status=200)
|
||||
assert requests_get.call_args[0][0] == 'http://www.example.net/search/?q=foo'
|
||||
assert '<li>' not in resp.text
|
||||
|
||||
resp = app.get('/ajax/search/%s/?q=foo%%23bar' % cell.pk, status=200)
|
||||
resp = app.get('/ajax/search/%s/search1/?q=foo%%23bar' % cell.pk, status=200)
|
||||
assert requests_get.call_args[0][0] == 'http://www.example.net/search/?q=foo%23bar'
|
||||
assert '<li>' not in resp.text
|
||||
|
||||
response['data'] = [{'url': 'http://test', 'text': 'barbarbar'}]
|
||||
resp = app.get('/ajax/search/%s/?q=foo' % cell.pk, status=200)
|
||||
resp = app.get('/ajax/search/%s/search1/?q=foo' % cell.pk, status=200)
|
||||
assert resp.text.count('<li>') == 1
|
||||
assert '<li><a href="http://test">barbarbar</a>' in resp.text
|
||||
|
||||
response['data'] = [{'url': 'http://test', 'text': 'barbarbar',
|
||||
'description': 'this is <b>html</b>'}]
|
||||
resp = app.get('/ajax/search/%s/?q=foo' % cell.pk, status=200)
|
||||
resp = app.get('/ajax/search/%s/search1/?q=foo' % cell.pk, status=200)
|
||||
assert resp.text.count('<li>') == 1
|
||||
assert '<li><a href="http://test">barbarbar</a>' in resp.text
|
||||
assert 'this is <b>html</b>' in resp.text
|
||||
|
||||
with override_settings(TEMPLATE_VARS=TEMPLATE_VARS):
|
||||
cell._search_service = 'search_tmpl'
|
||||
cell._search_services = {'data': ['search_tmpl']}
|
||||
cell.save()
|
||||
with mock.patch('combo.apps.search.models.requests.get') as requests_get:
|
||||
response = {'err': 0, 'data': []}
|
||||
mock_json = mock.Mock()
|
||||
mock_json.json.return_value = response
|
||||
requests_get.return_value = mock_json
|
||||
resp = app.get('/ajax/search/%s/?q=foo' % cell.pk, status=200)
|
||||
resp = app.get('/ajax/search/%s/search_tmpl/?q=foo' % cell.pk, status=200)
|
||||
assert requests_get.call_args[0][0] == 'http://search.example.net/?q=foo'
|
||||
|
||||
# TEMPLATE_VARS are accessible in template
|
||||
|
@ -103,7 +107,7 @@ def test_search_cell(app):
|
|||
templates_settings = [settings.TEMPLATES[0].copy()]
|
||||
templates_settings[0]['DIRS'] = ['%s/templates-1' % os.path.abspath(os.path.dirname(__file__))]
|
||||
with override_settings(TEMPLATES=templates_settings):
|
||||
resp = app.get('/ajax/search/%s/?q=bar' % cell.pk, status=200)
|
||||
resp = app.get('/ajax/search/%s/search_tmpl/?q=bar' % cell.pk, status=200)
|
||||
assert requests_get.call_args[0][0] == 'http://search.example.net/?q=bar'
|
||||
assert 'searchfoo results.data=[]' in resp.text
|
||||
assert 'search_url=http://search.example.net/' in resp.text
|
||||
|
@ -113,7 +117,7 @@ def test_search_global_context(app):
|
|||
page = Page(title='Search', slug='search_page', template_name='standard')
|
||||
page.save()
|
||||
cell = SearchCell(page=page, placeholder='content', order=0)
|
||||
cell._search_service = 'search1'
|
||||
cell._search_services = {'data': ['search1']}
|
||||
cell.save()
|
||||
assert cell.varname == ''
|
||||
|
||||
|
@ -143,7 +147,7 @@ def test_search_cell_visibility(app):
|
|||
cell = SearchCell(page=page, order=0)
|
||||
assert not cell.is_visible()
|
||||
|
||||
cell._search_service = '_text'
|
||||
cell._search_services = {'data': ['_text']}
|
||||
assert cell.is_visible()
|
||||
|
||||
def test_search_contents():
|
||||
|
@ -218,7 +222,7 @@ def test_search_api(app):
|
|||
|
||||
resp = app.get('/api/search/?q=foobar', status=404)
|
||||
|
||||
cell = SearchCell(page=page, _search_service='_text', order=0)
|
||||
cell = SearchCell(page=page, _search_services={'data': ['_text']}, order=0)
|
||||
cell.save()
|
||||
|
||||
resp = app.get('/api/search/?q=foobar', status=200)
|
||||
|
@ -242,7 +246,7 @@ def test_update_index_command(app):
|
|||
page = Page(title='example page', slug='example-page')
|
||||
page.save()
|
||||
|
||||
cell = SearchCell(page=page, _search_service='_text', order=0)
|
||||
cell = SearchCell(page=page, _search_services={'data': ['_text']}, order=0)
|
||||
cell.save()
|
||||
|
||||
call_command('update_index')
|
||||
|
@ -276,3 +280,42 @@ def test_update_index_command(app):
|
|||
resp = app.get('/api/search/?q=bar', status=200)
|
||||
assert len(resp.json['data']) == 1
|
||||
assert resp.json['data'][0]['url'] == 'http://example.net'
|
||||
|
||||
def test_manager_search_cell(app, admin_user):
|
||||
Page.objects.all().delete()
|
||||
page = Page(title='One', slug='one', template_name='standard')
|
||||
page.save()
|
||||
app = login(app)
|
||||
resp = app.get('/manage/pages/%s/' % page.id)
|
||||
resp = app.get(resp.html.find('option',
|
||||
**{'data-add-url': re.compile('search_searchcell')})['data-add-url'])
|
||||
|
||||
cells = Page.objects.get(id=page.id).get_cells()
|
||||
assert len(cells) == 1
|
||||
assert isinstance(cells[0], SearchCell)
|
||||
|
||||
resp = app.get('/manage/pages/%s/' % page.id)
|
||||
assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.text
|
||||
assert len(resp.form['c%s-_search_services' % cells[0].get_reference()].options) == 2
|
||||
|
||||
with SearchServices(SEARCH_SERVICES):
|
||||
resp = app.get('/manage/pages/%s/' % page.id)
|
||||
assert len(resp.form['c%s-_search_services' % cells[0].get_reference()].options) == 4
|
||||
# simulate reordering of options
|
||||
resp.form['c%s-_search_services' % cells[0].get_reference()].options = [
|
||||
(u'search_tmpl', False, u'Search with template'),
|
||||
(u'_text', False, u'Page Contents'),
|
||||
(u'search1', False, u'Search 1')]
|
||||
resp.form['c%s-_search_services' % cells[0].get_reference()].value = ['search_tmpl', '_text']
|
||||
resp = resp.form.submit()
|
||||
assert resp.status_int == 302
|
||||
|
||||
# check selected engines are selected and the first items of the list
|
||||
resp = app.get('/manage/pages/%s/' % page.id)
|
||||
assert set(resp.form['c%s-_search_services' % cells[0].get_reference()].value) == set(['search_tmpl', '_text'])
|
||||
assert resp.form['c%s-_search_services' % cells[0].get_reference()].options[0][0] == 'search_tmpl'
|
||||
assert resp.form['c%s-_search_services' % cells[0].get_reference()].options[1][0] == '_text'
|
||||
|
||||
# check there's no crash if search engines are removed from config
|
||||
resp = app.get('/manage/pages/%s/' % page.id)
|
||||
assert resp.form['c%s-_search_services' % cells[0].get_reference()].value == ['_text']
|
||||
|
|
|
@ -20,7 +20,7 @@ from django.utils.six.moves.urllib import parse as urlparse
|
|||
from combo.data.models import Page
|
||||
from combo.apps.wcs.models import (WcsFormCell, WcsCurrentFormsCell,
|
||||
WcsFormsOfCategoryCell, WcsCurrentDraftsCell, WcsCategoryCell,
|
||||
TrackingCodeInputCell)
|
||||
TrackingCodeInputCell, BackofficeSubmissionCell)
|
||||
|
||||
from combo.utils import NothingInCacheException
|
||||
|
||||
|
@ -143,9 +143,15 @@ formdef = FormDef()
|
|||
formdef.name = 'a private form'
|
||||
formdef.category_id = cats[2].id
|
||||
formdef.roles = [role.id]
|
||||
formdef.backoffice_submission_roles = [role.id]
|
||||
formdef.fields = []
|
||||
formdef.store()
|
||||
|
||||
user2 = get_publisher().user_class() # agent
|
||||
user2.name = 'foo2 bar2'
|
||||
user2.email = 'foo2@example.net'
|
||||
user2.roles = [role.id]
|
||||
user2.store()
|
||||
""",
|
||||
|
||||
}
|
||||
|
@ -643,3 +649,43 @@ def test_cell_assets(app, admin_user):
|
|||
resp = app.get('/manage/assets/')
|
||||
assert u'>Logo — Test 9<' in resp.text
|
||||
assert u'>Picture — form title<' in resp.text
|
||||
|
||||
@wcsctl_present
|
||||
def test_tracking_code_search(app):
|
||||
assert len(app.get('/api/search/tracking-code/').json.get('data')) == 0
|
||||
assert len(app.get('/api/search/tracking-code/?q=123').json.get('data')) == 0
|
||||
assert len(app.get('/api/search/tracking-code/?q=BBCCDFF').json.get('data')) == 0
|
||||
assert len(app.get('/api/search/tracking-code/?q=BBCCDDFF').json.get('data')) == 0
|
||||
assert len(app.get('/api/search/tracking-code/?q=CNPHNTFB').json.get('data')) == 1
|
||||
assert len(app.get('/api/search/tracking-code/?q=BBCCDDFFG').json.get('data')) == 0
|
||||
|
||||
@wcsctl_present
|
||||
def test_backoffice_submission_cell_render(context):
|
||||
page = Page(title='xxx', slug='test_backoffice_submission_cell_render', template_name='standard')
|
||||
page.save()
|
||||
cell = BackofficeSubmissionCell(page=page, placeholder='content', order=0)
|
||||
cell.wcs_site = 'default'
|
||||
cell.save()
|
||||
|
||||
context['synchronous'] = True # to get fresh content
|
||||
|
||||
result = cell.render(context)
|
||||
assert '/backoffice/submission/a-private-form/' not in result
|
||||
|
||||
class MockUser(object):
|
||||
email = 'foo@example.net'
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
context['request'].user = MockUser()
|
||||
|
||||
result = cell.render(context)
|
||||
assert '/backoffice/submission/a-private-form/' not in result
|
||||
|
||||
class MockUser(object):
|
||||
email = 'foo2@example.net'
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
context['request'].user = MockUser()
|
||||
|
||||
result = cell.render(context)
|
||||
assert '/backoffice/submission/a-private-form/' in result
|
||||
|
|
Loading…
Reference in New Issue