Skip to content

Commit

Permalink
Move utility/setup functions in frontend to separate modules
Browse files Browse the repository at this point in the history
The modules currently have the following responsibilities:
- `__init__` - only imports the other modules (including plugins) to
  ensure that all routes, filters etc. have been registered
- `base` - initialization of the app and db, definition of the session
  and basic globals
- `filters` - jinja2 filters for templates
- `model_additions` - properties monkey-patched on top of models, such
  as state icons
- `template_functions` - utility functions primarily aimed to be used
  within templates
- `util` - utility functions for flash messages and ordering
- `tabs` - definition of tab menu
- `forms` - form definitions and utilities
- `auth` - authentication
- `api` - API
- `views` - the actual controller part, could be further split
  • Loading branch information
msimacek committed Dec 21, 2017
1 parent 5f32714 commit 43d335b
Show file tree
Hide file tree
Showing 17 changed files with 554 additions and 409 deletions.
3 changes: 0 additions & 3 deletions aux/debug_frontend.py
Expand Up @@ -26,9 +26,6 @@
load_config(['config.cfg.template', 'aux/test-config.cfg'])

from koschei.frontend import app as application
import koschei.frontend.api
import koschei.frontend.views
import koschei.frontend.auth

logger = logging.getLogger("koschei.sql")
logger.setLevel(logging.DEBUG)
Expand Down
3 changes: 0 additions & 3 deletions koschei.wsgi
Expand Up @@ -3,6 +3,3 @@ from koschei.config import load_config
load_config(['/usr/share/koschei/config.cfg', '/etc/koschei/config-frontend.cfg'])

from koschei.frontend import app as application
import koschei.frontend.api
import koschei.frontend.views
import koschei.frontend.auth
137 changes: 9 additions & 128 deletions koschei/frontend/__init__.py
Expand Up @@ -18,134 +18,15 @@

from __future__ import print_function, absolute_import

import logging
import humanize
from koschei.frontend.base import app

from functools import wraps
import koschei.frontend.auth
import koschei.frontend.filters
import koschei.frontend.model_additions
import koschei.frontend.template_functions
import koschei.frontend.api
import koschei.frontend.views

from flask import Flask, abort, request, g, url_for, flash
from flask_sqlalchemy import BaseQuery, Pagination
from jinja2 import Markup
from sqlalchemy.orm import scoped_session, sessionmaker
from koschei import plugin

from koschei.session import KoscheiSession
from koschei.config import get_config
from koschei.db import Query, get_engine
from koschei.models import LogEntry

dirs = get_config('directories')
app = Flask('koschei', template_folder=dirs['templates'],
static_folder=dirs['static_folder'],
static_url_path=dirs['static_url'])
app.config.update(get_config('flask'))

frontend_config = get_config('frontend')


def flash_ack(message):
"""Send flask flash with message about operation that was successfully
completed."""
flash(message, 'success')


def flash_nak(message):
"""Send flask flash with with message about operation that was not
completed due to an error."""
flash(message, 'danger')


def flash_info(message):
"""Send flask flash with informational message."""
flash(message, 'info')


class FrontendQuery(Query, BaseQuery):
# pylint:disable=arguments-differ
def paginate(self, items_per_page):
try:
page = int(request.args.get('page', 1))
except ValueError:
abort(400)
if page < 1:
abort(404)
items = self.limit(items_per_page)\
.offset((page - 1) * items_per_page).all()
if not items and page != 1:
abort(404)
if page == 1 and len(items) < items_per_page:
total = len(items)
else:
total = self.order_by(None).count()
return Pagination(self, page, items_per_page, total, items)

db = scoped_session(sessionmaker(autocommit=False, bind=get_engine(),
query_cls=FrontendQuery))


class KoscheiFrontendSession(KoscheiSession):
db = db
log = logging.getLogger('koschei.frontend')

def log_user_action(self, message, **kwargs):
self.db.add(
LogEntry(environment='frontend', user=g.user, message=message, **kwargs),
)


session = KoscheiFrontendSession()


tabs = []


class Tab(object):
def __init__(self, name, order=0, requires_user=False):
self.name = name
self.order = order
self.requires_user = requires_user
self.master_endpoint = None
for i, tab in enumerate(tabs):
if tab.order > order:
tabs.insert(i, self)
break
else:
tabs.append(self)

def __call__(self, fn):
@wraps(fn)
def decorated(*args, **kwargs):
g.current_tab = self
return fn(*args, **kwargs)
return decorated

def master(self, fn):
self.master_endpoint = fn
return self(fn)

@property
def url(self):
name = self.master_endpoint.__name__
if self.requires_user:
return url_for(name, username=g.user.name)
return url_for(name)

@staticmethod
def get_tabs():
return [t for t in tabs if t.master_endpoint and not t.requires_user]

@staticmethod
def get_user_tabs():
return [t for t in tabs if t.master_endpoint and t.requires_user]


app.jinja_env.globals['get_tabs'] = Tab.get_tabs
app.jinja_env.globals['get_user_tabs'] = Tab.get_user_tabs

app.add_template_filter(humanize.intcomma, 'intcomma')
app.add_template_filter(humanize.naturaltime, 'naturaltime')
app.add_template_filter(humanize.naturaldelta, 'naturaldelta')


@app.template_filter()
def percentage(val):
return format(val * 10000, '.4f') + Markup('&nbsp;&#8241;')
plugin.load_plugins('frontend')
3 changes: 2 additions & 1 deletion koschei/frontend/api.py
Expand Up @@ -18,7 +18,8 @@

