From eb796b68841f37e9359cba300fcf4564be643d7a Mon Sep 17 00:00:00 2001 From: jessikitty Date: Fri, 25 Sep 2026 00:44:26 +1000 Subject: [PATCH] Add visibility column, access helpers and visible_to query to Set --- app/models/set.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/app/models/set.py b/app/models/set.py index 330ba70..90657c9 100644 --- a/app/models/set.py +++ b/app/models/set.py @@ -24,6 +24,13 @@ class Set(db.Model): image_url = db.Column(db.String(500)) # External URL (Brickset, etc.) cover_image = db.Column(db.String(500)) # Uploaded cover image path + # Access control + # 'private' - owner and admins only + # 'shared' - owner, admins, and users listed in set_shares + # 'public' - every logged-in user + visibility = db.Column(db.String(10), nullable=False, default='private', + server_default='private', index=True) + # Metadata user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) @@ -35,9 +42,66 @@ class Set(db.Model): cascade='all, delete-orphan') extra_files = db.relationship('ExtraFile', back_populates='lego_set', lazy='dynamic', cascade='all, delete-orphan', order_by='ExtraFile.uploaded_at.desc()') + shares = db.relationship('SetShare', lazy='dynamic', cascade='all, delete-orphan', + backref='lego_set') def __repr__(self): return f'' + + # ---- access control --------------------------------------------------- + + def can_manage(self, user): + """Edit, delete, upload to, or change sharing on this set.""" + if user is None or not getattr(user, 'is_authenticated', False): + return False + return bool(user.is_admin) or self.user_id == user.id + + def can_view(self, user): + """See this set and its files.""" + if user is None or not getattr(user, 'is_authenticated', False): + return False + if self.can_manage(user): + return True + if self.visibility == 'public': + return True + if self.visibility == 'shared': + from app.models.set_share import SetShare + return SetShare.query.filter_by(set_id=self.id, + user_id=user.id).first() is not None + return False + + def shared_with(self): + """Users this set is explicitly shared with.""" + return [share.user for share in self.shares.all()] + + @classmethod + def visible_to(cls, user): + """Query restricted to sets `user` is allowed to see.""" + from app.models.set_share import SetShare + query = cls.query + if user is None or not getattr(user, 'is_authenticated', False): + return query.filter(db.false()) + if user.is_admin: + return query + shared_ids = db.session.query(SetShare.set_id).filter( + SetShare.user_id == user.id) + return query.filter( + db.or_( + cls.user_id == user.id, + cls.visibility == 'public', + db.and_(cls.visibility == 'shared', cls.id.in_(shared_ids)), + ) + ) + + @property + def visibility_label(self): + return {'private': 'Private', 'shared': 'Shared', + 'public': 'Public'}.get(self.visibility, self.visibility) + + @property + def visibility_icon(self): + return {'private': 'bi-lock-fill', 'shared': 'bi-people-fill', + 'public': 'bi-globe'}.get(self.visibility, 'bi-question') def get_image(self): """Get the best available image (uploaded cover takes priority).""" @@ -71,6 +135,7 @@ class Set(db.Model): 'piece_count': self.piece_count, 'image_url': self.image_url, 'is_moc': self.is_moc, + 'visibility': self.visibility, 'moc_designer': self.moc_designer, 'moc_description': self.moc_description, 'instruction_count': self.instructions.count(),