aa-structures 2.10.0__tar.gz → 2.11.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. {aa_structures-2.10.0 → aa_structures-2.11.0}/PKG-INFO +1 -1
  2. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/__init__.py +1 -1
  3. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/admin.py +43 -4
  4. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/app_settings.py +1 -1
  5. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/managers.py +17 -4
  6. aa_structures-2.11.0/structures/migrations/0006_add_ownercharacter_disabled.py +27 -0
  7. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/models/owners.py +104 -51
  8. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/models/test_owners_1.py +95 -52
  9. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/models/test_owners_4.py +21 -5
  10. aa_structures-2.11.0/structures/tests/models/test_owners_6.py +31 -0
  11. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/test_admin.py +65 -0
  12. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/test_managers_2.py +3 -2
  13. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/testdata/factories.py +3 -4
  14. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/views/test_structures.py +58 -5
  15. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/views/structures.py +2 -2
  16. {aa_structures-2.10.0 → aa_structures-2.11.0}/LICENSE +0 -0
  17. {aa_structures-2.10.0 → aa_structures-2.11.0}/README.md +0 -0
  18. {aa_structures-2.10.0 → aa_structures-2.11.0}/pyproject.toml +0 -0
  19. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/apps.py +0 -0
  20. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/auth_hooks.py +0 -0
  21. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/constants.py +0 -0
  22. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/__init__.py +0 -0
  23. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/notification_embeds/__init__.py +0 -0
  24. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/notification_embeds/billing_embeds.py +0 -0
  25. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/notification_embeds/corporate_embeds.py +0 -0
  26. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/notification_embeds/helpers.py +0 -0
  27. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/notification_embeds/main.py +0 -0
  28. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/notification_embeds/moonmining_embeds.py +0 -0
  29. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/notification_embeds/orbital_embeds.py +0 -0
  30. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/notification_embeds/sov_embeds.py +0 -0
  31. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/notification_embeds/structures_embeds.py +0 -0
  32. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/notification_embeds/tower_embeds.py +0 -0
  33. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/notification_embeds/war_embeds.py +0 -0
  34. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/notification_timers.py +0 -0
  35. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/notification_types.py +0 -0
  36. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/serializers.py +0 -0
  37. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/sovereignty.py +0 -0
  38. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/core/starbases.py +0 -0
  39. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/forms.py +0 -0
  40. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/helpers.py +0 -0
  41. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/de/LC_MESSAGES/django.mo +0 -0
  42. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/de/LC_MESSAGES/django.po +0 -0
  43. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/django.pot +0 -0
  44. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/en/LC_MESSAGES/django.mo +0 -0
  45. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/en/LC_MESSAGES/django.po +0 -0
  46. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/es/LC_MESSAGES/django.mo +0 -0
  47. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/es/LC_MESSAGES/django.po +0 -0
  48. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/fr_FR/LC_MESSAGES/django.mo +0 -0
  49. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/fr_FR/LC_MESSAGES/django.po +0 -0
  50. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/it_IT/LC_MESSAGES/django.mo +0 -0
  51. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/it_IT/LC_MESSAGES/django.po +0 -0
  52. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/ja/LC_MESSAGES/django.mo +0 -0
  53. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/ja/LC_MESSAGES/django.po +0 -0
  54. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/ko_KR/LC_MESSAGES/django.mo +0 -0
  55. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/ko_KR/LC_MESSAGES/django.po +0 -0
  56. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/ru/LC_MESSAGES/django.mo +0 -0
  57. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/ru/LC_MESSAGES/django.po +0 -0
  58. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/uk/LC_MESSAGES/django.mo +0 -0
  59. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/uk/LC_MESSAGES/django.po +0 -0
  60. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  61. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/locale/zh_Hans/LC_MESSAGES/django.po +0 -0
  62. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/management/commands/__init__.py +0 -0
  63. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/management/commands/structures_load_eve.py +0 -0
  64. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/management/commands/structures_preload_eveuniverse.py +0 -0
  65. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/management/commands/structures_update_poco_planets.py +0 -0
  66. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/migrations/0001_initial_new.py +0 -0
  67. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/migrations/0002_remove_eveuniverse_relation_names.py +0 -0
  68. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/migrations/0003_add_localization_and_unique_key.py +0 -0
  69. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/migrations/0004_improve_localization.py +0 -0
  70. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/migrations/0005_add_notification_types.py +0 -0
  71. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/migrations/__init__.py +0 -0
  72. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/models/__init__.py +0 -0
  73. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/models/eveuniverse.py +0 -0
  74. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/models/notifications.py +0 -0
  75. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/models/structures_1.py +0 -0
  76. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/models/structures_2.py +0 -0
  77. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/providers.py +0 -0
  78. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/css/global.css +0 -0
  79. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/css/main.css +0 -0
  80. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/css/public.css +0 -0
  81. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/css/statistics.css +0 -0
  82. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/css/structures.css +0 -0
  83. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/bars-rotate-fade-black-36.svg +0 -0
  84. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/bars-rotate-fade-white-36.svg +0 -0
  85. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/eve_symbol_128.png +0 -0
  86. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/0h.png +0 -0
  87. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/0l.png +0 -0
  88. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/0m.png +0 -0
  89. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/0r.png +0 -0
  90. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/0s.png +0 -0
  91. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/1h.png +0 -0
  92. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/1l.png +0 -0
  93. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/1m.png +0 -0
  94. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/1r.png +0 -0
  95. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/2h.png +0 -0
  96. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/2l.png +0 -0
  97. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/2m.png +0 -0
  98. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/2r.png +0 -0
  99. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/3h.png +0 -0
  100. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/3l.png +0 -0
  101. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/3m.png +0 -0
  102. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/3r.png +0 -0
  103. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/4h.png +0 -0
  104. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/4l.png +0 -0
  105. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/4m.png +0 -0
  106. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/4s.png +0 -0
  107. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/5h.png +0 -0
  108. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/5l.png +0 -0
  109. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/5m.png +0 -0
  110. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/5s.png +0 -0
  111. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/6h.png +0 -0
  112. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/6l.png +0 -0
  113. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/6m.png +0 -0
  114. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/7h.png +0 -0
  115. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/7l.png +0 -0
  116. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/7m.png +0 -0
  117. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/8h.png +0 -0
  118. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/8l.png +0 -0
  119. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/8m.png +0 -0
  120. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/blank.png +0 -0
  121. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/circle.png +0 -0
  122. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/dustwheel.png +0 -0
  123. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/h.png +0 -0
  124. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/l.png +0 -0
  125. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/m.png +0 -0
  126. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/noship.png +0 -0
  127. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/r.png +0 -0
  128. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/tyrannis.png +0 -0
  129. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/tyrannis_blue.png +0 -0
  130. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/tyrannis_darkred.png +0 -0
  131. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/tyrannis_default.png +0 -0
  132. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/panel/tyrannis_revelations.png +0 -0
  133. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/img/structures_logo.png +0 -0
  134. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/js/global.js +0 -0
  135. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/js/public.js +0 -0
  136. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/js/statistics.js +0 -0
  137. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/js/structures.js +0 -0
  138. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/vendor/datatables/plugins/dataTables.rowGroup.min.js +0 -0
  139. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/vendor/datatables/plugins/datetime.js +0 -0
  140. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/vendor/datatables/plugins/filterDropDown.min.js +0 -0
  141. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/vendor/datatables/plugins/rowGroup.bootstrap.min.css +0 -0
  142. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/static/structures/vendor/datatables/plugins/rowGroup.dataTables.min.css +0 -0
  143. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tasks.py +0 -0
  144. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/base.html +0 -0
  145. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/modals/fitting_assets.html +0 -0
  146. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/modals/fitting_gfx.html +0 -0
  147. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/modals/poco_details.html +0 -0
  148. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/modals/starbase_detail.html +0 -0
  149. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/modals/structure_details.html +0 -0
  150. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/modals/tab_general_detail.html +0 -0
  151. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/modals/tab_services_detail.html +0 -0
  152. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/partials/menu.html +0 -0
  153. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/partials/public/poco_list.html +0 -0
  154. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/partials/statistics/structure_summary.html +0 -0
  155. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/partials/structures/active_tags.html +0 -0
  156. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/partials/structures/jump_gates_list.html +0 -0
  157. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/partials/structures/poco_list.html +0 -0
  158. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/partials/structures/starbase_list.html +0 -0
  159. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/partials/structures/structure_list.html +0 -0
  160. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/public.html +0 -0
  161. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/statistics.html +0 -0
  162. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/structures.html +0 -0
  163. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/templatetags/detail_title.html +0 -0
  164. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/templatetags/list_asset.html +0 -0
  165. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/templatetags/list_item.html +0 -0
  166. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/templatetags/list_tax_item.html +0 -0
  167. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templates/structures/templatetags/list_title.html +0 -0
  168. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templatetags/__init__.py +0 -0
  169. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/templatetags/structures.py +0 -0
  170. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/__init__.py +0 -0
  171. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/core/__init__.py +0 -0
  172. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/core/notification_embeds/__init__.py +0 -0
  173. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/core/notification_embeds/test_helpers.py +0 -0
  174. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/core/notification_embeds/test_main.py +0 -0
  175. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/core/test_notification_structuretimers.py +0 -0
  176. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/core/test_notification_types.py +0 -0
  177. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/core/test_notifications_timerboard.py +0 -0
  178. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/core/test_serializers.py +0 -0
  179. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/core/test_sovereignty.py +0 -0
  180. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/core/test_starbases.py +0 -0
  181. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/integration/__init__.py +0 -0
  182. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/integration/test_tasks.py +0 -0
  183. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/integration/test_views.py +0 -0
  184. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/models/__init__.py +0 -0
  185. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/models/test_eveuniverse.py +0 -0
  186. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/models/test_notifications_1.py +0 -0
  187. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/models/test_notifications_2.py +0 -0
  188. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/models/test_notifications_3.py +0 -0
  189. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/models/test_notifications_discord.py +0 -0
  190. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/models/test_owners_2.py +0 -0
  191. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/models/test_owners_3.py +0 -0
  192. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/models/test_owners_5.py +0 -0
  193. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/models/test_structures.py +0 -0
  194. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/test_helpers.py +0 -0
  195. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/test_managers_1.py +0 -0
  196. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/test_tasks.py +0 -0
  197. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/testdata/__init__.py +0 -0
  198. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/testdata/create_eveuniverse.py +0 -0
  199. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/testdata/entities.json +0 -0
  200. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/testdata/esi_data.json +0 -0
  201. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/testdata/eveuniverse.json +0 -0
  202. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/testdata/generate_notifications.py +0 -0
  203. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/testdata/generate_notifications_2.py +0 -0
  204. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/testdata/generate_structures.py +0 -0
  205. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/testdata/helpers.py +0 -0
  206. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/testdata/load_eveuniverse.py +0 -0
  207. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/testdata/tasks_loadtest.py +0 -0
  208. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/testdata/test_generate_structures.py +0 -0
  209. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/views/__init__.py +0 -0
  210. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/views/test_public.py +0 -0
  211. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/views/test_service_status.py +0 -0
  212. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/views/test_statistics.py +0 -0
  213. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/tests/views/utils.py +0 -0
  214. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/urls.py +0 -0
  215. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/views/__init__.py +0 -0
  216. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/views/common.py +0 -0
  217. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/views/public.py +0 -0
  218. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/views/statistics.py +0 -0
  219. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/views/status.py +0 -0
  220. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/webhooks/__init__.py +0 -0
  221. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/webhooks/core.py +0 -0
  222. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/webhooks/managers.py +0 -0
  223. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/webhooks/models.py +0 -0
  224. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/webhooks/tests/__init__.py +0 -0
  225. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/webhooks/tests/test_core.py +0 -0
  226. {aa_structures-2.10.0 → aa_structures-2.11.0}/structures/webhooks/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aa-structures
