nordabiz/blueprints/audit/routes.py
Maciej Pienczyn 555cb99c86
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
feat: capture and display Google account email for OAuth connections
After token exchange, fetches Google userinfo to save the email and
name of the Google account used for authorization. Displays this info
on the GBP audit page so users know which account to reconnect with.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 07:32:43 +01:00

655 lines
31 KiB
Python

"""
Audit Dashboard Routes - Audit blueprint
Migrated from app.py as part of the blueprint refactoring.
Contains user-facing audit dashboard pages for SEO, GBP, Social Media, and IT.
"""
import logging
from flask import abort, flash, redirect, render_template, url_for
from flask_login import current_user, login_required
from utils.decorators import is_audit_owner
from database import (
SessionLocal, Company, CompanyWebsiteAnalysis,
CompanySocialMedia, ITAudit, CompanyCitation, GBPReview, OAuthToken
)
from . import bp
logger = logging.getLogger(__name__)
# Check if GBP audit service is available
try:
from gbp_audit_service import (
get_company_audit as gbp_get_company_audit
)
GBP_AUDIT_AVAILABLE = True
GBP_AUDIT_VERSION = '1.0'
except ImportError:
GBP_AUDIT_AVAILABLE = False
GBP_AUDIT_VERSION = None
gbp_get_company_audit = None
# ============================================================
# SEO AUDIT USER-FACING DASHBOARD
# ============================================================
@bp.route('/seo/<slug>')
@login_required
def seo_audit_dashboard(slug):
"""
User-facing SEO audit dashboard for a specific company.
Displays SEO audit results with:
- PageSpeed Insights scores (SEO, Performance, Accessibility, Best Practices)
- Website analysis data
- Improvement recommendations
Access control:
- Admin users can view audit for any company
- Regular users can only view audit for their own company
Args:
slug: Company slug identifier
Returns:
Rendered seo_audit.html template with company and audit data
"""
if not is_audit_owner():
abort(404)
db = SessionLocal()
try:
# Find company by slug
company = db.query(Company).filter_by(slug=slug, status='active').first()
if not company:
flash('Firma nie została znaleziona.', 'error')
return redirect(url_for('dashboard'))
# Access control: users with company edit rights can view
if not current_user.can_edit_company(company.id):
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
return redirect(url_for('dashboard'))
# Get latest SEO analysis for this company
analysis = db.query(CompanyWebsiteAnalysis).filter(
CompanyWebsiteAnalysis.company_id == company.id
).order_by(CompanyWebsiteAnalysis.seo_audited_at.desc()).first()
# Get citations for this company
citations = db.query(CompanyCitation).filter(
CompanyCitation.company_id == company.id
).order_by(CompanyCitation.directory_name).all() if analysis else []
# Build SEO data dict if analysis exists
seo_data = None
if analysis and analysis.seo_audited_at:
seo_data = {
'seo_score': analysis.pagespeed_seo_score,
'performance_score': analysis.pagespeed_performance_score,
'accessibility_score': analysis.pagespeed_accessibility_score,
'best_practices_score': analysis.pagespeed_best_practices_score,
'audited_at': analysis.seo_audited_at,
'audit_version': analysis.seo_audit_version,
'url': analysis.website_url,
# Local SEO fields
'local_seo_score': analysis.local_seo_score,
'has_local_business_schema': analysis.has_local_business_schema,
'local_business_schema_fields': analysis.local_business_schema_fields,
'nap_on_website': analysis.nap_on_website,
'has_google_maps_embed': analysis.has_google_maps_embed,
'has_local_keywords': analysis.has_local_keywords,
'local_keywords_found': analysis.local_keywords_found,
'citations_count': analysis.citations_count,
'content_freshness_score': analysis.content_freshness_score,
'last_modified_date': analysis.last_modified_at,
# Core Web Vitals
'lcp_ms': analysis.largest_contentful_paint_ms,
'inp_ms': analysis.interaction_to_next_paint_ms,
'cls': float(analysis.cumulative_layout_shift) if analysis.cumulative_layout_shift is not None else None,
# Heading structure
'h1_count': analysis.h1_count,
'h1_text': analysis.h1_text,
'h2_count': analysis.h2_count,
'h3_count': analysis.h3_count,
# Image SEO
'total_images': analysis.total_images,
'images_without_alt': analysis.images_without_alt,
# Links
'internal_links_count': analysis.internal_links_count,
'external_links_count': analysis.external_links_count,
'broken_links_count': analysis.broken_links_count,
# SSL
'has_ssl': analysis.has_ssl,
'ssl_expires_at': analysis.ssl_expires_at,
# Analytics
'has_google_analytics': analysis.has_google_analytics,
'has_google_tag_manager': analysis.has_google_tag_manager,
# Social sharing
'has_og_tags': analysis.has_og_tags,
'has_twitter_cards': analysis.has_twitter_cards,
# Technical SEO details
'has_sitemap': analysis.has_sitemap,
'has_robots_txt': analysis.has_robots_txt,
'has_canonical': analysis.has_canonical,
'canonical_url': analysis.canonical_url,
'is_indexable': analysis.is_indexable,
'noindex_reason': analysis.noindex_reason,
'is_mobile_friendly': analysis.is_mobile_friendly,
'viewport_configured': analysis.viewport_configured,
'html_lang': analysis.html_lang,
'has_hreflang': analysis.has_hreflang,
# Meta tags
'meta_title': analysis.meta_title,
'meta_description': analysis.meta_description,
# Structured data
'has_structured_data': analysis.has_structured_data,
'structured_data_types': analysis.structured_data_types,
# Performance
'load_time_ms': analysis.load_time_ms,
'word_count_homepage': analysis.word_count_homepage,
# CrUX field data (real user metrics)
'crux_lcp_ms': analysis.crux_lcp_ms,
'crux_inp_ms': analysis.crux_inp_ms,
'crux_cls': float(analysis.crux_cls) if analysis.crux_cls is not None else None,
'crux_fcp_ms': analysis.crux_fcp_ms,
'crux_ttfb_ms': analysis.crux_ttfb_ms,
'crux_lcp_good_pct': float(analysis.crux_lcp_good_pct) if analysis.crux_lcp_good_pct is not None else None,
'crux_inp_good_pct': float(analysis.crux_inp_good_pct) if analysis.crux_inp_good_pct is not None else None,
# Security headers
'has_hsts': analysis.has_hsts,
'has_csp': analysis.has_csp,
'has_x_frame_options': analysis.has_x_frame_options,
'has_x_content_type_options': analysis.has_x_content_type_options,
'security_headers_count': analysis.security_headers_count,
# Image formats
'modern_image_count': analysis.modern_image_count,
'legacy_image_count': analysis.legacy_image_count,
'modern_image_ratio': float(analysis.modern_image_ratio) if analysis.modern_image_ratio is not None else None,
# Google Search Console (OAuth)
'gsc_clicks': analysis.gsc_clicks,
'gsc_impressions': analysis.gsc_impressions,
'gsc_ctr': float(analysis.gsc_ctr) if analysis.gsc_ctr is not None else None,
'gsc_avg_position': float(analysis.gsc_avg_position) if analysis.gsc_avg_position is not None else None,
'gsc_top_queries': analysis.gsc_top_queries,
'gsc_period_days': analysis.gsc_period_days,
# GSC Extended data
'gsc_top_pages': getattr(analysis, 'gsc_top_pages', None),
'gsc_device_breakdown': getattr(analysis, 'gsc_device_breakdown', None),
'gsc_index_status': getattr(analysis, 'gsc_index_status', None),
'gsc_last_crawl': getattr(analysis, 'gsc_last_crawl', None),
'gsc_crawled_as': getattr(analysis, 'gsc_crawled_as', None),
'gsc_sitemaps': getattr(analysis, 'gsc_sitemaps', None),
'gsc_country_breakdown': getattr(analysis, 'gsc_country_breakdown', None),
'gsc_search_type_breakdown': getattr(analysis, 'gsc_search_type_breakdown', None),
'gsc_trend_data': getattr(analysis, 'gsc_trend_data', None),
# Citations list
'citations': [{'directory_name': c.directory_name, 'listing_url': c.listing_url, 'status': c.status, 'nap_accurate': c.nap_accurate} for c in citations],
}
# Check if company has GSC OAuth token
has_gsc_token = db.query(OAuthToken).filter(
OAuthToken.company_id == company.id,
OAuthToken.provider == 'google',
OAuthToken.service == 'search_console'
).first() is not None
# Determine if user can run audit (user with company edit rights)
can_audit = current_user.can_edit_company(company.id)
logger.info(f"SEO audit dashboard viewed by {current_user.email} for company: {company.name}")
return render_template('seo_audit.html',
company=company,
seo_data=seo_data,
can_audit=can_audit,
has_gsc_token=has_gsc_token
)
finally:
db.close()
# ============================================================
# SOCIAL MEDIA AUDIT USER-FACING DASHBOARD
# ============================================================
@bp.route('/social/<slug>')
@login_required
def social_audit_dashboard(slug):
"""
User-facing Social Media audit dashboard for a specific company.
Displays social media presence audit with:
- Overall presence score (platforms found / total platforms)
- Platform-by-platform status
- Profile validation status
- Recommendations for missing platforms
Access control:
- Admins: Can view all companies
- Regular users: Can only view their own company
Args:
slug: Company URL slug
Returns:
Rendered social_audit.html template with company and social data
"""
if not is_audit_owner():
abort(404)
db = SessionLocal()
try:
# Find company by slug
company = db.query(Company).filter_by(slug=slug, status='active').first()
if not company:
flash('Firma nie została znaleziona.', 'error')
return redirect(url_for('dashboard'))
# Access control - users with company edit rights can view
if not current_user.can_edit_company(company.id):
flash('Brak uprawnień do wyświetlenia audytu social media tej firmy.', 'error')
return redirect(url_for('dashboard'))
# Get social media profiles for this company
social_profiles = db.query(CompanySocialMedia).filter(
CompanySocialMedia.company_id == company.id
).all()
# Define all platforms we track
all_platforms = ['facebook', 'instagram', 'linkedin', 'youtube', 'twitter', 'tiktok']
# Build social media data
profiles_dict = {}
for profile in social_profiles:
profiles_dict[profile.platform] = {
'url': profile.url,
'is_valid': profile.is_valid,
'check_status': profile.check_status,
'page_name': profile.page_name,
'followers_count': profile.followers_count,
'verified_at': profile.verified_at,
'last_checked_at': profile.last_checked_at,
# Enhanced audit fields
'has_profile_photo': profile.has_profile_photo,
'has_cover_photo': profile.has_cover_photo,
'has_bio': profile.has_bio,
'profile_description': profile.profile_description,
'posts_count_30d': profile.posts_count_30d,
'posts_count_365d': profile.posts_count_365d,
'last_post_date': profile.last_post_date,
'posting_frequency_score': profile.posting_frequency_score,
'engagement_rate': profile.engagement_rate,
'content_types': profile.content_types,
'profile_completeness_score': profile.profile_completeness_score,
'followers_history': profile.followers_history,
'source': profile.source,
}
# Calculate score (platforms with profiles / total platforms)
platforms_with_profiles = len([p for p in all_platforms if p in profiles_dict])
total_platforms = len(all_platforms)
score = int((platforms_with_profiles / total_platforms) * 100) if total_platforms > 0 else 0
social_data = {
'profiles': profiles_dict,
'all_platforms': all_platforms,
'platforms_count': platforms_with_profiles,
'total_platforms': total_platforms,
'score': score
}
# Determine if user can run audit (user with company edit rights)
can_audit = current_user.can_edit_company(company.id)
logger.info(f"Social Media audit dashboard viewed by {current_user.email} for company: {company.name}")
return render_template('social_audit.html',
company=company,
social_data=social_data,
can_audit=can_audit
)
finally:
db.close()
# ============================================================
# GBP AUDIT USER-FACING DASHBOARD
# ============================================================
@bp.route('/gbp/<slug>')
@login_required
def gbp_audit_dashboard(slug):
"""
User-facing GBP audit dashboard for a specific company.
Displays Google Business Profile completeness audit results with:
- Overall completeness score (0-100)
- Field-by-field status breakdown
- AI-generated improvement recommendations
- Historical audit data
Access control:
- Admin users can view audit for any company
- Regular users can only view audit for their own company
Args:
slug: Company slug identifier
Returns:
Rendered gbp_audit.html template with company and audit data
"""
if not is_audit_owner():
abort(404)
if not GBP_AUDIT_AVAILABLE:
flash('Usługa audytu Google Business Profile jest tymczasowo niedostępna.', 'error')
return redirect(url_for('dashboard'))
db = SessionLocal()
try:
# Find company by slug
company = db.query(Company).filter_by(slug=slug, status='active').first()
if not company:
flash('Firma nie została znaleziona.', 'error')
return redirect(url_for('dashboard'))
# Access control: users with company edit rights can view
if not current_user.can_edit_company(company.id):
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
return redirect(url_for('dashboard'))
# Get latest audit for this company
audit = gbp_get_company_audit(db, company.id)
# Get recent reviews for this company
recent_reviews = db.query(GBPReview).filter(
GBPReview.company_id == company.id
).order_by(GBPReview.publish_time.desc()).limit(5).all() if audit else []
# Get Places API enrichment data from CompanyWebsiteAnalysis
places_data = {}
analysis = db.query(CompanyWebsiteAnalysis).filter(
CompanyWebsiteAnalysis.company_id == company.id
).order_by(CompanyWebsiteAnalysis.analyzed_at.desc()).first()
# Polish translations for common Google Places types
_type_pl = {
'accounting': 'Biuro rachunkowe', 'airport': 'Lotnisko', 'atm': 'Bankomat',
'bakery': 'Piekarnia', 'bank': 'Bank', 'bar': 'Bar', 'beauty_salon': 'Salon kosmetyczny',
'book_store': 'Księgarnia', 'cafe': 'Kawiarnia', 'car_dealer': 'Dealer samochodowy',
'car_repair': 'Warsztat samochodowy', 'car_wash': 'Myjnia samochodowa',
'clothing_store': 'Sklep odzieżowy', 'construction_company': 'Firma budowlana',
'convenience_store': 'Sklep spożywczy', 'dentist': 'Dentysta', 'doctor': 'Lekarz',
'electrician': 'Elektryk', 'electronics_store': 'Sklep elektroniczny',
'establishment': 'Firma', 'finance': 'Finanse', 'florist': 'Kwiaciarnia',
'food': 'Gastronomia', 'furniture_store': 'Sklep meblowy', 'gas_station': 'Stacja benzynowa',
'general_contractor': 'Generalny wykonawca', 'grocery_or_supermarket': 'Sklep/supermarket',
'gym': 'Siłownia', 'hair_care': 'Fryzjer', 'hardware_store': 'Sklep budowlany',
'health': 'Zdrowie', 'home_goods_store': 'Sklep z wyposażeniem domu',
'insurance_agency': 'Agencja ubezpieczeniowa', 'lawyer': 'Prawnik',
'locksmith': 'Ślusarz', 'lodging': 'Nocleg', 'meal_delivery': 'Dostawa jedzenia',
'meal_takeaway': 'Jedzenie na wynos', 'moving_company': 'Firma przeprowadzkowa',
'painter': 'Malarz', 'parking': 'Parking', 'pet_store': 'Sklep zoologiczny',
'pharmacy': 'Apteka', 'physiotherapist': 'Fizjoterapeuta', 'plumber': 'Hydraulik',
'point_of_interest': 'Punkt zainteresowania', 'real_estate_agency': 'Agencja nieruchomości',
'restaurant': 'Restauracja', 'roofing_contractor': 'Dekarz',
'school': 'Szkoła', 'shoe_store': 'Sklep obuwniczy', 'shopping_mall': 'Centrum handlowe',
'spa': 'Spa', 'store': 'Sklep', 'supermarket': 'Supermarket',
'travel_agency': 'Biuro podróży', 'veterinary_care': 'Weterynarz',
'computer_store': 'Sklep komputerowy', 'it_services': 'Usługi IT',
'corporate_office': 'Biuro', 'consultant': 'Konsultant',
'marketing_agency': 'Agencja marketingowa', 'photographer': 'Fotograf',
'printing_service': 'Drukarnia', 'sign_shop': 'Reklama wizualna',
'auto_body_shop': 'Blacharnia', 'auto_parts_store': 'Sklep z częściami',
}
if analysis:
_ptype = getattr(analysis, 'google_primary_type', None) or ''
places_data = {
'primary_type': _ptype,
'primary_type_display': _type_pl.get(_ptype, _ptype.replace('_', ' ').title()) if _ptype else None,
'editorial_summary': getattr(analysis, 'google_editorial_summary', None),
'price_level': getattr(analysis, 'google_price_level', None),
'maps_links': getattr(analysis, 'google_maps_links', None),
'open_now': getattr(analysis, 'google_open_now', None),
'google_name': analysis.google_name,
'google_address': analysis.google_address,
'google_phone': analysis.google_phone,
# GBP Performance API data
'gbp_impressions_maps': getattr(analysis, 'gbp_impressions_maps', None),
'gbp_impressions_search': getattr(analysis, 'gbp_impressions_search', None),
'gbp_call_clicks': getattr(analysis, 'gbp_call_clicks', None),
'gbp_website_clicks': getattr(analysis, 'gbp_website_clicks', None),
'gbp_direction_requests': getattr(analysis, 'gbp_direction_requests', None),
'gbp_conversations': getattr(analysis, 'gbp_conversations', None),
'gbp_search_keywords': getattr(analysis, 'gbp_search_keywords', None),
'gbp_performance_period_days': getattr(analysis, 'gbp_performance_period_days', None),
# Owner data
'google_owner_responses_count': getattr(analysis, 'google_owner_responses_count', None),
'google_review_response_rate': float(analysis.google_review_response_rate) if getattr(analysis, 'google_review_response_rate', None) is not None else None,
'google_posts_data': getattr(analysis, 'google_posts_data', None),
'google_posts_count': getattr(analysis, 'google_posts_count', None),
# Additional data not previously exposed
'google_business_status': analysis.google_business_status,
'google_opening_hours': analysis.google_opening_hours,
'google_photos_count': analysis.google_photos_count,
'google_website': analysis.google_website,
'google_types': analysis.google_types,
'google_maps_url': analysis.google_maps_url,
'google_rating': float(analysis.google_rating) if analysis.google_rating is not None else None,
'google_reviews_count': analysis.google_reviews_count,
'google_attributes': getattr(analysis, 'google_attributes', None),
'google_reviews_data': getattr(analysis, 'google_reviews_data', None),
'google_photos_metadata': getattr(analysis, 'google_photos_metadata', None),
'google_place_id': analysis.google_place_id,
'has_google_analytics': getattr(analysis, 'has_google_analytics', None),
'has_google_tag_manager': getattr(analysis, 'has_google_tag_manager', None),
'has_google_maps_embed': getattr(analysis, 'has_google_maps_embed', None),
}
# Build GBP recommendations
gbp_recommendations = []
if analysis.google_name:
if not analysis.google_opening_hours:
gbp_recommendations.append({'severity': 'warning', 'text': 'Brak godzin otwarcia w Google. Klienci nie wiedza kiedy firma jest czynna.'})
if not analysis.google_phone:
gbp_recommendations.append({'severity': 'critical', 'text': 'Brak numeru telefonu w Google. To podstawowy sposob kontaktu klientow.'})
if (analysis.google_photos_count or 0) < 5:
gbp_recommendations.append({'severity': 'warning', 'text': 'Mniej niz 5 zdjec w profilu Google. Dodaj zdjecia — firmy ze zdjeciami otrzymuja 42%% wiecej zapytan o dojazd.'})
if (analysis.google_reviews_count or 0) < 5:
gbp_recommendations.append({'severity': 'warning', 'text': 'Mniej niz 5 opinii Google. Zachecaj klientow do wystawienia recenzji.'})
rr = float(analysis.google_review_response_rate) if analysis.google_review_response_rate is not None else None
if rr is not None and rr < 50:
gbp_recommendations.append({'severity': 'warning', 'text': 'Odpowiedzi na mniej niz 50%% opinii. Odpowiadanie na recenzje poprawia widocznosc.'})
if not analysis.google_website:
gbp_recommendations.append({'severity': 'info', 'text': 'Brak strony WWW w profilu Google. Dodaj link do strony firmy.'})
if analysis.google_business_status and analysis.google_business_status != 'OPERATIONAL':
gbp_recommendations.append({'severity': 'critical', 'text': 'Profil Google nie ma statusu OPERATIONAL. Sprawdz czy firma jest oznaczona jako czynna.'})
if not gbp_recommendations:
gbp_recommendations.append({'severity': 'success', 'text': 'Profil Google wyglada dobrze! Nie znaleziono powaznych problemow.'})
# GBP benchmarks — average across all members
gbp_benchmarks = {}
try:
from sqlalchemy import func as sqlfunc
bench = db.query(
sqlfunc.avg(CompanyWebsiteAnalysis.google_rating),
sqlfunc.avg(CompanyWebsiteAnalysis.google_reviews_count),
sqlfunc.avg(CompanyWebsiteAnalysis.google_photos_count),
sqlfunc.count(CompanyWebsiteAnalysis.id),
).join(Company, Company.id == CompanyWebsiteAnalysis.company_id
).filter(
Company.status == 'active',
CompanyWebsiteAnalysis.google_rating.isnot(None),
).first()
if bench and bench[3] > 1:
gbp_benchmarks = {
'count': bench[3],
'avg_rating': round(float(bench[0] or 0), 1),
'avg_reviews': round(float(bench[1] or 0)),
'avg_photos': round(float(bench[2] or 0)),
}
except Exception:
pass
# If no audit exists, we still render the page (template handles this)
# The user can trigger an audit from the dashboard
# Determine if user can run audit (user with company edit rights)
can_audit = current_user.can_edit_company(company.id)
# Check GBP OAuth connection status
gbp_oauth_token = db.query(OAuthToken).filter(
OAuthToken.company_id == company.id,
OAuthToken.provider == 'google',
OAuthToken.service == 'gbp'
).first()
gbp_connection = None
if gbp_oauth_token:
from datetime import datetime
is_expired = gbp_oauth_token.expires_at and gbp_oauth_token.expires_at < datetime.utcnow()
has_location = bool(gbp_oauth_token.metadata_json and isinstance(gbp_oauth_token.metadata_json, dict) and gbp_oauth_token.metadata_json.get('location_name'))
gbp_connection = {
'connected': True,
'is_active': gbp_oauth_token.is_active,
'is_expired': is_expired,
'has_location': has_location,
'created_at': gbp_oauth_token.created_at,
'expires_at': gbp_oauth_token.expires_at,
'has_refresh_token': bool(gbp_oauth_token.refresh_token),
'google_email': gbp_oauth_token.account_id,
'google_name': gbp_oauth_token.account_name,
}
logger.info(f"GBP audit dashboard viewed by {current_user.email} for company: {company.name}")
return render_template('gbp_audit.html',
company=company,
audit=audit,
recent_reviews=recent_reviews,
places_data=places_data,
can_audit=can_audit,
gbp_audit_available=GBP_AUDIT_AVAILABLE,
gbp_audit_version=GBP_AUDIT_VERSION,
gbp_recommendations=gbp_recommendations if analysis else [],
gbp_benchmarks=gbp_benchmarks if analysis else {},
gbp_connection=gbp_connection
)
finally:
db.close()
# ============================================================
# IT AUDIT USER-FACING DASHBOARD
# ============================================================
@bp.route('/it/<slug>')
@login_required
def it_audit_dashboard(slug):
"""
User-facing IT infrastructure audit dashboard for a specific company.
Displays IT audit results with:
- Overall score and maturity level
- Security, collaboration, and completeness sub-scores
- Technology stack summary (Azure AD, M365, backup, monitoring)
- AI-generated recommendations
Access control:
- Admin users can view audit for any company
- Regular users can only view audit for their own company
Args:
slug: Company slug identifier
Returns:
Rendered it_audit.html template with company and audit data
"""
if not is_audit_owner():
abort(404)
db = SessionLocal()
try:
# Find company by slug
company = db.query(Company).filter_by(slug=slug, status='active').first()
if not company:
flash('Firma nie została znaleziona.', 'error')
return redirect(url_for('dashboard'))
# Access control: users with company edit rights can view
if not current_user.can_edit_company(company.id):
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
return redirect(url_for('dashboard'))
# Get latest IT audit for this company
audit = db.query(ITAudit).filter(
ITAudit.company_id == company.id
).order_by(ITAudit.audit_date.desc()).first()
# Build audit data dict if audit exists
audit_data = None
if audit:
# Get maturity label
maturity_labels = {
'basic': 'Podstawowy',
'developing': 'Rozwijający się',
'established': 'Ugruntowany',
'advanced': 'Zaawansowany'
}
audit_data = {
'id': audit.id,
'overall_score': audit.overall_score,
'security_score': audit.security_score,
'collaboration_score': audit.collaboration_score,
'completeness_score': audit.completeness_score,
'maturity_level': audit.maturity_level,
'maturity_label': maturity_labels.get(audit.maturity_level, 'Nieznany'),
'audit_date': audit.audit_date,
'audit_source': audit.audit_source,
# Technology flags
'has_azure_ad': audit.has_azure_ad,
'has_m365': audit.has_m365,
'has_google_workspace': audit.has_google_workspace,
'has_local_ad': audit.has_local_ad,
'has_edr': audit.has_edr,
'has_mfa': audit.has_mfa,
'has_vpn': audit.has_vpn,
'has_proxmox_pbs': audit.has_proxmox_pbs,
'has_dr_plan': audit.has_dr_plan,
'has_mdm': audit.has_mdm,
# Solutions
'antivirus_solution': audit.antivirus_solution,
'backup_solution': audit.backup_solution,
'monitoring_solution': audit.monitoring_solution,
'virtualization_platform': audit.virtualization_platform,
# Collaboration flags
'open_to_shared_licensing': audit.open_to_shared_licensing,
'open_to_backup_replication': audit.open_to_backup_replication,
'open_to_teams_federation': audit.open_to_teams_federation,
'open_to_shared_monitoring': audit.open_to_shared_monitoring,
'open_to_collective_purchasing': audit.open_to_collective_purchasing,
'open_to_knowledge_sharing': audit.open_to_knowledge_sharing,
# Recommendations
'recommendations': audit.recommendations
}
# Determine if user can edit audit (user with company edit rights)
can_edit = current_user.can_edit_company(company.id)
logger.info(f"IT audit dashboard viewed by {current_user.email} for company: {company.name}")
return render_template('it_audit.html',
company=company,
audit_data=audit_data,
can_edit=can_edit
)
finally:
db.close()