Browse Source

Merge branch 'flask_ui' of athos/baangt into master

Bernhard Buhl 4 years ago
parent
commit
1c94e21f09

+ 4 - 0
.gitignore

@@ -8,3 +8,7 @@ geckodriver.log
 baangt.ini
 /Screenshots
 /docs/logs
+__pycache__
+*.pyc
+*.db
+venv

+ 4 - 0
flask/.dockerignore

@@ -0,0 +1,4 @@
+**/__pycache__
+**/*.pyc
+**/*.db
+venv

File diff suppressed because it is too large
+ 144 - 0
flask/CONCEPT_DB_UI.md


+ 24 - 0
flask/Dockerfile

@@ -0,0 +1,24 @@
+FROM python:3.7.5
+
+LABEL Testrun application
+
+ENV PYTHONDONTWRITEBYTECODE 1
+ENV FLASK_APP "application.py"
+ENV FLASK_DEBUG True
+
+RUN mkdir /testrun
+WORKDIR /testrun
+
+COPY requirements.txt requirements.txt
+
+RUN python -m venv venv
+
+RUN pip install pipenv && \
+    pipenv install --dev
+
+ADD . /testrun
+
+EXPOSE 5000
+
+RUN pipenv run python populate_db.py
+CMD pipenv run flask run --host=0.0.0.0

+ 23 - 0
flask/app/__init__.py

@@ -0,0 +1,23 @@
+import os
+from flask import Flask
+from flask_login import LoginManager
+from flask_sqlalchemy import SQLAlchemy
+from flask_migrate import Migrate
+
+app = Flask(__name__)
+
+
+#
+# configurations
+#
+app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') or 'secret!key'
+app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL') or \
+    'sqlite:///' + os.path.join(os.path.abspath(os.path.dirname(__file__)), 'testrun.db')
+app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
+
+db = SQLAlchemy(app)
+migrate = Migrate(app, db)
+login_manager = LoginManager(app)
+login_manager.login_view = 'login'
+
+from app import views, models, filters

+ 37 - 0
flask/app/filters.py

@@ -0,0 +1,37 @@
+from datetime import datetime
+from . import app
+
+@app.template_filter('name_by_type')
+def item_name(item_type, plural=True):
+	#
+	# get name of the item_type
+	#
+	
+	# categories
+	if item_type == 'main':
+		name = 'Main Item'
+
+	# main items 
+	elif item_type == 'testrun':
+		name = 'Testrun'
+	elif item_type == 'testcase_sequence':
+		name = 'Test Case Sequence'
+	elif item_type == 'testcase':
+		name = 'Test Case'
+	elif item_type == 'teststep_sequence':
+		name = 'Test Step Sequence'
+	elif item_type == 'teststep':
+		name = 'Test Step'
+	else:
+		# wrong item_type
+		return ''
+
+	# check for plurals
+	if plural:
+		name += 's'
+
+	return name
+
+@app.template_filter('time')
+def format_time(time):
+	return time.strftime('%Y-%m-%d %H:%M')

+ 124 - 0
flask/app/forms.py

@@ -0,0 +1,124 @@
+from flask_wtf import FlaskForm
+from wtforms.fields import StringField, PasswordField, SelectMultipleField, SelectField, FloatField
+from wtforms.widgets import TextArea, ListWidget, CheckboxInput
+from wtforms.validators import ValidationError, DataRequired, EqualTo
+from app.models import User
+from app import utils
+
+
+#
+# authantication forms
+#
+
+class LoginForm(FlaskForm):
+	username = StringField('Username', validators=[DataRequired()])
+	password = PasswordField('Password', validators=[DataRequired()])
+
+	def validate_username(self, username):
+		if User.query.filter_by(username=username.data).count() == 0:
+			raise ValidationError('Wrong username')
+
+	def validate_password(self, password):
+		user = User.query.filter_by(username=self.username.data).first()
+		if user and not user.verify_password(password.data):
+			raise ValidationError('Wrong password')
+
+class SingupForm(FlaskForm):
+	username = StringField('Username', validators=[DataRequired()])
+	password = PasswordField('Password', validators=[DataRequired()])
+	password2 = PasswordField('Password again', validators=[DataRequired(), EqualTo('password')])
+
+	def validate_username(self, username):
+		if User.query.filter_by(username=username.data).count() > 0:
+			raise ValidationError('This username is in use. Please try another username.')
+
+#
+# testrun items forms
+#
+
+class TestrunCreateForm(FlaskForm):
+	name = StringField('Name', validators=[DataRequired()])
+	description = StringField('Description', validators=[DataRequired()], widget=TextArea())
+	testcase_sequences = SelectMultipleField('Test Case Sequences')
+
+	@classmethod
+	def new(cls):
+		# update choices
+		form = cls()
+		form.testcase_sequences.choices = utils.getTestCaseSequences()
+		return form
+
+
+class TestCaseSequenceCreateForm(FlaskForm):
+	name = StringField('Name', validators=[DataRequired()])
+	description = StringField('Description', validators=[DataRequired()], widget=TextArea())
+	classname = SelectField('Class Name')
+	datafiles = SelectMultipleField('Data Files')
+	testcases = SelectMultipleField('Test Cases')
+
+	@classmethod
+	def new(cls):
+		# update choices
+		form = cls()
+		form.classname.choices = utils.getClassNames()
+		form.datafiles.choices = utils.getDataFiles()
+		form.testcases.choices = utils.getTestCases()
+		return form
+
+
+class TestCaseCreateForm(FlaskForm):
+	name = StringField('Name', validators=[DataRequired()])
+	description = StringField('Description', validators=[DataRequired()], widget=TextArea())
+	classname = SelectField('Class Name')
+	browser_type = SelectField('Browser Type')
+	testcase_type = SelectField('Test Case Type')
+	testcase_stepsequences = SelectMultipleField('Step Sequences')
+
+	@classmethod
+	def new(cls):
+		# update choices
+		form = cls()
+		form.classname.choices = utils.getClassNames()
+		form.browser_type.choices = utils.getBrowserTypes()
+		form.testcase_type.choices = utils.getTestCaseTypes()
+		form.testcase_stepsequences.choices = utils.getTestStepSequences()
+		return form
+
+
+class TestStepSequenceCreateForm(FlaskForm):
+	name = StringField('Name', validators=[DataRequired()])
+	description = StringField('Description', validators=[DataRequired()], widget=TextArea())
+	classname = SelectField('Class Name')
+	teststeps = SelectMultipleField('Test Steps')
+
+	@classmethod
+	def new(cls):
+		# update choices
+		form = cls()
+		form.classname.choices = utils.getClassNames()
+		form.teststeps.choices = utils.getTestSteps()
+		return form
+
+
+class TestStepCreateForm(FlaskForm):
+	name = StringField('Name', validators=[DataRequired()])
+	description = StringField('Description', validators=[DataRequired()], widget=TextArea())
+	activity_type = SelectField('Activity Type')
+	locator_type = SelectField('Locator Type')
+	# model extension
+	locator = StringField('Locator')
+	optional = SelectField('Optional', choices=(('1', 'True'), ('2', 'False')))
+	timeout = FloatField('Timeout')
+	release = StringField('Release')
+	value = StringField('Value')
+	value2 = StringField('Value 2')
+	comparision = StringField('Comparision')
+
+	@classmethod
+	def new(cls):
+		# update choices
+		form = cls()
+		form.activity_type.choices = utils.getActivityTypes()
+		form.locator_type.choices = utils.getLocatorTypes()
+		return form
+	

+ 265 - 0
flask/app/models.py

