From 45dc0a186ae5a18aa65254f51f9d43b866dd25e7 Mon Sep 17 00:00:00 2001
From: dcz2 <dcz@ipipan.waw.pl>
Date: Tue, 10 May 2022 20:14:42 +0200
Subject: [PATCH] Add a separate entries list for unification purposes

---
 common/templates/base.html                    |   2 +
 connections/models.py                         |   2 +
 entries/static/entries/js/entries.js          |  35 ++---
 entries/static/entries/js/entries_list.js     |  21 +++
 .../entries/js/unification_entries_list.js    |  76 +++++++++
 entries/static/entries/js/utils.js            |   6 +
 entries/templates/entries.html                | 142 +----------------
 entries/templates/entries_base.html           | 148 ++++++++++++++++++
 entries/templates/entry_display.html          |   2 +
 entries/templates/unification.html            |  15 ++
 .../templates/unification_entries_list.html   |  17 ++
 entries/urls.py                               |   5 +-
 entries/views.py                              |  55 +++++--
 meanings/models.py                            |   2 +
 semantics/choices.py                          |   8 +
 semantics/models.py                           |   7 +
 users/models.py                               |  15 ++
 17 files changed, 385 insertions(+), 173 deletions(-)
 create mode 100644 entries/static/entries/js/entries_list.js
 create mode 100644 entries/static/entries/js/unification_entries_list.js
 create mode 100644 entries/templates/entries_base.html
 create mode 100644 entries/templates/unification.html
 create mode 100644 entries/templates/unification_entries_list.html
 create mode 100644 semantics/choices.py
 create mode 100644 users/models.py

diff --git a/common/templates/base.html b/common/templates/base.html
index b041c54..93efcc3 100644
--- a/common/templates/base.html
+++ b/common/templates/base.html
@@ -24,7 +24,9 @@
     <script type="text/javascript" src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
     <script type="text/javascript">
         window.STATIC_URL = '{% static '' %}';
+        window.USER_USERNAME = '{{ request.user.username }}';
     </script>
+    {{ request.user.get_all_permissions|dictsort:0|json_script:"user-permissions" }}
     <script type="text/javascript" src="{% static 'common/js/utils.js' %}"></script>
     <script type="text/javascript" src="{% static 'common/js/init.js' %}"></script>
     <!-- translations: https://docs.djangoproject.com/en/2.2/topics/i18n/translation/#using-the-javascript-translation-catalog -->
diff --git a/connections/models.py b/connections/models.py
index 9b9ffb4..4ba3984 100644
--- a/connections/models.py
+++ b/connections/models.py
@@ -1,3 +1,4 @@
+from django.contrib.contenttypes.fields import GenericRelation
 from django.db import models
 
 from examples.models import Example
@@ -17,6 +18,7 @@ class Entry(models.Model):
     frames_count = models.PositiveIntegerField(null=False, default=0)
     lexical_units_count = models.PositiveIntegerField(null=False, default=0)
     import_error = models.BooleanField(default=False)
+    assignments = GenericRelation("users.Assignment", content_type_field="subject_ct", object_id_field="subject_id")
 
     class Meta:
         ordering = ['name']
diff --git a/entries/static/entries/js/entries.js b/entries/static/entries/js/entries.js
index ce013f8..aedb6d7 100644
--- a/entries/static/entries/js/entries.js
+++ b/entries/static/entries/js/entries.js
@@ -914,9 +914,9 @@ function get_show_reals_desc() {
     return $('#show-realisation-descriptions').prop('checked') === true;
 }
 
