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