Browse Source

docker env for tests and services

aguryev 3 years ago
parent
commit
579c3a2063
56 changed files with 6143 additions and 180276 deletions
  1. 1 0
      .gitignore
  2. 24 32
      api/Dockerfile
  3. 38 0
      api/Dockerfile.dev
  4. 83 0
      api/Dockerfile.prev
  5. BIN
      api/app/uploads/0ad6e841-3edf-4324-91d0-b01fc611c54f
  6. BIN
      api/app/uploads/10af23d2-81c0-439e-8885-8fa243f39930
  7. BIN
      api/app/uploads/2df7ba30-be6b-4147-930f-61cb3178e1a7
  8. BIN
      api/app/uploads/4835c1d6-ddd0-4762-9ad0-b447b2a11f02
  9. BIN
      api/app/uploads/5e143547-a07e-498f-b894-4b4107c275b1
  10. BIN
      api/app/uploads/7d103d73-a5b2-4fbb-b9d6-074f1c375198
  11. BIN
      api/app/uploads/7f14f626-10a8-4490-a12c-022fd38c79d9
  12. BIN
      api/app/uploads/8be789f0-51c4-4551-81c6-8311db1bee68
  13. BIN
      api/app/uploads/a41efe99-5804-45fe-acb0-aca4d27f3b57
  14. BIN
      api/app/uploads/b2eb597e-6dd0-4d5c-aac0-8958351bb02f
  15. BIN
      api/app/uploads/c27f36bf-6364-4cbc-97c7-e6a1a97e62dd
  16. 64 9
      api/patch/DataBaseORM.py
  17. 0 180217
      api/requirements.txt
  18. 3 0
      api/runservice.dev.sh
  19. 3 0
      api/runservice.rq.sh
  20. 2 2
      api/runservice.sh
  21. 17 0
      docker-compose.dev.yml
  22. 65 0
      docker-compose.mysql.yml
  23. 42 0
      docker-compose.test.yml
  24. 65 0
      docker-compose.yml
  25. 12 13
      files/Dockerfile
  26. 37 0
      files/Dockerfile.dev
  27. 4 0
      files/runservice.dev.sh
  28. 15 0
      requirements.test.txt
  29. 1 1
      run_services_db_permanent.sh
  30. 2 2
      run_services_in_docker.sh
  31. 34 0
      run_tests.sh
  32. BIN
      ui/.coverage
  33. 82 0
      ui/htmlcov/app___init___py.html
  34. 430 0
      ui/htmlcov/app_charts_py.html
  35. 75 0
      ui/htmlcov/app_docutils_py.html
  36. 113 0
      ui/htmlcov/app_filters_py.html
  37. 260 0
      ui/htmlcov/app_forms_py.html
  38. 712 0
      ui/htmlcov/app_models_py.html
  39. 1195 0
      ui/htmlcov/app_utils___init___py.html
  40. 109 0
      ui/htmlcov/app_utils_datafile_handler_py.html
  41. 641 0
      ui/htmlcov/app_utils_xlsx_handler_py.html
  42. 805 0
      ui/htmlcov/app_views_py.html
  43. 589 0
      ui/htmlcov/coverage_html.js
  44. 147 0
      ui/htmlcov/index.html
  45. 9 0
      ui/htmlcov/jquery.ba-throttle-debounce.min.js
  46. 99 0
      ui/htmlcov/jquery.hotkeys.js
  47. 53 0
      ui/htmlcov/jquery.isonscreen.js
  48. 5 0
      ui/htmlcov/jquery.min.js
  49. 2 0
      ui/htmlcov/jquery.tablesorter.min.js
  50. BIN
      ui/htmlcov/keybd_closed.png
  51. BIN
      ui/htmlcov/keybd_open.png
  52. 1 0
      ui/htmlcov/status.json
  53. 291 0
      ui/htmlcov/style.css
  54. 4 0
      ui/run_tests.sh
  55. 2 0
      ui/runservice.sh
  56. 7 0
      ui/waitforpsql.sh

+ 1 - 0
.gitignore

@@ -24,6 +24,7 @@ api/ini
 custom_globals.json
 run_docker_mysql.sh
 run_docker_postgres.sh
+test.html
 
 *.pyc
 *.db

+ 24 - 32
api/Dockerfile

@@ -1,31 +1,25 @@
-FROM ubuntu:bionic
+FROM ubuntu:focal
 
 LABEL Testrun API
 
 ENV PYTHONDONTWRITEBYTECODE 1
-ENV FLASK_APP "app.py"
-ENV FLASK_DEBUG True
-ENV LANG C.UTF-8
-ENV LC_ALL C.UTF-8
+ENV DEBIAN_FRONTEND noninteractive
 
 RUN apt-get update && apt-get install -y \
-    python3 python3-pip \
+    python3.8 python3-pip python3.8-venv libpq-dev \
     fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 \
     libnspr4 libnss3 lsb-release xdg-utils libxss1 libdbus-glib-1-2 libgbm1 \
-    curl unzip wget \
-    xvfb
+    openjdk-8-jre \
+    curl wget 
 
 # install geckodriver
-
 RUN GECKODRIVER_VERSION=`curl https://github.com/mozilla/geckodriver/releases/latest | grep -Po 'v[0-9]+.[0-9]+.[0-9]+'` && \
     wget https://github.com/mozilla/geckodriver/releases/download/$GECKODRIVER_VERSION/geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz && \
     tar -zxf geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz -C /usr/local/bin && \
     chmod +x /usr/local/bin/geckodriver && \
     rm geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz
 
-
-# install firefox and chrome
-
+# install firefox
 RUN FIREFOX_SETUP=firefox-setup.tar.bz2 && \
     apt-get purge firefox && \
     wget -O $FIREFOX_SETUP "https://download.mozilla.org/?product=firefox-latest&os=linux64" && \
@@ -33,50 +27,48 @@ RUN FIREFOX_SETUP=firefox-setup.tar.bz2 && \
     ln -s /opt/firefox/firefox /usr/bin/firefox && \
     rm $FIREFOX_SETUP
 
+# install chrome
 RUN CHROME_SETUP=google-chrome.deb && \
     wget -O $CHROME_SETUP "https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb" && \
     dpkg -i $CHROME_SETUP && \
     apt-get install -y -f && \
     rm $CHROME_SETUP
 
-
 # install browsermob proxy
+RUN apt-get install -y unzip
 RUN BROWSERMOB_SETUP=browsermob-proxy.zip && \
     wget -O $BROWSERMOB_SETUP "https://github.com/lightbody/browsermob-proxy/releases/download/browsermob-proxy-2.1.4/browsermob-proxy-2.1.4-bin.zip" && \
     unzip $BROWSERMOB_SETUP -d /opt/ && \
     rm $BROWSERMOB_SETUP
 
-# install java: needed by browsermob
-RUN apt-get install -y openjdk-8-jre
-
-# install virtual env
-RUN apt update
-RUN apt-get install -y python3.6-venv
-RUN apt-get install -y libpq-dev
-
 # create working directory
 RUN mkdir /baangt
 WORKDIR /baangt
 
-RUN pip3 install gunicorn
-RUN pip3 install pymysql cryptography psycopg2
-RUN pip3 install pyqt5==5.14
-
-COPY requirements.txt requirements.txt
-RUN pip3 install -r requirements.txt
-
 # set up baangt environment
 RUN ln -s /opt/browsermob-proxy-2.1.4 /baangt/browsermob-proxy
-
 RUN mkdir /baangt/browserDrivers && \
     ln -s /usr/local/bin/geckodriver /baangt/browserDrivers/geckodriver
 
+# create virtual env
+RUN pip3 install virtualenv && virtualenv venv
+RUN venv/bin/pip install --upgrade pip
+
+# install prod requirements
+RUN venv/bin/pip install gunicorn pymysql cryptography psycopg2
+
+# install package requirements
+COPY requirements.txt requirements.txt
+RUN venv/bin/pip install -r requirements.txt
+RUN venv/bin/pip install xlsclone
+
+# copy package
 ADD . /baangt
 
-# add patches here
-#COPY DataBaseORM.py /usr/local/lib/python3.6/dist-packages/baangt/base/DataBaseORM.py
+RUN chmod +x runservice.sh runservice.rq.sh runservice.dev.sh
 
-RUN chmod +x runservice.sh
+# add patches here
+COPY patch/DataBaseORM.py /baangt/venv/lib/python3.8/site-packages/baangt/base/DataBaseORM.py
 
 EXPOSE 5000
 ENTRYPOINT ["./runservice.sh"]

+ 38 - 0
api/Dockerfile.dev

@@ -0,0 +1,38 @@
+FROM ubuntu:bionic
+
+LABEL Testrun API
+
+RUN apt-get update && apt-get install -y \
+    python3 python3-pip \
+    fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 \
+    libnspr4 libnss3 lsb-release xdg-utils libxss1 libdbus-glib-1-2 libgbm1 \
+    curl unzip wget xvfb
+
+# install virtual env
+#RUN apt update
+#RUN apt-get install -y python3.6-venv libpq-dev
+
+# create working directory
+RUN mkdir /baangt
+WORKDIR /baangt
+
+RUN pip3 install --upgrade pip
+
+COPY requirements.txt requirements.txt
+RUN pip3 install -r requirements.txt
+
+ADD . /baangt
+
+RUN chmod +x runservice.dev.sh
+
+# set env
+ENV PYTHONDONTWRITEBYTECODE 1
+ENV LC_ALL C.UTF-8
+ENV LANG C.UTF-8
+ENV FLASK_APP app.py
+ENV FLASK_DEBUG 1
+ENV FLASK_RUN_HOST=0.0.0.0
+ENV FLASK_RUN_PORT=5000
+
+EXPOSE 5000
+ENTRYPOINT ["./runservice.dev.sh"]

+ 83 - 0
api/Dockerfile.prev

