Add Note Sharing feature.
This commit is contained in:
@@ -27,6 +27,7 @@ from .dashboard import DashboardWidget, WidgetTemplate
|
||||
from .work_config import WorkConfig
|
||||
from .invitation import CompanyInvitation
|
||||
from .note import Note, NoteVisibility, NoteLink, NoteFolder
|
||||
from .note_share import NoteShare
|
||||
|
||||
# Make all models available at package level
|
||||
__all__ = [
|
||||
@@ -47,5 +48,5 @@ __all__ = [
|
||||
'DashboardWidget', 'WidgetTemplate',
|
||||
'WorkConfig',
|
||||
'CompanyInvitation',
|
||||
'Note', 'NoteVisibility', 'NoteLink', 'NoteFolder'
|
||||
'Note', 'NoteVisibility', 'NoteLink', 'NoteFolder', 'NoteShare'
|
||||
]
|
||||
@@ -5,7 +5,7 @@ Migrated from models_old.py to maintain consistency with the new modular structu
|
||||
|
||||
import enum
|
||||
import re
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import UniqueConstraint
|
||||
|
||||
@@ -223,6 +223,44 @@ class Note(db.Model):
|
||||
self.tags = metadata['tags']
|
||||
if 'pinned' in metadata:
|
||||
self.is_pinned = bool(metadata['pinned'])
|
||||
|
||||
def create_share_link(self, expires_in_days=None, password=None, max_views=None, created_by=None):
|
||||
"""Create a public share link for this note"""
|
||||
from .note_share import NoteShare
|
||||
from flask import g
|
||||
|
||||
share = NoteShare(
|
||||
note_id=self.id,
|
||||
created_by_id=created_by.id if created_by else g.user.id
|
||||
)
|
||||
|
||||
# Set expiration
|
||||
if expires_in_days:
|
||||
share.expires_at = datetime.now() + timedelta(days=expires_in_days)
|
||||
|
||||
# Set password
|
||||
if password:
|
||||
share.set_password(password)
|
||||
|
||||
# Set view limit
|
||||
if max_views:
|
||||
share.max_views = max_views
|
||||
|
||||
db.session.add(share)
|
||||
return share
|
||||
|
||||
def get_active_shares(self):
|
||||
"""Get all active share links for this note"""
|
||||
return [s for s in self.shares if s.is_valid()]
|
||||
|
||||
def get_all_shares(self):
|
||||
"""Get all share links for this note"""
|
||||
from models.note_share import NoteShare
|
||||
return self.shares.order_by(NoteShare.created_at.desc()).all()
|
||||
|
||||
def has_active_shares(self):
|
||||
"""Check if this note has any active share links"""
|
||||
return any(s.is_valid() for s in self.shares)
|
||||
|
||||
|
||||
class NoteLink(db.Model):
|
||||
|
||||
107
models/note_share.py
Normal file
107
models/note_share.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from datetime import datetime, timedelta
|
||||
from . import db
|
||||
import secrets
|
||||
import string
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
|
||||
class NoteShare(db.Model):
|
||||
"""Public sharing links for notes"""
|
||||
__tablename__ = 'note_share'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
note_id = db.Column(db.Integer, db.ForeignKey('note.id', ondelete='CASCADE'), nullable=False)
|
||||
token = db.Column(db.String(64), unique=True, nullable=False, index=True)
|
||||
|
||||
# Share settings
|
||||
expires_at = db.Column(db.DateTime, nullable=True) # None means no expiry
|
||||
password_hash = db.Column(db.String(255), nullable=True) # Optional password protection
|
||||
view_count = db.Column(db.Integer, default=0)
|
||||
max_views = db.Column(db.Integer, nullable=True) # Limit number of views
|
||||
|
||||
# Metadata
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
last_accessed_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
note = db.relationship('Note', backref=db.backref('shares', cascade='all, delete-orphan', lazy='dynamic'))
|
||||
created_by = db.relationship('User', foreign_keys=[created_by_id])
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(NoteShare, self).__init__(**kwargs)
|
||||
if not self.token:
|
||||
self.token = self.generate_token()
|
||||
|
||||
@staticmethod
|
||||
def generate_token():
|
||||
"""Generate a secure random token"""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(32))
|
||||
|
||||
def set_password(self, password):
|
||||
"""Set password protection for the share"""
|
||||
if password:
|
||||
self.password_hash = generate_password_hash(password)
|
||||
else:
|
||||
self.password_hash = None
|
||||
|
||||
def check_password(self, password):
|
||||
"""Check if the provided password is correct"""
|
||||
if not self.password_hash:
|
||||
return True
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def is_valid(self):
|
||||
"""Check if share link is still valid"""
|
||||
# Check expiration
|
||||
if self.expires_at and datetime.now() > self.expires_at:
|
||||
return False
|
||||
|
||||
# Check view count
|
||||
if self.max_views and self.view_count >= self.max_views:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_expired(self):
|
||||
"""Check if the share has expired"""
|
||||
if self.expires_at and datetime.now() > self.expires_at:
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_view_limit_reached(self):
|
||||
"""Check if view limit has been reached"""
|
||||
if self.max_views and self.view_count >= self.max_views:
|
||||
return True
|
||||
return False
|
||||
|
||||
def record_access(self):
|
||||
"""Record that the share was accessed"""
|
||||
self.view_count += 1
|
||||
self.last_accessed_at = datetime.now()
|
||||
|
||||
def get_share_url(self, _external=True):
|
||||
"""Get the full URL for this share"""
|
||||
from flask import url_for
|
||||
return url_for('notes_public.view_shared_note',
|
||||
token=self.token,
|
||||
_external=_external)
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert share to dictionary for API responses"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'token': self.token,
|
||||
'url': self.get_share_url(),
|
||||
'expires_at': self.expires_at.isoformat() if self.expires_at else None,
|
||||
'has_password': bool(self.password_hash),
|
||||
'max_views': self.max_views,
|
||||
'view_count': self.view_count,
|
||||
'created_at': self.created_at.isoformat(),
|
||||
'created_by': self.created_by.username,
|
||||
'last_accessed_at': self.last_accessed_at.isoformat() if self.last_accessed_at else None,
|
||||
'is_valid': self.is_valid(),
|
||||
'is_expired': self.is_expired(),
|
||||
'is_view_limit_reached': self.is_view_limit_reached()
|
||||
}
|
||||
Reference in New Issue
Block a user