|
|
|
@ -123,6 +123,64 @@ EVENT_VISIBILITY_TRANSLATIONS = { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GroupVisibility(Enum): |
|
|
|
|
"""Enumeration for group visibility |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
#: The group is secret, ie. completely unlisted |
|
|
|
|
secret = 0 |
|
|
|
|
|
|
|
|
|
#: The group is closed, ie. it can be joined only with an invitation, but otherwise public |
|
|
|
|
closed = 1 |
|
|
|
|
|
|
|
|
|
#: The group is public |
|
|
|
|
public = 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 # pylint: disable=comparison-with-callable |
|
|
|
|
|
|
|
|
|
return Enum.__eq__(self, other) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
GROUP_VISIBILITY_TRANSLATIONS = { |
|
|
|
|
GroupVisibility.secret: _('Secret'), |
|
|
|
|
GroupVisibility.closed: _('Closed'), |
|
|
|
|
GroupVisibility.public: _('Public'), |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GroupMemberLevel(Enum): |
|
|
|
|
"""Enumeration for group membership level |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
#: The member is a spectator only (ie. read only) |
|
|
|
|
spectator = 0 |
|
|
|
|
|
|
|
|
|
#: The member is a user with all privileges |
|
|
|
|
user = 1 |
|
|
|
|
|
|
|
|
|
#: The member is a moderator |
|
|
|
|
moderator = 2 |
|
|
|
|
|
|
|
|
|
#: The member is an administrator |
|
|
|
|
admin = 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
GROUP_MEMBER_LEVEL_TRANSLATIONS = { |
|
|
|
|
GroupMemberLevel.spectator: _('Spectator'), |
|
|
|
|
GroupMemberLevel.user: _('User'), |
|
|
|
|
GroupMemberLevel.moderator: _('Moderator'), |
|
|
|
|
GroupMemberLevel.admin: _('Administrator'), |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SettingsProxy: |
|
|
|
|
"""Proxy object to get settings for a user |
|
|
|
|
""" |
|
|
|
@ -851,3 +909,150 @@ class AppState(app_state_base(db.Model)): # pylint: disable=too-few-public-meth |
|
|
|
|
|
|
|
|
|
def __repr__(self): |
|
|
|
|
return f'<AppState {self.env}:{self.key}="{self.value}"' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Group(db.Model): |
|
|
|
|
"""Database model for groups |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
__tablename__ = 'groups' |
|
|
|
|
__table_args__ = ( |
|
|
|
|
db.UniqueConstraint('handle', 'domain'), |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
id = db.Column(db.Integer(), primary_key=True) |
|
|
|
|
|
|
|
|
|
handle = db.Column(db.String(length=50), nullable=False) |
|
|
|
|
|
|
|
|
|
domain = db.Column(db.String(length=100), nullable=True) |
|
|
|
|
|
|
|
|
|
display_name = db.Column(db.Unicode(length=100), nullable=True) |
|
|
|
|
|
|
|
|
|
created_at = db.Column(db.DateTime(), default=datetime.utcnow) |
|
|
|
|
|
|
|
|
|
created_by_id = db.Column(db.Integer(), db.ForeignKey('profiles.id'), nullable=False) |
|
|
|
|
|
|
|
|
|
created_by = db.relationship('Profile') |
|
|
|
|
|
|
|
|
|
default_level = db.Column(db.Enum(GroupMemberLevel)) |
|
|
|
|
|
|
|
|
|
visibility = db.Column(db.Enum(GroupVisibility), nullable=False) |
|
|
|
|
|
|
|
|
|
@property |
|
|
|
|
def members(self): |
|
|
|
|
return Profile.query.join(GroupMembership, GroupMembership.profile_id == Profile.id).filter(GroupMembership.group == self) |
|
|
|
|
|
|
|
|
|
@property |
|
|
|
|
def fqn(self): |
|
|
|
|
"""The fully qualified name of the group |
|
|
|
|
|
|
|
|
|
For local profiles, this is in the form ``!username``; for remote users, it’s in the form |
|
|
|
|
``!handle@domain``. |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
if not self.domain: |
|
|
|
|
return f'!{self.handle}' |
|
|
|
|
|
|
|
|
|
return f'!{self.handle}@{self.domain}' |
|
|
|
|
|
|
|
|
|
@property |
|
|
|
|
def url(self): |
|
|
|
|
"""Get the URL for this group |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
from flask import url_for |
|
|
|
|
|
|
|
|
|
return url_for('group.display', fqn=self.fqn) |
|
|
|
|
|
|
|
|
|
def __repr__(self): |
|
|
|
|
return f'<Group {self.id}: !{self.handle}@{self.domain}>' |
|
|
|
|
|
|
|
|
|
def __str__(self): |
|
|
|
|
return self.fqn |
|
|
|
|
|
|
|
|
|
@classmethod |
|
|
|
|
def get_by_fqn(cls, fqn): |
|
|
|
|
import re |
|
|
|
|
|
|
|
|
|
match = re.match(r'^!([a-z0-9_]+)(@.*)?', fqn) |
|
|
|
|
|
|
|
|
|
if not match: |
|
|
|
|
raise ValueError(f'Invalid Group FQN {fqn}') |
|
|
|
|
|
|
|
|
|
handle, domain = match.groups() |
|
|
|
|
|
|
|
|
|
return Group.query.filter(Group.handle == handle).filter(Group.domain == domain).one() |
|
|
|
|
|
|
|
|
|
def visible_to(self, profile): |
|
|
|
|
"""Checks whether this group is visible to ``profile`` |
|
|
|
|
|
|
|
|
|
It is so if the group is public or closed or, given it is secret, ``profile`` is a member |
|
|
|
|
of the group. |
|
|
|
|
|
|
|
|
|
:param profile: a :class:`Profile` object, or ``None`` to check for anonymous access |
|
|
|
|
:type profile: Profile |
|
|
|
|
:returns: ``True`` if the group is visible, ``False`` otherwise |
|
|
|
|
:rtype: bool |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
if self.visibility == GroupVisibility.secret: |
|
|
|
|
try: |
|
|
|
|
GroupMembership.query \ |
|
|
|
|
.filter(GroupMembership.group == self) \ |
|
|
|
|
.filter(GroupMembership.profile == profile) \ |
|
|
|
|
.one() |
|
|
|
|
except NoResultFound: |
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
return True |
|
|
|
|
|
|
|
|
|
def details_visible_to(self, profile): |
|
|
|
|
"""Checks whether the details of this group is visible to ``profile`` |
|
|
|
|
|
|
|
|
|
Details include member list and events shared with this group. |
|
|
|
|
|
|
|
|
|
It is so if the group is public or ``profile`` is a member of the group. |
|
|
|
|
|
|
|
|
|
:param profile: a :class:`Profile` object, or ``None`` to check for anonymous access |
|
|
|
|
:type profile: Profile |
|
|
|
|
:returns: ``True`` if the group is visible, ``False`` otherwise |
|
|
|
|
:rtype: bool |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
if self.visibility != GroupVisibility.public: |
|
|
|
|
try: |
|
|
|
|
GroupMembership.query \ |
|
|
|
|
.filter(GroupMembership.group == self) \ |
|
|
|
|
.filter(GroupMembership.profile == profile) \ |
|
|
|
|
.one() |
|
|
|
|
except NoResultFound: |
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
return True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GroupMembership(db.Model): |
|
|
|
|
"""Database model for group membership |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
__tablename__ = 'group_members' |
|
|
|
|
|
|
|
|
|
group_id = db.Column(db.Integer(), db.ForeignKey('groups.id'), primary_key=True) |
|
|
|
|
|
|
|
|
|
group = db.relationship('Group') |
|
|
|
|
|
|
|
|
|
profile_id = db.Column(db.Integer(), db.ForeignKey('profiles.id'), primary_key=True) |
|
|
|
|
|
|
|
|
|
profile = db.relationship('Profile', foreign_keys=[profile_id]) |
|
|
|
|
|
|
|
|
|
level = db.Column(db.Enum(GroupMemberLevel), nullable=False) |
|
|
|
|
|
|
|
|
|
requested_at = db.Column(db.DateTime(), default=datetime.utcnow, nullable=False) |
|
|
|
|
|
|
|
|
|
joined_at = db.Column(db.DateTime(), nullable=True) |
|
|
|
|
|
|
|
|
|
accepted_at = db.Column(db.DateTime(), nullable=True) |
|
|
|
|
|
|
|
|
|
accepted_by_id = db.Column(db.Integer(), db.ForeignKey('profiles.id'), nullable=True) |
|
|
|
|
|
|
|
|
|
accepted_by = db.relationship('Profile', foreign_keys=[accepted_by_id]) |
|
|
|
|