@@ -0,0 +1,265 @@
+from flask_sqlalchemy import SQLAlchemy
+from flask_login import UserMixin
+from werkzeug.security import generate_password_hash, check_password_hash
+from datetime import datetime
+from app import db, login_manager
+
+#
+# user Model
+# TODO: extend user Model
+#
+class User(UserMixin, db.Model):
+	__tablename__ = 'users'
+	id = db.Column(db.Integer, primary_key=True)
+	username = db.Column(db.String(64), unique=True, nullable=False)
+	password = db.Column(db.String(128), nullable=False)
+	created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+	lastlogin = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+
+	def __str__(self):
+		return self.username
+
+	def set_password(self, password):
+		self.password = generate_password_hash(password)
+
+	def verify_password(self, password):
+		return check_password_hash(self.password, password)
+
+@login_manager.user_loader
+def load_user(id):
+	return User.query.get(int(id))
+
+#
+# relation tables
+#
+
+testrun_casesequence = db.Table(
+	'testrun_casesequence',
+	db.Column('testrun_id', db.Integer, db.ForeignKey('testruns.id'), primary_key=True),
+	db.Column('testcase_sequence_id', db.Integer, db.ForeignKey('testcase_sequences.id'), primary_key=True)
+)
+
+testcase_sequence_datafile = db.Table(
+	'testcase_sequence_datafile',
+	db.Column('testcase_sequence_id', db.Integer, db.ForeignKey('testcase_sequences.id'), primary_key=True),
+	db.Column('datafile_id', db.Integer, db.ForeignKey('datafiles.id'), primary_key=True)
+)
+
+testcase_sequence_case = db.Table(
+	'testcase_sequence_case',
+	db.Column('testcase_sequence_id', db.Integer, db.ForeignKey('testcase_sequences.id'), primary_key=True),
+	db.Column('testcase_id', db.Integer, db.ForeignKey('testcases.id'), primary_key=True)
+)
+
+testcase_stepsequence = db.Table(
+	'testcase_stepsequence',
+	db.Column('testcase_id', db.Integer, db.ForeignKey('testcases.id'), primary_key=True),
+	db.Column('teststep_sequence_id', db.Integer, db.ForeignKey('teststep_sequences.id'), primary_key=True)
+)
+
+
+#
+# main entities
+#
+class Testrun(db.Model):
+	__tablename__ = 'testruns'
+	id = db.Column(db.Integer, primary_key=True)
+	name = db.Column(db.String(64), nullable=False)
+	description = db.Column(db.String(512), nullable=False)
+	created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+	creator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
+	edited = db.Column(db.DateTime, nullable=True)
+	editor_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
+	creator = db.relationship('User', backref='created_testruns', lazy='immediate', foreign_keys=[creator_id])
+	editor = db.relationship('User', backref='edited_testruns', lazy='immediate', foreign_keys=[editor_id])
+
+	def __str__(self):
+		return self.name
+
+class TestCaseSequence(db.Model):
+	__tablename__ = 'testcase_sequences'
+	id = db.Column(db.Integer, primary_key=True)
+	name = db.Column(db.String(64), nullable=False)
+	description = db.Column(db.String(512), nullable=False)
+	created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+	creator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
+	edited = db.Column(db.DateTime, nullable=True)
+	editor_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
+	classname_id = db.Column(db.Integer, db.ForeignKey('classnames.id'), nullable=False)
+	creator = db.relationship('User', backref='created_testcase_sequances', lazy='immediate', foreign_keys=[creator_id])
+	editor = db.relationship('User', backref='edited_testcase_sequances', lazy='immediate', foreign_keys=[editor_id])
+	classname = db.relationship('ClassName', backref='testcase_sequences', lazy='immediate', foreign_keys=[classname_id])
+	testrun = db.relationship(
+		'Testrun',
+		secondary=testrun_casesequence,
+		lazy='subquery',
+		backref=db.backref('testcase_sequences', lazy=True),
+	)
+
+	def __str__(self):
+		return self.name
+
+class DataFile(db.Model):
+	__tablename__ = 'datafiles'
+	id = db.Column(db.Integer, primary_key=True)
+	filename = db.Column(db.String(64), nullable=False)
+	created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+	creator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
+	creator = db.relationship('User', backref='created_datafiles', lazy='immediate', foreign_keys=[creator_id])
+	testcase_sequence = db.relationship(
+		'TestCaseSequence',
+		secondary=testcase_sequence_datafile,
+		lazy='subquery',
+		backref=db.backref('datafiles', lazy=True),
+	)
+
+	def __str__(self):
+		return self.filename
+
+class TestCase(db.Model):
+	__tablename__ = 'testcases'
+	id = db.Column(db.Integer, primary_key=True)
+	name = db.Column(db.String(64), nullable=False)
+	description = db.Column(db.String(512), nullable=False)
+	created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+	creator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
+	edited = db.Column(db.DateTime, nullable=True)
+	editor_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
+	classname_id = db.Column(db.Integer, db.ForeignKey('classnames.id'), nullable=False)
+	browser_type_id = db.Column(db.Integer, db.ForeignKey('browser_types.id'), nullable=False)
+	testcase_type_id = db.Column(db.Integer, db.ForeignKey('testcase_types.id'), nullable=False)
+	creator = db.relationship('User', backref='created_testcases', lazy='immediate', foreign_keys=[creator_id])
+	editor = db.relationship('User', backref='edited_testcases', lazy='immediate', foreign_keys=[editor_id])
+	classname = db.relationship('ClassName', backref='testcases', lazy='immediate', foreign_keys=[classname_id])
+	browser_type = db.relationship('BrowserType', backref='testcases', lazy='immediate', foreign_keys=[browser_type_id])
+	testcase_type = db.relationship('TestCaseType', backref='testcases', lazy='immediate', foreign_keys=[testcase_type_id])
+	testcase_sequence = db.relationship(
+		'TestCaseSequence',
+		secondary=testcase_sequence_case,
+		lazy='subquery',
+		backref=db.backref('testcases', lazy=True),
+	)
+
+	def __str__(self):
+		return self.name
+
+class TestStepSequence(db.Model):
+	__tablename__ = 'teststep_sequences'
+	id = db.Column(db.Integer, primary_key=True)
+	name = db.Column(db.String(64), nullable=False)
+	description = db.Column(db.String(512), nullable=False)
+	created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+	creator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
+	edited = db.Column(db.DateTime, nullable=True)
+	editor_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
+	classname_id = db.Column(db.Integer, db.ForeignKey('classnames.id'), nullable=False)
+	creator = db.relationship('User', backref='created_teststep_sequences', lazy='immediate', foreign_keys=[creator_id])
+	editor = db.relationship('User', backref='edited_teststep_sequences', lazy='immediate', foreign_keys=[editor_id])
+	classname = db.relationship('ClassName', backref='teststep_sequences', lazy='immediate', foreign_keys=[classname_id])
+	testcase = db.relationship(
+		'TestCase',
+		secondary=testcase_stepsequence,
+		lazy='subquery',
+		backref=db.backref('teststep_sequences', lazy=True),
+	)
+
+	def __str__(self):
+		return self.name
+
+class TestStepExecution(db.Model):
+	__tablename__ = 'teststep_executions'
+	id = db.Column(db.Integer, primary_key=True)
+	name = db.Column(db.String(64), nullable=False)
+	description = db.Column(db.String(512), nullable=False)
+	created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+	creator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
+	edited = db.Column(db.DateTime, nullable=True)
+	editor_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
+	activity_type_id = db.Column(db.Integer, db.ForeignKey('activity_types.id'), nullable=False)
+	locator_type_id = db.Column(db.Integer, db.ForeignKey('locator_types.id'), nullable=False)
+	teststep_sequence_id = db.Column(db.Integer, db.ForeignKey('teststep_sequences.id'), nullable=True)
+	creator = db.relationship('User', backref='created_teststeps', lazy='immediate', foreign_keys=[creator_id])
+	editor = db.relationship('User', backref='edited_teststeps', lazy='immediate', foreign_keys=[editor_id])
+	activity_type = db.relationship('ActivityType', backref='teststeps', lazy='immediate', foreign_keys=[activity_type_id])
+	locator_type = db.relationship('LocatorType', backref='teststeps', lazy='immediate', foreign_keys=[locator_type_id])
+	teststep_sequence = db.relationship('TestStepSequence', backref='teststeps', lazy='immediate', foreign_keys=[teststep_sequence_id])
+	# model extention
+	locator = db.Column(db.String, nullable=True)
+	optional = db.Column(db.Boolean, nullable=True)
+	timeout = db.Column(db.Float(precision='5,2'), nullable=True)
+	release = db.Column(db.String, nullable=True)
+	value = db.Column(db.String, nullable=True)
+	value2 = db.Column(db.String, nullable=True)
+	comparision = db.Column(db.String, nullable=True)
+
+	def __str__(self):
+		return self.name
+
+#
+# supporting entities
+#
+class GlobalTestStepExecution(db.Model):
+	__tablename__ = 'global_teststep_executions'
+	id = db.Column(db.Integer, primary_key=True)
+	name = db.Column(db.String(64), nullable=False)
+	description = db.Column(db.String(512), nullable=False)
+	created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
+	creator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
+	edited = db.Column(db.DateTime, nullable=True)
+	editor_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
+	activity_type_id = db.Column(db.Integer, db.ForeignKey('activity_types.id'), nullable=False)
+	locator_type_id = db.Column(db.Integer, db.ForeignKey('locator_types.id'), nullable=False)
+	teststep_sequence_id = db.Column(db.Integer, db.ForeignKey('teststep_sequences.id'), nullable=True)
+	creator = db.relationship('User', backref='created_global_teststeps', lazy='immediate', foreign_keys=[creator_id])
+	editor = db.relationship('User', backref='edited_global_teststeps', lazy='immediate', foreign_keys=[editor_id])
+	activity_type = db.relationship('ActivityType', backref='global_teststeps', lazy='immediate', foreign_keys=[activity_type_id])
+	locator_type = db.relationship('LocatorType', backref='global_teststeps', lazy='immediate', foreign_keys=[locator_type_id])
+	teststep_sequence = db.relationship('TestStepSequence', backref='global_teststeps', lazy='immediate', foreign_keys=[teststep_sequence_id])
+	
+	def __str__(self):
+		return self.name
+
+class ClassName(db.Model):
+	__tablename__ = 'classnames'
+	id = db.Column(db.Integer, primary_key=True)
+	name = db.Column(db.String(64), nullable=False)
+	description = db.Column(db.String(512), nullable=False)
+
+	def __str__(self):
+		return self.name
+
+class BrowserType(db.Model):
+	__tablename__ = 'browser_types'
+	id = db.Column(db.Integer, primary_key=True)
+	name = db.Column(db.String(64), nullable=False)
+	description = db.Column(db.String(512), nullable=False)
+
+	def __str__(self):
+		return self.name
+	
+class TestCaseType(db.Model):
+	__tablename__ = 'testcase_types'
+	id = db.Column(db.Integer, primary_key=True)
+	name = db.Column(db.String(64), nullable=False)
+	description = db.Column(db.String(512), nullable=False)
+
+	def __str__(self):
+		return self.name
+
+class ActivityType(db.Model):
+	__tablename__ = 'activity_types'
+	id = db.Column(db.Integer, primary_key=True)
+	name = db.Column(db.String(64), nullable=False)
+	description = db.Column(db.String(512), nullable=False)
+
+	def __str__(self):
+		return self.name
+
+class LocatorType(db.Model):
+	__tablename__ = 'locator_types'
+	id = db.Column(db.Integer, primary_key=True)
+	name = db.Column(db.String(64), nullable=False)
+	description = db.Column(db.String(512), nullable=False)
+
+	def __str__(self):
+		return self.name

+ 26 - 0
flask/app/static/css/styles.css

@@ -0,0 +1,26 @@
+.chip {
+  display: inline-block;
+  padding: 0 10px;
+  height: 30px;
+  font-size: 16px;
+  /*line-height: 50px;*/
+  border-radius: 15px;
+  background-color: #f1f1f1;
+}
+
+.closebtn {
+  padding-left: 10px;
+  color: #888;
+  font-weight: bold;
+  float: right;
+  font-size: 20px;
+  cursor: pointer;
+}
+
+.closebtn:hover {
+  color: #000;
+}
+
+.fixed-label {
+  width: 11rem;
+}

+ 106 - 0
flask/app/static/js/testrun.js

@@ -0,0 +1,106 @@
+/*   document.addEventListener('DOMContentLoaded', () => {
+        // set links
+        document.querySelectorAll('.dropdown-item').forEach(link => {
+            link.onclick = () => {
+                // load item data to main
+                load_item(link.dataset.type, link.dataset.id);
+                return false;
+            };
+        });
+
+        document.querySelectorAll('.collapse>.btn').forEach(link => {
+            link.onclick = () => {
+                // create item
+                new_item(link.dataset.type)
+                //document.querySelector('main').innerHTML = "Done!"
+            };
+        });
+
+    });
+*/
+    // get item
+    function load_item(item_type, item_id) {
+        const request = new XMLHttpRequest();
+        request.open('GET', `/${item_type}/${item_id}`);
+        request.onload = () => {
+            const response = request.responseText;
+            document.querySelector('main').innerHTML = response;
+        };
+        request.send();
+    }
+
+    // create new item
+    function new_item(item_type) {
+        const request = new XMLHttpRequest();
+        request.open('GET', `/${item_type}/new`);
+        request.onload = () => {
+            const response = request.responseText;
+            document.querySelector('main').innerHTML = response;
+        };
+        request.send();
+    }
+
+    // edit item
+    function edit_item(item_type, item_id) {
+        const request = new XMLHttpRequest();
+        request.open('GET', `/${item_type}/${item_id}/edit`);
+        request.onload = () => {
+            const response = request.responseText;
+            document.querySelector('main').innerHTML = response;
+        };
+        request.send();
+    }
+
+    // delete item
+    function delete_item(item_type, item_name, item_id) {
+        if (confirm(`You are about to delete '${item_name}'`)) {
+            const request = new XMLHttpRequest();
+            request.open('POST', `/${item_type}/${item_id}/delete`);
+            request.onload = () => {
+                const response = request.responseText;
+                document.write(response);
+            }
+            request.send();
+        };
+    }
+
+    // select multiple with chips
+    function add_chip(e, name) {
+        var chip_area = document.getElementById(`chips_${name}`);
+        var new_chip = document.createElement('div');
+        new_chip.setAttribute('class', 'chip mr-1');
+        new_chip.setAttribute('data-id', e.options[e.selectedIndex].value);
+        new_chip.innerHTML = `
+            <small>${e.options[e.selectedIndex].text}</small>
+            <span class="closebtn" onclick="delete_chip(this.parentElement, '${name}')">&times;</span>
+            `;
+        e.options[e.selectedIndex].disabled = true;
+        e.selectedIndex = 0;
+        chip_area.appendChild(new_chip);
+    }
+
+    function delete_chip(e, name) {
+        var selector = document.getElementById(name);
+        for (var i = 1; i < selector.length; i++) {
+            if (selector.options[i].value == e.dataset['id']) {
+                selector.options[i].disabled = false;
+            }
+        }
+        e.parentElement.removeChild(e);
+    }
+
+    function get_chips() {
+        document.querySelectorAll('.multyselect').forEach(selector => {
+            selector.setAttribute('multiple','');
+            selector.options[0].selected = false;
+            for (var i = 1; i < selector.length; i++) {
+                if (selector.options[i].disabled) {
+                    selector.options[i].disabled = false;
+                    selector.options[i].selected = true;
+                }
+            }
+            console.log("Ok!");
+        });
+        return true;
+    }
+// type: "select-multiple"

