Compare commits

..

2 Commits

Author SHA1 Message Date
Ariel Rin
7412675bfb Version Bump v2.8.8 2021-10-17 09:34:41 +00:00
Joel Falknau
ad953efe77 Require Django-ESI 3.x
(cherry picked from commit 98619a0eb8)
2021-10-17 09:31:54 +00:00
548 changed files with 8801 additions and 18722 deletions

View File

@@ -1,28 +0,0 @@
# 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
[{Dockerfile,*.dockerfile}]
indent_style = space
indent_size = 4

2
.gitignore vendored
View File

@@ -38,6 +38,7 @@ htmlcov/
.tox/ .tox/
.coverage .coverage
.cache .cache
nosetests.xml
coverage.xml coverage.xml
# Translations # Translations
@@ -76,4 +77,3 @@ celerybeat-schedule
.flake8 .flake8
.pylintrc .pylintrc
Makefile Makefile
.isort.cfg

View File

@@ -1,15 +1,7 @@
.only-default: &only-default
only:
- master
- branches
- merge_requests
stages: stages:
- pre-commit
- gitlab - gitlab
- test - test
- deploy - deploy
- docker
include: include:
- template: Dependency-Scanning.gitlab-ci.yml - template: Dependency-Scanning.gitlab-ci.yml
@@ -21,19 +13,6 @@ before_script:
- python -V - python -V
- pip install wheel tox - pip install wheel tox
pre-commit-check:
<<: *only-default
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: []
@@ -41,142 +20,68 @@ sast:
dependency_scanning: dependency_scanning:
stage: gitlab stage: gitlab
before_script: before_script:
- apt-get update && apt-get install redis-server libmariadb-dev -y - apt-get update && apt-get install redis-server libmariadbclient-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:
<<: *only-default image: python:3.7-buster
image: python:3.7-bullseye script:
script:
- tox -e py37-core - tox -e py37-core
artifacts: artifacts:
when: always when: always
reports: reports:
coverage_report: cobertura: coverage.xml
coverage_format: cobertura
path: coverage.xml
test-3.8-core: test-3.8-core:
<<: *only-default image: python:3.8-buster
image: python:3.8-bullseye script:
script:
- tox -e py38-core - tox -e py38-core
artifacts: artifacts:
when: always when: always
reports: reports:
coverage_report: cobertura: coverage.xml
coverage_format: cobertura
path: coverage.xml
test-3.9-core: test-3.6-all:
<<: *only-default 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:
coverage_report: cobertura: coverage.xml
coverage_format: cobertura
path: coverage.xml
test-3.10-core:
<<: *only-default
image: python:3.10-bullseye
script:
- tox -e py310-core
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.11-core:
<<: *only-default
image: python:3.11-rc-bullseye
script:
- tox -e py311-core
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
allow_failure: true
test-3.7-all: test-3.7-all:
<<: *only-default image: python:3.7-buster
image: python:3.7-bullseye script:
script:
- tox -e py37-all - tox -e py37-all
artifacts: artifacts:
when: always when: always
reports: reports:
coverage_report: cobertura: coverage.xml
coverage_format: cobertura
path: coverage.xml
test-3.8-all: test-3.8-all:
<<: *only-default image: python:3.8-buster
image: python:3.8-bullseye script:
script:
- tox -e py38-all - tox -e py38-all
artifacts: artifacts:
when: always when: always
reports: reports:
coverage_report: cobertura: coverage.xml
coverage_format: cobertura
path: coverage.xml
test-3.9-all:
<<: *only-default
image: python:3.9-bullseye
script:
- tox -e py39-all
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.10-all:
<<: *only-default
image: python:3.10-bullseye
script:
- tox -e py310-all
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
test-3.11-all:
<<: *only-default
image: python:3.11-rc-bullseye
script:
- tox -e py311-all
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
allow_failure: true
test-docs:
<<: *only-default
image: python:3.9-bullseye
script:
- tox -e docs
deploy_production: deploy_production:
stage: deploy stage: deploy
image: python:3.10-bullseye image: python:3.8-buster
before_script: before_script:
- pip install twine wheel - pip install twine wheel
@@ -186,66 +91,4 @@ deploy_production:
- twine upload dist/* - twine upload dist/*
rules: rules:
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
build-image:
before_script: []
image: docker:20.10.10
stage: docker
services:
- docker:20.10.10-dind
script: |
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_SHORT_SHA
CURRENT_TAG=$CI_REGISTRY_IMAGE/auth:$CI_COMMIT_TAG
MINOR_TAG=$CI_REGISTRY_IMAGE/auth:$(echo $CI_COMMIT_TAG | cut -d '.' -f 1-2)
MAJOR_TAG=$CI_REGISTRY_IMAGE/auth:$(echo $CI_COMMIT_TAG | cut -d '.' -f 1)
LATEST_TAG=$CI_REGISTRY_IMAGE/auth:latest
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
docker build . -t $IMAGE_TAG -f docker/Dockerfile --build-arg AUTH_VERSION=$(echo $CI_COMMIT_TAG | cut -c 2-)
docker tag $IMAGE_TAG $CURRENT_TAG
docker tag $IMAGE_TAG $MINOR_TAG
docker tag $IMAGE_TAG $MAJOR_TAG
docker tag $IMAGE_TAG $LATEST_TAG
docker image push --all-tags $CI_REGISTRY_IMAGE/auth
rules:
- if: $CI_COMMIT_TAG
build-image-dev:
before_script: []
image: docker:20.10.10
stage: docker
services:
- docker:20.10.10-dind
script: |
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
docker build . -t $IMAGE_TAG -f docker/Dockerfile --build-arg AUTH_PACKAGE=git+https://gitlab.com/allianceauth/allianceauth@$CI_COMMIT_BRANCH
docker push $IMAGE_TAG
rules:
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == ""'
when: manual
- if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME != ""'
when: never
build-image-mr:
before_script: []
image: docker:20.10.10
stage: docker
services:
- docker:20.10.10-dind
script: |
CURRENT_DATE=$(echo $CI_COMMIT_TIMESTAMP | head -c 10 | tr -d -)
IMAGE_TAG=$CI_REGISTRY_IMAGE/auth:$CURRENT_DATE-$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-$CI_COMMIT_SHORT_SHA
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
docker build . -t $IMAGE_TAG -f docker/Dockerfile --build-arg AUTH_PACKAGE=git+$CI_MERGE_REQUEST_SOURCE_PROJECT_URL@$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
docker push $IMAGE_TAG
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: manual
- if: '$CI_PIPELINE_SOURCE != "merge_request_event"'
when: never

View File

@@ -1,8 +1,8 @@
# Bug # Bug
- I have searched [issues](https://gitlab.com/allianceauth/allianceauth/issues?scope=all&utf8=%E2%9C%93&state=all) (Y/N): - I have searched [issues](https://gitlab.com/allianceauth/allianceauth/issues?scope=all&utf8=%E2%9C%93&state=all) (Y/N):
- What Version of Alliance Auth: - What Version of Alliance Auth:
- What Operating System: - What Operating System:
- Version of other components relevant to issue eg. Service, Database: - Version of other components relevant to issue eg. Service, Database:
Please include a brief description of your issue here. Please include a brief description of your issue here.
@@ -11,4 +11,4 @@ Please include steps to reproduce the issue
Please include any tracebacks or logs Please include any tracebacks or logs
Please include the results of the command `pip list` Please include the results of the command `pip list`

View File

@@ -4,4 +4,4 @@
- Is this a Service (external integration), a Module (Alliance Auth extension) or an enhancement to an existing service/module. - Is this a Service (external integration), a Module (Alliance Auth extension) or an enhancement to an existing service/module.
- Describe why its useful to you or others. - Describe why its useful to you or others.

View File

@@ -1,34 +0,0 @@
# 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.54
hooks:
- id: editorconfig-checker
exclude: ^(LICENSE|allianceauth\/static\/css\/themes\/bootstrap-locals.less|allianceauth\/eveonline\/swagger.json|(.*.po)|(.*.mo))
- repo: https://github.com/asottile/pyupgrade
rev: v2.29.0
hooks:
- id: pyupgrade
args: [ --py37-plus ]

View File

@@ -20,4 +20,4 @@ formats: all
python: python:
version: 3.7 version: 3.7
install: install:
- requirements: docs/requirements.txt - requirements: docs/requirements.txt

View File

@@ -337,3 +337,4 @@ 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,8 +1,8 @@
# 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.13.0' __version__ = '2.8.8'
__title__ = 'Alliance Auth' __title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth' __url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = f'{__title__} v{__version__}' NAME = '%s v%s' % (__title__, __version__)
default_app_config = 'allianceauth.apps.AllianceAuthConfig' default_app_config = 'allianceauth.apps.AllianceAuthConfig'

View File

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

View File

@@ -1,21 +0,0 @@
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

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

View File

@@ -1,21 +0,0 @@
[
{
"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

@@ -1,52 +0,0 @@
from bs4 import BeautifulSoup
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
from .models import AnalyticsTokens, AnalyticsIdentifier
from .tasks import send_ga_tracking_web_view
import re
class AnalyticsMiddleware(MiddlewareMixin):
def process_response(self, request, response):
"""Django Middleware: Process Page Views and creates Analytics Celery Tasks"""
if getattr(settings, "ANALYTICS_DISABLED", False):
return response
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
ignore = False
for ignore_path in token.ignore_paths.values():
ignore_path_regex = re.compile(ignore_path["ignore_path"])
if re.search(ignore_path_regex, request.path) is not None:
ignore = True
if ignore is True:
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

@@ -1,42 +0,0 @@
# 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

@@ -1,34 +0,0 @@
# 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

@@ -1,30 +0,0 @@
# 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

@@ -1,42 +0,0 @@
# Generated by Django 3.1.13 on 2021-10-15 05:02
from django.core.exceptions import ObjectDoesNotExist
from django.db import migrations
def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
# Add /admin/ and /user_notifications_count/ path to ignore
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
admin = AnalyticsPath.objects.create(ignore_path=r"^\/admin\/.*")
user_notifications_count = AnalyticsPath.objects.create(ignore_path=r"^\/user_notifications_count\/.*")
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens.objects.get(token="UA-186249766-2")
token.ignore_paths.add(admin, user_notifications_count)
def undo_modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
#
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens.objects.get(token="UA-186249766-2")
try:
admin = AnalyticsPath.objects.get(ignore_path=r"^\/admin\/.*", analyticstokens=token)
user_notifications_count = AnalyticsPath.objects.get(ignore_path=r"^\/user_notifications_count\/.*", analyticstokens=token)
admin.delete()
user_notifications_count.delete()
except ObjectDoesNotExist:
# Its fine if it doesnt exist, we just dont want them building up when re-migrating
pass
class Migration(migrations.Migration):
dependencies = [
('analytics', '0003_Generate_Identifier'),
]
operations = [migrations.RunPython(modify_aa_team_token_add_page_ignore_paths, undo_modify_aa_team_token_add_page_ignore_paths)
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.8 on 2021-10-17 16:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('analytics', '0004_auto_20211015_0502'),
]
operations = [
migrations.AlterField(
model_name='analyticspath',
name='ignore_path',
field=models.CharField(default='/example/', help_text='Regex Expression, If matched no Analytics Page View is sent', max_length=254),
),
]

View File

@@ -1,40 +0,0 @@
# Generated by Django 3.2.8 on 2021-10-19 01:47
from django.core.exceptions import ObjectDoesNotExist
from django.db import migrations
def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
# Add the /account/activate path to ignore
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
account_activate = AnalyticsPath.objects.create(ignore_path=r"^\/account\/activate\/.*")
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens.objects.get(token="UA-186249766-2")
token.ignore_paths.add(account_activate)
def undo_modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
#
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
token = Tokens.objects.get(token="UA-186249766-2")
try:
account_activate = AnalyticsPath.objects.get(ignore_path=r"^\/account\/activate\/.*", analyticstokens=token)
account_activate.delete()
except ObjectDoesNotExist:
# Its fine if it doesnt exist, we just dont want them building up when re-migrating
pass
class Migration(migrations.Migration):
dependencies = [
('analytics', '0005_alter_analyticspath_ignore_path'),
]
operations = [
migrations.RunPython(modify_aa_team_token_add_page_ignore_paths, undo_modify_aa_team_token_add_page_ignore_paths)
]

View File

@@ -1,38 +0,0 @@
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().save(*args, **kwargs)
class AnalyticsPath(models.Model):
ignore_path = models.CharField(max_length=254, default="/example/", help_text="Regex Expression, If matched no Analytics Page View is sent")
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

@@ -1,55 +0,0 @@
import logging
from celery.signals import task_failure, task_success
from django.conf import settings
from allianceauth.analytics.tasks import analytics_event
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__)
if getattr(settings, "ANALYTICS_DISABLED", False):
return
category = sender.__module__
if 'allianceauth.analytics' not in category:
if category.endswith(".tasks"):
category = category[:-6]
action = sender.__name__
label = f"{exception.__class__.__name__}"
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__)
if getattr(settings, "ANALYTICS_DISABLED", False):
return
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

@@ -1,207 +0,0 @@
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
Args:
`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

@@ -1,108 +0,0 @@
from unittest.mock import patch
from urllib.parse import parse_qs
import requests_mock
from django.test import TestCase, override_settings
from allianceauth.analytics.tasks import ANALYTICS_URL
from allianceauth.eveonline.tasks import update_character
from allianceauth.tests.auth_utils import AuthUtils
@override_settings(CELERY_ALWAYS_EAGER=True)
@requests_mock.mock()
class TestAnalyticsForViews(TestCase):
@override_settings(ANALYTICS_DISABLED=False)
def test_should_run_analytics(self, requests_mocker):
# given
requests_mocker.post(ANALYTICS_URL)
user = AuthUtils.create_user("Bruce Wayne")
self.client.force_login(user)
# when
response = self.client.get("/dashboard/")
# then
self.assertEqual(response.status_code, 200)
self.assertTrue(requests_mocker.called)
@override_settings(ANALYTICS_DISABLED=True)
def test_should_not_run_analytics(self, requests_mocker):
# given
requests_mocker.post(ANALYTICS_URL)
user = AuthUtils.create_user("Bruce Wayne")
self.client.force_login(user)
# when
response = self.client.get("/dashboard/")
# then
self.assertEqual(response.status_code, 200)
self.assertFalse(requests_mocker.called)
@override_settings(CELERY_ALWAYS_EAGER=True)
@requests_mock.mock()
class TestAnalyticsForTasks(TestCase):
@override_settings(ANALYTICS_DISABLED=False)
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
def test_should_run_analytics_for_successful_task(
self, requests_mocker, mock_update_character
):
# given
requests_mocker.post(ANALYTICS_URL)
user = AuthUtils.create_user("Bruce Wayne")
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
# when
update_character.delay(character.character_id)
# then
self.assertTrue(mock_update_character.called)
self.assertTrue(requests_mocker.called)
payload = parse_qs(requests_mocker.last_request.text)
self.assertListEqual(payload["el"], ["Success"])
@override_settings(ANALYTICS_DISABLED=True)
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
def test_should_not_run_analytics_for_successful_task(
self, requests_mocker, mock_update_character
):
# given
requests_mocker.post(ANALYTICS_URL)
user = AuthUtils.create_user("Bruce Wayne")
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
# when
update_character.delay(character.character_id)
# then
self.assertTrue(mock_update_character.called)
self.assertFalse(requests_mocker.called)
@override_settings(ANALYTICS_DISABLED=False)
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
def test_should_run_analytics_for_failed_task(
self, requests_mocker, mock_update_character
):
# given
requests_mocker.post(ANALYTICS_URL)
mock_update_character.side_effect = RuntimeError
user = AuthUtils.create_user("Bruce Wayne")
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
# when
update_character.delay(character.character_id)
# then
self.assertTrue(mock_update_character.called)
self.assertTrue(requests_mocker.called)
payload = parse_qs(requests_mocker.last_request.text)
self.assertNotEqual(payload["el"], ["Success"])
@override_settings(ANALYTICS_DISABLED=True)
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
def test_should_not_run_analytics_for_failed_task(
self, requests_mocker, mock_update_character
):
# given
requests_mocker.post(ANALYTICS_URL)
mock_update_character.side_effect = RuntimeError
user = AuthUtils.create_user("Bruce Wayne")
character = AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
# when
update_character.delay(character.character_id)
# then
self.assertTrue(mock_update_character.called)
self.assertFalse(requests_mocker.called)

View File

@@ -1,23 +0,0 @@
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

@@ -1,26 +0,0 @@
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

@@ -1,119 +0,0 @@
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

@@ -1,55 +0,0 @@
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

@@ -1,36 +0,0 @@
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

@@ -1,44 +1,26 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import Group from django.contrib.auth.models import User as BaseUser, \
from django.contrib.auth.models import Permission as BasePermission Permission as BasePermission, Group
from django.contrib.auth.models import User as BaseUser
from django.db.models import Count, Q from django.db.models import Count, Q
from allianceauth.services.hooks import ServicesHook
from django.db.models.signals import pre_save, post_save, pre_delete, \
post_delete, m2m_changed
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.db.models.signals import (
m2m_changed,
post_delete,
post_save,
pre_delete,
pre_save
)
from django.dispatch import receiver from django.dispatch import receiver
from django.urls import reverse from django.forms import ModelForm
from django.utils.html import format_html from django.utils.html import format_html
from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from allianceauth.authentication.models import ( from allianceauth.authentication.models import State, get_guest_state,\
CharacterOwnership, CharacterOwnership, UserProfile, OwnershipRecord
OwnershipRecord,
State,
UserProfile,
get_guest_state
)
from allianceauth.eveonline.models import (
EveAllianceInfo,
EveCharacter,
EveCorporationInfo,
EveFactionInfo
)
from allianceauth.eveonline.tasks import update_character
from allianceauth.hooks import get_hooks from allianceauth.hooks import get_hooks
from allianceauth.services.hooks import ServicesHook from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\
EveAllianceInfo
from .app_settings import ( from allianceauth.eveonline.tasks import update_character
AUTHENTICATION_ADMIN_USERS_MAX_CHARS, from .app_settings import AUTHENTICATION_ADMIN_USERS_MAX_GROUPS, \
AUTHENTICATION_ADMIN_USERS_MAX_GROUPS AUTHENTICATION_ADMIN_USERS_MAX_CHARS
)
from .forms import UserChangeForm, UserProfileForm
def make_service_hooks_update_groups_action(service): def make_service_hooks_update_groups_action(service):
@@ -54,8 +36,8 @@ def make_service_hooks_update_groups_action(service):
for user in queryset: # queryset filtering doesn't work here? for user in queryset: # queryset filtering doesn't work here?
service.update_groups(user) service.update_groups(user)
update_service_groups.__name__ = str(f'update_{slugify(service.name)}_groups') update_service_groups.__name__ = str('update_{}_groups'.format(slugify(service.name)))
update_service_groups.short_description = f"Sync groups for selected {service.title} accounts" update_service_groups.short_description = "Sync groups for selected {} accounts".format(service.title)
return update_service_groups return update_service_groups
@@ -72,15 +54,24 @@ def make_service_hooks_sync_nickname_action(service):
for user in queryset: # queryset filtering doesn't work here? for user in queryset: # queryset filtering doesn't work here?
service.sync_nickname(user) service.sync_nickname(user)
sync_nickname.__name__ = str(f'sync_{slugify(service.name)}_nickname') sync_nickname.__name__ = str('sync_{}_nickname'.format(slugify(service.name)))
sync_nickname.short_description = f"Sync nicknames for selected {service.title} accounts" sync_nickname.short_description = "Sync nicknames for selected {} accounts".format(service.title)
return sync_nickname return sync_nickname
class QuerysetModelForm(ModelForm):
# allows specifying FK querysets through kwarg
def __init__(self, querysets=None, *args, **kwargs):
querysets = querysets or {}
super().__init__(*args, **kwargs)
for field, qs in querysets.items():
self.fields[field].queryset = qs
class UserProfileInline(admin.StackedInline): class UserProfileInline(admin.StackedInline):
model = UserProfile model = UserProfile
readonly_fields = ('state',) readonly_fields = ('state',)
form = UserProfileForm form = QuerysetModelForm
verbose_name = '' verbose_name = ''
verbose_name_plural = 'Profile' verbose_name_plural = 'Profile'
@@ -93,7 +84,7 @@ class UserProfileInline(admin.StackedInline):
if request.user.is_superuser: if request.user.is_superuser:
query |= Q(userprofile__isnull=True) query |= Q(userprofile__isnull=True)
else: else:
query |= Q(character_ownership__user=obj) query |= Q(character_ownership__user=obj)
formset = super().get_formset(request, obj=obj, **kwargs) formset = super().get_formset(request, obj=obj, **kwargs)
def get_kwargs(self, index): def get_kwargs(self, index):
@@ -108,7 +99,6 @@ class UserProfileInline(admin.StackedInline):
return False return False
@admin.display(description="")
def user_profile_pic(obj): def user_profile_pic(obj):
"""profile pic column data for user objects """profile pic column data for user objects
@@ -121,42 +111,46 @@ def user_profile_pic(obj):
'<img src="{}" class="img-circle">', '<img src="{}" class="img-circle">',
user_obj.profile.main_character.portrait_url(size=32) user_obj.profile.main_character.portrait_url(size=32)
) )
return None else:
return None
user_profile_pic.short_description = ''
@admin.display(description="user / main", ordering="username")
def user_username(obj): def user_username(obj):
"""user column data for user objects """user column data for user objects
works for both User objects and objects with `user` as FK to User works for both User objects and objects with `user` as FK to User
To be used for all user based admin lists To be used for all user based admin lists
""" """
link = reverse( link = reverse(
'admin:{}_{}_change'.format( 'admin:{}_{}_change'.format(
obj._meta.app_label, obj._meta.app_label,
type(obj).__name__.lower() type(obj).__name__.lower()
), ),
args=(obj.pk,) args=(obj.pk,)
) )
user_obj = obj.user if hasattr(obj, 'user') else obj user_obj = obj.user if hasattr(obj, 'user') else obj
if user_obj.profile.main_character: if user_obj.profile.main_character:
return format_html( return format_html(
'<strong><a href="{}">{}</a></strong><br>{}', '<strong><a href="{}">{}</a></strong><br>{}',
link, link,
user_obj.username, user_obj.username,
user_obj.profile.main_character.character_name user_obj.profile.main_character.character_name
) )
return format_html( else:
'<strong><a href="{}">{}</a></strong>', return format_html(
link, '<strong><a href="{}">{}</a></strong>',
user_obj.username, link,
) user_obj.username,
)
user_username.short_description = 'user / main'
user_username.admin_order_field = 'username'
@admin.display(
description="Corporation / Alliance (Main)",
ordering="profile__main_character__corporation_name"
)
def user_main_organization(obj): def user_main_organization(obj):
"""main organization column data for user objects """main organization column data for user objects
@@ -165,13 +159,23 @@ def user_main_organization(obj):
""" """
user_obj = obj.user if hasattr(obj, 'user') else obj user_obj = obj.user if hasattr(obj, 'user') else obj
if not user_obj.profile.main_character: if not user_obj.profile.main_character:
return '' result = None
result = user_obj.profile.main_character.corporation_name else:
if user_obj.profile.main_character.alliance_id: corporation = user_obj.profile.main_character.corporation_name
result += f'<br>{user_obj.profile.main_character.alliance_name}' if user_obj.profile.main_character.alliance_id:
elif user_obj.profile.main_character.faction_name: result = format_html(
result += f'<br>{user_obj.profile.main_character.faction_name}' '{}<br>{}',
return format_html(result) corporation,
user_obj.profile.main_character.alliance_name
)
else:
result = corporation
return result
user_main_organization.short_description = 'Corporation / Alliance (Main)'
user_main_organization.admin_order_field = \
'profile__main_character__corporation_name'
class MainCorporationsFilter(admin.SimpleListFilter): class MainCorporationsFilter(admin.SimpleListFilter):
@@ -190,20 +194,22 @@ class MainCorporationsFilter(admin.SimpleListFilter):
.distinct()\ .distinct()\
.order_by(Lower('corporation_name')) .order_by(Lower('corporation_name'))
return tuple( return tuple(
(x['corporation_id'], x['corporation_name']) for x in qs [(x['corporation_id'], x['corporation_name']) for x in qs]
) )
def queryset(self, request, qs): def queryset(self, request, qs):
if self.value() is None: if self.value() is None:
return qs.all() return qs.all()
if qs.model == User: else:
return qs.filter( if qs.model == User:
profile__main_character__corporation_id=self.value() return qs.filter(
) profile__main_character__corporation_id=self.value()
return qs.filter( )
user__profile__main_character__corporation_id=self.value() else:
) return qs.filter(
user__profile__main_character__corporation_id=self.value()
)
class MainAllianceFilter(admin.SimpleListFilter): class MainAllianceFilter(admin.SimpleListFilter):
"""Custom filter to filter on alliances from mains only """Custom filter to filter on alliances from mains only
@@ -215,62 +221,29 @@ class MainAllianceFilter(admin.SimpleListFilter):
parameter_name = 'main_alliance_id__exact' parameter_name = 'main_alliance_id__exact'
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
qs = ( qs = EveCharacter.objects\
EveCharacter.objects .exclude(alliance_id=None)\
.exclude(alliance_id=None) .exclude(userprofile=None)\
.exclude(userprofile=None) .values('alliance_id', 'alliance_name')\
.values('alliance_id', 'alliance_name') .distinct()\
.distinct()
.order_by(Lower('alliance_name')) .order_by(Lower('alliance_name'))
)
return tuple( return tuple(
(x['alliance_id'], x['alliance_name']) for x in qs [(x['alliance_id'], x['alliance_name']) for x in qs]
) )
def queryset(self, request, qs): def queryset(self, request, qs):
if self.value() is None: if self.value() is None:
return qs.all() return qs.all()
if qs.model == User: else:
return qs.filter(profile__main_character__alliance_id=self.value()) if qs.model == User:
return qs.filter( return qs.filter(profile__main_character__alliance_id=self.value())
user__profile__main_character__alliance_id=self.value() else:
) return qs.filter(
user__profile__main_character__alliance_id=self.value()
)
def update_main_character_model(modeladmin, request, queryset):
class MainFactionFilter(admin.SimpleListFilter):
"""Custom filter to filter on factions from mains only
works for both User objects and objects with `user` as FK to User
To be used for all user based admin lists
"""
title = 'faction'
parameter_name = 'main_faction_id__exact'
def lookups(self, request, model_admin):
qs = (
EveCharacter.objects
.exclude(faction_id=None)
.exclude(userprofile=None)
.values('faction_id', 'faction_name')
.distinct()
.order_by(Lower('faction_name'))
)
return tuple(
(x['faction_id'], x['faction_name']) for x in qs
)
def queryset(self, request, qs):
if self.value() is None:
return qs.all()
if qs.model == User:
return qs.filter(profile__main_character__faction_id=self.value())
return qs.filter(
user__profile__main_character__faction_id=self.value()
)
@admin.display(description="Update main character model from ESI")
def update_main_character_model(modeladmin, request, queryset):
tasks_count = 0 tasks_count = 0
for obj in queryset: for obj in queryset:
if obj.profile.main_character: if obj.profile.main_character:
@@ -278,75 +251,36 @@ def update_main_character_model(modeladmin, request, queryset):
tasks_count += 1 tasks_count += 1
modeladmin.message_user( modeladmin.message_user(
request, f'Update from ESI started for {tasks_count} characters' request,
'Update from ESI started for {} characters'.format(tasks_count)
) )
update_main_character_model.short_description = \
'Update main character model from ESI'
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin):
"""Extending Django's UserAdmin model """Extending Django's UserAdmin model
Behavior of groups and characters columns can be configured via settings Behavior of groups and characters columns can be configured via settings
""" """
inlines = BaseUserAdmin.inlines + [UserProfileInline]
ordering = ('username', )
list_select_related = ('profile__state', 'profile__main_character')
show_full_result_count = True
list_display = (
user_profile_pic,
user_username,
'_state',
'_groups',
user_main_organization,
'_characters',
'is_active',
'date_joined',
'_role'
)
list_display_links = None
list_filter = (
'profile__state',
'groups',
MainCorporationsFilter,
MainAllianceFilter,
MainFactionFilter,
'is_active',
'date_joined',
'is_staff',
'is_superuser'
)
search_fields = ('username', 'character_ownerships__character__character_name')
readonly_fields = ('date_joined', 'last_login')
filter_horizontal = ('groups', 'user_permissions',)
form = UserChangeForm
class Media: class Media:
css = { css = {
"all": ("authentication/css/admin.css",) "all": ("authentication/css/admin.css",)
} }
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) qs = super().get_queryset(request)
return qs.prefetch_related("character_ownerships__character", "groups") return qs.prefetch_related("character_ownerships__character", "groups")
def get_form(self, request, obj=None, **kwargs):
"""Inject current request into change form object."""
MyForm = super().get_form(request, obj, **kwargs)
if obj:
class MyFormInjected(MyForm):
def __new__(cls, *args, **kwargs):
kwargs['request'] = request
return MyForm(*args, **kwargs)
return MyFormInjected
return MyForm
def get_actions(self, request): def get_actions(self, request):
actions = super().get_actions(request) actions = super(BaseUserAdmin, self).get_actions(request)
actions[update_main_character_model.__name__] = ( actions[update_main_character_model.__name__] = (
update_main_character_model, update_main_character_model,
update_main_character_model.__name__, update_main_character_model.__name__,
update_main_character_model.short_description update_main_character_model.short_description
) )
@@ -356,21 +290,21 @@ class UserAdmin(BaseUserAdmin):
if svc.update_groups.__module__ != ServicesHook.update_groups.__module__: if svc.update_groups.__module__ != ServicesHook.update_groups.__module__:
action = make_service_hooks_update_groups_action(svc) action = make_service_hooks_update_groups_action(svc)
actions[action.__name__] = ( actions[action.__name__] = (
action, action,
action.__name__, action.__name__,
action.short_description action.short_description
) )
# Create sync nickname action if service implements it # Create sync nickname action if service implements it
if svc.sync_nickname.__module__ != ServicesHook.sync_nickname.__module__: if svc.sync_nickname.__module__ != ServicesHook.sync_nickname.__module__:
action = make_service_hooks_sync_nickname_action(svc) action = make_service_hooks_sync_nickname_action(svc)
actions[action.__name__] = ( actions[action.__name__] = (
action, action.__name__, action, action.__name__,
action.short_description action.short_description
) )
return actions return actions
def _list_2_html_w_tooltips(self, my_items: list, max_items: int) -> str: def _list_2_html_w_tooltips(self, my_items: list, max_items: int) -> str:
"""converts list of strings into HTML with cutoff and tooltip""" """converts list of strings into HTML with cutoff and tooltip"""
items_truncated_str = ', '.join(my_items[:max_items]) items_truncated_str = ', '.join(my_items[:max_items])
if not my_items: if not my_items:
@@ -386,24 +320,61 @@ class UserAdmin(BaseUserAdmin):
items_truncated_str items_truncated_str
) )
return result return result
inlines = BaseUserAdmin.inlines + [UserProfileInline]
ordering = ('username', )
list_select_related = ('profile__state', 'profile__main_character')
show_full_result_count = True
list_display = (
user_profile_pic,
user_username,
'_state',
'_groups',
user_main_organization,
'_characters',
'is_active',
'date_joined',
'_role'
)
list_display_links = None
list_filter = (
'profile__state',
'groups',
MainCorporationsFilter,
MainAllianceFilter,
'is_active',
'date_joined',
'is_staff',
'is_superuser'
)
search_fields = (
'username',
'character_ownerships__character__character_name'
)
def _characters(self, obj): def _characters(self, obj):
character_ownerships = list(obj.character_ownerships.all()) character_ownerships = list(obj.character_ownerships.all())
characters = [obj.character.character_name for obj in character_ownerships] characters = [obj.character.character_name for obj in character_ownerships]
return self._list_2_html_w_tooltips( return self._list_2_html_w_tooltips(
sorted(characters), sorted(characters),
AUTHENTICATION_ADMIN_USERS_MAX_CHARS AUTHENTICATION_ADMIN_USERS_MAX_CHARS
) )
@admin.display(ordering="profile__state") _characters.short_description = 'characters'
def _state(self, obj): def _state(self, obj):
return obj.profile.state.name return obj.profile.state.name
_state.short_description = 'state'
_state.admin_order_field = 'profile__state'
def _groups(self, obj): def _groups(self, obj):
my_groups = sorted(group.name for group in list(obj.groups.all())) my_groups = sorted([group.name for group in list(obj.groups.all())])
return self._list_2_html_w_tooltips( return self._list_2_html_w_tooltips(
my_groups, AUTHENTICATION_ADMIN_USERS_MAX_GROUPS my_groups, AUTHENTICATION_ADMIN_USERS_MAX_GROUPS
) )
_groups.short_description = 'groups'
def _role(self, obj): def _role(self, obj):
if obj.is_superuser: if obj.is_superuser:
@@ -411,9 +382,11 @@ class UserAdmin(BaseUserAdmin):
elif obj.is_staff: elif obj.is_staff:
role = 'Staff' role = 'Staff'
else: else:
role = 'User' role = 'User'
return role return role
_role.short_description = 'role'
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
return request.user.has_perm('auth.change_user') return request.user.has_perm('auth.change_user')
@@ -423,41 +396,26 @@ class UserAdmin(BaseUserAdmin):
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
return request.user.has_perm('auth.delete_user') return request.user.has_perm('auth.delete_user')
def get_object(self, *args , **kwargs):
obj = super().get_object(*args , **kwargs)
self.obj = obj # storing current object for use in formfield_for_manytomany
return obj
def formfield_for_manytomany(self, db_field, request, **kwargs): def formfield_for_manytomany(self, db_field, request, **kwargs):
"""overriding this formfield to have sorted lists in the form"""
if db_field.name == "groups": if db_field.name == "groups":
groups_qs = Group.objects.filter(authgroup__states__isnull=True) kwargs["queryset"] = Group.objects.all().order_by(Lower('name'))
obj_state = self.obj.profile.state
if obj_state:
matching_groups_qs = Group.objects.filter(authgroup__states=obj_state)
groups_qs = groups_qs | matching_groups_qs
kwargs["queryset"] = groups_qs.order_by(Lower("name"))
return super().formfield_for_manytomany(db_field, request, **kwargs) return super().formfield_for_manytomany(db_field, request, **kwargs)
def get_readonly_fields(self, request, obj=None):
if obj and not request.user.is_superuser:
return self.readonly_fields + (
"is_staff", "is_superuser", "user_permissions"
)
return self.readonly_fields
@admin.register(State) @admin.register(State)
class StateAdmin(admin.ModelAdmin): class StateAdmin(admin.ModelAdmin):
list_select_related = True list_select_related = True
list_display = ('name', 'priority', '_user_count') list_display = ('name', 'priority', '_user_count')
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) qs = super().get_queryset(request)
return qs.annotate(user_count=Count("userprofile__id")) return qs.annotate(user_count=Count("userprofile__id"))
@admin.display(description="Users", ordering="user_count")
def _user_count(self, obj): def _user_count(self, obj):
return obj.user_count return obj.user_count
_user_count.short_description = 'Users'
_user_count.admin_order_field = 'user_count'
fieldsets = ( fieldsets = (
(None, { (None, {
@@ -465,24 +423,22 @@ class StateAdmin(admin.ModelAdmin):
}), }),
('Membership', { ('Membership', {
'fields': ( 'fields': (
'public', 'public',
'member_characters', 'member_characters',
'member_corporations', 'member_corporations',
'member_alliances', 'member_alliances'
'member_factions'
), ),
}) })
) )
filter_horizontal = [ filter_horizontal = [
'member_characters', 'member_characters',
'member_corporations', 'member_corporations',
'member_alliances', 'member_alliances',
'member_factions',
'permissions' 'permissions'
] ]
def formfield_for_manytomany(self, db_field, request, **kwargs): def formfield_for_manytomany(self, db_field, request, **kwargs):
"""overriding this formfield to have sorted lists in the form""" """overriding this formfield to have sorted lists in the form"""
if db_field.name == "member_characters": if db_field.name == "member_characters":
kwargs["queryset"] = EveCharacter.objects.all()\ kwargs["queryset"] = EveCharacter.objects.all()\
.order_by(Lower('character_name')) .order_by(Lower('character_name'))
@@ -492,17 +448,14 @@ 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 == "member_factions":
kwargs["queryset"] = EveFactionInfo.objects.all()\
.order_by(Lower('faction_name'))
elif db_field.name == "permissions": elif db_field.name == "permissions":
kwargs["queryset"] = Permission.objects.select_related("content_type").all() 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):
if obj == get_guest_state(): if obj == get_guest_state():
return False return False
return super().has_delete_permission(request, obj=obj) return super(StateAdmin, self).has_delete_permission(request, obj=obj)
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):
if obj == get_guest_state(): if obj == get_guest_state():
@@ -511,17 +464,17 @@ class StateAdmin(admin.ModelAdmin):
'fields': ('permissions', 'priority'), 'fields': ('permissions', 'priority'),
}), }),
) )
return super().get_fieldsets(request, obj=obj) return super(StateAdmin, self).get_fieldsets(request, obj=obj)
def get_readonly_fields(self, request, obj=None):
if not request.user.is_superuser:
return self.readonly_fields + ("permissions",)
return self.readonly_fields
class BaseOwnershipAdmin(admin.ModelAdmin): class BaseOwnershipAdmin(admin.ModelAdmin):
class Media:
css = {
"all": ("authentication/css/admin.css",)
}
list_select_related = ( list_select_related = (
'user__profile__state', 'user__profile__main_character', 'character') 'user__profile__state', 'user__profile__main_character', 'character')
list_display = ( list_display = (
user_profile_pic, user_profile_pic,
user_username, user_username,
@@ -529,22 +482,16 @@ class BaseOwnershipAdmin(admin.ModelAdmin):
'character', 'character',
) )
search_fields = ( search_fields = (
'user__username', 'user__username',
'character__character_name', 'character__character_name',
'character__corporation_name', 'character__corporation_name',
'character__alliance_name', 'character__alliance_name'
'character__faction_name'
) )
list_filter = ( list_filter = (
MainCorporationsFilter, MainCorporationsFilter,
MainAllianceFilter, MainAllianceFilter,
) )
class Media:
css = {
"all": ("authentication/css/admin.css",)
}
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
if obj and obj.pk: if obj and obj.pk:
return 'owner_hash', 'character' return 'owner_hash', 'character'

View File

@@ -2,14 +2,14 @@ from django.conf import settings
def _clean_setting( def _clean_setting(
name: str, name: str,
default_value: object, default_value: object,
min_value: int = None, min_value: int = None,
max_value: int = None, max_value: int = None,
required_type: type = None required_type: type = None
): ):
"""cleans the input for a custom setting """cleans the input for a custom setting
Will use `default_value` if settings does not exit or has the wrong type Will use `default_value` if settings does not exit or has the wrong type
or is outside define boundaries (for int only) or is outside define boundaries (for int only)
@@ -18,22 +18,22 @@ def _clean_setting(
Will assume `min_value` of 0 for int (can be overriden) Will assume `min_value` of 0 for int (can be overriden)
Returns cleaned value for setting Returns cleaned value for setting
""" """
if default_value is None and not required_type: if default_value is None and not required_type:
raise ValueError('You must specify a required_type for None defaults') raise ValueError('You must specify a required_type for None defaults')
if not required_type: if not required_type:
required_type = type(default_value) required_type = type(default_value)
if min_value is None and required_type == int: if min_value is None and required_type == int:
min_value = 0 min_value = 0
if (hasattr(settings, name) if (hasattr(settings, name)
and isinstance(getattr(settings, name), required_type) and isinstance(getattr(settings, name), required_type)
and (min_value is None or getattr(settings, name) >= min_value) and (min_value is None or getattr(settings, name) >= min_value)
and (max_value is None or getattr(settings, name) <= max_value) and (max_value is None or getattr(settings, name) <= max_value)
): ):
return getattr(settings, name) return getattr(settings, name)
else: else:
return default_value return default_value
@@ -43,3 +43,4 @@ 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

@@ -3,14 +3,10 @@ from django.core.checks import register, Tags
class AuthenticationConfig(AppConfig): class AuthenticationConfig(AppConfig):
name = "allianceauth.authentication" name = 'allianceauth.authentication'
label = "authentication" label = 'authentication'
def ready(self): def ready(self):
from allianceauth.authentication import checks, signals # noqa: F401 super(AuthenticationConfig, self).ready()
from allianceauth.authentication.task_statistics import ( from allianceauth.authentication import checks, signals
signals as celery_signals,
)
register(Tags.security)(checks.check_login_scopes_setting) register(Tags.security)(checks.check_login_scopes_setting)
celery_signals.reset_counters()

View File

@@ -12,9 +12,9 @@ logger = logging.getLogger(__name__)
class StateBackend(ModelBackend): class StateBackend(ModelBackend):
@staticmethod @staticmethod
def _get_state_permissions(user_obj): def _get_state_permissions(user_obj):
"""returns permissions for state of given user object""" """returns permissions for state of given user object"""
if hasattr(user_obj, "profile") and user_obj.profile: if hasattr(user_obj, "profile") and user_obj.profile:
return Permission.objects.filter(state=user_obj.profile.state) return Permission.objects.filter(state=user_obj.profile.state)
else: else:
return Permission.objects.none() return Permission.objects.none()
@@ -36,17 +36,17 @@ class StateBackend(ModelBackend):
try: try:
ownership = CharacterOwnership.objects.get(character__character_id=token.character_id) ownership = CharacterOwnership.objects.get(character__character_id=token.character_id)
if ownership.owner_hash == token.character_owner_hash: if ownership.owner_hash == token.character_owner_hash:
logger.debug(f'Authenticating {ownership.user} by ownership of character {token.character_name}') logger.debug('Authenticating {0} by ownership of character {1}'.format(ownership.user, token.character_name))
return ownership.user return ownership.user
else: else:
logger.debug(f'{token.character_name} has changed ownership. Creating new user account.') logger.debug('{0} has changed ownership. Creating new user account.'.format(token.character_name))
ownership.delete() ownership.delete()
return self.create_user(token) return self.create_user(token)
except CharacterOwnership.DoesNotExist: except CharacterOwnership.DoesNotExist:
try: try:
# insecure legacy main check for pre-sso registration auth installs # insecure legacy main check for pre-sso registration auth installs
profile = UserProfile.objects.get(main_character__character_id=token.character_id) profile = UserProfile.objects.get(main_character__character_id=token.character_id)
logger.debug(f'Authenticating {profile.user} by their main character {profile.main_character} without active ownership.') logger.debug('Authenticating {0} by their main character {1} without active ownership.'.format(profile.user, profile.main_character))
# attach an ownership # attach an ownership
token.user = profile.user token.user = profile.user
CharacterOwnership.objects.create_by_token(token) CharacterOwnership.objects.create_by_token(token)
@@ -59,13 +59,13 @@ class StateBackend(ModelBackend):
user = records[0].user user = records[0].user
token.user = user token.user = user
co = CharacterOwnership.objects.create_by_token(token) co = CharacterOwnership.objects.create_by_token(token)
logger.debug(f'Authenticating {user} by matching owner hash record of character {co.character}') logger.debug('Authenticating {0} by matching owner hash record of character {1}'.format(user, co.character))
if not user.profile.main_character: if not user.profile.main_character:
# set this as their main by default if they have none # set this as their main by default if they have none
user.profile.main_character = co.character user.profile.main_character = co.character
user.profile.save() user.profile.save()
return user return user
logger.debug(f'Unable to authenticate character {token.character_name}. Creating new user.') logger.debug('Unable to authenticate character {0}. Creating new user.'.format(token.character_name))
return self.create_user(token) return self.create_user(token)
def create_user(self, token): def create_user(self, token):
@@ -77,7 +77,7 @@ class StateBackend(ModelBackend):
co = CharacterOwnership.objects.create_by_token(token) # assign ownership to this user co = CharacterOwnership.objects.create_by_token(token) # assign ownership to this user
user.profile.main_character = co.character # assign main character as token character user.profile.main_character = co.character # assign main character as token character
user.profile.save() user.profile.save()
logger.debug(f'Created new user {user}') logger.debug('Created new user {0}'.format(user))
return user return user
@staticmethod @staticmethod
@@ -87,10 +87,10 @@ class StateBackend(ModelBackend):
if User.objects.filter(username__startswith=name).exists(): if User.objects.filter(username__startswith=name).exists():
u = User.objects.filter(username__startswith=name) u = User.objects.filter(username__startswith=name)
num = len(u) num = len(u)
username = f"{name}_{num}" username = "%s_%s" % (name, num)
while u.filter(username=username).exists(): while u.filter(username=username).exists():
num += 1 num += 1
username = f"{name}_{num}" username = "%s_%s" % (name, num)
else: else:
username = name username = name
return username return username

View File

@@ -1,66 +1,8 @@
from django import forms from django import forms
from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import Group
from django.core.exceptions import ValidationError
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from allianceauth.authentication.models import User from allianceauth.authentication.models import User
class RegistrationForm(forms.Form): class RegistrationForm(forms.Form):
email = forms.EmailField(label=_('Email'), max_length=254, required=True) email = forms.EmailField(label=_('Email'), max_length=254, required=True)
class _meta: class _meta:
model = User model = User
class UserProfileForm(ModelForm):
"""Allows specifying FK querysets through kwarg"""
def __init__(self, querysets=None, *args, **kwargs):
querysets = querysets or {}
super().__init__(*args, **kwargs)
for field, qs in querysets.items():
self.fields[field].queryset = qs
class UserChangeForm(BaseUserChangeForm):
"""Add custom cleaning to UserChangeForm"""
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request") # Inject current request into form object
super().__init__(*args, **kwargs)
def clean(self):
cleaned_data = super().clean()
if not self.request.user.is_superuser:
if self.instance:
current_restricted = set(
self.instance.groups.filter(
authgroup__restricted=True
).values_list("pk", flat=True)
)
else:
current_restricted = set()
new_restricted = set(
cleaned_data["groups"].filter(
authgroup__restricted=True
).values_list("pk", flat=True)
)
if current_restricted != new_restricted:
restricted_removed = current_restricted - new_restricted
restricted_added = new_restricted - current_restricted
restricted_changed = restricted_removed | restricted_added
restricted_names_qs = Group.objects.filter(
pk__in=restricted_changed
).values_list("name", flat=True)
restricted_names = ",".join(list(restricted_names_qs))
raise ValidationError(
{
"groups": _(
"You are not allowed to add or remove these "
"restricted groups: %s" % restricted_names
)
}
)

View File

@@ -11,4 +11,4 @@ urlpatterns = [
url(r'^register/complete/$', views.registration_complete, name='registration_complete'), url(r'^register/complete/$', views.registration_complete, name='registration_complete'),
url(r'^register/closed/$', views.registration_closed, name='registration_disallowed'), url(r'^register/closed/$', views.registration_closed, name='registration_disallowed'),
url(r'', include('django.contrib.auth.urls')), url(r'', include('django.contrib.auth.urls')),
] ]

View File

@@ -11,10 +11,10 @@ class Command(BaseCommand):
if profiles.exists(): if profiles.exists():
for profile in profiles: for profile in profiles:
self.stdout.write(self.style.ERROR( self.stdout.write(self.style.ERROR(
'{} does not have an ownership. Resetting user {} main character.'.format(profile.main_character, '{0} does not have an ownership. Resetting user {1} main character.'.format(profile.main_character,
profile.user))) profile.user)))
profile.main_character = None profile.main_character = None
profile.save() profile.save()
self.stdout.write(self.style.WARNING(f'Reset {profiles.count()} main characters.')) self.stdout.write(self.style.WARNING('Reset {0} main characters.'.format(profiles.count())))
else: else:
self.stdout.write(self.style.SUCCESS('All main characters have active ownership.')) self.stdout.write(self.style.SUCCESS('All main characters have active ownership.'))

View File

@@ -16,8 +16,6 @@ def available_states_query(character):
query |= Q(member_corporations__corporation_id=character.corporation_id) query |= Q(member_corporations__corporation_id=character.corporation_id)
if character.alliance_id: if character.alliance_id:
query |= Q(member_alliances__alliance_id=character.alliance_id) query |= Q(member_alliances__alliance_id=character.alliance_id)
if character.faction_id:
query |= Q(member_factions__faction_id=character.faction_id)
return query return query
@@ -25,7 +23,8 @@ 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, owner_hash=token.character_owner_hash) return self.create(character=EveCharacter.objects.get(character_id=token.character_id), user=token.user,
owner_hash=token.character_owner_hash)
class StateQuerySet(QuerySet): class StateQuerySet(QuerySet):
@@ -51,7 +50,7 @@ class StateQuerySet(QuerySet):
for state in self: for state in self:
for profile in state.userprofile_set.all(): for profile in state.userprofile_set.all():
profile.assign_state(state=self.model.objects.exclude(pk=state.pk).get_for_user(profile.user)) profile.assign_state(state=self.model.objects.exclude(pk=state.pk).get_for_user(profile.user))
super().delete() super(StateQuerySet, self).delete()
class StateManager(Manager): class StateManager(Manager):

View File

@@ -1,4 +1,6 @@
# -*- 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 django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations from django.db import migrations

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations, models from django.db import migrations, models

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations from django.db import migrations

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations from django.db import migrations
@@ -15,7 +17,7 @@ def create_permissions(apps, schema_editor):
Permission = apps.get_model('auth', 'Permission') Permission = apps.get_model('auth', 'Permission')
ct = ContentType.objects.get_for_model(User) ct = ContentType.objects.get_for_model(User)
Permission.objects.get_or_create(codename="member", content_type=ct, name="member") Permission.objects.get_or_create(codename="member", content_type=ct, name="member")
Permission.objects.get_or_create(codename="blue_member", content_type=ct, name="blue_member") Permission.objects.get_or_create(codename="blue_member", content_type=ct, name="blue_member")
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations from django.db import migrations

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations from django.db import migrations

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations from django.db import migrations

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations, models from django.db import migrations, models

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations from django.db import migrations
@@ -8,7 +10,7 @@ def count_completed_fields(model):
def forward(apps, schema_editor): def forward(apps, schema_editor):
# this ensures only one model exists per user # this ensures only one model exists per user
AuthServicesInfo = apps.get_model('authentication', 'AuthServicesInfo') AuthServicesInfo = apps.get_model('authentication', 'AuthServicesInfo')
users = {a.user for a in AuthServicesInfo.objects.all()} users = set([a.user for a in AuthServicesInfo.objects.all()])
for u in users: for u in users:
auths = AuthServicesInfo.objects.filter(user=u) auths = AuthServicesInfo.objects.filter(user=u)
if auths.count() > 1: if auths.count() > 1:

View File

@@ -1,4 +1,6 @@
# -*- 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 django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations, models from django.db import migrations, models

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations from django.db import migrations

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations from django.db import migrations

View File

@@ -1,4 +1,6 @@
# -*- 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
import allianceauth.authentication.models import allianceauth.authentication.models
import django.db.models.deletion import django.db.models.deletion
@@ -105,8 +107,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:
@@ -169,7 +171,8 @@ 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, defaults={'main_char_id': profile.main_character.character_id}) AuthServicesInfo.objects.update_or_create(user=profile.user,
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,3 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations from django.db import migrations

View File

@@ -1,18 +0,0 @@
# Generated by Django 3.2.8 on 2021-10-20 05:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0017_remove_fleetup_permission'),
]
operations = [
migrations.AlterField(
model_name='state',
name='name',
field=models.CharField(max_length=32, unique=True),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 3.1.13 on 2021-10-12 20:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('eveonline', '0015_factions'),
('authentication', '0017_remove_fleetup_permission'),
]
operations = [
migrations.AddField(
model_name='state',
name='member_factions',
field=models.ManyToManyField(blank=True, help_text='Factions to whose members this state is available.', to='eveonline.EveFactionInfo'),
),
]

View File

@@ -1,14 +0,0 @@
# Generated by Django 3.2.8 on 2021-10-26 09:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentication', '0018_alter_state_name_length'),
('authentication', '0018_state_member_factions'),
]
operations = [
]

View File

@@ -3,7 +3,7 @@ import logging
from django.contrib.auth.models import User, Permission from django.contrib.auth.models import User, Permission
from django.db import models, transaction from django.db import models, transaction
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from allianceauth.notifications import notify from allianceauth.notifications import notify
from .managers import CharacterOwnershipManager, StateManager from .managers import CharacterOwnershipManager, StateManager
@@ -12,9 +12,10 @@ logger = logging.getLogger(__name__)
class State(models.Model): class State(models.Model):
name = models.CharField(max_length=32, 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, help_text="Users get assigned the state with the highest priority available to them.") priority = models.IntegerField(unique=True,
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.")
@@ -22,8 +23,6 @@ class State(models.Model):
help_text="Corporations to whose members this state is available.") help_text="Corporations to whose members this state is available.")
member_alliances = models.ManyToManyField(EveAllianceInfo, blank=True, member_alliances = models.ManyToManyField(EveAllianceInfo, blank=True,
help_text="Alliances to whose members this state is available.") help_text="Alliances to whose members this state is available.")
member_factions = models.ManyToManyField(EveFactionInfo, blank=True,
help_text="Factions 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()
@@ -44,7 +43,7 @@ class State(models.Model):
with transaction.atomic(): with transaction.atomic():
for profile in self.userprofile_set.all(): for profile in self.userprofile_set.all():
profile.assign_state(state=State.objects.exclude(pk=self.pk).get_for_user(profile.user)) profile.assign_state(state=State.objects.exclude(pk=self.pk).get_for_user(profile.user))
super().delete(**kwargs) super(State, self).delete(**kwargs)
def get_guest_state(): def get_guest_state():
@@ -72,21 +71,16 @@ class UserProfile(models.Model):
if self.state != state: if self.state != state:
self.state = state self.state = state
if commit: if commit:
logger.info(f'Updating {self.user} state to {self.state}') logger.info('Updating {} state to {}'.format(self.user, self.state))
self.save(update_fields=['state']) self.save(update_fields=['state'])
notify( notify(
self.user, self.user,
_('State changed to: %s' % state), _('State changed to: %s' % state),
_('Your user\'s state is now: %(state)s') _('Your user\'s state is now: %(state)s')
% ({'state': state}), % ({'state': state}),
'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
) )
@@ -107,7 +101,7 @@ class CharacterOwnership(models.Model):
objects = CharacterOwnershipManager() objects = CharacterOwnershipManager()
def __str__(self): def __str__(self):
return f"{self.user}: {self.character}" return "%s: %s" % (self.user, self.character)
class OwnershipRecord(models.Model): class OwnershipRecord(models.Model):
@@ -120,4 +114,4 @@ class OwnershipRecord(models.Model):
ordering = ['-created'] ordering = ['-created']
def __str__(self): def __str__(self):
return f"{self.user}: {self.character} on {self.created}" return "%s: %s on %s" % (self.user, self.character, self.created)

View File

@@ -29,32 +29,27 @@ def trigger_state_check(state):
@receiver(m2m_changed, sender=State.member_characters.through) @receiver(m2m_changed, sender=State.member_characters.through)
def state_member_characters_changed(sender, instance, action, *args, **kwargs): def state_member_characters_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'): if action.startswith('post_'):
logger.debug(f'State {instance} member characters changed. Re-evaluating membership.') logger.debug('State {} member characters changed. Re-evaluating membership.'.format(instance))
trigger_state_check(instance) trigger_state_check(instance)
@receiver(m2m_changed, sender=State.member_corporations.through) @receiver(m2m_changed, sender=State.member_corporations.through)
def state_member_corporations_changed(sender, instance, action, *args, **kwargs): def state_member_corporations_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'): if action.startswith('post_'):
logger.debug(f'State {instance} member corporations changed. Re-evaluating membership.') logger.debug('State {} member corporations changed. Re-evaluating membership.'.format(instance))
trigger_state_check(instance) trigger_state_check(instance)
@receiver(m2m_changed, sender=State.member_alliances.through) @receiver(m2m_changed, sender=State.member_alliances.through)
def state_member_alliances_changed(sender, instance, action, *args, **kwargs): def state_member_alliances_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'): if action.startswith('post_'):
logger.debug(f'State {instance} member alliances changed. Re-evaluating membership.') logger.debug('State {} member alliances changed. Re-evaluating membership.'.format(instance))
trigger_state_check(instance) trigger_state_check(instance)
@receiver(m2m_changed, sender=State.member_factions.through)
def state_member_factions_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'):
logger.debug(f'State {instance} member factions changed. Re-evaluating membership.')
trigger_state_check(instance)
@receiver(post_save, sender=State) @receiver(post_save, sender=State)
def state_saved(sender, instance, *args, **kwargs): def state_saved(sender, instance, *args, **kwargs):
logger.debug(f'State {instance} saved. Re-evaluating membership.') logger.debug('State {} saved. Re-evaluating membership.'.format(instance))
trigger_state_check(instance) trigger_state_check(instance)
@@ -65,7 +60,7 @@ def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
if not created: if not created:
update_fields = kwargs.pop('update_fields', []) or [] update_fields = kwargs.pop('update_fields', []) or []
if 'state' not in update_fields: if 'state' not in update_fields:
logger.debug(f'Profile for {instance.user} saved without state change. Re-evaluating state.') logger.debug('Profile for {} saved without state change. Re-evaluating state.'.format(instance.user))
instance.assign_state() instance.assign_state()
@@ -73,14 +68,15 @@ def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
def create_required_models(sender, instance, created, *args, **kwargs): def create_required_models(sender, instance, created, *args, **kwargs):
# ensure all users have a model # ensure all users have a model
if created: if created:
logger.debug(f'User {instance} created. Creating default UserProfile.') logger.debug('User {} created. Creating default UserProfile.'.format(instance))
UserProfile.objects.get_or_create(user=instance) UserProfile.objects.get_or_create(user=instance)
@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(f'New token for {instance.user} character {instance.character_name} saved. Evaluating ownership.') logger.debug('New token for {0} character {1} saved. Evaluating ownership.'.format(instance.user,
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:
@@ -89,21 +85,25 @@ 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(f'Token is for a new character. Creating model for {instance.character_name} ({instance.character_id})') logger.debug('Token is for a new character. Creating model for {0} ({1})'.format(instance.character_name,
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(f"Character {instance.character_name} is not yet owned. Assigning ownership to {instance.user}") logger.debug("Character {0} is not yet owned. Assigning ownership to {1}".format(instance.character_name,
CharacterOwnership.objects.update_or_create(character=char, defaults={'owner_hash': instance.character_owner_hash, 'user': instance.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)
def validate_main_character(sender, instance, *args, **kwargs): def validate_main_character(sender, instance, *args, **kwargs):
try: try:
if instance.user.profile.main_character == instance.character: if instance.user.profile.main_character == instance.character:
logger.info("Ownership of a main character {} has been revoked. Resetting {} main character.".format( logger.info("Ownership of a main character {0} has been revoked. Resetting {1} main character.".format(
instance.character, instance.user)) instance.character, instance.user))
# clear main character as user no longer owns them # clear main character as user no longer owns them
instance.user.profile.main_character = None instance.user.profile.main_character = None
@@ -116,7 +116,7 @@ def validate_main_character(sender, instance, *args, **kwargs):
@receiver(post_delete, sender=Token) @receiver(post_delete, sender=Token)
def validate_ownership(sender, instance, *args, **kwargs): def validate_ownership(sender, instance, *args, **kwargs):
if not Token.objects.filter(character_owner_hash=instance.character_owner_hash).filter(refresh_token__isnull=False).exists(): if not Token.objects.filter(character_owner_hash=instance.character_owner_hash).filter(refresh_token__isnull=False).exists():
logger.info(f"No remaining tokens to validate ownership of character {instance.character_name}. Revoking ownership.") logger.info("No remaining tokens to validate ownership of character {0}. Revoking ownership.".format(instance.character_name))
CharacterOwnership.objects.filter(owner_hash=instance.character_owner_hash).delete() CharacterOwnership.objects.filter(owner_hash=instance.character_owner_hash).delete()
@@ -127,11 +127,11 @@ def assign_state_on_active_change(sender, instance, *args, **kwargs):
old_instance = User.objects.get(pk=instance.pk) old_instance = User.objects.get(pk=instance.pk)
if old_instance.is_active != instance.is_active: if old_instance.is_active != instance.is_active:
if instance.is_active: if instance.is_active:
logger.debug(f"User {instance} has been activated. Assigning state.") logger.debug("User {0} has been activated. Assigning state.".format(instance))
instance.profile.assign_state() instance.profile.assign_state()
else: else:
logger.debug( logger.debug(
f"User {instance} has been deactivated. Revoking state and assigning to guest state.") "User {0} has been deactivated. Revoking state and assigning to guest state.".format(instance))
instance.profile.state = get_guest_state() instance.profile.state = get_guest_state()
instance.profile.save(update_fields=['state']) instance.profile.save(update_fields=['state'])
@@ -140,10 +140,10 @@ def assign_state_on_active_change(sender, instance, *args, **kwargs):
def check_state_on_character_update(sender, instance, *args, **kwargs): def check_state_on_character_update(sender, instance, *args, **kwargs):
# if this is a main character updating, check that user's state # if this is a main character updating, check that user's state
try: try:
logger.debug(f"Character {instance} has been saved. Assessing owner's state for changes.") logger.debug("Character {0} has been saved. Assessing owner's state for changes.".format(instance))
instance.userprofile.assign_state() instance.userprofile.assign_state()
except UserProfile.DoesNotExist: except UserProfile.DoesNotExist:
logger.debug(f"Character {instance} is not a main character. No state assessment required.") logger.debug("Character {0} is not a main character. No state assessment required.".format(instance))
pass pass
@@ -153,7 +153,7 @@ def ownership_record_creation(sender, instance, created, *args, **kwargs):
records = OwnershipRecord.objects.filter(owner_hash=instance.owner_hash).filter(character=instance.character) records = OwnershipRecord.objects.filter(owner_hash=instance.owner_hash).filter(character=instance.character)
if records.exists(): if records.exists():
if records[0].user == instance.user: # most recent record is sorted first if records[0].user == instance.user: # most recent record is sorted first
logger.debug(f"Already have ownership record of {instance.character} by user {instance.user}") logger.debug("Already have ownership record of {0} by user {1}".format(instance.character, instance.user))
return return
logger.info(f"Character {instance.character} has a new owner {instance.user}. Creating ownership record.") logger.info("Character {0} has a new owner {1}. Creating ownership record.".format(instance.character, instance.user))
OwnershipRecord.objects.create(user=instance.user, character=instance.character, owner_hash=instance.owner_hash) OwnershipRecord.objects.create(user=instance.user, character=instance.character, owner_hash=instance.owner_hash)

View File

@@ -1,12 +1,12 @@
/* /*
CSS for allianceauth admin site CSS for allianceauth admin site
*/ */
/* styling for profile pic */ /* styling for profile pic */
.img-circle { .img-circle {
border-radius: 50%; border-radius: 50%;
} }
.column-user_profile_pic { .column-user_profile_pic {
width: 1px; width: 1px;
white-space: nowrap; white-space: nowrap;
} }
@@ -26,4 +26,4 @@ CSS for allianceauth admin site
color: black ; color: black ;
background-color: rgb(255, 255, 204) ; background-color: rgb(255, 255, 204) ;
z-index: 1 ; z-index: 1 ;
} }

