Browse Source

Merge remote-tracking branch 'igorw/solr'

Jordi Boggiano 13 years ago
parent
commit
42cbbabfd8

+ 21 - 0
README.md

@@ -13,3 +13,24 @@ Installation
 - Run `app/console assets:install web` to deploy the assets on the web dir.
 - Make a VirtualHost with DocumentRoot pointing to web/
 - You should now be able to access the site, create a user, etc.
+
+Setting up search
+-----------------
+
+The search index uses [Solr](http://lucene.apache.org/solr/), so you will have to install that on your server.
+If you are running it on a non-standard host or port, you will have to adjust the configuration. See the
+[NelmioSolariumBundle](https://github.com/nelmio/NelmioSolariumBundle) for more details.
+
+You will also have to configure Solr. The standard `schema.xml` already covers most fields like `title` and
+`description`. The following need to be added though:
+
+    <fields>
+        ...
+
+        <field name="tags" type="text_general" indexed="true" stored="true" multiValued="true"/>
+
+        ....
+    </fields>
+
+To index packages, just run `app/console packagist:index`. It is recommended to set up a cron job for
+this command, and have it run every few minutes.

+ 1 - 0
app/AppKernel.php

@@ -22,6 +22,7 @@ class AppKernel extends Kernel
             new FOS\UserBundle\FOSUserBundle(),
             new Packagist\WebBundle\PackagistWebBundle(),
             new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(),
+            new Nelmio\SolariumBundle\NelmioSolariumBundle(),
         );
 
         if (in_array($this->getEnvironment(), array('dev', 'test'))) {

+ 1 - 0
app/Resources/views/base_nolayout.html.twig

@@ -0,0 +1 @@
+{% block content %}{% endblock %}

+ 2 - 0
app/autoload.php

@@ -19,10 +19,12 @@ $loader->registerNamespaces(array(
     'Packagist'        => __DIR__.'/../src',
     'WhiteOctober\PagerfantaBundle' => __DIR__.'/../vendor/bundles',
     'Pagerfanta'  => __DIR__.'/../vendor/pagerfanta/src',
+    'Nelmio'           => __DIR__.'/../vendor/bundles',
 ));
 $loader->registerPrefixes(array(
     'Twig_Extensions_' => __DIR__.'/../vendor/twig-extensions/lib',
     'Twig_'            => __DIR__.'/../vendor/twig/lib',
+    'Solarium_'        => __DIR__.'/../vendor/solarium/library',
 ));
 
 // intl

+ 4 - 1
app/config/config.yml

@@ -62,4 +62,7 @@ fos_user:
     use_username_form_type: true
 #    from_email:
 #        address:        webmaster@example.com
-#        sender_name:    Admin
+#        sender_name:    Admin
+
+nelmio_solarium:
+    adapter: ~

+ 1 - 0
app/config/config_dev.yml

@@ -1,5 +1,6 @@
 imports:
     - { resource: config.yml }
+    - { resource: twig_dev.yml }
 
 framework:
     router:   { resource: "%kernel.root_dir%/config/routing_dev.yml" }

+ 5 - 0
app/config/twig_dev.yml

@@ -0,0 +1,5 @@
+services:
+    twig.extension.debug:
+        class: Twig_Extensions_Extension_Debug
+        tags:
+            - { name: twig.extension }

+ 8 - 0
deps

@@ -72,3 +72,11 @@
 [WhiteOctoberPagerfantaBundle]
     git=http://github.com/whiteoctober/WhiteOctoberPagerfantaBundle.git
     target=/bundles/WhiteOctober/PagerfantaBundle
+
+[solarium]
+    git=https://github.com/basdenooijer/solarium.git
+    version=2.3.0-RC1
+
+[NelmioSolariumBundle]
+    git=https://github.com/nelmio/NelmioSolariumBundle.git
+    target=/bundles/Nelmio/SolariumBundle

+ 3 - 1
deps.lock

@@ -15,4 +15,6 @@ SensioGeneratorBundle c6af9719ae9e81fa4e086f40697d35e7090921cc
 AsseticBundle 41b5913b5086a0909af92adcb4a6005ee0051b16
 FOSUserBundle 67300def4b2628512197c1fe05cd25727c309e20
 WhiteOctoberPagerfanta bbbf256d830a6036a6a7671d6b1dc3038e38294a
-WhiteOctoberPagerfantaBundle 60db33325971201cfacb3cbe90d8e25c8c89fd66
+WhiteOctoberPagerfantaBundle 60db33325971201cfacb3cbe90d8e25c8c89fd66
+solarium 672d77be590bdbbd919fd4e3bfdcdd6107d08807
+NelmioSolariumBundle fa3a47ef54b6c909def9ac58a09e1c6fded2331b

+ 126 - 0
src/Packagist/WebBundle/Command/IndexPackagesCommand.php

@@ -0,0 +1,126 @@
+<?php
+
+/*
+ * This file is part of Packagist.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *     Nils Adermann <naderman@naderman.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Packagist\WebBundle\Command;
+
+use Packagist\WebBundle\Entity\Package;
+
+use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class IndexPackagesCommand extends ContainerAwareCommand
+{
+    /**
+     * {@inheritdoc}
+     */
+    protected function configure()
+    {
+        $this
+            ->setName('packagist:index')
+            ->setDefinition(array(
+                new InputOption('force', null, InputOption::VALUE_NONE, 'Force a re-indexing of all packages'),
+                new InputArgument('package', InputArgument::OPTIONAL, 'Package name to index'),
+            ))
+            ->setDescription('Indexes packages')
+            ->setHelp(<<<EOF
+
+EOF
+            )
+        ;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $verbose = $input->getOption('verbose');
+        $force = $input->getOption('force');
+        $package = $input->getArgument('package');
+
+        $doctrine = $this->getContainer()->get('doctrine');
+        $solarium = $this->getContainer()->get('solarium.client');
+
+        if ($force && !$package) {
+            if ($verbose) {
+                $output->writeln('Deleting existing index');
+            }
+
+            $update = $solarium->createUpdate();
+
+            $update->addDeleteQuery('*:*');
+            $update->addCommit();
+
+            $solarium->update($update);
+
+            $doctrine
+                ->getEntityManager()
+                ->createQuery('UPDATE PackagistWebBundle:Package p SET p.indexedAt = NULL')
+                ->getResult();
+        }
+
+        if ($package) {
+            $packages = array($doctrine->getRepository('PackagistWebBundle:Package')->findOneByName($package));
+        } elseif ($force) {
+            $packages = $doctrine->getRepository('PackagistWebBundle:Package')->findAll();
+        } else {
+            $packages = $doctrine->getRepository('PackagistWebBundle:Package')->getStalePackagesForIndexing();
+        }
+
+        foreach ($packages as $package) {
+            if ($verbose) {
+                $output->writeln('Indexing '.$package->getName());
+            }
+
+            try {
+                $update = $solarium->createUpdate();
+
+                $document = $update->createDocument();
+
+                $this->updateDocumentFromPackage($document, $package);
+
+                $update->addDocument($document);
+                $update->addCommit();
+
+                $solarium->update($update);
+
+                $package->setIndexedAt(new \DateTime);
+
+                $em = $doctrine->getEntityManager();
+                $em->flush();
+            } catch (\Exception $e) {
+                $output->writeln('<error>Exception: '.$e->getMessage().', skipping package '.$package->getName().'.</error>');
+            }
+        }
+    }
+
+    private function updateDocumentFromPackage(\Solarium_Document_ReadWrite $document, Package $package)
+    {
+        $document->id = $package->getId();
+        $document->name = $package->getName();
+        $document->description = $package->getDescription();
+
+        $tags = array();
+        foreach ($package->getVersions() as $version) {
+            foreach ($version->getTags() as $tag) {
+                $tags[] = $tag->getName();
+            }
+        }
+        $document->tags = array_unique($tags);
+    }
+}

+ 9 - 6
src/Packagist/WebBundle/Command/UpdatePackagesCommand.php

@@ -16,6 +16,7 @@ use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
 use Symfony\Bridge\Doctrine\RegistryInterface;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\HttpKernel\KernelInterface;
 use Symfony\Component\Finder\Finder;
@@ -53,7 +54,7 @@ class UpdatePackagesCommand extends ContainerAwareCommand
             ->setName('packagist:update')
             ->setDefinition(array(
                 new InputOption('force', null, InputOption::VALUE_NONE, 'Force a re-crawl of all packages'),
-                new InputOption('package', null, InputOption::VALUE_NONE, 'Package name to update (implicitly enables --force)'),
+                new InputArgument('package', InputArgument::OPTIONAL, 'Package name to update (implicitly enables --force)'),
             ))
             ->setDescription('Updates packages')
             ->setHelp(<<<EOF
@@ -69,15 +70,17 @@ EOF
     protected function execute(InputInterface $input, OutputInterface $output)
     {
         $verbose = $input->getOption('verbose');
-        $doctrine = $this->getContainer()->get('doctrine');
+        $force = $input->getOption('force');
+        $package = $input->getArgument('package');
 
+        $doctrine = $this->getContainer()->get('doctrine');
         $logger = $this->getContainer()->get('logger');
 
         $this->versionParser = new VersionParser;
 
-        if ($input->getOption('package')) {
-            $packages = array($doctrine->getRepository('PackagistWebBundle:Package')->findOneByName($input->getOption('package')));
-        } elseif ($input->getOption('force')) {
+        if ($package) {
+            $packages = array($doctrine->getRepository('PackagistWebBundle:Package')->findOneByName($package));
+        } elseif ($force) {
             $packages = $doctrine->getRepository('PackagistWebBundle:Package')->findAll();
         } else {
             $packages = $doctrine->getRepository('PackagistWebBundle:Package')->getStalePackages();
@@ -98,7 +101,7 @@ EOF
 
             try {
                 // clear versions to force a clean reloading if --force is enabled
-                if ($input->getOption('force')) {
+                if ($force) {
                     $versionRepo = $doctrine->getRepository('PackagistWebBundle:Version');
                     foreach ($package->getVersions() as $version) {
                         $versionRepo->remove($version);

+ 48 - 1
src/Packagist/WebBundle/Controller/WebController.php

@@ -14,6 +14,8 @@ namespace Packagist\WebBundle\Controller;
 
 use Packagist\WebBundle\Form\Type\AddMaintainerRequestType;
 use Packagist\WebBundle\Form\Model\AddMaintainerRequest;
+use Packagist\WebBundle\Form\Type\SearchQueryType;
+use Packagist\WebBundle\Form\Model\SearchQuery;
 use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 use Packagist\WebBundle\Entity\Package;
 use Packagist\WebBundle\Entity\Version;
@@ -24,9 +26,11 @@ use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
 use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
 use Symfony\Component\Security\Core\Exception\AccessDeniedException;
 use Pagerfanta\Pagerfanta;
 use Pagerfanta\Adapter\DoctrineORMAdapter;
+use Pagerfanta\Adapter\SolariumAdapter;
 
 /**
  * @author Jordi Boggiano <j.boggiano@seld.be>
@@ -44,7 +48,10 @@ class WebController extends Controller
      */
     public function indexAction()
     {
-        return array('page' => 'home');
+        $searchQuery = new SearchQuery;
+        $form = $this->createForm(new SearchQueryType, $searchQuery);
+
+        return array('page' => 'home', 'form' => $form->createView());
     }
 
     /**
@@ -76,6 +83,46 @@ class WebController extends Controller
         return $this->render('PackagistWebBundle:Web:browse.html.twig', array('packages' => $paginator));
     }
 
+    /**
+     * @Template()
+     * @Route("/search/", name="search")
+     */
+    public function searchAction(Request $req)
+    {
+        $searchQuery = new SearchQuery;
+        $form = $this->createForm(new SearchQueryType, $searchQuery);
+
+        if ($req->query->has('search_query')) {
+            $form->bindRequest($req);
+            if ($form->isValid()) {
+                $solarium = $this->get('solarium.client');
+
+                $select = $solarium->createSelect();
+
+                $dismax = $select->getDisMax();
+                $dismax->setQueryFields(array('name', 'description', 'tags'));
+                $dismax->setBoostQuery('name:"'.$searchQuery->getQuery().'"^2');
+                $dismax->setQueryParser('edismax');
+                $select->setQuery($searchQuery->getQuery());
+
+                $paginator = new Pagerfanta(new SolariumAdapter($solarium, $select));
+                $paginator->setMaxPerPage(15);
+                $paginator->setCurrentPage($req->query->get('page', 1), false, true);
+
+                if ($req->isXmlHttpRequest()) {
+                    return $this->render('PackagistWebBundle:Web:list.html.twig', array(
+                        'packages' => $paginator,
+                        'noLayout' => true,
+                    ));
+                }
+
+                return $this->render('PackagistWebBundle:Web:search.html.twig', array('packages' => $paginator, 'form' => $form->createView()));
+            }
+        }
+
+        return $this->render('PackagistWebBundle:Web:search.html.twig', array('form' => $form->createView()));
+    }
+
     /**
      * @Template()
      * @Route("/packages/submit", name="submit")

+ 25 - 0
src/Packagist/WebBundle/Entity/Package.php

@@ -88,6 +88,11 @@ class Package
      */
     private $crawledAt;
 
+    /**
+     * @ORM\Column(type="datetime", nullable=true)
+     */
+    private $indexedAt;
+
     private $entityRepository;
 
     public function __construct()
@@ -347,6 +352,26 @@ class Package
         return $this->crawledAt;
     }
 
+    /**
+     * Set indexedAt
+     *
+     * @param datetime $indexedAt
+     */
+    public function setIndexedAt($indexedAt)
+    {
+        $this->indexedAt = $indexedAt;
+    }
+
+    /**
+     * Get indexedAt
+     *
+     * @return datetime $indexedAt
+     */
+    public function getIndexedAt()
+    {
+        return $this->indexedAt;
+    }
+
     /**
      * Add maintainers
      *

+ 12 - 0
src/Packagist/WebBundle/Entity/PackageRepository.php

@@ -30,6 +30,18 @@ class PackageRepository extends EntityRepository
         return $qb->getQuery()->getResult();
     }
 
+    public function getStalePackagesForIndexing()
+    {
+        $qb = $this->getEntityManager()->createQueryBuilder();
+        $qb->select('p, v, t')
+            ->from('Packagist\WebBundle\Entity\Package', 'p')
+            ->leftJoin('p.versions', 'v')
+            ->leftJoin('v.tags', 't')
+            ->where('p.indexedAt IS NULL OR p.indexedAt < ?0')
+            ->setParameters(array(new \DateTime('-1hour')));
+        return $qb->getQuery()->getResult();
+    }
+
     public function findOneByName($name)
     {
         $qb = $this->getBaseQueryBuilder()

+ 33 - 0
src/Packagist/WebBundle/Form/Model/SearchQuery.php

@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of Packagist.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *     Nils Adermann <naderman@naderman.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Packagist\WebBundle\Form\Model;
+
+use Symfony\Component\Validator\Constraints as Assert;
+
+class SearchQuery
+{
+    /**
+     * @Assert\NotBlank()
+     */
+    protected $query;
+
+    public function setQuery($query)
+    {
+        $this->query = $query;
+    }
+
+    public function getQuery()
+    {
+        return $this->query;
+    }
+}

+ 40 - 0
src/Packagist/WebBundle/Form/Type/SearchQueryType.php

@@ -0,0 +1,40 @@
+<?php
+
+/*
+ * This file is part of Packagist.
+ *
+ * (c) Jordi Boggiano <j.boggiano@seld.be>
+ *     Nils Adermann <naderman@naderman.de>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Packagist\WebBundle\Form\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilder;
+
+/**
+ * @author Igor Wiedler <igor@wiedler.ch>
+ */
+class SearchQueryType extends AbstractType
+{
+    public function buildForm(FormBuilder $builder, array $options)
+    {
+        $builder->add('query', 'search');
+    }
+
+    public function getDefaultOptions(array $options)
+    {
+        return array(
+            'data_class' => 'Packagist\WebBundle\Form\Model\SearchQuery',
+            'csrf_protection' => false,
+        );
+    }
+
+    public function getName()
+    {
+        return 'search_query';
+    }
+}

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

@@ -337,7 +337,7 @@ input[type="submit"].loading {
   background-image: url("../img/loader.gif");
 }
 
-input[type="text"], input[type="password"], input[type="email"] {
+input[type="text"], input[type="password"], input[type="email"], input[type="search"] {
   padding: 4px;
   background-color: #fff;
   border: 1px solid #ccc;
@@ -346,16 +346,22 @@ input[type="text"], input[type="password"], input[type="email"] {
   border-radius: 6px;
   box-shadow: none;
 }
-input[type="text"]:hover, input[type="password"]:hover, input[type="email"]:hover,
-input[type="text"]:focus, input[type="password"]:focus, input[type="email"]:focus {
+input[type="text"]:hover, input[type="password"]:hover, input[type="email"]:hover, input[type="search"]:hover,
+input[type="text"]:focus, input[type="password"]:focus, input[type="email"]:focus, input[type="search"]:focus {
   border-color: #64c523;
   outline-style: none;
 }
-input[type="text"]:invalid, input[type="password"]:invalid, input[type="email"]:invalid {
+input[type="text"]:invalid, input[type="password"]:invalid, input[type="email"]:invalid, input[type="search"]:invalid {
   border-color: #c67700;
   color: #bf7300;
 }
 
+input[type="search"] {
+  -moz-appearance:none;
+  -webkit-appearance:none;
+  font-size: 25px;
+}
+
 
 input[type="checkbox"] {
   float: left;

+ 18 - 5
src/Packagist/WebBundle/Resources/public/js/layout.js

@@ -1,9 +1,22 @@
+"use strict";
+
 /*
     Adjust width for packades data
 */
-$(window).load(function()
-{
+$(window).load(function() {
     var row = $('ul.packages');
-    if(!row.length) return;
-    $('div.package-details > div').css('min-width', Math.max(400, Math.floor(860 - row.width())) + 'px');
-});
+    if (row.length) {
+        $('div.package-details > div').css({
+            'min-width': Math.max(400, Math.floor(860 - row.width())) + 'px'
+        });
+    }
+});
+
+/*
+    Ajax error handler
+*/
+$.ajaxSetup({
+    error: function (xhr) {
+        humane.info("We're so sorry, something is wrong on our end.");
+    }
+})

+ 55 - 0
src/Packagist/WebBundle/Resources/public/js/search.js

@@ -0,0 +1,55 @@
+"use strict";
+
+(function ($) {
+    var form = $('form#search-form'),
+        showResults,
+        doSearch,
+        searching = false,
+        searchQueued = false,
+        previousQuery;
+
+    showResults = function (page) {
+        var list = $('.package-list'),
+            newList = $(page);
+
+        if (newList.find('.packages li').length) {
+            list.replaceWith(newList);
+            list.show();
+        } else {
+            list.hide();
+        }
+
+        searching = false;
+
+        if (searchQueued) {
+            doSearch();
+            searchQueued = false;
+        }
+    };
+
+    doSearch = function () {
+        var currentQuery;
+
+        if (searching) {
+            searchQueued = true;
+            return;
+        }
+
+        currentQuery = form.serialize();
+
+        if (previousQuery === currentQuery) {
+            return;
+        }
+
+        $.ajax({
+            url: form.attr('action'),
+            data: currentQuery,
+            success: showResults
+        });
+
+        searching = true;
+        previousQuery = currentQuery;
+    };
+
+    form.bind('keyup search', doSearch);
+})(jQuery);

+ 15 - 0
src/Packagist/WebBundle/Resources/views/Web/index.html.twig

@@ -1,5 +1,9 @@
 {% extends "PackagistWebBundle::layout.html.twig" %}
 
+{% block scripts %}
+    <script src="{{ asset('bundles/packagistweb/js/search.js?v=2')}}"></script>
+{% endblock %}
+
 {% block content %}
     <div class="box">
         <p>Packagist is the main <a href="{{ path('about.composer') }}">Composer</a> repository. It aggregates all sorts of PHP packages that are installable with Composer.<br /><a href="{{ path('browse') }}">Browse packages</a> or <a href="{{ path('submit') }}">submit your own</a>.</p>
@@ -7,6 +11,17 @@
 
     &nbsp;
 
+    <div class="box">
+        {% include "PackagistWebBundle:Web:searchForm.html.twig" %}
+    </div>
+
+    &nbsp;
+
+    <div class="box package-list hidden">
+    </div>
+
+    &nbsp;
+
     <div class="box clearfix">
         <section class="getting-started">
             <h1>Getting Started</h1>

+ 2 - 2
src/Packagist/WebBundle/Resources/views/Web/list.html.twig

@@ -1,9 +1,9 @@
-{% extends "PackagistWebBundle::layout.html.twig" %}
+{% extends noLayout|default(false) ? "::base_nolayout.html.twig" : "PackagistWebBundle::layout.html.twig" %}
 
 {% import "PackagistWebBundle::macros.html.twig" as macros %}
 
 {% block content %}
-    <div class="box clearfix">
+    <div class="box clearfix package-list">
         {% block content_title %}<h1>Packages</h1>{% endblock %}
         {% if packages|length %}
             {{ macros.listPackages(packages, paginate is not defined or paginate) }}

+ 18 - 0
src/Packagist/WebBundle/Resources/views/Web/search.html.twig

@@ -0,0 +1,18 @@
+{% extends "PackagistWebBundle:Web:list.html.twig" %}
+
+{% block scripts %}
+    <script src="{{ asset('bundles/packagistweb/js/search.js?v=2')}}"></script>
+{% endblock %}
+
+{% block content %}
+    <div class="box clearfix">
+        <h1>Search packages</h1>
+        {% include "PackagistWebBundle:Web:searchForm.html.twig" %}
+    </div>
+
+    &nbsp;
+
+    {% if packages is defined %}
+        {{ parent() }}
+    {% endif %}
+{% endblock %}

+ 10 - 0
src/Packagist/WebBundle/Resources/views/Web/searchForm.html.twig

@@ -0,0 +1,10 @@
+<form id="search-form" action="{{ path('search') }}" method="GET" {{ form_enctype(form) }} autocomplete="off">
+    <div>
+        <p>
+            {{ form_errors(form.query) }}
+            {{ form_widget(form.query, {'attr': {'autocomplete': 'off'}}) }}
+        </p>
+        {{ form_rest(form) }}
+        <input id="submit" type="submit" value="Search" />
+    </div>
+</form>

+ 6 - 1
src/Packagist/WebBundle/Resources/views/layout.html.twig

@@ -18,8 +18,12 @@
         <![endif]-->
 
         <link rel="stylesheet" href="{{ asset('bundles/packagistweb/css/main.css?v=2') }}" />
+        <link rel="stylesheet" href="{{ asset('css/humane/jackedup.css?v=2') }}" />
 
-        {# {% stylesheets '@PackagistWebBundle/Resources/public/css/main.css' filter="yui_css" output='css/main.css' %}
+        {# {% stylesheets
+            '@PackagistWebBundle/Resources/public/css/main.css'
+            'css/humane/jackedup.css'
+            filter="yui_css" output='css/main.css' %}
             <link rel="stylesheet" href="{{ asset_url }}" />
         {% endstylesheets %} #}
 
@@ -86,6 +90,7 @@
 
         <script src="//ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js"></script>
         <script>!window.jQuery && document.write(unescape('%3Cscript src="/js/libs/jquery-1.5.2.min.js"%3E%3C/script%3E'))</script>
+        <script src="{{ asset('js/libs/humane.min.js') }}"></script>
         <script src="{{ asset('bundles/packagistweb/js/layout.js') }}"></script>
 
         {% if not app.debug and google_analytics.ga_key %}

+ 2 - 2
src/Packagist/WebBundle/Resources/views/macros.html.twig

@@ -3,8 +3,8 @@
         {% for package in packages %}
             <li>
                 <h1><a href="{{ path('view_package', { 'name' : package.name }) }}">{{ package.name }}</a></h1>
-                {% if package.versions|length and package.versions[0].description|length %}
-                    <p class="package-description">{{ package.versions[0].description }}</p>
+                {% if package.description %}
+                    <p class="package-description">{{ package.description }}</p>
                 {% endif %}
             </li>
         {% endfor %}

+ 124 - 0
web/css/humane/jackedup.css

@@ -0,0 +1,124 @@
+html,
+body {
+  height: 100%;
+}
+.humane {
+  position: fixed;
+  -moz-transition: all 0.6s ease-in-out;
+  -webkit-transition: all 0.6s ease-in-out;
+  -ms-transition: all 0.6s ease-in-out;
+  -o-transition: all 0.6s ease-in-out;
+  transition: all 0.6s ease-in-out;
+  z-index: -1;
+}
+.humane.humane-animate,
+.humane.humane-js-animate {
+  z-index: 100000;
+}
+.humane {
+  font-family: Helvetica Neue, Helvetica, san-serif;
+  font-size: 18px;
+  letter-spacing: -1px;
+  top: 20px;
+  left: 30%;
+  opacity: 0;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0);
+  width: 40%;
+  color: #333;
+  padding: 10px;
+  text-align: center;
+  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAACWCAYAAAAfduJyAAAABmJLR0QA/wD/AP+gvaeTAAAAIklEQVQokWNgYGCQZGJgYGDARTDSQnboGDqsnDt0DKWNLAAkiQFdC+vZNQAAAABJRU5ErkJggg==');
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0, rgba(0,0,0,0.10)), color-stop(1, rgba(0,0,0,0.20))) no-repeat;
+  background: -moz-linear-gradient(top, rgba(0,0,0,0.10) 0%, rgba(0,0,0,0.20) 100%) no-repeat;
+  background: -webkit-linear-gradient(top, rgba(0,0,0,0.10) 0%, rgba(0,0,0,0.20) 100%) no-repeat;
+  background: -ms-linear-gradient(top, rgba(0,0,0,0.10) 0%, rgba(0,0,0,0.20) 100%) no-repeat;
+  background: -o-linear-gradient(top, rgba(0,0,0,0.10) 0%, rgba(0,0,0,0.20) 100%) no-repeat;
+  background: linear-gradient(top, rgba(0,0,0,0.10) 0%, rgba(0,0,0,0.20) 100%) no-repeat;
+  background-color: #fff;
+  -moz-border-radius: 3px;
+  -webkit-border-radius: 3px;
+  -ms-border-radius: 3px;
+  -o-border-radius: 3px;
+  border-radius: 3px;
+  text-shadow: 0 1px 1px rgba(255,255,255,0.80);
+  -moz-box-shadow: 0 1px 2px rgba(0,0,0,0.50);
+  -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.50);
+  -ms-box-shadow: 0 1px 2px rgba(0,0,0,0.50);
+  -o-box-shadow: 0 1px 2px rgba(0,0,0,0.50);
+  box-shadow: 0 1px 2px rgba(0,0,0,0.50);
+  -moz-transform: translateY(-100px);
+  -webkit-transform: translateY(-100px);
+  -ms-transform: translateY(-100px);
+  -o-transform: translateY(-100px);
+  transform: translateY(-100px);
+}
+.humane p,
+.humane ul {
+  margin: 0;
+  padding: 0;
+}
+.humane ul {
+  list-style: none;
+}
+.humane.humane-info {
+  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADICAYAAAAp8ov1AAAABmJLR0QA/wD/AP+gvaeTAAAAR0lEQVQokWNISfn/n4mBgeE/EwMDAwMqQYQYmdoGlxgjI4rY//+Dx2nUFRsQZ2ALTrQQp8QL1DWeqASC014y7aCx8QwMDAwA1aZBIulmpvwAAAAASUVORK5CYII=');
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0, rgba(0,0,0,0.70)), color-stop(1, rgba(0,0,0,0.85))) no-repeat;
+  background: -moz-linear-gradient(top, rgba(0,0,0,0.70) 0%, rgba(0,0,0,0.85) 100%) no-repeat;
+  background: -webkit-linear-gradient(top, rgba(0,0,0,0.70) 0%, rgba(0,0,0,0.85) 100%) no-repeat;
+  background: -ms-linear-gradient(top, rgba(0,0,0,0.70) 0%, rgba(0,0,0,0.85) 100%) no-repeat;
+  background: -o-linear-gradient(top, rgba(0,0,0,0.70) 0%, rgba(0,0,0,0.85) 100%) no-repeat;
+  background: linear-gradient(top, rgba(0,0,0,0.70) 0%, rgba(0,0,0,0.85) 100%) no-repeat;
+  background-color: #fff;
+  color: #fff;
+  text-shadow: 0 -1px 1px rgba(0,0,0,0.35);
+}
+.humane.humane-success {
+  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADICAYAAAAp8ov1AAAABmJLR0QA/wD/AP+gvaeTAAAASElEQVQokc2SMQ4AIAgDD9/K/79QVzWaENTownAJbWnA5SqACkA/Aiy59hczrGVC30Q7y57EmNU5NL5zwln50IMsfZMel+UBKtFBQSLWM9wLAAAAAElFTkSuQmCC');
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #62c462), color-stop(1, #57a957)) no-repeat;
+  background: -moz-linear-gradient(top, #62c462 0%, #57a957 100%) no-repeat;
+  background: -webkit-linear-gradient(top, #62c462 0%, #57a957 100%) no-repeat;
+  background: -ms-linear-gradient(top, #62c462 0%, #57a957 100%) no-repeat;
+  background: -o-linear-gradient(top, #62c462 0%, #57a957 100%) no-repeat;
+  background: linear-gradient(top, #62c462 0%, #57a957 100%) no-repeat;
+  background-color: #64ff64;
+  color: #fff;
+  text-shadow: 0 -1px 1px rgba(0,0,0,0.35);
+}
+.humane.humane-error {
+  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADICAIAAACmkByiAAAABmJLR0QA/wD/AP+gvaeTAAAAf0lEQVQokY2TOQ7AIAwER/5mivy/yRc2RQDhA0jhghFYO5bhuS+TZMAoIUMEhhH4loGhfu71cenM3DutWMsaeGKjv3zO5N17KLPJ0+fQD8cpv5uVLPo4vnX0PpXj0nuaaeVzdmw+yXG1O96n2p3kozB757Ni1Z5UPsU9SP8AeAG1kHXE+7RlPAAAAABJRU5ErkJggg==');
+  background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #ee5f5b), color-stop(1, #c43c35)) no-repeat;
+  background: -moz-linear-gradient(top, #ee5f5b 0%, #c43c35 100%) no-repeat;
+  background: -webkit-linear-gradient(top, #ee5f5b 0%, #c43c35 100%) no-repeat;
+  background: -ms-linear-gradient(top, #ee5f5b 0%, #c43c35 100%) no-repeat;
+  background: -o-linear-gradient(top, #ee5f5b 0%, #c43c35 100%) no-repeat;
+  background: linear-gradient(top, #ee5f5b 0%, #c43c35 100%) no-repeat;
+  background-color: #ee5f5b;
+  color: #fff;
+  text-shadow: 0 -1px 1px rgba(0,0,0,0.35);
+}
+.humane.humane-animate {
+  opacity: 1;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
+  -moz-transform: translateY(0);
+  -webkit-transform: translateY(0);
+  -ms-transform: translateY(0);
+  -o-transform: translateY(0);
+  transform: translateY(0);
+}
+.humane.humane-animate:hover {
+  opacity: 0.7;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70);
+}
+.humane.humane-js-animate {
+  opacity: 1;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
+  -moz-transform: translateY(0);
+  -webkit-transform: translateY(0);
+  -ms-transform: translateY(0);
+  -o-transform: translateY(0);
+  transform: translateY(0);
+}
+.humane.humane-js-animate:hover {
+  opacity: 0.7;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70);
+}

File diff suppressed because it is too large
+ 11 - 0
web/js/libs/humane.min.js


Some files were not shown because too many files changed in this diff