Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions memberportal/access/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,9 @@ def bump(self, request=None):
# notify messaging apps of bump
profile = request.user.profile
if self.post_to_slack:
post_door_bump_to_slack(profile.get_full_name(), self.name)
post_door_bump_to_slack(profile.get_display_name(), self.name)
if self.post_to_discord:
post_door_bump_to_discord(profile.get_full_name(), self.name)
post_door_bump_to_discord(profile.get_display_name(), self.name)
self.log_access(request.user.id)
request.user.log_event(
f"Bumped the {self.name} {self._meta.verbose_name}.",
Expand Down Expand Up @@ -355,9 +355,11 @@ def log_access(self, member_id, success=True):

# TODO replace with generic post_to_messengers
if self.post_to_discord:
post_door_swipe_to_discord(profile.get_full_name(), self.name, success)
post_door_swipe_to_discord(
profile.get_display_name(), self.name, success
)
if self.post_to_slack:
post_door_swipe_to_slack(profile.get_full_name(), self.name, success)
post_door_swipe_to_slack(profile.get_display_name(), self.name, success)

elif success == "locked_out":
metrics.device_access_failures_total.labels(
Expand All @@ -367,11 +369,11 @@ def log_access(self, member_id, success=True):
# TODO replace with generic post_to_messengers
if self.post_to_discord:
post_door_swipe_to_discord(
profile.get_full_name(), self.name, "locked_out"
profile.get_display_name(), self.name, "locked_out"
)
if self.post_to_slack:
post_door_swipe_to_slack(
profile.get_full_name(), self.name, "locked_out"
profile.get_display_name(), self.name, "locked_out"
)

sms_message = sms.SMS()
Expand All @@ -385,10 +387,12 @@ def log_access(self, member_id, success=True):
# TODO replace with generic post_to_messengers
if self.post_to_discord:
post_door_swipe_to_discord(
profile.get_full_name(), self.name, "rejected"
profile.get_display_name(), self.name, "rejected"
)
if self.post_to_slack:
post_door_swipe_to_slack(profile.get_full_name(), self.name, "rejected")
post_door_swipe_to_slack(
profile.get_display_name(), self.name, "rejected"
)

sms_message = sms.SMS()
sms_message.send_inactive_swipe_alert(profile.phone)
Expand Down Expand Up @@ -437,7 +441,7 @@ def log_access(self, user, log_type="activated"):

if self.post_to_discord:
post_interlock_swipe_to_discord(
profile.get_full_name(), self.name, type=log_type
profile.get_display_name(), self.name, type=log_type
)

if log_type == "activated":
Expand Down Expand Up @@ -484,7 +488,7 @@ class DoorLog(ExportModelOperationsMixin("door-log"), models.Model):
success = models.BooleanField(default=True)

def __str__(self):
return f"{self.user.get_full_name()} ({self.user.profile.screen_name}) swiped at {self.door.name} {'successfully' if self.success else 'unsuccessfully'} on {self.date.date()}"
return f"{self.user.profile.get_display_name(include_screen_name=True)} swiped at {self.door.name} {'successfully' if self.success else 'unsuccessfully'} on {self.date.date()}"


class InterlockLog(ExportModelOperationsMixin("interlock-log"), models.Model):
Expand All @@ -510,7 +514,7 @@ class InterlockLog(ExportModelOperationsMixin("interlock-log"), models.Model):
total_cost = models.FloatField(default=None, blank=True, null=True)

def __str__(self):
return f"{self.user_started.get_full_name()} ({self.user_started.profile.screen_name}) swiped at {self.interlock.name} {'successfully' if self.success else 'unsuccessfully'} for {round(self.total_time.total_seconds() / 60)} mins at {self.date_started.date()}"
return f"{self.user_started.profile.get_display_name(include_screen_name=True)} swiped at {self.interlock.name} {'successfully' if self.success else 'unsuccessfully'} for {round(self.total_time.total_seconds() / 60)} mins at {self.date_started.date()}"

def calculate_cost(self):
total_cost = self.interlock.cost_per_session
Expand Down
2 changes: 1 addition & 1 deletion memberportal/api_access/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ def handle_other_packet(self, content):
description = f"{product.name} purchased from {self.device.name} ({product.external_id_name})."

post_purchase_to_discord(
f"{profile.get_full_name()} ({profile.screen_name}) just bought something from {self.device.name}."
f"{profile.get_display_name(include_screen_name=True)} just bought something from {self.device.name}."
)

transaction = MemberBucks()
Expand Down
17 changes: 17 additions & 0 deletions memberportal/api_meeting/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from constance import config
from rest_framework import permissions


class ProxyVotingPermission(permissions.BasePermission):
"""Allow proxy-voting APIs only for the enabled feature's users."""

message = "Proxy voting is not available to this user."

def has_permission(self, request, view):
if not config.ENABLE_PROXY_VOTING or not request.user.is_authenticated:
return False

profile = getattr(request.user, "profile", None)
return request.user.is_staff or (
profile is not None and profile.state == "active"
)
91 changes: 91 additions & 0 deletions memberportal/api_meeting/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from constance.test.unittest import override_config
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APIClient

from profile.models import Profile, User


class ProxyVotingEndpointTests(TestCase):
member_endpoints = (
"/api/tools/meetings/",
"/api/tools/members/",
"/api/proxies/",
)
proxy_write_endpoints = (
("post", "/api/proxies/"),
("delete", "/api/proxies/999/"),
)

def make_user(self, state="active", staff=False):
suffix = User.objects.count() + 1
user = User.objects.create_user(
f"proxy-endpoint-{suffix}@example.test",
password="test-password",
)
user.staff = staff
user.save(update_fields=["staff"])
Profile.objects.create(
user=user,
first_name="Proxy",
last_name=str(suffix),
state=state,
)
return user

def assert_gets_status(self, user, expected_status):
client = APIClient()
if user is not None:
client.force_authenticate(user=user)

for endpoint in self.member_endpoints:
with self.subTest(endpoint=endpoint):
self.assertEqual(client.get(endpoint).status_code, expected_status)

def test_unauthn_users_cannot_access_proxy_endpoints(self):
with override_config(ENABLE_PROXY_VOTING=True):
self.assert_gets_status(None, status.HTTP_401_UNAUTHORIZED)

client = APIClient()
for method, endpoint in self.proxy_write_endpoints:
with self.subTest(method=method, endpoint=endpoint):
response = getattr(client, method)(endpoint, format="json")
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_inactive_members_cannot_access_proxy_endpoints(self):
user = self.make_user(state="inactive")

with override_config(ENABLE_PROXY_VOTING=True):
self.assert_gets_status(user, status.HTTP_403_FORBIDDEN)

client = APIClient()
client.force_authenticate(user=user)
for method, endpoint in self.proxy_write_endpoints:
with self.subTest(method=method, endpoint=endpoint):
response = getattr(client, method)(endpoint, format="json")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_active_members_can_access_proxy_endpoints(self):
user = self.make_user(state="active")

with override_config(ENABLE_PROXY_VOTING=True):
self.assert_gets_status(user, status.HTTP_200_OK)

def test_staff_users_can_access_proxy_endpoints(self):
user = self.make_user(state="inactive", staff=True)

with override_config(ENABLE_PROXY_VOTING=True):
self.assert_gets_status(user, status.HTTP_200_OK)

def test_proxy_endpoints_are_disabled_with_the_feature(self):
user = self.make_user(state="active")

with override_config(ENABLE_PROXY_VOTING=False):
self.assert_gets_status(user, status.HTTP_403_FORBIDDEN)

client = APIClient()
client.force_authenticate(user=user)
for method, endpoint in self.proxy_write_endpoints:
with self.subTest(method=method, endpoint=endpoint):
response = getattr(client, method)(endpoint, format="json")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
29 changes: 14 additions & 15 deletions memberportal/api_meeting/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .models import Meeting, ProxyVote
from .permissions import ProxyVotingPermission
from profile.models import Profile
from django.utils.timezone import make_aware, localtime
from datetime import datetime
Expand All @@ -23,12 +24,12 @@ def get(self, request):
meetings = Meeting.objects.all()

def get_attendee(attendee):
return attendee.profile.get_full_name()
return attendee.profile.get_display_name()

def get_proxy(proxy):
return {
"name": proxy.user.profile.get_full_name(),
"proxyName": proxy.proxy_user.profile.get_full_name(),
"name": proxy.user.profile.get_display_name(),
"proxyName": proxy.proxy_user.profile.get_display_name(),
"date": proxy.created_date,
}

Expand Down Expand Up @@ -85,15 +86,15 @@ class Proxies(APIView):
delete: delete an existing proxy vote.
"""

permission_classes = [permissions.IsAuthenticated]
permission_classes = [ProxyVotingPermission]

def get(self, request):
proxies = ProxyVote.objects.filter(user=request.user)

def get_proxy_details(proxy):
return {
"id": proxy.id,
"name": proxy.proxy_user.profile.get_full_name(),
"name": proxy.proxy_user.profile.get_display_name(),
"date": proxy.meeting.date,
"type": proxy.meeting.get_type(),
}
Expand Down Expand Up @@ -127,10 +128,8 @@ def post(self, request):
meeting=meeting,
)

subject = (
f"{request.user.profile.get_full_name()} just assigned you as a proxy"
)
message = f"{request.user.profile.get_full_name()} just assigned you as a proxy for the {meeting.get_type()} meeting on {localtime(meeting.date)}."
subject = f"{request.user.profile.get_display_name()} just assigned you as a proxy"
message = f"{request.user.profile.get_display_name()} just assigned you as a proxy for the {meeting.get_type()} meeting on {localtime(meeting.date)}."
send_single_email(
to_email=proxy_user.email,
subject=subject,
Expand All @@ -141,8 +140,8 @@ def post(self, request):
user=request.user,
)

subject = f"{proxy_user.profile.get_full_name()} is confirmed as your proxy for the {meeting.get_type()} meeting"
message = f"{proxy_user.profile.get_full_name()} is confirmed as your proxy for the {meeting.get_type()} meeting on {localtime(meeting.date)}. You can manage this proxy from the member portal."
subject = f"{proxy_user.profile.get_display_name()} is confirmed as your proxy for the {meeting.get_type()} meeting"
message = f"{proxy_user.profile.get_display_name()} is confirmed as your proxy for the {meeting.get_type()} meeting on {localtime(meeting.date)}. You can manage this proxy from the member portal."
send_single_email(
to_email=request.user.email,
subject=subject,
Expand All @@ -161,9 +160,9 @@ def delete(self, request, proxy_id):
if request.user == proxy.user:
proxy.delete()
subject = (
f"{request.user.profile.get_full_name()} just removed you as a proxy"
f"{request.user.profile.get_display_name()} just removed you as a proxy"
)
message = f"{request.user.profile.get_full_name()} just removed you as a proxy for the {proxy.meeting.get_type()} meeting on {localtime(proxy.meeting.date)}."
message = f"{request.user.profile.get_display_name()} just removed you as a proxy for the {proxy.meeting.get_type()} meeting on {localtime(proxy.meeting.date)}."
send_single_email(
to_email=proxy.proxy_user.email,
subject=subject,
Expand All @@ -174,8 +173,8 @@ def delete(self, request, proxy_id):
user=request.user,
)

subject = f"{proxy.proxy_user.profile.get_full_name()} is no longer your proxy for the {proxy.meeting.get_type()} meeting"
message = f"{proxy.proxy_user.profile.get_full_name()} is no longer your proxy for the {proxy.meeting.get_type()} meeting on {localtime(proxy.meeting.date)}. You can manage this proxy from the member portal."
subject = f"{proxy.proxy_user.profile.get_display_name()} is no longer your proxy for the {proxy.meeting.get_type()} meeting"
message = f"{proxy.proxy_user.profile.get_display_name()} is no longer your proxy for the {proxy.meeting.get_type()} meeting on {localtime(proxy.meeting.date)}. You can manage this proxy from the member portal."
send_single_email(
to_email=request.user.email,
subject=subject,
Expand Down
17 changes: 10 additions & 7 deletions memberportal/api_member_tools/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from access.models import DoorLog, InterlockLog
from profile.models import Profile
from api_meeting.models import Meeting
from api_meeting.permissions import ProxyVotingPermission
from constance import config
from services.emails import send_email_to_admin
from services import discord
Expand Down Expand Up @@ -46,23 +47,23 @@ def get(self, request):
{
"name": door.door.name,
"date": door.date,
"user": door.user.profile.get_full_name(),
"user": door.user.profile.get_display_name(),
}
)

for interlock in recent_interlocks:
user_ended = None

if interlock.user_ended:
user_ended = interlock.user_ended.profile.get_full_name()
user_ended = interlock.user_ended.profile.get_display_name()

interlocks.append(
{
"name": interlock.interlock.name,
"sessionStart": interlock.date_started,
"sessionEnd": interlock.date_ended,
"sessionComplete": True if interlock.date_ended else False,
"userOn": interlock.user_started.profile.get_full_name(),
"userOn": interlock.user_started.profile.get_display_name(),
"userOff": user_ended,
}
)
Expand Down Expand Up @@ -95,15 +96,15 @@ def get(self, request):
last_seen.append(
{
"id": member.id,
"user": member.get_full_name(),
"user": member.get_display_name(),
"never": False,
"date": member.last_seen,
}
)

else:
last_seen.append(
{"id": member.id, "user": member.get_full_name(), "never": True}
{"id": member.id, "user": member.get_display_name(), "never": True}
)

return Response(last_seen, status=status.HTTP_200_OK)
Expand Down Expand Up @@ -331,7 +332,7 @@ class MeetingList(APIView):
get: Returns a list of upcoming meetings that a member is entitled to vote at.
"""

permission_classes = (permissions.IsAuthenticated,)
permission_classes = (ProxyVotingPermission,)
queryset = Meeting.objects.filter(date__gt=timezone.now())

def get(self, request):
Expand All @@ -354,11 +355,13 @@ class Members(APIView):
get: gets a list of all members.
"""

permission_classes = (ProxyVotingPermission,)

def get(self, request):
def get_member(member):
return {
"id": member.id,
"name": member.get_full_name(),
"name": member.get_display_name(),
"screenName": member.screen_name,
}

Expand Down
2 changes: 1 addition & 1 deletion memberportal/api_metrics/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def get(self, request):
on_site["count"] = members.count()

for member in members:
on_site["members"].append(member.user.profile.get_full_name())
on_site["members"].append(member.user.profile.get_display_name())

statistics["on_site"] = on_site

Expand Down
5 changes: 5 additions & 0 deletions memberportal/membermatters/constance_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,10 @@
False,
"Require members to set a public screen name during signup. Off means the screen name field is optional.",
),
"PREFER_SCREEN_NAME_OVER_FULL_NAME": (
False,
"Prefer a member's public screen name over their full name in member-facing displays when it is set.",
),
"MEMBER_CAN_EDIT_BASIC_DETAILS": (
True,
"Allow members to edit their own name, phone, address and similar basic profile fields. Turn off to lock profile editing to admins only.",
Expand Down Expand Up @@ -632,6 +636,7 @@
"MEMBER_CAN_EDIT_BASIC_DETAILS",
"MEMBER_CAN_EDIT_EMAIL",
"PROFILE_DEFAULT_PHONE_REGION",
"PREFER_SCREEN_NAME_OVER_FULL_NAME",
),
),
(
Expand Down
Loading
Loading