from flask import request, Response
from sqlalchemy.sql import literal_column, case
from koschei.frontend import app, db

from koschei.frontend.base import db, app
from koschei.models import Package, Collection, Build


Expand Down
8 changes: 5 additions & 3 deletions koschei/frontend/auth.py
Expand Up @@ -19,13 +19,15 @@

from __future__ import print_function, absolute_import

import re
import functools
import re

from flask import abort, request, session, redirect, url_for, g

from koschei.config import get_config
from koschei.frontend import app, db, flash_ack, flash_info
import koschei.models as m
from koschei.config import get_config
from koschei.frontend.base import app, db
from koschei.frontend.util import flash_info, flash_ack

bypass_login = get_config('bypass_login', None)
user_re = get_config('frontend.auth.user_re')
Expand Down
135 changes: 135 additions & 0 deletions koschei/frontend/base.py
@@ -0,0 +1,135 @@
# Copyright (C) 2014-2016 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Author: Michael Simacek <msimacek@redhat.com>

from __future__ import print_function, absolute_import

import logging

from flask import Flask, abort, request, g
from flask_sqlalchemy import BaseQuery, Pagination
from sqlalchemy.orm import scoped_session, sessionmaker

from koschei.config import get_config
from koschei.db import Query, get_engine
from koschei.models import LogEntry, Collection, Package, Build, ResolutionChange
from koschei.session import KoscheiSession

dirs = get_config('directories')
app = Flask('koschei', template_folder=dirs['templates'],
static_folder=dirs['static_folder'],
static_url_path=dirs['static_url'])
app.config.update(get_config('flask'))

frontend_config = get_config('frontend')


class FrontendQuery(Query, BaseQuery):
# pylint:disable=arguments-differ
def paginate(self, items_per_page):
try:
page = int(request.args.get('page', 1))
except ValueError:
abort(400)
if page < 1:
abort(404)
items = self.limit(items_per_page)\
.offset((page - 1) * items_per_page).all()
if not items and page != 1:
abort(404)
if page == 1 and len(items) < items_per_page:
total = len(items)
else:
total = self.order_by(None).count()
return Pagination(self, page, items_per_page, total, items)

db = scoped_session(sessionmaker(autocommit=False, bind=get_engine(),
query_cls=FrontendQuery))


class KoscheiFrontendSession(KoscheiSession):
db = db
log = logging.getLogger('koschei.frontend')

def log_user_action(self, message, **kwargs):
self.db.add(
LogEntry(environment='frontend', user=g.user, message=message, **kwargs),
)


session = KoscheiFrontendSession()


@app.teardown_appcontext
def shutdown_session(exception=None):
db.remove()


@app.context_processor
def inject_fedmenu():
if 'fedmenu_url' in frontend_config:
return {
'fedmenu_url': frontend_config['fedmenu_url'],
'fedmenu_data_url': frontend_config['fedmenu_data_url'],
}
return {}


@app.before_request
def get_collections():
if request.endpoint == 'static':
return
collection_name = request.args.get('collection')
g.collections = db.query(Collection)\
.order_by(Collection.order.desc(), Collection.name.desc())\
.all()
for collection in g.collections:
db.expunge(collection)
if not g.collections:
abort(500, "No collections setup")
g.collections_by_name = {c.name: c for c in g.collections}
g.collections_by_id = {c.id: c for c in g.collections}
g.current_collections = []
if collection_name:
try:
for component in collection_name.split(','):
g.current_collections.append(g.collections_by_name[component])
except KeyError:
abort(404, "Collection not found")
else:
g.current_collections = g.collections


def secondary_koji_url(collection):
if collection.secondary_mode:
return get_config('secondary_koji_config.weburl')
return get_config('koji_config.weburl')


app.jinja_env.globals.update(
# configuration variables
koschei_version=get_config('version'),
primary_koji_url=get_config('koji_config.weburl'),
secondary_koji_url=secondary_koji_url,
fedora_assets_url=frontend_config['fedora_assets_url'],
# builtin python functions
inext=next, iter=iter, min=min, max=max,
# model classes
Package=Package,
Build=Build,
ResolutionChange=ResolutionChange,
)
43 changes: 43 additions & 0 deletions koschei/frontend/filters.py
@@ -0,0 +1,43 @@
# Copyright (C) 2014-2017 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Author: Michael Simacek <msimacek@redhat.com>

from datetime import datetime

import humanize
from jinja2 import Markup

from koschei.frontend.base import app

app.add_template_filter(humanize.intcomma, 'intcomma')
app.add_template_filter(humanize.naturaltime, 'naturaltime')
app.add_template_filter(humanize.naturaldelta, 'naturaldelta')


@app.template_filter()
def percentage(val):
return format(val * 10000, '.4f') + Markup('&nbsp;&#8241;')


@app.template_filter('date')
def date_filter(date):
return date.strftime("%F %T") if date else ''


@app.template_filter()
def epoch(dt):
return int((dt - datetime.fromtimestamp(0)).total_seconds())
4 changes: 1 addition & 3 deletions koschei/frontend/forms.py
Expand Up @@ -22,16 +22,14 @@
import re

from flask_wtf import Form

from wtforms import (
StringField, TextAreaField, IntegerField, BooleanField,
)
from wtforms.validators import Regexp, ValidationError
from wtforms.widgets import HTMLString, HiddenInput

from koschei.config import get_koji_config

from koschei.frontend import flash_nak
from koschei.frontend.util import flash_nak


class CheckBoxField(BooleanField):
Expand Down

0 comments on commit 43d335b

Please sign in to comment.