test
Browse files- app/models/__pycache__/facebook_ad.cpython-312.pyc +0 -0
- app/models/facebook_ad.py +133 -72
- app/routes/__pycache__/facebook_ads.cpython-312.pyc +0 -0
- app/routes/facebook_ads.py +49 -231
- app/templates/base.html +56 -27
- app/templates/facebook_ads/ad_analysis.html +18 -9
- app/templates/facebook_ads/ad_detail.html +12 -1
- app/templates/facebook_ads/index.html +11 -0
- migrations/versions/__pycache__/fd9168d1a5fa_update_facebook_ad_model.cpython-312.pyc +0 -0
- migrations/versions/fd9168d1a5fa_update_facebook_ad_model.py +60 -0
app/models/__pycache__/facebook_ad.cpython-312.pyc
CHANGED
|
Binary files a/app/models/__pycache__/facebook_ad.cpython-312.pyc and b/app/models/__pycache__/facebook_ad.cpython-312.pyc differ
|
|
|
app/models/facebook_ad.py
CHANGED
|
@@ -4,94 +4,157 @@ import uuid
|
|
| 4 |
import json
|
| 5 |
|
| 6 |
class FacebookAd(db.Model):
|
| 7 |
-
"""Model for storing Facebook
|
| 8 |
-
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
| 9 |
|
| 10 |
-
|
| 11 |
-
ad_id = db.Column(db.String(255),
|
| 12 |
-
advertiser = db.Column(db.String(255),
|
| 13 |
-
advertiser_id = db.Column(db.String(255),
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
#
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
search_query = db.Column(db.String(255), nullable=True, index=True)
|
| 22 |
-
position = db.Column(db.Integer, nullable=True)
|
| 23 |
|
| 24 |
# Analysis results
|
| 25 |
-
sentiment = db.Column(db.
|
| 26 |
-
topics = db.Column(db.JSON, nullable=True)
|
| 27 |
-
entities = db.Column(db.JSON, nullable=True)
|
| 28 |
-
|
| 29 |
-
# Raw data for future processing
|
| 30 |
-
raw_data = db.Column(db.JSON, nullable=True)
|
| 31 |
-
raw_text = db.Column(db.Text, nullable=True)
|
| 32 |
|
| 33 |
# Timestamps
|
| 34 |
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 35 |
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 36 |
|
| 37 |
-
# User
|
| 38 |
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
| 39 |
|
| 40 |
-
def
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
@classmethod
|
| 44 |
-
def from_scraper_data(cls,
|
| 45 |
-
"""Create a FacebookAd instance from
|
| 46 |
-
#
|
|
|
|
|
|
|
| 47 |
ad = cls(
|
| 48 |
-
ad_id=
|
| 49 |
-
advertiser=
|
| 50 |
-
advertiser_id=
|
| 51 |
-
content=
|
| 52 |
-
|
| 53 |
-
search_query=ad_data.get('search_query'),
|
| 54 |
-
position=ad_data.get('position'),
|
| 55 |
user_id=user_id
|
| 56 |
)
|
| 57 |
|
| 58 |
-
#
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
if 'links' in ad_data and ad_data['links']:
|
| 63 |
-
ad.links = ad_data['links']
|
| 64 |
-
|
| 65 |
-
# Store the full raw data for future reference
|
| 66 |
-
ad.raw_data = {k: v for k, v in ad_data.items() if k not in ['images', 'links']}
|
| 67 |
|
| 68 |
return ad
|
| 69 |
|
| 70 |
-
def
|
| 71 |
-
"""Get
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
try:
|
| 77 |
-
return json.loads(self.images)
|
| 78 |
-
except:
|
| 79 |
-
return []
|
| 80 |
-
|
| 81 |
-
return self.images
|
| 82 |
|
| 83 |
-
def
|
| 84 |
-
"""Get
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
try:
|
| 90 |
-
return json.loads(self.links)
|
| 91 |
-
except:
|
| 92 |
-
return []
|
| 93 |
-
|
| 94 |
-
return self.links
|
| 95 |
|
| 96 |
def to_dict(self):
|
| 97 |
"""Convert the ad to a dictionary for API responses."""
|
|
@@ -101,13 +164,11 @@ class FacebookAd(db.Model):
|
|
| 101 |
'advertiser': self.advertiser,
|
| 102 |
'advertiser_id': self.advertiser_id,
|
| 103 |
'content': self.content,
|
| 104 |
-
'images': self.get_image_urls(),
|
| 105 |
-
'links': self.get_links(),
|
| 106 |
'search_query': self.search_query,
|
| 107 |
-
'
|
|
|
|
| 108 |
'sentiment': self.sentiment,
|
| 109 |
-
'topics': self.
|
| 110 |
-
'entities': self.entities,
|
| 111 |
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 112 |
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
| 113 |
}
|
|
|
|
| 4 |
import json
|
| 5 |
|
| 6 |
class FacebookAd(db.Model):
|
| 7 |
+
"""Model for storing Facebook ads data."""
|
|
|
|
| 8 |
|
| 9 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 10 |
+
ad_id = db.Column(db.String(255), index=True, unique=True)
|
| 11 |
+
advertiser = db.Column(db.String(255), index=True)
|
| 12 |
+
advertiser_id = db.Column(db.String(255), index=True, nullable=True)
|
| 13 |
+
content = db.Column(db.Text)
|
| 14 |
+
search_query = db.Column(db.String(255), index=True, nullable=True)
|
| 15 |
|
| 16 |
+
# Store JSON data as text fields
|
| 17 |
+
image_urls_json = db.Column(db.Text, nullable=True)
|
| 18 |
+
links_json = db.Column(db.Text, nullable=True)
|
| 19 |
+
metadata_json = db.Column(db.Text, nullable=True)
|
| 20 |
+
topics_json = db.Column(db.Text, nullable=True)
|
| 21 |
+
entities_json = db.Column(db.Text, nullable=True)
|
|
|
|
|
|
|
| 22 |
|
| 23 |
# Analysis results
|
| 24 |
+
sentiment = db.Column(db.Float, nullable=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
# Timestamps
|
| 27 |
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 28 |
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 29 |
|
| 30 |
+
# User relationship
|
| 31 |
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
| 32 |
|
| 33 |
+
def get_image_urls(self):
|
| 34 |
+
"""Get image URLs as a list."""
|
| 35 |
+
if not self.image_urls_json:
|
| 36 |
+
return []
|
| 37 |
+
try:
|
| 38 |
+
return json.loads(self.image_urls_json)
|
| 39 |
+
except:
|
| 40 |
+
return []
|
| 41 |
+
|
| 42 |
+
def set_image_urls(self, value):
|
| 43 |
+
"""Set image URLs from a list."""
|
| 44 |
+
if value is None:
|
| 45 |
+
self.image_urls_json = None
|
| 46 |
+
else:
|
| 47 |
+
self.image_urls_json = json.dumps(value)
|
| 48 |
+
|
| 49 |
+
image_urls = property(get_image_urls, set_image_urls)
|
| 50 |
+
|
| 51 |
+
def get_links(self):
|
| 52 |
+
"""Get links as a list."""
|
| 53 |
+
if not self.links_json:
|
| 54 |
+
return []
|
| 55 |
+
try:
|
| 56 |
+
return json.loads(self.links_json)
|
| 57 |
+
except:
|
| 58 |
+
return []
|
| 59 |
+
|
| 60 |
+
def set_links(self, value):
|
| 61 |
+
"""Set links from a list."""
|
| 62 |
+
if value is None:
|
| 63 |
+
self.links_json = None
|
| 64 |
+
else:
|
| 65 |
+
self.links_json = json.dumps(value)
|
| 66 |
+
|
| 67 |
+
links = property(get_links, set_links)
|
| 68 |
+
|
| 69 |
+
def get_metadata(self):
|
| 70 |
+
"""Get metadata as a dictionary."""
|
| 71 |
+
if not self.metadata_json:
|
| 72 |
+
return {}
|
| 73 |
+
try:
|
| 74 |
+
return json.loads(self.metadata_json)
|
| 75 |
+
except:
|
| 76 |
+
return {}
|
| 77 |
+
|
| 78 |
+
def set_metadata(self, value):
|
| 79 |
+
"""Set metadata from a dictionary."""
|
| 80 |
+
if value is None:
|
| 81 |
+
self.metadata_json = None
|
| 82 |
+
else:
|
| 83 |
+
self.metadata_json = json.dumps(value)
|
| 84 |
+
|
| 85 |
+
metadata = property(get_metadata, set_metadata)
|
| 86 |
+
|
| 87 |
+
def get_topics(self):
|
| 88 |
+
"""Get topics as a list."""
|
| 89 |
+
if not self.topics_json:
|
| 90 |
+
return []
|
| 91 |
+
try:
|
| 92 |
+
return json.loads(self.topics_json)
|
| 93 |
+
except:
|
| 94 |
+
return []
|
| 95 |
+
|
| 96 |
+
def set_topics(self, value):
|
| 97 |
+
"""Set topics from a list."""
|
| 98 |
+
if value is None:
|
| 99 |
+
self.topics_json = None
|
| 100 |
+
else:
|
| 101 |
+
self.topics_json = json.dumps(value)
|
| 102 |
+
|
| 103 |
+
topics = property(get_topics, set_topics)
|
| 104 |
+
|
| 105 |
+
def get_entities(self):
|
| 106 |
+
"""Get entities as a list of dictionaries."""
|
| 107 |
+
if not self.entities_json:
|
| 108 |
+
return []
|
| 109 |
+
try:
|
| 110 |
+
return json.loads(self.entities_json)
|
| 111 |
+
except:
|
| 112 |
+
return []
|
| 113 |
+
|
| 114 |
+
def set_entities(self, value):
|
| 115 |
+
"""Set entities from a list of dictionaries."""
|
| 116 |
+
if value is None:
|
| 117 |
+
self.entities_json = None
|
| 118 |
+
else:
|
| 119 |
+
self.entities_json = json.dumps(value)
|
| 120 |
+
|
| 121 |
+
entities = property(get_entities, set_entities)
|
| 122 |
|
| 123 |
@classmethod
|
| 124 |
+
def from_scraper_data(cls, data, user_id=None):
|
| 125 |
+
"""Create a FacebookAd instance from scraper data."""
|
| 126 |
+
# Generate a unique ID if not provided
|
| 127 |
+
ad_id = data.get('ad_id', str(uuid.uuid4()))
|
| 128 |
+
|
| 129 |
ad = cls(
|
| 130 |
+
ad_id=ad_id,
|
| 131 |
+
advertiser=data.get('advertiser', ''),
|
| 132 |
+
advertiser_id=data.get('advertiser_id'),
|
| 133 |
+
content=data.get('content', ''),
|
| 134 |
+
search_query=data.get('search_query'),
|
|
|
|
|
|
|
| 135 |
user_id=user_id
|
| 136 |
)
|
| 137 |
|
| 138 |
+
# Set JSON fields
|
| 139 |
+
ad.set_image_urls(data.get('image_urls', []))
|
| 140 |
+
ad.set_links(data.get('links', []))
|
| 141 |
+
ad.set_metadata(data.get('metadata', {}))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
return ad
|
| 144 |
|
| 145 |
+
def get_image_urls_limited(self, limit=None):
|
| 146 |
+
"""Get image URLs, optionally limited to a specific number."""
|
| 147 |
+
urls = self.get_image_urls()
|
| 148 |
+
if limit and len(urls) > limit:
|
| 149 |
+
return urls[:limit]
|
| 150 |
+
return urls
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
+
def get_links_limited(self, limit=None):
|
| 153 |
+
"""Get links, optionally limited to a specific number."""
|
| 154 |
+
links = self.get_links()
|
| 155 |
+
if limit and len(links) > limit:
|
| 156 |
+
return links[:limit]
|
| 157 |
+
return links
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
def to_dict(self):
|
| 160 |
"""Convert the ad to a dictionary for API responses."""
|
|
|
|
| 164 |
'advertiser': self.advertiser,
|
| 165 |
'advertiser_id': self.advertiser_id,
|
| 166 |
'content': self.content,
|
|
|
|
|
|
|
| 167 |
'search_query': self.search_query,
|
| 168 |
+
'image_urls': self.get_image_urls(),
|
| 169 |
+
'links': self.get_links(),
|
| 170 |
'sentiment': self.sentiment,
|
| 171 |
+
'topics': self.get_topics(),
|
|
|
|
| 172 |
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 173 |
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
| 174 |
}
|
app/routes/__pycache__/facebook_ads.cpython-312.pyc
CHANGED
|
Binary files a/app/routes/__pycache__/facebook_ads.cpython-312.pyc and b/app/routes/__pycache__/facebook_ads.cpython-312.pyc differ
|
|
|
app/routes/facebook_ads.py
CHANGED
|
@@ -1,9 +1,6 @@
|
|
| 1 |
from flask import Blueprint, render_template, request, jsonify, current_app, flash, redirect, url_for
|
| 2 |
from flask_login import login_required, current_user
|
| 3 |
-
from app
|
| 4 |
-
from app.models.facebook_ad import FacebookAd
|
| 5 |
-
from app.services.ai_processor import AIPipeline
|
| 6 |
-
from app import db, celery
|
| 7 |
import logging
|
| 8 |
import json
|
| 9 |
from datetime import datetime
|
|
@@ -11,11 +8,14 @@ from datetime import datetime
|
|
| 11 |
logger = logging.getLogger(__name__)
|
| 12 |
facebook_ads_bp = Blueprint('facebook_ads', __name__, url_prefix='/facebook-ads')
|
| 13 |
|
|
|
|
|
|
|
|
|
|
| 14 |
@facebook_ads_bp.route('/', methods=['GET'])
|
| 15 |
@login_required
|
| 16 |
def index():
|
| 17 |
"""Facebook Ads dashboard page."""
|
| 18 |
-
return render_template('facebook_ads/index.html')
|
| 19 |
|
| 20 |
@facebook_ads_bp.route('/search', methods=['GET', 'POST'])
|
| 21 |
@login_required
|
|
@@ -23,20 +23,15 @@ def search():
|
|
| 23 |
"""Search for Facebook ads."""
|
| 24 |
if request.method == 'POST':
|
| 25 |
search_query = request.form.get('search_query', '')
|
| 26 |
-
num_scrolls = int(request.form.get('num_scrolls', 5))
|
| 27 |
-
country_code = request.form.get('country_code', 'ALL')
|
| 28 |
|
| 29 |
if not search_query:
|
| 30 |
flash('Please enter a search query', 'warning')
|
| 31 |
-
return render_template('facebook_ads/search.html')
|
| 32 |
-
|
| 33 |
-
# Start the scraping task
|
| 34 |
-
task = scrape_facebook_ads.delay(search_query, num_scrolls, country_code, current_user.id)
|
| 35 |
|
| 36 |
-
flash(
|
| 37 |
-
return render_template('facebook_ads/search.html',
|
| 38 |
|
| 39 |
-
return render_template('facebook_ads/search.html')
|
| 40 |
|
| 41 |
@facebook_ads_bp.route('/page-search', methods=['GET', 'POST'])
|
| 42 |
@login_required
|
|
@@ -44,106 +39,82 @@ def page_search():
|
|
| 44 |
"""Search for ads from a specific Facebook page."""
|
| 45 |
if request.method == 'POST':
|
| 46 |
page_name = request.form.get('page_name', '')
|
| 47 |
-
num_scrolls = int(request.form.get('num_scrolls', 5))
|
| 48 |
|
| 49 |
if not page_name:
|
| 50 |
flash('Please enter a page name', 'warning')
|
| 51 |
-
return render_template('facebook_ads/page_search.html')
|
| 52 |
-
|
| 53 |
-
# Start the scraping task
|
| 54 |
-
task = scrape_facebook_page_ads.delay(page_name, num_scrolls, current_user.id)
|
| 55 |
|
| 56 |
-
flash(
|
| 57 |
-
return render_template('facebook_ads/page_search.html',
|
| 58 |
|
| 59 |
-
return render_template('facebook_ads/page_search.html')
|
| 60 |
|
| 61 |
@facebook_ads_bp.route('/results', methods=['GET'])
|
| 62 |
@login_required
|
| 63 |
def results():
|
| 64 |
"""View Facebook ads results."""
|
| 65 |
-
|
|
|
|
| 66 |
query = request.args.get('query', '')
|
| 67 |
advertiser = request.args.get('advertiser', '')
|
| 68 |
|
| 69 |
-
|
| 70 |
-
ads_query = FacebookAd.query
|
| 71 |
-
|
| 72 |
-
if query:
|
| 73 |
-
ads_query = ads_query.filter(FacebookAd.search_query.ilike(f'%{query}%'))
|
| 74 |
-
|
| 75 |
-
if advertiser:
|
| 76 |
-
ads_query = ads_query.filter(FacebookAd.advertiser.ilike(f'%{advertiser}%'))
|
| 77 |
-
|
| 78 |
-
# Get results
|
| 79 |
-
ads = ads_query.order_by(FacebookAd.created_at.desc()).limit(100).all()
|
| 80 |
-
|
| 81 |
-
return render_template('facebook_ads/results.html', ads=ads, query=query, advertiser=advertiser)
|
| 82 |
|
| 83 |
@facebook_ads_bp.route('/ad/<ad_id>', methods=['GET'])
|
| 84 |
@login_required
|
| 85 |
def view_ad(ad_id):
|
| 86 |
"""View details of a specific Facebook ad."""
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
@facebook_ads_bp.route('/advertisers', methods=['GET'])
|
| 91 |
@login_required
|
| 92 |
def advertisers():
|
| 93 |
"""View list of advertisers."""
|
| 94 |
-
#
|
| 95 |
-
advertisers_data =
|
| 96 |
-
FacebookAd.advertiser,
|
| 97 |
-
db.func.count(FacebookAd.id).label('ad_count')
|
| 98 |
-
).group_by(FacebookAd.advertiser).order_by(db.func.count(FacebookAd.id).desc()).limit(100).all()
|
| 99 |
|
| 100 |
-
return render_template('facebook_ads/advertisers.html', advertisers=advertisers_data)
|
| 101 |
|
| 102 |
@facebook_ads_bp.route('/advertiser/<advertiser_name>', methods=['GET'])
|
| 103 |
@login_required
|
| 104 |
def advertiser_detail(advertiser_name):
|
| 105 |
"""View details and ads for a specific advertiser."""
|
| 106 |
-
|
| 107 |
-
|
|
|
|
| 108 |
|
| 109 |
@facebook_ads_bp.route('/analyze/<ad_id>', methods=['GET'])
|
| 110 |
@login_required
|
| 111 |
def analyze_ad(ad_id):
|
| 112 |
"""Analyze a specific Facebook ad."""
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
@facebook_ads_bp.route('/api/ads', methods=['GET'])
|
| 123 |
@login_required
|
| 124 |
def api_get_ads():
|
| 125 |
"""API endpoint to get Facebook Ads data."""
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
limit = int(request.args.get('limit', 50))
|
| 129 |
-
|
| 130 |
-
# Build query
|
| 131 |
-
ads_query = FacebookAd.query
|
| 132 |
-
|
| 133 |
-
if query:
|
| 134 |
-
ads_query = ads_query.filter(
|
| 135 |
-
(FacebookAd.content.ilike(f'%{query}%')) |
|
| 136 |
-
(FacebookAd.search_query.ilike(f'%{query}%'))
|
| 137 |
-
)
|
| 138 |
-
|
| 139 |
-
if advertiser:
|
| 140 |
-
ads_query = ads_query.filter(FacebookAd.advertiser.ilike(f'%{advertiser}%'))
|
| 141 |
-
|
| 142 |
-
# Get results
|
| 143 |
-
ads = ads_query.order_by(FacebookAd.created_at.desc()).limit(limit).all()
|
| 144 |
-
|
| 145 |
-
# Convert to JSON
|
| 146 |
-
result = [ad.to_dict() for ad in ads]
|
| 147 |
|
| 148 |
return jsonify(result)
|
| 149 |
|
|
@@ -151,160 +122,7 @@ def api_get_ads():
|
|
| 151 |
@login_required
|
| 152 |
def api_get_advertisers():
|
| 153 |
"""API endpoint to get advertisers data."""
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
# Get unique advertisers and count their ads
|
| 157 |
-
advertisers_data = db.session.query(
|
| 158 |
-
FacebookAd.advertiser,
|
| 159 |
-
db.func.count(FacebookAd.id).label('ad_count')
|
| 160 |
-
).group_by(FacebookAd.advertiser).order_by(db.func.count(FacebookAd.id).desc()).limit(limit).all()
|
| 161 |
-
|
| 162 |
-
# Convert to JSON
|
| 163 |
-
result = [{"name": adv[0], "ad_count": adv[1]} for adv in advertisers_data if adv[0]]
|
| 164 |
-
|
| 165 |
-
return jsonify(result)
|
| 166 |
-
|
| 167 |
-
@celery.task
|
| 168 |
-
def scrape_facebook_ads(search_query, num_scrolls, country_code, user_id):
|
| 169 |
-
"""Celery task to scrape Facebook ads."""
|
| 170 |
-
try:
|
| 171 |
-
logger.info(f"Starting Facebook ads scraping for query: {search_query}")
|
| 172 |
-
|
| 173 |
-
# Initialize scraper
|
| 174 |
-
scraper = FacebookScraper()
|
| 175 |
-
|
| 176 |
-
# Scrape ads
|
| 177 |
-
ads_data = scraper.scrape_ads(search_query, num_scrolls, country_code)
|
| 178 |
-
|
| 179 |
-
logger.info(f"Scraped {len(ads_data)} Facebook ads")
|
| 180 |
-
|
| 181 |
-
# Process and store ads
|
| 182 |
-
ai_pipeline = AIPipeline()
|
| 183 |
-
|
| 184 |
-
for ad_data in ads_data:
|
| 185 |
-
# Create FacebookAd instance
|
| 186 |
-
ad = FacebookAd.from_scraper_data(ad_data, user_id)
|
| 187 |
-
|
| 188 |
-
# Process with AI if there's content
|
| 189 |
-
if ad.content:
|
| 190 |
-
try:
|
| 191 |
-
# Create a simple object with content for AI processing
|
| 192 |
-
ad_content = type('obj', (object,), {
|
| 193 |
-
'content': ad.content
|
| 194 |
-
})
|
| 195 |
-
|
| 196 |
-
# Process with AI
|
| 197 |
-
ai_results = ai_pipeline.process_ad(ad_content)
|
| 198 |
-
ad.sentiment = ai_results.get('sentiment')
|
| 199 |
-
except Exception as e:
|
| 200 |
-
logger.error(f"Error processing ad with AI: {e}")
|
| 201 |
-
|
| 202 |
-
# Save to database
|
| 203 |
-
db.session.add(ad)
|
| 204 |
-
|
| 205 |
-
db.session.commit()
|
| 206 |
-
logger.info(f"Saved {len(ads_data)} Facebook ads to database")
|
| 207 |
-
|
| 208 |
-
return {'status': 'success', 'count': len(ads_data)}
|
| 209 |
-
|
| 210 |
-
except Exception as e:
|
| 211 |
-
logger.error(f"Error in Facebook ads scraping task: {e}")
|
| 212 |
-
db.session.rollback()
|
| 213 |
-
return {'status': 'error', 'message': str(e)}
|
| 214 |
-
|
| 215 |
-
@celery.task
|
| 216 |
-
def scrape_facebook_page_ads(page_name, num_scrolls, user_id):
|
| 217 |
-
"""Celery task to scrape ads from a specific Facebook page."""
|
| 218 |
-
try:
|
| 219 |
-
logger.info(f"Starting Facebook page ads scraping for page: {page_name}")
|
| 220 |
-
|
| 221 |
-
# Initialize scraper
|
| 222 |
-
scraper = FacebookScraper()
|
| 223 |
-
|
| 224 |
-
# Scrape ads
|
| 225 |
-
ads_data = scraper.scrape_ads_by_page(page_name, num_scrolls)
|
| 226 |
-
|
| 227 |
-
logger.info(f"Scraped {len(ads_data)} Facebook ads from page {page_name}")
|
| 228 |
-
|
| 229 |
-
# Process and store ads
|
| 230 |
-
ai_pipeline = AIPipeline()
|
| 231 |
-
|
| 232 |
-
for ad_data in ads_data:
|
| 233 |
-
# Create FacebookAd instance
|
| 234 |
-
ad = FacebookAd.from_scraper_data(ad_data, user_id)
|
| 235 |
-
|
| 236 |
-
# Process with AI if there's content
|
| 237 |
-
if ad.content:
|
| 238 |
-
try:
|
| 239 |
-
# Create a simple object with content for AI processing
|
| 240 |
-
ad_content = type('obj', (object,), {
|
| 241 |
-
'content': ad.content
|
| 242 |
-
})
|
| 243 |
-
|
| 244 |
-
# Process with AI
|
| 245 |
-
ai_results = ai_pipeline.process_ad(ad_content)
|
| 246 |
-
ad.sentiment = ai_results.get('sentiment')
|
| 247 |
-
except Exception as e:
|
| 248 |
-
logger.error(f"Error processing ad with AI: {e}")
|
| 249 |
-
|
| 250 |
-
# Save to database
|
| 251 |
-
db.session.add(ad)
|
| 252 |
-
|
| 253 |
-
db.session.commit()
|
| 254 |
-
logger.info(f"Saved {len(ads_data)} Facebook ads to database")
|
| 255 |
-
|
| 256 |
-
return {'status': 'success', 'count': len(ads_data)}
|
| 257 |
-
|
| 258 |
-
except Exception as e:
|
| 259 |
-
logger.error(f"Error in Facebook page ads scraping task: {e}")
|
| 260 |
-
db.session.rollback()
|
| 261 |
-
return {'status': 'error', 'message': str(e)}
|
| 262 |
-
|
| 263 |
-
@celery.task
|
| 264 |
-
def analyze_facebook_ad(ad_id):
|
| 265 |
-
"""Celery task to analyze a Facebook ad."""
|
| 266 |
-
try:
|
| 267 |
-
logger.info(f"Starting analysis for Facebook ad: {ad_id}")
|
| 268 |
-
|
| 269 |
-
# Get the ad
|
| 270 |
-
ad = FacebookAd.query.get(ad_id)
|
| 271 |
-
|
| 272 |
-
if not ad:
|
| 273 |
-
logger.error(f"Ad not found: {ad_id}")
|
| 274 |
-
return {'status': 'error', 'message': 'Ad not found'}
|
| 275 |
-
|
| 276 |
-
# Initialize AI pipeline
|
| 277 |
-
ai_pipeline = AIPipeline()
|
| 278 |
-
|
| 279 |
-
# Process with AI if there's content
|
| 280 |
-
if ad.content:
|
| 281 |
-
try:
|
| 282 |
-
# Create a simple object with content for AI processing
|
| 283 |
-
ad_content = type('obj', (object,), {
|
| 284 |
-
'content': ad.content
|
| 285 |
-
})
|
| 286 |
-
|
| 287 |
-
# Process with AI
|
| 288 |
-
ai_results = ai_pipeline.process_ad(ad_content)
|
| 289 |
-
|
| 290 |
-
# Update ad with results
|
| 291 |
-
ad.sentiment = ai_results.get('sentiment')
|
| 292 |
-
ad.topics = ai_results.get('topics')
|
| 293 |
-
ad.entities = ai_results.get('entities')
|
| 294 |
-
|
| 295 |
-
# Save to database
|
| 296 |
-
db.session.commit()
|
| 297 |
-
|
| 298 |
-
logger.info(f"Successfully analyzed Facebook ad: {ad_id}")
|
| 299 |
-
return {'status': 'success', 'ad_id': ad_id}
|
| 300 |
-
except Exception as e:
|
| 301 |
-
logger.error(f"Error processing ad with AI: {e}")
|
| 302 |
-
return {'status': 'error', 'message': str(e)}
|
| 303 |
-
else:
|
| 304 |
-
logger.warning(f"No content to analyze for ad: {ad_id}")
|
| 305 |
-
return {'status': 'warning', 'message': 'No content to analyze'}
|
| 306 |
|
| 307 |
-
|
| 308 |
-
logger.error(f"Error in Facebook ad analysis task: {e}")
|
| 309 |
-
db.session.rollback()
|
| 310 |
-
return {'status': 'error', 'message': str(e)}
|
|
|
|
| 1 |
from flask import Blueprint, render_template, request, jsonify, current_app, flash, redirect, url_for
|
| 2 |
from flask_login import login_required, current_user
|
| 3 |
+
from app import db
|
|
|
|
|
|
|
|
|
|
| 4 |
import logging
|
| 5 |
import json
|
| 6 |
from datetime import datetime
|
|
|
|
| 8 |
logger = logging.getLogger(__name__)
|
| 9 |
facebook_ads_bp = Blueprint('facebook_ads', __name__, url_prefix='/facebook-ads')
|
| 10 |
|
| 11 |
+
# Flag to indicate if AI processing is available
|
| 12 |
+
AI_AVAILABLE = False
|
| 13 |
+
|
| 14 |
@facebook_ads_bp.route('/', methods=['GET'])
|
| 15 |
@login_required
|
| 16 |
def index():
|
| 17 |
"""Facebook Ads dashboard page."""
|
| 18 |
+
return render_template('facebook_ads/index.html', ai_available=AI_AVAILABLE)
|
| 19 |
|
| 20 |
@facebook_ads_bp.route('/search', methods=['GET', 'POST'])
|
| 21 |
@login_required
|
|
|
|
| 23 |
"""Search for Facebook ads."""
|
| 24 |
if request.method == 'POST':
|
| 25 |
search_query = request.form.get('search_query', '')
|
|
|
|
|
|
|
| 26 |
|
| 27 |
if not search_query:
|
| 28 |
flash('Please enter a search query', 'warning')
|
| 29 |
+
return render_template('facebook_ads/search.html', ai_available=AI_AVAILABLE)
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
flash('Facebook Ads scraping is not available. Please install required dependencies.', 'warning')
|
| 32 |
+
return render_template('facebook_ads/search.html', ai_available=AI_AVAILABLE)
|
| 33 |
|
| 34 |
+
return render_template('facebook_ads/search.html', ai_available=AI_AVAILABLE)
|
| 35 |
|
| 36 |
@facebook_ads_bp.route('/page-search', methods=['GET', 'POST'])
|
| 37 |
@login_required
|
|
|
|
| 39 |
"""Search for ads from a specific Facebook page."""
|
| 40 |
if request.method == 'POST':
|
| 41 |
page_name = request.form.get('page_name', '')
|
|
|
|
| 42 |
|
| 43 |
if not page_name:
|
| 44 |
flash('Please enter a page name', 'warning')
|
| 45 |
+
return render_template('facebook_ads/page_search.html', ai_available=AI_AVAILABLE)
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
+
flash('Facebook Ads scraping is not available. Please install required dependencies.', 'warning')
|
| 48 |
+
return render_template('facebook_ads/page_search.html', ai_available=AI_AVAILABLE)
|
| 49 |
|
| 50 |
+
return render_template('facebook_ads/page_search.html', ai_available=AI_AVAILABLE)
|
| 51 |
|
| 52 |
@facebook_ads_bp.route('/results', methods=['GET'])
|
| 53 |
@login_required
|
| 54 |
def results():
|
| 55 |
"""View Facebook ads results."""
|
| 56 |
+
# Placeholder for now
|
| 57 |
+
ads = []
|
| 58 |
query = request.args.get('query', '')
|
| 59 |
advertiser = request.args.get('advertiser', '')
|
| 60 |
|
| 61 |
+
return render_template('facebook_ads/results.html', ads=ads, query=query, advertiser=advertiser, ai_available=AI_AVAILABLE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
@facebook_ads_bp.route('/ad/<ad_id>', methods=['GET'])
|
| 64 |
@login_required
|
| 65 |
def view_ad(ad_id):
|
| 66 |
"""View details of a specific Facebook ad."""
|
| 67 |
+
# Placeholder for now
|
| 68 |
+
ad = {
|
| 69 |
+
'id': ad_id,
|
| 70 |
+
'advertiser': 'Example Advertiser',
|
| 71 |
+
'content': 'This is a placeholder ad content.',
|
| 72 |
+
'image_urls': [],
|
| 73 |
+
'links': [],
|
| 74 |
+
'created_at': datetime.utcnow()
|
| 75 |
+
}
|
| 76 |
+
return render_template('facebook_ads/ad_detail.html', ad=ad, ai_available=AI_AVAILABLE)
|
| 77 |
|
| 78 |
@facebook_ads_bp.route('/advertisers', methods=['GET'])
|
| 79 |
@login_required
|
| 80 |
def advertisers():
|
| 81 |
"""View list of advertisers."""
|
| 82 |
+
# Placeholder for now
|
| 83 |
+
advertisers_data = []
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
return render_template('facebook_ads/advertisers.html', advertisers=advertisers_data, ai_available=AI_AVAILABLE)
|
| 86 |
|
| 87 |
@facebook_ads_bp.route('/advertiser/<advertiser_name>', methods=['GET'])
|
| 88 |
@login_required
|
| 89 |
def advertiser_detail(advertiser_name):
|
| 90 |
"""View details and ads for a specific advertiser."""
|
| 91 |
+
# Placeholder for now
|
| 92 |
+
ads = []
|
| 93 |
+
return render_template('facebook_ads/advertiser_detail.html', advertiser=advertiser_name, ads=ads, ai_available=AI_AVAILABLE)
|
| 94 |
|
| 95 |
@facebook_ads_bp.route('/analyze/<ad_id>', methods=['GET'])
|
| 96 |
@login_required
|
| 97 |
def analyze_ad(ad_id):
|
| 98 |
"""Analyze a specific Facebook ad."""
|
| 99 |
+
# Placeholder for now
|
| 100 |
+
ad = {
|
| 101 |
+
'id': ad_id,
|
| 102 |
+
'advertiser': 'Example Advertiser',
|
| 103 |
+
'content': 'This is a placeholder ad content.',
|
| 104 |
+
'image_urls': [],
|
| 105 |
+
'links': [],
|
| 106 |
+
'created_at': datetime.utcnow()
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
flash('AI analysis is not available. Please install required dependencies.', 'warning')
|
| 110 |
+
return render_template('facebook_ads/ad_analysis.html', ad=ad, ai_available=AI_AVAILABLE)
|
| 111 |
|
| 112 |
@facebook_ads_bp.route('/api/ads', methods=['GET'])
|
| 113 |
@login_required
|
| 114 |
def api_get_ads():
|
| 115 |
"""API endpoint to get Facebook Ads data."""
|
| 116 |
+
# Placeholder for now
|
| 117 |
+
result = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
return jsonify(result)
|
| 120 |
|
|
|
|
| 122 |
@login_required
|
| 123 |
def api_get_advertisers():
|
| 124 |
"""API endpoint to get advertisers data."""
|
| 125 |
+
# Placeholder for now
|
| 126 |
+
result = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
+
return jsonify(result)
|
|
|
|
|
|
|
|
|
app/templates/base.html
CHANGED
|
@@ -14,52 +14,81 @@
|
|
| 14 |
<body>
|
| 15 |
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
| 16 |
<div class="container">
|
| 17 |
-
<a class="navbar-brand" href="{{ url_for('
|
| 18 |
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
| 19 |
<span class="navbar-toggler-icon"></span>
|
| 20 |
</button>
|
| 21 |
<div class="collapse navbar-collapse" id="navbarNav">
|
| 22 |
<ul class="navbar-nav me-auto">
|
| 23 |
-
|
| 24 |
-
<
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
| 39 |
</ul>
|
| 40 |
<ul class="navbar-nav">
|
| 41 |
-
|
| 42 |
-
<
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
</ul>
|
| 45 |
</div>
|
| 46 |
</div>
|
| 47 |
</nav>
|
| 48 |
|
| 49 |
-
<main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
{% block content %}{% endblock %}
|
| 51 |
</main>
|
| 52 |
|
| 53 |
-
<footer class="
|
| 54 |
-
<div class="container">
|
| 55 |
-
<
|
| 56 |
</div>
|
| 57 |
</footer>
|
| 58 |
|
| 59 |
-
<!-- Bootstrap
|
| 60 |
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
| 61 |
-
<!-- jQuery -->
|
| 62 |
-
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
| 63 |
{% block scripts %}{% endblock %}
|
| 64 |
</body>
|
| 65 |
</html>
|
|
|
|
| 14 |
<body>
|
| 15 |
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
| 16 |
<div class="container">
|
| 17 |
+
<a class="navbar-brand" href="{{ url_for('index') }}">Ad Analysis Tool</a>
|
| 18 |
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
| 19 |
<span class="navbar-toggler-icon"></span>
|
| 20 |
</button>
|
| 21 |
<div class="collapse navbar-collapse" id="navbarNav">
|
| 22 |
<ul class="navbar-nav me-auto">
|
| 23 |
+
{% if current_user.is_authenticated %}
|
| 24 |
+
<li class="nav-item">
|
| 25 |
+
<a class="nav-link" href="{{ url_for('google_ads.dashboard') }}">
|
| 26 |
+
<i class="fab fa-google"></i> Google Ads
|
| 27 |
+
</a>
|
| 28 |
+
</li>
|
| 29 |
+
<li class="nav-item">
|
| 30 |
+
<a class="nav-link" href="{{ url_for('facebook_ads.index') }}">
|
| 31 |
+
<i class="fab fa-facebook"></i> Facebook Ads
|
| 32 |
+
</a>
|
| 33 |
+
</li>
|
| 34 |
+
<li class="nav-item">
|
| 35 |
+
<a class="nav-link" href="{{ url_for('compliance.compliance_report') }}">
|
| 36 |
+
<i class="fas fa-check-circle"></i> Compliance
|
| 37 |
+
</a>
|
| 38 |
+
</li>
|
| 39 |
+
{% endif %}
|
| 40 |
</ul>
|
| 41 |
<ul class="navbar-nav">
|
| 42 |
+
{% if current_user.is_authenticated %}
|
| 43 |
+
<li class="nav-item">
|
| 44 |
+
<span class="nav-link">
|
| 45 |
+
<i class="fas fa-user"></i> {{ current_user.email }}
|
| 46 |
+
</span>
|
| 47 |
+
</li>
|
| 48 |
+
<li class="nav-item">
|
| 49 |
+
<a class="nav-link" href="{{ url_for('auth.logout') }}">
|
| 50 |
+
<i class="fas fa-sign-out-alt"></i> Logout
|
| 51 |
+
</a>
|
| 52 |
+
</li>
|
| 53 |
+
{% else %}
|
| 54 |
+
<li class="nav-item">
|
| 55 |
+
<a class="nav-link" href="{{ url_for('auth.login') }}">
|
| 56 |
+
<i class="fas fa-sign-in-alt"></i> Login
|
| 57 |
+
</a>
|
| 58 |
+
</li>
|
| 59 |
+
<li class="nav-item">
|
| 60 |
+
<a class="nav-link" href="{{ url_for('auth.register') }}">
|
| 61 |
+
<i class="fas fa-user-plus"></i> Register
|
| 62 |
+
</a>
|
| 63 |
+
</li>
|
| 64 |
+
{% endif %}
|
| 65 |
</ul>
|
| 66 |
</div>
|
| 67 |
</div>
|
| 68 |
</nav>
|
| 69 |
|
| 70 |
+
<main class="container mt-4">
|
| 71 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 72 |
+
{% if messages %}
|
| 73 |
+
{% for category, message in messages %}
|
| 74 |
+
<div class="alert alert-{{ category }}" role="alert">
|
| 75 |
+
{{ message }}
|
| 76 |
+
</div>
|
| 77 |
+
{% endfor %}
|
| 78 |
+
{% endif %}
|
| 79 |
+
{% endwith %}
|
| 80 |
+
|
| 81 |
{% block content %}{% endblock %}
|
| 82 |
</main>
|
| 83 |
|
| 84 |
+
<footer class="footer mt-5 py-3 bg-light">
|
| 85 |
+
<div class="container text-center">
|
| 86 |
+
<span class="text-muted">© 2024 Ad Analysis Tool. All rights reserved.</span>
|
| 87 |
</div>
|
| 88 |
</footer>
|
| 89 |
|
| 90 |
+
<!-- Bootstrap JS -->
|
| 91 |
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
|
|
|
|
|
|
| 92 |
{% block scripts %}{% endblock %}
|
| 93 |
</body>
|
| 94 |
</html>
|
app/templates/facebook_ads/ad_analysis.html
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
{% extends "base.html" %}
|
| 2 |
|
| 3 |
-
{% block title %}Ad Analysis
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
<div class="container mt-4">
|
|
@@ -13,6 +13,17 @@
|
|
| 13 |
</ol>
|
| 14 |
</nav>
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
<div class="card mb-4">
|
| 17 |
<div class="card-header">
|
| 18 |
<h2 class="mb-0">Ad Analysis</h2>
|
|
@@ -50,7 +61,7 @@
|
|
| 50 |
</p>
|
| 51 |
{% else %}
|
| 52 |
<div class="alert alert-info">
|
| 53 |
-
Sentiment analysis is
|
| 54 |
</div>
|
| 55 |
{% endif %}
|
| 56 |
</div>
|
|
@@ -72,7 +83,7 @@
|
|
| 72 |
</small>
|
| 73 |
{% else %}
|
| 74 |
<div class="alert alert-info">
|
| 75 |
-
Topic analysis is
|
| 76 |
</div>
|
| 77 |
{% endif %}
|
| 78 |
</div>
|
|
@@ -109,12 +120,10 @@
|
|
| 109 |
</small>
|
| 110 |
</div>
|
| 111 |
</div>
|
| 112 |
-
{%
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
<h4 class="alert-heading">Analysis in Progress</h4>
|
| 117 |
-
<p>The ad content is being analyzed. This process may take a few moments. Please refresh the page to see updated results.</p>
|
| 118 |
</div>
|
| 119 |
{% endif %}
|
| 120 |
</div>
|
|
|
|
| 1 |
{% extends "base.html" %}
|
| 2 |
|
| 3 |
+
{% block title %}Ad Analysis{% endblock %}
|
| 4 |
|
| 5 |
{% block content %}
|
| 6 |
<div class="container mt-4">
|
|
|
|
| 13 |
</ol>
|
| 14 |
</nav>
|
| 15 |
|
| 16 |
+
{% if not ai_available %}
|
| 17 |
+
<div class="alert alert-warning" role="alert">
|
| 18 |
+
<h4 class="alert-heading">Limited Functionality</h4>
|
| 19 |
+
<p>This is a placeholder view. Full functionality requires installing the AI dependencies.</p>
|
| 20 |
+
<hr>
|
| 21 |
+
<p class="mb-0">To enable full functionality, please install the required dependencies:</p>
|
| 22 |
+
<code>pip install transformers torch textblob spacy</code><br>
|
| 23 |
+
<code>python -m spacy download en_core_web_sm</code>
|
| 24 |
+
</div>
|
| 25 |
+
{% endif %}
|
| 26 |
+
|
| 27 |
<div class="card mb-4">
|
| 28 |
<div class="card-header">
|
| 29 |
<h2 class="mb-0">Ad Analysis</h2>
|
|
|
|
| 61 |
</p>
|
| 62 |
{% else %}
|
| 63 |
<div class="alert alert-info">
|
| 64 |
+
Sentiment analysis is not available.
|
| 65 |
</div>
|
| 66 |
{% endif %}
|
| 67 |
</div>
|
|
|
|
| 83 |
</small>
|
| 84 |
{% else %}
|
| 85 |
<div class="alert alert-info">
|
| 86 |
+
Topic analysis is not available.
|
| 87 |
</div>
|
| 88 |
{% endif %}
|
| 89 |
</div>
|
|
|
|
| 120 |
</small>
|
| 121 |
</div>
|
| 122 |
</div>
|
| 123 |
+
{% else %}
|
| 124 |
+
<div class="alert alert-info">
|
| 125 |
+
<h4 class="alert-heading">Entity Analysis Not Available</h4>
|
| 126 |
+
<p>Named entity recognition requires the AI dependencies to be installed.</p>
|
|
|
|
|
|
|
| 127 |
</div>
|
| 128 |
{% endif %}
|
| 129 |
</div>
|
app/templates/facebook_ads/ad_detail.html
CHANGED
|
@@ -12,6 +12,17 @@
|
|
| 12 |
</ol>
|
| 13 |
</nav>
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
<div class="card mb-4">
|
| 16 |
<div class="card-header">
|
| 17 |
<h2 class="mb-0">
|
|
@@ -117,7 +128,7 @@
|
|
| 117 |
<div class="card-footer text-muted">
|
| 118 |
<div class="row">
|
| 119 |
<div class="col-md-6">
|
| 120 |
-
Scraped: {{ ad.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
|
| 121 |
</div>
|
| 122 |
<div class="col-md-6 text-end">
|
| 123 |
Search Query: {{ ad.search_query or 'N/A' }}
|
|
|
|
| 12 |
</ol>
|
| 13 |
</nav>
|
| 14 |
|
| 15 |
+
{% if not ai_available %}
|
| 16 |
+
<div class="alert alert-warning" role="alert">
|
| 17 |
+
<h4 class="alert-heading">Limited Functionality</h4>
|
| 18 |
+
<p>This is a placeholder view. Full functionality requires installing the AI dependencies.</p>
|
| 19 |
+
<hr>
|
| 20 |
+
<p class="mb-0">To enable full functionality, please install the required dependencies:</p>
|
| 21 |
+
<code>pip install transformers torch textblob spacy</code><br>
|
| 22 |
+
<code>python -m spacy download en_core_web_sm</code>
|
| 23 |
+
</div>
|
| 24 |
+
{% endif %}
|
| 25 |
+
|
| 26 |
<div class="card mb-4">
|
| 27 |
<div class="card-header">
|
| 28 |
<h2 class="mb-0">
|
|
|
|
| 128 |
<div class="card-footer text-muted">
|
| 129 |
<div class="row">
|
| 130 |
<div class="col-md-6">
|
| 131 |
+
Scraped: {{ ad.created_at.strftime('%Y-%m-%d %H:%M:%S') if ad.created_at else 'N/A' }}
|
| 132 |
</div>
|
| 133 |
<div class="col-md-6 text-end">
|
| 134 |
Search Query: {{ ad.search_query or 'N/A' }}
|
app/templates/facebook_ads/index.html
CHANGED
|
@@ -6,6 +6,17 @@
|
|
| 6 |
<div class="container mt-4">
|
| 7 |
<h1 class="mb-4">Facebook Ads Dashboard</h1>
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
<div class="row">
|
| 10 |
<div class="col-md-6">
|
| 11 |
<div class="card mb-4">
|
|
|
|
| 6 |
<div class="container mt-4">
|
| 7 |
<h1 class="mb-4">Facebook Ads Dashboard</h1>
|
| 8 |
|
| 9 |
+
{% if not ai_available %}
|
| 10 |
+
<div class="alert alert-warning" role="alert">
|
| 11 |
+
<h4 class="alert-heading">Limited Functionality</h4>
|
| 12 |
+
<p>Some features are currently disabled because the required AI dependencies are not installed.</p>
|
| 13 |
+
<hr>
|
| 14 |
+
<p class="mb-0">To enable full functionality, please install the required dependencies:</p>
|
| 15 |
+
<code>pip install transformers torch textblob spacy</code><br>
|
| 16 |
+
<code>python -m spacy download en_core_web_sm</code>
|
| 17 |
+
</div>
|
| 18 |
+
{% endif %}
|
| 19 |
+
|
| 20 |
<div class="row">
|
| 21 |
<div class="col-md-6">
|
| 22 |
<div class="card mb-4">
|
migrations/versions/__pycache__/fd9168d1a5fa_update_facebook_ad_model.cpython-312.pyc
ADDED
|
Binary file (4.1 kB). View file
|
|
|
migrations/versions/fd9168d1a5fa_update_facebook_ad_model.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Update facebook_ad model
|
| 2 |
+
|
| 3 |
+
Revision ID: fd9168d1a5fa
|
| 4 |
+
Revises: dddcd665398d
|
| 5 |
+
Create Date: 2025-03-10 09:11:24.460987
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from alembic import op
|
| 9 |
+
import sqlalchemy as sa
|
| 10 |
+
from sqlalchemy.dialects import sqlite
|
| 11 |
+
|
| 12 |
+
# revision identifiers, used by Alembic.
|
| 13 |
+
revision = 'fd9168d1a5fa'
|
| 14 |
+
down_revision = 'dddcd665398d'
|
| 15 |
+
branch_labels = None
|
| 16 |
+
depends_on = None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def upgrade():
|
| 20 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 21 |
+
with op.batch_alter_table('facebook_ad', schema=None) as batch_op:
|
| 22 |
+
batch_op.drop_index('ix_facebook_ad_ad_id')
|
| 23 |
+
batch_op.drop_index('ix_facebook_ad_advertiser')
|
| 24 |
+
batch_op.drop_index('ix_facebook_ad_advertiser_id')
|
| 25 |
+
batch_op.drop_index('ix_facebook_ad_search_query')
|
| 26 |
+
|
| 27 |
+
op.drop_table('facebook_ad')
|
| 28 |
+
# ### end Alembic commands ###
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def downgrade():
|
| 32 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 33 |
+
op.create_table('facebook_ad',
|
| 34 |
+
sa.Column('id', sa.VARCHAR(length=36), nullable=False),
|
| 35 |
+
sa.Column('ad_id', sa.VARCHAR(length=255), nullable=True),
|
| 36 |
+
sa.Column('advertiser', sa.VARCHAR(length=255), nullable=True),
|
| 37 |
+
sa.Column('advertiser_id', sa.VARCHAR(length=255), nullable=True),
|
| 38 |
+
sa.Column('content', sa.TEXT(), nullable=True),
|
| 39 |
+
sa.Column('images', sqlite.JSON(), nullable=True),
|
| 40 |
+
sa.Column('links', sqlite.JSON(), nullable=True),
|
| 41 |
+
sa.Column('search_query', sa.VARCHAR(length=255), nullable=True),
|
| 42 |
+
sa.Column('position', sa.INTEGER(), nullable=True),
|
| 43 |
+
sa.Column('sentiment', sqlite.JSON(), nullable=True),
|
| 44 |
+
sa.Column('topics', sqlite.JSON(), nullable=True),
|
| 45 |
+
sa.Column('entities', sqlite.JSON(), nullable=True),
|
| 46 |
+
sa.Column('raw_data', sqlite.JSON(), nullable=True),
|
| 47 |
+
sa.Column('raw_text', sa.TEXT(), nullable=True),
|
| 48 |
+
sa.Column('created_at', sa.DATETIME(), nullable=True),
|
| 49 |
+
sa.Column('updated_at', sa.DATETIME(), nullable=True),
|
| 50 |
+
sa.Column('user_id', sa.INTEGER(), nullable=True),
|
| 51 |
+
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
| 52 |
+
sa.PrimaryKeyConstraint('id')
|
| 53 |
+
)
|
| 54 |
+
with op.batch_alter_table('facebook_ad', schema=None) as batch_op:
|
| 55 |
+
batch_op.create_index('ix_facebook_ad_search_query', ['search_query'], unique=False)
|
| 56 |
+
batch_op.create_index('ix_facebook_ad_advertiser_id', ['advertiser_id'], unique=False)
|
| 57 |
+
batch_op.create_index('ix_facebook_ad_advertiser', ['advertiser'], unique=False)
|
| 58 |
+
batch_op.create_index('ix_facebook_ad_ad_id', ['ad_id'], unique=False)
|
| 59 |
+
|
| 60 |
+
# ### end Alembic commands ###
|