Search
This commit is contained in:
parent
297d54fdc1
commit
ecdf3d5ae8
@ -4,7 +4,8 @@ Bouquins in Go
|
|||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
* search
|
* search (table results: authors, tags...)
|
||||||
|
* search in header
|
||||||
* About
|
* About
|
||||||
* translations
|
* translations
|
||||||
* tests
|
* tests
|
||||||
|
@ -1,7 +1,53 @@
|
|||||||
var app = new Vue({
|
Vue.component('results-list', {
|
||||||
el: '#app',
|
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: {
|
data: {
|
||||||
urlParams: {},
|
|
||||||
authors: [],
|
authors: [],
|
||||||
books: [],
|
books: [],
|
||||||
series: [],
|
series: [],
|
||||||
@ -14,43 +60,6 @@ var app = new Vue({
|
|||||||
perpage: 10
|
perpage: 10
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
searchParams: function(url) {
|
||||||
var res = url;
|
var res = url;
|
||||||
res += '?perpage=' + this.perpage;
|
res += '?perpage=' + this.perpage;
|
||||||
@ -63,24 +72,24 @@ var app = new Vue({
|
|||||||
},
|
},
|
||||||
searchAuthorsSuccess: function(res) {
|
searchAuthorsSuccess: function(res) {
|
||||||
this.authorsCount = res.count;
|
this.authorsCount = res.count;
|
||||||
this.authors = res.authors;
|
this.authors = res.results;
|
||||||
},
|
},
|
||||||
searchAuthors: function() {
|
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) {
|
searchBooksSuccess: function(res) {
|
||||||
this.booksCount = res.count;
|
this.booksCount = res.count;
|
||||||
this.books = res.books;
|
this.books = res.results;
|
||||||
},
|
},
|
||||||
searchBooks: function() {
|
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) {
|
searchSeriesSuccess: function(res) {
|
||||||
this.seriesCount = res.count;
|
this.seriesCount = res.count;
|
||||||
this.series = res.series;
|
this.series = res.results;
|
||||||
},
|
},
|
||||||
searchSeries: function() {
|
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() {
|
searchAll: function() {
|
||||||
this.clear();
|
this.clear();
|
||||||
@ -125,12 +134,33 @@ var app = new Vue({
|
|||||||
this.searchAll();
|
this.searchAll();
|
||||||
this.q = this.urlParams.q;
|
this.q = this.urlParams.q;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
created: function() {
|
|
||||||
this.urlParse();
|
|
||||||
},
|
|
||||||
mounted: function() {
|
|
||||||
this.searchUrl();
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
@ -20,12 +20,14 @@ const (
|
|||||||
TPL_AUTHORS = "author.html"
|
TPL_AUTHORS = "author.html"
|
||||||
TPL_SERIES = "series.html"
|
TPL_SERIES = "series.html"
|
||||||
TPL_INDEX = "index.html"
|
TPL_INDEX = "index.html"
|
||||||
|
TPL_SEARCH = "search.html"
|
||||||
|
|
||||||
PARAM_LIST = "list"
|
PARAM_LIST = "list"
|
||||||
PARAM_ORDER = "order"
|
PARAM_ORDER = "order"
|
||||||
PARAM_SORT = "sort"
|
PARAM_SORT = "sort"
|
||||||
PARAM_PAGE = "page"
|
PARAM_PAGE = "page"
|
||||||
PARAM_PERPAGE = "perpage"
|
PARAM_PERPAGE = "perpage"
|
||||||
|
PARAM_TERM = "term"
|
||||||
|
|
||||||
LIST_AUTHORS = "authors"
|
LIST_AUTHORS = "authors"
|
||||||
LIST_SERIES = "series"
|
LIST_SERIES = "series"
|
||||||
@ -35,6 +37,7 @@ const (
|
|||||||
URL_BOOKS = "/books/"
|
URL_BOOKS = "/books/"
|
||||||
URL_AUTHORS = "/authors/"
|
URL_AUTHORS = "/authors/"
|
||||||
URL_SERIES = "/series/"
|
URL_SERIES = "/series/"
|
||||||
|
URL_SEARCH = "/search/"
|
||||||
URL_JS = "/js/"
|
URL_JS = "/js/"
|
||||||
URL_CSS = "/css/"
|
URL_CSS = "/css/"
|
||||||
URL_FONTS = "/fonts/"
|
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 ResultsModel struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
More bool `json:"more"`
|
More bool `json:"more"`
|
||||||
|
CountResults int `json:"count,omitempty"`
|
||||||
}
|
}
|
||||||
type BooksResultsModel struct {
|
type BooksResultsModel struct {
|
||||||
ResultsModel
|
ResultsModel
|
||||||
Results []*BookAdv `json:"results,omitempty"`
|
Results []*BookAdv `json:"results,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBooksResultsModel(books []*BookAdv, more bool) *BooksResultsModel {
|
func NewBooksResultsModel(books []*BookAdv, more bool, count int) *BooksResultsModel {
|
||||||
return &BooksResultsModel{ResultsModel{"books", more}, books}
|
return &BooksResultsModel{ResultsModel{"books", more, count}, books}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorsResultsModel struct {
|
type AuthorsResultsModel struct {
|
||||||
@ -177,8 +189,8 @@ type AuthorsResultsModel struct {
|
|||||||
Results []*AuthorAdv `json:"results,omitempty"`
|
Results []*AuthorAdv `json:"results,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthorsResultsModel(authors []*AuthorAdv, more bool) *AuthorsResultsModel {
|
func NewAuthorsResultsModel(authors []*AuthorAdv, more bool, count int) *AuthorsResultsModel {
|
||||||
return &AuthorsResultsModel{ResultsModel{"authors", more}, authors}
|
return &AuthorsResultsModel{ResultsModel{"authors", more, count}, authors}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SeriesResultsModel struct {
|
type SeriesResultsModel struct {
|
||||||
@ -186,8 +198,8 @@ type SeriesResultsModel struct {
|
|||||||
Results []*SeriesAdv `json:"results,omitempty"`
|
Results []*SeriesAdv `json:"results,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSeriesResultsModel(series []*SeriesAdv, more bool) *SeriesResultsModel {
|
func NewSeriesResultsModel(series []*SeriesAdv, more bool, count int) *SeriesResultsModel {
|
||||||
return &SeriesResultsModel{ResultsModel{"series", more}, series}
|
return &SeriesResultsModel{ResultsModel{"series", more, count}, series}
|
||||||
}
|
}
|
||||||
|
|
||||||
type BookModel struct {
|
type BookModel struct {
|
||||||
@ -205,6 +217,15 @@ type AuthorModel struct {
|
|||||||
*AuthorFull
|
*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 {
|
func (app *Bouquins) render(res http.ResponseWriter, tpl string, model interface{}) error {
|
||||||
return app.Template.ExecuteTemplate(res, tpl, model)
|
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 {
|
func paramInt(name string, req *http.Request) int {
|
||||||
val := req.URL.Query().Get(name)
|
val := req.URL.Query().Get(name)
|
||||||
|
if val == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
valInt, err := strconv.Atoi(val)
|
valInt, err := strconv.Atoi(val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Invalid value for", name, ":", val)
|
log.Println("Invalid value for", name, ":", val)
|
||||||
@ -250,8 +274,7 @@ func paramOrder(req *http.Request) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// return limit, offset, sort, order
|
func params(req *http.Request) *ReqParams {
|
||||||
func paramPaginate(req *http.Request) (int, int, string, string) {
|
|
||||||
page, perpage := paramInt(PARAM_PAGE, req), paramInt(PARAM_PERPAGE, req)
|
page, perpage := paramInt(PARAM_PAGE, req), paramInt(PARAM_PERPAGE, req)
|
||||||
limit := perpage
|
limit := perpage
|
||||||
if perpage == 0 {
|
if perpage == 0 {
|
||||||
@ -263,7 +286,8 @@ func paramPaginate(req *http.Request) (int, int, string, string) {
|
|||||||
}
|
}
|
||||||
sort := req.URL.Query().Get(PARAM_SORT)
|
sort := req.URL.Query().Get(PARAM_SORT)
|
||||||
order := paramOrder(req)
|
order := paramOrder(req)
|
||||||
return limit, offset, sort, order
|
terms := req.URL.Query()[PARAM_TERM]
|
||||||
|
return &ReqParams{limit, offset, sort, order, terms, false}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ROUTES //
|
// ROUTES //
|
||||||
@ -281,16 +305,19 @@ func (app *Bouquins) IndexPage(res http.ResponseWriter, req *http.Request) {
|
|||||||
http.Error(res, err.Error(), 500)
|
http.Error(res, err.Error(), 500)
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
func (app *Bouquins) BooksListPage(res http.ResponseWriter, req *http.Request) error {
|
||||||
if isJson(req) {
|
if isJson(req) {
|
||||||
books, more, err := app.BooksAdv(paramPaginate(req))
|
books, count, more, err := app.BooksAdv(params(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeJson(res, NewBooksResultsModel(books, more))
|
return writeJson(res, NewBooksResultsModel(books, more, count))
|
||||||
}
|
}
|
||||||
return errors.New("Invalid mime")
|
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 {
|
func (app *Bouquins) AuthorsListPage(res http.ResponseWriter, req *http.Request) error {
|
||||||
if isJson(req) {
|
if isJson(req) {
|
||||||
authors, more, err := app.AuthorsAdv(paramPaginate(req))
|
authors, count, more, err := app.AuthorsAdv(params(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeJson(res, NewAuthorsResultsModel(authors, more))
|
return writeJson(res, NewAuthorsResultsModel(authors, more, count))
|
||||||
}
|
}
|
||||||
return errors.New("Invalid mime")
|
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 {
|
func (app *Bouquins) SeriesListPage(res http.ResponseWriter, req *http.Request) error {
|
||||||
if isJson(req) {
|
if isJson(req) {
|
||||||
series, more, err := app.SeriesAdv(paramPaginate(req))
|
series, count, more, err := app.SeriesAdv(params(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeJson(res, NewSeriesResultsModel(series, more))
|
return writeJson(res, NewSeriesResultsModel(series, more, count))
|
||||||
}
|
}
|
||||||
return errors.New("Invalid mime")
|
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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -33,11 +33,14 @@ const (
|
|||||||
STMT_AUTHORS_SEARCH = "SELECT id, name FROM authors WHERE "
|
STMT_AUTHORS_SEARCH = "SELECT id, name FROM authors WHERE "
|
||||||
STMT_SEARCH_TERM_AUTHOR = " sort like ? "
|
STMT_SEARCH_TERM_AUTHOR = " sort like ? "
|
||||||
|
|
||||||
STMT_PAGE = " LIMIT ? OFFSET ?"
|
STMT_PAGE = " LIMIT ? OFFSET ?"
|
||||||
STMT_WHERE = " WHERE "
|
STMT_WHERE = " WHERE "
|
||||||
STMT_BOOL_AND = " AND "
|
STMT_BOOL_AND = " AND "
|
||||||
STMT_BOOL_OR = " OR "
|
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_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,
|
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 (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SUB QUERIES //
|
// 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) {
|
func (app *Bouquins) queryAuthors(limit, offset int, sort, order string) ([]*AuthorAdv, bool, error) {
|
||||||
authors := make([]*AuthorAdv, 0, limit)
|
authors := make([]*AuthorAdv, 0, limit)
|
||||||
stmt, err := app.psSortAuthors(AUTHORS, sort, order)
|
stmt, err := app.psSortAuthors(AUTHORS, sort, order)
|
||||||
@ -110,12 +154,17 @@ func (app *Bouquins) queryAuthor(id int64) (*AuthorFull, error) {
|
|||||||
|
|
||||||
// DB LOADS //
|
// 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)
|
authors, more, err := app.queryAuthors(limit, offset, sort, order)
|
||||||
if err != nil {
|
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) {
|
func (app *Bouquins) AuthorFull(id int64) (*AuthorFull, error) {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package bouquins
|
package bouquins
|
||||||
|
|
||||||
import "database/sql"
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
// MERGE SUB QUERIES //
|
// MERGE SUB QUERIES //
|
||||||
func assignAuthorsTagsBooks(books []*BookAdv, authors map[int64][]*Author, tags map[int64][]string) {
|
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 //
|
// 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) {
|
func (app *Bouquins) queryBooks(limit, offset int, sort, order string) ([]*BookAdv, bool, error) {
|
||||||
books := make([]*BookAdv, 0, limit)
|
books := make([]*BookAdv, 0, limit)
|
||||||
stmt, err := app.psSortBooks(BOOKS, sort, order)
|
stmt, err := app.psSortBooks(BOOKS, sort, order)
|
||||||
@ -254,19 +310,24 @@ func (app *Bouquins) BookFull(id int64) (*BookFull, error) {
|
|||||||
return book, nil
|
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)
|
books, more, err := app.queryBooks(limit, offset, sort, order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, 0, false, err
|
||||||
}
|
}
|
||||||
authors, err := app.queryBooksAuthors(limit, offset, sort, order)
|
authors, err := app.queryBooksAuthors(limit, offset, sort, order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, 0, false, err
|
||||||
}
|
}
|
||||||
tags, err := app.queryBooksTags(limit, offset, sort, order)
|
tags, err := app.queryBooksTags(limit, offset, sort, order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, 0, false, err
|
||||||
}
|
}
|
||||||
assignAuthorsTagsBooks(books, authors, tags)
|
assignAuthorsTagsBooks(books, authors, tags)
|
||||||
return books, more, nil
|
return books, 0, more, nil
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package bouquins
|
package bouquins
|
||||||
|
|
||||||
|
import "log"
|
||||||
|
|
||||||
// MERGE SUB QUERIES //
|
// MERGE SUB QUERIES //
|
||||||
|
|
||||||
func assignAuthorsSeries(series []*SeriesAdv, authors map[int64][]*Author) {
|
func assignAuthorsSeries(series []*SeriesAdv, authors map[int64][]*Author) {
|
||||||
@ -10,6 +12,49 @@ func assignAuthorsSeries(series []*SeriesAdv, authors map[int64][]*Author) {
|
|||||||
|
|
||||||
// SUB QUERIES //
|
// 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) {
|
func (app *Bouquins) querySeriesList(limit, offset int, sort, order string) ([]*SeriesAdv, bool, error) {
|
||||||
series := make([]*SeriesAdv, 0, limit)
|
series := make([]*SeriesAdv, 0, limit)
|
||||||
stmt, err := app.psSortSeries(SERIES, sort, order)
|
stmt, err := app.psSortSeries(SERIES, sort, order)
|
||||||
@ -139,15 +184,20 @@ func (app *Bouquins) SeriesFull(id int64) (*SeriesFull, error) {
|
|||||||
return series, nil
|
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)
|
series, more, err := app.querySeriesList(limit, offset, sort, order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, 0, false, err
|
||||||
}
|
}
|
||||||
authors, err := app.querySeriesListAuthors(limit, offset, sort, order)
|
authors, err := app.querySeriesListAuthors(limit, offset, sort, order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, 0, false, err
|
||||||
}
|
}
|
||||||
assignAuthorsSeries(series, authors)
|
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_BOOKS, app.BooksPage)
|
||||||
http.HandleFunc(URL_AUTHORS, app.AuthorsPage)
|
http.HandleFunc(URL_AUTHORS, app.AuthorsPage)
|
||||||
http.HandleFunc(URL_SERIES, app.SeriesPage)
|
http.HandleFunc(URL_SERIES, app.SeriesPage)
|
||||||
|
http.HandleFunc(URL_SEARCH, app.SearchPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
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">
|
<div class="container">
|
||||||
<ul class="nav navbar-nav">
|
<ul class="nav navbar-nav">
|
||||||
<li class="active"><a href="/">Accueil</a></li>
|
<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>
|
<li><a href="#">A propos</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<form class="navbar-form navbar-right" role="search" method="get" action="search.html">
|
<form class="navbar-form navbar-right" role="search" method="get" action="search.html">
|
||||||
|
@ -13,30 +13,5 @@
|
|||||||
<paginate :page="page" :more="more"></paginate>
|
<paginate :page="page" :more="more"></paginate>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/x-template" id="results-template">
|
{{ template "components.html" }}
|
||||||
<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 "footer.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