3
- Version: 2.10.0
3
+ Version: 2.11.0
4
4
  Summary: App for managing Eve Online structures with Alliance Auth.
5
5
  Author-email: Erik Kalkoken <kalkoken87@gmail.com>
6
6
  Requires-Python: >=3.8
@@ -3,5 +3,5 @@
3
3
  # pylint: disable = invalid-name
4
4
  default_app_config = "structures.apps.StructuresConfig"
5
5
 
6
- __version__ = "2.10.0"
6
+ __version__ = "2.11.0"
7
7
  __title__ = "Structures"
@@ -348,6 +348,29 @@ class OwnerCharacterAdminInline(admin.TabularInline):
348
348
  return False
349
349
 
350
350
 
351
+ class DisabledCharactersFilter(admin.SimpleListFilter):
352
+ title = _("has disabled characters")
353
+ parameter_name = "has_disabled_characters"
354
+
355
+ def lookups(self, request, model_admin):
356
+ return (
357
+ ("yes", _("yes")),
358
+ ("no", _("no")),
359
+ )
360
+
361
+ def queryset(self, request, queryset):
362
+ """Return the filtered queryset"""
363
+ if self.value() == "yes":
364
+ return queryset.annotate_characters_count().filter(
365
+ characters_disabled_count__gt=0
366
+ )
367
+ if self.value() == "no":
368
+ return queryset.annotate_characters_count().filter(
369
+ characters_disabled_count=0
370
+ )
371
+ return queryset
372
+
373
+
351
374
  @admin.register(Owner)
