diff --git a/calsocial/models.py b/calsocial/models.py index 457f611..75e4c8c 100644 --- a/calsocial/models.py +++ b/calsocial/models.py @@ -123,6 +123,39 @@ 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 SettingsProxy: """Proxy object to get settings for a user """ @@ -404,6 +437,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 +863,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 diff --git a/tests/test_response_visibility.py b/tests/test_response_visibility.py new file mode 100644 index 0000000..5ba1bd7 --- /dev/null +++ b/tests/test_response_visibility.py @@ -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 . + +"""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