View File

@@ -1,40 +0,0 @@
from collections import namedtuple
import datetime as dt
from .event_series import EventSeries
"""Global series for counting task events."""
succeeded_tasks = EventSeries("SUCCEEDED_TASKS")
retried_tasks = EventSeries("RETRIED_TASKS")
failed_tasks = EventSeries("FAILED_TASKS")
_TaskCounts = namedtuple(
"_TaskCounts", ["succeeded", "retried", "failed", "total", "earliest_task", "hours"]
)
def dashboard_results(hours: int) -> _TaskCounts:
"""Counts of all task events within the given timeframe."""
def earliest_if_exists(events: EventSeries, earliest: dt.datetime) -> list:
my_earliest = events.first_event(earliest=earliest)
return [my_earliest] if my_earliest else []
earliest = dt.datetime.utcnow() - dt.timedelta(hours=hours)
earliest_events = list()
succeeded_count = succeeded_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(succeeded_tasks, earliest)
retried_count = retried_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(retried_tasks, earliest)
failed_count = failed_tasks.count(earliest=earliest)
earliest_events += earliest_if_exists(failed_tasks, earliest)
return _TaskCounts(
succeeded=succeeded_count,
retried=retried_count,
failed=failed_count,
total=succeeded_count + retried_count + failed_count,
earliest_task=min(earliest_events) if earliest_events else None,
hours=hours,
)

