Skip to content

Commit 66a9181

Browse files
authored
Merge pull request #90 from silverlogic/user-object-type
baseapp-auth[graphql]: add UserObjectType and UserQueries
2 parents dca7242 + df3dc80 commit 66a9181

File tree

48 files changed

+462
-138
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+462
-138
lines changed

.github/workflows/project-workflow.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
runs-on: ubuntu-latest
1919
strategy:
2020
matrix:
21-
python-version: ["3.11", "3.12"]
21+
python-version: ["3.12"]
2222
django-version: ["4.2", "5.0"]
2323
steps:
2424
- uses: actions/checkout@v4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import django_filters
2+
from django.contrib.auth import get_user_model
3+
from django.db.models import Q
4+
5+
6+
class UsersFilter(django_filters.FilterSet):
7+
q = django_filters.CharFilter(method="search")
8+
9+
order_by = django_filters.OrderingFilter(
10+
fields=(
11+
("date_joined", "date_joined"),
12+
("last_login", "last_login"),
13+
)
14+
)
15+
16+
class Meta:
17+
model = get_user_model()
18+
fields = ["q", "order_by"]
19+
20+
def filtesearchr_q(self, queryset, name, value):
21+
return queryset.filter(Q(first_name__icontains=value) | Q(last_name__icontains=value))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import graphene
2+
from avatar.templatetags.avatar_tags import avatar_url
3+
from baseapp_core.graphql import DjangoObjectType, File
4+
from django.apps import apps
5+
from django.contrib.auth import get_user_model
6+
from graphene import relay
7+
8+
from .filters import UsersFilter
9+
from .permissions import PermissionsInterface
10+
11+
User = get_user_model()
12+
13+
interfaces = (relay.Node, PermissionsInterface)
14+
15+
if apps.is_installed("baseapp_notifications"):
16+
from baseapp_notifications.graphql.object_types import NotificationsInterface
17+
18+
interfaces += (NotificationsInterface,)
19+
20+
21+
if apps.is_installed("baseapp_pages"):
22+
from baseapp_pages.graphql.object_types import MetadataObjectType, PageInterface
23+
24+
interfaces += (PageInterface,)
25+
26+
27+
class AbstractUserObjectType(object):
28+
is_authenticated = graphene.Boolean()
29+
full_name = graphene.String()
30+
short_name = graphene.String()
31+
32+
avatar = graphene.Field(File, width=graphene.Int(), height=graphene.Int())
33+
34+
# Make them not required
35+
email = graphene.String()
36+
phone_number = graphene.String()
37+
is_superuser = graphene.Boolean()
38+
is_staff = graphene.Boolean()
39+
is_email_verified = graphene.Boolean()
40+
password_changed_date = graphene.DateTime()
41+
new_email = graphene.String()
42+
is_new_email_confirmed = graphene.Boolean()
43+
44+
class Meta:
45+
model = User
46+
fields = (
47+
"pk",
48+
"first_name",
49+
"last_name",
50+
"full_name",
51+
"short_name",
52+
"email",
53+
"phone_number",
54+
"is_superuser",
55+
"is_staff",
56+
"is_active",
57+
"is_email_verified",
58+
"date_joined",
59+
"password_changed_date",
60+
"new_email",
61+
"is_new_email_confirmed",
62+
"is_authenticated",
63+
"pages",
64+
"comments",
65+
"reactions",
66+
"last_login",
67+
)
68+
interfaces = interfaces
69+
filterset_class = UsersFilter
70+
71+
def resolve_avatar(self, info, width, height):
72+
return File(url=avatar_url(self, width, height))
73+
74+
def resolve_metadata(self, info):
75+
return MetadataObjectType(
76+
meta_title=self.get_full_name(),
77+
)
78+
79+
def resolve_is_authenticated(self, info):
80+
return info.context.user.is_authenticated and self.pk == info.context.user.pk
81+
82+
def resolve_full_name(self, info):
83+
return self.get_full_name()
84+
85+
def resolve_short_name(self, info):
86+
return self.get_short_name()
87+
88+
def resolve_email(self, info):
89+
return view_user_private_field(self, info, "email")
90+
91+
def resolve_phone_number(self, info):
92+
return view_user_private_field(self, info, "phone_number")
93+
94+
def resolve_is_superuser(self, info):
95+
return view_user_private_field(self, info, "is_superuser")
96+
97+
def resolve_is_staff(self, info):
98+
return view_user_private_field(self, info, "is_staff")
99+
100+
def resolve_is_email_verified(self, info):
101+
return view_user_private_field(self, info, "is_email_verified")
102+
103+
def resolve_password_changed_date(self, info):
104+
return view_user_private_field(self, info, "password_changed_date")
105+
106+
def resolve_new_email(self, info):
107+
return view_user_private_field(self, info, "new_email")
108+
109+
def resolve_is_new_email_confirmed(self, info):
110+
return view_user_private_field(self, info, "is_new_email_confirmed")
111+
112+
@classmethod
113+
def get_queryset(cls, queryset, info):
114+
if info.context.user.is_anonymous:
115+
return queryset.filter(is_active=True)
116+
return queryset
117+
118+
@classmethod
119+
def get_node(self, info, id):
120+
node = super().get_node(info, id)
121+
if not info.context.user.has_perm(f"{User._meta.app_label}.view_user", node):
122+
return None
123+
return node
124+
125+
126+
class UserObjectType(AbstractUserObjectType, DjangoObjectType):
127+
class Meta(AbstractUserObjectType.Meta):
128+
pass
129+
130+
131+
def view_user_private_field(user, info, field_name):
132+
if info.context.user.has_perm(f"{User._meta.app_label}.view_user_{field_name}", user):
133+
return getattr(user, field_name)
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import graphene
2+
from django.utils.translation import gettext_lazy as _
23
from graphene import relay
34

