diff --git a/README.md b/README.md index a3ce393..74eb720 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ Bouquins in Go ## TODO -* search +* search (table results: authors, tags...) +* search in header * About * translations * tests diff --git a/assets/js/search.js b/assets/js/search.js index cc1d3f8..ee1c8cb 100644 --- a/assets/js/search.js +++ b/assets/js/search.js @@ -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; } + }, + 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(); } -}) +}); diff --git a/bouquins/bouquins.go b/bouquins/bouquins.go index a569ac9..715912f 100644 --- a/bouquins/bouquins.go +++ b/bouquins/bouquins.go @@ -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"` + 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) + } +} diff --git a/bouquins/db.go b/bouquins/db.go index 5ea609c..a1c692b 100644 --- a/bouquins/db.go +++ b/bouquins/db.go @@ -33,11 +33,14 @@ const ( STMT_AUTHORS_SEARCH = "SELECT id, name FROM authors WHERE " STMT_SEARCH_TERM_AUTHOR = " sort like ? " - STMT_PAGE = " LIMIT ? OFFSET ?" - STMT_WHERE = " WHERE " - STMT_BOOL_AND = " AND " - STMT_BOOL_OR = " OR " - STMT_SEARCH_ORDER = " ORDER BY books.sort" + STMT_PAGE = " LIMIT ? OFFSET ?" + STMT_WHERE = " WHERE " + STMT_BOOL_AND = " AND " + STMT_BOOL_OR = " OR " + + 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, diff --git a/bouquins/dbauthors.go b/bouquins/dbauthors.go index fb287c4..9e50c90 100644 --- a/bouquins/dbauthors.go +++ b/bouquins/dbauthors.go @@ -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) { diff --git a/bouquins/dbbooks.go b/bouquins/dbbooks.go index 7207af1..520cb07 100644 --- a/bouquins/dbbooks.go +++ b/bouquins/dbbooks.go @@ -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 } diff --git a/bouquins/dbseries.go b/bouquins/dbseries.go index 6408f65..0ae53da 100644 --- a/bouquins/dbseries.go +++ b/bouquins/dbseries.go @@ -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 } diff --git a/main.go b/main.go index 8d984cc..ffc058c 100644 --- a/main.go +++ b/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() { diff --git a/templates/components.html b/templates/components.html new file mode 100644 index 0000000..a4b8943 --- /dev/null +++ b/templates/components.html @@ -0,0 +1,38 @@ + + + diff --git a/templates/header.html b/templates/header.html index 3e56f53..90bab5a 100644 --- a/templates/header.html +++ b/templates/header.html @@ -17,7 +17,7 @@