feat(push): panel preferencji /konto/prywatnosc + triggery B2B (D.1)
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Migracja 101 dodaje 8 nowych kolumn notify_push_* na users (wszystkie default TRUE). Panel preferencji rozszerzony o kartę "Powiadomienia push (na urządzeniu)" z 3 podsekcjami (interakcje dot. mnie, aktualności Izby, wydarzenia) — 9 przełączników. "Nowa wiadomość prywatna" świadomie jest w obu kartach (e-mail + push) — userzy mogą niezależnie wybrać oba kanały. Triggery B2B: - zainteresowanie ogłoszeniem (ClassifiedInterest) → push do autora z notify_push_classified_interest - pytanie do ogłoszenia (ClassifiedQuestion) → push do autora z notify_push_classified_question Fazy D.2 (forum + broadcast) i D.3 (wydarzenia + cron) w kolejnych PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
31c8272e31
commit
46adb0ece7
@ -914,6 +914,18 @@ def konto_prywatnosc():
|
||||
user.contact_prefer_phone = request.form.get('prefer_phone') == 'on'
|
||||
user.contact_prefer_portal = request.form.get('prefer_portal') == 'on'
|
||||
user.notify_email_messages = request.form.get('notify_email_messages') == 'on'
|
||||
|
||||
# Web Push preferences per event type
|
||||
user.notify_push_messages = request.form.get('notify_push_messages') == 'on'
|
||||
user.notify_push_classified_interest = request.form.get('notify_push_classified_interest') == 'on'
|
||||
user.notify_push_classified_question = request.form.get('notify_push_classified_question') == 'on'
|
||||
user.notify_push_forum_reply = request.form.get('notify_push_forum_reply') == 'on'
|
||||
user.notify_push_forum_quote = request.form.get('notify_push_forum_quote') == 'on'
|
||||
user.notify_push_announcements = request.form.get('notify_push_announcements') == 'on'
|
||||
user.notify_push_board_meetings = request.form.get('notify_push_board_meetings') == 'on'
|
||||
user.notify_push_event_invites = request.form.get('notify_push_event_invites') == 'on'
|
||||
user.notify_push_event_reminders = request.form.get('notify_push_event_reminders') == 'on'
|
||||
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Privacy settings updated for user: {user.email}")
|
||||
|
||||
@ -521,6 +521,22 @@ def toggle_interest(classified_id):
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to send classified interest notification: {e}")
|
||||
|
||||
# Web Push to classified author (opt-in via notify_push_classified_interest)
|
||||
try:
|
||||
author = db.query(User).filter(User.id == classified.author_id).first()
|
||||
if author and author.notify_push_classified_interest is not False:
|
||||
from blueprints.push.push_service import send_push
|
||||
interested_name = current_user.name or current_user.email.split('@')[0]
|
||||
send_push(
|
||||
user_id=author.id,
|
||||
title='Zainteresowanie Twoim ogłoszeniem',
|
||||
body=f'{interested_name}: „{classified.title}"',
|
||||
url=f'/tablica/{classified_id}',
|
||||
tag=f'classified-interest-{classified_id}',
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to send classified interest push: {e}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'interested': True,
|
||||
@ -612,7 +628,7 @@ def ask_question(classified_id):
|
||||
classified.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
# Notify classified author (in-app + email)
|
||||
# Notify classified author (in-app + email + push)
|
||||
if classified.author_id != current_user.id:
|
||||
questioner_name = current_user.name or current_user.email.split('@')[0]
|
||||
try:
|
||||
@ -623,6 +639,19 @@ def ask_question(classified_id):
|
||||
send_classified_question_email(
|
||||
classified_id, classified.title, questioner_name, content,
|
||||
author.email, author.name or author.email.split('@')[0])
|
||||
# Web Push (opt-in via notify_push_classified_question)
|
||||
if author and author.notify_push_classified_question is not False:
|
||||
from blueprints.push.push_service import send_push
|
||||
import re as _re
|
||||
preview_plain = _re.sub(r'<[^>]+>', '', content or '').strip()
|
||||
preview = (preview_plain[:80] + '…') if len(preview_plain) > 80 else preview_plain
|
||||
send_push(
|
||||
user_id=author.id,
|
||||
title=f'Nowe pytanie: {classified.title[:60]}',
|
||||
body=f'{questioner_name}: {preview}' if preview else f'{questioner_name} zadał pytanie',
|
||||
url=f'/tablica/{classified_id}',
|
||||
tag=f'classified-question-{classified_id}',
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to send classified question notification: {e}")
|
||||
|
||||
|
||||
12
database.py
12
database.py
@ -339,7 +339,17 @@ class User(Base, UserMixin):
|
||||
|
||||
# Email notification preferences
|
||||
notify_email_messages = Column(Boolean, default=True) # Email when receiving private message
|
||||
notify_push_messages = Column(Boolean, default=True) # Web Push when receiving private message
|
||||
|
||||
# Web Push notification preferences (per event type)
|
||||
notify_push_messages = Column(Boolean, default=True) # Prywatna wiadomość
|
||||
notify_push_classified_interest = Column(Boolean, default=True) # Zainteresowanie ogłoszeniem B2B
|
||||
notify_push_classified_question = Column(Boolean, default=True) # Pytanie pod ogłoszeniem B2B
|
||||
notify_push_forum_reply = Column(Boolean, default=True) # Odpowiedź w moim wątku forum
|
||||
notify_push_forum_quote = Column(Boolean, default=True) # Cytat mojego wpisu forum
|
||||
notify_push_announcements = Column(Boolean, default=True) # Nowa aktualność Izby
|
||||
notify_push_board_meetings = Column(Boolean, default=True) # Posiedzenia Rady (utw./program/protokół)
|
||||
notify_push_event_invites = Column(Boolean, default=True) # Zaproszenie na wydarzenie
|
||||
notify_push_event_reminders = Column(Boolean, default=True) # Przypomnienie 24h przed wydarzeniem
|
||||
|
||||
# Relationships
|
||||
conversations = relationship('AIChatConversation', back_populates='user', cascade='all, delete-orphan')
|
||||
|
||||
15
database/migrations/101_add_push_preferences.sql
Normal file
15
database/migrations/101_add_push_preferences.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- Migration 101: per-event push notification preferences for users
|
||||
--
|
||||
-- 8 nowych kolumn notify_push_* na users (obok istniejących notify_email_messages
|
||||
-- i notify_push_messages z migracji 100). Wszystkie default TRUE — nowi i istniejący
|
||||
-- userzy domyślnie otrzymują wszystkie typy. Kontrola w panelu /konto/prywatnosc.
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS notify_push_classified_interest BOOLEAN DEFAULT TRUE,
|
||||
ADD COLUMN IF NOT EXISTS notify_push_classified_question BOOLEAN DEFAULT TRUE,
|
||||
ADD COLUMN IF NOT EXISTS notify_push_forum_reply BOOLEAN DEFAULT TRUE,
|
||||
ADD COLUMN IF NOT EXISTS notify_push_forum_quote BOOLEAN DEFAULT TRUE,
|
||||
ADD COLUMN IF NOT EXISTS notify_push_announcements BOOLEAN DEFAULT TRUE,
|
||||
ADD COLUMN IF NOT EXISTS notify_push_board_meetings BOOLEAN DEFAULT TRUE,
|
||||
ADD COLUMN IF NOT EXISTS notify_push_event_invites BOOLEAN DEFAULT TRUE,
|
||||
ADD COLUMN IF NOT EXISTS notify_push_event_reminders BOOLEAN DEFAULT TRUE;
|
||||
@ -369,6 +369,127 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<h2 style="display:flex;align-items:center;gap:8px">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="overflow:visible;color:var(--primary)">
|
||||
<rect x="7" y="3" width="10" height="18" rx="2"/>
|
||||
<line x1="11" y1="18" x2="13" y2="18"/>
|
||||
<path d="M19 8c1 1 1.5 2.5 1.5 4s-0.5 3-1.5 4" stroke-linecap="round"/>
|
||||
<path d="M21 6c1.5 1.5 2.3 3.8 2.3 6s-0.8 4.5-2.3 6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
Powiadomienia push (na urządzeniu)
|
||||
</h2>
|
||||
|
||||
<div style="background:#eff6ff;border-left:3px solid #3b82f6;padding:12px 16px;border-radius:6px;font-size:13px;margin-bottom:var(--spacing-md);line-height:1.5;color:#1e40af">
|
||||
<b>Jak to działa:</b> aby otrzymywać powiadomienia, w nagłówku strony kliknij ikonę telefonu z falami i zezwól — na każdym urządzeniu (komputer, Android, iPhone) trzeba to wykonać osobno. Poniższe przełączniki decydują, <b>za co</b> chcesz dostawać powiadomienia — niezależnie od urządzenia.
|
||||
</div>
|
||||
|
||||
<div style="font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-secondary);padding:12px 0 6px;font-weight:600">Interakcje dotyczące mnie</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Nowa wiadomość prywatna</div>
|
||||
<div class="setting-description">Ktoś napisał do Ciebie w rozmowie prywatnej na portalu</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" name="notify_push_messages" {% if user.notify_push_messages != False %}checked{% endif %}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Zainteresowanie Twoim ogłoszeniem B2B</div>
|
||||
<div class="setting-description">Ktoś kliknął „Jestem zainteresowany" przy Twoim ogłoszeniu na tablicy B2B</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" name="notify_push_classified_interest" {% if user.notify_push_classified_interest != False %}checked{% endif %}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Pytanie do Twojego ogłoszenia B2B</div>
|
||||
<div class="setting-description">Ktoś zadał publiczne pytanie pod Twoim ogłoszeniem</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" name="notify_push_classified_question" {% if user.notify_push_classified_question != False %}checked{% endif %}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Odpowiedź w Twoim temacie na forum</div>
|
||||
<div class="setting-description">Ktoś odpisał w wątku, który założyłeś</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" name="notify_push_forum_reply" {% if user.notify_push_forum_reply != False %}checked{% endif %}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Odpowiedź na Twoją wiadomość (cytat)</div>
|
||||
<div class="setting-description">Ktoś cytuje lub odpowiada bezpośrednio pod Twoim wpisem na forum</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" name="notify_push_forum_quote" {% if user.notify_push_forum_quote != False %}checked{% endif %}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-secondary);padding:12px 0 6px;font-weight:600">Aktualności Izby</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Nowa aktualność / ogłoszenie Izby</div>
|
||||
<div class="setting-description">Biuro Izby opublikowało nową aktualność dla wszystkich członków</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" name="notify_push_announcements" {% if user.notify_push_announcements != False %}checked{% endif %}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Posiedzenia Rady Izby</div>
|
||||
<div class="setting-description">Utworzenie nowego posiedzenia, publikacja programu oraz publikacja protokołu</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" name="notify_push_board_meetings" {% if user.notify_push_board_meetings != False %}checked{% endif %}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-secondary);padding:12px 0 6px;font-weight:600">Wydarzenia</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Zaproszenie na wydarzenie</div>
|
||||
<div class="setting-description">Zostałeś dodany do listy uczestników nowego wydarzenia w kalendarzu Izby</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" name="notify_push_event_invites" {% if user.notify_push_event_invites != False %}checked{% endif %}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Przypomnienie 24h przed wydarzeniem</div>
|
||||
<div class="setting-description">Automatyczne przypomnienie dzień przed wydarzeniami, na które jesteś zapisany</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" name="notify_push_event_reminders" {% if user.notify_push_event_reminders != False %}checked{% endif %}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user