From cb2b913ac0dd45184c8fdd1b61ad4279959c6b79 Mon Sep 17 00:00:00 2001
From: dcz <dcz@ipipan.waw.pl>
Date: Thu, 9 Nov 2023 12:00:49 +0100
Subject: [PATCH] Vertical relation first commit

---
 entries/static/entries/js/entries.js          |  28 +
 entries/templates/entries_base.html           |   3 +
 entries/templates/vertical_relations.html     |  24 +
 entries/urls.py                               |   2 +
 entries/views.py                              |  23 +
 .../vertical_relations/VerticalRelation.vue   |  73 ++
 .../VerticalRelationEdit.vue                  | 629 ++++++++++++++++++
 .../VerticalRelationElement.vue               | 188 ++++++
 .../VerticalRelationPreview.vue               |  64 ++
 .../VerticalRelationRightPane.vue             |  66 ++
 frontend/src/main.js                          |   2 +
 shellvalier/settings.py                       |   1 +
 shellvalier/urls.py                           |   1 +
 syntax/management/commands/import_tei.py      |  19 +
 unifier/views.py                              |  19 +-
 vertical_relations/__init__.py                |   0
 vertical_relations/apps.py                    |   6 +
 .../management/commands/__init__.py           |   0
 .../commands/db_import_relation_entries.py    |  15 +
 vertical_relations/models.py                  |  63 ++
 vertical_relations/urls.py                    |  15 +
 vertical_relations/views.py                   | 128 ++++
 22 files changed, 1368 insertions(+), 1 deletion(-)
 create mode 100644 entries/templates/vertical_relations.html
 create mode 100644 frontend/src/components/unification/vertical_relations/VerticalRelation.vue
 create mode 100644 frontend/src/components/unification/vertical_relations/VerticalRelationEdit.vue
 create mode 100644 frontend/src/components/unification/vertical_relations/VerticalRelationElement.vue
 create mode 100644 frontend/src/components/unification/vertical_relations/VerticalRelationPreview.vue
 create mode 100644 frontend/src/components/unification/vertical_relations/VerticalRelationRightPane.vue
 create mode 100644 vertical_relations/__init__.py
 create mode 100644 vertical_relations/apps.py
 create mode 100644 vertical_relations/management/commands/__init__.py
 create mode 100644 vertical_relations/management/commands/db_import_relation_entries.py
 create mode 100644 vertical_relations/models.py
 create mode 100644 vertical_relations/urls.py
 create mode 100644 vertical_relations/views.py

diff --git a/entries/static/entries/js/entries.js b/entries/static/entries/js/entries.js
index 6d83ac0..da416ee 100644
--- a/entries/static/entries/js/entries.js
+++ b/entries/static/entries/js/entries.js
@@ -13,6 +13,8 @@ var roles = []
 var role_attributes = []
 var role_sub_attributes = []
 var frame_opinions = []
+var main_vertical_relations = []
+var temporal_vertical_relations = []
 
 function make_opinion_row(item, span, width) {
     const opinion_row = document.createElement('tr');
@@ -1145,6 +1147,28 @@ function getFrameOpinions() {
     });
 }
 
