Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

widgets.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. """
  2. Form Widget classes specific to the Django admin site.
  3. """
  4. import copy
  5. import json
  6. from django import forms
  7. from django.conf import settings
  8. from django.core.exceptions import ValidationError
  9. from django.db.models.deletion import CASCADE
  10. from django.urls import reverse
  11. from django.urls.exceptions import NoReverseMatch
  12. from django.utils.html import smart_urlquote
  13. from django.utils.safestring import mark_safe
  14. from django.utils.text import Truncator
  15. from django.utils.translation import get_language, gettext as _
  16. class FilteredSelectMultiple(forms.SelectMultiple):
  17. """
  18. A SelectMultiple with a JavaScript filter interface.
  19. Note that the resulting JavaScript assumes that the jsi18n
  20. catalog has been loaded in the page
  21. """
  22. @property
  23. def media(self):
  24. extra = '' if settings.DEBUG else '.min'
  25. js = [
  26. 'vendor/jquery/jquery%s.js' % extra,
  27. 'jquery.init.js',
  28. 'core.js',
  29. 'SelectBox.js',
  30. 'SelectFilter2.js',
  31. ]
  32. return forms.Media(js=["admin/js/%s" % path for path in js])
  33. def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
  34. self.verbose_name = verbose_name
  35. self.is_stacked = is_stacked
  36. super().__init__(attrs, choices)
  37. def get_context(self, name, value, attrs):
  38. context = super().get_context(name, value, attrs)
  39. context['widget']['attrs']['class'] = 'selectfilter'
  40. if self.is_stacked:
  41. context['widget']['attrs']['class'] += 'stacked'
  42. context['widget']['attrs']['data-field-name'] = self.verbose_name
  43. context['widget']['attrs']['data-is-stacked'] = int(self.is_stacked)
  44. return context
  45. class AdminDateWidget(forms.DateInput):
  46. @property
  47. def media(self):
  48. extra = '' if settings.DEBUG else '.min'
  49. js = [
  50. 'vendor/jquery/jquery%s.js' % extra,
  51. 'jquery.init.js',
  52. 'calendar.js',
  53. 'admin/DateTimeShortcuts.js',
  54. ]
  55. return forms.Media(js=["admin/js/%s" % path for path in js])
  56. def __init__(self, attrs=None, format=None):
  57. attrs = {'class': 'vDateField', 'size': '10', **(attrs or {})}
  58. super().__init__(attrs=attrs, format=format)
  59. class AdminTimeWidget(forms.TimeInput):
  60. @property
  61. def media(self):
  62. extra = '' if settings.DEBUG else '.min'
  63. js = [
  64. 'vendor/jquery/jquery%s.js' % extra,
  65. 'jquery.init.js',
  66. 'calendar.js',
  67. 'admin/DateTimeShortcuts.js',
  68. ]
  69. return forms.Media(js=["admin/js/%s" % path for path in js])
  70. def __init__(self, attrs=None, format=None):
  71. attrs = {'class': 'vTimeField', 'size': '8', **(attrs or {})}
  72. super().__init__(attrs=attrs, format=format)
  73. class AdminSplitDateTime(forms.SplitDateTimeWidget):
  74. """
  75. A SplitDateTime Widget that has some admin-specific styling.
  76. """
  77. template_name = 'admin/widgets/split_datetime.html'
  78. def __init__(self, attrs=None):
  79. widgets = [AdminDateWidget, AdminTimeWidget]
  80. # Note that we're calling MultiWidget, not SplitDateTimeWidget, because
  81. # we want to define widgets.
  82. forms.MultiWidget.__init__(self, widgets, attrs)
  83. def get_context(self, name, value, attrs):
  84. context = super().get_context(name, value, attrs)
  85. context['date_label'] = _('Date:')
  86. context['time_label'] = _('Time:')
  87. return context
  88. class AdminRadioSelect(forms.RadioSelect):
  89. template_name = 'admin/widgets/radio.html'
  90. class AdminFileWidget(forms.ClearableFileInput):
  91. template_name = 'admin/widgets/clearable_file_input.html'
  92. def url_params_from_lookup_dict(lookups):
  93. """
  94. Convert the type of lookups specified in a ForeignKey limit_choices_to
  95. attribute to a dictionary of query parameters
  96. """
  97. params = {}
  98. if lookups and hasattr(lookups, 'items'):
  99. for k, v in lookups.items():
  100. if callable(v):
  101. v = v()
  102. if isinstance(v, (tuple, list)):
  103. v = ','.join(str(x) for x in v)
  104. elif isinstance(v, bool):
  105. v = ('0', '1')[v]
  106. else:
  107. v = str(v)
  108. params[k] = v
  109. return params
  110. class ForeignKeyRawIdWidget(forms.TextInput):
  111. """
  112. A Widget for displaying ForeignKeys in the "raw_id" interface rather than
  113. in a <select> box.
  114. """
  115. template_name = 'admin/widgets/foreign_key_raw_id.html'
  116. def __init__(self, rel, admin_site, attrs=None, using=None):
  117. self.rel = rel
  118. self.admin_site = admin_site
  119. self.db = using
  120. super().__init__(attrs)
  121. def get_context(self, name, value, attrs):
  122. context = super().get_context(name, value, attrs)
  123. rel_to = self.rel.model
  124. if rel_to in self.admin_site._registry:
  125. # The related object is registered with the same AdminSite
  126. related_url = reverse(
  127. 'admin:%s_%s_changelist' % (
  128. rel_to._meta.app_label,
  129. rel_to._meta.model_name,
  130. ),
  131. current_app=self.admin_site.name,
  132. )
  133. params = self.url_parameters()
  134. if params:
  135. related_url += '?' + '&amp;'.join('%s=%s' % (k, v) for k, v in params.items())
  136. context['related_url'] = mark_safe(related_url)
  137. context['link_title'] = _('Lookup')
  138. # The JavaScript code looks for this class.
  139. context['widget']['attrs'].setdefault('class', 'vForeignKeyRawIdAdminField')
  140. if context['widget']['value']:
  141. context['link_label'], context['link_url'] = self.label_and_url_for_value(value)
  142. return context
  143. def base_url_parameters(self):
  144. limit_choices_to = self.rel.limit_choices_to
  145. if callable(limit_choices_to):
  146. limit_choices_to = limit_choices_to()
  147. return url_params_from_lookup_dict(limit_choices_to)
  148. def url_parameters(self):
  149. from django.contrib.admin.views.main import TO_FIELD_VAR
  150. params = self.base_url_parameters()
  151. params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
  152. return params
  153. def label_and_url_for_value(self, value):
  154. key = self.rel.get_related_field().name
  155. try:
  156. obj = self.rel.model._default_manager.using(self.db).get(**{key: value})
  157. except (ValueError, self.rel.model.DoesNotExist, ValidationError):
  158. return '', ''
  159. try:
  160. url = reverse(
  161. '%s:%s_%s_change' % (
  162. self.admin_site.name,
  163. obj._meta.app_label,
  164. obj._meta.object_name.lower(),
  165. ),
  166. args=(obj.pk,)
  167. )
  168. except NoReverseMatch:
  169. url = '' # Admin not registered for target model.
  170. return Truncator(obj).words(14, truncate='...'), url
  171. class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
  172. """
  173. A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
  174. in a <select multiple> box.
  175. """
  176. template_name = 'admin/widgets/many_to_many_raw_id.html'
  177. def get_context(self, name, value, attrs):
  178. context = super().get_context(name, value, attrs)
  179. if self.rel.model in self.admin_site._registry:
  180. # The related object is registered with the same AdminSite
  181. context['widget']['attrs']['class'] = 'vManyToManyRawIdAdminField'
  182. return context
  183. def url_parameters(self):
  184. return self.base_url_parameters()
  185. def label_and_url_for_value(self, value):
  186. return '', ''
  187. def value_from_datadict(self, data, files, name):
  188. value = data.get(name)
  189. if value:
  190. return value.split(',')
  191. def format_value(self, value):
  192. return ','.join(str(v) for v in value) if value else ''
  193. class RelatedFieldWidgetWrapper(forms.Widget):
  194. """
  195. This class is a wrapper to a given widget to add the add icon for the
  196. admin interface.
  197. """
  198. template_name = 'admin/widgets/related_widget_wrapper.html'
  199. def __init__(self, widget, rel, admin_site, can_add_related=None,
  200. can_change_related=False, can_delete_related=False,
  201. can_view_related=False):
  202. self.needs_multipart_form = widget.needs_multipart_form
  203. self.attrs = widget.attrs
  204. self.choices = widget.choices
  205. self.widget = widget
  206. self.rel = rel
  207. # Backwards compatible check for whether a user can add related
  208. # objects.
  209. if can_add_related is None:
  210. can_add_related = rel.model in admin_site._registry
  211. self.can_add_related = can_add_related
  212. # XXX: The UX does not support multiple selected values.
  213. multiple = getattr(widget, 'allow_multiple_selected', False)
  214. self.can_change_related = not multiple and can_change_related
  215. # XXX: The deletion UX can be confusing when dealing with cascading deletion.
  216. cascade = getattr(rel, 'on_delete', None) is CASCADE
  217. self.can_delete_related = not multiple and not cascade and can_delete_related
  218. self.can_view_related = not multiple and can_view_related
  219. # so we can check if the related object is registered with this AdminSite
  220. self.admin_site = admin_site
  221. def __deepcopy__(self, memo):
  222. obj = copy.copy(self)
  223. obj.widget = copy.deepcopy(self.widget, memo)
  224. obj.attrs = self.widget.attrs
  225. memo[id(self)] = obj
  226. return obj
  227. @property
  228. def is_hidden(self):
  229. return self.widget.is_hidden
  230. @property
  231. def media(self):
  232. return self.widget.media
  233. def get_related_url(self, info, action, *args):
  234. return reverse("admin:%s_%s_%s" % (info + (action,)),
  235. current_app=self.admin_site.name, args=args)
  236. def get_context(self, name, value, attrs):
  237. from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR
  238. rel_opts = self.rel.model._meta
  239. info = (rel_opts.app_label, rel_opts.model_name)
  240. self.widget.choices = self.choices
  241. url_params = '&'.join("%s=%s" % param for param in [
  242. (TO_FIELD_VAR, self.rel.get_related_field().name),
  243. (IS_POPUP_VAR, 1),
  244. ])
  245. context = {
  246. 'rendered_widget': self.widget.render(name, value, attrs),
  247. 'name': name,
  248. 'url_params': url_params,
  249. 'model': rel_opts.verbose_name,
  250. 'can_add_related': self.can_add_related,
  251. 'can_change_related': self.can_change_related,
  252. 'can_delete_related': self.can_delete_related,
  253. 'can_view_related': self.can_view_related,
  254. }
  255. if self.can_add_related:
  256. context['add_related_url'] = self.get_related_url(info, 'add')
  257. if self.can_delete_related:
  258. context['delete_related_template_url'] = self.get_related_url(info, 'delete', '__fk__')
  259. if self.can_view_related or self.can_change_related:
  260. context['change_related_template_url'] = self.get_related_url(info, 'change', '__fk__')
  261. return context
  262. def value_from_datadict(self, data, files, name):
  263. return self.widget.value_from_datadict(data, files, name)
  264. def value_omitted_from_data(self, data, files, name):
  265. return self.widget.value_omitted_from_data(data, files, name)
  266. def id_for_label(self, id_):
  267. return self.widget.id_for_label(id_)
  268. class AdminTextareaWidget(forms.Textarea):
  269. def __init__(self, attrs=None):
  270. super().__init__(attrs={'class': 'vLargeTextField', **(attrs or {})})
  271. class AdminTextInputWidget(forms.TextInput):
  272. def __init__(self, attrs=None):
  273. super().__init__(attrs={'class': 'vTextField', **(attrs or {})})
  274. class AdminEmailInputWidget(forms.EmailInput):
  275. def __init__(self, attrs=None):
  276. super().__init__(attrs={'class': 'vTextField', **(attrs or {})})
  277. class AdminURLFieldWidget(forms.URLInput):
  278. template_name = 'admin/widgets/url.html'
  279. def __init__(self, attrs=None):
  280. super().__init__(attrs={'class': 'vURLField', **(attrs or {})})
  281. def get_context(self, name, value, attrs):
  282. context = super().get_context(name, value, attrs)
  283. context['current_label'] = _('Currently:')
  284. context['change_label'] = _('Change:')
  285. context['widget']['href'] = smart_urlquote(context['widget']['value']) if value else ''
  286. return context
  287. class AdminIntegerFieldWidget(forms.NumberInput):
  288. class_name = 'vIntegerField'
  289. def __init__(self, attrs=None):
  290. super().__init__(attrs={'class': self.class_name, **(attrs or {})})
  291. class AdminBigIntegerFieldWidget(AdminIntegerFieldWidget):
  292. class_name = 'vBigIntegerField'
  293. # Mapping of lower case language codes [returned by Django's get_language()]
  294. # to language codes supported by select2.
  295. # See django/contrib/admin/static/admin/js/vendor/select2/i18n/*
  296. SELECT2_TRANSLATIONS = {x.lower(): x for x in [
  297. 'ar', 'az', 'bg', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'et',
  298. 'eu', 'fa', 'fi', 'fr', 'gl', 'he', 'hi', 'hr', 'hu', 'id', 'is',
  299. 'it', 'ja', 'km', 'ko', 'lt', 'lv', 'mk', 'ms', 'nb', 'nl', 'pl',
  300. 'pt-BR', 'pt', 'ro', 'ru', 'sk', 'sr-Cyrl', 'sr', 'sv', 'th',
  301. 'tr', 'uk', 'vi',
  302. ]}
  303. SELECT2_TRANSLATIONS.update({'zh-hans': 'zh-CN', 'zh-hant': 'zh-TW'})
  304. class AutocompleteMixin:
  305. """
  306. Select widget mixin that loads options from AutocompleteJsonView via AJAX.
  307. Renders the necessary data attributes for select2 and adds the static form
  308. media.
  309. """
  310. url_name = '%s:%s_%s_autocomplete'
  311. def __init__(self, rel, admin_site, attrs=None, choices=(), using=None):
  312. self.rel = rel
  313. self.admin_site = admin_site
  314. self.db = using
  315. self.choices = choices
  316. self.attrs = {} if attrs is None else attrs.copy()
  317. def get_url(self):
  318. model = self.rel.model
  319. return reverse(self.url_name % (self.admin_site.name, model._meta.app_label, model._meta.model_name))
  320. def build_attrs(self, base_attrs, extra_attrs=None):
  321. """
  322. Set select2's AJAX attributes.
  323. Attributes can be set using the html5 data attribute.
  324. Nested attributes require a double dash as per
  325. https://select2.org/configuration/data-attributes#nested-subkey-options
  326. """
  327. attrs = super().build_attrs(base_attrs, extra_attrs=extra_attrs)
  328. attrs.setdefault('class', '')
  329. attrs.update({
  330. 'data-ajax--cache': 'true',
  331. 'data-ajax--type': 'GET',
  332. 'data-ajax--url': self.get_url(),
  333. 'data-theme': 'admin-autocomplete',
  334. 'data-allow-clear': json.dumps(not self.is_required),
  335. 'data-placeholder': '', # Allows clearing of the input.
  336. 'class': attrs['class'] + (' ' if attrs['class'] else '') + 'admin-autocomplete',
  337. })
  338. return attrs
  339. def optgroups(self, name, value, attr=None):
  340. """Return selected options based on the ModelChoiceIterator."""
  341. default = (None, [], 0)
  342. groups = [default]
  343. has_selected = False
  344. selected_choices = {
  345. str(v) for v in value
  346. if str(v) not in self.choices.field.empty_values
  347. }
  348. if not self.is_required and not self.allow_multiple_selected:
  349. default[1].append(self.create_option(name, '', '', False, 0))
  350. choices = (
  351. (obj.pk, self.choices.field.label_from_instance(obj))
  352. for obj in self.choices.queryset.using(self.db).filter(pk__in=selected_choices)
  353. )
  354. for option_value, option_label in choices:
  355. selected = (
  356. str(option_value) in value and
  357. (has_selected is False or self.allow_multiple_selected)
  358. )
  359. has_selected |= selected
  360. index = len(default[1])
  361. subgroup = default[1]
  362. subgroup.append(self.create_option(name, option_value, option_label, selected_choices, index))
  363. return groups
  364. @property
  365. def media(self):
  366. extra = '' if settings.DEBUG else '.min'
  367. i18n_name = SELECT2_TRANSLATIONS.get(get_language())
  368. i18n_file = ('admin/js/vendor/select2/i18n/%s.js' % i18n_name,) if i18n_name else ()
  369. return forms.Media(
  370. js=(
  371. 'admin/js/vendor/jquery/jquery%s.js' % extra,
  372. 'admin/js/vendor/select2/select2.full%s.js' % extra,
  373. ) + i18n_file + (
  374. 'admin/js/jquery.init.js',
  375. 'admin/js/autocomplete.js',
  376. ),
  377. css={
  378. 'screen': (
  379. 'admin/css/vendor/select2/select2%s.css' % extra,
  380. 'admin/css/autocomplete.css',
  381. ),
  382. },
  383. )
  384. class AutocompleteSelect(AutocompleteMixin, forms.Select):
  385. pass
  386. class AutocompleteSelectMultiple(AutocompleteMixin, forms.SelectMultiple):
  387. pass