Search
This commit is contained in:
parent
297d54fdc1
commit
ecdf3d5ae8
@ -4,7 +4,8 @@ Bouquins in Go
|
||||
|
||||
## TODO
|
||||
|
||||
* search
|
||||
* search (table results: authors, tags...)
|
||||
* search in header
|
||||
* About
|
||||
* translations
|
||||
* tests
|
||||
|
@ -1,7 +1,53 @@
|
||||
var app = new Vue({
|
||||
el: '#app',
|
||||
Vue.component('results-list', {
|
||||
template: '#results-list-template',
|
||||
props: ['results', 'count', 'type'],
|
||||
methods: {
|
||||
url: function(item) {
|
||||
return '/'+this.type+'/'+item.id;
|
||||
},
|
||||
label: function(item) {
|
||||
switch (this.type) {
|
||||
case 'books':
|
||||
return item.title;
|
||||
case 'authors':
|
||||
case 'series':
|
||||
return item.name;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
},
|
||||
iconClass: function() {
|
||||
return 'glyphicon glyphicon-' + this.icon();
|
||||
},
|
||||
icon: function() {
|
||||
switch (this.type) {
|
||||
case 'books':
|
||||
return 'icon';
|
||||
case 'authors':
|
||||
return 'user';
|
||||
case 'series':
|
||||
return 'list';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
},
|
||||
countlabel: function() {
|
||||
switch (this.type) {
|
||||
case 'books':
|
||||
return this.count > 1 ? 'livres' : 'livre';
|
||||
case 'authors':
|
||||
return this.count > 1 ? 'auteurs' : 'auteur';
|
||||
case 'series':
|
||||
return this.count > 1 ? 'series' : 'serie';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
var search = new Vue({
|
||||
el: '#search',
|
||||
data: {
|
||||
urlParams: {},
|
||||
authors: [],
|
||||
books: [],
|
||||
series: [],
|
||||
@ -14,43 +60,6 @@ var app = new Vue({
|
||||
perpage: 10
|
||||
},
|
||||
methods: {
|
||||
urlParse: function() {
|
||||
var match,
|
||||
pl = /\+/g, // Regex for replacing addition symbol with a space
|
||||
search = /([^&=]+)=?([^&]*)/g,
|
||||
decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
|
||||
query = window.location.search.substring(1);
|
||||
while (match = search.exec(query))
|
||||
this.urlParams[decode(match[1])] = decode(match[2]);
|
||||
},
|
||||
sendQuery: function(url, error, success) {
|
||||
var xmh = new XMLHttpRequest();
|
||||
var v;
|
||||
|
||||
xmh.onreadystatechange = function() {
|
||||
v = xmh.responseText;
|
||||
if (xmh.readyState === 4 && xmh.status === 200) {
|
||||
var res;
|
||||
try {
|
||||
res = JSON.parse(v);
|
||||
} catch (err) {
|
||||
if (null !== error)
|
||||
error(err.name, err.message);
|
||||
}
|
||||
if (null !== success)
|
||||
success(res);
|
||||
} else if (xmh.readyState === 4) {
|
||||
if (null !== error)
|
||||
error(xmh.status, v);
|
||||
}
|
||||
};
|
||||
|
||||
xmh.open('GET', url, true);
|
||||
xmh.send(null);
|
||||
},
|
||||
stdError: function(code, resp) {
|
||||
console.log('ERROR ' + code + ': ' + resp);
|
||||
},
|
||||
searchParams: function(url) {
|
||||
var res = url;
|
||||
res += '?perpage=' + this.perpage;
|
||||
@ -63,24 +72,24 @@ var app = new Vue({
|
||||
},
|
||||
searchAuthorsSuccess: function(res) {
|
||||
this.authorsCount = res.count;
|
||||
this.authors = res.authors;
|
||||
this.authors = res.results;
|
||||
},
|
||||
searchAuthors: function() {
|
||||
this.sendQuery(this.searchParams('cgi-bin/bouquins/authors'), this.stdError, this.searchAuthorsSuccess);
|
||||
this.sendQuery(this.searchParams('/authors/'), this.stdError, this.searchAuthorsSuccess);
|
||||
},
|
||||
searchBooksSuccess: function(res) {
|
||||
this.booksCount = res.count;
|
||||
this.books = res.books;
|
||||
this.books = res.results;
|
||||
},
|
||||
searchBooks: function() {
|
||||
this.sendQuery(this.searchParams('cgi-bin/bouquins/books'), this.stdError, this.searchBooksSuccess);
|
||||
this.sendQuery(this.searchParams('/books/'), this.stdError, this.searchBooksSuccess);
|
||||
},
|
||||
searchSeriesSuccess: function(res) {
|
||||
this.seriesCount = res.count;
|
||||
this.series = res.series;
|
||||
this.series = res.results;
|
||||
},
|
||||
searchSeries: function() {
|
||||
this.sendQuery(this.searchParams('cgi-bin/bouquins/series'), this.stdError, this.searchSeriesSuccess);
|
||||
this.sendQuery(this.searchParams('/series/'), this.stdError, this.searchSeriesSuccess);
|
||||
},
|
||||
searchAll: function() {
|
||||
this.clear();
|
||||
@ -125,12 +134,33 @@ var app = new Vue({
|
||||
this.searchAll();
|
||||
this.q = this.urlParams.q;
|
||||
}
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.urlParse();
|
||||
},
|
||||
mounted: function() {
|
||||
this.searchUrl();
|
||||
sendQuery: function(url, error, success) {
|
||||
var xmh = new XMLHttpRequest();
|
||||
var v;
|
||||
xmh.onreadystatechange = function() {
|
||||
v = xmh.responseText;
|
||||
if (xmh.readyState === 4 && xmh.status === 200) {
|
||||
var res;
|
||||
try {
|
||||
res = JSON.parse(v);
|
||||
} catch (err) {
|
||||
if (null !== error)
|
||||
error(err.name, err.message);
|
||||
}
|
||||
})
|
||||
if (null !== success)
|
||||
success(res);
|
||||
} else if (xmh.readyState === 4) {
|
||||
if (null !== error)
|
||||
error(xmh.status, v);
|
||||
}
|
||||
};
|
||||
xmh.open('GET', url, true);
|
||||
xmh.setRequestHeader('Accept','application/json');
|
||||
xmh.send(null);
|
||||
},
|
||||
stdError: function(code, resp) {
|
||||
console.log('ERROR ' + code + ': ' + resp);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -20,12 +20,14 @@ const (
|
||||
TPL_AUTHORS = "author.html"
|
||||
TPL_SERIES = "series.html"
|
||||
TPL_INDEX = "index.html"
|
||||
TPL_SEARCH = "search.html"
|
||||
|
||||
PARAM_LIST = "list"
|
||||
PARAM_ORDER = "order"
|
||||
PARAM_SORT = "sort"
|
||||
PARAM_PAGE = "page"
|
||||
PARAM_PERPAGE = "perpage"
|
||||
PARAM_TERM = "term"
|
||||
|
||||
LIST_AUTHORS = "authors"
|
||||
LIST_SERIES = "series"
|
||||
@ -35,6 +37,7 @@ const (
|
||||
URL_BOOKS = "/books/"
|
||||
URL_AUTHORS = "/authors/"
|
||||
URL_SERIES = "/series/"
|
||||
URL_SEARCH = "/search/"
|
||||
URL_JS = "/js/"
|
||||
URL_CSS = "/css/"
|
||||
URL_FONTS = "/fonts/"
|
||||
@ -159,17 +162,26 @@ func NewIndexModel(title, js string, count int64) *IndexModel {
|
||||
}
|
||||
}
|
||||
|
||||
type SearchModel struct {
|
||||
BouquinsModel
|
||||
}
|
||||
|
||||
func NewSearchModel() *SearchModel {
|
||||
return &SearchModel{*NewBouquinsModelJs("Recherche", "search.js")}
|
||||
}
|
||||
|
||||
type ResultsModel struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
More bool `json:"more"`
|
||||
CountResults int `json:"count,omitempty"`
|
||||
}
|
||||
type BooksResultsModel struct {
|
||||
ResultsModel
|
||||
Results []*BookAdv `json:"results,omitempty"`
|
||||
}
|
||||
|
||||
func NewBooksResultsModel(books []*BookAdv, more bool) *BooksResultsModel {
|
||||
return &BooksResultsModel{ResultsModel{"books", more}, books}
|
||||
func NewBooksResultsModel(books []*BookAdv, more bool, count int) *BooksResultsModel {
|
||||
return &BooksResultsModel{ResultsModel{"books", more, count}, books}
|
||||
}
|
||||
|
||||
type AuthorsResultsModel struct {
|
||||
@ -177,8 +189,8 @@ type AuthorsResultsModel struct {
|
||||
Results []*AuthorAdv `json:"results,omitempty"`
|
||||
}
|
||||
|
||||
func NewAuthorsResultsModel(authors []*AuthorAdv, more bool) *AuthorsResultsModel {
|
||||
return &AuthorsResultsModel{ResultsModel{"authors", more}, authors}
|
||||
func NewAuthorsResultsModel(authors []*AuthorAdv, more bool, count int) *AuthorsResultsModel {
|
||||
return &AuthorsResultsModel{ResultsModel{"authors", more, count}, authors}
|
||||
}
|
||||
|
||||
type SeriesResultsModel struct {
|
||||
@ -186,8 +198,8 @@ type SeriesResultsModel struct {
|
||||
Results []*SeriesAdv `json:"results,omitempty"`
|
||||
}
|
||||
|
||||
func NewSeriesResultsModel(series []*SeriesAdv, more bool) *SeriesResultsModel {
|
||||
return &SeriesResultsModel{ResultsModel{"series", more}, series}
|
||||
func NewSeriesResultsModel(series []*SeriesAdv, more bool, count int) *SeriesResultsModel {
|
||||
return &SeriesResultsModel{ResultsModel{"series", more, count}, series}
|
||||
}
|
||||
|
||||
type BookModel struct {
|
||||
@ -205,6 +217,15 @@ type AuthorModel struct {
|
||||
*AuthorFull
|
||||
}
|
||||
|
||||
type ReqParams struct {
|
||||
Limit int
|
||||
Offset int
|
||||
Sort string
|
||||
Order string
|
||||
Terms []string
|
||||
AllWords bool
|
||||
}
|
||||
|
||||
func (app *Bouquins) render(res http.ResponseWriter, tpl string, model interface{}) error {
|
||||
return app.Template.ExecuteTemplate(res, tpl, model)
|
||||
}
|
||||
@ -235,6 +256,9 @@ func isJson(req *http.Request) bool {
|
||||
}
|
||||
func paramInt(name string, req *http.Request) int {
|
||||
val := req.URL.Query().Get(name)
|
||||
if val == "" {
|
||||
return 0
|
||||
}
|
||||
valInt, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
log.Println("Invalid value for", name, ":", val)
|
||||
@ -250,8 +274,7 @@ func paramOrder(req *http.Request) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// return limit, offset, sort, order
|
||||
func paramPaginate(req *http.Request) (int, int, string, string) {
|
||||
func params(req *http.Request) *ReqParams {
|
||||
page, perpage := paramInt(PARAM_PAGE, req), paramInt(PARAM_PERPAGE, req)
|
||||
limit := perpage
|
||||
if perpage == 0 {
|
||||
@ -263,7 +286,8 @@ func paramPaginate(req *http.Request) (int, int, string, string) {
|
||||
}
|
||||
sort := req.URL.Query().Get(PARAM_SORT)
|
||||
order := paramOrder(req)
|
||||
return limit, offset, sort, order
|
||||
terms := req.URL.Query()[PARAM_TERM]
|
||||
return &ReqParams{limit, offset, sort, order, terms, false}
|
||||
}
|
||||
|
||||
// ROUTES //
|
||||
@ -281,16 +305,19 @@ func (app *Bouquins) IndexPage(res http.ResponseWriter, req *http.Request) {
|
||||
http.Error(res, err.Error(), 500)
|
||||
}
|
||||
} else {
|
||||
app.render(res, TPL_INDEX, model)
|
||||
err = app.render(res, TPL_INDEX, model)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
func (app *Bouquins) BooksListPage(res http.ResponseWriter, req *http.Request) error {
|
||||
if isJson(req) {
|
||||
books, more, err := app.BooksAdv(paramPaginate(req))
|
||||
books, count, more, err := app.BooksAdv(params(req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeJson(res, NewBooksResultsModel(books, more))
|
||||
return writeJson(res, NewBooksResultsModel(books, more, count))
|
||||
}
|
||||
return errors.New("Invalid mime")
|
||||
}
|
||||
@ -325,11 +352,11 @@ func (app *Bouquins) BooksPage(res http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
func (app *Bouquins) AuthorsListPage(res http.ResponseWriter, req *http.Request) error {
|
||||
if isJson(req) {
|
||||
authors, more, err := app.AuthorsAdv(paramPaginate(req))
|
||||
authors, count, more, err := app.AuthorsAdv(params(req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeJson(res, NewAuthorsResultsModel(authors, more))
|
||||
return writeJson(res, NewAuthorsResultsModel(authors, more, count))
|
||||
}
|
||||
return errors.New("Invalid mime")
|
||||
}
|
||||
@ -364,11 +391,11 @@ func (app *Bouquins) AuthorsPage(res http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
func (app *Bouquins) SeriesListPage(res http.ResponseWriter, req *http.Request) error {
|
||||
if isJson(req) {
|
||||
series, more, err := app.SeriesAdv(paramPaginate(req))
|
||||
series, count, more, err := app.SeriesAdv(params(req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeJson(res, NewSeriesResultsModel(series, more))
|
||||
return writeJson(res, NewSeriesResultsModel(series, more, count))
|
||||
}
|
||||
return errors.New("Invalid mime")
|
||||
}
|
||||
@ -401,3 +428,10 @@ func (app *Bouquins) SeriesPage(res http.ResponseWriter, req *http.Request) {
|
||||
http.Error(res, err.Error(), 500)
|
||||
}
|
||||
}
|
||||
func (app *Bouquins) SearchPage(res http.ResponseWriter, req *http.Request) {
|
||||
model := NewSearchModel()
|
||||
err := app.render(res, TPL_SEARCH, model)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,10 @@ const (
|
||||
STMT_WHERE = " WHERE "
|
||||
STMT_BOOL_AND = " AND "
|
||||
STMT_BOOL_OR = " OR "
|
||||
STMT_SEARCH_ORDER = " ORDER BY books.sort"
|
||||
|
||||
STMT_SEARCH_ORDER_BOOKS = " ORDER BY books.sort"
|
||||
STMT_SEARCH_ORDER_AUTHORS = " ORDER BY authors.sort"
|
||||
STMT_SEARCH_ORDER_SERIES = " ORDER BY series.sort"
|
||||
|
||||
STMT_BOOKS_COUNT = "SELECT count(id) FROM books"
|
||||
STMT_BOOK = `SELECT books.id AS id,title, series_index, series.name AS series_name, series.id AS series_id,
|
||||
|
@ -2,10 +2,54 @@ package bouquins
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
)
|
||||
|
||||
// SUB QUERIES //
|
||||
|
||||
func (app *Bouquins) searchAuthors(limit int, terms []string, all bool) ([]*AuthorAdv, int, error) {
|
||||
authors := make([]*AuthorAdv, 0, limit)
|
||||
count := 0
|
||||
query := STMT_AUTHORS_SEARCH
|
||||
queryTerms := make([]interface{}, 0, len(terms))
|
||||
for i, term := range terms {
|
||||
queryTerms = append(queryTerms, "%"+term+"%")
|
||||
query += STMT_SEARCH_TERM_AUTHOR
|
||||
if i < len(terms)-1 && all {
|
||||
query += STMT_BOOL_AND
|
||||
}
|
||||
if i < len(terms)-1 && !all {
|
||||
query += STMT_BOOL_OR
|
||||
}
|
||||
}
|
||||
query += STMT_SEARCH_ORDER_AUTHORS
|
||||
log.Println("Search:", query)
|
||||
|
||||
stmt, err := app.DB.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows, err := stmt.Query(queryTerms...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
if len(authors) <= limit {
|
||||
author := new(AuthorAdv)
|
||||
if err := rows.Scan(&author.Id, &author.Name); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
authors = append(authors, author)
|
||||
}
|
||||
count++
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return authors, count, nil
|
||||
}
|
||||
|
||||
func (app *Bouquins) queryAuthors(limit, offset int, sort, order string) ([]*AuthorAdv, bool, error) {
|
||||
authors := make([]*AuthorAdv, 0, limit)
|
||||
stmt, err := app.psSortAuthors(AUTHORS, sort, order)
|
||||
@ -110,12 +154,17 @@ func (app *Bouquins) queryAuthor(id int64) (*AuthorFull, error) {
|
||||
|
||||
// DB LOADS //
|
||||
|
||||
func (app *Bouquins) AuthorsAdv(limit, offset int, sort, order string) ([]*AuthorAdv, bool, error) {
|
||||
func (app *Bouquins) AuthorsAdv(params *ReqParams) ([]*AuthorAdv, int, bool, error) {
|
||||
limit, offset, sort, order := params.Limit, params.Offset, params.Sort, params.Order
|
||||
if len(params.Terms) > 0 {
|
||||
authors, count, err := app.searchAuthors(limit, params.Terms, params.AllWords)
|
||||
return authors, count, count > limit, err
|
||||
}
|
||||
authors, more, err := app.queryAuthors(limit, offset, sort, order)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, 0, false, err
|
||||
}
|
||||
return authors, more, nil
|
||||
return authors, 0, more, nil
|
||||
}
|
||||
|
||||
func (app *Bouquins) AuthorFull(id int64) (*AuthorFull, error) {
|
||||
|
@ -1,6 +1,9 @@
|
||||
package bouquins
|
||||
|
||||
import "database/sql"
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
)
|
||||
|
||||
// MERGE SUB QUERIES //
|
||||
func assignAuthorsTagsBooks(books []*BookAdv, authors map[int64][]*Author, tags map[int64][]string) {
|
||||
@ -12,6 +15,59 @@ func assignAuthorsTagsBooks(books []*BookAdv, authors map[int64][]*Author, tags
|
||||
|
||||
// SUB QUERIES //
|
||||
|
||||
func (app *Bouquins) searchBooks(limit int, terms []string, all bool) ([]*BookAdv, int, error) {
|
||||
books := make([]*BookAdv, 0, limit)
|
||||
// FIXME factorize searchAuthors,searchSeries
|
||||
count := 0
|
||||
query := STMT_BOOKS0 + STMT_WHERE
|
||||
queryTerms := make([]interface{}, 0, len(terms))
|
||||
for i, term := range terms {
|
||||
queryTerms = append(queryTerms, "%"+term+"%")
|
||||
query += STMT_SEARCH_TERM_BOOKS
|
||||
if i < len(terms)-1 && all {
|
||||
query += STMT_BOOL_AND
|
||||
}
|
||||
if i < len(terms)-1 && !all {
|
||||
query += STMT_BOOL_OR
|
||||
}
|
||||
}
|
||||
query += STMT_SEARCH_ORDER_BOOKS
|
||||
log.Println("Search:", query)
|
||||
|
||||
stmt, err := app.DB.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows, err := stmt.Query(queryTerms...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
// FIXME factorize queryBooks
|
||||
for rows.Next() {
|
||||
if len(books) <= limit {
|
||||
book := new(BookAdv)
|
||||
var series_name sql.NullString
|
||||
var series_id sql.NullInt64
|
||||
if err := rows.Scan(&book.Id, &book.Title, &book.SeriesIndex, &series_name, &series_id); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if series_name.Valid && series_id.Valid {
|
||||
book.Series = &Series{
|
||||
series_id.Int64,
|
||||
series_name.String,
|
||||
}
|
||||
}
|
||||
books = append(books, book)
|
||||
}
|
||||
count++
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return books, count, nil
|
||||
}
|
||||
|
||||
func (app *Bouquins) queryBooks(limit, offset int, sort, order string) ([]*BookAdv, bool, error) {
|
||||
books := make([]*BookAdv, 0, limit)
|
||||
stmt, err := app.psSortBooks(BOOKS, sort, order)
|
||||
@ -254,19 +310,24 @@ func (app *Bouquins) BookFull(id int64) (*BookFull, error) {
|
||||
return book, nil
|
||||
}
|
||||
|
||||
func (app *Bouquins) BooksAdv(limit, offset int, sort, order string) ([]*BookAdv, bool, error) {
|
||||
func (app *Bouquins) BooksAdv(params *ReqParams) ([]*BookAdv, int, bool, error) {
|
||||
limit, offset, sort, order := params.Limit, params.Offset, params.Sort, params.Order
|
||||
if len(params.Terms) > 0 {
|
||||
books, count, err := app.searchBooks(limit, params.Terms, params.AllWords)
|
||||
return books, count, count > limit, err
|
||||
}
|
||||
books, more, err := app.queryBooks(limit, offset, sort, order)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, 0, false, err
|
||||
}
|
||||
authors, err := app.queryBooksAuthors(limit, offset, sort, order)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, 0, false, err
|
||||
}
|
||||
tags, err := app.queryBooksTags(limit, offset, sort, order)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, 0, false, err
|
||||
}
|
||||
assignAuthorsTagsBooks(books, authors, tags)
|
||||
return books, more, nil
|
||||
return books, 0, more, nil
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package bouquins
|
||||
|
||||
import "log"
|
||||
|
||||
// MERGE SUB QUERIES //
|
||||
|
||||
func assignAuthorsSeries(series []*SeriesAdv, authors map[int64][]*Author) {
|
||||
@ -10,6 +12,49 @@ func assignAuthorsSeries(series []*SeriesAdv, authors map[int64][]*Author) {
|
||||
|
||||
// SUB QUERIES //
|
||||
|
||||
func (app *Bouquins) searchSeries(limit int, terms []string, all bool) ([]*SeriesAdv, int, error) {
|
||||
series := make([]*SeriesAdv, 0, limit)
|
||||
count := 0
|
||||
query := STMT_SERIES_SEARCH
|
||||
queryTerms := make([]interface{}, 0, len(terms))
|
||||
for i, term := range terms {
|
||||
queryTerms = append(queryTerms, "%"+term+"%")
|
||||
query += STMT_SEARCH_TERM_SERIES
|
||||
if i < len(terms)-1 && all {
|
||||
query += STMT_BOOL_AND
|
||||
}
|
||||
if i < len(terms)-1 && !all {
|
||||
query += STMT_BOOL_OR
|
||||
}
|
||||
}
|
||||
query += STMT_SEARCH_ORDER_SERIES
|
||||
log.Println("Search:", query)
|
||||
|
||||
stmt, err := app.DB.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows, err := stmt.Query(queryTerms...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
if len(series) <= limit {
|
||||
serie := new(SeriesAdv)
|
||||
if err := rows.Scan(&serie.Id, &serie.Name); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
series = append(series, serie)
|
||||
}
|
||||
count++
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return series, count, nil
|
||||
}
|
||||
|
||||
func (app *Bouquins) querySeriesList(limit, offset int, sort, order string) ([]*SeriesAdv, bool, error) {
|
||||
series := make([]*SeriesAdv, 0, limit)
|
||||
stmt, err := app.psSortSeries(SERIES, sort, order)
|
||||
@ -139,15 +184,20 @@ func (app *Bouquins) SeriesFull(id int64) (*SeriesFull, error) {
|
||||
return series, nil
|
||||
}
|
||||
|
||||
func (app *Bouquins) SeriesAdv(limit, offset int, sort, order string) ([]*SeriesAdv, bool, error) {
|
||||
func (app *Bouquins) SeriesAdv(params *ReqParams) ([]*SeriesAdv, int, bool, error) {
|
||||
limit, offset, sort, order := params.Limit, params.Offset, params.Sort, params.Order
|
||||
if len(params.Terms) > 0 {
|
||||
series, count, err := app.searchSeries(limit, params.Terms, params.AllWords)
|
||||
return series, count, count > limit, err
|
||||
}
|
||||
series, more, err := app.querySeriesList(limit, offset, sort, order)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, 0, false, err
|
||||
}
|
||||
authors, err := app.querySeriesListAuthors(limit, offset, sort, order)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, 0, false, err
|
||||
}
|
||||
assignAuthorsSeries(series, authors)
|
||||
return series, more, nil
|
||||
return series, 0, more, nil
|
||||
}
|
||||
|
1
main.go
1
main.go
@ -88,6 +88,7 @@ func router(app *Bouquins) {
|
||||
http.HandleFunc(URL_BOOKS, app.BooksPage)
|
||||
http.HandleFunc(URL_AUTHORS, app.AuthorsPage)
|
||||
http.HandleFunc(URL_SERIES, app.SeriesPage)
|
||||
http.HandleFunc(URL_SEARCH, app.SearchPage)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
38
templates/components.html
Normal file
38
templates/components.html
Normal file
@ -0,0 +1,38 @@
|
||||
<script type="text/x-template" id="results-template">
|
||||
<table class="table table-striped" v-if="results.length > 0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th v-for="col in cols">
|
||||
<template v-if="col.sort">
|
||||
<a href="#" @click="sortBy(col.sort)">{{ "{{" }}col.name{{ "}}" }}</a>
|
||||
<span v-if="sort_by == col.id" :class="['glyphicon', { 'glyphicon-chevron-up': order_desc , 'glyphicon-chevron-down': !order_desc}]"></span>
|
||||
</template>
|
||||
<template v-else>{{ "{{" }}col.name{{ "}}" }}</template>
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-for="item in results">
|
||||
<td is="result-cell" :col="col" :item="item" v-for="col in cols"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</script>
|
||||
<script type="text/x-template" id="paginate-template">
|
||||
<nav aria-label="Pages" v-if="page > 0">
|
||||
<ul class="pager">
|
||||
<li class="previous" v-bind:class="{ disabled: page <= 1 }"><a href="#" @click="prevPage"><span aria-hidden="true">←</span> Précédents</a></li>
|
||||
<li class="next" v-bind:class="{ disabled: !more }"><a href="#" @click="nextPage">Suivants <span aria-hidden="true">→</span></a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</script>
|
||||
<script type="text/x-template" id="results-list-template">
|
||||
<div v-if="count > 0">
|
||||
<h2>{{ "{{ count }} {{ countlabel() }}" }}</h2>
|
||||
<ul>
|
||||
<li v-for="item in results" class="list-unstyled">
|
||||
<span :class="iconClass()"></span>
|
||||
<a :href="url(item)">{{ "{{ label(item) }}" }}</a>
|
||||
</li>
|
||||
<li v-if="results.length < count" class="list-unstyled">...</li>
|
||||
</ul>
|
||||
</div>
|
||||
</script>
|
@ -17,7 +17,7 @@
|
||||
<div class="container">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="active"><a href="/">Accueil</a></li>
|
||||
<li><a href="search.html">Recherche</a></li>
|
||||
<li><a href="/search">Recherche</a></li>
|
||||
<li><a href="#">A propos</a></li>
|
||||
</ul>
|
||||
<form class="navbar-form navbar-right" role="search" method="get" action="search.html">
|
||||
|
@ -13,30 +13,5 @@
|
||||
<paginate :page="page" :more="more"></paginate>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/x-template" id="results-template">
|
||||
<table class="table table-striped" v-if="results.length > 0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th v-for="col in cols">
|
||||
<template v-if="col.sort">
|
||||
<a href="#" @click="sortBy(col.sort)">{{ "{{" }}col.name{{ "}}" }}</a>
|
||||
<span v-if="sort_by == col.id" :class="['glyphicon', { 'glyphicon-chevron-up': order_desc , 'glyphicon-chevron-down': !order_desc}]"></span>
|
||||
</template>
|
||||
<template v-else>{{ "{{" }}col.name{{ "}}" }}</template>
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-for="item in results">
|
||||
<td is="result-cell" :col="col" :item="item" v-for="col in cols"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</script>
|
||||
<script type="text/x-template" id="paginate-template">
|
||||
<nav aria-label="Pages" v-if="page > 0">
|
||||
<ul class="pager">
|
||||
<li class="previous" v-bind:class="{ disabled: page <= 1 }"><a href="#" @click="prevPage"><span aria-hidden="true">←</span> Précédents</a></li>
|
||||
<li class="next" v-bind:class="{ disabled: !more }"><a href="#" @click="nextPage">Suivants <span aria-hidden="true">→</span></a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</script>
|
||||
{{ template "components.html" }}
|
||||
{{ template "footer.html" . }}
|
||||
|
61
templates/search.html
Normal file
61
templates/search.html
Normal file
@ -0,0 +1,61 @@
|
||||
{{ template "header.html" . }}
|
||||
<div class="container" id="search">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<h3>Recherche</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form id="searchForm" @submit="searchFull" v-on:submit.prevent>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" placeholder="Recherche" v-model="q">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Parmi</label><br/>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" value="books" v-model="which"> livres
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" value="authors" v-model="which"> auteurs
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" value="series" v-model="which"> series
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" value="all" v-model="which"> tous
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nombre de resultats</label><br/>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" value="10" v-model="perpage"> 10
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" value="20" v-model="perpage"> 20
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" value="50" v-model="perpage"> 50
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" value="100" v-model="perpage"> 100
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" v-model="all" disabled> Tous les mots
|
||||
</label>
|
||||
<p class="help-block">Cocher pour rechercher les élements contenant tous les mots saisis</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Rechercher</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<results-list type="books" :count="booksCount" :results="books"></results-list>
|
||||
<results-list type="authors" :count="authorsCount" :results="authors"></results-list>
|
||||
<results-list type="series" :count="seriesCount" :results="series"></results-list>
|
||||
</div>
|
||||
</div>
|
||||
{{ template "components.html" }}
|
||||
{{ template "footer.html" . }}
|
Loading…
Reference in New Issue
Block a user