352
375
  class OwnerAdmin(admin.ModelAdmin):
353
376
  list_display = (
@@ -364,6 +387,7 @@ class OwnerAdmin(admin.ModelAdmin):
364
387
  list_filter = (
365
388
  "is_active",
366
389
  "is_up",
390
+ DisabledCharactersFilter,
367
391
  ("corporation__alliance", admin.RelatedOnlyFieldListFilter),
368
392
  "has_default_pings_enabled",
369
393
  "is_alliance_main",
@@ -376,6 +400,7 @@ class OwnerAdmin(admin.ModelAdmin):
376
400
  "fetch_notifications",
377
401
  "deactivate_owners",
378
402
  "activate_owners",
403
+ "reset_characters",
379
404
  )
380
405
  inlines = (OwnerCharacterAdminInline,)
381
406
  filter_horizontal = ("ping_groups", "webhooks")
@@ -485,9 +510,13 @@ class OwnerAdmin(admin.ModelAdmin):
485
510
  def has_add_permission(self, request):
486
511
  return False
487
512
 
488
- @admin.display(ordering="characters_count_2", description=_("characters"))
489
- def _characters(self, obj: Owner) -> int:
490
- return obj.characters_count_2
513
+ @admin.display(ordering="characters_enabled_count", description=_("characters"))
514
+ def _characters(self, obj: Owner) -> str:
515
+ enabled = obj.characters_enabled_count
516
+ disabled = obj.characters_disabled_count
517
+ if not disabled:
518
+ return enabled
519
+ return f"{enabled} ({disabled})"
491
520
 
492
521
  @admin.display(description=_("default pings"), boolean=True)
493
522
  def _has_default_pings_enabled(self, obj: Owner):
@@ -549,6 +578,16 @@ class OwnerAdmin(admin.ModelAdmin):
549
578
  queryset.update(is_active=False)
550
579
  self.message_user(request, _("Deactivated %d owners") % queryset.count())
551
580
 
581
+ @admin.action(description=_("Reset disabled characters for selected owners"))
582
+ def reset_characters(self, request, queryset):
583
+ owner_pks = queryset.values_list("pk", flat=True)
584
+ OwnerCharacter.objects.filter(
585
+ owner__pk__in=list(owner_pks), is_enabled=False
586
+ ).update(is_enabled=True, disabled_reason="", error_count=0)
587
+ self.message_user(
588
+ request, _("Characters have been reset for %d owners") % len(owner_pks)
589
+ )
590
+
552
591
  @admin.action(description=_("Update all from EVE server for selected owners"))
553
592
  def update_all(self, request, queryset):
554
593
  for obj in queryset:
@@ -631,7 +670,7 @@ class OwnerAdmin(admin.ModelAdmin):
631
670
  def _assets_last_update_fresh(self, obj: Owner) -> bool:
632
671
  return obj.is_assets_sync_fresh
633
672
 
634
- @admin.action(description=_("structures Count"))
673
+ @admin.display(description=_("structures Count"))
635
674
  def _structures_count(self, obj: Owner) -> int:
636
675
  return obj.structures.count()
637
676
 
@@ -55,7 +55,7 @@ STRUCTURES_FEATURE_STARBASES = clean_setting("STRUCTURES_FEATURE_STARBASES", Tru
55
55
  STRUCTURES_ESI_DIRECTOR_ERROR_MAX_RETRIES = clean_setting(
56
56
  "STRUCTURES_ESI_DIRECTOR_ERROR_MAX_RETRIES", 3
57
57
  )
58
- """Max retries before a character is deleted when ESI claims the character
58
+ """Max retries before a character is disabled when ESI claims the character
59
59
  is not a director (Since this sometimes is reported wrongly by ESI).
60
60
  """
61
61
 
@@ -181,14 +181,27 @@ GeneratedNotificationManager = GeneratedNotificationManagerBase.from_queryset(
181
181
 
182
182
  class OwnerQuerySet(models.QuerySet):
183
183
  def annotate_characters_count(self) -> models.QuerySet:
184
- """Add character count annotation."""
185
- return self.annotate(
186
- characters_count_2=Count(
184
+ """Add character count annotations."""
185
+ qs = self.annotate(
186
+ characters_enabled_count=Count(
187
+ "characters",
188
+ filter=Q(
189
+ characters__character_ownership__isnull=False,
190
+ characters__is_enabled=True,
191
+ ),
192
+ distinct=True,
193
+ )
194
+ ).annotate(
195
+ characters_disabled_count=Count(
187
196
  "characters",
188
- filter=Q(characters__character_ownership__isnull=False),
197
+ filter=Q(
198
+ characters__character_ownership__isnull=False,
199
+ characters__is_enabled=False,
200
+ ),
189
201
  distinct=True,
190
202
  )
191
203
  )
204
+ return qs
192
205
 
193
206
  def structures_last_updated(self) -> Optional[dt.datetime]:
194
207
  """Date/time when structures were last updated for any of the active owners."""
@@ -0,0 +1,27 @@
1
+ # Generated by Django 4.0.10 on 2024-06-23 14:26
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("structures", "0005_add_notification_types"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="ownercharacter",
15
+ name="disabled_reason",
16
+ field=models.TextField(default=""),
17
+ ),
18
+ migrations.AddField(
19
+ model_name="ownercharacter",
20
+ name="is_enabled",
21
+ field=models.BooleanField(
22
+ default=True,
23
+ help_text="Disabled characters are not used for syncing owners",
24
+ verbose_name="is enabled",
25
+ ),
26
+ ),
27
+ ]
@@ -338,12 +338,15 @@ class Owner(models.Model):
338
338
  f"Character {character_ownership.character} does not belong "
339
339
  "to owner corporation."
340
340
  )
341
- obj, _ = self.characters.get_or_create(character_ownership=character_ownership)
341
+ obj: OwnerCharacter = self.characters.get_or_create(
342
+ character_ownership=character_ownership
343
+ )[0]
344
+ obj.reset()
342
345
  return obj
343
346
 
344
- def characters_count(self) -> int:
347
+ def valid_characters_count(self) -> int:
345
348
  """Count of valid owner characters."""
346
- return self.characters.count()
349
+ return self.characters.filter(is_enabled=True).count()
347
350
 
348
351
  def has_sov(self, eve_solar_system: EveSolarSystem) -> bool:
349
352
  """Determine whether this owner has sov in the given solar system."""
@@ -351,42 +354,31 @@ class Owner(models.Model):
351
354
  eve_solar_system=eve_solar_system, corporation=self.corporation
352
355
  )
353
356
 
354
- def delete_character(
357
+ def disable_character(
355
358
  self,
356
359
  character: "OwnerCharacter",
357
- error: str,
358
- level: str = "warning",
360
+ reason: str,
359
361
  max_allowed_errors: int = 0,
360
362
  ) -> None:
361
- """Delete character and notify it's owner and admin about the reason
363
+ """Disable character and notify it's owner and admins about it.
362
364
 
363
365
  Args:
364
- - character: Character this error refers to
365
- - error: Error text
366
- - level: context level for the notification
367
- - max_error: how many errors are permitted before character is deleted
366
+ - character: Character to disable
367
+ - reason: User friendly reason for the deletion
368
+ - max_allowed_errors: Maximum number of allowed errors for this type of error
368
369
  """
369
370
  if character.error_count < max_allowed_errors:
370
- logger.warning(
371
- (
372
- "%s: Character encountered an error and will be deleted "
373
- "if this occurs more often (%d/%d): %s"
374
- ),
375
- character,
376
- character.error_count + 1,
377
- max_allowed_errors,
378
- error,
379
- )
380
- with transaction.atomic():
381
- character.error_count = F("error_count") + 1
382
- character.save(update_fields=["error_count"])
371
+ character.increase_error_count()
383
372
  return
384
373
 
385
- title = f"{__title__}: {self}: Invalid character has been removed"
374
+ character.disable(reason)
375
+
376
+ title = f"{__title__}: {self}: Character has been disabled"
377
+ level = "warning"
386
378
  message = (
387
- f"{character.character_ownership}: {error}\n"
388
- "The character has been removed. "
389
- "Please add a new character to restore the previous service level."
379
+ f"{character.character_ownership}: {reason}\n"
380
+ "This character caused too many errors and has been disabled. "
381
+ "Administrator action is required to resolve this issue."
390
382
  )
391
383
  notify(
392
384
  user=character.character_ownership.user,
@@ -394,15 +386,44 @@ class Owner(models.Model):
394
386
  message=message,
395
387
  level=level,
396
388
  )
397
- if self.characters.count() == 1:
389
+ if not self.valid_characters_count():
398
390
  message += (
399
391
  " This owner has no configured characters anymore "
400
392
  "and it's services are now down."
401
393
  )
402
394
  level = "danger"
395
+
403
396
  notify_admins(title=f"FYI: {title}", message=message, level=level)
397
+
398
+ def delete_character(self, character: "OwnerCharacter", reason: str) -> None:
399
+ """Delete character and notify it's owner and admin about the reason
400
+
401
+ Args:
402
+ - character: Character this error refers to
403
+ - reason: User friendly reason for the deletion
404
+ """
404
405
  character.delete()
405
406
 
407
+ title = f"{__title__}: {self}: Invalid character has been removed"
408
+ level = "warning"
409
+ message = (
410
+ f"{character.character_ownership}: {reason}\n"
411
+ "Your character is no longer valid for syncing this owner and has been removed. "
412
+ )
413
+ notify(
414
+ user=character.character_ownership.user,
415
+ title=title,
416
+ message=message,
417
+ level=level,
418
+ )
419
+ if not self.valid_characters_count():
420
+ message += (
421
+ " This owner has no valid characters anymore "
422
+ "and it's services are now down."
423
+ )
424
+ level = "danger"
425
+ notify_admins(title=f"FYI: {title}", message=message, level=level)
426
+
406
427
  def _rotate_character(
407
428
  self,
408
429
  character: "OwnerCharacter",
@@ -421,7 +442,7 @@ class Owner(models.Model):
421
442
  )
422
443
  try:
423
444
  minimum_time_between_rotations = max(
424
- rotate_characters.esi_cache_duration / self.characters.count(),
445
+ rotate_characters.esi_cache_duration / self.valid_characters_count(),
425
446
  60,
426
447
  )
427
448
  except ZeroDivisionError:
@@ -453,14 +474,18 @@ class Owner(models.Model):
453
474
  if rotate_characters
454
475
  else "notifications_last_used_at"
455
476
  )
456
- for character in self.characters.order_by(order_by_last_used):
477
+ enabled_characters: models.QuerySet[OwnerCharacter] = self.characters.filter(
478
+ is_enabled=True
479
+ ).order_by(order_by_last_used)
480
+ for character in enabled_characters:
457
481
  if (
458
482
  character.character_ownership.character.corporation_id
459
483
  != self.corporation.corporation_id
460
484
  ):
485
+ corporation_name = self.corporation.corporation_name
461
486
  self.delete_character(
462
487
  character=character,
463
- error="Character does no longer belong to the owner's corporation.",
488
+ reason=f"Character is no longer a member of {corporation_name}",
464
489
  )
465
490
  continue
466
491
 
@@ -469,17 +494,17 @@ class Owner(models.Model):
469
494
  ):
