diff --git a/pootle/core/views.py b/pootle/core/views.py index 6f3cbeab9a9..e8ccd490132 100644 --- a/pootle/core/views.py +++ b/pootle/core/views.py @@ -45,7 +45,8 @@ from .http import JsonResponse, JsonResponseBadRequest from .url_helpers import get_path_parts, get_previous_url from .utils.json import PootleJSONEncoder -from .utils.stats import get_translation_states +from .utils.stats import (get_top_scorers_data, get_translation_states, + TOP_CONTRIBUTORS_CHUNK_SIZE) def check_directory_permission(permission_codename, request, directory): @@ -685,6 +686,11 @@ def get_context_data(self, *args, **kwargs): super(PootleBrowseView, self).get_context_data(*args, **kwargs)) language_code, project_code = split_pootle_path(self.pootle_path)[:2] + top_scorers = User.top_scorers( + project=project_code, + language=language_code, + limit=TOP_CONTRIBUTORS_CHUNK_SIZE + 1, + ) ctx.update( {'page': 'browse', @@ -701,10 +707,12 @@ def get_context_data(self, *args, **kwargs): 'url_action_view_all': url_action_view_all, 'table': self.table, 'is_store': self.is_store, - 'top_scorers': User.top_scorers(project=project_code, - language=language_code, - limit=10), + 'top_scorers': top_scorers, + 'top_scorers_data': get_top_scorers_data( + top_scorers, + TOP_CONTRIBUTORS_CHUNK_SIZE), 'browser_extends': self.template_extends}) + return ctx diff --git a/pootle/static/css/scores.css b/pootle/static/css/scores.css index 17f683e5b67..12254e0bf41 100644 --- a/pootle/static/css/scores.css +++ b/pootle/static/css/scores.css @@ -122,7 +122,6 @@ html[dir="rtl"] .path-summary-more .top-scorers .top-scorer { font-size: 1em; width: 100%; - table-layout: fixed; } .path-summary-more .top-scorers-table tr td.number:last-child @@ -139,7 +138,15 @@ html[dir="rtl"] .path-summary-more .top-scorers-table tr td.number:last-child .path-summary-more .top-scorers-table tr td.number:first-child { text-align: left; - width: 2em; + width: 1em; + padding-right: 1em; +} + +html[dir="rtl"] .path-summary-more .top-scorers-table tr td.number:first-child +{ + text-align: right; + padding-left: 1em; + padding-right: auto; } .top-scorer a diff --git a/pootle/static/css/style.css b/pootle/static/css/style.css index 11c9b4cc275..60b411f435b 100644 --- a/pootle/static/css/style.css +++ b/pootle/static/css/style.css @@ -1342,6 +1342,11 @@ html[dir="rtl"] .check-count display: inherit; } +.more-top-contributors +{ + padding: 9px 0; +} + div.small { font-size: 1em; diff --git a/pootle/static/js/browser/components/Stats.js b/pootle/static/js/browser/components/Stats.js new file mode 100644 index 00000000000..b026bba6cf5 --- /dev/null +++ b/pootle/static/js/browser/components/Stats.js @@ -0,0 +1,60 @@ +/* + * Copyright (C) Pootle contributors. + * + * This file is a part of the Pootle project. It is distributed under the GPL3 + * or later license. See the LICENSE file for a copy of the license and the + * AUTHORS file for copyright and authorship information. + */ + +import React from 'react'; + +import StatsAPI from 'api/StatsAPI'; +import TopContributors from './TopContributors'; + + +const Stats = React.createClass({ + + propTypes: { + topContributors: React.PropTypes.array.isRequired, + hasMoreContributors: React.PropTypes.bool.isRequired, + pootlePath: React.PropTypes.string.isRequired, + }, + + getInitialState() { + return { + topContributors: this.props.topContributors, + hasMoreContributors: this.props.hasMoreContributors, + }; + }, + + onLoadMoreTopContributors(data) { + const topContributors = this.state.topContributors.concat(data.items); + this.setState({ + topContributors, + hasMoreContributors: data.has_more_items, + }); + }, + + loadMoreTopContributors() { + if (!this.state.hasMoreContributors) { + return false; + } + const params = { offset: this.state.topContributors.length }; + return StatsAPI.getTopContributors(this.props.pootlePath, params) + .done(this.onLoadMoreTopContributors); + }, + + render() { + return ( + + ); + }, + +}); + + +export default Stats; diff --git a/pootle/static/js/browser/components/TopContributors.js b/pootle/static/js/browser/components/TopContributors.js new file mode 100644 index 00000000000..2adbf4ad086 --- /dev/null +++ b/pootle/static/js/browser/components/TopContributors.js @@ -0,0 +1,82 @@ +/* + * Copyright (C) Pootle contributors. + * + * This file is a part of the Pootle project. It is distributed under the GPL3 + * or later license. See the LICENSE file for a copy of the license and the + * AUTHORS file for copyright and authorship information. + */ + +import React from 'react'; + +import Avatar from 'components/Avatar'; +import { t } from 'utils/i18n'; + + +function getScoreText(score) { + if (score > 0) { + return t('+%(score)s', { score }); + } + return score; +} + + +const TopContributors = React.createClass({ + + propTypes: { + items: React.PropTypes.array.isRequired, + hasMoreItems: React.PropTypes.bool.isRequired, + loadMore: React.PropTypes.func.isRequired, + }, + + createRow(item, index) { + const title = (` + ${item.suggested} suggested
+ ${item.translated} translated
+ ${item.reviewed} reviewed
+ `); + return ( + + {t('#%(position)s', { position: index + 1 })} + + + + + {getScoreText(item.public_total_score)} + + + ); + }, + + render() { + let loadMore; + + if (this.props.hasMoreItems) { + loadMore = ( +
+ + {gettext('More...')} + +
+ ); + } + return ( +
+ + + {this.props.items.map(this.createRow)} + +
+ {loadMore} +
+ ); + }, + +}); + + +export default TopContributors; diff --git a/pootle/static/js/shared/api/StatsAPI.js b/pootle/static/js/shared/api/StatsAPI.js index dcb45bd3ad9..4b0f242bd05 100644 --- a/pootle/static/js/shared/api/StatsAPI.js +++ b/pootle/static/js/shared/api/StatsAPI.js @@ -31,7 +31,7 @@ const StatsAPI = { const body = { path, offset }; return fetch({ - body: body, + body, url: `${this.apiRoot}contributors/`, }); }, diff --git a/pootle/static/js/stats.js b/pootle/static/js/stats.js index f8ad7feca35..c89c309086c 100644 --- a/pootle/static/js/stats.js +++ b/pootle/static/js/stats.js @@ -20,6 +20,7 @@ import TimeSince from 'components/TimeSince'; import UserEvent from 'components/UserEvent'; import cookie from 'utils/cookie'; +import Stats from './browser/components/Stats'; import VisibilityToggle from './browser/components/VisibilityToggle'; import msg from './msg'; @@ -62,7 +63,6 @@ const stats = { } this.retries = 0; - const isExpanded = (options.isInitiallyExpanded || window.location.search.indexOf('?details') !== -1); this.state = { @@ -119,6 +119,15 @@ const stats = { document.querySelector('.js-mnt-visibility-toggle')); } + ReactDOM.render( + , + document.querySelector('#js-mnt-top-contributors') + ); + // Retrieve async data if needed if (isExpanded) { this.loadChecks(); diff --git a/pootle/templates/browser/index.html b/pootle/templates/browser/index.html index 864871ca7bf..4f27c5c7309 100644 --- a/pootle/templates/browser/index.html +++ b/pootle/templates/browser/index.html @@ -75,9 +75,7 @@

{% trans "Failing Checks" %}

{% if top_scorers %}

{% trans "Top Contributors for the Last 30 Days" %}

-
- {% include 'core/_top_scorers_as_table.html' %} -
+
{% endif %}
@@ -190,6 +188,7 @@

{% trans "Translations" %}

isAdmin: {{ has_admin_access|yesno:"true,false" }}, isInitiallyExpanded: {{ is_store|yesno:"true,false" }}, initialData: {{ stats|to_js }}, + topContributorsData: {{ top_scorers_data|to_js }}, statsRefreshAttemptsCount: {{ stats_refresh_attempts_count }}, uiLocaleDir: '{{ LANGUAGE_BIDI|yesno:"rtl,ltr" }}', }); diff --git a/pootle/templates/core/_top_scorers_as_table.html b/pootle/templates/core/_top_scorers_as_table.html deleted file mode 100644 index a22e81a4896..00000000000 --- a/pootle/templates/core/_top_scorers_as_table.html +++ /dev/null @@ -1,28 +0,0 @@ -{% load i18n humanize profile_tags %} - - {% if top_scorers %} - - {% for item in top_scorers %} - {% with user=item.user %} - - - - - - {% endwith %} - {% endfor %} - - {% endif %} -
{% blocktrans with position=forloop.counter %}#{{ position }}{% endblocktrans %} - - - {{ user.display_name }} - - - - {% if item.public_total_score > 0 %}+{% endif %}{{ item.public_total_score|intcomma }} - -