<commit_before>from django_json_widget.widgets import JSONEditorWidget
import requests
from pyquery import PyQuery
from django import forms
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from django.contrib.postgres import fields
from django.db.models import Q, Count
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from simple_history.admin import SimpleHistoryAdmin
from .utils import fix_after_rails, clone_model_instance, CapapiCommunicationException, parse_cap_decision_date
from .models import Case, Link, User, Casebook, Section, \
    Resource, ContentCollaborator, ContentAnnotation, TextBlock, ContentNode, \
    EmailWhitelist

#
# Helpers
#

def edit_link(obj, as_str=False):
    """ Generate a link to the admin edit screen for the given object. """
    if not obj:
        return None
    url = reverse('admin:%s_%s_change' % (obj._meta.app_label,  obj._meta.model_name), args=[obj.id])
    if as_str:
        return format_html('<a href="{}">→{} ({})</a>', url, obj, obj.id)
    else:
        return format_html('<a href="{}">→{}</a>', url, obj.id)

#
# Admin site config
#

class CustomAdminSite(admin.AdminSite):
    site_header = 'H2O Admin'

admin_site = CustomAdminSite(name='h2oadmin')

# If using the default Django admin.SiteAdmin, rather than our custom class,
# we want to remove builtin models
# admin.site.unregister(Group)
# admin.site.unregister(BuiltInUser)

# change Django defaults, because 'extra' isn't helpful anymore now you can add more with javascript
admin.TabularInline.extra = 0
admin.StackedInline.extra = 0

# don't allow inline objects to be deleted or added by default:
admin.TabularInline.can_delete = False  # use True to allow deleting
admin.StackedInline.can_delete = False
admin.TabularInline.max_num = 0  # use max_num = None to allow adding
admin.StackedInline.max_num = 0


class BaseAdmin(admin.ModelAdmin):
    fix_after_rails("""
        The LogEntry class tracks additions, changes, and deletions of objects
        done through the admin interface. It requires the Django app to be
        fully integrated with the AUTH_USER_MODEL... which we aren't yet. So,
        for now, disable logging.
    """)
    def log_addition(self, request, object, message):
        pass
    def log_change(self, request, object, message):
        pass
    def log_deletion(self, request, object, object_repr):
        pass

    actions = None  # use ['delete_selected'] to allow delete action
    formfield_overrides = {
        fields.JSONField: {'widget': JSONEditorWidget},
    }

    def has_add_permission(self, request):
        """
            Don't allow objects to be added by default. To override, use this on a subclass:

                def has_add_permission(self, request):
                    return super(BaseAdmin, self).has_add_permission(request)
        """
        return False

    def has_delete_permission(self, request, obj=None):
        """
            Don't allow objects to be deleted by default. To override, use this on a subclass:

                def has_delete_permission(self, request, obj=None):
                    return super(BaseAdmin, self).has_delete_permission(request, obj)
        """
        return False

    def enable_richeditor_for_field(self, fieldname, db_field, **kwargs):
        """
            A helper for enabling the editing of a field using a rich text editor.
            Models using this helper should adjust their change_form.html to inherit
            from "admin/change_form_with_richeditor.html"
        """
        formfield = super().formfield_for_dbfield(db_field, **kwargs)
        if db_field.name == fieldname:
            formfield.widget.attrs['class'] = formfield.widget.attrs['class'] + ' richtext-editor-src'
            formfield.widget = forms.Textarea(attrs=formfield.widget.attrs)
        return formfield

#
# Filters
#

class InputFilter(admin.SimpleListFilter):
    """
    Text input filter, from:
    https://hakibenita.com/how-to-add-a-text-filter-to-django-admin
    """
    template = 'admin/input_filter.html'

    def lookups(self, request, model_admin):
        # Dummy, required to show the filter.
        return ((),)

    def choices(self, changelist):
        # Grab only the "all" option.
        all_choice = next(super().choices(changelist))
        all_choice['query_parts'] = (
            (k, v)
            for k, v in changelist.get_filters_params().items()
            if k != self.parameter_name
        )
        yield all_choice


class CasebookIdFilter(InputFilter):
    parameter_name = 'casebook'
    title = 'Casebook (by ID)'

    def queryset(self, request, queryset):
        value = self.value()
        if value is not None:
            return queryset.filter(casebook_id=value)