+ 54 - 0
flask/app/templates/base.html

@@ -0,0 +1,54 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <!-- Required meta tags -->
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+
+    <!-- Bootstrap CSS -->
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
+
+    <!-- Local Styles -->
+    <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
+
+    <title>
+    	{% block title %}
+    	{% endblock %}
+    </title>
+  </head>
+  
+  <body>
+    <nav class="navbar navbar-expand navbar-dark bg-dark flex-md-nowrap p-1 shadow">
+        <a class="navbar-brand ml-3 py-2" href="/">Testruns Definition</a>
+        <ul class="navbar-nav ml-auto mr-3">            
+            <li class="nav-item">
+                {% if current_user.is_authenticated %}
+                    <span class="nav-link">{{ current_user }}</span>
+                {% else %}
+                    <a class="nav-link" href="{{ url_for('login')}}">Login</a>
+                {% endif %}
+            </li>
+            <li class="nav-item">
+                {% if current_user.is_authenticated %}
+                    <a class="nav-link" href="{{ url_for('logout')}}">Sign out</a>
+                {% else %}
+                    <a class="nav-link" href="{{ url_for('signup')}}">Sign up</a>
+                {% endif %}
+            </li>
+        </ul>
+    </nav>
+      
+    {% block content %}
+    {% endblock %}
+
+
+    <!-- Local JS -->
+    <script src="{{ url_for('static', filename='js/testrun.js') }}"></script>
+
+    <!-- Optional JavaScript -->
+    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
+    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
+    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
+  </body>
+</html>

+ 88 - 0
flask/app/templates/testrun/create_item.html

@@ -0,0 +1,88 @@
+{% extends "base.html" %}
+
+{% block title %}
+    Create {{ type|name_by_type(False) }}
+{% endblock %}
+
+{% block content %}
+
+<main>
+
+<!-- bread crumb -->
+<nav aria-label="breadcrumb">
+    <ol class="breadcrumb">
+        <li class="breadcrumb-item"><a href="/">Home</a></li>
+        <li class="breadcrumb-item"><a href="/{{ type }}">{{ type|name_by_type }}</a></li>
+        <li class="breadcrumb-item active" aria-current="page">New</li>
+    </ol>
+</nav>
+
+<!-- flash messages -->
+{% with messages = get_flashed_messages(with_categories=true) %}
+    {% for category, msg in messages %}
+        <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
+            {{ msg }}
+            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+                <span aria-hidden="true">&times;</span>
+            </button>
+        </div>
+    {% endfor %}
+{% endwith %}
+
+<div class="container mt-5">
+    <div class="border-bottom pt-3">
+        <h1 >Create new {{ type|name_by_type(False) }}</h1>
+    </div>
+
+    <form class="pr-5" method="post" onsubmit="get_chips()" action="/{{ type }}/new">
+        {{ form.hidden_tag() }}
+
+        {% for field in form %}
+            {% if field.name != "csrf_token" %}
+            
+            <!-- error message -->
+            {% for error in field.errors %}
+                <p class="text-danger">{{ error }}</p>
+            {% endfor %}
+            
+            <div class="input-group my-3">
+                <div class="input-group-prepend">
+                    {{ field.label(class="input-group-text fixed-label") }}
+                </div>
+
+                {% if field.name in chips %}
+                <!-- choices for chips -->
+                    <select class="form-control multyselect" id="{{ field.name }}" name="{{ field.name }}"
+                    onchange="add_chip(this, '{{ field.name }}')">
+                        <option value="0" select="">&lt; Select &gt;</option>
+                        {% for option in field.choices %}
+                            <option value="{{ option.0 }}">
+                                {{ option.1 }}
+                            </option>
+                        {% endfor %}
+                    </select>
+                
+                {% else %}
+                <!-- other fields -->
+                    {{ field(class="form-control") }}
+                {% endif %}
+            </div>
+
+            <!-- chips area -->
+            {% if field.name in chips %}
+                <div id="chips_{{ field.name }}">
+                </div>
+            {% endif %}
+            {% endif %}
+
+        {% endfor %}
+
+        <p>
+            <button class="btn btn-primary px-5 my-3" type="submit">Create</button>
+        </p>
+    </form>
+</div>
+
+</main>
+
+{% endblock %}

+ 25 - 0
flask/app/templates/testrun/create_item_old.html

@@ -0,0 +1,25 @@
+<div class="border-bottom pt-3">
+    <h1 >Create a New {{ type|capitalize }}</h1>
+</div>
+
+<form class="pr-5" method="post" action="/{{ type }}/new">
+    {{ form.hidden_tag() }}
+
+    {% for field in form %}
+        {% if field.name != "csrf_token" %}
+        <div class="input-group my-3">
+            {% for error in field.errors %}
+                <p class="text-danger">{{ error }}</p>
+            {% endfor %}
+            <div class="input-group-prepend">
+                {{ field.label(class="input-group-text") }}
+            </div>
+            {{ field(class="form-control")}}
+        </div>
+        {% endif %}
+    {% endfor %}
+
+    <p>
+        <button class="btn btn-primary px-5 my-3" type="submit">Create</button>
+    </p>
+</form>

+ 94 - 0
flask/app/templates/testrun/edit_item.html

@@ -0,0 +1,94 @@
+{% extends "base.html" %}
+
+{% block title %}
+    {{ item.name }}
+{% endblock %}
+
+{% block content %}
+
+<main>
+
+<!-- bread crumb -->
+<nav aria-label="breadcrumb">
+    <ol class="breadcrumb">
+        <li class="breadcrumb-item"><a href="/">Home</a></li>
+        <li class="breadcrumb-item"><a href="/{{ type }}">{{ type|name_by_type }}</a></li>
+        <li class="breadcrumb-item active" aria-current="page">{{ item.name }}</li>
+    </ol>
+</nav>
+
+<!-- flash messages -->
+{% with messages = get_flashed_messages(with_categories=true) %}
+    {% for category, msg in messages %}
+        <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
+            {{ msg }}
+            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+                <span aria-hidden="true">&times;</span>
+            </button>
+        </div>
+    {% endfor %}
+{% endwith %}
+
+<div class="container mt-5">
+    <div class="border-bottom pt-3">
+        <h1 >Edit '{{ item.name }}'</h1>
+    </div>
+
+    <form class="pr-5" method="post" onsubmit="get_chips()" action="/{{ type }}/{{ item.id }}/edit">
+        {{ form.hidden_tag() }}
+
+        {% for field in form %}
+            {% if field.name != "csrf_token" %}
+            
+            <!-- show errors -->
+            {% for error in field.errors %}
+                <p class="text-danger">{{ error }}</p>
+            {% endfor %}
+            
+            <div class="input-group my-3">
+                <div class="input-group-prepend">
+                    {{ field.label(class="input-group-text fixed-label") }}
+                </div>
+
+                {% if field.data is iterable and field.data is not string %}
+                <!-- choices for chips -->
+                    <select class="form-control multyselect" id="{{ field.name }}" name="{{ field.name }}"
+                    onchange="add_chip(this, '{{ field.name }}')">
+                        <option value="0" select="">&lt; Select &gt;</option>
+                        {% for option in field.choices %}
+                            <option value="{{ option.0 }}" {% if option.0 in field.data %}disabled{% endif %}>
+                                {{ option.1 }}
+                            </option>
+                        {% endfor %}
+                    </select>
+                
+                {% else %}
+                <!-- other fields -->
+                    {{ field(class="form-control", value=field.data) }}
+                {% endif %}
+            </div>
+
+            <!-- chips -->
+            {% if field.data is iterable and field.data is not string %}
+                <div id="chips_{{ field.name }}">
+                    {% for option in field.choices if option.0 in field.data %}
+                        <div class="chip mr-1" data-id="{{ option.0 }}">
+                            <small>{{ option.1 }}</small>
+                            <span class="closebtn" onclick="delete_chip(this.parentElement, '{{ field.name }}')">&times;</span>
+                        </div>
+                    {% endfor %}
+                </div>
+            {% endif %}
+            {% endif %}
+
+        {% endfor %}
+
+        <p>
+            <button class="btn btn-primary px-5 my-3" type="submit">Save</button>
+        </p>
+    </form>
+</div>
+
+</main>
+
+{% endblock %}

+ 25 - 0
flask/app/templates/testrun/edit_item_old.html

@@ -0,0 +1,25 @@
+<div class="border-bottom pt-3">
+    <h1 >Edit '{{ item.name }}'</h1>
+</div>
+
+<form class="pr-5" method="post" action="/{{ type }}/{{ item.id }}/edit">
+    {{ form.hidden_tag() }}
+
+    {% for field in form %}
+        {% if field.name != "csrf_token" %}
+        <div class="input-group my-3">
+            {% for error in field.errors %}
+                <p class="text-danger">{{ error }}</p>
+            {% endfor %}
+            <div class="input-group-prepend">
+                {{ field.label(class="input-group-text") }}
+            </div>
+            {{ field(class="form-control", value=field.data)}}
+        </div>
+        {% endif %}
+    {% endfor %}
+
+    <p>
+        <button class="btn btn-primary px-5 my-3" type="submit">Save</button>
+    </p>
+</form>

+ 45 - 0
flask/app/templates/testrun/index.html

