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.

utils.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. import datetime
  2. import decimal
  3. from collections import defaultdict
  4. from django.core.exceptions import FieldDoesNotExist
  5. from django.db import models, router
  6. from django.db.models.constants import LOOKUP_SEP
  7. from django.db.models.deletion import Collector
  8. from django.forms.utils import pretty_name
  9. from django.urls import NoReverseMatch, reverse
  10. from django.utils import formats, timezone
  11. from django.utils.html import format_html
  12. from django.utils.text import capfirst
  13. from django.utils.translation import ngettext, override as translation_override
  14. class FieldIsAForeignKeyColumnName(Exception):
  15. """A field is a foreign key attname, i.e. <FK>_id."""
  16. pass
  17. def lookup_needs_distinct(opts, lookup_path):
  18. """
  19. Return True if 'distinct()' should be used to query the given lookup path.
  20. """
  21. lookup_fields = lookup_path.split(LOOKUP_SEP)
  22. # Go through the fields (following all relations) and look for an m2m.
  23. for field_name in lookup_fields:
  24. if field_name == 'pk':
  25. field_name = opts.pk.name
  26. try:
  27. field = opts.get_field(field_name)
  28. except FieldDoesNotExist:
  29. # Ignore query lookups.
  30. continue
  31. else:
  32. if hasattr(field, 'get_path_info'):
  33. # This field is a relation; update opts to follow the relation.
  34. path_info = field.get_path_info()
  35. opts = path_info[-1].to_opts
  36. if any(path.m2m for path in path_info):
  37. # This field is a m2m relation so distinct must be called.
  38. return True
  39. return False
  40. def prepare_lookup_value(key, value):
  41. """
  42. Return a lookup value prepared to be used in queryset filtering.
  43. """
  44. # if key ends with __in, split parameter into separate values
  45. if key.endswith('__in'):
  46. value = value.split(',')
  47. # if key ends with __isnull, special case '' and the string literals 'false' and '0'
  48. elif key.endswith('__isnull'):
  49. value = value.lower() not in ('', 'false', '0')
  50. return value
  51. def quote(s):
  52. """
  53. Ensure that primary key values do not confuse the admin URLs by escaping
  54. any '/', '_' and ':' and similarly problematic characters.
  55. Similar to urllib.parse.quote(), except that the quoting is slightly
  56. different so that it doesn't get automatically unquoted by the Web browser.
  57. """
  58. if not isinstance(s, str):
  59. return s
  60. res = list(s)
  61. for i in range(len(res)):
  62. c = res[i]
  63. if c in """:/_#?;@&=+$,"[]<>%\n\\""":
  64. res[i] = '_%02X' % ord(c)
  65. return ''.join(res)
  66. def unquote(s):
  67. """Undo the effects of quote(). Based heavily on urllib.parse.unquote()."""
  68. mychr = chr
  69. myatoi = int
  70. list = s.split('_')
  71. res = [list[0]]
  72. myappend = res.append
  73. del list[0]
  74. for item in list:
  75. if item[1:2]:
  76. try:
  77. myappend(mychr(myatoi(item[:2], 16)) + item[2:])
  78. except ValueError:
  79. myappend('_' + item)
  80. else:
  81. myappend('_' + item)
  82. return "".join(res)
  83. def flatten(fields):
  84. """
  85. Return a list which is a single level of flattening of the original list.
  86. """
  87. flat = []
  88. for field in fields:
  89. if isinstance(field, (list, tuple)):
  90. flat.extend(field)
  91. else:
  92. flat.append(field)
  93. return flat
  94. def flatten_fieldsets(fieldsets):
  95. """Return a list of field names from an admin fieldsets structure."""
  96. field_names = []
  97. for name, opts in fieldsets:
  98. field_names.extend(
  99. flatten(opts['fields'])
  100. )
  101. return field_names
  102. def get_deleted_objects(objs, request, admin_site):
  103. """
  104. Find all objects related to ``objs`` that should also be deleted. ``objs``
  105. must be a homogeneous iterable of objects (e.g. a QuerySet).
  106. Return a nested list of strings suitable for display in the
  107. template with the ``unordered_list`` filter.
  108. """
  109. try:
  110. obj = objs[0]
  111. except IndexError:
  112. return [], {}, set(), []
  113. else:
  114. using = router.db_for_write(obj._meta.model)
  115. collector = NestedObjects(using=using)
  116. collector.collect(objs)
  117. perms_needed = set()
  118. def format_callback(obj):
  119. model = obj.__class__
  120. has_admin = model in admin_site._registry
  121. opts = obj._meta
  122. no_edit_link = '%s: %s' % (capfirst(opts.verbose_name), obj)
  123. if has_admin:
  124. if not admin_site._registry[model].has_delete_permission(request, obj):
  125. perms_needed.add(opts.verbose_name)
  126. try:
  127. admin_url = reverse('%s:%s_%s_change'
  128. % (admin_site.name,
  129. opts.app_label,
  130. opts.model_name),
  131. None, (quote(obj.pk),))
  132. except NoReverseMatch:
  133. # Change url doesn't exist -- don't display link to edit
  134. return no_edit_link
  135. # Display a link to the admin page.
  136. return format_html('{}: <a href="{}">{}</a>',
  137. capfirst(opts.verbose_name),
  138. admin_url,
  139. obj)
  140. else:
  141. # Don't display link to edit, because it either has no
  142. # admin or is edited inline.
  143. return no_edit_link
  144. to_delete = collector.nested(format_callback)
  145. protected = [format_callback(obj) for obj in collector.protected]
  146. model_count = {model._meta.verbose_name_plural: len(objs) for model, objs in collector.model_objs.items()}
  147. return to_delete, model_count, perms_needed, protected
  148. class NestedObjects(Collector):
  149. def __init__(self, *args, **kwargs):
  150. super().__init__(*args, **kwargs)
  151. self.edges = {} # {from_instance: [to_instances]}
  152. self.protected = set()
  153. self.model_objs = defaultdict(set)
  154. def add_edge(self, source, target):
  155. self.edges.setdefault(source, []).append(target)
  156. def collect(self, objs, source=None, source_attr=None, **kwargs):
  157. for obj in objs:
  158. if source_attr and not source_attr.endswith('+'):
  159. related_name = source_attr % {
  160. 'class': source._meta.model_name,
  161. 'app_label': source._meta.app_label,
  162. }
  163. self.add_edge(getattr(obj, related_name), obj)
  164. else:
  165. self.add_edge(None, obj)
  166. self.model_objs[obj._meta.model].add(obj)
  167. try:
  168. return super().collect(objs, source_attr=source_attr, **kwargs)
  169. except models.ProtectedError as e:
  170. self.protected.update(e.protected_objects)
  171. def related_objects(self, related, objs):
  172. qs = super().related_objects(related, objs)
  173. return qs.select_related(related.field.name)
  174. def _nested(self, obj, seen, format_callback):
  175. if obj in seen:
  176. return []
  177. seen.add(obj)
  178. children = []
  179. for child in self.edges.get(obj, ()):
  180. children.extend(self._nested(child, seen, format_callback))
  181. if format_callback:
  182. ret = [format_callback(obj)]
  183. else:
  184. ret = [obj]
  185. if children:
  186. ret.append(children)
  187. return ret
  188. def nested(self, format_callback=None):
  189. """
  190. Return the graph as a nested list.
  191. """
  192. seen = set()
  193. roots = []
  194. for root in self.edges.get(None, ()):
  195. roots.extend(self._nested(root, seen, format_callback))
  196. return roots
  197. def can_fast_delete(self, *args, **kwargs):
  198. """
  199. We always want to load the objects into memory so that we can display
  200. them to the user in confirm page.
  201. """
  202. return False
  203. def model_format_dict(obj):
  204. """
  205. Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
  206. typically for use with string formatting.
  207. `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
  208. """
  209. if isinstance(obj, (models.Model, models.base.ModelBase)):
  210. opts = obj._meta
  211. elif isinstance(obj, models.query.QuerySet):
  212. opts = obj.model._meta
  213. else:
  214. opts = obj
  215. return {
  216. 'verbose_name': opts.verbose_name,
  217. 'verbose_name_plural': opts.verbose_name_plural,
  218. }
  219. def model_ngettext(obj, n=None):
  220. """
  221. Return the appropriate `verbose_name` or `verbose_name_plural` value for
  222. `obj` depending on the count `n`.
  223. `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
  224. If `obj` is a `QuerySet` instance, `n` is optional and the length of the
  225. `QuerySet` is used.
  226. """
  227. if isinstance(obj, models.query.QuerySet):
  228. if n is None:
  229. n = obj.count()
  230. obj = obj.model
  231. d = model_format_dict(obj)
  232. singular, plural = d["verbose_name"], d["verbose_name_plural"]
  233. return ngettext(singular, plural, n or 0)
  234. def lookup_field(name, obj, model_admin=None):
  235. opts = obj._meta
  236. try:
  237. f = _get_non_gfk_field(opts, name)
  238. except (FieldDoesNotExist, FieldIsAForeignKeyColumnName):
  239. # For non-field values, the value is either a method, property or
  240. # returned via a callable.
  241. if callable(name):
  242. attr = name
  243. value = attr(obj)
  244. elif hasattr(model_admin, name) and name != '__str__':
  245. attr = getattr(model_admin, name)
  246. value = attr(obj)
  247. else:
  248. attr = getattr(obj, name)
  249. if callable(attr):
  250. value = attr()
  251. else:
  252. value = attr
  253. f = None
  254. else:
  255. attr = None
  256. value = getattr(obj, name)
  257. return f, attr, value
  258. def _get_non_gfk_field(opts, name):
  259. """
  260. For historical reasons, the admin app relies on GenericForeignKeys as being
  261. "not found" by get_field(). This could likely be cleaned up.
  262. Reverse relations should also be excluded as these aren't attributes of the
  263. model (rather something like `foo_set`).
  264. """
  265. field = opts.get_field(name)
  266. if (field.is_relation and
  267. # Generic foreign keys OR reverse relations
  268. ((field.many_to_one and not field.related_model) or field.one_to_many)):
  269. raise FieldDoesNotExist()
  270. # Avoid coercing <FK>_id fields to FK
  271. if field.is_relation and not field.many_to_many and hasattr(field, 'attname') and field.attname == name:
  272. raise FieldIsAForeignKeyColumnName()
  273. return field
  274. def label_for_field(name, model, model_admin=None, return_attr=False, form=None):
  275. """
  276. Return a sensible label for a field name. The name can be a callable,
  277. property (but not created with @property decorator), or the name of an
  278. object's attribute, as well as a model field. If return_attr is True, also
  279. return the resolved attribute (which could be a callable). This will be
  280. None if (and only if) the name refers to a field.
  281. """
  282. attr = None
  283. try:
  284. field = _get_non_gfk_field(model._meta, name)
  285. try:
  286. label = field.verbose_name
  287. except AttributeError:
  288. # field is likely a ForeignObjectRel
  289. label = field.related_model._meta.verbose_name
  290. except FieldDoesNotExist:
  291. if name == "__str__":
  292. label = str(model._meta.verbose_name)
  293. attr = str
  294. else:
  295. if callable(name):
  296. attr = name
  297. elif hasattr(model_admin, name):
  298. attr = getattr(model_admin, name)
  299. elif hasattr(model, name):
  300. attr = getattr(model, name)
  301. elif form and name in form.fields:
  302. attr = form.fields[name]
  303. else:
  304. message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name)
  305. if model_admin:
  306. message += " or %s" % (model_admin.__class__.__name__,)
  307. if form:
  308. message += " or %s" % form.__class__.__name__
  309. raise AttributeError(message)
  310. if hasattr(attr, "short_description"):
  311. label = attr.short_description
  312. elif (isinstance(attr, property) and
  313. hasattr(attr, "fget") and
  314. hasattr(attr.fget, "short_description")):
  315. label = attr.fget.short_description
  316. elif callable(attr):
  317. if attr.__name__ == "<lambda>":
  318. label = "--"
  319. else:
  320. label = pretty_name(attr.__name__)
  321. else:
  322. label = pretty_name(name)
  323. except FieldIsAForeignKeyColumnName:
  324. label = pretty_name(name)
  325. attr = name
  326. if return_attr:
  327. return (label, attr)
  328. else:
  329. return label
  330. def help_text_for_field(name, model):
  331. help_text = ""
  332. try:
  333. field = _get_non_gfk_field(model._meta, name)
  334. except (FieldDoesNotExist, FieldIsAForeignKeyColumnName):
  335. pass
  336. else:
  337. if hasattr(field, 'help_text'):
  338. help_text = field.help_text
  339. return help_text
  340. def display_for_field(value, field, empty_value_display):
  341. from django.contrib.admin.templatetags.admin_list import _boolean_icon
  342. if getattr(field, 'flatchoices', None):
  343. return dict(field.flatchoices).get(value, empty_value_display)
  344. # BooleanField needs special-case null-handling, so it comes before the
  345. # general null test.
  346. elif isinstance(field, models.BooleanField):
  347. return _boolean_icon(value)
  348. elif value is None:
  349. return empty_value_display
  350. elif isinstance(field, models.DateTimeField):
  351. return formats.localize(timezone.template_localtime(value))
  352. elif isinstance(field, (models.DateField, models.TimeField)):
  353. return formats.localize(value)
  354. elif isinstance(field, models.DecimalField):
  355. return formats.number_format(value, field.decimal_places)
  356. elif isinstance(field, (models.IntegerField, models.FloatField)):
  357. return formats.number_format(value)
  358. elif isinstance(field, models.FileField) and value:
  359. return format_html('<a href="{}">{}</a>', value.url, value)
  360. else:
  361. return display_for_value(value, empty_value_display)
  362. def display_for_value(value, empty_value_display, boolean=False):
  363. from django.contrib.admin.templatetags.admin_list import _boolean_icon
  364. if boolean:
  365. return _boolean_icon(value)
  366. elif value is None:
  367. return empty_value_display
  368. elif isinstance(value, bool):
  369. return str(value)
  370. elif isinstance(value, datetime.datetime):
  371. return formats.localize(timezone.template_localtime(value))
  372. elif isinstance(value, (datetime.date, datetime.time)):
  373. return formats.localize(value)
  374. elif isinstance(value, (int, decimal.Decimal, float)):
  375. return formats.number_format(value)
  376. elif isinstance(value, (list, tuple)):
  377. return ', '.join(str(v) for v in value)
  378. else:
  379. return str(value)
  380. class NotRelationField(Exception):
  381. pass
  382. def get_model_from_relation(field):
  383. if hasattr(field, 'get_path_info'):
  384. return field.get_path_info()[-1].to_opts.model
  385. else:
  386. raise NotRelationField
  387. def reverse_field_path(model, path):
  388. """ Create a reversed field path.
  389. E.g. Given (Order, "user__groups"),
  390. return (Group, "user__order").
  391. Final field must be a related model, not a data field.
  392. """
  393. reversed_path = []
  394. parent = model
  395. pieces = path.split(LOOKUP_SEP)
  396. for piece in pieces:
  397. field = parent._meta.get_field(piece)
  398. # skip trailing data field if extant:
  399. if len(reversed_path) == len(pieces) - 1: # final iteration
  400. try:
  401. get_model_from_relation(field)
  402. except NotRelationField:
  403. break
  404. # Field should point to another model
  405. if field.is_relation and not (field.auto_created and not field.concrete):
  406. related_name = field.related_query_name()
  407. parent = field.remote_field.model
  408. else:
  409. related_name = field.field.name
  410. parent = field.related_model
  411. reversed_path.insert(0, related_name)
  412. return (parent, LOOKUP_SEP.join(reversed_path))
  413. def get_fields_from_path(model, path):
  414. """ Return list of Fields given path relative to model.
  415. e.g. (ModelX, "user__groups__name") -> [
  416. <django.db.models.fields.related.ForeignKey object at 0x...>,
  417. <django.db.models.fields.related.ManyToManyField object at 0x...>,
  418. <django.db.models.fields.CharField object at 0x...>,
  419. ]
  420. """
  421. pieces = path.split(LOOKUP_SEP)
  422. fields = []
  423. for piece in pieces:
  424. if fields:
  425. parent = get_model_from_relation(fields[-1])
  426. else:
  427. parent = model
  428. fields.append(parent._meta.get_field(piece))
  429. return fields
  430. def construct_change_message(form, formsets, add):
  431. """
  432. Construct a JSON structure describing changes from a changed object.
  433. Translations are deactivated so that strings are stored untranslated.
  434. Translation happens later on LogEntry access.
  435. """
  436. change_message = []
  437. if add:
  438. change_message.append({'added': {}})
  439. elif form.changed_data:
  440. change_message.append({'changed': {'fields': form.changed_data}})
  441. if formsets:
  442. with translation_override(None):
  443. for formset in formsets:
  444. for added_object in formset.new_objects:
  445. change_message.append({
  446. 'added': {
  447. 'name': str(added_object._meta.verbose_name),
  448. 'object': str(added_object),
  449. }
  450. })
  451. for changed_object, changed_fields in formset.changed_objects:
  452. change_message.append({
  453. 'changed': {
  454. 'name': str(changed_object._meta.verbose_name),
  455. 'object': str(changed_object),
  456. 'fields': changed_fields,
  457. }
  458. })
  459. for deleted_object in formset.deleted_objects:
  460. change_message.append({
  461. 'deleted': {
  462. 'name': str(deleted_object._meta.verbose_name),
  463. 'object': str(deleted_object),
  464. }
  465. })
  466. return change_message