Compare commits

...

102 Commits

Author SHA1 Message Date
Ariel Rin
4a9a2a670c Version Bump 2.9.0b1 2021-09-14 05:08:15 +00:00
Ariel Rin
5b25637de5 Debian Bullseye now Stable, libmariadbclient-dev name deprecated, now removed 2021-09-14 05:02:15 +00:00
Ariel Rin
cf2e368978 Merge branch 'docs-updates' into 'v2.9.x'
Docs Updates

See merge request allianceauth/allianceauth!1334
2021-09-14 04:21:23 +00:00
Peter Pfeufer
0d67673542 Docs Updates 2021-09-14 04:21:22 +00:00
Ariel Rin
d7e58fb557 Merge branch 'django-admin-favicons' into 'v2.9.x'
Add favicon support to django admin backend

See merge request allianceauth/allianceauth!1335
2021-09-14 04:19:33 +00:00
Peter Pfeufer
f8cffb64a1 add favicon support to django admin backend 2021-08-25 22:37:37 +02:00
Joel Falknau
dc97fb1cc5 Forgot pre-commit for master > 2.9.x 2021-08-20 14:50:44 +10:00
Joel Falknau
392a0c4dcb Merge remote-tracking branch 'upstream/master' into v2.9.x 2021-08-20 14:41:38 +10:00
Ariel Rin
970a111386 Merge branch 'analytics' into 'v2.9.x'
Analytics Enable migration typo

See merge request allianceauth/allianceauth!1331
2021-08-20 04:04:33 +00:00
Ariel Rin
cafa7cbf17 Merge branch 'top-menu-redesign' into 'v2.9.x'
Top menu redesign

See merge request allianceauth/allianceauth!1329
2021-08-20 04:04:23 +00:00
Ariel Rin
0f0c0441a9 Merge branch 'fix_logging_notifications' into 'master'
Fix logging notifications

See merge request allianceauth/allianceauth!1332
2021-08-20 04:03:48 +00:00
Erik Kalkoken
a0db8e8e2c Fix logging notifications 2021-08-20 04:03:48 +00:00
Peter Pfeufer
641356f31d css properly formatted 2021-08-18 07:40:47 +02:00
Peter Pfeufer
191b238a04 using "public/lang_select.html" 2021-08-18 07:39:36 +02:00
Joel Falknau
f6936c5f33 Typo in token enable flag 2021-08-18 11:43:05 +10:00
Ariel Rin
e8f8cb8aa3 Merge branch 'fix-for-the-fix' into 'v2.9.x'
(Hotfix) Missing import added

See merge request allianceauth/allianceauth!1330
2021-08-18 01:39:15 +00:00
Peter Pfeufer
96170a668f missing import added 2021-08-17 23:47:15 +02:00
Peter Pfeufer
4a3e807066 menu item text for mobile view 2021-08-14 14:02:25 +02:00
Peter Pfeufer
ab369d9aac [FIX] text color 2021-08-14 13:58:01 +02:00
Peter Pfeufer
742864fe6c adding eve time to top menu 2021-08-11 16:19:13 +02:00
Peter Pfeufer
c3df1c4d1f adding user menu 2021-08-11 16:18:59 +02:00
Ariel Rin
63d7fb80e6 Merge branch 'srp_evetools_killboard' into 'v2.9.x'
Add EveTools kill board as accepted source for srp request kill mails

See merge request allianceauth/allianceauth!1326
2021-08-11 13:06:19 +00:00
Ariel Rin
a7f468efd1 Merge branch 'fix_group_creation' into 'master'
Fix Group Creation

Closes #1161

See merge request allianceauth/allianceauth!1327
2021-08-11 06:07:24 +00:00
Ariel Rin
9f4ab9540b Merge branch 'improve_notifications' into 'master'
Improve notifications

See merge request allianceauth/allianceauth!1324
2021-08-11 06:06:24 +00:00
Erik Kalkoken
1e133b7c5d Improve notifications 2021-08-11 06:06:23 +00:00
Ariel Rin
4aa7530bbc Merge branch 'fix_hasgroupleader' into 'master'
Fix `Has Leader` column for groups that have group leader groups.

