Browse Source

Merge remote-tracking branch 'glaubinix/t/download-stats-major-versions'

Jordi Boggiano 5 years ago
parent
commit
512bf93663

+ 31 - 7
src/Packagist/WebBundle/Controller/PackageController.php

@@ -982,6 +982,7 @@ class PackageController extends Controller
      */
     public function statsAction(Request $req, Package $package)
     {
+        /** @var Version[] $versions */
         $versions = $package->getVersions()->toArray();
         usort($versions, Package::class.'::sortVersions');
         $date = $this->guessStatsStartDate($package);
@@ -1004,13 +1005,18 @@ class PackageController extends Controller
         $data['package'] = $package;
 
         $expandedVersion = reset($versions);
+        $majorVersions = [];
+        $foundExpandedVersion = false;
         foreach ($versions as $v) {
-            /** @var Version $v */
             if (!$v->isDevelopment()) {
-                $expandedVersion = $v;
-                break;
+                $majorVersions[] = $v->getMajorVersion();
+                if (!$foundExpandedVersion) {
+                    $expandedVersion = $v;
+                    $foundExpandedVersion = true;
+                }
             }
         }
+        $data['majorVersions'] = array_unique($majorVersions);
         $data['expandedId'] = $expandedVersion ? $expandedVersion->getId() : false;
 
         return $data;
@@ -1132,7 +1138,7 @@ class PackageController extends Controller
      * )
      * @ParamConverter("version", options={"exclude": {"name"}})
      */
