[WIP] Calculate response visibility #91
@ -70,6 +70,18 @@ class NotificationAction(Enum):
|
||||
#: A user has been invited to an event
|
||||
invite = 2
|
||||
|
||||
def __hash__(self):
|
||||
return Enum.__hash__(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, str):
|
||||
return self.name.lower() == other.lower() # pylint: disable=no-member
|
||||
|
||||
if isinstance(other, (int, float)):
|
||||
return self.value == other
|
||||
|
||||
return Enum.__eq__(self, other)
|
||||
|
||||
|
||||
NOTIFICATION_ACTION_MESSAGES = {
|
||||
NotificationAction.follow: (_('%(actor)s followed you'), _('%(actor)s followed %(item)s')),
|
||||
@ -123,6 +135,80 @@ EVENT_VISIBILITY_TRANSLATIONS = {
|
||||
}
|
||||
|
||||
|
||||
class ResponseVisibility(Enum):
|
||||
"""Enumeration for response visibility
|
||||
"""
|
||||
|
||||
#: The response is only visible to the invitee
|
||||
private = 0
|
||||
|
||||
#: The response is only visible to the event organisers
|
||||
organisers = 1
|
||||
|
||||
#: The response is only visible to the event attendees
|
||||
attendees = 2
|
||||
|
||||
#: The response is visible to the invitee’s friends (ie. mutual follows)
|
||||
friends = 3
|
||||
|
||||
#: The response is visible to the invitee’s followers
|
||||
followers = 4
|
||||
|
||||
#: The response is visible to anyone
|
||||
public = 5
|
||||
|
||||
|
||||
RESPONSE_VISIBILITY_TRANSLATIONS = {
|
||||
ResponseVisibility.private: _('Visible only to myself'),
|
||||
ResponseVisibility.organisers: _('Visible only to event organisers'),
|
||||
ResponseVisibility.attendees: _('Visible only to event attendees'),
|
||||
ResponseVisibility.friends: _('Visible only to my friends'),
|
||||
ResponseVisibility.followers: _('Visible only to my followers'),
|
||||
ResponseVisibility.public: _('Visible to anyone'),
|
||||
}
|
||||
|
||||
|
||||
class EventAvailability(Enum):
|
||||
"""Enumeration of event availabilities
|
||||
"""
|
||||
|
||||
free = 0
|
||||
busy = 1
|
||||
|
||||
def __hash__(self):
|
||||
return Enum.__hash__(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, str):
|
||||
return self.name.lower() == other.lower() # pylint: disable=no-member
|
||||
|
||||
if isinstance(other, (int, float)):
|
||||
return self.value == other
|
||||
|
||||
return Enum.__eq__(self, other)
|
||||
|
||||
|
||||
class UserAvailability(Enum):
|
||||
"""Enumeration of user availabilities
|
||||
"""
|
||||
|
||||
free = 0
|
||||
busy = 1
|
||||
tentative = 2
|
||||
|
||||
def __hash__(self):
|
||||
return Enum.__hash__(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, str):
|
||||
return self.name.lower() == other.lower() # pylint: disable=no-member
|
||||
|
||||
if isinstance(other, (int, float)):
|
||||
return self.value == other
|
||||
|
||||
return Enum.__eq__(self, other)
|
||||
|
||||
|
||||
class SettingsProxy:
|
||||
"""Proxy object to get settings for a user
|
||||
"""
|
||||
@ -404,6 +490,45 @@ class Profile(db.Model): # pylint: disable=too-few-public-methods
|
||||
|
||||
return notification
|
||||
|
||||
def is_following(self, profile):
|
||||
"""Check if this profile is following ``profile``
|
||||
"""
|
||||
|
||||
try:
|
||||
UserFollow.query.filter(UserFollow.follower == self).filter(UserFollow.followed == profile).one()
|
||||
except NoResultFound:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_friend_of(self, profile):
|
||||
"""Check if this profile is friends with ``profile``
|
||||
"""
|
||||
|
||||
reverse = db.aliased(UserFollow)
|
||||
|
||||
try:
|
||||
UserFollow.query \
|
||||
.filter(UserFollow.follower == self) \
|
||||
.join(reverse, UserFollow.followed_id == reverse.follower_id) \
|
||||
.filter(UserFollow.follower_id == reverse.followed_id) \
|
||||
.one()
|
||||
except NoResultFound:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_attending(self, event):
|
||||
"""Check if this profile is attending ``event``
|
||||
"""
|
||||
|
||||
try:
|
||||
Response.query.filter(Response.profile == self).filter(Response.event == event).one()
|
||||
except NoResultFound:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Event(db.Model):
|
||||
"""Database model for events
|
||||
@ -791,6 +916,54 @@ class Response(db.Model): # pylint: disable=too-few-public-methods
|
||||
#: The response itself
|
||||
response = db.Column(db.Enum(ResponseType), nullable=False)
|
||||
|
||||
#: The visibility of the response
|
||||
visibility = db.Column(db.Enum(ResponseVisibility), nullable=False)
|
||||
|
||||
def visible_to(self, profile):
|
||||
"""Checks if the response can be visible to ``profile``.
|
||||
|
||||
:param profile: the profile looking at the response. If None, it is viewed as anonymous
|
||||
:type profile: Profile, None
|
||||
:returns: ``True`` if the response should be visible, ``False`` otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
|
||||
if self.profile == profile:
|
||||
return True
|
||||
|
||||
if self.visibility == ResponseVisibility.private:
|
||||
return False
|
||||
|
||||
if self.visibility == ResponseVisibility.organisers:
|
||||
return profile == self.event.profile
|
||||
|
||||
if self.visibility == ResponseVisibility.attendees:
|
||||
return profile is not None and \
|
||||
(profile.is_attending(self.event) or \
|
||||
profile == self.event.profile)
|
||||
|
||||
# From this point on, if the event is not public, only attendees can see responses
|
||||
if self.event.visibility != EventVisibility.public:
|
||||
return profile is not None and \
|
||||
(profile.is_attending(self.event) or
|
||||
profile == self.event.profile)
|
||||
|
||||
if self.visibility == ResponseVisibility.friends:
|
||||
return profile is not None and \
|
||||
(profile.is_friend_of(self.profile) or \
|
||||
profile.is_attending(self.event) or \
|
||||
profile == self.event.profile or \
|
||||
profile == self.profile)
|
||||
|
||||
if self.visibility == ResponseVisibility.followers:
|
||||
return profile is not None and \
|
||||
(profile.is_following(self.profile) or \
|
||||
profile.is_attending(self.event) or \
|
||||
profile == self.event.profile or \
|
||||
profile == self.profile)
|
||||
|
||||
return self.visibility == ResponseVisibility.public
|
||||
|
||||
|
||||
class AppState(app_state_base(db.Model)): # pylint: disable=too-few-public-methods
|
||||
"""Database model for application state values
|
||||
|
175
tests/test_response_visibility.py
Normal file
175
tests/test_response_visibility.py
Normal file
@ -0,0 +1,175 @@
|
||||
# Calendar.social
|
||||
# Copyright (C) 2018 Gergely Polonkai
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Profile related tests for Calendar.social
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from calsocial.models import db, Event, EventVisibility, Profile, Response, ResponseType, \
|
||||
ResponseVisibility, UserFollow
|
||||
|
||||
from helpers import database
|
||||
|
||||
|
||||
def test_response_visibility(database):
|
||||
"""Test response visibility in different scenarios
|
||||
"""
|
||||
|
||||
test_data = (
|
||||
# Third element value descriptions:
|
||||
# none=not logged in
|
||||
# unknown=completely unrelated profile
|
||||
# follower=spectator is following respondent
|
||||
# friend=spectator and respondent are friends (mutual follow)
|
||||
# attendee=spectator is an attendee of the event
|
||||
# respondent=spectator is the respondent
|
||||
(EventVisibility.public, ResponseVisibility.public, 'anon', True),
|
||||
(EventVisibility.public, ResponseVisibility.public, 'unknown', True),
|
||||
(EventVisibility.public, ResponseVisibility.public, 'follower', True),
|
||||
(EventVisibility.public, ResponseVisibility.public, 'friend', True),
|
||||
(EventVisibility.public, ResponseVisibility.public, 'attendee', True),
|
||||
(EventVisibility.public, ResponseVisibility.public, 'organiser', True),
|
||||
(EventVisibility.public, ResponseVisibility.public, 'respondent', True),
|
||||
|
||||
(EventVisibility.public, ResponseVisibility.followers, 'anon', False),
|
||||
(EventVisibility.public, ResponseVisibility.followers, 'unknown', False),
|
||||
(EventVisibility.public, ResponseVisibility.followers, 'follower', True),
|
||||
(EventVisibility.public, ResponseVisibility.followers, 'friend', True),
|
||||
(EventVisibility.public, ResponseVisibility.followers, 'attendee', True),
|
||||
(EventVisibility.public, ResponseVisibility.followers, 'organiser', True),
|
||||
(EventVisibility.public, ResponseVisibility.followers, 'respondent', True),
|
||||
|
||||
(EventVisibility.public, ResponseVisibility.friends, 'anon', False),
|
||||
(EventVisibility.public, ResponseVisibility.friends, 'unknown', False),
|
||||
(EventVisibility.public, ResponseVisibility.friends, 'follower', False),
|
||||
(EventVisibility.public, ResponseVisibility.friends, 'friend', True),
|
||||
(EventVisibility.public, ResponseVisibility.friends, 'attendee', True),
|
||||
(EventVisibility.public, ResponseVisibility.friends, 'organiser', True),
|
||||
(EventVisibility.public, ResponseVisibility.friends, 'respondent', True),
|
||||
|
||||
(EventVisibility.public, ResponseVisibility.attendees, 'anon', False),
|
||||
(EventVisibility.public, ResponseVisibility.attendees, 'unknown', False),
|
||||
(EventVisibility.public, ResponseVisibility.attendees, 'follower', False),
|
||||
(EventVisibility.public, ResponseVisibility.attendees, 'friend', False),
|
||||
(EventVisibility.public, ResponseVisibility.attendees, 'attendee', True),
|
||||
(EventVisibility.public, ResponseVisibility.attendees, 'organiser', True),
|
||||
(EventVisibility.public, ResponseVisibility.attendees, 'respondent', True),
|
||||
|
||||
(EventVisibility.public, ResponseVisibility.organisers, 'anon', False),
|
||||
(EventVisibility.public, ResponseVisibility.organisers, 'unknown', False),
|
||||
(EventVisibility.public, ResponseVisibility.organisers, 'follower', False),
|
||||
(EventVisibility.public, ResponseVisibility.organisers, 'friend', False),
|
||||
(EventVisibility.public, ResponseVisibility.organisers, 'attendee', False),
|
||||
(EventVisibility.public, ResponseVisibility.organisers, 'organiser', True),
|
||||
(EventVisibility.public, ResponseVisibility.organisers, 'respondent', True),
|
||||
|
||||
(EventVisibility.public, ResponseVisibility.private, 'anon', False),
|
||||
(EventVisibility.public, ResponseVisibility.private, 'unknown', False),
|
||||
(EventVisibility.public, ResponseVisibility.private, 'follower', False),
|
||||
(EventVisibility.public, ResponseVisibility.private, 'friend', False),
|
||||
(EventVisibility.public, ResponseVisibility.private, 'attendee', False),
|
||||
(EventVisibility.public, ResponseVisibility.private, 'organiser', False),
|
||||
(EventVisibility.public, ResponseVisibility.private, 'respondent', True),
|
||||
|
||||
(EventVisibility.private, ResponseVisibility.public, 'anon', False),
|
||||
(EventVisibility.private, ResponseVisibility.public, 'unknown', False),
|
||||
(EventVisibility.private, ResponseVisibility.public, 'follower', False),
|
||||
(EventVisibility.private, ResponseVisibility.public, 'friend', False),
|
||||
(EventVisibility.private, ResponseVisibility.public, 'attendee', True),
|
||||
(EventVisibility.private, ResponseVisibility.public, 'organiser', True),
|
||||
(EventVisibility.private, ResponseVisibility.public, 'respondent', True),
|
||||
|
||||
(EventVisibility.private, ResponseVisibility.followers, 'anon', False),
|
||||
(EventVisibility.private, ResponseVisibility.followers, 'unknown', False),
|
||||
(EventVisibility.private, ResponseVisibility.followers, 'follower', False),
|
||||
(EventVisibility.private, ResponseVisibility.followers, 'friend', False),
|
||||
(EventVisibility.private, ResponseVisibility.followers, 'attendee', True),
|
||||
(EventVisibility.private, ResponseVisibility.followers, 'organiser', True),
|
||||
(EventVisibility.private, ResponseVisibility.followers, 'respondent', True),
|
||||
|
||||
(EventVisibility.private, ResponseVisibility.friends, 'anon', False),
|
||||
(EventVisibility.private, ResponseVisibility.friends, 'unknown', False),
|
||||
(EventVisibility.private, ResponseVisibility.friends, 'follower', False),
|
||||
(EventVisibility.private, ResponseVisibility.friends, 'friend', False),
|
||||
(EventVisibility.private, ResponseVisibility.friends, 'attendee', True),
|
||||
(EventVisibility.private, ResponseVisibility.friends, 'organiser', True),
|
||||
(EventVisibility.private, ResponseVisibility.friends, 'respondent', True),
|
||||
|
||||
(EventVisibility.private, ResponseVisibility.attendees, 'anon', False),
|
||||
(EventVisibility.private, ResponseVisibility.attendees, 'unknown', False),
|
||||
(EventVisibility.private, ResponseVisibility.attendees, 'follower', False),
|
||||
(EventVisibility.private, ResponseVisibility.attendees, 'friend', False),
|
||||
(EventVisibility.private, ResponseVisibility.attendees, 'attendee', True),
|
||||
(EventVisibility.private, ResponseVisibility.attendees, 'organiser', True),
|
||||
(EventVisibility.private, ResponseVisibility.attendees, 'respondent', True),
|
||||
|
||||
(EventVisibility.private, ResponseVisibility.organisers, 'anon', False),
|
||||
(EventVisibility.private, ResponseVisibility.organisers, 'unknown', False),
|
||||
(EventVisibility.private, ResponseVisibility.organisers, 'follower', False),
|
||||
(EventVisibility.private, ResponseVisibility.organisers, 'friend', False),
|
||||
(EventVisibility.private, ResponseVisibility.organisers, 'attendee', False),
|
||||
(EventVisibility.private, ResponseVisibility.organisers, 'organiser', True),
|
||||
(EventVisibility.private, ResponseVisibility.organisers, 'respondent', True),
|
||||
|
||||
(EventVisibility.private, ResponseVisibility.private, 'anon', False),
|
||||
(EventVisibility.private, ResponseVisibility.private, 'unknown', False),
|
||||
(EventVisibility.private, ResponseVisibility.private, 'follower', False),
|
||||
(EventVisibility.private, ResponseVisibility.private, 'friend', False),
|
||||
(EventVisibility.private, ResponseVisibility.private, 'attendee', False),
|
||||
(EventVisibility.private, ResponseVisibility.private, 'organiser', False),
|
||||
(EventVisibility.private, ResponseVisibility.private, 'respondent', True),
|
||||
|
||||
)
|
||||
|
||||
for evt_vis, resp_vis, relation, exp_ret in test_data:
|
||||
organiser = Profile(display_name='organiser')
|
||||
event = Event(profile=organiser, visibility=evt_vis, title='Test Event', time_zone='UTC', start_time=datetime.utcnow(), end_time=datetime.utcnow())
|
||||
respondent = Profile(display_name='Respondent')
|
||||
response = Response(event=event, visibility=resp_vis, profile=respondent, response=ResponseType.going)
|
||||
|
||||
db.session.add_all([event, response])
|
||||
|
||||
if relation is 'anon':
|
||||
spectator = None
|
||||
elif relation == 'respondent':
|
||||
spectator = respondent
|
||||
elif relation == 'organiser':
|
||||
spectator = organiser
|
||||
else:
|
||||
spectator = Profile(display_name='Spectator')
|
||||
db.session.add(spectator)
|
||||
|
||||
if relation == 'follower' or relation == 'friend':
|
||||
follow = UserFollow(follower=spectator, followed=respondent)
|
||||
db.session.add(follow)
|
||||
|
||||
if relation == 'friend':
|
||||
follow = UserFollow(follower=respondent, followed=spectator)
|
||||
db.session.add(follow)
|
||||
|
||||
if relation == 'attendee':
|
||||
att_response = Response(profile=spectator,
|
||||
event=event,
|
||||
response=ResponseType.going,
|
||||
visibility=ResponseVisibility.public)
|
||||
db.session.add(att_response)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
notvis = ' not' if exp_ret else ''
|
||||
assert_message = f'Response is{notvis} visible to {spectator} ({evt_vis}, {resp_vis}, {relation})'
|
||||
assert response.visible_to(spectator) is exp_ret, assert_message
|
Loading…
Reference in New Issue
Block a user