import logging
import collections
from deform import Form, ValidationFailure
from pyramid.interfaces import IRoutesMapper
from pyramid.settings import asbool
from sqlalchemy import String, LargeBinary
from sqlalchemy.sql.expression import desc, or_
from sqlalchemy.sql.functions import sum, count
from sqlalchemy.orm import joinedload
from sqlalchemy.orm.attributes import InstrumentedAttribute
from almir.meta import DBSession, get_database_size
from almir.models import Job, Client, Log, Media, Storage, Pool, Status, File, Path, Filename
from almir.forms import JobSchema, MediaSchema, LogSchema
from almir.lib.console_commands import CONSOLE_COMMANDS
from almir.lib.bconsole import BConsole, DirectorNotRunning
from almir.lib.utils import render_rst_section, get_jinja_macro
log = logging.getLogger(__name__)
[docs]def dashboard(request):
dbsession = DBSession()
jobs = Job.get_last()
running_jobs = Job.get_running()
upcoming_jobs = Job.get_upcoming()
try:
director_version = '<span title="Connected to" class="label label-success">%s</span>' % BConsole().get_version()
except DirectorNotRunning:
director_version = '<span class="label label-important">Director not running!</span>'
# statistics
num_clients = dbsession.query(count(Client.clientid)).scalar()
num_jobs = dbsession.query(count(Job.jobid)).scalar()
num_volumes = dbsession.query(count(Media.mediaid)).scalar()
sum_volumes = Media.format_byte_size(dbsession.query(sum(Media.volbytes)).scalar() or 0)
database_size = get_database_size(DBSession.bind)
return locals()
[docs]def about(request):
changelog = render_rst_section('changelog.rst')
about = render_rst_section('about.rst')
return locals()
[docs]def console(request):
command_array = ','.join(['"%s"' % name for name in CONSOLE_COMMANDS.keys()])
console_commands = CONSOLE_COMMANDS
return locals()
# restful generic view
[docs]class MixinView(object):
model = None
schema = None
def __init__(self, request):
self.request = request
self.context = {
'appstruct': {},
}
[docs] def list(self):
self.context['form'] = self.get_form()
self.context['objects'] = self.model.get_list(appstruct=self.context['appstruct'])
return self.context
[docs] def detail(self):
id_ = self.request.matchdict['id']
self.context['object'] = self.model.get_one(id_)
return self.context
[docs]class JobView(MixinView):
model = Job
schema = JobSchema
[docs] def list(self):
# TODO: cache this
self.context['status_values'] = [('', '---')] + Status.get_values()
return super(JobView, self).list()
[docs] def detail(self):
id_ = self.request.matchdict['id']
self.context['files'] = File.query.filter(File.jobid == int(id_))\
.options(joinedload('path'), joinedload('filename'))\
.join('path').join('filename')
return super(JobView, self).detail()
[docs]class ClientView(MixinView):
model = Client
[docs] def detail(self):
# TODO: get jobs as join/subquery on client
super(ClientView, self).detail()
id_ = self.request.matchdict['id']
jobs = Job.query.filter(Job.clientid == int(id_))
self.context['jobs'] = jobs.options(joinedload(Job.status))\
.order_by(desc(Job.schedtime))\
.limit(50) # TODO: dynamic datatables
self.context['job_statistics'] = jobs.with_entities(count().label('num_jobs'), sum(Job.jobbytes).label('total_size_backups')).first(),
self.context['last_successful_job'] = jobs.filter(Job.jobstatus == 'T').order_by(desc(Job.starttime)).first(),
return self.context
[docs]class StorageView(MixinView):
model = Storage
[docs]class VolumeView(MixinView):
model = Media
schema = MediaSchema
[docs] def list(self):
# UPSTREAM: we need to convert id to string, look at https://github.com/Pylons/deform/issues/81
# TODO: cache
self.context['storage_values'] = [('', '---')] + map(lambda x: (str(x[0]), x[1]), Storage.get_values())
self.context['pool_values'] = [('', '---')] + map(lambda x: (str(x[0]), x[1]), Pool.get_values())
return super(VolumeView, self).list()
[docs]class PoolView(MixinView):
model = Pool
[docs]class LogView(MixinView):
model = Log
schema = LogSchema
bconsole_session = None
command_cache = collections.deque(maxlen=10)
[docs]def httpexception(context, request):
request.response.status_int = context.code
return {'context': context}
[docs]def datatables(request):
"""Implements server side interface for datatables.
For field reference look at http://www.datatables.net/usage/server-side
"""
link = get_jinja_macro('link')
q = request.GET
iDisplayStart = int(q.get('iDisplayStart'))
iDisplayLength = int(q.get('iDisplayLength'))
iColumns = int(q.get('iColumns'))
sColumns = q.get('sColumns').split(',')
sSearch = q.get('sSearch')
#bRegex = q.get('bRegex')
#iSortingCols = int(q.get('iSortingCols'))
sEcho = int(q.get('sEcho'))
# get view from referrer url
for route in request.registry.getUtility(IRoutesMapper).routelist:
match = route.match(q.get('referrer'))
if match is not None:
break
for intro in request.registry.introspector.get_category('views'):
if intro['introspectable']['route_name'] == route.name:
break
view = intro['introspectable']['callable'](request)
# TODO: views will do form filtering, meaning total_records will give false information
if intro['introspectable']['attr'] == 'list':
query = view.list()[q.get('context', 'objects')]
else:
request.matchdict = match
query = view.detail()[q.get('context', 'object')]
# get model from query entity
model = query._entities[0].mapper.class_
total_records = query.count()
search_filter = []
for i in range(iColumns):
mDataProp = q.get('mDataProp_%d' % i)
try:
field = sColumns[i] or mDataProp
except IndexError:
# no information about columns
field = mDataProp
# handle search
bSearchable = asbool(q.get('bSearchable_%d' % i))
if bSearchable:
# TODO: handle regex
# TODO: column specific search: col_sSearch = q.get('sSearch_%d' % i)
# TODO refactor to models to provide generic search and edge cases for specific models
if sSearch:
if field == 'path.path;filename.name':
search_filter.append((Path.path + Filename.name).ilike('%' + sSearch + '%'))
else:
column = getattr(model, field)
if isinstance(column.property.columns[0].type, (String, LargeBinary)):
search_filter.append(column.ilike('%' + sSearch + '%'))
# TODO: support more type searches
# handle sorting
iSortCol = q.get('iSortCol_%d' % i)
bSortable = asbool(q.get('bSortable_%d' % i))
if iSortCol and bSortable:
# TODO refactor to models to provide generic ordering and edge cases for specific models
for prop in field.split(';'):
# get colum for sorting
order_column = model
for attr in prop.split('.'):
if isinstance(order_column, InstrumentedAttribute):
# handle relations
order_column = order_column.property.mapper.class_
order_column = getattr(order_column, attr)
# sort
if q.get('sSortDir_%d' % i) == 'desc':
query = query.order_by(desc(order_column))
else:
query = query.order_by(order_column)
query = query.filter(or_(*search_filter))
total_display_records = query.count()
aaData = []
for result in query[iDisplayStart:iDisplayStart + iDisplayLength]:
obj = {}
for i in range(iColumns):
mDataProp = q.get('mDataProp_%d' % i)
options = getattr(result, 'render_%s' % mDataProp)(request) or {}
obj[mDataProp] = link(options)
if options.get('cssclass'):
obj['DT_RowClass'] = options['cssclass']
aaData.append(obj)
return {
'sEcho': sEcho,
'iTotalDisplayRecords': total_display_records,
'iTotalRecords': total_records,
'aaData': aaData,
}