Skip to content
Browse files

Merge branch 'refactoring' into dns

  • Loading branch information...
2 parents c70b0f7 + 45df547 commit b875d82dea02498a5f699146e3bf1a44b2bf0b11 Dario Salerno committed Oct 29, 2012
Showing with 11,780 additions and 245 deletions.
  1. +7 −2 .gitignore
  2. +1 −2 nodeshot/contrib/dns/models.py
  3. +1 −0 nodeshot/contrib/planning/models.py
  4. 0 nodeshot/core/{contact → api}/__init__.py
  5. 0 nodeshot/core/{contact → api}/tests.py
  6. +38 −0 nodeshot/core/api/urls.py
  7. +4 −0 nodeshot/core/base/admin.py
  8. +26 −23 nodeshot/core/base/choices.py
  9. +58 −1 nodeshot/core/base/models.py
  10. +0 −3 nodeshot/core/contact/models.py
  11. +13 −0 nodeshot/core/links/admin.py
  12. +11 −6 nodeshot/core/links/models.py
  13. +13 −0 nodeshot/core/links/resources.py
  14. 0 nodeshot/{dependencies/placeholder → core/mailing/__init__.py}
  15. +60 −0 nodeshot/core/mailing/admin.py
  16. +7 −0 nodeshot/core/mailing/models/__init__.py
  17. +66 −0 nodeshot/core/mailing/models/choices.py
  18. +100 −0 nodeshot/core/mailing/models/inward.py
  19. +243 −0 nodeshot/core/mailing/models/outward.py
  20. +71 −0 nodeshot/core/mailing/resources.py
  21. +93 −0 nodeshot/core/mailing/templates/admin/outward_change_form.html
  22. +367 −0 nodeshot/core/mailing/tests.py
  23. 0 nodeshot/core/{contact → mailing}/views.py
  24. +1 −2 nodeshot/core/monitoring/models.py
  25. +19 −5 nodeshot/core/network/admin.py
  26. +0 −92 nodeshot/core/network/models.py
  27. +24 −0 nodeshot/core/network/models/__init__.py
  28. +22 −0 nodeshot/core/network/models/device.py
  29. +20 −0 nodeshot/core/network/models/interface.py
  30. 0 nodeshot/core/network/models/interfaces/__init__.py
  31. +12 −0 nodeshot/core/network/models/interfaces/bridge.py
  32. +14 −0 nodeshot/core/network/models/interfaces/ethernet.py
  33. +14 −0 nodeshot/core/network/models/interfaces/tunnel.py
  34. +21 −0 nodeshot/core/network/models/interfaces/vap.py
  35. +12 −0 nodeshot/core/network/models/interfaces/vlan.py
  36. +19 −0 nodeshot/core/network/models/interfaces/wireless.py
  37. +25 −0 nodeshot/core/network/models/ip.py
  38. +16 −0 nodeshot/core/network/models/routing_protocol.py
  39. +95 −0 nodeshot/core/network/resources.py
  40. +16 −8 nodeshot/core/nodes/admin.py
  41. +23 −0 nodeshot/core/nodes/fixtures/groups.json
  42. +107 −0 nodeshot/core/nodes/fixtures/test_nodes.json
  43. +122 −0 nodeshot/core/nodes/fixtures/test_users.json
  44. +0 −55 nodeshot/core/nodes/models.py
  45. +4 −0 nodeshot/core/nodes/models/__init__.py
  46. +18 −0 nodeshot/core/nodes/models/image.py
  47. +47 −0 nodeshot/core/nodes/models/node.py
  48. +95 −0 nodeshot/core/nodes/resources.py
  49. +5 −0 nodeshot/core/services/__init__.py
  50. +54 −0 nodeshot/core/services/admin.py
  51. +0 −43 nodeshot/core/services/models.py
  52. +6 −0 nodeshot/core/services/models/__init__.py
  53. +16 −0 nodeshot/core/services/models/category.py
  54. +18 −0 nodeshot/core/services/models/choices.py
  55. +22 −0 nodeshot/core/services/models/login.py
  56. +23 −0 nodeshot/core/services/models/service.py
  57. +36 −0 nodeshot/core/services/models/url.py
  58. +33 −0 nodeshot/core/services/resources.py
  59. 0 nodeshot/core/zones/__init__.py
  60. +31 −0 nodeshot/core/zones/admin.py
  61. +72 −0 nodeshot/core/zones/fixtures/test_zones.json
  62. +5 −0 nodeshot/core/zones/models/__init__.py
  63. +53 −0 nodeshot/core/zones/models/zone.py
  64. +21 −0 nodeshot/core/zones/models/zone_external.py
  65. +15 −0 nodeshot/core/zones/models/zone_point.py
  66. +37 −0 nodeshot/core/zones/resources.py
  67. +25 −0 nodeshot/core/zones/templates/admin/zone_change_form.html
  68. +39 −0 nodeshot/core/zones/tests.py
  69. 0 nodeshot/dependencies/__init__.py
  70. +99 −0 nodeshot/dependencies/fields.py
  71. +1 −0 projects/ninux/ninux/media/init
  72. +113 −0 projects/ninux/ninux/monitor.py
  73. +97 −3 projects/ninux/ninux/settings.example.py
  74. +835 −0 projects/ninux/ninux/static/admin/css/base.css
  75. +289 −0 projects/ninux/ninux/static/admin/css/changelists.css
  76. +30 −0 projects/ninux/ninux/static/admin/css/dashboard.css
  77. +358 −0 projects/ninux/ninux/static/admin/css/forms.css
  78. +63 −0 projects/ninux/ninux/static/admin/css/ie.css
  79. +57 −0 projects/ninux/ninux/static/admin/css/login.css
  80. +245 −0 projects/ninux/ninux/static/admin/css/rtl.css
  81. +563 −0 projects/ninux/ninux/static/admin/css/widgets.css
  82. BIN projects/ninux/ninux/static/admin/img/changelist-bg.gif
  83. BIN projects/ninux/ninux/static/admin/img/changelist-bg_rtl.gif
  84. BIN projects/ninux/ninux/static/admin/img/chooser-bg.gif
  85. BIN projects/ninux/ninux/static/admin/img/chooser_stacked-bg.gif
  86. BIN projects/ninux/ninux/static/admin/img/default-bg-reverse.gif
  87. BIN projects/ninux/ninux/static/admin/img/default-bg.gif
  88. BIN projects/ninux/ninux/static/admin/img/deleted-overlay.gif
  89. BIN projects/ninux/ninux/static/admin/img/gis/move_vertex_off.png
  90. BIN projects/ninux/ninux/static/admin/img/gis/move_vertex_on.png
  91. BIN projects/ninux/ninux/static/admin/img/icon-no.gif
  92. BIN projects/ninux/ninux/static/admin/img/icon-unknown.gif
  93. BIN projects/ninux/ninux/static/admin/img/icon-yes.gif
  94. BIN projects/ninux/ninux/static/admin/img/icon_addlink.gif
  95. BIN projects/ninux/ninux/static/admin/img/icon_alert.gif
  96. BIN projects/ninux/ninux/static/admin/img/icon_calendar.gif
  97. BIN projects/ninux/ninux/static/admin/img/icon_changelink.gif
  98. BIN projects/ninux/ninux/static/admin/img/icon_clock.gif
  99. BIN projects/ninux/ninux/static/admin/img/icon_deletelink.gif
  100. BIN projects/ninux/ninux/static/admin/img/icon_error.gif
  101. BIN projects/ninux/ninux/static/admin/img/icon_searchbox.png
  102. BIN projects/ninux/ninux/static/admin/img/icon_success.gif
  103. BIN projects/ninux/ninux/static/admin/img/inline-delete-8bit.png
  104. BIN projects/ninux/ninux/static/admin/img/inline-delete.png
  105. BIN projects/ninux/ninux/static/admin/img/inline-restore-8bit.png
  106. BIN projects/ninux/ninux/static/admin/img/inline-restore.png
  107. BIN projects/ninux/ninux/static/admin/img/inline-splitter-bg.gif
  108. BIN projects/ninux/ninux/static/admin/img/nav-bg-grabber.gif
  109. BIN projects/ninux/ninux/static/admin/img/nav-bg-reverse.gif
  110. BIN projects/ninux/ninux/static/admin/img/nav-bg-selected.gif
  111. BIN projects/ninux/ninux/static/admin/img/nav-bg.gif
  112. BIN projects/ninux/ninux/static/admin/img/selector-icons.gif
  113. BIN projects/ninux/ninux/static/admin/img/selector-search.gif
  114. BIN projects/ninux/ninux/static/admin/img/sorting-icons.gif
  115. BIN projects/ninux/ninux/static/admin/img/tool-left.gif
  116. BIN projects/ninux/ninux/static/admin/img/tool-left_over.gif
  117. BIN projects/ninux/ninux/static/admin/img/tool-right.gif
  118. BIN projects/ninux/ninux/static/admin/img/tool-right_over.gif
  119. BIN projects/ninux/ninux/static/admin/img/tooltag-add.gif
  120. BIN projects/ninux/ninux/static/admin/img/tooltag-add_over.gif
  121. BIN projects/ninux/ninux/static/admin/img/tooltag-arrowright.gif
  122. BIN projects/ninux/ninux/static/admin/img/tooltag-arrowright_over.gif
  123. +20 −0 projects/ninux/ninux/static/admin/js/LICENSE-JQUERY.txt
  124. +111 −0 projects/ninux/ninux/static/admin/js/SelectBox.js
  125. +165 −0 projects/ninux/ninux/static/admin/js/SelectFilter2.js
  126. +137 −0 projects/ninux/ninux/static/admin/js/actions.js
  127. +135 −0 projects/ninux/ninux/static/admin/js/actions.min.js
  128. +3 −0 projects/ninux/ninux/static/admin/js/admin/DateTimeShortcuts.js
  129. +179 −0 projects/ninux/ninux/static/admin/js/admin/RelatedObjectLookups.js
  130. +4 −0 projects/ninux/ninux/static/admin/js/admin/ordering.js
  131. +3 −0 projects/ninux/ninux/static/admin/js/calendar.js
  132. +3 −0 projects/ninux/ninux/static/admin/js/collapse.js
  133. +3 −0 projects/ninux/ninux/static/admin/js/collapse.min.js
  134. +47 −0 projects/ninux/ninux/static/admin/js/compress.py
  135. +211 −0 projects/ninux/ninux/static/admin/js/core.js
  136. +3 −0 projects/ninux/ninux/static/admin/js/dateparse.js
  137. +167 −0 projects/ninux/ninux/static/admin/js/getElementsBySelector.js
  138. +3 −0 projects/ninux/ninux/static/admin/js/inlines.js
  139. +3 −0 projects/ninux/ninux/static/admin/js/inlines.min.js
  140. +3 −0 projects/ninux/ninux/static/admin/js/jquery.init.js
  141. +3 −0 projects/ninux/ninux/static/admin/js/jquery.js
  142. +3 −0 projects/ninux/ninux/static/admin/js/jquery.min.js
  143. +29 −0 projects/ninux/ninux/static/admin/js/json.min.js
  144. +34 −0 projects/ninux/ninux/static/admin/js/prepopulate.js
  145. +34 −0 projects/ninux/ninux/static/admin/js/prepopulate.min.js
  146. +3 −0 projects/ninux/ninux/static/admin/js/timeparse.js
  147. +140 −0 projects/ninux/ninux/static/admin/js/urlify.js
  148. BIN projects/ninux/ninux/static/grappelli/images/backgrounds/changelist-results.png
  149. BIN projects/ninux/ninux/static/grappelli/images/backgrounds/loading-small.gif
  150. BIN projects/ninux/ninux/static/grappelli/images/backgrounds/messagelist.png
  151. BIN projects/ninux/ninux/static/grappelli/images/backgrounds/nav-grabber.gif
  152. BIN projects/ninux/ninux/static/grappelli/images/backgrounds/ui-sortable-placeholder.png
  153. BIN projects/ninux/ninux/static/grappelli/images/icons-s328be85df9.png
  154. BIN projects/ninux/ninux/static/grappelli/images/icons-s96d5c23000.png
  155. BIN projects/ninux/ninux/static/grappelli/images/icons-small-s9045a82f03.png
  156. BIN projects/ninux/ninux/static/grappelli/images/icons-small-sc276a63e67.png
  157. BIN projects/ninux/ninux/static/grappelli/images/icons-small/add-link.png
  158. BIN projects/ninux/ninux/static/grappelli/images/icons-small/add-link_hover.png
  159. BIN projects/ninux/ninux/static/grappelli/images/icons-small/change-link.png
  160. BIN projects/ninux/ninux/static/grappelli/images/icons-small/change-link_hover.png
  161. BIN projects/ninux/ninux/static/grappelli/images/icons-small/delete-link.png
  162. BIN projects/ninux/ninux/static/grappelli/images/icons-small/link-external.png
  163. BIN projects/ninux/ninux/static/grappelli/images/icons-small/link-external_hover.png
  164. BIN projects/ninux/ninux/static/grappelli/images/icons-small/link-internal.png
  165. BIN projects/ninux/ninux/static/grappelli/images/icons-small/link-internal_hover.png
  166. BIN projects/ninux/ninux/static/grappelli/images/icons-small/sort-remove.png
  167. BIN projects/ninux/ninux/static/grappelli/images/icons/add-another.png
  168. BIN projects/ninux/ninux/static/grappelli/images/icons/add-another_hover.png
  169. BIN projects/ninux/ninux/static/grappelli/images/icons/back-link.png
  170. BIN projects/ninux/ninux/static/grappelli/images/icons/back-link_hover.png
  171. BIN projects/ninux/ninux/static/grappelli/images/icons/breadcrumbs-rtl.png
  172. BIN projects/ninux/ninux/static/grappelli/images/icons/breadcrumbs-rtl_hover.png
  173. BIN projects/ninux/ninux/static/grappelli/images/icons/breadcrumbs.png
  174. BIN projects/ninux/ninux/static/grappelli/images/icons/breadcrumbs_hover.png
  175. BIN projects/ninux/ninux/static/grappelli/images/icons/date-hierarchy-back-rtl.png
  176. BIN projects/ninux/ninux/static/grappelli/images/icons/date-hierarchy-back-rtl_hover.png
  177. BIN projects/ninux/ninux/static/grappelli/images/icons/date-hierarchy-back.png
  178. BIN projects/ninux/ninux/static/grappelli/images/icons/date-hierarchy-back_hover.png
  179. BIN projects/ninux/ninux/static/grappelli/images/icons/datepicker.png
  180. BIN projects/ninux/ninux/static/grappelli/images/icons/datepicker_hover.png
  181. BIN projects/ninux/ninux/static/grappelli/images/icons/datetime-now.png
  182. BIN projects/ninux/ninux/static/grappelli/images/icons/datetime-now_hover.png
  183. BIN projects/ninux/ninux/static/grappelli/images/icons/form-select.png
  184. BIN projects/ninux/ninux/static/grappelli/images/icons/object-tools-add-link.png
  185. BIN projects/ninux/ninux/static/grappelli/images/icons/object-tools-viewsite-link.png
  186. BIN projects/ninux/ninux/static/grappelli/images/icons/pulldown-handler.png
  187. BIN projects/ninux/ninux/static/grappelli/images/icons/pulldown-handler_hover.png
  188. BIN projects/ninux/ninux/static/grappelli/images/icons/pulldown-handler_selected.png
  189. BIN projects/ninux/ninux/static/grappelli/images/icons/related-lookup-m2m.png
  190. BIN projects/ninux/ninux/static/grappelli/images/icons/related-lookup-m2m_hover.png
  191. BIN projects/ninux/ninux/static/grappelli/images/icons/related-lookup.png
  192. BIN projects/ninux/ninux/static/grappelli/images/icons/related-lookup_hover.png
  193. BIN projects/ninux/ninux/static/grappelli/images/icons/related-remove.png
  194. BIN projects/ninux/ninux/static/grappelli/images/icons/related-remove_hover.png
  195. BIN projects/ninux/ninux/static/grappelli/images/icons/searchbox.png
  196. BIN projects/ninux/ninux/static/grappelli/images/icons/selector-add-m2m-horizontal.png
  197. BIN projects/ninux/ninux/static/grappelli/images/icons/selector-add-m2m-horizontal_hover.png
  198. BIN projects/ninux/ninux/static/grappelli/images/icons/selector-add-m2m-vertical.png
  199. BIN projects/ninux/ninux/static/grappelli/images/icons/selector-add-m2m-vertical_hover.png
  200. BIN projects/ninux/ninux/static/grappelli/images/icons/selector-filter.png
  201. BIN projects/ninux/ninux/static/grappelli/images/icons/selector-remove-m2m-horizontal.png
  202. BIN projects/ninux/ninux/static/grappelli/images/icons/selector-remove-m2m-horizontal_hover.png
  203. BIN projects/ninux/ninux/static/grappelli/images/icons/selector-remove-m2m-vertical.png
  204. BIN projects/ninux/ninux/static/grappelli/images/icons/selector-remove-m2m-vertical_hover.png
  205. BIN projects/ninux/ninux/static/grappelli/images/icons/sort-remove.png
  206. BIN projects/ninux/ninux/static/grappelli/images/icons/sort-remove_hover.png
  207. BIN projects/ninux/ninux/static/grappelli/images/icons/sorted-ascending.png
  208. BIN projects/ninux/ninux/static/grappelli/images/icons/sorted-descending.png
  209. BIN projects/ninux/ninux/static/grappelli/images/icons/status-no.png
  210. BIN projects/ninux/ninux/static/grappelli/images/icons/status-unknown.png
  211. BIN projects/ninux/ninux/static/grappelli/images/icons/status-yes.png
  212. BIN projects/ninux/ninux/static/grappelli/images/icons/th-ascending.png
  213. BIN projects/ninux/ninux/static/grappelli/images/icons/th-descending.png
  214. BIN projects/ninux/ninux/static/grappelli/images/icons/timepicker.png
  215. BIN projects/ninux/ninux/static/grappelli/images/icons/timepicker_hover.png
  216. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-add-handler.png
  217. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-add-handler_hover.png
  218. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-arrow-down-handler.png
  219. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-arrow-down-handler_hover.png
  220. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-arrow-up-handler.png
  221. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-arrow-up-handler_hover.png
  222. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-close-handler.png
  223. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-close-handler_hover.png
  224. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-delete-handler.png
  225. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-delete-handler_hover.png
  226. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-drag-handler.png
  227. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-drag-handler_hover.png
  228. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-open-handler.png
  229. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-open-handler_hover.png
  230. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-remove-handler.png
  231. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-remove-handler_hover.png
  232. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-trash-handler.png
  233. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-trash-handler_hover.png
  234. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-trash-list-toggle-handler.png
  235. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-trash-list-toggle-handler_hover.png
  236. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-viewsite-link.png
  237. BIN projects/ninux/ninux/static/grappelli/images/icons/tools-viewsite-link_hover.png
  238. BIN projects/ninux/ninux/static/grappelli/images/icons/ui-datepicker-next.png
  239. BIN projects/ninux/ninux/static/grappelli/images/icons/ui-datepicker-next_hover.png
  240. BIN projects/ninux/ninux/static/grappelli/images/icons/ui-datepicker-prev.png
  241. BIN projects/ninux/ninux/static/grappelli/images/icons/ui-datepicker-prev_hover.png
  242. +20 −0 projects/ninux/ninux/static/grappelli/jquery/i18n/ui.datepicker-de.js
  243. +19 −0 projects/ninux/ninux/static/grappelli/jquery/i18n/ui.datepicker-fr.js
  244. +4 −0 projects/ninux/ninux/static/grappelli/jquery/jquery-1.7.2.min.js
  245. +7 −0 projects/ninux/ninux/static/grappelli/jquery/jquery.cookie.min.js
  246. +899 −0 projects/ninux/ninux/static/grappelli/jquery/jquery.form.js
  247. BIN ...cts/ninux/ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-bg_flat_0_888888_40x100.png
  248. BIN ...cts/ninux/ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-bg_flat_0_aaaaaa_40x100.png
  249. BIN ...ts/ninux/ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-bg_flat_75_ffffff_40x100.png
  250. BIN ...ts/ninux/ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-bg_glass_25_e1f0f5_1x400.png
  251. BIN ...ts/ninux/ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-bg_glass_55_444444_1x400.png
  252. BIN ...ts/ninux/ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-bg_glass_65_ffffff_1x400.png
  253. BIN ...ts/ninux/ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-bg_glass_75_dadada_1x400.png
  254. BIN ...ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png
  255. BIN ...nux/ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-bg_inset-soft_95_fef1ec_1x100.png
  256. BIN projects/ninux/ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-icons_222222_256x240.png
  257. BIN projects/ninux/ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-icons_309bbf_256x240.png
  258. BIN projects/ninux/ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-icons_454545_256x240.png
  259. BIN projects/ninux/ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-icons_bf3030_256x240.png
  260. BIN projects/ninux/ninux/static/grappelli/jquery/ui/css/custom-theme/images/ui-icons_ffffff_256x240.png
  261. +565 −0 projects/ninux/ninux/static/grappelli/jquery/ui/css/custom-theme/jquery-ui-1.8.18.custom.css
  262. BIN ...inux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png
  263. BIN ...inux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png
  264. BIN ...ts/ninux/ninux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-bg_flat_10_000000_40x100.png
  265. BIN ...s/ninux/ninux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png
  266. BIN ...s/ninux/ninux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png
  267. BIN ...ts/ninux/ninux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png
  268. BIN ...x/ninux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png
  269. BIN ...inux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png
  270. BIN ...ninux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png
  271. BIN projects/ninux/ninux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-icons_222222_256x240.png
  272. BIN projects/ninux/ninux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-icons_228ef1_256x240.png
  273. BIN projects/ninux/ninux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-icons_ef8c08_256x240.png
  274. BIN projects/ninux/ninux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-icons_ffd27a_256x240.png
  275. BIN projects/ninux/ninux/static/grappelli/jquery/ui/css/ui-lightness/images/ui-icons_ffffff_256x240.png
  276. +356 −0 projects/ninux/ninux/static/grappelli/jquery/ui/js/jquery-ui-1.8.18.custom.min.js
  277. +154 −0 projects/ninux/ninux/static/grappelli/js/grappelli.js
  278. +1 −0 projects/ninux/ninux/static/grappelli/js/grappelli.min.js
  279. +134 −0 projects/ninux/ninux/static/grappelli/js/jquery.grp_autocomplete_fk.js
  280. +165 −0 projects/ninux/ninux/static/grappelli/js/jquery.grp_autocomplete_generic.js
  281. +183 −0 projects/ninux/ninux/static/grappelli/js/jquery.grp_autocomplete_m2m.js
  282. +35 −0 projects/ninux/ninux/static/grappelli/js/jquery.grp_collapsible.js
  283. +54 −0 projects/ninux/ninux/static/grappelli/js/jquery.grp_collapsible_group.js
  284. +179 −0 projects/ninux/ninux/static/grappelli/js/jquery.grp_inline.js
  285. +61 −0 projects/ninux/ninux/static/grappelli/js/jquery.grp_related_fk.js
  286. +82 −0 projects/ninux/ninux/static/grappelli/js/jquery.grp_related_generic.js
  287. +59 −0 projects/ninux/ninux/static/grappelli/js/jquery.grp_related_m2m.js
  288. +167 −0 projects/ninux/ninux/static/grappelli/js/jquery.grp_timepicker.js
  289. +1 −0 projects/ninux/ninux/static/grappelli/stylesheets/mueller/grid/output-rtl.css
  290. +1 −0 projects/ninux/ninux/static/grappelli/stylesheets/mueller/grid/output.css
  291. 0 projects/ninux/ninux/static/grappelli/stylesheets/mueller/screen.css
  292. +1 −0 projects/ninux/ninux/static/grappelli/stylesheets/partials/custom/tinymce.css
  293. +1 −0 projects/ninux/ninux/static/grappelli/stylesheets/rtl.css
  294. +1 −0 projects/ninux/ninux/static/grappelli/stylesheets/screen.css
  295. +1,528 −0 projects/ninux/ninux/static/grappelli/tinymce/changelog.txt
  296. +101 −0 projects/ninux/ninux/static/grappelli/tinymce/examples/accessibility.html
  297. +105 −0 projects/ninux/ninux/static/grappelli/tinymce/examples/css/content.css
  298. +53 −0 projects/ninux/ninux/static/grappelli/tinymce/examples/css/word.css
  299. +111 −0 projects/ninux/ninux/static/grappelli/tinymce/examples/custom_formats.html
  300. +101 −0 projects/ninux/ninux/static/grappelli/tinymce/examples/full.html
