Răsfoiți Sursa

Merge branch '141-hierarchy-level-names' into 'master'

Resolve "hierarchy level names"

Closes #143 and #141

See merge request troebs/pycs!128
Eric Tröbs 3 ani în urmă
părinte
comite
9a96a7d097

+ 14 - 9
labels/lepiforum_version_7/Provider.py

@@ -32,20 +32,25 @@ class Provider(LabelProvider):
                              entries)
 
         # create result set
+        if self.csv_all_hierarchy_levels:
+            hierarchy_levels = (('superfamily', 'Überfamilie'),
+                                ('family', 'Familie'),
+                                ('subfamily', 'Unterfamilie'),
+                                ('tribe', 'Tribus'),
+                                ('genus', 'Gattung'))
+        else:
+            hierarchy_levels = (('family', 'Familie'),
+                                ('genus', 'Gattung'))
+
         for entry in entries:
             entry = entry.__dict__
             parent_reference = None
 
             # add hierarchy
-            if self.csv_all_hierarchy_levels:
-                hierarchy_levels = ('superfamily', 'family', 'subfamily', 'tribe', 'genus')
-            else:
-                hierarchy_levels = ('family', 'genus')
-
-            for tax in hierarchy_levels:
-                if entry[tax] is not None:
-                    reference, name = entry[tax].lower(), entry[tax]
-                    result.append(self.create_label(reference, name, parent_reference))
+            for level, level_name in hierarchy_levels:
+                if entry[level] is not None:
+                    reference, name = entry[level].lower(), entry[level]
+                    result.append(self.create_label(reference, name, parent_reference, level_name))
 
                     parent_reference = reference
 

+ 7 - 6
pycs/database/Database.py

@@ -74,12 +74,13 @@ class Database:
                     ''')
                     cursor.execute('''
                         CREATE TABLE IF NOT EXISTS labels (
-                            id        INTEGER PRIMARY KEY,
-                            project   INTEGER             NOT NULL,
-                            parent    INTEGER,
-                            created   INTEGER             NOT NULL,
-                            reference TEXT,
-                            name      TEXT                NOT NULL,
+                            id              INTEGER PRIMARY KEY,
+                            project         INTEGER             NOT NULL,
+                            parent          INTEGER,
+                            created         INTEGER             NOT NULL,
+                            reference       TEXT,
+                            name            TEXT                NOT NULL,
+                            hierarchy_level TEXT,
                             FOREIGN KEY (project) REFERENCES projects(id)
                                 ON UPDATE CASCADE ON DELETE CASCADE,
                             FOREIGN KEY (parent) REFERENCES labels(id)

+ 1 - 0
pycs/database/Label.py

@@ -15,6 +15,7 @@ class Label:
         self.created = row[3]
         self.reference = row[4]
         self.name = row[5]
+        self.hierarchy_level = row[6]
 
     def project(self):
         """

+ 42 - 5
pycs/database/Project.py

@@ -8,6 +8,7 @@ from pycs.database.File import File
 from pycs.database.Label import Label
 from pycs.database.LabelProvider import LabelProvider
 from pycs.database.Model import Model
+from pycs.database.util.TreeNodeLabel import TreeNodeLabel
 
 
 class Project:
@@ -60,6 +61,39 @@ class Project:
                 cursor.fetchall()
             ))
 
+    def label_tree(self) -> List[TreeNodeLabel]:
+        """
+        get a list of root labels associated with this project
+
+        :return: list of labels
+        """
+        with closing(self.database.con.cursor()) as cursor:
+            cursor.execute('''
+                WITH RECURSIVE
+                    tree AS (
+                        SELECT labels.* FROM labels
+                            WHERE project = ? AND parent IS NULL
+                        UNION ALL
+                        SELECT labels.* FROM labels
+                            JOIN tree ON labels.parent = tree.id
+                    )
+                SELECT * FROM tree
+            ''', [self.identifier])
+
+            result = []
+            lookup = {}
+
+            for row in cursor.fetchall():
+                label = TreeNodeLabel(self.database, row)
+                lookup[label.identifier] = label
+
+                if label.parent_id is None:
+                    result.append(label)
+                else:
+                    lookup[label.parent_id].children.append(label)
+
+            return result
+
     def label(self, identifier: int) -> Optional[Label]:
         """
         get a label using its unique identifier
@@ -95,7 +129,8 @@ class Project:
             return None
 
     def create_label(self, name: str, reference: str = None,
-                     parent: Union[Label, int, str] = None) -> Tuple[Optional[Label], bool]:
+                     parent: Union[Label, int, str] = None,
+                     hierarchy_level: str = None) -> Tuple[Optional[Label], bool]:
         """
         create a label for this project. If there is already a label with the same reference
         in the database its name is updated.
@@ -103,6 +138,7 @@ class Project:
         :param name: label name
         :param reference: label reference
         :param parent: either parent identifier, parent reference string or `Label` object
+        :param hierarchy_level: hierarchy level name
         :return: created or edited label, insert
         """
         created = int(time())
