diff --git a/rank_hugo_themes.py b/rank_hugo_themes.py index 0d91ac3..a4a6ae3 100755 --- a/rank_hugo_themes.py +++ b/rank_hugo_themes.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # rank_hugo_themes.py +from jinja2 import Environment, FileSystemLoader import re import toml from calendar import timegm @@ -17,6 +18,9 @@ from sqlalchemy.orm import deferred, sessionmaker engine = create_engine('sqlite:///hugothemes.db', echo=False) Base = declarative_base() +file_loader = FileSystemLoader('templates') +env = Environment(loader=file_loader) +template = env.get_template('base.html') class Tags(Base): @@ -72,17 +76,19 @@ class Hugothemes(Base): tags_list = Column(TEXT) num_tags = Column(Integer) default_branch = Column(TEXT) + features_list = Column(TEXT) + num_features = Column(Integer) def __repr__(self): repr_string = "<(name = '%s', ETag = '%s', url = '%s', commit_sha = '%s', commit_date = '%s'" repr_string += ", commit_date_in_seconds = '%s', repo_ETag = '%s', stargazers_count = '%s', themes_toml_ETag = '%s'" - repr_string += ", themes_toml_content = '%s', tags_list = '%s', num_tags = '%s', default_branch = '%s')>" + repr_string += ", themes_toml_content = '%s', tags_list = '%s', num_tags = '%s', default_branch = '%s', features_list = '%s', num_features = '%s')>" repr_values = ( self.name, self.ETag, self.url, self.commit_sha, self.commit_date, self.commit_date_in_seconds, self.repo_ETag, self.stargazers_count, self.themes_toml_ETag, self.themes_toml_content, self.tags_list, - self.num_tags, self.default_branch + self.num_tags, self.default_branch, self.features_list, self.num_features, ) return repr_string % repr_values @@ -384,9 +390,51 @@ def coalesce_themes(): session.commit() +def update_features_list_for_each_hugo_themes(): + session = sessionmaker(bind=engine)() + themes = [theme[0] for theme in session.query(Hugothemes.name).all()] + match = re.compile(r'\s(\d+\.\d+\.\d+)\s') + for hugo_theme in themes: + theme = session.query(Hugothemes).filter_by(name=hugo_theme).one() + if theme.themes_toml_content is not None: + content = b64decode(theme.themes_toml_content).decode('utf-8') + theme_toml = toml.loads(match.sub(r'"\1"\n', content)) + if 'features' in theme_toml: + if len(theme_toml['features']) > 0: + theme_features = [feature.lower() for feature in theme_toml['features'] if len(feature) > 0] + if theme.num_features != len(theme_features): theme.num_features = len(theme_features) + if theme.num_features > 0: + if theme.features_list != str(theme_features): theme.features_list = str(theme_features) + else: + if theme.features_list is not None: theme.features_list = None + else: + if theme.features_list is not None: theme.features_list = None + if theme.num_features != 0: theme.num_features = 0 + else: + if theme.features_list is not None: theme.features_list = None + if theme.num_features != 0: theme.num_features = 0 + else: + if theme.features_list is not None: theme.features_list = None + if theme.num_features != 0: theme.num_features = 0 + session.commit() + + +def get_corrected_tags(tags): + result = [] + correct = True + for tag in tags: + if (len(tag) > 50): correct = False + if not correct: + for tag in tags: + result += [x.lstrip() for x in tag.split(',')] + return result + else: + return tags + + def update_tags_list_for_each_hugo_themes(): session = sessionmaker(bind=engine)() - themes = [theme[0] for theme in session.query(Hugothemes.name).filter(Hugothemes.name != THEMESLISTREPO).all()] + themes = [theme[0] for theme in session.query(Hugothemes.name).all()] match = re.compile(r'\s(\d+\.\d+\.\d+)\s') for hugo_theme in themes: theme = session.query(Hugothemes).filter_by(name=hugo_theme).one() @@ -398,7 +446,8 @@ def update_tags_list_for_each_hugo_themes(): theme_toml = toml.loads(match.sub(r'"\1"\n', content)) if 'tags' in theme_toml: if len(theme_toml['tags']) > 0: - theme_tags = [tag.lower() for tag in theme_toml['tags'] if len(tag) > 0] + corrected_tags = get_corrected_tags(theme_toml['tags']) + theme_tags = [tag.lower() for tag in corrected_tags if len(tag) > 0] if theme.num_tags != len(theme_tags): theme.num_tags = len(theme_tags) if theme.num_tags > 0: if theme.tags_list != str(theme_tags): theme.tags_list = str(theme_tags) @@ -519,7 +568,33 @@ def write_reports(): by_date.close() +def generate_report(): + session = sessionmaker(bind=engine)() + hugo_themes = [ + { + 'name': theme.name, + 'commit': theme.commit_sha[0:6], + 'date': theme.commit_date[0:10], + 'date_in_seconds': theme.commit_date_in_seconds, + 'url': f'https://{theme.url}', + 'short_name': theme.name.split('/')[1], + 'num_stars': theme.stargazers_count, + 'tags': literal_eval(theme.tags_list) if theme.tags_list is not None else [], + 'features': literal_eval(theme.features_list) if theme.features_list is not None else [], + } for theme in session.query(Hugothemes).all() + ] + output = template.render(themes=hugo_themes) + index_page = open('hugo-themes-report/hugo-themes-report.html', 'w') + index_page.write(output) + index_page.close() + + if __name__ == "__main__": + ''' + update_tags_list_for_each_hugo_themes() + update_features_list_for_each_hugo_themes() + generate_report() + ''' get_hugo_themes_list() if len(THEMESLIST) > 300: clean_up() @@ -534,5 +609,7 @@ if __name__ == "__main__": get_theme_dot_toml_for_each_hugo_themes_from_gitlab() coalesce_themes() update_tags_list_for_each_hugo_themes() + update_features_list_for_each_hugo_themes() update_tag_table() - write_reports() + # write_reports() + generate_report() diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..1d646e3 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,28 @@ + + + + >Hugo Themes Report + + + + + +