45

56
class PermissionsInterface(relay.Node):
6-
has_perm = graphene.Boolean(perm=graphene.String(required=True))
7+
has_perm = graphene.Boolean(
8+
perm=graphene.String(required=True),
9+
description=_("Determine if the logged in user has a specific permission for this object."),
10+
)
711

812
def resolve_has_perm(self, info, perm, **kwargs):
13+
# Builds a permission string of the form "<app_label>.<perm>_<model_name>"
914
if "." not in perm:
10-
# Builds a permission string of the form "<app_label>.<perm>_<model_name>"
1115
opts = self._meta
12-
codename = "%s_%s" % (perm, opts.model_name)
16+
if "_" not in perm:
17+
codename = "%s_%s" % (perm, opts.model_name)
18+
else:
19+
codename = perm
1320
perm = "%s.%s" % (opts.app_label, codename)
1421

1522
return info.context.user.has_perm(perm, self)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from baseapp_core.graphql import Node
2+
from django.contrib.auth import get_user_model
3+
from graphene import Field
4+
from graphene_django.filter import DjangoFilterConnectionField
5+
6+
from .object_types import UserObjectType
7+
8+
User = get_user_model()
9+
10+
11+
def get_user_queries(CustomObjectType):
12+
class UsersQueries(object):
13+
users = DjangoFilterConnectionField(CustomObjectType)
14+
user = Node.Field(CustomObjectType)
15+
16+
me = Field(CustomObjectType)
17+
18+
def resolve_users(self, info, **kwargs):
19+
if info.context.user.has_perm(f"{User._meta.app_label}.view_all_users"):
20+
return CustomObjectType._meta.model.objects.all()
21+
return CustomObjectType._meta.model.objects.none()
22+
23+
def resolve_me(self, info, **kwargs):
24+
if info.context.user.is_authenticated:
25+
return info.context.user
26+
return None
27+
28+
return UsersQueries
29+
30+
31+
UsersQueries = get_user_queries(UserObjectType)

