This repository has been archived by the owner on Sep 5, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Archive search #159
Open
gh-PonyM
wants to merge
92
commits into
master
Choose a base branch
from
archive_search
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Archive search #159
Changes from all commits
Commits
Show all changes
92 commits
Select commit
Hold shift + click to select a range
ee1c38e
Adds form to search ArchivedResultsCollection (archive)
8af0232
Adds info tooltip to fields macro and adds column sizing
c87ec59
Applies styles to archive_breadcrumbs class on landing page
c944f4f
Fixes conflict with builtin name (date, type)
7e92068
Adds path and Collection for seareable archive
4f6f195
Adds a archive search view with form directive
7341f40
Adds template for Archive Search Page
2588eb0
Fixes ul li display with class archive-breadcrumbs
460deb9
Fixex conflict with builtins
f795bf7
Fixes wrong coerce
a6e51bf
Adds default to_date of today to ArchiveSearchForm
fbc296c
Prevents populating choices if already done before
b72ebe3
Asserts that a default to_date will be passed from the form
35e693e
Removes redundant parenthesis
af81af7
Stores labeled types of results as accessible class attribute
0d7d87b
Fixes model not being applied to ArchiveSearchForm
7eebdea
Adds to_date as default to_date that is used in the form as default
92cbe0d
Adds the choices from the model instance
23dda20
Adds the choices in the model instance
4c0e8de
Add test that domain of influences are coherent with ballot app
c7d75cc
Fixes date handling of form
fe2cf9b
Corrects info tooltip length
25a8ea5
Linting
fc46caa
Adds correct model filtering to the archive search for all params
6154969
Fixes order of entries to fail test
289aec8
Adds new Collection to __all__
30c3f37
Adds a method to reset params to default used for the query function
8244ac4
Adds answer options for vote and complex vote to be translated
8b4b2b0
Fixes choices of answers/results and the query to database
ac47fca
Linting
b247ff7
Adding first tests for archive search and collection
a212adc
Display results of archive search and count of results
fe57c21
Adds user info on how compound elections are displayed
6ad05bc
Adds locale and text search using locale to query function
d129789
Allows clear separation between two onegov modules
70862f4
Adds tests for query with voting result
befe153
Adds test for query using all query params
93350ec
Adds further tests
f4e7a4c
Fixes import of ArchiveSearchForm
3cf4309
Fixes implicit using of english for rumantsch to be explicit
5db43bb
Tests if title is queried in the correct language
ec618ab
Linting
c0fca64
Renames result to answer according to the vote model
58256f0
Adds simple test for url archive-search
df0c20f
Adds tests form archive search form
b95307a
Renames result to answer according to Vote model in ballot
40ee30f
Fixes variable name
b83a0a1
Adds link in archive macro for archive archive_search
54c15a1
Linting
7db2e26
Fixes bugs due to renaming of result to answer and type_ to types
30a6247
Fixes import error of missing column 'gewahlt' of majorz test data
7f86b76
Adds tabbed view to archive-search
b5fa241
Adds first test for grouping of archive query
61e4aef
Fixes archive link for archive view
53d807f
Adds item type to template
a36a052
Fixes renaming of type to types in Collection
a12585c
Overwrites group_items to archive search
1bb0483
Improves error message for frontend
70fedd7
Removes wrong code, original code was fine, but test data was broken
b7d9b02
Adds test for function to derive list_id from knr
f309546
Corrects and extends tests for searchable archive collection
50ca7bd
Improves test for searchable archive view
96b1826
Adds table for votes
e8b20e6
Adds method to retrieve for collection based on item_type query param
438aebb
Fixes election compound entry in archive page
f145901
Renders only vote and elections in tab menu
89180c0
Hides fields if render_kw['hidden'] is set to True
ef20f90
Moves domain Checkboxfield to the middle
f6c94b5
hides answer field if elections are chosen
5e9c26d
Treats compound_elections as election and vice versa
1841d52
Adds different sorting done in db for different principal domain
a131a09
Exludes votes that are belonging to a compound of elections
ab7b18e
Removes info tip since not needed fields will disappear based on domain
91ff799
Adds table to display archived elections
f298c26
Tests the output of the group_items function used in view/template
74ba5b5
Adds missing whitespace
2508965
Gets rid of shortcode, adds no wrap for th in tables
cd68526
Adds css and aligns table data vertically on top
8da5a7e
Linting
40f9061
Fixes not rendering fields if hidden kwarg is not set at all
6ab64e2
Improves error message returned if list_id not found in added_lists
3dd1b64
Comments failing test
def31cb
Remove unused statement
3dc4002
Adds pagination to archive search with tests
6bb8946
Linting
b7b7720
Update german translations
348e469
Adds translation field and translation in german
d59ff70
merges new release
9ce3c9c
Regenerates translations
61297a7
Linting
6cda589
Fixes un-applied translation strings
4a554ee
Adds german and french translations
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,8 +14,11 @@ | |
from sqlalchemy import extract | ||
from sqlalchemy import func | ||
from sqlalchemy import Integer | ||
from sqlalchemy import or_ | ||
from sqlalchemy.sql.expression import case | ||
from time import mktime | ||
from time import strptime | ||
from onegov.core.collection import Pagination | ||
|
||
|
||
def groupbydict(items, keyfunc, sortfunc=None): | ||
|
@@ -30,12 +33,12 @@ def groupbydict(items, keyfunc, sortfunc=None): | |
|
||
class ArchivedResultCollection(object): | ||
|
||
def __init__(self, session, date=None): | ||
def __init__(self, session, date_=None): | ||
self.session = session | ||
self.date = date | ||
self.date = date_ | ||
|
||
def for_date(self, date): | ||
return self.__class__(self.session, date) | ||
def for_date(self, date_): | ||
return self.__class__(self.session, date_) | ||
|
||
def query(self): | ||
return self.session.query(ArchivedResult) | ||
|
@@ -259,3 +262,227 @@ def delete(self, item, request): | |
|
||
self.session.delete(item) | ||
self.session.flush() | ||
|
||
|
||
class SearchableArchivedResultCollection( | ||
ArchivedResultCollection, Pagination): | ||
|
||
def __init__( | ||
self, | ||
session, | ||
date_=None, | ||
from_date=None, | ||
to_date=None, | ||
types=None, | ||
item_type=None, | ||
domain=None, | ||
term=None, | ||
answer=None, | ||
locale='de_CH', | ||
page=0 | ||
): | ||
""" | ||
|
||
:param session: | ||
gh-PonyM marked this conversation as resolved.
Show resolved
Hide resolved
|
||
:param date_: see parent class | ||
:param from_date: datetime.date | ||
:param to_date: datetime.date | ||
:param types: types as list, to search all the types | ||
neglected if item_type is set | ||
:param item_type: type from url path (used in tab_menu) | ||
:param domain: list of domains | ||
:param term: query string from form | ||
:param answer: list of answers | ||
:param locale: locale from request.locale | ||
""" | ||
super().__init__(session, date_=date_) | ||
self.from_date = from_date | ||
self.to_date = to_date or date.today() | ||
self.types = types | ||
self.item_type = item_type | ||
self.domain = domain | ||
self.term = term | ||
self.answer = answer | ||
self.locale = locale | ||
self.app_principal_domain = None | ||
self.page = page | ||
|
||
def __eq__(self, other): | ||
return self.page == other.page | ||
|
||
def subset(self): | ||
return self.query() | ||
|
||
@property | ||
def page_index(self): | ||
return self.page | ||
|
||
def page_by_index(self, index): | ||
return self.__class__( | ||
session=self.session, | ||
date_=self.date, | ||
from_date=self.from_date, | ||
to_date=self.to_date, | ||
types=self.types, | ||
item_type=self.item_type, | ||
domain=self.domain, | ||
term=self.term, | ||
answer=self.answer, | ||
locale=self.locale, | ||
page=index | ||
) | ||
|
||
def group_items(self, items, request): | ||
compounded = [ | ||
gh-PonyM marked this conversation as resolved.
Show resolved
Hide resolved
|
||
id_ for item in items for id_ in getattr(item, 'elections', []) | ||
] | ||
|
||
items = dict( | ||
votes=[v for v in items if v.type == 'vote'], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you don't intend to add any items to
Note that this is not the same as writing
Which would result in a generator, which is not what you want here. By using a tuple you use less memory and you communicate your intent of this list not changing later. |
||
elections=[ | ||
e for e in items if e.type in ['election', 'election_compound'] | ||
if e.url not in compounded | ||
] | ||
) | ||
|
||
return items | ||
|
||
@staticmethod | ||
def term_to_tsquery_string(term): | ||
""" Returns the current search term transformed to use within | ||
Postgres ``to_tsquery`` function. | ||
|
||
Removes all unwanted characters, replaces prefix matching, joins | ||
word together using FOLLOWED BY. | ||
""" | ||
|
||
def cleanup(word, whitelist_chars=',.-_'): | ||
result = ''.join( | ||
(c for c in word if c.isalnum() or c in whitelist_chars) | ||
) | ||
return f'{result}:*' if word.endswith('*') else result | ||
|
||
parts = [cleanup(part) for part in (term or '').split()] | ||
gh-PonyM marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return ' <-> '.join([part for part in parts if part]) | ||
|
||
@staticmethod | ||
def match_term(column, language, term): | ||
""" Usage: | ||
model.filter(match_term(model.col, 'german', 'my search term')) """ | ||
document_tsvector = func.to_tsvector(language, column) | ||
ts_query_object = func.to_tsquery(language, term) | ||
return document_tsvector.op('@@')(ts_query_object) | ||
|
||
@staticmethod | ||
def filter_text_by_locale(column, term, locale=None): | ||
""" Returns an SqlAlchemy filter statement based on the search term. | ||
If no locale is provided, it will use english as language. | ||
|
||
``to_tsquery`` creates a tsquery value from term, which must consist of | ||
single tokens separated by the Boolean operators | ||
& (AND), | (OR) and ! (NOT). | ||
|
||
``to_tsvector`` parses a textual document into tokens, reduces the | ||
tokens to lexemes, and returns a tsvector which lists the lexemes | ||
together with their positions in the document. The document is | ||
processed according to the specified or default text search | ||
configuration. """ | ||
|
||
mapping = {'de_CH': 'german', 'fr_CH': 'french', 'it_CH': 'italian', | ||
'rm_CH': 'english'} | ||
return SearchableArchivedResultCollection.match_term( | ||
column, mapping.get(locale, 'english'), term | ||
) | ||
|
||
@property | ||
def term_filter(self): | ||
assert self.term | ||
assert self.locale | ||
term = SearchableArchivedResultCollection.term_to_tsquery_string( | ||
self.term) | ||
# The title is a translations hybrid, .title is a shorthand | ||
return [ | ||
gh-PonyM marked this conversation as resolved.
Show resolved
Hide resolved
|
||
SearchableArchivedResultCollection.filter_text_by_locale( | ||
ArchivedResult.shortcode, term, self.locale), | ||
SearchableArchivedResultCollection.filter_text_by_locale( | ||
ArchivedResult.title, term, self.locale) | ||
] | ||
|
||
def check_from_date_to_date(self): | ||
if not self.to_date or not self.from_date: | ||
return | ||
|
||
if self.to_date > date.today(): | ||
self.to_date = date.today() | ||
if self.from_date > self.to_date: | ||
self.from_date = self.to_date | ||
|
||
def query(self, no_filter=False, sort=True): | ||
if no_filter: | ||
return self.session.query(ArchivedResult) | ||
|
||
self.check_from_date_to_date() | ||
assert self.to_date, 'to_date must have a datetime.date value' | ||
allowed_domains = [d[0] for d in ArchivedResult.types_of_domains] | ||
gh-PonyM marked this conversation as resolved.
Show resolved
Hide resolved
|
||
allowed_types = [t[0] for t in ArchivedResult.types_of_results] | ||
allowed_answers = [a[0] for a in ArchivedResult.types_of_answers] | ||
order = ('federation', 'canton', 'region', 'municipality') | ||
if self.app_principal_domain == 'municipality': | ||
order = ('municipality', 'federation', 'canton', 'region') | ||
|
||
def generate_cases(): | ||
return tuple( | ||
(ArchivedResult.domain == opt, ind + 1) for | ||
ind, opt in enumerate(order) | ||
) | ||
|
||
query = self.session.query(ArchivedResult) | ||
|
||
if self.item_type and self.item_type in allowed_types: | ||
# Treat compound election as elections | ||
if self.item_type == 'vote': | ||
query = query.filter(ArchivedResult.type == self.item_type) | ||
else: | ||
query = query.filter(ArchivedResult.type.in_( | ||
('election', 'election_compound') | ||
)) | ||
|
||
elif self.types and len(self.types) != len(allowed_types): | ||
query = query.filter(ArchivedResult.type.in_(self.types)) | ||
if self.domain and (len(self.domain) != len(allowed_domains)): | ||
query = query.filter(ArchivedResult.domain.in_(self.domain)) | ||
if self.from_date: | ||
query = query.filter(ArchivedResult.date >= self.from_date) | ||
if self.to_date != date.today(): | ||
query = query.filter(ArchivedResult.date <= self.to_date) | ||
|
||
is_vote = (self.item_type == 'vote' | ||
or (self.types and 'vote' in self.types)) | ||
|
||
answer_matters = ( | ||
self.answer and len(self.answer) != len(allowed_answers)) | ||
|
||
if answer_matters and is_vote: | ||
vote_answer = ArchivedResult.meta['answer'].astext | ||
query = query.filter( | ||
ArchivedResult.type == 'vote', | ||
vote_answer.in_(self.answer)) | ||
if self.term: | ||
query = query.filter(or_(*self.term_filter)) | ||
|
||
if sort: | ||
query = query.order_by( | ||
ArchivedResult.date.desc(), | ||
case(generate_cases()) | ||
) | ||
return query | ||
|
||
def reset_query_params(self): | ||
self.from_date = None | ||
self.to_date = date.today() | ||
self.types = None | ||
self.item_type = None | ||
self.domain = None | ||
self.term = None | ||
self.answer = None | ||
self.locale = 'de_CH' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
from onegov.election_day.models import ArchivedResult | ||
from onegov.form import Form | ||
from onegov.form.fields import MultiCheckboxField | ||
from wtforms.fields.html5 import DateField | ||
from wtforms import StringField | ||
from onegov.election_day import _ | ||
|
||
|
||
class ArchiveSearchForm(Form): | ||
|
||
term = StringField( | ||
label=_("Text Retrieval"), | ||
render_kw={'size': 4, 'clear': True}, | ||
description=_( | ||
"Searches the title of the election/vote. " | ||
"Use Wilcards (*) to find more results, e.g Nationalrat*." | ||
), | ||
) | ||
|
||
from_date = DateField( | ||
label=_("From date"), | ||
render_kw={'size': 4} | ||
) | ||
|
||
to_date = DateField( | ||
label=_("To date"), | ||
render_kw={'size': 4, 'clear': False} | ||
) | ||
|
||
answer = MultiCheckboxField( | ||
label=_("Voting result"), | ||
choices=ArchivedResult.types_of_answers, | ||
render_kw={'size': 4} | ||
) | ||
|
||
# Is always hidden since item_type in url will filter the types | ||
types = MultiCheckboxField( | ||
label=_("Type"), | ||
render_kw={'size': 4, 'clear': False, 'hidden': True}, | ||
choices=ArchivedResult.types_of_results, | ||
description=_( | ||
"Compound of elections field summarizes all related elections" | ||
" in one. To display all elections," | ||
" uncheck 'Compound of Elections'") | ||
) | ||
|
||
domain = MultiCheckboxField( | ||
label=_("Domain"), | ||
render_kw={'size': 8, 'clear': False}, | ||
choices=ArchivedResult.types_of_domains | ||
) | ||
|
||
def on_request(self): | ||
# Roves crf token from query params | ||
if hasattr(self, 'csrf_token'): | ||
self.delete_field('csrf_token') | ||
|
||
def select_all(self, name): | ||
field = getattr(self, name) | ||
if not field.data: | ||
field.data = list(next(zip(*field.choices))) | ||
|
||
def toggle_hidden_fields(self, model): | ||
""" Hides answer field for election view and move the field to | ||
the right side with render_kw. """ | ||
if model.item_type in ('election', 'election_compound'): | ||
self.answer.render_kw['hidden'] = True | ||
self.domain.render_kw['size'] = 12 | ||
else: | ||
self.domain.render_kw['size'] = 8 | ||
self.answer.render_kw['hidden'] = False | ||
|
||
def apply_model(self, model): | ||
|
||
self.term.data = model.term | ||
self.from_date.data = model.from_date | ||
self.to_date.data = model.to_date | ||
self.answer.data = model.answer | ||
self.types.data = model.types | ||
self.domain.data = model.domain | ||
|
||
self.select_all('domain') | ||
self.select_all('types') | ||
self.select_all('answer') | ||
self.toggle_hidden_fields(model) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since
domain
andanswer
are lists (i.e. contain a plural of elements), I'd name themdomains
andanswers
.