From 5d5c05e57b416bb489667da19b26200e2681c52e Mon Sep 17 00:00:00 2001
From: dcz2 <dcz@ipipan.waw.pl>
Date: Wed, 6 Apr 2022 22:01:14 +0200
Subject: [PATCH] Auth views, creating Permissions and Groups, static files
 cache bump

---
 common/static/common/css/common.css           |   5 +
 common/static/common/js/utils.js              |   2 +-
 common/templates/base.html                    |  27 ++-
 .../templates/dictionary_statistics.html      |   4 +-
 entries/forms.py                              |   4 +-
 entries/static/entries/css/entries.css        |   8 +-
 entries/static/entries/js/entries.js          |  10 +-
 .../checkboxselectmultiple_with_tooltips.html |   3 +-
 entries/templates/entries.html                |   4 +-
 .../role_checkboxselectmultiple_inner.html    |   2 +-
 entries/templates/test.html                   |   2 +-
 filters/static/filters/js/filters_test.js     |   4 +-
 locale/en/LC_MESSAGES/django.mo               | Bin 33488 -> 34225 bytes
 locale/en/LC_MESSAGES/django.po               | 167 +++++++++++++-----
 locale/en/LC_MESSAGES/djangojs.po             | 100 +++++------
 .../templates/phrase_expansions.html          |   2 +-
 reset_db.sh                                   |   1 +
 shellvalier/settings.py                       |  10 +-
 shellvalier/urls.py                           |   1 +
 users/__init__.py                             |   0
 users/apps.py                                 |   5 +
 users/forms.py                                |  87 +++++++++
 users/management/__init__.py                  |   0
 users/management/commands/__init__.py         |   0
 .../commands/create_groups_and_permissions.py |  22 +++
 users/templates/registration/login.html       |  19 ++
 .../registration/password_reset.html          |  19 ++
 users/templates/user_form.html                |  13 ++
 users/templates/user_list.html                |  38 ++++
 users/templates/user_profile.html             |  13 ++
 users/urls.py                                 |  29 +++
 users/views.py                                |  48 +++++
 32 files changed, 529 insertions(+), 120 deletions(-)
 create mode 100644 users/__init__.py
 create mode 100644 users/apps.py
 create mode 100644 users/forms.py
 create mode 100644 users/management/__init__.py
 create mode 100644 users/management/commands/__init__.py
 create mode 100644 users/management/commands/create_groups_and_permissions.py
 create mode 100644 users/templates/registration/login.html
 create mode 100644 users/templates/registration/password_reset.html
 create mode 100644 users/templates/user_form.html
 create mode 100644 users/templates/user_list.html
 create mode 100644 users/templates/user_profile.html
 create mode 100644 users/urls.py
 create mode 100644 users/views.py

diff --git a/common/static/common/css/common.css b/common/static/common/css/common.css
index f54a020..fa4ecbd 100644
--- a/common/static/common/css/common.css
+++ b/common/static/common/css/common.css
@@ -29,6 +29,11 @@ main {
     max-width: 500px;
 }
 