class CollaboratorNameFilter(InputFilter):
    parameter_name = 'collaborator-name'
    title = 'Collaborator (by name)'

    def queryset(self, request, queryset):
        value = self.value()
        if value is not None:
            users = User.objects.filter(Q(title__icontains=value) | Q(attribution__icontains=value))
            return queryset.filter(casebook__collaborators__in=users)

class CollaboratorIdFilter(InputFilter):
    parameter_name = 'collaborator-id'
    title = 'Collaborator (by id)'

    def queryset(self, request, queryset):
        value = self.value()
        if value is not None:
            users = User.objects.filter(id=value)
            return queryset.filter(collaborators__in=users)


class CasebookStateFilter(admin.SimpleListFilter):
    title = 'casebook state'
    parameter_name = 'state'

    def lookups(self, request, model_admin):
        return [(tag.value, tag.name) for tag in Casebook.LifeCycle]

    def queryset(self, request, queryset):
        value = self.value()
        return queryset.filter(state=value)

class ResourceIdFilter(InputFilter):
    parameter_name = 'resource-id'
    title = 'Resource (by id)'

    def queryset(self, request, queryset):
        value = self.value()
        if value is not None:
            return queryset.filter(resource_id=value)


#
# Inlines
#

class CollaboratorInline(admin.TabularInline):
    model = ContentCollaborator
    fields = ['user', 'casebook', 'has_attribution', 'can_edit']
    raw_id_fields = ['user', 'casebook']
    max_num = None
    can_delete = True


class AnnotationInline(admin.TabularInline):
    model = ContentAnnotation
    readonly_fields = ['id', 'created_at', 'updated_at', 'start_paragraph', 'end_paragraph', 'start_offset', 'end_offset', 'kind']
    fields = ['id', 'resource', ('global_start_offset', 'global_end_offset'), ('start_paragraph', 'end_paragraph'), ('start_offset', 'end_offset'), 'kind', 'content', 'created_at', 'updated_at']
    raw_id_fields = ['resource']
    ordering = ['global_start_offset',  'global_end_offset']
    max_num = None
    can_delete = True<commit_msg>Fix casebook filtering bug in adminterface<commit_after>from django_json_widget.widgets import JSONEditorWidget
import requests
from pyquery import PyQuery
from django import forms
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from django.contrib.postgres import fields
from django.db.models import Q, Count
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from simple_history.admin import SimpleHistoryAdmin
from .utils import fix_after_rails, clone_model_instance, CapapiCommunicationException, parse_cap_decision_date
from .models import Case, Link, User, Casebook, Section, \
    Resource, ContentCollaborator, ContentAnnotation, TextBlock, ContentNode, \
    EmailWhitelist

#
# Helpers
#

def edit_link(obj, as_str=False):
    """ Generate a link to the admin edit screen for the given object. """
    if not obj:
        return None
    url = reverse('admin:%s_%s_change' % (obj._meta.app_label,  obj._meta.model_name), args=[obj.id])
    if as_str:
        return format_html('<a href="{}">→{} ({})</a>', url, obj, obj.id)
    else:
        return format_html('<a href="{}">→{}</a>', url, obj.id)

#
# Admin site config
#

class CustomAdminSite(admin.AdminSite):
    site_header = 'H2O Admin'

admin_site = CustomAdminSite(name='h2oadmin')

# If using the default Django admin.SiteAdmin, rather than our custom class,
# we want to remove builtin models
# admin.site.unregister(Group)
# admin.site.unregister(BuiltInUser)

# change Django defaults, because 'extra' isn't helpful anymore now you can add more with javascript
admin.TabularInline.extra = 0
admin.StackedInline.extra = 0

# don't allow inline objects to be deleted or added by default:
admin.TabularInline.can_delete = False  # use True to allow deleting
admin.StackedInline.can_delete = False
admin.TabularInline.max_num = 0  # use max_num = None to allow adding
admin.StackedInline.max_num = 0


class BaseAdmin(admin.ModelAdmin):
    fix_after_rails("""
        The LogEntry class tracks additions, changes, and deletions of objects
        done through the admin interface. It requires the Django app to be
        fully integrated with the AUTH_USER_MODEL... which we aren't yet. So,
        for now, disable logging.
    """)
    def log_addition(self, request, object, message):
        pass
    def log_change(self, request, object, message):
        pass
    def log_deletion(self, request, object, object_repr):
        pass

    actions = None  # use ['delete_selected'] to allow delete action
    formfield_overrides = {
        fields.JSONField: {'widget': JSONEditorWidget},
    }

    def has_add_permission(self, request):
        """
            Don't allow objects to be added by default. To override, use this on a subclass:

                def has_add_permission(self, request):
                    return super(BaseAdmin, self).has_add_permission(request)
        """
        return False

    def has_delete_permission(self, request, obj=None):
        """
            Don't allow objects to be deleted by default. To override, use this on a subclass:

                def has_delete_permission(self, request, obj=None):
                    return super(BaseAdmin, self).has_delete_permission(request, obj)
        """
        return False

    def enable_richeditor_for_field(self, fieldname, db_field, **kwargs):
        """
            A helper for enabling the editing of a field using a rich text editor.
            Models using this helper should adjust their change_form.html to inherit
            from "admin/change_form_with_richeditor.html"
        """
        formfield = super().formfield_for_dbfield(db_field, **kwargs)
        if db_field.name == fieldname:
            formfield.widget.attrs['class'] = formfield.widget.attrs['class'] + ' richtext-editor-src'
            formfield.widget = forms.Textarea(attrs=formfield.widget.attrs)
        return formfield

#
# Filters
#

class InputFilter(admin.SimpleListFilter):
    """
    Text input filter, from:
    https://hakibenita.com/how-to-add-a-text-filter-to-django-admin
    """
    template = 'admin/input_filter.html'

    def lookups(self, request, model_admin):
        # Dummy, required to show the filter.
        return ((),)

    def choices(self, changelist):
        # Grab only the "all" option.
        all_choice = next(super().choices(changelist))
        all_choice['query_parts'] = (
            (k, v)
            for k, v in changelist.get_filters_params().items()
            if k != self.parameter_name
        )
        yield all_choice


class CasebookIdFilter(InputFilter):
    parameter_name = 'casebook'
    title = 'Casebook (by ID)'

    def queryset(self, request, queryset):
        value = self.value()
        if value is not None:
            return queryset.filter(casebook_id=value)


class CollaboratorNameFilter(InputFilter):
    parameter_name = 'collaborator-name'
    title = 'Collaborator (by name)'

    def queryset(self, request, queryset):
        value = self.value()
        if value is not None:
            users = User.objects.filter(Q(title__icontains=value) | Q(attribution__icontains=value))
            return queryset.filter(casebook__collaborators__in=users)

class CollaboratorIdFilter(InputFilter):
    parameter_name = 'collaborator-id'
    title = 'Collaborator (by id)'

    def queryset(self, request, queryset):
        value = self.value()
        if value is not None:
            users = User.objects.filter(id=value)
            return queryset.filter(collaborators__in=users)


class CasebookStateFilter(admin.SimpleListFilter):
    title = 'casebook state'
    parameter_name = 'state'

    def lookups(self, request, model_admin):
        return [(tag.value, tag.name) for tag in Casebook.LifeCycle]

    def queryset(self, request, queryset):
        value = self.value()
        return queryset.filter(state=value) if value else queryset

class ResourceIdFilter(InputFilter):
    parameter_name = 'resource-id'
    title = 'Resource (by id)'

    def queryset(self, request, queryset):
        value = self.value()
        if value is not None:
            return queryset.filter(resource_id=value)


#
# Inlines
#

class CollaboratorInline(admin.TabularInline):
    model = ContentCollaborator
    fields = ['user', 'casebook', 'has_attribution', 'can_edit']
    raw_id_fields = ['user', 'casebook']
    max_num = None
    can_delete = True


class AnnotationInline(admin.TabularInline):
    model = ContentAnnotation
    readonly_fields = ['id', 'created_at', 'updated_at', 'start_paragraph', 'end_paragraph', 'start_offset', 'end_offset', 'kind']
    fields = ['id', 'resource', ('global_start_offset', 'global_end_offset'), ('start_paragraph', 'end_paragraph'), ('start_offset', 'end_offset'), 'kind', 'content', 'created_at', 'updated_at']
    raw_id_fields = ['resource']
    ordering = ['global_start_offset',  'global_end_offset']
    max_num = None
    can_delete = True
{'fraction_non_alphanumeric': 0.10054999294880834, 'fraction_numerical': 0.0008461429981666902, 'mean_word_length': 3.3267236119585113, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 11, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4778655', 'n_tokens_mistral': 4167, 'n_tokens_neox': 4069, 'n_words': 1160}