Bläddra i källkod

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 år sedan
förälder
incheckning
9a96a7d097

+ 14 - 9
labels/lepiforum_version_7/Provider.py

@@ -32,20 +32,25 @@ class Provider(LabelProvider):
                              entries)
                              entries)
 
 
         # create result set
         # 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:
         for entry in entries:
             entry = entry.__dict__
             entry = entry.__dict__
             parent_reference = None
             parent_reference = None
 
 
             # add hierarchy
             # 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
                     parent_reference = reference
 
 

+ 7 - 6
pycs/database/Database.py

@@ -74,12 +74,13 @@ class Database:
                     ''')
                     ''')
                     cursor.execute('''
                     cursor.execute('''
                         CREATE TABLE IF NOT EXISTS labels (
                         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)
                             FOREIGN KEY (project) REFERENCES projects(id)
                                 ON UPDATE CASCADE ON DELETE CASCADE,
                                 ON UPDATE CASCADE ON DELETE CASCADE,
                             FOREIGN KEY (parent) REFERENCES labels(id)
                             FOREIGN KEY (parent) REFERENCES labels(id)

+ 1 - 0
pycs/database/Label.py

@@ -15,6 +15,7 @@ class Label:
         self.created = row[3]
         self.created = row[3]
         self.reference = row[4]
         self.reference = row[4]
         self.name = row[5]
         self.name = row[5]
+        self.hierarchy_level = row[6]
 
 
     def project(self):
     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.Label import Label
 from pycs.database.LabelProvider import LabelProvider
 from pycs.database.LabelProvider import LabelProvider
 from pycs.database.Model import Model
 from pycs.database.Model import Model
+from pycs.database.util.TreeNodeLabel import TreeNodeLabel
 
 
 
 
 class Project:
 class Project:
@@ -60,6 +61,39 @@ class Project:
                 cursor.fetchall()
                 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]:
     def label(self, identifier: int) -> Optional[Label]:
         """
         """
         get a label using its unique identifier
         get a label using its unique identifier