+.btn-xs {
+    padding: .2rem .4rem;
+    line-height: 0.8rem;
+}
+
 /* TODO: doesn’t work under Firefox 89, possibly older too */
 ::-webkit-scrollbar {
     width: 4px;
diff --git a/common/static/common/js/utils.js b/common/static/common/js/utils.js
index 2c5106e..8e8763c 100644
--- a/common/static/common/js/utils.js
+++ b/common/static/common/js/utils.js
@@ -15,7 +15,7 @@ function tooltipped_span(text, tooltip_text, cls) {
 }
 
 function tooltipped_info(text) {
-    return tooltipped_span('<img src="/static/common/img/info.svg" alt="info" width="14" height="14"/>', text);
+    return tooltipped_span('<img src="' + window.STATIC_URL + 'common/img/info.svg" alt="info" width="14" height="14"/>', text);
 }
 
 function activate_tooltips(selector) {
diff --git a/common/templates/base.html b/common/templates/base.html
index a24c611..b041c54 100644
--- a/common/templates/base.html
+++ b/common/templates/base.html
@@ -10,7 +10,7 @@
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     <title>{% block title %}{% endblock %} – Walenty [beta]</title>
-    <link rel="icon" href="/static/common/favicon.ico">
+    <link rel="icon" href="{% static 'common/favicon.ico' %}">
     <link rel="stylesheet" type="text/css" href="https://bootswatch.com/4/lux/bootstrap.min.css">
     <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;700&display=swap">
     {% block styles %}{% endblock %}
@@ -22,6 +22,9 @@
     <!--Bootstrap’s tooltips require Popper-->
     <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
     <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 '' %}';
+    </script>
     <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 -->
@@ -60,9 +63,31 @@
                     {% trans "Statystyki" %}
                     </a>
                 </li>
+                {% if perms.users.view_user %}
+                    <li class="nav-item" id="nav-users">
+                        <a class="nav-link text-light" href="{% url 'users:user_list' %}">
+                        {% trans "Użytkownicy" %}
+                        </a>
+                    </li>
+                {% endif %}
             </ul>
         </div>
         <span id="import-status" class="navbar-text text-warning mr-3"></span>
+        {% if request.user.is_authenticated %}
+            <div class="dropdown mr-3">
+                <a href="#" class="btn btn-sm btn-outline-light dropdown-toggle" data-toggle="dropdown">{{ request.user.get_full_name|default:request.user.username }}</a>
+                <div class="dropdown-menu dropdown-menu-right">
+                    <a href="{% url 'users:user_profile' %}" class="dropdown-item font-weight-bold text-dark text-uppercase">
+                        {% trans "Twój profil" %}
+                    </a>
+                    <a href="{% url 'users:logout' %}" class="dropdown-item font-weight-bold text-dark text-uppercase">
+                        {% trans "Wyloguj siÄ™" %}
+                    </a>
+                </div>
+            </div>
+        {% else %}
+            <a id="login-btn" class="btn btn-sm btn-outline-light mr-3" href="{% url 'users:login' %}">{% trans "Zaloguj siÄ™" %}</a>
+        {% endif %}
         <a
             id="lang-btn"
             class="btn btn-sm btn-outline-light"
diff --git a/dictionary_statistics/templates/dictionary_statistics.html b/dictionary_statistics/templates/dictionary_statistics.html
index 0b9d366..9736e81 100644
--- a/dictionary_statistics/templates/dictionary_statistics.html
+++ b/dictionary_statistics/templates/dictionary_statistics.html
@@ -47,7 +47,7 @@
                 <tr>
                     {% for opinion, opinion_key, n in schema_stats %}
                         <th style="width: 10em;" scope="col">
-                            {% if opinion_key != "all" %}<img src="/static/entries/img/{{ opinion_key }}.svg" width="12" height="12" alt="{{ opinion }}"> {% endif %}{{ opinion }}
+                            {% if opinion_key != "all" %}<img src="{% static 'entries/img' %}/{{ opinion_key }}.svg" width="12" height="12" alt="{{ opinion }}"> {% endif %}{{ opinion }}
                         </th>
                     {% endfor %}
                 </tr>
@@ -71,7 +71,7 @@
                 <tr>
                     {% for opinion, opinion_key, n in frame_stats %}
                         <th scope="col">
-                            {% if opinion_key != "all" %}<img src="/static/entries/img/{{ opinion_key }}.svg" width="12" height="12" alt="{{ opinion }}"> {% endif %}{{ opinion }}
+                            {% if opinion_key != "all" %}<img src="{% static 'entries/img' %}/{{ opinion_key }}.svg" width="12" height="12" alt="{{ opinion }}"> {% endif %}{{ opinion }}
                         </th>
                     {% endfor %}
                 </tr>
diff --git a/entries/forms.py b/entries/forms.py
index c104bad..c30edf0 100644
--- a/entries/forms.py
+++ b/entries/forms.py
@@ -1,7 +1,7 @@
 from random import randint
 
 from django import forms
-
+from django.conf import settings
 from django.db.models import OneToOneField, ForeignKey, CharField, ManyToManyField, Q
 
 from django.utils.text import format_lazy
@@ -118,7 +118,7 @@ def and_or_form_creator(button_label, button_id, field=None, data_add=None, add_
     if help:
         help_tooltip = ''
         if tooltip:
-            help_tooltip = ' <span data-toggle="tooltip" data-placement="bottom" title="{}"><img src="/static/common/img/info.svg" alt="info" width="12" height="12"/></span>'.format(tooltip)
+            help_tooltip = f' <span data-toggle="tooltip" data-placement="bottom" title="{tooltip}"><img src="{settings.STATIC_URL}common/img/info.svg" alt="info" width="12" height="12"/></span>'
         ret.insert(-1, layout.HTML('<small class="form-text text-muted">{}{}</small>'.format(help, help_tooltip)))
     return ret
 
diff --git a/entries/static/entries/css/entries.css b/entries/static/entries/css/entries.css
index 17b7bea..4a6d18c 100644
--- a/entries/static/entries/css/entries.css
+++ b/entries/static/entries/css/entries.css
@@ -156,7 +156,7 @@ legend {
 }
 
 .negated {
-    background-image: url("/static/entries/img/negated.png");
+    background-image: url("../img/negated.png");
     background-repeat: repeat;
 }
 
@@ -170,7 +170,7 @@ legend {
 }
 
 .frame.highlight .lemma, .example-role .lemma, .frame.active .lemma, .phrase.lemma {
-    background-image: url("/static/entries/img/lemma.png");
+    background-image: url("../img/lemma.png");
     background-repeat: repeat;
 }
 
@@ -196,12 +196,12 @@ legend {
 }
 
 .gutter.gutter-horizontal {
-    background-image: url("/static/entries/img/gutter-h.png");
+    background-image: url("../img/gutter-h.png");
     cursor: col-resize;
 }
 
 .gutter.gutter-vertical {
-    background-image: url("/static/entries/img/gutter-v.png");
+    background-image: url("../img/gutter-v.png");
     cursor: row-resize;
 }
 
diff --git a/entries/static/entries/js/entries.js b/entries/static/entries/js/entries.js
index 7230452..ce013f8 100644
--- a/entries/static/entries/js/entries.js
+++ b/entries/static/entries/js/entries.js
@@ -13,7 +13,7 @@ function make_opinion_row(item, span, width) {
     var opinion_row = document.createElement('tr');
     opinion_row.className = 'opinion-row';
     opinion_row.innerHTML = '<th scope="row" class="py-2 px-1 text-secondary" style="width: ' + width + 'em;">' + gettext('Opinia') + '</td>';
-    opinion_row.innerHTML += '<td class="opinion-cell py-2 px-1" colspan="' + span + '"><img src="/static/entries/img/' + item.opinion_key + '.svg" width="12" height="12" alt="' + item.opinion + '"> ' + item.opinion + '</td>';
+    opinion_row.innerHTML += '<td class="opinion-cell py-2 px-1" colspan="' + span + '"><img src="' + window.STATIC_URL + 'entries/img/' + item.opinion_key + '.svg" width="12" height="12" alt="' + item.opinion + '"> ' + item.opinion + '</td>';
     return opinion_row;
 }
 
@@ -126,9 +126,9 @@ function frame2dom(frame) {
             lu_html += ' ' + tooltipped_info('<i>' + tooltip.join('; ') + '</i>');
         }
         if (lu.url) {
-            //lu_html += ' ' + tooltipped_span('<a href="' + lu.url + '" target="_blank"><img src="/static/common/img/plwn.svg" alt="external link" height="14"/></a>', gettext('Przejdź do strony tej jednostki w <i>Słowosieci</i>'), 'plwn-url');
-            //lu_html += ' ' + tooltipped_span('<a href="' + lu.url + '" target="_blank"><img src="/static/common/img/ext-link.svg" alt="external link" height="14"/></a>', gettext('Przejdź do strony tej jednostki w <i>Słowosieci</i>'), 'plwn-url');
-            lu_html += ' <a class="lu-plwn" href="' + lu.url + '" target="_blank"><img src="/static/common/img/ext-link.svg" alt="external link" height="14"/></a>';
+//            lu_html += ' ' + tooltipped_span('<a href="' + lu.url + '" target="_blank"><img src="' + window.STATIC_URL + 'common/img/plwn.svg" alt="external link" height="14"/></a>', gettext('Przejdź do strony tej jednostki w <i>Słowosieci</i>'), 'plwn-url');
+//            lu_html += ' ' + tooltipped_span('<a href="' + lu.url + '" target="_blank"><img src="' + window.STATIC_URL + 'common/img/ext-link.svg" alt="external link" height="14"/></a>', gettext('Przejdź do strony tej jednostki w <i>Słowosieci</i>'), 'plwn-url');
+            lu_html += ' <a class="lu-plwn" href="' + lu.url + '" target="_blank"><img src="' + window.STATIC_URL + '/common/img/ext-link.svg" alt="external link" height="14"/></a>';
         }
         lexical_units.push(lu_html);
     }
@@ -163,7 +163,7 @@ function frame2dom(frame) {
             }
             preferences_html += '<div class="preference py-2 px-1' + cls + '">';
             if (preference.url) {
-                //preferences_html += ' <a class="synset-plwn" href="' + preference.url + '" target="_blank"><img src="/static/common/img/ext-link.svg" alt="external link" height="14"/></a>';
+//                preferences_html += ' <a class="synset-plwn" href="' + preference.url + '" target="_blank"><img src="' + window.STATIC_URL + 'common/img/ext-link.svg" alt="external link" height="14"/></a>';
                 preferences_html += ' <a class="synset-plwn" href="' + preference.url + '" target="_blank">' + preference.str + '</a>';
             } else {
                 preferences_html += preference.str;
diff --git a/entries/templates/checkboxselectmultiple_with_tooltips.html b/entries/templates/checkboxselectmultiple_with_tooltips.html
index 0ee0c68..b0cbaef 100644
--- a/entries/templates/checkboxselectmultiple_with_tooltips.html
+++ b/entries/templates/checkboxselectmultiple_with_tooltips.html
@@ -1,5 +1,6 @@
 {% load crispy_forms_filters %}
 {% load l10n %}
+{% load static %}
 
 <div class="{% if field_class %} {{ field_class }}{% endif %}"{% if flat_attrs %} {{ flat_attrs|safe }}{% endif %}>
 
@@ -8,7 +9,7 @@
         <input type="checkbox" class="{%if use_custom_control%}custom-control-input{% else %}form-check-input{% endif %}{%if is_bound %} is-{% if field.errors %}in{%endif%}valid{% endif %}"{% if choice.0 in field.value or choice.0|stringformat:"s" in field.value or choice.0|stringformat:"s" == field.value|default_if_none:""|stringformat:"s" %} checked="checked"{% endif %} name="{{ field.html_name }}" id="id_{{ field.html_name }}_{{ forloop.counter }}" value="{{ choice.0|unlocalize }}" {{ field.field.widget.attrs|flatatt }}>
         <label class="{%if use_custom_control%}custom-control-label{% else %}form-check-label{% endif %}" for="id_{{ field.html_name }}_{{ forloop.counter }}">
             {% if choice.1.1 %}
-            {{ choice.1.0|unlocalize }} <span data-toggle="tooltip" data-placement="bottom" title="{{ choice.1.1 }}"><img src="/static/common/img/info.svg" alt="info" width="14" height="14"/></span>
+            {{ choice.1.0|unlocalize }} <span data-toggle="tooltip" data-placement="bottom" title="{{ choice.1.1 }}"><img src="{% static 'common/img/info.svg' %}" alt="info" width="14" height="14"/></span>
             {% else %}
             {{ choice.1.0|unlocalize }}
             {% endif %}
diff --git a/entries/templates/entries.html b/entries/templates/entries.html
index 0814f1f..674d39f 100644
--- a/entries/templates/entries.html
+++ b/entries/templates/entries.html
@@ -63,13 +63,13 @@
         <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>
+            {% 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>
+            {% 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>
diff --git a/entries/templates/role_checkboxselectmultiple_inner.html b/entries/templates/role_checkboxselectmultiple_inner.html
index 23a00fe..6310fe0 100644
--- a/entries/templates/role_checkboxselectmultiple_inner.html
+++ b/entries/templates/role_checkboxselectmultiple_inner.html
@@ -26,7 +26,7 @@
         <input type="checkbox" class="{%if use_custom_control%}custom-control-input{% else %}form-check-input{% endif %}{%if is_bound %} is-{% if field.errors %}in{%endif%}valid{% endif %}"{% if choice.0 in field.value or choice.0|stringformat:"s" in field.value or choice.0|stringformat:"s" == field.value|default_if_none:""|stringformat:"s" %} checked="checked"{% endif %} name="{{ field.html_name }}" id="id_{{ field.html_name }}_{{ forloop.parentloop.parentloop.counter }}_{{ forloop.parentloop.counter }}_{{ forloop.counter }}" value="{{ choice.0|unlocalize }}" {{ field.field.widget.attrs|flatatt }}>
         <label class="{%if use_custom_control%}custom-control-label{% else %}form-check-label{% endif %} text-dark" for="id_{{ field.html_name }}_{{ forloop.parentloop.parentloop.counter }}_{{ forloop.parentloop.counter }}_{{ forloop.counter }}">
             {% if choice.1.1 %}
-            {{ choice.1.0|unlocalize }} <span data-toggle="tooltip" data-placement="bottom" title="{{ choice.1.1 }}"><img src="/static/common/img/info.svg" alt="info" width="14" height="14"/></span>
+            {{ choice.1.0|unlocalize }} <span data-toggle="tooltip" data-placement="bottom" title="{{ choice.1.1 }}"><img src="{% static 'common/img/info.svg' %}" alt="info" width="14" height="14"/></span>
             {% else %}
             {{ choice.1.0|unlocalize }}
             {% endif %}
diff --git a/entries/templates/test.html b/entries/templates/test.html
index 7f838aa..c3c96aa 100644
--- a/entries/templates/test.html
+++ b/entries/templates/test.html
@@ -8,7 +8,7 @@
 <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-    <link rel="icon" href="/static/common/favicon.ico">
+    <link rel="icon" href="{% static 'common/favicon.ico' %}">
     <link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css">
     <link rel="stylesheet" type="text/css" href="https://bootswatch.com/4/lux/bootstrap.min.css">
     <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
diff --git a/filters/static/filters/js/filters_test.js b/filters/static/filters/js/filters_test.js
index 43f4577..c0fa13b 100644
--- a/filters/static/filters/js/filters_test.js
+++ b/filters/static/filters/js/filters_test.js
@@ -34,14 +34,14 @@ function show_spinner() {
     clear_info();
     // silly cat gifs for development
     /*var rnd = Math.floor(Math.random() * 3) + 1;
-    show_status('<div class="text-center mb-1"><p>Siem filtruje!</p><img src="/static/common/img/loading' + rnd + '.gif" height=180px></div>');*/
+    show_status('<div class="text-center mb-1"><p>Siem filtruje!</p><img src="' + window.STATIC_URL + 'common/img/loading' + rnd + '.gif" height=180px></div>');*/
     show_status('<div class="text-center"><div class="spinner-grow text-light" role="status"> <span class="sr-only">Proszę czekać, trwa filtrowanie...</span></div></div>');
 }
 
 function show_error() {
     clear_info();
     // silly cat gif for development
-    /*show_status('<div class="text-center mb-1"><p>Ajajaj, straszny błąd!</p><img src="/static/common/img/error.gif" height="320px"></div>');*/
+    /*show_status('<div class="text-center mb-1"><p>Ajajaj, straszny błąd!</p><img src="' + window.STATIC_URL + 'common/img/error.gif" height="320px"></div>');*/
     show_warning_text('Coś poszło nie tak... :(');
     
 }
diff --git a/locale/en/LC_MESSAGES/django.mo b/locale/en/LC_MESSAGES/django.mo
index 63b3175bf7c12ea66fc505d606cae8959df2877b..f7595614722432da63fc8ccf1e3efcd5f783fe8f 100644
GIT binary patch
delta 11222
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

delta 10511
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
Wk(M*!*%3LD9d9!|Z_K#Xq5lht63E;D

diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po
index 020256e..56413e0 100644
--- a/locale/en/LC_MESSAGES/django.po
+++ b/locale/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-07-14 16:24+0200\n"
+"POT-Creation-Date: 2022-04-07 23:17+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,49 +18,69 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: common/templates/base.html:49 entries/templates/entries.html:7
+#: common/templates/base.html:52 entries/templates/entries.html:7
 #: entries/templates/entries.html:38
 msgid "Hasła"
 msgstr "Entries"
 
-#: common/templates/base.html:55
+#: common/templates/base.html:58
 msgid "Typy fraz"
 msgstr "Phrase types"
 
-#: common/templates/base.html:60
+#: common/templates/base.html:63
 #: dictionary_statistics/templates/dictionary_statistics.html:9
 msgid "Statystyki"
 msgstr "Statistics"
 
-#: common/templates/base.html:71
+#: common/templates/base.html:69 users/templates/user_list.html:5
+#: users/templates/user_list.html:9
+msgid "Użytkownicy"
+msgstr "Users"
+
+#: common/templates/base.html:81 users/templates/user_profile.html:7
+#: users/templates/user_profile.html:10
+msgid "Twój profil"
+msgstr "Your profile"
+
+#: common/templates/base.html:84
+msgid "Wyloguj siÄ™"
+msgstr "Sign out"
+
+#: common/templates/base.html:89 users/forms.py:71
+#: users/templates/registration/login.html:6
+#: users/templates/registration/login.html:12
+msgid "Zaloguj siÄ™"
+msgstr "Sign in"
+
+#: common/templates/base.html:96
 msgid "EN"
 msgstr "PL"
 
-#: common/templates/base.html:81
+#: common/templates/base.html:106
 msgid "Instytut Podstaw Informatyki PAN"
 msgstr "Institute of Computer Science PAS"
 
-#: common/templates/base.html:82
+#: common/templates/base.html:107
 msgid "Praca współfinansowana przez"
 msgstr "Work co-founded by"
 
-#: common/templates/base.html:83
+#: common/templates/base.html:108
 msgid "Strona wykorzystuje"
 msgstr "Webpage powered by"
 
-#: common/templates/base.html:84
+#: common/templates/base.html:109
 msgid "oraz"
 msgstr "and "
 
-#: common/templates/base.html:85
+#: common/templates/base.html:110
 msgid "z motywem opartym na"
 msgstr "with"
 
-#: common/templates/base.html:86
+#: common/templates/base.html:111
 msgid " i krojem pisma"
 msgstr "-based motive and"
 
-#: common/templates/base.html:87
+#: common/templates/base.html:112
 msgid "."
 msgstr " font."
 
@@ -122,7 +142,7 @@ msgstr "all"
 msgid "Pobieranie"
 msgstr "Download"
 
-#: entries/autocompletes.py:27 entries/views.py:464
+#: entries/autocompletes.py:27 entries/views.py:463
 msgid "definicja:"
 msgstr "definition:"
 
@@ -250,7 +270,7 @@ msgstr "Schema"
 msgid "Schemat(y) występujące w haśle"
 msgstr "Phrase type(s) occurring in the entry."
 
-#: entries/forms.py:147 entries/forms.py:422 entries/forms.py:643
+#: entries/forms.py:147 entries/forms.py:422
 msgid "Pozycja"
 msgstr "Position"
 
@@ -268,9 +288,9 @@ msgstr ""
 "positions co-occuring in one schema, use the POSITION filter inside SCHEMA "
 "filter above."
 
-#: entries/forms.py:151 entries/forms.py:483 entries/forms.py:648
-#: entries/forms.py:769 entries/forms.py:771 entries/forms.py:773
-#: entries/forms.py:775
+#: entries/forms.py:151 entries/forms.py:483 entries/forms.py:649
+#: entries/forms.py:770 entries/forms.py:772 entries/forms.py:774
+#: entries/forms.py:776
 msgid "Fraza"
 msgstr "Phrase"
 
@@ -296,7 +316,7 @@ msgstr "Frame"
 msgid "Rama/y występujące w haśle"
 msgstr "Frame(s) occurring in the entry."
 
-#: entries/forms.py:165 entries/forms.py:600
+#: entries/forms.py:165 entries/forms.py:593
 msgid "Argument"
 msgstr "Argument"
 
@@ -349,7 +369,7 @@ msgstr "Collapse"
 msgid "Usuń"
 msgstr "Remove"
 
-#: entries/forms.py:311 entries/forms.py:807
+#: entries/forms.py:311 entries/forms.py:808
 msgid "Zaneguj"
 msgstr "Negate"
 
@@ -433,7 +453,7 @@ msgstr "Lemma choice"
 msgid "Łączenie lematów"
 msgstr "Lemma joining"
 
-#: entries/forms.py:548 entries/forms.py:828
+#: entries/forms.py:548 entries/forms.py:829
 msgid "Typ składniowy frazy zleksykalizowanej."
 msgstr "Syntactic type of lexicalised phrase."
 
@@ -451,22 +471,22 @@ msgstr "Opinion"
 msgid "Liczba argumentów"
 msgstr "Number of arguments"
 
-#: entries/forms.py:594
-msgid "Liczba preferencyj selekcyjnych argumentu"
-msgstr "Number of argument’s selectional preferences"
-
-#: entries/forms.py:616
+#: entries/forms.py:609
 msgid "Argument semantyczny"
 msgstr "Semantic argument"
 
-#: entries/forms.py:622
+#: entries/forms.py:615
 msgid "Rola"
 msgstr "Role"
 
-#: entries/forms.py:629
+#: entries/forms.py:622
 msgid "Atrybut roli"
 msgstr "Role attribute"
 
+#: entries/forms.py:629
+msgid "Liczba preferencyj selekcyjnych argumentu"
+msgstr "Number of argument’s selectional preferences"
+
 #: entries/forms.py:636 entries/forms.py:639
 msgid "Preferencja selekcyjna"
 msgstr "Selectional preference"
@@ -483,59 +503,59 @@ msgstr "Expressed by relation"
 msgid "Wyrażona przez jednostkę leksykalną Słowosieci"
 msgstr "Expressed by plWordnet lexical unit"
 
-#: entries/forms.py:647
+#: entries/forms.py:648
 msgid "Typ frazy, przez którą może być realizowany argument."
 msgstr ""
 
-#: entries/forms.py:670
+#: entries/forms.py:671
 msgid "Preferencja predefiniowana"
 msgstr "Predefined preference"
 
-#: entries/forms.py:676
+#: entries/forms.py:677
 msgid "Predefiniowane"
 msgstr "Predefined"
 
-#: entries/forms.py:690
+#: entries/forms.py:691
 msgid "Preferencja – relacja"
 msgstr "Relational preference"
 
-#: entries/forms.py:696
+#: entries/forms.py:697
 msgid "Relacja"
 msgstr "Relation"
 
-#: entries/forms.py:707
+#: entries/forms.py:708
 msgid "Do: rola"
 msgstr "To: role"
 
-#: entries/forms.py:714
+#: entries/forms.py:715
 msgid "Do: atrybut"
 msgstr "To: attribute"
 
-#: entries/forms.py:726
+#: entries/forms.py:727
 msgid "Preferencja – Słowosieć"
 msgstr "plWordnet preference"
 
-#: entries/forms.py:732
+#: entries/forms.py:733
 msgid "Jednostka leksykalna"
 msgstr "Lexical unit"
 
-#: entries/forms.py:780
+#: entries/forms.py:781
 msgid "Fraza {}"
 msgstr "{} phrase"
 
-#: entries/forms.py:782 entries/phrase_descriptions/descriptions.py:124
+#: entries/forms.py:783 entries/phrase_descriptions/descriptions.py:124
 msgid "zleksykalizowana"
 msgstr "lexicalised"
 
-#: entries/forms.py:818
+#: entries/forms.py:819
 msgid "Realizacja składniowa frazy."
 msgstr "Syntactic realisation of the phrase."
 
-#: entries/forms.py:822
+#: entries/forms.py:823
 msgid "Fraza składowa zleksykalizowanej konstrukcji porównawczej."
 msgstr "Component phrase of lexicalised comparative construction."
 
-#: entries/forms.py:824
+#: entries/forms.py:825
 msgid "Fraza zleksykalizowana."
 msgstr "Lexicalised phrase."
 
@@ -2157,7 +2177,13 @@ msgid ""
 "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)."
-msgstr "When filtering entries, show (aside from entries satisfying filtering criteria) entries with related meanings, (eg. <i>podarować</i> – <i>podarunek</i> – <i>podarek</i>). Related entries that don’t satisfy filtering criteria are displayed in a lighter color on the entries list and are not subject to schema/frame filtering (ie. are always shown in their entirety regardless of schema/frame filters applied)."
+msgstr ""
+"When filtering entries, show (aside from entries satisfying filtering "
+"criteria) entries with related meanings, (eg. <i>podarować</i> – "
+"<i>podarunek</i> – <i>podarek</i>). Related entries that don’t satisfy "
+"filtering criteria are displayed in a lighter color on the entries list and "
+"are not subject to schema/frame filtering (ie. are always shown in their "
+"entirety regardless of schema/frame filters applied)."
 
 #: entries/templates/entries.html:103
 msgid "Filtrowanie haseł"
@@ -2205,22 +2231,22 @@ msgstr "Source"
 msgid "Brak przykładów"
 msgstr "No examples"
 
-#: entries/views.py:452
+#: entries/views.py:451
 msgid ""
 "Realizacja tego argumentu w zdaniu powinna być powiązana jakąkolwiek relacją"
 msgstr "Realisation of this argument in the sentence should be in any relation"
 
-#: entries/views.py:454
+#: entries/views.py:453
 msgid ""
 "Realizacja tego argumentu w zdaniu powinna być powiązana relacją <i>{}</i>"
 msgstr ""
 "Realisation of this argument in the sentence should be in <i>{}</i> relation"
 
-#: entries/views.py:455
+#: entries/views.py:454
 msgid "z realizacjÄ… argumentu <i>{}</i>."
 msgstr "with realisation of the <i>{}</i> argument."
 
-#: entries/views.py:468
+#: entries/views.py:467
 msgid "hiperonimy:"
 msgstr "hypernyms"
 
@@ -2264,6 +2290,55 @@ msgstr "distributive phrase"
 msgid "fraza posesywna"
 msgstr "possesive phrase"
 
+#: users/forms.py:10 users/templates/user_list.html:21
+msgid "Grupa"
+msgstr "Group"
+
+#: users/forms.py:29 users/forms.py:59
+msgid "Zapisz"
+msgstr "Save"
+
+#: users/forms.py:30 users/forms.py:60
+msgid "Wróć"
+msgstr "Back"
+
+#: users/forms.py:84
+msgid "Zresetuj hasło"
+msgstr "Reset password"
+
+#: users/templates/registration/password_reset.html:6
+#: users/templates/registration/password_reset.html:12
+msgid "Zresetuj swoje hasło"
+msgstr "Reset your password"
+
+#: users/templates/user_list.html:12 users/views.py:35
+msgid "Dodaj użytkownika"
+msgstr "Add a user"
+
+#: users/templates/user_list.html:19
+msgid "ImiÄ™ i nazwisko"
+msgstr "Full name"
+
+#: users/templates/user_list.html:20
+msgid "Nazwa użytkownika"
+msgstr "Username"
+
+#: users/templates/user_list.html:22
+msgid "Aktywny"
+msgstr "Active"
+
+#: users/templates/user_list.html:23
+msgid "Akcje"
+msgstr "Actions"
+
+#: users/templates/user_list.html:33
+msgid "Edytuj"
+msgstr "Edit"
+
+#: users/views.py:48
+msgid "Edytuj użytkownika"
+msgstr "Edit user"
+
 #~ msgid "niepewna"
 #~ msgstr "uncertain"
 
diff --git a/locale/en/LC_MESSAGES/djangojs.po b/locale/en/LC_MESSAGES/djangojs.po
index 43fea10..fb7fa6f 100644
--- a/locale/en/LC_MESSAGES/djangojs.po
+++ b/locale/en/LC_MESSAGES/djangojs.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-07-14 16:24+0200\n"
+"POT-Creation-Date: 2022-04-07 23:17+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -22,157 +22,157 @@ msgstr ""
 msgid "Trwa import danych!"
 msgstr "Data import in progress!"
 
-#: entries/static/entries/js/entries.js:14
+#: entries/static/entries/js/entries.js:15
 msgid "Opinia"
 msgstr "Opinion"
 
-#: entries/static/entries/js/entries.js:33
+#: entries/static/entries/js/entries.js:34
 msgid "Funkcja"
 msgstr "Function"
 
-#: entries/static/entries/js/entries.js:36
+#: entries/static/entries/js/entries.js:37
 msgid "Typy fraz"
 msgstr "Phrase types"
 
-#: entries/static/entries/js/entries.js:97
+#: entries/static/entries/js/entries.js:98
 msgid "brak schematów"
 msgstr "no schemata"
 
-#: entries/static/entries/js/entries.js:122
+#: entries/static/entries/js/entries.js:123
 msgid "nowa jednostka spoza <i>SÅ‚owosieci</i>"
 msgstr "new lexical unit not in <i>plWordnet</i>"
 
-#: entries/static/entries/js/entries.js:146
+#: entries/static/entries/js/entries.js:147
 msgid "Rola"
 msgstr "Role"
 
-#: entries/static/entries/js/entries.js:148
+#: entries/static/entries/js/entries.js:149
 msgid "Preferencje selekcyjne"
 msgstr "Selectional preferences"
 
-#: entries/static/entries/js/entries.js:195
+#: entries/static/entries/js/entries.js:196
 msgid "brak ram"
 msgstr "no frames"
 
-#: entries/static/entries/js/entries.js:213
+#: entries/static/entries/js/entries.js:214
 msgid "Kliknij, aby wyświetlić przykłady dla tego schematu."
 msgstr "Click to show examples for this schema."
 
-#: entries/static/entries/js/entries.js:250
+#: entries/static/entries/js/entries.js:251
 msgid "Kliknij, aby cofnąć wyświetlanie przykładów dla tego schematu."
 msgstr "Click to undo showing examples for this schema."
 
-#: entries/static/entries/js/entries.js:262
-#: entries/static/entries/js/entries.js:453
-#: entries/static/entries/js/entries.js:485
-#: entries/static/entries/js/entries.js:531
+#: entries/static/entries/js/entries.js:263
+#: entries/static/entries/js/entries.js:454
+#: entries/static/entries/js/entries.js:486
+#: entries/static/entries/js/entries.js:532
 msgid ""
 "Kliknij, aby cofnąć ograniczenie wyświetlanych przykładów do powiązanych z"
 msgstr "Click to undo restriction to examples linked to"
 
-#: entries/static/entries/js/entries.js:262
-#: entries/static/entries/js/entries.js:264
+#: entries/static/entries/js/entries.js:263
+#: entries/static/entries/js/entries.js:265
 msgid "tÄ… pozycjÄ…"
 msgstr "this position"
 
-#: entries/static/entries/js/entries.js:262
-#: entries/static/entries/js/entries.js:264
+#: entries/static/entries/js/entries.js:263
+#: entries/static/entries/js/entries.js:265
 msgid "tÄ… frazÄ…"
 msgstr "this phrase"
 
-#: entries/static/entries/js/entries.js:264
-#: entries/static/entries/js/entries.js:455
-#: entries/static/entries/js/entries.js:487
-#: entries/static/entries/js/entries.js:533
+#: entries/static/entries/js/entries.js:265
+#: entries/static/entries/js/entries.js:456
+#: entries/static/entries/js/entries.js:488
+#: entries/static/entries/js/entries.js:534
 msgid "Kliknij, aby wyświetlić wyłącznie przykłady powiązane z"
 msgstr "Click to show only examples linked to"
 
-#: entries/static/entries/js/entries.js:350
+#: entries/static/entries/js/entries.js:351
 msgid ""
 "Kliknij, aby wyświetlić przykłady dla tej ramy oraz jej realizacje "
 "składniowe."
 msgstr ""
 "Click to show examples linked to this frame and its syntactic realisations."
 
-#: entries/static/entries/js/entries.js:453
-#: entries/static/entries/js/entries.js:455
+#: entries/static/entries/js/entries.js:454
+#: entries/static/entries/js/entries.js:456
 msgid "tym znaczeniem"
 msgstr "this meaning"
 
-#: entries/static/entries/js/entries.js:485
-#: entries/static/entries/js/entries.js:487
+#: entries/static/entries/js/entries.js:486
+#: entries/static/entries/js/entries.js:488
 msgid "tÄ… rolÄ…"
 msgstr "this role"
 
-#: entries/static/entries/js/entries.js:531
-#: entries/static/entries/js/entries.js:533
+#: entries/static/entries/js/entries.js:532
+#: entries/static/entries/js/entries.js:534
 msgid "tym schematem"
 msgstr "this schema"
 
-#: entries/static/entries/js/entries.js:561
+#: entries/static/entries/js/entries.js:562
 msgid "Kliknij, aby cofnąć wybór tej ramy."
 msgstr "Click to undo choice if this frame."
 
-#: entries/static/entries/js/entries.js:664
-#: entries/static/entries/js/entries.js:770
+#: entries/static/entries/js/entries.js:665
+#: entries/static/entries/js/entries.js:771
 msgid "Komentarz"
 msgstr "Comment"
 
-#: entries/static/entries/js/entries.js:681
+#: entries/static/entries/js/entries.js:682
 msgid ""
 "Kliknij, aby cofnąć wyświetlanie typów fraz powiązanych z tym przykładem."
 msgstr "Click to undo showing phrase types linked to this example."
 
-#: entries/static/entries/js/entries.js:683
+#: entries/static/entries/js/entries.js:684
 msgid "Kliknij, aby wyświetlić typy fraz powiązane z tym przykładem."
 msgstr "Click to show phrase types linked to this example."
 
-#: entries/static/entries/js/entries.js:722
+#: entries/static/entries/js/entries.js:723
 msgid ""
 "Kliknij, aby cofnąć wyświetlanie argumentów i typów fraz powiązanych z tym "
 "przykładem."
 msgstr ""
 "Click to undo showing arguments and phrase types linked to this example."
 
-#: entries/static/entries/js/entries.js:724
+#: entries/static/entries/js/entries.js:725
 msgid ""
 "Kliknij, aby wyświetlić argumenty i typy fraz powiązane z tym przykładem."
 msgstr "Click to show arguments and phrase types linked to this example."
 
-#: entries/static/entries/js/entries.js:981
+#: entries/static/entries/js/entries.js:984
 msgid "Przetwarzanie..."
 msgstr "Processing"
 
-#: entries/static/entries/js/entries.js:982
-#: entries/static/entries/js/entries.js:1039
+#: entries/static/entries/js/entries.js:985
+#: entries/static/entries/js/entries.js:1049
 msgid "Szukaj:"
 msgstr "Search:"
 
-#: entries/static/entries/js/entries.js:983
+#: entries/static/entries/js/entries.js:986
 msgid "Liczba haseł: _TOTAL_"
 msgstr "_TOTAL_ entries"
 
-#: entries/static/entries/js/entries.js:984
-#: entries/static/entries/js/entries.js:1040
+#: entries/static/entries/js/entries.js:987
+#: entries/static/entries/js/entries.js:1050
 msgid "Liczba haseł: 0"
 msgstr "0 entries"
 
-#: entries/static/entries/js/entries.js:985
+#: entries/static/entries/js/entries.js:988
 msgid "(spośród _MAX_)"
 msgstr "(out of _MAX_)"
 
-#: entries/static/entries/js/entries.js:986
-#: entries/static/entries/js/entries.js:1041
+#: entries/static/entries/js/entries.js:989
+#: entries/static/entries/js/entries.js:1051
 msgid "Brak haseł do wyświetlenia."
 msgstr "No entries to display."
 
-#: entries/static/entries/js/entries.js:988
-#: entries/static/entries/js/entries.js:1043
+#: entries/static/entries/js/entries.js:991
+#: entries/static/entries/js/entries.js:1053
 msgid ": sortuj kolumnÄ™ rosnÄ…co"
 msgstr ": sort column in ascending order"
 
-#: entries/static/entries/js/entries.js:989
-#: entries/static/entries/js/entries.js:1044
+#: entries/static/entries/js/entries.js:992
+#: entries/static/entries/js/entries.js:1054
 msgid ": sortuj kolumnÄ™ malejÄ…co"
 msgstr ": sort column in descending order"
 
diff --git a/phrase_expansions/templates/phrase_expansions.html b/phrase_expansions/templates/phrase_expansions.html
index c5242ae..a975489 100644
--- a/phrase_expansions/templates/phrase_expansions.html
+++ b/phrase_expansions/templates/phrase_expansions.html
@@ -55,7 +55,7 @@
                                         {{ subtype_expansions.phrase_subtype }}
                                     </th>
                                     {% endif %}
-                                    <td class="py-2 px-1" style="width: 7em;"><img src="/static/entries/img/{{ expansion.opinion_sym }}.svg" alt="{{ expansion.opinion_str }}" width="12" height="12">
+                                    <td class="py-2 px-1" style="width: 7em;"><img src="{% static 'entries/img' %}/{{ expansion.opinion_sym }}.svg" alt="{{ expansion.opinion_str }}" width="12" height="12">
                                         {{ expansion.opinion_str }}
                                     </td>
                                     {% for position in expansion.positions %}
diff --git a/reset_db.sh b/reset_db.sh
index d4446d2..a905113 100755
--- a/reset_db.sh
+++ b/reset_db.sh
@@ -13,6 +13,7 @@ python manage.py migrate
 
 rm import.log || true
 
+time python manage.py create_groups_and_permissions
 time python manage.py start_import
 time python manage.py import_expansions
 time python manage.py import_plWordnet
diff --git a/shellvalier/settings.py b/shellvalier/settings.py
index 797463a..89ee949 100644
--- a/shellvalier/settings.py
+++ b/shellvalier/settings.py
@@ -11,6 +11,9 @@ https://docs.djangoproject.com/en/2.1/ref/settings/
 """
 
 import os
+import uuid
+
+from django.urls import reverse_lazy
 
 from .environment import get_environment, boolean_mapper, list_mapper_factory
 
@@ -58,6 +61,7 @@ INSTALLED_APPS = [
     'phrase_expansions.apps.PhraseExpansionsConfig',
     'dictionary_statistics.apps.DictionaryStatisticsConfig',
     'download.apps.DownloadConfig',
+    'users.apps.UsersConfig',
     'crispy_forms',
     'django_extensions',
 ]
@@ -151,9 +155,13 @@ LOCALE_PATHS = [
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/2.1/howto/static-files/
 
-STATIC_URL = '/static/'
+VERSION = get_environment('VERSION', default=str(uuid.uuid4()))
+STATIC_URL = f'/static/{VERSION}/'
 
 STATICFILES_DIRS = [
     os.path.join(BASE_DIR, 'common/static/'),
 ]
 
+LOGIN_URL = reverse_lazy("users:login")
+LOGIN_REDIRECT_URL = reverse_lazy('dash')
+LOGOUT_REDIRECT_URL = reverse_lazy('dash')
diff --git a/shellvalier/urls.py b/shellvalier/urls.py
index d1178a8..1fa7a87 100644
--- a/shellvalier/urls.py
+++ b/shellvalier/urls.py
@@ -13,6 +13,7 @@ urlpatterns = i18n_patterns(
     path('phrase_expansions/', include('phrase_expansions.urls')),
     path('dictionary_statistics/', include('dictionary_statistics.urls')),
     path('download/', include('download.urls')),
+    path('users/', include('users.urls')),
     path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
     path('admin/', admin.site.urls, name='admin'),
     path('', dash, name='dash'),
diff --git a/users/__init__.py b/users/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/users/apps.py b/users/apps.py
new file mode 100644
index 0000000..4ce1fab
--- /dev/null
+++ b/users/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class UsersConfig(AppConfig):
+    name = 'users'
diff --git a/users/forms.py b/users/forms.py
new file mode 100644
index 0000000..e0d98f1
--- /dev/null
+++ b/users/forms.py
@@ -0,0 +1,87 @@
+from django import forms
+from django.contrib.auth.models import Group, User
+from django.utils.translation import gettext as _
+
+from crispy_forms.helper import FormHelper
+from crispy_forms.layout import HTML, Layout, Fieldset, ButtonHolder, Submit
+
+
+class UserForm(forms.ModelForm):
+    group = forms.ModelChoiceField(queryset=Group.objects.all(), label=_("Grupa"))
+
+    class Meta:
+        model = User
+        fields = [
+            'first_name',
+            'last_name',
+            'username',
+            'email',
+            'group',
+            'is_active',
+        ]
+
+    def __init__(self, *args, instance, **kwargs):
+        super().__init__(*args, instance=instance, **kwargs)
+        self.helper = FormHelper()
+        self.helper.layout = Layout(
+            Fieldset('', 'first_name', 'last_name', 'username', 'email', 'group', 'is_active'),
+            ButtonHolder(
+                Submit('submit', _('Zapisz'), css_class='btn btn-sm btn-success'),
+                HTML('''<a class="btn btn-sm btn-light" href="{% url 'users:user_list' %}">''' + _('Wróć') + '</a>'),
+            ),
+        )
+        for field in ['first_name', 'last_name', 'email']:
+            self.fields[field].required = True
+        if instance.pk:
+            self.initial['group'] = instance.groups.first()
+
+    def save(self, commit=True):
+        instance = super().save(commit=commit)
+        instance.groups.set([self.cleaned_data['group']])
+        return instance
+
+
+class UserProfileForm(forms.ModelForm):
+    class Meta:
+        model = User
+        fields = [
+            'first_name',
+            'last_name',
+            'email',
+        ]
+
+    def __init__(self, *args, instance, **kwargs):
+        super().__init__(*args, instance=instance, **kwargs)
+        self.helper = FormHelper()
+        self.helper.layout = Layout(
+            Fieldset('', 'first_name', 'last_name', 'email'),
+            ButtonHolder(
+                Submit('submit', _('Zapisz'), css_class='btn btn-sm btn-success'),
+                HTML('''<a class="btn btn-sm btn-light" href="{% url 'dash' %}">''' + _('Wróć') + '</a>'),
+            ),
+        )
+        for field in ['first_name', 'last_name', 'email']:
+            self.fields[field].required = True
+
+
+login_form_helper = FormHelper()
+login_form_helper.layout = Layout(
+    Fieldset('', 'username', 'password'),
+    ButtonHolder(
+        Submit('submit', _('Zaloguj siÄ™'), css_class='btn btn-sm btn-success'),
+        HTML(
+            '''<a class="btn btn-sm btn-light" href="{% url 'users:password_reset' %}">'''
+            f"{_('Nie pamiętam hasła')}"
+            '</a>'
+        ),
+    ),
+)
+
+password_reset_form_helper = FormHelper()
+password_reset_form_helper.layout = Layout(
+    Fieldset('', 'email'),
+    ButtonHolder(
+        Submit('submit', _('Zresetuj hasło'), css_class='btn btn-sm btn-success'),
+        HTML('''<a class="btn btn-sm btn-light" href="{% url 'users:login' %}">''' f"{_('Wróć')}" '</a>'),
+    )
+)
diff --git a/users/management/__init__.py b/users/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/users/management/commands/__init__.py b/users/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/users/management/commands/create_groups_and_permissions.py b/users/management/commands/create_groups_and_permissions.py
new file mode 100644
index 0000000..195d2d1
--- /dev/null
+++ b/users/management/commands/create_groups_and_permissions.py
@@ -0,0 +1,22 @@
+from django.contrib.auth.models import Group, Permission, User
+from django.contrib.contenttypes.models import ContentType
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+    def handle(self, **options):
+        admins, __ = Group.objects.get_or_create(name="Admini")
+        admins.permissions.add(
+            Permission.objects.get(codename="view_user", content_type=ContentType.objects.get_for_model(User)),
+            Permission.objects.get(codename="add_user", content_type=ContentType.objects.get_for_model(User)),
+            Permission.objects.get(codename="change_user", content_type=ContentType.objects.get_for_model(User)),
+            Permission.objects.get(codename="delete_user", content_type=ContentType.objects.get_for_model(User)),
+        )
+        lexicographs, __ = Group.objects.get_or_create(name="Leksykografowie")
+        lexicographs.permissions.add(
+            # TODO
+        )
+        super_lexicographs, __ = Group.objects.get_or_create(name="Super Leksykografowie")
+        super_lexicographs.permissions.add(
+            # TODO
+        )
diff --git a/users/templates/registration/login.html b/users/templates/registration/login.html
new file mode 100644
index 0000000..10834c7
--- /dev/null
+++ b/users/templates/registration/login.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+{% load crispy_forms_filters %}
+
+{% block title %}{% trans "Zaloguj siÄ™" %}{% endblock %}
+
+{% block content %}
+    <div class="row m-0">
+        <div class="col-lg-4 col-md-6 m-auto">
+            <div class="bg-white mt-4 p-3 clearfix">
+                <h5 class="mt-2 mb-4">{% trans "Zaloguj siÄ™" %}</h5>
+                <form action="." method="post">
+                    {% crispy form helper %}
+                </form>
+            </div>
+        </div>
+    </div>
+{% endblock %}
diff --git a/users/templates/registration/password_reset.html b/users/templates/registration/password_reset.html
new file mode 100644
index 0000000..7d37fa9
--- /dev/null
+++ b/users/templates/registration/password_reset.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+{% load crispy_forms_filters %}
+
+{% block title %}{% trans "Zresetuj swoje hasło" %}{% endblock %}
+
+{% block content %}
+    <div class="row m-0">
+        <div class="col-lg-4 col-md-6 m-auto">
+            <div class="bg-white mt-4 p-3 clearfix">
+                <h5 class="mt-2 mb-4">{% trans "Zresetuj swoje hasło" %}</h5>
+                <form action="." method="post">
+                    {% crispy form helper %}
+                </form>
+            </div>
+        </div>
+    </div>
+{% endblock %}
diff --git a/users/templates/user_form.html b/users/templates/user_form.html
new file mode 100644
index 0000000..82fbbf3
--- /dev/null
+++ b/users/templates/user_form.html
@@ -0,0 +1,13 @@
+{% extends "base-margins.html" %}
+
+{% load i18n %}
+
+{% load crispy_forms_filters %}
+
+{% block title %}{{ title }}{% endblock %}
+
+{% block content2 %}
+    <h5 class="mt-4 mb-4">{{ title }}</h5>
+
+    {% crispy form %}
+{% endblock %}
diff --git a/users/templates/user_list.html b/users/templates/user_list.html
new file mode 100644
index 0000000..c3b0051
--- /dev/null
+++ b/users/templates/user_list.html
@@ -0,0 +1,38 @@
+{% extends "base-margins.html" %}
+
+{% load i18n %}
+
+{% block title %}{% trans 'Użytkownicy' %}{% endblock %}
+
+{% block content2 %}
+<div class="mt-3">
+    <h5 class="float-left mt-2">{% trans 'Użytkownicy' %}</h5>
+    <div class="mb-4 float-right">
+        {% if perms.users.add_user %}
+            <a href="{% url 'users:user_add' %}" class="btn btn-sm btn-outline-dark">+ {% trans 'Dodaj użytkownika' %}</a>
+        {% endif %}
+    </div>
+</div>
+<table class="table">
+    <thead>
+        <tr>
+            <th>{% trans "ImiÄ™ i nazwisko" %}</th>
+            <th>{% trans "Nazwa użytkownika" %}</th>
+            <th>{% trans "Grupa" %}</th>
+            <th>{% trans "Aktywny" %}</th>
+            <th>{% trans "Akcje" %}</th>
+        </tr>
+    </thead>
+    <tbody>
+    {% for user in users %}
+        <tr>
+            <td>{{ user.get_full_name }}</td>
+            <td>{{ user.username }}</td>
+            <td>{{ user.groups.all|join:", " }}</td>
+            <td>{{ user.is_active|yesno }}</td>
+            <td><a href="{% url 'users:user_edit' pk=user.pk %}" class="btn btn-xs btn-outline-dark">{% trans 'Edytuj' %}</a></td>
+        </tr>
+    {% endfor %}
+    </tbody>
+</table>
+{% endblock %}
diff --git a/users/templates/user_profile.html b/users/templates/user_profile.html
new file mode 100644
index 0000000..35db9c2
--- /dev/null
+++ b/users/templates/user_profile.html
@@ -0,0 +1,13 @@
+{% extends "base-margins.html" %}
+
+{% load i18n %}
+
+{% load crispy_forms_filters %}
+
+{% block title %}{% trans 'Twój profil' %}{% endblock %}
+
+{% block content2 %}
+    <h5 class="mt-4 mb-4">{% trans 'Twój profil' %}</h5>
+
+    {% crispy form %}
+{% endblock %}
diff --git a/users/urls.py b/users/urls.py
new file mode 100644
index 0000000..76ae1fa
--- /dev/null
+++ b/users/urls.py
@@ -0,0 +1,29 @@
+from django.urls import include, path, reverse_lazy
+
+from django.contrib.auth import views as auth_views
+
+from . import views
+from .forms import login_form_helper, password_reset_form_helper
+
+app_name = 'users'
+
+urlpatterns = [
+    path('', views.user_list, name="user_list"),
+    path('add/', views.user_add, name="user_add"),
+    path('<int:pk>/edit/', views.user_edit, name="user_edit"),
+    path('profile/', views.user_profile, name="user_profile"),
+    path(
+        'login/',
+        auth_views.LoginView.as_view(extra_context={"helper": login_form_helper}, success_url=reverse_lazy('dash')),
+        name="login",
+    ),
+    path('logout/', auth_views.LogoutView.as_view(), name="logout"),
+    path(
+        'password-reset/',
+        auth_views.PasswordResetView.as_view(
+            template_name="registration/password_reset.html",
+            extra_context={"helper": password_reset_form_helper},
+        ),
+        name="password_reset",
+    ),
+]
diff --git a/users/views.py b/users/views.py
new file mode 100644
index 0000000..97b3b5a
--- /dev/null
+++ b/users/views.py
@@ -0,0 +1,48 @@
+from django.contrib.auth.decorators import login_required, permission_required
+from django.contrib.auth.models import User
+from django.shortcuts import get_object_or_404, render, redirect
+from django.utils.translation import gettext_lazy as _
+
+from users.forms import UserForm, UserProfileForm
+
+
+@permission_required('users.view_user')
+def user_list(request):
+    return render(request, 'user_list.html', {'users': User.objects.order_by('username')})
+
+
+@login_required
+def user_profile(request):
+    if request.method == 'POST':
+        form = UserProfileForm(instance=request.user, data=request.POST)
+        if form.is_valid():
+            form.save()
+            return redirect('dash')
+    else:
+        form = UserProfileForm(instance=request.user)
+    return render(request, 'user_profile.html', {'form': form})
+
+
+@permission_required('users.add_user')
+def user_add(request):
+    if request.method == 'POST':
+        form = UserForm(instance=User(), data=request.POST)
+        if form.is_valid():
+            form.save()
+            return redirect('users:user_list')
+    else:
+        form = UserForm(instance=User())
+    return render(request, 'user_form.html', {'form': form, 'title': _('Dodaj użytkownika')})
+
+
+@permission_required('users.change_user')
+def user_edit(request, pk):
+    user = get_object_or_404(User, pk=pk)
+    if request.method == 'POST':
+        form = UserForm(instance=user, data=request.POST)
+        if form.is_valid():
+            form.save()
+            return redirect('users:user_list')
+    else:
+        form = UserForm(instance=user)
+    return render(request, 'user_form.html', {'form': form, 'title': _('Edytuj użytkownika')})
-- 
GitLab