diff --git a/README.md b/README.md index 92ba34d..ecc88f7 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,9 @@ To install DICAT source code on a computer, download and save the content of the Before running DICAT, make sure your systems contains a [Python](https://www.python.org) compiler with the [TkInter](https://wiki.python.org/moin/TkInter) library (usually, TkInter comes by default with most Python installations. The [PyDICOM](http://www.pydicom.org) package is also required by DICAT. -DICAT can be started by executing `DICAT_application.py` script with a Python compiler. On UNIX computers (Linux and Mac OS X), open a terminal, go to the main directory of DICAT source code (`dicat` directory) and run the following: +DICAT can be started by executing `DICAT.py` script with a Python compiler. On UNIX computers (Linux and Mac OS X), open a terminal, go to the main directory of DICAT source code (`dicat` directory) and run the following: -```python DICAT_application.py``` +```python DICAT.py``` ## How to use the DICOM de-identifier of DICAT? diff --git a/dicat/CalendarDialog/CalendarDialog.py b/dicat/CalendarDialog/CalendarDialog.py new file mode 100755 index 0000000..79b0e3c --- /dev/null +++ b/dicat/CalendarDialog/CalendarDialog.py @@ -0,0 +1,41 @@ +import Tkinter +import ttkcalendar + +import tkSimpleDialog + + +class CalendarDialog(tkSimpleDialog.Dialog): + """Dialog box that displays a calendar and returns the selected date""" + def body(self, master): + self.calendar = ttkcalendar.Calendar(master) + self.calendar.pack() + + def apply(self): + self.result = self.calendar.selection + +# Demo code: + + +class CalendarFrame(Tkinter.LabelFrame): + def __init__(self, master): + Tkinter.LabelFrame.__init__(self, master, text="CalendarDialog Demo") + + def getdate(): + cd = CalendarDialog(self) + result = cd.result + self.selected_date.set(result.strftime("%m/%d/%Y")) + + self.selected_date = Tkinter.StringVar() + + Tkinter.Entry(self, textvariable=self.selected_date).pack(side=Tkinter.LEFT) + Tkinter.Button(self, text="Choose a date", command=getdate).pack(side=Tkinter.LEFT) + + +def main(): + root = Tkinter.Tk() + root.wm_title("CalendarDialog Demo") + CalendarFrame(root).pack() + root.mainloop() + +if __name__ == "__main__": + main() diff --git a/dicat/CalendarDialog/__init__.py b/dicat/CalendarDialog/__init__.py new file mode 100644 index 0000000..77e44d0 --- /dev/null +++ b/dicat/CalendarDialog/__init__.py @@ -0,0 +1,7 @@ +__author__ = 'cmadjar' + +""" +These scripts have been downloaded from the following GitHub repository and +allows to have date picker widgets and maybe some calendar functionality? +https://github.com/moshekaplan/tkinter_components +""" \ No newline at end of file diff --git a/dicat/CalendarDialog/tkSimpleDialog.py b/dicat/CalendarDialog/tkSimpleDialog.py new file mode 100755 index 0000000..29b055e --- /dev/null +++ b/dicat/CalendarDialog/tkSimpleDialog.py @@ -0,0 +1,94 @@ +from Tkinter import * +from ttk import * + +class Dialog(Toplevel): + """Sourced from http://effbot.org/tkinterbook/tkinter-dialog-windows.htm""" + def __init__(self, parent, title = None): + + Toplevel.__init__(self, parent) + self.transient(parent) + + if title: + self.title(title) + + self.parent = parent + + self.result = None + + body = Frame(self) + self.initial_focus = self.body(body) + body.pack(padx=5, pady=5) + + self.buttonbox() + + self.grab_set() + + if not self.initial_focus: + self.initial_focus = self + + self.protocol("WM_DELETE_WINDOW", self.cancel) + + self.geometry("+%d+%d" % (parent.winfo_rootx()+50, + parent.winfo_rooty()+50)) + + self.initial_focus.focus_set() + + self.wait_window(self) + + # + # construction hooks + + def body(self, master): + # create dialog body. return widget that should have + # initial focus. this method should be overridden + + pass + + def buttonbox(self): + # add standard button box. override if you don't want the + # standard buttons + + box = Frame(self) + + w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE) + w.pack(side=LEFT, padx=5, pady=5) + w = Button(box, text="Cancel", width=10, command=self.cancel) + w.pack(side=LEFT, padx=5, pady=5) + + self.bind("", self.ok) + self.bind("", self.cancel) + + box.pack() + + # + # standard button semantics + + def ok(self, event=None): + + if not self.validate(): + self.initial_focus.focus_set() # put focus back + return + + self.withdraw() + self.update_idletasks() + + self.apply() + + self.cancel() + + def cancel(self, event=None): + + # put focus back to the parent window + self.parent.focus_set() + self.destroy() + + # + # command hooks + + def validate(self): + + return 1 # override + + def apply(self): + + pass # override diff --git a/dicat/CalendarDialog/ttkcalendar.py b/dicat/CalendarDialog/ttkcalendar.py new file mode 100755 index 0000000..14c289c --- /dev/null +++ b/dicat/CalendarDialog/ttkcalendar.py @@ -0,0 +1,236 @@ +# Source: http://svn.python.org/projects/sandbox/trunk/ttk-gsoc/samples/ttkcalendar.py + +""" +Simple calendar using ttk Treeview together with calendar and datetime +classes. +""" +import calendar + +try: + import Tkinter + import tkFont +except ImportError: # py3k + import tkinter as Tkinter + import tkinter.font as tkFont + +import ttk + +def get_calendar(locale, fwday): + # instantiate proper calendar class + if locale is None: + return calendar.TextCalendar(fwday) + else: + return calendar.LocaleTextCalendar(fwday, locale) + +class Calendar(ttk.Frame): + # XXX ToDo: cget and configure + + datetime = calendar.datetime.datetime + timedelta = calendar.datetime.timedelta + + def __init__(self, master=None, **kw): + """ + WIDGET-SPECIFIC OPTIONS + + locale, firstweekday, year, month, selectbackground, + selectforeground + """ + # remove custom options from kw before initializating ttk.Frame + fwday = kw.pop('firstweekday', calendar.MONDAY) + year = kw.pop('year', self.datetime.now().year) + month = kw.pop('month', self.datetime.now().month) + locale = kw.pop('locale', None) + sel_bg = kw.pop('selectbackground', '#ecffc4') + sel_fg = kw.pop('selectforeground', '#05640e') + + self._date = self.datetime(year, month, 1) + self._selection = None # no date selected + + ttk.Frame.__init__(self, master, **kw) + + self._cal = get_calendar(locale, fwday) + + self.__setup_styles() # creates custom styles + self.__place_widgets() # pack/grid used widgets + self.__config_calendar() # adjust calendar columns and setup tags + # configure a canvas, and proper bindings, for selecting dates + self.__setup_selection(sel_bg, sel_fg) + + # store items ids, used for insertion later + self._items = [self._calendar.insert('', 'end', values='') + for _ in range(6)] + # insert dates in the currently empty calendar + self._build_calendar() + + def __setitem__(self, item, value): + if item in ('year', 'month'): + raise AttributeError("attribute '%s' is not writeable" % item) + elif item == 'selectbackground': + self._canvas['background'] = value + elif item == 'selectforeground': + self._canvas.itemconfigure(self._canvas.text, item=value) + else: + ttk.Frame.__setitem__(self, item, value) + + def __getitem__(self, item): + if item in ('year', 'month'): + return getattr(self._date, item) + elif item == 'selectbackground': + return self._canvas['background'] + elif item == 'selectforeground': + return self._canvas.itemcget(self._canvas.text, 'fill') + else: + r = ttk.tclobjs_to_py({item: ttk.Frame.__getitem__(self, item)}) + return r[item] + + def __setup_styles(self): + # custom ttk styles + style = ttk.Style(self.master) + arrow_layout = lambda dir: ( + [('Button.focus', {'children': [('Button.%sarrow' % dir, None)]})] + ) + style.layout('L.TButton', arrow_layout('left')) + style.layout('R.TButton', arrow_layout('right')) + + def __place_widgets(self): + # header frame and its widgets + hframe = ttk.Frame(self) + lbtn = ttk.Button(hframe, style='L.TButton', command=self._prev_month) + rbtn = ttk.Button(hframe, style='R.TButton', command=self._next_month) + self._header = ttk.Label(hframe, width=15, anchor='center') + # the calendar + self._calendar = ttk.Treeview(self, show='', selectmode='none', height=7) + + # pack the widgets + hframe.pack(in_=self, side='top', pady=4, anchor='center') + lbtn.grid(in_=hframe) + self._header.grid(in_=hframe, column=1, row=0, padx=12) + rbtn.grid(in_=hframe, column=2, row=0) + self._calendar.pack(in_=self, expand=1, fill='both', side='bottom') + + def __config_calendar(self): + cols = self._cal.formatweekheader(3).split() + self._calendar['columns'] = cols + self._calendar.tag_configure('header', background='grey90') + self._calendar.insert('', 'end', values=cols, tag='header') + # adjust its columns width + font = tkFont.Font() + maxwidth = max(font.measure(col) for col in cols) + for col in cols: + self._calendar.column(col, width=maxwidth, minwidth=maxwidth, + anchor='e') + + def __setup_selection(self, sel_bg, sel_fg): + self._font = tkFont.Font() + self._canvas = canvas = Tkinter.Canvas(self._calendar, + background=sel_bg, borderwidth=0, highlightthickness=0) + canvas.text = canvas.create_text(0, 0, fill=sel_fg, anchor='w') + + canvas.bind('', lambda evt: canvas.place_forget()) + self._calendar.bind('', lambda evt: canvas.place_forget()) + self._calendar.bind('', self._pressed) + + def __minsize(self, evt): + width, height = self._calendar.master.geometry().split('x') + height = height[:height.index('+')] + self._calendar.master.minsize(width, height) + + def _build_calendar(self): + year, month = self._date.year, self._date.month + + # update header text (Month, YEAR) + header = self._cal.formatmonthname(year, month, 0) + self._header['text'] = header.title() + + # update calendar shown dates + cal = self._cal.monthdayscalendar(year, month) + for indx, item in enumerate(self._items): + week = cal[indx] if indx < len(cal) else [] + fmt_week = [('%02d' % day) if day else '' for day in week] + self._calendar.item(item, values=fmt_week) + + def _show_selection(self, text, bbox): + """Configure canvas for a new selection.""" + x, y, width, height = bbox + + textw = self._font.measure(text) + + canvas = self._canvas + canvas.configure(width=width, height=height) + canvas.coords(canvas.text, width - textw, height / 2 - 1) + canvas.itemconfigure(canvas.text, text=text) + canvas.place(in_=self._calendar, x=x, y=y) + + # Callbacks + + def _pressed(self, evt): + """Clicked somewhere in the calendar.""" + x, y, widget = evt.x, evt.y, evt.widget + item = widget.identify_row(y) + column = widget.identify_column(x) + + if not column or not item in self._items: + # clicked in the weekdays row or just outside the columns + return + + item_values = widget.item(item)['values'] + if not len(item_values): # row is empty for this month + return + + text = item_values[int(column[1]) - 1] + if not text: # date is empty + return + + bbox = widget.bbox(item, column) + if not bbox: # calendar not visible yet + return + + # update and then show selection + text = '%02d' % text + self._selection = (text, item, column) + self._show_selection(text, bbox) + + def _prev_month(self): + """Updated calendar to show the previous month.""" + self._canvas.place_forget() + + self._date = self._date - self.timedelta(days=1) + self._date = self.datetime(self._date.year, self._date.month, 1) + self._build_calendar() # reconstuct calendar + + def _next_month(self): + """Update calendar to show the next month.""" + self._canvas.place_forget() + + year, month = self._date.year, self._date.month + self._date = self._date + self.timedelta( + days=calendar.monthrange(year, month)[1] + 1) + self._date = self.datetime(self._date.year, self._date.month, 1) + self._build_calendar() # reconstruct calendar + + # Properties + + @property + def selection(self): + """Return a datetime representing the current selected date.""" + if not self._selection: + return None + + year, month = self._date.year, self._date.month + return self.datetime(year, month, int(self._selection[0])) + +def test(): + import sys + root = Tkinter.Tk() + root.title('Ttk Calendar') + ttkcal = Calendar(firstweekday=calendar.SUNDAY) + ttkcal.pack(expand=1, fill='both') + + if 'win' not in sys.platform: + style = ttk.Style() + style.theme_use('clam') + + root.mainloop() + +if __name__ == '__main__': + test() diff --git a/dicat/DICAT.py b/dicat/DICAT.py new file mode 100644 index 0000000..080316b --- /dev/null +++ b/dicat/DICAT.py @@ -0,0 +1,151 @@ +#!/usr/bin/python + +# Import from standard packages +import ttk +from Tkinter import * + +# Import DICAT libraries +from dicom_anonymizer_frame import dicom_deidentifier_frame_gui +from IDMapper import IDMapper_frame_gui +from scheduler_application import UserInterface +from welcome_frame import welcome_frame_gui +import ui.menubar as MenuBar + + + +class DicAT_application(): + + + def __init__(self, master, side=LEFT): + """ + Constructor of the class DICAT called with a parent widget ("master") + to which we will add a number of child widgets. The constructor starts + by creating a "Frame" widget. A frame is a simple container. + + :param master: + :type master: object + :param side: + :type side: + + """ + + self.dir_opt = {} + + # Title of the application + master.title("DICAT") + + # Use notebook (nb) from ttk from Tkinter to create tabs + self.nb = ttk.Notebook(master) + + # Add frames as pages for ttk.Notebook + self.page1 = ttk.Frame(self.nb) + + # Second page, DICOM anonymizer + self.page2 = ttk.Frame(self.nb) + + # Third page, Scheduler + self.page3 = ttk.Frame(self.nb) + + # Fourth page, ID key + self.page4 = ttk.Frame(self.nb) + + # Add the pages to the notebook + self.nb.add(self.page1, text='Welcome to DICAT!') + self.nb.add(self.page2, text='DICOM de-identifier') + self.nb.add(self.page3, text='Scheduler') + self.nb.add(self.page4, text='ID key') + + # Draw + self.nb.pack(expand=1, fill='both') + + # Draw content of the different tabs' frame + self.dicom_deidentifier_tab() + self.id_key_frame() + self.welcome_page() + self.scheduler_page(master) + + # refresh tab when selecting it + self.page3.bind("", self.update_scheduler) + self.page4.bind("", self.update_id_mapper) + + def dicom_deidentifier_tab(self): + """ + Start the DICOM de-identifier frame. + + """ + + # start dicom_anonymizer_frame_gui method + dicom_deidentifier_frame_gui(self.page2) + + + def id_key_frame(self): + """ + Start the ID Mapper frame. + + """ + + # start the ID mapper frame gui + self.IDMapper = IDMapper_frame_gui(self.page4) + + + def welcome_page(self): + """ + Start the Welcome frame. + + """ + + # start the Welcome page + welcome_frame_gui(self.page1) + + def scheduler_page(self, master): + """ + Start the scheduler frame. + + :param master: + :type master: + """ + + # description_frame_gui the menu bar and start the scheduler frame + menu = MenuBar.SchedulerMenuBar(master) + master.config(menu=menu) + self.scheduler = UserInterface(self.page3) + + def update_scheduler(self, event): + """ + Reload the scheduler tables when the tab is selected + + :param event: event + :type event: object + + """ + + # reload the scheduler + self.scheduler.load_xml() + + def update_id_mapper(self, event): + """ + Reload the IDMapper table when the tab is selected + + :param event: event + :type event: object + + """ + + # reload the IDMapper + self.IDMapper.load_xml() + +if __name__ == "__main__": + + # Create a Tk root widget. + root = Tk() + + app = DicAT_application(root) + + # The window won't appear until we've entered the Tkinter event loop. + # The program will stay in the event loop until we close the window. + root.mainloop() + + # Some development environments won't terminate the Python process unless + # it is explicitly mentioned to destroy the main window when the loop is + # terminated. +# root.destroy() diff --git a/dicat/DicAT_application.py b/dicat/DicAT_application.py deleted file mode 100644 index 8e02872..0000000 --- a/dicat/DicAT_application.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/python - -import ttk -from Tkinter import * - -from dicom_anonymizer_frame import dicom_deidentifier_frame_gui -from IDMapper import IDMapper_frame_gui -from welcome_frame import welcome_frame_gui - -class DicAT_application(): - - # Constructor of the class DicAT called with a parent widget ("master") - # to which we will add a number of child widgets. The constructor starts - # by creating a "Frame" widget. A frame is a simple container. - def __init__(self, master, side=LEFT): - - self.dir_opt = {} - - # Title of the application - master.title("DICAT") - - # Use notebook (nb) from ttk from Tkinter to create tabs - self.nb = ttk.Notebook(master) - - # Add frames as pages for ttk.Notebook - self.page1 = ttk.Frame(self.nb) - - # Second page, DICOM anonymizer - self.page2 = ttk.Frame(self.nb) - - # Third page, Scheduler - self.page3 = ttk.Frame(self.nb) - - # Fourth page, ID key - self.page4 = ttk.Frame(self.nb) - - # Add the pages to the notebook - self.nb.add(self.page1, text='Welcome to DicAT!') - self.nb.add(self.page2, text='DICOM de-identifier') - self.nb.add(self.page3, text='Scheduler', state='hidden') # hide scheduler for now - self.nb.add(self.page4, text='ID key') - - # Draw - self.nb.pack(expand=1, fill='both') - - # Draw content of the different tabs' frame - self.dicom_deidentifier_tab() - self.id_key_frame() - self.welcome_page() - - def dicom_deidentifier_tab(self): - - # start dicom_anonymizer_frame_gui method - dicom_deidentifier_frame_gui(self.page2) - - - def id_key_frame(self): - - # start the ID mapper frame gui - IDMapper_frame_gui(self.page4) - - - def welcome_page(self): - - # start the Welcome page - welcome_frame_gui(self.page1) - - - -if __name__ == "__main__": - - # Create a Tk root widget. - root = Tk() - - app = DicAT_application(root) - - # The window won't appear until we've entered the Tkinter event loop. - # The program will stay in the event loop until we close the window. - root.mainloop() - - # Some development environments won't terminate the Python process unless it is - # explicitly mentioned to destroy the main window when the loop is terminated. -# root.destroy() diff --git a/dicat/IDMapper.py b/dicat/IDMapper.py index 4748869..363021c 100644 --- a/dicat/IDMapper.py +++ b/dicat/IDMapper.py @@ -1,11 +1,13 @@ #!/usr/bin/python -import Tkinter, Tkconstants, tkFileDialog, tkMessageBox, re, datetime from Tkinter import * -from xml.dom import minidom +import ttk +import re +import lib.datamanagement as DataManagement +from lib.candidate import Candidate +import lib.multilanguage as MultiLanguage -import ttk def sortby(tree, col, descending): """Sort tree contents when a column is clicked on.""" @@ -26,7 +28,11 @@ def sortby(tree, col, descending): class IDMapper_frame_gui(Frame): def __init__(self, parent): - """Initialize the application""" + """ + Initialize the ID Mapper frame. + + """ + self.parent = parent # Set up the dictionary map @@ -37,60 +43,20 @@ def __init__(self, parent): def initialize(self): + """ + Initialize the ID Mapper GUI by calling self.init_ui(). - # initialize Frame - self.frame = Frame(self.parent) - self.frame.pack(expand=1, fill='both') - self.frame.columnconfigure(0, weight=1) - self.frame.columnconfigure(1, weight=1) - self.frame.columnconfigure(2, weight=6) - - - # select an existing candidate.xml file - # Initialize default text that will be in self.entry - self.entryVariable = Tkinter.StringVar() - self.entryVariable.set("Open an XML file with candidate's key") - - # Create an entry with a default text that will be replaced by the path - # to the XML file once directory selected - self.entry = Entry(self.frame, - width=40, - textvariable=self.entryVariable - ) - self.entry.focus_set() - self.entry.selection_range(0, Tkinter.END) - - # Create an open button to use to select an XML file with candidate's - # key info - self.buttonOpen = Button(self.frame, - text=u"Open an existing file", - command=self.openfilename - ) - - self.buttonCreate = Button(self.frame, - text=u"Create a new file", - command=self.createfilename - ) - - self.buttonCreate.grid(row=0, - column=0, - padx=(0, 15), - pady=10, - sticky=E + W - ) - self.buttonOpen.grid(row=0, - column=1, - padx=(0, 15), - pady=10, - sticky=E + W - ) - self.entry.grid(row=0, column=2, padx=15, pady=10, sticky=E + W) - - self.InitUI() - - - - def InitUI(self): + """ + + # Initialize GUI + self.init_ui() + + + def init_ui(self): + """ + Draws the ID Mapper GUI. + + """ self.frame = Frame(self.parent) self.frame.pack(expand=1, fill='both') @@ -102,296 +68,370 @@ def InitUI(self): for i in range(3, 4): self.frame.rowconfigure(i, weight=1) - self.labelID = Label(self.frame, text=u'Identifier') - self.labelName = Label(self.frame, text=u'Real Name') - self.labelDoB = Label(self.frame, text=u'Date of Birth (YYYY-MM-DD)') - - self.buttonAdd = Button(self.frame, width=12, - text=u'Add candidate', - command=self.AddIdentifierEvent - ) - self.buttonClear = Button(self.frame, width=12, - text=u'Clear fields', - command=self.clear - ) - self.buttonSearch = Button(self.frame, width=12, - text=u'Search candidate', - command=self.search - ) - self.buttonEdit = Button(self.frame, width=12, - text=u'Edit candidate', - command=self.edit - ) + self.labelID = Label(self.frame, text=u'Identifier') + self.labelFirstName = Label(self.frame, text=u'First Name') + self.labelLastName = Label(self.frame, text=u'Last Name') + self.labelDoB = Label( + self.frame, text=u'Date of Birth (YYYY-MM-DD)' + ) + + self.buttonAdd = Button( + self.frame, + width=12, + text=u'Add candidate', + command=self.add_identifier_event + ) + self.buttonClear = Button( + self.frame, + width=12, + text=u'Clear fields and filters', + command=self.clear_event + ) + self.buttonSearch = Button( + self.frame, + width=12, + text=u'Search candidate', + command=self.search_event + ) + self.buttonEdit = Button( + self.frame, + width=12, + text=u'Edit candidate', + command=self.edit_event + ) self.textCandId = StringVar() - self.candidateid = Entry(self.frame, - textvariable=self.textCandId, - width=20 - ) + self.candidateid = Entry( + self.frame, textvariable=self.textCandId, width=20 + ) self.candidateid.focus_set() - self.textCandName = StringVar() - self.candidatename = Entry(self.frame, - textvariable=self.textCandName, - width=20 - ) + self.textCandFirstName = StringVar() + self.candidateFirstName = Entry( + self.frame, textvariable=self.textCandFirstName, width=20 + ) + + self.textCandLastName = StringVar() + self.candidateLastName = Entry( + self.frame, textvariable=self.textCandLastName, width=20 + ) self.textCandDoB = StringVar() - self.candidateDoB = Entry(self.frame, - textvariable=self.textCandDoB, - width=20 - ) - - self.tableColumns = ("Identifier", "Real Name", "Date of Birth") - self.datatable = ttk.Treeview(self.frame, - selectmode='browse', - columns=self.tableColumns, - show="headings") + self.candidateDoB = Entry( + self.frame, textvariable=self.textCandDoB, width=20 + ) + + self.tableColumns = ( + "Identifier", "First Name", "Last Name", "Date of Birth" + ) + self.datatable = ttk.Treeview( + self.frame, + selectmode='browse', + columns=self.tableColumns, + show="headings" + ) for col in self.tableColumns: - self.datatable.heading(col, text=col.title(), - command=lambda c=col: sortby(self.datatable, c, 0)) + self.datatable.heading( + col, + text=col.title(), + command=lambda c=col: sortby(self.datatable, c, 0) + ) - self.datatable.bind("<>", self.OnRowClick) + self.datatable.bind("<>", self.on_row_click_event) self.ErrorMessage = StringVar() self.error = Label(self.frame, textvariable=self.ErrorMessage, fg='red') - self.labelID.grid(row=0, column=0, padx=(0,4), sticky=E+W) - self.labelName.grid(row=0, column=1, padx=(4,4), sticky=E+W) - self.labelDoB.grid(row=0, column=2, padx=(4,4), sticky=E+W) + self.labelID.grid( row=0, column=0, padx=(0,4), sticky=E+W) + self.labelFirstName.grid(row=0, column=1, padx=(4,4), sticky=E+W) + self.labelLastName.grid( row=0, column=2, padx=(4,4), sticky=E+W) + self.labelDoB.grid( row=0, column=3, padx=(4,4), sticky=E+W) - self.candidateid.grid(row=1, column=0, padx=(0,4), pady=(0,10), sticky=E+W) - self.candidatename.grid(row=1, column=1, padx=(4,4), pady=(0,10), sticky=E+W) - self.candidateDoB.grid(row=1, column=2, padx=(4,4), pady=(0,10), sticky=E+W) + self.candidateid.grid( + row=1, column=0, padx=(0,4), pady=(0,10), sticky=E+W + ) + self.candidateFirstName.grid( + row=1, column=1, padx=(4,4), pady=(0,10), sticky=E+W + ) + self.candidateLastName.grid( + row=1, column=2, padx=(4,4), pady=(0,10), sticky=E+W + ) + self.candidateDoB.grid( + row=1, column=3, padx=(4,4), pady=(0,10), sticky=E+W + ) - self.buttonAdd.grid(row=2, column=1, padx=(4,0), sticky=E+W) - self.buttonClear.grid(row=1, column=3, padx=(4,0), sticky=E+W) - self.buttonSearch.grid(row=2, column=0, padx=(4,0), sticky=E+W) - self.buttonEdit.grid(row=2, column=2, padx=(4,0), sticky=E+W) + self.buttonClear.grid( row=2, column=0, padx=(4,0), sticky=E+W) + self.buttonSearch.grid(row=2, column=1, padx=(4,0), sticky=E+W) + self.buttonEdit.grid( row=2, column=2, padx=(4,0), sticky=E+W) + self.buttonAdd.grid( row=2, column=3, padx=(4,0), sticky=E+W) + + self.datatable.grid( + row=3, column=0, columnspan=4, pady=10, sticky='nsew' + ) + self.error.grid(row=4, column=0, columnspan=4) - self.datatable.grid(row=3, column=0, columnspan=3, pady=10, sticky='nsew') - self.error.grid(row=3, column=3) + def load_xml(self): + """ + Parses the XML file and loads the data into the IDMapper. + Calls check_and_save_data with option action=False as we don't want to + save the candidates to be added to the datatable back in the XML. - def LoadXML(self, file): - global xmlitemlist - global xmldoc + """ + + global data # empty the datatable and data dictionary before loading new file self.datatable.delete(*self.datatable.get_children()) self.IDMap = {} - """Parses the XML file and loads the data into the current window""" try: - xmldoc = minidom.parse(file) - xmlitemlist = xmldoc.getElementsByTagName('Candidate') - for s in xmlitemlist: - identifier = s.getElementsByTagName("Identifier")[0].firstChild.nodeValue - realname = s.getElementsByTagName("RealName")[0].firstChild.nodeValue - dob = s.getElementsByTagName("DateOfBirth")[0].firstChild.nodeValue - self.AddIdentifierAction(identifier, realname, dob, False) - except: - pass - - - def SaveMapAction(self): + data = DataManagement.read_candidate_data() + for key in data: + identifier = data[key]["Identifier"] + firstname = data[key]["FirstName"] + lastname = data[key]["LastName"] + dob = data[key]["DateOfBirth"] + self.check_and_save_data( + identifier, firstname, lastname, dob, False + ) + except Exception as e: + #TODO add error login (in case a candidate data file does not exist) + print str(e) + + + def add_identifier_event(self): + """ + Event handler for the 'Add new candidate' button. Will call + check_and_save_data on what has been entered in the Entry boxes. + + """ - """Function which performs the action of writing the XML file""" - f = open(self.filename, "w") - f.write("\n\n") - for key in self.IDMap: - f.write("\t\n") - f.write("\t\t%s\n" % key) - f.write("\t\t%s\n" % self.IDMap[key][1]) - f.write("\t\t%s\n" % self.IDMap[key][2]) - f.write("\t\n") - f.write("") + firstname = self.candidateFirstName.get() + lastname = self.candidateLastName.get() + candid = self.candidateid.get() + dob = self.candidateDoB.get() + self.check_and_save_data(candid, firstname, lastname, dob, 'save') - def SaveMapEvent(self, event): - """Handles any wxPython event which should trigger a save action""" - self.SaveMapAction() + def on_row_click_event(self, event): + """ + Update the text boxes' data on row click. + """ - def AddIdentifierEvent(self): + item_id = str(self.datatable.focus()) + item = self.datatable.item(item_id)['values'] - name = self.candidatename.get() - candid = self.candidateid.get() - dob = self.candidateDoB.get() - self.AddIdentifierAction(candid, name, dob) + self.textCandId.set(item[0]) + self.textCandFirstName.set(item[1]) + self.textCandLastName.set(item[2]) + self.textCandDoB.set(item[3]) - def AddIdentifierAction(self, candid, realname, dob, save=True): + def clear_event(self): """ - Adds the given identifier and real name to the mapping. If - the "save" parameter is true, this also triggers the saving - of the XML file. - This is set to False on initial load. + Event handler for the clear_event button. Will clear_event all the + Entry boxes. + """ - self.ErrorMessage.set("") - # check that all fields are set - if not candid or not realname or not dob: - message = "ERROR:\nAll fields are\nrequired to add\na candidate" - self.ErrorMessage.set(message) - return + self.textCandId.set("") + self.textCandFirstName.set("") + self.textCandLastName.set("") + self.textCandDoB.set("") + self.candidateid.focus_set() - # check candid does not already exist - if candid in self.IDMap: - message = "ERROR:\nCandidate ID\nalready exists" - self.ErrorMessage.set(message) - return + self.load_xml() # Reload the entire dataset. - # check dob is in format YYYY-MM-DD - try: - datetime.datetime.strptime(dob,"%Y-%m-%d") - except ValueError: - message = "ERROR:\nDate of birth's\nformat should be\n'YYYY-MM-DD'" + + def search_event(self): + """ + Event handler for the search_event button. Will call find_candidate + function to find the proper candidate matching what has been filled in + the Entry boxes. + + """ + + # Grep the data from the Entry fields + data_captured = {} + if self.textCandId.get(): + data_captured['Identifier'] = self.textCandId.get() + if self.textCandFirstName.get(): + data_captured['FirstName'] = self.textCandFirstName.get() + if self.textCandLastName.get(): + data_captured['LastName'] = self.textCandLastName.get() + if self.textCandDoB.get(): + data_captured['DateOfBirth'] = self.textCandDoB.get() + + # If no data entered, write a message saying at least one field should + # be entered and return + if not data_captured: + message = MultiLanguage.dialog_no_data_entered self.ErrorMessage.set(message) return - mapList = [candid, realname, dob] - self.IDMap[candid] = mapList - - insertedList = [(candid, realname, dob)] - for item in insertedList: - self.datatable.insert('', 'end', values=item) - - if(save): - self.SaveMapAction() + # Use the function find_candidate to find all matching candidates and + # return them in the filtered data dictionary + filtered_data = self.find_candidate(data_captured) + # Display only the filtered data using DisplayCandidates(filtered_data) + self.display_filtered_data(filtered_data) - def OnRowClick(self, event): - - """Update the text boxes' data on row click""" - item_id = str(self.datatable.focus()) - item = self.datatable.item(item_id)['values'] - - self.textCandId.set(item[0]) - self.textCandName.set(item[1]) - self.textCandDoB.set(item[2]) + # Show a message warning to say that filters are set + message = MultiLanguage.warning_filters_set + self.ErrorMessage.set(message) - def clear(self): - - self.textCandId.set("") - self.textCandName.set("") - self.textCandDoB.set("") - self.candidateid.focus_set() + def find_candidate(self, data_captured): + """ + Find a candidate based on one of the fields entered in the Entry boxes. + :param data_captured: dictionary of the data captured in the Entry boxes + :type data_captured: dict - def search(self): - # Find a candidate based on its ID if it is set in text box - if self.textCandId.get(): - (candid, name, dob) = self.FindCandidate("candid", - self.textCandId.get() - ) - # or based on its name if it is set in text box - elif self.textCandName.get(): - (candid, name, dob) = self.FindCandidate("name", - self.textCandName.get() - ) - # print the values in the text box - self.textCandId.set(candid) - self.textCandName.set(name) - self.textCandDoB.set(dob) - - - def FindCandidate(self, key, value): - global xmlitemlist + :return filtered_data: dictionary of the matching candidates + :rtype filtered_data: dict + + """ + + global data # Loop through the candidate tree and return the candid, name and dob # that matches a given value - for s in xmlitemlist: - candid = s.getElementsByTagName("Identifier")[0].firstChild.nodeValue - name = s.getElementsByTagName("RealName")[0].firstChild.nodeValue - dob = s.getElementsByTagName("DateOfBirth")[0].firstChild.nodeValue - if (key == "candid" and value == candid): - return (candid, name, dob) - elif (key == "name" and value == name): - return (candid, name, dob) - elif (key == "dob" and value == dob): - return (candid, name, dob) - else: - continue - # if candidate was not found, return empty strings - return ("", "", "") - - - def edit(self): - self.EditIdentifierAction(self.textCandId.get(), - self.textCandName.get(), - self.textCandDoB.get() - ) - - def EditIdentifierAction(self, identifier, realname, realdob, edit=True): - global xmlitemlist - # Loop through the candidate tree, find a candidate based on its ID - # and check if name or DoB needs to be updated - for s in xmlitemlist: - # initialize update variable - update = False - - # get the candid, name and dob stored in the XML file - candid = s.getElementsByTagName("Identifier")[0].firstChild.nodeValue - name = s.getElementsByTagName("RealName")[0].firstChild.nodeValue - dob = s.getElementsByTagName("DateOfBirth")[0].firstChild.nodeValue - - # if name of candidate is changed - if (candid == identifier) and not (realname == name): - # update in the XML file - s.getElementsByTagName("RealName")[0].firstChild.nodeValue = realname - update = True # set update to True - - # if candidate's date of birth is changed - if (candid == identifier) and not (realdob == dob): - # update in the XML file - s.getElementsByTagName("DateOfBirth")[0].firstChild.nodeValue = realdob - update = True # set update to True - - if (update): - # update the XML file - f = open(self.filename, "w") - xmldoc.writexml(f) - - # update IDMap dictionary - mapList = [candid, realname, realdob] - self.IDMap[candid] = mapList - - # update datatable - item = self.datatable.selection() - updatedList = (candid, realname, realdob) - self.datatable.item(item, values=updatedList) - - def openfilename(self): - - """Returns a selected file name.""" - self.filename = tkFileDialog.askopenfilename( - filetypes=[("XML files", "*.xml")] - ) - self.entryVariable.set(self.filename) + # Create a filtered_data dictionary that will store all matching + # candidates + filtered_data = {} + # Create an 'add' boolean on whether should add a candidate to the + # filtered list and set it to False + add = False + for cand_key in data: + # Grep the candidate information from data + candid = data[cand_key]["Identifier"] + firstname = data[cand_key]["FirstName"] + lastname = data[cand_key]["LastName"] + dob = data[cand_key]["DateOfBirth"] + if 'DateOfBirth' in data_captured \ + and re.search(data_captured['DateOfBirth'], dob): + add = True # set the 'add' boolean to true to add candidate + if 'FirstName' in data_captured \ + and re.search(data_captured['FirstName'], firstname): + add = True # set the 'add' boolean to true to add candidate + if 'LastName' in data_captured \ + and re.search(data_captured['LastName'], lastname): + add = True # set the 'add' boolean to true to add candidate + if 'Identifier' in data_captured \ + and re.search(data_captured['Identifier'], candid): + add = True # set the 'add' boolean to true to add candidate + + # If add is set to True, add the candidate to the filtered list. + if add: + filtered_data[cand_key] = data[cand_key] + add = False # reset the 'add' boolean to false for next cand_key + + return filtered_data + + + def display_filtered_data(self, filtered_data): + """ + Displays only the filtered data matching the search_event. + + :param filtered_data: dictionary of the matching candidates + :type filtered_data: dict + + """ + + # Empty the datatable and data dictionary before loading filtered data + self.datatable.delete(*self.datatable.get_children()) + self.IDMap = {} + + # Loop through the data and display them in the datatable + for key in filtered_data: + identifier = filtered_data[key]["Identifier"] + firstname = filtered_data[key]["FirstName"] + lastname = filtered_data[key]["LastName"] + dob = filtered_data[key]["DateOfBirth"] + self.check_and_save_data( + identifier, firstname, lastname, dob, False + ) - if self.filename: - # Load the data - self.LoadXML(self.filename) - return self.filename + def edit_event(self): + """ + Edit event of the Edit button. Will call check_and_save_date with + data entered in the Entry boxes and action='edit_event'. - def createfilename(self): + """ - self.filename = tkFileDialog.asksaveasfilename( - defaultextension=[("*.xml")], - filetypes=[("XML files", "*.xml")] + self.check_and_save_data( + self.textCandId.get(), + self.textCandFirstName.get(), + self.textCandLastName.get(), + self.textCandDoB.get(), + 'edit' ) - self.entryVariable.set(self.filename) - if self.filename: - open(self.filename, 'w') - # Load the data - self.LoadXML(self.filename) + def check_and_save_data(self, id, firstname, lastname, dob, action=False): + """ + Grep the candidate data and check them before saving and updating the + XML file and the datatale. + + :param id: identifier of the candidate + :type id: str + :param firstname: firstname of the candidate + :type firstname: str + :param lastname: lastname of the candidate + :type lastname: str + :param dob: date of birth of the candidate + :type dob: str + :param action: either 'save' new candidate or 'edit' a candidate + :type action: bool/str + + :return: + + """ + + # Check all required data are available + cand_data = {} + cand_data["Identifier"] = id + cand_data["FirstName"] = firstname + cand_data["LastName"] = lastname + cand_data["DateOfBirth"] = dob + candidate = Candidate(cand_data) + + # Initialize message to False. Will be replaced by the appropriate error + # message if checks failed. + message = False + if action == 'save': + message = candidate.check_candidate_data('IDmapper', False) + elif action == 'edit': + message = candidate.check_candidate_data('IDmapper', id) + + # If message contains an error message, display it and return + if message: + self.ErrorMessage.set(message) + return + + # Save the candidate data in the XML + DataManagement.save_candidate_data(cand_data) + + # Update the IDMap dictionary + mapList = [id, firstname, lastname, dob] + self.IDMap[id] = mapList + + # Update datatable + if action == 'edit': + item = self.datatable.selection() + updatedList = (id, firstname, lastname, dob) + self.datatable.item(item, values=updatedList) + else: + insertedList = [(id, firstname, lastname, dob)] + for item in insertedList: + self.datatable.insert('', 'end', values=item) + + - return self.filename def main(): diff --git a/dicat/data/database_template.xml b/dicat/data/database_template.xml new file mode 100644 index 0000000..c0ecdbf --- /dev/null +++ b/dicat/data/database_template.xml @@ -0,0 +1,46 @@ + + + + DICAT test + This is a test project. + + 0 + 100 + 2000 + 2050 + + Project 1 + + Subproject 1 + + V0 + 1 + + 25-35 + + V1 + 2 + 55-65 + + V2 + 3 + 95-105 + + + V3 + 4 + + + + Subproject 2 + + V0 + 1 + + + + + + + + diff --git a/dicat/dicom_anonymizer_frame.py b/dicat/dicom_anonymizer_frame.py index 96bae38..e0262b8 100755 --- a/dicat/dicom_anonymizer_frame.py +++ b/dicat/dicom_anonymizer_frame.py @@ -38,7 +38,7 @@ def __init__(self, parent): def initialize(self): - # initialize main Frame + # description_frame_gui main Frame self.frame = Frame(self.parent) self.frame.pack(expand=1, fill='both') @@ -102,6 +102,11 @@ def askdirectory(self): return self.dirname def deidentify(self): + + # clear edit table if it exists + if hasattr(self, 'field_edit_win'): + self.field_edit_win.destroy() + # Read the XML file with the identifying DICOM fields load_xml = PathMethods.resource_path("data/fields_to_zap.xml") XML_filename = load_xml.return_path() @@ -267,4 +272,4 @@ def collect_edited_data(self): columnspan=2, padx=(0, 10), sticky=E+W - ) \ No newline at end of file + ) diff --git a/dicat/lib/__init__.py b/dicat/lib/__init__.py index 918ddf7..2da98fd 100644 --- a/dicat/lib/__init__.py +++ b/dicat/lib/__init__.py @@ -1,4 +1,10 @@ """ -From the documentation at https://docs.python.org/2/tutorial/modules.html#packages -The __init__.py files are required to make Python treat the directories as containing packages; this is done to prevent directories with a common name, such as string, from unintentionally hiding valid modules that occur later on the module search path. In the simplest case, __init__.py can just be an empty file, but it can also execute initialization code for the package or set the __all__ variable, described later. -""" +From https://docs.python.org/2/tutorial/modules.html#packages documentation + +The __init__.py files are required to make Python treat the directories as +containing packages; this is done to prevent directories with a common name, +such as string, from unintentionally hiding valid modules that occur later on +the module search path. In the simplest case, __init__.py can just be an empty +file, but it can also execute initialization code for the package or set the +__all__ variable, described later. +""" \ No newline at end of file diff --git a/dicat/lib/candidate.py b/dicat/lib/candidate.py new file mode 100644 index 0000000..b164f07 --- /dev/null +++ b/dicat/lib/candidate.py @@ -0,0 +1,253 @@ +# import standard packages +import datetime + +#from dicat.scheduler_visit import Visit + +# import internal packages +import lib.datamanagement as DataManagement +import lib.multilanguage as MultiLanguage +import lib.utilities as Utilities + + +class Candidate(): + """ + The Candidate class defines the candidates/participants of the study + + Attributes: + uid: A unique identifier using python's uuid1 method. Used as key to store and retrieve objects from + files and/or dictionaries. + firstname: First name of the candidate + lastname: Last name of the candidate + visitset: A dictionnary containing all visits (planed or not) + phone: A primary phone number + status: Status of this candidate + pscid: Loris (clinical results database) specific ID + + kwargs: Not implemented yet! + + Code example: + candidatedb = {} #setup a dict to receive the candidates + candidatedata = candidate.Candidate('Billy', 'Roberts', '451-784-9856', otherphone='514-874-9658') #instanciate one candidate + candidatedb[candidatedata.uid] = candidatedata #add candidate to dict + DataManagement.save_candidate_data(candidatedb) #save data to file + """ + def __init__(self, cand_data): + # Required fields in cand_data + self.pscid = cand_data['Identifier'] + self.dob = cand_data['DateOfBirth'] + self.firstname = cand_data['FirstName'] + self.lastname = cand_data['LastName'] + + # Optional fields + if 'Gender' in cand_data: + self.gender = cand_data['Gender'] + else: + self.gender = " " + if 'PhoneNumber' in cand_data: + self.phone = cand_data['PhoneNumber'] + else: + self.phone = "" + if 'CandidateStatus' in cand_data: + self.cand_status = cand_data['CandidateStatus'] + else: + self.cand_status = " " + + #TODO check if VisitSet necessary here. Commenting it for now. + #self.visitset = cand_data['VisitSet'] + + + def check_candidate_data(self, tab, candidate=''): + """ + Check that the data entered in the data window for a given candidate is + as expected. If not, will return an error message that can be displayed. + + :param tab: IDMapper or Scheduler tab + :type tab: str + + :return: error message determined by the checks + :rtype: str + + """ + + # Check that all required fields are set (a.k.a. 'Identifier', + # 'FirstName', 'LastName', 'Gender' and 'DateOfBirth'), if not, return + # an error. (Error message stored in + # MultiLanguage.dialog_missing_candidate_info variable) + if not self.pscid or not self.firstname or not self.lastname \ + or not self.dob or (tab == 'scheduler' and self.gender == " "): + # If it is the scheduler capturing the data, gender must be set + if tab == 'scheduler' and self.gender == " ": + return MultiLanguage.dialog_missing_cand_info_schedul + return MultiLanguage.dialog_missing_cand_info_IDmapper + + # If candidate is new, check that the 'Identifier' used is unique + candIDs_array = DataManagement.grep_list_of_candidate_ids() + # if candidate not populated with a candID, it means we are creating a + # new candidate so we need to check if the PSCID entered is unique. + if candidate == 'new' and self.pscid in candIDs_array: + return MultiLanguage.dialog_candID_already_exists + + # If Date of Birth does not match YYYY-MM-DD, return an error + # (Error message is stored in MultiLanguage.dialog_bad_dob_format) + date_ok = Utilities.check_date_format(self.dob) + if not date_ok: + return MultiLanguage.dialog_bad_dob_format + + # If we get there, it means all the data is good so no message needs + # to be displayed. Return False so that no message is displayed + return False + + + + + + def setup_visitset(self): + """ + When creating the first visit for a candidate, a complete set of visits is added based on a study/project visit list + There are no parameters passed to this method since it will simply create a new 'empty' + This method will: + 1-open studydata (the study visit list) and 'parse' the dict to a sorted list (dict cannot be sorted) + 2-create a temporary list of visit_labels that will serve as a key within the Candidate.visit_set dictionary + 3-instantiate individual visits based on the study visit list + Usage: + Called by Candidate.set_visit_date() + """ + visit_list =[] + visit_label_templist = [] + study_setup = dict() + try: + #1-open studydata + study_setup = dict(DataManagement.read_study_data()) + except Exception as e: + print str(e) #TODO add error login (in case a study data file does not exist) + for key, value in study_setup.iteritems(): + #2-parse into a sorted list + visit_list.append(study_setup[key]) + visit_list = sorted(visit_list, key=lambda visit: visit.rank) + #create a temporary list of visitlabels + for each in visit_list: + visit_label_templist.append(each.visitlabel) + #3-instantiate individual visit based on each instance of VisitSetup() + self.visitset = {} #setup a dict to receive a set of Visit() + count = 0 + #set values of : uid, rank, visit_label, previous_visit, visit_window, visitmargin, + for key in visit_list: + mainkey = str(visit_label_templist[count]) + rank =key.rank + visit_label = key.visitlabel + previous_visit = key.previousvisit + visit_window = key.visitwindow + visit_margin = key.visitmargin + visit_data = visit.Visit(rank, visit_label, previous_visit, visit_window, visit_margin,) + self.visitset[mainkey] = visit_data + count += 1 + + def set_visit_date(self, visitlabel, visitdate, visittime, visitwhere, visitwhom): + """ + This method update the visit information (according to visitlabel) + This method will: + 1-Check if visitset==None. If so, then Candidate.setup_visitset() is called to setup Candidate.visitset + 2-Check if a date is already set for this visitlabel + 3-Set values of Visit.when, 'Visit.where, Visit.whithwhom and Visit.status for current visit + Usage: Called by GUI methods + Return: current_visit as current Visit(Visit(VisitSetup) instance + """ + #1-Check to see if visitset == None before trying to create a new date instance + if self.visitset is None: + self.setup_visitset() + self.set_candidate_status_active() #Candidate.status='active' since we're setting up a first visit + #get current visit within visitset + current_visit = self.visitset.get(visitlabel) + #2-Check to see if this visit already has a date + if current_visit.when is not None: + print "date already exists" #TODO add confirmation of change log??? + pass + #concatenate visitdate and visittime and parse into a datetime object + visitwhen = visitdate + ' ' + visittime + when = datetime.datetime.strptime(visitwhen, '%Y-%m-%d %H:%M') + #3-Set values of Visit.when, 'Visit.where, Visit.whithwhom and Visit.status for current visit + current_visit.when = when + current_visit.where = visitwhere + current_visit.withwhom = visitwhom + current_visit.status = "active" #that is the status of the visit + return current_visit + + + """ + def set_next_visit_window(self, candidate, current_visit): + #get the current visit object as argument. Will search_event and look for the next visit (visit where previousvisit == current_visit_label) + next_visit_searchset = candidate.visitset + current_visit_label = current_visit.visitlabel + next_visit = "" + for key in next_visit_searchset: + visit_data = next_visit_searchset[key] + if visit_data.previousvisit == current_visit_label: + next_visit = candidate.visitset.get(visit_data.visitlabel) #TODO debug when current is last visit + #gather info about current_visit (mostly for my own biological computer! else I get lost) + current_visit_date = current_visit.when + current_visit_year = current_visit_date.year #get the year of the current visit date + next_visit_window = next_visit.visitwindow + next_visit_margin = next_visit.visitmargin + #set dates for the next visit + next_visit_early = int(current_visit_date.strftime('%j')) + (next_visit_window - next_visit_margin) #this properly handle change of year + next_visit_early_date = datetime.datetime(current_visit_year, 1, 1) + datetime.timedelta(next_visit_early - 1) + next_visit_late = int(current_visit_date.strftime('%j')) + (next_visit_window + next_visit_margin) + next_visit_late_date = datetime.datetime(current_visit_year, 1, 1) + datetime.timedelta(next_visit_late - 1) + next_visit.when_earliest = next_visit_early_date + next_visit.when_latest = next_visit_late_date + next_visit.status = "tentative" + Utilities.print_object((next_visit)) + """ + def set_next_visit_window(self, candidate, current_visit): + """ + This method will 'calculate' a min and max date when the next visit should occur + 1-Get candidate.visitset (as visit_searchset) and current_visit.visitlabel + 2-Identify which visit (in visitset) has previousvisit == current_visit.visitlabel + 3-Get + Usage: Currently called by GUI function (TODO RETHINK THIS LOGIC maybe it should be called when running Candidate.set_visit_date()) + """ + + + #get the current visit object as argument. Will search_event and look for the next visit (visit where previousvisit == current_visitlabel) + + #1- Get Candidate.visitset and current_visit / next_visit will == Visit(VisitSetup) of the next visit (relative to current_visit) + visit_searchset = candidate.visitset + current_visitlabel = current_visit.visitlabel + next_visit = "" + #2-Identify which visit (in visitset) has previousvisit == current_visit.visitlabel + for key in visit_searchset: + visit_data = visit_searchset[key] + if visit_data.previousvisit == current_visitlabel: + next_visit = candidate.visitset.get(visit_data.visitlabel) #TODO debug when current is last visit + #3-Calculate a min and max date for the next visit to occur based on Visit.visitwindow and Visit.visitmargin + current_visitdate = current_visit.when + current_visityear = current_visitdate.year #get the year of the current visit date + next_visitwindow = next_visit.visitwindow + nextvisitmargin = next_visit.visitmargin + #set dates for the next visit + nextvisitearly = int(current_visitdate.strftime('%j')) + (next_visitwindow - nextvisitmargin) #this properly handle change of year + nextvisitearlydate = datetime.datetime(current_visityear, 1, 1) + datetime.timedelta(nextvisitearly - 1) + nextvisitlate = int(current_visitdate.strftime('%j')) + (next_visitwindow + nextvisitmargin) + nextvisitlatedate = datetime.datetime(current_visityear, 1, 1) + datetime.timedelta(nextvisitlate - 1) + next_visit.whenearliest = nextvisitearlydate + next_visit.whenlatest = nextvisitlatedate + next_visit.status = "tentative" #set this visit.status + + def get_active_visit(self, candidate): + candidatefullname = str(candidate.firstname + ' ' + candidate.lastname) + currentvisitset = candidate.visitset + activevisit = [] + if currentvisitset is None: + return + elif currentvisitset is not None: + for key in currentvisitset: + if currentvisitset[key].status == MultiLanguage.status_active: + visitlabel = currentvisitset[key].visitlabel + when = currentvisitset[key].when.strftime('%Y-%m-%d %Hh%m') + where = currentvisitset[key].where + who = currentvisitset[key].withwhom + activevisit = [candidatefullname, visitlabel, when, where, who] + return activevisit + + def set_candidate_status_active(self): + self.status = MultiLanguage.status_active #set the Candidate.status to 'active' \ No newline at end of file diff --git a/dicat/lib/config.py b/dicat/lib/config.py new file mode 100644 index 0000000..9616884 --- /dev/null +++ b/dicat/lib/config.py @@ -0,0 +1,7 @@ +""" +This file will contain global variables that can be used in all classes, +assuming the config.py script gets imported. +Access to the variable would then be config.variablename +""" + +xmlfile = "" # to be access by config.xmlfile \ No newline at end of file diff --git a/dicat/lib/datamanagement.py b/dicat/lib/datamanagement.py new file mode 100644 index 0000000..e96b7fd --- /dev/null +++ b/dicat/lib/datamanagement.py @@ -0,0 +1,386 @@ +# Imports from standard packages +import os.path, re +from xml.dom import minidom + +# Imports from DICAT +import config as Config + +""" +This file contains functions related to data management only. + +Generic functions: + - read_data(xmlfile) + - read_xmlfile(xmlfile) + - remove_enpty_lines_from_file(file) + +Specific functions: + - read_candidate_data() + - read_visitset_data() + - read_visit_data(xmlvisitlist, cand, data) + - save_candidate_data(cand_data) + - read_study_data() + - save_study_data() +""" + +def read_xmlfile(xmlfile): + """ + Parses the XML file and return the data into xmldoc. + + :param: xmlfile + + :return: xmldoc + :rtype: object + + """ + + try: + xmldoc = minidom.parse(xmlfile) + return xmldoc + + except: + message = "ERROR: could not read file " + xmlfile + print message #TODO: create a log class to display the messages + + +def read_data(xmlfile): + """ + + :param xmlfile: XML file to read to grep the data + :type xmlfile: str + + :return xmldata: everthing that is available under the data tag in the XML + :rtype xmldata: object + :return xmlcandlist: list of candidates + :rtype xmlcandlist: list + + """ + + global xmldoc + + xmldoc = read_xmlfile(xmlfile) + xmldata = xmldoc.getElementsByTagName('data')[0] + xmlcandlist = xmldata.getElementsByTagName('Candidate') + + return xmldata, xmlcandlist + + +def read_candidate_data(): + """ + Read and return the candidate level content of an XML file specified in the + global variable Config.xmlfile. Returns an empty dictionary nothing if file + doesn't exist. + + :param: None + + :return: data + :rtype: dict + + """ + + data = {} + # check to see if file exists before loading it + if os.path.isfile(Config.xmlfile): + # read the xml file + (xmldata, xmlcandlist) = read_data(Config.xmlfile) + for cand in xmlcandlist: + data[cand] = {} + for elem in cand.childNodes: + tag = elem.localName + if not tag or tag == "Visit": + continue + val = cand.getElementsByTagName(tag)[0].firstChild.nodeValue + data[cand][tag] = val + else: + data = "" + + return data + + +def grep_list_of_candidate_ids(): + """ + Read the XML file and grep all candidate IDs into an array candIDs_array. + + :return: candIDs_array, or False if could not find the XML file + + """ + + candIDs_array = [] + + if os.path.isfile(Config.xmlfile): + # read the xml file + (xmldata, xmlcandlist) = read_data(Config.xmlfile) + + for cand in xmlcandlist: + for elem in cand.childNodes: + tag = elem.localName + if tag == 'Identifier': + cand_elem = cand.getElementsByTagName(tag)[0] + candID = cand_elem.firstChild.nodeValue + candIDs_array.append(candID) + else: + return False + + return candIDs_array + +def save_candidate_data(cand_data): + """ + Save the updated candidate information into the xml file (defined by the + global variable Config.xmlfile). + + :param cand_data: data dictionary with the updated information + :type cand_data: dict + + :return: None + + """ + + # Check to see if xmldoc global variable and file exist before saving + if os.path.isfile(Config.xmlfile) and xmldoc: + # Read the xml file + (xmldata, xmlcandlist) = read_data(Config.xmlfile) + updated = False + + # Loop through all the candidates that exist in the XML file + for cand in xmlcandlist: + for elem in cand.childNodes: + tag = elem.localName + if not tag: + continue + val = cand.getElementsByTagName(tag)[0].firstChild.nodeValue + + # If the candidate was found in the XML file, updates its info + if tag == "Identifier" and val == cand_data['Identifier']: + + # Grep the XML elements + xml_firstname = cand.getElementsByTagName("FirstName")[0] + xml_lastname = cand.getElementsByTagName("LastName")[0] + xml_gender = cand.getElementsByTagName("Gender")[0] + xml_dob = cand.getElementsByTagName("DateOfBirth")[0] + xml_phone = cand.getElementsByTagName("PhoneNumber")[0] + xml_status = cand.getElementsByTagName("CandidateStatus")[0] + + # Replace elements' value with what has been captured in + # the cand_data dictionary + xml_firstname.firstChild.nodeValue = cand_data['FirstName'] + xml_lastname.firstChild.nodeValue = cand_data['LastName'] + xml_dob.firstChild.nodeValue = cand_data['DateOfBirth'] + if 'Gender' in cand_data: + xml_gender.firstChild.nodeValue = cand_data['Gender'] + if 'CandidateStatus' in cand_data: + key = 'CandidateStatus' + xml_status.firstChild.nodeValue = cand_data[key] + if 'PhoneNumber' in cand_data: + key = 'PhoneNumber' + xml_phone.firstChild.nodeValue = cand_data[key] + + updated = True + break + + # If no candidate was updated, insert a new candidate + if not updated: + # Create a new Candidate element + cand = xmldoc.createElement("Candidate") + xmldata.appendChild(cand) + + # Loop through cand_data keys ('Identifier', 'FirstName' ...) + # and add them to the XML handler (xmldoc) + for key in cand_data: + # create the child tag ('Gender', 'DOB' etc...) and its value + xml_elem = xmldoc.createElement(key) + value = xmldoc.createTextNode(cand_data[key]) + # append the child tag and value to the 'Candidate' tag + cand.appendChild(xml_elem) + xml_elem.appendChild(value) + + # Loop through optional fields and add them to the XML handler + # with an empty string if the field was not present in cand_data + optional_fields = ['Gender', 'CandidateStatus', 'PhoneNumber'] + for key in optional_fields: + if key not in cand_data: + # create the new tag and its value + xml_elem = xmldoc.createElement(key) + value = xmldoc.createTextNode(" ") + # append the child tag and value to the 'Candidate' tag + cand.appendChild(xml_elem) + xml_elem.appendChild(value) + + # Update the xml file with the correct values + f = open(Config.xmlfile, "w") + xmldoc.writexml(f, addindent=" ", newl="\n") + f.close() + # Remove the empty lines inserted by writexml (weird bug from writexml) + remove_empty_lines_from_file(Config.xmlfile) + + +def read_visitset_data(): + """ + Read and return the visit set content of an XML file specified in the + global variable Config.xmlfile. Returns an empty dictionary nothing if + file doesn't exist. + + :param: None + + :return: data + :rtype: dict + + """ + + data = {} # Initialize data dictionary + + # Check to see if file exists before loading it + if os.path.isfile(Config.xmlfile): + # Read the xml file + (xmldata, xmlcandlist) = read_data(Config.xmlfile) + + # Loop through all candidates present in the XML file + for cand in xmlcandlist: + data[cand] = {} + for cand_elem in cand.childNodes: + cand_tag = cand_elem.localName + tags_to_ignore = ( + "Gender", "DateOfBirth", "PhoneNumber", "CandidateStatus" + ) + + # Continue if met a non-wanted tag + if not cand_tag or cand_tag in tags_to_ignore: + continue + + # If the tag is 'Visit', grep all visit information. + # This will be stored in data[cand]['VisitSet'] dictionary + if cand_tag == "Visit": + xmlvisitlist = cand.getElementsByTagName('Visit') + read_visit_data(xmlvisitlist, cand, data) + + # This will store candidate information (such as 'Identifier', + # 'Gender', 'Firstname' ...) into data[cand][cand_tag]. + elem = cand.getElementsByTagName(cand_tag)[0] + val = elem.firstChild.nodeValue + data[cand][cand_tag] = val + + else: + data = "" + + return data + + +def read_visit_data(xmlvisitlist, cand, data): + """ + Read the visit data related to a specific candidate and update the data + dictionary. + + :param xmlvisitlist: list of visits to loop through + :type xmlvisitlist: list + :param cand: candidate key + :type cand: object + :param data: data dictionary containing all candidate and visit's data + :type data: dict + + :return: None + + """ + + data[cand]["VisitSet"] = {} # Initialize a VisitSet dictionary + + # Loop through all visits present in the XML file for a given candidate + for visit in xmlvisitlist: + data[cand]["VisitSet"][visit] = {} # Initialize a Visit dictionary + + # Loop through all visit elements present under a given Visit tag + for visit_elem in visit.childNodes: + visit_tag = visit_elem.localName + + if not visit_tag: # continue if no tag + continue + + # Insert the visit tag and its value into the visit dictionary + elem = visit.getElementsByTagName(visit_tag)[0] + val = elem.firstChild.nodeValue + data[cand]["VisitSet"][visit][visit_tag] = val + + +def read_study_data(): #TODO: implement this function + """ + This function reads and returns the content of the XML 'projectInfo' tag. + It will return nothing if it could not find the information. + + :return: + + """ + #check to see if file exists before loading it + pass + + +def save_study_data(study_data): #TODO: implement this function + """ + Save study/project information into the XML file under the tag 'projectInfo' + + :param study_data: dictionary containing all the study/project information + :type study_data: dict + + :return: + """ + pass + + +def remove_empty_lines_from_file(file): + """ + This function allows to remove empty lines that are inserted by writexml + function from minidom. + + :param file: file that need empty lines to be removed + :type file: str + """ + + # grep all lines that are not empty into lines + with open(file) as f: + lines = [line for line in f if line.strip() is not ""] + # write lines into the file + with open(file, "w") as f: + f.writelines(lines) + + +def sort_candidate_visit_list(visitset): + """ + Sort a candidate's visit set and return it into sorted visit_list. + + :param visitset: visit set for a given candidate + :type visitset: dict + + :return visit_list: list of sorted visits for that candidate + :rtype visit_list: list + + """ + + # 1- Grep candidate's visitset and parse into a list + visit_list = [] + for key, value in visitset.iteritems(): + visit_list.append(visitset[key]) + + # 2- Sort list on visit.rank + visit_list = sorted( + visit_list, key=lambda visit: visit["VisitStartWhen"] + ) + + return visit_list + + +def dict_match(pattern, data_dict): + """ + Function that will return True if the pattern was matching one value of the + data dictionary (data_dict). False otherwise. + + :param pattern: pattern to be used in the regular expression + :type pattern: str + :param data_dict: data dictionary to look for matches + :type data_dict: dict + + :return: True if found a match, False otherwise + :rtype: bool + + """ + + for key in data_dict: + if re.search(pattern, data_dict[key]): + return True + + return False \ No newline at end of file diff --git a/dicat/lib/dicom_anonymizer_methods.py b/dicat/lib/dicom_anonymizer_methods.py index 7dd64d6..ff2d2fe 100755 --- a/dicat/lib/dicom_anonymizer_methods.py +++ b/dicat/lib/dicom_anonymizer_methods.py @@ -1,3 +1,4 @@ +# Imports from standard packages import os import sys import xml.etree.ElementTree as ET @@ -13,17 +14,16 @@ - newer versions of the PyDICOM module are imported using "import dicom" - returns true if the PyDICOM was imported, false otherwise """ -# Create a boolean variable that returns True if PyDICOM was imported, False -# otherwise +# Create a boolean variable (use_pydicom) that returns: +# - True if PyDICOM was succesfully imported +# - False otherwise use_pydicom = False try: import pydicom as dicom - use_pydicom = True # set to true as PyDICOM was found and imported except ImportError: try: # try importing newer versions of PyDICOM import dicom - use_pydicom = True # set to true as PyDICOM was found and imported except ImportError: use_pydicom = False # set to false as PyDICOM was not found @@ -118,6 +118,7 @@ def grep_dicom_fields(xml_file): :return: dicom_fields -> dictionary of DICOM fields :rtype: dict + """ xmldoc = ET.parse(xml_file) @@ -147,7 +148,7 @@ def grep_dicom_values(dicom_folder, dicom_fields): """ # Grep first DICOM of the directory - # TODO: Need to check if file is DICOM though, otherwise go to next one + # TODO: Need to check if file is DICOM though, otherwise go to next file (dicoms_list, subdirs_list) = grep_dicoms_from_folder(dicom_folder) dicom_file = dicoms_list[0] @@ -269,7 +270,7 @@ def dicom_zapping(dicom_folder, dicom_fields): move(orig_bak_dcm, original_dcm) # Zip the de-identified and original DICOM folders - (deidentified_zip, original_zip) = zip_dicom_directories(deidentified_dir, + (deidentified_zip, original_zip) = zip_dcm_directories(deidentified_dir, original_dir, subdirs_list, dicom_folder @@ -325,7 +326,7 @@ def dcmodify_zapping(dicom_file, dicom_fields): :returns: original_zip -> Path to the zip file containing original DICOM files - deidentified_zip -> Path to the zip file containing de-identified DICOM files + deidentified_zip -> Path to the zip containing the de-identified DICOMs :rtype: str """ @@ -351,7 +352,7 @@ def dcmodify_zapping(dicom_file, dicom_fields): subprocess.call(modify_cmd, shell=True) -def zip_dicom_directories(deidentified_dir, original_dir, subdirs_list, root_dir): +def zip_dcm_directories(deidentified_dir, original_dir, subdirs_list, root_dir): """ Zip the de-identified and origin DICOM directories. @@ -374,18 +375,18 @@ def zip_dicom_directories(deidentified_dir, original_dir, subdirs_list, root_dir # If de-identified and original folders exist, zip them if os.path.exists(deidentified_dir) and os.path.exists(original_dir): - original_zip = zip_dicom(original_dir) + original_zip = zip_dicom(original_dir) deidentified_zip = zip_dicom(deidentified_dir) else: sys.exit('Failed to find original and de-identify data folders') - # If archive de-identified and original DICOMs found, remove subdirectories in - # root directory + # If archive de-identified and original DICOMs found, remove subdirectories + # in root directory if os.path.exists(deidentified_zip) and os.path.exists(original_zip): for subdir in subdirs_list: shutil.rmtree(root_dir + os.path.sep + subdir) else: - sys.exit('Failed: could not zip de-identified and original data folders') + sys.exit('Failed: could not zip de-identified & original data folders') # Return zip files return deidentified_zip, original_zip @@ -413,16 +414,15 @@ def create_directories(dicom_folder, dicom_fields, subdirs_list): # Create an original_dcm and deidentified_dcm directory in the DICOM folder, # as well as subdirectories - original_dir = dicom_folder + os.path.sep + dicom_fields['0010,0010'][ - 'Value'] - deidentified_dir = dicom_folder + os.path.sep + dicom_fields['0010,0010'][ - 'Value'] + "_deidentified" - os.mkdir(original_dir, 0755) + name = dicom_fields['0010,0010']['Value'] + original_dir = dicom_folder + os.path.sep + name + deidentified_dir = dicom_folder + os.path.sep + name + "_deidentified" + os.mkdir(original_dir, 0755) os.mkdir(deidentified_dir, 0755) # Create subdirectories in original and de-identified directory, as found in # DICOM folder for subdir in subdirs_list: - os.mkdir(original_dir + os.path.sep + subdir, 0755) + os.mkdir(original_dir + os.path.sep + subdir, 0755) os.mkdir(deidentified_dir + os.path.sep + subdir, 0755) return original_dir, deidentified_dir diff --git a/dicat/lib/multilanguage.py b/dicat/lib/multilanguage.py new file mode 100644 index 0000000..1fd686a --- /dev/null +++ b/dicat/lib/multilanguage.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +#read language preference from appdata file +#from utilities import readappdata #TODO replace and remove +#language = readappdata()[0] + +language = "en" #TODO make dynamic + +if language == "fr": + ###################### TOP LEVEL MENU BAR ###################### + app_title = u"Outils LORIS" + #APPLICATION menu + application_menu = u"Application" + application_setting = u"Préferences" + application_quit = u"Quitter" + #PROJECT menu + #menuproject = u"Projet" #TODO remove? + #openproject = u"Ouvrir un projet" + #modifyproject = u"Modifier le projet ouvert" + #newproject = u"Créer un nouveau projet" + #CANDIDATE menu + candidate_menu = u"Candidat" + candidate_add = u"Nouveau candidat" + candidate_search = u"Trouver candidat" + candidate_update = u"Mettre à jour" + candidate_get_id = u"Obtenir l'identifiant d'un candidat" + candidate_exclude_include_toggle = u"Inclure/Exclure un candidat" + #clear_all_field = u"Effacer" + #ANONYMIZER menu + anonymizer_menu = u"DICOM" + anonymizer_run = u"Anonymizer" + #CALENDAR menu + calendar_menu = u"Calendrier" + calendar_new_appointment = u"Nouveau Rendez-vous" + #HELP menu + help_menu = u"Aide" + help_get_help = u"Obtenir de l'aide" + help_about_window = u"A propos de ..." + ###################### PROJECT INFO PANE ####################### + project_info_pane = u"Projet" + project_detail_pane = u"Détails du Projet" + visit_detail_pane = u"Détails des Visites" + target_recruitment = u"Cible de recrutement" + current_recruitment = u"Recrutement actuel" + project_name = u"Projet" + project_start = u"Début" + project_end = u"Fin" + total_visit = u"Nombre de Visites" + #################### MULTI-TAB DATA SECTION ##################### + calendar_pane = u"Calendrier" + candidate_pane = u"Candidats" + + datatable_id = u"ID" + datatable_dob = u"Date de Naissance" + datatable_phone = u"Téléphone" + datatable_city = u"Ville" + datatable_firstname = u"Prénom" + datatable_lastname = u"Nom de famille" + datatable_address = u"Adresse" + datatable_province = u"Province" + datatable_country = u"Pays" + datatable_postal_code = u"Code Postal" + label_candidate_table = u"Faites un double-clic sur l'une des lignes " \ + u"pour remplir les champs ci-dessus" + + calendar_monday = u"Lundi" + calendar_tuesday = u"Mardi" + calendar_wednesday = u"Mercredi" + calendar_thursday = u"Jeudi" + calendar_friday = u"Vendredi" + calendar_saturday = u"Samedi" + calendar_sunday = u"Dimanche" + calendar_january = u"Janvier" + calendar_february = u"Février" + calendar_march = u"Mars" + calendar_april = u"Avril" + calendar_may = u"Mai" + calendar_june = u"Juin" + calendar_jully = u"Juillet" + calendar_august = u"Août" + calendar_september = u"Septembre" + calendar_october = u"Octobre" + calendar_november = u"Novembre" + calendar_december = u"Décembre" + + ################ COLUMN HEADER ################## + col_candidate = u"Candidat" + col_visitlabel = u"Visite" + col_withwhom = u"Avec qui" + col_when = u"Date/Heure" + col_where = u"Endroit" + col_status = u"Statut" + + #################### STATUS ##################### + status_active = u"actif" + status_tentative = u"provisoire" + ################# DATA WINDOWS ################## + data_window_title = u"DATA WINDOW" #TODO trouver un titre français + ################## DIALOGBOX #################### + # very not sure what to do about that section + dialog_yes = u"Oui" + dialog_no = u"Non" + dialog_ok = u"OK" + dialog_close = u"Vous êtes sur le point de fermer cette fenêtre sans " \ + u"sauvegarder!\n\nVoulez-vous continuer?" + dialog_title_confirm = u"Veuillez confirmer!" + dialog_title_error = u"Erreur" + dialog_bad_dob_format = u"La date de naissance doit être formatté en " \ + u"AAAA-MM-JJ!" + dialog_no_data_entered = u"Au moins un des champs doit être entré pour " \ + u"chercher un candidat." + warning_filters_set = u"ATTENTION: des filtres sont en fonction. " \ + u"Seuls les candidats correspondant aux filtres " \ + u"sont montrés" + dialog_candID_already_exists = u"L'identifiant existe déjà!" + dialog_missing_cand_info_schedul = u"Les champs 'Identifiant', 'Prénom', " \ + u"'Nom de famille', 'Sexe' et " \ + u"'Date de naissance' sont requis!" + dialog_missing_cand_info_IDmapper = u"Les champs 'Identifiant', " \ + u"'Prénom', 'Nom de famille' " \ + u"et 'Date de naissance' sont requis!" + ################ DATA WINDOW ################### + schedule_pane = u"Calendrier" + candidate_pane = u"Candidat" + candidate_dob = u"Date de naissance (AAAA-MM-JJ)" + candidate_phone = u"Téléphone" + candidate_pscid = u"ID" + candidate_status = u"Status" + candidate_gender = u"Sexe" + candidate_firstname = u"Prénom" + candidate_lastname = u"Nom de famille" + + schedule_visit_label = u"Visite" + schedule_visit_rank = u"#" + schedule_visit_status = u"Status" + schedule_visit_when = u"Date" + schedule_optional = u"Optionnel" + schedule_no_visit_yet = u"Aucune visite de programmé pour ce candidat" + +elif language == "en": + app_title = u"LORIS tools" + #APPLICATION menu + application_menu = u"Application" + application_setting = u"Preferences" + application_quit = u"Quit" + #PROJECT menu + #menuproject = u"Project" #TODO remove? + #openproject = u"Open project" + #modifyproject = u"Modify open project" + #newproject = u"New project" + #CANDIDATE menu + candidate_menu = u"Candidate" + candidate_add = u"New candidate" + candidate_search = u"Search:" + candidate_update = u"Update" + candidate_get_id = u"Get a canditate ID" + candidate_exclude_include_toggle = u"Include/Exclude a candidate" + #clear_all_field = u"Clear" + #CALENDAR menu + calendar_menu = u"Calendar" + calendar_new_appointment = u"New appointment" + #ANONYMIZER menu + anonymizer_menu = u"DICOM" + anonymizer_run = u"Anonymizer" + #HELP menu + help_menu = u"Help" + help_get_help = u"Get some help" + help_about_window = u"About this..." + ###################### PROJECT INFO PANE ####################### + project_info_pane = u"Project Information" + project_detail_pane = u"Project Details" + visit_detail_pane = u"Visit Details" + target_recruitment = u"Recruitment target" + current_recruitment = u"Current recruitment" + project_name = u"Project" + project_start = u"Start" + project_end = u"End" + total_visit = u"Total number of Visits" + #################### MULTI-TAB DATA SECTION ##################### + calendar_pane = u"Calendar" + candidate_pane = u"Candidates" + datatable_id = u"ID" + datatable_dob = u"Date of Birth" + datatable_phone = u"Phone" + datatable_city = u"City" + datatable_address = u"Address" + datatable_province = u"Province" + datatable_country = u"Country" + datatable_firstname = u"First Name" + datatable_lastname = u"Last Name" + label_candidate_table = u"Double click on row to populate fields above" + datatable_postal_code = u"Postal Code" + + calendar_monday = u"Monday" + calendar_tuesday = u"Tuesday" + calendar_wednesday = u"Wednesday" + calendar_thursday = u"Thursday" + calendar_friday = u"Friday" + calendar_saturday = u"Saturday" + calendar_sunday = u"Sunday" + calendar_january = u"January" + calendar_february = u"February" + calendar_march = u"Marc" + calendar_april = u"April" + calendar_may = u"May" + calendar_june = u"June" + calendar_jully = u"Jully" + calendar_august = u"August" + calendar_september = u"September" + calendar_october = u"October" + calendar_november = u"November" + calendar_december = u"December" + + ################ COLUMN HEADER ################## + col_candidate = u"Candidate" + col_visitlabel = u"Visit" + col_withwhom = u"Whom" + col_when = u"Date/Time" + col_where = u"Place" + col_status = u"Status" + + #################### STATUS ##################### + status_active = u"active" + status_tentative = u"tentative" + + ################# DATA WINDOWS ################## + data_window_title = u"Data Window" + + ################## DIALOGBOX #################### + # very not sure what to do about that section + dialog_yes = u"Yes" + dialog_no = u"No" + dialog_ok = u"OK" + dialog_close = u"You are about to close this window without saving!\n\n" \ + u"Do you want to continue?" + dialog_title_confirm = u"Please confirm!" + dialog_title_error = u"Error" + dialog_bad_dob_format = u"Date of Birth should be in YYYY-MM-DD format!" + dialog_no_data_entered = u"At least one of the fields needs to be entered " \ + u"to search_event a candidate." + warning_filters_set = u"WARNING: filters are set. Only matching " \ + u"candidates are shown." + dialog_candID_already_exists = u"Identifier already exists!" + dialog_missing_cand_info_schedul = u"'Identifier', 'Firstname', " \ + u"'Lastname', 'Date of Birth' and " \ + u"'Gender' fields are required!" + dialog_missing_cand_info_IDmapper = u"'Identifier', 'Firstname', " \ + u"'Lastname' and 'Date of Birth' " \ + u"fields are required!" + + ################ DATA WINDOW ################### + schedule_pane = u"Calendar" + candidate_pane = u"Candidate" + candidate_dob = u"Date of Birth (YYYY-MM-DD)" + candidate_phone = u"Phone" + candidate_pscid = u"ID" + candidate_status = u"Status" + candidate_gender = u"Sex" + candidate_firstname = u"Firstname" + candidate_lastname = u"Lastname" + schedule_visit_label = u"Visit" + schedule_visit_rank = u"#" + schedule_visit_status = u"Status" + schedule_visit_when = u"Date" + schedule_optional = u"Optional" + schedule_no_visit_yet = u"No visit scheduled for that candidate yet" \ No newline at end of file diff --git a/dicat/lib/resource_path_methods.py b/dicat/lib/resource_path_methods.py index c7aee92..d093512 100644 --- a/dicat/lib/resource_path_methods.py +++ b/dicat/lib/resource_path_methods.py @@ -1,15 +1,39 @@ #!/usr/bin/python +# Import from standard packages import os import sys class resource_path(): + """ + This class allows to get the absolute path to the resource scripts. It works + for development installation as well as for PyInstaller builds (.app, .exe). + + It has been created for Pyinstaller. Linked images or external files will be + loaded using these methods, otherwise the created application (.app, .exe) + would not find them. + + """ def __init__(self, relative_path): + """ + Initialize resource_path class. + + :param relative_path: relative path to the file from the dicat root dir + :type relative_path: str + + """ + self.relative_path = relative_path + def return_path(self): - """ Get absolute path to resource, works for dev and for PyInstaller """ + """ + Get absolute path to resource, works for dev and for PyInstaller + + :return: relative path to be used to find all DICAT scripts + :rtype: str + """ if hasattr(sys, '_MEIPASS'): return os.path.join(sys._MEIPASS, self.relative_path) diff --git a/dicat/lib/utilities.py b/dicat/lib/utilities.py new file mode 100644 index 0000000..c2f5848 --- /dev/null +++ b/dicat/lib/utilities.py @@ -0,0 +1,135 @@ +#imports from standard packages +from uuid import uuid1 +import time, datetime + +""" +This file contains utility functions used throughout the application +""" + +def is_unique(data, dataset): + """ + will verify if 'data' passed as argument is unique in a dataset + """ + seen = set() + return not any(value in seen or seen.add(value) for value in data) + + +def describe(something): + """ + this will return the object(something) class name and type as well as all attributes excluding those starting with a double underscore + """ + #will return the object type and class (Type:Class) as text + objectclass = str(something.__class__.__name__) + objecttype = str(type(something)) + #will return a list of all non"__" attributes, including use defined ones (**kwargs) + attributes = str(filter(lambda a: not a.startswith('__'), dir(something))) + returnvalue = objectclass, " (", objecttype, "): ", attributes + print returnvalue + + +def get_current_date(): + """ + will return today's date as a string of format yyyymmdd + """ + return time.strftime("%Y%m%d") + + +def get_current_time(option): + """ + will return current time as a string of format hhmmss + """ + if option == 1: + return time.strftime("%H%M%S") + elif option == 2: + return time.strftime("%H:%M:%S") + else: + pass + + +def error_log(message): + # append/save timestamp and exception to errorlog.txt + # object Exception e is sent as is and this method is taking care of parsing it to string + # and adding a timestamp + console = 1 #TODO use this value to send error message to the console instead of the file + if console == 0: + timestamp = time.strftime("%c") # date and time representation using this format: 11/07/14 12:50:35 + fullmessage = timestamp + ": " + str(message) + "\n" + anyfile = open("errorlog.txt", "a") #this is required since a file object is later expected + anyfile.write(fullmessage) + anyfile.close() + elif console == 1: + print message + "\n" #TODO remove when done + + +def center_window(win): + win.update_idletasks() + width = win.winfo_width() + height = win.winfo_height() + x = (win.winfo_screenwidth() // 2) - (width // 2) + y = (win.winfo_screenheight() // 2) - (height // 2)-200 + win.geometry('{}x{}+{}+{}'.format(width, height, x, y)) + + +######################################################################################## +def gather_attributes(something): + """ + Receive an object and return an array containing the object attributes and value + """ + attributes =[] + for key in sorted(something.__dict__): + attributes.append('%s' %(key)) + return attributes + + +def search_uid(db, value): + return filter(lambda candidate: candidate['uid'] == value, db) + + +def print_object(something): + #for dev only! + #will print key, attributes, value in the console. + #most likely will be an dict or a class instance. + #so this is for the dict + if type(something) is dict: + for key, value in something.iteritems(): + c = something.get(key) + for attr, value in c.__dict__.iteritems(): + print attr,": ", value + print "\n" + elif type(something) is list: + for item in something: + print item + else: #and this for the class instance + for attr, value in something.__dict__.iteritems(): + print attr, value + print "\n\n" + + +def check_date_format(date): + """ + Function that checks that the date format is YYYY-MM-DD + + :param date: date to check + :type date: str + + :return: True if date is in YYYY-MM-DD format, False otherwise + :rtype: bool + """ + + try: + datetime.datetime.strptime(date,"%Y-%m-%d") + return True + + except ValueError: + return False + + + + + + +# self-test "module" TODO remove before release +if __name__ == '__main__': + import lib.datamanagement as DataManagement + data=dict(DataManagement.read_study_data()) + print_object(data) \ No newline at end of file diff --git a/dicat/new_data_test.xml b/dicat/new_data_test.xml new file mode 100644 index 0000000..917b744 --- /dev/null +++ b/dicat/new_data_test.xml @@ -0,0 +1,284 @@ + + + + DICAT test + This is a test project. + + 0 + 100 + 2000 + 2050 + + Project 1 + + Subproject 1 + + V0 + 1 + + 25-35 + + V1 + 2 + 55-65 + + V2 + 3 + 95-105 + + + V3 + 4 + + + + Subproject 2 + + V0 + 1 + + + + + + + + MTL0002 + + Sepia + Calamari + Female + 2015-06-14 + 444-555-6666 + active + + V0 + completed + + 2016-01-06 13:15:00 + 2016-01-06 14:15:00 + Douglas + Annie + + + V1 + completed + 2016-04-05 13:15:00 + 2016-04-05 14:15:00 + Douglas + Jennifer + + + V2 + scheduled + 2016-06-05 13:15:00 + 2016-06-05 14:15:00 + Douglas + Jennifer + + + + MTL0003 + Bibi + LaPraline + Female + 2010-08-12 + 450-345-6789 + excluded + + V0 + completed + + 2016-01-06 13:15:00 + 2016-01-06 14:15:00 + Douglas + Annie + + + + MTL0001 + Blues + Singer + Male + 2015-06-14 + 444-555-6666 + active + + V0 + completed + + 2016-01-06 13:15:00 + 2016-01-06 14:15:00 + Douglas + Annie + + + V1 + completed + 2016-04-05 13:15:00 + 2016-04-05 14:15:00 + Douglas + Jennifer + + + V2 + scheduled + 2016-06-05 13:15:00 + 2016-06-05 14:15:00 + Douglas + Jennifer + + + + MTL0006 + Ali + Gator + Male + 2014-02-05 + 514-758-8903 + active + + V0 + completed + + 2016-01-06 13:15:00 + 2016-01-06 14:15:00 + Douglas + Annie + + + V1 + scheduled + 2016-04-05 13:15:00 + 2016-04-05 14:15:00 + Douglas + Jennifer + + + + MTL0004 + Pikachu + Pokemon + Male + 1996-01-01 + 543-453-5432 + withdrawn + + V0 + completed + + 2016-01-06 13:15:00 + 2016-01-06 14:15:00 + Douglas + Annie + + + + MTL0005 + Bilou + Doudou + Female + 2014-02-03 + 450-758-9385 + withdrawn + + V0 + completed + + 2016-01-06 13:15:00 + 2016-01-06 14:15:00 + Douglas + Annie + + + + MTL9999 + Lego + Phantom + Male + 2012-04-29 + 432-654-9093 + active + + V0 + completed + + 2016-01-06 13:15:00 + 2016-01-06 14:15:00 + Douglas + Annie + + + V1 + scheduled + 2016-04-05 13:15:00 + 2016-04-05 14:15:00 + Douglas + Jennifer + + + + MTL0025 + Santa + Claus + Male + 2015-12-25 + + ineligible + + + + James + Bond + 1977-07-07 + + Male + MTL0007 + + + ineligible + Bugs + Bunny + 1938-04-30 + + Male + MTL0008 + + + Gonzales + MTL0009 + Speedy + 1953-08-29 + + + + + + + Lucky + Luke + 1946-12-07 + + Male + MTL0010 + + + + Hopie + Chipmunk + 2008-04-20 + + Female + MTL0011 + + + death + Chatran + Cat + 1992-04-05 + + Male + MTL0012 + + + diff --git a/dicat/scheduler_application.py b/dicat/scheduler_application.py new file mode 100644 index 0000000..a5ba39a --- /dev/null +++ b/dicat/scheduler_application.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python + +#import standard packages +from Tkinter import * +from ttk import * + +#import internal packages +import ui.datatable as DataTable +import ui.datawindow as DataWindow +import lib.multilanguage as MultiLanguage + + +class UserInterface(Frame): + + def __init__(self, parent): + """ + Initialize the UserInterface class. + + :param parent: parent widget in which to insert the UserInterface + :type parent: object + + """ + + Frame.__init__(self) + self.parent = parent + #self.parent.title(MultiLanguage.app_title) + self.initialize() + + + def initialize(self): + """ + Creates the paned window with the project information, candidate and + calendar panes. + + """ + + # Initialize frame + self.frame = Frame(self.parent) + self.frame.pack(side=TOP, expand=YES, fill=BOTH, padx=10, pady=10) + + #TODO implement button to be able to choose the XML file + #Config.xmlfile = "new_data_test.xml" + + ## Create the PanedWindow + + # This area (datapane) is one Panedwindow containing 3 Labelframes + # TODO add dynamic resize + self.data_pane = Panedwindow( + self.frame, width=1000, height=500, orient=HORIZONTAL + ) + self.data_pane.pack(side=RIGHT, expand=YES, fill=BOTH) + + ## Create the 3 LabelFrames that will be part of the PanedWindow + + # Project info pane + # TODO create class for project info pane + # TODO add dynamic resize + self.project_infopane = Labelframe( + self.data_pane, text=MultiLanguage.project_info_pane, width=250, + height=350, borderwidth=10 + ) + + # Candidate pane + # TODO add dynamic resize + self.candidate_pane = Labelframe( + self.data_pane, text=MultiLanguage.candidate_pane, width=100, + height=450, borderwidth=10 + ) + + # Visit pane + # TODO add dynamic resize + self.visit_pane = Labelframe( + self.data_pane, text=MultiLanguage.calendar_pane, width=100, + height=350, borderwidth=10 + ) + self.data_pane.add(self.project_infopane) + self.data_pane.add(self.candidate_pane) + self.data_pane.add(self.visit_pane) + + ## Plot the button actions in the candidate pane frame + + # Create a frame that will contain the buttons + self.buttonBox = Frame(self.candidate_pane) + self.buttonBox.pack(side=TOP, anchor=W) + + # Create a 'new candidate' button to be added to self.buttonBox + self.buttonNewCandidate = Button( # new candidate button widget + self.buttonBox, + width=15, + text=MultiLanguage.candidate_add, + command=self.add_candidate + ) + self.labelSearchCandidate = Label( # search candidate Label widget + self.buttonBox, + width=8, + text=MultiLanguage.candidate_search, + justify=RIGHT, + anchor=E + ) + self.textSearchCandValue = StringVar() + self.textSearchCandValue.trace('w', self.find_matching_candidates) + self.entrySearchCandidate = Entry( # search candidate entry widget + self.buttonBox, text=self.textSearchCandValue, width=20, + ) + # Draw the buttons + self.buttonNewCandidate.grid( + row=0, column=0, padx=(0,10), pady=(0,5), sticky=E+W + ) + self.labelSearchCandidate.grid( + row=0, column=1, padx=(5,0), pady=(0,5), sticky=E+W + ) + self.entrySearchCandidate.grid( + row=0, column=2, padx=(0,0), pady=(0,5), sticky=E+W + ) + + ## Create data tables (using Treeview) + + # Candidate datatable + candidate_column_headers = [ + 'identifier', 'firstname', 'lastname', 'date of birth', + 'gender', 'phone', 'status' + ] + self.cand_table = DataTable.ParticipantsList( + self.candidate_pane, candidate_column_headers + ) + self.cand_table.pack(side=BOTTOM, expand=YES, fill=BOTH) + + # Calendar datatable + visit_column_headers = [ + 'identifier', 'candidate', 'visitlabel', 'when', 'where', 'status' + ] + self.visit_table = DataTable.VisitList( + self.visit_pane, visit_column_headers + ) + self.visit_table.pack(side=BOTTOM, expand=YES, fill=BOTH) + + + def find_matching_candidates(self, *args): + """ + Updates data table with matching candidates. + + :param args: + :type args: list + + """ + pattern = self.textSearchCandValue.get() + self.cand_table.update_data(pattern) + + + def add_candidate(self): + """ + This function will allow functionality to add a new candidate using + the same data window as when editing a subject. + + """ + + # Open the datawindow with candidate=False as no existing ID associated + # yet for the new candidate + DataWindow.DataWindow(self, 'new') + + # Update the candidate datatable when save the new candidate + self.cand_table.update_data() + + + def load_xml(self): + """ + Update candidate and calendar/visit datatables with data extracted from + XML file stored in Config.xmlfile. + + """ + + # Load XML data into candidate datatable + self.cand_table.update_data() + + # Load XML data into visit datatable + self.visit_table.update_data() \ No newline at end of file diff --git a/dicat/scheduler_visit.py b/dicat/scheduler_visit.py new file mode 100644 index 0000000..c9644ce --- /dev/null +++ b/dicat/scheduler_visit.py @@ -0,0 +1,93 @@ +#import standard packages +#import internal packages +import datetime + +import lib.utilities as utilities + + +class VisitSetup(): + """ + The VisitSetup() class is used to define a study (or project) in terms of sequence of visits and also serves as a + base class for Visit(VisitSetup). A study (or project) can have as many visits as required. There is no class for + study as it is merely a simple dictionnary. + + Code example: This study contains 3 visits. Since V0 is the first visit, it doesn't have any values for + 'previousvisit', 'visitwindow' and ' visitmargin' + study = {} + visit = visit.VisitSetup(1, 'V0') #a uid (uuid1) is automatically generated + study[visit.uid] = visit #VisitSetup.uid is a unique ID used as key + visit = visit.VisitSetup(2, 'V1', 'V0', 10, 2) + study[visit.uid] = visit + visit = visit.VisitSetup(3, 'V2', 'V1', 20, 10) + study[visit.uid] = visit + + This is the parent class of Visit(VisitSetup), furthermore Visit(VisitSetup) objects will be 'instanciated' from + each instance of VisitSetup(object). + Both VisitSetup(object) class and instances are used to create individual instances of Visit(VisitSetup) + + Attributes: + uid: A unique identifier using python's uuid1 method. Used as key to store and retrieve objects from + files and/or dictionaries. + rank: The rank of the visit in the sequence (int). Useful to sort the visits in order of occurrence. + visitlabel: The label of the visit (string) such as V1, V2 or anything else the user may come up with + previousvisit: The visitlabel (string) of the visit occurring before this one. Used to plan this visit based on + the date of the previous visit. (default to None) + visitwindow: The number of days (int) between this visit and the previous visit. (default to None) + visitmargin: The margin (in number of days (int)) that is an allowed deviation (a +/- few days ). Basically, + this allow the 'calculation' of a date window when this visit should occur. (default to None) + mandatory: Indicate if this visit is mandatory. Default to Yes + actions: A list of action points (or simply reminders) specific to that visit (i.e.'reserve room 101'). + This is not implemented yet (default to None) + """ + + def __init__(self, rank, visitlabel, previousvisit = None, visitwindow = None, visitmargin = None, actions = None, uid=None): + self.uid = utilities.generate_uid() + self.rank = rank + self.visitlabel = visitlabel + self.previousvisit = previousvisit + self.visitwindow = visitwindow + self.visitmargin = visitmargin + self.actions = actions #not implemented yet! + + +class Visit(VisitSetup): + """ + The Visit(VisitSetup) class help define individual visit of the candidate using VisitSetup(object) instances as 'templates'. + Upon creation of the first meeting with a candidate, the Candidate(object) instance will get a full set of Visit(VisitSetup) instances. + This set of visits is contained in Candidate.visitset + Each time a visit is being setup, a 'time window' is calculated to define the earliest and latest date at which + the 'nextVisit' should occur. + + Attributes: (In addition of parent class attributes.) + when: Date at which this visit is occurring. (default to None) + when_earliest: Earliest date when this visit should occur. Set to value when previous visit is activated. (default to None) + when_latest: Latest date when this visit should occur. Set to value when previous visit is activated. (default to None) + where: Place where this meeting is taking place. (default to None) + whitwhom: Professional meeting the study candidate at the reception. (default to None) + status: Status of this visit. Set to active when 'when' is set (default to None) + """ + def __init__(self, rank, visitlabel, previousvisit, visitwindow, visitmargin, actions=None, uid=None, when = None, + whenearliest = None, whenlatest = None, where = None, withwhom = None, status = None): + VisitSetup.__init__(self, rank, visitlabel, previousvisit, visitwindow, visitmargin, actions, uid) + self.when = when + self.whenearliest = whenearliest + self.whenlatest = whenlatest + self.where = where + self.withwhom = withwhom + self.status = status + + def visit_date_range(self): + #need to handle the case where a visit has no dates + #this works but Exception handling seems to broad + try: + early_date = datetime.datetime.date(self.whenearliest) + late_date = datetime.datetime.date(self.whenlatest) + date_range = str(early_date), '<>', str(late_date) + except Exception as e: + #print e + date_range = "" + return date_range + + + + diff --git a/dicat/ui/__init__.py b/dicat/ui/__init__.py new file mode 100644 index 0000000..2da98fd --- /dev/null +++ b/dicat/ui/__init__.py @@ -0,0 +1,10 @@ +""" +From https://docs.python.org/2/tutorial/modules.html#packages documentation + +The __init__.py files are required to make Python treat the directories as +containing packages; this is done to prevent directories with a common name, +such as string, from unintentionally hiding valid modules that occur later on +the module search path. In the simplest case, __init__.py can just be an empty +file, but it can also execute initialization code for the package or set the +__all__ variable, described later. +""" \ No newline at end of file diff --git a/dicat/ui/datatable.py b/dicat/ui/datatable.py new file mode 100644 index 0000000..1fcef8b --- /dev/null +++ b/dicat/ui/datatable.py @@ -0,0 +1,370 @@ +# import standard packages +from Tkinter import * +from ttk import * + +# import internal packages +import ui.datawindow as DataWindow +import lib.datamanagement as DataManagement + +class DataTable(Frame): + """ + DataTable is a base class (which inherits from Frame) defining the + functionality of the Tkinter.ttk.treeview widget. + Children classes are ParticipantsList(DataTable) and VisitList(DataTable) + + """ + + + def __init__(self, parent, colheaders): + Frame.__init__(self) + self.parent = parent + self.init_datatable(parent, colheaders) + + + def init_datatable(self, parent, colheaders): + """ + Initialize datatable (which is a tk.treeview & add scroll bars) + + :param parent: frame in which the datatable should take place + :type parent: frame + :param colheaders: array with the list of column header strings + :type colheaders: list + + """ + + # Initialize the Treeview datatable + self.datatable = Treeview( + parent, selectmode='browse', columns=colheaders, show="headings" + ) + + # Add column headers to datatable + for col in colheaders: + self.datatable.heading( + col, + text=col.title(), + command=lambda c=col: self.treeview_sortby( + self.datatable, c, 0 + ) + ) + self.datatable.column( + col, width=100, stretch="Yes", anchor="center" + ) + + # Add vertical and horizontal scroll bars + self.verticalscroll = Scrollbar( + parent, orient="vertical", command=self.datatable.yview + ) + self.horizontalscroll = Scrollbar( + parent, orient="horizontal", command=self.datatable.xview + ) + self.datatable.configure( + yscrollcommand=self.verticalscroll.set, + xscroll=self.horizontalscroll.set + ) + self.verticalscroll.pack( side=RIGHT, expand=NO, fill=BOTH ) + self.horizontalscroll.pack( side=BOTTOM, expand=NO, fill=BOTH ) + + # Draw the datatable + self.datatable.pack( side=LEFT, expand=YES, fill=BOTH ) + + # Bind with events + self.datatable.bind('', self.ondoubleclick) + self.datatable.bind("<>", self.onrowclick ) + self.datatable.bind('', self.onrightclik ) + + + def load_data(self, pattern=False): + """ + Should be overriden in child's class + + """ + pass + + + def update_data(self, pattern=False): + """ + Delete everything in datatable and reload its content with the updated + data coming from the XML file. + + """ + + for i in self.datatable.get_children(): + self.datatable.delete(i) # delete all data from the datatable + self.load_data(pattern) # reload all data with updated values + + + def treeview_sortby(self, tree, column, descending): + """ + Sort treeview contents when a column is clicked on. + + :param tree: treview table + :type tree: + :param column: column to sort by + :type column: + :param descending: descending sorting + :type descending: + + """ + + # grab values to sort + data = \ + [(tree.set(child, column), child) for child in tree.get_children('')] + + # reorder data + data.sort(reverse=descending) + for index, item in enumerate(data): + tree.move(item[1], '', index) + + # switch the heading so that it will sort in the opposite direction + tree.heading( + column, + command=lambda column=column: self.treeview_sortby( + tree, column, int(not descending) + ) + ) + + + def ondoubleclick(self, event): + """ + Double clicking on a treeview line opens a 'data window'. + Treeview data will be reloaded once the 'data window' has been closed. + + :param event: + :type event: + + """ + + # Double click on a blank line of the Treeview datatable generates an + # IndexOutOfRange error which is taken care of by this try:except block + try: + itemID = self.datatable.selection()[0] + item = self.datatable.item(itemID)['tags'] + parent = self.parent + candidate_id = item[1] + DataWindow.DataWindow(parent, candidate_id) + self.update_data() + + except Exception as e: + # TODO: deal with exceptions + print "Datatable ondoubleclick ", str(e) + + + def onrightclik(self, event): #TODO: to implement this or not? + """ + Not used yet + + :param event: + :type event: + + """ + + print 'contextual menu for this item' + pass + + + def onrowclick(self, event): #TODO: to implement this or not? + """ + Not used yet + + :param event: + :type event: + + """ + + item_id = str(self.datatable.focus()) + item = self.datatable.item(item_id)['values'] + + + + +class ParticipantsList(DataTable): + """ + Class ParticipantsList(DataTable) takes care of the data table holding the + list of participants. That list contains all participants (even those that + have never been called). + + """ + + def __init__(self, parent, colheaders): # expected is dataset + """ + __init__ function of ParticipantsList class + + :param parent: frame in which to insert the candidate datatable + :type parent: frame + :param colheaders: array of column headers to use in the datatable + :type colheaders: list + + """ + + DataTable.__init__(self, parent, colheaders) + + self.colheaders = colheaders # description_frame_gui the column headers + self.load_data() # load the data in the datatable + + # TODO add color settings in a 'settings & preferences' section + # TODO replace 'active' tag by status variable value + self.datatable.tag_configure('active', background='#F1F8FF') + + + def load_data(self, pattern=False): + """ + Load candidates information into the candidate datatable. If pattern is + set, will only load candidates that have info that matches the pattern. + + :param pattern: pattern to use to find matching candidates + :type pattern: str + + """ + + # Read candidates information into a cand_data dictionary + cand_data = DataManagement.read_candidate_data() + + try: + # Loop through all candidates + matching_cand = {} + for key in cand_data: + + # If pattern, check if found its match in cand_data[key] dict + # DataManagement.dict_match function will return: + # - True if found a match, + # - False otherwise + if pattern and \ + not DataManagement.dict_match(pattern, cand_data[key]): + continue # continue to the following candidate + + # Populate the matching_cand dictionary with the candidate info + matching_cand[key] = cand_data[key] + + # Deal with occurences where CandidateStatus is not set + if "CandidateStatus" not in cand_data[key].keys(): + status = " " + else: + status = cand_data[key]["CandidateStatus"] + + # Deal with occurences where PhoneNumber is not set + if "PhoneNumber" not in cand_data[key].keys(): + phone = "" + else: + phone = cand_data[key]["PhoneNumber"] + + # Insert a given candidate into the datatable + self.datatable.insert( + '', + 'end', + values=[ + matching_cand[key]["Identifier"], + matching_cand[key]["FirstName"], + matching_cand[key]["LastName"], + matching_cand[key]["DateOfBirth"], + matching_cand[key]["Gender"], + phone, + status + ], + tags=(status, matching_cand[key]["Identifier"]) + ) + + except Exception as e: # TODO proper exception handling + print "datatable.ParticipantsList.load_data ", str(e) + pass + + + + +class VisitList(DataTable): + """ + This class takes care of the data table holding the list of all + appointments, even those that have not been confirmed yet. + + """ + + def __init__(self, parent, colheaders): + """ + __init__ function of ParticipantsList class + + :param parent: frame in which to insert the visit list datatable + :type parent: frame + :param colheaders: array of column headers to use in the datatable + :type colheaders: list + + """ + + DataTable.__init__(self, parent, colheaders) + + self.colheaders = colheaders # description_frame_gui the column headers + self.load_data() # load the data in the datatable + + # TODO add color settings in a 'settings and preferences' section + # TODO change 'active' & 'tentative' for non-language parameter + self.datatable.tag_configure('active', background='#F1F8FF') + self.datatable.tag_configure('tentative', background='#F0F0F0') + + + def load_data(self, pattern=False): + """ + Load the visit list into the datatable. + + """ + + # Read visit list and visit information into a visit_data dictionary + visit_data = DataManagement.read_visitset_data() + + try: + # Loop through candidates + for cand_key, value in visit_data.iteritems(): + + # Skip the search_event if visitset == None for that candidate + if "VisitSet" in visit_data[cand_key].keys(): + + # set this candidate.visitset for the next step + current_visitset = visit_data[cand_key]["VisitSet"] + + # gather information about the candidate + candidate_id = visit_data[cand_key]["Identifier"] + candidate_firstname = visit_data[cand_key]["FirstName"] + candidate_lastname = visit_data[cand_key]["LastName"] + candidate_fullname = str( + candidate_firstname + ' ' + candidate_lastname + ) + + # Loop through all visits for that candidate + for visit_key, value in current_visitset.iteritems(): + + if "VisitStatus" in current_visitset[visit_key].keys(): + # Set visit status and label + status = current_visitset[visit_key]["VisitStatus"] + visit_label = current_visitset[visit_key]["VisitLabel"] + + # Check at what time the visit has been scheduled + field = 'VisitStartWhen' + if field not in current_visitset[visit_key].keys(): + when = '' + #TODO check what would be next visit + #TODO & set its status to "to_schedule" + #when = current_visitset[visit_key].whenearliest + + else: + when_key = 'VisitStartWhen' + when = current_visitset[visit_key][when_key] + + # Check if the location of the visit has been set + field = 'VisitWhere' + if field not in current_visitset[visit_key].keys(): + where = '' + + else: + where = current_visitset[visit_key]["VisitWhere"] + # Check that all values could be found + row_values = [ + candidate_id, candidate_fullname, visit_label, + when, where, status + ] + row_tags = (status, candidate_id, visit_label) + self.datatable.insert( + '', 'end', values = row_values, tags = row_tags + ) + + except Exception as err: + #TODO deal with exception + #TODO add proper error handling + print "Could not load visits" + pass diff --git a/dicat/ui/datawindow.py b/dicat/ui/datawindow.py new file mode 100644 index 0000000..a73586f --- /dev/null +++ b/dicat/ui/datawindow.py @@ -0,0 +1,558 @@ +# import standard packages +from Tkinter import * +from ttk import * + +# import external package from https://github.com/moshekaplan/tkinter_components +import CalendarDialog.CalendarDialog as CalendarDialog + +# import internal packages +import ui.dialogbox as DialogBox +import lib.utilities as Utilities +import lib.multilanguage as MultiLanguage +import lib.datamanagement as DataManagement +from lib.candidate import Candidate + + +# ref: http://effbot.org/tkinterbook/tkinter-newDialog-windows.htm +# TODO this class needs a major clean-up + + +class DataWindow(Toplevel): + + def __init__(self, parent, candidate=''): + """ + Initialize the DataWindow class. + + :param parent: parent frame of the data window + :type parent: object + :param candidate: candidate ID or 'new' for a new candidate + :type candidate: str + + """ + + Toplevel.__init__(self, parent) + + # Create a transient window on top of parent window + self.transient(parent) + self.parent = parent + self.candidate = candidate + #TODO find a better title for the data window + self.title(MultiLanguage.data_window_title) + body = Frame(self) + + # Draw the body of the data window + self.initial_focus = self.body(body) + body.pack(padx=5, pady=5) + + # Draw the button box of the data window + self.button_box() + + self.grab_set() + if not self.initial_focus: + self.initial_focus = self + self.protocol("WM_DELETE_WINDOW", self.close_dialog) + Utilities.center_window(self) + self.initial_focus.focus_set() + self.wait_window(self) + + + def body(self, master): + """ + Creates the body of the 'data window'. + + :param master: frame in which to draw the body of the data window + :type master: object + + """ + + # Load the candidate and visitset data + cand_info = [] + visitset = [] + if not self.candidate == 'new': + (cand_info, visitset) = self.load_data() + + ## Create a candidate section in the data window + self.candidate_pane = Labelframe( + self, + text=MultiLanguage.candidate_pane, + width=250, + height=350, + borderwidth=10 + ) + self.candidate_pane.pack( + side=TOP, expand=YES, fill=BOTH, padx=5, pady=5 + ) + + # Draw in the candidate section of the data window + self.candidate_pane_ui(cand_info) + + # Draw the visit section if self.candidate is not 'new' or 'search' + if not self.candidate == 'new': + # Create a calendar section in the data window + self.schedule_pane = Labelframe( + self, + text=MultiLanguage.schedule_pane, + width=250, + height=350, + borderwidth=10 + ) + self.schedule_pane.pack( + side=TOP, expand=YES, fill=BOTH, padx=5, pady=5 + ) + # Draw in the calendar section of the data window + self.schedule_pane_ui(visitset) + + + def candidate_pane_ui(self, cand_info): + """ + Draws the candidate section of the datawindow and populates it fields + based on what is store in cand_info + + :param cand_info: dictionary with the candidate's information + :type cand_info: dict + + """ + + # Initialize text variables that will contain the field values + self.text_pscid_var = StringVar() + self.text_firstname_var = StringVar() + self.text_lastname_var = StringVar() + self.text_dob_var = StringVar() + self.text_gender_var = StringVar() + self.text_status_var = StringVar() + self.text_phone_var = StringVar() + + # If self.candidate is populated with a candID populate the fields with + # values available in cand_info dictionary, otherwise populate with + # empty str or " " in the case of drop down menus + if self.candidate == 'new': + self.text_pscid_var.set("") + self.text_firstname_var.set("") + self.text_lastname_var.set("") + self.text_dob_var.set("") + self.text_gender_var.set(" ") + self.text_status_var.set(" ") + self.text_phone_var.set("") + else: + self.text_pscid_var.set(cand_info["Identifier"]) + self.text_firstname_var.set(cand_info["FirstName"]) + self.text_lastname_var.set(cand_info["LastName"]) + self.text_dob_var.set(cand_info["DateOfBirth"]) + self.text_gender_var.set(cand_info["Gender"]) + self.text_status_var.set(cand_info["CandidateStatus"]) + self.text_phone_var.set(cand_info["PhoneNumber"]) + + # Create widgets to be displayed + # (typically a label with a text box underneath per variable to display) + self.label_pscid = Label( # identifier label + self.candidate_pane, text=MultiLanguage.candidate_pscid + ) + self.text_pscid = Entry( # identifier text box + self.candidate_pane, textvariable=self.text_pscid_var + ) + self.label_firstname = Label( # firstname label + self.candidate_pane, text=MultiLanguage.candidate_firstname + ) + self.text_firstname = Entry( # firstname text box + self.candidate_pane, textvariable=self.text_firstname_var + ) + self.label_lastname = Label( # lastname label + self.candidate_pane, text=MultiLanguage.candidate_lastname + ) + self.text_lastname = Entry( # lastname text box + self.candidate_pane, textvariable=self.text_lastname_var + ) + self.label_dob = Label( # date of birth label + self.candidate_pane, text=MultiLanguage.candidate_dob + ) + self.text_dob = Entry( # date of birth text box + self.candidate_pane, textvariable=self.text_dob_var, width=15 + ) + self.dob_picker = Button( # date of birth date picker + self.candidate_pane, + text='Date Picker', + command=self.date_picker_event + ) + self.label_gender = Label( # gender label + self.candidate_pane, text=MultiLanguage.candidate_gender + ) + gender_options = [' ', 'Male', 'Female'] + self.text_gender = OptionMenu( # gender selected from a drop down menu + self.candidate_pane, + self.text_gender_var, # variable in which to store the selection + self.text_gender_var.get(), # default value to be used at display + *gender_options # list of drop down options + ) + self.label_status = Label( # candidate status label + self.candidate_pane, text=MultiLanguage.candidate_status + ) + #TODO: grep the status_options list from the project information + status_options = [ + ' ', 'active', 'withdrawn', 'excluded', + 'death', 'ineligible', 'completed' + ] + self.text_status = OptionMenu( # cand. status selected from drop down + self.candidate_pane, + self.text_status_var, # variable in which to store the selection + self.text_status_var.get(), # default value to be used at display + *status_options # list of drop down options + ) + self.label_phone = Label( # phone number label + self.candidate_pane, text=MultiLanguage.candidate_phone + ) + self.text_phone = Entry( # phone number text box + self.candidate_pane, textvariable=self.text_phone_var + ) + + # Draw widgets in the candidate pane section + self.label_pscid.grid( # draw identifier label + column=0, row=0, padx=10, pady=5, sticky=N+S+E+W + ) + self.text_pscid.grid( # draw identifier text box + column=0, row=1, padx=10, pady=5, sticky=N+S+E+W + ) + self.label_firstname.grid( # draw firstname label + column=1, row=0, padx=10, pady=5, sticky=N+S+E+W + ) + self.text_firstname.grid( # draw firstname text box + column=1, row=1, padx=10, pady=5, sticky=N+S+E+W + ) + self.label_lastname.grid( # draw lastname label + column=2, row=0, padx=10, pady=5, sticky=N+S+E+W + ) + self.text_lastname.grid( # draw lastname text box + column=2, row=1, padx=10, pady=5, sticky=N+S+E+W + ) + self.label_dob.grid( # draw date of birth label + column=3, row=0, columnspan=2, padx=10, pady=5, sticky=N+S+E+W + ) + self.text_dob.grid( # draw date of birth text box + column=3, row=1, padx=(10, 0), pady=5, sticky=N+S+E+W + ) + self.dob_picker.grid( + column=4, row=1, padx=(0,10), pady=5, sticky=N+S+E+W + ) + self.label_gender.grid( # draw gender label + column=0, row=2, padx=10, pady=5, sticky=N+S+E+W + ) + self.text_gender.grid( # draw gender text box + column=0, row=3, padx=10, pady=5, sticky=N+S+E+W + ) + self.label_status.grid( # draw candidate status label + column=1, row=2, padx=10, pady=5, sticky=N+S+E+W + ) + self.text_status.grid( # draw candidate status text box + column=1, row=3, padx=10, pady=5, sticky=N+S+E+W + ) + self.label_phone.grid( # draw phone number label + column=2, row=2, padx=10, pady=5, sticky=N+S+E+W + ) + self.text_phone.grid( # draw phone number text box + column=2, row=3, padx=10, pady=5, sticky=N+S+E+W + ) + + + def date_picker_event(self): + """ + Date picker event. Once a date have been chosen from CalendarDialog, + will print the result into the self.text_dob_var entry variable with + the proper format. + + """ + + # Initialize the calendar dialog window + cd = CalendarDialog.CalendarDialog(self.candidate_pane) + + # Grep the date picked and print it in self.text_dob_var entry variable + date_picked = cd.result + self.text_dob_var.set(date_picked.strftime("%Y-%m-%d")) + + + def schedule_pane_ui(self, visitset): + + # If the candidate has not visit set, display a message on the calendar + # section to say that no visit has been scheduled yet for that candidate + if not visitset: + self.label_no_visit = Label( + self.schedule_pane, text=MultiLanguage.schedule_no_visit_yet + ) + self.label_no_visit.grid( + row=0, column=1, columnspan=4, padx=5, sticky=N+S+E+W + ) + return + + # Create top row (header) widgets + self.label_visit_label = Label( # create visit label widget + self.schedule_pane, text=MultiLanguage.col_visitlabel + ) + self.label_visit_when = Label( # create visit when widget + self.schedule_pane, text=MultiLanguage.col_when + ) + self.label_visit_where = Label( # create visit where widget + self.schedule_pane, text=MultiLanguage.col_where + ) + self.label_visit_withwhom = Label( # create visit withwhom widget + self.schedule_pane, text=MultiLanguage.col_withwhom + ) + self.label_visit_status = Label( # create visit status widget + self.schedule_pane, text=MultiLanguage.col_status + ) + + # Draw the top row (header) widgets + self.label_visit_label.grid( # draw visit label widget + row=0, column=1, padx=5, pady=5, sticky=N+S+E+W + ) + self.label_visit_when.grid( # draw visit when widget + row=0, column=2, padx=5, pady=5, sticky=N+S+E+W + ) + self.label_visit_where.grid( # draw visit where widget + row=0, column=3, padx=5, pady=5, sticky=N+S+E+W + ) + self.label_visit_withwhom.grid( # draw visit withwhom widget + row=0, column=4, padx=5, pady=5, sticky=N+S+E+W + ) + self.label_visit_status.grid( # draw visit status widget + row=0, column=5, padx=5, pady=5, sticky=N+S+E+W + ) + + # Sort visit list based on the VisitStartWhen field + visit_list = DataManagement.sort_candidate_visit_list(visitset) + + # Show values on ui + row_number=1 + for visit in visit_list: + + # Check if values are set for VisitStartWhen, VisitWhere, + # VisitWindow & VisitStatus keys. If not, set it to empty string as + # we need a text to display in the corresponding label widgets. + visit_when = "" + visit_where = "" + visit_status = "" + visit_with_whom = "" + if "VisitStartWhen" in visit.keys(): + #TODO: implement automatic range for next visit + visit_when = visit["VisitStartWhen"] + if "VisitWhere" in visit.keys(): + visit_where = visit["VisitWhere"] + if "VisitWithWhom" in visit.keys(): + visit_with_whom = visit["VisitWithWhom"] + if "VisitStatus" in visit.keys(): + visit_status = visit["VisitStatus"] + + # Create the visit row widgets + label_visit_label = Label( # visit label widget + self.schedule_pane, text=visit["VisitLabel"] + ) + label_visit_when = Label( # visit when widget + self.schedule_pane, text=visit_when + ) + label_visit_where = Label( # visit where widget + self.schedule_pane, text=visit_where + ) + label_visit_with_whom = Label( # visit with whom widget + self.schedule_pane, text=visit_with_whom + ) + label_visit_status = Label( # visit status widget + self.schedule_pane, text=visit_status + ) + + # Draw the visit row widget + label_visit_label.grid( + row=row_number+1, column=1, padx=5, pady=5, sticky=N+S+E+W + ) + label_visit_when.grid( + row=row_number+1, column=2, padx=5, pady=5, sticky=N+S+E+W + ) + label_visit_where.grid( + row=row_number+1, column=3, padx=5, pady=5, sticky=N+S+E+W + ) + label_visit_with_whom.grid( + row=row_number+1, column=4, padx=5, pady=5, sticky=N+S+E+W + ) + label_visit_status.grid( + row=row_number+1, column=5, padx=5, pady=5, sticky=N+S+E+W + ) + + # Increment row_number for the next visit to be displayed + row_number += 1 + + + def load_data(self): + """ + Read the XML data and return the candidate's (self.candidate) + information as well as its visit information. + + :return cand_data: data dictionary with candidate information + :rtype cand_data: dict + :return visit_data: data dictionary with visit information + :rtype visit_data: dict + + """ + + try: + # Read candidate information + cand_data = DataManagement.read_candidate_data() + # Read visit information + visit_data = DataManagement.read_visitset_data() + visitset = {} # Create a visitset dictionary + cand_info = {} # Create a candidate information dictionary + + # Loop through all candidates + for cand_key in cand_data: + # Grep candidate's information from cand_data dictionary + if cand_data[cand_key]["Identifier"] == self.candidate: + cand_info = cand_data[cand_key] + break + + # Loop through candidates' visit data + for cand_key in visit_data: + # Grep candidate's visit set information from visit_data + if visit_data[cand_key]["Identifier"] == self.candidate \ + and 'VisitSet' in visit_data[cand_key]: + visitset = visit_data[cand_key]["VisitSet"] + break + + except Exception as e: + print "datawindow.body ", str(e) # TODO manage exceptions + return + + return cand_info, visitset + + + def button_box(self): + """ + Draws the button box at the bottom of the data window. + + """ + + # add standard button box + box = Frame(self) + + # description_frame_gui buttons + ok = Button( + box, text="OK", width=10, command=self.ok_button, default=ACTIVE + ) + cancel = Button( + box, text="Cancel", width=10, command=self.cancel_button + ) + + # draw the buttons + ok.pack(side=LEFT, padx=5, pady=5) + cancel.pack(side=LEFT, padx=5, pady=5) + + # bind key handlers to button functions + self.bind("", self.ok_button) + self.bind("", self.close_dialog) + + # draw the button box + box.pack() + + + def ok_button(self, event=None): + """ + Event handler for the OK button. If something was missing in the data + and it could not be saved, it will pop up an error message with the + appropriate error message. + + :param event: + :type event: + + :return: + + """ + + message = self.capture_data() + + if message: + parent = Frame(self) + newwin = DialogBox.ErrorMessage(parent, message) + if newwin.buttonvalue == 1: + return # to stay on the candidate pop up page after clicking OK + + if not self.validate(): + self.initial_focus.focus_set() # put focus back + return + + #need to call treeview update here + self.withdraw() + self.close_dialog() + + + def cancel_button(self, event=None): + """ + Event handler for the cancel button. Will ask confirmation if want to + cancel, if yes put focus back to the datatable without saving, else put + focus back to the data window. + + :param event: + :type event: + + :return: + + """ + + parent = Frame(self) + newwin = DialogBox.ConfirmYesNo(parent, MultiLanguage.dialog_close) + if newwin.buttonvalue == 1: + self.close_dialog() + else: + return + + + def close_dialog(self, event=None): + """ + Close dialog handler: will put focus back to the parent window. + + :param event: + :return: + + """ + + # put focus back to parent window before destroying the window + self.parent.focus_set() + self.destroy() + + + def validate(self): + return 1 + + + def capture_data(self): + """ + Grep the information from the pop up window's text fields and save the + candidate information based on the pscid. + + :param: None + + :return: None + + """ + + # Initialize the candidate dictionary with new values + cand_data = {} + + # Capture data from fields + cand_data['Identifier'] = self.text_pscid.get() + cand_data['FirstName'] = self.text_firstname.get() + cand_data['LastName'] = self.text_lastname.get() + cand_data['DateOfBirth'] = self.text_dob.get() + cand_data['Gender'] = self.text_gender_var.get() + cand_data['PhoneNumber'] = self.text_phone.get() + cand_data['CandidateStatus'] = self.text_status_var.get() + + # Set CandidateStatus to space string if not defined in cand_data + if not cand_data['CandidateStatus']: + cand_data['CandidateStatus'] = " " + + # Set PhoneNumber to space string if not defined in cand_data + if not cand_data['PhoneNumber']: + cand_data['PhoneNumber'] = " " + + # Check fields format and required fields + candidate = Candidate(cand_data) + message = candidate.check_candidate_data('scheduler', self.candidate) + if message: + return message + + # Save candidate data + DataManagement.save_candidate_data(cand_data) diff --git a/dicat/ui/dialogbox.py b/dicat/ui/dialogbox.py new file mode 100644 index 0000000..b5accc5 --- /dev/null +++ b/dicat/ui/dialogbox.py @@ -0,0 +1,189 @@ +#import standard packages +from Tkinter import * + +#import internal packages +import lib.utilities as Utilities +import lib.multilanguage as MultiLanguage + +class DialogBox(Toplevel): + """ + This class was created mainly because the native dialog box don't work as + expected when called from a top-level window. + This class (although it could be improved in many aspects) insure that the + parent window cannot get focus while a dialog box is still active. + + """ + + def __init__(self,parent, title, message, button1, button2): + """ + Initialize the dialog box window. + + :param parent: parent window to the dialog window + :type parent: object + :param title: title to give to the dialog window + :type title: str + :param message: message to be displayed in the dialog window + :type message: str + :param button1: what should be written on button 1 of the dialog window + :type button1: str + :param button2: what should be written on button 2 of the dialog window + :type button2: str + + """ + + Toplevel.__init__(self,parent) + self.transient(parent) + self.parent = parent + self.title(title) + body = Frame(self) + self.initial_focus = self.body(body, message) + body.pack(padx=4, pady=4) + self.buttonbox(button1, button2) + self.grab_set() + + if not self.initial_focus: + self.initial_focus = self + + self.protocol("WM_DELETE_WINDOW", self.button2) + Utilities.center_window(self) + self.initial_focus.focus_set() + self.deiconify() + self.wait_window(self) + + + def body(self, parent, message): + """ + Draw the body of the dialog box + + :param parent: parent window of the dialog box + :type parent: object + :param message: message to be drawn on the dialog box + :type message: str + + """ + + # Draw the message in the dialog box + label = Label(self, text=message) + label.pack(padx=4, pady=4) + + + def buttonbox(self, button1, button2): + """ + Draws the button box at the bottom of the dialog box. + + :param button1: button 1 of the button box + :type button1: str + :param button2: button 2 of the button box + :type button2: str + + """ + + #add a standard button box + box = Frame(self) + b1 = Button( + box, text=button1, width=12, command=self.button1, default=ACTIVE + ) + b1.pack(side=LEFT, padx=4, pady=4) + self.bind("", self.button1) + if button2: + b2 = Button( + box, text=button2, width=12, command=self.button2, + default=ACTIVE + ) + b2.pack(side=LEFT, padx=4, pady=4) + self.bind("", self.button2) + box.pack() + + + def button1(self, event=None): + """ + Event handler for button1. + + :param event: + :type event: + + """ + + if not self.validate(): + self.initial_focus.focus_set() #put focus on Button + return + self.buttonvalue = 1 + self.closedialog() + + + def button2(self, event=None): + """ + Event handler for button2. + + :param event: + :type event: + + """ + self.buttonvalue = 2 + self.closedialog() + + + def closedialog(self, event=None): + """ + Event handler to close the dialog box. + + :param event: + :type event: + + """ + + #put focus back to parent window before destroying the window + self.parent.focus_set() + self.destroy() + + + def validate(self): + return 1 + + + + +class ConfirmYesNo(DialogBox): + """ + Confirmation on closing a window class -> Yes or No. + + """ + + def __init__(self, parent, message): + """ + Initialization of the confirmation window class. + + :param parent: parent of the confirmation window + :type parent: object + :param message: message to print in the confirmation window. + :type message: str + + """ + title = MultiLanguage.dialog_title_confirm + button1 = MultiLanguage.dialog_yes + button2 = MultiLanguage.dialog_no + DialogBox.__init__(self, parent, title, message, button1, button2) + + + + +class ErrorMessage(DialogBox): + """ + Error Message pop up window. + + """ + + def __init__(self, parent, message): + """ + Initialization of the error message window. + + :param parent: parent of the error message window to be displayed + :type parent: object + :param message: message to be displayed on the error window. + :type message: str + + """ + + title = MultiLanguage.dialog_title_error + button = MultiLanguage.dialog_ok + DialogBox.__init__(self, parent, title, message, button, None) diff --git a/dicat/ui/menubar.py b/dicat/ui/menubar.py new file mode 100644 index 0000000..dbd98ef --- /dev/null +++ b/dicat/ui/menubar.py @@ -0,0 +1,76 @@ +#import standard packages +import Tkinter + +#import internal packages +import lib.multilanguage as MultiLanguage +import ui.datawindow as DataWindow + +class SchedulerMenuBar(Tkinter.Menu): + + def __init__(self, parent): + """ + Initialize the Menu bar of the application + + :param parent: parent in which to put the menu bar of the application + :type parent: object + + """ + + Tkinter.Menu.__init__(self, parent) + + # Create an APPLICATION pulldown menu + application_menu = Tkinter.Menu(self, tearoff=False) + self.add_cascade( + label=MultiLanguage.application_menu, + underline=0, + menu=application_menu + ) + application_menu.add_command( + label=MultiLanguage.application_setting, + underline=1, + command=self.app_settings + ) + application_menu.add_separator() + application_menu.add_command( + label=MultiLanguage.application_quit, + underline=1, + command=self.quit_application + ) + + # Create a HELP pulldown menu + help_menu = Tkinter.Menu(self, tearoff=0) + self.add_cascade( + label=MultiLanguage.help_menu, underline=0, menu=help_menu + ) + help_menu.add_command( + label=MultiLanguage.help_get_help, command=self.open_help + ) + help_menu.add_command( + label=MultiLanguage.help_about_window, + command=self.about_application + ) + + + def app_settings(self): + #TODO implement app_settings() + print 'running appsettings' + pass + + + def quit_application(self): + #TODO implement quit_application() + print 'running quit_application' + self.quit() + pass + + + def open_help(self): + #TODO open_help() + print 'running open_help' + pass + + + def about_application(self): + #TODO about_application() + print 'running about_application' + pass \ No newline at end of file diff --git a/dicat/ui/projectpane.py b/dicat/ui/projectpane.py new file mode 100644 index 0000000..2d4ae70 --- /dev/null +++ b/dicat/ui/projectpane.py @@ -0,0 +1,22 @@ +# Import from standard packages +from Tkinter import * + +# Import from DICAT libraries +import lib.multilanguage as multilanguage + +class ProjectPane(LabelFrame): + """ + This class will contain everything about the Project Pane. + + """ + + def __init__(self, parent): + """ + Initialize the ProjectPane class + + :param parent: frame where to put the project pane + :type parent: object + + """ + + LabelFrame.__init__(self, parent) \ No newline at end of file diff --git a/dicat/welcome_frame.py b/dicat/welcome_frame.py index 1fa5cdd..1820188 100644 --- a/dicat/welcome_frame.py +++ b/dicat/welcome_frame.py @@ -1,30 +1,48 @@ #!/usr/bin/python +# import from standard packages from Tkinter import * +import tkFileDialog -''' -lib.resource_path_methods has been created for Pyinstaller. -Need to load images or external files using these methods, otherwise the -created application would not find them. -''' -import lib.resource_path_methods as PathMethods +# import from DICAT libraries +import lib.config as Config +import lib.resource_path_methods as PathMethods # needed for PyInstaller builds class welcome_frame_gui(Frame): + """ + Welcome frame GUI class. + + """ def __init__(self, parent): + """ + Initialization of the welcome frame gui class. + + :param parent: parent widget in which to display the welcome frame + :type parent: object + + """ + self.parent = parent - self.initialize() + self.description_frame_gui() + self.load_database_gui() + + + def description_frame_gui(self): + """ + Draws the description frame with the LOGO image. + + """ - def initialize(self): - self.frame = Frame(self.parent) - self.frame.pack(expand=1, fill='both') + frame = Frame(self.parent) + frame.pack(expand=1, fill='both') # Insert DICAT logo on the right side of the screen load_img = PathMethods.resource_path("images/DICAT_logo.gif") imgPath = load_img.return_path() logo = PhotoImage(file = imgPath) - logo_image = Label(self.frame, + logo_image = Label(frame, image = logo, bg='white' ) @@ -33,8 +51,8 @@ def initialize(self): logo_image.pack(side='left', fill='both') # Create the Welcome to DICAT text variable - text = Text(self.frame, padx=40, wrap='word') - scroll = Scrollbar(self.frame, command=text.yview) + text = Text(frame, padx=40, wrap='word') + scroll = Scrollbar(frame, command=text.yview) text.configure(yscrollcommand=scroll.set) text.tag_configure('title', font=('Verdana', 20, 'bold', 'italic'), @@ -76,8 +94,129 @@ def initialize(self): ''' text.insert(END, IDkey, 'default') + # Insert explanation of the scheduler tab into the text variable + tab3 = "\n\nThe scheduler tab allows to:\n" + text.insert(END, tab3, 'bold') + + # TODO: develop on the functionality of scheduler + Sched = ''' + 1) Store patient information + 2) Schedule visits + 3) View the schedule + 4) blablabla + ''' + text.insert(END, Sched, 'default') + + # Display the text variable text.pack(side='left', fill='both', expand=1) scroll.pack(side="right", fill='y') - # Disable the edit functionality of the displayed text + # Disable the edit_event functionality of the displayed text text.config(state='disabled') + + + def load_database_gui(self): + """ + Load database GUI including: + - a button to create a new database based on a template file + - a button to select and open an existing database + - an entry where the path to the loaded database file is displayed + + """ + + frame = Frame(self.parent, bd=5, relief='groove') + frame.pack(expand=0, fill='both') + + # Label + label = Label( + frame, + text=u"Open a DICAT database (.xml file)", + font=('Verdana', 15, 'bold', 'italic') + ) + + # select an existing candidate.xml file + # Initialize default text that will be in self.entry + self.entryVariable = StringVar() + self.entryVariable.set("Open a DICAT database (.xml file)") + + # Create an entry with a default text that will be replaced by the path + # to the XML file once directory selected + entry = Entry( + frame, width=60, textvariable=self.entryVariable + ) + entry.focus_set() + entry.selection_range(0, END) + + # Create an open button to use to select an XML file with candidate's + # key info + buttonOpen = Button( + frame, + text=u"Open an existing database", + command=self.open_existing_database + ) + + buttonCreate = Button( + frame, + text=u"Create a new database", + command=self.create_new_database + ) + + label.grid( + row=0, column=0, columnspan=3, padx=(0,15), pady=0, sticky=E+W + ) + buttonCreate.grid( + row=1, column=0, padx=(15,15), pady=10, sticky=E+W + ) + buttonOpen.grid( + row=1, column=1, padx=(0,15), pady=10, sticky=E+W + ) + entry.grid( + row=1, column=2, padx=(0,15), pady=10, sticky=E+W + ) + + + def open_existing_database(self): + """ + Opens and loads the selected XML database in DICAT. + + """ + + self.filename = tkFileDialog.askopenfilename( + filetypes=[("XML files", "*.xml")] + ) + self.entryVariable.set(self.filename) + + if self.filename: + # Set the database xmlfile in Config.xmlfile to self.filename + Config.xmlfile = self.filename + else: + #TODO: proper error handling + print "file could not be opened." + + print Config.xmlfile + + + def create_new_database(self): + """ + Uses a template XML file to create a new database and saves it. + + """ + + self.filename = tkFileDialog.asksaveasfilename( + defaultextension=[("*.xml")], + filetypes=[("XML files", "*.xml")] + ) + self.entryVariable.set(self.filename) + + # Fetch the database template file + load_template = PathMethods.resource_path("data/database_template.xml") + template_file = load_template.return_path() + + # If both the new file and the template file exists, copy the template + # lines in the new file + if self.filename and template_file: + # Set the database xmlfile in Config.xmlfile to self.filename + Config.xmlfile = self.filename + with open(Config.xmlfile, 'a') as f1: + for line in open(template_file, 'r'): + f1.write(line) diff --git a/docs/How_To_Create_DICAT_Executables.md b/docs/How_To_Create_DICAT_Executables.md index e198a3e..ce3f218 100644 --- a/docs/How_To_Create_DICAT_Executables.md +++ b/docs/How_To_Create_DICAT_Executables.md @@ -2,27 +2,27 @@ Here is described the procedure used to create DICAT executables for Window, Mac OS X and Linux workstation. In order to be able to follow the steps mentioned below, you will need to install [Pyinstaller](http://www.pyinstaller.org). -## 1) Run pyinstaller on DICAT_application.py +## 1) Run pyinstaller on DICAT.py -In the terminal/console, go into the `dicat` directory hosting the DICAT_application.py script and run the following command depending on the OS used. +In the terminal/console, go into the `dicat` directory hosting the DICAT.py script and run the following command depending on the OS used. ### On Mac OS X -```pyinstaller --onefile --windowed --icon=images/DICAT_logo.icns DICAT_application.py``` +```pyinstaller --onefile --windowed --icon=images/DICAT_logo.icns DICAT.py``` ### On Windows -```pyinstaller --onefile --windowed --icon=images\dicat_logo_HsB_2.ico DICAT_application.py``` +```pyinstaller --onefile --windowed --icon=images\dicat_logo_HsB_2.ico DICAT.py``` ### On Linux (tested on Ubuntu) -```pyinstaller --onefile DICAT_application.py``` +```pyinstaller --onefile DICAT.py``` -Executing this command will create a `DICAT_application.spec` file in the same directory, as well as a `build` and `dist` directory. +Executing this command will create a `DICAT.spec` file in the same directory, as well as a `build` and `dist` directory. -## 2) Edit DICAT_application.spec +## 2) Edit DICAT.spec -Edit the `DICAT_application.spec` file to include the path to the image and the XML file used by the application (a.k.a. `images/DICAT_logo.gif` and `data/fields_to_zap.xml`). +Edit the `DICAT.spec` file to include the path to the image and the XML file used by the application (a.k.a. `images/DICAT_logo.gif` and `data/fields_to_zap.xml`). To do so, insert the following lines after the `Analysis` block of the spec file (Note, the /PATH/TO/DICOM_anonymizer should be updated with the proper full path). @@ -42,7 +42,7 @@ a.datas += [ FYI, the analysis block of the spec file looks like: ``` -a = Analysis(['DICAT_application.py'], +a = Analysis(['DICAT.py'], pathex=['/PATH/TO/DICOM_anonymizer/dicat'], binaries=None, datas=None, @@ -55,15 +55,15 @@ a = Analysis(['DICAT_application.py'], cipher=block_cipher) ``` -## 3) Rerun pyinstaller using DICAT_application.spec +## 3) Rerun pyinstaller using DICAT.spec Once the path to the images have been added, rerun the pyinstaller command on the spec file as follows. ### On Mac OS X -```pyinstaller --onefile --windowed --icon=images/DICAT_logo.icns DICAT_application.spec``` +```pyinstaller --onefile --windowed --icon=images/DICAT_logo.icns DICAT.spec``` -A `DICAT_application.app` will be located in the dist directory created by the pyinstaller command. +A `DICAT.app` will be located in the dist directory created by the pyinstaller command. To include the DICAT logo to the app, execute the following steps: @@ -74,9 +74,9 @@ To include the DICAT logo to the app, execute the following steps: * Choose 'Copy' from the 'Edit' menu. * Close the info window -2. Paste the icon to the `DICAT_application.app` item - * Go to the `DICAT_application.app` item in the Finder - * Click on the `DICAT_application.app` item +2. Paste the icon to the `DICAT.app` item + * Go to the `DICAT.app` item in the Finder + * Click on the `DICAT.app` item * Choose 'Get Info' from the 'File' menu. * In the info window that pops up, click on the icon * Choose 'Paste' from the 'Edit' menu. @@ -88,15 +88,15 @@ Congratulations! You just created the DICAT app for Mac OS X!! Note: Make sure the paths in the spec file contains \\ instead of \. -```pyinstaller --onefile --windowed --icon=images\DICAT_logo.icns DICAT_application.spec``` +```pyinstaller --onefile --windowed --icon=images\DICAT_logo.icns DICAT.spec``` -A `DICAT_application.exe` will be located in the dist directory created by the pyinstaller command. +A `DICAT.exe` will be located in the dist directory created by the pyinstaller command. Congratulations! You just created the DICAT executable for Windows!! ### On Linux (tested on Ubuntu) -```pyinstaller --onefile DICAT_application.spec``` +```pyinstaller --onefile DICAT.spec``` To include the DICAT logo to the application, execute the following steps: @@ -108,3 +108,4 @@ Congratulations! You just created the DICAT application for Linux!! +DICAT \ No newline at end of file