views.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. from copy import deepcopy
  2. from django.apps import apps
  3. from django.shortcuts import render
  4. from django.conf import settings
  5. from django.db.models.fields import related
  6. from django.template.loader import get_template
  7. import json
  8. from django.views.generic import TemplateView
  9. class Plate(TemplateView):
  10. """
  11. This class-based-view serves up spaghetti and meatballs.
  12. Override the following class properties when calling `as_view`:
  13. * `settings` - sets a view specific to use instead of the `SPAGHETTI_SAUCE` django settings
  14. * `override_settings` - overrides specified settings from `SPAGHETTI_SAUCE` django settings
  15. * `plate_template_name` - overrides the template name for the whole view
  16. * `meatball_template_name` - overrides the template used to render nodes
  17. For example the below URL pattern would specify a path to a view that displayed
  18. models from the `auth` app with the given templates::
  19. url(r'^user_graph/$',
  20. Plate.as_view(
  21. settings = {
  22. 'apps': ['auth'],
  23. }
  24. meatball_template_name = "my_app/user_node.html",
  25. plate_template_name = "my_app/auth_details.html"
  26. )
  27. """
  28. settings = None
  29. override_settings = {}
  30. plate_template_name = 'django_spaghetti/plate.html'
  31. meatball_template_name = "django_spaghetti/meatball.html"
  32. def get(self, request):
  33. return self.plate()
  34. def get_view_settings(self):
  35. if self.settings is None:
  36. graph_settings = deepcopy(getattr(settings, 'SPAGHETTI_SAUCE', {}))
  37. graph_settings.update(self.override_settings)
  38. else:
  39. graph_settings = self.settings
  40. return graph_settings
  41. def get_apps_list(self):
  42. return self.get_view_settings().get('apps', [])
  43. def get_excluded_models(self):
  44. return [
  45. "%s__%s" % (app, model)
  46. for app, models in self.get_view_settings().get('exclude', {}).items()
  47. for model in models
  48. ]
  49. def get_models(self):
  50. apps_list = self.get_apps_list()
  51. excludes = self.get_excluded_models()
  52. models = apps.get_models()
  53. _models = []
  54. for model in models:
  55. if (model is None):
  56. continue
  57. app_label = model._meta.app_label
  58. model_name = model._meta.model_name
  59. if app_label not in apps_list:
  60. continue
  61. model.is_proxy = model._meta.proxy
  62. if (model.is_proxy and not self.get_view_settings().get('show_proxy', False)):
  63. continue
  64. _id = "%s__%s" % (app_label, model_name)
  65. if _id in excludes:
  66. continue
  67. _models.append(model)
  68. return _models
  69. def get_group(self, model):
  70. return model._meta.app_label
  71. def get_colours(self):
  72. return ['red', 'blue', 'green', 'yellow', 'orange']
  73. def get_groups(self):
  74. colours = self.get_colours()
  75. groups = {}
  76. for app, colour in zip(sorted(self.get_apps_list()), colours):
  77. app_info = apps.get_app_config(app)
  78. groups.update({
  79. app: {
  80. "color": {
  81. 'background': colour,
  82. 'border': 'gray'
  83. },
  84. "data": {
  85. 'name': str(app_info.verbose_name)
  86. }
  87. }
  88. })
  89. return groups
  90. def include_link_to_field(self, model, field):
  91. return True
  92. def generate_edge_style(self, model, field):
  93. edge_style = {}
  94. if str(field.name).endswith('_ptr'):
  95. # fields that end in _ptr are pointing to a parent object
  96. edge_style.update({
  97. 'arrows': {'to': {'scaleFactor': 0.75}}, # needed to draw from-to
  98. 'font': {'align': 'middle'},
  99. 'label': 'is a',
  100. 'dashes': True
  101. })
  102. elif isinstance(field, related.ForeignKey):
  103. edge_style.update({
  104. 'arrows': {'to': {'scaleFactor': 0.75}}
  105. })
  106. elif isinstance(field, related.OneToOneField):
  107. edge_style.update({
  108. 'font': {'align': 'middle'},
  109. 'label': '|'
  110. })
  111. elif isinstance(field, related.ManyToManyField):
  112. edge_style.update({
  113. 'color': {'color': 'gray'},
  114. 'arrows': {'to': {'scaleFactor': 1}, 'from': {'scaleFactor': 1}},
  115. })
  116. return edge_style
  117. def get_fields_for_model(self, model):
  118. fields = [f for f in model._meta.fields]
  119. many = [f for f in model._meta.many_to_many]
  120. return fields + many
  121. def get_link_fields_for_model(self, model):
  122. return [
  123. f
  124. for f in self.get_fields_for_model(model)
  125. if f.remote_field is not None and self.include_link_to_field(model, f)
  126. ]
  127. def get_edge_data(self, field):
  128. return {
  129. 'from_model': str(field.model._meta.verbose_name.title()),
  130. 'to_model': str(field.remote_field.model._meta.verbose_name.title()),
  131. 'help_text': str(field.help_text),
  132. # 'many_to_many': field.many_to_many,
  133. # 'one_to_one': field.one_to_one,
  134. }
  135. def get_id_for_model(self, model):
  136. app_label = model._meta.app_label
  137. model_name = model._meta.model_name
  138. return "%s__%s" % (app_label, model_name)
  139. def get_model_display_information(self, model):
  140. return model.__doc__
  141. def plate(self):
  142. """
  143. Serves up a delicious plate with your models
  144. """
  145. request = self.request
  146. graph_settings = self.get_view_settings()
  147. excludes = self.get_excluded_models()
  148. nodes = []
  149. edges = []
  150. for model in self.get_models():
  151. app_label = model._meta.app_label
  152. model_name = model._meta.model_name
  153. model.doc = self.get_model_display_information(model)
  154. _id = self.get_id_for_model(model)
  155. label = self.get_node_label(model)
  156. node_fields = self.get_fields_for_model(model)
  157. if graph_settings.get('show_fields', True):
  158. label += "\n%s\n" % ("-" * len(model_name))
  159. label += "\n".join([str(f.name) for f in node_fields])
  160. edge_color = {'inherit': 'from'}
  161. for f in self.get_link_fields_for_model(model):
  162. m = f.remote_field.model
  163. to_id = self.get_id_for_model(f.remote_field.model)
  164. if to_id in excludes:
  165. pass
  166. elif _id == to_id and graph_settings.get('ignore_self_referential', False):
  167. pass
  168. else:
  169. if m._meta.app_label != app_label:
  170. edge_color = {'inherit': 'both'}
  171. edge = {
  172. 'from': _id,
  173. 'to': to_id,
  174. 'color': edge_color,
  175. 'title': f.verbose_name.title(),
  176. 'data': self.get_edge_data(f)
  177. }
  178. edge.update(self.generate_edge_style(model, f))
  179. edges.append(edge)
  180. if model.is_proxy:
  181. proxy = model._meta.proxy_for_model._meta
  182. model.proxy = proxy
  183. edge = {
  184. 'to': _id,
  185. 'from': "%s__%s" % (proxy.app_label, proxy.model_name),
  186. 'color': edge_color,
  187. }
  188. edges.append(edge)
  189. nodes.append(
  190. {
  191. 'id': _id,
  192. 'label': label,
  193. 'shape': 'box',
  194. 'group': self.get_group(model),
  195. 'title': get_template(self.meatball_template_name).render(
  196. {'model': model, 'model_meta': model._meta, 'fields': node_fields}
  197. ),
  198. 'data': self.get_extra_node_data(model)
  199. }
  200. )
  201. context = self.get_context_data()
  202. context.update({
  203. 'meatballs': json.dumps(nodes),
  204. 'spaghetti': json.dumps(edges),
  205. 'groups': json.dumps(self.get_groups()),
  206. "pyobj": {
  207. 'meatballs': nodes,
  208. 'spaghetti': edges,
  209. 'groups': self.get_groups(),
  210. }
  211. })
  212. return render(request, self.plate_template_name, context)
  213. def get_extra_node_data(self, model):
  214. return {}
  215. def get_node_label(self, model):
  216. """
  217. Defines how labels are constructed from models.
  218. Default - uses verbose name, lines breaks where sensible
  219. """
  220. if model.is_proxy:
  221. label = "(P) %s" % (model._meta.verbose_name.title())
  222. else:
  223. label = "%s" % (model._meta.verbose_name.title())
  224. line = ""
  225. new_label = []
  226. for w in label.split(" "):
  227. if len(line + w) > 15:
  228. new_label.append(line)
  229. line = w
  230. else:
  231. line += " "
  232. line += w
  233. new_label.append(line)
  234. return "\n".join(new_label)
  235. plate = Plate.as_view()