|
|
|
@ -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])
|
|
|
|
|