View File

@@ -1,130 +0,0 @@
import datetime as dt
import logging
from typing import List, Optional
from pytz import utc
from redis import Redis, RedisError
from django.core.cache import cache
logger = logging.getLogger(__name__)
class _RedisStub:
"""Stub of a Redis client.
It's purpose is to prevent EventSeries objects from trying to access Redis
when it is not available. e.g. when the Sphinx docs are rendered by readthedocs.org.
"""
def delete(self, *args, **kwargs):
pass
def incr(self, *args, **kwargs):
return 0
def zadd(self, *args, **kwargs):
pass
def zcount(self, *args, **kwargs):
pass
def zrangebyscore(self, *args, **kwargs):
pass
class EventSeries:
"""API for recording and analyzing a series of events."""
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
def __init__(self, key_id: str, redis: Redis = None) -> None:
self._redis = cache.get_master_client() if not redis else redis
try:
if not self._redis.ping():
raise RuntimeError()
except (AttributeError, RedisError, RuntimeError):
logger.exception(
"Failed to establish a connection with Redis. "
"This EventSeries object is disabled.",
)
self._redis = _RedisStub()
self._key_id = str(key_id)
self.clear()
@property
def is_disabled(self):
"""True when this object is disabled, e.g. Redis was not available at startup."""
return isinstance(self._redis, _RedisStub)
@property
def _key_counter(self):
return f"{self._ROOT_KEY}_{self._key_id}_COUNTER"
@property
def _key_sorted_set(self):
return f"{self._ROOT_KEY}_{self._key_id}_SORTED_SET"
def add(self, event_time: dt.datetime = None) -> None:
"""Add event.
Args:
- event_time: timestamp of event. Will use current time if not specified.
"""
if not event_time:
event_time = dt.datetime.utcnow()
id = self._redis.incr(self._key_counter)
self._redis.zadd(self._key_sorted_set, {id: event_time.timestamp()})
def all(self) -> List[dt.datetime]:
"""List of all known events."""
return [
event[1]
for event in self._redis.zrangebyscore(
self._key_sorted_set,
"-inf",
"+inf",
withscores=True,
score_cast_func=self._cast_scores_to_dt,
)
]
def clear(self) -> None:
"""Clear all events."""
self._redis.delete(self._key_sorted_set)
self._redis.delete(self._key_counter)
def count(self, earliest: dt.datetime = None, latest: dt.datetime = None) -> int:
"""Count of events, can be restricted to given timeframe.
Args:
- earliest: Date of first events to count(inclusive), or -infinite if not specified
- latest: Date of last events to count(inclusive), or +infinite if not specified
"""
min = "-inf" if not earliest else earliest.timestamp()
max = "+inf" if not latest else latest.timestamp()
return self._redis.zcount(self._key_sorted_set, min=min, max=max)
def first_event(self, earliest: dt.datetime = None) -> Optional[dt.datetime]:
"""Date/Time of first event. Returns `None` if series has no events.
Args:
- earliest: Date of first events to count(inclusive), or any if not specified
"""
min = "-inf" if not earliest else earliest.timestamp()
event = self._redis.zrangebyscore(
self._key_sorted_set,
min,
"+inf",
withscores=True,
start=0,
num=1,
score_cast_func=self._cast_scores_to_dt,
)
if not event:
return None
return event[0][1]
@staticmethod
def _cast_scores_to_dt(score) -> dt.datetime:
return dt.datetime.fromtimestamp(float(score), tz=utc)

View File

@@ -1,54 +0,0 @@
from celery.signals import (
task_failure,
task_internal_error,
task_retry,
task_success,
worker_ready
)
from django.conf import settings
from .counters import failed_tasks, retried_tasks, succeeded_tasks
def reset_counters():
"""Reset all counters for the celery status."""
succeeded_tasks.clear()
failed_tasks.clear()
retried_tasks.clear()
def is_enabled() -> bool:
return not bool(
getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED", False)
)
@worker_ready.connect
def reset_counters_when_celery_restarted(*args, **kwargs):
if is_enabled():
reset_counters()
@task_success.connect
def record_task_succeeded(*args, **kwargs):
if is_enabled():
succeeded_tasks.add()
@task_retry.connect
def record_task_retried(*args, **kwargs):
if is_enabled():
retried_tasks.add()
@task_failure.connect
def record_task_failed(*args, **kwargs):
if is_enabled():
failed_tasks.add()
@task_internal_error.connect
def record_task_internal_error(*args, **kwargs):
if is_enabled():
failed_tasks.add()

View File

@@ -1,51 +0,0 @@
import datetime as dt
from django.test import TestCase
from django.utils.timezone import now
from allianceauth.authentication.task_statistics.counters import (
dashboard_results,
succeeded_tasks,
retried_tasks,
failed_tasks,
)
class TestDashboardResults(TestCase):
def test_should_return_counts_for_given_timeframe_only(self):
# given
earliest_task = now() - dt.timedelta(minutes=15)
succeeded_tasks.clear()
succeeded_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
succeeded_tasks.add(earliest_task)
succeeded_tasks.add()
succeeded_tasks.add()
retried_tasks.clear()
retried_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
retried_tasks.add(now() - dt.timedelta(seconds=30))
retried_tasks.add()
failed_tasks.clear()
failed_tasks.add(now() - dt.timedelta(hours=1, seconds=1))
failed_tasks.add()
# when
results = dashboard_results(hours=1)
# then
self.assertEqual(results.succeeded, 3)
self.assertEqual(results.retried, 2)
self.assertEqual(results.failed, 1)
self.assertEqual(results.total, 6)
self.assertEqual(results.earliest_task, earliest_task)
def test_should_work_with_no_data(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
results = dashboard_results(hours=1)
# then
self.assertEqual(results.succeeded, 0)
self.assertEqual(results.retried, 0)
self.assertEqual(results.failed, 0)
self.assertEqual(results.total, 0)
self.assertIsNone(results.earliest_task)

View File

@@ -1,168 +0,0 @@
import datetime as dt
from unittest.mock import patch
from pytz import utc
from redis import RedisError
from django.test import TestCase
from django.utils.timezone import now
from allianceauth.authentication.task_statistics.event_series import (
EventSeries,
_RedisStub,
)
MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
class TestEventSeries(TestCase):
def test_should_abort_without_redis_client(self):
# when
with patch(MODULE_PATH + ".cache.get_master_client") as mock:
mock.return_value = None
events = EventSeries("dummy")
# then
self.assertTrue(events._redis, _RedisStub)
self.assertTrue(events.is_disabled)
def test_should_disable_itself_if_redis_not_available_1(self):
# when
with patch(MODULE_PATH + ".cache.get_master_client") as mock_get_master_client:
mock_get_master_client.return_value.ping.side_effect = RedisError
events = EventSeries("dummy")
# then
self.assertIsInstance(events._redis, _RedisStub)
self.assertTrue(events.is_disabled)
def test_should_disable_itself_if_redis_not_available_2(self):
# when
with patch(MODULE_PATH + ".cache.get_master_client") as mock_get_master_client:
mock_get_master_client.return_value.ping.return_value = False
events = EventSeries("dummy")
# then
self.assertIsInstance(events._redis, _RedisStub)
self.assertTrue(events.is_disabled)
def test_should_add_event(self):
# given
events = EventSeries("dummy")
# when
events.add()
# then
result = events.all()
self.assertEqual(len(result), 1)
self.assertAlmostEqual(result[0], now(), delta=dt.timedelta(seconds=30))
def test_should_add_event_with_specified_time(self):
# given
events = EventSeries("dummy")
my_time = dt.datetime(2021, 11, 1, 12, 15, tzinfo=utc)
# when
events.add(my_time)
# then
result = events.all()
self.assertEqual(len(result), 1)
self.assertAlmostEqual(result[0], my_time, delta=dt.timedelta(seconds=30))
def test_should_count_events(self):
# given
events = EventSeries("dummy")
events.add()
events.add()
# when
result = events.count()
# then
self.assertEqual(result, 2)
def test_should_count_zero(self):
# given
events = EventSeries("dummy")
# when
result = events.count()
# then
self.assertEqual(result, 0)
def test_should_count_events_within_timeframe_1(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.count(
earliest=dt.datetime(2021, 12, 1, 12, 8, tzinfo=utc),
latest=dt.datetime(2021, 12, 1, 12, 17, tzinfo=utc),
)
# then
self.assertEqual(result, 2)
def test_should_count_events_within_timeframe_2(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.count(earliest=dt.datetime(2021, 12, 1, 12, 8))
# then
self.assertEqual(result, 3)
def test_should_count_events_within_timeframe_3(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.count(latest=dt.datetime(2021, 12, 1, 12, 12))
# then
self.assertEqual(result, 2)
def test_should_clear_events(self):
# given
events = EventSeries("dummy")
events.add()
events.add()
# when
events.clear()
# then
self.assertEqual(events.count(), 0)
def test_should_return_date_of_first_event(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.first_event()
# then
self.assertEqual(result, dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
def test_should_return_date_of_first_event_with_range(self):
# given
events = EventSeries("dummy")
events.add(dt.datetime(2021, 12, 1, 12, 0, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 15, tzinfo=utc))
events.add(dt.datetime(2021, 12, 1, 12, 30, tzinfo=utc))
# when
result = events.first_event(
earliest=dt.datetime(2021, 12, 1, 12, 8, tzinfo=utc)
)
# then
self.assertEqual(result, dt.datetime(2021, 12, 1, 12, 10, tzinfo=utc))
def test_should_return_all_events(self):
# given
events = EventSeries("dummy")
events.add()
events.add()
# when
results = events.all()
# then
self.assertEqual(len(results), 2)

View File

@@ -1,93 +0,0 @@
from unittest.mock import patch
from celery.exceptions import Retry
from django.test import TestCase, override_settings
from allianceauth.authentication.task_statistics.counters import (
failed_tasks,
retried_tasks,
succeeded_tasks,
)
from allianceauth.authentication.task_statistics.signals import (
reset_counters,
is_enabled,
)
from allianceauth.eveonline.tasks import update_character
@override_settings(
CELERY_ALWAYS_EAGER=True,ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False
)
class TestTaskSignals(TestCase):
fixtures = ["disable_analytics"]
def test_should_record_successful_task(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
) as mock_update:
mock_update.return_value = None
update_character.delay(1)
# then
self.assertEqual(succeeded_tasks.count(), 1)
self.assertEqual(retried_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 0)
def test_should_record_retried_task(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
) as mock_update:
mock_update.side_effect = Retry
update_character.delay(1)
# then
self.assertEqual(succeeded_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 0)
self.assertEqual(retried_tasks.count(), 1)
def test_should_record_failed_task(self):
# given
succeeded_tasks.clear()
retried_tasks.clear()
failed_tasks.clear()
# when
with patch(
"allianceauth.eveonline.tasks.EveCharacter.objects.update_character"
) as mock_update:
mock_update.side_effect = RuntimeError
update_character.delay(1)
# then
self.assertEqual(succeeded_tasks.count(), 0)
self.assertEqual(retried_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 1)
def test_should_reset_counters(self):
# given
succeeded_tasks.add()
retried_tasks.add()
failed_tasks.add()
# when
reset_counters()
# then
self.assertEqual(succeeded_tasks.count(), 0)
self.assertEqual(retried_tasks.count(), 0)
self.assertEqual(failed_tasks.count(), 0)
class TestIsEnabled(TestCase):
@override_settings(ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=False)
def test_enabled(self):
self.assertTrue(is_enabled())
@override_settings(ALLIANCEAUTH_DASHBOARD_TASK_STATISTICS_DISABLED=True)
def test_disabled(self):
self.assertFalse(is_enabled())

View File

@@ -22,13 +22,13 @@ def check_character_ownership(owner_hash):
continue continue
except (KeyError, IncompleteResponseError): except (KeyError, IncompleteResponseError):
# We can't validate the hash hasn't changed but also can't assume it has. Abort for now. # We can't validate the hash hasn't changed but also can't assume it has. Abort for now.
logger.warning("Failed to validate owner hash of {} due to problems contacting SSO servers.".format( logger.warning("Failed to validate owner hash of {0} due to problems contacting SSO servers.".format(
tokens[0].character_name)) tokens[0].character_name))
break break
if not t.character_owner_hash == old_hash: if not t.character_owner_hash == old_hash:
logger.info( logger.info(
f'Character {t.character_name} has changed ownership. Revoking {tokens.count()} tokens.') 'Character %s has changed ownership. Revoking %s tokens.' % (t.character_name, tokens.count()))
tokens.delete() tokens.delete()
break break

View File

@@ -2,10 +2,10 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Dashboard" %}{% endblock %} {% block page_title %}{% trans "Dashboard" %}{% endblock %}
{% block content %} {% block content %}
<h1 class="page-header text-center">{% translate "Dashboard" %}</h1> <h1 class="page-header text-center">{% trans "Dashboard" %}</h1>
{% if user.is_staff %} {% if user.is_staff %}
{% include 'allianceauth/admin-status/include.html' %} {% include 'allianceauth/admin-status/include.html' %}
{% endif %} {% endif %}
@@ -60,57 +60,36 @@
<td class="text-center">{{ main.alliance_name }}</td> <td class="text-center">{{ main.alliance_name }}</td>
<tr> <tr>
</table> </table>
{% elif main.faction_id %}
<table class="table">
<tr>
<td class="text-center">
<img class="ra-avatar"src="{{ main.faction_logo_url_128 }}">
</td>
</tr>
<tr>
<td class="text-center">{{ main.faction_name }}</td>
<tr>
</table>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="table visible-xs-block"> <div class="table visible-xs-block">
<p> <p>
<img class="ra-avatar" src="{{ main.portrait_url_64 }}"> <img class="ra-avatar" src="{{ main.portrait_url_64 }}">
<img class="ra-avatar" src="{{ main.corporation_logo_url_64 }}"> <img class="ra-avatar" src="{{ main.corporation_logo_url_64 }}">
{% if main.alliance_id %} <img class="ra-avatar" src="{{ main.alliance_logo_url_64 }}">
<img class="ra-avatar" src="{{ main.alliance_logo_url_64 }}">
{% endif %}
{% if main.faction_id %}
<img class="ra-avatar" src="{{ main.faction_logo_url_64 }}">
{% endif %}
</p> </p>
<p> <p>
<strong>{{ main.character_name }}</strong><br> <strong>{{ main.character_name }}</strong><br>
{{ main.corporation_name }}<br> {{ main.corporation_name }}<br>
{% if main.alliance_id %} {{ main.alliance_name }}
{{ main.alliance_name }}<br>
{% endif %}
{% if main.faction_id %}
{{ main.faction_name }}
{% endif %}
</p> </p>
</div> </div>
{% endwith %} {% endwith %}
{% else %} {% else %}
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
{% translate "No main character set." %} {% trans "No main character set." %}
</div> </div>
{% endif %} {% endif %}
<div class="clearfix"></div> <div class="clearfix"></div>
<div class="row"> <div class="row">
<div class="col-sm-6 button-wrapper"> <div class="col-sm-6 button-wrapper">
<a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info" <a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info"
title="Add Character">{% translate 'Add Character' %}</a> title="Add Character">{% trans 'Add Character' %}</a>
</div> </div>
<div class="col-sm-6 button-wrapper"> <div class="col-sm-6 button-wrapper">
<a href="{% url 'authentication:change_main_character' %}" class="btn btn-block btn-info" <a href="{% url 'authentication:change_main_character' %}" class="btn btn-block btn-info"
title="Change Main Character">{% translate "Change Main" %}</a> title="Change Main Character">{% trans "Change Main" %}</a>
</div> </div>
</div> </div>
</div> </div>
@@ -119,7 +98,7 @@
<div class="col-sm-6 text-center"> <div class="col-sm-6 text-center">
<div class="panel panel-success" style="height:100%"> <div class="panel panel-success" style="height:100%">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">{% translate "Group Memberships" %}</h3> <h3 class="panel-title">{% trans "Group Memberships" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div style="height: 240px;overflow:-moz-scrollbars-vertical;overflow-y:auto;"> <div style="height: 240px;overflow:-moz-scrollbars-vertical;overflow-y:auto;">
@@ -139,34 +118,34 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title text-center" style="text-align: center"> <h3 class="panel-title text-center" style="text-align: center">
{% translate 'Characters' %} {% trans 'Characters' %}
</h3> </h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<table class="table table-aa hidden-xs"> <table class="table table-aa hidden-xs">
<thead> <thead>
<tr> <tr>
<th class="text-center"></th> <th class="text-center"></th>
<th class="text-center">{% translate 'Name' %}</th> <th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% translate 'Corp' %}</th> <th class="text-center">{% trans 'Corp' %}</th>
<th class="text-center">{% translate 'Alliance' %}</th> <th class="text-center">{% trans 'Alliance' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for char in characters %} {% for char in characters %}
<tr> <tr>
<td class="text-center"><img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}"> <td class="text-center"><img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}">
</td> </td>
<td class="text-center">{{ char.character_name }}</td> <td class="text-center">{{ char.character_name }}</td>
<td class="text-center">{{ char.corporation_name }}</td> <td class="text-center">{{ char.corporation_name }}</td>
<td class="text-center">{{ char.alliance_name }}</td> <td class="text-center">{{ char.alliance_name }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<table class="table table-aa visible-xs-block" style="width: 100%"> <table class="table table-aa visible-xs-block" style="width: 100%">
<tbody> <tbody>
{% for char in characters %} {% for char in characters %}
<tr> <tr>
<td class="text-center" style="vertical-align: middle"> <td class="text-center" style="vertical-align: middle">
<img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}"> <img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}">
@@ -175,7 +154,7 @@
<strong>{{ char.character_name }}</strong><br> <strong>{{ char.character_name }}</strong><br>
{{ char.corporation_name }}<br> {{ char.corporation_name }}<br>
{{ char.alliance_name|default:"" }} {{ char.alliance_name|default:"" }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -12,7 +12,7 @@
{% include 'allianceauth/icons.html' %} {% include 'allianceauth/icons.html' %}
<title>{% block title %}{% block page_title %}{% endblock page_title %} - {{ SITE_NAME }}{% endblock title %}</title> <title>{% block title %}{{ SITE_NAME }}{% endblock %}</title>
{% include 'bundles/bootstrap-css.html' %} {% include 'bundles/bootstrap-css.html' %}
{% include 'bundles/fontawesome.html' %} {% include 'bundles/fontawesome.html' %}

View File

@@ -1,12 +1,8 @@
{% extends 'public/middle_box.html' %} {% extends 'public/middle_box.html' %}
{% load i18n %}
{% load static %} {% load static %}
{% block page_title %}Login{% endblock %}
{% block page_title %}{% translate "Login" %}{% endblock %} {% block middle_box_content %}
{% block middle_box_content %}
<a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{request.GET.next}}{%endif%}"> <a href="{% url 'auth_sso_login' %}{% if request.GET.next %}?next={{request.GET.next}}{%endif%}">
<img class="img-responsive center-block" src="{% static 'img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" border=0> <img class="img-responsive center-block" src="{% static 'img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" border=0>
</a> </a>
{% endblock %} {% endblock %}

View File

@@ -1,5 +1,6 @@
{% extends 'public/base.html' %} {% extends 'public/base.html' %}
{% load static %} {% load static %}
{% block title %}Login{% endblock %}
{% block content %} {% block content %}
<div class="col-md-4 col-md-offset-4"> <div class="col-md-4 col-md-offset-4">
{% if messages %} {% if messages %}
@@ -20,4 +21,4 @@
{% endblock %} {% endblock %}
{% block extra_include %} {% block extra_include %}
{% include 'bundles/bootstrap-js.html' %} {% include 'bundles/bootstrap-js.html' %}
{% endblock %} {% endblock %}

View File

@@ -1,17 +1,13 @@
{% extends 'public/base.html' %} {% extends 'public/base.html' %}
{% load static %} {% load static %}
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block page_title %}Registration{% endblock %}
{% block page_title %}{% translate "Registration" %}{% endblock %}
{% block extra_include %} {% block extra_include %}
{% include 'bundles/bootstrap-css.html' %} {% include 'bundles/bootstrap-css.html' %}
{% include 'bundles/fontawesome.html' %} {% include 'bundles/fontawesome.html' %}
{% include 'bundles/bootstrap-js.html' %} {% include 'bundles/bootstrap-js.html' %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="col-md-4 col-md-offset-4"> <div class="col-md-4 col-md-offset-4">
<div class="panel panel-default panel-transparent"> <div class="panel panel-default panel-transparent">
@@ -19,7 +15,7 @@
<form method="POST"> <form method="POST">
{% csrf_token %} {% csrf_token %}
{{ form|bootstrap }} {{ form|bootstrap }}
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Register" %}</button> <button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Register" %}</button>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
{% extends 'public/middle_box.html' %} {% extends 'public/middle_box.html' %}
{% load i18n %} {% load i18n %}
{% block middle_box_content %} {% block middle_box_content %}
<div class="alert alert-danger">{% translate 'Invalid or expired activation link.' %}</div> <div class="alert alert-danger">{% trans 'Invalid or expired activation link.' %}</div>
{% endblock %} {% endblock %}

View File

@@ -2,8 +2,12 @@ You're receiving this email because someone has entered this email address while
If this was you, please click on the link below to confirm your email address: If this was you, please click on the link below to confirm your email address:
<a href="{{ scheme }}://{{ url }}">Confirm email address</a>
Link not working? Try copy/pasting this URL into your browser:
{{ scheme }}://{{ url }} {{ scheme }}://{{ url }}
This link will expire in {{ expiration_days }} day(s). This link will expire in {{ expiration_days }} day(s).
If this was not you, it is safe to ignore this email. If this was not you, it is safe to ignore this email.

View File

@@ -1,19 +0,0 @@
<p>
You're receiving this email because someone has entered this email address while registering for an account on {{ site.domain }}
</p>
<p>
If this was you, please click on the link below to confirm your email address:
<p>
<p>
<a href="{{ scheme }}://{{ url }}">Confirm email address</a>
</p>
<p>
This link will expire in {{ expiration_days }} day(s).
</p>
<p>
If this was not you, it is safe to ignore this email.
</p>

View File

@@ -1 +1 @@
Confirm your Alliance Auth account email address Confirm your Alliance Auth account email address

View File

@@ -2,13 +2,13 @@
{% blocktrans trimmed %}You're receiving this email because you requested a password reset for your {% blocktrans trimmed %}You're receiving this email because you requested a password reset for your
user account.{% endblocktrans %} user account.{% endblocktrans %}
{% translate "Please go to the following page and choose a new password:" %} {% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %} {% block reset_link %}
{{domain}}{% url 'password_reset_confirm' uidb64=uid token=token %} {{domain}}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %} {% endblock %}
{% translate "Your username, in case you've forgotten:" %} {{ user.get_username }} {% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
{% translate "Thanks for using our site!" %} {% trans "Thanks for using our site!" %}
{% blocktrans %}Your IT Team{% endblocktrans %} {% blocktrans %}Your IT Team{% endblocktrans %}

View File

@@ -2,13 +2,13 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% block page_title %}{% translate "Register" %}{% endblock %} {% block page_title %}Register{% endblock %}
{% block middle_box_content %} {% block middle_box_content %}
<form class="form-signin" role="form" action="" method="POST"> <form class="form-signin" role="form" action="" method="POST">
{% csrf_token %} {% csrf_token %}
{{ form|bootstrap }} {{ form|bootstrap }}
<br/> <br/>
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Submit" %}</button> <button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Submit" %}</button>
<br/> <br/>
</form> </form>
{% endblock %} {% endblock %}

View File

@@ -1,9 +1,6 @@
from bs4 import BeautifulSoup
from urllib.parse import quote from urllib.parse import quote
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
from django_webtest import WebTest
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.test import TestCase, RequestFactory, Client from django.test import TestCase, RequestFactory, Client
@@ -12,23 +9,22 @@ from allianceauth.authentication.models import (
CharacterOwnership, State, OwnershipRecord CharacterOwnership, State, OwnershipRecord
) )
from allianceauth.eveonline.models import ( from allianceauth.eveonline.models import (
EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo EveCharacter, EveCorporationInfo, EveAllianceInfo
) )
from allianceauth.services.hooks import ServicesHook from allianceauth.services.hooks import ServicesHook
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from ..admin import ( from ..admin import (
BaseUserAdmin, BaseUserAdmin,
CharacterOwnershipAdmin, CharacterOwnershipAdmin,
StateAdmin, StateAdmin,
MainCorporationsFilter, MainCorporationsFilter,
MainAllianceFilter, MainAllianceFilter,
MainFactionFilter,
OwnershipRecordAdmin, OwnershipRecordAdmin,
User, User,
UserAdmin, UserAdmin,
user_main_organization, user_main_organization,
user_profile_pic, user_profile_pic,
user_username, user_username,
update_main_character_model, update_main_character_model,
make_service_hooks_update_groups_action, make_service_hooks_update_groups_action,
@@ -40,7 +36,7 @@ from . import get_admin_change_view_url, get_admin_search_url
MODULE_PATH = 'allianceauth.authentication.admin' MODULE_PATH = 'allianceauth.authentication.admin'
class MockRequest: class MockRequest(object):
def __init__(self, user=None): def __init__(self, user=None):
self.user = user self.user = user
@@ -55,7 +51,7 @@ class TestCaseWithTestData(TestCase):
EveAllianceInfo, EveCorporationInfo, EveCharacter, Group, User EveAllianceInfo, EveCorporationInfo, EveCharacter, Group, User
]: ]:
MyModel.objects.all().delete() MyModel.objects.all().delete()
# groups # groups
cls.group_1 = Group.objects.create( cls.group_1 = Group.objects.create(
name='Group 1' name='Group 1'
@@ -88,16 +84,16 @@ class TestCaseWithTestData(TestCase):
alliance = EveAllianceInfo.objects.create( alliance = EveAllianceInfo.objects.create(
alliance_id=3001, alliance_id=3001,
alliance_name='Wayne Enterprises', alliance_name='Wayne Enterprises',
alliance_ticker='WE', alliance_ticker='WE',
executor_corp_id=2001 executor_corp_id=2001
) )
EveCorporationInfo.objects.create( EveCorporationInfo.objects.create(
corporation_id=2001, corporation_id=2001,
corporation_name='Wayne Technologies', corporation_name='Wayne Technologies',
corporation_ticker='WT', corporation_ticker='WT',
member_count=42, member_count=42,
alliance=alliance alliance=alliance
) )
cls.user_1 = User.objects.create_user( cls.user_1 = User.objects.create_user(
character_1.character_name.replace(' ', '_'), character_1.character_name.replace(' ', '_'),
'abc@example.com', 'abc@example.com',
@@ -115,7 +111,7 @@ class TestCaseWithTestData(TestCase):
) )
cls.user_1.profile.main_character = character_1 cls.user_1.profile.main_character = character_1
cls.user_1.profile.save() cls.user_1.profile.save()
cls.user_1.groups.add(cls.group_1) cls.user_1.groups.add(cls.group_1)
# user 2 - corp only, staff # user 2 - corp only, staff
character_2 = EveCharacter.objects.create( character_2 = EveCharacter.objects.create(
@@ -129,7 +125,7 @@ class TestCaseWithTestData(TestCase):
EveCorporationInfo.objects.create( EveCorporationInfo.objects.create(
corporation_id=2002, corporation_id=2002,
corporation_name='Daily Plane', corporation_name='Daily Plane',
corporation_ticker='DP', corporation_ticker='DP',
member_count=99, member_count=99,
alliance=None alliance=None
) )
@@ -148,7 +144,7 @@ class TestCaseWithTestData(TestCase):
cls.user_2.groups.add(cls.group_2) cls.user_2.groups.add(cls.group_2)
cls.user_2.is_staff = True cls.user_2.is_staff = True
cls.user_2.save() cls.user_2.save()
# user 3 - no main, no group, superuser # user 3 - no main, no group, superuser
character_3 = EveCharacter.objects.create( character_3 = EveCharacter.objects.create(
character_id=1101, character_id=1101,
@@ -161,7 +157,7 @@ class TestCaseWithTestData(TestCase):
EveCorporationInfo.objects.create( EveCorporationInfo.objects.create(
corporation_id=2101, corporation_id=2101,
corporation_name='Lex Corp', corporation_name='Lex Corp',
corporation_ticker='LC', corporation_ticker='LC',
member_count=666, member_count=666,
alliance=None alliance=None
) )
@@ -184,57 +180,31 @@ class TestCaseWithTestData(TestCase):
cls.user_3.is_superuser = True cls.user_3.is_superuser = True
cls.user_3.save() cls.user_3.save()
# user 4 - corp and faction, no alliance
cls.character_4 = EveCharacter.objects.create(
character_id=4321,
character_name='Professor X',
corporation_id=5432,
corporation_name="Xavier's School for Gifted Youngsters",
corporation_ticker='MUTNT',
alliance_id=None,
faction_id=999,
faction_name='The X-Men',
)
cls.user_4 = User.objects.create_user(
cls.character_4.character_name.replace(' ', '_'),
'abc@example.com',
'password'
)
CharacterOwnership.objects.create(
character=cls.character_4,
owner_hash='x1' + cls.character_4.character_name,
user=cls.user_4
)
cls.user_4.profile.main_character = cls.character_4
cls.user_4.profile.save()
EveFactionInfo.objects.create(faction_id=999, faction_name='The X-Men')
def make_generic_search_request(ModelClass: type, search_term: str): def make_generic_search_request(ModelClass: type, search_term: str):
User.objects.create_superuser( User.objects.create_superuser(
username='superuser', password='secret', email='admin@example.com' username='superuser', password='secret', email='admin@example.com'
) )
c = Client() c = Client()
c.login(username='superuser', password='secret') c.login(username='superuser', password='secret')
return c.get( return c.get(
f'{get_admin_search_url(ModelClass)}?q={quote(search_term)}' '%s?q=%s' % (get_admin_search_url(ModelClass), quote(search_term))
) )
class TestCharacterOwnershipAdmin(TestCaseWithTestData): class TestCharacterOwnershipAdmin(TestCaseWithTestData):
fixtures = ["disable_analytics"]
def setUp(self): def setUp(self):
self.modeladmin = CharacterOwnershipAdmin( self.modeladmin = CharacterOwnershipAdmin(
model=User, admin_site=AdminSite() model=User, admin_site=AdminSite()
) )
def test_change_view_loads_normally(self): def test_change_view_loads_normally(self):
User.objects.create_superuser( User.objects.create_superuser(
username='superuser', password='secret', email='admin@example.com' username='superuser', password='secret', email='admin@example.com'
) )
c = Client() c = Client()
c.login(username='superuser', password='secret') c.login(username='superuser', password='secret')
ownership = self.user_1.character_ownerships.first() ownership = self.user_1.character_ownerships.first()
response = c.get(get_admin_change_view_url(ownership)) response = c.get(get_admin_change_view_url(ownership))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@@ -249,19 +219,18 @@ class TestCharacterOwnershipAdmin(TestCaseWithTestData):
class TestOwnershipRecordAdmin(TestCaseWithTestData): class TestOwnershipRecordAdmin(TestCaseWithTestData):
fixtures = ["disable_analytics"]
def setUp(self): def setUp(self):
self.modeladmin = OwnershipRecordAdmin( self.modeladmin = OwnershipRecordAdmin(
model=User, admin_site=AdminSite() model=User, admin_site=AdminSite()
) )
def test_change_view_loads_normally(self): def test_change_view_loads_normally(self):
User.objects.create_superuser( User.objects.create_superuser(
username='superuser', password='secret', email='admin@example.com' username='superuser', password='secret', email='admin@example.com'
) )
c = Client() c = Client()
c.login(username='superuser', password='secret') c.login(username='superuser', password='secret')
ownership_record = OwnershipRecord.objects\ ownership_record = OwnershipRecord.objects\
.filter(user=self.user_1)\ .filter(user=self.user_1)\
.first() .first()
@@ -276,24 +245,23 @@ class TestOwnershipRecordAdmin(TestCaseWithTestData):
class TestStateAdmin(TestCaseWithTestData): class TestStateAdmin(TestCaseWithTestData):
fixtures = ["disable_analytics"]
def setUp(self):
self.modeladmin = StateAdmin(
model=User, admin_site=AdminSite()
)
@classmethod def test_change_view_loads_normally(self):
def setUpClass(cls) -> None:
super().setUpClass()
cls.modeladmin = StateAdmin(model=User, admin_site=AdminSite())
def test_change_view_loads_normally(self):
User.objects.create_superuser( User.objects.create_superuser(
username='superuser', password='secret', email='admin@example.com' username='superuser', password='secret', email='admin@example.com'
) )
c = Client() c = Client()
c.login(username='superuser', password='secret') c.login(username='superuser', password='secret')
guest_state = AuthUtils.get_guest_state() guest_state = AuthUtils.get_guest_state()
response = c.get(get_admin_change_view_url(guest_state)) response = c.get(get_admin_change_view_url(guest_state))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
member_state = AuthUtils.get_member_state() member_state = AuthUtils.get_member_state()
response = c.get(get_admin_change_view_url(member_state)) response = c.get(get_admin_change_view_url(member_state))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@@ -306,7 +274,6 @@ class TestStateAdmin(TestCaseWithTestData):
class TestUserAdmin(TestCaseWithTestData): class TestUserAdmin(TestCaseWithTestData):
fixtures = ["disable_analytics"]
def setUp(self): def setUp(self):
self.factory = RequestFactory() self.factory = RequestFactory()
@@ -314,12 +281,12 @@ class TestUserAdmin(TestCaseWithTestData):
model=User, admin_site=AdminSite() model=User, admin_site=AdminSite()
) )
self.character_1 = self.user_1.character_ownerships.first().character self.character_1 = self.user_1.character_ownerships.first().character
def test_user_profile_pic_u1(self): def test_user_profile_pic_u1(self):
expected = ( expected = (
'<img src="https://images.evetech.net/characters/1001/' '<img src="https://images.evetech.net/characters/1001/'
'portrait?size=32" class="img-circle">' 'portrait?size=32" class="img-circle">'
) )
self.assertEqual(user_profile_pic(self.user_1), expected) self.assertEqual(user_profile_pic(self.user_1), expected)
def test_user_profile_pic_u3(self): def test_user_profile_pic_u3(self):
@@ -348,13 +315,9 @@ class TestUserAdmin(TestCaseWithTestData):
self.assertEqual(user_main_organization(self.user_2), expected) self.assertEqual(user_main_organization(self.user_2), expected)
def test_user_main_organization_u3(self): def test_user_main_organization_u3(self):
expected = '' expected = None
self.assertEqual(user_main_organization(self.user_3), expected) self.assertEqual(user_main_organization(self.user_3), expected)
def test_user_main_organization_u4(self):
expected = "Xavier's School for Gifted Youngsters<br>The X-Men"
self.assertEqual(user_main_organization(self.user_4), expected)
def test_characters_u1(self): def test_characters_u1(self):
expected = 'Batman, Bruce Wayne' expected = 'Batman, Bruce Wayne'
result = self.modeladmin._characters(self.user_1) result = self.modeladmin._characters(self.user_1)
@@ -369,18 +332,18 @@ class TestUserAdmin(TestCaseWithTestData):
expected = 'Lex Luthor' expected = 'Lex Luthor'
result = self.modeladmin._characters(self.user_3) result = self.modeladmin._characters(self.user_3)
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_groups_u1(self): def test_groups_u1(self):
expected = 'Group 1' expected = 'Group 1'
result = self.modeladmin._groups(self.user_1) result = self.modeladmin._groups(self.user_1)
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_groups_u2(self): def test_groups_u2(self):
expected = 'Group 2' expected = 'Group 2'
result = self.modeladmin._groups(self.user_2) result = self.modeladmin._groups(self.user_2)
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_groups_u3(self): def test_groups_u3(self):
result = self.modeladmin._groups(self.user_3) result = self.modeladmin._groups(self.user_3)
self.assertIsNone(result) self.assertIsNone(result)
@@ -424,10 +387,10 @@ class TestUserAdmin(TestCaseWithTestData):
expected = None expected = None
result = self.modeladmin._list_2_html_w_tooltips(items, 5) result = self.modeladmin._list_2_html_w_tooltips(items, 5)
self.assertEqual(expected, result) self.assertEqual(expected, result)
# actions # actions
@patch(MODULE_PATH + '.UserAdmin.message_user', auto_spec=True, unsafe=True) @patch(MODULE_PATH + '.UserAdmin.message_user', auto_spec=True)
@patch(MODULE_PATH + '.update_character') @patch(MODULE_PATH + '.update_character')
def test_action_update_main_character_model( def test_action_update_main_character_model(
self, mock_task, mock_message_user self, mock_task, mock_message_user
@@ -438,14 +401,14 @@ class TestUserAdmin(TestCaseWithTestData):
) )
self.assertEqual(mock_task.delay.call_count, 2) self.assertEqual(mock_task.delay.call_count, 2)
self.assertTrue(mock_message_user.called) self.assertTrue(mock_message_user.called)
# filters # filters
def test_filter_main_corporations(self): def test_filter_main_corporations(self):
class UserAdminTest(BaseUserAdmin): class UserAdminTest(BaseUserAdmin):
list_filter = (MainCorporationsFilter,) list_filter = (MainCorporationsFilter,)
my_modeladmin = UserAdminTest(User, AdminSite()) my_modeladmin = UserAdminTest(User, AdminSite())
# Make sure the lookups are correct # Make sure the lookups are correct
@@ -454,29 +417,28 @@ class TestUserAdmin(TestCaseWithTestData):
changelist = my_modeladmin.get_changelist_instance(request) changelist = my_modeladmin.get_changelist_instance(request)
filters = changelist.get_filters(request) filters = changelist.get_filters(request)
filterspec = filters[0][0] filterspec = filters[0][0]
expected = [ expected = [
(2002, 'Daily Planet'), (2002, 'Daily Planet'),
(2001, 'Wayne Technologies'), (2001, 'Wayne Technologies'),
(5432, "Xavier's School for Gifted Youngsters"),
] ]
self.assertEqual(filterspec.lookup_choices, expected) self.assertEqual(filterspec.lookup_choices, expected)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
request = self.factory.get( request = self.factory.get(
'/', '/',
{'main_corporation_id__exact': self.character_1.corporation_id} {'main_corporation_id__exact': self.character_1.corporation_id}
) )
request.user = self.user_1 request.user = self.user_1
changelist = my_modeladmin.get_changelist_instance(request) changelist = my_modeladmin.get_changelist_instance(request)
queryset = changelist.get_queryset(request) queryset = changelist.get_queryset(request)
expected = [self.user_1] expected = [self.user_1]
self.assertSetEqual(set(queryset), set(expected)) self.assertSetEqual(set(queryset), set(expected))
def test_filter_main_alliances(self): def test_filter_main_alliances(self):
class UserAdminTest(BaseUserAdmin): class UserAdminTest(BaseUserAdmin):
list_filter = (MainAllianceFilter,) list_filter = (MainAllianceFilter,)
my_modeladmin = UserAdminTest(User, AdminSite()) my_modeladmin = UserAdminTest(User, AdminSite())
# Make sure the lookups are correct # Make sure the lookups are correct
@@ -485,56 +447,28 @@ class TestUserAdmin(TestCaseWithTestData):
changelist = my_modeladmin.get_changelist_instance(request) changelist = my_modeladmin.get_changelist_instance(request)
filters = changelist.get_filters(request) filters = changelist.get_filters(request)
filterspec = filters[0][0] filterspec = filters[0][0]
expected = [ expected = [
(3001, 'Wayne Enterprises'), (3001, 'Wayne Enterprises'),
] ]
self.assertEqual(filterspec.lookup_choices, expected) self.assertEqual(filterspec.lookup_choices, expected)
# Make sure the correct queryset is returned # Make sure the correct queryset is returned
request = self.factory.get( request = self.factory.get(
'/', '/',
{'main_alliance_id__exact': self.character_1.alliance_id} {'main_alliance_id__exact': self.character_1.alliance_id}
) )
request.user = self.user_1 request.user = self.user_1
changelist = my_modeladmin.get_changelist_instance(request) changelist = my_modeladmin.get_changelist_instance(request)
queryset = changelist.get_queryset(request) queryset = changelist.get_queryset(request)
expected = [self.user_1] expected = [self.user_1]
self.assertSetEqual(set(queryset), set(expected)) self.assertSetEqual(set(queryset), set(expected))
def test_filter_main_factions(self):
class UserAdminTest(BaseUserAdmin):
list_filter = (MainFactionFilter,)
my_modeladmin = UserAdminTest(User, AdminSite())
# Make sure the lookups are correct
request = self.factory.get('/')
request.user = self.user_4
changelist = my_modeladmin.get_changelist_instance(request)
filters = changelist.get_filters(request)
filterspec = filters[0][0]
expected = [
(999, 'The X-Men'),
]
self.assertEqual(filterspec.lookup_choices, expected)
# Make sure the correct queryset is returned
request = self.factory.get(
'/',
{'main_faction_id__exact': self.character_4.faction_id}
)
request.user = self.user_4
changelist = my_modeladmin.get_changelist_instance(request)
queryset = changelist.get_queryset(request)
expected = [self.user_4]
self.assertSetEqual(set(queryset), set(expected))
def test_change_view_loads_normally(self): def test_change_view_loads_normally(self):
User.objects.create_superuser( User.objects.create_superuser(
username='superuser', password='secret', email='admin@example.com' username='superuser', password='secret', email='admin@example.com'
) )
c = Client() c = Client()
c.login(username='superuser', password='secret') c.login(username='superuser', password='secret')
response = c.get(get_admin_change_view_url(self.user_1)) response = c.get(get_admin_change_view_url(self.user_1))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@@ -545,237 +479,14 @@ class TestUserAdmin(TestCaseWithTestData):
self.assertEqual(response.status_code, expected) self.assertEqual(response.status_code, expected)
class TestStateAdminChangeFormSuperuserExclusiveEdits(WebTest):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.super_admin = User.objects.create_superuser("super_admin")
cls.staff_admin = User.objects.create_user("staff_admin")
cls.staff_admin.is_staff = True
cls.staff_admin.save()
cls.staff_admin = AuthUtils.add_permissions_to_user_by_name(
[
"authentication.add_state",
"authentication.change_state",
"authentication.view_state",
],
cls.staff_admin
)
cls.superuser_exclusive_fields = ["permissions",]
def test_should_show_all_fields_to_superuser_for_add(self):
# given
self.app.set_user(self.super_admin)
page = self.app.get("/admin/authentication/state/add/")
# when
form = page.forms["state_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertIn(field, form.fields)
def test_should_not_show_all_fields_to_staff_admins_for_add(self):
# given
self.app.set_user(self.staff_admin)
page = self.app.get("/admin/authentication/state/add/")
# when
form = page.forms["state_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertNotIn(field, form.fields)
def test_should_show_all_fields_to_superuser_for_change(self):
# given
self.app.set_user(self.super_admin)
state = AuthUtils.get_member_state()
page = self.app.get(f"/admin/authentication/state/{state.pk}/change/")
# when
form = page.forms["state_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertIn(field, form.fields)
def test_should_not_show_all_fields_to_staff_admin_for_change(self):
# given
self.app.set_user(self.staff_admin)
state = AuthUtils.get_member_state()
page = self.app.get(f"/admin/authentication/state/{state.pk}/change/")
# when
form = page.forms["state_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertNotIn(field, form.fields)
class TestUserAdminChangeForm(TestCase):
fixtures = ["disable_analytics"]
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.modeladmin = UserAdmin(model=User, admin_site=AdminSite())
def test_should_show_groups_available_to_user_with_blue_state_only(self):
# given
superuser = User.objects.create_superuser("Super")
user = AuthUtils.create_user("bruce_wayne")
character = AuthUtils.add_main_character_2(
user,
name="Bruce Wayne",
character_id=1001,
corp_id=2001,
corp_name="Wayne Technologies"
)
blue_state = State.objects.get(name="Blue")
blue_state.member_characters.add(character)
member_state = AuthUtils.get_member_state()
group_1 = Group.objects.create(name="Group 1")
group_2 = Group.objects.create(name="Group 2")
group_2.authgroup.states.add(blue_state)
group_3 = Group.objects.create(name="Group 3")
group_3.authgroup.states.add(member_state)
self.client.force_login(superuser)
# when
response = self.client.get(f"/admin/authentication/user/{user.pk}/change/")
# then
self.assertEqual(response.status_code, 200)
soup = BeautifulSoup(response.rendered_content, features="html.parser")
groups_select = soup.find("select", {"id": "id_groups"}).find_all('option')
group_ids = {int(option["value"]) for option in groups_select}
self.assertSetEqual(group_ids, {group_1.pk, group_2.pk})
class TestUserAdminChangeFormSuperuserExclusiveEdits(WebTest):
fixtures = ["disable_analytics"]
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.super_admin = User.objects.create_superuser("super_admin")
cls.staff_admin = User.objects.create_user("staff_admin")
cls.staff_admin.is_staff = True
cls.staff_admin.save()
cls.staff_admin = AuthUtils.add_permissions_to_user_by_name(
[
"auth.change_user",
"auth.view_user",
"authentication.change_user",
"authentication.change_userprofile",
"authentication.view_user"
],
cls.staff_admin
)
cls.superuser_exclusive_fields = [
"is_staff", "is_superuser", "user_permissions"
]
def setUp(self) -> None:
self.user = AuthUtils.create_user("bruce_wayne")
def test_should_show_all_fields_to_superuser_for_change(self):
# given
self.app.set_user(self.super_admin)
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
# when
form = page.forms["user_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertIn(field, form.fields)
def test_should_not_show_all_fields_to_staff_admin_for_change(self):
# given
self.app.set_user(self.staff_admin)
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
# when
form = page.forms["user_form"]
# then
for field in self.superuser_exclusive_fields:
with self.subTest(field=field):
self.assertNotIn(field, form.fields)
def test_should_allow_super_admin_to_add_restricted_group_to_user(self):
# given
self.app.set_user(self.super_admin)
group_restricted = Group.objects.create(name="restricted group")
group_restricted.authgroup.restricted = True
group_restricted.authgroup.save()
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
form = page.forms["user_form"]
# when
form["groups"].select_multiple(texts=["restricted group"])
response = form.submit("save")
# then
self.assertEqual(response.status_code, 302)
self.user.refresh_from_db()
self.assertIn(
"restricted group", self.user.groups.values_list("name", flat=True)
)
def test_should_not_allow_staff_admin_to_add_restricted_group_to_user(self):
# given
self.app.set_user(self.staff_admin)
group_restricted = Group.objects.create(name="restricted group")
group_restricted.authgroup.restricted = True
group_restricted.authgroup.save()
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
form = page.forms["user_form"]
# when
form["groups"].select_multiple(texts=["restricted group"])
response = form.submit("save")
# then
self.assertEqual(response.status_code, 200)
self.assertIn(
"You are not allowed to add or remove these restricted groups",
response.text
)
def test_should_not_allow_staff_admin_to_remove_restricted_group_from_user(self):
# given
self.app.set_user(self.staff_admin)
group_restricted = Group.objects.create(name="restricted group")
group_restricted.authgroup.restricted = True
group_restricted.authgroup.save()
self.user.groups.add(group_restricted)
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
form = page.forms["user_form"]
# when
form["groups"].select_multiple(texts=[])
response = form.submit("save")
# then
self.assertEqual(response.status_code, 200)
self.assertIn(
"You are not allowed to add or remove these restricted groups",
response.text
)
def test_should_allow_staff_admin_to_add_normal_group_to_user(self):
# given
self.app.set_user(self.super_admin)
Group.objects.create(name="normal group")
page = self.app.get(f"/admin/authentication/user/{self.user.pk}/change/")
form = page.forms["user_form"]
# when
form["groups"].select_multiple(texts=["normal group"])
response = form.submit("save")
# then
self.assertEqual(response.status_code, 302)
self.user.refresh_from_db()
self.assertIn("normal group", self.user.groups.values_list("name", flat=True))
class TestMakeServicesHooksActions(TestCaseWithTestData): class TestMakeServicesHooksActions(TestCaseWithTestData):
class MyServicesHookTypeA(ServicesHook): class MyServicesHookTypeA(ServicesHook):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.name = 'My Service A' self.name = 'My Service A'
def update_groups(self, user): def update_groups(self, user):
pass pass
@@ -787,7 +498,7 @@ class TestMakeServicesHooksActions(TestCaseWithTestData):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.name = 'My Service B' self.name = 'My Service B'
def update_groups(self, user): def update_groups(self, user):
pass pass
@@ -799,32 +510,32 @@ class TestMakeServicesHooksActions(TestCaseWithTestData):
def sync_nicknames_bulk(self, user): def sync_nicknames_bulk(self, user):
pass pass
def test_service_has_update_groups_only(self): def test_service_has_update_groups_only(self):
service = self.MyServicesHookTypeA() service = self.MyServicesHookTypeA()
mock_service = MagicMock(spec=service) mock_service = MagicMock(spec=service)
action = make_service_hooks_update_groups_action(mock_service) action = make_service_hooks_update_groups_action(mock_service)
action(MagicMock(), MagicMock(), [self.user_1]) action(MagicMock(), MagicMock(), [self.user_1])
self.assertTrue(mock_service.update_groups.called) self.assertTrue(mock_service.update_groups.called)
def test_service_has_update_groups_bulk(self): def test_service_has_update_groups_bulk(self):
service = self.MyServicesHookTypeB() service = self.MyServicesHookTypeB()
mock_service = MagicMock(spec=service) mock_service = MagicMock(spec=service)
action = make_service_hooks_update_groups_action(mock_service) action = make_service_hooks_update_groups_action(mock_service)
action(MagicMock(), MagicMock(), [self.user_1]) action(MagicMock(), MagicMock(), [self.user_1])
self.assertFalse(mock_service.update_groups.called) self.assertFalse(mock_service.update_groups.called)
self.assertTrue(mock_service.update_groups_bulk.called) self.assertTrue(mock_service.update_groups_bulk.called)
def test_service_has_sync_nickname_only(self): def test_service_has_sync_nickname_only(self):
service = self.MyServicesHookTypeA() service = self.MyServicesHookTypeA()
mock_service = MagicMock(spec=service) mock_service = MagicMock(spec=service)
action = make_service_hooks_sync_nickname_action(mock_service) action = make_service_hooks_sync_nickname_action(mock_service)
action(MagicMock(), MagicMock(), [self.user_1]) action(MagicMock(), MagicMock(), [self.user_1])
self.assertTrue(mock_service.sync_nickname.called) self.assertTrue(mock_service.sync_nickname.called)
def test_service_has_sync_nicknames_bulk(self): def test_service_has_sync_nicknames_bulk(self):
service = self.MyServicesHookTypeB() service = self.MyServicesHookTypeB()
mock_service = MagicMock(spec=service) mock_service = MagicMock(spec=service)
action = make_service_hooks_sync_nickname_action(mock_service) action = make_service_hooks_sync_nickname_action(mock_service)
action(MagicMock(), MagicMock(), [self.user_1]) action(MagicMock(), MagicMock(), [self.user_1])
self.assertFalse(mock_service.sync_nickname.called) self.assertFalse(mock_service.sync_nickname.called)

View File

@@ -9,19 +9,19 @@ MODULE_PATH = 'allianceauth.authentication'
class TestSetAppSetting(TestCase): class TestSetAppSetting(TestCase):
@patch(MODULE_PATH + '.app_settings.settings') @patch(MODULE_PATH + '.app_settings.settings')
def test_default_if_not_set(self, mock_settings): def test_default_if_not_set(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = Mock(spec=None) mock_settings.TEST_SETTING_DUMMY = Mock(spec=None)
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
False, False,
) )
self.assertEqual(result, False) self.assertEqual(result, False)
@patch(MODULE_PATH + '.app_settings.settings') @patch(MODULE_PATH + '.app_settings.settings')
def test_default_if_not_set_for_none(self, mock_settings): def test_default_if_not_set_for_none(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = Mock(spec=None) mock_settings.TEST_SETTING_DUMMY = Mock(spec=None)
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
None, None,
required_type=int required_type=int
) )
@@ -31,8 +31,8 @@ class TestSetAppSetting(TestCase):
def test_true_stays_true(self, mock_settings): def test_true_stays_true(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = True mock_settings.TEST_SETTING_DUMMY = True
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
False, False,
) )
self.assertEqual(result, True) self.assertEqual(result, True)
@@ -40,7 +40,7 @@ class TestSetAppSetting(TestCase):
def test_false_stays_false(self, mock_settings): def test_false_stays_false(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = False mock_settings.TEST_SETTING_DUMMY = False
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
False False
) )
self.assertEqual(result, False) self.assertEqual(result, False)
@@ -49,7 +49,7 @@ class TestSetAppSetting(TestCase):
def test_default_for_invalid_type_bool(self, mock_settings): def test_default_for_invalid_type_bool(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = 'invalid type' mock_settings.TEST_SETTING_DUMMY = 'invalid type'
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
False False
) )
self.assertEqual(result, False) self.assertEqual(result, False)
@@ -58,7 +58,7 @@ class TestSetAppSetting(TestCase):
def test_default_for_invalid_type_int(self, mock_settings): def test_default_for_invalid_type_int(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = 'invalid type' mock_settings.TEST_SETTING_DUMMY = 'invalid type'
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
50 50
) )
self.assertEqual(result, 50) self.assertEqual(result, 50)
@@ -67,7 +67,7 @@ class TestSetAppSetting(TestCase):
def test_default_if_below_minimum_1(self, mock_settings): def test_default_if_below_minimum_1(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = -5 mock_settings.TEST_SETTING_DUMMY = -5
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
default_value=50 default_value=50
) )
self.assertEqual(result, 50) self.assertEqual(result, 50)
@@ -76,7 +76,7 @@ class TestSetAppSetting(TestCase):
def test_default_if_below_minimum_2(self, mock_settings): def test_default_if_below_minimum_2(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = -50 mock_settings.TEST_SETTING_DUMMY = -50
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
default_value=50, default_value=50,
min_value=-10 min_value=-10
) )
@@ -86,7 +86,7 @@ class TestSetAppSetting(TestCase):
def test_default_for_invalid_type_int(self, mock_settings): def test_default_for_invalid_type_int(self, mock_settings):
mock_settings.TEST_SETTING_DUMMY = 1000 mock_settings.TEST_SETTING_DUMMY = 1000
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
default_value=50, default_value=50,
max_value=100 max_value=100
) )
@@ -97,6 +97,6 @@ class TestSetAppSetting(TestCase):
mock_settings.TEST_SETTING_DUMMY = 'invalid type' mock_settings.TEST_SETTING_DUMMY = 'invalid type'
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
result = app_settings._clean_setting( result = app_settings._clean_setting(
'TEST_SETTING_DUMMY', 'TEST_SETTING_DUMMY',
default_value=None default_value=None
) )

View File

@@ -23,27 +23,27 @@ class TestStatePermissions(TestCase):
self.permission_2 = AuthUtils.get_permission_by_name(PERMISSION_2) self.permission_2 = AuthUtils.get_permission_by_name(PERMISSION_2)
# group # group
self.group_1 = Group.objects.create(name="Group 1") self.group_1 = Group.objects.create(name="Group 1")
self.group_2 = Group.objects.create(name="Group 2") self.group_2 = Group.objects.create(name="Group 2")
# state # state
self.state_1 = AuthUtils.get_member_state() self.state_1 = AuthUtils.get_member_state()
self.state_2 = AuthUtils.create_state("Other State", 75) self.state_2 = AuthUtils.create_state("Other State", 75)
# user # user
self.user = AuthUtils.create_user("Bruce Wayne") self.user = AuthUtils.create_user("Bruce Wayne")
self.main = AuthUtils.add_main_character_2(self.user, self.user.username, 123) self.main = AuthUtils.add_main_character_2(self.user, self.user.username, 123)
def test_user_has_user_permissions(self): def test_user_has_user_permissions(self):
self.user.user_permissions.add(self.permission_1) self.user.user_permissions.add(self.permission_1)
user = User.objects.get(pk=self.user.pk) user = User.objects.get(pk=self.user.pk)
self.assertTrue(user.has_perm(PERMISSION_1)) self.assertTrue(user.has_perm(PERMISSION_1))
def test_user_has_group_permissions(self): def test_user_has_group_permissions(self):
self.group_1.permissions.add(self.permission_1) self.group_1.permissions.add(self.permission_1)
self.user.groups.add(self.group_1) self.user.groups.add(self.group_1)
user = User.objects.get(pk=self.user.pk) user = User.objects.get(pk=self.user.pk)
self.assertTrue(user.has_perm(PERMISSION_1)) self.assertTrue(user.has_perm(PERMISSION_1))
@@ -55,7 +55,7 @@ class TestStatePermissions(TestCase):
self.assertTrue(user.has_perm(PERMISSION_1)) self.assertTrue(user.has_perm(PERMISSION_1))
def test_when_user_changes_state_perms_change_accordingly(self): def test_when_user_changes_state_perms_change_accordingly(self):
self.state_1.permissions.add(self.permission_1) self.state_1.permissions.add(self.permission_1)
self.state_1.member_characters.add(self.main) self.state_1.member_characters.add(self.main)
user = User.objects.get(pk=self.user.pk) user = User.objects.get(pk=self.user.pk)
self.assertTrue(user.has_perm(PERMISSION_1)) self.assertTrue(user.has_perm(PERMISSION_1))
@@ -68,16 +68,16 @@ class TestStatePermissions(TestCase):
self.assertTrue(user.has_perm(PERMISSION_2)) self.assertTrue(user.has_perm(PERMISSION_2))
def test_state_permissions_are_returned_for_current_user_object(self): def test_state_permissions_are_returned_for_current_user_object(self):
# verify state permissions are returns for the current user object # verify state permissions are returns for the current user object
# and not for it's instance in the database, which might be outdated # and not for it's instance in the database, which might be outdated
self.state_1.permissions.add(self.permission_1) self.state_1.permissions.add(self.permission_1)
self.state_2.permissions.add(self.permission_2) self.state_2.permissions.add(self.permission_2)
self.state_1.member_characters.add(self.main) self.state_1.member_characters.add(self.main)
user = User.objects.get(pk=self.user.pk) user = User.objects.get(pk=self.user.pk)
user.profile.state = self.state_2 user.profile.state = self.state_2
self.assertFalse(user.has_perm(PERMISSION_1)) self.assertFalse(user.has_perm(PERMISSION_1))
self.assertTrue(user.has_perm(PERMISSION_2)) self.assertTrue(user.has_perm(PERMISSION_2))
class TestAuthenticate(TestCase): class TestAuthenticate(TestCase):
@classmethod @classmethod
@@ -114,12 +114,12 @@ class TestAuthenticate(TestCase):
def test_authenticate_main_character(self): def test_authenticate_main_character(self):
t = Token(character_id=self.main_character.character_id, character_owner_hash='1') t = Token(character_id=self.main_character.character_id, character_owner_hash='1')
user = StateBackend().authenticate(token=t) user = StateBackend().authenticate(token=t)
self.assertEqual(user, self.user) self.assertEquals(user, self.user)
def test_authenticate_alt_character(self): def test_authenticate_alt_character(self):
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2') t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
user = StateBackend().authenticate(token=t) user = StateBackend().authenticate(token=t)
self.assertEqual(user, self.user) self.assertEquals(user, self.user)
def test_authenticate_unclaimed_character(self): def test_authenticate_unclaimed_character(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3') t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3')
@@ -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

