@@ -20,37 +20,42 @@
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import traceback
import calendar
import logging
from time import mktime
from datetime import datetime, timedelta, date
from datetime import timedelta, date
from itertools import product
from vcs.exceptions import ChangesetError
from vcs.exceptions import ChangesetError, EmptyRepositoryError, \
NodeDoesNotExistError
from pylons import tmpl_context as c, request, url
from pylons.i18n.translation import _
from rhodecode.model.db import Statistics, Repository
from rhodecode.model.repo import RepoModel
from rhodecode.model.db import Statistics
from rhodecode.lib import ALL_READMES, ALL_EXTS
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
from rhodecode.lib.base import BaseRepoController, render
from rhodecode.lib.utils import EmptyChangeset
from rhodecode.lib.markup_renderer import MarkupRenderer
from rhodecode.lib.celerylib import run_task
from rhodecode.lib.celerylib.tasks import get_commits_stats, \
LANGUAGES_EXTENSIONS_MAP
from rhodecode.lib.helpers import RepoPage
from rhodecode.lib.compat import json, OrderedDict
log = logging.getLogger(__name__)
README_FILES = [''.join([x[0][0], x[1][0]]) for x in
sorted(list(product(ALL_READMES, ALL_EXTS)),
key=lambda y:y[0][1] + y[1][1])]
class SummaryController(BaseRepoController):
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
@@ -158,14 +163,39 @@ class SummaryController(BaseRepoControll
c.no_data = True
c.enable_downloads = dbrepo.enable_downloads
if c.enable_downloads:
c.download_options = self._get_download_links(c.rhodecode_repo)
c.readme_data,c.readme_file = self.__get_readme_data()
return render('summary/summary.html')
def __get_readme_data(self):
readme_data = None
readme_file = None
try:
cs = c.rhodecode_repo.get_changeset('tip')
renderer = MarkupRenderer()
for f in README_FILES:
readme = cs.get_node(f)
readme_file = f
readme_data = renderer.render(readme.content, f)
break
except NodeDoesNotExistError:
continue
except ChangesetError:
pass
except EmptyRepositoryError:
except Exception:
log.error(traceback.format_exc())
return readme_data, readme_file
def _get_download_links(self, repo):
download_l = []
branches_group = ([], _("Branches"))
tags_group = ([], _("Tags"))
@@ -178,6 +208,7 @@ class SummaryController(BaseRepoControll
for name, chs in c.rhodecode_repo.tags.items():
#chs = chs.split(':')[-1]
tags_group[0].append((chs, name),)
download_l.append(tags_group)
return download_l
@@ -63,12 +63,40 @@ LANGUAGES_EXTENSIONS_MAP = __get_lem()
# Additional mappings that are not present in the pygments lexers
# NOTE: that this will overide any mappings in LANGUAGES_EXTENSIONS_MAP
ADDITIONAL_MAPPINGS = {'xaml': 'XAML'}
LANGUAGES_EXTENSIONS_MAP.update(ADDITIONAL_MAPPINGS)
# list of readme files to search in file tree and display in summary
# attached weights defines the search order lower is first
ALL_READMES = [
('readme', 0), ('README', 0), ('Readme', 0),
('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1),
('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2),
('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2),
('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2),
]
# extension together with weights to search lower is first
RST_EXTS = [
('', 0), ('.rst', 1),('.rest', 1),
('.RST', 2) ,('.REST', 2),
('.txt', 3), ('.TXT', 3)
MARKDOWN_EXTS = [
('.md', 1), ('.MD', 1),
('.mkdn', 2), ('.MKDN', 2),
('.mdown', 3), ('.MDOWN', 3),
('.markdown', 4), ('.MARKDOWN', 4)
PLAIN_EXTS = [('.text', 2),('.TEXT', 2)]
ALL_EXTS = MARKDOWN_EXTS + RST_EXTS + PLAIN_EXTS
def str2bool(_str):
"""
returs True/False value from given string, it tries to translate the
string into boolean
@@ -3008,7 +3008,99 @@ div#legend_container table td,div#legend
}
#node_filter {
border: 0px solid #545454;
color: #AAAAAA;
padding-left: 3px;
\ No newline at end of file
/*README STYLE*/
div.readme {
padding:0px;
div.readme h2 {
font-weight: normal;
div.readme .readme_box {
background-color: #fafafa;
clear:both;
overflow:hidden;
margin:0;
padding:0 20px 10px;
div.readme .readme_box h1, div.readme .readme_box h2, div.readme .readme_box h3, div.readme .readme_box h4, div.readme .readme_box h5, div.readme .readme_box h6 {
border-bottom: 0 !important;
margin: 0 !important;
padding: 0 !important;
line-height: 1.5em !important;
div.readme .readme_box h1:first-child {
padding-top: .25em !important;
div.readme .readme_box h2, div.readme .readme_box h3 {
margin: 1em 0 !important;
div.readme .readme_box h2 {
margin-top: 1.5em !important;
border-top: 4px solid #e0e0e0 !important;
padding-top: .5em !important;
div.readme .readme_box p {
color: black !important;
div.readme .readme_box ul {
list-style: disc !important;
margin: 1em 0 1em 2em !important;
div.readme .readme_box ol {
list-style: decimal;
div.readme .readme_box pre, code {
font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
div.readme .readme_box code {
font-size: 12px !important;
background-color: ghostWhite !important;
color: #444 !important;
padding: 0 .2em !important;
border: 1px solid #dedede !important;
div.readme .readme_box pre code {
background-color: #eee !important;
border: none !important;
div.readme .readme_box pre {
margin: 1em 0;
font-size: 12px;
background-color: #eee;
border: 1px solid #ddd;
padding: 5px;
color: #444;
overflow: auto;
-webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
@@ -61,43 +61,35 @@
##REPO NAME
<span class="repo_name">${h.repo_link(c.dbrepo.groups_and_repo)}</span>
##FORK
%if c.dbrepo.fork:
<div style="margin-top:5px;clear:both"">
<a href="${h.url('summary_home',repo_name=c.dbrepo.fork.repo_name)}">
<img class="icon" alt="${_('public')}"
title="${_('Fork of')} ${c.dbrepo.fork.repo_name}"
src="${h.url('/images/icons/arrow_divide.png')}"/>
${_('Fork of')} ${c.dbrepo.fork.repo_name}
<a href="${h.url('summary_home',repo_name=c.dbrepo.fork.repo_name)}"><img class="icon" alt="${_('public')}" title="${_('Fork of')} ${c.dbrepo.fork.repo_name}" src="${h.url('/images/icons/arrow_divide.png')}"/>
</a>
</div>
%endif
##REMOTE
%if c.dbrepo.clone_uri:
<div style="margin-top:5px;clear:both">
<a href="${h.url(str(h.hide_credentials(c.dbrepo.clone_uri)))}">
<img class="icon" alt="${_('remote clone')}"
title="${_('Clone from')} ${h.hide_credentials(c.dbrepo.clone_uri)}"
src="${h.url('/images/icons/connect.png')}"/>
${_('Clone from')} ${h.hide_credentials(c.dbrepo.clone_uri)}
<a href="${h.url(str(h.hide_credentials(c.dbrepo.clone_uri)))}"><img class="icon" alt="${_('remote clone')}" title="${_('Clone from')} ${h.hide_credentials(c.dbrepo.clone_uri)}" src="${h.url('/images/icons/connect.png')}"/>
<div class="field">
<div class="label">
<label>${_('Description')}:</label>
<div class="input-short desc">${h.urlify_text(c.dbrepo.description)}</div>
<label>${_('Contact')}:</label>
<div class="input-short">
<div class="gravatar">
@@ -116,13 +108,12 @@
<b>${'r%s:%s' % (h.get_changeset_safe(c.rhodecode_repo,'tip').revision,
h.get_changeset_safe(c.rhodecode_repo,'tip').short_id)}</b> -
<span class="tooltip" title="${c.rhodecode_repo.last_change}">
${h.age(c.rhodecode_repo.last_change)}</span><br/>
${_('by')} ${h.get_changeset_safe(c.rhodecode_repo,'tip').author}
<label>${_('Clone url')}:</label>
@@ -184,129 +175,12 @@
${h.link_to(_('Atom'),h.url('atom_feed_home',repo_name=c.dbrepo.repo_name),class_='atom_icon')}
<script type="text/javascript">
YUE.onDOMReady(function(e){
id = 'clone_url';
YUE.on(id,'click',function(e){
if(YUD.hasClass(id,'selected')){
return
else{
YUD.addClass(id,'selected');
YUD.get(id).select();
})
var data = ${c.trending_languages|n};
var total = 0;
var no_data = true;
for (k in data){
total += data[k].count;
no_data = false;
var tbl = document.createElement('table');
tbl.setAttribute('class','trending_language_tbl');
var cnt = 0;
cnt += 1;
var hide = cnt>2;
var tr = document.createElement('tr');
if (hide){
tr.setAttribute('style','display:none');
tr.setAttribute('class','stats_hidden');
var percentage = Math.round((data[k].count/total*100),2);
var value = data[k].count;
var td1 = document.createElement('td');
td1.width = 150;
var trending_language_label = document.createElement('div');
trending_language_label.innerHTML = data[k].desc+" ("+k+")";
td1.appendChild(trending_language_label);
var td2 = document.createElement('td');
td2.setAttribute('style','padding-right:14px !important');
var trending_language = document.createElement('div');
var nr_files = value+" ${_('files')}";
trending_language.title = k+" "+nr_files;
if (percentage>22){
trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"% "+nr_files+ "</b>";
trending_language.innerHTML = "<b style='font-size:0.8em'>"+percentage+"%</b>";
trending_language.setAttribute("class", 'trending_language top-right-rounded-corner bottom-right-rounded-corner');
trending_language.style.width=percentage+"%";
td2.appendChild(trending_language);
tr.appendChild(td1);
tr.appendChild(td2);
tbl.appendChild(tr);
if(cnt == 3){
var show_more = document.createElement('tr');
var td = document.createElement('td');
lnk = document.createElement('a');
lnk.href='#';
lnk.innerHTML = "${_('show more')}";
lnk.id='code_stats_show_more';
td.appendChild(lnk);
show_more.appendChild(td);
show_more.appendChild(document.createElement('td'));
tbl.appendChild(show_more);
if(no_data){
td1.innerHTML = "${c.no_data_msg}";
YUD.get('lang_stats').appendChild(tbl);
YUE.on('code_stats_show_more','click',function(){
l = YUD.getElementsByClassName('stats_hidden')
for (e in l){
YUD.setStyle(l[e],'display','');
};
YUD.setStyle(YUD.get('code_stats_show_more'),
'display','none');
var tmpl_links = {}
%for cnt,archive in enumerate(c.rhodecode_repo._get_archives()):
tmpl_links['${archive['type']}'] = '${h.link_to(archive['type'],
h.url('files_archive_home',repo_name=c.dbrepo.repo_name,
fname='__CS__'+archive['extension'],subrepos='__SUB__'),class_="archive_icon")}';
%endfor
YUE.on(['download_options','archive_subrepos'],'change',function(e){
var sm = YUD.get('download_options');
var new_cs = sm.options[sm.selectedIndex];
for(k in tmpl_links){
var s = YUD.get(k+'_link');
title_tmpl = "${_('Download %s as %s') % ('__CS_NAME__','__CS_EXT__')}";
s.title = title_tmpl.replace('__CS_NAME__',new_cs.text);
s.title = s.title.replace('__CS_EXT__',k);
var url = tmpl_links[k].replace('__CS__',new_cs.value);
var subrepos = YUD.get('archive_subrepos').checked
url = url.replace('__SUB__',subrepos);
s.innerHTML = url
});
</script>
<div class="box box-right" style="min-height:455px">
<!-- box / title -->
<div class="title">
<h5>${_('Commit activity by day / author')}</h5>
@@ -316,385 +190,489 @@
<div style="padding:0 10px 10px 15px;font-size: 1.2em;">
%if c.no_data:
${c.no_data_msg}
%if h.HasPermissionAll('hg.admin')('enable stats on from summary'):
${h.link_to(_('enable'),h.url('edit_repo',repo_name=c.repo_name),class_="ui-button-small")}
%else:
${_('Loaded in')} ${c.stats_percentage} %
<div id="commit_history" style="width:450px;height:300px;float:left"></div>
<div style="clear: both;height: 10px"></div>
<div id="overview" style="width:450px;height:100px;float:left"></div>
<div id="legend_data" style="clear:both;margin-top:10px;">
<div id="legend_container"></div>
<div id="legend_choices">
<table id="legend_choices_tables" style="font-size:smaller;color:#545454"></table>
<table id="legend_choices_tables" class="noborder" style="font-size:smaller;color:#545454"></table>
/**
* Plots summary graph
*
* @class SummaryPlot
* @param {from} initial from for detailed graph
* @param {to} initial to for detailed graph
* @param {dataset}
* @param {overview_dataset}
*/
function SummaryPlot(from,to,dataset,overview_dataset) {
var initial_ranges = {
"xaxis":{
"from":from,
"to":to,
},
var dataset = dataset;
var overview_dataset = [overview_dataset];
var choiceContainer = YUD.get("legend_choices");
var choiceContainerTable = YUD.get("legend_choices_tables");
var plotContainer = YUD.get('commit_history');
var overviewContainer = YUD.get('overview');
var plot_options = {
bars: {show:true,align:'center',lineWidth:4},
legend: {show:true, container:"legend_container"},
points: {show:true,radius:0,fill:false},
yaxis: {tickDecimals:0,},
xaxis: {
mode: "time",
timeformat: "%d/%m",
min:from,
max:to,
grid: {
hoverable: true,
clickable: true,
autoHighlight:true,
color: "#999"
//selection: {mode: "x"}
var overview_options = {
legend:{show:false},
bars: {show:true,barWidth: 2,},
shadowSize: 0,
xaxis: {mode: "time", timeformat: "%d/%m/%y",},
yaxis: {ticks: 3, min: 0,tickDecimals:0,},
grid: {color: "#999",},
selection: {mode: "x"}
*get dummy data needed in few places
function getDummyData(label){
return {"label":label,
"data":[{"time":0,
"commits":0,
"added":0,
"changed":0,
"removed":0,
}],
"schema":["commits"],
"color":'#ffffff',
* generate checkboxes accordindly to data
* @param keys
* @returns
function generateCheckboxes(data) {
//append checkboxes
var i = 0;
choiceContainerTable.innerHTML = '';
for(var pos in data) {
data[pos].color = i;
i++;
if(data[pos].label != ''){
choiceContainerTable.innerHTML += '<tr><td>'+
'<input type="checkbox" name="' + data[pos].label +'" checked="checked" />'
+data[pos].label+
'</td></tr>';
* ToolTip show
function showTooltip(x, y, contents) {
var div=document.getElementById('tooltip');
if(!div) {
div = document.createElement('div');
div.id="tooltip";
div.style.position="absolute";
div.style.border='1px solid #fdd';
div.style.padding='2px';
div.style.backgroundColor='#fee';
document.body.appendChild(div);
YUD.setStyle(div, 'opacity', 0);
div.innerHTML = contents;
div.style.top=(y + 5) + "px";
div.style.left=(x + 5) + "px";
var anim = new YAHOO.util.Anim(div, {opacity: {to: 0.8}}, 0.2);
anim.animate();
* This function will detect if selected period has some changesets
for this user if it does this data is then pushed for displaying
Additionally it will only display users that are selected by the checkbox
function getDataAccordingToRanges(ranges) {
var data = [];
var new_dataset = {};
var keys = [];
var max_commits = 0;
for(var key in dataset){
for(var ds in dataset[key].data){
commit_data = dataset[key].data[ds];
if (commit_data.time >= ranges.xaxis.from && commit_data.time <= ranges.xaxis.to){
if(new_dataset[key] === undefined){
new_dataset[key] = {data:[],schema:["commits"],label:key};
new_dataset[key].data.push(commit_data);
if (new_dataset[key] !== undefined){
data.push(new_dataset[key]);
if (data.length > 0){
return data;
//just return dummy data for graph to plot itself
return [getDummyData('')];
* redraw using new checkbox data
function plotchoiced(e,args){
var cur_data = args[0];
var cur_ranges = args[1];
var new_data = [];
var inputs = choiceContainer.getElementsByTagName("input");
//show only checked labels
for(var i=0; i<inputs.length; i++) {
var checkbox_key = inputs[i].name;
if(inputs[i].checked){
for(var d in cur_data){
if(cur_data[d].label == checkbox_key){
new_data.push(cur_data[d]);
//push dummy data to not hide the label
new_data.push(getDummyData(checkbox_key));
var new_options = YAHOO.lang.merge(plot_options, {
min: cur_ranges.xaxis.from,
max: cur_ranges.xaxis.to,
mode:"time",
if (!new_data){
new_data = [[0,1]];
// do the zooming
plot = YAHOO.widget.Flot(plotContainer, new_data, new_options);
plot.subscribe("plotselected", plotselected);
//resubscribe plothover
plot.subscribe("plothover", plothover);
// don't fire event on the overview to prevent eternal loop
overview.setSelection(cur_ranges, true);
* plot only selected items from overview
* @param ranges
function plotselected(ranges,cur_data) {
//updates the data for new plot
var data = getDataAccordingToRanges(ranges);
generateCheckboxes(data);
min: ranges.xaxis.from,
max: ranges.xaxis.to,
plot = YAHOO.widget.Flot(plotContainer, data, new_options);
overview.setSelection(ranges, true);
//resubscribe choiced
YUE.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, ranges]);
var previousPoint = null;
function plothover(o) {
var pos = o.pos;
var item = o.item;
//YUD.get("x").innerHTML = pos.x.toFixed(2);
//YUD.get("y").innerHTML = pos.y.toFixed(2);
if (item) {
if (previousPoint != item.datapoint) {
previousPoint = item.datapoint;
var tooltip = YUD.get("tooltip");
if(tooltip) {
tooltip.parentNode.removeChild(tooltip);
var x = item.datapoint.x.toFixed(2);
var y = item.datapoint.y.toFixed(2);
if (!item.series.label){
item.series.label = 'commits';
var d = new Date(x*1000);
var fd = d.toDateString()
var nr_commits = parseInt(y);
var cur_data = dataset[item.series.label].data[item.dataIndex];
var added = cur_data.added;
var changed = cur_data.changed;
var removed = cur_data.removed;
var nr_commits_suffix = " ${_('commits')} ";
var added_suffix = " ${_('files added')} ";
var changed_suffix = " ${_('files changed')} ";
var removed_suffix = " ${_('files removed')} ";
if(nr_commits == 1){nr_commits_suffix = " ${_('commit')} ";}
if(added==1){added_suffix=" ${_('file added')} ";}
if(changed==1){changed_suffix=" ${_('file changed')} ";}
if(removed==1){removed_suffix=" ${_('file removed')} ";}
showTooltip(item.pageX, item.pageY, item.series.label + " on " + fd
+'<br/>'+
nr_commits + nr_commits_suffix+'<br/>'+
added + added_suffix +'<br/>'+
changed + changed_suffix + '<br/>'+
removed + removed_suffix + '<br/>');
else {
previousPoint = null;
* MAIN EXECUTION
var data = getDataAccordingToRanges(initial_ranges);
//main plot
var plot = YAHOO.widget.Flot(plotContainer,data,plot_options);
//overview
var overview = YAHOO.widget.Flot(overviewContainer,
overview_dataset, overview_options);
//show initial selection on overview
overview.setSelection(initial_ranges);
plot.subscribe("plothover", plothover)
overview.subscribe("plotselected", function (ranges) {
plot.setSelection(ranges);
YUE.on(choiceContainer.getElementsByTagName("input"), "click", plotchoiced, [data, initial_ranges]);
SummaryPlot(${c.ts_min},${c.ts_max},${c.commit_data|n},${c.overview_data|n});
<div class="box">
<div class="breadcrumbs">${h.link_to(_('Shortlog'),h.url('shortlog_home',repo_name=c.repo_name))}</div>
<div class="table">
<div id="shortlog_data">
<%include file='../shortlog/shortlog_data.html'/>
##%if c.repo_changesets:
## ${h.link_to(_('show more'),h.url('changelog_home',repo_name=c.repo_name))}
##%endif
%if c.readme_data:
<div class="box" style="background-color: #FAFAFA">
<div class="breadcrumbs"><a href="${h.url('files_home',repo_name=c.repo_name,revision='tip',f_path=c.readme_file)}">${c.readme_file}</a></div>
<div class="readme">
<div class="readme_box">
${c.readme_data|n}
<div class="breadcrumbs">${h.link_to(_('Tags'),h.url('tags_home',repo_name=c.repo_name))}</div>
<%include file='../tags/tags_data.html'/>
%if c.repo_changesets:
${h.link_to(_('show more'),h.url('tags_home',repo_name=c.repo_name))}
<div class="breadcrumbs">${h.link_to(_('Branches'),h.url('branches_home',repo_name=c.repo_name))}</div>
<%include file='../branches/branches_data.html'/>
${h.link_to(_('show more'),h.url('branches_home',repo_name=c.repo_name))}
</%def>
@@ -21,13 +21,15 @@ requirements = [
"whoosh<1.8",
"celery>=2.2.5,<2.3",
"babel",
"python-dateutil>=1.5.0,<2.0.0",
"dulwich>=0.8.0,<0.9.0",
"vcs>=0.2.3.dev",
"webob==1.0.8"
"webob==1.0.8",
"markdown==2.0.3",
"docutils==0.8.1",
dependency_links = [
"https://secure.rhodecode.org/vcs/archive/default.zip#egg=vcs-0.2.3.dev",
"https://bitbucket.org/marcinkuzminski/vcs/get/default.zip#egg=vcs-0.2.3.dev",
Status change: