diff --git a/app.py b/app.py index 4c0e494..ae2b320 100644 --- a/app.py +++ b/app.py @@ -107,7 +107,9 @@ from database import ( MembershipFee, MembershipFeeConfig, Person, - CompanyPerson + CompanyPerson, + GBPAudit, + ITAudit ) # Import services @@ -632,6 +634,16 @@ def company_detail(company_id): Person.nazwisko ).all() + # Load GBP audit (most recent) + gbp_audit = db.query(GBPAudit).filter_by( + company_id=company_id + ).order_by(GBPAudit.audit_date.desc()).first() + + # Load IT audit (most recent) + it_audit = db.query(ITAudit).filter_by( + company_id=company_id + ).order_by(ITAudit.audit_date.desc()).first() + return render_template('company_detail.html', company=company, maturity_data=maturity_data, @@ -643,7 +655,9 @@ def company_detail(company_id): social_media=social_media, contacts=contacts, recommendations=recommendations, - people=people + people=people, + gbp_audit=gbp_audit, + it_audit=it_audit ) finally: db.close() @@ -8109,7 +8123,8 @@ def admin_zopk_news_approve(news_id): except Exception as e: db.rollback() - return jsonify({'success': False, 'error': str(e)}), 500 + logger.error(f"Error approving ZOPK news {news_id}: {e}") + return jsonify({'success': False, 'error': 'Wystąpił błąd podczas zatwierdzania'}), 500 finally: db.close() @@ -8143,7 +8158,8 @@ def admin_zopk_news_reject(news_id): except Exception as e: db.rollback() - return jsonify({'success': False, 'error': str(e)}), 500 + logger.error(f"Error rejecting ZOPK news {news_id}: {e}") + return jsonify({'success': False, 'error': 'Wystąpił błąd podczas odrzucania'}), 500 finally: db.close() @@ -8274,7 +8290,8 @@ def admin_zopk_reject_old_news(): except Exception as e: db.rollback() - return jsonify({'success': False, 'error': str(e)}), 500 + logger.error(f"Error rejecting old ZOPK news: {e}") + return jsonify({'success': False, 'error': 'Wystąpił błąd podczas odrzucania starych newsów'}), 500 finally: db.close() @@ -8308,7 +8325,8 @@ def admin_zopk_evaluate_ai(): except Exception as e: db.rollback() - return jsonify({'success': False, 'error': str(e)}), 500 + logger.error(f"Error evaluating ZOPK news with AI: {e}") + return jsonify({'success': False, 'error': 'Wystąpił błąd podczas oceny AI'}), 500 finally: db.close() @@ -8342,7 +8360,8 @@ def admin_zopk_reevaluate_scores(): except Exception as e: db.rollback() - return jsonify({'success': False, 'error': str(e)}), 500 + logger.error(f"Error reevaluating ZOPK news scores: {e}") + return jsonify({'success': False, 'error': 'Wystąpił błąd podczas ponownej oceny'}), 500 finally: db.close() @@ -8425,13 +8444,13 @@ def api_zopk_search_news(): # Update job status on error try: fetch_job.status = 'failed' - fetch_job.error_message = str(e) + fetch_job.error_message = str(e) # Keep internal log fetch_job.completed_at = datetime.now() db.commit() except: pass - return jsonify({'success': False, 'error': str(e)}), 500 + return jsonify({'success': False, 'error': 'Wystąpił błąd podczas wyszukiwania newsów'}), 500 finally: db.close() diff --git a/database.py b/database.py index 77dcc17..548bb9f 100644 --- a/database.py +++ b/database.py @@ -45,6 +45,57 @@ DATABASE_URL = os.getenv( IS_SQLITE = DATABASE_URL.startswith('sqlite') +def normalize_social_url(url: str, platform: str = None) -> str: + """ + Normalize social media URLs to prevent duplicates. + + Handles: + - www vs non-www (removes www.) + - http vs https (forces https) + - Trailing slashes (removes) + - Platform-specific canonicalization + + Examples: + normalize_social_url('http://www.facebook.com/inpipl/') + -> 'https://facebook.com/inpipl' + + normalize_social_url('https://www.instagram.com/user/') + -> 'https://instagram.com/user' + """ + if not url: + return url + + url = url.strip() + + # Force https + if url.startswith('http://'): + url = 'https://' + url[7:] + elif not url.startswith('https://'): + url = 'https://' + url + + # Remove www. prefix + url = url.replace('https://www.', 'https://') + + # Remove trailing slash + url = url.rstrip('/') + + # Platform-specific normalization + if platform == 'facebook' or 'facebook.com' in url: + # fb.com -> facebook.com + url = url.replace('https://fb.com/', 'https://facebook.com/') + url = url.replace('https://m.facebook.com/', 'https://facebook.com/') + + if platform == 'twitter' or 'twitter.com' in url or 'x.com' in url: + # x.com -> twitter.com (or vice versa, pick one canonical) + url = url.replace('https://x.com/', 'https://twitter.com/') + + if platform == 'linkedin' or 'linkedin.com' in url: + # Remove locale prefix + url = url.replace('/pl/', '/').replace('/en/', '/') + + return url + + class StringArray(TypeDecorator): """ Platform-agnostic array type. diff --git a/scripts/social_media_audit.py b/scripts/social_media_audit.py index 6ba9a0f..f67a138 100644 --- a/scripts/social_media_audit.py +++ b/scripts/social_media_audit.py @@ -54,6 +54,25 @@ import whois from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +try: + from database import normalize_social_url +except ImportError: + # Fallback: define locally if import fails + def normalize_social_url(url: str, platform: str = None) -> str: + """Normalize social media URLs to prevent duplicates.""" + if not url: + return url + url = url.strip() + if url.startswith('http://'): + url = 'https://' + url[7:] + elif not url.startswith('https://'): + url = 'https://' + url + url = url.replace('https://www.', 'https://') + url = url.rstrip('/') + return url + # Configure logging logging.basicConfig( level=logging.INFO, @@ -1097,6 +1116,9 @@ class SocialMediaAuditor: # Save social media for platform, url in result.get('social_media', {}).items(): + # Normalize URL to prevent www vs non-www duplicates + normalized_url = normalize_social_url(url, platform) + upsert_social = text(""" INSERT INTO company_social_media ( company_id, platform, url, verified_at, source, is_valid @@ -1112,7 +1134,7 @@ class SocialMediaAuditor: session.execute(upsert_social, { 'company_id': company_id, 'platform': platform, - 'url': url, + 'url': normalized_url, 'verified_at': result['audit_date'], 'source': 'website_scrape', 'is_valid': True, diff --git a/templates/company_detail.html b/templates/company_detail.html index 137daed..0f62caf 100755 --- a/templates/company_detail.html +++ b/templates/company_detail.html @@ -2261,6 +2261,441 @@ {% endif %} + +{% if gbp_audit and gbp_audit.completeness_score is not none %} +
+

+ Audyt Google Business Profile + + ({{ gbp_audit.audit_date.strftime('%d.%m.%Y') }}) + +

+ + +
+
+ {{ gbp_audit.completeness_score }} +
+
+
+ {% if gbp_audit.completeness_score >= 90 %}Doskonały profil GBP{% elif gbp_audit.completeness_score >= 70 %}Dobry profil GBP{% elif gbp_audit.completeness_score >= 50 %}Przeciętny profil GBP{% else %}Profil GBP wymaga uzupełnienia{% endif %} +
+
+ {% if gbp_audit.completeness_score >= 90 %}Profil Google Business jest kompletny i dobrze zoptymalizowany{% elif gbp_audit.completeness_score >= 70 %}Profil jest dobrze uzupełniony, ale można go jeszcze ulepszyć{% elif gbp_audit.completeness_score >= 50 %}Profil wymaga uzupełnienia kilku ważnych informacji{% else %}Profil wymaga uzupełnienia podstawowych danych{% endif %} +
+
+
+ + +
+ +
+
+ + + +
+
+ {% if gbp_audit.review_count is not none %}{{ gbp_audit.review_count }}{% else %}-{% endif %} +
+
Opinii
+
+ + +
+
+ + + +
+
+ {% if gbp_audit.average_rating is not none %}{{ gbp_audit.average_rating|round(1) }}{% else %}-{% endif %} +
+
Średnia ocen
+
+ + +
+
+ + + +
+
+ {% if gbp_audit.photo_count is not none %}{{ gbp_audit.photo_count }}{% else %}-{% endif %} +
+
Zdjęć
+
+ + +
+
+ + + +
+
+ {{ gbp_audit.completeness_score }}% +
+
Kompletność
+
+
+ + +
+ +
+

+ + + + Informacje podstawowe +

+
+
+ Nazwa firmy + {% if gbp_audit.has_name %} + OK + {% else %} + Brak + {% endif %} +
+
+ Adres + {% if gbp_audit.has_address %} + OK + {% else %} + Brak + {% endif %} +
+
+ Telefon + {% if gbp_audit.has_phone %} + OK + {% else %} + Brak + {% endif %} +
+
+ Strona www + {% if gbp_audit.has_website %} + OK + {% else %} + Brak + {% endif %} +
+
+ Godziny otwarcia + {% if gbp_audit.has_hours %} + OK + {% else %} + Brak + {% endif %} +
+
+
+ + +
+

+ + + + Treści i zaangażowanie +

+
+
+ Opis firmy + {% if gbp_audit.has_description %} + OK + {% else %} + Brak + {% endif %} +
+
+ Kategorie + {% if gbp_audit.has_categories %} + OK + {% else %} + Brak + {% endif %} +
+
+ Usługi/produkty + {% if gbp_audit.has_services %} + OK + {% else %} + Brak + {% endif %} +
+
+ Zdjęcia + {% if gbp_audit.has_photos %} + {{ gbp_audit.photo_count or 'OK' }} + {% else %} + Brak + {% endif %} +
+
+ Opinie klientów + {% if gbp_audit.has_reviews %} + {{ gbp_audit.review_count }} + {% else %} + Brak + {% endif %} +
+
+
+
+ + {% if gbp_audit.google_maps_url %} +
+ + + + + Zobacz w Google Maps + +
+ {% endif %} +
+{% endif %} + + +{% if social_media and social_media|length > 0 %} +
+

+ Audyt Social Media +

+ + + {% set active_platforms = social_media|selectattr('is_valid', 'equalto', true)|list|length %} + {% set total_followers = social_media|selectattr('followers_count')|sum(attribute='followers_count') %} +
+
+ {{ active_platforms }} +
+
+
+ {% if active_platforms >= 4 %}Doskonała obecność w Social Media{% elif active_platforms >= 2 %}Dobra obecność w Social Media{% else %}Podstawowa obecność w Social Media{% endif %} +
+
+ {{ active_platforms }} aktywn{{ 'a platforma' if active_platforms == 1 else ('e platformy' if active_platforms in [2, 3, 4] else 'ych platform') }} + {% if total_followers > 0 %} · {{ "{:,}".format(total_followers|int).replace(",", " ") }} obserwujących łącznie{% endif %} +
+
+
+ + +
+ {% set platform_icons = { + 'facebook': {'icon': '', 'color': '#1877F2'}, + 'instagram': {'icon': '', 'color': '#E4405F'}, + 'linkedin': {'icon': '', 'color': '#0A66C2'}, + 'youtube': {'icon': '', 'color': '#FF0000'}, + 'twitter': {'icon': '', 'color': '#000000'}, + 'tiktok': {'icon': '', 'color': '#000000'} + } %} + + {% for sm in social_media %} + +
+ + {{ platform_icons[sm.platform]['icon']|safe if sm.platform in platform_icons else '' }} + +
+
{{ sm.platform }}
+ {% if sm.followers_count %} +
{{ "{:,}".format(sm.followers_count|int).replace(",", " ") }} obserwujących
+ {% elif sm.page_name %} +
{{ sm.page_name|truncate(20) }}
+ {% else %} +
Aktywny profil
+ {% endif %} + {% if sm.is_valid %} + Zweryfikowany + {% endif %} +
+ {% endfor %} +
+
+{% endif %} + + +{% if it_audit and it_audit.overall_score is not none %} +
+

+ Audyt IT + + ({{ it_audit.audit_date.strftime('%d.%m.%Y') }}) + +

+ + +
+
+ {{ it_audit.overall_score }} +
+
+
+ {% if it_audit.maturity_level == 'advanced' %}Zaawansowana infrastruktura IT{% elif it_audit.maturity_level == 'established' %}Rozwinięta infrastruktura IT{% elif it_audit.maturity_level == 'developing' %}Rozwijająca się infrastruktura IT{% else %}Podstawowa infrastruktura IT{% endif %} +
+
+ {% if it_audit.overall_score >= 90 %}Firma posiada nowoczesną i dobrze zabezpieczoną infrastrukturę{% elif it_audit.overall_score >= 70 %}Infrastruktura IT na dobrym poziomie z potencjałem do rozwoju{% elif it_audit.overall_score >= 50 %}Infrastruktura IT wymaga modernizacji w niektórych obszarach{% else %}Infrastruktura IT wymaga znaczącej rozbudowy{% endif %} +
+
+
+ + +
+ +
+
+ + + +
+
+ {{ it_audit.overall_score }} +
+
Ogólny wynik
+
+ + +
+
+ + + +
+
+ {% if it_audit.security_score is not none %}{{ it_audit.security_score }}{% else %}-{% endif %} +
+
Bezpieczeństwo
+
+ + +
+
+ + + +
+
+ {% if it_audit.collaboration_score is not none %}{{ it_audit.collaboration_score }}{% else %}-{% endif %} +
+
Współpraca
+
+ + +
+
+ + + +
+
+ {% if it_audit.maturity_level == 'advanced' %}Zaawansowany{% elif it_audit.maturity_level == 'established' %}Rozwinięty{% elif it_audit.maturity_level == 'developing' %}Rozwijający{% else %}Podstawowy{% endif %} +
+
Poziom dojrzałości
+
+
+ + +
+ +
+

+ + + + Chmura i tożsamość +

+
+
+ Microsoft 365 + {% if it_audit.has_m365 %} + Tak + {% else %} + Nie + {% endif %} +
+
+ Azure AD / Entra ID + {% if it_audit.has_azure_ad %} + Tak + {% else %} + Nie + {% endif %} +
+
+ Google Workspace + {% if it_audit.has_google_workspace %} + Tak + {% else %} + Nie + {% endif %} +
+
+
+ + +
+

+ + + + Bezpieczeństwo +

+
+
+ MFA (2FA) + {% if it_audit.has_mfa %} + Tak + {% else %} + Nie + {% endif %} +
+
+ VPN + {% if it_audit.has_vpn %} + Tak + {% else %} + Nie + {% endif %} +
+
+ EDR + {% if it_audit.has_edr %} + Tak + {% else %} + Nie + {% endif %} +
+
+
+
+
+{% endif %} + {# Company Events - UKRYTE (2026-01-11) - do przywrócenia w przyszłości {% if events %}
diff --git a/update_social_media.py b/update_social_media.py index 428e877..69336a8 100644 --- a/update_social_media.py +++ b/update_social_media.py @@ -18,7 +18,7 @@ from datetime import datetime # The database module will fall back to a safe placeholder if not set. # NEVER commit real credentials to version control (CWE-798). -from database import SessionLocal, Company, CompanySocialMedia +from database import SessionLocal, Company, CompanySocialMedia, normalize_social_url from sqlalchemy import func PLATFORMS = ['facebook', 'instagram', 'youtube', 'linkedin', 'tiktok', 'twitter'] @@ -63,14 +63,19 @@ def update_social_media(dry_run=False): if not url: continue - # Check if profile already exists + # Normalize URL to prevent www vs non-www duplicates + url = normalize_social_url(url, platform) + + # Check if profile already exists for this platform existing = db.query(CompanySocialMedia).filter( CompanySocialMedia.company_id == company_id, CompanySocialMedia.platform == platform ).first() if existing: - if existing.url != url: + # Normalize existing URL for comparison + existing_normalized = normalize_social_url(existing.url, platform) + if existing_normalized != url: old_url = existing.url if not dry_run: existing.url = url