@@ -4,7 +4,7 @@ from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\ from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\
EveAllianceInfo, EveFactionInfo EveAllianceInfo
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from esi.errors import IncompleteResponseError from esi.errors import IncompleteResponseError
from esi.models import Token from esi.models import Token
@@ -36,8 +36,8 @@ class CharacterOwnershipTestCase(TestCase):
character_owner_hash='1', character_owner_hash='1',
) )
co = CharacterOwnership.objects.get(character=self.character) co = CharacterOwnership.objects.get(character=self.character)
self.assertEqual(co.user, self.user) self.assertEquals(co.user, self.user)
self.assertEqual(co.owner_hash, '1') self.assertEquals(co.owner_hash, '1')
def test_transfer_ownership(self): def test_transfer_ownership(self):
Token.objects.create( Token.objects.create(
@@ -54,7 +54,7 @@ class CharacterOwnershipTestCase(TestCase):
) )
co = CharacterOwnership.objects.get(character=self.character) co = CharacterOwnership.objects.get(character=self.character)
self.assertNotEqual(self.user, co.user) self.assertNotEqual(self.user, co.user)
self.assertEqual(self.alt_user, co.user) self.assertEquals(self.alt_user, co.user)
def test_clear_main_character(self): def test_clear_main_character(self):
Token.objects.create( Token.objects.create(
@@ -80,15 +80,13 @@ 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', faction_id=1337, corp_name='Test Corp', alliance_name='Test Alliance')
faction_name='Permabanned')
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.test_faction = EveFactionInfo.objects.create(faction_id=1337, faction_name='Permabanned')
cls.member_state = State.objects.create( cls.member_state = State.objects.create(
name='Test Member', name='Test Member',
priority=150, priority=150,
@@ -100,38 +98,29 @@ class StateTestCase(TestCase):
def test_state_assignment_on_character_change(self): def test_state_assignment_on_character_change(self):
self.member_state.member_characters.add(self.test_character) self.member_state.member_characters.add(self.test_character)
self._refresh_user() self._refresh_user()
self.assertEqual(self.user.profile.state, self.member_state) self.assertEquals(self.user.profile.state, self.member_state)
self.member_state.member_characters.remove(self.test_character) self.member_state.member_characters.remove(self.test_character)
self._refresh_user() self._refresh_user()
self.assertEqual(self.user.profile.state, self.guest_state) self.assertEquals(self.user.profile.state, self.guest_state)
def test_state_assignment_on_corporation_change(self): def test_state_assignment_on_corporation_change(self):
self.member_state.member_corporations.add(self.test_corporation) self.member_state.member_corporations.add(self.test_corporation)
self._refresh_user() self._refresh_user()
self.assertEqual(self.user.profile.state, self.member_state) self.assertEquals(self.user.profile.state, self.member_state)
self.member_state.member_corporations.remove(self.test_corporation) self.member_state.member_corporations.remove(self.test_corporation)
self._refresh_user() self._refresh_user()
self.assertEqual(self.user.profile.state, self.guest_state) self.assertEquals(self.user.profile.state, self.guest_state)
def test_state_assignment_on_alliance_addition(self): def test_state_assignment_on_alliance_addition(self):
self.member_state.member_alliances.add(self.test_alliance) self.member_state.member_alliances.add(self.test_alliance)
self._refresh_user() self._refresh_user()
self.assertEqual(self.user.profile.state, self.member_state) self.assertEquals(self.user.profile.state, self.member_state)
self.member_state.member_alliances.remove(self.test_alliance) self.member_state.member_alliances.remove(self.test_alliance)
self._refresh_user() self._refresh_user()
self.assertEqual(self.user.profile.state, self.guest_state) self.assertEquals(self.user.profile.state, self.guest_state)
def test_state_assignment_on_faction_change(self):
self.member_state.member_factions.add(self.test_faction)
self._refresh_user()
self.assertEqual(self.user.profile.state, self.member_state)
self.member_state.member_factions.remove(self.test_faction)
self._refresh_user()
self.assertEqual(self.user.profile.state, self.guest_state)
def test_state_assignment_on_higher_priority_state_creation(self): def test_state_assignment_on_higher_priority_state_creation(self):
self.member_state.member_characters.add(self.test_character) self.member_state.member_characters.add(self.test_character)
@@ -141,10 +130,10 @@ class StateTestCase(TestCase):
) )
higher_state.member_characters.add(self.test_character) higher_state.member_characters.add(self.test_character)
self._refresh_user() self._refresh_user()
self.assertEqual(higher_state, self.user.profile.state) self.assertEquals(higher_state, self.user.profile.state)
higher_state.member_characters.clear() higher_state.member_characters.clear()
self._refresh_user() self._refresh_user()
self.assertEqual(self.member_state, self.user.profile.state) self.assertEquals(self.member_state, self.user.profile.state)
self.member_state.member_characters.clear() self.member_state.member_characters.clear()
def test_state_assignment_on_lower_priority_state_creation(self): def test_state_assignment_on_lower_priority_state_creation(self):
@@ -155,10 +144,10 @@ class StateTestCase(TestCase):
) )
lower_state.member_characters.add(self.test_character) lower_state.member_characters.add(self.test_character)
self._refresh_user() self._refresh_user()
self.assertEqual(self.member_state, self.user.profile.state) self.assertEquals(self.member_state, self.user.profile.state)
lower_state.member_characters.clear() lower_state.member_characters.clear()
self._refresh_user() self._refresh_user()
self.assertEqual(self.member_state, self.user.profile.state) self.assertEquals(self.member_state, self.user.profile.state)
self.member_state.member_characters.clear() self.member_state.member_characters.clear()
def test_state_assignment_on_priority_change(self): def test_state_assignment_on_priority_change(self):
@@ -172,11 +161,11 @@ class StateTestCase(TestCase):
lower_state.priority = 500 lower_state.priority = 500
lower_state.save() lower_state.save()
self._refresh_user() self._refresh_user()
self.assertEqual(lower_state, self.user.profile.state) self.assertEquals(lower_state, self.user.profile.state)
lower_state.priority = 125 lower_state.priority = 125
lower_state.save() lower_state.save()
self._refresh_user() self._refresh_user()
self.assertEqual(self.member_state, self.user.profile.state) self.assertEquals(self.member_state, self.user.profile.state)
def test_state_assignment_on_state_deletion(self): def test_state_assignment_on_state_deletion(self):
self.member_state.member_characters.add(self.test_character) self.member_state.member_characters.add(self.test_character)
@@ -186,11 +175,11 @@ class StateTestCase(TestCase):
) )
higher_state.member_characters.add(self.test_character) higher_state.member_characters.add(self.test_character)
self._refresh_user() self._refresh_user()
self.assertEqual(higher_state, self.user.profile.state) self.assertEquals(higher_state, self.user.profile.state)
higher_state.delete() higher_state.delete()
self.assertFalse(State.objects.filter(name='Higher State').count()) self.assertFalse(State.objects.filter(name='Higher State').count())
self._refresh_user() self._refresh_user()
self.assertEqual(self.member_state, self.user.profile.state) self.assertEquals(self.member_state, self.user.profile.state)
def test_state_assignment_on_public_toggle(self): def test_state_assignment_on_public_toggle(self):
self.member_state.member_characters.add(self.test_character) self.member_state.member_characters.add(self.test_character)
@@ -199,26 +188,26 @@ class StateTestCase(TestCase):
priority=200, priority=200,
) )
self._refresh_user() self._refresh_user()
self.assertEqual(self.member_state, self.user.profile.state) self.assertEquals(self.member_state, self.user.profile.state)
higher_state.public = True higher_state.public = True
higher_state.save() higher_state.save()
self._refresh_user() self._refresh_user()
self.assertEqual(higher_state, self.user.profile.state) self.assertEquals(higher_state, self.user.profile.state)
higher_state.public = False higher_state.public = False
higher_state.save() higher_state.save()
self._refresh_user() self._refresh_user()
self.assertEqual(self.member_state, self.user.profile.state) self.assertEquals(self.member_state, self.user.profile.state)
def test_state_assignment_on_active_changed(self): def test_state_assignment_on_active_changed(self):
self.member_state.member_characters.add(self.test_character) self.member_state.member_characters.add(self.test_character)
self.user.is_active = False self.user.is_active = False
self.user.save() self.user.save()
self._refresh_user() self._refresh_user()
self.assertEqual(self.user.profile.state, self.guest_state) self.assertEquals(self.user.profile.state, self.guest_state)
self.user.is_active = True self.user.is_active = True
self.user.save() self.user.save()
self._refresh_user() self._refresh_user()
self.assertEqual(self.user.profile.state, self.member_state) self.assertEquals(self.user.profile.state, self.member_state)
class CharacterOwnershipCheckTestCase(TestCase): class CharacterOwnershipCheckTestCase(TestCase):
@@ -226,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

