Example #1
0
class CWR(QtGui.QWidget):
    
    def __init__(self):
        self._dm = DataModel()              # Primary data model.

        self._topics    = []                # All topics for which judgments have been loaded.
        self._documents = []                # Documents for which we have a judgment for the
                                            # currently selected topic.

        self._selected_topic    = None      # Currently selected topic.
        self._selected_document = None      # Currently selected document.
        self._rationales        = []        # Rationales for the currently selected document.

        self._display_text = None           # Text of document being manipulated.

        super(CWR, self).__init__()

        self.init_UI()

        # For testing WebView.
        ''' 
        test_url = 'https://en.wikipedia.org/wiki/The_Beatles'
        content  = requests.get(test_url).text        
        content
        test_text = BeautifulSoup(content, "html.parser").get_text()
        self._display_text = test_text
        '''
        
    def init_UI(self):        
        grid = QtGui.QGridLayout()
        grid.setSpacing(10)
            
        #####################################
        # Summary of UI Elements            #
        #####################################
        # These are UI elements that are updated after creation.
        self._confusion_matrix = None       # String form of confusion matrix for current view.


        #####################################
        # Topic View                        #
        #####################################
        # Contains Topic List.
        topic_view = QtGui.QGroupBox("Topics")
        topic_layout = QtGui.QVBoxLayout()
        topic_view.setLayout(topic_layout)
        grid.addWidget(topic_view, 0, 0)

        # Topic List
        self._topic_list = QtGui.QListWidget()
        self._topic_list.setSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum)
        self._topic_list.itemClicked.connect(self._topic_selected)
        topic_layout.addWidget(self._topic_list)
        

        #####################################
        # Document View                     #
        #####################################
        # Contains Document List.
        document_view = QtGui.QGroupBox("Documents")
        document_layout = QtGui.QVBoxLayout()
        document_view.setLayout(document_layout)
        grid.addWidget(document_view, 0, 1)

        # Document List
        self._document_list = QtGui.QListWidget()
        self._document_list.setSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
        self._document_list.itemClicked.connect(self._document_selected)
        document_layout.addWidget(self._document_list)


        #####################################
        # Statistics View                   #
        #####################################
        # Below document view. Contains the confusion matrix, list of
        # rationales, gold standard values, and user judgments.
        #stat_label = QtGui.QLabel()
        #stat_label.setText("<B>Statistics</B>")
        #stat_layout        = QtGui.QVBoxLayout()
        #document_layout.addWidget(stat_label)
        #self._stat_display.setLayout(stat_layout)
        #self._stat_display.setSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum)

        # Topic Display
        topic = QtGui.QLabel("<b>Topic</b>: N/A")
        document_layout.addWidget(topic)
        narrative_label = QtGui.QLabel("<b>Narrative</b:")
        document_layout.addWidget(narrative_label)
        narrative_view = QtGui.QTextEdit()
        document_layout.addWidget(narrative_view)

        # Confusion Matrix for current Topic or Topic-Document Pair
        confusion_matrix_label = QtGui.QLabel()
        confusion_matrix_label.setText("Confusion Matrix")
        document_layout.addWidget(confusion_matrix_label)
        confusion_matrix       = QtGui.QLabel()
        confusion_matrix.setText("  -    -    -    -   \n"
                                 "  -    -    -    -   \n"
                                 "  -    -    -    -   \n")
        document_layout.addWidget(confusion_matrix)

        # Gold Standard for current Topic-Document.
        gold_standard_view = QtGui.QLabel()
        gold_standard_view.setText("Gold Standard: N/A")
        document_layout.addWidget(gold_standard_view)

        # Degree 1 Agreement
        d1_agreement_view = QtGui.QLabel()
        d1_agreement_view.setText("D1 Agreement: N/A")
        document_layout.addWidget(d1_agreement_view)

        # Degree 2 Agreement
        d2_agreement_view = QtGui.QLabel()
        d1_agreement_view.setText("D2 Agreement: N/A")
        document_layout.addWidget(d2_agreement_view)

        # Give pointers to updateable elements.
        self._topic_view = topic
        self._narrative_view = narrative_view
        self._confusion_matrix   = confusion_matrix
        self._gold_standard_view = gold_standard_view
        self._d1_agreement_view  = d1_agreement_view
        self._d2_agreement_view  = d2_agreement_view

   
        #####################################
        # Rationale View                    #
        #####################################
        # Contains the rationale check boxes, text display, and statistics.
        rationale_view = QtGui.QGridLayout()
        grid.addLayout(rationale_view, 0, 2)


        # Rationale Display
        display = HighlightWebView()
        display.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
        display.load(QUrl('https://en.wikipedia.org/wiki/The_Beatles'))
        display.show()
        self._rationale_display = display
        rationale_view.addWidget(self._rationale_display, 2, 0)
        
        # Rationale Selection
        selection_view = QtGui.QGroupBox()
        self._selection_layout = QtGui.QHBoxLayout()
        selection_view.setLayout(self._selection_layout)
        rationale_view.addWidget(selection_view, 1, 0)


        #####################################
        # Worker View                       #
        #####################################
        # Below rationale view. Contains pure text of worker rationales,
        # judgments, and the gold standard.
        self._worker_display = QtGui.QGroupBox("Workers")
        worker_layout        = QtGui.QVBoxLayout()
        grid.addLayout(worker_layout, 0, 3)
        #self._worker_display.setLayout(worker_layout)


        # Worker Rationales for current Topic-Document.
        worker_rationale_list_label = QtGui.QLabel()
        worker_rationale_list_label.setText("Worker Rationales")
        worker_layout.addWidget(worker_rationale_list_label)
        worker_rationale_list = QtGui.QTextEdit()
        worker_rationale_list.setText("Initial Text.")
        worker_layout.addWidget(worker_rationale_list)

        # Worker Judgments for current Topic-Document.
        worker_judgment_list_label = QtGui.QLabel()
        worker_judgment_list_label.setText("Worker Judgments")
        worker_layout.addWidget(worker_judgment_list_label)
        worker_judgment_list = QtGui.QTextEdit()
        worker_judgment_list.setText("Initial Text.")
        worker_layout.addWidget(worker_judgment_list)

        # Give pointers to updateable elements.
        self._worker_judgments   = worker_judgment_list
        self._worker_rationales  = worker_rationale_list
        

        #####################################
        # Main Application Properties       #
        #####################################
        self.setLayout(grid)
        self.setGeometry(300, 300, 1000, 1000)
        self.setWindowTitle("The Crowdworker's Rationale")
        self.show()
        
    def load(self, directory):
        '''
        Loads rationale data from the specified file.
        '''
        dm = self._dm
        for dirpath, _, filenames in os.walk(directory):
            for f in filenames:
                if f.endswith(".csv"): 
                    absolute_path = os.path.abspath(os.path.join(dirpath, f))
                    dm.load(absolute_path)
        self.update_topic_list(dm.judged_topics())
        self.update_document_list([])