@@ -0,0 +1,45 @@
+{% extends "base.html" %}
+
+{% block title %}
+    Home
+{% endblock %}
+
+{% block content %}
+
+<!-- bread crumb -->
+<nav aria-label="breadcrumb">
+    <ol class="breadcrumb">
+        <li class="breadcrumb-item active" aria-current="page">Home</li>
+    </ol>
+</nav>
+
+<!-- flash messages -->
+{% with messages = get_flashed_messages(with_categories=true) %}
+    {% for category, msg in messages %}
+        <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
+            {{ msg }}
+            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+                <span aria-hidden="true">&times;</span>
+            </button>
+        </div>
+    {% endfor %}
+{% endwith %}
+
+<!-- item list -->
+<div class="container mt-5">
+    <div class="row">
+        <div class="list-group col-12">
+            {% for category, item_list in items.items() %}
+                <!-- header -->
+                <p class="list-group-item active h4">{{ category|name_by_type }}</p>
+                
+                <!-- items -->
+                {% for item in item_list %}
+                    <a class="list-group-item list-group-item-action h5" href="/{{ item }}">{{ item|name_by_type }}</a>
+                {% endfor %}
+            {% endfor %}
+        </div>
+    </div>
+</div>
+
+{% endblock %}

+ 100 - 0
flask/app/templates/testrun/index_old.html

@@ -0,0 +1,100 @@
+{% extends "base.html" %}
+
+{% block title %}
+    Testruns
+{% endblock %}
+
+{% block content %}
+<div class="container-fluid mt-5">
+<div class="row">
+    <nav class="col-md-2 d-none d-md-block bg-light sidebar">
+        <div class="sidebar-sticky">
+            <ul class="nav flex-column my-3">
+                <li class="nav-item">
+                    <a class="nav-link" data-toggle="collapse" href="#Testruns">
+                        Testruns                        </a>
+                    <div class="collapse" id="Testruns">
+                        <a class="btn btn-primary btn-sm ml-4" data-type="testrun" href="#">Create New Testrun</a>
+                        {% for item in items.testruns %}
+                            <a class="dropdown-item" data-type="testrun" data-id="{{ item.id }}" href="">{{ item }}</a>
+                        {% endfor %}
+                    </div>
+                </li>
+                <li class="nav-item">
+                    <a class="nav-link" data-toggle="collapse" href="#TestCaseSequences">
+                        Test Case Sequences
+                    </a>
+                    <div class="collapse" id="TestCaseSequences">
+                        <a class="btn btn-primary btn-sm ml-4" data-type="testcase_sequence" href="#">Create New Test Case Sequence</a>
+                        {% for item in items.testcase_sequances %}
+                            <a class="dropdown-item" data-type="testcase_sequence" data-id="{{ item.id }}" href="">{{ item }}</a>
+                        {% endfor %}
+                    </div>
+                </li>
+                <!-- DataFile excludded
+                <li class="nav-item">
+                    <a class="nav-link" data-toggle="collapse" href="#DataFiles">
+                        Data Files
+                    </a>
+                    <div class="collapse" id="DataFiles">
+                        {% for item in items.datafiles %}
+                            <a class="dropdown-item" data-type="datafile" data-id="{{ item.id }}" href="">{{ item }}</a>
+                        {% endfor %}
+                    </div>
+                </li>
+            -->
+                <li class="nav-item">
+                    <a class="nav-link" data-toggle="collapse" href="#TestCases">
+                        Test Cases
+                    <div class="collapse" id="TestCases">
+                        <a class="btn btn-primary btn-sm ml-4" data-type="testcase" href="#">Create New Test Case</a>
+                        {% for item in items.testcases %}
+                            <a class="dropdown-item" data-type="testcase" data-id="{{ item.id }}" href="">{{ item }}</a>
+                        {% endfor %}
+                    </div>
+                </li>
+                <li class="nav-item">
+                    <a class="nav-link" data-toggle="collapse" href="#TestStepSequences">
+                        Test Step Sequences
+                    </a>
+                    <div class="collapse" id="TestStepSequences">
+                        <a class="btn btn-primary btn-sm ml-4" data-type="teststep_sequence" href="#">Create New Test Step Sequence</a>
+                        {% for item in items.teststep_sequences %}
+                            <a class="dropdown-item" data-type="teststep_sequence" data-id="{{ item.id }}" href="">{{ item }}</a>
+                        {% endfor %}
+                    </div>
+                </li>
+                <li class="nav-item">
+                    <a class="nav-link" data-toggle="collapse" href="#TestSteps">
+                        Test Steps
+                    </a>
+                    <div class="collapse" id="TestSteps">
+                        <a class="btn btn-primary btn-sm ml-4" data-type="teststep" href="#">Create New Test Step</a>
+                        {% for item in items.teststeps %}
+                            <a class="dropdown-item" data-type="teststep" data-id="{{ item.id }}" href="">{{ item }}</a>
+                        {% endfor %}
+                    </div>
+                </li>
+            </ul>
+        </div>
+    </nav>
+
+    <main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-4">
+
+        <h1 class="py-2">
+            {% with msg = get_flashed_messages() %}
+                {% if msg %}
+                    {{ msg|last }}
+                {% endif %}
+            {% endwith %}
+        </h1>
+        <h3 class="border-bottom pb-2">Please select an item.</h3> 
+
+    </main>
+</div>
+</div>
+
+<!-- Load local JS -->
+<script src="{{ url_for('static', filename='testrun.js') }}"></script>
+
+{% endblock %}

+ 162 - 0
flask/app/templates/testrun/item.html

@@ -0,0 +1,162 @@
+    <div class="border-bottom pt-3">
+        <h1 >{{ item }}</h1>
+        <p><small>
+                Created {{ item.created.strftime('%Y-%m-%d %H:%M') }} by {{ item.creator }}
+        </small></p>
+    
+        {% if item.edited %}
+        <p><small>
+            Last edidted {{ item.edited.strftime('%Y-%m-%d %H:%M') }} by {{ item.editor }}
+        </small></p>
+        {% endif %}
+    </div>
+
+    <div class="border-bottom pt-3">
+        <h4>Description:</h4>
+        <p>{{ item.description }}</div>
+    <div>
+
+    {% if type == "testrun" %}
+    <div class="border-bottom pt-3">
+        <h4>Test Case Sequences:</h4>
+        {% if item.testcase_sequences %}
+            <ul>
+            {% for sequence in item.testcase_sequences %}
+                <li>{{ sequence }}</li>
+            {% endfor %}
+            </ul>
+        {% else %}
+            <p>No Test Case Sequence Defined</p>
+        {% endif %}
+    </div>
+    {% endif %}
+
+    {% if type == "testcase_sequence" or type == "testcase" or type == "teststep_sequences" %}
+    <div class="border-bottom pt-3">
+        <h4>Class Name:</h4>
+        {{ item.classname }}
+    </div>
+    {% endif %}
+
+    {% if type == "testcase_sequence" %}
+    <div class="border-bottom pt-3">
+        <h4>Data Files:</h4>
+        {% if item.datafiles %}
+            <ul>
+            {% for datafile in item.datafiles %}
+                <li>{{ datafile }}</li>
+            {% endfor %}
+            </ul>
+        {% else %}
+            <p>No Data File Defined</p>
+        {% endif %}
+    </div>
+
+    <div class="border-bottom pt-3">
+        <h4>Test Cases:</h4>
+        {% if item.testcases %}
+            <ul>
+            {% for testcase in item.testcases %}
+                <li>{{ testcase }}</li>
+            {% endfor %}
+            </ul>
+        {% else %}
+            <p>No Test Case Defined</p>
+        {% endif %}
+    </div>
+    {% endif %}
+
+    {% if type == "testcase" %}
+    <div class="border-bottom pt-3">
+        <h4>Browser Type:</h4>
+        {{ item.browser_type }}
+    </div>
+
+    <div class="border-bottom pt-3">
+        <h4>Test Case Type:</h4>
+        {{ item.testcase_type }}
+    </div>
+
+    <div class="border-bottom pt-3">
+        <h4>Test Case Step Sequence:</h4>
+        {% if item.teststep_sequences %}
+            <ul>
+            {% for sequence in item.teststep_sequences %}
+                <li>{{ sequence }}</li>
+            {% endfor %}
+            </ul>
+        {% else %}
+            <p>No Test Case Step Sequence Defined</p>
+        {% endif %}
+    </div>
+    {% endif %}
+
+    {% if type == "teststep_sequence" %}
+    <div class="border-bottom pt-3">
+        <h4>Test Steps:</h4>
+        {% if item.teststeps %}
+            <ul>
+            {% for step in item.teststeps %}
+                <li>{{ step }}</li>
+            {% endfor %}
+            </ul>
+        {% else %}
+            <p>No Test Step Defined</p>
+        {% endif %}
+    </div>
+    {% endif %}
+
+    {% if type == "teststep" %}
+    <div class="border-bottom pt-3">
+        <h4>Activity Type:</h4>
+        {{ item.activity_type }}
+    </div>
+
+    <div class="border-bottom pt-3">
+        <h4>Locator Type:</h4>
+        {{ item.locator_type }}
+    </div>
+
+    <div class="border-bottom pt-3">
+        <h4>Locator:</h4>
+        {{ item.locator }}
+    </div>
+
+    <div class="border-bottom pt-3">
+        <h4>Optional:</h4>
+        {{ item.optional }}
+    </div>
+
+    <div class="border-bottom pt-3">
+        <h4>Timeout:</h4>
+        {{ item.timeout }}
+    </div>
+
+    <div class="border-bottom pt-3">
+        <h4>Release:</h4>
+        {{ item.release }}
+    </div>
+
+    <div class="border-bottom pt-3">
+        <h4>Value:</h4>
+        {{ item.value }}
+    </div>
+
+    <div class="border-bottom pt-3">
+        <h4>Value 2:</h4>
+        {{ item.value2 }}
+    </div>
+
+    <div class="border-bottom pt-3">
+        <h4>Comparision:</h4>
+        {{ item.comparision }}
+    </div>
+    {% endif %}
+
+    <p>
+        <button class="btn btn-primary px-5 my-3 mr-3" onclick="edit_item('{{ type }}', '{{ item.id }}')">Edit</button>
+        <button class="btn btn-danger px-5 my-3" onclick="delete_item('{{ type }}', '{{ item.name}}', '{{ item.id }}')">Delete</button>
+    </p>
+
+
+

+ 266 - 0
flask/app/templates/testrun/item_list.html