-    public function overallStatsAction(Request $req, Package $package, Version $version = null)
+    public function overallStatsAction(Request $req, Package $package, Version $version = null, int $majorVersion = null)
     {
         if ($from = $req->query->get('from')) {
             $from = new DateTimeImmutable($from);
@@ -1146,19 +1152,25 @@ class PackageController extends Controller
         }
         $average = $req->query->get('average', $this->guessStatsAverage($from, $to));
 
-        if ($version) {
+        $dlData = [];
+        if ($majorVersion) {
+            $dlData = $this->getDoctrine()->getRepository('PackagistWebBundle:Download')->findDataByMajorVersion($package, $majorVersion);
+        } elseif ($version) {
             $downloads = $this->getDoctrine()->getRepository('PackagistWebBundle:Download')->findOneBy(['id' => $version->getId(), 'type' => Download::TYPE_VERSION]);
+            $dlData[] = $downloads ? $downloads->getData() : [];
         } else {
             $downloads = $this->getDoctrine()->getRepository('PackagistWebBundle:Download')->findOneBy(['id' => $package->getId(), 'type' => Download::TYPE_PACKAGE]);
+            $dlData[] = $downloads ? $downloads->getData() : [];
         }
 
         $datePoints = $this->createDatePoints($from, $to, $average);
-        $dlData = $downloads ? $downloads->getData() : [];
 
         foreach ($datePoints as $label => $values) {
             $value = 0;
             foreach ($values as $valueKey) {
-                $value += $dlData[$valueKey] ?? 0;
+                foreach ($dlData as $data) {
+                    $value += $data[$valueKey] ?? 0;
+                }
             }
             $datePoints[$label] = ceil($value / count($values));
         }
@@ -1181,6 +1193,18 @@ class PackageController extends Controller
         return $response;
     }
 
+    /**
+     * @Route(
+     *      "/packages/{name}/stats/major/{majorVersion}.json",
+     *      name="major_version_stats",
+     *      requirements={"name"="[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?", "majorVersion"="[0-9]+?"}
+     * )
+     */
+    public function majorVersionStatsAction(Request $req, Package $package, $majorVersion)
+    {
+        return $this->overallStatsAction($req, $package, null, $majorVersion);
+    }
+
     /**
      * @Route(
      *      "/packages/{name}/stats/{version}.json",

+ 22 - 0
src/Packagist/WebBundle/Entity/DownloadRepository.php

@@ -18,4 +18,26 @@ class DownloadRepository extends ServiceEntityRepository
 
         $conn->executeUpdate('DELETE FROM download WHERE package_id = :id', ['id' => $package->getId()]);
     }
+
+    public function findDataByMajorVersion(Package $package, int $majorVersion)
+    {
+        $sql = '
+            SELECT d.data
+            FROM package_version v
+            INNER JOIN download d ON d.id=v.id AND d.type = :versionType
+            WHERE v.package_id = :package AND v.normalizedVersion LIKE :majorVersion
+        ';
+
+        $stmt = $this->getEntityManager()->getConnection()
+            ->executeQuery(
+                $sql,
+                ['package' => $package->getId(), 'versionType' => Download::TYPE_VERSION, 'majorVersion' => $majorVersion . '.%']
+            );
+        $result = $stmt->fetchAll();
+        $stmt->closeCursor();
+
+        return array_map(function (array $row) {
+            return $row['data'] ? json_decode($row['data'], true) : [];
+        }, $result);
+    }
 }

+ 5 - 0
src/Packagist/WebBundle/Entity/Version.php

@@ -1046,4 +1046,9 @@ class Version
 
         return $aIndex <=> $bIndex;
     }
+
+    public function getMajorVersion(): int
+    {
+        return (int) explode('.', $this->normalizedVersion, 2)[0];
+    }
 }

+ 10 - 0
src/Packagist/WebBundle/Resources/public/css/main.css

@@ -1663,6 +1663,16 @@ svg.chart {
   margin-right: 5%;
   position: relative;
 }
+.stats-toggle-wrapper {
+    display: flex;
+}
+.stats-toggle-wrapper a {
+    flex: 1;
+    padding: 2px 15px 2px 15px;
+}
+.stats-toggle-wrapper a.open {
+    color: #000;
+}
 
 .two-factor-key {
     max-width: 200px;

+ 20 - 0
src/Packagist/WebBundle/Resources/public/js/charts.js

@@ -119,6 +119,14 @@
             });
         }
 
+        function toggleStatsType(statsType) {
+            $('.package .stats-toggler.open').removeClass('open');
+            $('.package .stats-toggler[data-stats-type=' + statsType + ']').addClass('open');
+
+            $('.package .stats-wrapper').hide();
+            $('.package .stats-wrapper[data-stats-type=' + statsType + ']').show();
+        }
+
         // initializer for #<version-id> present on page load
         if (hash.length > 1) {
             hash = hash.substring(1);
@@ -126,13 +134,25 @@
             if (match.length) {
                 $('.package .details-toggler.open').removeClass('open');
                 match.addClass('open');
+
+                toggleStatsType(match.closest('[data-stats-type]').attr('data-stats-type'));
             }
+        } else {
+            match = $('.package .details-toggler.open');
+            toggleStatsType(match.closest('[data-stats-type]').attr('data-stats-type'));
         }
 
         if ($('.package .details-toggler.open').length) {
             loadVersionChart($('.package .details-toggler.open').attr('data-version-id'));
         }
 
+        $('.package .stats-toggler').on('click', function () {
+            var target = $(this);
+            toggleStatsType($(this).attr('data-stats-type'));
+
+            $('.package .details-toggler[data-version-id="' + target.attr('href').substr(1) + '"]').trigger('click');
+        });
+
         $('.package .details-toggler').on('click', function () {
             var res, target = $(this), versionId = target.attr('data-version-id');
 

+ 30 - 1
src/Packagist/WebBundle/Resources/views/package/stats.html.twig

@@ -61,7 +61,36 @@
                         </svg>
                     </div>
                 </div>
-                {% include 'PackagistWebBundle:package:version_list.html.twig' with {versions: versions, expandedId: expandedId} %}
+                <div class="col-md-3 no-padding">
+                    <div class="stats-toggle-wrapper">
+                        {% if versions|length > 0 and majorVersions|length > 0 %}
+                            <a class="stats-toggler" href="#{{ versions[0].version }}" data-stats-type="version">Versions</a>
+                            <a class="stats-toggler" href="#major/{{ majorVersions[0] }}" data-stats-type="major">Major Versions</a>
+                        {% endif %}
+                    </div>
+                    <div class="stats-wrapper" data-stats-type="version">
+                        {% include 'PackagistWebBundle:package:version_list.html.twig' with {versions: versions, expandedId: expandedId} %}
+                    </div>
+                    {% if majorVersions|length > 0 %}
+                        <div class="stats-wrapper" data-stats-type="major">
+                            <div class="package-aside versions-wrapper">
+                                <ul class="versions">
+                                    {% for majorVersion in majorVersions %}
+                                        {% set expanded = majorVersion == expandedId|default(false) %}
+                                        <li class="details-toggler version{% if loop.last %} last{% endif %}{% if expanded %} open{% endif %}" data-version-id="major/{{ majorVersion }}">
+                                            <a href="#major/{{ majorVersion }}" class="version-number">
+                                                {{- majorVersion -}}
+                                            </a>
+                                        </li>
+                                    {% endfor %}
+                                </ul>
+                                <div class="hidden versions-expander">
+                                    <i class="glyphicon glyphicon-chevron-down"></i>
+                                </div>
+                            </div>
+                        </div>
+                    {% endif %}
+                </div>
             </div>
         </div>
     </section>

+ 44 - 46
src/Packagist/WebBundle/Resources/views/package/version_list.html.twig

@@ -1,50 +1,48 @@
-<div class="col-md-3 no-padding">
-    <div class="package-aside versions-wrapper">
-        <ul class="versions">
-            {% for version in versions %}
-                {% set expanded = version.id == expandedId|default(false) %}
-                <li class="details-toggler version{% if loop.last %} last{% endif %}{% if expanded %} open{% endif %}" data-version-id="{{ version.version }}" data-load-more="{{ path('view_version', {versionId: version.id, _format: 'json'}) }}">
-                    <a href="#{{ version.version }}" class="version-number">
-                        {{- version.version -}}
-                        {% if version.hasVersionAlias() %}
-                            / {{ version.versionAlias }}
-                        {% endif -%}
-                    </a>
-
-                    {% if hasVersionSecurityAdvisories[version.id]|default(false) %}
-                        <a class="advisory-alert" href="{{ path('view_package_advisories', {name: package.name, version: version.id}) }}">
-                            <i class="glyphicon glyphicon-alert " title="Version has security advisories"></i>
-                        </a>
-                    {% endif %}
+<div class="package-aside versions-wrapper">
+    <ul class="versions">
+        {% for version in versions %}
+            {% set expanded = version.id == expandedId|default(false) %}
+            <li class="details-toggler version{% if loop.last %} last{% endif %}{% if expanded %} open{% endif %}" data-version-id="{{ version.version }}" data-load-more="{{ path('view_version', {versionId: version.id, _format: 'json'}) }}">
+                <a href="#{{ version.version }}" class="version-number">
+                    {{- version.version -}}
+                    {% if version.hasVersionAlias() %}
+                        / {{ version.versionAlias }}
+                    {% endif -%}
+                </a>
 
-                    {% if deleteVersionCsrfToken is defined and deleteVersionCsrfToken is not empty %}
-                    <form class="delete-version" action="{{ path("delete_version", {"versionId": version.id}) }}" method="DELETE">
-                        <input type="hidden" name="_token" value="{{ deleteVersionCsrfToken }}" />
-                        <i class="submit glyphicon glyphicon-remove"></i>
-                    </form>
-                    {% endif %}
-                </li>
-            {% endfor %}
-        </ul>
-        <div class="hidden versions-expander">
-            <i class="glyphicon glyphicon-chevron-down"></i>
-        </div>
-    </div>
+                {% if hasVersionSecurityAdvisories[version.id]|default(false) %}
+                    <a class="advisory-alert" href="{{ path('view_package_advisories', {name: package.name, version: version.id}) }}">
+                        <i class="glyphicon glyphicon-alert " title="Version has security advisories"></i>
+                    </a>
+                {% endif %}
 
-    {% if showUpdated is defined and showUpdated and package.getUpdatedAt() %}
-        <div class="last-update">
-            {% if not package.isAutoUpdated() %}
-                <p class="auto-update-danger">This package is <strong>not</strong> auto-updated.</p>
-            {% else %}
-                <p class="auto-update-success">This package is auto-updated.</p>
-            {% endif %}
-            <p>Last update: {{ package.getUpdatedAt()|date('Y-m-d H:i:s') }} UTC</p>
-            {% if showUpdateButton %}
-                <form class="force-update action" action="{{ path('update_package', {name: package.name, type: 'public_update'}) }}" method="PUT">
-                    <input type="hidden" name="update" value="1" />
-                    <a class="force-update-trigger">Update Now</a>
+                {% if deleteVersionCsrfToken is defined and deleteVersionCsrfToken is not empty %}
+                <form class="delete-version" action="{{ path("delete_version", {"versionId": version.id}) }}" method="DELETE">
+                    <input type="hidden" name="_token" value="{{ deleteVersionCsrfToken }}" />
+                    <i class="submit glyphicon glyphicon-remove"></i>
                 </form>
-            {% endif %}
-        </div>
-    {% endif %}
+                {% endif %}
+            </li>
+        {% endfor %}
+    </ul>
+    <div class="hidden versions-expander">
+        <i class="glyphicon glyphicon-chevron-down"></i>
+    </div>
 </div>
+
+{% if showUpdated is defined and showUpdated and package.getUpdatedAt() %}
+    <div class="last-update">
+        {% if not package.isAutoUpdated() %}
+            <p class="auto-update-danger">This package is <strong>not</strong> auto-updated.</p>
+        {% else %}
+            <p class="auto-update-success">This package is auto-updated.</p>
+        {% endif %}
+        <p>Last update: {{ package.getUpdatedAt()|date('Y-m-d H:i:s') }} UTC</p>
+        {% if showUpdateButton %}
+            <form class="force-update action" action="{{ path('update_package', {name: package.name, type: 'public_update'}) }}" method="PUT">
+                <input type="hidden" name="update" value="1" />
+                <a class="force-update-trigger">Update Now</a>
+            </form>
+        {% endif %}
+    </div>
+{% endif %}

+ 11 - 9
src/Packagist/WebBundle/Resources/views/package/view_package.html.twig

@@ -291,15 +291,17 @@
                             {% include 'PackagistWebBundle:package:version_details.html.twig' with {version: expandedVersion} %}
                         {% endif %}
                     </div>
-                    {% include 'PackagistWebBundle:package:version_list.html.twig' with {
-                        versions: versions,
-                        expandedId: expandedVersion.id,
-                        deleteVersionCsrfToken: deleteVersionCsrfToken|default(null),
-                        package: package,
-                        showUpdated: true,
-                        showUpdateButton: not hasActions and app.user and not package.wasUpdatedInTheLast24Hours(),
-                        hasVersionSecurityAdvisories: hasVersionSecurityAdvisories
-                    } %}
+                    <div class="col-md-3 no-padding">
+                        {% include 'PackagistWebBundle:package:version_list.html.twig' with {
+                            versions: versions,
+                            expandedId: expandedVersion.id,
+                            deleteVersionCsrfToken: deleteVersionCsrfToken|default(null),
+                            package: package,
+                            showUpdated: true,
+                            showUpdateButton: not hasActions and app.user and not package.wasUpdatedInTheLast24Hours(),
+                            hasVersionSecurityAdvisories: hasVersionSecurityAdvisories
+                        } %}
+                    </div>
                 </div>
             {% elseif package.crawledAt is null %}
                 <p class="col-xs-12">This package has not been crawled yet, some information is missing.</p>