#########################################################################################
# Updateable UI Elements                                                                #
#########################################################################################

    def update_topic_list(self, topics):
        '''
        Updates the topics displayed in Topic View.
        '''
        self._topics = topics
        self._topic_list.clear()
        self._topic_list.addItems([t.id for t in topics])
        self._topic_list.sortItems()   
        
    def update_document_list(self, documents):
        '''
        Updates the documents displayed in Document View.
        '''
        self._document_list.clear()
        self._document_list.addItems(documents)
        self._document_list.sortItems() 

    def update_rationale_selection(self, rationales):
        '''
        Regenerates data structures and display logic for rationale selection.
        '''
        # Compact holder for all rationale logic.
        container = namedtuple('RationaleContainer', ['rationale', 'color', 'display'])

        # Friendly display colors used to highlight rationales in source texts.
        colors = [QtGui.QColor(102, 255, 102), # Light Green
                  QtGui.QColor(255, 102, 102), # Light Red 
                  QtGui.QColor(102, 201, 255), # Electric Blue
                  QtGui.QColor(102, 178, 255), # Baby Blue
                  QtGui.QColor(178, 102, 255), # Light Purple
                  QtGui.QColor(255, 205, 255), # Light Magenta
                  QtGui.QColor(255, 102, 178), # Light Pink
                  QtGui.QColor(192, 192, 192)] # Light Grey

        # Remove old rationale widgets.
        layout = self._selection_layout
        for i in reversed(range(layout.count())): 
            layout.itemAt(i).widget().setParent(None)

        # Re-generate rationale data structures.
        self._rationales = []
        for i in range(len(rationales)):
            r = rationales[i]
            c = colors[min(i, len(colors) - 1)]
            d = QCheckBox(r.label)
            d.stateChanged.connect(self._rationale_selection_changed)
            self._rationales.append(container(rationale = r, color = c, display = d))
            self._selection_layout.addWidget(d)

    def update_rationale_display(self):
        '''
        Recomputes rationale overlap and updates display. Expensive.
        '''
        print ("Updating rationale display...")
        
        # Disable rationale selection while updating.
        for this_rationale in self._rationales:
            this_rationale.display.setEnabled(False)
        
        # Locate selected rationales.
        selected = []
        for this_rationale in self._rationales:
            if this_rationale.display.isChecked():
                selected.extend([r for r in self._rationales if this_rationale.display.text() == r.rationale.label])
        print (selected)

        # Gather text.
        text = self._rationale_display.get_text()

        # Compute overlap.
        just_rationales = [r.rationale for r in selected]
        result = Rationale.compute_overlap(text, just_rationales)

        # Update display with rationale matches.
        display = self._rationale_display
        display.clear()
        
        # If more than one rationale is selected, highlight overlap.
        '''
        if result.overlap:
            for string in result.overlap:
                print ("Highlighted string: %s" % string)
                display.highlight(string)
        # Else, highlight single rationale
        elif result.matches:
            strings = [QtCore.QString(s) for s in itertools.chain(*[s for s in result.matches.values()])]
            for string in strings:
                print ("Highlighting string: %s" % string)
                display.highlight(string)
        '''

        for r in just_rationales:
            display.highlight(r.rationale.rationale)
            
        # This is code exclusively for a multi-color highlight interface.
        '''
        for rationale, matches in result.matches.items():
            # Map back to container to find correct color.
            color = [c.color for c in selected if c.rationale is rationale][0]
            
            # Highlight in display.
            for match in matches:
                display.highlight(match, color)

        overlap_color = QtGui.QColor(255, 255, 102) # Light Yellow
        for string in result.overlap:
            display.highlight(string, overlap_color)
        '''

        # Re-enable rationale selection.
        for this_rationale in self._rationales:
            this_rationale.display.setEnabled(True)

    def update_statistics(self, topic=None, document=None):
        self.update_gold_standard_view(topic, document)
        self.update_topic_view(topic)
        self.update_rationale_list()
        self.update_judgment_list()
        self.update_confusion_matrix(topic, document)
        self.update_agreement_view(topic, document)

    def update_confusion_matrix(self, topic=None, document=None):
        cm = self._dm.confusion_matrix(topic, document)
        self._confusion_matrix.setText(cm)

    def update_gold_standard_view(self, topic, document):
        '''
        Updates the current gold standard view.
        '''
        if topic and document:
            value = self._dm.gold_standard(topic, document)
            self._gold_standard_view.setText("Gold Standard: %s" % value)

    def update_topic_view(self, topic):
        '''
        Updates the current topic and rationale display.
        '''
        (topic, narrative) = self._dm.topic_information(topic)
        self._topic_view.setText("<b>Topic</b>: %s" % topic)
        self._narrative_view.setText("%s" % narrative)
        
    def update_agreement_view(self, topic, document):
        '''
        Computes and updates the agreement for currently selected Topic or Document.
        '''
        d1_agree = self._dm.agreement(1, topic, document)
        self._d1_agreement_view.setText("D1 Agreement: %f" % d1_agree)
        d2_agree = self._dm.agreement(2, topic, document)
        self._d2_agreement_view.setText("D2 Agreement: %f" % d2_agree)
        

    def update_rationale_list(self):
        '''
        Updates the worker rationale list. If one or more worker IDs is 
        selected, this will display only the rationales by the selected 
        workers. Otherwise, this will display all rationales for the 
        selected Topic and Document.
        '''
        display_text = ''
        selected = [r for r in self._rationales if r.display.isChecked()]
        # If none are selected, display all.
        selected = selected if selected else self._rationales
        for r in selected:
            display_text += ('%s\n\n%s\n\n' % (r.rationale.label, r.rationale.rationale.rationale))
        self._worker_rationales.setText(display_text)

    def update_judgment_list(self, topic=None, document=None):
        '''
        Updates the worker judgment list. If one or more worker IDs is
        selected, this will display only the judgments of those selected
        workers. Otherwise, this will display the judgments from all workers.
        '''
        display_text = ''
        selected = [r for r in self._rationales if r.display.isChecked()]
        # If none are selected, display all.
        selected = selected if selected else self._rationales
        for r in selected:
            display_text += ('%s: %s\n' % (r.rationale.label, r.rationale.rationale.value))
        self._worker_judgments.setText(display_text)
        
    def update_rationale_text(self, text):
        '''
        This updates the text in the rationale display.
        '''
        self._rationale_display.set_text(text)
    
    def highlight_rationale(self, text):
        self._rationale_display.highlight(text)

    def load_document(self, url):
        self._rationale_display.load(QUrl(url))
        