470
495
  self.delete_character(
471
496
  character=character,
472
- error="Character does not have sufficient permission to sync.",
497
+ reason="Character no longer has permission to sync",
473
498
  )
474
499
  continue
475
500
 
476
501
  token = character.valid_token()
477
502
  if not token:
478
- self.delete_character(
479
- character=character,
480
- error="Character has no valid token for sync.",
503
+ self.disable_character(
504
+ character=character, reason="No valid token found for character"
481
505
  )
482
506
  continue
507
+
483
508
  found_character = character
484
509
  break # leave the for loop if we have found a valid token
485
510
 
@@ -797,6 +822,13 @@ class Owner(models.Model):
797
822
 
798
823
  Return True when successful, else False.
799
824
  """
825
+ try:
826
+ character: OwnerCharacter = self.characters.get(
827
+ character_ownership__character__character_id=token.character_id
828
+ )
829
+ except ObjectDoesNotExist:
830
+ return False
831
+
800
832
  structures = []
801
833
  try:
802
834
  starbases_data = (
@@ -824,21 +856,11 @@ class Owner(models.Model):
824
856
  self._store_raw_data("starbases", structures)
825
857
 
826
858
  except HTTPForbidden:
827
- try:
828
- character = self.characters.get(
829
- character_ownership__character__character_id=token.character_id
830
- )
831
- except ObjectDoesNotExist:
832
- pass
833
- else:
834
- self.delete_character(
835
- character=character,
836
- error=(
837
- "Character is not a director or CEO and therefore "
838
- "can not fetch starbases."
839
- ),
840
- max_allowed_errors=STRUCTURES_ESI_DIRECTOR_ERROR_MAX_RETRIES,
841
- )
859
+ self.disable_character(
860
+ character=character,
861
+ reason=("This character is not a director or CEO"),
862
+ max_allowed_errors=STRUCTURES_ESI_DIRECTOR_ERROR_MAX_RETRIES,
863
+ )
842
864
  return False
843
865
 
844
866
  except OSError as ex:
@@ -849,6 +871,7 @@ class Owner(models.Model):
849
871
  structures_qs=self.structures.filter_starbases(),
850
872
  new_structures=structures,
851
873
  )
874
+ character.reset_error_counter()
852
875
  return True
853
876
 
854
877
  def _store_updates_for_starbases(self, token, structures):
@@ -1337,6 +1360,12 @@ class OwnerCharacter(models.Model):
1337
1360
  verbose_name=_("error count"),
1338
1361
  help_text="Count of ESI errors which happened with this character.",
1339
1362
  )
1363
+ is_enabled = models.BooleanField(
1364
+ default=True,
1365
+ verbose_name=_("is enabled"),
1366
+ help_text=_("Disabled characters are not used for syncing owners"),
1367
+ )
1368
+ disabled_reason = models.TextField(default="")
1340
1369
  created_at = models.DateTimeField(auto_now_add=True)
1341
1370
 
1342
1371
  class Meta:
@@ -1369,3 +1398,27 @@ class OwnerCharacter(models.Model):
1369
1398
  .require_valid()
1370
1399
  .first()
1371
1400
  )
1401
+
1402
+ def reset(self) -> None:
1403
+ """Resets a disabled owner character."""
1404
+ self.is_enabled = True
1405
+ self.disabled_reason = ""
1406
+ self.error_count = 0
1407
+ self.save()
1408
+
1409
+ def reset_error_counter(self) -> None:
1410
+ """Reset the error counter"""
1411
+ self.error_count = 0
1412
+ self.save(update_fields=["error_count"])
1413
+
1414
+ def disable(self, reason: str = "") -> None:
1415
+ """Disables a character."""
1416
+ self.is_enabled = False
1417
+ self.disabled_reason = reason
1418
+ self.save()
1419
+
1420
+ def increase_error_count(self):
1421
+ """Increase error count of this character by one."""
1422
+ with transaction.atomic():
1423
+ self.error_count = F("error_count") + 1
1424
+ self.save(update_fields=["error_count"])
@@ -318,20 +318,21 @@ class TestOwnerFetchToken(NoSocketsTestCase):
318
318
  self.assertTrue(mock_notify.called)
319
319
  self.assertEqual(owner.characters.count(), 0)
320
320
 
321
- def test_raise_error_when_token_not_found_and_delete_character(
321
+ def test_raise_error_when_no_valid_token_found_and_disable_character(
322
322
  self, mock_notify_admins, mock_notify
323
323
  ):
324
324
  # given
325
- character = EveCharacterFactory()
326
- user = UserMainDefaultOwnerFactory(main_character__character=character)
327
- owner = OwnerFactory(user=user, characters=[character])
328
- user.token_set.first().scopes.clear()
325
+ eve_character = EveCharacterFactory()
326
+ user = UserMainDefaultOwnerFactory(main_character__character=eve_character)
327
+ owner = OwnerFactory(user=user, characters=[eve_character])
328
+ user.token_set.first().scopes.clear() # token no longer valid
329
329
  # when/then
330
330
  with self.assertRaises(TokenError):
331
331
  owner.fetch_token()
332
+ character = owner.characters.first()
333
+ self.assertFalse(character.is_enabled)
332
334
  self.assertTrue(mock_notify_admins.called)
333
335
  self.assertTrue(mock_notify.called)
334
- self.assertEqual(owner.characters.count(), 0)
335
336
 
336
337
  def test_raise_error_when_character_no_longer_a_corporation_member_and_delete_it(
337
338
  self, mock_notify_admins, mock_notify
@@ -351,37 +352,7 @@ class TestOwnerFetchToken(NoSocketsTestCase):
351
352
  self.assertTrue(mock_notify.called)
352
353
  self.assertEqual(owner.characters.count(), 0)
353
354
 
354
- def test_should_delete_invalid_characters_and_return_token_from_valid_char(
355
- self, mock_notify_admins, mock_notify
356
- ):
357
- # given
358
- character_1 = EveCharacterFactory()
359
- user = UserMainDefaultOwnerFactory(main_character__character=character_1)
360
- owner = OwnerFactory(
361
- user=user,
362
- characters=[character_1],
363
- characters__notifications_last_used_at=dt.datetime(
364
- 2021, 1, 1, 1, 2, tzinfo=utc
365
- ),
366
- )
367
- character_2 = EveCharacterFactory()
368
- OwnerCharacterFactory(
369
- owner=owner,
370
- eve_character=character_2,
371
- notifications_last_used_at=dt.datetime(2021, 1, 1, 1, 1, tzinfo=utc),
372
- )
373
-
374
- # when
375
- token = owner.fetch_token()
376
-
377
- # then
378
- self.assertIsInstance(token, Token)
379
- self.assertEqual(token.user, user)
380
- self.assertTrue(mock_notify_admins.called)
381
- self.assertTrue(mock_notify.called)
382
- self.assertEqual(owner.characters.count(), 1)
383
-
384
- def test_should_rotate_through_characters_for_notification(
355
+ def test_should_rotate_through_enabled_characters_for_notification(
385
356
  self, mock_notify_admins, mock_notify
386
357
  ):
387
358
  # given
@@ -398,6 +369,9 @@ class TestOwnerFetchToken(NoSocketsTestCase):
398
369
  owner=owner,
399
370
  notifications_last_used_at=dt.datetime(2021, 1, 1, 3, 0, tzinfo=utc),
400
371
  )
372
+ OwnerCharacterFactory(
373
+ owner=owner, is_enabled=False
374
+ ) # this one should be ignore
401
375
  tokens_received = []
402
376
 
403
377
  # when
@@ -493,6 +467,36 @@ class TestOwnerFetchToken(NoSocketsTestCase):
493
467
  ],
494
468
  )
495
469
 
470
+ def test_should_delete_invalid_characters_and_return_token_from_valid_char(
471
+ self, mock_notify_admins, mock_notify
472
+ ):
473
+ # given
474
+ character_1 = EveCharacterFactory()
475
+ user = UserMainDefaultOwnerFactory(main_character__character=character_1)
476
+ owner = OwnerFactory(
477
+ user=user,
478
+ characters=[character_1],
479
+ characters__notifications_last_used_at=dt.datetime(
480
+ 2021, 1, 1, 1, 2, tzinfo=utc
481
+ ),
482
+ )
483
+ character_2 = EveCharacterFactory() # invalid, because of different corporation
484
+ OwnerCharacterFactory(
485
+ owner=owner,
486
+ eve_character=character_2,
487
+ notifications_last_used_at=dt.datetime(2021, 1, 1, 1, 1, tzinfo=utc),
488
+ )
489
+
490
+ # when
491
+ token = owner.fetch_token()
492
+
493
+ # then
494
+ self.assertIsInstance(token, Token)
495
+ self.assertEqual(token.user, user)
496
+ self.assertTrue(mock_notify_admins.called)
497
+ self.assertTrue(mock_notify.called)
498
+ self.assertEqual(owner.characters.count(), 1)
499
+
496
500
 
497
501
  class TestOwnerCharacters(NoSocketsTestCase):
498
502
  @classmethod
@@ -500,12 +504,6 @@ class TestOwnerCharacters(NoSocketsTestCase):
500
504
  super().setUpClass()
501
505
  cls.owner = OwnerFactory()
502
506
 
503
- def test_should_return_str(self):
504
- # given
505
- character = OwnerCharacterFactory(owner=self.owner)
506
- # when/then
507
- self.assertTrue(str(character))
508
-
509
507
  def test_should_add_new_character(self):
510
508
  # given
511
509
  character = EveCharacterFactory(corporation=self.owner.corporation)
@@ -546,22 +544,35 @@ class TestOwnerCharacters(NoSocketsTestCase):
546
544
  with self.assertRaises(ValueError):
547
545
  self.owner.add_character(character_ownership)
548
546
 
549
- def test_should_count_characters(self):
547
+ def test_should_count_enabled_characters_only(self):
550
548
  # given
551
- OwnerCharacterFactory(owner=self.owner)
549
+ OwnerCharacterFactory(owner=self.owner, is_enabled=False)
552
550
  # when
553
- result = self.owner.characters_count()
551
+ result = self.owner.valid_characters_count()
554
552
  # then
555
- self.assertEqual(result, 2)
553
+ self.assertEqual(result, 1)
556
554
 
557
555
  def test_should_count_characters_when_empty(self):
558
556
  # given
559
557
  owner = OwnerFactory(characters=False)
560
558
  # when
561
- result = owner.characters_count()
559
+ result = owner.valid_characters_count()
562
560
  # then
563
561
  self.assertEqual(result, 0)
564
562
 
563
+ def test_should_reset_character_when_re_adding(self):
564
+ # given
565
+ character: OwnerCharacter = self.owner.characters.first()
566
+ character.is_enabled = False
567
+ character.disabled_reason = "some reason"
568
+ character.save()
569
+ # when
570
+ self.owner.add_character(character.character_ownership)
571
+ # then
572
+ character.refresh_from_db()
573
+ self.assertTrue(character.is_enabled)
574
+ self.assertFalse(character.disabled_reason)
575
+
565
576
 
566
577
  @patch(MODULE_PATH + ".notify", spec=True)
567
578
  @patch(MODULE_PATH + ".notify_admins", spec=True)
@@ -577,7 +588,7 @@ class TestOwnerDeleteCharacter(NoSocketsTestCase):
577
588
  user = character.character_ownership.user
578
589
 
579
590
  # when
580
- self.owner.delete_character(character=character, error="dummy error")
591
+ self.owner.delete_character(character=character, reason="dummy error")
581
592
 
582
593
  # then
583
594
  self.assertEqual(self.owner.characters.count(), 0)
@@ -591,18 +602,50 @@ class TestOwnerDeleteCharacter(NoSocketsTestCase):
591
602
  self.assertEqual(kwargs["user"], user)
592
603
  self.assertEqual(kwargs["level"], "warning")
593
604
 
594
- def test_should_not_delete_when_errors_are_allowed(
605
+
606
+ @patch(MODULE_PATH + ".notify", spec=True)
607
+ @patch(MODULE_PATH + ".notify_admins", spec=True)
608
+ class TestOwnerDisableCharacters(NoSocketsTestCase):
609
+ @classmethod
610
+ def setUpClass(cls):
611
+ super().setUpClass()
612
+ cls.owner = OwnerFactory(characters=False)
613
+
614
+ def test_should_disable_character_and_notify(self, mock_notify_admins, mock_notify):
615
+ # given
616
+ character = OwnerCharacterFactory(owner=self.owner)
617
+ user = character.character_ownership.user
618
+
619
+ # when
620
+ self.owner.disable_character(character=character, reason="dummy error")
621
+
622
+ # then
623
+ character.refresh_from_db()
624
+ self.assertFalse(character.is_enabled)
625
+ self.assertTrue(character.disabled_reason)
626
+ self.assertTrue(mock_notify_admins.called)
627
+ _, kwargs = mock_notify_admins.call_args
628
+ self.assertIn("dummy error", kwargs["message"])
629
+ self.assertEqual(kwargs["level"], "danger")
630
+ self.assertTrue(mock_notify.called)
631
+ _, kwargs = mock_notify.call_args
632
+ self.assertIn("dummy error", kwargs["message"])
633
+ self.assertEqual(kwargs["user"], user)
634
+ self.assertEqual(kwargs["level"], "warning")
635
+
636
+ def test_should_not_disable_when_error_counter_above_zero(
595
637
  self, mock_notify_admins, mock_notify
596
638
  ):
597
639
  # given
598
640
  character = OwnerCharacterFactory(owner=self.owner)
599
641
 
600
642
  # when
601
- self.owner.delete_character(
602
- character=character, error="dummy error", max_allowed_errors=1
643
+ self.owner.disable_character(
644
+ character=character, reason="dummy error", max_allowed_errors=1
603
645
  )
604
646
  # then
605
647
  character.refresh_from_db()
648
+ self.assertTrue(character.is_enabled)
606
649
  self.assertEqual(character.error_count, 1)
607
650
  self.assertFalse(mock_notify_admins.called)
608
651
  self.assertFalse(mock_notify.called)