diff options
-rw-r--r-- | bootstrap.py | 5 | ||||
-rw-r--r-- | conf.py | 133 | ||||
-rw-r--r-- | conf/conf.cfg-sample | 2 | ||||
-rw-r--r-- | pyaggr3g470r/controllers/abstract.py | 13 | ||||
-rw-r--r-- | pyaggr3g470r/models/feed.py | 2 | ||||
-rw-r--r-- | pyaggr3g470r/templates/home.html | 8 | ||||
-rw-r--r-- | pyaggr3g470r/templates/layout.html | 12 | ||||
-rw-r--r-- | pyaggr3g470r/templates/search.html | 48 | ||||
-rw-r--r-- | pyaggr3g470r/views/views.py | 114 | ||||
-rwxr-xr-x | runserver.py | 3 |
10 files changed, 149 insertions, 191 deletions
diff --git a/bootstrap.py b/bootstrap.py index 9c2ce049..7a5a9b6e 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -22,13 +22,12 @@ from flask.ext.sqlalchemy import SQLAlchemy # Create Flask application application = Flask('pyaggr3g470r') -application.debug = conf.WEBSERVER_DEBUG +application.debug = conf.LOG_LEVEL <= logging.DEBUG scheme, domain, _, _, _ = urlsplit(conf.PLATFORM_URL) application.config['SERVER_NAME'] = domain application.config['PREFERRED_URL_SCHEME'] = scheme -set_logging(conf.LOG_PATH, - log_level=logging.DEBUG if conf.WEBSERVER_DEBUG else logging.INFO) +set_logging(conf.LOG_PATH, log_level=conf.LOG_LEVEL) # Create dummy secrey key so we can use sessions application.config['SECRET_KEY'] = getattr(conf, 'WEBSERVER_SECRET', None) @@ -1,12 +1,11 @@ #! /usr/bin/env python -#-*- coding: utf-8 -*- - +# -*- coding: utf-8 -*- """ Program variables. This file contain the variables used by the application. """ - import os +import logging basedir = os.path.abspath(os.path.dirname(__file__)) PATH = os.path.abspath(".") @@ -29,11 +28,11 @@ DEFAULTS = {"python": "/usr/bin/python3.4", "nb_worker": "100", "default_max_error": "3", "log_path": "pyaggr3g470r.log", - "user_agent": "pyAggr3g470r " \ + "log_level": "info", + "user_agent": "pyAggr3g470r " "(https://bitbucket.org/cedricbonhomme/pyaggr3g470r)", "resolve_article_url": "false", "http_proxy": "", - "debug": "true", "secret": "", "enabled": "false", "email": "", @@ -42,7 +41,8 @@ DEFAULTS = {"python": "/usr/bin/python3.4", "host": "0.0.0.0", "port": "5000", "crawling_method": "classic", -} + "webzine_root": "/tmp", + } if not ON_HEROKU: try: @@ -52,71 +52,64 @@ if not ON_HEROKU: # load the configuration config = confparser.SafeConfigParser(defaults=DEFAULTS) config.read(os.path.join(basedir, "conf/conf.cfg")) - - PLATFORM_URL = config.get('misc', 'platform_url') - ADMIN_EMAIL = config.get('misc', 'admin_email') - RECAPTCHA_PUBLIC_KEY = config.get('misc', 'recaptcha_public_key') - RECAPTCHA_PRIVATE_KEY = config.get('misc', - 'recaptcha_private_key') - LOG_PATH = config.get('misc', 'log_path') - PYTHON = config.get('misc', 'python') - NB_WORKER = config.getint('misc', 'nb_worker') - - WHOOSH_ENABLED = True - - SQLALCHEMY_DATABASE_URI = config.get('database', 'uri') - - HTTP_PROXY = config.get('feedparser', 'http_proxy') - USER_AGENT = config.get('feedparser', 'user_agent') - RESOLVE_ARTICLE_URL = config.getboolean('feedparser', - 'resolve_article_url') - DEFAULT_MAX_ERROR = config.getint('feedparser', - 'default_max_error') - CRAWLING_METHOD = config.get('feedparser', 'crawling_method') - - WEBSERVER_DEBUG = config.getboolean('webserver', 'debug') - WEBSERVER_HOST = config.get('webserver', 'host') - WEBSERVER_PORT = config.getint('webserver', 'port') - WEBSERVER_SECRET = config.get('webserver', 'secret') - - NOTIFICATION_EMAIL = config.get('notification', 'email') - NOTIFICATION_HOST = config.get('notification', 'host') - NOTIFICATION_PORT = config.getint('notification', 'port') - NOTIFICATION_TLS = config.getboolean('notification', 'tls') - NOTIFICATION_SSL = config.getboolean('notification', 'ssl') - NOTIFICATION_USERNAME = config.get('notification', 'username') - NOTIFICATION_PASSWORD = config.get('notification', 'password') - - WEBZINE_ROOT = PATH + "/pyaggr3g470r/var/export/" - else: - PLATFORM_URL = os.environ.get('PLATFORM_URL', - 'https://pyaggr3g470r.herokuapp.com/') - ADMIN_EMAIL = os.environ.get('ADMIN_EMAIL', '') - RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', '') - RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', '') - LOG_PATH = os.environ.get('LOG_PATH', 'pyaggr3g470r.log') - PYTHON = 'python' - - SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL'] - - HTTP_PROXY = "" - USER_AGENT = "Mozilla/5.0 " \ - "(X11; Debian; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0" - RESOLVE_ARTICLE_URL = int(os.environ.get('RESOLVE_ARTICLE_URL', 0)) == 1 - DEFAULT_MAX_ERROR = int(os.environ.get('DEFAULT_MAX_ERROR', 6)) - CRAWLING_METHOD = os.environ.get('CRAWLING_METHOD', DEFAULTS['crawling_method']) - - WEBSERVER_DEBUG = False - WEBSERVER_HOST = '0.0.0.0' - WEBSERVER_PORT = int(os.environ.get('PORT', 5000)) - WEBSERVER_SECRET = os.environ.get('SECRET_KEY', None) - - NOTIFICATION_EMAIL = os.environ.get('NOTIFICATION_EMAIL', '') - POSTMARK_API_KEY = os.environ.get('POSTMARK_API_KEY', '') - - WEBZINE_ROOT = "/tmp/" - + class Config(object): + def get(self, _, name): + return os.environ.get(name.upper(), DEFAULTS.get(name)) + + def getint(self, _, name): + return int(self.get(_, name)) + + def getboolean(self, _, name): + value = self.get(_, name) + if value == 'true': + return True + elif value == 'false': + return False + return None + config = Config() + + +PLATFORM_URL = config.get('misc', 'platform_url') +ADMIN_EMAIL = config.get('misc', 'admin_email') +RECAPTCHA_PUBLIC_KEY = config.get('misc', 'recaptcha_public_key') +RECAPTCHA_PRIVATE_KEY = config.get('misc', + 'recaptcha_private_key') +LOG_PATH = config.get('misc', 'log_path') +PYTHON = config.get('misc', 'python') +NB_WORKER = config.getint('misc', 'nb_worker') + +WHOOSH_ENABLED = True + +SQLALCHEMY_DATABASE_URI = config.get('database', 'uri') + +HTTP_PROXY = config.get('feedparser', 'http_proxy') +USER_AGENT = config.get('feedparser', 'user_agent') +RESOLVE_ARTICLE_URL = config.getboolean('feedparser', + 'resolve_article_url') +DEFAULT_MAX_ERROR = config.getint('feedparser', + 'default_max_error') +CRAWLING_METHOD = config.get('feedparser', 'crawling_method') + +LOG_LEVEL = {'debug': logging.DEBUG, + 'info': logging.INFO, + 'warn': logging.WARN, + 'error': logging.ERROR, + 'fatal': logging.FATAL}[config.get('misc', 'log_level')] + +WEBSERVER_HOST = config.get('webserver', 'host') +WEBSERVER_PORT = config.getint('webserver', 'port') +WEBSERVER_SECRET = config.get('webserver', 'secret') + +NOTIFICATION_EMAIL = config.get('notification', 'email') +NOTIFICATION_HOST = config.get('notification', 'host') +NOTIFICATION_PORT = config.getint('notification', 'port') +NOTIFICATION_TLS = config.getboolean('notification', 'tls') +NOTIFICATION_SSL = config.getboolean('notification', 'ssl') +NOTIFICATION_USERNAME = config.get('notification', 'username') +NOTIFICATION_PASSWORD = config.get('notification', 'password') + +WEBZINE_ROOT = config.get('webserver', 'webzine_root') CSRF_ENABLED = True # slow database query threshold (in seconds) diff --git a/conf/conf.cfg-sample b/conf/conf.cfg-sample index 159af449..c594cd4a 100644 --- a/conf/conf.cfg-sample +++ b/conf/conf.cfg-sample @@ -6,6 +6,7 @@ recaptcha_private_key = log_path = ./pyaggr3g470r/var/pyaggr3g470r.log python = python3.3 nb_worker = 5 +log_level = info [database] uri = postgres://pgsqluser:pgsqlpwd@127.0.0.1:5432/aggregator [feedparser] @@ -15,7 +16,6 @@ resolve_article_url = false default_max_error = 6 crawling_method = classic [webserver] -debug = true host = 0.0.0.0 port = 5000 secret = a secret only you know diff --git a/pyaggr3g470r/controllers/abstract.py b/pyaggr3g470r/controllers/abstract.py index 8f0a8e3f..9a9004af 100644 --- a/pyaggr3g470r/controllers/abstract.py +++ b/pyaggr3g470r/controllers/abstract.py @@ -1,5 +1,6 @@ import logging from bootstrap import db +from sqlalchemy import or_ from werkzeug.exceptions import Forbidden, NotFound logger = logging.getLogger(__name__) @@ -25,13 +26,13 @@ class AbstractController(object): each parameters of the function is treated as an equality unless the name of the parameter ends with either "__gt", "__lt", "__ge", "__le", - "__ne" or "__in". + "__ne", "__in" ir "__like". """ - if self.user_id is not None: - filters[self._user_id_key] = self.user_id db_filters = set() for key, value in filters.items(): - if key.endswith('__gt'): + if key == '__or__': + db_filters.add(or_(*self._to_filters(**value))) + elif key.endswith('__gt'): db_filters.add(getattr(self._db_cls, key[:-4]) > value) elif key.endswith('__lt'): db_filters.add(getattr(self._db_cls, key[:-4]) < value) @@ -43,11 +44,15 @@ class AbstractController(object): db_filters.add(getattr(self._db_cls, key[:-4]) != value) elif key.endswith('__in'): db_filters.add(getattr(self._db_cls, key[:-4]).in_(value)) + elif key.endswith('__like'): + db_filters.add(getattr(self._db_cls, key[:-6]).like(value)) else: db_filters.add(getattr(self._db_cls, key) == value) return db_filters def _get(self, **filters): + if self.user_id is not None: + filters[self._user_id_key] = self.user_id return self._db_cls.query.filter(*self._to_filters(**filters)) def get(self, **filters): diff --git a/pyaggr3g470r/models/feed.py b/pyaggr3g470r/models/feed.py index a36d9573..aff11460 100644 --- a/pyaggr3g470r/models/feed.py +++ b/pyaggr3g470r/models/feed.py @@ -36,7 +36,7 @@ class Feed(db.Model): Represent a feed. """ id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(), default="No title") + title = db.Column(db.String(), default="") description = db.Column(db.String(), default="FR") link = db.Column(db.String()) site_link = db.Column(db.String(), default="") diff --git a/pyaggr3g470r/templates/home.html b/pyaggr3g470r/templates/home.html index 461b6928..a31d65e1 100644 --- a/pyaggr3g470r/templates/home.html +++ b/pyaggr3g470r/templates/home.html @@ -14,7 +14,7 @@ {% if not feed_id %}</b>{% endif %} </a></li> {% for fid, nbunread in unread|dictsort(by='value')|reverse %} - <li class="feed-menu"><a href="{{ gen_url(feed=fid) }}"> + <li class="feed-menu"><a href="{{ gen_url(feed_id=fid) }}"> {% if feed_id == fid %}<b>{% endif %} {% if in_error.get(fid, 0) > 0 %} <span style="background-color: {{ "red" if in_error[fid] > conf.DEFAULT_MAX_ERROR -1 else "orange" }} ;" class="badge pull-right" title="Some errors occured while trying to retrieve that feed.">{{ in_error[fid] }} {{ _("error") }}{% if in_error[fid] > 1 %}s{% endif %}</span> @@ -33,7 +33,7 @@ </span></li> {% endfor %} {% for fid, ftitle in feeds|dictsort(case_sensitive=False, by='value') if not fid in unread %} - <li class="feed-menu"><a href="{{ gen_url(feed=fid) }}"> + <li class="feed-menu"><a href="{{ gen_url(feed_id=fid) }}"> {% if in_error.get(fid, 0) > 0 %} <span style="background-color: {{ "red" if in_error[fid] > conf.DEFAULT_MAX_ERROR - 1 else "orange" }} ;" class="badge pull-right" title="Some errors occured while trying to retrieve that feed.">{{ in_error[fid] }} {{ _("error") }}{% if in_error[fid] > 1 %}s{% endif %}</span> {% endif %} @@ -73,9 +73,9 @@ <thead> <tr> <th></th> - <th><a href="{{ gen_url(sort_='feed') }}">{{ _('Feed') }}</a></th> + <th><a href="{{ gen_url(sort_='-feed' if sort_ == 'feed' else 'feed') }}">{{ _('Feed') }}</a></th> <th>{{ _('Article') }}</th> - <th><a href="{{ gen_url(sort_='date') }}">{{ _('Date') }}</a></th> + <th><a href="{{ gen_url(sort_='-date' if sort_ == 'date' else 'date') }}">{{ _('Date') }}</a></th> </tr> </thead> <tbody> diff --git a/pyaggr3g470r/templates/layout.html b/pyaggr3g470r/templates/layout.html index 484bbdc7..a54125ea 100644 --- a/pyaggr3g470r/templates/layout.html +++ b/pyaggr3g470r/templates/layout.html @@ -86,6 +86,7 @@ <li><a href="{{ url_for("profile") }}"><span class="glyphicon glyphicon-user"></span> {{ _('Profile') }}</a></li> <li><a href="{{ url_for("management") }}"><span class="glyphicon glyphicon-cog"></span> {{ _('Your data') }}</a></li> {% if g.user.is_admin() %} + <li><a href="{{ url_for("about") }}"><span class="glyphicon glyphicon-question-sign"></span> {{ _('About') }}</a></li> <li role="presentation" class="divider"></li> <li><a href="{{ url_for("dashboard") }}"><span class="glyphicon glyphicon-dashboard"></span> {{ _('Dashboard') }}</a></li> <li role="presentation" class="divider"></li> @@ -101,9 +102,16 @@ </a> <ul class="dropdown-menu"> <li> - <form class="navbar-form" method=get action="/search" role="search"> + <form class="navbar-form" method=get action="{{ url_for("search") }}" role="search"> <div class="input-group"> - <input type="text" class="form-control" name="query" placeholder="Search"> + {% if filter_ %}<input type="hidden" name="filter_" value="{{ filter_ }}" />{% endif %} + {% if limit %}<input type="hidden" name="limit" value="{{ limit }}" />{% endif %} + {% if feed_id %}<input type="hidden" name="feed_id" value="{{ feed_id }}" />{% endif %} + <label for="search_title">{{ _("Title") }}</label> + <input type="checkbox" name="search_title" {% if search_title == 'on' or not (search_title == 'on' or search_content == 'on') %}checked{%endif%}/> + <label for="search_content">{{ _("Content") }}</label> + <input type="checkbox" name="search_content" {% if search_content == 'on' %}checked{%endif%}/> + <input type="text" class="form-control" name="query" placeholder="{{ _("Search") }}" {% if search_query %} value="{{ search_query }}"{% endif %} /> </div> </form> </li> diff --git a/pyaggr3g470r/templates/search.html b/pyaggr3g470r/templates/search.html deleted file mode 100644 index d3a1558d..00000000 --- a/pyaggr3g470r/templates/search.html +++ /dev/null @@ -1,48 +0,0 @@ - {% extends "layout.html" %} -{% block content %} -<div class="container"> - {% if feeds|count == 0 %} - <div class="page-header"> - <h1>No results</h1> - </div> - {% else %} - <div class="page-header"> - <h1>{{ nb_articles }} {% if nb_articles !=1 %} results {% else %} result {% endif %} for <i>{{ query }}</i></h1> - </div> - {% for feed in feeds|sort(attribute="title") %} - <div class="row"> - <div class="col-md-6 col-md-offset-3"> - <h1>{{ feed.title|safe }}</h1> - <a href="/articles/{{ feed.id }}/100"><i class="glyphicon glyphicon-th-list" title="More articles"></i></a> - <a href="/feed/{{ feed.id }}"><i class="glyphicon glyphicon-info-sign" title="Details"></i></a> - <a href="/edit_feed/{{ feed.id }}"><i class="glyphicon glyphicon-edit" title="Edit this feed"></i></a> - </div> - </div> - {% for number in range(0, feed.articles|length-(feed.articles|length % 3), 3) %} - <div class="row"> - {% for n in range(number, number+3) %} - <div class="col-xs-6 col-sm-4 col-md-4"> - {% if feed.articles[n].readed %}<h3>{% else %}<h1>{% endif %} - <a href="/article/{{ feed.articles[n].id }}">{{ feed.articles[n].title|safe }}</a> - {% if feed.articles[n].readed %}</h3>{% else %}</h1>{% endif %} - <h6>{{ feed.articles[n].date }}</h6> - </div> - {% endfor %} - </div> - {% endfor %} - {% if feed.articles|length % 3 != 0 %} - <div class="row"> - {% for n in range(feed.articles|length-(feed.articles|length % 3), feed.articles|length) %} - <div class="col-xs-6 col-sm-4 col-md-4"> - {% if feed.articles[n].readed %}<h3>{% else %}<h1>{% endif %} - <a href="/article/{{ feed.articles[n].id }}">{{ feed.articles[n].title|safe }}</a> - {% if feed.articles[n].readed %}</h3>{% else %}</h1>{% endif %} - <h6>{{ feed.articles[n].date }}</h6> - </div> - {% endfor %} - </div> - {% endif %} - {% endfor %} - {% endif %} -</div><!-- /.container --> -{% endblock %} diff --git a/pyaggr3g470r/views/views.py b/pyaggr3g470r/views/views.py index d0c06da0..3b018c6d 100644 --- a/pyaggr3g470r/views/views.py +++ b/pyaggr3g470r/views/views.py @@ -235,13 +235,10 @@ def signup(): return render_template('signup.html', form=form) -@app.route('/') -@login_required -def home(favorites=False): - """ - Home page for connected users. Displays by default unread articles. - """ - head_title = gettext('Favorites') if favorites else '' + +def render_home(filters=None, head_title='', page_to_render='home', **kwargs): + if filters is None: + filters = {} feed_contr = FeedController(g.user.id) arti_contr = ArticleController(g.user.id) feeds = {feed.id: feed.title for feed in feed_contr.read()} @@ -250,48 +247,83 @@ def home(favorites=False): in_error = {feed.id: feed.error_count for feed in feed_contr.read(error_count__gt=2)} - filter_ = request.args.get('filter_', 'all' if favorites else 'unread') + filter_ = request.args.get('filter_', + 'unread' if page_to_render == 'home' else 'all') sort_ = request.args.get('sort_', 'date') - feed_id = int(request.args.get('feed', 0)) + feed_id = int(request.args.get('feed_id', 0)) limit = request.args.get('limit', 1000) - filters = {} - if favorites: - filters['like'] = True if filter_ != 'all': filters['readed'] = filter_ == 'read' if feed_id: filters['feed_id'] = feed_id - if head_title: - head_title += ' - ' - head_title += feed_contr.get(id=feed_id).title + head_title = "%s%s" % (feed_contr.get(id=feed_id).title, + (' - %s' % head_title) if head_title else '') - articles = arti_contr.read(**filters).order_by(Article.date.desc()) + sort_param = {"feed": Article.title.desc(), + "date": Article.date.desc(), + "-feed": Article.title.asc(), + "-date": Article.date.asc(), + }.get(sort_, Article.date.desc()) + + articles = arti_contr.read(**filters).order_by(sort_param) if limit != 'all': limit = int(limit) articles = articles.limit(limit) - def gen_url(filter_=filter_, sort_=sort_, limit=limit, feed=feed_id): - return url_for('favorites' if favorites else 'home', - filter_=filter_, sort_=sort_, limit=limit, feed=feed) + def gen_url(filter_=filter_, sort_=sort_, limit=limit, feed_id=feed_id, + **kwargs): + if page_to_render == 'search': + kwargs['query'] = request.args.get('query', '') + kwargs['search_title'] = request.args.get('search_title', 'on') + kwargs['search_content'] = request.args.get('searc_content', 'off') + return url_for(page_to_render, filter_=filter_, sort_=sort_, + limit=limit, feed_id=feed_id, **kwargs) articles = list(articles) - if not articles and not favorites and feed_id: + if (page_to_render == 'home' and feed_id or page_to_render == 'search') \ + and filter_ != 'all' and not articles: return redirect(gen_url(filter_='all')) - if sort_ == "feed": - articles.sort(key=lambda article: article.source.title) - return render_template('home.html', gen_url=gen_url, feed_id=feed_id, filter_=filter_, limit=limit, feeds=feeds, unread=unread, articles=articles, in_error=in_error, - head_title=head_title, favorites=favorites) + head_title=head_title, sort_=sort_, **kwargs) + + +@app.route('/') +@login_required +def home(): + "Home page for connected users. Displays by default unread articles." + return render_home() @app.route('/favorites') @login_required def favorites(): - return home(favorites=True) + return render_home({'like': True}, gettext('Favorites'), 'favorites') + + +@app.route('/search', methods=['GET']) +@login_required +def search(): + "Search articles corresponding to the query." + if 'query' not in request.args: + flash(gettext("No text to search were provided."), "warning") + return render_home() + query = request.args['query'] + filters = {} + search_title = request.args.get('search_title') + search_content = request.args.get('search_content') + if search_title == 'on': + filters['title__like'] = "%%%s%%" % query + if search_content == 'on': + filters['content__like'] = "%%%s%%" % query + if len(filters) > 1: + filters = {"__or__": filters} + return render_home(filters, "%s %s" % (gettext('Search:'), query), + 'search', search_query=query, search_title=search_title, + search_content=search_content) @app.route('/fetch', methods=['GET']) @@ -472,38 +504,6 @@ def export_opml(): response.headers['Content-Disposition'] = 'attachment; filename=feeds.opml' return response -@app.route('/search', methods=['GET']) -@login_required -def search(): - """ - Search articles corresponding to the query. - """ - if conf.ON_HEROKU: - flash(gettext("Full text search is not yet implemented for Heroku."), "warning") - return redirect(url_for('home')) - user = User.query.filter(User.id == g.user.id).first() - - search_result, result = [], [] - nb_articles = 0 - - query = request.args.get('query', None) - if query is not None: - try: - search_result, nb_articles = fastsearch.search(user.id, query) - except Exception as e: - flash(gettext('An error occured') + ' (%s).' % e, 'danger') - light_feed = namedtuple('Feed', ['id', 'title', 'articles'], verbose=False, rename=False) - for feed_id in search_result: - for feed in user.feeds: - if feed.id == feed_id: - articles = [] - for article_id in search_result[feed_id]: - current_article = Article.query.filter(Article.user_id == g.user.id, Article.id == article_id).first() - articles.append(current_article) - articles = sorted(articles, key=lambda t: t.date, reverse=True) - result.append(light_feed(feed.id, feed.title, articles)) - break - return render_template('search.html', feeds=result, nb_articles=nb_articles, query=query) @app.route('/management', methods=['GET', 'POST']) @login_required diff --git a/runserver.py b/runserver.py index a80b0c39..5f20ddd4 100755 --- a/runserver.py +++ b/runserver.py @@ -18,6 +18,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import logging import calendar from bootstrap import conf, application, populate_g from flask.ext.babel import Babel @@ -55,4 +56,4 @@ with application.app_context(): if __name__ == '__main__': application.run(host=conf.WEBSERVER_HOST, port=conf.WEBSERVER_PORT, - debug=conf.WEBSERVER_DEBUG) + debug=conf.LOG_LEVEL <= logging.DEBUG) |