#########################################################################################
# Signals                                                                               #
#########################################################################################

    def _topic_selected(self, item):
        '''
        Handler function - user selects a topic in the Topic View.
        
        Refreshes the list of documents in the Document View with 
        all documents for which a worker judgment has been loaded
        for that topic. Computes statistics across that topic.
        '''
        topic_id = item.text()

        # Update control selection.
        self._selected_topic = topic_id

        # Update statistics view.
        self.update_statistics(topic=str(topic_id))

        print ("Loading documents for topic %s" % topic_id)
        documents = self._dm.judged_documents_by_topic(topic_id)
        self.update_document_list([d.id for d in documents])

    def _document_selected(self, item):
        '''
        Handler function - user select a document in the Document View.
        
        Loads the text from that document into the rationale display
        and computes statistics for that document.
        '''
        document_id       = item.text()
        
        # Update control selection.
        self._selected_document = document_id

        # Grab control selections.
        selected_topic    = self._selected_topic
        selected_document = self._selected_document

        print ("Loading rationales for document %s, topic %s" % (selected_document, selected_topic))
        rationales = self._dm.judgments(selected_topic, selected_document)
        rationales = [Rationale(str(random.randint(1,10000)), r) for r in rationales]
        self.update_rationale_selection(rationales)

        # Update statistics view.
        self.update_statistics(str(selected_topic), str(selected_document))

        # Load document.
        document = next((d for d in self._dm.judged_documents() if d.id == selected_document), None)
        self.load_document(document.url)
        
    def _rationale_selection_changed(self, state):
        '''
        Handler function called when a user selects or deselects a rationale
        check box.
        '''
        worker = Thread(target=self.update_rationale_display)
        worker.start()