Replace ~170 manual `if not current_user.is_admin` checks with: - @role_required(SystemRole.ADMIN) for user management, security, ZOPK - @role_required(SystemRole.OFFICE_MANAGER) for content management - current_user.can_access_admin_panel() for admin UI access - current_user.can_moderate_forum() for forum moderation - current_user.can_edit_company(id) for company permissions Add @office_manager_required decorator shortcut. Add SQL migration to sync existing users' role field. Role hierarchy: UNAFFILIATED(10) < MEMBER(20) < EMPLOYEE(30) < MANAGER(40) < OFFICE_MANAGER(50) < ADMIN(100) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
300 lines
12 KiB
Python
300 lines
12 KiB
Python
"""
|
|
Contacts Routes
|
|
===============
|
|
|
|
External contacts management - page endpoints only.
|
|
API endpoints (/api/contacts/*) remain in app.py for backwards compatibility.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from flask import render_template, request, redirect, url_for, flash, current_app
|
|
from flask_login import login_required, current_user
|
|
from sqlalchemy import or_
|
|
|
|
from . import bp
|
|
from database import SessionLocal, ExternalContact
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@bp.route('/', endpoint='contacts_list')
|
|
@login_required
|
|
def list():
|
|
"""
|
|
Lista kontaktów zewnętrznych - urzędy, instytucje, partnerzy.
|
|
Dostępna dla wszystkich zalogowanych członków.
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
page = request.args.get('page', 1, type=int)
|
|
per_page = 20
|
|
search = request.args.get('q', '').strip()
|
|
org_type = request.args.get('type', '')
|
|
project = request.args.get('project', '')
|
|
|
|
query = db.query(ExternalContact).filter(ExternalContact.is_active == True)
|
|
|
|
# Search filter
|
|
if search:
|
|
search_pattern = f'%{search}%'
|
|
query = query.filter(
|
|
or_(
|
|
ExternalContact.first_name.ilike(search_pattern),
|
|
ExternalContact.last_name.ilike(search_pattern),
|
|
ExternalContact.organization_name.ilike(search_pattern),
|
|
ExternalContact.position.ilike(search_pattern),
|
|
ExternalContact.project_name.ilike(search_pattern),
|
|
ExternalContact.tags.ilike(search_pattern)
|
|
)
|
|
)
|
|
|
|
# Organization type filter
|
|
if org_type and org_type in ExternalContact.ORGANIZATION_TYPES:
|
|
query = query.filter(ExternalContact.organization_type == org_type)
|
|
|
|
# Project filter
|
|
if project:
|
|
query = query.filter(ExternalContact.project_name.ilike(f'%{project}%'))
|
|
|
|
# Order by organization name, then last name
|
|
query = query.order_by(
|
|
ExternalContact.organization_name,
|
|
ExternalContact.last_name
|
|
)
|
|
|
|
# Pagination
|
|
total = query.count()
|
|
contacts = query.offset((page - 1) * per_page).limit(per_page).all()
|
|
total_pages = (total + per_page - 1) // per_page
|
|
|
|
# Get unique projects for filter dropdown
|
|
projects = db.query(ExternalContact.project_name).filter(
|
|
ExternalContact.is_active == True,
|
|
ExternalContact.project_name.isnot(None),
|
|
ExternalContact.project_name != ''
|
|
).distinct().order_by(ExternalContact.project_name).all()
|
|
project_names = [p[0] for p in projects if p[0]]
|
|
|
|
return render_template('contacts/list.html',
|
|
contacts=contacts,
|
|
page=page,
|
|
total_pages=total_pages,
|
|
total=total,
|
|
search=search,
|
|
org_type=org_type,
|
|
project=project,
|
|
org_types=ExternalContact.ORGANIZATION_TYPES,
|
|
org_type_labels=ExternalContact.ORGANIZATION_TYPE_LABELS,
|
|
project_names=project_names)
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/<int:contact_id>', endpoint='contact_detail')
|
|
@login_required
|
|
def detail(contact_id):
|
|
"""
|
|
Szczegóły kontaktu zewnętrznego - pełna karta osoby.
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
contact = db.query(ExternalContact).filter(
|
|
ExternalContact.id == contact_id,
|
|
ExternalContact.is_active == True
|
|
).first()
|
|
|
|
if not contact:
|
|
flash('Kontakt nie został znaleziony.', 'error')
|
|
return redirect(url_for('.contacts_list'))
|
|
|
|
# Get other contacts from the same organization
|
|
related_contacts = db.query(ExternalContact).filter(
|
|
ExternalContact.organization_name == contact.organization_name,
|
|
ExternalContact.id != contact.id,
|
|
ExternalContact.is_active == True
|
|
).order_by(ExternalContact.last_name).limit(5).all()
|
|
|
|
# Check if current user can edit (creator or admin)
|
|
can_edit = (current_user.can_access_admin_panel() or
|
|
(contact.created_by and contact.created_by == current_user.id))
|
|
|
|
return render_template('contacts/detail.html',
|
|
contact=contact,
|
|
related_contacts=related_contacts,
|
|
can_edit=can_edit,
|
|
org_type_labels=ExternalContact.ORGANIZATION_TYPE_LABELS)
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/dodaj', methods=['GET', 'POST'], endpoint='contact_add')
|
|
@login_required
|
|
def add():
|
|
"""
|
|
Dodawanie nowego kontaktu zewnętrznego.
|
|
Każdy zalogowany użytkownik może dodać kontakt.
|
|
"""
|
|
if request.method == 'POST':
|
|
db = SessionLocal()
|
|
try:
|
|
# Parse related_links from form (JSON)
|
|
related_links_json = request.form.get('related_links', '[]')
|
|
try:
|
|
related_links = json.loads(related_links_json) if related_links_json else []
|
|
except json.JSONDecodeError:
|
|
related_links = []
|
|
|
|
contact = ExternalContact(
|
|
first_name=request.form.get('first_name', '').strip(),
|
|
last_name=request.form.get('last_name', '').strip(),
|
|
position=request.form.get('position', '').strip() or None,
|
|
photo_url=request.form.get('photo_url', '').strip() or None,
|
|
phone=request.form.get('phone', '').strip() or None,
|
|
phone_secondary=request.form.get('phone_secondary', '').strip() or None,
|
|
email=request.form.get('email', '').strip() or None,
|
|
website=request.form.get('website', '').strip() or None,
|
|
linkedin_url=request.form.get('linkedin_url', '').strip() or None,
|
|
facebook_url=request.form.get('facebook_url', '').strip() or None,
|
|
twitter_url=request.form.get('twitter_url', '').strip() or None,
|
|
organization_name=request.form.get('organization_name', '').strip(),
|
|
organization_type=request.form.get('organization_type', 'other'),
|
|
organization_address=request.form.get('organization_address', '').strip() or None,
|
|
organization_website=request.form.get('organization_website', '').strip() or None,
|
|
organization_logo_url=request.form.get('organization_logo_url', '').strip() or None,
|
|
project_name=request.form.get('project_name', '').strip() or None,
|
|
project_description=request.form.get('project_description', '').strip() or None,
|
|
source_type='manual',
|
|
source_url=request.form.get('source_url', '').strip() or None,
|
|
related_links=related_links,
|
|
tags=request.form.get('tags', '').strip() or None,
|
|
notes=request.form.get('notes', '').strip() or None,
|
|
created_by=current_user.id
|
|
)
|
|
|
|
db.add(contact)
|
|
db.commit()
|
|
|
|
flash(f'Kontakt {contact.full_name} został dodany.', 'success')
|
|
return redirect(url_for('.contact_detail', contact_id=contact.id))
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
current_app.logger.error(f"Error adding contact: {e}")
|
|
flash('Wystąpił błąd podczas dodawania kontaktu.', 'error')
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
# GET - show form
|
|
return render_template('contacts/form.html',
|
|
contact=None,
|
|
org_types=ExternalContact.ORGANIZATION_TYPES,
|
|
org_type_labels=ExternalContact.ORGANIZATION_TYPE_LABELS)
|
|
|
|
|
|
@bp.route('/<int:contact_id>/edytuj', methods=['GET', 'POST'], endpoint='contact_edit')
|
|
@login_required
|
|
def edit(contact_id):
|
|
"""
|
|
Edycja kontaktu zewnętrznego.
|
|
Może edytować twórca kontaktu lub admin.
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
contact = db.query(ExternalContact).filter(
|
|
ExternalContact.id == contact_id
|
|
).first()
|
|
|
|
if not contact:
|
|
flash('Kontakt nie został znaleziony.', 'error')
|
|
return redirect(url_for('.contacts_list'))
|
|
|
|
# Check permissions - creator or admin
|
|
if not current_user.can_access_admin_panel() and contact.created_by != current_user.id:
|
|
flash('Nie masz uprawnień do edycji tego kontaktu.', 'error')
|
|
return redirect(url_for('.contact_detail', contact_id=contact_id))
|
|
|
|
if request.method == 'POST':
|
|
# Parse related_links from form (JSON)
|
|
related_links_json = request.form.get('related_links', '[]')
|
|
try:
|
|
related_links = json.loads(related_links_json) if related_links_json else []
|
|
except json.JSONDecodeError:
|
|
related_links = contact.related_links or []
|
|
|
|
contact.first_name = request.form.get('first_name', '').strip()
|
|
contact.last_name = request.form.get('last_name', '').strip()
|
|
contact.position = request.form.get('position', '').strip() or None
|
|
contact.photo_url = request.form.get('photo_url', '').strip() or None
|
|
contact.phone = request.form.get('phone', '').strip() or None
|
|
contact.phone_secondary = request.form.get('phone_secondary', '').strip() or None
|
|
contact.email = request.form.get('email', '').strip() or None
|
|
contact.website = request.form.get('website', '').strip() or None
|
|
contact.linkedin_url = request.form.get('linkedin_url', '').strip() or None
|
|
contact.facebook_url = request.form.get('facebook_url', '').strip() or None
|
|
contact.twitter_url = request.form.get('twitter_url', '').strip() or None
|
|
contact.organization_name = request.form.get('organization_name', '').strip()
|
|
contact.organization_type = request.form.get('organization_type', 'other')
|
|
contact.organization_address = request.form.get('organization_address', '').strip() or None
|
|
contact.organization_website = request.form.get('organization_website', '').strip() or None
|
|
contact.organization_logo_url = request.form.get('organization_logo_url', '').strip() or None
|
|
contact.project_name = request.form.get('project_name', '').strip() or None
|
|
contact.project_description = request.form.get('project_description', '').strip() or None
|
|
contact.source_url = request.form.get('source_url', '').strip() or None
|
|
contact.related_links = related_links
|
|
contact.tags = request.form.get('tags', '').strip() or None
|
|
contact.notes = request.form.get('notes', '').strip() or None
|
|
contact.updated_at = datetime.now()
|
|
|
|
db.commit()
|
|
|
|
flash(f'Kontakt {contact.full_name} został zaktualizowany.', 'success')
|
|
return redirect(url_for('.contact_detail', contact_id=contact.id))
|
|
|
|
# GET - show form with existing data
|
|
return render_template('contacts/form.html',
|
|
contact=contact,
|
|
org_types=ExternalContact.ORGANIZATION_TYPES,
|
|
org_type_labels=ExternalContact.ORGANIZATION_TYPE_LABELS)
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/<int:contact_id>/usun', methods=['POST'], endpoint='contact_delete')
|
|
@login_required
|
|
def delete(contact_id):
|
|
"""
|
|
Usuwanie kontaktu zewnętrznego (soft delete).
|
|
Może usunąć twórca kontaktu lub admin.
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
contact = db.query(ExternalContact).filter(
|
|
ExternalContact.id == contact_id
|
|
).first()
|
|
|
|
if not contact:
|
|
flash('Kontakt nie został znaleziony.', 'error')
|
|
return redirect(url_for('.contacts_list'))
|
|
|
|
# Check permissions - creator or admin
|
|
if not current_user.can_access_admin_panel() and contact.created_by != current_user.id:
|
|
flash('Nie masz uprawnień do usunięcia tego kontaktu.', 'error')
|
|
return redirect(url_for('.contact_detail', contact_id=contact_id))
|
|
|
|
# Soft delete
|
|
contact.is_active = False
|
|
contact.updated_at = datetime.now()
|
|
db.commit()
|
|
|
|
flash(f'Kontakt {contact.full_name} został usunięty.', 'success')
|
|
return redirect(url_for('.contacts_list'))
|
|
|
|
finally:
|
|
db.close()
|