Skip to content

Commit

Permalink
Adding memory to agenda item collapsed
Browse files Browse the repository at this point in the history
  • Loading branch information
cyrillkuettel committed Nov 26, 2024
1 parent 0308637 commit 4d2db30
Show file tree
Hide file tree
Showing 11 changed files with 549 additions and 36 deletions.
8 changes: 7 additions & 1 deletion src/privatim/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
from privatim.models.consultation import Consultation
from privatim.models.comment import Comment
from privatim.models.meeting import Meeting, AgendaItem
from privatim.models.association_tables import MeetingUserAttendance
from privatim.models.association_tables import (
MeetingUserAttendance,
AgendaItemDisplayState,
AgendaItemStatePreference,
)
from privatim.models.file import GeneralFile, SearchableFile
from privatim.models.password_change_token import PasswordChangeToken
from privatim.models.tan import TAN
Expand All @@ -30,6 +34,8 @@
Comment
Meeting
MeetingUserAttendance
AgendaItemDisplayState
AgendaItemStatePreference
AgendaItem
PasswordChangeToken
GeneralFile
Expand Down
41 changes: 39 additions & 2 deletions src/privatim/models/association_tables.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from enum import Enum as PyEnum
from sqlalchemy import Enum
from sqlalchemy import ForeignKey
from enum import IntEnum
from sqlalchemy import ForeignKey, Integer, Enum
from privatim.orm.meta import UUIDStrPK

from sqlalchemy.orm import relationship, mapped_column, Mapped
from privatim.orm import Base
from privatim.orm.uuid_type import UUIDStr
Expand Down Expand Up @@ -43,3 +45,38 @@ class MeetingUserAttendance(Base):

def __repr__(self) -> str:
return f'<MeetingUserAttendance {self.user_id} {self.status}>'


class AgendaItemDisplayState(IntEnum):
COLLAPSED = 0
EXPANDED = 1


class AgendaItemStatePreference(Base):
"""Tracks user preferences for agenda item display states
(expanded/collapsed)"""

__tablename__ = 'agenda_item_state_preferences'

id: Mapped[UUIDStrPK]

user_id: Mapped[UUIDStr] = mapped_column(
ForeignKey('users.id', ondelete='CASCADE')
)

agenda_item_id: Mapped[UUIDStr] = mapped_column(
ForeignKey('agenda_items.id', ondelete='CASCADE')
)

state: Mapped[AgendaItemDisplayState] = mapped_column(
Integer,
default=AgendaItemDisplayState.COLLAPSED
)

user: Mapped['User'] = relationship(
'User',
back_populates='agenda_item_state_preferences'
)

def __repr__(self) -> str:
return f'<AgendaItemStatePreference {self.state}>'
22 changes: 21 additions & 1 deletion src/privatim/models/meeting.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@

from privatim.orm.meta import UUIDStr as UUIDStrType
from privatim.models import SearchableMixin
from privatim.models.association_tables import AttendanceStatus
from privatim.models.association_tables import AttendanceStatus, \
AgendaItemDisplayState, AgendaItemStatePreference
from privatim.models.association_tables import MeetingUserAttendance
from privatim.orm.uuid_type import UUIDStr
from privatim.orm import Base
Expand All @@ -25,6 +26,7 @@
from privatim.models import WorkingGroup
from privatim.types import ACL
from sqlalchemy.orm import InstrumentedAttribute
from pyramid.interfaces import IRequest


class AgendaItemCreationError(Exception):
Expand Down Expand Up @@ -103,6 +105,24 @@ def searchable_fields(cls) -> Iterator['InstrumentedAttribute[str]']:
yield cls.title
yield cls.description

def get_display_state_for_user(
self,
request: 'IRequest',
) -> AgendaItemDisplayState:
session = request.dbsession
user = request.user
if not session:
return AgendaItemDisplayState.COLLAPSED

preference = session.execute(
select(AgendaItemStatePreference).where(
AgendaItemStatePreference.agenda_item_id
== self.id, AgendaItemStatePreference.user_id == user.id,
)
).scalar_one_or_none()
return preference.state if preference \
else (AgendaItemDisplayState.COLLAPSED)

def __acl__(self) -> list['ACL']:
return [
(Allow, Authenticated, ['view']),
Expand Down
27 changes: 26 additions & 1 deletion src/privatim/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@

from typing import TYPE_CHECKING
if TYPE_CHECKING:
from privatim.models.association_tables import MeetingUserAttendance
from privatim.models.association_tables import (
MeetingUserAttendance,
AgendaItemStatePreference,
AgendaItemDisplayState,
)
from privatim.types import ACL
from sqlalchemy.orm import Session
from pyramid.interfaces import IRequest
Expand Down Expand Up @@ -180,6 +184,26 @@ def _meetings_expression(cls) -> 'ScalarSelect[Meeting]':
foreign_keys='Meeting.creator_id',
)

agenda_item_state_preferences: Mapped[list['AgendaItemStatePreference']] \
= (relationship(
'AgendaItemStatePreference',
back_populates='user',
cascade='all, delete-orphan',
)
)

def get_agenda_item_state(
self,
agenda_item_id: str
) -> 'AgendaItemDisplayState':
"""Get the display state for a specific agenda item"""
pref = next(
(p for p in self.agenda_item_state_preferences
if p.agenda_item_id == agenda_item_id),
None
)
return pref.state if pref else AgendaItemDisplayState.COLLAPSED

def set_password(self, password: str) -> None:
password = password or ''
pwhash = bcrypt.hashpw(password.encode('utf8'), bcrypt.gensalt())
Expand Down Expand Up @@ -221,6 +245,7 @@ def fullname(self) -> str:

@property
def is_admin(self) -> bool:
""" This is only used for the badge in the user list (!) """
return ('admin' in self.first_name.lower() or 'admin' in
self.last_name.lower())

Expand Down
87 changes: 77 additions & 10 deletions src/privatim/static/js/custom/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ document.addEventListener('DOMContentLoaded', function () {
setupCommentAnswerField();
addEditorForCommentsEdit();
makeConsultationsInActivitiesClickable();
setupAgendaItemGlobalToggle();
setupExpandOrCollapseAll();
setupDeleteModalListeners();
autoHideSuccessMessages();
addTestSystemBadge();
fixCSSonProfilePage();
handleSingleAgendaItemClickToggleStateUpdate();
});



function setupDeleteModalListeners() {
var active_popover = null;
var popover_timeout = null;
Expand Down Expand Up @@ -269,21 +271,31 @@ function setupCommentAnswerField() {
}
}

// Expand / collapse all Agenda Items
function setupAgendaItemGlobalToggle() {
function setupExpandOrCollapseAll() {
if (!window.location.href.includes('/meeting')) {
return;
}

const toggleBtn = document.getElementById('toggleAllItems');
if (!toggleBtn) {
return;
}
const accordionItems = document.querySelectorAll('.accordion-collapse');
let isExpanded = false;

function toggleAll() {
// Initialize isExpanded based on actual state of items
let isExpanded = Array.from(accordionItems)
.every(item => item.classList.contains('show'));

// Update button initial state to match
const btnText = toggleBtn.querySelector('span');
const btnIcon = toggleBtn.querySelector('i');
if (isExpanded) {
btnText.textContent = toggleBtn.dataset.collapseText;
btnIcon.classList.replace('fa-caret-down', 'fa-caret-up');
}

async function toggleAll() {
isExpanded = !isExpanded;
// Update UI
accordionItems.forEach(item => {
const bsCollapse = new bootstrap.Collapse(item, {
toggle: false
Expand All @@ -294,21 +306,38 @@ function setupAgendaItemGlobalToggle() {
bsCollapse.hide();
}
});

// Update button text and icon
const btnText = toggleBtn.querySelector('span');
const btnIcon = toggleBtn.querySelector('i');
if (isExpanded) {
btnText.textContent = toggleBtn.dataset.collapseText;
btnIcon.classList.replace('fa-caret-down', 'fa-caret-up');
} else {
btnText.textContent = toggleBtn.dataset.expandText;
btnIcon.classList.replace('fa-caret-up', 'fa-caret-down');
}
// Get meeting ID from URL
const id = window.location.pathname.split('/').pop();
console.log('Meeting ID:', id);
// Update server state
try {
const response = await fetch(`/meeting/${id}/agenda_items/bulk/state`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('[name=csrf_token]').value
},
body: JSON.stringify({
state: isExpanded ? 1 : 0,
})
});
if (!response.ok) {
throw new Error('Failed to update states');
}
} catch (error) {
console.error('Error updating agenda item states:', error);
}
}

toggleBtn.addEventListener('click', toggleAll);

}


Expand Down Expand Up @@ -474,3 +503,41 @@ function fixCSSonProfilePage() {
window.location.reload(true);
}
}


function handleSingleAgendaItemClickToggleStateUpdate() {
const accordion = document.querySelector('#agenda-items');
if (!accordion) return;

// Initialize individual item toggling
document.querySelectorAll('.agenda-item-accordion').forEach(button => {
button.addEventListener('click', async () => {
const id = button.closest('.accordion-item')
.querySelector('.accordion-collapse').id.replace('item-', '');
const isExpanded = button.getAttribute('aria-expanded') === 'true';
const newState = isExpanded ? 1 : 0;
console.log(`Toggling state for item ${id} to ${newState}`);
await updateItemState(id, newState);
});
});

}

// Helper function to update item state
async function updateItemState(id, newState) {
try {
const response = await fetch(`/agenda_items/${id}/state`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('[name=csrf_token]').value
},
body: JSON.stringify({state: newState})
});
if (!response.ok) {
throw new Error('Failed to update state');
}
} catch (error) {
console.error('Error updating agenda item state:', error);
}
}
42 changes: 42 additions & 0 deletions src/privatim/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from privatim.views.agenda_items import (
add_agenda_item_view,
copy_agenda_item_view,
update_single_agenda_item_state,
update_bulk_agenda_items_state
)
from privatim.views.agenda_items import delete_agenda_item_view
from privatim.views.agenda_items import edit_agenda_item_view
Expand Down Expand Up @@ -462,6 +464,46 @@ def includeme(config: 'Configurator') -> None:
xhr=True
)

config.add_route(
'update_single_agenda_item_state,',
'/agenda_items/{id}/state',
factory=agenda_item_factory
)
config.add_view(
update_single_agenda_item_state,
route_name='update_single_agenda_item_state,',
renderer='json',
request_method='POST',
xhr=False
)
config.add_view(
update_single_agenda_item_state,
route_name='update_single_agenda_item_state,',
renderer='json',
request_method='POST',
xhr=True
)

config.add_route(
'update_bulk_agenda_items_state',
'/meeting/{id}/agenda_items/bulk/state',
factory=meeting_factory
)
config.add_view(
update_bulk_agenda_items_state,
route_name='update_bulk_agenda_items_state',
renderer='json',
request_method='POST',
xhr=False
)
config.add_view(
update_bulk_agenda_items_state,
route_name='update_bulk_agenda_items_state',
renderer='json',
request_method='POST',
xhr=True
)

config.add_route(
'sortable_agenda_items',
'/meetings/agenda_items/{id}/move/{subject_id}/{direction}/{'
Expand Down
Loading

0 comments on commit 4d2db30

Please sign in to comment.