@@ -0,0 +1,266 @@
+{% extends "base.html" %}
+
+{% block title %}
+    {{ type|name_by_type }}
+{% endblock %}
+
+{% block content %}
+
+<main>
+
+<!-- bread crumb -->
+<nav aria-label="breadcrumb">
+    <ol class="breadcrumb">
+        <li class="breadcrumb-item"><a href="/">Home</a></li>
+        <li class="breadcrumb-item active" aria-current="page">{{ type|name_by_type }}</li>
+    </ol>
+</nav>
+
+<!-- flash messages -->
+{% with messages = get_flashed_messages(with_categories=true) %}
+    {% for category, msg in messages %}
+        <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
+            {{ msg }}
+            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+                <span aria-hidden="true">&times;</span>
+            </button>
+        </div>
+    {% endfor %}
+{% endwith %}
+
+<!-- item list -->
+<div class="container mt-5">
+
+    <!-- item title -->
+    <div class="row mb-3">
+        <div class="col-md-9">
+            <h2>{{ type|name_by_type }}</h2>
+        </div>
+        <!-- create button -->
+        <div class="col-md-3">
+            <a class="btn btn-primary" href="/{{ type }}/new" role="button">
+                <strong>&plus; </strong>new {{ type|name_by_type(False) }}
+            </a>
+        </div>
+    </div>
+
+    <!-- header -->
+    <div class="row border-top border-bottom py-3">
+        <div class="col-md-4"><strong>Name</strong></div>
+        <div class="col-md-3"><strong>Created By</strong></div>
+        <div class="col"><strong>Created</strong></div>
+    </div>
+
+    <!-- item list -->
+    {% for item in items %}
+        <div class="row border-top py-3">
+            <div class="col-md-4">
+                <a data-toggle="collapse" href="#item{{ item.id }}" aria-expanded="false">
+                    {{ item.name }}
+                </a>
+            </div>
+            <div class="col-md-3">{{ item.creator.username }}</div>
+            <div class="col">{{ item.created|time }}</div>
+
+            <div class="col-12 collapse mt-1" id="item{{ item.id }}">
+                <!-- edited data -->
+                {% if item.edited %}
+                    <p><small>
+                        Last edidted {{ item.edited.strftime('%Y-%m-%d %H:%M') }} by {{ item.editor }}
+                    </small></p>
+                {% endif %}
+
+                <ul class="list-group">
+
+                    <!-- description -->
+                    <li class="list-group-item h5 list-group-item-dark">Description</li>
+                    <li class="list-group-item">{{ item.description }}</li>
+
+                    <!-- Testrun fields -->
+                    {% if type == 'testrun' %}
+
+                    <li class="list-group-item h5 list-group-item-dark">{{ 'testcase_sequence'|name_by_type }}</li>
+                    {% if item.testcase_sequences %}
+                        {% for sequence in item.testcase_sequences %}
+                        <li class="list-group-item">{{ sequence }}</li>
+                        {% endfor %}
+                    {% else %}
+                        <li class="list-group-item">No Test Case Sequence Defined</li>
+                    {% endif %}
+
+                    {% endif %}
+
+                    <!-- Class Name -->
+                    {% if type == 'testcase_sequence' or type == 'testcase' or type == 'teststep_sequences' %}
+
+                    <li class="list-group-item h5 list-group-item-dark">Class Name</li>
+                    <li class="list-group-item">{{ item.classname }}</li>
+                    
+                    {% endif %}
+
+                    <!-- Testcase Sequence fields -->
+                    {% if type == 'testcase_sequence' %}
+
+                    <li class="list-group-item h5 list-group-item-dark">Data Files</li>
+                    {% if item.datafiles %}
+                        {% for datafile in item.datafiles %}
+                        <li class="list-group-item">{{ datafile }}</li>
+                        {% endfor %}
+                    {% else %}
+                        <li class="list-group-item">No Data File Defined</li>
+                    {% endif %}
+
+                    <li class="list-group-item h5 list-group-item-dark">{{ 'testcase'|name_by_type }}</li>
+                    {% if item.testcases %}
+                        {% for testcase in item.testcases %}
+                        <li class="list-group-item">{{ testcase }}</li>
+                        {% endfor %}
+                    {% else %}
+                        <li class="list-group-item">No Test Case Defined</li>
+                    {% endif %}
+
+                    {% endif %}
+
+                    <!-- Test Case fields -->
+                    {% if type == 'testcase' %}
+
+                    <li class="list-group-item h5 list-group-item-dark">Browser Type</li>
+                    <li class="list-group-item">{{ item.browser_type }}</li>
+
+                    <li class="list-group-item h5 list-group-item-dark">Test Case Type</li>
+                    <li class="list-group-item">{{ item.testcase_type }}</li>
+
+                    <li class="list-group-item h5 list-group-item-dark">{{ 'teststep_sequence'|name_by_type }}</li>
+                    {% if item.teststep_sequences %}
+                        {% for sequence in item.teststep_sequences %}
+                        <li class="list-group-item">{{ sequence }}</li>
+                        {% endfor %}
+                    {% else %}
+                        <li class="list-group-item">No Test Case Step Sequence Defined</li>
+                    {% endif %}
+
+                    {% endif %}
+
+                    <!-- Test Step Sequence fields -->
+                    {% if type == 'teststep_sequence' %}
+
+                    <li class="list-group-item h5 list-group-item-dark">{{ 'teststep'|name_by_type }}</li>
+                    {% if item.teststeps %}
+                        {% for step in item.teststeps %}
+                        <li class="list-group-item">{{ step }}</li>
+                        {% endfor %}
+                    {% else %}
+                        <li class="list-group-item">No Test Step Defined</li>
+                    {% endif %}
+
+                    {% endif %}
+
+                    <!-- Test Step fields -->
+                    {% if type == 'teststep' %}
+                    <li class="list-group-item h5 list-group-item-dark">Parameters</li>
+                    <li class="list-group-item">
+                        <div class="row border-bottom py-2">
+                            <div class="col-md-3">
+                                <strong>Activity Type</strong>
+                            </div>
+                            <div class="col">
+                                {{ item.activity_type }}
+                            </div>
+                        </div>
+
+                        <div class="row border-bottom py-2">
+                            <div class="col-md-3">
+                                <strong>Locator Type</strong>
+                            </div>
+                            <div class="col-md-9">
+                                {{ item.locator_type }}
+                            </div>
+                        </div>
+
+                        <div class="row border-bottom py-2">
+                            <div class="col-md-3">
+                                <strong>Locator</strong>
+                            </div>
+                            <div class="col-md-9">
+                                {{ item.locator }}
+                            </div>
+                        </div>
+
+                        <div class="row border-bottom py-2">
+                            <div class="col-md-3">
+                                <strong>Optional</strong>
+                            </div>
+                            <div class="col-md-9">
+                                {{ item.optional }}
+                            </div>
+                        </div>
+
+                        <div class="row border-bottom py-2">
+                            <div class="col-md-3">
+                                <strong>Timeout</strong>
+                            </div>
+                            <div class="col-md-9">
+                                {{ item.timeout }}
+                            </div>
+                        </div>
+
+                        <div class="row border-bottom py-2">
+                            <div class="col-md-3">
+                                <strong>Release</strong>
+                            </div>
+                            <div class="col-md-9">
+                                {{ item.release }}
+                            </div>
+                        </div>
+
+                        <div class="row border-bottom py-2">
+                            <div class="col-md-3">
+                                <strong>Value</strong>
+                            </div>
+                            <div class="col-md-9">
+                                {{ item.value }}
+                            </div>
+                        </div>
+
+                        <div class="row border-bottom py-2">
+                            <div class="col-md-3">
+                                <strong>Value 2</strong>
+                            </div>
+                            <div class="col-md-9">
+                                {{ item.value2 }}
+                            </div>
+                        </div>
+
+                        <div class="row border-bottom py-2">
+                            <div class="col-md-3">
+                                <strong>Comparision</strong>
+                            </div>
+                            <div class="col-md-9">
+                                {{ item.comparision }}
+                            </div>
+                        </div>
+                    </li>
+
+                    {% endif %}
+
+                </ul>
+                
+                <!-- buttons -->
+                <p>
+                    <a class="btn btn-primary px-5 my-3 mr-3" href="/{{ type }}/{{ item.id }}/edit" role="button">Edit</a>
+                    <button class="btn btn-danger px-5 my-3" onclick="delete_item('{{ type }}', '{{ item.name}}', '{{ item.id }}')">
+                        Delete
+                    </button>
+                </p>
+               
+            </div>
+        </div>
+    {% endfor %}
+
+
+
+</div>
+</main>
+
+
+{% endblock %}

+ 38 - 0
flask/app/templates/testrun/login.html

@@ -0,0 +1,38 @@
+{% extends "base.html" %}
+
+{% block title %}
+Login
+{% endblock %}
+
+{% block content %}
+<div class="d-flex flex-column text-center mt-5">
+	<h1 class="py-5">Please Login</h1>
+	
+	<!-- login form -->
+	<form method="post">
+		{{ form.hidden_tag() }}
+
+		<p>
+			{% for error in form.username.errors %}
+				<p class="text-danger">{{ error }}</p>
+			{% endfor %}
+			{{ form.username(placeholder="Username") }}
+		</p>
+		<p>
+			{% for error in form.password.errors %}
+				<p class="text-danger">{{ error }}</p>
+			{% endfor %}
+			{{ form.password(placeholder="Password") }}
+		</p>
+		<p>
+			<button class="btn btn-primary px-5" type="submit">Login</button>
+		</p>
+	</form>
+
+	<div class="mt-2">
+		Not registered yet? <a href="/signup">Sign up here</a>
+	</div>
+
+</div>
+
+{% endblock %}

+ 43 - 0
flask/app/templates/testrun/signup.html

@@ -0,0 +1,43 @@
+{% extends "base.html" %}
+
+{% block title %}
+Sign up
+{% endblock %}
+
+{% block content %}
+<div class="d-flex flex-column text-center mt-5">
+	<h1 class="py-5">Register a new user</h1>
+	
+	<!-- sign up form -->
+	<form method="post">
+		{{ form.hidden_tag() }}
+		<p>
+			{% for error in form.username.errors %}
+				<p class="text-danger">{{ error }}</p>
+			{% endfor %}
+			{{ form.username(placeholder="Username") }}
+		</p>
+		<p>
+			{% for error in form.password.errors %}
+				<p class="text-danger">{{ error }}</p>
+			{% endfor %}
+			{{ form.password(placeholder="Password") }}
+		</p>
+		<p>
+			{% for error in form.password2.errors %}
+				<p class="text-danger">{{ error }}</p>
+			{% endfor %}
+			{{ form.password2(placeholder="Password again") }}
+		</p>
+		<p>
+			<button class="btn btn-primary px-5" type="submit">Sign up</button>
+		</p>
+	</form>
+
+	<div class="mt-2">
+		Already registered? <a href="/login">Log in here</a>
+	</div>
+
+</div>
+
+{% endblock %}

+ 80 - 0
flask/app/utils.py

