nordabiz/blueprints/community/contacts/routes.py
Maciej Pienczyn 4181a2e760 refactor: Migrate access control from is_admin to role-based system
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>
2026-02-01 21:05:22 +01:00

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()