-function update_entries() {
-    
-    $('#entries-table').DataTable({
+function setup_datatable(options) {
+
+    var datatable = $('#entries-table').DataTable({
         // https://datatables.net/manual/tech-notes/3
         destroy: true,
         //paging: false,
@@ -926,16 +926,12 @@ function update_entries() {
         scrollY: $('#entries-list-div').height() - $('#entries-table_filter').outerHeight() - $('.dataTables_scrollHead').outerHeight() - $('#entries-table_info').outerHeight(),
         serverSide: true,
         ajax: {
-            url: '/' + lang + '/entries/get_entries/',
+            url: options.url,
             type: 'POST',
         },
         // https://datatables.net/reference/option/dom
         dom: 'ftri',
-        columns: [
-            { data: 'lemma' },
-            { data: 'status' },
-            { data: 'POS' },
-        ],
+        columns: options.columns,
         orderMulti: false,
         // show processing indicator when sorting etc.
         processing: true,
@@ -949,9 +945,10 @@ function update_entries() {
                     $(td).prop('scope', 'row');
                 }
             },
+            // add a padding to every cell
             {
                 className: 'p-1',
-                targets: [0, 1, 2],
+                targets: options.columns.map(function (column, index) { return index; } ),
             },
             // make only the lemma searchable
             {
@@ -966,14 +963,6 @@ function update_entries() {
             if (related) {
                 $(row).addClass('text-muted');
             }
-            $(row).click(function() {
-                var selected_entry = $(this).data('entry');
-                if (selected_entry !== curr_entry) {
-                    $('.entry[data-entry="' + curr_entry + '"]').removeClass('table-primary');
-                    get_entry(selected_entry, related);
-                    $(this).addClass('table-primary');
-                }
-            });
         },
         initComplete: function(settings, json) {
             // display the first entry once it’s loaded
@@ -993,8 +982,10 @@ function update_entries() {
             }
         }
     });
-    
+
     curr_entry = null;
+
+    return datatable;
 }
 
 function clear_results() {
@@ -1073,13 +1064,13 @@ $(document).ready(function() {
         },
     });
     
-    Split(['#semantics-top-pane', '#semantics-examples-pane'], {
+    $('#semantics-top-pane').length && Split(['#semantics-top-pane', '#semantics-examples-pane'], {
         direction: 'vertical',
         sizes: [75, 25],
         gutterSize: 4,
     });
     
-    Split(['#semantics-frames-pane', '#semantics-schemata-pane'], {
+    $('#semantics-frames-pane').length && Split(['#semantics-frames-pane', '#semantics-schemata-pane'], {
         sizes: [40, 60],
         minSize: 400,
         gutterSize: 4,
@@ -1090,7 +1081,7 @@ $(document).ready(function() {
         },
     });
     
-    Split(['#syntax-schemata-pane', '#syntax-examples-pane'], {
+    $('#semantics-schemata-pane').length && Split(['#syntax-schemata-pane', '#syntax-examples-pane'], {
         direction: 'vertical',
         sizes: [75, 25],
         gutterSize: 4,
diff --git a/entries/static/entries/js/entries_list.js b/entries/static/entries/js/entries_list.js
new file mode 100644
index 0000000..d5e8579
--- /dev/null
+++ b/entries/static/entries/js/entries_list.js
@@ -0,0 +1,21 @@
+function update_entries() {
+    var datatable = setup_datatable({
+        url: '/' + lang + '/entries/get_entries/',
+        columns: [
+            { data: 'lemma' },
+            { data: 'status' },
+            { data: 'POS' },
+        ]
+    });
+    datatable.on('click', 'tr.entry', function () {
+        var selected_entry = $(this).data('entry');
+        var data = datatable.row(this).data();
+        if (!data) return;
+        var related = data.related === true;
+        if (selected_entry !== curr_entry) {
+            $('.entry[data-entry="' + curr_entry + '"]').removeClass('table-primary');
+            get_entry(selected_entry, related);
+            $(this).addClass('table-primary');
+        }
+    });
+}
diff --git a/entries/static/entries/js/unification_entries_list.js b/entries/static/entries/js/unification_entries_list.js
new file mode 100644
index 0000000..e419a50
--- /dev/null
+++ b/entries/static/entries/js/unification_entries_list.js
@@ -0,0 +1,76 @@
+function update_entries() {
+    const can_see_assignees = has_permission("auth.view_user");
+
+    function is_assigned_to_user_renderer (data) {
+        return (
+            data
+            && data.lexical_units
+            && data.lexical_units.some(lu => lu.assignee_username === window.USER_USERNAME && lu.status == 'O')
+        ) ? gettext("tak") : gettext("nie");
+    }
+
+    var datatable = setup_datatable({
+        url: '/' + lang + '/entries/get_entries/?with_lexical_units=true',
+        columns: [
+            { data: 'lemma' },
+            { data: 'POS' },
+            can_see_assignees ? { data: 'assignee_username' } : { render: is_assigned_to_user_renderer },
+        ]
+    });
+    datatable.on('click', 'tr.entry', function () {
+        var row = datatable.row(this);
+        var has_drilldown = row.child.isShown();
+        $('.drilldown:visible').each(function () { datatable.row($(this).data("row")).child.hide(); });
+        if (!has_drilldown) {
+            if (!row.data()) return;
+            var drilldown = $("<div>").addClass("drilldown").data("row", this);
+            row.child(drilldown).show();
+            setup_lexical_units_table(drilldown, row.data().lexical_units, can_see_assignees);
+            drilldown.closest("td").addClass("p-0 pl-4");
+        }
+    });
+}
+
+function setup_lexical_units_table(drilldown, lexical_units, can_see_assignees) {
+    if (!lexical_units.length) {
+        return '';
+    }
+
+    function get_lexical_unit_row(lexical_unit) {
+        const is_assigned_to_user = lexical_unit.assignee_username === window.USER_USERNAME;
+        return $(`
+            <tr class="lexical-unit">
+                <td class="p-1">${lexical_unit.display}</td>
+                <td class="p-1">${lexical_unit.status}</td>
+                ` + (
+                    can_see_assignees
+                        ? `<td class="p-1">${lexical_unit.assignee_username || ""}</td>`
+                        : `<td class="p-1">${is_assigned_to_user ? gettext("tak") : gettext("nie")}</td>`
+                ) + `
+            </tr>
+        `).click(function () {
+            $(this).addClass('table-primary').siblings().removeClass("table-primary");
+            get_entry($(drilldown.data("row")).data("entry"), false);  // TODO replace with loading LU details view
+        });
+    }
+    var table = $(`
+        <table class="table">
+            <thead>
+                <tr>
+                    <th class="p-1">${gettext("Jednostka Leksykalna")}</th>
+                    <th class="p-1">${gettext("Status")}</th>
+                    ` + (
+                        can_see_assignees
+                            ? `<th class="p-1">${gettext("Leksykograf")}</th>`
+                            : `<th class="p-1">${gettext("Moje")}</th>`
+                    ) + `
+                </tr>
+            </thead>
+            <tbody></tbody>
+        </table>
+    `);
+    drilldown.append(table);
+    lexical_units.map(function (lexical_unit) {
+        $("tbody", table).append(get_lexical_unit_row(lexical_unit));
+    });
+}
diff --git a/entries/static/entries/js/utils.js b/entries/static/entries/js/utils.js
index a1cdcbd..9bc705d 100644
--- a/entries/static/entries/js/utils.js
+++ b/entries/static/entries/js/utils.js
@@ -12,3 +12,9 @@ function show_entry_spinners() {
     $('#syntax-schemata').append(spinner);
     $('#unmatched-examples').append(spinner);
 }
+
+var permissions = JSON.parse(document.getElementById('user-permissions').textContent);
+
+function has_permission(permission) {
+    return permissions.indexOf(permission) !== -1;
+}
diff --git a/entries/templates/entries.html b/entries/templates/entries.html
index 674d39f..0148c03 100644
--- a/entries/templates/entries.html
+++ b/entries/templates/entries.html
@@ -1,147 +1,15 @@
-{% extends "base.html" %}
+{% extends "entries_base.html" %}
 
 {% load i18n %}
 {% load static %}
-{% load crispy_forms_tags %}
 
 {% block title %}{% trans "Hasła" %}{% endblock %}
 
-{% block styles %}
-    <!-- for autocomplete -->
-    <link rel="stylesheet" type="text/css" href="//code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css">
-    <!-- https://datatables.net/ -->
-    <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/dt-1.10.22/sc-2.0.3/datatables.min.css"/>
-    <!--link rel="stylesheet" type="text/css" href="{% static 'entries/css/panels.css' %}"-->
-    <link rel="stylesheet" type="text/css" href="{% static 'entries/css/entries.css' %}">
-    <link rel="stylesheet" type="text/css" href="{% static 'common/css/role_colours.css' %}">
-{% endblock %}
-
 {% block scripts %}
-    <!-- https://www.cssscript.com/split-view/ -->
-    <script src="https://unpkg.com/split.js/dist/split.min.js"></script>
-    <!-- https://datatables.net/ -->
-    <script type="text/javascript" src="https://cdn.datatables.net/v/bs4/dt-1.10.22/sc-2.0.3/datatables.min.js"></script>
-    <script src="{% static 'common/js/csrf.js' %}"></script>
-    <!--script src="{% static 'entries/js/panels.js' %}"></script-->
-    <script src="{% static 'entries/js/forms.js' %}"></script>
-    <script src="{% static 'entries/js/utils.js' %}"></script>
-    <script src="{% static 'entries/js/entries.js' %}"></script>
-{% endblock %}
-
-{% block additional-nav-items %}
-<li class="nav-item dropdown">
-    <a class="nav-link dropdown-toggle text-light" href="#" id="nav-filters" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-        {% trans "Filtrowanie" %}
-    </a>
-    <div class="dropdown-menu" id="filters-visited-dropdown" aria-labelledby="nav-filters">
-        <a href="#" class="dropdown-item font-weight-bold text-dark text-uppercase" id="filter-button" data-toggle="modal" data-target="#entry-filters">
-            {% trans "Hasła" %}
-        </a>
-        <a href="#" class="dropdown-item font-weight-bold text-dark text-uppercase" id="filter-frames-button" data-toggle="modal" data-target="#frame-filters">
-            {% trans "Ramy" %}
-        </a>
-        <a href="#" class="dropdown-item font-weight-bold text-dark text-uppercase" id="filter-schemata-button" data-toggle="modal" data-target="#schema-filters">
-            {% trans "Schematy" %}
-        </a>
-    </div>
-</li>
-<li class="nav-item dropdown">
-    <a class="nav-link dropdown-toggle text-light" href="#" id="nav-last" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-        {% trans "Ostatnio oglÄ…dane" %}
-    </a>
-    <div class="dropdown-menu" id="last-visited-dropdown" aria-labelledby="nav-last">
-    {% for lemma, eid in request.session.last_visited|slice:":-1" %}
-        <a class="dropdown-item font-weight-bold text-dark text-uppercase last-visited" data-entry="{{ eid }}" href="#">{{ lemma }}</a>
-    {% endfor %}
-    </div>
-</li>
-<li class="nav-item dropdown">
-    <a class="nav-link dropdown-toggle text-light" href="#" id="nav-options" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-        {% trans "Opcje" %}
-    </a>
-    <div class="dropdown-menu px-1" id="options-dropdown" aria-labelledby="nav-options">
-        <div class="form-check custom-control custom-checkbox">
-            <input type="checkbox" class="custom-control-input" id="show-realisation-descriptions"{% if request.session.show_reals_desc %} checked{% endif %}>
-            <label class="custom-control-label" for="show-realisation-descriptions">
-            {% trans "Wyświetlaj opisy realizacji" %} <span data-toggle="tooltip" data-placement="bottom" title="{% trans "Po wybraniu ramy wyświetlaj opisy jej realizacji składniowych. Opisy (dla całej realizacji i dla poszczególnych fraz) są wyświetlane wewnątrz schematów." %}"><img src="{% static 'common/img/info.svg' %}" alt="info" width="10" height="10"/></span>
-            </label>
-        </div>
-        <div class="form-check custom-control custom-checkbox">
-            <input type="checkbox" class="custom-control-input" id="show-linked-entries"{% if request.session.show_linked_entries %} checked{% endif %}>
-            <label class="custom-control-label" for="show-linked-entries">
-            {% trans "Wyświetlaj powiązane hasła" %} <span data-toggle="tooltip" data-placement="bottom" data-html="true" title="{% trans "Przy filtrowaniu haseł wyświetlaj, oprócz haseł spełniających kryteria filtrowania, hasła powiązane z nimi znaczeniowo (np. <i>podarować</i> – <i>podarunek</i> – <i>podarek</i>). Hasła powiązane niespełniające kryteriów filtrowania są wyróżnione jaśniejszym kolorem na liście oraz nie podlegają filtrowaniu schematów i ram (są zawsze wyświetlane w całości niezależnie od użytych filtrów dla schematów/ram)." %}"><img src="{% static 'common/img/info.svg' %}" alt="info" width="10" height="10"/></span>
-            </label>
-        </div>
-    </div>
-</li>
-{% endblock %}
-
-{% block content %}
-
-<div class="row h-100 m-0 p-0 bg-secondary">
-    <!-- left panel: list of entries -->
-    <div id="entries-list" class="col h-100 w-100 px-0">
-        <div id="entries-list-div" class="col p-0 h-100 w-100 overflow-auto">
-            {% include "entries_list.html" %}
-        </div>
-    </div>
-    
-    <!-- right panel: entry display (syntax, semantics, examples) -->
-    <div id="entry-display" class="col h-100 p-0">
-        {% include "entry_display.html" %}
-    </div>
-</div>
-
+    {{ block.super }}
+    <script src="{% static 'entries/js/entries_list.js' %}"></script>
 {% endblock %}
 
-{% block modals %}
-
-<div class="modal fade" id="entry-filters" tabindex="-1" role="dialog" aria-labelledby="entry-filtersLabel" aria-hidden="true">
-  <div class="modal-dialog modal-xl" role="document">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h5 class="modal-title" id="entry-filtersLabel">{% trans "Filtrowanie haseł" %}</h5>
-        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
-          <span aria-hidden="true">&times;</span>
-        </button>
-      </div>
-      <div class="modal-body text-dark">
-        {% crispy entries_form %}
-      </div>
-    </div>
-  </div>
-</div>
+{% block left_pane %}{% include "entries_list.html" %}{% endblock %}
 
-<div class="modal fade" id="frame-filters" tabindex="-1" role="dialog" aria-labelledby="frame-filtersLabel" aria-hidden="true">
-  <div class="modal-dialog modal-xl" role="document">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h5 class="modal-title" id="frame-filtersLabel">{% trans "Filtrowanie ram" %}</h5>
-        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
-          <span aria-hidden="true">&times;</span>
-        </button>
-      </div>
-      <div class="modal-body text-dark">
-        {% crispy frames_form %}
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="modal fade" id="schema-filters" tabindex="-1" role="dialog" aria-labelledby="schema-filtersLabel" aria-hidden="true">
-  <div class="modal-dialog modal-xl" role="document">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h5 class="modal-title" id="schema-filtersLabel">{% trans "Filtrowanie schematów" %}</h5>
-        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
-          <span aria-hidden="true">&times;</span>
-        </button>
-      </div>
-      <div class="modal-body text-dark">
-        {% crispy schemata_form %}
-      </div>
-    </div>
-  </div>
-</div>
-
-{% endblock %}
+{% block right_pane %}{% include "entry_display.html" with show_tabs=True %}{% endblock %}
diff --git a/entries/templates/entries_base.html b/entries/templates/entries_base.html
new file mode 100644
index 0000000..7b43abc
--- /dev/null
+++ b/entries/templates/entries_base.html
@@ -0,0 +1,148 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+{% load static %}
+{% load crispy_forms_tags %}
+
+{% block styles %}
+    <!-- for autocomplete -->
+    <link rel="stylesheet" type="text/css" href="//code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css">
+    <!-- https://datatables.net/ -->
+    <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/dt-1.10.22/sc-2.0.3/datatables.min.css"/>
+    <!--link rel="stylesheet" type="text/css" href="{% static 'entries/css/panels.css' %}"-->
+    <link rel="stylesheet" type="text/css" href="{% static 'entries/css/entries.css' %}">
+    <link rel="stylesheet" type="text/css" href="{% static 'common/css/role_colours.css' %}">
+{% endblock %}
+
+{% block scripts %}
+    <!-- https://www.cssscript.com/split-view/ -->
+    <script src="https://unpkg.com/split.js/dist/split.min.js"></script>
+    <!-- https://datatables.net/ -->
+    <script type="text/javascript" src="https://cdn.datatables.net/v/bs4/dt-1.10.22/sc-2.0.3/datatables.min.js"></script>
+    <script src="{% static 'common/js/csrf.js' %}"></script>
+    <!--script src="{% static 'entries/js/panels.js' %}"></script-->
+    <script src="{% static 'entries/js/forms.js' %}"></script>
+    <script src="{% static 'entries/js/utils.js' %}"></script>
+    <script src="{% static 'entries/js/entries.js' %}"></script>
+{% endblock %}
+
+{% block additional-nav-items %}
+{% if request.user.is_authenticated %}
+    <li class="nav-item"><a href="{% url 'entries:unification' %}" class="nav-link">{% trans "Unifikacja" %}</a></li>
+{% endif %}
+<li class="nav-item dropdown">
+    <a class="nav-link dropdown-toggle" href="#" id="nav-filters" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+        {% trans "Filtrowanie" %}
+    </a>
+    <div class="dropdown-menu" id="filters-visited-dropdown" aria-labelledby="nav-filters">
+        <a href="#" class="dropdown-item font-weight-bold text-dark text-uppercase" id="filter-button" data-toggle="modal" data-target="#entry-filters">
+            {% trans "Hasła" %}
+        </a>
+        <a href="#" class="dropdown-item font-weight-bold text-dark text-uppercase" id="filter-frames-button" data-toggle="modal" data-target="#frame-filters">
+            {% trans "Ramy" %}
+        </a>
+        <a href="#" class="dropdown-item font-weight-bold text-dark text-uppercase" id="filter-schemata-button" data-toggle="modal" data-target="#schema-filters">
+            {% trans "Schematy" %}
+        </a>
+    </div>
+</li>
+<li class="nav-item dropdown">
+    <a class="nav-link dropdown-toggle" href="#" id="nav-last" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+        {% trans "Ostatnio oglÄ…dane" %}
+    </a>
+    <div class="dropdown-menu" id="last-visited-dropdown" aria-labelledby="nav-last">
+    {% for lemma, eid in request.session.last_visited|slice:":-1" %}
+        <a class="dropdown-item font-weight-bold text-dark text-uppercase last-visited" data-entry="{{ eid }}" href="#">{{ lemma }}</a>
+    {% endfor %}
+    </div>
+</li>
+<li class="nav-item dropdown">
+    <a class="nav-link dropdown-toggle" href="#" id="nav-options" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+        {% trans "Opcje" %}
+    </a>
+    <div class="dropdown-menu px-1" id="options-dropdown" aria-labelledby="nav-options">
+        <div class="form-check custom-control custom-checkbox">
+            <input type="checkbox" class="custom-control-input" id="show-realisation-descriptions"{% if request.session.show_reals_desc %} checked{% endif %}>
+            <label class="custom-control-label" for="show-realisation-descriptions">
+            {% trans "Wyświetlaj opisy realizacji" %} <span data-toggle="tooltip" data-placement="bottom" title="{% trans "Po wybraniu ramy wyświetlaj opisy jej realizacji składniowych. Opisy (dla całej realizacji i dla poszczególnych fraz) są wyświetlane wewnątrz schematów." %}"><img src="{% static 'common/img/info.svg' %}" alt="info" width="10" height="10"/></span>
+            </label>
+        </div>
+        <div class="form-check custom-control custom-checkbox">
+            <input type="checkbox" class="custom-control-input" id="show-linked-entries"{% if request.session.show_linked_entries %} checked{% endif %}>
+            <label class="custom-control-label" for="show-linked-entries">
+            {% trans "Wyświetlaj powiązane hasła" %} <span data-toggle="tooltip" data-placement="bottom" data-html="true" title="{% trans "Przy filtrowaniu haseł wyświetlaj, oprócz haseł spełniających kryteria filtrowania, hasła powiązane z nimi znaczeniowo (np. <i>podarować</i> – <i>podarunek</i> – <i>podarek</i>). Hasła powiązane niespełniające kryteriów filtrowania są wyróżnione jaśniejszym kolorem na liście oraz nie podlegają filtrowaniu schematów i ram (są zawsze wyświetlane w całości niezależnie od użytych filtrów dla schematów/ram)." %}"><img src="{% static 'common/img/info.svg' %}" alt="info" width="10" height="10"/></span>
+            </label>
+        </div>
+    </div>
+</li>
+{% endblock %}
+
+{% block content %}
+
+<div class="row h-100 m-0 p-0 bg-secondary">
+    <!-- left panel: list of entries -->
+    <div id="entries-list" class="col h-100 w-100 px-0">
+        <div id="entries-list-div" class="col p-0 h-100 w-100 overflow-auto">
+            {% block left_pane %}{% endblock %}
+        </div>
+    </div>
+    
+    <!-- right panel: entry display (syntax, semantics, examples) -->
+    <div id="entry-display" class="col h-100 p-0">
+        {% block right_pane %}{% endblock %}
+    </div>
+</div>
+
+{% endblock %}
+
+{% block modals %}
+
+<div class="modal fade" id="entry-filters" tabindex="-1" role="dialog" aria-labelledby="entry-filtersLabel" aria-hidden="true">
+  <div class="modal-dialog modal-xl" role="document">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h5 class="modal-title" id="entry-filtersLabel">{% trans "Filtrowanie haseł" %}</h5>
+        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+          <span aria-hidden="true">&times;</span>
+        </button>
+      </div>
+      <div class="modal-body text-dark">
+        {% crispy entries_form %}
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="modal fade" id="frame-filters" tabindex="-1" role="dialog" aria-labelledby="frame-filtersLabel" aria-hidden="true">
+  <div class="modal-dialog modal-xl" role="document">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h5 class="modal-title" id="frame-filtersLabel">{% trans "Filtrowanie ram" %}</h5>
+        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+          <span aria-hidden="true">&times;</span>
+        </button>
+      </div>
+      <div class="modal-body text-dark">
+        {% crispy frames_form %}
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="modal fade" id="schema-filters" tabindex="-1" role="dialog" aria-labelledby="schema-filtersLabel" aria-hidden="true">
+  <div class="modal-dialog modal-xl" role="document">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h5 class="modal-title" id="schema-filtersLabel">{% trans "Filtrowanie schematów" %}</h5>
+        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+          <span aria-hidden="true">&times;</span>
+        </button>
+      </div>
+      <div class="modal-body text-dark">
+        {% crispy schemata_form %}
+      </div>
+    </div>
+  </div>
+</div>
+
+{% endblock %}
diff --git a/entries/templates/entry_display.html b/entries/templates/entry_display.html
index cf620b8..42d07fa 100644
--- a/entries/templates/entry_display.html
+++ b/entries/templates/entry_display.html
@@ -1,5 +1,6 @@
 {% load i18n %}
 
+{% if show_tabs %}
 <ul class="nav nav-pills nav-justified p-1" id="entryTabs" role="tablist">
     <li class="nav-item mr-1">
         <a class="btn btn-sm btn-outline-dark nav-link active" id="semantics-tab" data-toggle="tab" href="#semantics" role="tab" aria-controls="semantics" aria-selected="true">
@@ -17,6 +18,7 @@
         </a>
     </li>
 </ul>
+{% endif %}
 
 <div class="tab-content h-100 w-100 p-0" id="entryTabsContent">
     <div class="col h-100 w-100 p-0 tab-pane show active" id="semantics" role="tabpanel" aria-labelledby="semantics-tab">
diff --git a/entries/templates/unification.html b/entries/templates/unification.html
new file mode 100644
index 0000000..a0bf18d
--- /dev/null
+++ b/entries/templates/unification.html
@@ -0,0 +1,15 @@
+{% extends "entries_base.html" %}
+
+{% load i18n %}
+{% load static %}
+
+{% block title %}{% trans "Hasła" %}{% endblock %}
+
+{% block scripts %}
+    {{ block.super }}
+    <script src="{% static 'entries/js/unification_entries_list.js' %}"></script>
+{% endblock %}
+
+{% block left_pane %}{% include "unification_entries_list.html" %}{% endblock %}
+
+{% block right_pane %}{% include "entry_display.html" %}{% endblock %}
diff --git a/entries/templates/unification_entries_list.html b/entries/templates/unification_entries_list.html
new file mode 100644
index 0000000..48b7373
--- /dev/null
+++ b/entries/templates/unification_entries_list.html
@@ -0,0 +1,17 @@
+{% load i18n %}
+
+<table id="entries-table" class="table table-sm table-hover text-dark">
+    <thead>
+        <tr>
+            <th class="p-1">{% trans "Lemat" %}</th>
+            <th class="p-1">{% trans "Część mowy" %}</th>
+            {% if perms.users.view_user %}
+                <th class="p-1">{% trans "Semantyk" %}</th>
+            {% else %}
+                <th class="p-1">{% trans "Moje (w opracowaniu)" %}</th>
+            {% endif %}
+        </tr>
+    </thead>
+    <tbody id="entries">
+    </tbody>
+</table>
diff --git a/entries/urls.py b/entries/urls.py
index 3c49764..37deeed 100644
--- a/entries/urls.py
+++ b/entries/urls.py
@@ -14,9 +14,10 @@ urlpatterns = [
     path('get_subform/', views.get_subform, name='get_subform'),
     path('change_show_reals_desc/', views.change_show_reals_desc, name='change_show_reals_desc'),
     path('change_show_linked_entries/', views.change_show_linked_entries, name='change_show_linked_entries'),
-    
+    path('unification', views.unification, name='unification'),
+
     path('autocomplete/', autocompletes.autocomplete, name='autocomplete'),
-    
+
     # TODO remove!
     #path('test/', views.test, name='test'),
     path('', views.entries, name='entries'),
diff --git a/entries/views.py b/entries/views.py
index fa20fa6..7d026a7 100644
--- a/entries/views.py
+++ b/entries/views.py
@@ -7,7 +7,8 @@ from itertools import chain, product
 
 import simplejson
 
-from django.db.models import Q
+from django.contrib.auth.decorators import login_required
+from django.db.models import Prefetch, Q
 from django.http import JsonResponse, QueryDict
 from django.shortcuts import render
 from django.template.context_processors import csrf
@@ -65,9 +66,22 @@ def entries(request):
             'schemata_form' : SchemaFormFactory.get_form(as_subform=False)
         })
 
+
+@login_required
+def unification(request):
+    return render(
+        request,
+        'unification.html',
+        {
+            'entries_form' : EntryForm(),
+            'frames_form': FrameFormFactory.get_form(as_subform=False),
+            'schemata_form': SchemaFormFactory.get_form(as_subform=False)
+        },
+    )
+
+
 FORM_TYPES = {
     'entry'      : EntryForm,
-    
 }
 
 FORM_FACTORY_TYPES = {
@@ -336,6 +350,7 @@ def get_entries(request):
         # form should already be validated if it passed through send_form
         assert(not errors_dict)
         scroller_params = get_scroller_params(request.POST)
+        with_lexical_units = request.GET.get('with_lexical_units') == 'true'
         entries = get_filtered_objects(forms).filter(import_error=False)
         
         # TODO restrictions for testing – remove!!!
@@ -351,10 +366,10 @@ def get_entries(request):
         linked_ids = set()
         if request.session['show_linked_entries']:
             entries_linked = Entry.objects.filter(subentries__schema_hooks__argument_connections__schema_connections__subentry__entry__in=entries).distinct().exclude(id__in=entries)
-            entries = entries.union(entries_linked)
+            entries = entries | entries_linked
             linked_ids = set(e.id for e in entries_linked)
         
-        i, j = scroller_params['start'], scroller_params['start'] + scroller_params['length']
+        first_index, last_index = scroller_params['start'], scroller_params['start'] + scroller_params['length']
         order_field, order_dir = scroller_params['order']
         if order_field == 0:
             order_field = 'name'
@@ -364,22 +379,38 @@ def get_entries(request):
             order_field = 'pos__tag'
         if order_dir == 'desc':
             order_field = '-' + order_field
-        entries = entries.order_by(order_field)
+        entries = entries.order_by(order_field).only('id', 'name', 'status__key', 'pos__tag')
+        if with_lexical_units:
+            entries = entries.prefetch_related("lexical_units")
         status_names = STATUS()
         POS_names = POS()
-        entries_list = list(entries.values('id', 'name', 'status__key', 'pos__tag'))
         result = {
             'draw' : scroller_params['draw'],
             'recordsTotal': total,
             'recordsFiltered': filtered,
             'data': [
                 {
-                    'id'      : e['id'],
-                    'lemma'   : e['name'],
-                    'status'  : status_names[e['status__key']],
-                    'POS'     : POS_names[e['pos__tag']],
-                    'related' : e['id'] in linked_ids,
-                } for e in entries_list[i:j]
+                    'id'      : e.id,
+                    'lemma'   : e.name,
+                    'status'  : status_names[e.status.key],
+                    'POS'     : POS_names[e.pos.tag],
+                    'related' : e.id in linked_ids,
+                    'assignee_username': assignment.user.username if (assignment := e.assignments.first()) else None,
+                    **(
+                        {
+                            'lexical_units': [
+                                {
+                                    'display': str(lu),
+                                    'assignee_username': (
+                                        assignment.user.username if (assignment := lu.assignments.first()) else None
+                                    ),
+                                    'status': frame.status if (frame := lu.frames.first()) else ""
+                                } for lu in e.lexical_units.all()
+                            ]
+                        }
+                        if with_lexical_units else {}
+                    ),
+                } for e in list(entries)[first_index:last_index]
             ],
         }
         return JsonResponse(result)
diff --git a/meanings/models.py b/meanings/models.py
index 7a46b0a..3eaaabf 100644
--- a/meanings/models.py
+++ b/meanings/models.py
@@ -1,3 +1,4 @@
+from django.contrib.contenttypes.fields import GenericRelation
 from django.db import models
 
 
@@ -12,6 +13,7 @@ class LexicalUnit(models.Model):
     definition = models.TextField(default='')
     gloss = models.TextField(default='')
     text_rep = models.TextField()
+    assignments = GenericRelation("users.Assignment", content_type_field="subject_ct", object_id_field="subject_id")
 
     class Meta:
         unique_together = ('base', 'sense', 'pos',)
diff --git a/semantics/choices.py b/semantics/choices.py
new file mode 100644
index 0000000..d248493
--- /dev/null
+++ b/semantics/choices.py
@@ -0,0 +1,8 @@
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+
+class LexicalUnitStatus(models.TextChoices):
+    PROCESSING = "O", _("w obróbce")
+    READY = "G", _("gotowe")
+    VERIFIED = "S", _("sprawdzone")
diff --git a/semantics/models.py b/semantics/models.py
index 8327300..17b8cee 100644
--- a/semantics/models.py
+++ b/semantics/models.py
@@ -2,11 +2,18 @@ from django.db import models
 
 from meanings.models import LexicalUnit, Synset
 
+from . import choices
+
 
 class Frame(models.Model):
     lexical_units = models.ManyToManyField(LexicalUnit, related_name='frames')
     opinion = models.ForeignKey('FrameOpinion', on_delete=models.PROTECT)
     arguments_count = models.PositiveIntegerField(null=False, default=0)
+    status = models.TextField(
+        max_length=10,
+        choices=choices.LexicalUnitStatus.choices,
+        default=choices.LexicalUnitStatus.PROCESSING,
+    )
 
     def sorted_arguments(self):  # TODO: zaimplementowac wlasciwe sortowanie
         return Argument.objects.filter(frame=self)
diff --git a/users/models.py b/users/models.py
new file mode 100644
index 0000000..91ce143
--- /dev/null
+++ b/users/models.py
@@ -0,0 +1,15 @@
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+
+
+class Assignment(models.Model):
+    user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
+    subject_ct = models.ForeignKey(ContentType, on_delete=models.PROTECT)
+    subject_id = models.PositiveIntegerField()
+    subject = GenericForeignKey('subject_ct', 'subject_id')
+
+    class Meta:
+        unique_together = [
+            ("subject_ct", "subject_id"),
+        ]
-- 
GitLab