Commit 6b8c97c9 authored by Julien Lepiller's avatar Julien Lepiller

Initial commit

parents
dist
*.egg-info
.guix-profile*
_build
build
*~
__pycache__
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = python3 -msphinx
SPHINXPROJ = transmon
SOURCEDIR = doc
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
upload:
rm -rf dist
python3 setup.py sdist
gpg --detach-sign -a dist/*.tar.gz
twine upload dist/*
update-data:
python3 extractdata.py
Offlate
=======
Offlate is a translation interface for offline translation of projects using
online platforms. Its features include:
* Finding your project and configuring it
* Offline translation
* Submission to your upstream's favorite platform
How to Use?
-----------
Some projects are already available. All you need to do is to open the `new`
menu and select your project from there. If your project is not available, you
can still create a new project with name, a language and a translation system.
The translation system is one of the following methods of sending your work to
upstream:
Currently Supported Platforms
-----------------------------
Offlate currently supports getting translations from and sending them to the
[Translation Project](https://translationproject.org).
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Android Strings Lib documentation build configuration file, created by
# sphinx-quickstart on Sat Feb 17 19:48:43 2018.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.coverage']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'Offlate'
copyright = '2018, Julien Lepiller'
author = 'Julien Lepiller'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.1'
# The full version, including alpha/beta/rc tags.
release = '0.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.guix-profile*']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {
'**': [
'about.html',
'navigation.html',
'relations.html', # needs 'show_related': True theme option to display
'searchbox.html',
'donate.html',
]
}
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'Offlatedoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'offlate.tex', 'Offlate Documentation',
'Julien Lepiller', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'offlate', 'Offlate Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Offlate', 'Offlate Documentation',
author, 'Offlate', 'Local translation through online platforms.',
'Miscellaneous'),
]
.. Offlate documentation master file, created by
sphinx-quickstart on Sat Feb 17 19:48:43 2018.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Offlate's documentation!
===============================================
.. toctree::
:maxdepth: 2
:caption: Contents:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
from lxml import html
import requests
import json
data = []
tplist = requests.get("https://translationproject.org/domain/index.html")
if(tplist.status_code == 200):
tree = html.fromstring(tplist.content)
domains = tree.xpath('//table/tr/td[1]/a/text()')
for d in domains:
data.append({"name": d, "system": 0})
with open('offlate/data.json', 'w') as f:
f.write(json.dumps(data))
(specifications->manifest
'("arc-icon-theme"; for displaying standard icons
"coreutils"
"diffutils"
"git"
"gnupg"
"grep"
"guix"
"gzip"
"less"
"make"
"neovim"
"openssh"
"python"
"python-android-stringslib"
"python-lxml"
"python-neovim"
"python-polib"
"python-pyqt"
"python-sphinx"
"python-twine"
"sed"
"tar"))
(specifications->manifest
'("arc-icon-theme"; for displaying standard icons
"make"
"python"
"python-android-stringslib"
"python-lxml"
"python-polib"
"python-pyqt"
"python-sphinx"
"python-twine"))
[{"name": "a2ps", "system": 0}, {"name": "aegis", "system": 0}, {"name": "anubis", "system": 0}, {"name": "aspell", "system": 0}, {"name": "bash", "system": 0}, {"name": "bfd", "system": 0}, {"name": "binutils", "system": 0}, {"name": "bison", "system": 0}, {"name": "bison-runtime", "system": 0}, {"name": "buzztrax", "system": 0}, {"name": "ccd2cue", "system": 0}, {"name": "ccide", "system": 0}, {"name": "cflow", "system": 0}, {"name": "clisp", "system": 0}, {"name": "coreutils", "system": 0}, {"name": "cpio", "system": 0}, {"name": "cppi", "system": 0}, {"name": "cpplib", "system": 0}, {"name": "cryptsetup", "system": 0}, {"name": "datamash", "system": 0}, {"name": "denemo", "system": 0}, {"name": "dfarc", "system": 0}, {"name": "dialog", "system": 0}, {"name": "dico", "system": 0}, {"name": "diffutils", "system": 0}, {"name": "dink", "system": 0}, {"name": "direvent", "system": 0}, {"name": "doodle", "system": 0}, {"name": "dos2unix", "system": 0}, {"name": "dos2unix-man", "system": 0}, {"name": "e2fsprogs", "system": 0}, {"name": "enscript", "system": 0}, {"name": "exif", "system": 0}, {"name": "fetchmail", "system": 0}, {"name": "findutils", "system": 0}, {"name": "flex", "system": 0}, {"name": "freedink", "system": 0}, {"name": "fusionforge", "system": 0}, {"name": "gas", "system": 0}, {"name": "gawk", "system": 0}, {"name": "gcal", "system": 0}, {"name": "gcc", "system": 0}, {"name": "gdbm", "system": 0}, {"name": "gettext-examples", "system": 0}, {"name": "gettext-runtime", "system": 0}, {"name": "gettext-tools", "system": 0}, {"name": "gjay", "system": 0}, {"name": "glunarclock", "system": 0}, {"name": "gnubiff", "system": 0}, {"name": "gnubik", "system": 0}, {"name": "gnucash", "system": 0}, {"name": "gnuchess", "system": 0}, {"name": "gnucobol", "system": 0}, {"name": "gnulib", "system": 0}, {"name": "gnunet", "system": 0}, {"name": "gnunet-gtk", "system": 0}, {"name": "gold", "system": 0}, {"name": "gphoto2", "system": 0}, {"name": "gprof", "system": 0}, {"name": "gramadoir", "system": 0}, {"name": "grep", "system": 0}, {"name": "grip", "system": 0}, {"name": "grub", "system": 0}, {"name": "gsasl", "system": 0}, {"name": "gss", "system": 0}, {"name": "gst-plugins-bad", "system": 0}, {"name": "gst-plugins-base", "system": 0}, {"name": "gst-plugins-good", "system": 0}, {"name": "gst-plugins-ugly", "system": 0}, {"name": "gstreamer", "system": 0}, {"name": "gtick", "system": 0}, {"name": "gtkam", "system": 0}, {"name": "gtkspell", "system": 0}, {"name": "guix", "system": 0}, {"name": "guix-packages", "system": 0}, {"name": "gutenprint", "system": 0}, {"name": "hello", "system": 0}, {"name": "help2man", "system": 0}, {"name": "help2man-texi", "system": 0}, {"name": "hylafax", "system": 0}, {"name": "idutils", "system": 0}, {"name": "jwhois", "system": 0}, {"name": "kbd", "system": 0}, {"name": "klavaro", "system": 0}, {"name": "ld", "system": 0}, {"name": "leafpad", "system": 0}, {"name": "libc", "system": 0}, {"name": "libexif", "system": 0}, {"name": "libextractor", "system": 0}, {"name": "libgnutls", "system": 0}, {"name": "libgphoto2", "system": 0}, {"name": "libgphoto2_port", "system": 0}, {"name": "libgsasl", "system": 0}, {"name": "libiconv", "system": 0}, {"name": "libidn", "system": 0}, {"name": "libidn2", "system": 0}, {"name": "lilypond", "system": 0}, {"name": "lordsawar", "system": 0}, {"name": "lprng", "system": 0}, {"name": "lynx", "system": 0}, {"name": "m4", "system": 0}, {"name": "mailfromd", "system": 0}, {"name": "mailutils", "system": 0}, {"name": "make", "system": 0}, {"name": "man-db", "system": 0}, {"name": "man-db-manpages", "system": 0}, {"name": "midi-instruments", "system": 0}, {"name": "minicom", "system": 0}, {"name": "mkisofs", "system": 0}, {"name": "muibase", "system": 0}, {"name": "myserver", "system": 0}, {"name": "nano", "system": 0}, {"name": "opcodes", "system": 0}, {"name": "parted", "system": 0}, {"name": "pies", "system": 0}, {"name": "pnmixer", "system": 0}, {"name": "popt", "system": 0}, {"name": "procps-ng", "system": 0}, {"name": "procps-ng-man", "system": 0}, {"name": "psmisc", "system": 0}, {"name": "pspp", "system": 0}, {"name": "pushover", "system": 0}, {"name": "pwdutils", "system": 0}, {"name": "pyspread", "system": 0}, {"name": "radius", "system": 0}, {"name": "recode", "system": 0}, {"name": "recutils", "system": 0}, {"name": "rpm", "system": 0}, {"name": "rush", "system": 0}, {"name": "sarg", "system": 0}, {"name": "savane", "system": 0}, {"name": "sed", "system": 0}, {"name": "sharutils", "system": 0}, {"name": "shepherd", "system": 0}, {"name": "shishi", "system": 0}, {"name": "skribilo", "system": 0}, {"name": "solfege", "system": 0}, {"name": "solfege-manual", "system": 0}, {"name": "spotmachine", "system": 0}, {"name": "sudo", "system": 0}, {"name": "sudoers", "system": 0}, {"name": "sysstat", "system": 0}, {"name": "tar", "system": 0}, {"name": "texinfo", "system": 0}, {"name": "texinfo_document", "system": 0}, {"name": "tigervnc", "system": 0}, {"name": "tin", "system": 0}, {"name": "tin-man", "system": 0}, {"name": "tracgoogleappsauthplugin", "system": 0}, {"name": "trader", "system": 0}, {"name": "util-linux", "system": 0}, {"name": "ve", "system": 0}, {"name": "vmm", "system": 0}, {"name": "vorbis-tools", "system": 0}, {"name": "wastesedge", "system": 0}, {"name": "wcd", "system": 0}, {"name": "wcd-man", "system": 0}, {"name": "wdiff", "system": 0}, {"name": "wget", "system": 0}, {"name": "wyslij-po", "system": 0}, {"name": "xboard", "system": 0}, {"name": "xdg-user-dirs", "system": 0}, {"name": "xkeyboard-config", "system": 0}]
\ No newline at end of file
from pathlib import Path
from .systems.tp import TPProject
import json
import os
class ProjectSettings:
def __init__(self, confdir):
self.confdir = confdir
self.reload()
def write(self):
with open(self.confdir + '/conf.json', 'w') as f:
f.write(json.dumps(self.conf))
def reload(self):
try:
with open(self.confdir + '/conf.json') as f:
self.conf = json.load(f)
except Exception:
with open(self.confdir + '/conf.json', 'w') as f:
f.write(json.dumps({}))
class ProjectManager:
def __init__(self):
self.projects = []
self.project_list = dict()
home = str(Path.home())
self.basedir = home + '/.local/share/offlate'
self.confdir = home + '/.config/offlate'
Path(self.basedir).mkdir(parents=True, exist_ok=True)
Path(self.confdir).mkdir(parents=True, exist_ok=True)
self.settings = ProjectSettings(self.confdir)
try:
with open(self.basedir + '/projects.json') as f:
self.projects = json.load(f)
for p in self.projects:
if p['system'] == 0:
if not "TP" in self.settings.conf:
self.settings.conf["TP"] = {}
self.project_list[p['name']] = \
TPProject(self.settings.conf["TP"], p['name'], p['lang'])
self.project_list[p['name']].open(self.basedir+'/'+p['name'], p['info']['version'])
except Exception:
with open(self.basedir + '/projects.json', 'w') as f:
f.write(json.dumps([]))
def createProject(self, name, lang, system):
projectpath = self.basedir + '/' + name
Path(projectpath).mkdir(parents=True)
if system == 0: #TP
if not "TP" in self.settings.conf:
self.settings.conf["TP"] = {}
proj = TPProject(self.settings.conf["TP"], name, lang)
proj.initialize(projectpath)
self.project_list[name] = proj
self.projects.append({"name": name, "lang": lang, "system": system,
"info": {"version": proj.version}})
self.writeProjects()
def writeProjects(self):
with open(self.basedir + '/projects.json', 'w') as f:
f.write(json.dumps(self.projects))
def listProjects(self):
return self.projects
def getProject(self, name):
return self.project_list[name]
def updateSettings(self, data=None):
if data == None:
self.settings.conf = data
self.settings.update()
else:
self.settings.write()
def getConf(self):
return self.settings.conf
""" The Translation Project system connector. """
import smtplib
from email.message import EmailMessage
from lxml import html
import requests
import polib
import re
import os
from pathlib import Path
class TPProject:
def __init__(self, conf, name, lang):
self.uri = "https://translationproject.org"
self.conf = conf
self.name = name
self.lang = lang
self.basedir = ''
def open(self, basedir, version):
self.version = version
self.basedir = basedir
self.updateFileName()
self.updateGettextNames()
def initialize(self, basedir):
self.basedir = basedir
self.updateVersion()
self.updateFileName()
self.updateGettextNames()
self.getpot()
self.getpo()
def getpo(self):
pofile = requests.get('https://translationproject.org/PO-files/' + self.lang + '/' + self.filename)
if(pofile.status_code == 200):
with open(self.popath, 'w') as f:
f.write(pofile.text)
def getpot(self):
with open(self.potpath, 'w') as f:
potfile = requests.get('http://translationproject.org/POT-files/'
+ self.name + '-' + self.version + '.pot')
f.write(potfile.text)
def updateGettextNames(self):
self.popath = self.basedir + '/' + self.filename
self.potpath = self.basedir + '/orig.pot'
def updateVersion(self):
url = 'https://translationproject.org/domain/' + self.name + '.html'
page = requests.get(url)
tree = html.fromstring(page.content)
pot = tree.xpath('//a[contains(@href,"POT-file")]/text()')
self.version = re.sub(self.name+'-(.*).pot$', '\\1', str(pot[0]))
def updateFileName(self):
self.filename = self.name + '-' + self.version + '.' + self.lang + '.po'
def update(self, callback):
oldversion = self.version
oldname = self.filename
oldpath = self.popath
self.updateVersion()
self.updateFileName()
self.updateGettextNames()
self.getpot()
po = polib.pofile(oldpath)
po.merge(polib.pofile(self.potpath))
po.save()
if oldname == self.filename:
self.popath = self.popath + '.new.po'
self.getpo()
self.popath = oldpath
self.merge(self.popath + '.new.po', oldpath, callback)
os.remove(self.popath)
os.rename(self.popath + '.new.po', self.popath)
else:
self.getpo()
self.merge(self.filename, oldpath, callback)
os.remove(oldpath)
def merge(self, tofile, fromfile, callback):
newpo = polib.pofile(tofile)
oldpo = polib.pofile(fromfile)
# If msgid is not found in to file, it's an old one, so ignore.
# Otherwise, attempt to merge.
for oentry in oldpo:
for nentry in newpo:
if oentry.msgid == nentry.msgid:
if oentry.msgstr == nentry.msgstr:
break
if oentry.msgstr == "":
break
if nentry.msgstr == "":
nentry.msgstr = oentry.msgstr
break
# else, nentry and oentry have a different msgstr
nentry.msgstr = callback(nentry.msgid, oentry.msgstr, nentry.msgstr)
break
newpo.save()
def send(self, interface):
msg = EmailMessage()
msg['Subject'] = self.filename
msg['From'] = self.conf["email"]
msg['To'] = 'robot@translationproject.org'
with open(self.popath, 'rb') as f:
msg.add_attachment(f.read(), maintype='text', subtype='plain',
filename=self.filename)
with smtplib.SMTP(self.conf['server']+':587') as s:
s.starttls()
s.login(self.conf['user'], interface.askPassword())
s.send_message(msg)
def content(self):
po = polib.pofile(self.popath)
return po
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import json
import re
from .manager import ProjectManager
class ProjectTab(QTabWidget):
def __init__(self, parent = None):
super(ProjectTab, self).__init__(parent)
class Interface:
def __init__(self):
self.value = None
def ok(self):
self.value = self.qd.textValue()
def askPassword(self):
self.qd = QInputDialog()
self.qd.setTextEchoMode(QLineEdit.Password)
self.qd.accepted.connect(self.ok)
self.qd.exec_()
return self.value
class ProjectView(QWidget):
def __init__(self, project, showTranslated = True, showUntranslated = True, showFuzzy = True, parent = None):
super(ProjectView, self).__init__(parent)
self.project = project
self.content = self.project.content()
self.showTranslated = showTranslated
self.showUntranslated = showUntranslated
self.showFuzzy = showFuzzy
self.initUI()
def updateContent(self):
self.treeWidget.clear()
items = []
for entry in self.content:
if entry.obsolete:
continue
cont = False
if self.showTranslated and entry.translated():
cont = True
if self.showUntranslated and not entry.translated():
cont = True
if self.showFuzzy and 'fuzzy' in entry.flags:
cont = True
if not cont:
continue
print(entry.flags)
print(entry.translated())
print("")
msgstr = entry.msgstr
if 0 in entry.msgstr_plural:
msgstr = entry.msgstr_plural[0]
item = QTreeWidgetItem([entry.msgid, msgstr])
item.setData(0, Qt.UserRole, entry)
items.append(item)
self.treeWidget.insertTopLevelItems(0, items)
def initUI(self):
vbox = QVBoxLayout()
self.setLayout(vbox)
model = QStandardItemModel()
self.treeWidget = QTreeWidget()
self.treeWidget.setColumnCount(2)
self.msgid = QTextEdit()
self.msgid.setReadOnly(True)
self.msgstr = QTextEdit()
self.updateContent()
vbox.addWidget(self.treeWidget, 4)
self.hbox = QHBoxLayout()
self.hbox.addWidget(self.msgid)
self.hbox.addWidget(self.msgstr)
vbox.addLayout(self.hbox, 1)
size = self.treeWidget.size()
self.treeWidget.setColumnWidth(0, size.width()/2)
self.treeWidget.currentItemChanged.connect(self.selectItem)
def selectItem(self, current, old):
if current == None:
return
data = current.data(0, Qt.UserRole)
self.hbox.removeWidget(self.msgid)
self.hbox.removeWidget(self.msgstr)
self.msgid.deleteLater()
self.msgstr.deleteLater()
print(data.msgstr_plural)
if 0 in data.msgstr_plural:
self.msgid = QTabWidget();
self.msgstr = QTabWidget();
singular = QTextEdit()
singular.setReadOnly(True)
singular.setText(data.msgid)
plural = QTextEdit()
plural.setReadOnly(True)
plural.setText(data.msgid_plural)
self.msgid.addTab(singular, "Singular")
self.msgid.addTab(plural, "Plural")
i = 0
while i in data.msgstr_plural:
form = QTextEdit()
form.setText(data.msgstr_plural[i])
form.textChanged.connect(self.modify)
self.msgstr.addTab(form, str(i))
i=i+1
else:
self.msgid = QTextEdit()
self.msgid.setReadOnly(True)
self.msgstr = QTextEdit()
self.msgid.setText(data.msgid)
self.msgstr.setText(data.msgstr)
self.msgstr.textChanged.connect(self.modify)
self.hbox.addWidget(self.msgid)
self.hbox.addWidget(self.msgstr)
def modify(self):
item = self.treeWidget.currentItem()
data = item.data(0, Qt.UserRole)
if self.msgstr.__class__.__name__ == "QTextEdit":
data.msgstr = self.msgstr.toPlainText()
item.setText(1, data.msgstr)
else:
i = 0
while i in data.msgstr_plural:
data.msgstr_plural[i] = self.msgstr.widget(i).toPlainText()
i=i+1
item.setText(1, data.msgstr_plural[0])
def save(self):
self.content.save()
def send(self):
self.content.save()
self.project.send(Interface())
def askmerge(self, msgid, oldstr, newstr):
print("Merging {}: {} -> {}".format(msgid, oldstr, newstr))
return newstr
def update(self):
self.content.save()
self.project.update(self.askmerge)
self.content = self.project.content()
self.updateContent()
def filter(self, showTranslated, showUntranslated, showFuzzy):
self.showTranslated = showTranslated
self.showUntranslated = showUntranslated
self.showFuzzy = showFuzzy
self.updateContent()
class NewWindow(QDialog):
def __init__(self, manager, parent = None):
super().__init__(parent)
self.name = ""
self.lang = ""
self.system = 0
self.manager = manager
self.askNew = False
self.initUI()
def initUI(self):
hbox = QHBoxLayout()
predefinedbox = QVBoxLayout()
self.searchfield = QLineEdit()
predefinedbox.addWidget(self.searchfield)
self.predefinedprojects = QListWidget()
with open('offlate/data.json') as f:
self.projectdata = json.load(f)
for d in self.projectdata:
item = QListWidgetItem(d['name'])
item.setData(Qt.UserRole, d)
self.predefinedprojects.addItem(item)