Initial commit
This commit is contained in:
commit
06c495c679
44 changed files with 2185 additions and 0 deletions
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
*.pyc
|
||||
*.pyo
|
||||
*.swp
|
||||
*__pycache__*
|
||||
.ropeproject/
|
||||
conf/settings.py
|
||||
*.sqlite
|
||||
*.sass-cache
|
||||
*build/
|
||||
*dist/
|
||||
*.egg*
|
||||
/env
|
||||
*.tar.gz
|
||||
*.rpm
|
||||
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
recursive-include testdays/templates *
|
||||
recursive-include testdays/static *
|
||||
87
Makefile
Normal file
87
Makefile
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#
|
||||
# Copyright 2013, 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.
|
||||
#
|
||||
|
||||
.PHONY: test test-ci pylint pep8 docs clean virtualenv
|
||||
|
||||
# general variables
|
||||
VENV=test_env
|
||||
SRC=testdays
|
||||
|
||||
# Variables used for packaging
|
||||
SPECFILE=$(SRC).spec
|
||||
BASEARCH:=$(shell uname -i)
|
||||
DIST:=$(shell rpm --eval '%{dist}')
|
||||
VERSION:=$(shell rpmspec -q --queryformat="%{VERSION}\n" $(SPECFILE) | uniq)
|
||||
RELEASE:=$(subst $(DIST),,$(shell rpmspec -q --queryformat="%{RELEASE}\n" $(SPECFILE) | uniq))
|
||||
NVR:=$(SRC)-$(VERSION)-$(RELEASE)
|
||||
GITBRANCH:=$(shell git rev-parse --abbrev-ref HEAD)
|
||||
TARGETDIST:=fc20
|
||||
BUILDTARGET=fedora-20-x86_64
|
||||
|
||||
test: $(VENV)
|
||||
sh -c "TEST='true' . $(VENV)/bin/activate; py.test --cov $(SRC) testing/; deactivate"
|
||||
|
||||
test-ci: $(VENV)
|
||||
sh -c "TEST='true' . $(VENV)/bin/activate; py.test --cov-report xml --cov $(SRC) testing/; deactivate"
|
||||
|
||||
pylint:
|
||||
pylint -f parseable $(SRC) | tee pylint.out
|
||||
|
||||
pep8:
|
||||
pep8 $(SRC)/*.py $(SRC)/*/*.py | tee pep8.out
|
||||
|
||||
ci: test-ci pylint pep8
|
||||
|
||||
docs:
|
||||
sphinx-build -b html -d docs/_build/doctrees docs/source docs/_build/html
|
||||
|
||||
clean:
|
||||
rm -rf dist
|
||||
rm -rf testdays.egg-info
|
||||
rm -rf build
|
||||
rm -f pep8.out
|
||||
rm -f pylint.out
|
||||
|
||||
archive: $(SRC)-$(VERSION).tar.gz
|
||||
|
||||
$(SRC)-$(VERSION).tar.gz:
|
||||
git archive $(GITBRANCH) --prefix=$(SRC)-$(VERSION)/ | gzip -c9 > $@
|
||||
|
||||
mocksrpm: archive
|
||||
mock -r $(BUILDTARGET) --buildsrpm --spec $(SPECFILE) --sources .
|
||||
cp /var/lib/mock/$(BUILDTARGET)/result/$(NVR).$(TARGETDIST).src.rpm .
|
||||
|
||||
mockbuild: mocksrpm
|
||||
mock -r $(BUILDTARGET) --no-clean --rebuild $(NVR).$(TARGETDIST).src.rpm
|
||||
cp /var/lib/mock/$(BUILDTARGET)/result/$(NVR).$(TARGETDIST).noarch.rpm .
|
||||
|
||||
#kojibuild: mocksrpm
|
||||
# koji build --scratch dist-6E-epel-testing-candidate $(NVR).$(TARGETDIST).src.rpm
|
||||
|
||||
nvr:
|
||||
@echo $(NVR)
|
||||
|
||||
cleanvenv:
|
||||
rm -rf $(VENV)
|
||||
|
||||
virtualenv: $(VENV)
|
||||
|
||||
$(VENV):
|
||||
virtualenv --distribute --system-site-packages $(VENV)
|
||||
sh -c ". $(VENV)/bin/activate; pip install --force-reinstall -r requirements.txt; deactivate"
|
||||
|
||||
1
README.md
Normal file
1
README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
testdays
|
||||
74
alembic.ini
Normal file
74
alembic.ini
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = alembic
|
||||
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
#truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; this defaults
|
||||
# to alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path
|
||||
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
[alembic-packaged]
|
||||
# path to migration scripts on a packaged install
|
||||
script_location = /usr/share/testdays/alembic
|
||||
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
79
alembic/env.py
Normal file
79
alembic/env.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
from __future__ import with_statement
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
|
||||
# add '.' to the pythonpath to support migration inside development env
|
||||
import sys
|
||||
sys.path.append('.')
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
from testdays import db
|
||||
target_metadata = db.metadata
|
||||
#target_metadata = None
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(url=url)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
alembic_config = config.get_section(config.config_ini_section)
|
||||
from testdays import app
|
||||
alembic_config['sqlalchemy.url'] = app.config['SQLALCHEMY_DATABASE_URI']
|
||||
|
||||
engine = engine_from_config(
|
||||
alembic_config,
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
|
||||
connection = engine.connect()
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata
|
||||
)
|
||||
|
||||
try:
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
24
alembic/script.py.mako
Normal file
24
alembic/script.py.mako
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
||||
28
alembic/versions/15f5eeb9f635_initial_revision.py
Normal file
28
alembic/versions/15f5eeb9f635_initial_revision.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
"""Initial revision
|
||||
|
||||
Revision ID: 15f5eeb9f635
|
||||
Revises:
|
||||
Create Date: 2015-04-29 13:43:16.481727
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '15f5eeb9f635'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
### end Alembic commands ###
|
||||
34
alembic/versions/293b84dc3f9e_add_the_user_table.py
Normal file
34
alembic/versions/293b84dc3f9e_add_the_user_table.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
"""Add the User table
|
||||
|
||||
Revision ID: 293b84dc3f9e
|
||||
Revises: 15f5eeb9f635
|
||||
Create Date: 2015-04-29 13:44:16.481727
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '293b84dc3f9e'
|
||||
down_revision = '15f5eeb9f635'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('user',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.String(length=80), nullable=True),
|
||||
sa.Column('pw_hash', sa.String(length=120), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('username')
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('user')
|
||||
### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
"""Added Testday-related tables
|
||||
|
||||
Revision ID: 3e508b27e5b6
|
||||
Revises: 293b84dc3f9e
|
||||
Create Date: 2015-05-07 11:04:01.305712
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3e508b27e5b6'
|
||||
down_revision = '293b84dc3f9e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('event',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.Text(), nullable=True),
|
||||
sa.Column('metadata_url', sa.Text(), nullable=True),
|
||||
sa.Column('testday_url', sa.Text(), nullable=True),
|
||||
sa.Column('resultsdb_job_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('category',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('event_id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.Text(), nullable=True),
|
||||
sa.Column('col_1_name', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['event_id'], ['event.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('category_fk_event_id', 'category', ['event_id'], unique=False)
|
||||
op.create_table('testcase',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('category_id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.Text(), nullable=True),
|
||||
sa.Column('type', sa.Enum('tc', 'txt', name='testcase_type'), nullable=True),
|
||||
sa.Column('url', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['category_id'], ['category.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('testcase_fk_category_id', 'testcase', ['category_id'], unique=False)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('testcase_fk_category_id', table_name='testcase')
|
||||
op.drop_table('testcase')
|
||||
op.drop_index('category_fk_event_id', table_name='category')
|
||||
op.drop_table('category')
|
||||
op.drop_table('event')
|
||||
### end Alembic commands ###
|
||||
10
conf/settings.py.example
Normal file
10
conf/settings.py.example
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# you can use this as a template
|
||||
SECRET_KEY = 'not-really-a-secret'
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://dbuser:dbpassword@dbhost:dbport/dbname'
|
||||
SHOW_DB_URI = False
|
||||
PRODUCTION = True
|
||||
|
||||
FILE_LOGGING = False
|
||||
SYSLOG_LOGGING = False
|
||||
STREAM_LOGGING = True
|
||||
LOGFILE = '/var/log/testdays/testdays.log'
|
||||
33
conf/testdays.conf
Normal file
33
conf/testdays.conf
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
WSGIDaemonProcess testdays user=apache group=apache threads=5
|
||||
WSGIScriptAlias /testdays /usr/share/testdays/testdays.wsgi
|
||||
WSGISocketPrefix run/wsgi
|
||||
|
||||
# this isn't the best way to force SSL but it works for now
|
||||
#RewriteEngine On
|
||||
#RewriteCond %{HTTPS} !=on
|
||||
#RewriteRule ^/testdays/admin/?(.*) https://%{SERVER_NAME}/$1 [R,L]
|
||||
|
||||
<Directory /usr/share/testdays>
|
||||
WSGIProcessGroup testdays
|
||||
WSGIApplicationGroup %{GLOBAL}
|
||||
WSGIScriptReloading On
|
||||
<IfModule mod_authz_core.c>
|
||||
# Apache 2.4
|
||||
<RequireAny>
|
||||
Require all granted
|
||||
</RequireAny>
|
||||
</IfModule>
|
||||
<IfModule !mod_auth_core.c>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</IfModule>
|
||||
|
||||
</Directory>
|
||||
|
||||
#Alias /testdays/static /var/www/testdays/testdays/static
|
||||
|
||||
#<Directory /var/www/testdays/testdays/static>
|
||||
#Order allow,deny
|
||||
#Allow from all
|
||||
#</Directory>
|
||||
|
||||
15
conf/testdays.wsgi
Normal file
15
conf/testdays.wsgi
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# This is required for running on EL6
|
||||
import __main__
|
||||
__main__.__requires__ = ['SQLAlchemy >= 0.7', 'Flask >= 0.9', 'jinja2 >= 2.6']
|
||||
import pkg_resources
|
||||
|
||||
# if you're running the app from a virtualenv, uncomment these lines
|
||||
#activate_this = '/var/www/testdays/env/bin/activate_this.py'
|
||||
#execfile(activate_this, dict(__file__=activate_this))
|
||||
#import sys
|
||||
#sys.path.insert(0,"/var/www/testdays/testdays/")
|
||||
|
||||
import os
|
||||
os.environ['TESTDAYS_CONFIG'] = '/etc/testdays/settings.py'
|
||||
|
||||
from testdays import app as application
|
||||
8
init_db.sh
Normal file
8
init_db.sh
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/bash
|
||||
# this is a simple script to aid in the setup of a new db for F18
|
||||
|
||||
# init db
|
||||
python run_cli.py init_db
|
||||
|
||||
# insert mock data
|
||||
python run_cli.py mock_data
|
||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
Flask>=0.9
|
||||
Flask-SQLAlchemy>=0.16
|
||||
SQLAlchemy>= 0.8.7
|
||||
WTForms>2.0
|
||||
Flask-WTF>=0.10.0
|
||||
Flask-Login>=0.2.2
|
||||
python-fedora
|
||||
29
run_cli.py
Normal file
29
run_cli.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright 2014, 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.
|
||||
#
|
||||
# Authors:
|
||||
# Josef Skladanka <jskladan@redhat.com>
|
||||
|
||||
import sys
|
||||
from testdays import cli
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit = cli.main()
|
||||
if exit:
|
||||
sys.exit(exit)
|
||||
|
||||
33
runapp.py
Normal file
33
runapp.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# runapp.py - script to facilitate running the testdays app from the CLI
|
||||
#
|
||||
# Copyright 2014, 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.
|
||||
#
|
||||
# Authors:
|
||||
# Josef Skladanka <jskladan@redhat.com>
|
||||
|
||||
import os
|
||||
import testdays
|
||||
|
||||
if __name__ == '__main__':
|
||||
testdays.app.run(
|
||||
host = testdays.app.config['RUN_HOST'],
|
||||
port = testdays.app.config['RUN_PORT'],
|
||||
debug = testdays.app.config['DEBUG'],
|
||||
)
|
||||
|
||||
32
setup.py
Normal file
32
setup.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
from setuptools import setup
|
||||
import codecs
|
||||
import re
|
||||
import os
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
def read(*parts):
|
||||
return codecs.open(os.path.join(here, *parts), 'r').read()
|
||||
|
||||
|
||||
def find_version(*file_paths):
|
||||
version_file = read(*file_paths)
|
||||
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
|
||||
version_file, re.M)
|
||||
if version_match:
|
||||
return version_match.group(1)
|
||||
raise RuntimeError("Unable to find version string.")
|
||||
|
||||
setup(name='testdays',
|
||||
version=find_version('testdays', '__init__.py'),
|
||||
description='',
|
||||
author='Josef Skladanka',
|
||||
author_email='jskladan@redhat.com',
|
||||
license='GPLv2+',
|
||||
packages=['testdays', 'testdays.controllers', 'testdays.models'],
|
||||
package_dir={'testdays':'testdays'},
|
||||
entry_points=dict(console_scripts=['testdays=testdays.cli:main']),
|
||||
include_package_data=True,
|
||||
)
|
||||
57
testdays.spec
Normal file
57
testdays.spec
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
%if ! (0%{?fedora} > 12 || 0%{?rhel} > 5)
|
||||
%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")}
|
||||
%{!?python_sitearch: %global python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")}
|
||||
%endif
|
||||
|
||||
Name: testdays
|
||||
Version: 0.0.1
|
||||
Release: 1%{?dist}
|
||||
Summary: I was lazy enough not-to change summary
|
||||
|
||||
License: GPLv2+
|
||||
URL: https://bitbucket.org/fedoraqa/testdays
|
||||
Source0: https://qadevel.cloud.fedoraproject.org/releases/%{name}/%{name}-%{version}.tar.gz
|
||||
|
||||
BuildArch: noarch
|
||||
|
||||
Requires: python-flask
|
||||
Requires: python-flask-sqlalchemy
|
||||
Requires: python-flask-wtf
|
||||
Requires: python-flask-login
|
||||
Requires: python-alembic
|
||||
BuildRequires: python2-devel python-setuptools
|
||||
|
||||
%description
|
||||
I was so lazy, that I did not change the description.
|
||||
|
||||
%prep
|
||||
%setup -q
|
||||
|
||||
%build
|
||||
%{__python2} setup.py build
|
||||
|
||||
%install
|
||||
%{__python2} setup.py install --skip-build --root %{buildroot}
|
||||
|
||||
# apache and wsgi settings
|
||||
mkdir -p %{buildroot}%{_datadir}/testdays/conf
|
||||
cp conf/testdays.conf %{buildroot}%{_datadir}/testdays/conf/.
|
||||
cp conf/testdays.wsgi %{buildroot}%{_datadir}/testdays/.
|
||||
|
||||
mkdir -p %{buildroot}%{_sysconfdir}/testdays
|
||||
install conf/settings.py.example %{buildroot}%{_sysconfdir}/testdays/settings.py.example
|
||||
|
||||
%files
|
||||
%doc README.md conf/*
|
||||
%{python_sitelib}/testdays
|
||||
%{python_sitelib}/*.egg-info
|
||||
|
||||
%attr(755,root,root) %{_bindir}/testdays
|
||||
%dir %{_sysconfdir}/testdays
|
||||
%{_sysconfdir}/testdays/*
|
||||
%dir %{_datadir}/testdays
|
||||
%{_datadir}/testdays/*
|
||||
|
||||
%changelog
|
||||
* Thu Feb 6 2014 Josef Skladanka <jskladan@redhat.com> - 0.0.1
|
||||
- initial packaging
|
||||
118
testdays/__init__.py
Normal file
118
testdays/__init__.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# Copyright 2014, 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.
|
||||
#
|
||||
# Authors:
|
||||
# Josef Skladanka <jskladan@redhat.com>
|
||||
|
||||
from flask import Flask, render_template
|
||||
from flask.ext.login import LoginManager
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
||||
# the version as used in setup.py
|
||||
__version__ = "0.0.1"
|
||||
|
||||
|
||||
# Flask App
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'not-really-a-secret'
|
||||
app.jinja_env.trim_blocks = True
|
||||
app.jinja_env.lstrip_blocks = True
|
||||
|
||||
# Load default config, then override that with a config file
|
||||
if os.getenv('PROD') == 'true':
|
||||
default_config_obj = 'testdays.config.ProductionConfig'
|
||||
default_config_file = '/etc/testdays/settings.py'
|
||||
elif os.getenv('TEST') == 'true':
|
||||
default_config_obj = 'testdays.config.TestingConfig'
|
||||
default_config_file = os.getcwd() + '/conf/settings.py'
|
||||
else:
|
||||
default_config_obj = 'testdays.config.DevelopmentConfig'
|
||||
default_config_file = os.getcwd() + '/conf/settings.py'
|
||||
|
||||
app.config.from_object(default_config_obj)
|
||||
|
||||
config_file = os.environ.get('TESTDAYS_CONFIG', default_config_file)
|
||||
|
||||
if os.path.exists(config_file):
|
||||
app.config.from_pyfile(config_file)
|
||||
|
||||
if app.config['PRODUCTION']:
|
||||
if app.secret_key == 'not-really-a-secret':
|
||||
raise Warning("You need to change the app.secret_key value for production")
|
||||
|
||||
# setup logging
|
||||
fmt = '[%(filename)s:%(lineno)d] ' if app.debug else '%(module)-12s '
|
||||
fmt += '%(asctime)s %(levelname)-7s %(message)s'
|
||||
datefmt = '%Y-%m-%d %H:%M:%S'
|
||||
loglevel = logging.DEBUG if app.debug else logging.INFO
|
||||
formatter = logging.Formatter(fmt=fmt, datefmt=datefmt)
|
||||
|
||||
def setup_logging():
|
||||
root_logger = logging.getLogger('')
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
|
||||
if app.config['STREAM_LOGGING']:
|
||||
print "doing stream logging"
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(loglevel)
|
||||
stream_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(stream_handler)
|
||||
app.logger.addHandler(stream_handler)
|
||||
|
||||
if app.config['SYSLOG_LOGGING']:
|
||||
print "doing syslog logging"
|
||||
syslog_handler = logging.handlers.SysLogHandler(address='/dev/log',
|
||||
facility=logging.handlers.SysLogHandler.LOG_LOCAL4)
|
||||
syslog_handler.setLevel(loglevel)
|
||||
syslog_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(syslog_handler)
|
||||
app.logger.addHandler(syslog_handler)
|
||||
|
||||
if app.config['FILE_LOGGING'] and app.config['LOGFILE']:
|
||||
print "doing file logging to %s" % app.config['LOGFILE']
|
||||
file_handler = logging.handlers.RotatingFileHandler(app.config['LOGFILE'], maxBytes=500000, backupCount=5)
|
||||
file_handler.setLevel(loglevel)
|
||||
file_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(file_handler)
|
||||
app.logger.addHandler(file_handler)
|
||||
|
||||
setup_logging()
|
||||
|
||||
if app.config['SHOW_DB_URI']:
|
||||
app.logger.debug('using DBURI: %s' % app.config['SQLALCHEMY_DATABASE_URI'])
|
||||
|
||||
|
||||
# database
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
# setup login manager
|
||||
login_manager = LoginManager()
|
||||
login_manager.setup_app(app)
|
||||
login_manager.login_view = 'login_page.login'
|
||||
|
||||
# register blueprints
|
||||
from testdays.controllers.main import main
|
||||
app.register_blueprint(main)
|
||||
|
||||
from testdays.controllers.login_page import login_page
|
||||
app.register_blueprint(login_page)
|
||||
|
||||
from testdays.controllers.admin import admin
|
||||
app.register_blueprint(admin)
|
||||
133
testdays/cli.py
Normal file
133
testdays/cli.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Copyright 2014, 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.
|
||||
#
|
||||
# Authors:
|
||||
# Josef Skladanka <jskladan@redhat.com>
|
||||
|
||||
# This is required for running on EL6
|
||||
import __main__
|
||||
__main__.__requires__ = ['SQLAlchemy >= 0.7', 'Flask >= 0.9', 'jinja2 >= 2.6']
|
||||
import pkg_resources
|
||||
|
||||
import os
|
||||
import sys
|
||||
from optparse import OptionParser
|
||||
|
||||
from alembic.config import Config
|
||||
from alembic import command as al_command
|
||||
from alembic.migration import MigrationContext
|
||||
|
||||
from testdays import db
|
||||
from testdays.models.user import User
|
||||
|
||||
def get_alembic_config():
|
||||
# the location of the alembic ini file and alembic scripts changes when
|
||||
# installed via package
|
||||
if os.path.exists("./alembic.ini"):
|
||||
alembic_cfg = Config("./alembic.ini")
|
||||
else:
|
||||
alembic_cfg = Config("/usr/share/resultsdb/alembic.ini",
|
||||
ini_section='alembic-packaged')
|
||||
return alembic_cfg
|
||||
|
||||
def upgrade_db(*args):
|
||||
alembic_cfg = get_alembic_config()
|
||||
|
||||
context = MigrationContext.configure(db.engine.connect())
|
||||
current_rev = context.get_current_revision()
|
||||
print "Upgrading Database to `head` from `%s`" % current_rev
|
||||
|
||||
al_command.upgrade(alembic_cfg, "head")
|
||||
|
||||
def init_alembic(*args):
|
||||
alembic_cfg = get_alembic_config()
|
||||
|
||||
# check to see if the db has already been initialized by checking for an
|
||||
# alembic revision
|
||||
context = MigrationContext.configure(db.engine.connect())
|
||||
current_rev = context.get_current_revision()
|
||||
|
||||
if not current_rev:
|
||||
print "Initializing alembic"
|
||||
print " - Setting the version to the first revision"
|
||||
al_command.stamp(alembic_cfg, "15f5eeb9f635")
|
||||
else:
|
||||
print "Alembic already initialized"
|
||||
|
||||
def initialize_db(destructive):
|
||||
alembic_cfg = get_alembic_config()
|
||||
|
||||
print "Initializing database"
|
||||
if destructive:
|
||||
print " - Dropping all tables"
|
||||
db.drop_all()
|
||||
print " - Creating tables"
|
||||
db.create_all()
|
||||
print " - Stamping alembic's current version to 'head'"
|
||||
al_command.stamp(alembic_cfg, "head")
|
||||
|
||||
init_alembic()
|
||||
upgrade_db()
|
||||
|
||||
|
||||
def mock_data(destructive):
|
||||
print "Populating tables with mock-data"
|
||||
|
||||
if destructive or not db.session.query(User).count():
|
||||
print " - User"
|
||||
data_users = [('admin', 'admin'), ('user', 'user')]
|
||||
|
||||
for d in data_users:
|
||||
u = User(*d)
|
||||
db.session.add(u)
|
||||
|
||||
db.session.commit()
|
||||
else:
|
||||
print " - skipped User"
|
||||
|
||||
def main():
|
||||
possible_commands = ['init_db', 'mock_data', 'upgrade_db', 'init_alembic']
|
||||
|
||||
usage = 'usage: [DEV=true] %prog ' + "(%s)" % ' | '.join(possible_commands)
|
||||
parser = OptionParser(usage=usage)
|
||||
parser.add_option("-d", "--destructive",
|
||||
action="store_true", dest="destructive", default=False,
|
||||
help="Drop tables in `init_db`; Store data in `mock_data` "
|
||||
"even if the tables are not empty")
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
if len(args) != 1 or args[0] not in possible_commands:
|
||||
print usage
|
||||
print
|
||||
print 'Please use one of the following commands: %s' % str(possible_commands)
|
||||
sys.exit(1)
|
||||
|
||||
command = {
|
||||
'init_db': initialize_db,
|
||||
'mock_data': mock_data,
|
||||
'upgrade_db': upgrade_db,
|
||||
'init_alembic': init_alembic,
|
||||
}[args[0]]
|
||||
|
||||
if not options.destructive:
|
||||
print "Proceeding with non-destructive init. To perform destructive "\
|
||||
"steps use -d option."
|
||||
|
||||
command(options.destructive)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
55
testdays/config.py
Normal file
55
testdays/config.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Copyright 2014, 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.
|
||||
#
|
||||
# Authors:
|
||||
# Josef Skladanka <jskladan@redhat.com>
|
||||
|
||||
class Config(object):
|
||||
DEBUG = True
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite://'
|
||||
|
||||
LOGFILE = '/var/log/testdays/testdays.log'
|
||||
FILE_LOGGING = False
|
||||
SYSLOG_LOGGING = False
|
||||
STREAM_LOGGING = True
|
||||
|
||||
RUN_HOST = None
|
||||
RUN_PORT = None
|
||||
|
||||
PRODUCTION = False
|
||||
TESTING = False
|
||||
|
||||
SHOW_DB_URI = False
|
||||
|
||||
RESULTSDB_URL = 'http://taskotron-local/resultsdb_api/api/v1.0/'
|
||||
|
||||
|
||||
class ProductionConfig(Config):
|
||||
DEBUG = False
|
||||
PRODUCTION = True
|
||||
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
TRAP_BAD_REQUEST_ERRORS = True
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/testdays_db.sqlite'
|
||||
|
||||
RUN_HOST = '0.0.0.0'
|
||||
RUN_PORT = 5000
|
||||
|
||||
|
||||
class TestingConfig(Config):
|
||||
TRAP_BAD_REQUEST_ERRORS = True
|
||||
TESTING = True
|
||||
0
testdays/controllers/__init__.py
Normal file
0
testdays/controllers/__init__.py
Normal file
210
testdays/controllers/admin.py
Normal file
210
testdays/controllers/admin.py
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
# Copyright 2014, 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.
|
||||
#
|
||||
# Authors:
|
||||
# Josef Skladanka <jskladan@redhat.com>
|
||||
|
||||
import re
|
||||
|
||||
from flask import Blueprint, render_template, flash, url_for, redirect, make_response
|
||||
from flask.ext.login import login_required
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
from resultsdb_api import ResultsDBapi
|
||||
|
||||
from testdays import app, db
|
||||
from testdays.controllers.main import preparse_results
|
||||
import testdays.lib.wiki as wiki
|
||||
from testdays.lib.forms import AddEventForm
|
||||
from testdays.models.testday import Event, Category, Testcase, TESTCASE_TYPES
|
||||
|
||||
admin = Blueprint('admin', __name__)
|
||||
|
||||
RDB_API = None
|
||||
|
||||
@app.before_first_request
|
||||
def before_first_request():
|
||||
global RDB_API
|
||||
RDB_API = ResultsDBapi(app.config['RESULTSDB_URL'])
|
||||
|
||||
|
||||
@admin.route('/admin')
|
||||
@admin.route('/admin/')
|
||||
#@login_required
|
||||
def admin_index():
|
||||
return render_template('admin/index.html')
|
||||
|
||||
def event_from_metadata(url):
|
||||
|
||||
# grab the respective section from wikipage
|
||||
data = wiki.get_page_section(url, "TestdayApp Metadata")
|
||||
|
||||
|
||||
# matches text from lines enclosed in '=' characters
|
||||
re_groupname = re.compile("^=+\s*(.*?)\s*=*$")
|
||||
|
||||
td = {"name":"", "url":"", "categories": []}
|
||||
category = {"name":"", "testcases": []}
|
||||
testcase = {"name":"", "url":"", "type":""}
|
||||
|
||||
# splits text on newlines, strips whitespaces and filters relevant lines
|
||||
# the first line is deemed irrelevant, as it is just the name of the section
|
||||
data = [line.strip() for line in data.split('\n')[1:]]
|
||||
data = [line for line in data if line.startswith("*") or line.startswith("=")]
|
||||
|
||||
# Testday Name & URL
|
||||
# the first line must contain testday's name and URL in the '*name;url' format
|
||||
name_url = data.pop(0)
|
||||
if not name_url.startswith("*"):
|
||||
return (False, "Missing Testday name & URL")
|
||||
try:
|
||||
# remove * at the beginning, split by ';' in two parts, and strip whitespaces
|
||||
td_name, td_url = [part.strip() for part in name_url[1:].split(';', 2)]
|
||||
except ValueError: # split failed
|
||||
return (False, "Testday name or URL missing")
|
||||
|
||||
# sanity check
|
||||
if len(td_name) < 5:
|
||||
return (False, "Testday name must be longer than 5 characters")
|
||||
if not td_url.startswith("http"):
|
||||
return (False, "Testday URL must start with 'http'")
|
||||
|
||||
if td_url.startswith("https"):
|
||||
td_url = td_url.replace("https", "http", 1)
|
||||
|
||||
event = Event(td_name, url, td_url)
|
||||
|
||||
# Testcase Sections
|
||||
category = None
|
||||
for line in data:
|
||||
# Create a new category, when we encounter category metadata
|
||||
if line.startswith("="):
|
||||
name = re_groupname.findall(line)[0]
|
||||
if not name:
|
||||
return (False, "Category name must be at least 1 character long at line %r" % line)
|
||||
category = Category(name, event)
|
||||
continue
|
||||
|
||||
# skip until we get a category
|
||||
if category is None:
|
||||
continue
|
||||
# skip until we get a testcase
|
||||
if not line.startswith("*"):
|
||||
continue
|
||||
|
||||
# Parse the testcase metadata
|
||||
|
||||
# remove * at the beginning, split by ';' and strip whitespaces
|
||||
l = [part.strip() for part in line[1:].split(';')]
|
||||
# append default testcase result type (pass/fail/info)
|
||||
l.append('tc')
|
||||
|
||||
if len(l) < 3:
|
||||
return (False, "Testcase name or url missing on line %r" % line)
|
||||
|
||||
testcase = Testcase(l[0], l[1], l[2].lower(), category)
|
||||
|
||||
if len(testcase.name) < 1:
|
||||
return (False, "Testcase name must be at least 1 character long at line %r" % line)
|
||||
if not testcase.url.startswith('http'):
|
||||
return (False, "Testcase url does not start with 'http' on line %r" % line)
|
||||
|
||||
# convert unknown types to the default "testcase" type
|
||||
if testcase.type not in TESTCASE_TYPES:
|
||||
testcase.type = 'tc'
|
||||
|
||||
# change https to http
|
||||
testcase.url = testcase.url.replace("https", "http", 1)
|
||||
|
||||
return(True, event)
|
||||
|
||||
|
||||
@admin.route('/admin/add_event', methods=['GET', 'POST'])
|
||||
def add_event():
|
||||
form_ = AddEventForm()
|
||||
|
||||
if form_.validate_on_submit():
|
||||
was_ok, data = event_from_metadata(form_.metadata_url.data)
|
||||
if was_ok:
|
||||
event = data
|
||||
try:
|
||||
old_event = db.session.query(Event).filter(Event.metadata_url == event.metadata_url).one()
|
||||
# Event with this metadata url not found -> create new event
|
||||
except NoResultFound:
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
db.session.add(event)
|
||||
|
||||
# start resultsdb job
|
||||
rdb_job = RDB_API.create_job(
|
||||
url_for('main.show_event', event_id=event.id),
|
||||
name="Testday - %s" % event.testday_url.split('/')[-1],
|
||||
)
|
||||
RDB_API.update_job(rdb_job['id'], status='RUNNING')
|
||||
|
||||
event.resultsdb_job_id = rdb_job['id']
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
flash('Event was successfully created')
|
||||
# Event with this metadata url found -> update
|
||||
else:
|
||||
# Remove old categories and testcases
|
||||
for category in old_event.categories:
|
||||
for testcase in category.testcases:
|
||||
db.session.delete(testcase)
|
||||
db.session.delete(category)
|
||||
|
||||
event.id = old_event.id
|
||||
event.resultsdb_job_id = old_event.resultsdb_job_id
|
||||
db.session.merge(event)
|
||||
db.session.commit()
|
||||
flash('Event was successfully updated')
|
||||
|
||||
return redirect(url_for('main.show_event', event_id = event.id))
|
||||
|
||||
# an error was encountered, flash the error and show the add_event form
|
||||
flash(data, 'error')
|
||||
|
||||
return render_template('admin/add_event.html',
|
||||
form = form_,
|
||||
form_action = url_for(".add_event"),
|
||||
header = u"Add/Edit Event",
|
||||
)
|
||||
|
||||
@admin.route('/admin/export_event', methods=['GET', 'POST'])
|
||||
def export_event():
|
||||
form_ = AddEventForm()
|
||||
|
||||
if form_.validate_on_submit():
|
||||
try:
|
||||
event = db.session.query(Event).filter(Event.metadata_url == form_.metadata_url.data).one()
|
||||
|
||||
except NoResultFound:
|
||||
flash('Event not found', 'error')
|
||||
|
||||
results = preparse_results(event)
|
||||
output = render_template('admin/export_event.txt', event=event, results=results)
|
||||
response = make_response(output)
|
||||
response.headers["Content-Type"] = "text/plain; charset=utf-8"
|
||||
return response
|
||||
|
||||
|
||||
return render_template('admin/add_event.html',
|
||||
form = form_,
|
||||
form_action = url_for(".export_event"),
|
||||
header = u"Export Event",
|
||||
)
|
||||
|
||||
80
testdays/controllers/login_page.py
Normal file
80
testdays/controllers/login_page.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Copyright 2014, 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.
|
||||
#
|
||||
# Authors:
|
||||
# Josef Skladanka <jskladan@redhat.com>
|
||||
|
||||
from flask import Blueprint, render_template, redirect, flash, url_for, request
|
||||
from flask.ext.wtf import Form
|
||||
from wtforms import TextField, PasswordField, HiddenField, RadioField
|
||||
from wtforms.validators import Required
|
||||
from flask.ext.login import login_user, logout_user, login_required, current_user, AnonymousUserMixin
|
||||
|
||||
|
||||
from testdays import app, login_manager
|
||||
from testdays.models.user import User
|
||||
|
||||
login_page = Blueprint('login_page', __name__)
|
||||
|
||||
|
||||
class LoginForm(Form):
|
||||
username = TextField(u'Username', validators = [Required()])
|
||||
password = PasswordField(u'Password', validators = [Required()])
|
||||
next_page = HiddenField()
|
||||
|
||||
|
||||
# handle login stuff
|
||||
@login_manager.user_loader
|
||||
def load_user(userid):
|
||||
app.logger.debug("getting info for user %s" % str(userid))
|
||||
user = User.query.get(userid)
|
||||
if user:
|
||||
return user
|
||||
else:
|
||||
return AnonymousUserMixin
|
||||
|
||||
|
||||
@login_page.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
login_form = LoginForm()
|
||||
|
||||
if login_form.validate_on_submit():
|
||||
user = User.query.filter_by(username = login_form.username.data).first()
|
||||
if user and user.check_password(login_form.password.data):
|
||||
login_user(user)
|
||||
|
||||
app.logger.info('Successful login for user %s' % login_form.username.data)
|
||||
flash('Logged In Successfully!')
|
||||
|
||||
return redirect(login_form.next_page.data)
|
||||
else:
|
||||
app.logger.info('FAILED login for user %s' % login_form.username.data)
|
||||
flash('Login Failed! Please Try again!')
|
||||
|
||||
login_form.next_page.data = request.args.get('next') or url_for('main.index')
|
||||
return render_template('login.html', form = login_form)
|
||||
|
||||
|
||||
@login_page.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
app.logger.info('logout for user %s' % current_user.username)
|
||||
logout_user()
|
||||
flash('Logged Out Successfully!')
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
|
||||
|
||||
353
testdays/controllers/main.py
Normal file
353
testdays/controllers/main.py
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
# Copyright 2014, 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.
|
||||
#
|
||||
# Authors:
|
||||
# Josef Skladanka <jskladan@redhat.com>
|
||||
|
||||
import re
|
||||
import json
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, session
|
||||
|
||||
import resultsdb_api
|
||||
|
||||
from testdays import app, db
|
||||
from testdays.models.testday import Event, Category, Testcase
|
||||
from testdays.lib.forms import EnterResultForm
|
||||
|
||||
main = Blueprint('main', __name__)
|
||||
|
||||
RDB_API = None
|
||||
TESTCASES = []
|
||||
RE_TESTCASE_NAME_NORMALIZE = re.compile(r'[^a-zA-Z0-9_]')
|
||||
|
||||
@app.before_first_request
|
||||
def before_first_request():
|
||||
global RDB_API
|
||||
RDB_API = resultsdb_api.ResultsDBapi(app.config['RESULTSDB_URL'])
|
||||
|
||||
@main.route('/')
|
||||
@main.route('/index')
|
||||
def index():
|
||||
return redirect(url_for('.events'))
|
||||
|
||||
@main.route('/events')
|
||||
def events():
|
||||
events = db.session.query(Event).order_by(Event.created_at)
|
||||
return render_template('list_events.html', events=events)
|
||||
|
||||
|
||||
# TODO: use Memcached instead of SimpleCache
|
||||
from werkzeug.contrib.cache import SimpleCache
|
||||
CACHE = SimpleCache()
|
||||
CACHE_TIMEOUT = 10
|
||||
|
||||
def preparse_results(event):
|
||||
global CACHE
|
||||
|
||||
cached = CACHE.get("event_%s_matrix" % event.id)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
results = RDB_API.get_job(event.resultsdb_job_id)['results']
|
||||
|
||||
# results is a list of dicts like this:
|
||||
# {
|
||||
# "href": "http://taskotron-local/resultsdb_api/api/v1.0/results/1510407",
|
||||
# "id": 1510407,
|
||||
# "job_url": "http://taskotron-local/resultsdb_api/api/v1.0/jobs/115516",
|
||||
# "log_url": null,
|
||||
# "outcome": "PASSED",
|
||||
# "result_data": {
|
||||
# "bugs": [
|
||||
# ""
|
||||
# ],
|
||||
# "col_1": [
|
||||
# "Fedora 19"
|
||||
# ],
|
||||
# "result": [
|
||||
# "cats!"
|
||||
# ],
|
||||
# "username": [
|
||||
# "jskladan"
|
||||
# ]
|
||||
# },
|
||||
# "submit_time": "2015-05-14T09:20:36.392351",
|
||||
# "summary": "Comment number 4",
|
||||
# "testcase": {
|
||||
# "href": "http://taskotron-local/resultsdb_api/api/v1.0/testcases/testdays_QA_Testcase_Power_Management_tuned_off_idle",
|
||||
# "name": "testdays_QA_Testcase_Power_Management_tuned_off_idle",
|
||||
# "url": "http://fedoraproject.org/wiki/QA:Testcase_Power_Management_tuned_off_idle"
|
||||
# }
|
||||
# }
|
||||
|
||||
# The following code will rearrange the results into structure like this one:
|
||||
#
|
||||
# {$username: {$col_1: {$tcurl: [{'bugs': $bugs.split(';'),
|
||||
# 'comment': $summary,
|
||||
# 'result': $result,
|
||||
# }]}}}
|
||||
#
|
||||
# Where $username, $col_1, and $result are from ['result_data'],
|
||||
# $tcurl is the testcase url from ['testcase']['url']
|
||||
|
||||
by_user = {}
|
||||
for result in results:
|
||||
data = result['result_data']
|
||||
|
||||
username = data['username'][0]
|
||||
col_1 = data['col_1'][0]
|
||||
tcurl = result['testcase']['url']
|
||||
|
||||
outcome = data['result'][0]
|
||||
bugs = []
|
||||
if data['bugs'][0]:
|
||||
bugs = data['bugs'][0].split(';')
|
||||
comment = result['summary']
|
||||
|
||||
by_user.setdefault(username, {})
|
||||
by_user[username].setdefault(col_1, {})
|
||||
by_user[username][col_1].setdefault(tcurl, [])
|
||||
|
||||
by_user[username][col_1][tcurl].append(
|
||||
dict(
|
||||
result = outcome,
|
||||
bugs = bugs,
|
||||
comment = comment
|
||||
)
|
||||
)
|
||||
|
||||
# At this point, by_user contains something like this:
|
||||
# {'usr1': {'moofoo': {'http://test2': [{'bugs': [],
|
||||
# 'comment': '',
|
||||
# 'result': 'INFO',
|
||||
# }]}},
|
||||
# 'usr2': {(foobar': {'http://test1': [{'bugs': [],
|
||||
# 'comment': None,
|
||||
# 'result': 'FAILED',
|
||||
# }],
|
||||
# 'http://test2': [{'bugs': ['12345',
|
||||
# '6431'],
|
||||
# 'comment': 'Does not work!',
|
||||
# 'result': 'PASSED',
|
||||
# }]}}}
|
||||
|
||||
# We'll prepare the data even further - the event's
|
||||
# testcases are split into categories. We'll prepare the by-category
|
||||
# matrix, se the template to show the results can stay fairly simple
|
||||
# The result of the transformation is:
|
||||
# {1: [{
|
||||
# 'username': u'jskladan',
|
||||
# 'profile': u'Ubuntu 4.1',
|
||||
# 'results': [[(u'FAILED', 1), (u'PASSED', 2)], []],
|
||||
# 'comments': [([u'123', u'456'], u'Comment number 2'),
|
||||
# ([], u'Comment number 1')],
|
||||
# },
|
||||
# {
|
||||
# 'username': u'jskladan',
|
||||
# 'profile': u'Fedora 19',
|
||||
# 'results': [[(u'INFO', False), (u'PASSED', 1)], [(u'cats!', 2)]],
|
||||
# 'comments': [([], u'Comment number 3'), ([], u'Comment number 4')],
|
||||
# }],
|
||||
# 2: []}
|
||||
#
|
||||
# $results represents the results per testcase - each sublist of the top list
|
||||
# 'maps' (positionaly) to the Testcase in the respective Category. It can
|
||||
# contain multiple tuples, each representing one result (one user with one
|
||||
# profile can submit multiple results per Testcase). The first item in the
|
||||
# tuple is the actual result. The second item is either False or Int.
|
||||
# False means that the result has no bugs/comment associated with it.
|
||||
# Int is the index+1 of the comment in the $comments field. This way
|
||||
# it is very simple to create link-references from result to comment/bugs
|
||||
#
|
||||
# $comments is a list of tuples, each tuple represents bugs/comment for the
|
||||
# respective result (see above).
|
||||
# First item in the tuple is a list of BUG numbers
|
||||
# Second item is the comment itself.
|
||||
|
||||
matrix = {}
|
||||
# We want to have the users sorted alphabetically in the table
|
||||
users = by_user.keys()
|
||||
users.sort()
|
||||
|
||||
# Create per-category pre-parsed lines of results
|
||||
for category in event.categories:
|
||||
matrix[category.id] = []
|
||||
relevant_testcases = set([tc.url for tc in category.testcases])
|
||||
|
||||
# Create a line-of-results per each user's profile
|
||||
for user in users:
|
||||
# Also, sort by the profile-info alphabetically
|
||||
user_profiles = by_user[user].keys()
|
||||
user_profiles.sort()
|
||||
for profile in user_profiles:
|
||||
data = by_user[user][profile]
|
||||
# Check whether this user-profile set any results
|
||||
# for testcases from this category
|
||||
if not relevant_testcases.intersection(data.keys()):
|
||||
continue
|
||||
|
||||
# Prepare the per-line data
|
||||
# see the explanation of the structure above
|
||||
line = dict(
|
||||
username=user,
|
||||
profile=profile,
|
||||
results=[],
|
||||
comments=[])
|
||||
|
||||
comment_id = 0
|
||||
for testcase in category.testcases:
|
||||
# Append new results-container for the respective
|
||||
# testcase.
|
||||
results_by_tc = []
|
||||
line['results'].append(results_by_tc)
|
||||
|
||||
# If the user-profile did not fill any result for this
|
||||
# specific testcase, go to the next one.
|
||||
# The "no results filled" state is represented by the empty
|
||||
# list.
|
||||
if testcase.url not in data:
|
||||
continue
|
||||
|
||||
# Go through all the details of the results for this
|
||||
# user-profile-testcase combination
|
||||
for detail in data[testcase.url]:
|
||||
has_comment = bool(detail['bugs'] or detail['comment'])
|
||||
# If there are bugs and/or comments for the result,
|
||||
# add them to the line container
|
||||
if has_comment:
|
||||
comment_id += 1
|
||||
line['comments'].append((detail['bugs'], detail['comment']))
|
||||
has_comment = comment_id
|
||||
results_by_tc.append((detail['result'], has_comment))
|
||||
matrix[category.id].append(line)
|
||||
|
||||
CACHE.set("event_%s_matrix" % event.id, matrix, CACHE_TIMEOUT)
|
||||
return matrix
|
||||
|
||||
|
||||
@main.route('/events/<int:event_id>')
|
||||
def show_event(event_id):
|
||||
event = db.session.query(Event).filter(Event.id == event_id).one()
|
||||
results = preparse_results(event)
|
||||
|
||||
prof = json.loads(session.get('profile', '{}'))
|
||||
username = prof.get('username')
|
||||
col_1 = prof.get('col_1')
|
||||
filled = prof.get('filled', {}).get(username, {}).get(col_1, [])
|
||||
profile = dict(username=username, col_1=col_1, filled=filled)
|
||||
|
||||
return render_template('show_event.html', event=event, results=results,
|
||||
profile=profile)
|
||||
|
||||
def testcase_to_name(testcase):
|
||||
prefix = "testdays"
|
||||
name = testcase.url.split('/')[-1]
|
||||
name = RE_TESTCASE_NAME_NORMALIZE.sub('_', name)
|
||||
return "%s_%s" % (prefix, name)
|
||||
|
||||
def ensure_testcase(testcase):
|
||||
global TESTCASES
|
||||
|
||||
name = testcase_to_name(testcase)
|
||||
if name not in TESTCASES:
|
||||
try:
|
||||
RDB_API.get_testcase(name)
|
||||
except resultsdb_api.ResultsDBapiException:
|
||||
try:
|
||||
RDB_API.create_testcase(name, testcase.url)
|
||||
except resultsdb_api.ResultsDBapiException:
|
||||
raise
|
||||
TESTCASES.append(name)
|
||||
return name
|
||||
|
||||
|
||||
def update_profile_cookies(username, col_1, profile=None):
|
||||
if not profile:
|
||||
profile = json.loads(session.get('profile', '{}'))
|
||||
profile['username'] = username
|
||||
profile['col_1'] = col_1
|
||||
filled = profile.setdefault('filled', {})
|
||||
filled.setdefault(username, {})
|
||||
filled[username].setdefault(col_1, [])
|
||||
|
||||
session['profile'] = json.dumps(profile)
|
||||
return profile
|
||||
|
||||
def update_filled_cookies(username, col_1, testcase_name, profile=None):
|
||||
profile = update_profile_cookies(username, col_1, profile)
|
||||
profile['filled'][username][col_1].append(testcase_name)
|
||||
|
||||
session['profile'] = json.dumps(profile)
|
||||
return profile
|
||||
|
||||
|
||||
@main.route('/events/<int:event_id>/enter_result/<int:tc_id>', methods=['GET', 'POST'])
|
||||
def enter_result(event_id, tc_id):
|
||||
testcase = db.session.query(Testcase).filter(Testcase.id == tc_id).one()
|
||||
form_ = EnterResultForm(testcase)
|
||||
|
||||
session_profile = json.loads(session.get('profile', '{}'))
|
||||
|
||||
if form_.validate_on_submit():
|
||||
outcome = form_.result.data
|
||||
if testcase.type == 'txt':
|
||||
outcome = 'PASSED'
|
||||
|
||||
testcase_name = ensure_testcase(testcase)
|
||||
|
||||
result_dict = dict(
|
||||
job_id=testcase.category.event.resultsdb_job_id,
|
||||
testcase_name=testcase_name,
|
||||
outcome=outcome,
|
||||
result=form_.result.data,
|
||||
col_1=form_.col_1.data,
|
||||
username=form_.username.data,
|
||||
bugs=form_.bugs.data,
|
||||
summary=form_.comment.data,
|
||||
)
|
||||
try:
|
||||
r = RDB_API.create_result(**result_dict)
|
||||
flash("Result Saved")
|
||||
update_filled_cookies(form_.username.data, form_.col_1.data, testcase.name, session_profile)
|
||||
return redirect(url_for('.show_event', event_id=event_id))
|
||||
except resultsdb_api.ResultsDBapiException as e:
|
||||
update_profile_cookies(form_.username.data, form_.col_1.data, session_profile)
|
||||
flash("There was an error when saving the result (%s)" % e.message, 'error')
|
||||
|
||||
if not form_.errors:
|
||||
if not form_.username.data:
|
||||
form_.username.data = session_profile.get('username', '')
|
||||
if not form_.col_1.data:
|
||||
form_.col_1.data = session_profile.get('col_1', '')
|
||||
|
||||
return render_template('enter_result.html',
|
||||
form = form_,
|
||||
form_action = url_for('.enter_result', event_id=event_id, tc_id=tc_id),
|
||||
header = "Enter result",
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
0
testdays/lib/__init__.py
Normal file
0
testdays/lib/__init__.py
Normal file
69
testdays/lib/forms.py
Normal file
69
testdays/lib/forms.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
from flask.ext.wtf import Form
|
||||
from wtforms.fields import TextField, SelectField, TextAreaField
|
||||
from wtforms.validators import URL, Required, AnyOf, Regexp
|
||||
|
||||
class AddEventForm(Form):
|
||||
metadata_url = TextField(u'Metadata URL', validators = [URL()])
|
||||
|
||||
class EnterResultFormTC(Form):
|
||||
username = TextField(u'Username', validators = [Required()])
|
||||
col_1 = TextField(u'Profile')
|
||||
|
||||
result = SelectField(
|
||||
u'Result',
|
||||
choices = [
|
||||
('', '-- Select Result --'),
|
||||
('PASSED', 'PASSED'),
|
||||
('FAILED', 'FAILED'),
|
||||
('INFO', 'INFO')
|
||||
],
|
||||
validators = [
|
||||
AnyOf(('PASSED', 'FAILED', 'INFO'))
|
||||
],
|
||||
)
|
||||
|
||||
bugs = TextField(
|
||||
u'Bugs',
|
||||
validators = [
|
||||
Regexp(r"^([0-9]+;?)*$",
|
||||
message ="Please enter bug numbers divided by semicolons. i.e. '752855;25532'",
|
||||
)
|
||||
],
|
||||
#help_text = "Bug numbers divided by semicolons, e.g. '752855;25532'"
|
||||
)
|
||||
|
||||
comment = TextAreaField(u'Comment')
|
||||
|
||||
def __init__(self, col_1_name=None, *args, **kwargs):
|
||||
super(EnterResultFormTC, self).__init__(*args, **kwargs)
|
||||
if col_1_name is not None:
|
||||
self.col_1.label.text = col_1_name
|
||||
|
||||
class EnterResultFormTXT(Form):
|
||||
username = TextField(u'Username', validators = [Required()])
|
||||
col_1 = TextField(u'Profile')
|
||||
|
||||
result = TextField(u'Result', validators = [Required()])
|
||||
|
||||
bugs = TextField(
|
||||
u'Bugs',
|
||||
validators = [
|
||||
Regexp(r"^([0-9]+;?)*$",
|
||||
message ="Please enter bug numbers divided by semicolons. i.e. '752855;25532'",
|
||||
)
|
||||
],
|
||||
#help_text = "Bug numbers divided by semicolons, e.g. '752855;25532'"
|
||||
)
|
||||
|
||||
comment = TextAreaField(u'Comment')
|
||||
|
||||
def __init__(self, col_1_name=None, *args, **kwargs):
|
||||
super(EnterResultFormTXT, self).__init__(*args, **kwargs)
|
||||
if col_1_name is not None:
|
||||
self.col_1.label.text = col_1_name
|
||||
|
||||
def EnterResultForm(testcase):
|
||||
if testcase.type == 'txt':
|
||||
return EnterResultFormTXT(testcase.category.col_1_name)
|
||||
else:
|
||||
return EnterResultFormTC(testcase.category.col_1_name)
|
||||
75
testdays/lib/wiki.py
Normal file
75
testdays/lib/wiki.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/python
|
||||
# wiki.py - wiki interaction module for autoqa
|
||||
|
||||
import fedora.client
|
||||
from urlparse import urljoin
|
||||
import urllib
|
||||
import simplejson
|
||||
|
||||
def __strip_url(url):
|
||||
for s in ["https://fedoraproject.org/wiki/", fedora.client.wiki.Wiki().base_url]:
|
||||
if url.startswith(s):
|
||||
url = url[len(s):]
|
||||
return url
|
||||
|
||||
def __get_sections(page):
|
||||
page = __strip_url(page)
|
||||
w = fedora.client.wiki.Wiki()
|
||||
params = {'action':'parse',
|
||||
'format':'json',
|
||||
'prop':'sections',
|
||||
'text':'{{:%s}}__TOC__' % page}
|
||||
result = w.send_request('api.php', req_params=params)
|
||||
return result['parse']['sections']
|
||||
|
||||
def __read_page_section(page, secnum):
|
||||
page = __strip_url(page)
|
||||
w = fedora.client.wiki.Wiki()
|
||||
# w.send_request chokes if it doesn't get json data back, so we can't
|
||||
# use that.
|
||||
params = {'action':'raw','templates':'expand','section':secnum,'title':page}
|
||||
url = urljoin(w.base_url, 'index.php')
|
||||
url += '?' + urllib.urlencode(params, doseq=True)
|
||||
section = urllib.urlopen(url)
|
||||
return section.read()
|
||||
|
||||
def get_page_section(page, secname):
|
||||
page = __strip_url(page)
|
||||
secnum = None
|
||||
secname = secname.lower()
|
||||
for s in __get_sections(page):
|
||||
if s['line'].lower().strip() == secname:
|
||||
secnum = s['number']
|
||||
if not secnum:
|
||||
return None
|
||||
return __read_page_section(page, secnum)
|
||||
|
||||
|
||||
def wiki_json_parse(sectiondata):
|
||||
decoder = simplejson.JSONDecoder()
|
||||
jsonitems = []
|
||||
# TODO: this could be more robust. We could parse each line by itself
|
||||
# and then merge all the valid lines at the end, rather than completely
|
||||
# dying if there's any invalid data..
|
||||
for line in sectiondata.split("\n"):
|
||||
if line.startswith('*'):
|
||||
jsonitems.append(line[1:].strip())
|
||||
elif jsonitems:
|
||||
break
|
||||
jsondata = '{' + ','.join(jsonitems) + '}'
|
||||
data = decoder.decode(jsondata)
|
||||
return data
|
||||
|
||||
def get_autoqa_metadata(page):
|
||||
page = __strip_url(page)
|
||||
section = get_page_section(page, 'autoqa metadata')
|
||||
if section is None:
|
||||
return dict()
|
||||
return wiki_json_parse(section)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import pprint
|
||||
page = 'User:Jskladan/Sandbox:TestdayAppTemplate'
|
||||
print "Fetching metadata from %s" % page
|
||||
pprint.pprint(get_page_section(page, 'TestdayApp Metadata'))
|
||||
0
testdays/models/__init__.py
Normal file
0
testdays/models/__init__.py
Normal file
79
testdays/models/testday.py
Normal file
79
testdays/models/testday.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Copyright 2014, 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.
|
||||
#
|
||||
# Authors:
|
||||
# Josef Skladanka <jskladan@redhat.com>
|
||||
|
||||
import datetime
|
||||
|
||||
from testdays import db
|
||||
|
||||
TESTCASE_TYPES = ('tc', 'txt')
|
||||
|
||||
class Event(db.Model):
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.Text)
|
||||
metadata_url = db.Column(db.Text)
|
||||
testday_url = db.Column(db.Text)
|
||||
resultsdb_job_id = db.Column(db.Integer)
|
||||
created_at = db.Column(db.DateTime, default = datetime.datetime.utcnow)
|
||||
|
||||
categories = db.relation('Category', backref='event', order_by='Category.id')
|
||||
|
||||
def __init__(self, name, metadata_url, testday_url=None, resultsdb_job_id=None):
|
||||
self.name = name
|
||||
self.metadata_url = metadata_url
|
||||
self.testday_url = testday_url
|
||||
self.resultsdb_job_id = resultsdb_job_id
|
||||
|
||||
class Category(db.Model):
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('category_fk_event_id', 'event_id'),
|
||||
)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
|
||||
name = db.Column(db.Text)
|
||||
col_1_name = db.Column(db.Text)
|
||||
|
||||
testcases = db.relation('Testcase', backref='category', order_by='Testcase.id')
|
||||
|
||||
def __init__(self, name, event=None):
|
||||
self.name = name
|
||||
if event is not None:
|
||||
self.event = event
|
||||
|
||||
class Testcase(db.Model):
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('testcase_fk_category_id', 'category_id'),
|
||||
)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
|
||||
name = db.Column(db.Text)
|
||||
url = db.Column(db.Text)
|
||||
type = db.Column(db.Enum(*TESTCASE_TYPES, name='testcase_type'))
|
||||
|
||||
def __init__(self, name, url, type, category=None):
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.url = url
|
||||
if category is not None:
|
||||
self.category = category
|
||||
|
||||
42
testdays/models/user.py
Normal file
42
testdays/models/user.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Copyright 2014, 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.
|
||||
#
|
||||
# Authors:
|
||||
# Josef Skladanka <jskladan@redhat.com>
|
||||
|
||||
from testdays import db
|
||||
from flask.ext.login import UserMixin
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True)
|
||||
pw_hash = db.Column(db.String(120), unique=False)
|
||||
|
||||
def __init__(self, username, password):
|
||||
self.username = username
|
||||
self.set_password(password)
|
||||
|
||||
def __repr__(self):
|
||||
return '<User %r>' % self.username
|
||||
|
||||
def set_password(self, password):
|
||||
self.pw_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.pw_hash, password)
|
||||
|
||||
2
testdays/static/css/style.css
Normal file
2
testdays/static/css/style.css
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
//body { padding-top: 70px; }
|
||||
|
||||
BIN
testdays/static/favicon.ico
Normal file
BIN
testdays/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
55
testdays/templates/_formhelpers.html
Normal file
55
testdays/templates/_formhelpers.html
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<!-- inspired by https://gist.github.com/maximebf/3986659 -->
|
||||
{% macro form_field(field) -%}
|
||||
{% set with_label = kwargs.pop('with_label', False) -%}
|
||||
{% set inline_ = '' -%}
|
||||
|
||||
{% if field.type == 'BooleanField' %}
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
{{ field(**kwargs) }}
|
||||
{{ field.label.text|safe }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{% elif field.type == 'RadioField' %}
|
||||
{% for subfield in field %}
|
||||
<div class="radio">
|
||||
<label>
|
||||
{{ subfield(**kwargs) }}
|
||||
{{ subfield.label.text|safe }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% else -%}
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{% set placeholder = '' -%}
|
||||
{% if not with_label -%}
|
||||
{% set placeholder = field.label.text -%}
|
||||
{% endif -%}
|
||||
|
||||
{% set class_ = 'form-control' -%}
|
||||
{% if field.type == 'FileField' -%}
|
||||
{% set class_ = class_ + ' input-file' %}
|
||||
{% endif -%}
|
||||
|
||||
|
||||
{% if with_label -%}
|
||||
<label for="{{ field.id }}">
|
||||
{{ field.label.text }}{% if field.flags.required %} *{% endif %}:
|
||||
</label>
|
||||
{% endif -%}
|
||||
|
||||
{{ field(class_=class_, placeholder=placeholder, **kwargs) }}
|
||||
|
||||
{% if field.errors -%}
|
||||
<span class="help-block">{{ field.errors|join(', ') }}</span>
|
||||
{% endif -%}
|
||||
|
||||
{% if field.description -%}
|
||||
<p class="help-block">{{ field.description|safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif -%}
|
||||
{% endmacro -%}
|
||||
16
testdays/templates/admin/add_event.html
Normal file
16
testdays/templates/admin/add_event.html
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% from "_formhelpers.html" import form_field %}
|
||||
|
||||
{% block body %}
|
||||
<h2>{{header}}</h2>
|
||||
<div class="container-fluid">
|
||||
<form method="post" action="{{form_action}}">
|
||||
{{ form.csrf_token }}
|
||||
{{ form_field(form.metadata_url, with_label = True) }}
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-success" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
26
testdays/templates/admin/export_event.txt
Normal file
26
testdays/templates/admin/export_event.txt
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{% for category in event.categories %}
|
||||
=== {{category.name}} ===
|
||||
{|
|
||||
! User
|
||||
! {{category.col_1_name or "Profile"}}
|
||||
{% for testcase in category.testcases %}
|
||||
! [{{testcase.url}} {{testcase.name}}]
|
||||
{% endfor %}
|
||||
! References
|
||||
{% for line in results[category.id] %}
|
||||
|-
|
||||
| [[User:{{line['username']}}|{{line['username']}}]]
|
||||
| {{line['profile']}}
|
||||
{% for results in line['results'] %}
|
||||
| {% for result in results %}{% if result[0] == 'PASSED' %}{{"{{result|pass}}"}}{% elif result[0] == 'FAILED' %}{{"{{result|fail}}"}}{% elif result[0] == 'INFO' %}{{"{{result|warn}}"}}{% else %}{{ result[0] }}{% endif %}{% if result[1] %}<ref>{% for bug in line['comments'][result[1]-1][0] %}{{"{{bz|"}}{{bug}}{{"}} "}}{% endfor %}{{ line['comments'][result[1]-1][1] }}</ref>{% endif %}{% endfor %}{# result in results #}
|
||||
|
||||
{% endfor %}{# results in line #}
|
||||
| <references/>
|
||||
{% endfor %}{# line #}
|
||||
|
||||
|-
|
||||
|}
|
||||
|
||||
{% endfor %} {# category #}
|
||||
|
||||
|
||||
13
testdays/templates/admin/index.html
Normal file
13
testdays/templates/admin/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<h1>Welcome to the Secret Admin Interface!</h1>
|
||||
|
||||
<a href={{url_for('admin.add_event')}}>Add/Update event</a><br />
|
||||
<a href={{url_for('admin.export_event')}}>Export event</a><br />
|
||||
|
||||
<h2>Need help?</h2>
|
||||
<a href="https://fedoraproject.org/wiki/QA/SOP_Test_Day_management">Test Day management SOP</a><br />
|
||||
<a href="https://fedoraproject.org/wiki/QA:TestdayApp">Testday App's wiki page</a>
|
||||
{% endblock %}
|
||||
20
testdays/templates/enter_result.html
Normal file
20
testdays/templates/enter_result.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% from "_formhelpers.html" import form_field %}
|
||||
|
||||
{% block body %}
|
||||
<h2>{{header}}</h2>
|
||||
<div class="container-fluid">
|
||||
<form method="post" action="{{form_action}}">
|
||||
{{ form.csrf_token }}
|
||||
{{ form_field(form.username, with_label = True) }}
|
||||
{{ form_field(form.col_1, with_label = True) }}
|
||||
{{ form_field(form.result, with_label = True) }}
|
||||
{{ form_field(form.bugs, with_label = True) }}
|
||||
{{ form_field(form.comment, with_label = True) }}
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-success" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
11
testdays/templates/index.html
Normal file
11
testdays/templates/index.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<h1>Welcome to testdays!</h1>
|
||||
|
||||
<p>
|
||||
This is super-awesome page for the Flask Project. You can try logging in (admin/admin), or accessing the <a href="{{ url_for('admin.admin_index') }}">Admin interface</a>.
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
67
testdays/templates/layout.html
Normal file
67
testdays/templates/layout.html
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ title|default('testdays') }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- Bootstrap -->
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{# <div class="navbar navbar-default navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<span class="navbar-brand"><b>testdays</b></span>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a href="{{ url_for('admin.admin_index') }}">Admin</a></li>
|
||||
</ul>
|
||||
|
||||
<!-- <div class="navbar-right">
|
||||
<input type="button" class="btn btn-default navbar-btn" value="Search" onclick="location.href='';">
|
||||
</div> -->
|
||||
|
||||
</div><!--/.nav-collapse -->
|
||||
</div><!--/.container -->
|
||||
</div><!--/.navbar --> #}
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
{% for category, message in get_flashed_messages(with_categories=True) %}
|
||||
<div class="alert alert-info {% if category == 'error' %} alert-danger{%endif%}">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
{% block body %}
|
||||
<h1> My First Flask Application! </h1>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="container-fluid">
|
||||
{% block footer %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
||||
<script src="//code.jquery.com/jquery.js"></script>
|
||||
<!-- Include all compiled plugins -->
|
||||
<script src="//netdna.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
10
testdays/templates/list_events.html
Normal file
10
testdays/templates/list_events.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<h1>All Events</h1>
|
||||
|
||||
{% for event in events %}
|
||||
<a href="{{ url_for('main.show_event', event_id=event.id)}}">{{ event.name }}</a><br />
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
19
testdays/templates/login.html
Normal file
19
testdays/templates/login.html
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% from "_formhelpers.html" import form_field %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<form method="post" action="login">
|
||||
{{ form.csrf_token }}
|
||||
{{ form.next_page }}
|
||||
{{ form_field(form.username) }}
|
||||
{{ form_field(form.password) }}
|
||||
<input type="submit" class="btn btn-success" value="Login"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
106
testdays/templates/show_event.html
Normal file
106
testdays/templates/show_event.html
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<h1>{{ event.name }}</h1>
|
||||
More information about the event can be found here:
|
||||
<a class="external" target="_blank" href="{{event.testday_url}}">
|
||||
{{event.testday_url}}
|
||||
</a>
|
||||
<br />
|
||||
|
||||
Go back to <a href="{{url_for('main.events')}}"> List of Events</a>.
|
||||
|
||||
<h3>Results</h3>
|
||||
|
||||
Clicking on the testcase name will show you the appropriate "how to test" page. <br />
|
||||
Click on the <strong class="btn btn-success btn-xs">Enter result</strong> button, to enter result. <br />
|
||||
<div class="btn btn-warning">
|
||||
<strong>Note:</strong> results are cached and realoaded from the database each 10 seconds.
|
||||
</div>
|
||||
|
||||
{% for category in event.categories %}
|
||||
<h3>{{category.name}}</h3>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>{{category.col_1_name or "Profile"}}</th>
|
||||
{% for testcase in category.testcases %}
|
||||
<th>
|
||||
<a href="{{testcase.url}}" class="external" target="_blank">
|
||||
<strong>{{testcase.name}}</strong>
|
||||
</a>
|
||||
</th>
|
||||
{% endfor %}
|
||||
<th>Comments</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>
|
||||
{% for testcase in category.testcases %}
|
||||
<th>
|
||||
{% if testcase.name in profile.filled %}
|
||||
<a href="{{url_for('main.enter_result', event_id=testcase.category.event.id, tc_id=testcase.id)}}" class="btn btn-primary btn-xs ">
|
||||
<strong>Already entered</strong>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{url_for('main.enter_result', event_id=testcase.category.event.id, tc_id=testcase.id)}}" class="btn btn-success btn-sm ">
|
||||
<strong>Enter result</strong>
|
||||
</a>
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for line in results[category.id] %}
|
||||
<tr>
|
||||
<td>{{line['username']}}</td>
|
||||
<td>{{line['profile']}}</td>
|
||||
{% for results in line['results'] %}
|
||||
<td>
|
||||
{% for result in results %}
|
||||
{%- if result[0] == 'PASSED' -%}
|
||||
<span class="glyphicon glyphicon-ok text-success" aria-hidden="true"></span>
|
||||
{%- elif result[0] == 'FAILED' -%}
|
||||
<span class="glyphicon glyphicon-remove text-danger" aria-hidden="true"></span>
|
||||
{%- elif result[0] == 'INFO' -%}
|
||||
<span class="glyphicon glyphicon-warning-sign text-warning" aria-hidden="true"></span>
|
||||
{%- else -%}
|
||||
{{ result[0] }}
|
||||
{%- endif -%}
|
||||
{%- if result[1] -%}
|
||||
<sup>[{{result[1]}}]</sup>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td>
|
||||
{% for comment in line['comments'] %}
|
||||
{{loop.index}}.
|
||||
{% for bug in comment[0] %}
|
||||
<a class="external" target="_blank" href="https://bugzilla.redhat.com/show_bug.cgi?id={{bug}}">#{{bug}}</a>,
|
||||
{% endfor %}
|
||||
{{comment[1]}}
|
||||
{% if comment[0] or comment[1] %}
|
||||
<br />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block footer %}
|
||||
<div align="right">
|
||||
<a class="external" href="{{event.metadata_url}}">Wiki Metadata</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue