Report generator is now complete. Fixed some bugs with report CSS/JS.

This commit is contained in:
WrinklyNinja
2013-05-13 00:44:20 +01:00
parent b49e486840
commit a261ca9de6
7 changed files with 195 additions and 92 deletions
+6 -32
View File
@@ -60,40 +60,13 @@ BOSSv3 won't have a command line interface, to simplify things. It also allows B
- [ ] Make validity checks non-fatal.
- [ ] Add a massive "RUN THE GAME LAUNCHER IF YOUR GAME IS NOT DETECTED" message somewhere.
- [ ] Generalise Total Conversion support, so that any TC for any of the supported 'base' games can be used with BOSS.
- [ ] Write XHTML report generator.
- [x] Write XHTML report generator.
- [ ] Write report CSS.
- [ ] Write report Javascript.
## BOSS Report
Once BOSS has applied a load order, it will display a report, similar to the BOSS Log, that covers the following:
* The number of messages, broken down into the total, the number of warnings and the number of errors.
* The masterlist version used.
* Any parser or validity checker errors encountered.
* If there were any changes since BOSS was last run.
* The new load order.
* The messages attached to plugins, version numbers read, CRCs calculated and whether or not each plugin is active.
The current BOSS Log has a number of useful filters. Depending on the UI used for the report, some or all of these filters may be useful for implementation:
* Hide versions
* Hide whether a plugin is active or not.
* Hide CRCs
* Hide "say" messages.
* Hide Bash Tag suggestions.
* Hide all messages.
* Hide "do not clean" messages.
* Hide inactive plugins.
* Hide messageless plugins.
The report data will be stored in a YAML file, which could use an extended form of the metadata file syntax, with the following additions:
* Plugins get 'version' (string) and 'crc' (int) nodes added to them.
* A new top-level node that holds a 'masterlist version' (string), a 'masterlist updated' (boolean) and a 'report changed' (boolean) node.
There are a whole slew of options when it comes to how the report UI will be implemented. Using a native interface isn't a realistic option because it's so difficult to get anything that isn't very limited implemented, and the report is mostly static so wouldn't take advantage of native controls anyway. A HTML/CSS/JS approach is therefore preferable. The neatest way to do this would be to write the HTML as XHTML using PugiXML, then include external CSS & JS that gets distributed with BOSS.
- [ ] Double-check ghosted file support.
- [ ] Add a setting that lets users choose whether to use the viewer window or their own browser when viewing BOSS's report.
- [ ] Somehow implement filter settings memory for the BOSS report.
- [ ] Implement checking of details part of report against previous report.
## Optimisation
@@ -133,6 +106,7 @@ BOSS uses the following libraries:
* Boost
* Libespm
* Libloadorder
* PugiXML
* wxWidgets
* yaml-cpp
+11 -10
View File
@@ -25,17 +25,13 @@ if (!document.getElementsByClassName) {
function showElement(element) {
if (element != null) {
if (element.className.indexOf('hidden') != -1) {
element.className = element.className.replace(' hidden', '');
} else if (element.className.indexOf('visible') == -1) {
element.className += ' visible';
element.className = element.className.replace('hidden', '');
}
}
}
function hideElement(element) {
if (element != null) {
if (element.className.indexOf('visible') != -1) {
element.className = element.className.replace('visible', '');
} else if (element.className.indexOf('hidden') == -1) {
if (element.className.indexOf('hidden') == -1) {
element.className += ' hidden';
}
}
@@ -58,7 +54,7 @@ function toggleDisplayCSS(evt) {
}
} else {
for (var i = 0, z = e.length; i < z; i++) {
e[i].className = e[i].className.replace(' hidden', '');
e[i].className = e[i].className.replace('hidden', '');
}
}
}
@@ -68,15 +64,20 @@ function toggleFilters(evt) {
evt.target.className += ' current';
} else {
hideElement(filters);
evt.target.className = evt.target.className.replace(' current', '');
evt.target.className = evt.target.className.replace('current', '');
}
}
function showSection(evt) {
hideElement(document.querySelector('#summary,#plugins'));
elemArr = document.getElementById('nav').querySelectorAll('.button[data-section]');
var i = elemArr.length - 1;
while (i > -1) {
hideElement(document.getElementById(elemArr[i].getAttribute('data-section')));
i--;
}
showElement(document.getElementById(evt.target.getAttribute('data-section')));
var elem = document.querySelector('#nav div.current[data-section]');
if (elem != null) {
elem.className = elem.className.replace(' current', '');
elem.className = elem.className.replace('current', '');
}
if (evt.target.className.indexOf('current') == -1) {
evt.target.className += ' current';
+4 -4
View File
@@ -88,12 +88,12 @@ table tr:last-child td:last-child {
position:fixed;
right:0;
top:32px;
height:0;
}
#filters.visible {
height:auto;
display:block;
box-shadow:0 0 3px 1px rgba(0,0,0,0.5);
}
#filters.hidden {
display:none;
}
#filters label {
display:block;
padding:0.2em 0.5em;
+120 -34
View File
@@ -34,7 +34,14 @@
namespace boss {
void WriteMessage(pugi::xml_node& listItem, std::string content) {
void WriteMessage(pugi::xml_node& listItem, std::string type, std::string content) {
if (type == "say")
content = "Note: " + content;
else if (type == "warn")
content = "Warning: " + content;
else
content = "Error: " + content;
size_t pos1f = content.find("\"file:");
size_t pos1h = content.find("\"http");
@@ -136,6 +143,28 @@ namespace boss {
div.text().set("Filters");
}
void AppendMessages(pugi::xml_node& parent, const std::list<Message>& messages, int& warnNo, int& errorNo) {
if (!messages.empty()) {
pugi::xml_node list = parent.append_child();
list.set_name("ul");
for (std::list<Message>::const_iterator it=messages.begin(), endit=messages.end(); it != endit; ++it) {
pugi::xml_node li = list.append_child();
li.set_name("li");
li.append_attribute("class").set_value(it->Type().c_str());
//Turn any urls into hyperlinks.
WriteMessage(li, it->Type(), it->Content());
if (boost::iequals(it->Type(), "warn"))
++warnNo;
else if (boost::iequals(it->Type(), "error"))
++errorNo;
}
}
}
void AppendSummary(pugi::xml_node& main,
bool hasChanged,
const std::string& masterlistVersion,
@@ -143,8 +172,7 @@ namespace boss {
int messageNo,
int warnNo,
int errorNo,
const std::list<Message>& messages,
const std::string& language) {
const std::list<Message>& messages) {
pugi::xml_node summary = main.append_child();
summary.set_name("div");
@@ -187,6 +215,16 @@ namespace boss {
else
cell.text().set("Disabled");
if (hasChanged) {
pugi::xml_node note = summary.append_child();
note.set_name("div");
note.append_attribute("id").set_value("noChanges");
note.text().set("No change in details since last run.");
}
AppendMessages(summary, messages, warnNo, errorNo);
messageNo += messages.size();
row = table.append_child();
row.set_name("tr");
cell = row.append_child();
@@ -213,43 +251,84 @@ namespace boss {
cell = row.append_child();
cell.set_name("td");
cell.text().set(IntToString(errorNo).c_str());
}
if (hasChanged) {
pugi::xml_node note = summary.append_child();
note.set_name("div");
note.append_attribute("id").set_value("noChanges");
note.text().set("No changes since last run.");
}
void AppendDetails(pugi::xml_node& main, const std::list<Plugin>& plugins, int& messageNo, int& warnNo, int& errorNo) {
if (!messages.empty()) {
pugi::xml_node list = summary.append_child();
list.set_name("ul");
pugi::xml_node details = main.append_child();
details.set_name("div");
details.append_attribute("id").set_value("plugins");
details.append_attribute("class").set_value("hidden");
for (std::list<Message>::const_iterator it=messages.begin(), endit=messages.end(); it != endit; ++it) {
//Need to weed out different language messages.
if (it->Language().empty() || boost::iequals(it->Language(), language)) {
if (!plugins.empty()) {
pugi::xml_node li = list.append_child();
li.set_name("li");
li.append_attribute("class").set_value(it->Type().c_str());
details = details.append_child();
details.set_name("ul");
//Turn any urls into hyperlinks.
WriteMessage(li, it->Content());
for (std::list<Plugin>::const_iterator it=plugins.begin(), endit=plugins.end(); it != endit; ++it) {
pugi::xml_node plugin = details.append_child();
plugin.set_name("li");
pugi::xml_node node = plugin.append_child();
node.set_name("span");
node.append_attribute("class").set_value("mod");
node.text().set(it->Name().c_str());
if (!it->Version().empty()) {
node = plugin.append_child();
node.set_name("span");
node.append_attribute("class").set_value("version");
node.text().set(("Version: " + it->Version()).c_str());
}
if (it->Crc() > 0) {
node = plugin.append_child();
node.set_name("span");
node.append_attribute("class").set_value("crc");
node.text().set(("CRC: " + IntToHexString(it->Crc())).c_str());
}
if (it->IsActive()) {
node = plugin.append_child();
node.set_name("span");
node.append_attribute("class").set_value("active");
node.text().set("Active");
}
std::list<Message> messages = it->Messages();
std::set<Tag> tags = it->Tags();
if (!tags.empty()) {
std::string add, remove, content;
for (std::set<Tag>::const_iterator jt=tags.begin(), endjt=tags.end(); jt != endjt; ++jt) {
if (jt->IsAddition())
add += ", " + jt->Name();
else
remove += ", " + jt->Name();
}
if (!add.empty())
content += "Bash Tags suggested for addition are " + add.substr(2) + ". ";
if (!remove.empty())
content += "Bash Tags suggested for removal are " + remove.substr(2) + ". ";
messages.push_back(Message("say", content));
}
AppendMessages(plugin, messages, warnNo, errorNo);
messageNo += messages.size();
}
}
}
void AppendMain(pugi::xml_node& body,
bool hasChanged,
const std::string& masterlistVersion,
bool masterlistUpdateEnabled,
int messageNo,
int warnNo,
int errorNo,
const std::list<Message>& messages,
const std::string& language) {
const std::list<Plugin>& plugins,
int& pluginMessageNo
) {
pugi::xml_node main = body.append_child();
main.set_name("div");
@@ -263,7 +342,11 @@ namespace boss {
div.set_name("div");
div.text().set("The BOSS Report requires Javascript to be enabled in order to function.");
AppendSummary(main, hasChanged, masterlistVersion, masterlistUpdateEnabled, messageNo, warnNo, errorNo, messages, language);
int messageNo=0, warnNo=0, errorNo=0;
AppendDetails(main, plugins, messageNo, warnNo, errorNo);
pluginMessageNo = messageNo;
AppendSummary(main, hasChanged, masterlistVersion, masterlistUpdateEnabled, messageNo, warnNo, errorNo, messages);
}
void AppendFilters(pugi::xml_node& body, int messageNo, int pluginNo) {
@@ -271,6 +354,7 @@ namespace boss {
pugi::xml_node filters = body.append_child();
filters.set_name("div");
filters.append_attribute("id").set_value("filters");
filters.append_attribute("class").set_value("hidden");
pugi::xml_node label, input;
@@ -384,7 +468,12 @@ namespace boss {
}
void GenerateReport(const std::string& file) {
void GenerateReport(const std::string& file,
const std::list<Message>& messages,
const std::list<Plugin>& plugins,
const std::string& masterlistVersion,
const bool masterlistUpdateEnabled,
const bool hasChanged) {
pugi::xml_document doc;
@@ -395,18 +484,15 @@ namespace boss {
AppendNav(body);
std::list<Message> messages;
messages.push_back(Message("say", "This is a test message with a link to \"http://www.google.com Google\" in it."));
AppendMain(body, true, "4308 (2013-06-06)", true, 500, 4, 2, messages, "eng");
int messageNo=0;
AppendMain(body, hasChanged, masterlistVersion, masterlistUpdateEnabled, messages, plugins, messageNo);
AppendFilters(body, 5, 5);
AppendFilters(body, messageNo, plugins.size());
AppendScripts(body);
if (!doc.save_file(file.c_str(), "\t", pugi::format_default | pugi::format_no_declaration))
throw std::runtime_error("Could not write XML file.");
throw boss::error(ERROR_PATH_WRITE_FAIL, "Could not write BOSS report.");
}
+8 -2
View File
@@ -340,6 +340,7 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) {
string filename = it->path().filename().string();
out << "Reading plugin: " << filename << endl;
boss::Plugin plugin(_game, filename);
//boss::Plugin plugin(filename);
plugins.push_back(plugin);
progDia->Pulse();
@@ -436,7 +437,7 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) {
for (list<boss::Plugin>::iterator it=plugins.begin(), endIt=plugins.end(); it != endIt; ++it) {
try {
it->EvalAllConditions(_game);
it->EvalAllConditions(_game, _settings["Language"].as<string>());
} catch (boss::error& e) {
//LOG_ERROR("Error: %s", e.what());
wxMessageBox(
@@ -515,7 +516,12 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) {
out << "Writing results file..." << endl;
GenerateReport(_game.ReportPath().string());
GenerateReport(_game.ReportPath().string(),
messages,
plugins,
"4030 (2020-13-13)",
_settings["Update Masterlist"].as<bool>(),
true);
progDia->Pulse();
+38 -8
View File
@@ -154,6 +154,13 @@ namespace boss {
return (boost::iequals(Type(), rhs.Type()) && boost::iequals(Content(), rhs.Content()));
}
bool Message::EvalCondition(boss::Game& game, const std::string& lang) const {
if (language.empty() || boost::iequals(language, lang))
return ConditionalData::EvalCondition(game);
else
return false;
}
std::string Message::Type() const {
return type;
}
@@ -236,12 +243,12 @@ namespace boss {
return Data();
}
Plugin::Plugin() : enabled(true), priority(0), isMaster(false) {}
Plugin::Plugin(const std::string& n) : name(n), enabled(true), priority(0), isMaster(false) {}
Plugin::Plugin() : enabled(true), priority(0), isMaster(false), isActive(false), crc(0) {}
Plugin::Plugin(const std::string& n) : name(n), enabled(true), priority(0), isMaster(false), isActive(false), crc(0) {}
Plugin::Plugin(const boss::Game& game, const std::string& n)
Plugin::Plugin(boss::Game& game, const std::string& n)
: name(n), enabled(true), priority(0) {
/*
// Get data from file contents using libespm. Assumes libespm has already been initialised.
boost::filesystem::path filepath = game.DataPath() / n;
espm::File file(filepath.string(), game.espm_settings, false, false);
@@ -281,7 +288,9 @@ namespace boss {
size_t pos1 = text.find("{{BASH:");
if (pos1 == string::npos)
continue;
break;
pos1 += 7;
size_t pos2 = text.find("}}", pos1);
if (pos2 == string::npos)
@@ -298,7 +307,20 @@ namespace boss {
break;
}
}*/
}
//Calculate the plugin's CRC, and add it to the hashset.
boost::unordered_map<string,uint32_t>::iterator it = game.crcCache.find(boost::to_lower_copy(name));
if (it != game.crcCache.end())
crc = it->second;
else {
crc = GetCrc32(game.DataPath() / name);
game.crcCache.emplace(boost::to_lower_copy(name), crc);
}
//Check if plugin is active.
isActive = game.IsActive(name);
}
void Plugin::Merge(const Plugin& plugin, bool ifDisabled) {
@@ -437,7 +459,7 @@ namespace boss {
tags = t;
}
void Plugin::EvalAllConditions(boss::Game& game) {
void Plugin::EvalAllConditions(boss::Game& game, const std::string& language) {
for (set<File>::iterator it = loadAfter.begin(); it != loadAfter.end();) {
if (!it->EvalCondition(game))
loadAfter.erase(it++);
@@ -460,7 +482,7 @@ namespace boss {
}
for (list<Message>::iterator it = messages.begin(); it != messages.end();) {
if (!it->EvalCondition(game))
if (!it->EvalCondition(game, language))
it = messages.erase(it);
else
++it;
@@ -559,6 +581,14 @@ namespace boss {
return version;
}
uint32_t Plugin::Crc() const {
return crc;
}
bool Plugin::IsActive() const {
return isActive;
}
bool Plugin::MustLoadAfter(const Plugin& plugin) const {
for (vector<string>::const_iterator it=masters.begin(), endIt=masters.end(); it != endIt; ++it) {
if (boost::iequals(*it, plugin.Name()))
+8 -2
View File
@@ -85,6 +85,8 @@ namespace boss {
bool operator < (const Message& rhs) const;
bool operator == (const Message& rhs) const;
bool EvalCondition(boss::Game& game, const std::string& language) const;
std::string Type() const;
std::string Language() const;
std::string Content() const;
@@ -129,7 +131,7 @@ namespace boss {
public:
Plugin();
Plugin(const std::string& name);
Plugin(const boss::Game& game, const std::string& name);
Plugin(boss::Game& game, const std::string& name);
void Merge(const Plugin& plugin, bool ifdDisabled = false);
Plugin DiffMetadata(const Plugin& plugin) const;
@@ -147,6 +149,8 @@ namespace boss {
std::vector<std::string> Masters() const;
bool IsMaster() const; //Checks master bit flag.
std::string Version() const;
uint32_t Crc() const;
bool IsActive() const;
void Name(const std::string& name);
void Enabled(const bool enabled);
@@ -157,7 +161,7 @@ namespace boss {
void Messages(const std::list<Message>& messages);
void Tags(const std::set<Tag>& tags);
void EvalAllConditions(boss::Game& game);
void EvalAllConditions(boss::Game& game, const std::string& language);
bool HasNameOnly() const;
bool IsRegexPlugin() const;
@@ -191,6 +195,8 @@ namespace boss {
std::set<FormID> formIDs;
std::string version; //Obtained from description field.
bool isMaster;
bool isActive;
uint32_t crc;
};
bool operator == (const File& lhs, const Plugin& rhs);