baseapp-auth/baseapp_auth/models.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,16 @@
1111
from .managers import UserManager
1212

1313

14-
class AbstractUser(PermissionsMixin, AbstractBaseUser):
14+
def use_relay_model():
15+
try:
16+
from baseapp_core.graphql.models import RelayModel
17+
18+
return RelayModel
19+
except ImportError:
20+
return object
21+
22+
23+
class AbstractUser(PermissionsMixin, AbstractBaseUser, use_relay_model()):
1524
email = CaseInsensitiveEmailField(unique=True, db_index=True)
1625
is_email_verified = models.BooleanField(default=False)
1726
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
@@ -54,6 +63,17 @@ class Meta:
5463
abstract = True
5564
verbose_name = _("user")
5665
verbose_name_plural = _("users")
66+
permissions = [
67+
("view_all_users", _("can view all users")),
68+
("view_user_email", _("can view user's email field")),
69+
("view_user_phone_number", _("can view user's phone number field")),
70+
("view_user_is_superuser", _("can view user's is_superuser field")),
71+
("view_user_is_staff", _("can view user's is_staff field")),
72+
("view_user_is_email_verified", _("can view user's is_email_verified field")),
73+
("view_user_password_changed_date", _("can view user's password_changed_date field")),
74+
("view_user_new_email", _("can view user's new_email field")),
75+
("view_user_is_new_email_confirmed", _("can view user's is_new_email_confirmed field")),
76+
]
5777

5878
def __str__(self):
5979
return self.get_full_name()
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from django.contrib.auth import get_user_model
2+
from django.contrib.auth.backends import BaseBackend
3+
4+
User = get_user_model()
5+
6+
private_field_perms = [
7+
f"{User._meta.app_label}.view_user_email",
8+
f"{User._meta.app_label}.view_user_phone_number",
9+
f"{User._meta.app_label}.view_user_is_superuser",
10+
f"{User._meta.app_label}.view_user_is_staff",
11+
f"{User._meta.app_label}.view_user_is_email_verified",
12+
f"{User._meta.app_label}.view_user_password_changed_date",
13+
f"{User._meta.app_label}.view_user_new_email",
14+
f"{User._meta.app_label}.view_user_is_new_email_confirmed",
15+
]
16+
17+
18+
class UsersPermissionsBackend(BaseBackend):
19+
def has_perm(self, user_obj, perm, obj=None):
20+
if perm == f"{User._meta.app_label}.view_user":
21+
# every body can view all users
22+
# TO DO: maybe check if user is_active
23+
return True
24+
25+
if perm == f"{User._meta.app_label}.view_all_users":
26+
return True
27+
28+
if perm in private_field_perms and obj is not None:
29+
if (
30+
isinstance(obj, User)
31+
and user_obj.is_authenticated
32+
and (obj.pk == user_obj.pk or (user_obj.is_superuser or user_obj.is_staff))
33+
):
34+
return True
35+
else:
36+
# Anyone with permission set also can:
37+
return user_obj.has_perm(perm)

baseapp-auth/baseapp_auth/tests/graphql/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from baseapp_core.graphql.testing.fixtures import * # noqa
2+
from baseapp_core.tests.fixtures import * # noqa
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import pytest
2+
3+
pytestmark = pytest.mark.django_db
4+
5+
QUERY = """
6+
query {
7+
me {
8+
id
9+
isAuthenticated
10+
}
11+
}
12+
"""
13+
14+
15+
def test_anon_cant_query_me(graphql_client):
16+
response = graphql_client(QUERY)
17+
content = response.json()
18+
19+
assert content["data"]["me"] is None
20+
21+
22+
def test_user_cant_query_me(django_user_client, graphql_user_client):
23+
response = graphql_user_client(QUERY)
24+
content = response.json()
25+
26+
assert content["data"]["me"]["id"] == django_user_client.user.relay_id
27+
assert content["data"]["me"]["isAuthenticated"] is True

0 commit comments

Comments
 (0)