@@ -95,7 +129,8 @@ class Project:
             return None
             return None
 
 
     def create_label(self, name: str, reference: str = 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
         create a label for this project. If there is already a label with the same reference
         in the database its name is updated.
         in the database its name is updated.
@@ -103,6 +138,7 @@ class Project:
         :param name: label name
         :param name: label name
         :param reference: label reference
         :param reference: label reference
         :param parent: either parent identifier, parent reference string or `Label` object
         :param parent: either parent identifier, parent reference string or `Label` object
+        :param hierarchy_level: hierarchy level name
         :return: created or edited label, insert
         :return: created or edited label, insert
         """
         """
         created = int(time())
         created = int(time())
@@ -114,11 +150,12 @@ class Project:
 
 
         with closing(self.database.con.cursor()) as cursor:
         with closing(self.database.con.cursor()) as cursor:
             cursor.execute('''
             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
                 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.
             # lastrowid is 0 if on conflict clause applies.
             # If this is the case we do an extra query to receive the row id.
             # 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.CreateLabel import CreateLabel
 from pycs.frontend.endpoints.labels.EditLabelName import EditLabelName
 from pycs.frontend.endpoints.labels.EditLabelName import EditLabelName
 from pycs.frontend.endpoints.labels.EditLabelParent import EditLabelParent
 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.ListLabels import ListLabels
 from pycs.frontend.endpoints.labels.RemoveLabel import RemoveLabel
 from pycs.frontend.endpoints.labels.RemoveLabel import RemoveLabel
 from pycs.frontend.endpoints.pipelines.FitModel import FitModel
 from pycs.frontend.endpoints.pipelines.FitModel import FitModel
@@ -151,6 +152,10 @@ class WebServer:
             '/projects/<int:identifier>/labels',
             '/projects/<int:identifier>/labels',
             view_func=ListLabels.as_view('list_labels', database)
             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(
         self.__flask.add_url_rule(
             '/projects/<int:identifier>/labels',
             '/projects/<int:identifier>/labels',
             view_func=CreateLabel.as_view('create_label', database, notifications)
             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
         # start transaction
         with self.db:
         with self.db:
             # insert label
             # insert label
-            label, _ = project.create_label(name, parent_id=parent)
+            label, _ = project.create_label(name, parent=parent)
 
 
         # send notification
         # send notification
         self.nm.create_label(label)
         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):
         def result(provided_labels):
             with db:
             with db:
                 for label in provided_labels:
                 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:
                     if insert:
                         nm.create_label(created_label)
                         nm.create_label(created_label)

+ 5 - 2
pycs/interfaces/LabelProvider.py

@@ -32,17 +32,20 @@ class LabelProvider:
         raise NotImplementedError
         raise NotImplementedError
 
 
     @staticmethod
     @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
         create a label result
 
 
         :param reference: label reference string
         :param reference: label reference string
         :param name: label name
         :param name: label name
         :param parent_reference: parent's reference string
         :param parent_reference: parent's reference string
+        :param hierarchy_level: hierarchy level name
         :return:
         :return:
         """
         """
         return {
         return {
             'reference': reference,
             'reference': reference,
             'name': name,
             '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="label selectable"
            :class="{selected: index === selectedIndex}"
            :class="{selected: index === selectedIndex}"
            @click="select(label)">
            @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>
     </div>
     </div>
 
 
@@ -45,13 +50,32 @@ export default {
     }
     }
   },
   },
   computed: {
   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 () {
     results: function () {
       const search = this.search.toLowerCase();
       const search = this.search.toLowerCase();
 
 
       return [{
       return [{
         identifier: null,
         identifier: null,
         name: 'None'
         name: 'None'
-      }, ...this.labels]
+      }, ...this.sortedLabels]
           .filter(l => !this.search || !l.identifier || l.name.toLowerCase().includes(search));
           .filter(l => !this.search || !l.identifier || l.name.toLowerCase().includes(search));
     }
     }
   },
   },
@@ -129,4 +153,9 @@ input {
   background-color: var(--primary);
   background-color: var(--primary);
   padding: 0.25rem 0.5rem;
   padding: 0.25rem 0.5rem;
 }
 }
+
+.hierarchy {
+  font-size: 77%;
+  opacity: 0.8;
+}
 </style>
 </style>

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

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

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

@@ -7,18 +7,26 @@
     <editable-headline :value="label.name"
     <editable-headline :value="label.name"
                        @change="editLabel(label.identifier, $event)"
                        @change="editLabel(label.identifier, $event)"
                        @remove="removeLabel(label.identifier)">
                        @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>
     </editable-headline>
 
 
     <template v-if="!collapse">
     <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"
                        :label="child"
                        :indent="indent"
                        :indent="indent"
                        :targetable="droppable"
                        :targetable="droppable"
@@ -49,6 +57,9 @@ export default {
       return {
       return {
         marginLeft: this.indent
         marginLeft: this.indent
       };
       };
+    },
+    sortedChildren: function () {
+      return [...this.label.children].sort((a, b) => a.name < b.name ? -1 : +1);
     }
     }
   },
   },
   methods: {
   methods: {
@@ -100,4 +111,21 @@ export default {
 .target > .editable-headline {
 .target > .editable-headline {
   text-decoration: underline;
   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>
 </style>

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

@@ -7,7 +7,7 @@
     </h1>
     </h1>
 
 
     <template v-if="labels !== false">
     <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"
                        :label="label"
                        :targetable="true"
                        :targetable="true"
                        indent="2rem"/>
                        indent="2rem"/>
@@ -45,15 +45,15 @@ export default {
 
 
     // subscribe to changes
     // subscribe to changes
     this.$root.socket.on('connect', this.getLabels);
     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 () {
   destroyed: function () {
     this.$root.socket.off('connect', this.getLabels);
     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 () {
   data: function () {
     return {
     return {
@@ -63,85 +63,95 @@ export default {
     }
     }
   },
   },
   computed: {
   computed: {
-    // TODO remove
     sortedLabels: function () {
     sortedLabels: function () {
       return [...this.labels].sort((a, b) => a.name < b.name ? -1 : +1);
       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: {
   methods: {
     getLabels: function () {
     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(response => response.json())
           .then(labels => {
           .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)
       if (label['project_id'] !== this.$root.project.identifier)
         return;
         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)
         if (l.identifier === label.identifier)
           return;
           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;
           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;
           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 () {
     createLabel: function () {
       if (!this.createLabelValue)
       if (!this.createLabelValue)
         return;
         return;
@@ -181,22 +191,4 @@ h1.target {
   width: 4rem;
   width: 4rem;
   height: 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>
 </style>