[WIP] Calculate response visibility #91

Draft
gergely wants to merge 2 commits from response-visibility into master
2 changed files with 348 additions and 0 deletions

View File

@ -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 invitees friends (ie. mutual follows)
friends = 3
#: The response is visible to the invitees 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

View 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