@@ -0,0 +1,83 @@
+FROM ubuntu:bionic
+
+LABEL Testrun API
+
+ENV PYTHONDONTWRITEBYTECODE 1
+ENV FLASK_APP "app.py"
+ENV FLASK_DEBUG True
+ENV LANG C.UTF-8
+ENV LC_ALL C.UTF-8
+
+RUN apt-get update && apt-get install -y \
+    python3 python3-pip \
+    fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 \
+    libnspr4 libnss3 lsb-release xdg-utils libxss1 libdbus-glib-1-2 libgbm1 \
+    curl unzip wget \
+    xvfb
+
+# install geckodriver
+
+RUN GECKODRIVER_VERSION=`curl https://github.com/mozilla/geckodriver/releases/latest | grep -Po 'v[0-9]+.[0-9]+.[0-9]+'` && \
+    wget https://github.com/mozilla/geckodriver/releases/download/$GECKODRIVER_VERSION/geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz && \
+    tar -zxf geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz -C /usr/local/bin && \
+    chmod +x /usr/local/bin/geckodriver && \
+    rm geckodriver-$GECKODRIVER_VERSION-linux64.tar.gz
+
+
+# install firefox and chrome
+
+RUN FIREFOX_SETUP=firefox-setup.tar.bz2 && \
+    apt-get purge firefox && \
+    wget -O $FIREFOX_SETUP "https://download.mozilla.org/?product=firefox-latest&os=linux64" && \
+    tar xjf $FIREFOX_SETUP -C /opt/ && \
+    ln -s /opt/firefox/firefox /usr/bin/firefox && \
+    rm $FIREFOX_SETUP
+
+RUN CHROME_SETUP=google-chrome.deb && \
+    wget -O $CHROME_SETUP "https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb" && \
+    dpkg -i $CHROME_SETUP && \
+    apt-get install -y -f && \
+    rm $CHROME_SETUP
+
+
+# install browsermob proxy
+RUN BROWSERMOB_SETUP=browsermob-proxy.zip && \
+    wget -O $BROWSERMOB_SETUP "https://github.com/lightbody/browsermob-proxy/releases/download/browsermob-proxy-2.1.4/browsermob-proxy-2.1.4-bin.zip" && \
+    unzip $BROWSERMOB_SETUP -d /opt/ && \
+    rm $BROWSERMOB_SETUP
+
+# install java: needed by browsermob
+RUN apt-get install -y openjdk-8-jre
+
+# install virtual env
+RUN apt update
+RUN apt-get install -y python3.6-venv
+RUN apt-get install -y libpq-dev
+
+# create working directory
+RUN mkdir /baangt
+WORKDIR /baangt
+
+RUN pip3 install --upgrade pip
+RUN pip3 install gunicorn
+RUN pip3 install pymysql cryptography psycopg2
+#RUN pip3 install pyqt5==5.14.2
+
+COPY requirements.txt requirements.txt
+RUN pip3 install -r requirements.txt
+
+# set up baangt environment
+RUN ln -s /opt/browsermob-proxy-2.1.4 /baangt/browsermob-proxy
+
+RUN mkdir /baangt/browserDrivers && \
+    ln -s /usr/local/bin/geckodriver /baangt/browserDrivers/geckodriver
+
+ADD . /baangt
+
+# add patches here
+#COPY DataBaseORM.py /usr/local/lib/python3.6/dist-packages/baangt/base/DataBaseORM.py
+
+RUN chmod +x runservice.sh
+
+EXPOSE 5000
+ENTRYPOINT ["./runservice.sh"]

BIN
api/app/uploads/0ad6e841-3edf-4324-91d0-b01fc611c54f


BIN
api/app/uploads/10af23d2-81c0-439e-8885-8fa243f39930


BIN
api/app/uploads/2df7ba30-be6b-4147-930f-61cb3178e1a7


BIN
api/app/uploads/4835c1d6-ddd0-4762-9ad0-b447b2a11f02


BIN
api/app/uploads/5e143547-a07e-498f-b894-4b4107c275b1


BIN
api/app/uploads/7d103d73-a5b2-4fbb-b9d6-074f1c375198


BIN
api/app/uploads/7f14f626-10a8-4490-a12c-022fd38c79d9


BIN
api/app/uploads/8be789f0-51c4-4551-81c6-8311db1bee68


BIN
api/app/uploads/a41efe99-5804-45fe-acb0-aca4d27f3b57


BIN
api/app/uploads/b2eb597e-6dd0-4d5c-aac0-8958351bb02f


BIN
api/app/uploads/c27f36bf-6364-4cbc-97c7-e6a1a97e62dd


+ 64 - 9
api/patch/DataBaseORM.py

@@ -1,12 +1,11 @@
 from sqlalchemy import Column, String, Integer, DateTime, Boolean, Table, ForeignKey
-#from sqlalchemy.types import Binary(16), TypeDecorator
 from sqlalchemy.orm import relationship
 from sqlalchemy import create_engine
 from sqlalchemy.ext.declarative import declarative_base
-import os
-import re
-import uuid
+import os, re, uuid
+from datetime import timedelta
 from baangt.base.PathManagement import ManagedPaths
+from baangt.base.GlobalConstants import EXECUTION_STAGE, TIMING_DURATION, TESTCASESTATUS
 
 
 managedPaths = ManagedPaths()
@@ -47,6 +46,7 @@ class TestrunLog(base):
 	statusOk = Column(Integer, nullable=False)
 	statusFailed = Column(Integer, nullable=False)
 	statusPaused = Column(Integer, nullable=False)
+	RLPJson = Column(String, nullable=True) 
 	# relationships
 	globalVars = relationship('GlobalAttribute')
 	testcase_sequences = relationship('TestCaseSequenceLog')
@@ -58,7 +58,14 @@ class TestrunLog(base):
 	@property
 	def duration(self):
 		return (self.endTime - self.startTime).seconds
-	
+
+	@property
+	def stage(self):
+		for gv in self.globalVars:
+			if gv.name == EXECUTION_STAGE:
+				return gv.value
+
+		return None
 
 	def __str__(self):
 		return str(uuid.UUID(bytes=self.id))
@@ -73,8 +80,8 @@ class TestrunLog(base):
 				'Paused': self.statusPaused,
 				'Error': self.statusFailed,
 				'LogFile': self.logfileName,
-				'StartTime': self.startTime.strftime('%Y-%m-%d %H:%M:%S'),
-				'EndTime': self.endTime.strftime('%Y-%m-%d %H:%M:%S'),
+				'StartTime': self.startTime.strftime('%H:%M:%S'),
+				'EndTime': self.endTime.strftime('%H:%M:%S'),
 				'Duration': str(self.endTime - self.startTime),
 			},
 			'GlobalSettings': {gv.name: gv.value for gv in self.globalVars},
@@ -109,11 +116,21 @@ class TestCaseSequenceLog(base):
 	__tablename__ = 'testCaseSequences'
 	# columns
 	id = Column(Binary(16), primary_key=True, default=uuidAsBytes)
+	number = Column(Integer, nullable=False)
 	testrun_id = Column(Binary(16), ForeignKey('testruns.id'), nullable=False)
 	# relationships
 	testrun = relationship('TestrunLog', foreign_keys=[testrun_id])
 	testcases = relationship('TestCaseLog')
 
+	@property
+	def duration(self):
+		#
+		# duration in seconds
+		#
+
+		return sum([tc.duration for tc in self.testcases if tc.duration])
+
+
 	def __str__(self):
 		return str(uuid.UUID(bytes=self.id))
 
@@ -135,15 +152,53 @@ class TestCaseLog(base):
 	__tablename__ = 'testCases'
 	# columns
 	id = Column(Binary(16), primary_key=True, default=uuidAsBytes)
+	number = Column(Integer, nullable=False)
 	testcase_sequence_id = Column(Binary(16), ForeignKey('testCaseSequences.id'), nullable=False)
 	# relationships
 	testcase_sequence = relationship('TestCaseSequenceLog', foreign_keys=[testcase_sequence_id])
-	fields = relationship('TestCaseField')
+	fields = relationship('TestCaseField', lazy='select')
 	networkInfo = relationship('TestCaseNetworkInfo')
 
+	@property
+	def status(self):
+		#
+		# testcase status
+		#
+
+		for field in self.fields:
+			if field.name == TESTCASESTATUS:
+				return field.value
+
+		return None	
+
+	@property
+	def duration(self):
+		#
+		# duration in seconds
+		#
+
+		for field in self.fields:
+			if field.name == TIMING_DURATION:
+				# parse value from H:M:S.microseconds
+				m = re.search(r'(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)', field.value)
+				if m:
+					factors = {
+						'hours': 3600,
+						'minutes': 60,
+						'seconds': 1,
+					}
+					#duration = {key: float(value) for key, value in m.groupdict().items()}
+					#return timedelta(**duration)
+					return sum([factors[key]*float(value) for key, value in m.groupdict().items()])
+
+		return None
+
 	def __str__(self):
 		return str(uuid.UUID(bytes=self.id))
 
