|
13 | 13 |
|
14 | 14 | from ietf.doc.models import Document, DocAlias |
15 | 15 | from ietf.doc.utils import uppercase_std_abbreviated_name |
| 16 | +from ietf.utils.fields import SearchableField |
16 | 17 |
|
17 | 18 | def select2_id_doc_name(objs): |
18 | 19 | return [{ |
19 | | - "id": o.pk, |
20 | | - "text": escape(uppercase_std_abbreviated_name(o.name)), |
| 20 | + "id": o.pk, |
| 21 | + "text": escape(uppercase_std_abbreviated_name(o.name)), |
21 | 22 | } for o in objs] |
22 | 23 |
|
23 | 24 |
|
24 | 25 | def select2_id_doc_name_json(objs): |
25 | 26 | return json.dumps(select2_id_doc_name(objs)) |
26 | 27 |
|
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 | 28 |
|
| 29 | +class SearchableDocumentsField(SearchableField): |
| 30 | + """Server-based multi-select field for choosing documents using select2.js. """ |
| 31 | + model = Document |
| 32 | + default_hint_text = "Type name to search for document" |
| 33 | + |
| 34 | + def __init__(self, doc_type="draft", *args, **kwargs): |
51 | 35 | super(SearchableDocumentsField, self).__init__(*args, **kwargs) |
| 36 | + self.doc_type = doc_type |
52 | 37 |
|
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={ |
| 38 | + def doc_type_filter(self, queryset): |
| 39 | + """Filter to include only desired doc type""" |
| 40 | + return queryset.filter(type=self.doc_type) |
| 41 | + |
| 42 | + def get_model_instances(self, item_ids): |
| 43 | + """Get model instances corresponding to item identifiers in select2 field value |
| 44 | +
|
| 45 | + Accepts both names and pks as IDs |
| 46 | + """ |
| 47 | + names = [ i for i in item_ids if not i.isdigit() ] |
| 48 | + ids = [ i for i in item_ids if i.isdigit() ] |
| 49 | + objs = self.model.objects.filter( |
| 50 | + Q(name__in=names)|Q(id__in=ids) |
| 51 | + ) |
| 52 | + return self.doc_type_filter(objs) |
| 53 | + |
| 54 | + def make_select2_data(self, model_instances): |
| 55 | + """Get select2 data items""" |
| 56 | + return select2_id_doc_name(model_instances) |
| 57 | + |
| 58 | + def ajax_url(self): |
| 59 | + """Get the URL for AJAX searches""" |
| 60 | + return urlreverse('ietf.doc.views_search.ajax_select2_search_docs', kwargs={ |
88 | 61 | "doc_type": self.doc_type, |
89 | 62 | "model_name": self.model.__name__.lower() |
90 | 63 | }) |
91 | 64 |
|
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 | 65 |
|
113 | 66 | 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) |
| 67 | + """Specialized to only return one Document""" |
| 68 | + max_entries = 1 |
| 69 | + |
118 | 70 |
|
119 | | - def clean(self, value): |
120 | | - return super(SearchableDocumentField, self).clean(value).first() |
121 | | - |
122 | 71 | class SearchableDocAliasesField(SearchableDocumentsField): |
123 | | - def __init__(self, model=DocAlias, *args, **kwargs): |
124 | | - super(SearchableDocAliasesField, self).__init__(model=model, *args, **kwargs) |
| 72 | + """Search DocAliases instead of Documents""" |
| 73 | + model = DocAlias |
125 | 74 |
|
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) |
| 75 | + def doc_type_filter(self, queryset): |
| 76 | + """Filter to include only desired doc type |
131 | 77 |
|
132 | | - def clean(self, value): |
133 | | - return super(SearchableDocAliasField, self).clean(value).first() |
| 78 | + For DocAlias, pass through to the docs to check type. |
| 79 | + """ |
| 80 | + return queryset.filter(docs__type=self.doc_type) |
134 | 81 |
|
135 | | - |
| 82 | +class SearchableDocAliasField(SearchableDocAliasesField): |
| 83 | + """Specialized to only return one DocAlias""" |
| 84 | + max_entries = 1 |
0 commit comments