Repository: gratipay/gratipay.com

Issue:
API to list members of a community
I could not find it mentioned in https://github.com/gittip/www.gittip.com#api

Is there an API call to fetch the list of members of a given community?

If not, let me ask for one.



Discussion:
There's a json endpoint underlying the listings we have on the site now. Probably just needs to be documented?

Labelled ★★☆.

Could you, please point me to the source of that endpoint?

I lied, sorry. :-(

We're computing the list server-side in https://github.com/gittip/www.gittip.com/blob/f2b3d4a8bc279c267b0be26e86c057eaa49d8e01/www/for/%25slug/index.html.spt. There is no json endpoint for this yet after all.

@whit537 If you don't mind, I could implement it.

@ivanmushketyk Don't ask permission, do it!

@clone1018 I am still new to a gittip, so I am not sure if a task that is not labelled as "Ready to Start" can be done :)


Resolving patch:
diff --git a/gittip/models/community.py b/gittip/models/community.py
--- a/gittip/models/community.py
+++ b/gittip/models/community.py
@@ -55,16 +55,31 @@ def get_list_for(db, username):
 
     """.format(member_test, sort_order), params)
 
-
 class Community(Model):
     """Model a community on Gittip.
     """
 
     typname = "community_summary"
 
+    @classmethod
+    def from_slug(cls, slug):
+        return cls.db.one("""
+            SELECT community_summary.*::community_summary
+            FROM community_summary WHERE slug=%s;
+        """, (slug,))
+
+    def get_members(self, limit=None, offset=None):
+        return self.db.all("""
+            SELECT p.*::participants
+              FROM current_communities c
+              JOIN participants p ON p.username = c.participant
+             WHERE c.slug = %s
+          ORDER BY c.ctime
+             LIMIT %s
+            OFFSET %s;
+        """, (self.slug, limit, offset))
+
     def check_membership(self, participant):
         return self.db.one("""
-
-        SELECT * FROM current_communities WHERE slug=%s AND participant=%s
-
+            SELECT * FROM current_communities WHERE slug=%s AND participant=%s
         """, (self.slug, participant.username)) is not None
diff --git a/gittip/models/participant.py b/gittip/models/participant.py
--- a/gittip/models/participant.py
+++ b/gittip/models/participant.py
@@ -1089,6 +1089,79 @@ def credit_card_expiring(self, request, response):
         response.headers.cookie[NOTIFIED_ABOUT_EXPIRATION] = self.session_token
         return card_expiring
 
+    def to_dict(self, details=False, inquirer=None):
+        output = { 'id': self.id
+                 , 'username': self.username
+                 , 'avatar': self.avatar_url
+                 , 'number': self.number
+                 , 'on': 'gittip'
+                 }
+
+        if not details:
+            return output
+
+        # Key: npatrons
+        output['npatrons'] = self.get_number_of_backers()
+
+        # Key: goal
+        # Values:
+        #   undefined - user is not here to receive tips, but will generally regift them
+        #   null - user has no funding goal
+        #   3.00 - user wishes to receive at least this amount
+        if self.goal != 0:
+            if self.goal > 0:
+                goal = str(self.goal)
+            else:
+                goal = None
+            output['goal'] = goal
+
+        # Key: receiving
+        # Values:
+        #   null - user is receiving anonymously
+        #   3.00 - user receives this amount in tips
+        if not self.anonymous_receiving:
+            receiving = str(self.get_dollars_receiving())
+        else:
+            receiving = None
+        output['receiving'] = receiving
+
+        # Key: giving
+        # Values:
+        #   null - user is giving anonymously
+        #   3.00 - user gives this amount in tips
+        if not self.anonymous_giving:
+            giving = str(self.get_dollars_giving())
+        else:
+            giving = None
+        output['giving'] = giving
+
+        # Key: my_tip
+        # Values:
+        #   undefined - user is not authenticated
+        #   "self" - user == participant
+        #   null - user has never tipped this person
+        #   0.00 - user used to tip this person but now doesn't
+        #   3.00 - user tips this person this amount
+        if inquirer:
+            if inquirer.username == self.username:
+                my_tip = 'self'
+            else:
+                my_tip = inquirer.get_tip_to(self.username)
+            output['my_tip'] = str(my_tip)
+
+        # Key: elsewhere
+        accounts = self.get_accounts_elsewhere()
+        elsewhere = output['elsewhere'] = {}
+        for platform, account in accounts.items():
+            fields = ['id', 'user_id', 'user_name']
+            elsewhere[platform] = {k: getattr(account, k, None) for k in fields}
+
+        # Key: bitcoin
+        if self.bitcoin_address is not None:
+            output['bitcoin'] = 'https://blockchain.info/address/%s' % self.bitcoin_address
+
+        return output
+
 
 class NeedConfirmation(Exception):
     """Represent the case where we need user confirmation during a merge.


Test patch:
diff --git a/tests/py/test_for_community_json.py b/tests/py/test_for_community_json.py
new file mode 100644
--- /dev/null
+++ b/tests/py/test_for_community_json.py
@@ -0,0 +1,58 @@
+import json
+
+from gittip.models.community import slugize
+from gittip.testing import Harness
+
+
+class TestForCommunityJson(Harness):
+
+    def setUp(self):
+        Harness.setUp(self)
+        self.add_participant('alice')
+        self.add_participant('bob')
+        carl = self.add_participant('carl')
+        carl.insert_into_communities(False, 'test', 'test')
+
+    def add_participant(self, participant_name):
+        participant = self.make_participant(participant_name)
+        participant.insert_into_communities(True, 'test', slugize('test'))
+        return participant
+
+    def test_get_non_existing_community(self):
+        response = self.client.GxT('/for/NonExisting/index.json')
+        assert response.code == 404
+
+    def test_get_existing_community(self):
+        response = self.client.GET('/for/test/index.json')
+        result = json.loads(response.body)
+        assert len(result['members']) == 2
+        assert result['name'] == 'test'
+
+    def test_post_not_supported(self):
+        response = self.client.PxST('/for/test/index.json')
+        assert response.code == 405
+
+    def test_limit(self):
+        response = self.client.GET('/for/test/index.json?limit=1')
+        result = json.loads(response.body)
+        assert len(result['members']) == 1
+
+    def test_offset(self):
+        response = self.client.GET('/for/test/index.json?offset=1')
+        result = json.loads(response.body)
+        assert len(result['members']) == 1
+
+    def test_max_limit(self):
+        for i in range(110):
+            self.add_participant(str(i))
+        response = self.client.GET('/for/test/index.json?limit=200')
+        result = json.loads(response.body)
+        assert len(result['members']) == 100
+
+    def test_invalid_limit(self):
+        response = self.client.GxT('/for/test/index.json?limit=abc')
+        assert response.code == 400
+
+    def test_invalid_offset(self):
+        response = self.client.GxT('/for/test/index.json?offset=abc')
+        assert response.code == 400