+	def fields_as_dict(self):
+		return {pr.name: pr.value for pr in self.fields}
+
 	def to_json(self):
 		return {
 			'id': str(self),
@@ -206,4 +261,4 @@ class TestCaseNetworkInfo(base):
 
 # create tables
 #if __name__ == '__main__':
-base.metadata.create_all(engine)
+#base.metadata.create_all(engine)

File diff suppressed because it is too large
+ 0 - 180217
api/requirements.txt


+ 3 - 0
api/runservice.dev.sh

@@ -0,0 +1,3 @@
+#!/bin/bash
+source venv/bin/activate
+exec flask run

+ 3 - 0
api/runservice.rq.sh

@@ -0,0 +1,3 @@
+#!/bin/bash
+source venv/bin/activate
+rq worker -u $REDIS_URL/0 baangt-tasks

+ 2 - 2
api/runservice.sh

@@ -1,3 +1,3 @@
-#!/bin/sh
-echo source venv/bin/activate
+#!/bin/bash
+source venv/bin/activate
 exec gunicorn -b :5000 --access-logfile - --error-logfile - app:app

+ 17 - 0
docker-compose.dev.yml

@@ -0,0 +1,17 @@
+version: '3.8'
+
+services:
+  api:
+    build:
+      context: ./api
+      dockerfile: Dockerfile.dev
+    ports:
+      - "8000:5000"
+    environment:
+      - BAANGT_DATAFILE_HOST=172.17.0.1:5050
+  datafiles:
+    build:
+      context: ./files
+      dockerfile: Dockerfile.dev
+    ports:
+      - "5050:5000"

+ 65 - 0
docker-compose.mysql.yml

@@ -0,0 +1,65 @@
+version: '3.8'
+
+services:
+  ui:
+    build: ./ui
+    ports: 
+      - "8000:5000"
+    environment:
+      - BAANGT_DATAFILE_HOST=172.17.0.1:5050
+      - BAANGT_API_HOST=172.17.0.1:5000
+      - DATABASE_URL=mysql+pymysql://baangt:12345@172.17.0.1:3305/definition
+    depends_on:
+      - db
+    links:
+      - db
+  api:
+    build: ./api
+    ports:
+      - "5000:5000"
+    environment:
+      - BAANGT_DATAFILE_HOST=172.17.0.1:5050
+      - DATABASE_URL=mysql+pymysql://baangt:12345@172.17.0.1:3305/execution
+      - REDIS_URL=redis://172.17.0.1:6380
+    depends_on:
+      - db
+      - rd
+      - rq
+    links:
+      - db
+      - rd
+      - rq
+  datafiles:
+    build: ./files
+    ports:
+      - "5050:5000"
+  rq:
+    build: ./api
+    entrypoint: ./runservice.rq.sh
+    environment:
+      - BAANGT_DATAFILE_HOST=172.17.0.1:5050
+      - DATABASE_URL=mysql+pymysql://baangt:12345@172.17.0.1:3305/execution
+      - REDIS_URL=redis://172.17.0.1:6380
+    depends_on:
+      - db
+      - rd
+    links:
+      - db
+      - rd
+  rd:
+    image: redis:5-alpine
+    ports:
+      - "6380:6379"
+  db:
+    build: ./mysql
+    ports:
+      - "3305:3306"
+    environment:
+      - MYSQL_RANDOM_ROOT_PASSWORD=yes
+  
+
+  
+  
+
+
+

+ 42 - 0
docker-compose.test.yml

@@ -0,0 +1,42 @@
+version: '3.8'
+
+services:
+  api:
+    build: ./api
+    entrypoint: ./runservice.dev.sh
+    command: run
+    ports:
+      - "8000:5000"
+    environment:
+      - BAANGT_DATAFILE_HOST=172.17.0.1:5050
+      - REDIS_URL=redis://172.17.0.1:6380
+      - FLASK_APP=app.py
+      - FLASK_DEBUG=1
+      - FLASK_RUN_HOST=0.0.0.0
+      - FLASK_RUN_PORT=5000
+    depends_on:
+      - rd
+      - rq
+  datafiles:
+    build: ./files
+    entrypoint: ./runservice.dev.sh
+    ports:
+      - "5050:5000"
+    environment:
+      - FLASK_APP=app.py
+      - FLASK_DEBUG=1
+      - FLASK_RUN_HOST=0.0.0.0
+      - FLASK_RUN_PORT=5000
+  rq:
+    build: ./api
+    entrypoint: ./runservice.rq.sh
+    environment:
+      - BAANGT_DATAFILE_HOST=172.17.0.1:5050
+      - REDIS_URL=redis://172.17.0.1:6380
+    depends_on:
+      - rd
+  rd:
+    image: redis:5-alpine
+    ports:
+      - "6380:6379"
+

+ 65 - 0
docker-compose.yml

@@ -0,0 +1,65 @@
+version: '3.8'
+
+services:
+  ui:
+    build: ./ui
+    ports: 
+      - "8000:5000"
+    environment:
+      - BAANGT_DATAFILE_HOST=172.17.0.1:5050
+      - BAANGT_API_HOST=172.17.0.1:5000
+      - DATABASE_URL=postgresql://baangt:12345@172.17.0.1:5430/definition
+    depends_on:
+      - db
+    links:
+      - db
+  api:
+    build: ./api
+    ports:
+      - "5000:5000"
+    environment:
+      - BAANGT_DATAFILE_HOST=172.17.0.1:5050
+      - DATABASE_URL=postgresql://baangt:12345@172.17.0.1:5430/execution
+      - REDIS_URL=redis://172.17.0.1:6380
+    depends_on:
+      - db
+      - rd
+      - rq
+    links:
+      - db
+      - rd
+      - rq
+  datafiles:
+    build: ./files
+    ports:
+      - "5050:5000"
+  rq:
+    build: ./api
+    entrypoint: ./runservice.rq.sh
+    environment:
+      - BAANGT_DATAFILE_HOST=172.17.0.1:5050
+      - DATABASE_URL=postgresql://baangt:12345@172.17.0.1:5430/execution
+      - REDIS_URL=redis://172.17.0.1:6380
+    depends_on:
+      - db
+      - rd
+    links:
+      - db
+      - rd
+  rd:
+    image: redis:5-alpine
+    ports:
+      - "6380:6379"
+  db:
+    build: ./postgres
+    ports:
+      - "5430:5432"
+    environment:
+      - POSTGRES_PASSWORD=12345
+  
+
+  
+  
+
+
+

+ 12 - 13
files/Dockerfile

@@ -2,29 +2,28 @@ FROM python:3.8.2-alpine
 
 LABEL BAANGT File Service
 
-#RUN adduser -D baangt
-
-
-#WORKDIR /home/baangt
+ENV PYTHONDONTWRITEBYTECODE 1
 
+# create working directory
 RUN mkdir /baangt
 WORKDIR /baangt
 
-COPY requirements.txt requirements.txt
+# create virtual env
 RUN python -m venv venv
-RUN venv/bin/pip install -r requirements.txt
+RUN venv/bin/pip install --upgrade pip
+
+# install prod requirements
 RUN venv/bin/pip install gunicorn
 
-#ADD . /home/baangt
+# install package requirements
+COPY requirements.txt requirements.txt
+RUN venv/bin/pip install -r requirements.txt
+
+# copy package
 ADD . /baangt
 
 RUN chmod +x runservice.sh
-#RUN chown -R baangt:baangt ./
-
-#ENV FLASK_APP "app.py"
-#ENV FLASK_DEBUG True
-
-#USER baangt
+RUN chmod +x runservice.dev.sh
 
 EXPOSE 5000
 ENTRYPOINT ["./runservice.sh"]

+ 37 - 0
files/Dockerfile.dev

@@ -0,0 +1,37 @@
+FROM python:3.8.2-alpine
+
+LABEL BAANGT File Service
+
+#RUN adduser -D baangt
+
+
+#WORKDIR /home/baangt
+
+RUN mkdir /baangt
+WORKDIR /baangt
+
+COPY requirements.txt requirements.txt
+RUN python -m venv venv
+RUN venv/bin/pip install --upgrade pip
+RUN venv/bin/pip install -r requirements.txt
+
+#ADD . /home/baangt
+ADD . /baangt
+
+RUN chmod +x runservice.dev.sh
+#RUN chown -R baangt:baangt ./
+
+#ENV FLASK_APP "app.py"
+#ENV FLASK_DEBUG True
+
+#USER baangt
+
+# set env
+ENV PYTHONDONTWRITEBYTECODE 1
+ENV FLASK_APP app.py
+ENV FLASK_DEBUG 1
+ENV FLASK_RUN_HOST=0.0.0.0
+ENV FLASK_RUN_PORT=5000
+
+EXPOSE 5000
+ENTRYPOINT ["./runservice.dev.sh"]

+ 4 - 0
files/runservice.dev.sh

@@ -0,0 +1,4 @@
+#!/bin/sh
+source venv/bin/activate
+python set_uploads.py
+exec flask run

+ 15 - 0
requirements.test.txt

@@ -0,0 +1,15 @@
+attrs==20.2.0
+coverage==5.2.1
+importlib-metadata==1.7.0
+iniconfig==1.0.1
+lxml==4.5.2
+more-itertools==8.5.0
+packaging==20.4
+pluggy==0.13.1
+py==1.9.0
+pyparsing==2.4.7
+pytest==6.0.1
+pytest-cov==2.10.1
+six==1.15.0
+toml==0.10.1
+zipp==3.1.0

+ 1 - 1
run_services_db_permanent.sh

@@ -83,7 +83,7 @@ docker run -d --name rq-worker  \
 # postgres
 echo
 echo Starting PostgreSQL container...
-docker run -d -p ${port}:5432 --name baangt-postgres -e POSTGRES_PASSWORD=12345 baangt-postgres:latest
+docker run -d -p ${ports[4]}:5432 --name baangt-postgres -e POSTGRES_PASSWORD=12345 baangt-postgres:latest
 docker start baangt-postgres
 
 # Data Files

+ 2 - 2
run_services_in_docker.sh

@@ -71,8 +71,8 @@ echo Starting Redis Worker...
 docker run -d --name rq-worker  \
     -e BAANGT_DATAFILE_HOST=172.17.0.1:${ports[2]} --rm \
     -e DATABASE_URL=postgresql://baangt:12345@172.17.0.1:${ports[4]}/baangt \
-    -e REDIS_URL=redis://172.17.0.1:${ports[3]}/0 --entrypoint rq \
-    baangt-api:latest worker -u redis://172.17.0.1:${ports[3]}/0 baangt-tasks 
+    -e REDIS_URL=redis://172.17.0.1:${ports[3]}/0 --entrypoint "./runservice.rq.sh" \
+    baangt-api:latest
 
 # PostgreSQL
 echo

+ 34 - 0
run_tests.sh

@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# build flag
+build=0
+
+# parse arguments
+args=("$@")
+
+i=0
+while [ $i -lt $# ]
+do
+  case ${args[$i]} in
+    "-build")
+      build=1
+      ;;
+  esac
+  let i=$i+1
+done
+
+# start services in docker
+if [ $build = 1 ]
+then
+  docker-compose -f docker-compose.test.yml up -d --build
+else
+  docker-compose -f docker-compose.test.yml up -d
+fi
+
+# runtests
+cd ui
+pytest -v --cov=app
+coverage html
+
+# stop services
+docker-compose down

BIN
ui/.coverage


+ 82 - 0
ui/htmlcov/app___init___py.html

@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=emulateIE7" />
+    <title>Coverage for app/__init__.py: 100%</title>
+    <link rel="stylesheet" href="style.css" type="text/css">
+    <script type="text/javascript" src="jquery.min.js"></script>
+    <script type="text/javascript" src="jquery.hotkeys.js"></script>
+    <script type="text/javascript" src="jquery.isonscreen.js"></script>
+    <script type="text/javascript" src="coverage_html.js"></script>
+    <script type="text/javascript">
+        jQuery(document).ready(coverage.pyfile_ready);
+    </script>
+</head>
+<body class="pyfile">
+<div id="header">
+    <div class="content">
+        <h1>Coverage for <b>app/__init__.py</b> :
+            <span class="pc_cov">100%</span>
+        </h1>
+        <img id="keyboard_icon" src="keybd_closed.png" alt="Show keyboard shortcuts" />
+        <h2 class="stats">
+            13 statements &nbsp;
+            <button type="button" class="run shortkey_r button_toggle_run" title="Toggle lines run">13 run</button>
+            <button type="button" class="mis show_mis shortkey_m button_toggle_mis" title="Toggle lines missing">0 missing</button>
+            <button type="button" class="exc show_exc shortkey_x button_toggle_exc" title="Toggle lines excluded">0 excluded</button>
+        </h2>
+    </div>
+</div>
+<div class="help_panel">
+    <img id="panel_icon" src="keybd_open.png" alt="Hide keyboard shortcuts" />
+    <p class="legend">Hot-keys on this page</p>
+    <div>
+    <p class="keyhelp">
+        <span class="key">r</span>
+        <span class="key">m</span>
+        <span class="key">x</span>
+        <span class="key">p</span> &nbsp; toggle line displays
+    </p>
+    <p class="keyhelp">
+        <span class="key">j</span>
+        <span class="key">k</span> &nbsp; next/prev highlighted chunk
+    </p>
+    <p class="keyhelp">
+        <span class="key">0</span> &nbsp; (zero) top of page
+    </p>
+    <p class="keyhelp">
+        <span class="key">1</span> &nbsp; (one) first highlighted chunk
+    </p>
+    </div>
+</div>
+<div id="source">
+    <p id="t1" class="run"><span class="n"><a href="#t1">1</a></span><span class="t"><span class="key">import</span> <span class="nam">os</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t2" class="run"><span class="n"><a href="#t2">2</a></span><span class="t"><span class="key">from</span> <span class="nam">flask</span> <span class="key">import</span> <span class="nam">Flask</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t3" class="run"><span class="n"><a href="#t3">3</a></span><span class="t"><span class="key">from</span> <span class="nam">flask_login</span> <span class="key">import</span> <span class="nam">LoginManager</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t4" class="run"><span class="n"><a href="#t4">4</a></span><span class="t"><span class="key">from</span> <span class="nam">flask_sqlalchemy</span> <span class="key">import</span> <span class="nam">SQLAlchemy</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t5" class="run"><span class="n"><a href="#t5">5</a></span><span class="t"><span class="key">from</span> <span class="nam">flask_migrate</span> <span class="key">import</span> <span class="nam">Migrate</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t6" class="run"><span class="n"><a href="#t6">6</a></span><span class="t"><span class="key">from</span> <span class="nam">config</span> <span class="key">import</span> <span class="nam">Config</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t7" class="pln"><span class="n"><a href="#t7">7</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
+    <p id="t8" class="pln"><span class="n"><a href="#t8">8</a></span><span class="t"><span class="com"># create application</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t9" class="run"><span class="n"><a href="#t9">9</a></span><span class="t"><span class="nam">app</span> <span class="op">=</span> <span class="nam">Flask</span><span class="op">(</span><span class="nam">__name__</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t10" class="run"><span class="n"><a href="#t10">10</a></span><span class="t"><span class="nam">app</span><span class="op">.</span><span class="nam">config</span><span class="op">.</span><span class="nam">from_object</span><span class="op">(</span><span class="nam">Config</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t11" class="pln"><span class="n"><a href="#t11">11</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
+    <p id="t12" class="pln"><span class="n"><a href="#t12">12</a></span><span class="t"><span class="com"># initialize services</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t13" class="run"><span class="n"><a href="#t13">13</a></span><span class="t"><span class="nam">db</span> <span class="op">=</span> <span class="nam">SQLAlchemy</span><span class="op">(</span><span class="nam">app</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t14" class="run"><span class="n"><a href="#t14">14</a></span><span class="t"><span class="nam">migrate</span> <span class="op">=</span> <span class="nam">Migrate</span><span class="op">(</span><span class="nam">app</span><span class="op">,</span> <span class="nam">db</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t15" class="run"><span class="n"><a href="#t15">15</a></span><span class="t"><span class="nam">login_manager</span> <span class="op">=</span> <span class="nam">LoginManager</span><span class="op">(</span><span class="nam">app</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t16" class="run"><span class="n"><a href="#t16">16</a></span><span class="t"><span class="nam">login_manager</span><span class="op">.</span><span class="nam">login_view</span> <span class="op">=</span> <span class="str">'login'</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t17" class="pln"><span class="n"><a href="#t17">17</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
+    <p id="t18" class="run"><span class="n"><a href="#t18">18</a></span><span class="t"><span class="key">from</span> <span class="nam">app</span> <span class="key">import</span> <span class="nam">views</span><span class="op">,</span> <span class="nam">models</span><span class="op">,</span> <span class="nam">filters</span>&nbsp;</span><span class="r"></span></p>
+</div>
+<div id="footer">
+    <div class="content">
+        <p>
+            <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.io">coverage.py v5.2.1</a>,
+            created at 2020-09-09 00:00 +0300
+        </p>
+    </div>
+</div>
+</body>
+</html>

File diff suppressed because it is too large
+ 430 - 0
ui/htmlcov/app_charts_py.html


+ 75 - 0
ui/htmlcov/app_docutils_py.html

@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=emulateIE7" />
+    <title>Coverage for app/docutils.py: 100%</title>
+    <link rel="stylesheet" href="style.css" type="text/css">
+    <script type="text/javascript" src="jquery.min.js"></script>
+    <script type="text/javascript" src="jquery.hotkeys.js"></script>
+    <script type="text/javascript" src="jquery.isonscreen.js"></script>
+    <script type="text/javascript" src="coverage_html.js"></script>
+    <script type="text/javascript">
+        jQuery(document).ready(coverage.pyfile_ready);
+    </script>
+</head>
+<body class="pyfile">
+<div id="header">
+    <div class="content">
+        <h1>Coverage for <b>app/docutils.py</b> :
+            <span class="pc_cov">100%</span>
+        </h1>
+        <img id="keyboard_icon" src="keybd_closed.png" alt="Show keyboard shortcuts" />
+        <h2 class="stats">
+            1 statements &nbsp;
+            <button type="button" class="run shortkey_r button_toggle_run" title="Toggle lines run">1 run</button>
+            <button type="button" class="mis show_mis shortkey_m button_toggle_mis" title="Toggle lines missing">0 missing</button>
+            <button type="button" class="exc show_exc shortkey_x button_toggle_exc" title="Toggle lines excluded">0 excluded</button>
+        </h2>
+    </div>
+</div>
+<div class="help_panel">
+    <img id="panel_icon" src="keybd_open.png" alt="Hide keyboard shortcuts" />
+    <p class="legend">Hot-keys on this page</p>
+    <div>
+    <p class="keyhelp">
+        <span class="key">r</span>
+        <span class="key">m</span>
+        <span class="key">x</span>
+        <span class="key">p</span> &nbsp; toggle line displays
+    </p>
+    <p class="keyhelp">
+        <span class="key">j</span>
+        <span class="key">k</span> &nbsp; next/prev highlighted chunk
+    </p>
+    <p class="keyhelp">
+        <span class="key">0</span> &nbsp; (zero) top of page
+    </p>
+    <p class="keyhelp">
+        <span class="key">1</span> &nbsp; (one) first highlighted chunk
+    </p>
+    </div>
+</div>
+<div id="source">
+    <p id="t1" class="run"><span class="n"><a href="#t1">1</a></span><span class="t"><span class="nam">topics</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t2" class="pln"><span class="n"><a href="#t2">2</a></span><span class="t">        <span class="str">'get_started'</span><span class="op">:</span> <span class="str">'Get Started'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t3" class="pln"><span class="n"><a href="#t3">3</a></span><span class="t">        <span class="str">'testrun'</span><span class="op">:</span> <span class="str">'Test Run'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t4" class="pln"><span class="n"><a href="#t4">4</a></span><span class="t">        <span class="str">'testcase_sequence'</span><span class="op">:</span> <span class="str">'Test Case Sequence'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t5" class="pln"><span class="n"><a href="#t5">5</a></span><span class="t">        <span class="str">'testcase'</span><span class="op">:</span> <span class="str">'Test Case'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t6" class="pln"><span class="n"><a href="#t6">6</a></span><span class="t">        <span class="str">'teststep_sequence'</span><span class="op">:</span> <span class="str">'Test Step Sequence'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t7" class="pln"><span class="n"><a href="#t7">7</a></span><span class="t">        <span class="str">'teststep'</span><span class="op">:</span> <span class="str">'Test Step'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t8" class="pln"><span class="n"><a href="#t8">8</a></span><span class="t">        <span class="str">'datafile'</span><span class="op">:</span> <span class="str">'DataFile'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t9" class="pln"><span class="n"><a href="#t9">9</a></span><span class="t">        <span class="str">'dashboard'</span><span class="op">:</span> <span class="str">'Dashboard'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t10" class="pln"><span class="n"><a href="#t10">10</a></span><span class="t">        <span class="str">'report'</span><span class="op">:</span> <span class="str">'TestRun Report'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t11" class="pln"><span class="n"><a href="#t11">11</a></span><span class="t"><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
+</div>
+<div id="footer">
+    <div class="content">
+        <p>
+            <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.io">coverage.py v5.2.1</a>,
+            created at 2020-09-09 00:00 +0300
+        </p>
+    </div>
+</div>
+</body>
+</html>

+ 113 - 0
ui/htmlcov/app_filters_py.html

@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=emulateIE7" />
+    <title>Coverage for app/filters.py: 97%</title>
+    <link rel="stylesheet" href="style.css" type="text/css">
+    <script type="text/javascript" src="jquery.min.js"></script>
+    <script type="text/javascript" src="jquery.hotkeys.js"></script>
+    <script type="text/javascript" src="jquery.isonscreen.js"></script>
+    <script type="text/javascript" src="coverage_html.js"></script>
+    <script type="text/javascript">
+        jQuery(document).ready(coverage.pyfile_ready);
+    </script>
+</head>
+<body class="pyfile">
+<div id="header">
+    <div class="content">
+        <h1>Coverage for <b>app/filters.py</b> :
+            <span class="pc_cov">97%</span>
+        </h1>
+        <img id="keyboard_icon" src="keybd_closed.png" alt="Show keyboard shortcuts" />
+        <h2 class="stats">
+            29 statements &nbsp;
+            <button type="button" class="run shortkey_r button_toggle_run" title="Toggle lines run">28 run</button>
+            <button type="button" class="mis show_mis shortkey_m button_toggle_mis" title="Toggle lines missing">1 missing</button>
+            <button type="button" class="exc show_exc shortkey_x button_toggle_exc" title="Toggle lines excluded">0 excluded</button>
+        </h2>
+    </div>
+</div>
+<div class="help_panel">
+    <img id="panel_icon" src="keybd_open.png" alt="Hide keyboard shortcuts" />
+    <p class="legend">Hot-keys on this page</p>
+    <div>
+    <p class="keyhelp">
+        <span class="key">r</span>
+        <span class="key">m</span>
+        <span class="key">x</span>
+        <span class="key">p</span> &nbsp; toggle line displays
+    </p>
+    <p class="keyhelp">
+        <span class="key">j</span>
+        <span class="key">k</span> &nbsp; next/prev highlighted chunk
+    </p>
+    <p class="keyhelp">
+        <span class="key">0</span> &nbsp; (zero) top of page
+    </p>
+    <p class="keyhelp">
+        <span class="key">1</span> &nbsp; (one) first highlighted chunk
+    </p>
+    </div>
+</div>
+<div id="source">
+    <p id="t1" class="run"><span class="n"><a href="#t1">1</a></span><span class="t"><span class="key">from</span> <span class="nam">datetime</span> <span class="key">import</span> <span class="nam">datetime</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t2" class="run"><span class="n"><a href="#t2">2</a></span><span class="t"><span class="key">from</span> <span class="nam">app</span> <span class="key">import</span> <span class="nam">app</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t3" class="run"><span class="n"><a href="#t3">3</a></span><span class="t"><span class="key">from</span> <span class="nam">app</span><span class="op">.</span><span class="nam">docutils</span> <span class="key">import</span> <span class="nam">topics</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t4" class="pln"><span class="n"><a href="#t4">4</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
+    <p id="t5" class="run"><span class="n"><a href="#t5">5</a></span><span class="t"><span class="op">@</span><span class="nam">app</span><span class="op">.</span><span class="nam">template_filter</span><span class="op">(</span><span class="str">'name_by_type'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t6" class="run"><span class="n"><a href="#t6">6</a></span><span class="t"><span class="key">def</span> <span class="nam">item_name</span><span class="op">(</span><span class="nam">item_type</span><span class="op">,</span> <span class="nam">plural</span><span class="op">=</span><span class="key">True</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t7" class="pln"><span class="n"><a href="#t7">7</a></span><span class="t">        <span class="com">#</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t8" class="pln"><span class="n"><a href="#t8">8</a></span><span class="t">        <span class="com"># get name of the item_type</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t9" class="pln"><span class="n"><a href="#t9">9</a></span><span class="t">        <span class="com">#</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t10" class="pln"><span class="n"><a href="#t10">10</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
+    <p id="t11" class="pln"><span class="n"><a href="#t11">11</a></span><span class="t">        <span class="com"># categories</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t12" class="run"><span class="n"><a href="#t12">12</a></span><span class="t">        <span class="key">if</span> <span class="nam">item_type</span> <span class="op">==</span> <span class="str">'main'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t13" class="run"><span class="n"><a href="#t13">13</a></span><span class="t">                <span class="nam">name</span> <span class="op">=</span> <span class="str">'Main Item'</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t14" class="pln"><span class="n"><a href="#t14">14</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
+    <p id="t15" class="pln"><span class="n"><a href="#t15">15</a></span><span class="t">        <span class="com"># main items </span>&nbsp;</span><span class="r"></span></p>
+    <p id="t16" class="run"><span class="n"><a href="#t16">16</a></span><span class="t">        <span class="key">elif</span> <span class="nam">item_type</span> <span class="op">==</span> <span class="str">'testrun'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t17" class="run"><span class="n"><a href="#t17">17</a></span><span class="t">                <span class="nam">name</span> <span class="op">=</span> <span class="str">'Testrun'</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t18" class="run"><span class="n"><a href="#t18">18</a></span><span class="t">        <span class="key">elif</span> <span class="nam">item_type</span> <span class="op">==</span> <span class="str">'testcase_sequence'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t19" class="run"><span class="n"><a href="#t19">19</a></span><span class="t">                <span class="nam">name</span> <span class="op">=</span> <span class="str">'Test Case Sequence'</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t20" class="run"><span class="n"><a href="#t20">20</a></span><span class="t">        <span class="key">elif</span> <span class="nam">item_type</span> <span class="op">==</span> <span class="str">'testcase'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t21" class="run"><span class="n"><a href="#t21">21</a></span><span class="t">                <span class="nam">name</span> <span class="op">=</span> <span class="str">'Test Case'</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t22" class="run"><span class="n"><a href="#t22">22</a></span><span class="t">        <span class="key">elif</span> <span class="nam">item_type</span> <span class="op">==</span> <span class="str">'teststep_sequence'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t23" class="run"><span class="n"><a href="#t23">23</a></span><span class="t">                <span class="nam">name</span> <span class="op">=</span> <span class="str">'Test Step Sequence'</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t24" class="run"><span class="n"><a href="#t24">24</a></span><span class="t">        <span class="key">elif</span> <span class="nam">item_type</span> <span class="op">==</span> <span class="str">'teststep'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t25" class="run"><span class="n"><a href="#t25">25</a></span><span class="t">                <span class="nam">name</span> <span class="op">=</span> <span class="str">'Test Step'</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t26" class="pln"><span class="n"><a href="#t26">26</a></span><span class="t">        <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t27" class="pln"><span class="n"><a href="#t27">27</a></span><span class="t">                <span class="com"># wrong item_type</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t28" class="mis show_mis"><span class="n"><a href="#t28">28</a></span><span class="t">                <span class="key">return</span> <span class="str">''</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t29" class="pln"><span class="n"><a href="#t29">29</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
+    <p id="t30" class="pln"><span class="n"><a href="#t30">30</a></span><span class="t">        <span class="com"># check for plurals</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t31" class="run"><span class="n"><a href="#t31">31</a></span><span class="t">        <span class="key">if</span> <span class="nam">plural</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t32" class="run"><span class="n"><a href="#t32">32</a></span><span class="t">                <span class="nam">name</span> <span class="op">+=</span> <span class="str">'s'</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t33" class="pln"><span class="n"><a href="#t33">33</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
+    <p id="t34" class="run"><span class="n"><a href="#t34">34</a></span><span class="t">        <span class="key">return</span> <span class="nam">name</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t35" class="pln"><span class="n"><a href="#t35">35</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
+    <p id="t36" class="run"><span class="n"><a href="#t36">36</a></span><span class="t"><span class="op">@</span><span class="nam">app</span><span class="op">.</span><span class="nam">template_filter</span><span class="op">(</span><span class="str">'time'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t37" class="pln"><span class="n"><a href="#t37">37</a></span><span class="t"><span class="key">def</span> <span class="nam">format_time</span><span class="op">(</span><span class="nam">time</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t38" class="run"><span class="n"><a href="#t38">38</a></span><span class="t">        <span class="key">return</span> <span class="nam">time</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">'%Y-%m-%d %H:%M'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t39" class="pln"><span class="n"><a href="#t39">39</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
+    <p id="t40" class="run"><span class="n"><a href="#t40">40</a></span><span class="t"><span class="op">@</span><span class="nam">app</span><span class="op">.</span><span class="nam">template_filter</span><span class="op">(</span><span class="str">'doc_title'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t41" class="pln"><span class="n"><a href="#t41">41</a></span><span class="t"><span class="key">def</span> <span class="nam">documentation_title</span><span class="op">(</span><span class="nam">topic</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t42" class="run"><span class="n"><a href="#t42">42</a></span><span class="t">        <span class="key">if</span> <span class="nam">topic</span> <span class="key">in</span> <span class="nam">topics</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t43" class="run"><span class="n"><a href="#t43">43</a></span><span class="t">                <span class="key">return</span> <span class="nam">topics</span><span class="op">[</span><span class="nam">topic</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t44" class="pln"><span class="n"><a href="#t44">44</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
+    <p id="t45" class="run"><span class="n"><a href="#t45">45</a></span><span class="t">        <span class="key">return</span> <span class="str">"Index"</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t46" class="pln"><span class="n"><a href="#t46">46</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
+    <p id="t47" class="run"><span class="n"><a href="#t47">47</a></span><span class="t"><span class="op">@</span><span class="nam">app</span><span class="op">.</span><span class="nam">context_processor</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t48" class="pln"><span class="n"><a href="#t48">48</a></span><span class="t"><span class="key">def</span> <span class="nam">doc_topics</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
+    <p id="t49" class="run"><span class="n"><a href="#t49">49</a></span><span class="t">    <span class="key">return</span> <span class="nam">dict</span><span class="op">(</span><span class="nam">topics</span><span class="op">=</span><span class="nam">topics</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
+</div>
+<div id="footer">
+    <div class="content">
+        <p>
+            <a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="https://coverage.readthedocs.io">coverage.py v5.2.1</a>,
+            created at 2020-09-10 19:25 +0300
+        </p>
+    </div>
+</div>
+</body>
+</html>

File diff suppressed because it is too large
+ 260 - 0
ui/htmlcov/app_forms_py.html


File diff suppressed because it is too large
+ 712 - 0
ui/htmlcov/app_models_py.html


File diff suppressed because it is too large
+ 1195 - 0
ui/htmlcov/app_utils___init___py.html


File diff suppressed because it is too large
+ 109 - 0
ui/htmlcov/app_utils_datafile_handler_py.html


File diff suppressed because it is too large
+ 641 - 0
ui/htmlcov/app_utils_xlsx_handler_py.html


File diff suppressed because it is too large
+ 805 - 0
ui/htmlcov/app_views_py.html


+ 589 - 0
ui/htmlcov/coverage_html.js

@@ -0,0 +1,589 @@
+// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+// For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+// Coverage.py HTML report browser code.
+/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */
+/*global coverage: true, document, window, $ */
+
+coverage = {};
+
+// Find all the elements with shortkey_* class, and use them to assign a shortcut key.
+coverage.assign_shortkeys = function () {
+    $("*[class*='shortkey_']").each(function (i, e) {
+        $.each($(e).attr("class").split(" "), function (i, c) {
+            if (/^shortkey_/.test(c)) {
+                $(document).bind('keydown', c.substr(9), function () {
+                    $(e).click();
+                });
+            }
+        });
+    });
+};
+
+// Create the events for the help panel.
+coverage.wire_up_help_panel = function () {
+    $("#keyboard_icon").click(function () {
+        // Show the help panel, and position it so the keyboard icon in the
+        // panel is in the same place as the keyboard icon in the header.
+        $(".help_panel").show();
+        var koff = $("#keyboard_icon").offset();
+        var poff = $("#panel_icon").position();
+        $(".help_panel").offset({
+            top: koff.top-poff.top,
+            left: koff.left-poff.left
+        });
+    });
+    $("#panel_icon").click(function () {
+        $(".help_panel").hide();
+    });
+};
+
+// Create the events for the filter box.
+coverage.wire_up_filter = function () {
+    // Cache elements.
+    var table = $("table.index");
+    var table_rows = table.find("tbody tr");
+    var table_row_names = table_rows.find("td.name a");
+    var no_rows = $("#no_rows");
+
+    // Create a duplicate table footer that we can modify with dynamic summed values.
+    var table_footer = $("table.index tfoot tr");
+    var table_dynamic_footer = table_footer.clone();
+    table_dynamic_footer.attr('class', 'total_dynamic hidden');
+    table_footer.after(table_dynamic_footer);
+
+    // Observe filter keyevents.
+    $("#filter").on("keyup change", $.debounce(150, function (event) {
+        var filter_value = $(this).val();
+
+        if (filter_value === "") {
+            // Filter box is empty, remove all filtering.
+            table_rows.removeClass("hidden");
+
+            // Show standard footer, hide dynamic footer.
+            table_footer.removeClass("hidden");
+            table_dynamic_footer.addClass("hidden");
+
+            // Hide placeholder, show table.
+            if (no_rows.length > 0) {
+                no_rows.hide();
+            }
+            table.show();
+
+        }
+        else {
+            // Filter table items by value.
+            var hidden = 0;
+            var shown = 0;
+
+            // Hide / show elements.
+            $.each(table_row_names, function () {
+                var element = $(this).parents("tr");
+
+                if ($(this).text().indexOf(filter_value) === -1) {
+                    // hide
+                    element.addClass("hidden");
+                    hidden++;
+                }
+                else {
+                    // show
+                    element.removeClass("hidden");
+                    shown++;
+                }
+            });
+
+            // Show placeholder if no rows will be displayed.
+            if (no_rows.length > 0) {
+                if (shown === 0) {
+                    // Show placeholder, hide table.
+                    no_rows.show();
+                    table.hide();
+                }
+                else {
+                    // Hide placeholder, show table.
+                    no_rows.hide();
+                    table.show();
+                }
+            }
+
+            // Manage dynamic header:
+            if (hidden > 0) {
+                // Calculate new dynamic sum values based on visible rows.
+                for (var column = 2; column < 20; column++) {
+                    // Calculate summed value.
+                    var cells = table_rows.find('td:nth-child(' + column + ')');
+                    if (!cells.length) {
+                        // No more columns...!
+                        break;
+                    }
+
+                    var sum = 0, numer = 0, denom = 0;
+                    $.each(cells.filter(':visible'), function () {
+                        var ratio = $(this).data("ratio");
+                        if (ratio) {
+                            var splitted = ratio.split(" ");
+                            numer += parseInt(splitted[0], 10);
+                            denom += parseInt(splitted[1], 10);
+                        }
+                        else {
+                            sum += parseInt(this.innerHTML, 10);
+                        }
+                    });
+
+                    // Get footer cell element.
+                    var footer_cell = table_dynamic_footer.find('td:nth-child(' + column + ')');
+
+                    // Set value into dynamic footer cell element.
+                    if (cells[0].innerHTML.indexOf('%') > -1) {
+                        // Percentage columns use the numerator and denominator,
+                        // and adapt to the number of decimal places.
+                        var match = /\.([0-9]+)/.exec(cells[0].innerHTML);
+                        var places = 0;
+                        if (match) {
+                            places = match[1].length;
+                        }
+                        var pct = numer * 100 / denom;
+                        footer_cell.text(pct.toFixed(places) + '%');
+                    }
+                    else {
+                        footer_cell.text(sum);
+                    }
+                }
+
+                // Hide standard footer, show dynamic footer.
+                table_footer.addClass("hidden");
+                table_dynamic_footer.removeClass("hidden");
+            }
+            else {
+                // Show standard footer, hide dynamic footer.
+                table_footer.removeClass("hidden");
+                table_dynamic_footer.addClass("hidden");
+            }
+        }
+    }));
+
+    // Trigger change event on setup, to force filter on page refresh
+    // (filter value may still be present).
+    $("#filter").trigger("change");
+};
+
+// Loaded on index.html
+coverage.index_ready = function ($) {
+    // Look for a localStorage item containing previous sort settings:
+    var sort_list = [];
+    var storage_name = "COVERAGE_INDEX_SORT";
+    var stored_list = undefined;
+    try {
+        stored_list = localStorage.getItem(storage_name);
+    } catch(err) {}
+
+    if (stored_list) {
+        sort_list = JSON.parse('[[' + stored_list + ']]');
+    }
+
+    // Create a new widget which exists only to save and restore
+    // the sort order:
+    $.tablesorter.addWidget({
+        id: "persistentSort",
+
+        // Format is called by the widget before displaying:
+        format: function (table) {
+            if (table.config.sortList.length === 0 && sort_list.length > 0) {
+                // This table hasn't been sorted before - we'll use
+                // our stored settings:
+                $(table).trigger('sorton', [sort_list]);
+            }
+            else {
+                // This is not the first load - something has
+                // already defined sorting so we'll just update
+                // our stored value to match:
+                sort_list = table.config.sortList;
+            }
+        }
+    });
+
+    // Configure our tablesorter to handle the variable number of
+    // columns produced depending on report options:
+    var headers = [];
+    var col_count = $("table.index > thead > tr > th").length;
+
+    headers[0] = { sorter: 'text' };
+    for (i = 1; i < col_count-1; i++) {
+        headers[i] = { sorter: 'digit' };
+    }
+    headers[col_count-1] = { sorter: 'percent' };
+
+    // Enable the table sorter:
+    $("table.index").tablesorter({
+        widgets: ['persistentSort'],
+        headers: headers
+    });
+
+    coverage.assign_shortkeys();
+    coverage.wire_up_help_panel();
+    coverage.wire_up_filter();
+
+    // Watch for page unload events so we can save the final sort settings:
+    $(window).unload(function () {
+        try {
+            localStorage.setItem(storage_name, sort_list.toString())
+        } catch(err) {}
+    });
+};
+
+// -- pyfile stuff --
+
+coverage.pyfile_ready = function ($) {
+    // If we're directed to a particular line number, highlight the line.
+    var frag = location.hash;
+    if (frag.length > 2 && frag[1] === 't') {
+        $(frag).addClass('highlight');
+        coverage.set_sel(parseInt(frag.substr(2), 10));
+    }
+    else {
+        coverage.set_sel(0);
+    }
+
+    $(document)
+        .bind('keydown', 'j', coverage.to_next_chunk_nicely)
+        .bind('keydown', 'k', coverage.to_prev_chunk_nicely)
+        .bind('keydown', '0', coverage.to_top)
+        .bind('keydown', '1', coverage.to_first_chunk)
+        ;
+
+    $(".button_toggle_run").click(function (evt) {coverage.toggle_lines(evt.target, "run");});
+    $(".button_toggle_exc").click(function (evt) {coverage.toggle_lines(evt.target, "exc");});
+    $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");});
+    $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");});
+
+    coverage.assign_shortkeys();
+    coverage.wire_up_help_panel();
+
+    coverage.init_scroll_markers();
+
+    // Rebuild scroll markers when the window height changes.
+    $(window).resize(coverage.build_scroll_markers);
+};
+
+coverage.toggle_lines = function (btn, cls) {
+    btn = $(btn);
+    var show = "show_"+cls;
+    if (btn.hasClass(show)) {
+        $("#source ." + cls).removeClass(show);
+        btn.removeClass(show);
+    }
+    else {
+        $("#source ." + cls).addClass(show);
+        btn.addClass(show);
+    }
+    coverage.build_scroll_markers();
+};
+
+// Return the nth line div.
+coverage.line_elt = function (n) {
+    return $("#t" + n);
+};
+
+// Return the nth line number div.
+coverage.num_elt = function (n) {
+    return $("#n" + n);
+};
+
+// Set the selection.  b and e are line numbers.
+coverage.set_sel = function (b, e) {
+    // The first line selected.
+    coverage.sel_begin = b;
+    // The next line not selected.
+    coverage.sel_end = (e === undefined) ? b+1 : e;
+};
+
+coverage.to_top = function () {
+    coverage.set_sel(0, 1);
+    coverage.scroll_window(0);
+};
+
+coverage.to_first_chunk = function () {
+    coverage.set_sel(0, 1);
+    coverage.to_next_chunk();
+};
+
+// Return a string indicating what kind of chunk this line belongs to,
+// or null if not a chunk.
+coverage.chunk_indicator = function (line_elt) {
+    var klass = line_elt.attr('class');
+    if (klass) {
+        var m = klass.match(/\bshow_\w+\b/);
+        if (m) {
+            return m[0];
+        }
+    }
+    return null;
+};
+
+coverage.to_next_chunk = function () {
+    var c = coverage;
+
+    // Find the start of the next colored chunk.
+    var probe = c.sel_end;
+    var chunk_indicator, probe_line;
+    while (true) {
+        probe_line = c.line_elt(probe);
+        if (probe_line.length === 0) {
+            return;
+        }
+        chunk_indicator = c.chunk_indicator(probe_line);
+        if (chunk_indicator) {
+            break;
+        }
+        probe++;
+    }
+
+    // There's a next chunk, `probe` points to it.
+    var begin = probe;
+
+    // Find the end of this chunk.
+    var next_indicator = chunk_indicator;
+    while (next_indicator === chunk_indicator) {
+        probe++;
+        probe_line = c.line_elt(probe);
+        next_indicator = c.chunk_indicator(probe_line);
+    }
+    c.set_sel(begin, probe);
+    c.show_selection();
+};
+
+coverage.to_prev_chunk = function () {
+    var c = coverage;
+
+    // Find the end of the prev colored chunk.
+    var probe = c.sel_begin-1;
+    var probe_line = c.line_elt(probe);
+    if (probe_line.length === 0) {
+        return;
+    }
+    var chunk_indicator = c.chunk_indicator(probe_line);
+    while (probe > 0 && !chunk_indicator) {
+        probe--;
+        probe_line = c.line_elt(probe);
+        if (probe_line.length === 0) {
+            return;
+        }
+        chunk_indicator = c.chunk_indicator(probe_line);
+    }
+
+    // There's a prev chunk, `probe` points to its last line.
+    var end = probe+1;
+
+    // Find the beginning of this chunk.
+    var prev_indicator = chunk_indicator;
+    while (prev_indicator === chunk_indicator) {
+        probe--;
+        probe_line = c.line_elt(probe);
+        prev_indicator = c.chunk_indicator(probe_line);
+    }
+    c.set_sel(probe+1, end);
+    c.show_selection();
+};
+
+// Return the line number of the line nearest pixel position pos
+coverage.line_at_pos = function (pos) {
+    var l1 = coverage.line_elt(1),
+        l2 = coverage.line_elt(2),
+        result;
+    if (l1.length && l2.length) {
+        var l1_top = l1.offset().top,
+            line_height = l2.offset().top - l1_top,
+            nlines = (pos - l1_top) / line_height;
+        if (nlines < 1) {
+            result = 1;
+        }
+        else {
+            result = Math.ceil(nlines);
+        }
+    }
+    else {
+        result = 1;
+    }
+    return result;
+};
+
+// Returns 0, 1, or 2: how many of the two ends of the selection are on
+// the screen right now?
+coverage.selection_ends_on_screen = function () {
+    if (coverage.sel_begin === 0) {
+        return 0;
+    }
+
+    var top = coverage.line_elt(coverage.sel_begin);
+    var next = coverage.line_elt(coverage.sel_end-1);
+
+    return (
+        (top.isOnScreen() ? 1 : 0) +
+        (next.isOnScreen() ? 1 : 0)
+    );
+};
+
+coverage.to_next_chunk_nicely = function () {
+    coverage.finish_scrolling();
+    if (coverage.selection_ends_on_screen() === 0) {
+        // The selection is entirely off the screen: select the top line on
+        // the screen.
+        var win = $(window);
+        coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop()));
+    }
+    coverage.to_next_chunk();
+};
+
+coverage.to_prev_chunk_nicely = function () {
+    coverage.finish_scrolling();
+    if (coverage.selection_ends_on_screen() === 0) {
+        var win = $(window);
+        coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height()));
+    }
+    coverage.to_prev_chunk();
+};
+
+// Select line number lineno, or if it is in a colored chunk, select the
+// entire chunk
+coverage.select_line_or_chunk = function (lineno) {
+    var c = coverage;
+    var probe_line = c.line_elt(lineno);
+    if (probe_line.length === 0) {
+        return;
+    }
+    var the_indicator = c.chunk_indicator(probe_line);
+    if (the_indicator) {
+        // The line is in a highlighted chunk.
+        // Search backward for the first line.
+        var probe = lineno;
+        var indicator = the_indicator;
+        while (probe > 0 && indicator === the_indicator) {
+            probe--;
+            probe_line = c.line_elt(probe);
+            if (probe_line.length === 0) {
+                break;
+            }
+            indicator = c.chunk_indicator(probe_line);
+        }
+        var begin = probe + 1;
+
+        // Search forward for the last line.
+        probe = lineno;
+        indicator = the_indicator;
+        while (indicator === the_indicator) {
+            probe++;
+            probe_line = c.line_elt(probe);
+            indicator = c.chunk_indicator(probe_line);
+        }
+
+        coverage.set_sel(begin, probe);
+    }
+    else {
+        coverage.set_sel(lineno);
+    }
+};
+
+coverage.show_selection = function () {
+    var c = coverage;
+
+    // Highlight the lines in the chunk
+    $(".linenos .highlight").removeClass("highlight");
+    for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) {
+        c.num_elt(probe).addClass("highlight");
+    }
+
+    c.scroll_to_selection();
+};
+
+coverage.scroll_to_selection = function () {
+    // Scroll the page if the chunk isn't fully visible.
+    if (coverage.selection_ends_on_screen() < 2) {
+        // Need to move the page. The html,body trick makes it scroll in all
+        // browsers, got it from http://stackoverflow.com/questions/3042651
+        var top = coverage.line_elt(coverage.sel_begin);
+        var top_pos = parseInt(top.offset().top, 10);
+        coverage.scroll_window(top_pos - 30);
+    }
+};
+
+coverage.scroll_window = function (to_pos) {
+    $("html,body").animate({scrollTop: to_pos}, 200);
+};
+
+coverage.finish_scrolling = function () {
+    $("html,body").stop(true, true);
+};
+
+coverage.init_scroll_markers = function () {
+    var c = coverage;
+    // Init some variables
+    c.lines_len = $('#source p').length;
+    c.body_h = $('body').height();
+    c.header_h = $('div#header').height();
+
+    // Build html
+    c.build_scroll_markers();
+};
+
+coverage.build_scroll_markers = function () {
+    var c = coverage,
+        min_line_height = 3,
+        max_line_height = 10,
+        visible_window_h = $(window).height();
+
+    c.lines_to_mark = $('#source').find('p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par');
+    $('#scroll_marker').remove();
+    // Don't build markers if the window has no scroll bar.
+    if (c.body_h <= visible_window_h) {
+        return;
+    }
+
+    $("body").append("<div id='scroll_marker'>&nbsp;</div>");
+    var scroll_marker = $('#scroll_marker'),
+        marker_scale = scroll_marker.height() / c.body_h,
+        line_height = scroll_marker.height() / c.lines_len;
+
+    // Line height must be between the extremes.
+    if (line_height > min_line_height) {
+        if (line_height > max_line_height) {
+            line_height = max_line_height;
+        }
+    }
+    else {
+        line_height = min_line_height;
+    }
+
+    var previous_line = -99,
+        last_mark,
+        last_top,
+        offsets = {};
+
+    // Calculate line offsets outside loop to prevent relayouts
+    c.lines_to_mark.each(function() {
+        offsets[this.id] = $(this).offset().top;
+    });
+    c.lines_to_mark.each(function () {
+        var id_name = $(this).attr('id'),
+            line_top = Math.round(offsets[id_name] * marker_scale),
+            line_number = parseInt(id_name.substring(1, id_name.length));
+
+        if (line_number === previous_line + 1) {
+            // If this solid missed block just make previous mark higher.
+            last_mark.css({
+                'height': line_top + line_height - last_top
+            });
+        }
+        else {
+            // Add colored line in scroll_marker block.
+            scroll_marker.append('<div id="m' + line_number + '" class="marker"></div>');
+            last_mark = $('#m' + line_number);
+            last_mark.css({
+                'height': line_height,
+                'top': line_top
+            });
+            last_top = line_top;
+        }
+
+        previous_line = line_number;
+    });
+};