See merge request allianceauth/allianceauth!1328
2021-08-11 06:06:13 +00:00
colcrunch
2e0ddf2e7a has_leader should return true when a group has a group_leader_group 2021-07-13 18:00:21 -04:00
colcrunch
e24bc2a05d Revert "Update tests."
This reverts commit 7c1d1074f9.
2021-07-13 11:35:19 -04:00
colcrunch
a8c0db3fd7 Revert "Update autogroups."
This reverts commit eaa1cde01a.
2021-07-13 11:35:06 -04:00
colcrunch
7b77a6cd40 Revert "Add authutil for creating groups with authgroups."
This reverts commit 15db817382.
2021-07-13 11:34:55 -04:00
colcrunch
b8b8e470f2 Revert "Update tests to use the create_group util."
This reverts commit 0897383e41.
2021-07-13 11:34:46 -04:00
colcrunch
ad92ea243d Revert "Fix missed test."
This reverts commit 37005b1c68.
2021-07-13 11:34:35 -04:00
colcrunch
489a8456f7 Revert "More test fixes."
This reverts commit 6c3650d9f2.
2021-07-13 11:34:19 -04:00
colcrunch
122e389c38 Add signals back. 2021-07-13 11:33:51 -04:00
colcrunch
8318add6d5 Update test_admin.py 2021-07-13 10:18:39 -04:00
colcrunch
6c3650d9f2 More test fixes. 2021-07-13 09:28:31 -04:00
colcrunch
37005b1c68 Fix missed test. 2021-07-13 09:18:53 -04:00
colcrunch
0897383e41 Update tests to use the create_group util. 2021-07-13 09:13:47 -04:00
colcrunch
15db817382 Add authutil for creating groups with authgroups. 2021-07-13 09:13:17 -04:00
colcrunch
eaa1cde01a Update autogroups. 2021-07-13 08:41:36 -04:00
colcrunch
7c1d1074f9 Update tests. 2021-07-13 08:41:25 -04:00
colcrunch
0f0f9b6062 Fix group creation ignoring AuthGroup settings. (Fixes #1161) 2021-07-13 08:09:40 -04:00
Peter Pfeufer
839232dc98 add evetools killboard as accepted source for srp requests 2021-07-04 15:04:25 +02:00
Ariel Rin
356df45583 Version Bump v2.9.0a4 2021-07-03 04:44:17 +00:00
Ariel Rin
6b78992f07 Merge branch 'googleanalytics' into 'v2.9.x'
Analytics Module

See merge request allianceauth/allianceauth!1286
2021-07-03 04:43:17 +00:00
Ariel Rin
5af33facb6 Analytics Module 2021-07-03 04:43:16 +00:00
Ariel Rin
a7b9bcf3e8 Merge branch 'bump-celery-version' into 'v2.9.x'
allow celery 5 since it is pulled in by celery-beat>2.2.0

See merge request allianceauth/allianceauth!1325
2021-07-03 04:25:54 +00:00
Ariel Rin
eafc6074c1 Merge branch 'purge_state_cache' into 'v2.9.x'
Refresh user profile on state change to force permission changes.

See merge request allianceauth/allianceauth!1296
2021-07-03 04:25:34 +00:00
Peter Pfeufer
99383a482b allow celery 5 since it is pulled in by celery-beat
This commit is so that celery 5 can be tested
2021-07-02 18:56:54 +02:00
Ariel Rin
6f2807cba2 Version Bump v2.8.6 2021-07-02 16:47:06 +00:00
Ariel Rin
39a40a8c43 Limit Django-Celery-Beat to 2.2.0 for Celery 4.x 2021-07-02 16:42:07 +00:00
Ariel Rin
31123f7e97 Merge branch 'v2.9.x' of https://gitlab.com/allianceauth/allianceauth into v2.9.x 2021-06-29 14:02:00 +10:00
Ariel Rin
96300a012a Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v2.9.x 2021-06-29 14:01:52 +10:00
Ariel Rin
5f98b5350e Version Bump v2.8.5 2021-06-29 03:27:30 +00:00
Ariel Rin
8874eceb5f Merge branch 'name-formatting' into 'v2.9.x'
Name formatter fixes

See merge request allianceauth/allianceauth!1321
2021-06-29 03:15:02 +00:00
Ariel Rin
9de4d557e3 Merge branch 'discord-existing' into 'master'
Update member when they are already in Discord server.

See merge request allianceauth/allianceauth!1322
2021-06-29 03:10:23 +00:00
Aaron Kable
1d5f2634f1 Update member when they are already in Discord server. 2021-06-29 03:10:23 +00:00
Ariel Rin
710d26d36d Merge branch 'message-icons-fix' into 'master'
[FIX] icons in alert messages

See merge request allianceauth/allianceauth!1323
2021-06-29 03:10:01 +00:00
Peter Pfeufer
47793e6198 [FIX] icons in alert messages
This way they don't interfere with possible HTML in the message itself.
2021-06-28 21:20:59 +02:00
Peter Pfeufer
3c799eb40b None is neither an allince, a corp or a character 2021-06-23 12:29:33 +02:00
Ariel Rin
0d7b8e315f tidy up missed pre-commits from master merge 2021-06-23 10:03:57 +10:00
Ariel Rin
ae7cfbff35 Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v2.9.x 2021-06-23 09:58:25 +10:00
Ariel Rin
a0a497ab33 Merge branch 'sanity-checks-and-editorconfig' into 'v2.9.x'
Basic code sanity checks and editorconfig

See merge request allianceauth/allianceauth!1316
2021-06-22 23:38:54 +00:00
Peter Pfeufer
fd86471a0f exluded .po, .mo and swagger.json from checks
For demonstration, the last empty line has been removed from swagger.json. Usually it would have been added again through pre-commit, but not with the exclusion here.
2021-06-22 16:42:10 +02:00
Ariel Rin
5fcb56a087 Merge branch 'notification_with_icons' into 'master'
Improve messages

See merge request allianceauth/allianceauth!1320
2021-06-21 10:39:56 +00:00
Erik Kalkoken
808080d5ee Improve messages 2021-06-21 10:39:56 +00:00
Ariel Rin
e6037f1680 Typo (Allince<Alliance) Credit @Thalimet 2021-06-21 10:23:40 +00:00
Ariel Rin
5c3ded6b07 Merge branch 'admin_performance_tuning' into 'master'
Admin performance tuning for group and state

See merge request allianceauth/allianceauth!1318
2021-06-01 11:40:32 +00:00
Erik Kalkoken
0c14e35d15 Admin performance tuning for group and state 2021-06-01 11:40:32 +00:00
Ariel Rin
c13be5d39a Merge branch 'ts3_update_button' into 'master'
Add button to update TS3 groups to admin page

See merge request allianceauth/allianceauth!1313
2021-06-01 11:38:24 +00:00
Erik Kalkoken
e4b515c1d5 Add button to update TS3 groups to admin page 2021-06-01 11:38:24 +00:00
Ariel Rin
65e2c87e8f Merge branch 'improve_eveonline_manager' into 'master'
Improve performance in EveCharacter manager

See merge request allianceauth/allianceauth!1314
2021-06-01 11:36:29 +00:00
Ariel Rin
68ce25854a Merge branch 'quick-syntax-fix-in-docu' into 'master'
quick fix in documentation

See merge request allianceauth/allianceauth!1317
2021-06-01 11:35:20 +00:00
Peter Pfeufer
1f80a02be9 version updated 2021-05-17 23:27:17 +02:00
Peter Pfeufer
3df6643513 this is a shell command ... 2021-05-17 17:41:28 +02:00
Peter Pfeufer
e6a4cea4de editorconfig applied 2021-05-17 11:42:28 +02:00
Peter Pfeufer
bad36a69e8 added editorconfig 2021-05-17 09:52:29 +02:00
Peter Pfeufer
2697fb5317 remove coding pragma. it's not needed since python 3.x 2021-05-17 09:51:09 +02:00
Peter Pfeufer
b47392ba7f check for LF as line ending 2021-05-17 09:49:58 +02:00
Peter Pfeufer
a99a375375 one and only one empty line at the end of the file 2021-05-17 09:48:57 +02:00
Peter Pfeufer
8c3df89d52 remove trailing whitespaces 2021-05-17 09:46:11 +02:00
Ariel Rin
6ea0ebc9f9 Merge branch 'db-install-docs-update' into 'master'
Added a quick hint that it's ok to leave the SQL shell, and some commas

See merge request allianceauth/allianceauth!1315
2021-05-16 02:38:43 +00:00
Peter Pfeufer
26236f5886 Added a quick hint that it's ok to leave the SQL shell, and some commas 2021-05-15 15:27:50 +02:00
ErikKalkoken
1420c71ec5 Improve get_character_by_id() 2021-05-15 13:58:23 +02:00
Ariel Rin
10bd77d761 Version Bump v2.9.0a2 2021-05-13 21:43:24 +10:00
Ariel Rin
192d286cf2 Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v2.9.x 2021-05-13 21:38:22 +10:00
Ariel Rin
8ab9d2d5ad Merge branch 'revert-to-autofield' into 'v2.9.x'
revert migrations & set DEFAULT_AUTO_FIELD to django.db.models.AutoField

See merge request allianceauth/allianceauth!1312
2021-05-10 11:53:47 +00:00
Peter Pfeufer
3630812b92 revert migrations & set DEFAULT_AUTO_FIELD to django.db.models.AutoField 2021-05-10 13:35:40 +02:00
Ariel Rin
2f320bc256 Version Bump 2.9.0a1 2021-05-09 06:45:17 +00:00
Ariel Rin
45b8d42b8e Merge branch 'javascript-modernization' into 'v2.9.x'
Cleanup and modernize JS

See merge request allianceauth/allianceauth!1305
2021-05-09 06:43:58 +00:00
Peter Pfeufer
bd2d19f867 Cleanup and modernize JS 2021-05-09 06:43:58 +00:00
Ariel Rin
0be404baca Merge branch 'py36remove' into 'v2.9.x'
Remove Python36 Testing and Support

See merge request allianceauth/allianceauth!1301
2021-05-09 06:42:56 +00:00
Ariel Rin
e6cee9ac83 Remove Python36 Testing and Support 2021-05-09 06:42:56 +00:00
Ariel Rin
d173a59441 Merge branch 'add-robots-txt' into 'v2.9.x'
Adding robots.txt to fix random 404 message

See merge request allianceauth/allianceauth!1302
2021-04-27 14:11:14 +00:00
Peter Pfeufer
8f59f2549a robots.txt added
This fixes the randomly appearing 404 error in Auth  (issue #1232)
2021-04-15 20:59:25 +02:00
Ariel Rin
630400fee4 Merge branch 'django-3-2-update' into 'v2.9.x'
Django 3.2 compatibility update

See merge request allianceauth/allianceauth!1300
2021-04-08 05:07:01 +00:00
Ariel Rin
1ec6929e91 Merge branch 'python39' into 'v2.9.x'
Add  Python3.9 Testing

See merge request allianceauth/allianceauth!1284
2021-04-08 04:49:15 +00:00
Ariel Rin
8c6bdd8ae2 Add Python3.9 Testing 2021-04-08 04:49:15 +00:00
Peter Pfeufer
e04138bced migrations 2021-04-07 18:40:39 +02:00
Peter Pfeufer
db5ad85811 add new default for PKs 2021-04-07 18:16:33 +02:00
Peter Pfeufer
5b44fd376d revert version cap on Django, so we get 3.2 again 2021-04-07 18:11:01 +02:00
Aaron Kable
b1b79d1245 Refresh user profile on state change to force permision changes. 2021-03-26 18:20:39 +08:00
394 changed files with 5062 additions and 3658 deletions

24
.editorconfig Normal file
View File

@@ -0,0 +1,24 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
tab_width = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{yaml,yml,less}]
indent_size = 2
[*.md]
indent_size = 2
# Makefiles always use tabs for indentation
[Makefile]
indent_style = tab
[*.bat]
indent_style = tab

View File

@@ -1,4 +1,5 @@
stages: stages:
- pre-commit
- gitlab - gitlab
- test - test
- deploy - deploy
@@ -13,6 +14,18 @@ before_script:
- python -V - python -V
- pip install wheel tox - pip install wheel tox
pre-commit-check:
stage: pre-commit
image: python:3.6-buster
variables:
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
cache:
paths:
- ${PRE_COMMIT_HOME}
script:
- pip install pre-commit
- pre-commit run --all-files
sast: sast:
stage: gitlab stage: gitlab
before_script: [] before_script: []
@@ -20,22 +33,13 @@ sast:
dependency_scanning: dependency_scanning:
stage: gitlab stage: gitlab
before_script: before_script:
- apt-get update && apt-get install redis-server libmariadbclient-dev -y - apt-get update && apt-get install redis-server libmariadb-dev -y
- redis-server --daemonize yes - redis-server --daemonize yes
- python -V - python -V
- pip install wheel tox - pip install wheel tox
test-3.6-core:
image: python:3.6-buster
script:
- tox -e py36-core
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.7-core: test-3.7-core:
image: python:3.7-buster image: python:3.7-bullseye
script: script:
- tox -e py37-core - tox -e py37-core
artifacts: artifacts:
@@ -44,7 +48,7 @@ test-3.7-core:
cobertura: coverage.xml cobertura: coverage.xml
test-3.8-core: test-3.8-core:
image: python:3.8-buster image: python:3.8-bullseye
script: script:
- tox -e py38-core - tox -e py38-core
artifacts: artifacts:
@@ -52,17 +56,17 @@ test-3.8-core:
reports: reports:
cobertura: coverage.xml cobertura: coverage.xml
test-3.6-all: test-3.9-core:
image: python:3.6-buster image: python:3.9-bullseye
script: script:
- tox -e py36-all - tox -e py39-core
artifacts: artifacts:
when: always when: always
reports: reports:
cobertura: coverage.xml cobertura: coverage.xml
test-3.7-all: test-3.7-all:
image: python:3.7-buster image: python:3.7-bullseye
script: script:
- tox -e py37-all - tox -e py37-all
artifacts: artifacts:
@@ -71,7 +75,7 @@ test-3.7-all:
cobertura: coverage.xml cobertura: coverage.xml
test-3.8-all: test-3.8-all:
image: python:3.8-buster image: python:3.8-bullseye
script: script:
- tox -e py38-all - tox -e py38-all
artifacts: artifacts:
@@ -79,9 +83,18 @@ test-3.8-all:
reports: reports:
cobertura: coverage.xml cobertura: coverage.xml
test-3.9-all:
image: python:3.9-bullseye
script:
- tox -e py39-all
artifacts:
when: always
reports:
cobertura: coverage.xml
deploy_production: deploy_production:
stage: deploy stage: deploy
image: python:3.8-buster image: python:3.9-bullseye
before_script: before_script:
- pip install twine wheel - pip install twine wheel

28
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,28 @@
# Apply to all files without committing:
# pre-commit run --all-files
# Update this file:
# pre-commit autoupdate
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: check-case-conflict
- id: check-json
- id: check-xml
- id: check-yaml
- id: fix-byte-order-marker
- id: trailing-whitespace
exclude: (\.min\.css|\.min\.js|\.mo|\.po|swagger\.json)$
- id: end-of-file-fixer
exclude: (\.min\.css|\.min\.js|\.mo|\.po|swagger\.json)$
- id: mixed-line-ending
args: [ '--fix=lf' ]
- id: fix-encoding-pragma
args: [ '--remove' ]
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
rev: 2.3.5
hooks:
- id: editorconfig-checker
exclude: ^(LICENSE|allianceauth\/static\/css\/themes\/bootstrap-locals.less|allianceauth\/eveonline\/swagger.json|(.*.po)|(.*.mo))

View File

@@ -337,4 +337,3 @@ proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. Public License instead of this License.

View File

@@ -1,7 +1,7 @@
# This will make sure the app is always imported when # This will make sure the app is always imported when
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.
__version__ = '2.8.4' __version__ = '2.9.0b1'
__title__ = 'Alliance Auth' __title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth' __url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = '%s v%s' % (__title__, __version__) NAME = '%s v%s' % (__title__, __version__)

View File

@@ -0,0 +1 @@
default_app_config = 'allianceauth.analytics.apps.AnalyticsConfig'

View File

@@ -0,0 +1,21 @@
from django.contrib import admin
from .models import AnalyticsIdentifier, AnalyticsPath, AnalyticsTokens
@admin.register(AnalyticsIdentifier)
class AnalyticsIdentifierAdmin(admin.ModelAdmin):
search_fields = ['identifier', ]
list_display = ('identifier',)
@admin.register(AnalyticsTokens)
class AnalyticsTokensAdmin(admin.ModelAdmin):
search_fields = ['name', ]
list_display = ('name', 'type',)
@admin.register(AnalyticsPath)
class AnalyticsPathAdmin(admin.ModelAdmin):
search_fields = ['ignore_path', ]
list_display = ('ignore_path',)

View File

@@ -0,0 +1,9 @@
from django.apps import AppConfig
class AnalyticsConfig(AppConfig):
name = 'allianceauth.analytics'
label = 'analytics'
def ready(self):
import allianceauth.analytics.signals

View File

@@ -0,0 +1,21 @@
[
{
"model": "analytics.AnalyticsTokens",
"pk": 1,
"fields": {
"name": "AA Team Public Google Analytics (Universal)",
"type": "GA-V4",
"token": "UA-186249766-2",
"send_page_views": "False",
"send_celery_tasks": "False",
"send_stats": "False"
}
},
{
"model": "analytics.AnalyticsIdentifier",
"pk": 1,
"fields": {
"identifier": "ab33e241fbf042b6aa77c7655a768af7"
}
}
]

View File

@@ -0,0 +1,41 @@
from bs4 import BeautifulSoup
from django.utils.deprecation import MiddlewareMixin
from .models import AnalyticsTokens, AnalyticsIdentifier
from .tasks import send_ga_tracking_web_view
class AnalyticsMiddleware(MiddlewareMixin):
def process_response(self, request, response):
"""Django Middleware: Process Page Views and creates Analytics Celery Tasks"""
analyticstokens = AnalyticsTokens.objects.all()
client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex
try:
title = BeautifulSoup(
response.content, "html.parser").html.head.title.text
except AttributeError:
title = ''
for token in analyticstokens:
# Check if Page View Sending is Disabled
if token.send_page_views is False:
continue
# Check Exclusions
if request.path in token.ignore_paths.all():
continue
tracking_id = token.token
locale = request.LANGUAGE_CODE
path = request.path
try:
useragent = request.headers["User-Agent"]
except KeyError:
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
send_ga_tracking_web_view.s(tracking_id=tracking_id,
client_id=client_id,
page=path,
title=title,
locale=locale,
useragent=useragent).\
apply_async(priority=9)
return response

View File

@@ -0,0 +1,42 @@
# Generated by Django 3.1.4 on 2020-12-30 13:11
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='AnalyticsIdentifier',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identifier', models.UUIDField(default=uuid.uuid4, editable=False)),
],
),
migrations.CreateModel(
name='AnalyticsPath',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ignore_path', models.CharField(default='/example/', max_length=254)),
],
),
migrations.CreateModel(
name='AnalyticsTokens',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=254)),
('type', models.CharField(choices=[('GA-U', 'Google Analytics Universal'), ('GA-V4', 'Google Analytics V4')], max_length=254)),
('token', models.CharField(max_length=254)),
('send_page_views', models.BooleanField(default=False)),
('send_celery_tasks', models.BooleanField(default=False)),
('send_stats', models.BooleanField(default=False)),
('ignore_paths', models.ManyToManyField(blank=True, to='analytics.AnalyticsPath')),
],
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 3.1.4 on 2020-12-30 08:53
from django.db import migrations
def add_aa_team_token(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens()
token.type = 'GA-U'
token.token = 'UA-186249766-2'
token.send_page_views = True
token.send_celery_tasks = True
token.send_stats = True
token.name = 'AA Team Public Google Analytics (Universal)'
token.save()
def remove_aa_team_token(apps, schema_editor):
# Have to define some code to remove this identifier
# In case of migration rollback?
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens.objects.filter(token="UA-186249766-2").delete()
class Migration(migrations.Migration):
dependencies = [
('analytics', '0001_initial'),
]
operations = [migrations.RunPython(add_aa_team_token, remove_aa_team_token)
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 3.1.4 on 2020-12-30 08:53
from uuid import uuid4
from django.db import migrations
def generate_identifier(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
Identifier = apps.get_model('analytics', 'AnalyticsIdentifier')
identifier = Identifier()
identifier.id = 1
identifier.save()
def zero_identifier(apps, schema_editor):
# Have to define some code to remove this identifier
# In case of migration rollback?
Identifier = apps.get_model('analytics', 'AnalyticsIdentifier')
Identifier.objects.filter(id=1).delete()
class Migration(migrations.Migration):
dependencies = [
('analytics', '0002_add_AA_Team_Token'),
]
operations = [migrations.RunPython(generate_identifier, zero_identifier)
]

View File

@@ -0,0 +1,38 @@
from django.db import models
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from uuid import uuid4
class AnalyticsIdentifier(models.Model):
identifier = models.UUIDField(default=uuid4,
editable=False)
def save(self, *args, **kwargs):
if not self.pk and AnalyticsIdentifier.objects.exists():
# Force a single object
raise ValidationError('There is can be only one \
AnalyticsIdentifier instance')
self.pk = self.id = 1 # If this happens to be deleted and recreated, force it to be 1
return super(AnalyticsIdentifier, self).save(*args, **kwargs)
class AnalyticsPath(models.Model):
ignore_path = models.CharField(max_length=254, default="/example/")
class AnalyticsTokens(models.Model):
class Analytics_Type(models.TextChoices):
GA_U = 'GA-U', _('Google Analytics Universal')
GA_V4 = 'GA-V4', _('Google Analytics V4')
name = models.CharField(max_length=254)
type = models.CharField(max_length=254, choices=Analytics_Type.choices)
token = models.CharField(max_length=254, blank=False)
send_page_views = models.BooleanField(default=False)
send_celery_tasks = models.BooleanField(default=False)
send_stats = models.BooleanField(default=False)
ignore_paths = models.ManyToManyField(AnalyticsPath, blank=True)

View File

@@ -0,0 +1,50 @@
from allianceauth.analytics.tasks import analytics_event
from celery.signals import task_failure, task_success
import logging
logger = logging.getLogger(__name__)
@task_failure.connect
def process_failure_signal(
exception, traceback,
sender, task_id, signal,
args, kwargs, einfo, **kw):
logger.debug("Celery task_failure signal %s" % sender.__class__.__name__)
category = sender.__module__
if 'allianceauth.analytics' not in category:
if category.endswith(".tasks"):
category = category[:-6]
action = sender.__name__
label = f"{exception.__class__.__name__}: {str(exception)}"
analytics_event(category=category,
action=action,
label=label)
@task_success.connect
def celery_success_signal(sender, result=None, **kw):
logger.debug("Celery task_success signal %s" % sender.__class__.__name__)
category = sender.__module__
if 'allianceauth.analytics' not in category:
if category.endswith(".tasks"):
category = category[:-6]
action = sender.__name__
label = "Success"
value = 0
if isinstance(result, int):
value = result
analytics_event(category=category,
action=action,
label=label,
value=value)

View File

@@ -0,0 +1,207 @@
import requests
import logging
from django.conf import settings
from django.apps import apps
from celery import shared_task
from allianceauth import __version__
from .models import AnalyticsTokens, AnalyticsIdentifier
from .utils import (
install_stat_addons,
install_stat_tokens,
install_stat_users)
logger = logging.getLogger(__name__)
BASE_URL = "https://www.google-analytics.com/"
DEBUG_URL = f"{BASE_URL}debug/collect"
COLLECTION_URL = f"{BASE_URL}collect"
if getattr(settings, "ANALYTICS_ENABLE_DEBUG", False) and settings.DEBUG:
# Force sending of analytics data during in a debug/test environemt
# Usefull for developers working on this feature.
logger.warning(
"You have 'ANALYTICS_ENABLE_DEBUG' Enabled! "
"This debug instance will send analytics data!")
DEBUG_URL = COLLECTION_URL
ANALYTICS_URL = COLLECTION_URL
if settings.DEBUG is True:
ANALYTICS_URL = DEBUG_URL
def analytics_event(category: str,
action: str,
label: str,
value: int = 0,
event_type: str = 'Celery'):
"""
Send a Google Analytics Event for each token stored
Includes check for if its enabled/disabled
Parameters
-------
`category` (str): Celery Namespace
`action` (str): Task Name
`label` (str): Optional, Task Success/Exception
`value` (int): Optional, If bulk, Query size, can be a binary True/False
`event_type` (str): Optional, Celery or Stats only, Default to Celery
"""
analyticstokens = AnalyticsTokens.objects.all()
client_id = AnalyticsIdentifier.objects.get(id=1).identifier.hex
for token in analyticstokens:
if event_type == 'Celery':
allowed = token.send_celery_tasks
elif event_type == 'Stats':
allowed = token.send_stats
else:
allowed = False
if allowed is True:
tracking_id = token.token
send_ga_tracking_celery_event.s(tracking_id=tracking_id,
client_id=client_id,
category=category,
action=action,
label=label,
value=value).\
apply_async(priority=9)
@shared_task()
def analytics_daily_stats():
"""Celery Task: Do not call directly
Gathers a series of daily statistics and sends analytics events containing them"""
users = install_stat_users()
tokens = install_stat_tokens()
addons = install_stat_addons()
logger.debug("Running Daily Analytics Upload")
analytics_event(category='allianceauth.analytics',
action='send_install_stats',
label='existence',
value=1,
event_type='Stats')
analytics_event(category='allianceauth.analytics',
action='send_install_stats',
label='users',
value=users,
event_type='Stats')
analytics_event(category='allianceauth.analytics',
action='send_install_stats',
label='tokens',
value=tokens,
event_type='Stats')
analytics_event(category='allianceauth.analytics',
action='send_install_stats',
label='addons',
value=addons,
event_type='Stats')
for appconfig in apps.get_app_configs():
analytics_event(category='allianceauth.analytics',
action='send_extension_stats',
label=appconfig.label,
value=1,
event_type='Stats')
@shared_task()
def send_ga_tracking_web_view(
tracking_id: str,
client_id: str,
page: str,
title: str,
locale: str,
useragent: str) -> requests.Response:
"""Celery Task: Do not call directly
Sends Page View events to GA, Called only via analytics.middleware
Parameters
----------
`tracking_id` (str): Unique Server Identifier
`client_id` (str): GA Token
`page` (str): Page Path
`title` (str): Page Title
`locale` (str): Browser Language
`useragent` (str): Browser UserAgent
Returns
-------
requests.Reponse Object
"""
headers = {"User-Agent": useragent}
payload = {
'v': '1',
'tid': tracking_id,
'cid': client_id,
't': 'pageview',
'dp': page,
'dt': title,
'ul': locale,
'ua': useragent,
'aip': 1,
'an': "allianceauth",
'av': __version__
}
response = requests.post(
ANALYTICS_URL, data=payload,
timeout=5, headers=headers)
logger.debug(f"Analytics Page View HTTP{response.status_code}")
return response
@shared_task()
def send_ga_tracking_celery_event(
tracking_id: str,
client_id: str,
category: str,
action: str,
label: str,
value: int) -> requests.Response:
"""Celery Task: Do not call directly
Sends Page View events to GA, Called only via analytics.middleware
Parameters
----------
`tracking_id` (str): Unique Server Identifier
`client_id` (str): GA Token
`category` (str): Celery Namespace
`action` (str): Task Name
`label` (str): Optional, Task Success/Exception
`value` (int): Optional, If bulk, Query size, can be a binary True/False
Returns
-------
requests.Reponse Object
"""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"}
payload = {
'v': '1',
'tid': tracking_id,
'cid': client_id,
't': 'event',
'ec': category,
'ea': action,
'el': label,
'ev': value,
'aip': 1,
'an': "allianceauth",
'av': __version__
}
response = requests.post(
ANALYTICS_URL, data=payload,
timeout=5, headers=headers)
logger.debug(f"Analytics Celery/Stats Event HTTP{response.status_code}")
return response

View File

View File

@@ -0,0 +1,23 @@
from allianceauth.analytics.middleware import AnalyticsMiddleware
from unittest.mock import Mock
from django.test.testcases import TestCase
class TestAnalyticsMiddleware(TestCase):
def setUp(self):
self.middleware = AnalyticsMiddleware()
self.request = Mock()
self.request.headers = {
"User-Agent": "AUTOMATED TEST"
}
self.request.path = '/testURL/'
self.request.session = {}
self.request.LANGUAGE_CODE = 'en'
self.response = Mock()
self.response.content = 'hello world'
def test_middleware(self):
response = self.middleware.process_response(self.request, self.response)
self.assertEqual(self.response, response)

View File

@@ -0,0 +1,26 @@
from allianceauth.analytics.models import AnalyticsIdentifier
from django.core.exceptions import ValidationError
from django.test.testcases import TestCase
from uuid import UUID, uuid4
# Identifiers
uuid_1 = "ab33e241fbf042b6aa77c7655a768af7"
uuid_2 = "7aa6bd70701f44729af5e3095ff4b55c"
class TestAnalyticsIdentifier(TestCase):
def test_identifier_random(self):
self.assertNotEqual(AnalyticsIdentifier.objects.get(), uuid4)
def test_identifier_singular(self):
AnalyticsIdentifier.objects.all().delete()
AnalyticsIdentifier.objects.create(identifier=uuid_1)
# Yeah i have multiple asserts here, they all do the same thing
with self.assertRaises(ValidationError):
AnalyticsIdentifier.objects.create(identifier=uuid_2)
self.assertEqual(AnalyticsIdentifier.objects.count(), 1)
self.assertEqual(AnalyticsIdentifier.objects.get(pk=1).identifier, UUID(uuid_1))

View File

@@ -0,0 +1,119 @@
from allianceauth.analytics.tasks import (
analytics_event,
send_ga_tracking_celery_event,
send_ga_tracking_web_view)
from django.test.testcases import TestCase
class TestAnalyticsTasks(TestCase):
def test_analytics_event(self):
analytics_event(
category='allianceauth.analytics',
action='send_tests',
label='test',
value=1,
event_type='Stats')
def test_send_ga_tracking_web_view_sent(self):
# This test sends if the event SENDS to google
# Not if it was successful
tracking_id = 'UA-186249766-2'
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
page = '/index/'
title = 'Hello World'
locale = 'en'
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
response = send_ga_tracking_web_view(
tracking_id,
client_id,
page,
title,
locale,
useragent)
self.assertEqual(response.status_code, 200)
def test_send_ga_tracking_web_view_success(self):
tracking_id = 'UA-186249766-2'
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
page = '/index/'
title = 'Hello World'
locale = 'en'
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
json_response = send_ga_tracking_web_view(
tracking_id,
client_id,
page,
title,
locale,
useragent).json()
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
def test_send_ga_tracking_web_view_invalid_token(self):
tracking_id = 'UA-IntentionallyBadTrackingID-2'
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
page = '/index/'
title = 'Hello World'
locale = 'en'
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
json_response = send_ga_tracking_web_view(
tracking_id,
client_id,
page,
title,
locale,
useragent).json()
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
self.assertEqual(json_response["hitParsingResult"][0]["parserMessage"][1]["description"], "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.")
# [{'valid': False, 'parserMessage': [{'messageType': 'INFO', 'description': 'IP Address from this hit was anonymized to 1.132.110.0.', 'messageCode': 'VALUE_MODIFIED'}, {'messageType': 'ERROR', 'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.", 'messageCode': 'VALUE_INVALID', 'parameter': 'tid'}], 'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'}]
def test_send_ga_tracking_celery_event_sent(self):
tracking_id = 'UA-186249766-2'
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
category = 'test'
action = 'test'
label = 'test'
value = '1'
response = send_ga_tracking_celery_event(
tracking_id,
client_id,
category,
action,
label,
value)
self.assertEqual(response.status_code, 200)
def test_send_ga_tracking_celery_event_success(self):
tracking_id = 'UA-186249766-2'
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
category = 'test'
action = 'test'
label = 'test'
value = '1'
json_response = send_ga_tracking_celery_event(
tracking_id,
client_id,
category,
action,
label,
value).json()
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
def test_send_ga_tracking_celery_event_invalid_token(self):
tracking_id = 'UA-IntentionallyBadTrackingID-2'
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
category = 'test'
action = 'test'
label = 'test'
value = '1'
json_response = send_ga_tracking_celery_event(
tracking_id,
client_id,
category,
action,
label,
value).json()
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
self.assertEqual(json_response["hitParsingResult"][0]["parserMessage"][1]["description"], "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.")
# [{'valid': False, 'parserMessage': [{'messageType': 'INFO', 'description': 'IP Address from this hit was anonymized to 1.132.110.0.', 'messageCode': 'VALUE_MODIFIED'}, {'messageType': 'ERROR', 'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.", 'messageCode': 'VALUE_INVALID', 'parameter': 'tid'}], 'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'}]

View File

@@ -0,0 +1,55 @@
from django.apps import apps
from allianceauth.authentication.models import User
from esi.models import Token
from allianceauth.analytics.utils import install_stat_users, install_stat_tokens, install_stat_addons
from django.test.testcases import TestCase
def create_testdata():
User.objects.all().delete()
User.objects.create_user(
'user_1'
'abc@example.com',
'password'
)
User.objects.create_user(
'user_2'
'abc@example.com',
'password'
)
#Token.objects.all().delete()
#Token.objects.create(
# character_id=101,
# character_name='character1',
# access_token='my_access_token'
#)
#Token.objects.create(
# character_id=102,
# character_name='character2',
# access_token='my_access_token'
#)
class TestAnalyticsUtils(TestCase):
def test_install_stat_users(self):
create_testdata()
expected = 2
users = install_stat_users()
self.assertEqual(users, expected)
#def test_install_stat_tokens(self):
# create_testdata()
# expected = 2
#
# tokens = install_stat_tokens()
# self.assertEqual(tokens, expected)
def test_install_stat_addons(self):
# this test does what its testing...
# but helpful for existing as a sanity check
expected = len(list(apps.get_app_configs()))
addons = install_stat_addons()
self.assertEqual(addons, expected)

View File

@@ -0,0 +1,36 @@
from django.apps import apps
from allianceauth.authentication.models import User
from esi.models import Token
def install_stat_users() -> int:
"""Count and Return the number of User accounts
Returns
-------
int
The Number of User objects"""
users = User.objects.count()
return users
def install_stat_tokens() -> int:
"""Count and Return the number of ESI Tokens Stored
Returns
-------
int
The Number of Token Objects"""
tokens = Token.objects.count()
return tokens
def install_stat_addons() -> int:
"""Count and Return the number of Django Applications Installed
Returns
-------
int
The Number of Installed Apps"""
addons = len(list(apps.get_app_configs()))
return addons

View File

@@ -448,6 +448,8 @@ class StateAdmin(admin.ModelAdmin):
elif db_field.name == "member_alliances": elif db_field.name == "member_alliances":
kwargs["queryset"] = EveAllianceInfo.objects.all()\ kwargs["queryset"] = EveAllianceInfo.objects.all()\
.order_by(Lower('alliance_name')) .order_by(Lower('alliance_name'))
elif db_field.name == "permissions":
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
return super().formfield_for_manytomany(db_field, request, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs)
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):

View File

@@ -43,4 +43,3 @@ AUTHENTICATION_ADMIN_USERS_MAX_GROUPS = \
AUTHENTICATION_ADMIN_USERS_MAX_CHARS = \ AUTHENTICATION_ADMIN_USERS_MAX_CHARS = \
_clean_setting('AUTHENTICATION_ADMIN_USERS_MAX_CHARS', 5) _clean_setting('AUTHENTICATION_ADMIN_USERS_MAX_CHARS', 5)

View File

@@ -23,8 +23,7 @@ class CharacterOwnershipManager(Manager):
def create_by_token(self, token): def create_by_token(self, token):
if not EveCharacter.objects.filter(character_id=token.character_id).exists(): if not EveCharacter.objects.filter(character_id=token.character_id).exists():
EveCharacter.objects.create_character(token.character_id) EveCharacter.objects.create_character(token.character_id)
return self.create(character=EveCharacter.objects.get(character_id=token.character_id), user=token.user, return self.create(character=EveCharacter.objects.get(character_id=token.character_id), user=token.user, owner_hash=token.character_owner_hash)
owner_hash=token.character_owner_hash)
class StateQuerySet(QuerySet): class StateQuerySet(QuerySet):

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-05 21:38 # Generated by Django 1.10.1 on 2016-09-05 21:38
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-07 19:14 # Generated by Django 1.10.1 on 2016-09-07 19:14
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 20:29 # Generated by Django 1.10.1 on 2016-09-09 20:29
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 23:19 # Generated by Django 1.10.1 on 2016-09-09 23:19
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 23:11 # Generated by Django 1.10.1 on 2016-09-09 23:11
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-10 05:42 # Generated by Django 1.10.1 on 2016-09-10 05:42
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-10 21:50 # Generated by Django 1.10.1 on 2016-09-10 21:50
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-12 13:04 # Generated by Django 1.10.1 on 2016-09-12 13:04
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-10-21 02:28 # Generated by Django 1.10.2 on 2016-10-21 02:28
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2017-01-07 06:47 # Generated by Django 1.10.1 on 2017-01-07 06:47
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2017-01-07 07:11 # Generated by Django 1.10.1 on 2017-01-07 07:11
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-01-12 00:59 # Generated by Django 1.10.5 on 2017-01-12 00:59
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-12-11 23:14 # Generated by Django 1.10.2 on 2016-12-11 23:14
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 23:19 # Generated by Django 1.10.1 on 2016-09-09 23:19
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-22 23:09 # Generated by Django 1.10.5 on 2017-03-22 23:09
from __future__ import unicode_literals from __future__ import unicode_literals
@@ -107,8 +106,8 @@ def populate_ownerships(apps, schema_editor):
EveCharacter = apps.get_model('eveonline', 'EveCharacter') EveCharacter = apps.get_model('eveonline', 'EveCharacter')
unique_character_owners = [t['character_id'] for t in unique_character_owners = [t['character_id'] for t in
Token.objects.all().values('character_id').annotate(n=models.Count('user')) if Token.objects.all().values('character_id').annotate(n=models.Count('user')) if
t['n'] == 1 and EveCharacter.objects.filter(character_id=t['character_id']).exists()] t['n'] == 1 and EveCharacter.objects.filter(character_id=t['character_id']).exists()]
tokens = Token.objects.filter(character_id__in=unique_character_owners) tokens = Token.objects.filter(character_id__in=unique_character_owners)
for c_id in unique_character_owners: for c_id in unique_character_owners:
@@ -171,8 +170,7 @@ def recreate_authservicesinfo(apps, schema_editor):
# repopulate main characters # repopulate main characters
for profile in UserProfile.objects.exclude(main_character__isnull=True).select_related('user', 'main_character'): for profile in UserProfile.objects.exclude(main_character__isnull=True).select_related('user', 'main_character'):
AuthServicesInfo.objects.update_or_create(user=profile.user, AuthServicesInfo.objects.update_or_create(user=profile.user, defaults={'main_char_id': profile.main_character.character_id})
defaults={'main_char_id': profile.main_character.character_id})
# repopulate states we understand # repopulate states we understand
for profile in UserProfile.objects.exclude(state__name='Guest').filter( for profile in UserProfile.objects.exclude(state__name='Guest').filter(

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations from django.db import migrations

View File

@@ -14,15 +14,11 @@ logger = logging.getLogger(__name__)
class State(models.Model): class State(models.Model):
name = models.CharField(max_length=20, unique=True) name = models.CharField(max_length=20, unique=True)
permissions = models.ManyToManyField(Permission, blank=True) permissions = models.ManyToManyField(Permission, blank=True)
priority = models.IntegerField(unique=True, priority = models.IntegerField(unique=True, help_text="Users get assigned the state with the highest priority available to them.")
help_text="Users get assigned the state with the highest priority available to them.")
member_characters = models.ManyToManyField(EveCharacter, blank=True, member_characters = models.ManyToManyField(EveCharacter, blank=True, help_text="Characters to which this state is available.")
help_text="Characters to which this state is available.") member_corporations = models.ManyToManyField(EveCorporationInfo, blank=True, help_text="Corporations to whose members this state is available.")
member_corporations = models.ManyToManyField(EveCorporationInfo, blank=True, member_alliances = models.ManyToManyField(EveAllianceInfo, blank=True, help_text="Alliances to whose members this state is available.")
help_text="Corporations to whose members this state is available.")
member_alliances = models.ManyToManyField(EveAllianceInfo, blank=True,
help_text="Alliances to whose members this state is available.")
public = models.BooleanField(default=False, help_text="Make this state available to any character.") public = models.BooleanField(default=False, help_text="Make this state available to any character.")
objects = StateManager() objects = StateManager()
@@ -81,6 +77,11 @@ class UserProfile(models.Model):
'info' 'info'
) )
from allianceauth.authentication.signals import state_changed from allianceauth.authentication.signals import state_changed
# We need to ensure we get up to date perms here as they will have just changed.
# Clear all attribute caches and reload the model that will get passed to the signals!
self.refresh_from_db()
state_changed.send( state_changed.send(
sender=self.__class__, user=self.user, state=self.state sender=self.__class__, user=self.user, state=self.state
) )

View File

@@ -75,8 +75,7 @@ def create_required_models(sender, instance, created, *args, **kwargs):
@receiver(post_save, sender=Token) @receiver(post_save, sender=Token)
def record_character_ownership(sender, instance, created, *args, **kwargs): def record_character_ownership(sender, instance, created, *args, **kwargs):
if created: if created:
logger.debug('New token for {0} character {1} saved. Evaluating ownership.'.format(instance.user, logger.debug('New token for {0} character {1} saved. Evaluating ownership.'.format(instance.user, instance.character_name))
instance.character_name))
if instance.user: if instance.user:
query = Q(owner_hash=instance.character_owner_hash) & Q(user=instance.user) query = Q(owner_hash=instance.character_owner_hash) & Q(user=instance.user)
else: else:
@@ -85,18 +84,14 @@ def record_character_ownership(sender, instance, created, *args, **kwargs):
CharacterOwnership.objects.filter(character__character_id=instance.character_id).exclude(query).delete() CharacterOwnership.objects.filter(character__character_id=instance.character_id).exclude(query).delete()
# create character if needed # create character if needed
if EveCharacter.objects.filter(character_id=instance.character_id).exists() is False: if EveCharacter.objects.filter(character_id=instance.character_id).exists() is False:
logger.debug('Token is for a new character. Creating model for {0} ({1})'.format(instance.character_name, logger.debug('Token is for a new character. Creating model for {0} ({1})'.format(instance.character_name, instance.character_id))
instance.character_id))
EveCharacter.objects.create_character(instance.character_id) EveCharacter.objects.create_character(instance.character_id)
char = EveCharacter.objects.get(character_id=instance.character_id) char = EveCharacter.objects.get(character_id=instance.character_id)
# check if we need to create ownership # check if we need to create ownership
if instance.user and not CharacterOwnership.objects.filter( if instance.user and not CharacterOwnership.objects.filter(
character__character_id=instance.character_id).exists(): character__character_id=instance.character_id).exists():
logger.debug("Character {0} is not yet owned. Assigning ownership to {1}".format(instance.character_name, logger.debug("Character {0} is not yet owned. Assigning ownership to {1}".format(instance.character_name, instance.user))
instance.user)) CharacterOwnership.objects.update_or_create(character=char, defaults={'owner_hash': instance.character_owner_hash, 'user': instance.user})
CharacterOwnership.objects.update_or_create(character=char,
defaults={'owner_hash': instance.character_owner_hash,
'user': instance.user})
@receiver(pre_delete, sender=CharacterOwnership) @receiver(pre_delete, sender=CharacterOwnership)

View File

@@ -138,7 +138,7 @@ class TestAuthenticate(TestCase):
def test_iterate_username(self): def test_iterate_username(self):
t = Token(character_id=self.unclaimed_character.character_id, t = Token(character_id=self.unclaimed_character.character_id,
character_name=self.unclaimed_character.character_name, character_owner_hash='3') character_name=self.unclaimed_character.character_name, character_owner_hash='3')
username = StateBackend().authenticate(token=t).username username = StateBackend().authenticate(token=t).username
t.character_owner_hash = '4' t.character_owner_hash = '4'
username_1 = StateBackend().authenticate(token=t).username username_1 = StateBackend().authenticate(token=t).username

View File

@@ -80,11 +80,11 @@ class StateTestCase(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True) cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1', AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1',
corp_name='Test Corp', alliance_name='Test Alliance') corp_name='Test Corp', alliance_name='Test Alliance')
cls.guest_state = get_guest_state() cls.guest_state = get_guest_state()
cls.test_character = EveCharacter.objects.get(character_id='1') cls.test_character = EveCharacter.objects.get(character_id='1')
cls.test_corporation = EveCorporationInfo.objects.create(corporation_id='1', corporation_name='Test Corp', cls.test_corporation = EveCorporationInfo.objects.create(corporation_id='1', corporation_name='Test Corp',
corporation_ticker='TEST', member_count=1) corporation_ticker='TEST', member_count=1)
cls.test_alliance = EveAllianceInfo.objects.create(alliance_id='1', alliance_name='Test Alliance', cls.test_alliance = EveAllianceInfo.objects.create(alliance_id='1', alliance_name='Test Alliance',
alliance_ticker='TEST', executor_corp_id='1') alliance_ticker='TEST', executor_corp_id='1')
cls.member_state = State.objects.create( cls.member_state = State.objects.create(
@@ -215,7 +215,7 @@ class CharacterOwnershipCheckTestCase(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True) cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1', AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1',
corp_name='Test Corp', alliance_name='Test Alliance') corp_name='Test Corp', alliance_name='Test Alliance')
cls.character = EveCharacter.objects.get(character_id=1) cls.character = EveCharacter.objects.get(character_id=1)
cls.token = Token.objects.create( cls.token = Token.objects.create(
user=cls.user, user=cls.user,

View File

@@ -186,7 +186,7 @@ class ActivationView(BaseActivationView):
def validate_key(self, activation_key): def validate_key(self, activation_key):
try: try:
dump = signing.loads(activation_key, salt=REGISTRATION_SALT, dump = signing.loads(activation_key, salt=REGISTRATION_SALT,
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400) max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400)
return dump return dump
except signing.BadSignature: except signing.BadSignature:
return None return None

View File

@@ -6,11 +6,13 @@ from allianceauth.corputils import urls
class CorpStats(MenuItemHook): class CorpStats(MenuItemHook):
def __init__(self): def __init__(self):
MenuItemHook.__init__(self, MenuItemHook.__init__(
_('Corporation Stats'), self,
'fas fa-share-alt fa-fw', _('Corporation Stats'),
'corputils:view', 'fas fa-share-alt fa-fw',
navactive=['corputils:']) 'corputils:view',
navactive=['corputils:']
)
def render(self, request): def render(self, request):
if request.user.has_perm('corputils.view_corp_corpstats') or request.user.has_perm( if request.user.has_perm('corputils.view_corp_corpstats') or request.user.has_perm(

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-12-14 21:36 # Generated by Django 1.10.1 on 2016-12-14 21:36
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-12-14 21:48 # Generated by Django 1.10.1 on 2016-12-14 21:48
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-22 23:35 # Generated by Django 1.10.5 on 2017-03-22 23:35
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-26 20:13 # Generated by Django 1.10.5 on 2017-03-26 20:13
from __future__ import unicode_literals from __future__ import unicode_literals
@@ -13,8 +12,7 @@ def convert_json_to_members(apps, schema_editor):
for cs in CorpStats.objects.all(): for cs in CorpStats.objects.all():
members = json.loads(cs._members) members = json.loads(cs._members)
CorpMember.objects.bulk_create( CorpMember.objects.bulk_create(
[CorpMember(corpstats=cs, character_id=member_id, character_name=member_name) for member_id, member_name in [CorpMember(corpstats=cs, character_id=member_id, character_name=member_name) for member_id, member_name in members.items()]
members.items()]
) )

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-06-10 15:34 # Generated by Django 1.11.2 on 2017-06-10 15:34
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -6,8 +6,7 @@ from bravado.exception import HTTPForbidden
from django.db import models from django.db import models
from esi.errors import TokenError from esi.errors import TokenError
from esi.models import Token from esi.models import Token
from allianceauth.eveonline.models import EveCorporationInfo, EveCharacter,\ from allianceauth.eveonline.models import EveCorporationInfo, EveCharacter, EveAllianceInfo
EveAllianceInfo
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.corputils.managers import CorpStatsManager from allianceauth.corputils.managers import CorpStatsManager
@@ -49,8 +48,7 @@ class CorpStats(models.Model):
def update(self): def update(self):
try: try:
c = self.token.get_esi_client(spec_file=SWAGGER_SPEC_PATH) c = self.token.get_esi_client(spec_file=SWAGGER_SPEC_PATH)
assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()[ assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()['corporation_id'] == int(self.corp.corporation_id)
'corporation_id'] == int(self.corp.corporation_id)
member_ids = c.Corporation.get_corporations_corporation_id_members( member_ids = c.Corporation.get_corporations_corporation_id_members(
corporation_id=self.corp.corporation_id).result() corporation_id=self.corp.corporation_id).result()
@@ -58,18 +56,15 @@ class CorpStats(models.Model):
# the swagger spec doesn't have a maxItems count # the swagger spec doesn't have a maxItems count
# manual testing says we can do over 350, but let's not risk it # manual testing says we can do over 350, but let's not risk it
member_id_chunks = [member_ids[i:i + 255] for i in range(0, len(member_ids), 255)] member_id_chunks = [member_ids[i:i + 255] for i in range(0, len(member_ids), 255)]
member_name_chunks = [c.Universe.post_universe_names(ids=id_chunk).result() for id_chunk in member_name_chunks = [c.Universe.post_universe_names(ids=id_chunk).result() for id_chunk in member_id_chunks]
member_id_chunks]
member_list = {} member_list = {}
for name_chunk in member_name_chunks: for name_chunk in member_name_chunks:
member_list.update({m['id']: m['name'] for m in name_chunk}) member_list.update({m['id']: m['name'] for m in name_chunk})
# bulk create new member models # bulk create new member models
missing_members = [m_id for m_id in member_ids if missing_members = [m_id for m_id in member_ids if not CorpMember.objects.filter(corpstats=self, character_id=m_id).exists()]
not CorpMember.objects.filter(corpstats=self, character_id=m_id).exists()]
CorpMember.objects.bulk_create( CorpMember.objects.bulk_create(
[CorpMember(character_id=m_id, character_name=member_list[m_id], corpstats=self) for m_id in [CorpMember(character_id=m_id, character_name=member_list[m_id], corpstats=self) for m_id in missing_members])
missing_members])
# purge old members # purge old members
self.members.exclude(character_id__in=member_ids).delete() self.members.exclude(character_id__in=member_ids).delete()
@@ -80,21 +75,22 @@ class CorpStats(models.Model):
except TokenError as e: except TokenError as e:
logger.warning("%s failed to update: %s" % (self, e)) logger.warning("%s failed to update: %s" % (self, e))
if self.token.user: if self.token.user:
notify(self.token.user, "%s failed to update with your ESI token." % self, notify(
message="Your token has expired or is no longer valid. Please add a new one to create a new CorpStats.", self.token.user, "%s failed to update with your ESI token." % self,
level="error") message="Your token has expired or is no longer valid. Please add a new one to create a new CorpStats.",
level="error")
self.delete() self.delete()
except HTTPForbidden as e: except HTTPForbidden as e:
logger.warning("%s failed to update: %s" % (self, e)) logger.warning("%s failed to update: %s" % (self, e))
if self.token.user: if self.token.user:
notify(self.token.user, "%s failed to update with your ESI token." % self, notify(self.token.user, "%s failed to update with your ESI token." % self, message="%s: %s" % (e.status_code, e.message), level="error")
message="%s: %s" % (e.status_code, e.message), level="error")
self.delete() self.delete()
except AssertionError: except AssertionError:
logger.warning("%s token character no longer in corp." % self) logger.warning("%s token character no longer in corp." % self)
if self.token.user: if self.token.user:
notify(self.token.user, "%s cannot update with your ESI token." % self, notify(
message="%s cannot update with your ESI token as you have left corp." % self, level="error") self.token.user, "%s cannot update with your ESI token." % self,
message="%s cannot update with your ESI token as you have left corp." % self, level="error")
self.delete() self.delete()
@property @property
@@ -127,9 +123,7 @@ class CorpStats(models.Model):
@property @property
def mains(self): def mains(self):
return self.members.filter(pk__in=[m.pk for m in self.members.all() if return self.members.filter(pk__in=[m.pk for m in self.members.all() if m.main_character and int(m.main_character.character_id) == int(m.character_id)])
m.main_character and int(m.main_character.character_id) == int(
m.character_id)])
def visible_to(self, user): def visible_to(self, user):
return CorpStats.objects.filter(pk=self.pk).visible_to(user).exists() return CorpStats.objects.filter(pk=self.pk).visible_to(user).exists()

View File

@@ -58,8 +58,7 @@
{% for id, main in mains.items %} {% for id, main in mains.items %}
<tr> <tr>
<td class="text-center" style="vertical-align:middle"> <td class="text-center" style="vertical-align:middle">
<div class="thumbnail" <div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
style="border: 0 none; box-shadow: none; background: transparent;">
<img src="{{ main.main.portrait_url_64 }}" class="img-circle"> <img src="{{ main.main.portrait_url_64 }}" class="img-circle">
<div class="caption text-center"> <div class="caption text-center">
{{ main.main }} {{ main.main }}
@@ -88,8 +87,7 @@
<td class="text-center" style="width:30%">{{ alt.corporation_name }}</td> <td class="text-center" style="width:30%">{{ alt.corporation_name }}</td>
<td class="text-center" style="width:30%">{{ alt.alliance_name }}</td> <td class="text-center" style="width:30%">{{ alt.alliance_name }}</td>
<td class="text-center" style="width:5%"> <td class="text-center" style="width:5%">
<a href="https://zkillboard.com/character/{{ alt.character_id }}/" <a href="https://zkillboard.com/character/{{ alt.character_id }}/" class="label label-danger" target="_blank">
class="label label-danger" target="_blank">
{% trans "Killboard" %} {% trans "Killboard" %}
</a> </a>
</td> </td>
@@ -123,10 +121,9 @@
<tr> <tr>
<td><img src="{{ member.portrait_url }}" class="img-circle"></td> <td><img src="{{ member.portrait_url }}" class="img-circle"></td>
<td class="text-center">{{ member }}</td> <td class="text-center">{{ member }}</td>
<td class="text-center"><a <td class="text-center">
href="https://zkillboard.com/character/{{ member.character_id }}/" <a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">{% trans "Killboard" %}</a>
class="label label-danger" </td>
target="_blank">{% trans "Killboard" %}</a></td>
<td class="text-center">{{ member.character_ownership.user.profile.main_character.character_name }}</td> <td class="text-center">{{ member.character_ownership.user.profile.main_character.character_name }}</td>
<td class="text-center">{{ member.character_ownership.user.profile.main_character.corporation_name }}</td> <td class="text-center">{{ member.character_ownership.user.profile.main_character.corporation_name }}</td>
<td class="text-center">{{ member.character_ownership.user.profile.main_character.alliance_name }}</td> <td class="text-center">{{ member.character_ownership.user.profile.main_character.alliance_name }}</td>
@@ -136,10 +133,9 @@
<tr class="danger"> <tr class="danger">
<td><img src="{{ member.portrait_url }}" class="img-circle"></td> <td><img src="{{ member.portrait_url }}" class="img-circle"></td>
<td class="text-center">{{ member.character_name }}</td> <td class="text-center">{{ member.character_name }}</td>
<td class="text-center"><a <td class="text-center">
href="https://zkillboard.com/character/{{ member.character_id }}/" <a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">{% trans "Killboard" %}</a>
class="label label-danger" </td>
target="_blank">{% trans "Killboard" %}</a></td>
<td class="text-center"></td> <td class="text-center"></td>
<td class="text-center"></td> <td class="text-center"></td>
<td class="text-center"></td> <td class="text-center"></td>
@@ -167,9 +163,7 @@
<td><img src="{{ member.portrait_url }}" class="img-circle"></td> <td><img src="{{ member.portrait_url }}" class="img-circle"></td>
<td class="text-center">{{ member.character_name }}</td> <td class="text-center">{{ member.character_name }}</td>
<td class="text-center"> <td class="text-center">
<a href="https://zkillboard.com/character/{{ member.character_id }}/" <a href="https://zkillboard.com/character/{{ member.character_id }}/" class="label label-danger" target="_blank">
class="label label-danger"
target="_blank">
{% trans "Killboard" %} {% trans "Killboard" %}
</a> </a>
</td> </td>

View File

@@ -96,7 +96,7 @@ def corpstats_view(request, corp_id=None):
character_ownership__user__profile__main_character__corporation_id=corpstats.corp.corporation_id) character_ownership__user__profile__main_character__corporation_id=corpstats.corp.corporation_id)
linked_chars = linked_chars.select_related('character_ownership', linked_chars = linked_chars.select_related('character_ownership',
'character_ownership__user__profile__main_character') \ 'character_ownership__user__profile__main_character') \
.prefetch_related('character_ownership__user__character_ownerships') \ .prefetch_related('character_ownership__user__character_ownerships') \
.prefetch_related('character_ownership__user__character_ownerships__character') .prefetch_related('character_ownership__user__character_ownerships__character')

View File

@@ -31,12 +31,11 @@ class AutogroupsConfigAdmin(admin.ModelAdmin):
def get_actions(self, request): def get_actions(self, request):
actions = super(AutogroupsConfigAdmin, self).get_actions(request) actions = super(AutogroupsConfigAdmin, self).get_actions(request)
actions['sync_user_groups'] = (sync_user_groups, actions['sync_user_groups'] = (sync_user_groups,
'sync_user_groups', 'sync_user_groups',
'Sync all users groups for this Autogroup Config') 'Sync all users groups for this Autogroup Config')
return actions return actions
admin.site.register(AutogroupsConfig, AutogroupsConfigAdmin) admin.site.register(AutogroupsConfig, AutogroupsConfigAdmin)
admin.site.register(ManagedCorpGroup) admin.site.register(ManagedCorpGroup)
admin.site.register(ManagedAllianceGroup) admin.site.register(ManagedAllianceGroup)

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-12-23 04:30 # Generated by Django 1.11.6 on 2017-12-23 04:30
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -57,25 +57,21 @@ class AutogroupsConfig(models.Model):
states = models.ManyToManyField(State, related_name='autogroups') states = models.ManyToManyField(State, related_name='autogroups')
corp_groups = models.BooleanField(default=False, corp_groups = models.BooleanField(default=False, help_text="Setting this to false will delete all the created groups.")
help_text="Setting this to false will delete all the created groups.")
corp_group_prefix = models.CharField(max_length=50, default='Corp ', blank=True) corp_group_prefix = models.CharField(max_length=50, default='Corp ', blank=True)
corp_name_source = models.CharField(max_length=20, choices=NAME_OPTIONS, default=OPT_NAME) corp_name_source = models.CharField(max_length=20, choices=NAME_OPTIONS, default=OPT_NAME)
alliance_groups = models.BooleanField(default=False, alliance_groups = models.BooleanField(default=False, help_text="Setting this to false will delete all the created groups.")
help_text="Setting this to false will delete all the created groups.")
alliance_group_prefix = models.CharField(max_length=50, default='Alliance ', blank=True) alliance_group_prefix = models.CharField(max_length=50, default='Alliance ', blank=True)
alliance_name_source = models.CharField(max_length=20, choices=NAME_OPTIONS, default=OPT_NAME) alliance_name_source = models.CharField(max_length=20, choices=NAME_OPTIONS, default=OPT_NAME)
corp_managed_groups = models.ManyToManyField( corp_managed_groups = models.ManyToManyField(
Group, through='ManagedCorpGroup', related_name='corp_managed_config', Group, through='ManagedCorpGroup', related_name='corp_managed_config',
help_text='A list of corporation groups created and maintained by this AutogroupConfig. ' help_text='A list of corporation groups created and maintained by this AutogroupConfig. You should not edit this list unless you know what you\'re doing.')
'You should not edit this list unless you know what you\'re doing.')
alliance_managed_groups = models.ManyToManyField( alliance_managed_groups = models.ManyToManyField(
Group, through='ManagedAllianceGroup', related_name='alliance_managed_config', Group, through='ManagedAllianceGroup', related_name='alliance_managed_config',
help_text='A list of alliance groups created and maintained by this AutogroupConfig. ' help_text='A list of alliance groups created and maintained by this AutogroupConfig. You should not edit this list unless you know what you\'re doing.')
'You should not edit this list unless you know what you\'re doing.')
replace_spaces = models.BooleanField(default=False) replace_spaces = models.BooleanField(default=False)
replace_spaces_with = models.CharField( replace_spaces_with = models.CharField(

View File

@@ -32,10 +32,12 @@ class EveCharacterManager(models.Manager):
def update_character(self, character_id): def update_character(self, character_id):
return self.get(character_id=character_id).update_character() return self.get(character_id=character_id).update_character()
def get_character_by_id(self, char_id): def get_character_by_id(self, character_id: int):
if self.filter(character_id=char_id).exists(): """Return character by character ID or None if not found."""
return self.get(character_id=char_id) try:
return None return self.get(character_id=character_id)
except self.model.DoesNotExist:
return None
class EveAllianceProviderManager: class EveAllianceProviderManager:

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-05 21:39 # Generated by Django 1.10.1 on 2016-09-05 21:39
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-10 20:20 # Generated by Django 1.10.1 on 2016-09-10 20:20
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-10-26 01:49 # Generated by Django 1.10.2 on 2016-10-26 01:49
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-11-01 04:20 # Generated by Django 1.10.2 on 2016-11-01 04:20
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-12-16 23:22 # Generated by Django 1.10.1 on 2016-12-16 23:22
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2017-01-02 19:23 # Generated by Django 1.10.1 on 2017-01-02 19:23
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-01-18 13:20 # Generated by Django 1.10.5 on 2017-01-18 13:20
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-22 23:09 # Generated by Django 1.10.5 on 2017-03-22 23:09
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-09-28 02:16 # Generated by Django 1.11.5 on 2017-09-28 02:16
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -91,9 +91,7 @@ class EveCorporationInfo(models.Model):
provider = EveCorporationProviderManager() provider = EveCorporationProviderManager()
class Meta: class Meta:
indexes = [ indexes = [models.Index(fields=['ceo_id',]),]
models.Index(fields=['ceo_id',]),
]
def update_corporation(self, corp: providers.Corporation = None): def update_corporation(self, corp: providers.Corporation = None):
if corp is None: if corp is None:
@@ -157,11 +155,11 @@ class EveCharacter(models.Model):
class Meta: class Meta:
indexes = [ indexes = [
models.Index(fields=['corporation_id',]), models.Index(fields=['corporation_id',]),
models.Index(fields=['alliance_id',]), models.Index(fields=['alliance_id',]),
models.Index(fields=['corporation_name',]), models.Index(fields=['corporation_name',]),
models.Index(fields=['alliance_name',]), models.Index(fields=['alliance_name',]),
] ]
@property @property
def alliance(self) -> Union[EveAllianceInfo, None]: def alliance(self) -> Union[EveAllianceInfo, None]:

View File

@@ -1,2 +0,0 @@

View File

@@ -5,6 +5,4 @@ from django.utils.translation import ugettext_lazy as _
class FatlinkForm(forms.Form): class FatlinkForm(forms.Form):
fleet = forms.CharField(label=_("Fleet Name"), max_length=50) fleet = forms.CharField(label=_("Fleet Name"), max_length=50)
duration = forms.IntegerField(label=_("Duration of fat-link"), required=True, initial=30, min_value=1, duration = forms.IntegerField(label=_("Duration of fat-link"), required=True, initial=30, min_value=1, max_value=2147483647, help_text=_('minutes'))
max_value=2147483647, help_text=_('minutes'))

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-05 21:39 # Generated by Django 1.10.1 on 2016-09-05 21:39
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-05 22:20 # Generated by Django 1.10.1 on 2016-09-05 22:20
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-06 23:54 # Generated by Django 1.10.1 on 2016-09-06 23:54
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-22 23:35 # Generated by Django 1.10.5 on 2017-03-22 23:35
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -12,7 +12,7 @@
<div class="panel-heading">{{ character_name }}</div> <div class="panel-heading">{{ character_name }}</div>
<div class="panel-body"> <div class="panel-body">
<div class="col-lg-2 col-sm-2"> <div class="col-lg-2 col-sm-2">
<img class="ra-avatar img-responsive" src="{{ character_portrait_url }}"> <img class="ra-avatar img-responsive" src="{{ character_portrait_url }}">
</div> </div>
<div class="col-lg-10 col-sm-2"> <div class="col-lg-10 col-sm-2">
<div class="alert alert-danger" role="alert">{% trans "Character not registered!" %}</div> <div class="alert alert-danger" role="alert">{% trans "Character not registered!" %}</div>

View File

@@ -30,4 +30,3 @@
</div> </div>
{% endblock content %} {% endblock content %}

View File

@@ -23,7 +23,7 @@
<th class="col-md-2 text-center">{% trans "Characters" %}</th> <th class="col-md-2 text-center">{% trans "Characters" %}</th>
<th class="col-md-2 text-center">{% trans "Fats" %}</th> <th class="col-md-2 text-center">{% trans "Fats" %}</th>
<th class="col-md-2 text-center">{% trans "Average fats" %} <th class="col-md-2 text-center">{% trans "Average fats" %}
<i class="glyphicon glyphicon-question-sign" rel="tooltip" title="Fats ÷ Characters"></i> <i class="glyphicon glyphicon-question-sign" rel="tooltip" title="Fats ÷ Characters"></i>
</th> </th>
</tr> </tr>
{% for memberStat in fatStats %} {% for memberStat in fatStats %}
@@ -42,6 +42,7 @@
</div> </div>
{% endblock content %} {% endblock content %}
{% block extra_script %} {% block extra_script %}
$(document).ready(function(){ $(document).ready(function () {
$("[rel=tooltip]").tooltip(); $("[rel=tooltip]").tooltip();
});
{% endblock extra_script %} {% endblock extra_script %}

View File

@@ -24,7 +24,7 @@
<th class="col-md-2 text-center">{% trans "Members" %}</th> <th class="col-md-2 text-center">{% trans "Members" %}</th>
<th class="col-md-2 text-center">{% trans "Fats" %}</th> <th class="col-md-2 text-center">{% trans "Fats" %}</th>
<th class="col-md-2 text-center">{% trans "Average fats" %} <th class="col-md-2 text-center">{% trans "Average fats" %}
<i class="glyphicon glyphicon-question-sign" rel="tooltip" title="Fats ÷ Characters"></i> <i class="glyphicon glyphicon-question-sign" rel="tooltip" title="Fats ÷ Characters"></i>
</th> </th>
</tr> </tr>
{% for corpStat in fatStats %} {% for corpStat in fatStats %}
@@ -44,6 +44,7 @@
</div> </div>
{% endblock content %} {% endblock content %}
{% block extra_script %} {% block extra_script %}
$(document).ready(function(){ $(document).ready(function () {
$("[rel=tooltip]").tooltip(); $("[rel=tooltip]").tooltip();
});
{% endblock extra_script %} {% endblock extra_script %}

View File

@@ -135,8 +135,7 @@ def fatlink_statistics_corp_view(request, corpid, year=None, month=None):
stat_list.sort(key=lambda stat: stat.mainchar.character_name) stat_list.sort(key=lambda stat: stat.mainchar.character_name)
stat_list.sort(key=lambda stat: (stat.n_fats, stat.avg_fat), reverse=True) stat_list.sort(key=lambda stat: (stat.n_fats, stat.avg_fat), reverse=True)
context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year, context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year, 'previous_month': start_of_previous_month, 'corpid': corpid}
'previous_month': start_of_previous_month, 'corpid': corpid}
if datetime.datetime.now() > start_of_next_month: if datetime.datetime.now() > start_of_next_month:
context.update({'next_month': start_of_next_month}) context.update({'next_month': start_of_next_month})
@@ -163,16 +162,14 @@ def fatlink_statistics_view(request, year=datetime.date.today().year, month=date
for fat in fats_in_span.exclude(character__corporation_id__in=fat_stats): for fat in fats_in_span.exclude(character__corporation_id__in=fat_stats):
if EveCorporationInfo.objects.filter(corporation_id=fat.character.corporation_id).exists(): if EveCorporationInfo.objects.filter(corporation_id=fat.character.corporation_id).exists():
fat_stats[fat.character.corporation_id] = CorpStat(fat.character.corporation_id, start_of_month, fat_stats[fat.character.corporation_id] = CorpStat(fat.character.corporation_id, start_of_month, start_of_next_month)
start_of_next_month)
# collect and sort stats # collect and sort stats
stat_list = [fat_stats[x] for x in fat_stats] stat_list = [fat_stats[x] for x in fat_stats]
stat_list.sort(key=lambda stat: stat.corp.corporation_name) stat_list.sort(key=lambda stat: stat.corp.corporation_name)
stat_list.sort(key=lambda stat: (stat.n_fats, stat.avg_fat), reverse=True) stat_list.sort(key=lambda stat: (stat.n_fats, stat.avg_fat), reverse=True)
context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year, context = {'fatStats': stat_list, 'month': start_of_month.strftime("%B"), 'year': year, 'previous_month': start_of_previous_month}
'previous_month': start_of_previous_month}
if datetime.datetime.now() > start_of_next_month: if datetime.datetime.now() > start_of_next_month:
context.update({'next_month': start_of_next_month}) context.update({'next_month': start_of_next_month})
@@ -199,8 +196,7 @@ def fatlink_personal_statistics_view(request, year=datetime.date.today().year):
monthlystats = [(i + 1, datetime.date(year, i + 1, 1).strftime("%h"), monthlystats[i]) for i in range(12)] monthlystats = [(i + 1, datetime.date(year, i + 1, 1).strftime("%h"), monthlystats[i]) for i in range(12)]
if datetime.datetime.now() > datetime.datetime(year + 1, 1, 1): if datetime.datetime.now() > datetime.datetime(year + 1, 1, 1):
context = {'user': user, 'monthlystats': monthlystats, 'year': year, 'previous_year': year - 1, context = {'user': user, 'monthlystats': monthlystats, 'year': year, 'previous_year': year - 1, 'next_year': year + 1}
'next_year': year + 1}
else: else:
context = {'user': user, 'monthlystats': monthlystats, 'year': year, 'previous_year': year - 1} context = {'user': user, 'monthlystats': monthlystats, 'year': year, 'previous_year': year - 1}
@@ -229,9 +225,11 @@ def fatlink_monthly_personal_statistics_view(request, year, month, char_id=None)
for fat in personal_fats: for fat in personal_fats:
ship_statistics[fat.shiptype] = ship_statistics.setdefault(fat.shiptype, 0) + 1 ship_statistics[fat.shiptype] = ship_statistics.setdefault(fat.shiptype, 0) + 1
n_fats += 1 n_fats += 1
context = {'user': user, 'shipStats': sorted(ship_statistics.items()), 'month': start_of_month.strftime("%h"), context = {
'year': year, 'n_fats': n_fats, 'char_id': char_id, 'previous_month': start_of_previous_month, 'user': user, 'shipStats': sorted(ship_statistics.items()), 'month': start_of_month.strftime("%h"),
'next_month': start_of_next_month} 'year': year, 'n_fats': n_fats, 'char_id': char_id, 'previous_month': start_of_previous_month,
'next_month': start_of_next_month
}
created_fats = Fatlink.objects.filter(creator=user).filter(fatdatetime__gte=start_of_month).filter( created_fats = Fatlink.objects.filter(creator=user).filter(fatdatetime__gte=start_of_month).filter(
fatdatetime__lt=start_of_next_month) fatdatetime__lt=start_of_next_month)
@@ -257,8 +255,7 @@ def click_fatlink_view(request, token, fat_hash=None):
location = c.Location.get_characters_character_id_location(character_id=token.character_id).result() location = c.Location.get_characters_character_id_location(character_id=token.character_id).result()
ship = c.Location.get_characters_character_id_ship(character_id=token.character_id).result() ship = c.Location.get_characters_character_id_ship(character_id=token.character_id).result()
location['solar_system_name'] = \ location['solar_system_name'] = \
c.Universe.get_universe_systems_system_id(system_id=location['solar_system_id']).result()[ c.Universe.get_universe_systems_system_id(system_id=location['solar_system_id']).result()['name']
'name']
if location['station_id']: if location['station_id']:
location['station_name'] = \ location['station_name'] = \
c.Universe.get_universe_stations_station_id(station_id=location['station_id']).result()['name'] c.Universe.get_universe_stations_station_id(station_id=location['station_id']).result()['name']

View File

@@ -1,4 +1,5 @@
from django.apps import apps from django.apps import apps
from django.contrib.auth.models import Permission
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import Group as BaseGroup, User from django.contrib.auth.models import Group as BaseGroup, User
from django.db.models import Count from django.db.models import Count
@@ -39,9 +40,6 @@ class AuthGroupInlineAdmin(admin.StackedInline):
kwargs["queryset"] = Group.objects.order_by(Lower('name')) kwargs["queryset"] = Group.objects.order_by(Lower('name'))
return super().formfield_for_manytomany(db_field, request, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs)
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
return False return False
@@ -122,7 +120,7 @@ class GroupAdmin(admin.ModelAdmin):
qs = super().get_queryset(request) qs = super().get_queryset(request)
if _has_auto_groups: if _has_auto_groups:
qs = qs.prefetch_related('managedalliancegroup_set', 'managedcorpgroup_set') qs = qs.prefetch_related('managedalliancegroup_set', 'managedcorpgroup_set')
qs = qs.prefetch_related('authgroup__group_leaders') qs = qs.prefetch_related('authgroup__group_leaders').select_related('authgroup')
qs = qs.annotate( qs = qs.annotate(
member_count=Count('user', distinct=True), member_count=Count('user', distinct=True),
) )
@@ -138,7 +136,7 @@ class GroupAdmin(admin.ModelAdmin):
_member_count.admin_order_field = 'member_count' _member_count.admin_order_field = 'member_count'
def has_leader(self, obj): def has_leader(self, obj):
return obj.authgroup.group_leaders.exists() return obj.authgroup.group_leaders.exists() or obj.authgroup.group_leader_groups.exists()
has_leader.boolean = True has_leader.boolean = True
@@ -168,6 +166,18 @@ class GroupAdmin(admin.ModelAdmin):
filter_horizontal = ('permissions',) filter_horizontal = ('permissions',)
inlines = (AuthGroupInlineAdmin,) inlines = (AuthGroupInlineAdmin,)
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "permissions":
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
return super().formfield_for_manytomany(db_field, request, **kwargs)
def save_formset(self, request, form, formset, change):
for inline_form in formset:
ag_instance = inline_form.save(commit=False)
ag_instance.group = form.instance
ag_instance.save()
formset.save()
class Group(BaseGroup): class Group(BaseGroup):
class Meta: class Meta:

View File

@@ -45,7 +45,7 @@ class GroupManager:
@staticmethod @staticmethod
def get_group_leaders_groups(user: User): def get_group_leaders_groups(user: User):
return Group.objects.select_related('authgroup').filter(authgroup__group_leaders__in=[user]) | \ return Group.objects.select_related('authgroup').filter(authgroup__group_leaders__in=[user]) | \
Group.objects.select_related('authgroup').filter(authgroup__group_leader_groups__in=user.groups.all()) Group.objects.select_related('authgroup').filter(authgroup__group_leader_groups__in=user.groups.all())
@staticmethod @staticmethod
def joinable_group(group: Group, state: State) -> bool: def joinable_group(group: Group, state: State) -> bool:

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-05 21:39 # Generated by Django 1.10.1 on 2016-09-05 21:39
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-06 23:54 # Generated by Django 1.10.1 on 2016-09-06 23:54
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 23:22 # Generated by Django 1.10.1 on 2016-09-09 23:22
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-12-04 10:25 # Generated by Django 1.10.2 on 2016-12-04 10:25
from __future__ import unicode_literals from __future__ import unicode_literals
@@ -61,8 +60,7 @@ def reverse_group_models(apps, schema_editor):
pass pass
if len(group.authgroup.description): if len(group.authgroup.description):
GroupDescription.objects.update_or_create(group=group, GroupDescription.objects.update_or_create(group=group, defaults={'description': group.authgroup.description})
defaults={'description': group.authgroup.description})
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -77,8 +75,7 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='AuthGroup', name='AuthGroup',
fields=[ fields=[
('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, ('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='auth.Group')),
serialize=False, to='auth.Group')),
('internal', models.BooleanField(default=True, help_text='Internal group, users cannot see, join or request to join this group.<br>Used for groups such as Members, Corp_*, Alliance_* etc.<br><b>Overrides Hidden and Open options when selected.</b>')), ('internal', models.BooleanField(default=True, help_text='Internal group, users cannot see, join or request to join this group.<br>Used for groups such as Members, Corp_*, Alliance_* etc.<br><b>Overrides Hidden and Open options when selected.</b>')),
('hidden', models.BooleanField(default=True, help_text='Group is hidden from users but can still join with the correct link.')), ('hidden', models.BooleanField(default=True, help_text='Group is hidden from users but can still join with the correct link.')),
('open', models.BooleanField(default=False, help_text='Group is open and users will be automatically added upon request. <br>If the group is not open users will need their request manually approved.')), ('open', models.BooleanField(default=False, help_text='Group is open and users will be automatically added upon request. <br>If the group is not open users will need their request manually approved.')),

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-04 06:11 # Generated by Django 1.10.5 on 2017-02-04 06:11
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-04 07:17 # Generated by Django 1.10.5 on 2017-02-04 07:17
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-09-28 02:16 # Generated by Django 1.11.5 on 2017-09-28 02:16
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-02-23 23:09 # Generated by Django 1.11.10 on 2018-02-23 23:09
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -55,7 +55,6 @@ class RequestLog(models.Model):
return user.profile.main_character return user.profile.main_character
class AuthGroup(models.Model): class AuthGroup(models.Model):
""" """
Extends Django Group model with a one-to-one field Extends Django Group model with a one-to-one field
@@ -79,35 +78,35 @@ class AuthGroup(models.Model):
group = models.OneToOneField(Group, on_delete=models.CASCADE, primary_key=True) group = models.OneToOneField(Group, on_delete=models.CASCADE, primary_key=True)
internal = models.BooleanField(default=True, internal = models.BooleanField(default=True,
help_text="Internal group, users cannot see, join or request to join this group.<br>" help_text="Internal group, users cannot see, join or request to join this group.<br>"
"Used for groups such as Members, Corp_*, Alliance_* etc.<br>" "Used for groups such as Members, Corp_*, Alliance_* etc.<br>"
"<b>Overrides Hidden and Open options when selected.</b>") "<b>Overrides Hidden and Open options when selected.</b>")
hidden = models.BooleanField(default=True, hidden = models.BooleanField(default=True, help_text="Group is hidden from users but can still join with the correct link.")
help_text="Group is hidden from users but can still join with the correct link.")
open = models.BooleanField(default=False, open = models.BooleanField(default=False,
help_text="Group is open and users will be automatically added upon request. <br>" help_text="Group is open and users will be automatically added upon request. <br>"
"If the group is not open users will need their request manually approved.") "If the group is not open users will need their request manually approved.")
public = models.BooleanField(default=False, public = models.BooleanField(default=False,
help_text="Group is public. Any registered user is able to join this group, with " help_text="Group is public. Any registered user is able to join this group, with "
"visibility based on the other options set for this group.<br> Auth will " "visibility based on the other options set for this group.<br> Auth will "
"not remove users from this group automatically when they are no longer " "not remove users from this group automatically when they are no longer "
"authenticated.") "authenticated.")
# Group leaders have management access to this group # Group leaders have management access to this group
group_leaders = models.ManyToManyField(User, related_name='leads_groups', blank=True, group_leaders = models.ManyToManyField(User, related_name='leads_groups', blank=True,
help_text="Group leaders can process group requests for this group " help_text="Group leaders can process group requests for this group "
"specifically. Use the auth.group_management permission to allow " "specifically. Use the auth.group_management permission to allow "
"a user to manage all groups.") "a user to manage all groups.")
# allow groups to be *group leads* # allow groups to be *group leads*
group_leader_groups = models.ManyToManyField(Group, related_name='leads_group_groups', blank=True, group_leader_groups = models.ManyToManyField(Group, related_name='leads_group_groups', blank=True,
help_text="Group leaders can process group requests for this group " help_text="Group leaders can process group requests for this group "
"specifically. Use the auth.group_management permission to allow " "specifically. Use the auth.group_management permission to allow "
"a user to manage all groups.") "a user to manage all groups.")
states = models.ManyToManyField(State, related_name='valid_states', blank=True, states = models.ManyToManyField(State, related_name='valid_states', blank=True,
help_text="States listed here will have the ability to join this group provided " help_text="States listed here will have the ability to join this group provided "
"they have the proper permissions.") "they have the proper permissions.")
description = models.TextField(max_length=512, blank=True, help_text="Short description <i>(max. 512 characters)</i> of the group shown to users.") description = models.TextField(max_length=512, blank=True, help_text="Short description <i>(max. 512 characters)"
"</i> of the group shown to users.")
def __str__(self): def __str__(self):
return self.group.name return self.group.name

View File

@@ -83,7 +83,7 @@
{% block extra_script %} {% block extra_script %}
$.fn.dataTable.moment = function(format, locale) { $.fn.dataTable.moment = function(format, locale) {
var types = $.fn.dataTable.ext.type; let types = $.fn.dataTable.ext.type;
// Add type detection // Add type detection
types.detect.unshift(function(d) { types.detect.unshift(function(d) {

View File

@@ -48,6 +48,7 @@ class TestGroupAdmin(TestCase):
cls.group_2 = Group.objects.create(name='Group 2') cls.group_2 = Group.objects.create(name='Group 2')
cls.group_2.authgroup.description = 'Internal Group' cls.group_2.authgroup.description = 'Internal Group'
cls.group_2.authgroup.internal = True cls.group_2.authgroup.internal = True
cls.group_2.authgroup.group_leader_groups.add(cls.group_1)
cls.group_2.authgroup.save() cls.group_2.authgroup.save()
# group 3 - has leader # group 3 - has leader
@@ -237,10 +238,14 @@ class TestGroupAdmin(TestCase):
result = self.modeladmin._member_count(obj) result = self.modeladmin._member_count(obj)
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_has_leader(self): def test_has_leader_user(self):
result = self.modeladmin.has_leader(self.group_1) result = self.modeladmin.has_leader(self.group_1)
self.assertTrue(result) self.assertTrue(result)
def test_has_leader_group(self):
result = self.modeladmin.has_leader(self.group_2)
self.assertTrue(result)
def test_properties_1(self): def test_properties_1(self):
expected = ['Default'] expected = ['Default']
result = self.modeladmin._properties(self.group_1) result = self.modeladmin._properties(self.group_1)

View File

@@ -97,4 +97,3 @@ class TestCheckGroupsOnStateChange(TestCase):
self.assertEqual(self.user.profile.state, self.test_state_2) self.assertEqual(self.user.profile.state, self.test_state_2)
groups = self.user.groups.all() groups = self.user.groups.all()
self.assertIn(state_group, groups) self.assertIn(state_group, groups)

View File

@@ -78,8 +78,7 @@ def group_membership_audit(request, group_id):
# Check its a joinable group i.e. not corp or internal # Check its a joinable group i.e. not corp or internal
# And the user has permission to manage it # And the user has permission to manage it
if not GroupManager.check_internal_group(group) or not GroupManager.can_manage_group(request.user, group): if not GroupManager.check_internal_group(group) or not GroupManager.can_manage_group(request.user, group):
logger.warning("User %s attempted to view the membership of group %s but permission was denied" % logger.warning("User %s attempted to view the membership of group %s but permission was denied" % (request.user, group_id))
(request.user, group_id))
raise PermissionDenied raise PermissionDenied
except ObjectDoesNotExist: except ObjectDoesNotExist:
@@ -140,15 +139,13 @@ def group_membership_list(request, group_id):
@login_required @login_required
@user_passes_test(GroupManager.can_manage_groups) @user_passes_test(GroupManager.can_manage_groups)
def group_membership_remove(request, group_id, user_id): def group_membership_remove(request, group_id, user_id):
logger.debug("group_membership_remove called by user %s for group id %s on user id %s" % logger.debug("group_membership_remove called by user %s for group id %s on user id %s" % (request.user, group_id, user_id))
(request.user, group_id, user_id))
group = get_object_or_404(Group, id=group_id) group = get_object_or_404(Group, id=group_id)
try: try:
# Check its a joinable group i.e. not corp or internal # Check its a joinable group i.e. not corp or internal
# And the user has permission to manage it # And the user has permission to manage it
if not GroupManager.check_internal_group(group) or not GroupManager.can_manage_group(request.user, group): if not GroupManager.check_internal_group(group) or not GroupManager.can_manage_group(request.user, group):
logger.warning("User %s attempted to remove a user from group %s but permission was denied" % (request.user, logger.warning("User %s attempted to remove a user from group %s but permission was denied" % (request.user, group_id))
group_id))
raise PermissionDenied raise PermissionDenied
try: try:
@@ -189,13 +186,12 @@ def group_accept_request(request, group_request_id):
logger.info("User %s accepted group request from user %s to group %s" % ( logger.info("User %s accepted group request from user %s to group %s" % (
request.user, group_request.user, group_request.group.name)) request.user, group_request.user, group_request.group.name))
notify(group_request.user, "Group Application Accepted", level="success", notify(group_request.user, "Group Application Accepted", level="success",
message="Your application to %s has been accepted." % group_request.group) message="Your application to %s has been accepted." % group_request.group)
messages.success(request, messages.success(request,
_('Accepted application from %(mainchar)s to %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group}) _('Accepted application from %(mainchar)s to %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group})
except PermissionDenied as p: except PermissionDenied as p:
logger.warning("User %s attempted to accept group join request %s but permission was denied" % logger.warning("User %s attempted to accept group join request %s but permission was denied" % (request.user, group_request_id))
(request.user, group_request_id))
raise p raise p
except: except:
messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group}) messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group})
@@ -221,14 +217,12 @@ def group_reject_request(request, group_request_id):
log = RequestLog(request_type=group_request.leave_request,group=group_request.group,request_info=group_request.__str__(),action=0,request_actor=request.user) log = RequestLog(request_type=group_request.leave_request,group=group_request.group,request_info=group_request.__str__(),action=0,request_actor=request.user)
log.save() log.save()
group_request.delete() group_request.delete()
notify(group_request.user, "Group Application Rejected", level="danger", notify(group_request.user, "Group Application Rejected", level="danger", message="Your application to %s has been rejected." % group_request.group)
message="Your application to %s has been rejected." % group_request.group)
messages.success(request, messages.success(request,
_('Rejected application from %(mainchar)s to %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group}) _('Rejected application from %(mainchar)s to %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group})
except PermissionDenied as p: except PermissionDenied as p:
logger.warning("User %s attempted to reject group join request %s but permission was denied" % logger.warning("User %s attempted to reject group join request %s but permission was denied" % (request.user, group_request_id))
(request.user, group_request_id))
raise p raise p
except: except:
messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group}) messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group})
@@ -258,12 +252,11 @@ def group_leave_accept_request(request, group_request_id):
logger.info("User %s accepted group leave request from user %s to group %s" % ( logger.info("User %s accepted group leave request from user %s to group %s" % (
request.user, group_request.user, group_request.group.name)) request.user, group_request.user, group_request.group.name))
notify(group_request.user, "Group Leave Request Accepted", level="success", notify(group_request.user, "Group Leave Request Accepted", level="success",
message="Your request to leave %s has been accepted." % group_request.group) message="Your request to leave %s has been accepted." % group_request.group)
messages.success(request, messages.success(request,
_('Accepted application from %(mainchar)s to leave %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group}) _('Accepted application from %(mainchar)s to leave %(group)s.') % {"mainchar": group_request.main_char, "group": group_request.group})
except PermissionDenied as p: except PermissionDenied as p:
logger.warning("User %s attempted to accept group leave request %s but permission was denied" % logger.warning("User %s attempted to accept group leave request %s but permission was denied" % (request.user, group_request_id))
(request.user, group_request_id))
raise p raise p
except: except:
messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to leave %(group)s.') % { messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to leave %(group)s.') % {
@@ -291,13 +284,11 @@ def group_leave_reject_request(request, group_request_id):
group_request.delete() group_request.delete()
logger.info("User %s rejected group leave request from user %s for group %s" % ( logger.info("User %s rejected group leave request from user %s for group %s" % (
request.user, group_request.user, group_request.group.name)) request.user, group_request.user, group_request.group.name))
notify(group_request.user, "Group Leave Request Rejected", level="danger", notify(group_request.user, "Group Leave Request Rejected", level="danger", message="Your request to leave %s has been rejected." % group_request.group)
message="Your request to leave %s has been rejected." % group_request.group)
messages.success(request, _('Rejected application from %(mainchar)s to leave %(group)s.') % { messages.success(request, _('Rejected application from %(mainchar)s to leave %(group)s.') % {
"mainchar": group_request.main_char, "group": group_request.group}) "mainchar": group_request.main_char, "group": group_request.group})
except PermissionDenied as p: except PermissionDenied as p:
logger.warning("User %s attempted to reject group leave request %s but permission was denied" % logger.warning("User %s attempted to reject group leave request %s but permission was denied" % (request.user, group_request_id))
(request.user, group_request_id))
raise p raise p
except: except:
messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to leave %(group)s.') % { messages.error(request, _('An unhandled error occurred while processing the application from %(mainchar)s to leave %(group)s.') % {
@@ -337,20 +328,17 @@ def group_request_add(request, group_id):
group = Group.objects.get(id=group_id) group = Group.objects.get(id=group_id)
state = request.user.profile.state state = request.user.profile.state
if not GroupManager.joinable_group(group, state): if not GroupManager.joinable_group(group, state):
logger.warning("User %s attempted to join group id %s but it is not a joinable group" % logger.warning("User %s attempted to join group id %s but it is not a joinable group" % (request.user, group_id))
(request.user, group_id))
messages.warning(request, _("You cannot join that group")) messages.warning(request, _("You cannot join that group"))
return redirect('groupmanagement:groups') return redirect('groupmanagement:groups')
if group in request.user.groups.all(): if group in request.user.groups.all():
# User is already a member of this group. # User is already a member of this group.
logger.warning("User %s attempted to join group id %s but they are already a member." % logger.warning("User %s attempted to join group id %s but they are already a member." % (request.user, group_id))
(request.user, group_id))
messages.warning(request, _("You are already a member of that group.")) messages.warning(request, _("You are already a member of that group."))
return redirect('groupmanagement:groups') return redirect('groupmanagement:groups')
if not request.user.has_perm('groupmanagement.request_groups') and not group.authgroup.public: if not request.user.has_perm('groupmanagement.request_groups') and not group.authgroup.public:
# Does not have the required permission, trying to join a non-public group # Does not have the required permission, trying to join a non-public group
logger.warning("User %s attempted to join group id %s but it is not a public group" % logger.warning("User %s attempted to join group id %s but it is not a public group" % (request.user, group_id))
(request.user, group_id))
messages.warning(request, _("You cannot join that group")) messages.warning(request, _("You cannot join that group"))
return redirect('groupmanagement:groups') return redirect('groupmanagement:groups')
if group.authgroup.open: if group.authgroup.open:
@@ -381,13 +369,11 @@ def group_request_leave(request, group_id):
logger.debug("group_request_leave called by user %s for group id %s" % (request.user, group_id)) logger.debug("group_request_leave called by user %s for group id %s" % (request.user, group_id))
group = Group.objects.get(id=group_id) group = Group.objects.get(id=group_id)
if not GroupManager.check_internal_group(group): if not GroupManager.check_internal_group(group):
logger.warning("User %s attempted to leave group id %s but it is not a joinable group" % logger.warning("User %s attempted to leave group id %s but it is not a joinable group" % (request.user, group_id))
(request.user, group_id))
messages.warning(request, _("You cannot leave that group")) messages.warning(request, _("You cannot leave that group"))
return redirect('groupmanagement:groups') return redirect('groupmanagement:groups')
if group not in request.user.groups.all(): if group not in request.user.groups.all():
logger.debug("User %s attempted to leave group id %s but they are not a member" % logger.debug("User %s attempted to leave group id %s but they are not a member" % (request.user, group_id))
(request.user, group_id))
messages.warning(request, _("You are not a member of that group")) messages.warning(request, _("You are not a member of that group"))
return redirect('groupmanagement:groups') return redirect('groupmanagement:groups')
if group.authgroup.open: if group.authgroup.open:

View File

@@ -5,16 +5,16 @@ All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met: are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, 1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer. this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright 2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution. documentation and/or other materials provided with the distribution.
3. Neither the name of Torchbox nor the names of its contributors may be used 3. Neither the name of Torchbox nor the names of its contributors may be used
to endorse or promote products derived from this software without to endorse or promote products derived from this software without
specific prior written permission. specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
@@ -122,4 +122,3 @@ def get_hooks(name):
""" """
register_all_hooks() register_all_hooks()
return _hooks.get(name, []) return _hooks.get(name, [])

View File

@@ -9,11 +9,12 @@ from .models import Application
class ApplicationsMenu(MenuItemHook): class ApplicationsMenu(MenuItemHook):
def __init__(self): def __init__(self):
MenuItemHook.__init__(self, MenuItemHook.__init__(
_('Applications'), self,
'far fa-file fa-fw', _('Applications'),
'hrapplications:index', 'far fa-file fa-fw',
navactive=['hrapplications:']) 'hrapplications:index',
navactive=['hrapplications:'])
def render(self, request): def render(self, request):
app_count = Application.objects.pending_requests_count_for_user(request.user) app_count = Application.objects.pending_requests_count_for_user(request.user)

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-05 21:39 # Generated by Django 1.10.1 on 2016-09-05 21:39
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-23 19:46 # Generated by Django 1.11.4 on 2017-08-23 19:46
from __future__ import unicode_literals from __future__ import unicode_literals

Some files were not shown because too many files have changed in this diff Show More