@@ -1,7 +1,6 @@
from math import ceil from math import ceil
from unittest.mock import patch from unittest.mock import patch
import requests
import requests_mock import requests_mock
from packaging.version import Version as Pep440Version from packaging.version import Version as Pep440Version
@@ -11,9 +10,9 @@ from django.test import TestCase
from allianceauth.templatetags.admin_status import ( from allianceauth.templatetags.admin_status import (
status_overview, status_overview,
_fetch_list_from_gitlab, _fetch_list_from_gitlab,
_current_notifications, _current_notifications,
_current_version_summary, _current_version_summary,
_fetch_notification_issues_from_gitlab, _fetch_notification_issues_from_gitlab,
_latests_versions _latests_versions
) )
@@ -55,17 +54,17 @@ TEST_VERSION = '2.6.5'
class TestStatusOverviewTag(TestCase): class TestStatusOverviewTag(TestCase):
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION) @patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@patch(MODULE_PATH + '.admin_status._fetch_celery_queue_length') @patch(MODULE_PATH + '.admin_status._fetch_celery_queue_length')
@patch(MODULE_PATH + '.admin_status._current_version_summary') @patch(MODULE_PATH + '.admin_status._current_version_summary')
@patch(MODULE_PATH + '.admin_status._current_notifications') @patch(MODULE_PATH + '.admin_status._current_notifications')
def test_status_overview( def test_status_overview(
self, self,
mock_current_notifications, mock_current_notifications,
mock_current_version_info, mock_current_version_info,
mock_fetch_celery_queue_length mock_fetch_celery_queue_length
): ):
# given
notifications = { notifications = {
'notifications': GITHUB_NOTIFICATION_ISSUES[:5] 'notifications': GITHUB_NOTIFICATION_ISSUES[:5]
} }
@@ -83,20 +82,22 @@ class TestStatusOverviewTag(TestCase):
} }
mock_current_version_info.return_value = version_info mock_current_version_info.return_value = version_info
mock_fetch_celery_queue_length.return_value = 3 mock_fetch_celery_queue_length.return_value = 3
# when
result = status_overview() result = status_overview()
# then expected = {
self.assertEqual(result["notifications"], GITHUB_NOTIFICATION_ISSUES[:5]) 'notifications': GITHUB_NOTIFICATION_ISSUES[:5],
self.assertTrue(result["latest_major"]) 'latest_major': True,
self.assertTrue(result["latest_minor"]) 'latest_minor': True,
self.assertTrue(result["latest_patch"]) 'latest_patch': True,
self.assertFalse(result["latest_beta"]) 'latest_beta': False,
self.assertEqual(result["current_version"], TEST_VERSION) 'current_version': TEST_VERSION,
self.assertEqual(result["latest_major_version"], '2.4.5') 'latest_major_version': '2.4.5',
self.assertEqual(result["latest_minor_version"], '2.4.0') 'latest_minor_version': '2.4.0',
self.assertEqual(result["latest_patch_version"], '2.4.5') 'latest_patch_version': '2.4.5',
self.assertEqual(result["latest_beta_version"], '2.4.4a1') 'latest_beta_version': '2.4.4a1',
self.assertEqual(result["task_queue_length"], 3) 'task_queue_length': 3,
}
self.assertEqual(result, expected)
class TestNotifications(TestCase): class TestNotifications(TestCase):
@@ -110,7 +111,7 @@ class TestNotifications(TestCase):
url = ( url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues' 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
'?labels=announcement' '?labels=announcement'
) )
requests_mocker.get(url, json=GITHUB_NOTIFICATION_ISSUES) requests_mocker.get(url, json=GITHUB_NOTIFICATION_ISSUES)
# when # when
result = _fetch_notification_issues_from_gitlab() result = _fetch_notification_issues_from_gitlab()
@@ -126,13 +127,13 @@ class TestNotifications(TestCase):
# then # then
self.assertEqual(result['notifications'], GITHUB_NOTIFICATION_ISSUES[:5]) self.assertEqual(result['notifications'], GITHUB_NOTIFICATION_ISSUES[:5])
@requests_mock.mock() @requests_mock.mock()
def test_current_notifications_failed(self, requests_mocker): def test_current_notifications_failed(self, requests_mocker):
# given # given
url = ( url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues' 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues'
'?labels=announcement' '?labels=announcement'
) )
requests_mocker.get(url, status_code=404) requests_mocker.get(url, status_code=404)
# when # when
result = _current_notifications() result = _current_notifications()
@@ -162,13 +163,17 @@ class TestVersionTags(TestCase):
@patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION) @patch(MODULE_PATH + '.admin_status.__version__', TEST_VERSION)
@patch(MODULE_PATH + '.admin_status.cache') @patch(MODULE_PATH + '.admin_status.cache')
def test_current_version_info_normal(self, mock_cache): def test_current_version_info_normal(self, mock_cache):
# given # given
mock_cache.get_or_set.return_value = GITHUB_TAGS mock_cache.get_or_set.return_value = GITHUB_TAGS
# when # when
result = _current_version_summary() result = _current_version_summary()
# then # then
self.assertTrue(result['latest_major'])
self.assertTrue(result['latest_minor'])
self.assertTrue(result['latest_patch']) self.assertTrue(result['latest_patch'])
self.assertEqual(result['latest_major_version'], '2.0.0')
self.assertEqual(result['latest_minor_version'], '2.4.0')
self.assertEqual(result['latest_patch_version'], '2.4.5') self.assertEqual(result['latest_patch_version'], '2.4.5')
self.assertEqual(result['latest_beta_version'], '2.4.6a1') self.assertEqual(result['latest_beta_version'], '2.4.6a1')
@@ -179,7 +184,7 @@ class TestVersionTags(TestCase):
url = ( url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth' 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
'/repository/tags' '/repository/tags'
) )
requests_mocker.get(url, status_code=500) requests_mocker.get(url, status_code=500)
# when # when
result = _current_version_summary() result = _current_version_summary()
@@ -192,7 +197,7 @@ class TestVersionTags(TestCase):
url = ( url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth' 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
'/repository/tags' '/repository/tags'
) )
requests_mocker.get(url, json=GITHUB_TAGS) requests_mocker.get(url, json=GITHUB_TAGS)
# when # when
result = _current_version_summary() result = _current_version_summary()
@@ -203,7 +208,7 @@ class TestVersionTags(TestCase):
@patch(MODULE_PATH + '.admin_status.cache') @patch(MODULE_PATH + '.admin_status.cache')
def test_current_version_info_return_no_data(self, mock_cache): def test_current_version_info_return_no_data(self, mock_cache):
# given # given
mock_cache.get_or_set.return_value = None mock_cache.get_or_set.return_value = None
# when # when
result = _current_version_summary() result = _current_version_summary()
# then # then
@@ -213,37 +218,43 @@ class TestVersionTags(TestCase):
class TestLatestsVersion(TestCase): class TestLatestsVersion(TestCase):
def test_all_version_types_defined(self): def test_all_version_types_defined(self):
tags = create_tags_list( tags = create_tags_list(
['2.1.1', '2.1.0', '2.0.0', '2.1.1a1', '1.1.1', '1.1.0', '1.0.0'] ['2.1.1', '2.1.0', '2.0.0', '2.1.1a1', '1.1.1', '1.1.0', '1.0.0']
) )
patch, beta = _latests_versions(tags) major, minor, patch, beta = _latests_versions(tags)
self.assertEqual(major, Pep440Version('2.0.0'))
self.assertEqual(minor, Pep440Version('2.1.0'))
self.assertEqual(patch, Pep440Version('2.1.1')) self.assertEqual(patch, Pep440Version('2.1.1'))
self.assertEqual(beta, Pep440Version('2.1.1a1')) self.assertEqual(beta, Pep440Version('2.1.1a1'))
def test_major_and_minor_not_defined_with_zero(self): def test_major_and_minor_not_defined_with_zero(self):
tags = create_tags_list( tags = create_tags_list(
['2.1.2', '2.1.1', '2.0.1', '2.1.1a1', '1.1.1', '1.1.0', '1.0.0'] ['2.1.2', '2.1.1', '2.0.1', '2.1.1a1', '1.1.1', '1.1.0', '1.0.0']
) )
patch, beta = _latests_versions(tags) major, minor, patch, beta = _latests_versions(tags)
self.assertEqual(major, Pep440Version('2.0.1'))
self.assertEqual(minor, Pep440Version('2.1.1'))
self.assertEqual(patch, Pep440Version('2.1.2')) self.assertEqual(patch, Pep440Version('2.1.2'))
self.assertEqual(beta, Pep440Version('2.1.1a1')) self.assertEqual(beta, Pep440Version('2.1.1a1'))
def test_can_ignore_invalid_versions(self): def test_can_ignore_invalid_versions(self):
tags = create_tags_list( tags = create_tags_list(
['2.1.1', '2.1.0', '2.0.0', '2.1.1a1', 'invalid'] ['2.1.1', '2.1.0', '2.0.0', '2.1.1a1', 'invalid']
) )
patch, beta = _latests_versions(tags) major, minor, patch, beta = _latests_versions(tags)
self.assertEqual(major, Pep440Version('2.0.0'))
self.assertEqual(minor, Pep440Version('2.1.0'))
self.assertEqual(patch, Pep440Version('2.1.1')) self.assertEqual(patch, Pep440Version('2.1.1'))
self.assertEqual(beta, Pep440Version('2.1.1a1')) self.assertEqual(beta, Pep440Version('2.1.1a1'))
class TestFetchListFromGitlab(TestCase): class TestFetchListFromGitlab(TestCase):
page_size = 2 page_size = 2
def setUp(self): def setUp(self):
self.url = ( self.url = (
'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth' 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth'
@@ -255,8 +266,8 @@ class TestFetchListFromGitlab(TestCase):
page = int(request.qs['page'][0]) page = int(request.qs['page'][0])
start = (page - 1) * cls.page_size start = (page - 1) * cls.page_size
end = start + cls.page_size end = start + cls.page_size
return GITHUB_TAGS[start:end] return GITHUB_TAGS[start:end]
@requests_mock.mock() @requests_mock.mock()
def test_can_fetch_one_page_with_header(self, requests_mocker): def test_can_fetch_one_page_with_header(self, requests_mocker):
headers = { headers = {
@@ -268,7 +279,7 @@ class TestFetchListFromGitlab(TestCase):
self.assertEqual(requests_mocker.call_count, 1) self.assertEqual(requests_mocker.call_count, 1)
@requests_mock.mock() @requests_mock.mock()
def test_can_fetch_one_page_wo_header(self, requests_mocker): def test_can_fetch_one_page_wo_header(self, requests_mocker):
requests_mocker.get(self.url, json=GITHUB_TAGS) requests_mocker.get(self.url, json=GITHUB_TAGS)
result = _fetch_list_from_gitlab(self.url) result = _fetch_list_from_gitlab(self.url)
self.assertEqual(result, GITHUB_TAGS) self.assertEqual(result, GITHUB_TAGS)
@@ -285,7 +296,7 @@ class TestFetchListFromGitlab(TestCase):
self.assertEqual(requests_mocker.call_count, 1) self.assertEqual(requests_mocker.call_count, 1)
@requests_mock.mock() @requests_mock.mock()
def test_can_fetch_multiple_pages(self, requests_mocker): def test_can_fetch_multiple_pages(self, requests_mocker):
total_pages = ceil(len(GITHUB_TAGS) / self.page_size) total_pages = ceil(len(GITHUB_TAGS) / self.page_size)
headers = { headers = {
'x-total-pages': str(total_pages) 'x-total-pages': str(total_pages)
@@ -296,7 +307,7 @@ class TestFetchListFromGitlab(TestCase):
self.assertEqual(requests_mocker.call_count, total_pages) self.assertEqual(requests_mocker.call_count, total_pages)
@requests_mock.mock() @requests_mock.mock()
def test_can_fetch_given_number_of_pages_only(self, requests_mocker): def test_can_fetch_given_number_of_pages_only(self, requests_mocker):
total_pages = ceil(len(GITHUB_TAGS) / self.page_size) total_pages = ceil(len(GITHUB_TAGS) / self.page_size)
headers = { headers = {
'x-total-pages': str(total_pages) 'x-total-pages': str(total_pages)
@@ -306,25 +317,3 @@ class TestFetchListFromGitlab(TestCase):
result = _fetch_list_from_gitlab(self.url, max_pages=max_pages) result = _fetch_list_from_gitlab(self.url, max_pages=max_pages)
self.assertEqual(result, GITHUB_TAGS[:4]) self.assertEqual(result, GITHUB_TAGS[:4])
self.assertEqual(requests_mocker.call_count, max_pages) self.assertEqual(requests_mocker.call_count, max_pages)
@requests_mock.mock()
@patch(MODULE_PATH + '.admin_status.logger')
def test_should_not_raise_any_exception_from_github_request_but_log_as_warning(
self, requests_mocker, mock_logger
):
for my_exception in [
requests.exceptions.ConnectionError,
requests.exceptions.HTTPError,
requests.exceptions.URLRequired,
requests.exceptions.TooManyRedirects,
requests.exceptions.ConnectTimeout,
requests.exceptions.Timeout,
]:
requests_mocker.get(self.url, exc=my_exception)
try:
result = _fetch_list_from_gitlab(self.url)
except Exception as ex:
self.fail(f"Unexpected exception raised: {ex}")
self.assertTrue(mock_logger.warning.called)
self.assertListEqual(result, [])

View File

@@ -9,8 +9,8 @@ app_name = 'authentication'
urlpatterns = [ urlpatterns = [
url(r'^$', views.index, name='index'), url(r'^$', views.index, name='index'),
url( url(
r'^account/login/$', r'^account/login/$',
TemplateView.as_view(template_name='public/login.html'), TemplateView.as_view(template_name='public/login.html'),
name='login' name='login'
), ),
url( url(
@@ -19,9 +19,9 @@ urlpatterns = [
name='change_main_character' name='change_main_character'
), ),
url( url(
r'^account/characters/add/$', r'^account/characters/add/$',
views.add_character, views.add_character,
name='add_character' name='add_character'
), ),
url(r'^dashboard/$', views.dashboard, name='dashboard'), url(r'^dashboard/$', views.dashboard, name='dashboard'),
] ]

View File

@@ -6,10 +6,8 @@ from django.contrib.auth import login, authenticate
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import signing from django.core import signing
from django.core.mail import EmailMultiAlternatives
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.template.loader import render_to_string
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -18,8 +16,8 @@ from esi.decorators import token_required
from esi.models import Token from esi.models import Token
from django_registration.backends.activation.views import ( from django_registration.backends.activation.views import (
RegistrationView as BaseRegistrationView, RegistrationView as BaseRegistrationView,
ActivationView as BaseActivationView, ActivationView as BaseActivationView,
REGISTRATION_SALT REGISTRATION_SALT
) )
from django_registration.signals import user_registered from django_registration.signals import user_registered
@@ -54,7 +52,7 @@ def dashboard(request):
.filter(character_ownership__user=request.user)\ .filter(character_ownership__user=request.user)\
.select_related()\ .select_related()\
.order_by('character_name') .order_by('character_name')
context = { context = {
'groups': groups, 'groups': groups,
'characters': characters 'characters': characters
@@ -65,7 +63,7 @@ def dashboard(request):
@login_required @login_required
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES) @token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
def main_character_change(request, token): def main_character_change(request, token):
logger.debug(f"main_character_change called by user {request.user} for character {token.character_name}") logger.debug("main_character_change called by user %s for character %s" % (request.user, token.character_name))
try: try:
co = CharacterOwnership.objects.get(character__character_id=token.character_id, user=request.user) co = CharacterOwnership.objects.get(character__character_id=token.character_id, user=request.user)
except CharacterOwnership.DoesNotExist: except CharacterOwnership.DoesNotExist:
@@ -73,7 +71,7 @@ def main_character_change(request, token):
co = CharacterOwnership.objects.create_by_token(token) co = CharacterOwnership.objects.create_by_token(token)
else: else:
messages.error( messages.error(
request, request,
_('Cannot change main character to %(char)s: character owned by a different account.') % ({'char': token.character_name}) _('Cannot change main character to %(char)s: character owned by a different account.') % ({'char': token.character_name})
) )
co = None co = None
@@ -139,51 +137,8 @@ class RegistrationView(BaseRegistrationView):
form_class = RegistrationForm form_class = RegistrationForm
template_name = "public/register.html" template_name = "public/register.html"
email_body_template = "registration/activation_email.txt" email_body_template = "registration/activation_email.txt"
email_body_template_html = "registration/activation_email_html.txt"
email_subject_template = "registration/activation_email_subject.txt" email_subject_template = "registration/activation_email_subject.txt"
success_url = reverse_lazy('registration_complete') success_url = reverse_lazy('registration_complete')
def send_activation_email(self, user):
"""
Implement our own way to send a mail to make sure we
send a RFC conform multipart email
:param user:
:type user:
"""
activation_key = self.get_activation_key(user)
context = self.get_email_context(activation_key)
context["user"] = user
# email subject
subject = render_to_string(
template_name=self.email_subject_template,
context=context,
request=self.request,
)
subject = "".join(subject.splitlines())
# plaintext email body part
message = render_to_string(
template_name=self.email_body_template,
context=context,
request=self.request,
)
# html email body part
message_html = render_to_string(
template_name=self.email_body_template_html,
context=context,
request=self.request,
)
# send it
user.email_user(
subject,
message,
settings.DEFAULT_FROM_EMAIL,
**{'html_message': message_html},
)
def get_success_url(self, user): def get_success_url(self, user):
if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True): if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
@@ -199,7 +154,7 @@ class RegistrationView(BaseRegistrationView):
if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True): if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
# Keep the request so the user can be automagically logged in. # Keep the request so the user can be automagically logged in.
setattr(self, 'request', request) setattr(self, 'request', request)
return super().dispatch(request, *args, **kwargs) return super(RegistrationView, self).dispatch(request, *args, **kwargs)
def register(self, form): def register(self, form):
user = User.objects.get(pk=self.request.session.get('registration_uid')) user = User.objects.get(pk=self.request.session.get('registration_uid'))
@@ -218,7 +173,7 @@ class RegistrationView(BaseRegistrationView):
return signing.dumps(obj=[getattr(user, User.USERNAME_FIELD), user.email], salt=REGISTRATION_SALT) return signing.dumps(obj=[getattr(user, User.USERNAME_FIELD), user.email], salt=REGISTRATION_SALT)
def get_email_context(self, activation_key): def get_email_context(self, activation_key):
context = super().get_email_context(activation_key) context = super(RegistrationView, self).get_email_context(activation_key)
context['url'] = context['site'].domain + reverse('registration_activate', args=[activation_key]) context['url'] = context['site'].domain + reverse('registration_activate', args=[activation_key])
return context return context
@@ -226,12 +181,12 @@ class RegistrationView(BaseRegistrationView):
# Step 3 # Step 3
class ActivationView(BaseActivationView): class ActivationView(BaseActivationView):
template_name = "registration/activate.html" template_name = "registration/activate.html"
success_url = reverse_lazy('registration_activation_complete') success_url = reverse_lazy('registration_activation_complete')
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

@@ -41,7 +41,7 @@ def create_project(parser, options, args):
# Call the command with extra context # Call the command with extra context
call_command(StartProject(), *args, **command_options) call_command(StartProject(), *args, **command_options)
print(f"Success! {args[0]} has been created.") # noqa print("Success! %(project_name)s has been created." % {'project_name': args[0]}) # noqa
def update_settings(parser, options, args): def update_settings(parser, options, args):
@@ -69,10 +69,10 @@ def update_settings(parser, options, args):
template_settings_path = os.path.join(template_path, 'project_name/settings/base.py') template_settings_path = os.path.join(template_path, 'project_name/settings/base.py')
# overwrite the local project's base settings # overwrite the local project's base settings
with open(template_settings_path) as template, open(settings_path, 'w') as target: with open(template_settings_path, 'r') as template, open(settings_path, 'w') as target:
target.write(template.read()) target.write(template.read())
print(f"Successfully updated {project_name} settings.") print("Successfully updated %(project_name)s settings." % {'project_name': project_name})
COMMANDS = { COMMANDS = {

View File

@@ -3,4 +3,4 @@ from django.contrib import admin
from .models import CorpStats, CorpMember from .models import CorpStats, CorpMember
admin.site.register(CorpStats) admin.site.register(CorpStats)
admin.site.register(CorpMember) admin.site.register(CorpMember)

View File

@@ -6,13 +6,11 @@ from allianceauth.corputils import urls
class CorpStats(MenuItemHook): class CorpStats(MenuItemHook):
def __init__(self): def __init__(self):
MenuItemHook.__init__( MenuItemHook.__init__(self,
self, _('Corporation Stats'),
_('Corporation Stats'), 'fas fa-share-alt fa-fw',
'fas fa-share-alt fa-fw', 'corputils:view',
'corputils:view', navactive=['corputils:'])
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

@@ -29,7 +29,7 @@ class CorpStatsQuerySet(models.QuerySet):
if user.has_perm('corputils.view_state_corpstats'): if user.has_perm('corputils.view_state_corpstats'):
queries.append(models.Q(corp__in=user.profile.state.member_corporations.all())) queries.append(models.Q(corp__in=user.profile.state.member_corporations.all()))
queries.append(models.Q(corp__alliance__in=user.profile.state.member_alliances.all())) queries.append(models.Q(corp__alliance__in=user.profile.state.member_alliances.all()))
logger.debug(f'{len(queries)} queries for user {user} visible corpstats.') logger.debug('%s queries for user %s visible corpstats.' % (len(queries), user))
# filter based on queries # filter based on queries
query = queries.pop() query = queries.pop()
for q in queries: for q in queries:
@@ -37,7 +37,7 @@ class CorpStatsQuerySet(models.QuerySet):
return self.filter(query) return self.filter(query)
except AssertionError: except AssertionError:
logger.debug('User %s has no main character. No corpstats visible.' % user) logger.debug('User %s has no main character. No corpstats visible.' % user)
return self.none() return self.none()
class CorpStatsManager(models.Manager): class CorpStatsManager(models.Manager):

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations from django.db import migrations

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations from django.db import migrations

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@@ -11,7 +13,8 @@ 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 members.items()] [CorpMember(corpstats=cs, character_id=member_id, character_name=member_name) for member_id, member_name in
members.items()]
) )
@@ -47,7 +50,7 @@ class Migration(migrations.Migration):
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='corpmember', name='corpmember',
unique_together={('corpstats', 'character_id')}, unique_together=set([('corpstats', 'character_id')]),
), ),
migrations.RunPython(convert_json_to_members, convert_members_to_json), migrations.RunPython(convert_json_to_members, convert_members_to_json),
migrations.RemoveField( migrations.RemoveField(

View File

@@ -1,4 +1,6 @@
# -*- 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 django.db import migrations from django.db import migrations

View File

@@ -6,7 +6,8 @@ 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, EveAllianceInfo from allianceauth.eveonline.models import EveCorporationInfo, EveCharacter,\
EveAllianceInfo
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.corputils.managers import CorpStatsManager from allianceauth.corputils.managers import CorpStatsManager
@@ -43,12 +44,13 @@ class CorpStats(models.Model):
objects = CorpStatsManager() objects = CorpStatsManager()
def __str__(self): def __str__(self):
return f"{self.__class__.__name__} for {self.corp}" return "%s for %s" % (self.__class__.__name__, self.corp)
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()['corporation_id'] == int(self.corp.corporation_id) assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()[
'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()
@@ -56,15 +58,18 @@ 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_id_chunks] member_name_chunks = [c.Universe.post_universe_names(ids=id_chunk).result() for id_chunk in
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 not CorpMember.objects.filter(corpstats=self, character_id=m_id).exists()] missing_members = [m_id for m_id in member_ids if
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 missing_members]) [CorpMember(character_id=m_id, character_name=member_list[m_id], corpstats=self) for m_id in
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()
@@ -73,24 +78,23 @@ class CorpStats(models.Model):
self.save() self.save()
except TokenError as e: except TokenError as e:
logger.warning(f"{self} failed to update: {e}") logger.warning("%s failed to update: %s" % (self, e))
if self.token.user: if self.token.user:
notify( notify(self.token.user, "%s failed to update with your ESI token." % self,
self.token.user, "%s failed to update with your ESI token." % self, message="Your token has expired or is no longer valid. Please add a new one to create a new CorpStats.",
message="Your token has expired or is no longer valid. Please add a new one to create a new CorpStats.", level="error")
level="error")
self.delete() self.delete()
except HTTPForbidden as e: except HTTPForbidden as e:
logger.warning(f"{self} failed to update: {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, message=f"{e.status_code}: {e.message}", level="error") notify(self.token.user, "%s failed to update with your ESI token." % self,
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( notify(self.token.user, "%s cannot update with your ESI token." % self,
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")
message="%s cannot update with your ESI token as you have left corp." % self, level="error")
self.delete() self.delete()
@property @property
@@ -99,7 +103,7 @@ class CorpStats(models.Model):
@property @property
def user_count(self): def user_count(self):
return len({m.main_character for m in self.members.all() if m.main_character}) return len(set([m.main_character for m in self.members.all() if m.main_character]))
@property @property
def registered_member_count(self): def registered_member_count(self):
@@ -123,7 +127,9 @@ 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 m.main_character and int(m.main_character.character_id) == int(m.character_id)]) 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)])
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()

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +1,15 @@
{% extends 'allianceauth/base.html' %} {% extends 'allianceauth/base.html' %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Corporation Member Data" %}{% endblock %} {% block page_title %}{% trans "Corporation Member Data" %}{% endblock %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% translate "Corporation Member Data" %}</h1> <h1 class="page-header text-center">{% trans "Corporation Member Data" %}</h1>
<div class="col-lg-10 col-lg-offset-1 container"> <div class="col-lg-10 col-lg-offset-1 container">
<nav class="navbar navbar-default"> <nav class="navbar navbar-default">
<div class="container-fluid"> <div class="container-fluid">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li class="dropdown"> <li class="dropdown">
<a href="#" id="dLabel" class="dropdown-toggle" role="button" data-toggle="dropdown" aria-haspopup="false" aria-expanded="false">{% translate "Corporations" %}<span class="caret"></span></a> <a href="#" id="dLabel" class="dropdown-toggle" role="button" data-toggle="dropdown" aria-haspopup="false" aria-expanded="false">{% trans "Corporations" %}<span class="caret"></span></a>
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"> <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
{% for corpstat in available %} {% for corpstat in available %}
<li> <li>
@@ -20,13 +20,13 @@
</li> </li>
{% if perms.corputils.add_corpstats %} {% if perms.corputils.add_corpstats %}
<li> <li>
<a href="{% url 'corputils:add' %}">{% translate "Add" %}</a> <a href="{% url 'corputils:add' %}">{% trans "Add" %}</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
<form class="navbar-form navbar-right" role="search" action="{% url 'corputils:search' %}" method="GET"> <form class="navbar-form navbar-right" role="search" action="{% url 'corputils:search' %}" method="GET">
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" name="search_string" placeholder="{% if search_string %}{{ search_string }}{% else %}{% translate "Search all corporations..." %}{% endif %}"> <input type="text" class="form-control" name="search_string" placeholder="{% if search_string %}{{ search_string }}{% else %}{% trans "Search all corporations..." %}{% endif %}">
</div> </div>
</form> </form>
</div> </div>
@@ -34,4 +34,4 @@
{% block member_data %}{% endblock %} {% block member_data %}{% endblock %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

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