+ 147 - 0
ui/htmlcov/index.html

@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <title>Coverage report</title>
+    <link rel="stylesheet" href="style.css" type="text/css">
+    <script type="text/javascript" src="jquery.min.js"></script>
+    <script type="text/javascript" src="jquery.ba-throttle-debounce.min.js"></script>
+    <script type="text/javascript" src="jquery.tablesorter.min.js"></script>
+    <script type="text/javascript" src="jquery.hotkeys.js"></script>
+    <script type="text/javascript" src="coverage_html.js"></script>
+    <script type="text/javascript">
+        jQuery(document).ready(coverage.index_ready);
+    </script>
+</head>
+<body class="indexfile">
+<div id="header">
+    <div class="content">
+        <h1>Coverage report:
+            <span class="pc_cov">75%</span>
+        </h1>
+        <img id="keyboard_icon" src="keybd_closed.png" alt="Show keyboard shortcuts" />
+        <form id="filter_container">
+            <input id="filter" type="text" value="" placeholder="filter..." />
+        </form>
+    </div>
+</div>
+<div class="help_panel">
+    <img id="panel_icon" src="keybd_open.png" alt="Hide keyboard shortcuts" />
+    <p class="legend">Hot-keys on this page</p>
+    <div>
+    <p class="keyhelp">
+        <span class="key">n</span>
+        <span class="key">s</span>
+        <span class="key">m</span>
+        <span class="key">x</span>
+        <span class="key">c</span> &nbsp; change column sorting
+    </p>
+    </div>
+</div>
+<div id="index">
+    <table class="index">
+        <thead>
+            <tr class="tablehead" title="Click to sort">
+                <th class="name left headerSortDown shortkey_n">Module</th>
+                <th class="shortkey_s">statements</th>
+                <th class="shortkey_m">missing</th>
+                <th class="shortkey_x">excluded</th>
+                <th class="right shortkey_c">coverage</th>
+            </tr>
+        </thead>
+        <tfoot>
+            <tr class="total">
+                <td class="name left">Total</td>
+                <td>1768</td>
+                <td>446</td>
+                <td>0</td>
+                <td class="right" data-ratio="1322 1768">75%</td>
+            </tr>
+        </tfoot>
+        <tbody>
+            <tr class="file">
+                <td class="name left"><a href="app___init___py.html">app/__init__.py</a></td>
+                <td>13</td>
+                <td>0</td>
+                <td>0</td>
+                <td class="right" data-ratio="13 13">100%</td>
+            </tr>
+            <tr class="file">
+                <td class="name left"><a href="app_charts_py.html">app/charts.py</a></td>
+                <td>42</td>
+                <td>16</td>
+                <td>0</td>
+                <td class="right" data-ratio="26 42">62%</td>
+            </tr>
+            <tr class="file">
+                <td class="name left"><a href="app_docutils_py.html">app/docutils.py</a></td>
+                <td>1</td>
+                <td>0</td>
+                <td>0</td>
+                <td class="right" data-ratio="1 1">100%</td>
+            </tr>
+            <tr class="file">
+                <td class="name left"><a href="app_filters_py.html">app/filters.py</a></td>
+                <td>29</td>
+                <td>1</td>
+                <td>0</td>
+                <td class="right" data-ratio="28 29">97%</td>
+            </tr>
+            <tr class="file">
+                <td class="name left"><a href="app_forms_py.html">app/forms.py</a></td>
+                <td>122</td>
+                <td>0</td>
+                <td>0</td>
+                <td class="right" data-ratio="122 122">100%</td>
+            </tr>
+            <tr class="file">
+                <td class="name left"><a href="app_models_py.html">app/models.py</a></td>
+                <td>290</td>
+                <td>12</td>
+                <td>0</td>
+                <td class="right" data-ratio="278 290">96%</td>
+            </tr>
+            <tr class="file">
+                <td class="name left"><a href="app_utils___init___py.html">app/utils/__init__.py</a></td>
+                <td>545</td>
+                <td>314</td>
+                <td>0</td>
+                <td class="right" data-ratio="231 545">42%</td>
+            </tr>
+            <tr class="file">
+                <td class="name left"><a href="app_utils_datafile_handler_py.html">app/utils/datafile_handler.py</a></td>
+                <td>23</td>
+                <td>7</td>
+                <td>0</td>
+                <td class="right" data-ratio="16 23">70%</td>
+            </tr>
+            <tr class="file">
+                <td class="name left"><a href="app_utils_xlsx_handler_py.html">app/utils/xlsx_handler.py</a></td>
+                <td>278</td>
+                <td>17</td>
+                <td>0</td>
+                <td class="right" data-ratio="261 278">94%</td>
+            </tr>
+            <tr class="file">
+                <td class="name left"><a href="app_views_py.html">app/views.py</a></td>
+                <td>425</td>
+                <td>79</td>
+                <td>0</td>
+                <td class="right" data-ratio="346 425">81%</td>
+            </tr>
+        </tbody>
+    </table>
+    <p id="no_rows">
+        No items found using the specified filter.
+    </p>
+</div>
+<div id="footer">
+    <div class="content">
+        <p>
+            <a class="nav" href="https://coverage.readthedocs.io">coverage.py v5.2.1</a>,
+            created at 2020-09-10 19:25 +0300
+        </p>
+    </div>
+</div>
+</body>
+</html>