@@ -0,0 +1,80 @@
+from app import models
+
+#
+# item categories
+#
+def getItemCategories():
+	categories = {}
+	categories['main'] = [
+		'testrun',
+		'testcase_sequence',
+		'testcase',
+		'teststep_sequence',
+		'teststep',
+	]
+
+	return categories
+
+#
+# generate choices of items
+#
+
+def getTestCaseSequences():
+	choices = []
+	for item in models.TestCaseSequence.query.all():
+		choices.append((f'{item.id}', item.name))
+	return choices
+
+def getDataFiles():
+	choices = []
+	for item in models.DataFile.query.all():
+		choices.append((f'{item.id}', item.filename))
+	return choices
+
+def getTestCases():
+	choices = []
+	for item in models.TestCase.query.all():
+		choices.append((f'{item.id}', item.name))
+	return choices
+
+def getTestStepSequences():
+	choices = []
+	for item in models.TestStepSequence.query.all():
+		choices.append((f'{item.id}', item.name))
+	return choices
+
+def getTestSteps():
+	choices = []
+	for item in models.TestStepExecution.query.all():
+		choices.append((f'{item.id}', item.name))
+	return choices
+
+def getClassNames():
+	choices = []
+	for item in models.ClassName.query.all():
+		choices.append((f'{item.id}', item.name))
+	return choices
+
+def getBrowserTypes():
+	choices = []
+	for item in models.BrowserType.query.all():
+		choices.append((f'{item.id}', item.name))
+	return choices
+
+def getTestCaseTypes():
+	choices = []
+	for item in models.TestCaseType.query.all():
+		choices.append((f'{item.id}', item.name))
+	return choices
+
+def getActivityTypes():
+	choices = []
+	for item in models.ActivityType.query.all():
+		choices.append((f'{item.id}', item.name))
+	return choices
+
+def getLocatorTypes():
+	choices = []
+	for item in models.LocatorType.query.all():
+		choices.append((f'{item.id}', item.name))
+	return choices

+ 355 - 0
flask/app/views.py

@@ -0,0 +1,355 @@
+
+from flask import render_template, redirect, flash, request, url_for
+from flask_login import login_required, current_user, login_user, logout_user
+from app import app, db, models, forms, utils
+from datetime import datetime
+
+@app.route('/')
+@login_required
+def index():
+	return render_template('testrun/index.html', items=utils.getItemCategories())
+
+@app.route('/<string:item_type>')
+@login_required
+def item_list(item_type):
+	# get item list by type
+	if item_type == 'testrun':
+		items = models.Testrun.query.all()
+	elif item_type == 'testcase_sequence':
+		items = models.TestCaseSequence.query.all()
+	elif item_type == 'testcase':
+		items = models.TestCase.query.all()
+	elif item_type == 'teststep_sequence':
+		items = models.TestStepSequence.query.all()
+	elif item_type == 'teststep':
+		items = models.TestStepExecution.query.all()
+	else:
+		flash(f'Item type "{item_type}" does not exist.', 'warning')
+		return redirect(url_for('index'))
+
+
+	return render_template('testrun/item_list.html', type=item_type, items=items)
+
+
+'''
+@app.route('/index')
+@login_required
+def index_0():
+	# get the whole bunch of items
+	items = {}
+	items['testruns'] = models.Testrun.query.all()
+	items['testcase_sequances'] = models.TestCaseSequence.query.all()
+	#items['datafiles'] = models.DataFile.query.all()
+	items['testcases'] = models.TestCase.query.all()
+	items['teststep_sequences'] = models.TestStepSequence.query.all()
+	items['teststeps'] = models.TestStepExecution.query.all()
+
+	return render_template('testrun/index.html', items=items)
+'''
+
+@app.route('/<string:item_type>/<int:item_id>', methods=['GET', 'POST'])
+@login_required
+def get_item(item_type, item_id):
+	# get item by type and id
+	if item_type == 'testrun':
+		item = models.Testrun.query.get(item_id)
+	elif item_type == 'testcase_sequence':
+		item = models.TestCaseSequence.query.get(item_id)
+	elif item_type == 'testcase':
+		item = models.TestCase.query.get(item_id)
+	elif item_type == 'teststep_sequence':
+		item = models.TestStepSequence.query.get(item_id)
+	elif item_type == 'teststep':
+		item = models.TestStepExecution.query.get(item_id)
+	else:
+		return 'ERROR: Wrong Item'
+
+	
+
+
+	return render_template('testrun/item.html', type=item_type, item=item)
+
+
+@app.route('/<string:item_type>/<int:item_id>/delete', methods=['POST'])
+@login_required
+def delete_item(item_type, item_id):
+	#
+	# delete item
+	#
+	if request.method == 'POST':
+		# get item by type and id
+		if item_type == 'testrun':
+			item = models.Testrun.query.get(item_id)
+		elif item_type == 'testcase_sequence':
+			item = models.TestCaseSequence.query.get(item_id)
+		elif item_type == 'testcase':
+			item = models.TestCase.query.get(item_id)
+		elif item_type == 'teststep_sequence':
+			item = models.TestStepSequence.query.get(item_id)
+		elif item_type == 'teststep':
+			item = models.TestStepExecution.query.get(item_id)
+		else:
+			return 'ERROR: Wrong Item'
+
+		db.session.delete(item)
+		db.session.commit()
+		flash(f'Item "{item.name}" successfully deleted.', 'success')
+		return redirect(url_for('item_list', item_type=item_type))
+
+	return 'ERROR: Wring request method'
+
+
+@app.route('/<string:item_type>/<int:item_id>/edit', methods=['GET', 'POST'])
+@login_required
+def edit_item(item_type, item_id):
+	#
+	# edit item
+	#
+	if item_type == 'testrun':
+		item = models.Testrun.query.get(item_id)
+		form = forms.TestrunCreateForm.new()
+		if request.method == 'GET':
+			form.testcase_sequences.data = [f'{x.id}' for x in item.testcase_sequences]
+	elif item_type == 'testcase_sequence':
+		item = models.TestCaseSequence.query.get(item_id)
+		form = forms.TestCaseSequenceCreateForm.new()
+		if request.method == 'GET':
+			form.classname.data = f'{item.classname.id}'
+			form.datafiles.data = [f'{x.id}' for x in item.datafiles]
+			form.testcases.data = [f'{x.id}' for x in item.testcases]
+	elif item_type == 'testcase':
+		item = models.TestCase.query.get(item_id)
+		form = forms.TestCaseCreateForm.new()
+		if request.method == 'GET':
+			form.classname.data = f'{item.classname.id}'
+			form.browser_type.data = f'{item.browser_type.id}'
+			form.testcase_type.data = f'{item.testcase_type.id}'
+			form.testcase_stepsequences.data = [f'{x.id}' for x in item.teststep_sequences]
+	elif item_type == 'teststep_sequence':
+		item = models.TestStepSequence.query.get(item_id)
+		form = forms.TestStepSequenceCreateForm.new()
+		if request.method == 'GET':
+			form.classname.data = f'{item.classname.id}'
+			form.teststeps.data =[f'{x.id}' for x in item.teststeps]
+	elif item_type == 'teststep':
+		item = models.TestStepExecution.query.get(item_id)
+		form = forms.TestStepCreateForm.new()
+		if request.method == 'GET':
+			form.activity_type.data = f'{item.activity_type.id}'
+			form.locator_type.data = f'{item.locator_type.id}'
+			# model extension
+			form.locator.data = item.locator
+			if item.optional:
+				form.optional.data = '1'
+			else:
+				form.optional.data = '2'
+			form.timeout.data = item.timeout
+			form.release.data = item.release
+			form.value.data = item.value
+			form.value2.data = item.value2
+			form.comparision.data = item.comparision
+
+	else:
+		flash('ERROR: Wrong Item Type', 'warning')
+		return None
+
+	if request.method == 'GET':
+		form.name.data = item.name
+		form.description.data = item.description
+
+	if  form.validate_on_submit():
+		# update item data
+		item.name = form.name.data
+		item.description = form.description.data
+		# testrun
+		if item_type == 'testrun':
+			item.editor = current_user
+			item.edited = datetime.utcnow()
+			item.testcase_sequences=[models.TestCaseSequence.query.get(int(x)) for x in form.testcase_sequences.data]
+		# testcase sequence
+		elif item_type == 'testcase_sequence':
+			item.editor = current_user
+			item.edited = datetime.utcnow()
+			item.classname = models.ClassName.query.get(int(form.classname.data))
+			item.datafiles = [models.DataFile.query.get(int(x)) for x in form.datafiles.data]
+			item.testcases = [models.TestCase.query.get(int(x)) for x in form.testcases.data]
+		# testcase
+		elif item_type == 'testcase':
+			item.editor = current_user
+			item.edited = datetime.utcnow()
+			item.classname = models.ClassName.query.get(int(form.classname.data))
+			item.browser_type = models.BrowserType.query.get(int(form.browser_type.data))
+			item.testcase_type = models.TestCaseType.query.get(int(form.testcase_type.data))
+			item.teststep_sequences = [models.TestStepSequence.query.get(int(x)) for x in form.testcase_stepsequences.data]
+		# step sequence
+		elif item_type == 'teststep_sequence':
+			item.editor = current_user
+			item.edited = datetime.utcnow()
+			item.classname = models.ClassName.query.get(int(form.classname.data))
+			item.teststeps = [models.TestStepExecution.query.get(int(x)) for x in form.teststeps.data]
+		# test step
+		elif item_type == 'teststep':
+			item.editor = current_user
+			item.edited = datetime.utcnow()
+			item.activity_type = models.ActivityType.query.get(int(form.activity_type.data))
+			item.locator_type = models.LocatorType.query.get(int(form.locator_type.data))
+			# model extension
+			item.locator=form.locator.data
+			item.optional=[None, True, False][int(form.optional.data)]
+			item.timeout=form.timeout.data
+			item.release=form.release.data
+			item.value=form.value.data
+			item.value2=form.value2.data
+			item.comparision=form.comparision.data
+			
+
+		# update item in db
+		db.session.commit()
+		flash(f'Item "{item.name}" successfully updated.', 'success')
+		return redirect(url_for('index'))
+
+
+	return render_template('testrun/edit_item.html', type=item_type, item=item, form=form)
+
+
+
+
+
+@app.route('/<string:item_type>/new', methods=['GET', 'POST'])
+@login_required
+def new_item(item_type):
+	#
+	# create new item
+	#
+	if item_type == 'testrun':
+		form = forms.TestrunCreateForm.new()
+		chips = ['testcase_sequences']
+	elif item_type == 'testcase_sequence':
+		form = forms.TestCaseSequenceCreateForm.new()
+		chips = ['datafiles', 'testcases']
+	elif item_type == 'testcase':
+		form = forms.TestCaseCreateForm.new()
+		chips = ['testcase_stepsequences']
+	elif item_type == 'teststep_sequence':
+		form = forms.TestStepSequenceCreateForm.new()
+		chips = ['teststeps']
+	elif item_type == 'teststep':
+		form = forms.TestStepCreateForm.new()
+		chips = []
+	else:
+		flash('ERROR: Wrong Item Type', 'warning')
+		return None
+
+	if form.validate_on_submit():
+		# create new item
+		# testrun
+		if item_type == 'testrun':
+			item = models.Testrun(
+				name=form.name.data,
+				description=form.description.data,
+				creator=current_user,
+				testcase_sequences=[models.TestCaseSequence.query.get(int(x)) for x in form.testcase_sequences.data],
+			)
+		# testcase sequence
+		elif item_type == 'testcase_sequence':
+			item = models.TestCaseSequence(
+				name=form.name.data,
+				description=form.description.data,
+				creator=current_user,
+				classname=models.ClassName.query.get(int(form.classname.data)),
+				datafiles=[models.DataFile.query.get(int(x)) for x in form.datafiles.data],
+				testcases=[models.TestCase.query.get(int(x)) for x in form.testcases.data],
+			)
+		# testcase
+		elif item_type == 'testcase':
+			item = models.TestCase(
+				name=form.name.data,
+				description=form.description.data,
+				creator=current_user,
+				classname=models.ClassName.query.get(int(form.classname.data)),
+				browser_type=models.BrowserType.query.get(int(form.browser_type.data)),
+				testcase_type=models.TestCaseType.query.get(int(form.testcase_type.data)),
+				teststep_sequences=[models.TestStepSequence.query.get(int(x)) for x in form.testcase_stepsequences.data],
+			)
+		# step sequence
+		elif item_type == 'teststep_sequence':
+			item = models.TestStepSequence(
+				name=form.name.data,
+				description=form.description.data,
+				creator=current_user,
+				classname=models.ClassName.query.get(int(form.classname.data)),
+				teststeps=[models.TestStepExecution.query.get(int(x)) for x in form.teststeps.data],
+			)
+		# test step
+		elif item_type == 'teststep':
+			print('*********** Timeout')
+			print(type(form.timeout.data))
+			item = models.TestStepExecution(
+				name=form.name.data,
+				description=form.description.data,
+				creator=current_user,
+				activity_type=models.ActivityType.query.get(int(form.activity_type.data)),
+				locator_type=models.LocatorType.query.get(int(form.locator_type.data)),
+				# model extension
+				locator=form.locator.data,
+				optional=[None, True, False][int(form.optional.data)],
+				timeout=form.timeout.data,
+				release=form.release.data,
+				value=form.value.data,
+				value2=form.value2.data,
+				comparision=form.comparision.data,
+			)
+
+		# save item to db
+		db.session.add(item)
+		db.session.commit()
+		flash(f'Item "{item.name}" successfully created.', 'success')
+		return redirect(url_for('index'))
+
+	return render_template('testrun/create_item.html', type=item_type, chips=chips, form=form)
+	#return render_template('testrun/edit_item.html', type=item_type, item=None, form=form)
+
+#
+# user authentication
+#
+
+@app.route('/signup', methods=['GET', 'POST'])
+def signup():
+	if current_user.is_authenticated:
+		return redirect(url_for('index'))
+
+	form = forms.SingupForm()
+	if form.validate_on_submit():
+		# create user
+		user = models.User(username=form.username.data)
+		user.set_password(form.password.data)
+		db.session.add(user)
+		db.session.commit()
+		# login
+		login_user(user, remember=True)
+		flash(f'User {user.username.capitalize()} successfully created!', 'succsess')
+		return redirect(url_for('index'))
+
+	return render_template('testrun/signup.html', form=form)
+
+@app.route('/login', methods=['GET', 'POST'])
+def login():
+	if current_user.is_authenticated:
+		return redirect(url_for('index'))
+
+	form = forms.LoginForm()
+	if form.validate_on_submit():
+		user = models.User.query.filter_by(username=form.username.data).first()
+		if user and user.verify_password(form.password.data):
+			login_user(user, remember=True)
+			flash(f'Welcome {user.username.capitalize()}!', 'success')
+			return redirect(url_for('index'))
+
+	return render_template('testrun/login.html', form=form)
+
+@app.route('/logout')
+def logout():
+	logout_user()
+
+	return redirect(url_for('login'))
+	

