|
4 | 4 |
|
5 | 5 | import json |
6 | 6 |
|
| 7 | +from typing import Type # pyflakes:ignore |
| 8 | + |
7 | 9 | from django.utils.html import escape |
8 | | -from django import forms |
| 10 | +from django.db import models # pyflakes:ignore |
9 | 11 | from django.db.models import Q |
10 | 12 | from django.urls import reverse as urlreverse |
11 | 13 |
|
12 | 14 | import debug # pyflakes:ignore |
13 | 15 |
|
14 | 16 | from ietf.doc.models import Document, DocAlias |
15 | 17 | from ietf.doc.utils import uppercase_std_abbreviated_name |
| 18 | +from ietf.utils.fields import SearchableField |
16 | 19 |
|
17 | 20 | def select2_id_doc_name(objs): |
18 | 21 | return [{ |
19 | | - "id": o.pk, |
20 | | - "text": escape(uppercase_std_abbreviated_name(o.name)), |
| 22 | + "id": o.pk, |
| 23 | + "text": escape(uppercase_std_abbreviated_name(o.name)), |
21 | 24 | } for o in objs] |
22 | 25 |
|
23 | 26 |
|
24 | 27 | def select2_id_doc_name_json(objs): |
25 | 28 | return json.dumps(select2_id_doc_name(objs)) |
26 | 29 |
|
27 | | -# FIXME: select2 version 4 uses a standard select for the AJAX case - |
28 | | -# switching to that would allow us to derive from the standard |
29 | | -# multi-select machinery in Django instead of the manual CharField |
30 | | -# stuff below |
31 | | - |
32 | | -class SearchableDocumentsField(forms.CharField): |
33 | | - """Server-based multi-select field for choosing documents using |
34 | | - select2.js. |
35 | | -
|
36 | | - The field uses a comma-separated list of primary keys in a |
37 | | - CharField element as its API with some extra attributes used by |
38 | | - the Javascript part.""" |
39 | | - |
40 | | - def __init__(self, |
41 | | - max_entries=None, # max number of selected objs |
42 | | - model=Document, |
43 | | - hint_text="Type in name to search for document", |
44 | | - doc_type="draft", |
45 | | - *args, **kwargs): |
46 | | - kwargs["max_length"] = 10000 |
47 | | - self.max_entries = max_entries |
48 | | - self.doc_type = doc_type |
49 | | - self.model = model |
50 | 30 |
|
| 31 | +class SearchableDocumentsField(SearchableField): |
| 32 | + """Server-based multi-select field for choosing documents using select2.js. """ |
| 33 | + model = Document # type: Type[models.Model] |
| 34 | + default_hint_text = "Type name to search for document" |
| 35 | + |
| 36 | + def __init__(self, doc_type="draft", *args, **kwargs): |
51 | 37 | super(SearchableDocumentsField, self).__init__(*args, **kwargs) |
| 38 | + self.doc_type = doc_type |
52 | 39 |
|
53 | | - self.widget.attrs["class"] = "select2-field form-control" |
54 | | - self.widget.attrs["data-placeholder"] = hint_text |
55 | | - if self.max_entries != None: |
56 | | - self.widget.attrs["data-max-entries"] = self.max_entries |
57 | | - |
58 | | - def parse_select2_value(self, value): |
59 | | - return [x.strip() for x in value.split(",") if x.strip()] |
60 | | - |
61 | | - def prepare_value(self, value): |
62 | | - if not value: |
63 | | - value = "" |
64 | | - if isinstance(value, int): |
65 | | - value = str(value) |
66 | | - if isinstance(value, str): |
67 | | - items = self.parse_select2_value(value) |
68 | | - # accept both names and pks here |
69 | | - names = [ i for i in items if not i.isdigit() ] |
70 | | - ids = [ i for i in items if i.isdigit() ] |
71 | | - value = self.model.objects.filter(Q(name__in=names)|Q(id__in=ids)) |
72 | | - filter_args = {} |
73 | | - if self.model == DocAlias: |
74 | | - filter_args["docs__type"] = self.doc_type |
75 | | - else: |
76 | | - filter_args["type"] = self.doc_type |
77 | | - value = value.filter(**filter_args) |
78 | | - if isinstance(value, self.model): |
79 | | - value = [value] |
80 | | - |
81 | | - self.widget.attrs["data-pre"] = json.dumps({ |
82 | | - d['id']: d for d in select2_id_doc_name(value) |
83 | | - }) |
84 | | - |
85 | | - # doing this in the constructor is difficult because the URL |
86 | | - # patterns may not have been fully constructed there yet |
87 | | - self.widget.attrs["data-ajax-url"] = urlreverse('ietf.doc.views_search.ajax_select2_search_docs', kwargs={ |
| 40 | + def doc_type_filter(self, queryset): |
| 41 | + """Filter to include only desired doc type""" |
| 42 | + return queryset.filter(type=self.doc_type) |
| 43 | + |
| 44 | + def get_model_instances(self, item_ids): |
| 45 | + """Get model instances corresponding to item identifiers in select2 field value |
| 46 | +
|
| 47 | + Accepts both names and pks as IDs |
| 48 | + """ |
| 49 | + names = [ i for i in item_ids if not i.isdigit() ] |
| 50 | + ids = [ i for i in item_ids if i.isdigit() ] |
| 51 | + objs = self.model.objects.filter( |
| 52 | + Q(name__in=names)|Q(id__in=ids) |
| 53 | + ) |
| 54 | + return self.doc_type_filter(objs) |
| 55 | + |
| 56 | + def make_select2_data(self, model_instances): |
| 57 | + """Get select2 data items""" |
| 58 | + return select2_id_doc_name(model_instances) |
| 59 | + |
| 60 | + def ajax_url(self): |
| 61 | + """Get the URL for AJAX searches""" |
| 62 | + return urlreverse('ietf.doc.views_search.ajax_select2_search_docs', kwargs={ |
88 | 63 | "doc_type": self.doc_type, |
89 | 64 | "model_name": self.model.__name__.lower() |
90 | 65 | }) |
91 | 66 |
|
92 | | - return ",".join(str(o.pk) for o in value) |
93 | | - |
94 | | - def clean(self, value): |
95 | | - value = super(SearchableDocumentsField, self).clean(value) |
96 | | - pks = self.parse_select2_value(value) |
97 | | - |
98 | | - try: |
99 | | - objs = self.model.objects.filter(pk__in=pks) |
100 | | - except ValueError as e: |
101 | | - raise forms.ValidationError("Unexpected field value; %s" % e) |
102 | | - |
103 | | - found_pks = [ str(o.pk) for o in objs ] |
104 | | - failed_pks = [ x for x in pks if x not in found_pks ] |
105 | | - if failed_pks: |
106 | | - raise forms.ValidationError("Could not recognize the following documents: {names}. You can only input documents already registered in the Datatracker.".format(names=", ".join(failed_pks))) |
107 | | - |
108 | | - if self.max_entries != None and len(objs) > self.max_entries: |
109 | | - raise forms.ValidationError("You can select at most %s entries." % self.max_entries) |
110 | | - |
111 | | - return objs |
112 | 67 |
|
113 | 68 | class SearchableDocumentField(SearchableDocumentsField): |
114 | | - """Specialized to only return one Document.""" |
115 | | - def __init__(self, model=Document, *args, **kwargs): |
116 | | - kwargs["max_entries"] = 1 |
117 | | - super(SearchableDocumentField, self).__init__(model=model, *args, **kwargs) |
| 69 | + """Specialized to only return one Document""" |
| 70 | + max_entries = 1 |
| 71 | + |
118 | 72 |
|
119 | | - def clean(self, value): |
120 | | - return super(SearchableDocumentField, self).clean(value).first() |
121 | | - |
122 | 73 | class SearchableDocAliasesField(SearchableDocumentsField): |
123 | | - def __init__(self, model=DocAlias, *args, **kwargs): |
124 | | - super(SearchableDocAliasesField, self).__init__(model=model, *args, **kwargs) |
| 74 | + """Search DocAliases instead of Documents""" |
| 75 | + model = DocAlias # type: Type[models.Model] |
125 | 76 |
|
126 | | -class SearchableDocAliasField(SearchableDocumentsField): |
127 | | - """Specialized to only return one DocAlias.""" |
128 | | - def __init__(self, model=DocAlias, *args, **kwargs): |
129 | | - kwargs["max_entries"] = 1 |
130 | | - super(SearchableDocAliasField, self).__init__(model=model, *args, **kwargs) |
| 77 | + def doc_type_filter(self, queryset): |
| 78 | + """Filter to include only desired doc type |
131 | 79 |
|
132 | | - def clean(self, value): |
133 | | - return super(SearchableDocAliasField, self).clean(value).first() |
| 80 | + For DocAlias, pass through to the docs to check type. |
| 81 | + """ |
| 82 | + return queryset.filter(docs__type=self.doc_type) |
134 | 83 |
|
135 | | - |
| 84 | +class SearchableDocAliasField(SearchableDocAliasesField): |
| 85 | + """Specialized to only return one DocAlias""" |
| 86 | + max_entries = 1 |
0 commit comments