165 lines
6.3 KiB
Python
165 lines
6.3 KiB
Python
from datetime import datetime
|
|
from app import db
|
|
|
|
|
|
class Set(db.Model):
|
|
"""Model for LEGO sets."""
|
|
|
|
__tablename__ = 'sets'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
set_number = db.Column(db.String(20), unique=True, nullable=False, index=True)
|
|
set_name = db.Column(db.String(200), nullable=False)
|
|
theme = db.Column(db.String(100), nullable=False, index=True)
|
|
year_released = db.Column(db.Integer, nullable=False, index=True)
|
|
piece_count = db.Column(db.Integer)
|
|
|
|
# MOC (My Own Creation) support
|
|
is_moc = db.Column(db.Boolean, default=False, nullable=False, index=True)
|
|
moc_designer = db.Column(db.String(100), nullable=True) # Designer/creator name
|
|
moc_description = db.Column(db.Text, nullable=True) # Detailed description
|
|
|
|
# Brickset integration
|
|
brickset_id = db.Column(db.Integer, unique=True, nullable=True)
|
|
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)
|
|
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow,
|
|
onupdate=datetime.utcnow)
|
|
|
|
# Relationships
|
|
instructions = db.relationship('Instruction', backref='set', lazy='dynamic',
|
|
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'<Set {self.set_number}: {self.set_name}>'
|
|
|
|
# ---- 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)."""
|
|
if self.cover_image:
|
|
# Ensure forward slashes for web URLs
|
|
clean_path = self.cover_image.replace('\\', '/')
|
|
return f'/static/uploads/{clean_path}'
|
|
return self.image_url
|
|
|
|
@property
|
|
def has_cover_image(self):
|
|
"""Check if set has an uploaded cover image."""
|
|
return bool(self.cover_image)
|
|
|
|
@property
|
|
def cover_image_url(self):
|
|
"""Get the uploaded cover image URL."""
|
|
if self.cover_image:
|
|
clean_path = self.cover_image.replace('\\', '/')
|
|
return f'/static/uploads/{clean_path}'
|
|
return None
|
|
|
|
def to_dict(self):
|
|
"""Convert set to dictionary."""
|
|
return {
|
|
'id': self.id,
|
|
'set_number': self.set_number,
|
|
'set_name': self.set_name,
|
|
'theme': self.theme,
|
|
'year_released': self.year_released,
|
|
'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(),
|
|
'created_at': self.created_at.isoformat(),
|
|
'updated_at': self.updated_at.isoformat()
|
|
}
|
|
|
|
@property
|
|
def instruction_files(self):
|
|
"""Get all instruction files for this set."""
|
|
return self.instructions.order_by(Instruction.page_number).all()
|
|
|
|
@property
|
|
def pdf_instructions(self):
|
|
"""Get only PDF instructions."""
|
|
return self.instructions.filter_by(file_type='PDF').all()
|
|
|
|
@property
|
|
def image_instructions(self):
|
|
"""Get only image instructions."""
|
|
return self.instructions.filter_by(file_type='IMAGE').order_by(
|
|
Instruction.page_number).all()
|
|
|
|
|
|
# Import here to avoid circular imports
|
|
from app.models.instruction import Instruction
|