+ 1 - 0
flask/application.py

@@ -0,0 +1 @@
+from app import app

File diff suppressed because it is too large
+ 1258 - 0
flask/application.sublime-workspace


+ 6 - 0
flask/discussion.txt

@@ -0,0 +1,6 @@
+First: In the TestSteps apart from the locatoryType should also be the locator - otherwise it won't work :)
+Also there are new fields on this level: Optional (boolean), Timeout (dec 5,2), Release (string), value2 (String), comparison (string). You can see the fields for instance in SimpleTheInternet.xlsx. I guess that shouldn't be a big problem :)
+The assignment of elements to other elements though is not ideal. Think of a bank with 100 systems, 1000 Testruns and 100.000 Teststeps. As I see the assignment from one level to the next is simply by clicking on a (full) list of elements of the lower level. I think this needs another idea :)
+About testing the functionality: There is a repository baangt-Docker that uses VNC and has all necessary dependencies installed (Firefox, Geckodriver, etc.). This should be a good base for testing. 
+anyway. About interfacing the database with the testrun: In the same way as now in TestRun.py
+called from __init__ there are methods _loadJSONTestRunDefinition and _loadExcelTestRunDefinition. Either you export the structures below a given testrun from the database into JSON-format, that can be read by this method OR we make a new method like _loadDbTestRunDefinition where the DB-Structures are filled into Dict.

+ 1 - 0
flask/migrations/README

@@ -0,0 +1 @@
+Generic single-database configuration.

+ 45 - 0
flask/migrations/alembic.ini

@@ -0,0 +1,45 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# 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

+ 96 - 0
flask/migrations/env.py