+function getMainVerticalRelations() {
+    $.ajax({
+        dataType: "json",
+        url: '/' + lang + '/vertical_relations/ajax_main_vertical_relations',
+        success: function(data){
+            main_vertical_relations = data.main_vertical_relations;
+        },
+        async: false
+    });
+}
+
+function getTemporalVerticalRelations() {
+    $.ajax({
+        dataType: "json",
+        url: '/' + lang + '/vertical_relations/ajax_temporal_vertical_relations',
+        success: function(data){
+            temporal_vertical_relations = data.temporal_vertical_relations;
+        },
+        async: false
+    });
+}
+
 
 $(document).ready(function() {
     bind_last_visited();
@@ -1181,6 +1205,10 @@ $(document).ready(function() {
 
     getFrameOpinions();
 
+    getMainVerticalRelations();
+
+    getTemporalVerticalRelations();
+
     // $.getJSON('relations', function(data){
     //     memorizeRelations(data.relations);
     // });
diff --git a/entries/templates/entries_base.html b/entries/templates/entries_base.html
index 1788c91..2acdd29 100644
--- a/entries/templates/entries_base.html
+++ b/entries/templates/entries_base.html
@@ -35,6 +35,9 @@
                             class="nav-link cursor-pointer">{% trans "Hierarchia" %}</a></li>
     {% if is_superlexicograf %}
         <li class="nav-item mr-1"><a href="{% url 'entries:lu_free' %}" class="nav-link">{% trans "Wolne jednostki" %}</a></li>
+        <li class="nav-item mr-1"><a
+                onclick='window.location.replace(window.currUnifiedFrameId ? "/pl/entries/vertical-relations/?unified_frame_id="+window.currUnifiedFrameId : "/pl/entries/vertical-relations")'
+                class="nav-link cursor-pointer">{% trans "Relacie poziome" %}</a></li>
     {% endif %}
 {% endif %}
 <li class="nav-item dropdown mr-1">
diff --git a/entries/templates/vertical_relations.html b/entries/templates/vertical_relations.html
new file mode 100644
index 0000000..61a407d
--- /dev/null
+++ b/entries/templates/vertical_relations.html
@@ -0,0 +1,24 @@
+{% extends "entries_base.html" %}
+
+{% load i18n %}
+{% load static %}
+
+{% block title %}{% trans "Hasła" %}{% endblock %}
+
+{% block scripts %}
+    {{ block.super }}
+    <link rel="stylesheet" type="text/css" href="{% static 'entries/css/unification_frames.css' %}">
+    <link rel="stylesheet" type="text/css" href="{% static 'common/css/role_colours.css' %}">
+    <script src="{% static 'entries/js/unification_entries_list.js' %}"></script>
+    <script src="{% static 'entries/js/unification_frames_list.js' %}"></script>
+    <script src="{% static 'entries/js/unification_entries_for_frames_list.js' %}"></script>
+    <script src="{% static 'entries/js/jquery-impromptu.min.js' %}"></script>
+    <script>
+        window.currUnifiedFrameId = {{ unified_frame_id|default:'null' }};
+    </script>
+{% endblock %}
+
+{% block modals %}
+    {{ block.super }}
+    <div id="lexical-unit-notes-template" class="d-none">{% include 'notes.html' %}</div>
+{% endblock %}
diff --git a/entries/urls.py b/entries/urls.py
index f434ba6..9fd3101 100644
--- a/entries/urls.py
+++ b/entries/urls.py
@@ -19,6 +19,8 @@ urlpatterns = [
     path('change_show_linked_entries/', views.change_show_linked_entries, name='change_show_linked_entries'),
     path('unification/', views.unification, name='unification'),
     path('hierarchy/', views.hierarchy, name='hierarchy'),
+    path('vertical-relations/', views.vertical_relations, name='vertical-relations'),
+
     path('lu_free/', views.lu_free, name='lu_free'),
 
     path('autocomplete/', autocompletes.autocomplete, name='autocomplete'),
diff --git a/entries/views.py b/entries/views.py
index 013fc94..8bca9f3 100644
--- a/entries/views.py
+++ b/entries/views.py
@@ -138,6 +138,29 @@ def lu_free(request):
     )
 
 
+@login_required
+def vertical_relations(request):
+
+    unified_frame_id = None
+    if "unified_frame_id" in request.GET:
+        unified_frame_id = request.GET.get("unified_frame_id")
+
+    user = request.user
+    return render(
+        request,
+        'hierarchy.html',
+        {
+            'is_vue_app': True,
+            'unified_frame_id': unified_frame_id,
+            'entries_form': EntryForm(),
+            'frames_form': FrameFormFactory.get_form(as_subform=False),
+            'schemata_form': SchemaFormFactory.get_form(as_subform=False),
+            'unified_frames_form': UnifiedFrameFormFactory.get_form(as_subform=False),
+            'is_superlexicograf': user.groups.filter(name=settings.SUPER_LEXICOGRAPHS_GROUP_NAME).exists()
+        },
+    )
+
+
 FORM_TYPES = {
     'entry'      : EntryForm,
 }
diff --git a/frontend/src/components/unification/vertical_relations/VerticalRelation.vue b/frontend/src/components/unification/vertical_relations/VerticalRelation.vue
new file mode 100644
index 0000000..ce8456e
--- /dev/null
+++ b/frontend/src/components/unification/vertical_relations/VerticalRelation.vue
@@ -0,0 +1,73 @@
+<script>
+import UnificationFramesList from "../Unification/UnificationFramesList.vue";
+import VerticalRelationRightPane from "./VerticalRelationRightPane.vue";
+
+export default {
+  data () {
+    return {
+      entryId: null,
+      lexicalUnitId: null,
+      unifiedFrameId: null,
+      gettext: window.gettext,
+      unificationEntriesListRefreshKey: 1,
+    };
+  },
+  components: {VerticalRelationRightPane, UnificationFramesList},
+  methods: {
+    lexicalUnitSelected (entryId, lexicalUnitId) {
+      this.entryId = entryId;
+      this.lexicalUnitId = lexicalUnitId;
+    },
+    unifiedFrameSelected (unifiedFrameId, entryId, lexicalUnitId) {
+      this.unifiedFrameId = unifiedFrameId;
+      this.entryId = entryId;
+      this.lexicalUnitId = lexicalUnitId;
+    },
+    refreshEntriesList() {
+      this.unificationEntriesListRefreshKey++;
+    }
+  },
+  setup() {
+    const unified_frame_id = new URL(location.href).searchParams.get('unified_frame_id');
+    return {
+      initial_unified_frame_id: parseInt(unified_frame_id),
+    };
+  },
+  mounted () {
+    this.unifiedFrameSelected(window.currUnifiedFrameId);
+    $('#entries-list').length && Split(['#entries-list', '#entry-display'], {
+      sizes: [20, 80],
+      gutterSize: 4,
+      minSize: 10,
+      elementStyle: (dimension, size, gutterSize) => {
+        return {
+          'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)'
+        }
+      },
+    });
+  },
+};
+</script>
+
+<template>
+  <div id="entries-list" class="col h-100 w-100 pr-0 overflow-hidden">
+    <div id="entries-list-div" class="col p-0 h-100 w-100 overflow-hidden">
+      <unification-frames-list
+              :unificationEntriesListRefreshKey="unificationEntriesListRefreshKey"
+              :initialUnifiedFrameId="initial_unified_frame_id"
+              :setupHierarchyMarking="true"
+              @unified-frame-selected="unifiedFrameSelected"
+      />
+    </div>
+  </div>
+  <div id="entry-display" class="col h-100 p-0 overflow-hidden">
+    <vertical-relation-right-pane
+        ref="hierarchyRightPane"
+        :entryId="entryId"
+        :lexicalUnitId="lexicalUnitId"
+        :initialUnifiedFrameId="unifiedFrameId"
+        @refresh-entries-list="refreshEntriesList"
+    />
+  </div>
+</template>
+
diff --git a/frontend/src/components/unification/vertical_relations/VerticalRelationEdit.vue b/frontend/src/components/unification/vertical_relations/VerticalRelationEdit.vue
new file mode 100644
index 0000000..3ea0a31
--- /dev/null
+++ b/frontend/src/components/unification/vertical_relations/VerticalRelationEdit.vue
@@ -0,0 +1,629 @@
+<script>
+import InfoTooltip from "../../shared/InfoTooltip.vue";
+import Spinner from "../../shared/Spinner.vue";
+import ExamplesComponent from "../shared/frame-components/ExamplesComponent.vue";
+import SlowalFrameComponent from "../shared/frame-components/SlowalFrameComponent.vue";
+import SemanticsSchemataComponent from "../shared/frame-components/SemanticsSchemataComponent.vue";
+import MeaningComponent from "../shared/frame-components/MeaningComponent.vue";
+import SelectionalPreference from "../Unification/SelectionalPreference.js";
+import { slowal_frames2selecional_preferencies } from "../shared/utils.js";
+import HierarchyPreview from "../hierarchy/HierarchyPreview.vue";
+import HierarchyElement from "../hierarchy/HierarchyElement.vue";
+import {frames2lexical_units, send_post_request} from "../shared/utils";
+
+
+export default {
+  props: {
+    unifiedFrameId: Number,
+    previewedUnifiedFrameId: Number,
+    forceRefresh: Number,
+  },
+  data() {
+    return {
+      gettext: window.gettext,
+      unified_frame: {},
+      unified_frame_title: '',
+      unified_frame_arguments: [],
+      active_unified_frame_argument: null,
+      slowal_frames2selecional_preferencies_mapping: {},
+      lexical_units: [],
+      img_prefix: window.STATIC_URL,
+      frames: [],
+      right_pane_tabs: [
+        {id: 'hierarchy', label: gettext('Hierarchia')},
+        {id: 'notes', label: gettext('Notatki')},
+      ],
+      right_pane_tab: 'hierarchy',
+      currentPreviewedUnifiedFrameId: this.previewedUnifiedFrameId,
+      internalForceRefresh: this.forceRefresh,
+      statusButtonTitle: '',
+      active_slowal_frame: null,
+      showVerifiedFrames: false,
+      subentries: null,
+      alternations: null,
+      realisation_phrases: null,
+      realisation_descriptions: null,
+      examples: null,
+      selectedFrameArguments: null,
+      frame_arguments_or_type: false,
+      selectedLus: null,
+      selectedSchemas: null,
+      selectedExamples: null,
+      hidden_frames: [],
+      target_unified_frame: null,
+      hierarchy_hyponyms: null,
+      hierarchy_hyperonyms: null,
+      current_vertical_relation: null,
+      vertical_relations: null,
+    }
+  },
+  components: {HierarchyElement, InfoTooltip, Spinner, HierarchyPreview, SlowalFrameComponent, ExamplesComponent, SemanticsSchemataComponent, MeaningComponent},
+  emits: ['goToDisplay', 'refresh', 'swapFrames', 'refreshEntriesList', 'clearUnifiedFrameView'],
+  watch: {
+    forceRefresh(newVal, oldVal) {
+      this.loadFrame();
+    }
+  },
+  computed: {
+    selectionalPreference() {
+      return new SelectionalPreference();
+    },
+  },
+  methods: {
+    createHierarchyTree() {
+      return {
+        'unified_frame': this.unified_frame,
+        'hyponyms': this.hierarchy_hyponyms,
+        'hypyronyms': this.hierarchy_hyperonyms
+      };
+    },
+    hasWhiteSpace(s) {
+      return /\s/g.test(s);
+    },
+    async loadFrame() {
+      try {
+        const data = {'unified_frame_id': this.unifiedFrameId, 'no_filters' : false};
+        $.ajax({
+          type: 'post',
+          url: '/' + lang + '/unifier/get_unified_frame/',
+          dataType: 'json',
+          data: data,
+          timeout: 60000,
+          success: function (response) {
+
+            this.img_prefix = window.STATIC_URL;
+            this.lexical_units = this.frames2lexical_units(response.frames);
+            this.unified_frame = response.unified_frame;
+            this.unified_frame_title = this.unified_frame.title;
+            this.unified_frame_arguments = this.unified_frame.arguments;
+            this.frames = response.frames;
+            this.vertical_relations = response.vertical_relations;
+            this.slowal_frames2selecional_preferencies_mapping = slowal_frames2selecional_preferencies(this.unified_frame, response.frames);
+
+            this.subentries = response.subentries;
+            this.alternations = response.alternations;
+            this.realisation_phrases = response.realisation_phrases;
+            this.realisation_descriptions = response.realisation_descriptions;
+            this.examples = response.examples;
+
+            this.fulfill_slowal_frames_arguments_with_empty_elems(response.unified_frame, response.frames)
+            window.update_last_visited(response.last_visited);
+            window.clear_info();
+
+            if (!this.active_slowal_frame) {
+              this.setup_notes_unified_frame();
+            }
+          }.bind(this),
+          error: function (request, errorType, errorMessage) {
+            show_error(errorType + ' (' + errorMessage + ')');
+          }
+        });
+        this.loadHierarchy();
+      } catch (error) {
+        console.log(error);
+      }
+    },
+    loadHierarchy() {
+      $.ajax({
+        type: 'post',
+        url: '/' + lang + '/unifier/get_hierarchy_hyponyms/'+this.unifiedFrameId+"/",
+        dataType: 'json',
+        timeout: 60000,
+        success: function (response) {
+          this.hierarchy_hyponyms = response.hyponyms;
+        }.bind(this),
+        error: function (request, errorType, errorMessage) {
+          show_error(errorType + ' (' + errorMessage + ')');
+        }
+      });
+
+      $.ajax({
+        type: 'post',
+        url: '/' + lang + '/unifier/get_hierarchy_hyperonyms/'+this.unifiedFrameId+"/",
+        dataType: 'json',
+        timeout: 60000,
+        success: function (response) {
+          this.hierarchy_hyperonyms = response.hyperonyms;
+        }.bind(this),
+        error: function (request, errorType, errorMessage) {
+          show_error(errorType + ' (' + errorMessage + ')');
+        }
+      });
+    },
+    setup_notes_unified_frame() {
+      setup_notes($('#notes-component'), $('#lexical-unit-notes-template'), this.unified_frame.id, 'unifier.UnifiedFrame',
+              this.setup_notes_unified_frame, 'hierarchy_frame', true);
+    },
+    unifiedFrameArgumentSelected(argument) {
+      if (this.active_unified_frame_argument === argument) {
+        this.active_unified_frame_argument = null;
+        this.frame_arguments_or_type = false;
+        this.deselectSlowalFrameSelectedElements();
+      } else {
+        this.active_slowal_frame = null;
+        this.active_unified_frame_argument = argument;
+        const slowalFrameArguments = this.getSlowalFrameArgumentsBy(argument);
+        this.frame_arguments_or_type = true;
+        this.deselectSlowalFrameSelectedElements();
+        this.selectedFrameArguments = slowalFrameArguments;
+      }
+      this.unifiedFrameArgumentHovered(argument);
+    },
+    unifiedFrameArgumentHovered(argument) {
+      clear_info();
+      if (argument && this.active_unified_frame_argument === argument) {
+        show_info(gettext('Kliknij, aby cofnąć wybór kolumny do edycji.'));
+      }
+      if (argument && this.active_unified_frame_argument !== argument) {
+        show_info(gettext('Kliknij, aby wybrać kolumnę do edycji.'));
+      }
+    },
+    getSlowalFrameArgumentsBy(unified_frame_argument) {
+      const slowalFrameArgumentIds = [];
+      for (let i in this.unified_frame.slowal_frame_mapping) {
+        const slowal_frame_mapping = this.unified_frame.slowal_frame_mapping[i];
+        const slowalFrame = this.frames.find(frame => frame.id === slowal_frame_mapping.slowal_frame_id);
+        if(slowalFrame != null) {
+          for (let j in slowal_frame_mapping.slowal_frame_argument_mapping) {
+            const slowal_frame_argument_mapping = slowal_frame_mapping.slowal_frame_argument_mapping[j];
+            if (slowal_frame_argument_mapping.unified_frame_agrument_id == unified_frame_argument.id) {
+              const slowalFrameArgument = slowalFrame.arguments.find(arg => arg.argument_id === slowal_frame_argument_mapping.slowal_frame_agrument_id);
+              slowalFrameArgumentIds.push(slowalFrameArgument);
+            }
+          }
+        }
+      }
+      return slowalFrameArgumentIds;
+    },
+    isSuperLeksykograf() {
+      return has_permission("users.view_assignment");
+    },
+    isFrameVerified(frame) {
+      const isSuperLeksykograf = this.isSuperLeksykograf();
+      return (!isSuperLeksykograf && frame.status === 'G') || (isSuperLeksykograf && frame.status === 'S')
+    },
+    select_slowal_frame_req(to_invoke) {
+      if (this.active_slowal_frame) {
+        to_invoke();
+      } else {
+        alert(gettext("Wybierz ramę, dla której chcesz zmienić status."));
+      }
+    },
+    isFrameVisible(status) {
+      return (status != 'B' && status != 'C') || this.isSuperLeksykograf();
+    },
+    deselectSlowalFrameSelectedElements() {
+      this.subentries.forEach(subentry => {
+        subentry.schemata.forEach(s => {
+          s.selected = false;
+        });
+      });
+      this.frames.forEach(frame => {
+        frame.lexical_units.forEach(lu => {
+          lu.selected = false;
+        });
+        frame.arguments.forEach(argument => {
+          argument.selected = false;
+        });
+      });
+      this.examples.forEach(example => {
+        example.selected = false;
+      });
+      this.selectedLus = [];
+      this.selectedFrameArguments = [];
+      this.selectedSchemas = [];
+      this.selectedExamples = [];
+    },
+    slowalFrameSelected(frame) {
+      this.deselectSlowalFrameSelectedElements();
+      if (this.active_slowal_frame === frame) {
+        this.active_slowal_frame = null;
+      } else {
+        this.active_slowal_frame = frame;
+      }
+    },
+    isSelectedFrame(frame) {
+      if (this.active_slowal_frame) {
+        return frame.id === this.active_slowal_frame.id;
+      } else {
+        return false;
+      }
+    },
+    changePreviewedUnifiedFrameId(unifiedFrameId) {
+      this.currentPreviewedUnifiedFrameId = unifiedFrameId;
+    },
+    getArgumentCSS(argument) {
+      return (argument.role ? argument.role.str + ' ' : '') + (argument == this.active_unified_frame_argument ? 'active' : '');
+    },
+    schemataSelected(schemas) {
+      this.selectedSchemas = schemas;
+    },
+    exampleSelected(selectedExamples) {
+      this.selectedExamples = selectedExamples;
+    },
+    isReadOnlyForSuperLeksykograf() {
+      return (this.isSuperLeksykograf() && this.unified_frame.status === 'O') && this.unified_frame.assignee_username !== window.USER_USERNAME;
+    },
+    frames2lexical_units(frames) {
+      const lexical_units = []
+      for (let i in frames) {
+        const frame = frames[i];
+        for (let j in frame.lexical_units) {
+          const lexical_unit = frame.lexical_units[j];
+          lexical_unit.opinion = frame.opinion;
+          lexical_unit.opinion_key = frame.opinion_key;
+          lexical_unit.frame_status = frame.status;
+          lexical_unit.frame = frame;
+          lexical_units.push(lexical_unit);
+        }
+      }
+      return lexical_units;
+    },
+    fulfill_slowal_frames_arguments_with_empty_elems(unified_frame, slowal_frames) {
+      for (let i in unified_frame.slowal_frame_mapping) {
+        const slowal_frame_mapping = unified_frame.slowal_frame_mapping[i];
+        let slowal_frame = slowal_frames.find(o => o.id === slowal_frame_mapping.slowal_frame_id);
+        if(slowal_frame != null) {
+          let new_slowal_frame_arguments = [];
+          for (let j in unified_frame.arguments) {
+            const unified_frame_argument = unified_frame.arguments[j];
+            let unified_frame_argument_mapping = slowal_frame_mapping.slowal_frame_argument_mapping.find(o => o.unified_frame_agrument_id === unified_frame_argument.id);
+            let slowal_frame_argument = null;
+            if (unified_frame_argument_mapping == null) {
+              slowal_frame_argument = {
+                'str': 'Empty',
+                'id': slowal_frame.id + '-_' + (unified_frame_argument.id),
+                'role': 'Empty',
+                'role_type': 'Empty',
+                'preferences': [],
+                'proposed_roles': [],
+              }
+            } else {
+              slowal_frame_argument = slowal_frame.arguments.find(o => o.argument_id === unified_frame_argument_mapping.slowal_frame_agrument_id);
+            }
+            new_slowal_frame_arguments.push(slowal_frame_argument)
+          }
+          slowal_frame.arguments = new_slowal_frame_arguments;
+        }
+      }
+    },
+    createFrameHierarchyRepresentationHTML(unified_frame) {
+
+    },
+    createHierarchyHTML() {
+      this.unified_frame
+    },
+    save_main_vertical_relation() {
+      const main_vertical_relation_select = function () {
+        return main_vertical_relations.map(frame_opinion => {
+          return `<label><input type="radio" name="opinion" value="${frame_opinion.id}" /> ${frame_opinion.name}</label><br />`;
+        }).join("");
+      }.bind(this);
+
+      const main_vertical_relation_select_popup = {
+        state0: {
+          title: 'Wybierz relację główną',
+          html: main_vertical_relation_select,
+          buttons: {Anuluj: 0, Wybierz: 1},
+          focus: -1,
+          submit: function (e, v, m, f) {
+            if (v == 0) {
+              e.preventDefault();
+              $.prompt.close();
+            }
+            if (v === 1) {
+              e.preventDefault();
+              let relation_id = normalizeFormData(f.opinion)[0];
+              send_post_request('/vertical_relations/save_main_vertical_relation/',
+                      {
+                        'source_unified_frame_id': this.unified_frame.id,
+                        'target_unified_frame_id': this.target_unified_frame ? this.target_unified_frame.id : null,
+                        'main_vertical_relation_id': relation_id
+                      },
+                      (reponse) => {
+                        show_info('Relacja główne została ustawiona.');
+                        this.current_vertical_relation = reponse['vertical_relation'];
+                        this.loadFrame();
+                        $.prompt.close();
+                      },
+                      (request, errorType, errorMessage) => {
+                        show_error(errorType + ' (' + errorMessage + ')');
+                        alert(gettext("Ustawienie relacji głównej nie powiodła się. Błąd: " + errorType + ' (' + errorMessage + ')'));
+                        $.prompt.close();
+                      })
+            }
+          }.bind(this)
+        }
+      }
+      $.prompt(main_vertical_relation_select_popup);
+    },
+    save_temporal_vertical_relation() {
+      if(this.current_vertical_relation) {
+        const temporal_vertical_relations_ids = new Set(this.current_vertical_relation.temporal_vertical_relation.map(l => l.id));
+        const temporal_vertical_relation_select = function () {
+          return temporal_vertical_relations.map(relation => {
+            return `<label><input type="checkbox" ${temporal_vertical_relations_ids.has(relation.id) ? 'checked' : ''} name="opinion" value="${relation.id}" /> ${relation.name}</label><br />`;
+          }).join("");
+        }.bind(this);
+
+
+        const temporal_vertical_relation_select_popup = {
+          state0: {
+            title: 'Wybierz relacje pomocnicze',
+            html: temporal_vertical_relation_select,
+            buttons: {Anuluj: 0, Wybierz: 1},
+            focus: -1,
+            submit: function (e, v, m, f) {
+              if (v == 0) {
+                e.preventDefault();
+                $.prompt.close();
+              }
+              if (v === 1) {
+                e.preventDefault();
+                let relation_ids = normalizeFormData(f.opinion);
+                send_post_request('/vertical_relations/save_temporal_vertical_relation/',
+                        {
+                          'source_unified_frame_id': this.unified_frame.id,
+                          'target_unified_frame_id': this.target_unified_frame ? this.target_unified_frame.id : null,
+                          'temporal_vertical_relation_ids': JSON.stringify(relation_ids)
+                        },
+                        (reponse) => {
+                          show_info('Relacje pomocnicze zostały ustawione.');
+                          this.current_vertical_relation = reponse['vertical_relation'];
+                          this.loadFrame();
+                          $.prompt.close();
+                        },
+                        (request, errorType, errorMessage) => {
+                          show_error(errorType + ' (' + errorMessage + ')');
+                          alert(gettext("Ustawienie relacji pomocniczych nie powiodła się. Błąd: " + errorType + ' (' + errorMessage + ')'));
+                          $.prompt.close();
+                        })
+              }
+            }.bind(this)
+          }
+        }
+        $.prompt(temporal_vertical_relation_select_popup);
+      } else {
+        alert(gettext("Dodanie relacji pomocniczych wymaga ustawienia relacji głównej."));
+      }
+    },
+    load_vertical_relation() {
+      const load_vertical_relation_select = function () {
+        return this.vertical_relations.map(vertical_relation => {
+          return `<label><input type="radio" name="opinion" value="${vertical_relation.id}" /> ${vertical_relation.str}</label><br />`;
+        }).join("");
+      }.bind(this);
+
+      const load_vertical_relation_select_popup = {
+        state0: {
+          title: 'Wybierz relację do załadowania',
+          html: load_vertical_relation_select,
+          buttons: {Anuluj: 0, Wybierz: 1},
+          focus: -1,
+          submit: function (e, v, m, f) {
+            if (v == 0) {
+              e.preventDefault();
+              $.prompt.close();
+            }
+            if (v === 1) {
+              e.preventDefault();
+              let relation_id = normalizeFormData(f.opinion)[0];
+              send_post_request('/vertical_relations/load_vertical_relation/',
+                      {
+                        'vertical_relation_id': relation_id,
+                      },
+                      (reponse) => {
+                        show_info('Relacja została załadowana.');
+                        this.current_vertical_relation = reponse['vertical_relation'];
+                        this.loadFrame();
+                        $.prompt.close();
+                      },
+                      (request, errorType, errorMessage) => {
+                        show_error(errorType + ' (' + errorMessage + ')');
+                        alert(gettext("Ładowanie relacji nie powiodła się. Błąd: " + errorType + ' (' + errorMessage + ')'));
+                        $.prompt.close();
+                      })
+            }
+          }.bind(this)
+        }
+      }
+      $.prompt(load_vertical_relation_select_popup);
+    },
+    remove_vertical_relation() {
+      if(this.current_vertical_relation) {
+        send_post_request('/vertical_relations/remove_vertical_relation/',
+                {
+                  'vertical_relation_id': this.current_vertical_relation.id,
+                },
+                (reponse) => {
+                  show_info('Relacja została usunięta.');
+                  this.current_vertical_relation = null;
+                  this.loadFrame();
+                  $.prompt.close();
+                },
+                (request, errorType, errorMessage) => {
+                  show_error(errorType + ' (' + errorMessage + ')');
+                  alert(gettext("Usunięcie relacji nie powiodła się. Błąd: " + errorType + ' (' + errorMessage + ')'));
+                  $.prompt.close();
+                })
+      } else {
+        alert(gettext("Rama źródłowa nie posiada jeszcze ustawionej relacji."));
+      }
+    }
+  },
+  mounted() {
+
+    if(this.unifiedFrameId) {
+      this.loadFrame();
+    }
+
+    Split(['#semantics-frames-pane', '#semantics-schemata-pane'], {
+      sizes: [40, 60],
+      minSize: 10,
+      gutterSize: 4,
+      elementStyle: (dimension, size, gutterSize) => {
+        return {
+          'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)'
+        }
+      },
+    });
+    Split(['#source-unified-frame-pane', '#target-unified-frame-pane', '#examples'], {
+      sizes: [40, 40, 20],
+      direction: 'vertical',
+      gutterSize: 4,
+      minSize: 10,
+    });
+  }
+}
+
+</script>
+
+<template>
+    <div class="col h-100 px-0 pt-0 pb-0 overflow-auto" id="semantics-frames-pane">
+      <div id="source-unified-frame-pane" class="col w-100 p-0 overflow-auto">
+        <table v-if="!isReadOnlyForSuperLeksykograf()" class="table-button-menu sticky-top" cellspacing="1">
+          <tr style="background-color: white;">
+            <td id="change-main-relation" @click="save_main_vertical_relation()" style="padding: 10px 15px 10px 15px; color: #000000;">Dodaj relacjÄ™</td>
+            <td id="change-temporal-relation" @click="save_temporal_vertical_relation()" style="padding: 10px 15px 10px 15px; color: #000000;">Dodaj podrelacjÄ™</td>
+            <td id="remove-relation" @click="remove_vertical_relation()" style="padding: 10px 15px 10px 15px; color: #000000;">Usuń relację</td>
+            <td id="available-relations" @click="load_vertical_relation()" style="padding: 10px 15px 10px 15px; color: #000000;">Dostępne relacje</td>
+          </tr>
+        </table>
+        <spinner />
+        <div align="center">
+          <div align="left" style="display: table;">
+            <div class="unifiedFrame mt-3" v-bind:data-frame_id="unified_frame.id" id="unified-frame-title" v-html="unified_frame_title"></div>
+            <table v-if="unified_frame.id" id="unified-frame" class="m-0 table-borderless border border-secondary text-dark frame active">
+            <tbody>
+              <tr>
+                <template v-for="argument in unified_frame_arguments">
+                <td
+                  class="argument py-2 px-1 border-top border-left border-secondary role-column"
+                      :class="getArgumentCSS(argument)"
+                      @click="unifiedFrameArgumentSelected(argument)"
+                      @mouseover="unifiedFrameArgumentHovered(argument)"
+                      @mouseleave="unifiedFrameArgumentHovered(null)"
+                  >
+                  {{ argument.role_type }}
+
+                  <div
+                    v-if="argument.role"
+                  >
+                    [{{ argument.role.str }}]
+                  </div>
+                  <div v-else>
+                    <ul class="ul-role">
+                      <li v-for="proposed_role in argument.proposed_roles">
+                        {{ proposed_role.str }}
+                      </li>
+                    </ul>
+                  </div>
+                </td>
+                </template>
+
+
+              </tr>
+              <tr>
+                <td class="preferences py-0 px-0 border-top border-left border-secondary role-column align-top"
+                    v-for='argument in unified_frame_arguments'
+                    :key='argument.id'
+                >
+                  <ul class="ul-preference" v-if="argument.preferences.length > 0">
+                    <li v-for='preference in argument.preferences'>
+                      <div
+                        v-if="preference.url != null"
+                        class="preference py-2 px-1 preference-bold"
+                      >
+                        <a class="synset-plwn" v-bind:href="preference.url" target="_blank">{{ preference.str }}</a>
+                      </div>
+                      <div v-else class="preference py-2 px-1 preference-bold">{{ preference.str }}</div>
+                    </li>
+                  </ul>
+                  <ul class="ul-preference" v-if="unified_frame.status !== 'S'">
+                    <li v-for="preference in slowal_frames2selecional_preferencies_mapping[argument.id]">
+                      <span v-if="preference.url != null" class="preference py-2 px-1">
+                        <a class="synset-plwn" v-bind:href="preference.url" target="_blank">{{ preference.str }}</a>
+                      </span>
+                      <span v-else class="preference py-2 px-1">{{ preference.str }}</span>
+                      <info-tooltip v-if="preference.info" :text="preference.info" />
+                    </li>
+                  </ul>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+          </div>
+          <div v-if="unified_frame.id" class="lu-table mt-3 mb-3">
+              <table class="m-0 table-borderless border border-secondary text-dark">
+                <td class="argument py-2 px-1 border-top border-left border-secondary">
+                  <template v-for='lexical_unit in lexical_units'>
+                    {{ lexical_unit.str }},
+                  </template>
+                </td>
+              </table>
+            </div>
+        </div>
+        <div align="left">
+          <div v-if="current_vertical_relation" class="lu-table mt-3 mb-3" style="padding-left: 50px;">
+            <table class="m-0 table-borderless text-dark">
+              <tr>
+                <td class="argument py-2 px-1 border-top border-left border-secondary" colspan="2">
+                  {{current_vertical_relation.main_vertical_relation.name}}
+                </td>
+              </tr>
+            </table>
+          </div>
+        </div>
+      </div>
+      <div id="target-unified-frame-pane" class="col w-100 p-0 overflow-auto">
+      </div>
+      <div id="examples" class="col w-100 p-0 tab-pane overflow-auto">
+         <examples-component v-if="examples"
+              :examples="examples" 
+              :frame="active_slowal_frame"
+              :frame_arguments="selectedFrameArguments"
+              :frame_arguments_or_type="frame_arguments_or_type"
+              :lus="selectedLus"
+              :schemas="selectedSchemas"
+              :key="examples"
+              @example-selected="exampleSelected"
+          />
+        </div>
+    </div>
+    <div class="col h-100 px-1 pt-0 pb-0 overflow-auto" style="padding-left: 0px!important; padding-right: 0px!important;" id="semantics-schemata-pane">
+      <ul class="nav nav-pills nav-justified p-1" id="entryTabs" role="tablist">
+        <li
+          v-for="tab in right_pane_tabs"
+          class="btn btn-sm btn-outline-dark nav-link mx-1"
+          :class="right_pane_tab === tab.id && 'active'"
+          @click="right_pane_tab = tab.id"
+        >
+          {{ tab.label }}
+        </li>
+      </ul>
+      <div v-if="right_pane_tab === 'hierarchy'" class="px-0 pt-0 pb-0 overflow-auto">
+          <hierarchy-element :node="createHierarchyTree()" :spacing_elem_type="'none'"/>
+      </div>
+      <div :class="right_pane_tab !== 'notes' && 'd-none'" id="notes-component"></div>
+    </div>
+</template>
diff --git a/frontend/src/components/unification/vertical_relations/VerticalRelationElement.vue b/frontend/src/components/unification/vertical_relations/VerticalRelationElement.vue
new file mode 100644
index 0000000..ee8d86e
--- /dev/null
+++ b/frontend/src/components/unification/vertical_relations/VerticalRelationElement.vue
@@ -0,0 +1,188 @@
+<script>
+
+export default {
+    name: 'VerticalRelationElement',
+    props: {
+        node: {
+            type: Object,
+            required: true
+        },
+        spacing: {
+            type: Number,
+            default: 0
+        },
+        spacing_elem_type: String
+    },
+    data() {
+        return {
+            showHyponyms: false,
+            showHypyronyms: false
+        }
+    },
+    computed: {
+        nodeMargin() {
+            return {
+                'margin-left': `${this.spacing}px`
+            }
+        },
+        hasHyponyms() {
+            const {hyponyms} = this.node
+            return hyponyms && hyponyms.length > 0
+        },
+        hasHypyronyms() {
+            const {hypyronyms} = this.node
+            return hypyronyms && hypyronyms.length > 0
+        },
+        toggleChildrenIcon() {
+            return this.showHypyronyms ? 'fas fa-angle-down' : 'fas fa-angle-right'
+        },
+        getSpacingElem() {
+            if(this.spacing_elem_type === "std") {
+                return "&#9474; &#9500;&#8594; &#9474;";
+            } else if(this.spacing_elem_type === "top_corner") {
+                return "&#160; &#160; &#160; &#160; &#9484;&#8594; &#9474; &#9474;";
+            } else if(this.spacing_elem_type === "down_corner") {
+                return "&#9474; &#9474; &#9492;&#8594;";
+            } else {
+                return "";
+            }
+        },
+    },
+    methods: {
+        loadChildren (children) {
+            for (let i in children) {
+                const hypyronym = children[i];
+                if (hypyronym.hasChildrenLoaded !== true) {
+                    $.ajax({
+                        type: 'post',
+                        url: '/' + lang + '/unifier/get_hierarchy_hyperonyms/' + hypyronym.unified_frame_id + "/",
+                        dataType: 'json',
+                        timeout: 60000,
+                        success: function (response) {
+                            hypyronym.hypyronyms = response.hyperonyms;
+                        }.bind(this),
+                        error: function (request, errorType, errorMessage) {
+                            show_error(errorType + ' (' + errorMessage + ')');
+                        }
+                    });
+                    $.ajax({
+                        type: 'post',
+                        url: '/' + lang + '/unifier/get_hierarchy_hyponyms/' + hypyronym.unified_frame_id + "/",
+                        dataType: 'json',
+                        timeout: 60000,
+                        success: function (response) {
+                            hypyronym.hyponyms = response.hyponyms;
+                        }.bind(this),
+                        error: function (request, errorType, errorMessage) {
+                            show_error(errorType + ' (' + errorMessage + ')');
+                        }
+                    });
+                }
+            }
+        }, toggleHypyronyms() {
+            this.loadChildren(this.node.hypyronyms);
+            this.showHypyronyms = !this.showHypyronyms
+        },
+        toggleHyponyms() {
+            this.loadChildren(this.node.hyponyms);
+            this.showHyponyms = !this.showHyponyms
+        }
+    }
+}
+
+</script>
+
+
+<template>
+    <div :style="nodeMargin" style="height: 100%">
+        <div v-if="hasHypyronyms" v-show="showHypyronyms">
+            <hierarchy-element
+                    v-for="(child, index) in node.hypyronyms"
+                    :key="child.id"
+                    :node="child"
+                    :spacing_elem_type='index === 0 ? "top_corner" : "std"'
+                    :spacing="spacing + 40"
+            />
+        </div>
+        <div show class="d-flex justify-content-between mb-1">
+            <div class="row">
+                <div class="col" style="max-width: 10px" v-html="getSpacingElem"/>
+                <div class="col">
+                    <div class="row">
+                        <div class="col cursor-pointer" style="text-align: center; font-size: large"><span
+                                v-if="hasHypyronyms"
+                                :class="toggleChildrenIcon"
+                                @click="toggleHypyronyms"
+                                @keypress="toggleHypyronyms">&#9650;</span></div>
+                    </div>
+                    <div class="row">
+                        <div class="col" align="left" style="display: table;">
+                            <div class="unifiedFrame" id="hierarchy-unified-frame-title" v-html="node.unified_frame.title"></div>
+                            <table id="hierarchy-unified-frame" class="m-0 table-borderless border border-secondary text-dark frame active">
+                                <tbody>
+                                <tr>
+                                    <template v-for="argument in node.unified_frame.arguments">
+                                        <td
+                                                class="argument py-2 px-1 border-top border-left border-secondary role-column"
+                                                :class="argument.role ? argument.role.str + ' ' : ''"
+                                        >
+                                            {{ argument.role_type }}
+
+                                            <div
+                                                    v-if="argument.role"
+                                            >
+                                                [{{ argument.role.str }}]
+                                            </div>
+                                            <div v-else>
+                                                <ul class="ul-role">
+                                                    <li v-for="proposed_role in argument.proposed_roles">
+                                                        {{ proposed_role.str }}
+                                                    </li>
+                                                </ul>
+                                            </div>
+                                        </td>
+                                    </template>
+                                </tr>
+                                <tr>
+                                    <td hidden class="preferences py-0 px-0 border-top border-left border-secondary role-column align-top"
+                                        v-for='argument in node.unified_frame.arguments'
+                                        :key='argument.id'
+                                    >
+                                        <ul class="ul-preference" v-if="argument.preferences.length > 0">
+                                            <li v-for='preference in argument.preferences'>
+                                                <div
+                                                        v-if="preference.url != null"
+                                                        class="preference py-2 px-1 preference-bold"
+                                                >
+                                                    <a class="synset-plwn" v-bind:href="preference.url" target="_blank">{{ preference.str }}</a>
+                                                </div>
+                                                <div v-else class="preference py-2 px-1 preference-bold">{{ preference.str }}</div>
+                                            </li>
+                                        </ul>
+                                    </td>
+                                </tr>
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                    <div class="row align-content-center">
+                        <div class="col cursor-pointer" style="text-align: center; font-size: large"><span
+                                v-if="hasHyponyms"
+                                :class="toggleChildrenIcon"
+                                @click="toggleHyponyms"
+                                @keypress="toggleHyponyms">&#9660;</span></div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div v-if="hasHyponyms" v-show="showHyponyms">
+            <vertical-relation-element
+                    v-for="(child, index) in node.hyponyms"
+                    :key="child.id"
+                    :node="child"
+                    :spacing_elem_type='index === node.hyponyms.length - 1 ? "down_corner" : "std"'
+                    :spacing="spacing + 40"
+            />
+        </div>
+    </div>
+</template>
diff --git a/frontend/src/components/unification/vertical_relations/VerticalRelationPreview.vue b/frontend/src/components/unification/vertical_relations/VerticalRelationPreview.vue
new file mode 100644
index 0000000..a88db17
--- /dev/null
+++ b/frontend/src/components/unification/vertical_relations/VerticalRelationPreview.vue
@@ -0,0 +1,64 @@
+<script>
+import UnificationFramesList from "../Unification/UnificationFramesList.vue";
+
+export default {
+  props: {
+    initialUnifiedFrameId: Number,
+    initialLexicalUnitId: Number,
+    forceRefresh: Number,
+    hierarchyEditComponent: Object,
+  },
+  data () {
+    return {
+      unifiedFrameId: this.initialUnifiedFrameId
+    };
+  },
+  components: {UnificationFramesList},
+  emits: ['changeFrame', 'refreshEntriesList'],
+  methods: {
+    unifiedFrameSelected (unifiedFrameId) {
+      this.$emit('changeFrame', unifiedFrameId);
+    },
+    refreshEntriesList() {
+      this.$emit('refreshEntriesList')
+    },
+  },
+  mounted () {
+    Split(['#frame-preview-left-pane', '#frame-preview-right-pane'], {
+      sizes: [60, 40],
+      minSize: 20,
+      gutterSize: 4,
+      elementStyle: (dimension, size, gutterSize) => {
+        return {
+          'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)'
+        }
+      },
+    });
+  }
+};
+</script>
+
+<template>
+  <div class="row h-100 overflow-hidden">
+    <div :key="unifiedFrameId" class="col h-100 pr-0 pt-0 pb-0 overflow-auto" id="frame-preview-left-pane">
+      <component v-bind:is="hierarchyEditComponent"
+        v-if="unifiedFrameId !== -1"
+        :readOnly="true"
+        :unifiedFrameId="unifiedFrameId"
+        :forceRefresh="forceRefresh"
+        @refresh-entries-list="refreshEntriesList"
+      />
+      <div v-else class="h-100">
+        Brak ramy do wyświetlenia
+      </div>
+    </div>
+    <div class="col h-100 pl-1 pt-0 pb-0 overflow-auto" id="frame-preview-right-pane">
+<!--      <unification-switchable-list-->
+<!--        @unified-frame-selected="unifiedFrameSelected"-->
+<!--      />-->
+      <unification-frames-list
+            @unified-frame-selected="unifiedFrameSelected"
+      />
+    </div>
+  </div>
+</template>
diff --git a/frontend/src/components/unification/vertical_relations/VerticalRelationRightPane.vue b/frontend/src/components/unification/vertical_relations/VerticalRelationRightPane.vue
new file mode 100644
index 0000000..b240ac4
--- /dev/null
+++ b/frontend/src/components/unification/vertical_relations/VerticalRelationRightPane.vue
@@ -0,0 +1,66 @@
+<script>
+import VerticalRelationEdit from './VerticalRelationEdit.vue';
+
+export default {
+  components: {VerticalRelationEdit},
+  props: {
+    entryId: Number,
+    lexicalUnitId: Number,
+    initialUnifiedFrameId: Number,
+  },
+  emits: ['refreshEntriesList'],
+  data() {
+    return this.getInitialData();
+  },
+  methods: {
+    getInitialData() {
+      return {
+        key: this.lexicalUnitId,
+        entryIdLocal: this.entryId,
+        unifiedFrameId: this.initialUnifiedFrameId,
+        previewedUnifiedFrameId: -1
+      };
+    },
+    goToDisplay() {
+      this.unifiedFrameId = null;
+    },
+    refresh() {
+      this.key = null;
+      setTimeout(() => {
+        this.key = this.lexicalUnitId;
+      }, 0);
+    },
+    swapFrames(previewedUnifiedFrameId) {
+      this.previewedUnifiedFrameId = this.unifiedFrameId;
+      this.unifiedFrameId = previewedUnifiedFrameId;
+      this.refresh();
+    },
+    refreshEntriesList() {
+      this.$emit('refreshEntriesList');
+    }
+  },
+  watch: {
+    lexicalUnitId() {
+      Object.assign(this, this.getInitialData());
+    },
+    initialUnifiedFrameId() {
+      Object.assign(this, this.getInitialData());
+    }
+  },
+};
+</script>
+
+<template>
+  <div v-if="key || unifiedFrameId" :key="(key, entryIdLocal, unifiedFrameId)" class="row h-100 m-0 p-0 overflow-auto" id="semantics-top-pane">
+    <vertical-relation-edit
+      ref="hierarchyEdit"
+      v-if="unifiedFrameId"
+      :key="unifiedFrameId"
+      :unifiedFrameId="unifiedFrameId"
+      :previewedUnifiedFrameId="previewedUnifiedFrameId"
+      @go-to-display="goToDisplay"
+      @swap-frames="swapFrames"
+      @refresh-entries-list="refreshEntriesList"
+    />
+  </div>
+</template>
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 6162fa0..4771d25 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -7,6 +7,7 @@ import Entries from "./components/unification/Entries/Entries.vue";
 import Unification from "./components/unification/Unification/Unification.vue";
 import Hierarchy from "./components/unification/hierarchy/Hierarchy.vue";
 import FreeLu from "./components/unification/free_lu/FreeLu.vue";
+import VerticalRelation from "./components/unification/vertical_relations/VerticalRelation.vue";
 
 const router = createRouter({
   history: createWebHistory(),
@@ -15,6 +16,7 @@ const router = createRouter({
     { path: '/:lang/entries/unification/', component: Unification },
     { path: '/:lang/entries/hierarchy/', component: Hierarchy },
     { path: '/:lang/entries/lu_free/', component: FreeLu },
+    { path: '/:lang/entries/vertical-relations/', component: VerticalRelation },
     { path: '/:pathMatch(.*)*', component: null, name: '404' },
   ]
 });
diff --git a/shellvalier/settings.py b/shellvalier/settings.py
index 0fff663..96df497 100644
--- a/shellvalier/settings.py
+++ b/shellvalier/settings.py
@@ -68,6 +68,7 @@ INSTALLED_APPS = [
     'unifier.apps.UnifierConfig',
     'financial_settlement.apps.FinStatementConfig',
     'freelus.apps.FreeLusConfig',
+    'vertical_relations.apps.VerticalRelationsConfig',
 ]
 
 CRISPY_TEMPLATE_PACK = 'bootstrap4'
diff --git a/shellvalier/urls.py b/shellvalier/urls.py
index 2d79020..8ce3c85 100644
--- a/shellvalier/urls.py
+++ b/shellvalier/urls.py
@@ -20,6 +20,7 @@ urlpatterns = i18n_patterns(
     path('unifier/', include('unifier.urls')),
     path('financial_settlement/', include('financial_settlement.urls')),
     path('freelus/', include('freelus.urls')),
+    path('vertical_relations/', include('vertical_relations.urls')),
     path('', dash, name='dash'),
     # uncomment to leave default (Polish) urls unchanged
     #prefix_default_language=False,
diff --git a/syntax/management/commands/import_tei.py b/syntax/management/commands/import_tei.py
index e98c014..f1afea6 100644
--- a/syntax/management/commands/import_tei.py
+++ b/syntax/management/commands/import_tei.py
@@ -26,6 +26,7 @@ from syntax.models_phrase import (
     LemmaOperator, LemmaCooccur,
     ModificationType,
 )
+from vertical_relations.models import MainVerticalRelation, TemporalVerticalRelation
 
 
 class Command(BaseCommand):
@@ -357,3 +358,21 @@ def import_modification_types():
     for pri, name in modtypes:
         modtype = ModificationType(name=name, priority=pri)
         modtype.save()
+
+
+def import_vertical_relations():
+    main_relations = [(1, u'cause(s)'), (2, u'conditioned'), (3, u'presuppose(s) (pressuposition)'),
+                      (4, u'exclude(s)'), (5, u'antonimic'), (6, u'implicate(s)')]
+
+    for pri, name in main_relations:
+        relation = MainVerticalRelation(name=name, priority=pri)
+        relation.save()
+
+    temporal_relations = [(1, u'before'), (2, u'meets'), (3, u'overlaps'),
+                          (4, u'begins'), (5, u'during'), (6, u'ends'), (7, u'equals'),
+                          (8, u'after'), (9, u'met'), (10, u'overlapped'), (11, u'begun'),
+                          (12, u'contains'), (13, u'ended')]
+
+    for pri, name in temporal_relations:
+        relation = TemporalVerticalRelation(name=name, priority=pri)
+        relation.save()
diff --git a/unifier/views.py b/unifier/views.py
index e11c3b0..f7b4ada 100644
--- a/unifier/views.py
+++ b/unifier/views.py
@@ -27,6 +27,7 @@ from syntax.models import Schema
 from unifier.models import UnifiedFrameArgument, UnifiedRelationalSelectionalPreference, UnifiedFrame, \
     UnifiedFrame2SlowalFrameMapping, UnifiedFrameArgumentSlowalFrameMapping, HierarchyModel
 from users.models import Assignment
+from vertical_relations.models import UnifiedFrameVerticalRelation
 from . import choices
 from .apps import synset_hierarchy_dict, SynsetHierarchy
 from .choices import UnifiedFrameStatus
@@ -485,12 +486,28 @@ def get_unified_frame_json(unified_frame, request):
 
     unified_frame_dict = unified_frame_2_dict(unified_frame)
 
-    return {'unified_frame_id': unified_frame.id, 'unified_frame': unified_frame_dict, 'subentries': subentries,
+    vertical_relations = UnifiedFrameVerticalRelation.objects.filter(source_unified_frame=unified_frame)
+    vertical_relations = vertical_relations.prefetch_related("target_unified_frame") \
+        .prefetch_related("target_unified_frame").prefetch_related("main_vertical_relation")\
+        .prefetch_related("temporal_vertical_relation")\
+        .prefetch_related("iterated_target_unified_frame").prefetch_related("iterated_temporal_vertical_relation")
+
+    return {'vertical_relations': vertical_relations_2_dict(vertical_relations), 'unified_frame_id': unified_frame.id, 'unified_frame': unified_frame_dict, 'subentries': subentries,
             'frames': slowal_frames_dict, 'alternations': alternations, 'realisation_phrases': realisation_phrases,
             'realisation_descriptions': realisation_descriptions, 'examples': examples,
             'last_visited': request.session['last_visited']}
 
 
+def vertical_relations_2_dict(vertical_relations):
+    return [
+            {
+                'str': str(vertical_relation),
+                'id': vertical_relation.id,
+                'source_unified_frame_id': vertical_relation.source_unified_frame.id,
+            } for vertical_relation in sorted(vertical_relations.all(), key=lambda x: x.id)
+        ]
+
+
 @ajax_required
 @login_required
 def get_unified_frame(request):
diff --git a/vertical_relations/__init__.py b/vertical_relations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/vertical_relations/apps.py b/vertical_relations/apps.py
new file mode 100644
index 0000000..62debb1
--- /dev/null
+++ b/vertical_relations/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class VerticalRelationsConfig(AppConfig):
+    name = 'vertical_relations'
+
diff --git a/vertical_relations/management/commands/__init__.py b/vertical_relations/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/vertical_relations/management/commands/db_import_relation_entries.py b/vertical_relations/management/commands/db_import_relation_entries.py
new file mode 100644
index 0000000..2dcb4a3
--- /dev/null
+++ b/vertical_relations/management/commands/db_import_relation_entries.py
@@ -0,0 +1,15 @@
+from django.core.management.base import BaseCommand
+
+from syntax.management.commands.import_tei import import_vertical_relations
+
+
+class Command(BaseCommand):
+    args = ''
+    help = 'Import relation types to database.'
+
+    def handle(self, **options):
+        import_vertical_relations_cmd()
+
+
+def import_vertical_relations_cmd():
+    import_vertical_relations()
diff --git a/vertical_relations/models.py b/vertical_relations/models.py
new file mode 100644
index 0000000..c4eea01
--- /dev/null
+++ b/vertical_relations/models.py
@@ -0,0 +1,63 @@
+from django.db import models
+
+from unifier.models import UnifiedFrame, UnifiedFrameArgument
+
+
+class MainVerticalRelation(models.Model):
+    name = models.CharField(max_length=100)
+    priority = models.PositiveIntegerField()
+
+    class Meta:
+        ordering = ['priority']
+
+    def __str__(self):
+        return self.name
+
+
+class TemporalVerticalRelation(models.Model):
+    name = models.CharField(max_length=100)
+    priority = models.PositiveIntegerField()
+
+    class Meta:
+        ordering = ['priority']
+
+    def __str__(self):
+        return self.name
+
+
+class UnifiedFrameArgument2UnifiedFrameArgumentMapping(object):
+    pass
+
+
+class UnifiedFrameVerticalRelation(models.Model):
+    source_unified_frame = models.ForeignKey(UnifiedFrame, related_name='source_vertical_relation',
+                                             default=None, blank=True, null=True, on_delete=models.PROTECT)
+    target_unified_frame = models.ForeignKey(UnifiedFrame, related_name='target_vertical_relation',
+                                             default=None, blank=True, null=True, on_delete=models.PROTECT)
+    main_vertical_relation = models.ForeignKey(MainVerticalRelation, related_name='unified_frame_vertical_relation',
+                                               default=None, blank=True, null=True, on_delete=models.PROTECT)
+    temporal_vertical_relation = models.ManyToManyField(related_name='unified_frame_vertical_relation',
+                                                        to=TemporalVerticalRelation)
+    is_negated = models.BooleanField(default=False)
+
+    iterated_target_unified_frame = models.ForeignKey(UnifiedFrame, related_name='iterated_target_vertical_relation',
+                                                      default=None, blank=True, null=True, on_delete=models.PROTECT)
+    iterated_temporal_vertical_relation = models.ManyToManyField(related_name='iterated_unified_frame_vertical_relation',
+                                                                 to=TemporalVerticalRelation)
+
+    def __str__(self):
+        iter_str = ('(iter:' + self.iterated_target_unified_frame.ttile + ' - ' +
+         (','.join(self.iterated_temporal_vertical_relation.all()) if self.iterated_temporal_vertical_relation else '') + ')') if self.iterated_target_unified_frame else ''
+
+        return self.source_unified_frame.title + ':' + (self.target_unified_frame.title if self.target_unified_frame else '') + \
+               ' - ' + ('[neg]' if self.is_negated else '') + self.main_vertical_relation.name + ', ' + \
+               (','.join(self.temporal_vertical_relation.all()) if self.is_negated else '') + iter_str
+
+
+class UnifiedFrameArgument2UnifiedFrameArgumentMapping(models.Model):
+    source_unified_agrument = models.ForeignKey(UnifiedFrameArgument, related_name='source_unified_agrument_mapping',
+                                                on_delete=models.PROTECT)
+    target_unified_agrument = models.ForeignKey(UnifiedFrameArgument, related_name='target_unified_agrument_mapping', on_delete=models.PROTECT)
+    unified_frame_vertical_relation = models.ForeignKey(UnifiedFrameVerticalRelation,
+                                                        related_name='unified_frame_arguments_mapping', on_delete=models.PROTECT)
+
diff --git a/vertical_relations/urls.py b/vertical_relations/urls.py
new file mode 100644
index 0000000..0c597d4
--- /dev/null
+++ b/vertical_relations/urls.py
@@ -0,0 +1,15 @@
+from django.urls import path
+
+from . import views
+
+app_name = 'vertical_relations'
+
+urlpatterns = [
+    path('save_main_vertical_relation/', views.save_main_vertical_relation, name='save_main_vertical_relation'),
+    path('ajax_main_vertical_relations/', views.ajax_main_vertical_relations, name='ajax_main_vertical_relations'),
+    path('ajax_temporal_vertical_relations/', views.ajax_temporal_vertical_relations, name='ajax_temporal_vertical_relations'),
+    path('load_vertical_relation/', views.load_vertical_relation, name='load_vertical_relation'),
+    path('save_temporal_vertical_relation/', views.save_temporal_vertical_relation, name='save_temporal_vertical_relation'),
+    path('remove_vertical_relation/', views.remove_vertical_relation, name='remove_vertical_relation'),
+
+]
diff --git a/vertical_relations/views.py b/vertical_relations/views.py
new file mode 100644
index 0000000..ed17d73
--- /dev/null
+++ b/vertical_relations/views.py
@@ -0,0 +1,128 @@
+import json
+
+from django.db import transaction
+from django.http import JsonResponse
+
+from common.decorators import ajax_required, ajax
+from unifier.views import unified_frame_2_dict
+from vertical_relations.models import UnifiedFrameVerticalRelation, MainVerticalRelation, TemporalVerticalRelation
+
+
+@ajax(method='get', encode_result=True)
+def ajax_main_vertical_relations(request):
+    main_vertical_relations = []
+    for main_vertical_relation in MainVerticalRelation.objects.order_by('priority'):
+        main_vertical_relations.append({"id": main_vertical_relation.id, "name": main_vertical_relation.name})
+
+    context = {
+        'main_vertical_relations': main_vertical_relations,
+    }
+
+    return context
+
+
+@ajax(method='get', encode_result=True)
+def ajax_temporal_vertical_relations(request):
+    temporal_vertical_relations = []
+    for temporal_vertical_relation in TemporalVerticalRelation.objects.order_by('priority'):
+        temporal_vertical_relations.append({"id": temporal_vertical_relation.id, "name": temporal_vertical_relation.name})
+
+    context = {
+        'temporal_vertical_relations': temporal_vertical_relations,
+    }
+
+    return context
+
+
+@ajax_required
+@transaction.atomic
+def save_main_vertical_relation(request):
+    if request.method == 'POST':
+        source_unified_frame_id = request.POST['source_unified_frame_id']
+        target_unified_frame_id = request.POST['target_unified_frame_id']
+        main_vertical_relation_id = request.POST['main_vertical_relation_id']
+
+        if target_unified_frame_id:
+            relation, _ = UnifiedFrameVerticalRelation.objects.get_or_create(source_unified_frame_id=source_unified_frame_id,
+                                                                             target_unified_frame_id=target_unified_frame_id)
+        else:
+            relation, _ = UnifiedFrameVerticalRelation.objects.get_or_create(source_unified_frame_id=source_unified_frame_id,
+                                                                             target_unified_frame_id=None)
+
+        main_vertical_relation = MainVerticalRelation.objects.get(id=main_vertical_relation_id)
+        relation.main_vertical_relation = main_vertical_relation
+        relation.save()
+
+        return JsonResponse({'vertical_relation': get_vertical_relation(relation.id)})
+
+    return JsonResponse({})
+
+
+@ajax_required
+@transaction.atomic
+def save_temporal_vertical_relation(request):
+    if request.method == 'POST':
+        source_unified_frame_id = request.POST['source_unified_frame_id']
+        target_unified_frame_id = request.POST['target_unified_frame_id']
+        temporal_vertical_relation_ids = json.loads(request.POST['temporal_vertical_relation_ids'])
+
+        relation = UnifiedFrameVerticalRelation.objects.get(source_unified_frame_id=source_unified_frame_id,
+                                                            target_unified_frame_id=target_unified_frame_id if target_unified_frame_id else None)
+
+        for temporal_vertical_relation_id in temporal_vertical_relation_ids:
+            temporal_relation = TemporalVerticalRelation.objects.get(id=temporal_vertical_relation_id)
+            relation.temporal_vertical_relation.add(temporal_relation)
+
+        relation.save()
+
+        return JsonResponse({'vertical_relation': get_vertical_relation(relation.id)})
+
+    return JsonResponse({})
+
+
+@ajax_required
+@transaction.atomic
+def load_vertical_relation(request):
+    if request.method == 'POST':
+        vertical_relation_id = request.POST['vertical_relation_id']
+        return JsonResponse({'vertical_relation': get_vertical_relation(vertical_relation_id)})
+
+    return JsonResponse({})
+
+
+def relation2dict(vertical_relation):
+    return {
+        'id': vertical_relation.id,
+        'is_negated': vertical_relation.is_negated,
+        'target_unified_frame': unified_frame_2_dict(vertical_relation.target_unified_frame) if vertical_relation.target_unified_frame else None,
+        'main_vertical_relation': {
+            'id': vertical_relation.main_vertical_relation.id,
+            'name': vertical_relation.main_vertical_relation.name,
+        },
+        'temporal_vertical_relation': [{
+            'id': temporal_relation.id,
+            'name': temporal_relation.name,
+        } for temporal_relation in vertical_relation.temporal_vertical_relation.all()] if vertical_relation.temporal_vertical_relation else None,
+        'iterated_target_unified_frame': unified_frame_2_dict(vertical_relation.iterated_target_unified_frame) if vertical_relation.iterated_target_unified_frame else None,
+        'iterated_temporal_vertical_relation': [{
+            'id': temporal_relation.id,
+            'name': temporal_relation.name,
+        } for temporal_relation in vertical_relation.iterated_temporal_vertical_relation.all()] if vertical_relation.iterated_temporal_vertical_relation else None,
+    }
+
+
+def get_vertical_relation(vertical_relation_id):
+    vertical_relation = UnifiedFrameVerticalRelation.objects.get(id=vertical_relation_id)
+
+    return relation2dict(vertical_relation)
+
+
+@ajax_required
+@transaction.atomic
+def remove_vertical_relation(request):
+    if request.method == 'POST':
+        vertical_relation_id = request.POST['vertical_relation_id']
+        UnifiedFrameVerticalRelation.objects.get(id=vertical_relation_id).delete()
+        return JsonResponse({})
+
+    return JsonResponse({})
-- 
GitLab