+ 9 - 0
ui/htmlcov/jquery.ba-throttle-debounce.min.js

@@ -0,0 +1,9 @@
+/*
+ * jQuery throttle / debounce - v1.1 - 3/7/2010
+ * http://benalman.com/projects/jquery-throttle-debounce-plugin/
+ *
+ * Copyright (c) 2010 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ */
+(function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this);

+ 99 - 0
ui/htmlcov/jquery.hotkeys.js

@@ -0,0 +1,99 @@
+/*
+ * jQuery Hotkeys Plugin
+ * Copyright 2010, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ *
+ * Based upon the plugin by Tzury Bar Yochay:
+ * http://github.com/tzuryby/hotkeys
+ *
+ * Original idea by:
+ * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
+*/
+
+(function(jQuery){
+
+	jQuery.hotkeys = {
+		version: "0.8",
+
+		specialKeys: {
+			8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause",
+			20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home",
+			37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del",
+			96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7",
+			104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/",
+			112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8",
+			120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta"
+		},
+
+		shiftNums: {
+			"`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&",
+			"8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<",
+			".": ">",  "/": "?",  "\\": "|"
+		}
+	};
+
+	function keyHandler( handleObj ) {
+		// Only care when a possible input has been specified
+		if ( typeof handleObj.data !== "string" ) {
+			return;
+		}
+
+		var origHandler = handleObj.handler,
+			keys = handleObj.data.toLowerCase().split(" ");
+
+		handleObj.handler = function( event ) {
+			// Don't fire in text-accepting inputs that we didn't directly bind to
+			if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) ||
+				 event.target.type === "text") ) {
+				return;
+			}
+
+			// Keypress represents characters, not special keys
+			var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ],
+				character = String.fromCharCode( event.which ).toLowerCase(),
+				key, modif = "", possible = {};
+
+			// check combinations (alt|ctrl|shift+anything)
+			if ( event.altKey && special !== "alt" ) {
+				modif += "alt+";
+			}
+
+			if ( event.ctrlKey && special !== "ctrl" ) {
+				modif += "ctrl+";
+			}
+
+			// TODO: Need to make sure this works consistently across platforms
+			if ( event.metaKey && !event.ctrlKey && special !== "meta" ) {
+				modif += "meta+";
+			}
+
+			if ( event.shiftKey && special !== "shift" ) {
+				modif += "shift+";
+			}
+
+			if ( special ) {
+				possible[ modif + special ] = true;
+
+			} else {
+				possible[ modif + character ] = true;
+				possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true;
+
+				// "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
+				if ( modif === "shift+" ) {
+					possible[ jQuery.hotkeys.shiftNums[ character ] ] = true;
+				}
+			}
+
+			for ( var i = 0, l = keys.length; i < l; i++ ) {
+				if ( possible[ keys[i] ] ) {
+					return origHandler.apply( this, arguments );
+				}
+			}
+		};
+	}
+
+	jQuery.each([ "keydown", "keyup", "keypress" ], function() {
+		jQuery.event.special[ this ] = { add: keyHandler };
+	});
+
+})( jQuery );