Sorry, we could not display the entire diff because too many files (697) changed.
View
9 .gitignore
@@ -2,5 +2,10 @@
*.pyc
.komodotools/
projects/ninux/ninux/settings.py
-MyProject.komodoproject
-.DS_Store
+projects/ninux/ninux/settings_test.py
+*.komodoproject
+*.DS_Store
+projects/ninux/ninux/media/*
+projects/ninux/ninux/debug.log
+*~
+nodeshot/core/zones/migrations/
View
3 nodeshot/contrib/dns/models.py
@@ -5,7 +5,6 @@
from django.contrib.contenttypes import generic
from django.contrib.auth.models import User
from nodeshot.core.base.models import BaseDate
-from nodeshot.core.nodes.models import Zone, Node
from nodeshot.core.network.models import Device, Interface
import hashlib
@@ -122,7 +121,7 @@ def __unicode__(self):
OneToOne tables for customize automatic DNS names
"""
class ZoneToDns(models.Model):
- zone = models.OneToOneField(Zone, db_index=True, unique=True)
+ zone = models.OneToOneField('zones.Zone', db_index=True, unique=True)
value = models.SlugField(_('value'), max_length=20)
def __unicode__(self):
View
1 nodeshot/contrib/planning/models.py
@@ -9,3 +9,4 @@ class PlannedNode(Node):
class Meta:
db_table = 'planned_node'
+ permissions = (('can_view_planned_nodes', 'Can view planned nodes'),)
View
0 nodeshot/core/contact/__init__.py → nodeshot/core/api/__init__.py 100755 → 100644
File renamed without changes.
View
0 nodeshot/core/contact/tests.py → nodeshot/core/api/tests.py 100755 → 100644
File renamed without changes.
View
38 nodeshot/core/api/urls.py
@@ -0,0 +1,38 @@
+from django.conf.urls import patterns, include, url
+from django.conf import settings
+# importlib is available since python 2.7
+from importlib import import_module
+
+# import tastypie
+from tastypie.api import Api
+
+# initialize tastypie
+v1_api = Api(api_name='v1')
+
+# loop over all the strings listed in settings.NODESHOT['API']['APPS_ENABLED]
+for app_path in settings.NODESHOT['API']['APPS_ENABLED']:
+ # determine name
+ # eg: nodeshot.core.nodes becomes nodes
+ #app_name = app_path.split('.')[-1]
+
+ # determine import path for api module of the current app
+ # eg: nodeshot.core.nodes will become nodeshot.core.nodes.api
+ resources_path = '%s.resources' % app_path
+
+ # import api
+ resources = import_module(resources_path); # eg: app = import_module('nodeshot.core.nodes.api')
+ # retrieve attributes of api module
+ attrs = dir(resources) # eg: dir(api)
+
+ # loop over attributes and determine which resources will be imported
+ for attr in attrs:
+ # select only attributes which are named SomethingResource except ModelResource wich is a base class
+ if 'Resource' in attr and attr != 'ModelResource':
+ # register resource
+ # eg: v1_api.register(NodeResource())
+ v1_api.register(getattr(resources, attr)())
+
+urlpatterns = patterns('',
+ # The normal jazz here then...
+ (r'^', include(v1_api.urls)),
+)
View
4 nodeshot/core/base/admin.py
@@ -14,4 +14,8 @@ class BaseAdmin(admin.ModelAdmin):
class BaseStackedInline(admin.StackedInline):
readonly_fields = ('added', 'updated')
+ extra = 0
+
+class BaseTabularInline(admin.TabularInline):
+ readonly_fields = ('added', 'updated')
extra = 0
View
49 nodeshot/core/base/choices.py
@@ -1,20 +1,22 @@
from django.utils.translation import ugettext_lazy as _
+from django.conf import settings
NODE_STATUS = (
(-1, _('archived')),
(0, _('potential')),
(1, _('planned')),
(2, _('testing')),
(3, _('active')),
+ (4, _('mantainance')),
)
ROUTING_PROTOCOLS = (
- ('batman','B.A.T.M.A.N.'),
- ('babel','Babel'),
- ('olsr','OLSR'),
- ('bgp','BGP'),
- ('static',_('Static Routing')),
- ('none',_('None')),
+ ('batman', 'Batman-adv'),
+ ('babel', 'Babel'),
+ ('olsr', 'OLSR'),
+ ('bgp', 'BGP'),
+ ('static', _('Static Routing')),
+ ('none', _('None')),
)
DEVICE_TYPES = (
@@ -157,23 +159,22 @@
('10/100/1000', '10/100/1000 Gigabit Ethernet'),
)
-ACCESS_LEVELS = (
- ('public', _('public')),
- ('community', _('community')),
- ('private', _('private')),
-)
+ACCESS_LEVELS = [('public', _('public'))]
+ACCESS_LEVELS += [group for group in settings.NODESHOT['CHOICES']['ACCESS_LEVELS']]
+ACCESS_LEVELS += [('private', _('private'))]
IP_PROTOCOLS = (
('ipv4', 'ipv4'),
('ipv6', 'ipv6')
)
LINK_STATUS = (
- (-1, _('aborted')),
- (0, _('disconnected')),
- (1, _('operational')),
- (2, _('planned')),
- (3, _('released')),
+ (-3, _('archived')),
+ (-2, _('disconnected')),
+ (-1, _('down')),
+ (0, _('planned')),
+ (1, _('testing')),
+ (2, _('active')),
)
LINK_TYPE = (
@@ -194,14 +195,11 @@
(2, _('write')),
)
-PORT_PROTOCOLS = (
- ('tcp', 'TCP'),
- ('udp', 'UDP'),
-)
-
DEVICE_STATUS = (
- (1, 'reachable'),
- (2, 'not reachable'),
+ (-2, _('archived')),
+ (-1, _('unplugged')),
+ (0, _('not reachable')),
+ (1, _('reachable')),
)
SERVICE_STATUS = (
@@ -220,3 +218,8 @@
('full', 'full-duplex'),
('half', 'half-duplex')
)
+
+# from 0 to 18
+MAP_ZOOM = (
+ [(n, n) for n in range(0, 19)]
+)
View
59 nodeshot/core/base/models.py
@@ -1,6 +1,7 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.timezone import utc
+from django.conf import settings
from datetime import datetime
from nodeshot.core.base.choices import ACCESS_LEVELS
@@ -27,11 +28,67 @@ class Meta:
class BaseAccessLevel(BaseDate):
"""
Base Abstract Model that extends BaseDate and provides an additional field for access level management.
+ The field default and editable attributes value can be overriden by adding some directives in the settings file.
+
+ DEFAULT VALUE
+ To edit the default value for the access_level field of a certain model you will have to add the following setting in the settings.py file:
+
+ NODESHOT['DEFAULTS']['ACL_{APP_NAME}_{MODEL_NAME}'] = 'public'
+
+ where {APP_NAME} is the uppercase name of an app like "nodes" or "network"
+ and {MODEL_NAME} is the uppercase name of a model like "Node" or "Device"
+ The values will have to be one of the possible values specified in "nodeshot.core.base.choices.ACCESS_LEVELS"
+ The possible values are public, private or the id of the group saved in the database (default ones are 1 for registered and 2 for community)
+
+ For the cases in which no setting is specified the fallback setting NODESHOT['DEFAULTS']['ACL_GLOBAL'] will be used.
+
+ EDITABLE
+ If you want to disable the possibility to edit the access_level field for a given model you will have to add the following settings in the settings.py file:
+
+ NODESHOT['DEFAULTS']['ACL_{APP_NAME}_{MODEL_NAME}_EDITABLE'] = False
+
+ where {APP_NAME} is the uppercase name of an app like "nodes" or "network"
+ and {MODEL_NAME} is the uppercase name of a model like "Node" or "Device"
+
+ For the cases in which no setting is specified the fallback setting NODESHOT['DEFAULTS']['ACL_GLOBAL_EDITABLE'] will be used.
+
"""
access_level = models.CharField(_('access level'), max_length=10, choices=ACCESS_LEVELS, default=ACCESS_LEVELS[0][0])
class Meta:
abstract = True
+
+ def __init__(self, *args, **kwargs):
+ """
+ Determines default value for field "access_level" and determines if is editable
+ In the case the field is not editable it won't show up at all
+ """
+
+ # {APP_NAME}_{MODEL_NAME}, eg: NODES_NODE, SERVICES_SERVICE, NETWORK_IP
+ app_descriptor = '%s_%s' % (self._meta.app_label.upper(), self._meta.object_name.upper())
+
+ try:
+ # looks up in settings.py
+ # example NODESHOT['DEFAULTS']['ACL_NETWORK_NODE']
+ ACL_DEFAULT = settings.NODESHOT['DEFAULTS']['ACL_%s' % app_descriptor]
+ except KeyError:
+ # if setting is not found use the global setting
+ ACL_DEFAULT = settings.NODESHOT['DEFAULTS']['ACL_GLOBAL']
+
+ try:
+ # looks up in settings.py
+ # example NODESHOT['SETTINGS']['ACL_NETWORK_NODE_EDITABLE']
+ ACL_EDITABLE = settings.NODESHOT['SETTINGS']['ACL_%s_EDITABLE' % app_descriptor]
+ except KeyError:
+ # if setting is not found use the global setting
+ ACL_EDITABLE = settings.NODESHOT['SETTINGS']['ACL_GLOBAL_EDITABLE']
+
+ # set "default" and "editable" attributes
+ self._meta.get_field('access_level').default = ACL_DEFAULT
+ self._meta.get_field('access_level').editable = ACL_EDITABLE
+
+ # call super __init__
+ super(BaseAccessLevel, self).__init__(*args, **kwargs)
class BaseOrdered(BaseAccessLevel):
"""
@@ -49,7 +106,7 @@ def save(self):
self.order = self.__class__.objects.all().order_by("-order")[0].order + 1
except IndexError:
self.order = 1
- super(OrderedModel, self).save()
+ super(BaseOrdered, self).save()
def order_link(self):
"""
View
3 nodeshot/core/contact/models.py
@@ -1,3 +0,0 @@
-from django.db import models
-
-# Create your models here.
View
13 nodeshot/core/links/admin.py
@@ -0,0 +1,13 @@
+from django.contrib import admin
+from django.conf import settings
+from nodeshot.core.links.models import Link, LinkRadio
+from nodeshot.core.base.admin import BaseAdmin#, BaseStackedInline, BaseTabularInline
+
+class LinkAdmin(BaseAdmin):
+ list_display = ('interface_a', 'interface_b', 'type', 'status', 'added', 'updated')
+ list_filter = ('status', 'type')
+ date_hierarchy = 'added'
+ ordering = ('-id',)
+
+admin.site.register(Link, LinkAdmin)
+admin.site.register(LinkRadio, LinkAdmin)
View
17 nodeshot/core/links/models.py
@@ -9,14 +9,19 @@ class Link(BaseAccessLevel):
interface_b = models.ForeignKey(Interface, related_name='interface_b')
type = models.SmallIntegerField(_('type'), max_length=10, choices=LINK_TYPE, default=LINK_TYPE[0][0])
metric_type = models.CharField(_('metric type'), max_length=6, choices=METRIC_TYPES, default=METRIC_TYPES[0][0])
- metric_value = models.FloatField(_('metric value'))
- tx_rate = models.IntegerField(_('TX rate average'), null=True, default=None)
- rx_rate = models.IntegerField(_('RX rate average'), null=True, default=None)
- status = models.SmallIntegerField(_('status'), choices=LINK_STATUS, default=LINK_STATUS[0][0])
+ metric_value = models.FloatField(_('metric value'), blank=True)
+ tx_rate = models.IntegerField(_('TX rate average'), null=True, default=None, blank=True)
+ rx_rate = models.IntegerField(_('RX rate average'), null=True, default=None, blank=True)
+ status = models.SmallIntegerField(_('status'), choices=LINK_STATUS, default=LINK_STATUS[3][0])
+
+ class Meta:
+ permissions = (('can_view_links', 'Can view links'),)
class LinkRadio(Link):
- dbm = models.IntegerField(_('dBm average'), null=True, default=None)
- noise = models.IntegerField(_('noise average'), null=True, default=None)
+ dbm = models.IntegerField(_('dBm average'), null=True, default=None, blank=True)
+ noise = models.IntegerField(_('noise average'), null=True, default=None, blank=True)
class Meta:
db_table = 'links_radio_link'
+ verbose_name = _('radio link')
+ verbose_name_plural = _('radio links')
View
13 nodeshot/core/links/resources.py
@@ -0,0 +1,13 @@
+from tastypie.resources import ModelResource, ALL
+from tastypie import fields
+from models import Link, LinkRadio
+
+class LinkResource(ModelResource):
+
+ class Meta:
+ queryset = Link.objects.all()
+ resource_name = 'links'
+ limit = 0
+ include_resource_uri = False
+
+ excludes = ['added', 'updated']
View
0 nodeshot/dependencies/placeholder → nodeshot/core/mailing/__init__.py
File renamed without changes.
View
60 nodeshot/core/mailing/admin.py
@@ -0,0 +1,60 @@
+import os
+from django.contrib import admin
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.contrib import messages
+from django.utils.translation import ugettext_lazy as _
+from django.core.exceptions import ImproperlyConfigured
+from nodeshot.core.base.admin import BaseAdmin, BaseStackedInline#, BaseTabularInline
+from nodeshot.core.zones.models import Zone
+from models import Inward, Outward
+
+class InwardAdmin(BaseAdmin):
+ list_display = ('from_email', 'from_name', 'to', 'status', 'added', 'updated')
+ search_fields = ('from_email', 'from_name')
+ #ordering = ('name',)
+ #search_fields = ('name', 'description')
+ # define the autocomplete_lookup_fields
+ if 'grappelli' in settings.INSTALLED_APPS:
+ autocomplete_lookup_fields = {
+ 'generic': [['content_type', 'object_id']],
+ }
+
+def send_now(modeladmin, request, queryset):
+ """
+ Send now action available in change outward list
+ """
+ objects = queryset
+ for object in objects:
+ object.send()
+ send_now.short_description = _('Send selected messages now')
+ # show message in the admin
+ messages.info(request, _('Message sent successfully'))
+
+class OutwardAdmin(BaseAdmin):
+ list_display = ('subject', 'status', 'is_scheduled', 'added', 'updated')
+ list_filter = ('status', 'is_scheduled')
+ filter_horizontal = ['zones', 'users']
+ search_fields = ('subject',)
+ actions = [send_now]
+ change_form_template = '%s/templates/admin/outward_change_form.html' % os.path.dirname(os.path.realpath(__file__))
+
+ def formfield_for_manytomany(self, db_field, request, **kwargs):
+ if db_field.name == 'zones':
+ kwargs['queryset'] = Zone.objects.filter(is_external=False)
+ if db_field.name == 'users':
+ kwargs['queryset'] = User.objects.filter(is_active=True)
+ return super(OutwardAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
+
+ if settings.NODESHOT['SETTINGS']['CONTACT_OUTWARD_HTML'] is True:
+ if 'grappelli' not in settings.INSTALLED_APPS:
+ raise ImproperlyConfigured(_("settings.NODESHOT['SETTINGS']['CONTACT_OUTWARD_HTML'] is set to True but grappelli is not in settings.INSTALLED_APPS"))
+ class Media:
+ js = [
+ '%sgrappelli/tinymce/jscripts/tiny_mce/tiny_mce.js' % settings.STATIC_URL,
+ '%sgrappelli/tinymce_setup/tinymce_setup_ns.js' % settings.STATIC_URL,
+ ]
+
+admin.site.register(Inward, InwardAdmin)
+admin.site.register(Outward, OutwardAdmin)
+
View
7 nodeshot/core/mailing/models/__init__.py
@@ -0,0 +1,7 @@
+from inward import Inward
+from outward import Outward
+
+__all__ = [
+ 'Inward',
+ 'Outward',
+]
View
66 nodeshot/core/mailing/models/choices.py
@@ -0,0 +1,66 @@
+from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
+
+try:
+ AVAILABLE_CRONJOBS = settings.NODESHOT['CHOICES']['AVAILABLE_CRONJOBS']
+except KeyError:
+ AVAILABLE_CRONJOBS = (
+ ('00', _('midnight')),
+ ('04', _('04:00 AM')),
+ )
+
+SCHEDULE_CHOICES = (
+ (0, _("Don't schedule, send immediately")),
+ (1, _('Schedule')),
+)
+
+FILTERING_CHOICES = (
+ (0, _('Send to all')),
+ (1, _('Send accordingly to selected filters')),
+)
+
+FILTER_CHOICES = (
+ (1, _('users of the selected groups')),
+ (2, _('users which have a node in one of the selected zones')),
+ (3, _('chosen users')),
+)
+
+# this is just for convenience and readability
+FILTERS = {
+ 'groups': str(FILTER_CHOICES[0][0]),
+ 'zones': str(FILTER_CHOICES[1][0]),
+ 'users': str(FILTER_CHOICES[2][0])
+}
+
+OUTWARD_STATUS_CHOICES = (
+ (-1, _('error')),
+ (0, _('draft')),
+ (1, _('scheduled')),
+ (2, _('sent')),
+ (3, _('cancelled'))
+)
+
+# this is just for convenience and readability
+OUTWARD_STATUS = {
+ 'error': OUTWARD_STATUS_CHOICES[0][0],
+ 'draft': OUTWARD_STATUS_CHOICES[1][0],
+ 'scheduled': OUTWARD_STATUS_CHOICES[2][0],
+ 'sent': OUTWARD_STATUS_CHOICES[3][0],
+ 'cancelled': OUTWARD_STATUS_CHOICES[4][0]
+}
+
+GROUPS = []
+DEFAULT_GROUPS = ''
+# convert strings to integers
+for group in settings.NODESHOT['CHOICES']['ACCESS_LEVELS']:
+ GROUPS += [(int(group[0]), group[1])]
+ DEFAULT_GROUPS += '%s,' % group[0]
+GROUPS += [(0, _('super users'))]
+DEFAULT_GROUPS += '0'
+
+INWARD_STATUS_CHOICES = (
+ (-1, _('Error')),
+ (0, _('Not sent yet')),
+ (1, _('Sent')),
+ (2, _('Cancelled')),
+)
View
100 nodeshot/core/mailing/models/inward.py
@@ -0,0 +1,100 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+from django.core.validators import MaxLengthValidator, MinLengthValidator
+from django.core.mail import EmailMessage
+from django.conf import settings
+from nodeshot.core.base.models import BaseDate
+from choices import INWARD_STATUS_CHOICES
+
+
+limit = models.Q(app_label = 'nodes', model = 'Node') | models.Q(app_label = 'auth', model = 'User') | models.Q(app_label = 'zones', model = 'Zone')
+
+
+class Inward(BaseDate):
+ """
+ Incoming messages from users
+ """
+ # could be a node, an user or a zone
+ status = models.IntegerField(_('status'), choices=INWARD_STATUS_CHOICES, default=INWARD_STATUS_CHOICES[1][0])
+ content_type = models.ForeignKey(ContentType, limit_choices_to=limit)
+ object_id = models.PositiveIntegerField()
+ to = generic.GenericForeignKey('content_type', 'object_id')
+ user = models.ForeignKey(User, verbose_name=_('user'), blank=not settings.NODESHOT['SETTINGS']['CONTACT_INWARD_REQUIRE_AUTH'], null=True)
+ from_name = models.CharField(_('name'), max_length=50)
+ from_email = models.EmailField(_('email'), max_length=50)
+ message = models.TextField(_('message'), validators=[
+ MaxLengthValidator(settings.NODESHOT['SETTINGS']['CONTACT_INWARD_MAXLENGTH']),
+ MinLengthValidator(settings.NODESHOT['SETTINGS']['CONTACT_INWARD_MINLENGTH'])
+ ])
+ ip = models.GenericIPAddressField(verbose_name=_('ip address'), blank=True, null=True)
+ user_agent = models.CharField(max_length=255, blank=True)
+ http_referer = models.CharField(max_length=255, blank=True)
+ accept_language = models.CharField(max_length=255, blank=True)
+
+ class Meta:
+ verbose_name = _('Inward message')
+ verbose_name_plural = _('Inward messages')
+ app_label= 'mailing'
+ ordering = ['-status']
+
+ def __unicode__(self):
+ return _(u'Message from %(from)s to %(to)s') % ({'from':self.from_name, 'to':self.content_type})
+
+ def send(self):
+ """
+ Sends the email to the recipient
+ If the sending fails will set the status of the instance to "error" and will log the error according to your project's django-logging configuration
+ """
+ if self.content_type.name == 'node':
+ to = [self.to.user.email]
+ elif self.content_type.name == 'zone':
+ to = [self.to.email]
+ # zone case is slightly special, mantainers need to be notified as well
+ # TODO: consider making the mantainers able to switch off notifications
+ for mantainer in self.to.mantainers.all().only('email'):
+ to += [mantainer.email]
+ else:
+ to = [self.to.email]
+
+ email = EmailMessage(
+ # subject
+ _('Contact request from %(sender)s - %(site)s') % {'sender': self.from_name, 'site': settings.SITE_NAME},
+ # message
+ self.message,
+ # from
+ settings.DEFAULT_FROM_EMAIL,
+ # to
+ to,
+ # reply-to header
+ headers = {'Reply-To': self.from_email}
+ )
+
+ import socket
+ # try sending email
+ try:
+ email.send()
+ self.status = 1
+ # if error
+ except socket.error as e:
+ # log the error
+ import logging
+ log = logging.getLogger(__name__)
+ log.error('nodeshot.core.mailing.models.inward.send(): %s' % e)
+ # set status of the instance as "error"
+ self.status = -1
+
+ def save(self, *args, **kwargs):
+ """
+ Custom save method
+ """
+ # if not sent yet
+ if int(self.status) < 1:
+ # tries sending email (will modify self.status!)
+ self.send()
+
+ # save in the database unless logging is explicitly turned off in the settings file
+ if settings.NODESHOT['SETTINGS']['CONTACT_INWARD_LOG']:
+ super(Inward, self).save(*args, **kwargs)
View
243 nodeshot/core/mailing/models/outward.py
@@ -0,0 +1,243 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth.models import User
+from django.core.validators import MaxLengthValidator, MinLengthValidator
+from django.core.exceptions import ValidationError
+from django.core.mail import EmailMessage, EmailMultiAlternatives
+from django.db.models import Q
+from nodeshot.core.base.models import BaseDate
+from nodeshot.core.nodes.models import Node
+from nodeshot.dependencies.fields import MultiSelectField
+from choices import *
+
+from datetime import datetime
+from django.utils.timezone import utc
+
+
+class Outward(BaseDate):
+ """
+ This is a tool that can be used to send out newsletters or important communications to your community.
+ It's aimed to be useful and flexible.
+ """
+ status = models.IntegerField(_('status'), choices=OUTWARD_STATUS_CHOICES, default=OUTWARD_STATUS.get('draft'))
+ subject = models.CharField(_('subject'), max_length=50)
+ message = models.TextField(_('message'), validators=[
+ MinLengthValidator(settings.NODESHOT['SETTINGS']['CONTACT_OUTWARD_MINLENGTH']),
+ MaxLengthValidator(settings.NODESHOT['SETTINGS']['CONTACT_OUTWARD_MAXLENGTH'])
+ ])
+ is_scheduled = models.SmallIntegerField(_('schedule sending'), choices=SCHEDULE_CHOICES, default=1 if settings.NODESHOT['DEFAULTS']['MAILING_SCHEDULE_OUTWARD'] is True else 0)
+ scheduled_date = models.DateField(_('scheduled date'), blank=True, null=True)
+ scheduled_time = models.CharField(_('scheduled time'), max_length=20, choices=AVAILABLE_CRONJOBS, default=settings.NODESHOT['DEFAULTS']['CRONJOB'], blank=True)
+ is_filtered = models.SmallIntegerField(_('recipient filtering'), choices=FILTERING_CHOICES, default=0)
+ filters = MultiSelectField(max_length=255, choices=FILTER_CHOICES, blank=True, help_text=_('specify recipient filters'))
+ groups = MultiSelectField(max_length=255, choices=GROUPS, default=DEFAULT_GROUPS, blank=True, help_text=_('filter specific groups of users'))
+
+ if 'nodeshot.core.zones' in settings.INSTALLED_APPS:
+ zones = models.ManyToManyField('zones.Zone', verbose_name=_('zones'), blank=True)
+
+ users = models.ManyToManyField(User, verbose_name=_('users'), blank=True)
+
+ class Meta:
+ verbose_name = _('Outward message')
+ verbose_name_plural = _('Outward messages')
+ app_label= 'mailing'
+ ordering = ['status']
+
+ def __unicode__(self):
+ return '%s' % self.subject
+
+ def get_recipients(self):
+ """
+ Determine recipients depending on selected filtering which can be either:
+ * group based
+ * zone based
+ * user based
+
+ Choosing "group" and "zone" filtering together has the effect of sending the message only to users for which the following conditions are both true:
+ * have a node assigned to one of the selected zones
+ * are part of any of the specified groups (eg: registered, community, trusted)
+
+ The user based filtering has instead the effect of translating in an **OR** query. Here's a practical example:
+ if selecting "group" and "user" filtering the message will be sent to all the users for which ANY of the following conditions is true:
+ * are part of any of the specified groups (eg: registered, community, trusted)
+ * selected users
+ """
+ # prepare email list
+ emails = []
+
+ # the following code is a bit ugly. Considering the titanic amount of work required to build all the cools functionalities that I have in my mind, I can't be bothered to waste time on making it nicer right now.
+ # if you have ideas on how to improve it to make it cleaner and less cluttered, please join in
+ # this method has unit tests written for it, therefore if you try to change it be sure to check unit tests do not fail after your changes
+ # python manage.py test mailing
+
+ # send to all case
+ if not self.is_filtered:
+ # retrieve only email DB column of all active users
+ users = User.objects.filter(is_active=True).only('email')
+ # loop over users list
+ for user in users:
+ # add email to the recipient list if not already there
+ if not user.email in emails:
+ emails += [user.email]
+ else:
+ # selected users
+ if FILTERS.get('users') in self.filters:
+ # retrieve selected users
+ users = self.users.all().only('email')
+ # loop over selected users
+ for user in users:
+ # add email to the recipient list if not already there
+ if not user.email in emails:
+ emails += [user.email]
+
+ # Q is a django object for "complex" filtering queries (not that complex in this case)
+ # init empty Q object that will be needed in case of group filtering
+ q = Q()
+ q2 = Q()
+
+ # if group filtering is checked
+ if FILTERS.get('groups') in self.filters:
+ # loop over each group
+ for group in self.groups:
+ # if not superusers
+ if group != '0':
+ # add the group to the Q object
+ # this means that the query will look for users of that specific group
+ q = q | Q(groups=int(group))
+ q2 = q2 | Q(user__groups=int(group))
+ else:
+ # this must be done manually because superusers is not a group but an attribute of the User model
+ q = q | Q(is_superuser=True)
+ q2 = q2 | Q(user__is_superuser=True)
+
+ # plus users must be active
+ q = q & Q(is_active=True)
+
+ # if zone filtering is checked
+ if FILTERS.get('zones') in self.filters:
+ # retrieve non-external zones
+ zones = self.zones.all().only('id')
+ # init empty q3
+ q3 = Q()
+ # loop over zones to form q3 object
+ for zone in zones:
+ q3 = q3 | Q(zone=zone)
+ # q2: user group if present
+ # q3: zones
+ # retrieve nodes
+ nodes = Node.objects.filter(q2 & q3)
+ # loop over nodes of a zone and get their email
+ for node in nodes:
+ # add email to the recipient list if not already there
+ if not node.user.email in emails:
+ emails += [node.user.email]
+ # else if group filterins is checked but not zones
+ elif FILTERS.get('groups') in self.filters and not FILTERS.get('zones') in self.filters:
+ # retrieve only email DB column of all active users
+ users = User.objects.filter(q).only('email')
+ # loop over users list
+ for user in users:
+ # add email to the recipient list if not already there
+ if not user.email in emails:
+ emails += [user.email]
+
+ return emails
+
+ def send(self):
+ """
+ Sends the email to the recipients
+ """
+ # if it has already been sent don't send again
+ if self.status is OUTWARD_STATUS.get('sent'):
+ return False
+ # determine recipients
+ recipients = self.get_recipients()
+ # init empty list that will contain django's email objects
+ emails = []
+
+ # prepare text plain if necessary
+ if settings.NODESHOT['SETTINGS']['CONTACT_OUTWARD_HTML'] is True:
+ # store plain text in var
+ from lxml import html
+ html_content = self.message
+ message = html.fromstring(self.message).text_content()
+ # set EmailClass to EmailMultiAlternatives
+ EmailClass = EmailMultiAlternatives
+ else:
+ EmailClass = EmailMessage
+ message = self.message
+
+ # loop over recipients and fill "emails" list
+ for recipient in recipients:
+ msg = EmailClass(
+ # subject
+ self.subject,
+ # message
+ message,
+ # from
+ settings.DEFAULT_FROM_EMAIL,
+ # to
+ [recipient],
+ )
+ if settings.NODESHOT['SETTINGS']['CONTACT_OUTWARD_HTML'] is True:
+ msg.attach_alternative(html_content, "text/html")
+ # prepare email object
+ emails.append(msg)
+
+
+ import socket, time
+ # try sending email
+ try:
+ # counter will count how many emails have been sent
+ counter = 0
+ for email in emails:
+ # if step reached
+ if counter == settings.NODESHOT['SETTINGS']['CONTACT_OUTWARD_STEP']:
+ # reset counter
+ counter = 0
+ # sleep
+ time.sleep(settings.NODESHOT['SETTINGS']['CONTACT_OUTWARD_DELAY'])
+ # send email
+ email.send()
+ # increase counter
+ counter += 1
+ # if error (connection refused, SMTP down)
+ except socket.error as e:
+ # log the error
+ from logging import error
+ error('nodeshot.core.mailing.models.outward.send(): %s' % e)
+ # set status of the instance as "error"
+ self.status = OUTWARD_STATUS.get('error')
+ # change status
+ self.status = OUTWARD_STATUS.get('sent')
+ # save
+ self.save()
+
+ def save(self, *args, **kwargs):
+ """
+ Custom save method
+ """
+ # change status to scheduled if necessary
+ if self.is_scheduled and self.status is not OUTWARD_STATUS.get('scheduled'):
+ self.status = OUTWARD_STATUS.get('scheduled')
+
+ # call super.save()
+ super(Outward, self).save(*args, **kwargs)
+
+ def clean(self, *args, **kwargs):
+ """
+ Custom validation
+ """
+ if self.is_scheduled is 1 and (self.scheduled_date == '' or self.scheduled_date is None or self.scheduled_time == '' or self.scheduled_time is None):
+ raise ValidationError(_('If message is scheduled both fields "scheduled date" and "scheduled time" must be specified'))
+
+ if self.is_scheduled is 1 and self.scheduled_date < datetime.utcnow().replace(tzinfo=utc).date():
+ raise ValidationError(_('The scheduled date is set to a past date'))
+
+ if self.is_filtered is 1 and (len(self.filters) < 1 or self.filters == [''] or self.filters == [u''] or self.filters == '' or self.filters is None):
+ raise ValidationError(_('If "recipient filtering" is active one of the filtering options should be selected'))
+
+ if self.is_filtered is 1 and FILTERS.get('groups') in self.filters and (len(self.groups) < 1 or self.groups == [''] or self.groups == [u''] or self.groups == '' or self.groups is None):
+ raise ValidationError(_('If group filtering is active at least one group of users should be selected'))
+
+ # TODO: unfortunately zones and users can't be validated easily because they are a many2many field
View
71 nodeshot/core/mailing/resources.py
@@ -0,0 +1,71 @@
+from tastypie.resources import ModelResource
+from tastypie import fields
+from tastypie.authorization import Authorization
+from tastypie.authentication import Authentication
+from models import Inward
+
+from django.contrib.contenttypes.models import ContentType
+
+
+class ContentTypeResource(ModelResource):
+
+ class Meta:
+ queryset = ContentType.objects.all()
+ resource_name = 'content_type'
+ allowed_methods = ['get',]
+
+ def dehydrate(self, bundle):
+ # detail view
+ bundle.data['content_type'] = bundle.obj.id
+ return bundle
+
+
+class InwardResource(ModelResource):
+
+ content_type = fields.ToOneField(ContentTypeResource, attribute = 'content_type')
+
+ class Meta:
+ resource_name = 'contact'
+ #allowed_methods = ['post']
+ queryset = Inward.objects.all()
+ authorization = Authorization()
+ authentication = Authentication()
+
+ #def dehydrate(self, bundle):
+ # # detail view
+ # bundle.data['content_type'] = bundle.obj.content_type.id
+ # return bundle
+
+
+#from tastypie.resources import ModelResource
+#from tastypie.contrib.contenttypes.fields import GenericForeignKeyField
+#from tastypie.authorization import Authorization
+#from tastypie.authentication import Authentication
+#from models import Inward
+#
+## TODO: dependencies
+#from nodeshot.core.zones.models import Zone
+#from nodeshot.core.zones.resources import ZoneResource
+#from nodeshot.core.nodes.models import Node
+#from nodeshot.core.nodes.resources import NodeResource
+#
+#
+#class InwardResource(ModelResource):
+#
+# to = GenericForeignKeyField({
+# Node: NodeResource,
+# Zone: ZoneResource
+# }, 'to')
+#
+# class Meta:
+# resource_name = 'contact'
+# #allowed_methods = ['post']
+# queryset = Inward.objects.all()
+# authorization = Authorization()
+# authentication = Authentication()
+# default_format = 'application/json'
+#
+# def dehydrate(self, bundle):
+# # detail view
+# bundle.data['to'] = bundle.obj.content_type.id
+# return bundle
View
93 nodeshot/core/mailing/templates/admin/outward_change_form.html
@@ -0,0 +1,93 @@
+{% extends "admin/change_form.html" %}
+
+<!-- LOADING -->
+{% load url from future %}
+{% load admin_static i18n admin_modify grp_tags %}
+
+<!-- JAVASCRIPTS -->
+{% block javascripts %}
+ {{ block.super }}
+ <script type="text/javascript" charset="utf-8">
+ (function($) {
+ $(document).ready(function() {
+ // nodeshot specific
+ var schedule_fields = $('.scheduled_date, .scheduled_time');
+ var schedule_select = $('#id_is_scheduled');
+ schedule_select.change(function(e){
+ if($(this).val() == false){
+ schedule_fields.fadeOut(300);
+ }
+ else{
+ schedule_fields.fadeIn(300)
+ }
+ });
+ if(schedule_select.val() == false){
+ schedule_fields.hide();
+ }
+
+ var get_filtering_fields = function(){
+ // fields array
+ var fields = ['groups', 'zones', 'users']
+ // .filters is always in the selector
+ var selector = '.filters';
+
+ $('input[name=filters]').each(function(i, item){
+ if(item.checked){
+ selector += ', .' + fields[i]
+ }
+ });
+ console.log(selector)
+ return $(selector);
+ }
+
+ $('.filters, .groups, .zones, .users').hide();
+ var filtered_select = $('#id_is_filtered');
+ filtered_select.change(function(e){
+
+ fields = get_filtering_fields();
+ if($(this).val() == false){
+ fields.fadeOut(300);
+ }
+ else{
+ fields.fadeIn(300)
+ }
+ });
+
+ if(filtered_select.val() == false){
+ get_filtering_fields().hide();
+ }
+ else{
+ get_filtering_fields().show();
+ }
+
+ // toggle groups
+ $('#id_filters_0').change(function(e){
+ if(this.checked){
+ $('.groups').fadeIn(300);
+ }
+ else{
+ $('.groups').fadeOut(300);
+ }
+ });
+
+ $('#id_filters_1').change(function(e){
+ if(this.checked){
+ $('.zones').fadeIn(300);
+ }
+ else{
+ $('.zones').fadeOut(300);
+ }
+ });
+
+ $('#id_filters_2').change(function(e){
+ if(this.checked){
+ $('.users').fadeIn(300);
+ }
+ else{
+ $('.users').fadeOut(300);
+ }
+ });
+ });
+ })(grp.jQuery);
+ </script>
+{% endblock %}
View
367 nodeshot/core/mailing/tests.py
@@ -0,0 +1,367 @@
+"""
+nodeshot.core.mailing unit tests
+"""
+
+from django.test import TestCase
+from django.core.exceptions import ValidationError
+
+from django.db.models import Q
+from django.contrib.auth.models import User, Group
+from nodeshot.core.zones.models import Zone
+from nodeshot.core.nodes.models import Node
+from nodeshot.core.mailing.models import Inward, Outward
+from nodeshot.core.mailing.models.choices import GROUPS, FILTERS
+
+import datetime
+from django.utils.timezone import utc
+
+
+class OutwardTest(TestCase):
+
+ fixtures = ['groups.json', 'test_users.json', 'test_zones.json', 'test_nodes.json']
+
+ def setUp(self):
+ # create outward record
+ self.message = Outward.objects.create(
+ status=0,
+ subject='test message',
+ message='This is a test message, be sure this text is longer than the minimum required',
+ is_scheduled=False,
+ is_filtered=False
+ )
+ self.msg = Outward(
+ status = 0,
+ subject = 'This is a test',
+ message = self.message.subject,
+ )
+
+ def test_fixtures(self):
+ """ *** Tests fixtures have loaded properly *** """
+ zones = Zone.objects.all()
+ nodes = Node.objects.all()
+ users = User.objects.filter(is_active=True)
+
+ self.assertEqual(len(zones), 4)
+ self.assertEqual(len(nodes), 6)
+ self.assertEqual(len(users), 8)
+
+ def test_no_filter(self):
+ """ *** Test no filtering, send to all *** """
+ # count users
+ users = User.objects.filter(is_active=True)
+ recipients = self.message.get_recipients()
+
+ # simplest case: send to all
+ # fail if recipient list length and user list length differ
+ self.assertEqual(len(recipients), len(users))
+ # fail if user emails are not in recipients
+ for user in users:
+ self.assertTrue(user.email in recipients)
+
+ def test_group_filtering(self):
+ """ *** Test group filtering in multiple combinations *** """
+ combinations = [
+ '1','2','3','0',
+ '1,2','1,3','1,0','2,3','2,0','3,0',
+ '1,2,3','1,2,0','1,3,0','2,3,0',
+ '1,2,3,0'
+ ]
+ # prepare record
+ message = self.message
+ message.is_filtered=True
+ message.filters = '1'
+
+ for combo in combinations:
+ # test multiple groups
+ groups = combo.split(',')
+ # init empty Q
+ q = Q()
+ for group in groups:
+ # if not superuser case
+ if group != '0':
+ # add another group to the Q
+ q = q | Q(groups=group)
+ # superuser case
+ else:
+ # group 0 wouldn't be correct, therefore we use is_superuser=True
+ q = q | Q(is_superuser=True)
+ # and is_active = True
+ q = q & Q(is_active=True)
+ # retrieve users
+ users = User.objects.filter(q)
+ message.groups = combo
+ recipients = message.get_recipients()
+ # fail if recipient list length and user list length differ
+ self.assertEqual(len(recipients), len(users))
+ # fail if user emails are not in recipients
+ for user in users:
+ self.assertTrue(user.email in recipients)
+
+ def test_zone_filtering(self):
+ """ *** Test zone filtering in multiple combinations *** """
+ combinations = [
+ '1','2','3',
+ '1,2','1,3','2,3',
+ '1,2,3'
+ ]
+ # prepare record
+ message = self.message
+ message.is_filtered=True
+ message.filters = '2'
+
+ for combo in combinations:
+ zones = combo.split(',')
+ # init empty Q
+ q = Q()
+ for zone in zones:
+ q = q | Q(zone=zone)
+ # retrieve nodes
+ nodes = Node.objects.filter(q)
+ # zones id list
+ message.zones = [int(zone) for zone in zones]
+ # retrieve recipients according to model code
+ recipients = message.get_recipients()
+ # retrieve list of emails
+ emails = []
+ for node in nodes:
+ if not node.user.email in emails:
+ emails += [node.user.email]
+ # fail if recipient list length and user list length differ
+ self.assertEqual(len(recipients), len(emails))
+ # fail if user emails are not in recipients
+ for email in emails:
+ self.assertTrue(email in recipients)
+
+ def test_user_filtering(self):
+ """ *** Test recipient filtering by user *** """
+ combinations = [
+ '1','2','3','4','5','6','7','8',
+ '1,2','2,3','3,4','5,6'
+ '1,2,3','3,4,5','6,7,8'
+ '1,2,3,4','5,6,7,8',
+ '1,2,3,4,5,6,7,8'
+ ]
+ # prepare record
+ message = self.message
+ message.is_filtered=True
+ message.filters = '3'
+
+ for combo in combinations:
+ # users id list
+ users_string = combo.split(',')
+ user_ids = [int(user) for user in users_string]
+ # selected users
+ message.users = user_ids
+ # retrieve recipients according to the model code
+ recipients = message.get_recipients()
+ # init empty Q
+ q = Q()
+ # retrieve users
+ for user in user_ids:
+ q = q | Q(id=user)
+ # only active users
+ q = q & Q(is_active=True)
+ users = User.objects.filter(q)
+ # fail if recipient list length and user list length differ
+ self.assertEqual(len(recipients), len(users))
+ # fail if user emails are not in recipients
+ for user in users:
+ self.assertTrue(user.email in recipients)
+
+ def test_group_and_zone_filtering(self):
+ """ *** Test group & zone filtering in multiple combinations *** """
+ combinations = [
+ { 'groups': '1', 'zones': '1' },
+ { 'groups': '2', 'zones': '2' },
+ { 'groups': '1,2', 'zones': '1' },
+ { 'groups': '1,2,3', 'zones': '1,2' },
+ { 'groups': '1,2', 'zones': '1,2,3' },
+ { 'groups': '3', 'zones': '1,2,3' }
+ ]
+ # prepare record
+ message = self.message
+ message.is_filtered=True
+ message.filters = '1,2'
+
+ for combo in combinations:
+ groups = combo['groups'].split(',')
+ zones = combo['zones'].split(',')
+
+ # GROUPS
+ q1 = Q()
+ for group in groups:
+ # if not superuser case
+ if group != '0':
+ # add another group to the Q
+ q1 = q1 | Q(user__groups=group)
+ # superuser case
+ else:
+ # group 0 wouldn't be correct, therefore we use is_superuser=True
+ q1 = q1 | Q(user__is_superuser=True)
+ # and is_active = True
+ q1 = q1 & Q(user__is_active=True)
+
+ # ZONES
+ q2 = Q()
+ for zone in zones:
+ q2 = q2 | Q(zone=zone)
+ # retrieve nodes
+ nodes = Node.objects.filter(q1 & q2).select_related()
+
+ # message group & zones
+ message.groups = combo['groups']
+ message.zones = [int(zone) for zone in zones]
+
+ # retrieve recipients according to model code
+ recipients = message.get_recipients()
+ # retrieve list of emails
+ emails = []
+ for node in nodes:
+ if not node.user.email in emails:
+ emails += [node.user.email]
+ # fail if recipient list length and user list length differ
+ self.assertEqual(len(recipients), len(emails))
+ # fail if user emails are not in recipients
+ for email in emails:
+ self.assertTrue(email in recipients)
+
+ def test_user_and_zone_filtering(self):
+ """ *** Test recipient filtering by user & zones *** """
+ combinations = [
+ { 'users': '1', 'zones': '1' },
+ { 'users': '2', 'zones': '2' },
+ { 'users': '1,2', 'zones': '1' },
+ { 'users': '1,2,3', 'zones': '1,2' },
+ { 'users': '1,2', 'zones': '1,2,3' },
+ { 'users': '3', 'zones': '1,2,3' },
+ { 'users': '3,4,5,6,7', 'zones': '1' }
+ ]
+ # prepare record
+ message = self.message
+ message.is_filtered=True
+ message.filters = '2,3'
+
+ for combo in combinations:
+ users = combo['users'].split(',')
+ zones = combo['zones'].split(',')
+
+ # ZONES
+ q1 = Q()
+ for zone in zones:
+ q1 = q1 | Q(zone=zone)
+ # retrieve nodes
+ nodes = Node.objects.filter(q1).select_related()
+
+ # prepare Q object for user query
+ q2 = Q()
+ for user in users:
+ q2 = q2 | Q(pk=user)
+ q2 = q2 & Q(is_active=True)
+
+ # message users & zones
+ message.users = [int(user) for user in users]
+ message.zones = [int(zone) for zone in zones]
+
+ # retrieve chosen users
+ users = User.objects.filter(q2)
+
+ # retrieve recipients according to model code
+ recipients = message.get_recipients()
+
+ # retrieve list of emails
+ emails = []
+ for node in nodes:
+ if not node.user.email in emails:
+ emails.append(node.user.email)
+
+ # add emails of selected users if necessary
+ for user in users:
+ if not user.email in emails:
+ emails.append(user.email)
+
+ # fail if recipient list length and user list length differ
+ self.assertEqual(len(recipients), len(emails))
+ # fail if user emails are not in recipients
+ for email in emails:
+ self.assertTrue(email in recipients)
+
+ def test_outward_validation_scheduled1(self):
+ """ *** A scheduled message without any value for scheduled date and time should not pass validation *** """
+ self.msg.is_scheduled = 1
+ self.msg.scheduled_date = None
+ self.msg.scheduled_time = None
+ self.assertRaises(ValidationError, self.msg.full_clean)
+
+ def test_outward_validation_scheduled2(self):
+ """ *** A scheduled message without any value for scheduled time should not pass validation *** """
+ self.msg.is_scheduled = 1
+ self.msg.scheduled_time = None
+ self.msg.scheduled_date = datetime.datetime.utcnow().replace(tzinfo=utc).date() + datetime.timedelta(days=1)
+ self.assertRaises(ValidationError, self.msg.full_clean)
+
+ def test_outward_validation_scheduled3(self):
+ """ *** A scheduled message without any value for scheduled date should not pass validation *** """
+ self.msg.is_scheduled = 1
+ self.msg.scheduled_date = None
+ self.msg.scheduled_time = '00'
+ self.assertRaises(ValidationError, self.msg.full_clean)
+
+ def test_outward_validation_scheduled4(self):
+ """ *** A scheduled message with both date and time should pass validation *** """
+ self.msg.is_scheduled = 1
+ self.msg.scheduled_date = datetime.datetime.utcnow().replace(tzinfo=utc).date() + datetime.timedelta(days=1)
+ self.msg.scheduled_time = '00'
+ self.msg.full_clean()
+
+ def test_outward_validate_scheduled5(self):
+ """ *** A new message with a past scheduled date should not pass validation *** """
+ self.msg.is_scheduled = 1
+ self.msg.scheduled_date = datetime.datetime.utcnow().replace(tzinfo=utc).date() - datetime.timedelta(days=1)
+ self.msg.scheduled_time = '00'
+ self.assertRaises(ValidationError, self.msg.full_clean)
+
+ def test_outward_validate_filters1(self):
+ """ *** A new message with is_filtered == True and no selected filter should not pass validation *** """
+ self.msg.is_scheduled = 0
+ self.msg.scheduled_date = None
+ self.msg.scheduled_time = None
+ self.msg.is_filtered = 1
+ self.assertRaises(ValidationError, self.msg.full_clean)
+
+ def test_outward_validate_filters2(self):
+ """ *** A new message with group filtering active but not selected group should not pass validation *** """
+ self.msg.is_scheduled = 0
+ self.msg.is_filtered = 1
+ self.msg.filters = '%s' % FILTERS.get('groups')
+ self.msg.groups = ''
+ self.assertRaises(ValidationError, self.msg.full_clean)
+
+ # the following two validation routine require more cumbersome procedures ... can't be bothered right now!
+ #
+ #def test_outward_validate_filters3(self):
+ # """ *** A new message with user filtering active but not selected group should not pass validation *** """
+ # self.msg.is_scheduled = 0
+ # self.msg.is_filtered = 1
+ # self.msg.filters = '%s' % FILTERS.get('users')
+ # self.msg.users = []
+ # self.assertRaises(ValidationError, self.msg.full_clean)
+ #
+ #def test_outward_validate_filters4(self):
+ # """ *** A new message with zone filtering active but not selected group should not pass validation *** """
+ # self.msg.is_scheduled = 0
+ # self.msg.is_filtered = 1
+ # self.msg.filters = '%s' % FILTERS.get('zones')
+ # self.msg.zones = []
+ # self.assertRaises(ValidationError, self.msg.full_clean)
+
+ def test_outward_plaintext(self):
+ """ *** TODO: write a test that verifies email is correctly sent as plain text *** """
+ self.assertEqual(False, True)
+
+ def test_outward_html(self):
+ """ *** TODO: write a test that verifies email is correctly sent both as plain text and HTML *** """
+ self.assertEqual(False, True)
+
+ def test_inward_validate_require_auth(self):
+ """ *** TODO: write a test that verifies require auth for inward messages works correctly *** """
+ self.assertEqual(False, True)
View
0 nodeshot/core/contact/views.py → nodeshot/core/mailing/views.py
File renamed without changes.
View
3 nodeshot/core/monitoring/models.py
@@ -1,11 +1,10 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from nodeshot.core.base.models import BaseDate
-from nodeshot.core.nodes.models import Zone
from nodeshot.core.network.models import Device
class Server(BaseDate):
- zone = models.OneToOneField(Zone, verbose_name=_('zone'))
+ zone = models.OneToOneField('zones.Zone', verbose_name=_('zone'))
email = models.EmailField(_('email'), blank=True, null=True)
url = models.URLField(_('url'))
devices = models.ManyToManyField(Device, through='MonitoredDevices', verbose_name=_('devices'))
View
24 nodeshot/core/network/admin.py
@@ -2,8 +2,20 @@
from nodeshot.core.network.models import Device, Interface, Ethernet, Wireless, Bridge, Tunnel, Vap, Vlan, RoutingProtocol, Ip
from nodeshot.core.base.admin import BaseAdmin, BaseStackedInline
-class InterfaceInline(BaseStackedInline):
- model = Interface
+class EthernetInline(BaseStackedInline):
+ model = Ethernet
+
+class WirelessInline(BaseStackedInline):
+ model = Wireless
+
+class BridgeInline(BaseStackedInline):
+ model = Bridge
+
+class TunnelInline(BaseStackedInline):
+ model = Tunnel
+
+class VlanInline(BaseStackedInline):
+ model = Vlan
class IpInline(BaseStackedInline):
model = Ip
@@ -15,7 +27,7 @@ class DeviceAdmin(BaseAdmin):
list_filter = ('added', 'updated', 'node')
list_display = ('name', 'node', 'type', 'added', 'updated')
search_fields = ('name', 'type')
- inlines = (InterfaceInline,)
+ inlines = (EthernetInline, WirelessInline, BridgeInline, TunnelInline, VlanInline)
class InterfaceAdmin(BaseAdmin):
list_display = ('mac', 'name', 'type', 'device', 'added', 'updated')
@@ -26,10 +38,12 @@ class WirelessAdmin(InterfaceAdmin):
inlines = (IpInline,VapInline)
class RoutingProtocolAdmin(BaseAdmin):
- pass
+ list_display = ('name', 'version', 'url')
class IpAdmin(BaseAdmin):
- pass
+ list_display = ('address', 'netmask', 'protocol', 'added', 'updated')
+ list_filter = ('protocol', 'added', 'updated')
+ search_fields = ('address',)
admin.site.register(Device, DeviceAdmin)
#admin.site.register(Interface, InterfaceAdmin)
View
92 nodeshot/core/network/models.py
@@ -1,92 +0,0 @@
-from django.db import models
-from django.utils.translation import ugettext_lazy as _
-from nodeshot.core.base.models import BaseDate, BaseAccessLevel
-from nodeshot.core.nodes.models import Node
-from nodeshot.core.base.choices import ROUTING_PROTOCOLS, DEVICE_STATUS, DEVICE_TYPES, INTERFACE_TYPE, IP_PROTOCOLS, ETHERNET_STANDARDS, WIRELESS_STANDARDS, DUPLEX_CHOICES
-
-class RoutingProtocol(BaseDate):
- name = models.CharField(_('name'), max_length=50, choices=ROUTING_PROTOCOLS)
- version = models.CharField(_('version'), max_length=10)
- url = models.URLField(_('url'))
-
- class Meta:
- db_table = 'network_routing_protocol'
-
-class Device(BaseAccessLevel):
- name = models.CharField(_('name'), max_length=50)
- node = models.ForeignKey(Node, verbose_name=_('node'))
- type = models.CharField(_('type'), max_length=50, choices=DEVICE_TYPES)
- routing_protocols = models.ManyToManyField(RoutingProtocol, blank=True)
- status = models.CharField(_('status'), max_length=1, choices=DEVICE_STATUS, default=DEVICE_STATUS[1][0])
- firmware = models.CharField(_('firmware'), max_length=20, blank=True, null=True)
- os = models.CharField(_('operating system'), max_length=20, blank=True, null=True)
- description = models.CharField(_('description'), max_length=255, blank=True, null=True)
- #notes = models.TextField(_('notes'), blank=True, null=True)
-
-class Interface(BaseAccessLevel):
- device = models.ForeignKey(Device)
- type = models.CharField(_('type'), max_length=10, choices=INTERFACE_TYPE)
- name = models.CharField(_('name'), max_length=10, blank=True, null=True)
- mac = models.CharField(_('mac_address'), max_length=17, unique=True, default=None)
- mtu = models.IntegerField(_('MTU (Maximum Trasmission Unit)'), blank=True, null=True)
- tx_rate = models.IntegerField(_('TX Rate'), null=True, default=None)
- rx_rate = models.IntegerField(_('RX Rate'), null=True, default=None)
-
-class Ip(BaseAccessLevel):
- interface = models.ForeignKey(Interface, verbose_name=_('interface'))
- address = models.GenericIPAddressField(verbose_name=_('ip address'), unique=True)
- protocol = models.CharField(_('IP Protocol Version'), max_length=4, choices=IP_PROTOCOLS, default=IP_PROTOCOLS[0][0])
- netmask = models.CharField(_('netmask'), max_length=100)
-
-class Ethernet(Interface):
- standard = models.CharField(_('type'), max_length=15, choices=ETHERNET_STANDARDS)
- duplex = models.CharField(_('type'), max_length=15, choices=DUPLEX_CHOICES)
-
- class Meta:
- db_table = 'network_interface_ethernet'
-
-class Wireless(Interface):
- wireless_mode = models.CharField(max_length=5)
- wireless_standard = models.CharField(max_length=7, choices=WIRELESS_STANDARDS)
- wireless_channel = models.CharField(max_length=4, blank=True, null=True)
- channel_width = models.CharField(max_length=6, blank=True, null=True)
- transmitpower = models.IntegerField(null=True, blank=True)
- dbm = models.IntegerField(_('dBm'), null=True, default=None)
- noise = models.IntegerField(_('noise'), null=True, default=None)
-
- class Meta:
- db_table = 'network_interface_wireless'
-
-class Bridge(Interface):
- interfaces = models.ManyToManyField(Interface, verbose_name=_('interfaces'), related_name='bridge_interfaces')
-
- class Meta:
- db_table = 'network_interface_bridge'
-
-class Tunnel(Interface):
- sap = models.CharField(max_length=10, null=True, blank=True)
- protocol = models.CharField(max_length=10) # GRE, ... ecc
- endpoint = models.ForeignKey('Ip', verbose_name=_('ip address'))
-
- class Meta:
- db_table = 'network_interface_tunnel'
-
-class Vlan(Interface):
- tag = models.CharField(max_length=10)
-
- class Meta:
- db_table = 'network_interface_vlan'
-
-class Vap(BaseDate):
- interface = models.ForeignKey(Wireless, verbose_name='wireless interface')
- essid = models.CharField(max_length=50)
- bssid = models.CharField(max_length=50, null=True, blank=True)
- encryption = models.CharField(max_length=50, null=True, blank=True)
- key = models.CharField(max_length=100, null=True, blank=True)
- auth_server = models.CharField(max_length=50, null=True, blank=True)
- auth_port = models.IntegerField(null=True, blank=True)
- accounting_server = models.CharField(max_length=50, null=True, blank=True)
- accounting_port = models.IntegerField(null=True, blank=True)
-
- class Meta:
- db_table = 'network_interface_vap'
View
24 nodeshot/core/network/models/__init__.py
@@ -0,0 +1,24 @@
+from routing_protocol import RoutingProtocol
+from device import Device
+from interface import Interface
+from ip import Ip
+
+from interfaces.ethernet import Ethernet
+from interfaces.wireless import Wireless
+from interfaces.bridge import Bridge
+from interfaces.tunnel import Tunnel
+from interfaces.vlan import Vlan
+from interfaces.vap import Vap
+
+__all__ = [