diff options
author | Karen Arutyunov <karen@codesynthesis.com> | 2024-08-06 22:03:31 +0300 |
---|---|---|
committer | Karen Arutyunov <karen@codesynthesis.com> | 2024-08-07 19:19:22 +0300 |
commit | 443088f6093d3420212be0e1af3b9e802dca9362 (patch) | |
tree | b1ec3b0c62ee0b8d66b0cbf21e21d68ae0d4f806 /mod/mod-advanced-search.cxx | |
parent | 7db53790ca2d2c004bfd00b503eca59a8d084870 (diff) |
Add support for advanced package search
Diffstat (limited to 'mod/mod-advanced-search.cxx')
-rw-r--r-- | mod/mod-advanced-search.cxx | 342 |
1 files changed, 342 insertions, 0 deletions
diff --git a/mod/mod-advanced-search.cxx b/mod/mod-advanced-search.cxx new file mode 100644 index 0000000..5de3b9a --- /dev/null +++ b/mod/mod-advanced-search.cxx @@ -0,0 +1,342 @@ +// file : mod/mod-advanced-search.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <mod/mod-advanced-search.hxx> + +#include <libstudxml/serializer.hxx> + +#include <odb/database.hxx> +#include <odb/transaction.hxx> + +#include <web/server/module.hxx> +#include <web/server/mime-url-encoding.hxx> + +#include <web/xhtml/serialization.hxx> + +#include <libbrep/package.hxx> +#include <libbrep/package-odb.hxx> + +#include <mod/page.hxx> +#include <mod/utility.hxx> // wildcard_to_similar_to_pattern() +#include <mod/module-options.hxx> + +using namespace std; +using namespace butl; +using namespace web; +using namespace odb::core; +using namespace brep::cli; + +// While currently the user-defined copy constructor is not required (we don't +// need to deep copy nullptr's), it is a good idea to keep the placeholder +// ready for less trivial cases. +// +brep::advanced_search:: +advanced_search (const advanced_search& r) + : database_module (r), + options_ (r.initialized_ ? r.options_ : nullptr) +{ +} + +void brep::advanced_search:: +init (scanner& s) +{ + HANDLER_DIAG; + + options_ = make_shared<options::advanced_search> ( + s, unknown_mode::fail, unknown_mode::fail); + + database_module::init (*options_, options_->package_db_retry ()); + + if (options_->root ().empty ()) + options_->root (dir_path ("/")); +} + +template <typename T, typename C> +static inline query<T> +match (const C qc, const string& pattern) +{ + return qc + + "SIMILAR TO" + + query<T>::_val (brep::wildcard_to_similar_to_pattern (pattern)); +} + +template <typename T> +static inline query<T> +package_query (const brep::params::advanced_search& params) +{ + using namespace brep; + using query = query<T>; + + query q (query::internal_repository.canonical_name.is_not_null ()); + + // Note that there is no error reported if the filter parameters parsing + // fails. Instead, it is considered that no package builds match such a + // query. + // + try + { + // Package name. + // + if (!params.name ().empty ()) + q = q && match<T> (query::id.name, params.name ()); + + // Package version. + // + if (!params.version ().empty () && params.version () != "*") + { + // May throw invalid_argument. + // + version v (params.version (), version::none); + + q = q && compare_version_eq (query::id.version, + canonical_version (v), + v.revision.has_value ()); + } + + // Package project. + // + if (!params.project ().empty ()) + q = q && match<T> (query::project, params.project ()); + + // Reviews. + // + const string& rs (params.reviews ()); + + if (rs != "*") + { + if (rs == "reviewed") + q = q && query::reviews.pass.is_not_null (); + else if (rs == "unreviewed") + q = q && query::reviews.pass.is_null (); + else + throw invalid_argument (""); + } + } + catch (const invalid_argument&) + { + return query (false); + } + + return q; +} + +static const vector<pair<string, string>> reviews ({ + {"*", "*"}, + {"reviewed", "reviewed"}, + {"unreviewed", "unreviewed"}}); + +bool brep::advanced_search:: +handle (request& rq, response& rs) +{ + using namespace web::xhtml; + + HANDLER_DIAG; + + // Note that while we could potentially support the multi-tenant mode, that + // would require to invent the package/tenant view to filter out the private + // tenants from the search. This doesn't look of much use at the moment. + // Thus, let's keep it simple for now and just respond with the 501 status + // code (not implemented) if such a mode is detected. + // + if (!tenant.empty ()) + throw invalid_request (501, "not implemented"); + + const size_t res_page (options_->search_page_entries ()); + const dir_path& root (options_->root ()); + + params::advanced_search params; + + try + { + name_value_scanner s (rq.parameters (8 * 1024)); + params = params::advanced_search (s, + unknown_mode::fail, + unknown_mode::fail); + } + catch (const cli::exception& e) + { + throw invalid_request (400, e.what ()); + } + + const char* title ("Advanced Package Search"); + + xml::serializer s (rs.content (), title); + + s << HTML + << HEAD + << TITLE << title << ~TITLE + << CSS_LINKS (path ("advanced-search.css"), root) + << ~HEAD + << BODY + << DIV_HEADER (options_->logo (), options_->menu (), root, tenant) + << DIV(ID="content"); + + transaction t (package_db_->begin ()); + + size_t count ( + package_db_->query_value<package_count> ( + package_query<package_count> (params))); + + // Print the package builds filter form on the first page only. + // + size_t page (params.page ()); + + if (page == 0) + { + // The 'action' attribute is optional in HTML5. While the standard + // doesn't specify browser behavior explicitly for the case the + // attribute is omitted, the only reasonable behavior is to default it + // to the current document URL. + // + s << FORM + << TABLE(ID="filter", CLASS="proplist") + << TBODY + << TR_INPUT ("name", "advanced-search", params.name (), "*", true) + << TR_INPUT ("version", "pv", params.version (), "*") + << TR_INPUT ("project", "pr", params.project (), "*"); + + if (options_->reviews_url_specified ()) + s << TR_SELECT ("reviews", "rv", params.reviews (), reviews); + + s << ~TBODY + << ~TABLE + << TABLE(CLASS="form-table") + << TBODY + << TR + << TD(ID="package-version-count") + << DIV_COUNTER (count, "Package Version", "Package Versions") + << ~TD + << TD(ID="filter-btn") + << *INPUT(TYPE="submit", VALUE="Filter") + << ~TD + << ~TR + << ~TBODY + << ~TABLE + << ~FORM; + } + else + s << DIV_COUNTER (count, "Package Version", "Package Versions"); + + using query = query<package>; + + // Note that we query an additional package version which we will not + // display, but will use to check if it belongs to the same package and/or + // project as the last displayed package version. If that's the case we will + // display the '...' mark(s) at the end of the page, indicating that there a + // more package versions from this package/project on the next page(s). + // + query q (package_query<package> (params) + + "ORDER BY tenant, project, name, version_epoch DESC, " + "version_canonical_upstream DESC, version_canonical_release DESC, " + "version_revision DESC" + + "OFFSET" + to_string (page * res_page) + + "LIMIT" + to_string (res_page + 1)); + + package_name prj; + package_name pkg; + size_t n (0); + + for (package& p: package_db_->query<package> (q)) + { + if (!p.id.tenant.empty ()) + throw invalid_request (501, "not implemented"); + + if (n++ == res_page) + { + if (p.project == prj) + { + if (p.name == pkg) + s << DIV(ID="package-break") << "..." << ~DIV; + + s << DIV(ID="project-break") << "..." << ~DIV; + } + + break; + } + + if (p.project != prj) + { + prj = move (p.project); + pkg = package_name (); + + s << TABLE(CLASS="proplist project") + << TBODY + << TR_VALUE ("project", prj.string ()) + << ~TBODY + << ~TABLE; + } + + if (p.name != pkg) + { + pkg = move (p.name); + + s << TABLE(CLASS="proplist package") + << TBODY + << TR_NAME (pkg, root, p.tenant) + << TR_SUMMARY (p.summary) + << TR_LICENSE (p.license_alternatives) + << ~TBODY + << ~TABLE; + } + + s << TABLE(CLASS="proplist version") + << TBODY + << TR_VERSION (pkg, p.version, root, tenant, p.upstream_version); + + assert (p.internal ()); + + const repository_location& rl (p.internal_repository.load ()->location); + + s << TR_REPOSITORY (rl, root, tenant) + << TR_DEPENDS (p.dependencies, root, tenant) + << TR_REQUIRES (p.requirements); + + if (options_->reviews_url_specified ()) + { + package_db_->load (p, p.reviews_section); + + s << TR_REVIEWS_SUMMARY (p.reviews, options_->reviews_url ()); + } + + s << ~TBODY + << ~TABLE; + } + + t.commit (); + + string u (root.string () + "?advanced-search"); + + if (!params.name ().empty ()) + { + u += '='; + u += mime_url_encode (params.name ()); + } + + auto add_filter = [&u] (const char* pn, + const string& pv, + const char* def = "") + { + if (pv != def) + { + u += '&'; + u += pn; + u += '='; + u += mime_url_encode (pv); + } + }; + + add_filter ("pv", params.version ()); + add_filter ("pr", params.project ()); + add_filter ("rv", params.reviews (), "*"); + + s << DIV_PAGER (page, + count, + res_page, + options_->search_pages (), + u) + << ~DIV + << ~BODY + << ~HTML; + + return true; +} |