+ Hugo Themes Report +

+ +
+
+
+
+ + + diff --git a/templates/css/main.css b/templates/css/main.css new file mode 100644 index 0000000..57c893d --- /dev/null +++ b/templates/css/main.css @@ -0,0 +1,78 @@ +body { + font-family: sans-serif; + width: 1200px; + max-width: 98%; + margin: 0 auto 0 auto; +} + +#selection-menu { + display: none; +} + +#title { + text-align: center; +} + +.collapsible { + background-color: #eee; + color: #444; + cursor: pointer; + padding: 1.1rem; + width: 100%; + border: none; + text-align: left; + outline: none; + font-size: 1.1rem; +} + +.active, .collapsible:hover { + background-color: #ccc; +} + +.content { + padding: 0 1.1rem; + display: none; + overflow: hidden; + background-color: #f1f1f1; +} + +.collapsible:after { + content: '\02795'; + font-size: .8rem; + color: white; + float: right; + margin-left: .4rem; +} + +.active:after { + content: '\2796'; +} + +#resultsTable { + width: 100%; + min-width: 500px; + border-spacing: 0; + padding: 2px; +} + +#resultsTable tr{ + height: 2rem; +} + +#resultsTable tr:nth-child(even) { + background-color: #f2f2f2; +} + +#themeTH { + text-align: left; +} + +#results { + overflow-x: scroll; +} + +td, th { + padding-left: 1rem; + padding-right: 1rem; + text-align: center; +} diff --git a/templates/js/buildPage.js b/templates/js/buildPage.js new file mode 100644 index 0000000..a094677 --- /dev/null +++ b/templates/js/buildPage.js @@ -0,0 +1,84 @@ +function getSortBy() { + let sortByLastCommitInput = document.getElementById('sortByDate'); + if (sortByLastCommitInput === null) { + return 'date'; + } else { + return sortByLastCommitInput.checked ? 'date' : 'stars'; + } +} + +function getSortedThemes(themeList, sortedBy) { + if (sortedBy === 'date') { + return themeList.sort((a, b) => b.date_in_seconds - a.date_in_seconds); + } else { + return themeList.sort((a, b) => b.num_stars - a.num_stars); + } +} + +function getSelectedTags() { + let tagSelectionInputs = document.getElementsByClassName('tagSelectionInput'); + if (tagSelectionInputs.length > 0) { + return [...tagSelectionInputs].filter((x) => x.checked).map((y) => y.value); + } else { + return []; + } +} + +function getSelectedFeatures() { + let featureSelectionInputs = document.getElementsByClassName('featureSelectionInput'); + if (featureSelectionInputs.length > 0) { + return [...featureSelectionInputs].filter((x) => x.checked).map((y) => y.value); + } else { + return []; + } +} + +function getFilteredThemes(selectedTags, selectedFeatures) { + if ((selectedTags.length === 0) && selectedFeatures.length === 0) { + return themes; + } else { + return themes + .filter((x) => selectedTags.every((y) => x.tags.includes(y)) ) + .filter((z) => selectedFeatures.every((w) => z.features.includes(w)) ); + } +} + +function buildResults() { + let resultsDiv = document.getElementById('results'); + resultsDiv.innerHTML = ''; + let resultsTable = document.createElement("table"); + resultsTable.id = 'resultsTable'; + resultsTable.style.border = '1px solid black'; + resultsTable.style.fontSize = '.9rem'; + + let resultsTableHeadRow = document.createElement("tr"); + resultsDiv.appendChild(resultsTable); + resultsTable.appendChild(resultsTableHeadRow); + + let themeTH = document.createElement("th"); + themeTH.innerHTML = "theme"; + resultsTableHeadRow.appendChild(themeTH); + + let dateTH = document.createElement("th"); + dateTH.innerHTML = "date"; + resultsTableHeadRow.appendChild(dateTH); + + let starsTH = document.createElement("th"); + starsTH.innerHTML = "stars"; + resultsTableHeadRow.appendChild(starsTH); + + let commitTH = document.createElement("th"); + commitTH.innerHTML = "commit"; + resultsTableHeadRow.appendChild(commitTH); + + let selectedTags = getSelectedTags(); + let selectedFeatures = getSelectedFeatures(); + let sortedBy = getSortBy(); + let filtered_themes = getFilteredThemes(selectedTags, selectedFeatures); + let sorted_themes = getSortedThemes(filtered_themes, sortedBy); + sorted_themes.forEach(theme => addThemeTableRow(theme)); + + buildSelectionMenu(sorted_themes, sortedBy, selectedTags, selectedFeatures); +}; + +buildResults(); diff --git a/templates/js/buildSelectionMenu.js b/templates/js/buildSelectionMenu.js new file mode 100644 index 0000000..9e6b368 --- /dev/null +++ b/templates/js/buildSelectionMenu.js @@ -0,0 +1,101 @@ +function buildTagSelectionInput(tag, selected, tagSelectionRow) { + let tagSelectionInputDiv = document.createElement('div'); + tagSelectionInputDiv.style.width = '15rem'; + tagSelectionInputDiv.style.maxWidth = '50%'; + tagSelectionInputDiv.style.marginTop = '.5rem'; + tagSelectionInputDiv.style.marginBottom = '.5rem'; + + let tagSelectionInput = document.createElement('input'); + tagSelectionInput.type = "checkbox"; + tagSelectionInput.id = tag.tag + "-selection-input"; + tagSelectionInput.name = tag.tag + "-selection-input"; + tagSelectionInput.value = tag.tag; + tagSelectionInput.checked = (selected) ? true : false; + tagSelectionInput.classList.add('tagSelectionInput'); + tagSelectionInput.onclick = function() { buildResults(); }; + tagSelectionInputDiv.appendChild(tagSelectionInput); + + let tagSelectionInputLabel = document.createElement('label'); + tagSelectionInputLabel.for = tag.tag + "-selection-input"; + tagSelectionInputLabel.innerHTML = tag.tag + ' (' + tag.num_themes + ')'; + tagSelectionInputDiv.appendChild(tagSelectionInputLabel); + + tagSelectionRow.appendChild(tagSelectionInputDiv); +} + +function buildTagSelectionDiv(selectedTags, availableTags) { + let selectionMenuDiv = document.getElementById('selection-menu'); + let tagSelectionHeading = document.createElement('h2'); + tagSelectionHeading.innerHTML = "Select Tags"; + selectionMenuDiv.appendChild(tagSelectionHeading); + + let tagSelectionRow = document.createElement('div'); + tagSelectionRow.style.display = 'flex'; + tagSelectionRow.style.flexWrap = 'wrap'; + tagSelectionRow.style.justifyContent = 'space-around'; + + selectionMenuDiv.appendChild(tagSelectionRow); + + availableTags + .filter((x) => selectedTags.includes(x.tag)) + .forEach((y) => { buildTagSelectionInput(y, true, tagSelectionRow); }); + + availableTags + .filter((x) => !selectedTags.includes(x.tag)) + .forEach((y) => { buildTagSelectionInput(y, false, tagSelectionRow); }); +} + +function buildFeatureSelectionInput(feature, selected, featureSelectionRow) { + let featureSelectionInputDiv = document.createElement('div'); + featureSelectionInputDiv.style.width = '30rem'; + featureSelectionInputDiv.style.maxWidth = '50%'; + featureSelectionInputDiv.style.marginTop = '.5rem'; + featureSelectionInputDiv.style.marginBottom = '.5rem'; + + let featureSelectionInput = document.createElement('input'); + featureSelectionInput.type = "checkbox"; + featureSelectionInput.id = feature.feature + "-selection-input"; + featureSelectionInput.name = feature.feature + "-selection-input"; + featureSelectionInput.value = feature.feature; + featureSelectionInput.checked = (selected) ? true : false; + featureSelectionInput.classList.add('featureSelectionInput'); + featureSelectionInput.onclick = function() { buildResults(); }; + featureSelectionInputDiv.appendChild(featureSelectionInput); + + let featureSelectionInputLabel = document.createElement('label'); + featureSelectionInputLabel.for = feature.feature + "-selection-input"; + featureSelectionInputLabel.innerHTML = feature.feature + ' (' + feature.num_themes + ')'; + featureSelectionInputDiv.appendChild(featureSelectionInputLabel); + + featureSelectionRow.appendChild(featureSelectionInputDiv); +} + +function buildFeatureSelectionDiv(selectedFeatures, availableFeatures) { + let selectionMenuDiv = document.getElementById('selection-menu'); + let featureSelectionHeading = document.createElement('h2'); + featureSelectionHeading.innerHTML = "Select Features"; + selectionMenuDiv.appendChild(featureSelectionHeading); + + let featureSelectionRow = document.createElement('div'); + featureSelectionRow.style.display = 'flex'; + featureSelectionRow.style.flexWrap = 'wrap'; + featureSelectionRow.style.justifyContent = 'space-around'; + + selectionMenuDiv.appendChild(featureSelectionRow); + + availableFeatures + .filter((x) => selectedFeatures.includes(x.feature)) + .forEach((y) => { buildFeatureSelectionInput(y, true, featureSelectionRow); }); + + availableFeatures + .filter((x) => !selectedFeatures.includes(x.feature)) + .forEach((y) => { buildFeatureSelectionInput(y, false, featureSelectionRow); }); +} + +function buildSelectionMenu(sorted_themes, sortedBy, selectedTags, selectedFeatures) { + let availableTags = getAvailableTags(sorted_themes); + let availableFeatures = getAvailableFeatures(sorted_themes); + buildSortByDiv(sortedBy); + buildTagSelectionDiv(selectedTags, availableTags); + buildFeatureSelectionDiv(selectedFeatures, availableFeatures); +} diff --git a/templates/js/buildSortByDiv.js b/templates/js/buildSortByDiv.js new file mode 100644 index 0000000..b74ef3f --- /dev/null +++ b/templates/js/buildSortByDiv.js @@ -0,0 +1,53 @@ +function buildSortByDiv(sortedBy) { + let menuDiv = document.getElementById('selection-menu'); + menuDiv.innerHTML = ''; + menuDiv.style.maxWidth = '100%'; + + let sortByRow = document.createElement('div'); + sortByRow.id = 'sortByRow'; + sortByRow.style.width = '500px'; + sortByRow.style.maxWidth = '100%'; + sortByRow.style.display = 'flex'; + sortByRow.style.justifyContent = 'space-around'; + sortByRow.style.margin = '1rem auto 1rem auto'; + + let sortByPrompt = document.createElement('div'); + sortByPrompt.innerHTML = "Sort By:"; + sortByRow.appendChild(sortByPrompt); + + let sortByStarsDiv = document.createElement('div'); + let sortByStarsInput = document.createElement('input'); + sortByStarsInput.type = 'radio'; + sortByStarsInput.id = 'sortByStars'; + sortByStarsInput.name = 'sortBy'; + sortByStarsInput.value = 'stars'; + sortByStarsInput.checked = sortedBy === 'stars' ? true : false; + sortByStarsInput.onclick = function() { buildResults(); }; + sortByStarsDiv.appendChild(sortByStarsInput); + + let sortByStarsLabel = document.createElement('label'); + sortByStarsLabel.for = 'stars'; + sortByStarsLabel.innerHTML = 'Stars'; + sortByStarsDiv.appendChild(sortByStarsLabel); + + let sortByLastCommitDiv = document.createElement('div'); + let sortByLastCommitInput = document.createElement('input'); + sortByLastCommitInput.type = 'radio'; + sortByLastCommitInput.id = 'sortByDate'; + sortByLastCommitInput.name = 'sortBy'; + sortByLastCommitInput.value = 'date'; + sortByLastCommitInput.checked = sortedBy === 'date' ? true : false; + sortByLastCommitInput.onclick = function() { buildResults(); }; + sortByLastCommitDiv.appendChild(sortByLastCommitInput); + + let sortByLastCommitLabel = document.createElement('label'); + sortByLastCommitLabel.for = 'date'; + sortByLastCommitLabel.innerHTML = 'Latest Commit Date'; + sortByLastCommitDiv.appendChild(sortByLastCommitLabel); + + + sortByRow.appendChild(sortByStarsDiv); + sortByRow.appendChild(sortByLastCommitDiv); + + menuDiv.appendChild(sortByRow); +} diff --git a/templates/js/buildThemeTableRow.js b/templates/js/buildThemeTableRow.js new file mode 100644 index 0000000..12a40b4 --- /dev/null +++ b/templates/js/buildThemeTableRow.js @@ -0,0 +1,28 @@ +function addThemeTableRow(theme) { + let resultsTable = document.getElementById('resultsTable'); + let resultsTableRow = document.createElement("tr"); + + let themeTD = document.createElement("td"); + themeTD.innerHTML = '' + theme.short_name + ''; + themeTD.style.whiteSpace = 'nowrap'; + themeTD.style.overFlow = 'hidden'; + themeTD.style.width = '20%'; + resultsTableRow.appendChild(themeTD); + + let dateTD = document.createElement("td"); + dateTD.innerHTML = theme.date; + dateTD.style.textAlign = 'center'; + dateTD.style.minWidth = '8rem'; + resultsTableRow.appendChild(dateTD); + + let starsTD = document.createElement("td"); + starsTD.innerHTML = theme.num_stars; + resultsTableRow.appendChild(starsTD); + + let commitTD = document.createElement("td"); + commitTD.innerHTML = theme.commit; + commitTD.style.minWidth = '7rem'; + resultsTableRow.appendChild(commitTD); + + resultsTable.appendChild(resultsTableRow); +}; diff --git a/templates/js/getAvailableTagsAndFeatures.js b/templates/js/getAvailableTagsAndFeatures.js new file mode 100644 index 0000000..abe34d0 --- /dev/null +++ b/templates/js/getAvailableTagsAndFeatures.js @@ -0,0 +1,48 @@ +function getAvailableFeatures(sorted_themes) { + let result = []; + sorted_themes.forEach(x => { + x.features.forEach(feature => { + if (result.length === 0) { + result.push({'feature': feature, 'num_themes': 1}); + } else { + let features_in_result = result.map(y => y.feature); + if (features_in_result.includes(feature)) { + result.forEach(w => { + if (w.feature === feature) { + w.num_themes += 1; + } + }); + } else { + result.push({'feature': feature, 'num_themes': 1}); + } + } + }); + }); + // return result.sort((a, b) => a.feature.localeCompare(b.feature)); + return result.sort((a, b) => b.num_themes - a.num_themes); +} + +function getAvailableTags(sorted_themes) { + let result = []; + sorted_themes.forEach(x => { + x.tags.forEach(tag => { + if (result.length === 0) { + result.push({'tag': tag, 'num_themes': 1}); + } else { + let tags_in_result = result.map(y => y.tag); + if (tags_in_result.includes(tag)) { + result.forEach(w => { + if (w.tag === tag) { + w.num_themes += 1; + } + }); + } else { + result.push({'tag': tag, 'num_themes': 1}); + } + } + }); + }); + return result.sort((a, b) => b.num_themes - a.num_themes); +} + + diff --git a/templates/js/selectionMenuCollapse.js b/templates/js/selectionMenuCollapse.js new file mode 100644 index 0000000..51b3c1b --- /dev/null +++ b/templates/js/selectionMenuCollapse.js @@ -0,0 +1,11 @@ +var menuButton = document.getElementById("selection-button"); + +menuButton.addEventListener("click", function() { + menuButton.classList.toggle("active"); + var selectionMenu = document.getElementById("selection-menu"); + if (selectionMenu.style.display === "block") { + selectionMenu.style.display = "none"; + } else { + selectionMenu.style.display = "block"; + } +});