@@ -114,11 +150,12 @@ class Project:
 
         with closing(self.database.con.cursor()) as cursor:
             cursor.execute('''
-                INSERT INTO labels (project, parent, created, reference, name)
-                VALUES (?, ?, ?, ?, ?)
+                INSERT INTO labels (project, parent, created, reference, name, hierarchy_level)
+                VALUES (?, ?, ?, ?, ?, ?)
                 ON CONFLICT (project, reference) DO
-                UPDATE SET parent = ?, name = ?
-            ''', (self.identifier, parent, created, reference, name, parent, name))
+                UPDATE SET parent = ?, name = ?, hierarchy_level = ?
+            ''', (self.identifier, parent, created, reference, name, hierarchy_level,
+                  parent, name, hierarchy_level))
 
             # lastrowid is 0 if on conflict clause applies.
             # If this is the case we do an extra query to receive the row id.

+ 7 - 0
pycs/database/util/TreeNodeLabel.py

@@ -0,0 +1,7 @@
+from pycs.database.Label import Label
+
+
+class TreeNodeLabel(Label):
+    def __init__(self, database, row):
+        super().__init__(database, row)
+        self.children = []

+ 5 - 0
pycs/frontend/WebServer.py

@@ -21,6 +21,7 @@ from pycs.frontend.endpoints.jobs.RemoveJob import RemoveJob
 from pycs.frontend.endpoints.labels.CreateLabel import CreateLabel
 from pycs.frontend.endpoints.labels.EditLabelName import EditLabelName
 from pycs.frontend.endpoints.labels.EditLabelParent import EditLabelParent
+from pycs.frontend.endpoints.labels.ListLabelTree import ListLabelTree
 from pycs.frontend.endpoints.labels.ListLabels import ListLabels
 from pycs.frontend.endpoints.labels.RemoveLabel import RemoveLabel
 from pycs.frontend.endpoints.pipelines.FitModel import FitModel
@@ -151,6 +152,10 @@ class WebServer:
             '/projects/<int:identifier>/labels',
             view_func=ListLabels.as_view('list_labels', database)
         )
+        self.__flask.add_url_rule(
+            '/projects/<int:identifier>/labels/tree',
+            view_func=ListLabelTree.as_view('list_label_tree', database)
+        )
         self.__flask.add_url_rule(
             '/projects/<int:identifier>/labels',
             view_func=CreateLabel.as_view('create_label', database, notifications)

+ 1 - 1
pycs/frontend/endpoints/labels/CreateLabel.py

@@ -35,7 +35,7 @@ class CreateLabel(View):
         # start transaction
         with self.db:
             # insert label
-            label, _ = project.create_label(name, parent_id=parent)
+            label, _ = project.create_label(name, parent=parent)
 
         # send notification
         self.nm.create_label(label)

+ 28 - 0
pycs/frontend/endpoints/labels/ListLabelTree.py

@@ -0,0 +1,28 @@
+from flask import abort, jsonify
+from flask.views import View
+
+from pycs.database.Database import Database
+
+
+class ListLabelTree(View):
+    """
+    return a list of labels for a given project
+    """
+    # pylint: disable=arguments-differ
+    methods = ['GET']
+
+    def __init__(self, db: Database):
+        # pylint: disable=invalid-name
+        self.db = db
+
+    def dispatch_request(self, identifier):
+        # find project
+        project = self.db.project(identifier)
+        if project is None:
+            abort(404)
+
+        # get labels
+        labels = project.label_tree()
+
+        # return labels
+        return jsonify(labels)

+ 3 - 2
pycs/frontend/endpoints/projects/ExecuteLabelProvider.py

@@ -75,8 +75,9 @@ class ExecuteLabelProvider(View):
         def result(provided_labels):
             with db:
                 for label in provided_labels:
-                    created_label, insert = project.create_label(label['name'], label['reference'],
-                                                                 label['parent'])
+                    created_label, insert = project.create_label(
+                        label['name'], label['reference'], label['parent'], label['hierarchy_level']
+                    )
 
                     if insert:
                         nm.create_label(created_label)

+ 5 - 2
pycs/interfaces/LabelProvider.py

@@ -32,17 +32,20 @@ class LabelProvider:
         raise NotImplementedError
 
     @staticmethod
-    def create_label(reference: str, name: str, parent_reference=None):
+    def create_label(reference: str, name: str,
+                     parent_reference: str = None, hierarchy_level: str = None):
         """
         create a label result
 
         :param reference: label reference string
         :param name: label name
         :param parent_reference: parent's reference string
+        :param hierarchy_level: hierarchy level name
         :return:
         """
         return {
             'reference': reference,
             'name': name,
-            'parent': parent_reference
+            'parent': parent_reference,
+            'hierarchy_level': hierarchy_level
         }

+ 31 - 2
webui/src/components/media/label-selector.vue

@@ -8,7 +8,12 @@
            class="label selectable"
            :class="{selected: index === selectedIndex}"
            @click="select(label)">
-        {{ label.name }}
+        <div class="hierarchy" v-if="label.hierarchy_level">
+          {{ label.hierarchy_level }}
+        </div>
+        <div class="name">
+          {{ label.name }}
+        </div>
       </div>
     </div>
 
@@ -45,13 +50,32 @@ export default {
     }
   },
   computed: {
+    sortedLabels: function () {
+      return [...this.labels].sort((a, b) => {
+        if (a.hierarchy_level !== b.hierarchy_level) {
+          if (a.hierarchy_level === null)
+            return -1;
+          if (b.hierarchy_level === null)
+            return +1;
+          if (a.hierarchy_level < b.hierarchy_level)
+            return -1;
+          else
+            return +1;
+        }
+
+        if (a.name < b.name)
+          return -1;
+        else
+          return +1;
+      });
+    },
     results: function () {
       const search = this.search.toLowerCase();
 
       return [{
         identifier: null,
         name: 'None'
-      }, ...this.labels]
+      }, ...this.sortedLabels]
           .filter(l => !this.search || !l.identifier || l.name.toLowerCase().includes(search));
     }
   },
@@ -129,4 +153,9 @@ input {
   background-color: var(--primary);
   padding: 0.25rem 0.5rem;
 }
+
+.hierarchy {
+  font-size: 77%;
+  opacity: 0.8;
+}
 </style>

+ 1 - 1
webui/src/components/media/options-bar.vue

@@ -295,7 +295,7 @@ img {
   left: 50%;
   z-index: 101;
 
-  width: 20rem;
+  width: 30rem;
   max-width: 90vw;
   max-height: 90vh;
 

+ 37 - 9
webui/src/components/other/LabelTreeView.vue

@@ -7,18 +7,26 @@
     <editable-headline :value="label.name"
                        @change="editLabel(label.identifier, $event)"
                        @remove="removeLabel(label.identifier)">
-      <img v-if="collapse"
-           src="@/assets/icons/triangle-down.svg"
-           :style="{opacity: label.children.length > 0 ? 1 : 0.1}"
-           @click="collapse = false">
-      <img v-else
-           src="@/assets/icons/triangle-up.svg"
-           :style="{opacity: label.children.length > 0 ? 1 : 0.1}"
-           @click="collapse = true">
+      <div class="hierarchy"
+           :class="{margined: label.hierarchy_level}"
+           @click="collapse = !collapse">
+        <img v-if="collapse"
+             alt="expand"
+             src="@/assets/icons/triangle-down.svg"
+             :style="{opacity: label.children.length > 0 ? 1 : 0.1}">
+        <img v-else
+             alt="collapse"
+             src="@/assets/icons/triangle-up.svg"
+             :style="{opacity: label.children.length > 0 ? 1 : 0.1}">
+
+        <div class="name" v-if="label.hierarchy_level">
+          {{ label.hierarchy_level }}
+        </div>
+      </div>
     </editable-headline>
 
     <template v-if="!collapse">
-      <label-tree-view v-for="child of label.children" :key="child.identifier"
+      <label-tree-view v-for="child of sortedChildren" :key="child.identifier"
                        :label="child"
                        :indent="indent"
                        :targetable="droppable"
@@ -49,6 +57,9 @@ export default {
       return {
         marginLeft: this.indent
       };
+    },
+    sortedChildren: function () {
+      return [...this.label.children].sort((a, b) => a.name < b.name ? -1 : +1);
     }
   },
   methods: {
@@ -100,4 +111,21 @@ export default {
 .target > .editable-headline {
   text-decoration: underline;
 }
+
+.hierarchy {
+  display: flex;
+  flex-direction: column;
+  justify-content: right;
+  align-items: center;
+}
+
+.hierarchy.margined {
+  margin-right: 0.3rem;
+}
+
+.hierarchy .name {
+  margin-top: -0.5rem;
+  opacity: 0.8;
+  font-size: 50%;
+}
 </style>

+ 72 - 80
webui/src/components/projects/project-labels-window.vue

@@ -7,7 +7,7 @@
     </h1>
 
     <template v-if="labels !== false">
-      <label-tree-view v-for="label in labelTree" :key="label.identifier"
+      <label-tree-view v-for="label in sortedLabels" :key="label.identifier"
                        :label="label"
                        :targetable="true"
                        indent="2rem"/>
@@ -45,15 +45,15 @@ export default {
 
     // subscribe to changes
     this.$root.socket.on('connect', this.getLabels);
-    this.$root.socket.on('create-label', this.addLabelToList);
-    this.$root.socket.on('remove-label', this.removeLabelFromList);
-    this.$root.socket.on('edit-label', this.editLabelInList);
+    this.$root.socket.on('create-label', this.addLabel);
+    this.$root.socket.on('remove-label', this.removeLabel);
+    this.$root.socket.on('edit-label', this.editLabel);
   },
   destroyed: function () {
     this.$root.socket.off('connect', this.getLabels);
-    this.$root.socket.off('create-label', this.addLabelToList);
-    this.$root.socket.off('remove-label', this.removeLabelFromList);
-    this.$root.socket.off('edit-label', this.editLabelInList);
+    this.$root.socket.off('create-label', this.addLabel);
+    this.$root.socket.off('remove-label', this.removeLabel);
+    this.$root.socket.off('edit-label', this.editLabel);
   },
   data: function () {
     return {
@@ -63,85 +63,95 @@ export default {
     }
   },
   computed: {
-    // TODO remove
     sortedLabels: function () {
       return [...this.labels].sort((a, b) => a.name < b.name ? -1 : +1);
-    },
-    labelTree: function () {
-      const labels = [...this.labels].sort((a, b) => a.name < b.name ? +1 : -1);
-
-      const tree = [];
-      const references = {};
-
-      while (labels.length > 0) {
-        const length = labels.length;
-
-        for (let i = labels.length - 1; i >= 0; i--) {
-          if (labels[i]['parent_id'] === null) {
-            const label = Object.assign({
-              // parent: null,
-              children: []
-            }, labels.splice(i, 1)[0]);
-
-            tree.push(label);
-            references[label.identifier] = label;
-          } else if (labels[i]['parent_id'] in references) {
-            const parent = references[labels[i]['parent_id']];
-            const label = Object.assign({
-              // parent: parent,
-              children: []
-            }, labels.splice(i, 1)[0]);
-
-            parent.children.push(label);
-            references[label.identifier] = label;
-          }
-        }
-
-        if (labels.length === length) {
-          // TODO show in ui
-          console.log('I could not parse all items. Sorry!')
-          return tree;
-        }
-      }
-
-      return tree;
     }
   },
   methods: {
     getLabels: function () {
-      this.$root.socket.get(`/projects/${this.$root.project.identifier}/labels`)
+      this.$root.socket.get(`/projects/${this.$root.project.identifier}/labels/tree`)
           .then(response => response.json())
           .then(labels => {
-            this.labels = [];
-            labels.forEach(this.addLabelToList);
+            this.labels = labels;
           });
     },
-    addLabelToList: function (label) {
+    findLabel: function (labels, identifier) {
+      for (let label of labels) {
+        if (label.identifier === identifier)
+          return label;
+
+        const child = this.findLabel(label.children, identifier);
+        if (child)
+          return child;
+      }
+
+      return null;
+    },
+    addLabel: function (label) {
+      // abort if label is not part of this project
       if (label['project_id'] !== this.$root.project.identifier)
         return;
 
-      for (let l of this.labels)
+      // prepare some values
+      let labels = this.labels;
+
+      if (!label.children)
+        label.children = [];
+
+      // label is not on root level
+      if (label.parent_id !== null) {
+        const parent = this.findLabel(labels, label.parent_id);
+
+        if (parent) {
+          labels = parent.children;
+        } else {
+          console.warn('could not find parent');
+          return;
+        }
+      }
+
+      // abort if label is already contained
+      for (let l of labels)
         if (l.identifier === label.identifier)
           return;
 
-      this.labels.push(label);
+      // add label
+      labels.push(label);
     },
-    removeLabelFromList: function (label) {
-      for (let i = 0; i < this.labels.length; i++) {
-        if (this.labels[i].identifier === label.identifier) {
-          this.labels.splice(i, 1);
+    removeLabel: function (label) {
+      let labels = this.labels;
+
+      // label is not on root level
+      if (label.parent_id !== null) {
+        const parent = this.findLabel(labels, label.parent_id);
+
+        if (parent) {
+          labels = parent.children;
+        } else {
+          console.warn('could not find parent');
           return;
         }
       }
-    },
-    editLabelInList: function (label) {
-      for (let i = 0; i < this.labels.length; i++) {
-        if (this.labels[i].identifier === label.identifier) {
-          this.$set(this.labels, i, label);
+
+      // remove label from list
+      for (let i = 0; i < labels.length; i++) {
+        if (labels[i].identifier === label.identifier) {
+          labels.splice(i, 1);
           return;
         }
       }
     },
+    editLabel: function (label) {
+      // remove label if it is already contained
+      const l = this.findLabel(this.labels, label.identifier);
+      if (l) {
+        this.removeLabel(l);
+        label.children = l.children;
+      }
+
+      // add label on it's new position
+      this.addLabel(label);
+    },
     createLabel: function () {
       if (!this.createLabelValue)
         return;
@@ -181,22 +191,4 @@ h1.target {
   width: 4rem;
   height: 4rem;
 }
-
-/*
-/deep/ .element {
-  border: none;
-}
-
-/deep/ .element:not(:first-child) {
-  margin-left: 0.1rem;
-}
-
-/deep/ .element:not(:last-child) {
-  margin-right: 0.1rem;
-}
-
-.no-elements {
-  margin-top: 0.5rem;
-}
-*/
 </style>