+ 53 - 0
ui/htmlcov/jquery.isonscreen.js

@@ -0,0 +1,53 @@
+/* Copyright (c) 2010
+ * @author Laurence Wheway
+ * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
+ * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
+ *
+ * @version 1.2.0
+ */
+(function($) {
+	jQuery.extend({
+		isOnScreen: function(box, container) {
+			//ensure numbers come in as intgers (not strings) and remove 'px' is it's there
+			for(var i in box){box[i] = parseFloat(box[i])};
+			for(var i in container){container[i] = parseFloat(container[i])};
+
+			if(!container){
+				container = {
+					left: $(window).scrollLeft(),
+					top: $(window).scrollTop(),
+					width: $(window).width(),
+					height: $(window).height()
+				}
+			}
+
+			if(	box.left+box.width-container.left > 0 &&
+				box.left < container.width+container.left &&
+				box.top+box.height-container.top > 0 &&
+				box.top < container.height+container.top
+			) return true;
+			return false;
+		}
+	})
+
+
+	jQuery.fn.isOnScreen = function (container) {
+		for(var i in container){container[i] = parseFloat(container[i])};
+
+		if(!container){
+			container = {
+				left: $(window).scrollLeft(),
+				top: $(window).scrollTop(),
+				width: $(window).width(),
+				height: $(window).height()
+			}
+		}
+
+		if(	$(this).offset().left+$(this).width()-container.left > 0 &&
+			$(this).offset().left < container.width+container.left &&
+			$(this).offset().top+$(this).height()-container.top > 0 &&
+			$(this).offset().top < container.height+container.top
+		) return true;
+		return false;
+	}
+})(jQuery);

