refac
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
"""add pinned_note table
|
||||||
|
|
||||||
|
Revision ID: 4de81c2a3af1
|
||||||
|
Revises: 56359461a091
|
||||||
|
Create Date: 2026-05-09 04:29:27.651341
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import open_webui.internal.db
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '4de81c2a3af1'
|
||||||
|
down_revision: Union[str, None] = '56359461a091'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
import time
|
||||||
|
from sqlalchemy import select, update, insert
|
||||||
|
from sqlalchemy.sql import table, column
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'pinned_note',
|
||||||
|
sa.Column('id', sa.Text(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Text(), nullable=False),
|
||||||
|
sa.Column('note_id', sa.Text(), sa.ForeignKey('note.id', ondelete='CASCADE'), nullable=False),
|
||||||
|
sa.Column('created_at', sa.BigInteger(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('user_id', 'note_id', name='uq_pinned_note')
|
||||||
|
)
|
||||||
|
|
||||||
|
conn = op.get_bind()
|
||||||
|
|
||||||
|
note_table = table('note',
|
||||||
|
column('id', sa.Text),
|
||||||
|
column('user_id', sa.Text),
|
||||||
|
column('is_pinned', sa.Boolean)
|
||||||
|
)
|
||||||
|
|
||||||
|
pinned_note_table = table('pinned_note',
|
||||||
|
column('id', sa.Text),
|
||||||
|
column('user_id', sa.Text),
|
||||||
|
column('note_id', sa.Text),
|
||||||
|
column('created_at', sa.BigInteger)
|
||||||
|
)
|
||||||
|
|
||||||
|
notes = conn.execute(
|
||||||
|
select(note_table.c.id, note_table.c.user_id).where(note_table.c.is_pinned == True)
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
if notes:
|
||||||
|
now = int(time.time_ns())
|
||||||
|
conn.execute(
|
||||||
|
insert(pinned_note_table),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": str(uuid.uuid4()),
|
||||||
|
"user_id": note[1],
|
||||||
|
"note_id": note[0],
|
||||||
|
"created_at": now
|
||||||
|
}
|
||||||
|
for note in notes
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with op.batch_alter_table('note', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('is_pinned')
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
with op.batch_alter_table('note', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('is_pinned', sa.Boolean(), nullable=True))
|
||||||
|
|
||||||
|
conn = op.get_bind()
|
||||||
|
|
||||||
|
note_table = table('note',
|
||||||
|
column('id', sa.Text),
|
||||||
|
column('is_pinned', sa.Boolean)
|
||||||
|
)
|
||||||
|
|
||||||
|
pinned_note_table = table('pinned_note',
|
||||||
|
column('note_id', sa.Text)
|
||||||
|
)
|
||||||
|
|
||||||
|
notes = conn.execute(select(pinned_note_table.c.note_id)).fetchall()
|
||||||
|
|
||||||
|
for note in notes:
|
||||||
|
conn.execute(
|
||||||
|
update(note_table).where(note_table.c.id == note[0]).values(is_pinned=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
op.drop_table('pinned_note')
|
||||||
@@ -13,7 +13,7 @@ from open_webui.models.access_grants import AccessGrantModel, AccessGrants
|
|||||||
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
from sqlalchemy import BigInteger, Column, Text, JSON
|
from sqlalchemy import BigInteger, Column, Text, JSON, ForeignKey
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Note DB Schema
|
# Note DB Schema
|
||||||
@@ -29,7 +29,6 @@ class Note(Base):
|
|||||||
title = Column(Text)
|
title = Column(Text)
|
||||||
data = Column(JSON, nullable=True)
|
data = Column(JSON, nullable=True)
|
||||||
meta = Column(JSON, nullable=True)
|
meta = Column(JSON, nullable=True)
|
||||||
is_pinned = Column(Boolean, default=False, nullable=True)
|
|
||||||
|
|
||||||
created_at = Column(BigInteger)
|
created_at = Column(BigInteger)
|
||||||
updated_at = Column(BigInteger)
|
updated_at = Column(BigInteger)
|
||||||
@@ -52,6 +51,15 @@ class NoteModel(BaseModel):
|
|||||||
updated_at: int # timestamp in epoch
|
updated_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
|
||||||
|
class PinnedNote(Base):
|
||||||
|
__tablename__ = 'pinned_note'
|
||||||
|
|
||||||
|
id = Column(Text, primary_key=True)
|
||||||
|
user_id = Column(Text, nullable=False)
|
||||||
|
note_id = Column(Text, ForeignKey('note.id', ondelete='CASCADE'), nullable=False)
|
||||||
|
created_at = Column(BigInteger, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
####################
|
####################
|
||||||
@@ -100,6 +108,7 @@ class NoteTable:
|
|||||||
access_grants: Optional[list[AccessGrantModel]] = None,
|
access_grants: Optional[list[AccessGrantModel]] = None,
|
||||||
db: Optional[AsyncSession] = None,
|
db: Optional[AsyncSession] = None,
|
||||||
) -> NoteModel:
|
) -> NoteModel:
|
||||||
|
# We exclude access_grants to inject them
|
||||||
note_data = NoteModel.model_validate(note).model_dump(exclude={'access_grants'})
|
note_data = NoteModel.model_validate(note).model_dump(exclude={'access_grants'})
|
||||||
note_data['access_grants'] = (
|
note_data['access_grants'] = (
|
||||||
access_grants if access_grants is not None else await self._get_access_grants(note_data['id'], db=db)
|
access_grants if access_grants is not None else await self._get_access_grants(note_data['id'], db=db)
|
||||||
@@ -314,15 +323,29 @@ class NoteTable:
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
return await self._to_note_model(note, db=db) if note else None
|
return await self._to_note_model(note, db=db) if note else None
|
||||||
|
|
||||||
async def toggle_note_pinned_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[NoteModel]:
|
async def toggle_note_pinned_by_id(self, id: str, user_id: str, db: Optional[AsyncSession] = None) -> Optional[NoteModel]:
|
||||||
try:
|
try:
|
||||||
async with get_async_db_context(db) as db:
|
async with get_async_db_context(db) as db:
|
||||||
result = await db.execute(select(Note).filter(Note.id == id))
|
result = await db.execute(select(Note).filter(Note.id == id))
|
||||||
note = result.scalars().first()
|
note = result.scalars().first()
|
||||||
if not note:
|
if not note:
|
||||||
return None
|
return None
|
||||||
note.is_pinned = not note.is_pinned
|
|
||||||
note.updated_at = int(time.time_ns())
|
# Check if already pinned
|
||||||
|
pin_result = await db.execute(select(PinnedNote).filter_by(user_id=user_id, note_id=id))
|
||||||
|
pinned_note = pin_result.scalars().first()
|
||||||
|
|
||||||
|
if pinned_note:
|
||||||
|
await db.execute(delete(PinnedNote).filter_by(user_id=user_id, note_id=id))
|
||||||
|
else:
|
||||||
|
new_pin = PinnedNote(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
user_id=user_id,
|
||||||
|
note_id=id,
|
||||||
|
created_at=int(time.time_ns())
|
||||||
|
)
|
||||||
|
db.add(new_pin)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return await self._to_note_model(note, db=db)
|
return await self._to_note_model(note, db=db)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -338,7 +361,7 @@ class NoteTable:
|
|||||||
user_groups = await Groups.get_groups_by_member_id(user_id, db=db)
|
user_groups = await Groups.get_groups_by_member_id(user_id, db=db)
|
||||||
user_group_ids = [group.id for group in user_groups]
|
user_group_ids = [group.id for group in user_groups]
|
||||||
|
|
||||||
stmt = select(Note).filter(Note.is_pinned == True).order_by(Note.updated_at.desc())
|
stmt = select(Note).join(PinnedNote, PinnedNote.note_id == Note.id).filter(PinnedNote.user_id == user_id).order_by(PinnedNote.created_at.desc())
|
||||||
stmt = self._has_permission(db, stmt, {'user_id': user_id, 'group_ids': user_group_ids}, permission)
|
stmt = self._has_permission(db, stmt, {'user_id': user_id, 'group_ids': user_group_ids}, permission)
|
||||||
|
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
@@ -351,11 +374,17 @@ class NoteTable:
|
|||||||
try:
|
try:
|
||||||
async with get_async_db_context(db) as db:
|
async with get_async_db_context(db) as db:
|
||||||
await AccessGrants.revoke_all_access('note', id, db=db)
|
await AccessGrants.revoke_all_access('note', id, db=db)
|
||||||
|
await db.execute(delete(PinnedNote).filter(PinnedNote.note_id == id))
|
||||||
await db.execute(delete(Note).filter(Note.id == id))
|
await db.execute(delete(Note).filter(Note.id == id))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def get_pinned_note_ids(self, user_id: str, db: Optional[AsyncSession] = None) -> list[str]:
|
||||||
|
async with get_async_db_context(db) as db:
|
||||||
|
result = await db.execute(select(PinnedNote.note_id).filter_by(user_id=user_id))
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
Notes = NoteTable()
|
Notes = NoteTable()
|
||||||
|
|||||||
@@ -92,10 +92,13 @@ async def get_notes(
|
|||||||
user_ids = list(set(note.user_id for note in notes))
|
user_ids = list(set(note.user_id for note in notes))
|
||||||
users = {user.id: user for user in await Users.get_users_by_user_ids(user_ids, db=db)}
|
users = {user.id: user for user in await Users.get_users_by_user_ids(user_ids, db=db)}
|
||||||
|
|
||||||
|
pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
NoteUserResponse(
|
NoteUserResponse(
|
||||||
**{
|
**{
|
||||||
**note.model_dump(),
|
**note.model_dump(),
|
||||||
|
'is_pinned': note.id in pinned_note_ids,
|
||||||
'data': _truncate_note_data(note.data),
|
'data': _truncate_note_data(note.data),
|
||||||
'user': UserResponse(**users[note.user_id].model_dump()),
|
'user': UserResponse(**users[note.user_id].model_dump()),
|
||||||
}
|
}
|
||||||
@@ -135,6 +138,7 @@ async def get_pinned_notes(
|
|||||||
NoteUserResponse(
|
NoteUserResponse(
|
||||||
**{
|
**{
|
||||||
**note.model_dump(),
|
**note.model_dump(),
|
||||||
|
'is_pinned': True,
|
||||||
'data': _truncate_note_data(note.data),
|
'data': _truncate_note_data(note.data),
|
||||||
'user': UserResponse(**users[note.user_id].model_dump()),
|
'user': UserResponse(**users[note.user_id].model_dump()),
|
||||||
}
|
}
|
||||||
@@ -190,7 +194,9 @@ async def search_notes(
|
|||||||
filter['user_id'] = user.id
|
filter['user_id'] = user.id
|
||||||
|
|
||||||
result = await Notes.search_notes(user.id, filter, skip=skip, limit=limit, db=db)
|
result = await Notes.search_notes(user.id, filter, skip=skip, limit=limit, db=db)
|
||||||
|
pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db)
|
||||||
for note in result.items:
|
for note in result.items:
|
||||||
|
note.is_pinned = note.id in pinned_note_ids
|
||||||
note.data = _truncate_note_data(note.data)
|
note.data = _truncate_note_data(note.data)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -287,7 +293,8 @@ async def get_note_by_id(
|
|||||||
or has_public_write_access_grant(note.access_grants)
|
or has_public_write_access_grant(note.access_grants)
|
||||||
)
|
)
|
||||||
|
|
||||||
return NoteResponse(**note.model_dump(), write_access=write_access)
|
pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db)
|
||||||
|
return NoteResponse(**note.model_dump(), write_access=write_access, is_pinned=note.id in pinned_note_ids)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
@@ -338,6 +345,9 @@ async def update_note_by_id(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
note = await Notes.update_note_by_id(id, form_data, db=db)
|
note = await Notes.update_note_by_id(id, form_data, db=db)
|
||||||
|
pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db)
|
||||||
|
note.is_pinned = note.id in pinned_note_ids
|
||||||
|
|
||||||
await sio.emit(
|
await sio.emit(
|
||||||
'note-events',
|
'note-events',
|
||||||
note.model_dump(),
|
note.model_dump(),
|
||||||
@@ -401,7 +411,10 @@ async def update_note_access_by_id(
|
|||||||
|
|
||||||
await AccessGrants.set_access_grants('note', id, form_data.access_grants, db=db)
|
await AccessGrants.set_access_grants('note', id, form_data.access_grants, db=db)
|
||||||
|
|
||||||
return await Notes.get_note_by_id(id, db=db)
|
note = await Notes.get_note_by_id(id, db=db)
|
||||||
|
pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db)
|
||||||
|
note.is_pinned = note.id in pinned_note_ids
|
||||||
|
return note
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
@@ -440,7 +453,9 @@ async def pin_note_by_id(
|
|||||||
):
|
):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
||||||
|
|
||||||
note = await Notes.toggle_note_pinned_by_id(id, db=db)
|
note = await Notes.toggle_note_pinned_by_id(id, user.id, db=db)
|
||||||
|
pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db)
|
||||||
|
note.is_pinned = note.id in pinned_note_ids
|
||||||
return note
|
return note
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user