From b9ae4396e52e3e83fda2025b45f3076db9d72ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Thu, 24 Aug 2017 12:35:31 +0200 Subject: [PATCH] Orgs: show "My Organizations" in the user's menu This is shown only when the user is member of or administrator for one or more organizations, otherwise it's hidden. --- pillar/api/organizations/__init__.py | 15 +++++++++++++++ pillar/auth/__init__.py | 19 +++++++++++++++++-- pillar/web/jinja.py | 2 ++ src/templates/_macros/_menu.jade | 9 ++++++++- tests/test_api/test_organizations.py | 8 ++++++++ 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/pillar/api/organizations/__init__.py b/pillar/api/organizations/__init__.py index 1bbe8e2e..27463eb9 100644 --- a/pillar/api/organizations/__init__.py +++ b/pillar/api/organizations/__init__.py @@ -361,6 +361,21 @@ class OrgManager: projection={'_id': 1, 'full_name': 1, 'email': 1}) return list(users) + def user_has_organizations(self, user_id: bson.ObjectId) -> bool: + """Returns True iff the user has anything to do with organizations. + + That is, if the user is admin for and/or member of any organization. + """ + + org_coll = current_app.db('organizations') + + org_count = org_coll.count({'$or': [ + {'admin_uid': user_id}, + {'members': user_id} + ]}) + + return bool(org_count) + def setup_app(app): from . import patch, hooks diff --git a/pillar/auth/__init__.py b/pillar/auth/__init__.py index b0693001..13ef6877 100644 --- a/pillar/auth/__init__.py +++ b/pillar/auth/__init__.py @@ -42,11 +42,14 @@ class UserClass(flask_login.UserMixin): self.group_ids: typing.List[bson.ObjectId] = [] self.capabilities: typing.Set[str] = set() + # Lazily evaluated + self._has_organizations: typing.Optional[bool] = None + @classmethod def construct(cls, token: str, db_user: dict) -> 'UserClass': """Constructs a new UserClass instance from a Mongo user document.""" - user = UserClass(token) + user = cls(token) user.user_id = db_user['_id'] user.roles = db_user.get('roles') or [] @@ -63,7 +66,7 @@ class UserClass(flask_login.UserMixin): return user - def __str__(self): + def __repr__(self): return f'UserClass(user_id={self.user_id})' def __getitem__(self, item): @@ -138,6 +141,15 @@ class UserClass(flask_login.UserMixin): return not bool(require_roles) or bool(intersection) + def has_organizations(self) -> bool: + """Returns True iff this user administers or is member of any organization.""" + + if self._has_organizations is None: + assert self.user_id + self._has_organizations = current_app.org_manager.user_has_organizations(self.user_id) + + return bool(self._has_organizations) + class AnonymousUser(flask_login.AnonymousUserMixin, UserClass): def __init__(self): @@ -149,6 +161,9 @@ class AnonymousUser(flask_login.AnonymousUserMixin, UserClass): def has_cap(self, *capabilities): return False + def has_organizations(self) -> bool: + return False + def _load_user(token) -> typing.Union[UserClass, AnonymousUser]: """Loads a user by their token. diff --git a/pillar/web/jinja.py b/pillar/web/jinja.py index ae097aa9..04a7c280 100644 --- a/pillar/web/jinja.py +++ b/pillar/web/jinja.py @@ -4,6 +4,7 @@ import logging import typing import flask +import flask_login import jinja2.filters import jinja2.utils import werkzeug.exceptions as wz_exceptions @@ -157,3 +158,4 @@ def setup_jinja_env(jinja_env): jinja_env.filters['repr'] = repr jinja_env.globals['url_for_node'] = do_url_for_node jinja_env.globals['session'] = flask.session + jinja_env.globals['current_user'] = flask_login.current_user diff --git a/src/templates/_macros/_menu.jade b/src/templates/_macros/_menu.jade index d7671c80..6f3d5d24 100644 --- a/src/templates/_macros/_menu.jade +++ b/src/templates/_macros/_menu.jade @@ -62,7 +62,14 @@ li(class="dropdown") title="My Projects") i.pi-star | My Projects - + | {% if current_user.has_organizations() %} + li + a.navbar-item( + href="{{ url_for('pillar.web.organizations.index') }}" + title="My Organizations") + i.pi-users + | My Organizations + | {% endif %} li a.navbar-item( href="{{ url_for('users.settings_profile') }}" diff --git a/tests/test_api/test_organizations.py b/tests/test_api/test_organizations.py index dfc0718a..62c6106e 100644 --- a/tests/test_api/test_organizations.py +++ b/tests/test_api/test_organizations.py @@ -203,6 +203,8 @@ class OrganizationPatchTest(AbstractPillarTest): org_doc = om.create_new_org('Хакеры', admin_uid, 25) org_id = org_doc['_id'] + self.assertFalse(om.user_has_organizations(member1_uid)) + # Try the PATCH resp = self.patch(f'/api/organizations/{org_id}', json={ @@ -218,6 +220,9 @@ class OrganizationPatchTest(AbstractPillarTest): self.assertEqual([member1_uid], db_org['members']) self.assertEqual([str(member1_uid)], new_org_doc['members']) + # The user should now have an organization + self.assertTrue(om.user_has_organizations(member1_uid)) + def test_assign_users_access_denied(self): self.enter_app_context() @@ -299,6 +304,7 @@ class OrganizationPatchTest(AbstractPillarTest): org_id = org_doc['_id'] om.assign_users(org_id, ['member1@example.com', 'member2@example.com']) + self.assertTrue(om.user_has_organizations(member_uid)) # Try the PATCH to remove a known user resp = self.patch(f'/api/organizations/{org_id}', @@ -318,6 +324,8 @@ class OrganizationPatchTest(AbstractPillarTest): self.assertEqual([], new_org_doc['members']) self.assertEqual(['member2@example.com'], new_org_doc['unknown_members']) + self.assertFalse(om.user_has_organizations(member_uid)) + # Try the PATCH to remove an unknown user resp = self.patch(f'/api/organizations/{org_id}', json={