@@ -0,0 +1,96 @@
+from __future__ import with_statement
+
+import logging
+from logging.config import fileConfig
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+
+from alembic import context
+
+# 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)
+logger = logging.getLogger('alembic.env')
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+from flask import current_app
+config.set_main_option(
+    'sqlalchemy.url', current_app.config.get(
+        'SQLALCHEMY_DATABASE_URI').replace('%', '%%'))
+target_metadata = current_app.extensions['migrate'].db.metadata
+
+# 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, target_metadata=target_metadata, literal_binds=True
+    )
+
+    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.
+
+    """
+
+    # this callback is used to prevent an auto-migration from being generated
+    # when there are no changes to the schema
+    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
+    def process_revision_directives(context, revision, directives):
+        if getattr(config.cmd_opts, 'autogenerate', False):
+            script = directives[0]
+            if script.upgrade_ops.is_empty():
+                directives[:] = []
+                logger.info('No changes in schema detected.')
+
+    connectable = engine_from_config(
+        config.get_section(config.config_ini_section),
+        prefix='sqlalchemy.',
+        poolclass=pool.NullPool,
+    )
+
+    with connectable.connect() as connection:
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata,
+            process_revision_directives=process_revision_directives,
+            **current_app.extensions['migrate'].configure_args
+        )
+
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()

+ 24 - 0
flask/migrations/script.py.mako

@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+    ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+    ${downgrades if downgrades else "pass"}

+ 212 - 0
flask/migrations/versions/afb044a83b98_.py

@@ -0,0 +1,212 @@
+"""empty message
+
+Revision ID: afb044a83b98
+Revises: 
+Create Date: 2020-02-03 16:38:12.041603
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'afb044a83b98'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('activity_types',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(length=64), nullable=False),
+    sa.Column('description', sa.String(length=512), nullable=False),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('browser_types',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(length=64), nullable=False),
+    sa.Column('description', sa.String(length=512), nullable=False),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('classnames',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(length=64), nullable=False),
+    sa.Column('description', sa.String(length=512), nullable=False),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('locator_types',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(length=64), nullable=False),
+    sa.Column('description', sa.String(length=512), nullable=False),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('testcase_types',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(length=64), nullable=False),
+    sa.Column('description', sa.String(length=512), nullable=False),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('users',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('username', sa.String(length=64), nullable=False),
+    sa.Column('password', sa.String(length=128), nullable=False),
+    sa.Column('created', sa.DateTime(), nullable=False),
+    sa.Column('lastlogin', sa.DateTime(), nullable=False),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('username')
+    )
+    op.create_table('datafiles',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('filename', sa.String(length=64), nullable=False),
+    sa.Column('created', sa.DateTime(), nullable=False),
+    sa.Column('creator_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('testcase_sequences',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(length=64), nullable=False),
+    sa.Column('description', sa.String(length=512), nullable=False),
+    sa.Column('created', sa.DateTime(), nullable=False),
+    sa.Column('creator_id', sa.Integer(), nullable=False),
+    sa.Column('edited', sa.DateTime(), nullable=True),
+    sa.Column('editor_id', sa.Integer(), nullable=True),
+    sa.Column('classname_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['classname_id'], ['classnames.id'], ),
+    sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ),
+    sa.ForeignKeyConstraint(['editor_id'], ['users.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('testcases',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(length=64), nullable=False),
+    sa.Column('description', sa.String(length=512), nullable=False),
+    sa.Column('created', sa.DateTime(), nullable=False),
+    sa.Column('creator_id', sa.Integer(), nullable=False),
+    sa.Column('edited', sa.DateTime(), nullable=True),
+    sa.Column('editor_id', sa.Integer(), nullable=True),
+    sa.Column('classname_id', sa.Integer(), nullable=False),
+    sa.Column('browser_type_id', sa.Integer(), nullable=False),
+    sa.Column('testcase_type_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['browser_type_id'], ['browser_types.id'], ),
+    sa.ForeignKeyConstraint(['classname_id'], ['classnames.id'], ),
+    sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ),
+    sa.ForeignKeyConstraint(['editor_id'], ['users.id'], ),
+    sa.ForeignKeyConstraint(['testcase_type_id'], ['testcase_types.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('testruns',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(length=64), nullable=False),
+    sa.Column('description', sa.String(length=512), nullable=False),
+    sa.Column('created', sa.DateTime(), nullable=False),
+    sa.Column('creator_id', sa.Integer(), nullable=False),
+    sa.Column('edited', sa.DateTime(), nullable=True),
+    sa.Column('editor_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ),
+    sa.ForeignKeyConstraint(['editor_id'], ['users.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('teststep_sequences',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(length=64), nullable=False),
+    sa.Column('description', sa.String(length=512), nullable=False),
+    sa.Column('created', sa.DateTime(), nullable=False),
+    sa.Column('creator_id', sa.Integer(), nullable=False),
+    sa.Column('edited', sa.DateTime(), nullable=True),
+    sa.Column('editor_id', sa.Integer(), nullable=True),
+    sa.Column('classname_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['classname_id'], ['classnames.id'], ),
+    sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ),
+    sa.ForeignKeyConstraint(['editor_id'], ['users.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('global_teststep_executions',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(length=64), nullable=False),
+    sa.Column('description', sa.String(length=512), nullable=False),
+    sa.Column('created', sa.DateTime(), nullable=False),
+    sa.Column('creator_id', sa.Integer(), nullable=False),
+    sa.Column('edited', sa.DateTime(), nullable=True),
+    sa.Column('editor_id', sa.Integer(), nullable=True),
+    sa.Column('activity_type_id', sa.Integer(), nullable=False),
+    sa.Column('locator_type_id', sa.Integer(), nullable=False),
+    sa.Column('teststep_sequence_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['activity_type_id'], ['activity_types.id'], ),
+    sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ),
+    sa.ForeignKeyConstraint(['editor_id'], ['users.id'], ),
+    sa.ForeignKeyConstraint(['locator_type_id'], ['locator_types.id'], ),
+    sa.ForeignKeyConstraint(['teststep_sequence_id'], ['teststep_sequences.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    op.create_table('testcase_sequence_case',
+    sa.Column('testcase_sequence_id', sa.Integer(), nullable=False),
+    sa.Column('testcase_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['testcase_id'], ['testcases.id'], ),
+    sa.ForeignKeyConstraint(['testcase_sequence_id'], ['testcase_sequences.id'], ),
+    sa.PrimaryKeyConstraint('testcase_sequence_id', 'testcase_id')
+    )
+    op.create_table('testcase_sequence_datafile',
+    sa.Column('testcase_sequence_id', sa.Integer(), nullable=False),
+    sa.Column('datafile_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['datafile_id'], ['datafiles.id'], ),
+    sa.ForeignKeyConstraint(['testcase_sequence_id'], ['testcase_sequences.id'], ),
+    sa.PrimaryKeyConstraint('testcase_sequence_id', 'datafile_id')
+    )
+    op.create_table('testcase_stepsequence',
+    sa.Column('testcase_id', sa.Integer(), nullable=False),
+    sa.Column('teststep_sequence_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['testcase_id'], ['testcases.id'], ),
+    sa.ForeignKeyConstraint(['teststep_sequence_id'], ['teststep_sequences.id'], ),
+    sa.PrimaryKeyConstraint('testcase_id', 'teststep_sequence_id')
+    )
+    op.create_table('testrun_casesequence',
+    sa.Column('testrun_id', sa.Integer(), nullable=False),
+    sa.Column('testcase_sequence_id', sa.Integer(), nullable=False),
+    sa.ForeignKeyConstraint(['testcase_sequence_id'], ['testcase_sequences.id'], ),
+    sa.ForeignKeyConstraint(['testrun_id'], ['testruns.id'], ),
+    sa.PrimaryKeyConstraint('testrun_id', 'testcase_sequence_id')
+    )
+    op.create_table('teststep_executions',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.String(length=64), nullable=False),
+    sa.Column('description', sa.String(length=512), nullable=False),
+    sa.Column('created', sa.DateTime(), nullable=False),
+    sa.Column('creator_id', sa.Integer(), nullable=False),
+    sa.Column('edited', sa.DateTime(), nullable=True),
+    sa.Column('editor_id', sa.Integer(), nullable=True),
+    sa.Column('activity_type_id', sa.Integer(), nullable=False),
+    sa.Column('locator_type_id', sa.Integer(), nullable=False),
+    sa.Column('teststep_sequence_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['activity_type_id'], ['activity_types.id'], ),
+    sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ),
+    sa.ForeignKeyConstraint(['editor_id'], ['users.id'], ),
+    sa.ForeignKeyConstraint(['locator_type_id'], ['locator_types.id'], ),
+    sa.ForeignKeyConstraint(['teststep_sequence_id'], ['teststep_sequences.id'], ),
+    sa.PrimaryKeyConstraint('id')
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('teststep_executions')
+    op.drop_table('testrun_casesequence')
+    op.drop_table('testcase_stepsequence')
+    op.drop_table('testcase_sequence_datafile')
+    op.drop_table('testcase_sequence_case')
+    op.drop_table('global_teststep_executions')
+    op.drop_table('teststep_sequences')
+    op.drop_table('testruns')
+    op.drop_table('testcases')
+    op.drop_table('testcase_sequences')
+    op.drop_table('datafiles')
+    op.drop_table('users')
+    op.drop_table('testcase_types')
+    op.drop_table('locator_types')
+    op.drop_table('classnames')
+    op.drop_table('browser_types')
+    op.drop_table('activity_types')
+    # ### end Alembic commands ###

+ 40 - 0
flask/migrations/versions/c7fc082b1d55_.py

@@ -0,0 +1,40 @@
+"""empty message
+
+Revision ID: c7fc082b1d55
+Revises: afb044a83b98
+Create Date: 2020-02-06 13:40:37.665770
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'c7fc082b1d55'
+down_revision = 'afb044a83b98'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('teststep_executions', sa.Column('comparision', sa.String(), nullable=True))
+    op.add_column('teststep_executions', sa.Column('locator', sa.String(), nullable=True))
+    op.add_column('teststep_executions', sa.Column('optional', sa.Boolean(), nullable=True))
+    op.add_column('teststep_executions', sa.Column('release', sa.String(), nullable=True))
+    op.add_column('teststep_executions', sa.Column('timeout', sa.Float(precision='5,2'), nullable=True))
+    op.add_column('teststep_executions', sa.Column('value', sa.String(), nullable=True))
+    op.add_column('teststep_executions', sa.Column('value2', sa.String(), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('teststep_executions', 'value2')
+    op.drop_column('teststep_executions', 'value')
+    op.drop_column('teststep_executions', 'timeout')
+    op.drop_column('teststep_executions', 'release')
+    op.drop_column('teststep_executions', 'optional')
+    op.drop_column('teststep_executions', 'locator')
+    op.drop_column('teststep_executions', 'comparision')
+    # ### end Alembic commands ###

+ 183 - 0
flask/populate_db.py

@@ -0,0 +1,183 @@
+from app import db
+from app.models import *
+
+#DATABASE_URL = 'sqlite:///testrun.db'
+#engine = create_engine(DATABASE_URL)
+
+# create a db.session
+#db.session = db.sessionmaker(bind=engine)
+#db.session = db.session()
+
+# recreate db structure
+db.drop_all()
+db.create_all()
+
+# create users
+print('Creating users...')
+admin = User(username='admin')
+admin.set_password('12345')
+user = User(username='simple_user')
+user.set_password('12345')
+db.session.add(admin)
+db.session.add(user)
+db.session.commit()
+print('Done.')
+
+# create supports
+print('Creating supports...')
+browsers = {
+	'FF': 'Mozilla Firefox',
+	'Chrome': 'Google Chrome',
+	'IE': 'MS Internet Exploer',
+	'Safari': 'Safari',
+	'Edge': 'MS Edge',
+}
+for key, value in browsers.items():
+	browser = BrowserType(name=key, description=value)
+	db.session.add(browser)
+
+testcases = {
+	'Browser': 'Browser',
+	'API-Rest': 'API-Rest',
+	'API-SOAP': 'API-SOAP',
+	'API-oDataV2': 'API-oDataV2',
+	'API-oDataV4': 'API-oDataV4',
+}
+for key, value in testcases.items():
+	testcase = TestCaseType(name=key, description=value)
+	db.session.add(testcase)
+
+activities = {
+	'GOTOURL': 'Go to an URL',
+	'SETTEXT': 'Set Text of an Element',
+	'CLICK': 'Click on an Element',
+}
+for key, value in activities.items():
+	activity = ActivityType(name=key, description=value)
+	db.session.add(activity)
+
+locators = {
+	'xpath': 'Locate via XPATH-Expression',
+	'css': 'Locate via CSS-Path of the Element',
+	'id': 'Locate via ID of the Element',
+}
+for key, value in locators.items():
+	locator = LocatorType(name=key, description=value)
+	db.session.add(locator)
+
+classnames = {
+	'Class A': 'A Simple Class Name',
+	'Class B': 'One more Simple Class Name',
+	'Class C': 'A Complex Class Name',
+}
+for key, value in classnames.items():
+	classname = ClassName(name=key, description=value)
+	db.session.add(classname)
+
+db.session.commit()
+print('Done.')
+
+# create mains
+print('Creating mains...')
+for i in range(5):
+	testrun  = Testrun(
+		name=f'Testrun #{i}',
+		description=f'Testrun #{i} is intended for testing the application UI. There are several features wich are described here.',
+		creator=admin,
+	)
+	db.session.add(testrun)
+
+db.session.commit()
+
+for i in range(5):
+	if i < 3:
+		u = admin
+	else:
+		u = user
+	testseq  = TestCaseSequence(
+		name=f'Test Case Sequence #{i}',
+		description=f'Test Case Sequence #{i} is intended for testing the application UI. There are several features wich are described here.',
+		creator=u,
+		classname=classname,
+	)
+	testseq.testrun.append(testrun)
+	if i == 2 or i == 3:
+		another_testrun = Testrun.query.get(i)
+		testseq.testrun.append(another_testrun)
+	db.session.add(testseq)
+	
+db.session.commit()
+
+for i in range(5):
+	if i < 3:
+		u = admin
+	else:
+		u = user
+	datafile  = DataFile(
+		filename=f'data_file_{i}.xlsx',
+		creator=u,
+	)
+	testseq = TestCaseSequence.query.get(i+1)
+	datafile.testcase_sequence.append(testseq)
+	db.session.add(datafile)
+	
+db.session.commit()
+
+# get supports
+browsers = BrowserType.query.all()
+testtypes = TestCaseType.query.all()
+activities = ActivityType.query.all()
+locators = LocatorType.query.all()
+
+for i in range(12):
+	if i%3 == 0:
+		u = admin
+	else:
+		u = user
+	testcase  = TestCase(
+		name=f'Test Case #{i}',
+		description=f'Test Case #{i} is intended for testing the application UI. There are several features wich are described here.',
+		creator=u,
+		classname=classname,
+		browser_type=browsers[i%len(browsers)],
+		testcase_type=testtypes[i%len(testtypes)], 
+	)
+	testseq = TestCaseSequence.query.get(i%5 + 1)
+	testcase.testcase_sequence.append(testseq)
+	db.session.add(testcase)
+
+db.session.commit()
+
+for i in range(7):
+	if i%3 == 0:
+		u = admin
+	else:
+		u = user
+	teststepseq  = TestStepSequence(
+		name=f'Test Step Sequence #{i}',
+		description=f'Test Step Sequence #{i} is intended for testing the application UI. There are several features wich are described here.',
+		creator=u,
+		classname=classname,
+	)
+	teststepseq.testcase.append(testcase)
+	db.session.add(testcase)
+
+db.session.commit()
+
+for i in range(4):
+	if i%3 == 0:
+		u = admin
+	else:
+		u = user
+	teststepex  = TestStepExecution(
+		name=f'Test Step Execution #{i}',
+		description=f'Test Step Execution #{i} is intended for testing the application UI. There are several features wich are described here.',
+		creator=u,
+		activity_type=activities[i%len(activities)],
+		locator_type=locators[i%len(locators)],
+		teststep_sequence=TestStepSequence.query.get(i%2 + 1),
+	)
+	db.session.add(testcase)
+
+db.session.commit()
+print('Done.')

+ 6 - 0
flask/requirements.txt

@@ -0,0 +1,6 @@
+flask>=1.1.1
+sqlalchemy>=1.3.12
+flask_sqlalchemy>=2.4.1
+flask_login>=0.4.1
+flask_wtf>=0.14.2
+flask_migrate>=2.5.2

+ 9 - 0
flask/start_docker.sh

@@ -0,0 +1,9 @@
+docker build -t testrun .
+echo .....
+echo User = admin, password = 12345 . goto http://localhost:5000
+echo .....
+echo To stop docker:
+echo docker ps
+echo docker stop _name_of_dockerimage_
+echo .....
+docker run -d -p 5000:5000 testrun