class ChecklistWidget(ScrollView): screen_manager = ObjectProperty(None) def __init__(self, *args, **kwargs): super(ChecklistWidget, self).__init__(*args, **kwargs) Window.bind(on_keyboard=self.on_back_btn) self.checklist_layout = GridLayout(cols=1) self.checklist_layout.size_hint_y = None self.checklist_layout.id = 'checklist' self.checklist_layout.bind( minimum_height=self.checklist_layout.setter('height')) self.title = '' self.submit_button = Button() self.filewidget = FileWidget() self.confirmation_popup = Popup() self.data = None self.question_list = [] self.response_list = [] self.rect = Rectangle() self.checklist_menu_layout = GridLayout(cols=1) self.file_select_popup = Popup() self.set_background(self.checklist_menu_layout) self.do_get_checklists() def set_background(self, layout): """ Sets a solid color as a background. :param layout: The layout for whichever part of the screen should be set. """ layout.bind(size=self._update_rect, pos=self._update_rect) with layout.canvas.before: Color(0, 0, 0, 1) self.rect = Rectangle(size=layout.size, pos=layout.pos) def _update_rect(self, instance, value): """ Ensures that the canvas fits to the screen should the layout ever change. """ self.rect.pos = instance.pos self.rect.size = instance.size def do_get_checklists(self): """ Creates the base screen for the checklist manager. It includes previously loaded checklists as well as a button for new checklists. """ self.clear_widgets() self.checklist_menu_layout.clear_widgets() titles = [] new_json_button = Button(text='Load New Checklist', size_hint_y=None) new_json_button.bind(on_release=lambda x: self.do_popup_file_select()) self.checklist_menu_layout.add_widget(new_json_button) for dirname, dirnames, _ in os.walk('/sdcard/operator/checklists/'): for subdirname in dirnames: loc = os.path.join(dirname, subdirname) name = loc.split("/") titles.append(name[len(name) - 1]) for title in titles: button_to_checklist = Button(text=title, size_hint_y=None) button_to_checklist.bind( on_release=functools.partial(self.do_open_checklist, title)) self.checklist_menu_layout.add_widget(button_to_checklist) self.add_widget(self.checklist_menu_layout) def do_popup_file_select(self): """ Prompts the user to navigate to the .JSON file """ self.filewidget = FileWidget() box = BoxLayout(orientation='vertical') box_int = BoxLayout(orientation='horizontal', size_hint=(1, .2)) close_button = Button(text='Load') close_button.bind( on_release=lambda x: self.do_add_checklist_location()) dismiss_button = Button(text='Cancel') dismiss_button.bind( on_release=lambda x: self.file_select_popup.dismiss()) box.clear_widgets() box_int.add_widget(close_button) box_int.add_widget(dismiss_button) box.add_widget(self.filewidget) box.add_widget(box_int) self.file_select_popup = Popup(title='Choose File', content=box, size_hint=(None, None), size=(800, 1000), auto_dismiss=False) self.file_select_popup.open() def do_load_true_path(self, path, filename): """ Does a series of a checks to make sure the file that is trying to be loaded is valid. :param str path: The directory of the file. :param list filename: The name of the file. :return: The path to the validated JSON file. If the path is deemed invalid, None is returned. :rtype: str """ full_path = os.path.join(path, filename[0]) if not os.path.isfile(full_path): toast("Not a valid file!", True) return if not os.access(full_path, (os.R_OK | os.W_OK)): toast("No permission, please move file", True) return if not full_path.endswith('.json'): toast("Not a JSON file!", True) return with open(full_path) as f: path_list = str(f).split("'") true_path = path_list[1] if not os.path.exists(true_path): toast("Not a valid path!", True) return true_path def do_add_checklist_location(self): """ Creates the subdirectory for the checklist. """ path = self.do_load_true_path(self.filewidget.path, self.filewidget.filename) if not isinstance(path, str): return with open(path) as p: self.data = json.load(p) title = self.data.get('title', DEFAULT_CHECKLIST_NAME) d = os.path.join('/sdcard/operator/checklists/', title) if not os.path.exists(d): os.makedirs(d) open(os.path.join(d, title + '_template.json'), 'a') shutil.copyfile(path, os.path.join(d, title + '_template.json')) self.do_get_checklists() self.file_select_popup.dismiss() def do_open_checklist(self, title, event): """ Opens the checklist, depending on the request from the user in the base menu. :param str title: The title of the checklist, according to the title field in the .JSON file. """ toast("Loading...", True) self.clear_widgets() self.title = title json_path = self.get_recent_json(title) if json_path: self.gen_checklist(json_path) def get_recent_json(self, title): """ Load the most recent .JSON file in the subdirectory. :param str title: The title of the checklist, according to the title field in the .JSON file. """ newest = max(glob.iglob( os.path.join('/sdcard/operator/checklists', title, '*.[Jj][Ss][Oo][Nn]')), key=os.path.getctime) return newest def on_back_btn(self, window, key, *args): """ Saves when back button is called. """ if key == 27: self.submit_button.trigger_action(duration=0) def _get_checklist_widget_check(self, question, i): multiple_responses = BoxLayout(orientation='vertical', id='Response ' + i, size_hint_y=None, height=(len(question.answer) * 70), spacing=20) for key, value in question.answer.items(): response = BoxLayout(orientation='horizontal', id='sub Response ' + i) response.add_widget(Label(text=key, id=key)) if key.lower().startswith('other'): widget = TextInput(id=key, size_hint_x=.8) if isinstance(value, (str, unicode)): widget.text = value else: widget = CheckBox(id=key) widget.active = value response.add_widget(widget) multiple_responses.add_widget(response) return multiple_responses def _get_checklist_widget_response_check(self, widget): results = {} for child in widget.walk(restrict=True): if isinstance(child, TextInput): value = child.text elif isinstance(child, CheckBox): value = child.active else: continue results[child.id] = value return results def _get_checklist_widget_date(self, question, i): date_widget = BoxLayout(orientation='horizontal', id='Response ' + i, size_hint_y=None) month_spinner = Spinner(id='Month Response ' + i, text=question.answer[0], values=MONTHS) day_spinner = Spinner( id='Day Response ' + i, text=question.answer[1], values=["{0:02}".format(j) for j in range(1, 32)]) year_spinner = Spinner( id='Year Response ' + i, text=question.answer[2], values=["{0:04}".format(j) for j in range(2100, 1900, -1)]) date_widget.add_widget(month_spinner) date_widget.add_widget(day_spinner) date_widget.add_widget(year_spinner) return date_widget def _get_checklist_widget_response_date(self, widget): for child in widget.children: if 'Month' in child.id: month = child.text elif 'Day' in child.id: day = child.text elif 'Year' in child.id: year = child.text return (month, day, year) def _get_checklist_widget_num(self, question, i): float_widget = FloatInput(hint_text=question.question, id='Response ' + i, size_hint_y=None) float_widget.text = str(question.answer) return float_widget def _get_checklist_widget_response_num(self, widget): return float(widget.text) def _get_checklist_widget_text(self, question, i): text_widget = TextInput(hint_text=question.question, id='Response ' + i, size_hint_y=None) text_widget.text = question.answer return text_widget def _get_checklist_widget_response_text(self, widget): return widget.text def _get_checklist_widget_yes_no(self, question, i): yes_no_widget = BoxLayout(orientation='vertical', id='Response ' + i, size_hint_y=None) yes_response = BoxLayout(orientation='horizontal', id='sub Response' + i) yes_response.add_widget(Label(text='Yes', id='Response ' + i)) yes_checkbox = CheckBox(id='Yes Response ' + i, group='yes_no' + i) yes_response.add_widget(yes_checkbox) no_response = BoxLayout(orientation='horizontal', id='sub Response' + i) no_response.add_widget(Label(text='No', id='Response ' + i)) no_checkbox = CheckBox(id='No Response ' + i, group='yes_no' + i) no_response.add_widget(no_checkbox) if question.answer: yes_checkbox.active = True else: no_checkbox.active = True yes_no_widget.add_widget(yes_response) yes_no_widget.add_widget(no_response) return yes_no_widget def _get_checklist_widget_response_yes_no(self, widget): for child in widget.children: for child2 in child.children: if isinstance(child2, CheckBox): if 'Yes' in child2.id: return bool(child2.active) else: return not bool(child2.active) def gen_checklist(self, json_path): """ Generates the actual layout, based on the parsed .JSON :param str json_path: The path to the JSON checklist file to load. """ self.checklist_layout.clear_widgets() with open(json_path) as file_h: self.data = json.load(file_h) self.question_list = self.decode_json(self.data) self.response_list = [] for i, question in enumerate(self.question_list): i = str(i) widget_handler = getattr(self, '_get_checklist_widget_' + question.type, None) if widget_handler: # Adds a Label for each question response = ChecklistResponse( question=question, label_widget=Label(text=question.question, id='Question ' + i, size_hint_y=None), response_widget=widget_handler(question, i)) self.checklist_layout.add_widget(response.label_widget) self.checklist_layout.add_widget(response.response_widget) self.response_list.append(response) clear_and_delete = BoxLayout(orientation='horizontal', id='clear_and_delete', size_hint_y=None) clear_button = Button(text='Clear', id='clear') delete_button = Button(text='Delete', id='delete') clear_button.bind(on_release=lambda x: self.do_confirm_action('clear')) delete_button.bind( on_release=lambda x: self.do_confirm_action('delete')) clear_and_delete.add_widget(clear_button) clear_and_delete.add_widget(delete_button) self.checklist_layout.add_widget(clear_and_delete) self.submit_button = Button(text='Save', id='submit', size_hint_y=None) self.submit_button.bind(on_release=self.do_store_data) self.checklist_layout.add_widget(self.submit_button) self.clear_widgets() self.set_background(self.checklist_layout) self.add_widget(self.checklist_layout) def do_store_data(self, event): """ Reads each response and writes a .JSON file with questions and responses. """ store = [] answer = None for response in self.response_list: question = response.question widget = response.response_widget widget_handler = getattr( self, '_get_checklist_widget_response_' + question.type, None) if widget_handler: answer = widget_handler(widget) store.append( ChecklistQuestion(question=question.question, type=question.type, answer=answer)._asdict()) json_filename = time.strftime('%H:%M_%m-%d-%Y') + '.json' file_location = os.path.join('/sdcard/operator/checklists', self.title) if not os.path.exists(file_location): os.makedirs(file_location) store = dict(title=self.title, questions=store) with open(os.path.join(file_location, json_filename), 'w') as file_h: json.dump(store, file_h, sort_keys=True, indent=2, separators=(',', ': ')) self.clear_widgets() self.do_get_checklists() def decode_json(self, json_data): """ Parse through the .JSON input :param json_data: JSON file. :rtype: list :return: An list of three arrays containing strings """ questions = [] for question in json_data.get('questions', []): if 'question' not in question: continue q_type = question.get('type', 'text') q_answer = question.get('answer') if q_answer is None: q_answer = { 'check': {}, 'date': ('January', '01', '2015'), 'num': 0, 'text': '', 'yes_no': False }[q_type] if isinstance(q_answer, dict) and len(q_answer) == 0: for opt in question.get('options', []): q_answer[opt] = False questions.append( ChecklistQuestion(question=question['question'], type=q_type, answer=q_answer)) return questions def do_confirm_action(self, method): """ This function will generate a popup that prompts the user to confirm their decision. :param str method: String to indicate the type of action. :rtype: bool :return: Returns a boolean. If true, the action is confirmed. """ confirmation_box = BoxLayout(orientation='vertical') confirmation_box.add_widget( Label(text='Are you sure?\nThis action is permanent.')) affirm_and_neg = BoxLayout(orientation='horizontal', spacing=50) affirmative = Button(text='Yes') negative = Button(text='Cancel') if method == "clear": affirmative.bind( on_release=lambda x: self.do_set_response(True, "clear")) negative.bind( on_release=lambda x: self.do_set_response(False, "clear")) else: affirmative.bind( on_release=lambda x: self.do_set_response(True, "delete")) negative.bind( on_release=lambda x: self.do_set_response(False, "delete")) affirm_and_neg.add_widget(affirmative) affirm_and_neg.add_widget(negative) confirmation_box.add_widget(affirm_and_neg) self.confirmation_popup = Popup(title='Confirmation', content=confirmation_box, size_hint=(.7, None), size=(650, 500), auto_dismiss=False) self.confirmation_popup.open() def do_set_response(self, response, method): """ Calls the appropriate method after being confirmed. If not confirmed, the popup is dismissed with no action taken. :param bool response: Boolean to confirm response. :param str method: String to confirm the type of action. """ if method == "clear" and response: self.do_clear_data() elif method == "delete" and response: self.do_delete_data() self.confirmation_popup.dismiss() def do_clear_data(self): """ Clears all responses of the current checklist. This does not delete the checklist. """ for child in self.checklist_layout.walk(): if isinstance(child, (TextInput, FloatInput)): child.text = '' elif isinstance(child, Spinner): if 'day' in child.id: child.text = '01' elif 'month' in child.id: child.text = 'January' elif 'year' in child.id: child.text = '2015' elif isinstance(child, CheckBox): child.active = False def do_delete_data(self): """ Deletes the desired checklist (the directory). """ shutil.rmtree('/sdcard/operator/checklists/' + self.title) self.clear_widgets() self.do_get_checklists()