Repository: gratipay/gratipay.com

Issue:
Include npm packages in search results
They should appear under projects.


Discussion:
Actually, this will require us to add paging or otherwise deal with the fact that we now have 400,000 potential results instead of 400.
Looks like our current solution to that is to only return the top 10 results.

Resolving patch:
diff --git a/gratipay/models/team/__init__.py b/gratipay/models/team/__init__.py
--- a/gratipay/models/team/__init__.py
+++ b/gratipay/models/team/__init__.py
@@ -12,6 +12,7 @@
 from .closing import Closing
 from .membership import Membership
 from .package import Package
+from .review_status import ReviewStatus
 from .takes import Takes
 from .tip_migration import TipMigration
 from ...exceptions import InvalidTeamName
@@ -37,7 +38,7 @@ def slugize(name):
     return slug
 
 
-class Team(Model, Available, Closing, Membership, Package, Takes, TipMigration):
+class Team(Model, Available, Closing, Membership, Package, ReviewStatus, Takes, TipMigration):
     """Represent a Gratipay team.
     """
 
@@ -295,14 +296,6 @@ def update_receiving(self, cursor=None):
                             )
 
 
-    @property
-    def status(self):
-        return { None: 'unreviewed'
-               , False: 'rejected'
-               , True: 'approved'
-                }[self.is_approved]
-
-
     def to_dict(self):
         return {
             'homepage': self.homepage,
diff --git a/gratipay/models/team/review_status.py b/gratipay/models/team/review_status.py
new file mode 100644
--- /dev/null
+++ b/gratipay/models/team/review_status.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+
+class ReviewStatus(object):
+    """This is a mixin to provide review status API on the Team object. Teams
+    must be approved before they can use Gratipay to receive and distribute
+    funds.
+    """
+
+    @property
+    def status(self):
+        """The review status of the team.
+        """
+        return { None: 'unreviewed'
+               , False: 'rejected'
+               , True: 'approved'
+                }[self.is_approved]
+
+
+    def update_review_status(self, status, recorder):
+        """Takes the new status as a string and a participant object for the
+        admin making the change.
+        """
+        with self.db.get_cursor() as cursor:
+            is_approved = {'approved': True, 'rejected': False, 'unreviewed': None}[status]
+            if not(is_approved is self.is_approved):
+                self._set_is_approved(cursor, is_approved, recorder)
+
+
+    def _set_is_approved(self, cursor, is_approved, recorder):
+        is_approved = self.db.one("""
+
+            UPDATE teams
+               SET is_approved=%s
+             WHERE slug=%s
+         RETURNING is_approved
+
+            """, (is_approved, self.slug))
+
+        self.set_attributes(is_approved=is_approved)
+
+        status = {True: 'approved', False: 'rejected', None: 'unreviewed'}[is_approved]
+
+        self.app.add_event(cursor, 'team', dict(
+            id=self.id,
+            recorder=dict(id=recorder.id, username=recorder.username),
+            action='set', values=dict(status=status)
+        ))
+
+        if status in ('rejected', 'approved'):
+            from ..participant import Participant
+            owner = Participant.from_username(self.owner)
+            self.app.email_queue.put( owner
+                                    , 'team-'+status
+                                    , team_name=self.name
+                                    , review_url=self.review_url
+                                    , include_unsubscribe=False
+                                    , _user_initiated=False
+                                     )


Test patch:
diff --git a/gratipay/testing/harness.py b/gratipay/testing/harness.py
--- a/gratipay/testing/harness.py
+++ b/gratipay/testing/harness.py
@@ -179,14 +179,7 @@ def make_team(self, *a, **kw):
         if 'ctime' not in _kw:
             _kw['ctime'] = utcnow()
 
-
-        if Participant.from_username(_kw['owner']) is None:
-            self.make_participant( _kw['owner']
-                                 , claimed_time='now'
-                                 , last_paypal_result=''
-                                 , email_address=_kw['owner']+'@example.com'
-                                 , verified_in='TT'
-                                  )
+        self.make_owner(_kw['owner'])
 
         team = self.db.one("""
             INSERT INTO teams
@@ -201,15 +194,36 @@ def make_team(self, *a, **kw):
         return team
 
 
+    def make_owner(self, username='picard'):
+        owner = Participant.from_username(username)
+        if owner is None:
+            owner = self.make_participant( username
+                                         , claimed_time='now'
+                                         , last_paypal_result=''
+                                         , email_address=username+'@example.com'
+                                         , verified_in='TT'
+                                          )
+        return owner
+
+
+    def make_admin(self, username='admin'):
+        return self.make_participant(username, claimed_time='now', is_admin=True)
+
+
     def make_package(self, package_manager=NPM, name='foo', description='Foo fooingly.',
-                                                                     emails=['alice@example.com']):
+                                                      emails=['alice@example.com'], claimed=False):
         """Factory for packages.
         """
         self.db.run( 'INSERT INTO packages (package_manager, name, description, emails) '
                      'VALUES (%s, %s, %s, %s) RETURNING *'
                    , (package_manager, name, description, emails)
                     )
-        return Package.from_names(NPM, name)
+        package = Package.from_names(NPM, name)
+        if claimed:
+            admin = self.make_admin()
+            team = package.get_or_create_linked_team(self.db, self.make_owner('picard'))
+            team.update_review_status('approved', admin)
+        return package
 
 
     def make_participant(self, username, **kw):
diff --git a/tests/py/test_search.py b/tests/py/test_search.py
--- a/tests/py/test_search.py
+++ b/tests/py/test_search.py
@@ -75,3 +75,17 @@ def test_excerpts_statement_appropriately(self):
         alice.upsert_statement('en', FULL)
         assert ('&middot; &hellip; consecrate—we can not <b>hallow</b>—this ground. The brave '
                 'men, living &hellip;') in self.search('hallow')
+
+    def test_includes_unclaimed_packages_with_projects(self):
+        self.make_package()
+        body = self.search('fo')
+        assert 'foo' in body
+        assert 'has not joined' in body
+        assert 'owned by'       not in body
+
+    def test_does_not_include_claimed_packages(self):
+        self.make_package(claimed=True)
+        body = self.search('fo')
+        assert 'foo' in body
+        assert 'has not joined' not in body
+        assert 'owned by'       in body