diff --git a/entries/static/entries/js/unification_entries_list.js b/entries/static/entries/js/unification_entries_list.js index ee4f091145721f1087482b8642bbcf93e677110b..cad878f4673672070f6a9e6b8f93a54cba33ecba 100644 --- a/entries/static/entries/js/unification_entries_list.js +++ b/entries/static/entries/js/unification_entries_list.js @@ -1,5 +1,5 @@ function update_entries() { - const can_see_assignees = has_permission("auth.view_user"); + const can_see_assignees = has_permission("users.view_assignment"); function is_assigned_to_user_renderer (data) { return ( @@ -14,6 +14,7 @@ function update_entries() { columns: [ { data: 'lemma' }, { data: 'POS' }, + { render: data => (data && data.lexical_units && data.lexical_units.some(lu => lu.assignee_username !== null)) ? gettext("nie") : gettext("tak") }, can_see_assignees ? { data: 'assignee_username' } : { render: is_assigned_to_user_renderer }, ] }); @@ -51,7 +52,7 @@ function setup_lexical_units_table(drilldown, lexical_units, can_see_assignees) </tr> `).click(function () { $(this).addClass('table-primary').siblings().removeClass("table-primary"); - get_entry($(drilldown.data("row")).data("entry"), false); // TODO replace with loading LU details view + get_lexical_unit(lexical_unit.pk, $(drilldown.data("row")).data("entry")); }); } var table = $(` @@ -75,3 +76,42 @@ function setup_lexical_units_table(drilldown, lexical_units, can_see_assignees) $("tbody", table).append(get_lexical_unit_row(lexical_unit)); }); } + +function setup_notes($container, $template, lexical_unit_pk, entry) { + $container.html($template.children().clone()); + $('.show-note-form', $container).click(function () { + $('.note-form', $container).html($('#note-form-template > div', $container).clone()); + $('.hide-note-form', $container).click(function () { $('.note-form', $container).html(''); }); + $('.add-note', $container).click(function () { + console.log($('textarea[name=note]', $container).val()); + $.ajax({ + type : 'post', + url : $('#note-form-template').data('url').replace('MODEL', 'meanings.LexicalUnit').replace('PK', lexical_unit_pk), + dataType : 'json', + data : { note: $('.note-form textarea[name=note]').val() }, + timeout : 5000, + success : function (response) { + get_lexical_unit(lexical_unit_pk, entry); + }, + error : function () { + alert(gettext('Nie udaÅ‚o siÄ™ dodać notatki.')); + } + }); + }); + return false + }); + $.ajax({ + type: 'get', + url: $('.notes-table').data('url').replace('MODEL', 'meanings.LexicalUnit').replace('PK', lexical_unit_pk), + success: function (data) { + data.notes.map(function (note) { + $('.notes-table tbody', $container).append(`<tr><td>${note.note}</td><td>${note.owner_label}</td></tr>`); + }); + } + }) +} + +function get_lexical_unit(pk, entry) { + get_entry(entry, false); // TODO replace with loading LU details view + setup_notes($('#lexical-unit-notes'), $('#lexical-unit-notes-template'), pk, entry); +} diff --git a/entries/static/entries/js/unification_frames_list.js b/entries/static/entries/js/unification_frames_list.js index b8066eaf1eb5438def8c3cb7eba5355e47b73c2c..8a7c55e2d38d0efbd4d1f07e28d062d1fcf437f1 100644 --- a/entries/static/entries/js/unification_frames_list.js +++ b/entries/static/entries/js/unification_frames_list.js @@ -1,5 +1,5 @@ function update_entries() { - const can_see_assignees = has_permission("auth.view_user"); + const can_see_assignees = has_permission("users.view_assignment"); function is_assigned_to_user_renderer (data) { return ( diff --git a/entries/templates/entries.html b/entries/templates/entries.html index 0148c03c9aad6250841d17dcd48e5ff209394373..1179c4894db5de3b9b6726ab578ae0708c4f4737 100644 --- a/entries/templates/entries.html +++ b/entries/templates/entries.html @@ -12,4 +12,4 @@ {% block left_pane %}{% include "entries_list.html" %}{% endblock %} -{% block right_pane %}{% include "entry_display.html" with show_tabs=True %}{% endblock %} +{% block right_pane %}{% include "entry_display.html" %}{% endblock %} diff --git a/entries/templates/entry_display.html b/entries/templates/entry_display.html index 42d07fa913653e02cf583d9a1347383bc7bb3c21..cf620b880ec55094d84ddca819c75a16fe9bd4fa 100644 --- a/entries/templates/entry_display.html +++ b/entries/templates/entry_display.html @@ -1,6 +1,5 @@ {% load i18n %} -{% if show_tabs %} <ul class="nav nav-pills nav-justified p-1" id="entryTabs" role="tablist"> <li class="nav-item mr-1"> <a class="btn btn-sm btn-outline-dark nav-link active" id="semantics-tab" data-toggle="tab" href="#semantics" role="tab" aria-controls="semantics" aria-selected="true"> @@ -18,7 +17,6 @@ </a> </li> </ul> -{% endif %} <div class="tab-content h-100 w-100 p-0" id="entryTabsContent"> <div class="col h-100 w-100 p-0 tab-pane show active" id="semantics" role="tabpanel" aria-labelledby="semantics-tab"> diff --git a/entries/templates/unification.html b/entries/templates/unification.html index a0bf18d6cf4a5f6f5b4220ec0dbfc2cdb4ee37b3..7ed3f878aed755c497f92fabe21080fc0ac198f0 100644 --- a/entries/templates/unification.html +++ b/entries/templates/unification.html @@ -12,4 +12,4 @@ {% block left_pane %}{% include "unification_entries_list.html" %}{% endblock %} -{% block right_pane %}{% include "entry_display.html" %}{% endblock %} +{% block right_pane %}{% include "unification_lexical_unit_display.html" %}{% endblock %} diff --git a/entries/templates/unification_entries_list.html b/entries/templates/unification_entries_list.html index 48b737359c71740f56d4fc6838528a859613a25a..7768aff2893ff5f35e17220f07fe6dce50d902d1 100644 --- a/entries/templates/unification_entries_list.html +++ b/entries/templates/unification_entries_list.html @@ -5,7 +5,8 @@ <tr> <th class="p-1">{% trans "Lemat" %}</th> <th class="p-1">{% trans "Część mowy" %}</th> - {% if perms.users.view_user %} + <th class="p-1">{% trans "Do pobrania" %}</th> + {% if perms.users.view_assignment %} <th class="p-1">{% trans "Semantyk" %}</th> {% else %} <th class="p-1">{% trans "Moje (w opracowaniu)" %}</th> diff --git a/entries/templates/unification_lexical_unit_display.html b/entries/templates/unification_lexical_unit_display.html new file mode 100644 index 0000000000000000000000000000000000000000..cd6bcb568d4bdcfa6faaa5444cf63085649fc721 --- /dev/null +++ b/entries/templates/unification_lexical_unit_display.html @@ -0,0 +1,63 @@ +{% load i18n %} + +<div class="tab-content h-100 w-100 p-0" id="entryTabsContent"> + <div class="col h-100 w-100 p-0 tab-pane show active" id="semantics" role="tabpanel" aria-labelledby="semantics-tab"> + <div class="row m-0 p-0" id="semantics-top-pane"> + <div class="col h-100 px-1 pt-0 pb-0 overflow-auto" id="semantics-frames-pane"> + <div id="semantics-frames"></div> + <div id="lexical-unit-notes-template" class="d-none">{% include 'notes.html' %}</div> + <div id="lexical-unit-notes"></div> + </div> + <div class="col h-100 px-1 pt-0 pb-0 overflow-auto" id="semantics-schemata-pane"> + <div id="semantics-schemata"></div> + </div> + </div> + <div class="row m-0 p-0 overflow-auto" id="semantics-examples-pane"> + <table id="semantics-examples" class="table table-sm table-hover"> + <thead> + <tr> + <th scope="col">{% trans "PrzykÅ‚ad" %}<i id="examples-argument"></i><i id="examples-lu"></i><i id="examples-schema"></i></th> + <th scope="col">{% trans "ŹródÅ‚o" %}</th> + <th scope="col">{% trans "Opinia" %}</th> + </tr> + </thead> + <tbody id="semantics-examples-list"> + </tbody> + </table> + <p class="mx-1 my-1"id="semantics-no-examples">{% trans "Brak przykÅ‚adów" %}</p> + </div> + </div> + <div class="col h-100 w-100 p-0 tab-pane" id="syntax" role="tabpanel" aria-labelledby="syntax-tab"> + <div class="col w-100 px-1 pt-0 pb-0 overflow-auto" id="syntax-schemata-pane"> + <div id="syntax-schemata"></div> + </div> + <div class="col w-100 p-0 overflow-auto" id="syntax-examples-pane"> + <table id="syntax-examples" class="table table-sm table-hover"> + <thead> + <tr> + <th scope="col">{% trans "PrzykÅ‚ad" %}</th> + <th scope="col">{% trans "ŹródÅ‚o" %}</th> + <th scope="col">{% trans "Opinia" %}</th> + </tr> + </thead> + <tbody id="syntax-examples-list"> + </tbody> + </table> + <p class="mx-1 my-1"id="syntax-no-examples">{% trans "Brak przykÅ‚adów" %}</p> + </div> + </div> + <div class="col h-100 w-100 p-0 tab-pane" id="examples" role="tabpanel" aria-labelledby="examples-tab"> + <table id="unmatched-examples" class="table table-sm table-hover"> + <thead> + <tr> + <th scope="col">{% trans "PrzykÅ‚ad" %}</th> + <th scope="col">{% trans "ŹródÅ‚o" %}</th> + <th scope="col">{% trans "Opinia" %}</th> + </tr> + </thead> + <tbody id="unmatched-examples-list"> + </tbody> + </table> + <p class="mx-1 my-1"id="unmatched-no-examples">{% trans "Brak przykÅ‚adów" %}</p> + </div> +</div> diff --git a/entries/views.py b/entries/views.py index 7ac820a5ab214550741c2a07c124cbeff8cfcabe..3336070b401ffd66e42e2206495cae315e65611b 100644 --- a/entries/views.py +++ b/entries/views.py @@ -412,6 +412,7 @@ def get_entries(request): { 'lexical_units': [ { + 'pk': lu.pk, 'display': str(lu), 'assignee_username': ( assignment.user.username if (assignment := lu.assignments.first()) else None diff --git a/shellvalier/settings.py b/shellvalier/settings.py index d9f15d452f99db27e47078a998ae710c9d9504ad..345e13cf0f1bd5338422888e772f1c5147f1259e 100644 --- a/shellvalier/settings.py +++ b/shellvalier/settings.py @@ -174,3 +174,5 @@ EMAIL_HOST_USER = get_environment('EMAIL_HOST_USER') EMAIL_HOST_PASSWORD = get_environment('EMAIL_HOST_PASSWORD') EMAIL_USE_TLS = get_environment('EMAIL_USE_TLS', mapper=boolean_mapper) EMAIL_USE_SSL = get_environment('EMAIL_USE_SSL', mapper=boolean_mapper) + +SUPER_LEXICOGRAPHS_GROUP_NAME = 'Super Leksykografowie' diff --git a/users/forms.py b/users/forms.py index 054b6aff9a2e3eba0224de9188072d5fc486beb2..da41112a26828d4e545fc0ee5ccc51bb9d5265d5 100644 --- a/users/forms.py +++ b/users/forms.py @@ -7,6 +7,8 @@ from django.utils.translation import gettext_lazy as _ from crispy_forms.helper import FormHelper from crispy_forms.layout import HTML, Layout, Fieldset, ButtonHolder, Submit +from users.models import Note + class UserForm(forms.ModelForm): group = forms.ModelChoiceField(queryset=Group.objects.all(), label=_('Grupa')) @@ -102,3 +104,9 @@ password_reset_set_password_form_helper.layout = Layout( HTML(format_lazy('<a class="btn btn-sm btn-light" href="{}">{}</a>', reverse_lazy('dash'), _("Wróć"))), ) ) + + +class NoteForm(forms.ModelForm): + class Meta: + model = Note + fields = ["note"] diff --git a/users/management/commands/create_groups_and_permissions.py b/users/management/commands/create_groups_and_permissions.py index a4361ca1c071820a42947ba2a3c222300bba2e0c..d6f71f3d2402252801da7f4b7e0d72e0c2d14684 100644 --- a/users/management/commands/create_groups_and_permissions.py +++ b/users/management/commands/create_groups_and_permissions.py @@ -1,22 +1,36 @@ +from django.conf import settings from django.contrib.auth.models import Group, Permission, User from django.contrib.contenttypes.models import ContentType from django.core.management.base import BaseCommand +from users.models import Assignment, Note + class Command(BaseCommand): def handle(self, **options): + Permission.objects.update_or_create( + content_type=ContentType.objects.get_for_model(Note), + codename="view_all_notes", + defaults={"name": "View all notes"} + ) 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)), + self._get_permission(User, 'view_user'), + self._get_permission(User, 'add_user'), + self._get_permission(User, 'change_user'), + self._get_permission(User, 'delete_user'), + self._get_permission(Assignment, 'view_assignment'), + self._get_permission(Note, 'view_all_notes'), ) lexicographs, __ = Group.objects.get_or_create(name='Leksykografowie') lexicographs.permissions.add( # TODO ) - super_lexicographs, __ = Group.objects.get_or_create(name='Super Leksykografowie') + super_lexicographs, __ = Group.objects.get_or_create(name=settings.SUPER_LEXICOGRAPHS_GROUP_NAME) super_lexicographs.permissions.add( - # TODO + self._get_permission(Assignment, 'view_assignment'), + self._get_permission(Note, 'view_all_notes'), ) + + def _get_permission(self, model, codename) -> Permission: + return Permission.objects.get(codename=codename, content_type=ContentType.objects.get_for_model(model)) diff --git a/users/models.py b/users/models.py index 91ce143a6a157f17210b3cf026423d24da8b65d6..10496b8a218808f581b6e074d3e3d111be7e6222 100644 --- a/users/models.py +++ b/users/models.py @@ -1,6 +1,8 @@ +from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models +from django.utils.translation import gettext_lazy as _ class Assignment(models.Model): @@ -13,3 +15,31 @@ class Assignment(models.Model): unique_together = [ ("subject_ct", "subject_id"), ] + + +class NoteQuerySet(models.QuerySet): + def for_user(self, user): + notes = ( + self.filter(author=user).annotate(owner_label=models.Value(_('wÅ‚asne'), output_field=models.CharField())) + ) + if user.has_perm('user.view_all_notes'): + notes |= ( + self.exclude(author=user).annotate(owner_label=models.F('author__username')) + ) + else: + notes |= ( + self.exclude(author=user).filter(author__groups__name=settings.SUPER_LEXICOGRAPHS_GROUP_NAME) + .annotate(owner_label=models.Value(_('Super'), output_field=models.CharField())) + ) + return notes + + +class Note(models.Model): + author = models.ForeignKey("auth.User", on_delete=models.PROTECT) + subject_ct = models.ForeignKey(ContentType, on_delete=models.PROTECT) + subject_id = models.PositiveIntegerField() + subject = GenericForeignKey('subject_ct', 'subject_id') + note = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + objects = models.Manager.from_queryset(NoteQuerySet)() diff --git a/users/templates/notes.html b/users/templates/notes.html new file mode 100644 index 0000000000000000000000000000000000000000..361ded95cf2f1e47497fec921f34af323550442a --- /dev/null +++ b/users/templates/notes.html @@ -0,0 +1,26 @@ +{% load i18n %} + +<div class="border p-2"> + <div id="note-form-template" class="d-none" data-url="{% url 'users:add_note' model='MODEL' pk='PK' %}"> + <div class="mb-2 border-bottom pb-2"> + <div class="clearfix"> + <h6 class="float-left">{% trans 'Dodaj notatkÄ™' %}</h6> + <a href="#" class="btn btn-xs btn-outline-dark float-right hide-note-form">×</a> + </div> + <div class="d-flex"> + <textarea name="note" class="form-control mr-3"></textarea> + <a href="#" class="btn btn-sm btn-outline-dark ml-auto align-self-end add-note">{% trans 'Dodaj' %}</a> + </div> + </div> + </div> + <div class="note-form"></div> + <div> + <div class="mb-2"> + <h6 class="mt-4 d-inline">{% trans 'Notatki' %}</h6> + <a href="#" class="show-note-form btn btn-xs btn-outline-dark ml-2">{% trans 'Dodaj' %}</a> + </div> + <table class="table table-sm table-striped border notes-table" data-url="{% url 'users:get_notes' model='MODEL' pk='PK' %}"> + <tbody></tbody> + </table> + </div> +</div> diff --git a/users/urls.py b/users/urls.py index 03b60f160ea1186d8f4275c17bb56203c36910b5..08c20de4635764614d5bef88e47e3c965c99d87c 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,5 +1,5 @@ from django.contrib.auth import views as auth_views -from django.urls import include, path, reverse_lazy +from django.urls import include, path, re_path, reverse_lazy from django.views.generic import TemplateView from . import views @@ -54,4 +54,6 @@ urlpatterns = [ ), name='password_reset_confirm' ), + re_path(r'^notes/(?P<model>\w+.\w+)/(?P<pk>\w+)/$', views.get_notes, name='get_notes'), + re_path(r'^notes/(?P<model>\w+.\w+)/(?P<pk>\w+)/add/$', views.add_note, name='add_note'), ] diff --git a/users/views.py b/users/views.py index 9e9a6a806937cc2fe1e0b705e9faa0d4ee1cabec..d12ab449cdce46ccf1d04a71cdb9847d32be4cb0 100644 --- a/users/views.py +++ b/users/views.py @@ -1,10 +1,16 @@ +from django.apps import apps from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType from django.contrib.sites.shortcuts import get_current_site +from django.http import JsonResponse from django.shortcuts import get_object_or_404, render, redirect from django.utils.translation import gettext_lazy as _ +from django.views.decorators.http import require_http_methods -from users.forms import UserForm, UserProfileForm +from common.decorators import ajax +from users.forms import UserForm, UserProfileForm, NoteForm +from users.models import Note from users.utils import send_new_user_email @@ -49,3 +55,32 @@ def user_edit(request, pk): else: form = UserForm(instance=user) return render(request, 'user_form.html', {'form': form, 'title': _('Edytuj użytkownika')}) + + +@login_required +def get_notes(request, model, pk): + model = apps.get_model(*model.split('.')) + subject = get_object_or_404(model, pk=pk) + ct = ContentType.objects.get_for_model(model) + notes = Note.objects.filter(subject_ct=ct, subject_id=subject.pk).for_user(request.user).order_by('created_at') + return JsonResponse({ + "notes": [{ + "pk": note.pk, + "owner_label": note.owner_label, + "created_at": note.created_at, + "note": note.note, + } for note in notes], + }) + + +@require_http_methods(["POST"]) +@login_required +def add_note(request, model, pk): + model = apps.get_model(*model.split('.')) + subject = get_object_or_404(model, pk=pk) + note = Note(author=request.user, subject=subject) + form = NoteForm(instance=note, data=request.POST) + if form.is_valid(): + form.save() + return JsonResponse({}) + return JsonResponse(form.errors.get_json_data(), status=400)