Increased flexibility and privacy.
This commit is contained in:
15
README.md
15
README.md
@@ -113,13 +113,26 @@ This is meant to import .xslx files created from the Design & Analysis Software
|
|||||||
## SETUP:
|
## SETUP:
|
||||||
|
|
||||||
## Download:
|
## Download:
|
||||||
|
*Python v3.11 or greater must be installed on your system for this.*
|
||||||
|
|
||||||
1. Clone or download from github.
|
1. Clone or download from github.
|
||||||
2. Enter the downloaded folder.
|
2. Enter the downloaded folder.
|
||||||
|
3. Open a terminal in the folder with the 'src' folder.
|
||||||
|
4. Create a new virtual environment: ```python -m venv venv```
|
||||||
|
5. Activate the virtual environment: (Windows) ```venv\Scripts\activate.bat```
|
||||||
|
6. Install dependencies: ```pip install -r requirements.txt```
|
||||||
|
|
||||||
## Database:
|
## Database:
|
||||||
|
|
||||||
1. Copy 'alembic_default.ini' to 'alembic.ini' in the same folder.
|
1. Copy 'alembic_default.ini' to 'alembic.ini' in the same folder.
|
||||||
2. Open 'alembic.ini' and edit 'sqlalchemy.url' to the desired path of the database.
|
2. Open 'alembic.ini' and edit 'sqlalchemy.url' to the desired path of the database.
|
||||||
1. The path by default is sqlite based. Postgresql support is available.
|
1. The path by default is sqlite based. Postgresql support is available.
|
||||||
2. Postgres path
|
3. Open a terminal in the folder with the 'src' folder.
|
||||||
|
4. Run database migration: ```alembic upgrade head```
|
||||||
|
|
||||||
|
## First Run:
|
||||||
|
|
||||||
|
1. On first run, the application copies src/config.yml to C:\Users\{USERNAME}\Local\submissions\config
|
||||||
|
2. Initially, the 'directory_path' variable is set to the 'sqlalchemy.url' variable in alembic.ini
|
||||||
|
3. If this folder cannot be found, C:\Users\{USERNAME}\Documents\submissions will be used.
|
||||||
|
1. If using Postgres, the 'database_path' and other variables will have to be updated manually.
|
||||||
@@ -55,7 +55,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
|
|||||||
# are written from script.py.mako
|
# are written from script.py.mako
|
||||||
# output_encoding = utf-8
|
# output_encoding = utf-8
|
||||||
|
|
||||||
sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
|
sqlalchemy.url = sqlite:///{{PUT DATABASE PATH HERE!!}}
|
||||||
|
|
||||||
[post_write_hooks]
|
[post_write_hooks]
|
||||||
# post_write_hooks defines scripts or Python functions that are run
|
# post_write_hooks defines scripts or Python functions that are run
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@@ -229,7 +229,7 @@ class SubmissionsSheet(QTableView):
|
|||||||
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
|
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
|
||||||
|
|
||||||
@report_result
|
@report_result
|
||||||
def generate_report(self):
|
def generate_report(self, *args):
|
||||||
"""
|
"""
|
||||||
Make a report
|
Make a report
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ Contains miscellaenous functions used by both frontend and backend.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import pprint
|
||||||
|
import weakref
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
import jinja2
|
import jinja2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -21,11 +23,14 @@ from PyQt6.QtGui import QPageSize
|
|||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from openpyxl.worksheet.worksheet import Worksheet
|
from openpyxl.worksheet.worksheet import Worksheet
|
||||||
from PyQt6.QtPrintSupport import QPrinter
|
from PyQt6.QtPrintSupport import QPrinter
|
||||||
|
from __init__ import project_path
|
||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
package_dir = Path(__file__).parents[2].resolve()
|
# package_dir = Path(__file__).parents[2].resolve()
|
||||||
logger.debug(f"Package dir: {package_dir}")
|
# package_dir = project_path
|
||||||
|
logger.debug(f"Package dir: {project_path}")
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
os_config_dir = "AppData/local"
|
os_config_dir = "AppData/local"
|
||||||
@@ -224,7 +229,7 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
database_schema: str
|
database_schema: str
|
||||||
directory_path: Path
|
directory_path: Path | None = None
|
||||||
database_user: str | None = None
|
database_user: str | None = None
|
||||||
database_password: str | None = None
|
database_password: str | None = None
|
||||||
database_name: str
|
database_name: str
|
||||||
@@ -255,11 +260,27 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
@field_validator('directory_path', mode="before")
|
@field_validator('directory_path', mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def ensure_directory_exists(cls, value):
|
def ensure_directory_exists(cls, value):
|
||||||
|
if value is None:
|
||||||
|
print("No value for dir path")
|
||||||
|
if check_if_app():
|
||||||
|
alembic_path = Path(sys._MEIPASS).joinpath("files", "alembic.ini")
|
||||||
|
else:
|
||||||
|
alembic_path = project_path.joinpath("alembic.ini")
|
||||||
|
print(f"Getting alembic path: {alembic_path}")
|
||||||
|
value = cls.get_alembic_db_path(alembic_path=alembic_path)
|
||||||
|
print(f"Using {value}")
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
value = Path(value)
|
value = Path(value)
|
||||||
if not value.exists():
|
try:
|
||||||
value = Path().home()
|
check = value.exists()
|
||||||
# metadata.directory_path = value
|
except AttributeError:
|
||||||
|
check = False
|
||||||
|
if not check:
|
||||||
|
print(f"No directory found, using Documents/submissions")
|
||||||
|
value = Path.home().joinpath("Documents", "submissions")
|
||||||
|
value.mkdir()
|
||||||
|
# metadata.directory_path = value
|
||||||
|
print(f"Final return of directory_path: {value}")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@field_validator('database_path', mode="before")
|
@field_validator('database_path', mode="before")
|
||||||
@@ -267,12 +288,14 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
def ensure_database_exists(cls, value, values):
|
def ensure_database_exists(cls, value, values):
|
||||||
# if value == ":memory:":
|
# if value == ":memory:":
|
||||||
# return value
|
# return value
|
||||||
|
if value is None:
|
||||||
|
value = values.data['directory_path']
|
||||||
match values.data['database_schema']:
|
match values.data['database_schema']:
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
value = f"/{Path(value).absolute().__str__()}/{values.data['database_name']}.db"
|
value = Path(f"{Path(value).absolute().__str__()}/{values.data['database_name']}.db")
|
||||||
# db_name = f"{values.data['database_name']}.db"
|
# db_name = f"{values.data['database_name']}.db"
|
||||||
case _:
|
case _:
|
||||||
value = f"@{value}/{values.data['database_name']}"
|
value = f"{value}/{values.data['database_name']}"
|
||||||
# db_name = values.data['database_name']
|
# db_name = values.data['database_name']
|
||||||
# match value:
|
# match value:
|
||||||
# case str():
|
# case str():
|
||||||
@@ -283,7 +306,6 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
# return value
|
# return value
|
||||||
# else:
|
# else:
|
||||||
# raise FileNotFoundError(f"Couldn't find database at {value}")
|
# raise FileNotFoundError(f"Couldn't find database at {value}")
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@field_validator('database_session', mode="before")
|
@field_validator('database_session', mode="before")
|
||||||
@@ -292,9 +314,15 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
if value is not None:
|
if value is not None:
|
||||||
return value
|
return value
|
||||||
else:
|
else:
|
||||||
|
match values.data['database_schema']:
|
||||||
|
case "sqlite":
|
||||||
|
value = f"/{values.data['database_path']}"
|
||||||
|
# db_name = f"{values.data['database_name']}.db"
|
||||||
|
case _:
|
||||||
|
value = f"@{values.data['database_path']}"
|
||||||
template = jinja_template_loading().from_string(
|
template = jinja_template_loading().from_string(
|
||||||
"{{ values['database_schema'] }}://{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}{{ values['database_path'] }}")
|
"{{ values['database_schema'] }}://{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}{{ value }}")
|
||||||
database_path = template.render(values=values.data)
|
database_path = template.render(values=values.data, value=value)
|
||||||
# print(f"Using {database_path} for database path")
|
# print(f"Using {database_path} for database path")
|
||||||
# database_path = values.data['database_path']
|
# database_path = values.data['database_path']
|
||||||
# if database_path is None:
|
# if database_path is None:
|
||||||
@@ -303,7 +331,7 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
# database_path = Path.home().joinpath(".submissions", "submissions.db")
|
# database_path = Path.home().joinpath(".submissions", "submissions.db")
|
||||||
# # NOTE: finally, look in the local dir
|
# # NOTE: finally, look in the local dir
|
||||||
# else:
|
# else:
|
||||||
# database_path = package_dir.joinpath("submissions.db")
|
# database_path = project_path.joinpath("submissions.db")
|
||||||
# else:
|
# else:
|
||||||
# if database_path == ":memory:":
|
# if database_path == ":memory:":
|
||||||
# pass
|
# pass
|
||||||
@@ -315,7 +343,7 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
# database_path = database_path
|
# database_path = database_path
|
||||||
# else:
|
# else:
|
||||||
# raise FileNotFoundError("No database file found. Exiting program.")
|
# raise FileNotFoundError("No database file found. Exiting program.")
|
||||||
logger.info(f"Using {database_path} for database file.")
|
print(f"Using {database_path} for database file.")
|
||||||
# engine = create_engine(f"sqlite:///{database_path}") #, echo=True, future=True)
|
# engine = create_engine(f"sqlite:///{database_path}") #, echo=True, future=True)
|
||||||
# engine = create_engine("postgresql+psycopg2://postgres:RE,4321q@localhost:5432/submissions")
|
# engine = create_engine("postgresql+psycopg2://postgres:RE,4321q@localhost:5432/submissions")
|
||||||
engine = create_engine(database_path)
|
engine = create_engine(database_path)
|
||||||
@@ -334,11 +362,13 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.set_from_db(db_path=kwargs['database_path'])
|
self.set_from_db(db_path=kwargs['database_path'])
|
||||||
|
|
||||||
|
|
||||||
def set_from_db(self, db_path: Path):
|
def set_from_db(self, db_path: Path):
|
||||||
if 'pytest' in sys.modules:
|
if 'pytest' in sys.modules:
|
||||||
output = dict(power_users=['lwark', 'styson', 'ruwang'])
|
output = dict(power_users=['lwark', 'styson', 'ruwang'])
|
||||||
else:
|
else:
|
||||||
# session = Session(create_engine(f"sqlite:///{db_path}"))
|
# session = Session(create_engine(f"sqlite:///{db_path}"))
|
||||||
|
logger.debug(self.__dict__)
|
||||||
session = self.database_session
|
session = self.database_session
|
||||||
config_items = session.execute(text("SELECT * FROM _configitem")).all()
|
config_items = session.execute(text("SELECT * FROM _configitem")).all()
|
||||||
session.close()
|
session.close()
|
||||||
@@ -354,6 +384,36 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
if not hasattr(self, k):
|
if not hasattr(self, k):
|
||||||
self.__setattr__(k, v)
|
self.__setattr__(k, v)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_alembic_db_path(cls, alembic_path) -> Path:
|
||||||
|
c = ConfigParser()
|
||||||
|
c.read(alembic_path)
|
||||||
|
path = c['alembic']['sqlalchemy.url'].replace("sqlite:///", "")
|
||||||
|
return Path(path).parent
|
||||||
|
|
||||||
|
def save(self, settings_path:Path):
|
||||||
|
if not settings_path.exists():
|
||||||
|
dicto = {}
|
||||||
|
for k,v in self.__dict__.items():
|
||||||
|
if k in ['package', 'database_session']:
|
||||||
|
continue
|
||||||
|
match v:
|
||||||
|
case Path():
|
||||||
|
print("Path")
|
||||||
|
if v.is_dir():
|
||||||
|
print("dir")
|
||||||
|
v = v.absolute().__str__()
|
||||||
|
elif v.is_file():
|
||||||
|
print("file")
|
||||||
|
v = v.parent.absolute().__str__()
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
print(f"Key: {k}, Value: {v}")
|
||||||
|
dicto[k] = v
|
||||||
|
with open(settings_path, 'w') as f:
|
||||||
|
yaml.dump(dicto, f)
|
||||||
|
# return settings
|
||||||
|
|
||||||
|
|
||||||
def get_config(settings_path: Path | str | None = None) -> Settings:
|
def get_config(settings_path: Path | str | None = None) -> Settings:
|
||||||
"""
|
"""
|
||||||
@@ -400,12 +460,17 @@ def get_config(settings_path: Path | str | None = None) -> Settings:
|
|||||||
if check_if_app():
|
if check_if_app():
|
||||||
settings_path = Path(sys._MEIPASS).joinpath("files", "config.yml")
|
settings_path = Path(sys._MEIPASS).joinpath("files", "config.yml")
|
||||||
else:
|
else:
|
||||||
settings_path = package_dir.joinpath('src', 'config.yml')
|
settings_path = project_path.joinpath('src', 'config.yml')
|
||||||
with open(settings_path, "r") as dset:
|
with open(settings_path, "r") as dset:
|
||||||
default_settings = yaml.load(dset, Loader=yaml.Loader)
|
default_settings = yaml.load(dset, Loader=yaml.Loader)
|
||||||
|
|
||||||
# NOTE: Tell program we need to copy the config.yml to the user directory
|
# NOTE: Tell program we need to copy the config.yml to the user directory
|
||||||
# NOTE: copy settings to config directory
|
# NOTE: copy settings to config directory
|
||||||
return Settings(**copy_settings(settings_path=CONFIGDIR.joinpath("config.yml"), settings=default_settings))
|
# settings = Settings(**copy_settings(settings_path=CONFIGDIR.joinpath("config.yml"), settings=default_settings))
|
||||||
|
settings = Settings(**default_settings)
|
||||||
|
settings.save(settings_path=CONFIGDIR.joinpath("config.yml"))
|
||||||
|
print(f"Default settings: {pprint.pprint(settings.__dict__)}")
|
||||||
|
return settings
|
||||||
else:
|
else:
|
||||||
# NOTE: check if user defined path is directory
|
# NOTE: check if user defined path is directory
|
||||||
if settings_path.is_dir():
|
if settings_path.is_dir():
|
||||||
@@ -417,7 +482,9 @@ def get_config(settings_path: Path | str | None = None) -> Settings:
|
|||||||
logger.error("No config.yml file found. Writing to directory.")
|
logger.error("No config.yml file found. Writing to directory.")
|
||||||
with open(settings_path, "r") as dset:
|
with open(settings_path, "r") as dset:
|
||||||
default_settings = yaml.load(dset, Loader=yaml.Loader)
|
default_settings = yaml.load(dset, Loader=yaml.Loader)
|
||||||
return Settings(**copy_settings(settings_path=settings_path, settings=default_settings))
|
# return Settings(**copy_settings(settings_path=settings_path, settings=default_settings))
|
||||||
|
settings = Settings(**default_settings)
|
||||||
|
settings.save(settings_path=settings_path)
|
||||||
# logger.debug(f"Using {settings_path} for config file.")
|
# logger.debug(f"Using {settings_path} for config file.")
|
||||||
with open(settings_path, "r") as stream:
|
with open(settings_path, "r") as stream:
|
||||||
settings = yaml.load(stream, Loader=yaml.Loader)
|
settings = yaml.load(stream, Loader=yaml.Loader)
|
||||||
|
|||||||
73
submissions.spec
Normal file
73
submissions.spec
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
#### custom for automation of documentation building ####
|
||||||
|
import sys, subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.append(Path(".").parent.joinpath('src').absolute().__str__())
|
||||||
|
from submissions import __version__, __project__, bcolors, project_path
|
||||||
|
|
||||||
|
doc_path = project_path.joinpath("docs").absolute()
|
||||||
|
build_path = project_path.joinpath(".venv", "Scripts", "sphinx-build").absolute().__str__()
|
||||||
|
print(bcolors.BOLD + "Running Sphinx subprocess to generate rst files..." + bcolors.ENDC)
|
||||||
|
api_path = project_path.joinpath(".venv", "Scripts", "sphinx-apidoc").absolute().__str__()
|
||||||
|
subprocess.run([api_path, "-o", doc_path.joinpath("source").__str__(), project_path.joinpath("src", "submissions").__str__(), "-f"])
|
||||||
|
print(bcolors.BOLD + "Running Sphinx subprocess to generate html docs..." + bcolors.ENDC)
|
||||||
|
subprocess.run([build_path, doc_path.joinpath("source").__str__(), doc_path.joinpath("build").__str__(), "-a"])
|
||||||
|
#########################################################
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['src\\submissions\\__main__.py'],
|
||||||
|
pathex=[project_path.absolute().__str__()],
|
||||||
|
binaries=[],
|
||||||
|
datas=[
|
||||||
|
("src\\config.yml", "files"),
|
||||||
|
("src\\submissions\\templates\\*", "files\\templates"),
|
||||||
|
("src\\submissions\\templates\\css\\*", "files\\templates\\css"),
|
||||||
|
("docs\\build", "files\\docs"),
|
||||||
|
("src\\submissions\\resources\\*", "files\\resources"),
|
||||||
|
("alembic.ini", "files"),
|
||||||
|
],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=["*.xlsx"],
|
||||||
|
win_no_prefer_redirects=False,
|
||||||
|
win_private_assemblies=False,
|
||||||
|
cipher=block_cipher,
|
||||||
|
noarchive=False,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name=f"{__project__}_{__version__}",
|
||||||
|
debug=True,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
# Change these for non-beta versions
|
||||||
|
#console=False,
|
||||||
|
#disable_windowed_traceback=True,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
name=f"{__project__}_{__version__}",
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user