]> git.ipfire.org Git - pbs.git/commitdiff
builds: Show scratch builds for packages and enhance search
authorMichael Tremer <michael.tremer@ipfire.org>
Sun, 4 Jun 2023 11:27:41 +0000 (11:27 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Sun, 4 Jun 2023 11:28:06 +0000 (11:28 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
src/buildservice/builds.py
src/buildservice/users.py
src/templates/builds/index.html
src/templates/builds/modules/list.html
src/templates/packages/name/builds.html [new file with mode: 0644]
src/templates/packages/name/index.html
src/web/__init__.py
src/web/builds.py
src/web/packages.py

index a55300d68cccc32a72094e0199dbf38788405513..68257d3d121478eb69bce093bc57b39cfec7d0da 100644 (file)
@@ -342,6 +342,7 @@ dist_templates_packages_modules_DATA = \
 templates_packages_modulesdir = $(templates_packagesdir)/modules
 
 dist_templates_packages_name_DATA = \
+       src/templates/packages/name/builds.html \
        src/templates/packages/name/index.html
 
 templates_packages_namedir = $(templates_packagesdir)/name
index 5583600b58163b0ed934660febbd8c98ab599f8b..e91b2ffb384b0f703993ebaae1e62f5aeda86179 100644 (file)
@@ -90,10 +90,111 @@ class Builds(base.Object):
                        name,
                )
 
-       def get_recent(self, limit=None, offset=None):
+       def get_by_name(self, name, limit=None):
+               """
+                       Returns all builds by this name
+               """
+               builds = self._get_builds("""
+                       SELECT
+                               builds.*
+                       FROM
+                               builds
+                       JOIN
+                               packages ON builds.pkg_id = packages.id
+                       WHERE
+                               builds.deleted_at IS NULL
+                       AND
+                               packages.deleted_at IS NULL
+                       AND
+                               packages.name = %s
+                       ORDER BY
+                               builds.created_at
+                       """, name,
+               )
+
+               return list(builds)
+
+       def get_release_builds_by_name(self, name, limit=None):
+               builds = self._get_builds("""
+                       WITH builds AS (
+                               SELECT
+                                       builds.*,
+
+                                       -- Number all builds per distribution so that we can filter out
+                                       -- the first N builds later
+                                       ROW_NUMBER() OVER (
+                                               PARTITION BY packages.distro_id ORDER BY builds.created_at DESC
+                                       ) AS _number
+                               FROM
+                                       builds
+                               JOIN
+                                       packages ON builds.pkg_id = packages.id
+                               WHERE
+                                       builds.deleted_at IS NULL
+                               AND
+                                       packages.deleted_at IS NULL
+                               AND
+                                       packages.name = %s
+                       )
+
+                       SELECT
+                               *
+                       FROM
+                               builds
+                       WHERE
+                               %s IS NULL
+                       OR
+                               _number <= %s
+                       """, name, limit, limit,
+               )
+
+               return list(builds)
+
+       def get_scratch_builds_by_name(self, name, limit=None):
+               builds = self._get_builds("""
+                       WITH builds AS (
+                               SELECT
+                                       builds.*,
+
+                                       -- Number all builds per user so that we can filter out
+                                       -- the first N builds later
+                                       ROW_NUMBER() OVER (
+                                               PARTITION BY builds.owner_id ORDER BY builds.created_at DESC
+                                       ) AS _number
+                               FROM
+                                       builds
+                               JOIN
+                                       packages ON builds.pkg_id = packages.id
+                               WHERE
+                                       builds.deleted_at IS NULL
+                               AND
+                                       builds.owner_id IS NOT NULL
+                               AND
+                                       packages.deleted_at IS NULL
+                               AND
+                                       packages.name = %s
+                       )
+
+                       SELECT
+                               *
+                       FROM
+                               builds
+                       WHERE
+                               %s IS NULL
+                       OR
+                               _number <= %s
+                       """, name, limit, limit,
+               )
+
+               return list(builds)
+
+       def get_recent(self, name=None, limit=None, offset=None):
                """
                        Returns the most recent (non-test) builds
                """
+               if name:
+                       return self.get_recent_by_name(name, limit=limit, offset=offset)
+
                builds = self._get_builds("""
                        SELECT
                                *
@@ -114,28 +215,32 @@ class Builds(base.Object):
 
                return list(builds)
 
-       def get_by_user(self, user, limit=None, offset=None):
+       def get_recent_by_name(self, name, limit=None, offset=None):
                """
-                       Returns builds by a certain user
+                       Returns the most recent (non-test) builds
                """
                builds = self._get_builds("""
                        SELECT
-                               *
+                               builds.*
                        FROM
                                builds
+                       JOIN
+                               packages ON builds.pkg_id = packages.id
                        WHERE
-                               deleted_at IS NULL
+                               builds.deleted_at IS NULL
                        AND
-                               test IS FALSE
+                               builds.test IS FALSE
+                       AND
+                               packages.deleted_at IS NULL
                        AND
-                               owner_id = %s
+                               packages.name = %s
                        ORDER BY
                                created_at DESC
                        LIMIT
                                %s
                        OFFSET
-                               %s
-                       """, user, limit, offset,
+                               %s""",
+                       name, limit, offset,
                )
 
                return list(builds)
index f2964f556b2bb17434ebf3eb7c71974a2d27fd0e..1a6fc47deca9889473c3f8997cf7323addc1f61e 100644 (file)
@@ -650,6 +650,67 @@ class User(base.DataObject):
 
                return 0
 
+       # Builds
+
+       def get_builds(self, name=None, limit=None, offset=None):
+               """
+                       Returns builds by a certain user
+               """
+               if name:
+                       return self.get_builds_by_name(name, limit=limit, offset=offset)
+
+               builds = self.backend.builds._get_builds("""
+                       SELECT
+                               *
+                       FROM
+                               builds
+                       WHERE
+                               deleted_at IS NULL
+                       AND
+                               test IS FALSE
+                       AND
+                               owner_id = %s
+                       ORDER BY
+                               created_at DESC
+                       LIMIT
+                               %s
+                       OFFSET
+                               %s
+                       """, self.id, limit, offset,
+               )
+
+               return list(builds)
+
+       def get_builds_by_name(self, name, limit=None, offset=None):
+               """
+                       Fetches all builds matching name
+               """
+               builds = self.backend.builds._get_builds("""
+                       SELECT
+                               builds.*
+                       FROM
+                               builds
+                       JOIN
+                               packages ON builds.pkg_id = packages.id
+                       WHERE
+                               builds.deleted_at IS NULL
+                       AND
+                               builds.test IS FALSE
+                       AND
+                               builds.owner_id = %s
+                       AND
+                               packages.deleted_at IS NULL
+                       AND
+                               packages.name = %s
+                       LIMIT
+                               %s
+                       OFFSET
+                               %s
+                       """, self.id, name, limit, offset,
+               )
+
+               return list(builds)
+
        # Stats
 
        @lazy_property
index 00cc01679fe91b7248bb4a01505ab831b946122f..8c6456ca1022b9347051d788c00ce7f6066d237c 100644 (file)
@@ -3,26 +3,37 @@
 {% block title %}{{ _("Builds") }}{% end block %}
 
 {% block body %}
-       <section class="section">
-               <div class="container">
-                       <nav class="breadcrumb" aria-label="breadcrumbs">
-                               <ul>
-                                       <li class="is-active">
-                                               <a href="#" aria-current="page">
-                                                       {{ _("Builds") }}
-                                               </a>
-                                       </li>
-                               </ul>
-                       </nav>
+       <section class="hero is-light">
+               <div class="hero-body">
+                       <div class="container">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
+                                       <ul>
+                                               <li class="is-active">
+                                                       <a href="#" aria-current="page">
+                                                               {{ _("Builds") }}
+                                                       </a>
+                                               </li>
+                                       </ul>
+                               </nav>
 
-                       <h1 class="title">
-                               {% if user %}
-                                       {{ _("%s's Builds") % user }}
-                               {% else %}
-                                       {{ _("Recent Builds") }}
-                               {% end %}
-                       </h1>
+                               <h1 class="title">
+                                       {% if user and name %}
+                                               {{ _("%(user)s's Builds Of '%(name)s'") \
+                                                       % { "user" : user, "name" : name } }}
+                                       {% elif user %}
+                                               {{ _("%s's Builds") % user }}
+                                       {% elif name %}
+                                               {{ _("Builds Of '%s'") % name }}
+                                       {% else %}
+                                               {{ _("Recent Builds") }}
+                                       {% end %}
+                               </h1>
+                       </div>
+               </div>
+       </section>
 
+       <section class="section">
+               <div class="container">
                        {# Render all builds #}
                        {% for date in builds %}
                                <div class="block">
                        <div class="block">
                                <nav class="pagination is-centered" role="navigation" aria-label="pagination">
                                        <a class="pagination-previous {% if not offset %}is-disabled{% end %}"
-                                                       href="{{ make_url("/builds", offset=offset - limit, limit=limit, user=user.name if user else None) }}">
+                                                       href="{{ make_url("/builds", offset=offset - limit, limit=limit, name=name, user=user.name if user else None) }}">
                                                {{ _("Previous Page") }}
                                        </a>
 
                                        <a class="pagination-next"
-                                                       href="{{ make_url("/builds", offset=offset + limit, limit=limit, user=user.name if user else None) }}">
+                                                       href="{{ make_url("/builds", offset=offset + limit, limit=limit, name=name, user=user.name if user else None) }}">
                                                {{ _("Next Page") }}
                                        </a>
                                </nav>
index 7928508a8485fab8fcaee9da7526990963e80e3b..4c932d2d8ebba8b8b696c1b2975e64b17fd5e71c 100644 (file)
-{% for build in builds %}
-       {% set package = build.pkg %}
-
+{% if builds %}
        <div class="block">
-               <div class="box">
-                       <h5 class="title is-5 mb-2">
-                               <a href="/builds/{{ build.uuid }}">{{ build }}</a>
-
-                               {% if build.jobs %}
-                                       <div class="tags is-pulled-right">
-                                               {% for job in sorted(build.jobs) %}
-                                                       {# Pending #}
-                                                       {% if job.is_pending() %}
-                                                               <span class="tag">
-                                                                       <span class="icon-text">
-                                                                               <span class="icon">
-                                                                                       <i class="fa-solid fa-clock"></i>
+               <nav class="panel">
+                       {% for build in builds %}
+                               <a class="panel-block is-block p-4" href="/builds/{{ build.uuid }}">
+                                       {% if build.jobs %}
+                                               <div class="tags is-pulled-right is-hidden-mobile">
+                                                       {% for job in sorted(build.jobs) %}
+                                                               {# Pending #}
+                                                               {% if job.is_pending() %}
+                                                                       <span class="tag">
+                                                                               <span class="icon-text">
+                                                                                       <span class="icon">
+                                                                                               <i class="fa-solid fa-clock"></i>
+                                                                                       </span>
+
+                                                                                       <span>{{ job.arch }}</span>
                                                                                </span>
-
-                                                                               <span>{{ job.arch }}</span>
                                                                        </span>
-                                                               </span>
 
-                                                       {# Running #}
-                                                       {% elif job.is_running() %}
-                                                               <span class="tag">
-                                                                       <span class="icon-text">
-                                                                               <span class="icon">
-                                                                                       <i class="fa-solid fa-gear fa-spin"></i>
-                                                                               </span>
+                                                               {# Running #}
+                                                               {% elif job.is_running() %}
+                                                                       <span class="tag">
+                                                                               <span class="icon-text">
+                                                                                       <span class="icon">
+                                                                                               <i class="fa-solid fa-gear fa-spin"></i>
+                                                                                       </span>
 
-                                                                               <span>{{ job.arch }}</span>
+                                                                                       <span>{{ job.arch }}</span>
+                                                                               </span>
                                                                        </span>
-                                                               </span>
 
-                                                       {# Failed #}
-                                                       {% elif job.has_failed() %}
-                                                               <span class="tag is-danger">
-                                                                       <span class="icon-text">
-                                                                               <span class="icon">
-                                                                                       <i class="fa-solid fa-bug"></i>
-                                                                               </span>
+                                                               {# Failed #}
+                                                               {% elif job.has_failed() %}
+                                                                       <span class="tag is-danger">
+                                                                               <span class="icon-text">
+                                                                                       <span class="icon">
+                                                                                               <i class="fa-solid fa-bug"></i>
+                                                                                       </span>
 
-                                                                               <span>{{ job.arch }}</span>
+                                                                                       <span>{{ job.arch }}</span>
+                                                                               </span>
                                                                        </span>
-                                                               </span>
 
-                                                       {# Aborted #}
-                                                       {% elif job.is_aborted() %}
-                                                               <span class="tag is-dark">
-                                                                       <span class="icon-text">
-                                                                               <span class="icon">
-                                                                                       <i class="fa-solid fa-xmark"></i>
-                                                                               </span>
+                                                               {# Aborted #}
+                                                               {% elif job.is_aborted() %}
+                                                                       <span class="tag is-dark">
+                                                                               <span class="icon-text">
+                                                                                       <span class="icon">
+                                                                                               <i class="fa-solid fa-xmark"></i>
+                                                                                       </span>
 
-                                                                               <span>{{ job.arch }}</span>
+                                                                                       <span>{{ job.arch }}</span>
+                                                                               </span>
                                                                        </span>
-                                                               </span>
 
-                                                       {# Finished #}
-                                                       {% elif job.has_finished() %}
-                                                               <span class="tag is-success">
-                                                                       <span class="icon-text">
-                                                                               <span class="icon">
-                                                                                       <i class="fa-solid fa-check"></i>
-                                                                               </span>
+                                                               {# Finished #}
+                                                               {% elif job.has_finished() %}
+                                                                       <span class="tag is-success">
+                                                                               <span class="icon-text">
+                                                                                       <span class="icon">
+                                                                                               <i class="fa-solid fa-check"></i>
+                                                                                       </span>
 
-                                                                               <span>{{ job.arch }}</span>
+                                                                                       <span>{{ job.arch }}</span>
+                                                                               </span>
                                                                        </span>
-                                                               </span>
 
-                                                       {# Unknown State #}
-                                                       {% else %}
-                                                               <span class="tag is-light">
-                                                                       {{ _("Unknown State") }} - {{ job.arch }}
-                                                               </span>
+                                                               {# Unknown State #}
+                                                               {% else %}
+                                                                       <span class="tag is-light">
+                                                                               {{ _("Unknown State") }} - {{ job.arch }}
+                                                                       </span>
+                                                               {% end %}
                                                        {% end %}
-                                               {% end %}
-                                       </div>
-                               {% end %}
-                       </h5>
-
-                       <div class="level">
-                               <div class="level-left">
-                                       {% if build.owner %}
-                                               <div class="level-item">
-                                                       <span class="icon-text">
-                                                               <span class="icon">
-                                                                       <figure class="image">
-                                                                               <img class="is-rounded" src="{{ build.owner.avatar(32) }}"
-                                                                                       alt="{{ build.owner }}">
-                                                                       </figure>
-                                                               </span>
-                                                               <span>
-                                                                       <a href="/users/{{ build.owner.name }}">
-                                                                               {{ build.owner }}
-                                                                       </a>
-                                                               </span>
-                                                       </span>
                                                </div>
                                        {% end %}
 
-                                       <div class="level-item">
-                                               {{ _("Created %s") % locale.format_date(build.created_at, shorter=True) }}
-                                       </div>
-                               </div>
-                       </div>
-
-                       {# XXX show repository #}
-               </div>
+                                       <strong>
+                                               {{ build }}
+                                       </strong>
+
+                                       <p>
+                                               <small>
+                                                       {% if build.owner %}
+                                                               {{ _("Created %(when)s by %(owner)s") % {
+                                                                       "when"  : locale.format_date(build.created_at, shorter=True),
+                                                                       "owner" : build.owner,
+                                                               } }}
+                                                       {% else %}
+                                                               {{ _("Created %s") % locale.format_date(build.created_at, shorter=True) }}
+                                                       {% end %}
+                                               </small>
+                                       </p>
+                               </a>
+                       {% end %}
+
+                       {% if rest and more_url %}
+                               <a class="panel-block is-justify-content-center" href="{{ more_url }}">
+                                       {{ _("Show One More", "Show %(num)s More", len(rest)) % { "num" : len(rest) } }}
+                               </a>
+                       {% end %}
+               </nav>
        </div>
 {% end %}
-
diff --git a/src/templates/packages/name/builds.html b/src/templates/packages/name/builds.html
new file mode 100644 (file)
index 0000000..e79108c
--- /dev/null
@@ -0,0 +1,60 @@
+{% extends "../../base.html" %}
+
+{% block title %}{{ _("Package") }} - {{ package.name }}{% end block %}
+
+{% block body %}
+       <section class="hero is-light">
+               <div class="hero-body">
+                       <div class="container">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
+                                       <ul>
+                                               <li>
+                                                       <a href="/packages">{{ _("Packages") }}</a>
+                                               </li>
+                                               <li>
+                                                       <a href="/packages/{{ package.name }}">{{ package.name }}</a>
+                                               </li>
+                                               <li class="is-active">
+                                                       <a href="#" aria-current="page">{{ _("Builds") }}</a>
+                                               </li>
+                                       </ul>
+                               </nav>
+
+                               <h1 class="title is-1">{{ package.name }}</h1>
+                               <h4 class="subtitle is-4">{{ _("Builds") }}</h4>
+                       </div>
+               </div>
+       </section>
+
+       {# Release Builds #}
+       {% if distros %}
+               <section class="section">
+                       <div class="container">
+                               <h4 class="title is-4">{{ _("Release Builds") }}</h4>
+
+                               {% for distro in sorted(distros, reverse=True) %}
+                                       <h5 class="title is-5">{{ distro }}</h5>
+
+                                       {% module BuildsList(distros[distro], limit=limit,
+                                               more_url=make_url("/builds", name=package.name)) %}
+                               {% end %}
+                       </div>
+               </section>
+       {% end %}
+
+       {# Scratch Builds #}
+       {% if users %}
+               <section class="section">
+                       <div class="container">
+                               <h4 class="title is-4">{{ _("Scratch Builds") }}</h4>
+
+                               {% for user in users %}
+                                       <h5 class="title is-5">{{ user }}</h5>
+
+                                       {% module BuildsList(users[user], limit=limit,
+                                               more_url=make_url("/builds", name=package.name, user=user.name)) %}
+                               {% end %}
+                       </div>
+               </section>
+       {% end %}
+{% end block %}
index 94fd7bb7ab2037d51e79b2e7a05f87a0dff3f0a7..e349e5e0589a45bacf4eb07f12357c1086d7e024 100644 (file)
@@ -37,7 +37,7 @@
                                                {% if builds %}
                                                        <h4 class="title is-4">{{ repo }}</h4>
 
-                                                       <div class="panel">
+                                                       <nav class="panel">
                                                                {% for build in builds %}
                                                                        <a class="panel-block" href="/builds/{{ build.uuid }}">
                                                                                <span class="panel-icon">
                                                                                {{ build }}
                                                                        </a>
                                                                {% end %}
-                                                       </div>
+                                                       </nav>
                                                {% end %}
                                        {% end %}
+
+                                       {% if distro in scratch_builds %}
+                                               <h4 class="title is-4">{{ _("My Scratch Builds") }}</h4>
+
+                                               <nav class="panel">
+                                                       {% for build in scratch_builds[distro] %}
+                                                               <a class="panel-block" href="/builds/{{ build.uuid }}">
+                                                                       <span class="panel-icon">
+                                                                               <i class="fa-solid fa-box" aria-hidden="true"></i>
+                                                                       </span>
+
+                                                                       {{ build }}
+                                                               </a>
+                                                       {% end %}
+                                               </nav>
+                                       {% end %}
                                </div>
 
                                {# Release Monitoring #}
                                {% if current_user and current_user.is_admin() %}
-                                       {% if monitoring %}
-                                               <div class="buttons">
+                                       <div class="buttons">
+                                               {% if monitoring %}
                                                        <a class="button is-light" href="{{ monitoring.url }}">
                                                                {{ _("Show Upstream Releases") }}
                                                        </a>
                                                        <a class="button is-warning" href="{{ monitoring.url }}/edit">
                                                                {{ _("Edit Release Monitoring") }}
                                                        </a>
-                                               </div>
-                                       {% else %}
-                                               <a class="button is-success" href="/distros/{{ distro.slug }}/monitorings/{{ package.name }}/create">
-                                                       {{ _("Enable Release Monitoring") }}
-                                               </a>
-                                       {% end %}
+                                               {% else %}
+                                                       <a class="button is-success" href="/distros/{{ distro.slug }}/monitorings/{{ package.name }}/create">
+                                                               {{ _("Enable Release Monitoring") }}
+                                                       </a>
+                                               {% end %}
+                                       </div>
                                {% end %}
                        {% end %}
+
+                       <div class="buttons">
+                               <a class="button is-light" href="/packages/{{ package.name }}/builds">
+                                       {{ _("Show All Builds") }}
+                               </a>
+                       </div>
                </div>
        </section>
 
index f0c68cd8efa300f0c9738cb66433e975183ee34c..1b2541bd27bdfb019eff91e5e1d05440ef98616d 100644 (file)
@@ -144,6 +144,7 @@ class Application(tornado.web.Application):
                        (r"/packages", packages.IndexHandler),
                        (r"/packages/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})", packages.ShowHandler),
                        (r"/packages/([\w\-\+]+)", packages.NameHandler),
+                       (r"/packages/([\w\-\+]+)/builds", packages.NameBuildsHandler),
                        (r"/packages/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/download(.*)",
                                packages.FileDownloadHandler),
                        (r"/packages/([\w]{8}-[\w]{4}-[\w]{4}-[\w]{4}-[\w]{12})/view(.*)",
index f805893858249fbbacf6c21af4f2ae7483c9711a..070594cd479cc6c054f3963b4afdeda5f8d302d8 100644 (file)
@@ -68,18 +68,19 @@ class IndexHandler(base.BaseHandler):
                limit  = self.get_argument_int("limit", None) or 25
 
                # Filters
+               name = self.get_argument("name", None)
                user = self.get_argument_user("user", None)
 
                # Fetch the most recent builds
                if user:
-                       builds = self.backend.builds.get_by_user(user, limit=limit, offset=offset)
+                       builds = user.get_builds(name, limit=limit, offset=offset)
                else:
-                       builds = self.backend.builds.get_recent(limit=limit, offset=offset)
+                       builds = self.backend.builds.get_recent(name=name, limit=limit, offset=offset)
 
                # Group builds by date
                builds = misc.group(builds, lambda build: build.created_at.date())
 
-               self.render("builds/index.html", builds=builds, user=user,
+               self.render("builds/index.html", builds=builds, name=name, user=user,
                        limit=limit, offset=offset)
 
 
@@ -345,8 +346,15 @@ class GroupShowHandler(base.BaseHandler):
 
 
 class ListModule(ui_modules.UIModule):
-       def render(self, builds):
-               return self.render_string("builds/modules/list.html", builds=builds)
+       def render(self, builds, limit=None, more_url=None):
+               rest = None
+
+               # Limit builds
+               if limit:
+                       builds, rest = builds[:limit], builds[limit:]
+
+               return self.render_string("builds/modules/list.html", builds=builds,
+                       rest=rest, more_url=more_url)
 
 
 class GroupListModule(ui_modules.UIModule):
index 82a7893f6bca55177bf09a7678e18af9462e23e6..7f4c72486c6ef043bb0b052d5b0589d72c461ea2 100644 (file)
@@ -4,11 +4,11 @@ import asyncio
 import os.path
 import tornado.web
 
+from .. import misc
+
 from . import base
 from . import ui_modules
 
-from ..constants import BUFFER_SIZE
-
 class IndexHandler(base.BaseHandler):
        def get(self):
                # Sort all packages in an array like "<first char>" --> [packages, ...]
@@ -41,7 +41,41 @@ class NameHandler(base.BaseHandler):
                                for distro in distros),
                )))
 
-               self.render("packages/name/index.html", package=build.pkg, distros=distros, bugs=bugs)
+               # Fetch my own builds
+               if self.current_user:
+                       scratch_builds = misc.group(
+                               self.current_user.get_builds_by_name(name), lambda build: build.distro,
+                       )
+               else:
+                       scratch_builds = None
+
+               self.render("packages/name/index.html", package=build.pkg, distros=distros,
+                       scratch_builds=scratch_builds, bugs=bugs)
+
+
+class NameBuildsHandler(base.BaseHandler):
+       def get(self, name):
+               build = self.backend.builds.get_latest_by_name(name)
+               if not build:
+                       raise tornado.web.HTTPError(404, "Package '%s' was not found" % name)
+
+               # Fetch the limit
+               limit = self.get_argument_int("limit", 5)
+
+               # Find the latest release builds
+               release_builds = self.backend.builds.get_release_builds_by_name(name)
+
+               # Group them by distribution
+               distros = misc.group(release_builds, lambda build: build.distro)
+
+               # Find the latest scratch builds
+               scratch_builds = self.backend.builds.get_scratch_builds_by_name(name)
+
+               # Group them by user
+               users = misc.group(scratch_builds, lambda build: build.owner)
+
+               self.render("packages/name/builds.html", limit=limit,
+                       package=build.pkg, distros=distros, users=users)
 
 
 class ShowHandler(base.BaseHandler):