File diff suppressed because it is too large
+ 5 - 0
ui/htmlcov/jquery.min.js


File diff suppressed because it is too large
+ 2 - 0
ui/htmlcov/jquery.tablesorter.min.js


BIN
ui/htmlcov/keybd_closed.png


BIN
ui/htmlcov/keybd_open.png


File diff suppressed because it is too large
+ 1 - 0
ui/htmlcov/status.json


+ 291 - 0
ui/htmlcov/style.css

@@ -0,0 +1,291 @@
+@charset "UTF-8";
+/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */
+/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */
+/* Don't edit this .css file. Edit the .scss file instead! */
+html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; }
+
+body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; }
+
+@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } }
+
+@media (prefers-color-scheme: dark) { body { color: #eee; } }
+
+html > body { font-size: 16px; }
+
+a:active, a:focus { outline: 2px dashed #007acc; }
+
+p { font-size: .875em; line-height: 1.4em; }
+
+table { border-collapse: collapse; }
+
+td { vertical-align: top; }
+
+table tr.hidden { display: none !important; }
+
+p#no_rows { display: none; font-size: 1.2em; }
+
+a.nav { text-decoration: none; color: inherit; }
+
+a.nav:hover { text-decoration: underline; color: inherit; }
+
+#header { background: #f8f8f8; width: 100%; border-bottom: 1px solid #eee; }
+
+@media (prefers-color-scheme: dark) { #header { background: black; } }
+
+@media (prefers-color-scheme: dark) { #header { border-color: #333; } }
+
+.indexfile #footer { margin: 1rem 3rem; }
+
+.pyfile #footer { margin: 1rem 1rem; }
+
+#footer .content { padding: 0; color: #666; font-style: italic; }
+
+@media (prefers-color-scheme: dark) { #footer .content { color: #aaa; } }
+
+#index { margin: 1rem 0 0 3rem; }
+
+#header .content { padding: 1rem 3rem; }
+
+h1 { font-size: 1.25em; display: inline-block; }
+
+#filter_container { float: right; margin: 0 2em 0 0; }
+
+#filter_container input { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; }
+
+@media (prefers-color-scheme: dark) { #filter_container input { border-color: #444; } }
+
+@media (prefers-color-scheme: dark) { #filter_container input { background: #1e1e1e; } }
+
+@media (prefers-color-scheme: dark) { #filter_container input { color: #eee; } }
+
+#filter_container input:focus { border-color: #007acc; }
+
+h2.stats { margin-top: .5em; font-size: 1em; }
+
+.stats button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; color: inherit; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; }
+
+@media (prefers-color-scheme: dark) { .stats button { border-color: #444; } }
+
+.stats button:active, .stats button:focus { outline: 2px dashed #007acc; }
+
+.stats button:active, .stats button:focus { outline: 2px dashed #007acc; }
+
+.stats button.run { background: #eeffee; }
+
+@media (prefers-color-scheme: dark) { .stats button.run { background: #373d29; } }
+
+.stats button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; }
+
+@media (prefers-color-scheme: dark) { .stats button.run.show_run { background: #373d29; } }
+
+.stats button.mis { background: #ffeeee; }
+
+@media (prefers-color-scheme: dark) { .stats button.mis { background: #4b1818; } }
+
+.stats button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; }
+
+@media (prefers-color-scheme: dark) { .stats button.mis.show_mis { background: #4b1818; } }
+
+.stats button.exc { background: #f7f7f7; }
+
+@media (prefers-color-scheme: dark) { .stats button.exc { background: #333; } }
+
+.stats button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; }
+
+@media (prefers-color-scheme: dark) { .stats button.exc.show_exc { background: #333; } }
+
+.stats button.par { background: #ffffd5; }
+
+@media (prefers-color-scheme: dark) { .stats button.par { background: #650; } }
+
+.stats button.par.show_par { background: #ffa; border: 2px solid #dddd00; margin: 0 .1em; }
+
+@media (prefers-color-scheme: dark) { .stats button.par.show_par { background: #650; } }
+
+.help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; }
+
+#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; }
+
+#keyboard_icon { float: right; margin: 5px; cursor: pointer; }
+
+.help_panel { padding: .5em; border: 1px solid #883; }
+
+.help_panel .legend { font-style: italic; margin-bottom: 1em; }
+
+.indexfile .help_panel { width: 20em; min-height: 4em; }
+
+.pyfile .help_panel { width: 16em; min-height: 8em; }
+
+#panel_icon { float: right; cursor: pointer; }
+
+.keyhelp { margin: .75em; }
+
+.keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; }
+
+#source { padding: 1em 0 1em 3rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
+
+#source p { position: relative; white-space: pre; }
+
+#source p * { box-sizing: border-box; }
+
+#source p .n { float: left; text-align: right; width: 3rem; box-sizing: border-box; margin-left: -3rem; padding-right: 1em; color: #999; }
+
+@media (prefers-color-scheme: dark) { #source p .n { color: #777; } }
+
+#source p .n a { text-decoration: none; color: #999; }
+
+@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } }
+
+#source p .n a:hover { text-decoration: underline; color: #999; }
+
+@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } }
+
+#source p.highlight .n { background: #ffdd00; }
+
+#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; }
+
+@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } }
+
+#source p .t:hover { background: #f2f2f2; }
+
+@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } }
+
+#source p .t:hover ~ .r .annotate.long { display: block; }
+
+#source p .t .com { color: #008000; font-style: italic; line-height: 1px; }
+
+@media (prefers-color-scheme: dark) { #source p .t .com { color: #6A9955; } }
+
+#source p .t .key { font-weight: bold; line-height: 1px; }
+
+#source p .t .str { color: #0451A5; }
+
+@media (prefers-color-scheme: dark) { #source p .t .str { color: #9CDCFE; } }
+
+#source p.mis .t { border-left: 0.2em solid #ff0000; }
+
+#source p.mis.show_mis .t { background: #fdd; }
+
+@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } }
+
+#source p.mis.show_mis .t:hover { background: #f2d2d2; }
+
+@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } }
+
+#source p.run .t { border-left: 0.2em solid #00dd00; }
+
+#source p.run.show_run .t { background: #dfd; }
+
+@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } }
+
+#source p.run.show_run .t:hover { background: #d2f2d2; }
+
+@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } }
+
+#source p.exc .t { border-left: 0.2em solid #808080; }
+
+#source p.exc.show_exc .t { background: #eee; }
+
+@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } }
+
+#source p.exc.show_exc .t:hover { background: #e2e2e2; }
+
+@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } }
+
+#source p.par .t { border-left: 0.2em solid #dddd00; }
+
+#source p.par.show_par .t { background: #ffa; }
+
+@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } }
+
+#source p.par.show_par .t:hover { background: #f2f2a2; }
+
+@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } }
+
+#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; }
+
+#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; }
+
+@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } }
+
+#source p .annotate.short:hover ~ .long { display: block; }
+
+#source p .annotate.long { width: 30em; right: 2.5em; }
+
+#source p input { display: none; }
+
+#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; }
+
+#source p input ~ .r label.ctx::before { content: "▶ "; }
+
+#source p input ~ .r label.ctx:hover { background: #d5f7ff; color: #666; }
+
+@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } }
+
+@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } }
+
+#source p input:checked ~ .r label.ctx { background: #aef; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; }
+
+@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } }
+
+@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } }
+
+#source p input:checked ~ .r label.ctx::before { content: "▼ "; }
+
+#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; }
+
+#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; }
+
+@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } }
+
+#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #aef; border-radius: .25em; margin-right: 1.75em; }
+
+@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } }
+
+#source p .ctxs span { display: block; text-align: right; }
+
+#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; }
+
+#index table.index { margin-left: -.5em; }
+
+#index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; }
+
+@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } }
+
+#index td.name, #index th.name { text-align: left; width: auto; }
+
+#index th { font-style: italic; color: #333; cursor: pointer; }
+
+@media (prefers-color-scheme: dark) { #index th { color: #ddd; } }
+
+#index th:hover { background: #eee; }
+
+@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } }
+
+#index th.headerSortDown, #index th.headerSortUp { white-space: nowrap; background: #eee; }
+
+@media (prefers-color-scheme: dark) { #index th.headerSortDown, #index th.headerSortUp { background: #333; } }
+
+#index th.headerSortDown:after { content: " ↑"; }
+
+#index th.headerSortUp:after { content: " ↓"; }
+
+#index td.name a { text-decoration: none; color: inherit; }
+
+#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; }
+
+#index tr.file:hover { background: #eee; }
+
+@media (prefers-color-scheme: dark) { #index tr.file:hover { background: #333; } }
+
+#index tr.file:hover td.name { text-decoration: underline; color: inherit; }
+
+#scroll_marker { position: fixed; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; }
+
+@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } }
+
+@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } }
+
+#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; }
+
+@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } }

+ 4 - 0
ui/run_tests.sh

@@ -0,0 +1,4 @@
+#!/bin/sh
+docker-compose up -d
+pytest tests/test_upload_files.py -v
+docker-compose down

+ 2 - 0
ui/runservice.sh

@@ -1,4 +1,6 @@
 #!/bin/sh
+echo Waiting for db...
+sleep 15
 source venv/bin/activate
 flask db init
 flask db migrate

+ 7 - 0
ui/waitforpsql.sh

@@ -0,0 +1,7 @@
+#!/bin/bash
+RETRIES=5
+
+until psql -h $PG_HOST -U $PG_USER -d $PG_DATABASE -c "select 1" > /dev/null 2>&1 || [ $RETRIES -eq 0 ]; do
+  echo "Waiting for postgres server, $((RETRIES--)) remaining attempts..."
+  sleep 1
+done