Compare commits

...

59 Commits

Author SHA1 Message Date
Ariel Rin
71c9faaf28 Version Bump 3.0.0b2 2022-07-07 18:15:37 +10:00
Ariel Rin
236c70316c Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v3.x 2022-07-07 18:07:49 +10:00
Ariel Rin
0d0686f58a Merge branch 'allianceauth-prefix-static-files-directory' into v3.x 2022-07-07 18:01:12 +10:00
Ariel Rin
3706a1aedf Merge branch 'improve-autodocs-for-models' into 'master'
Improve autodocs for models & more

See merge request allianceauth/allianceauth!1435
2022-07-07 07:38:58 +00:00
Ariel Rin
47f1b77320 Merge branch 'consolidate-redis-client-access' into 'master'
Ensure backwards compatibility when fetching a redis client

See merge request allianceauth/allianceauth!1428
2022-07-07 07:37:21 +00:00
Erik Kalkoken
8dec242a93 Ensure backwards compatibility when fetching a redis client 2022-07-07 07:37:21 +00:00
Ariel Rin
6b934060dd Merge branch 'remove-old-unused-templates' into 'v3.x'
Remove unused templates

See merge request allianceauth/allianceauth!1431
2022-07-07 07:36:21 +00:00
Ariel Rin
ff88a16163 Merge branch 'smf-2.1-compatibility' into 'v3.x'
SMF 2.1 compatibility

See merge request allianceauth/allianceauth!1432
2022-07-07 07:35:58 +00:00
Ariel Rin
e81a66b74b Merge branch 'setup.cfg-vs-manifest-part-2' into 'v3.x'
setup.cfg vs MANIFEST.in (Round 2)

See merge request allianceauth/allianceauth!1434
2022-07-07 07:34:17 +00:00
ErikKalkoken
2ff200c566 Refer to django-esi docs 2022-06-27 13:43:45 +02:00
ErikKalkoken
091a2637ea Add extension to improve autodocs for Django models & enable source links 2022-06-27 13:41:15 +02:00
Peter Pfeufer
6c7729308c Should be find_namespace 2022-06-27 05:39:45 +02:00
Peter Pfeufer
0195ef23d5 Attempt to remove MANIFEST.in file (Part 2) 2022-06-27 05:38:31 +02:00
Peter Pfeufer
a7afa4a0c3 Background for login page fixed 2022-06-25 14:44:28 +02:00
Peter Pfeufer
004100091f Moved authentication and services into allianceauth folder 2022-06-25 14:01:32 +02:00
Peter Pfeufer
20231ce198 Load static files from their new place 2022-06-25 13:51:46 +02:00
Peter Pfeufer
0851a6d085 [Cleanup] Removed {% load static %} when no static files are loaded 2022-06-25 13:51:10 +02:00
Peter Pfeufer
0cd36ad5bc Moved SSO button from "root" to "authentication" 2022-06-25 13:46:56 +02:00
Peter Pfeufer
7618dd0f91 Removed obsolete attribute 2022-06-25 13:31:12 +02:00
Peter Pfeufer
cf49a2cb65 Moved static files to their own directory 2022-06-25 13:30:32 +02:00
Peter Pfeufer
cbdce18633 Use regex to determine the SMF version (thanks @colcrunch) 2022-06-24 23:01:16 +02:00
Peter Pfeufer
a0d14eb1d3 SQL queries need to be different depending on SMF version
It's not a work of art, but it does the job. If anyone has a better idea, hit the comments ...
2022-06-24 22:40:04 +02:00
Peter Pfeufer
53ce4d2453 f-strings in log messages
Easier to read and modernized the code
2022-06-24 21:09:32 +02:00
Peter Pfeufer
1ddb041d6d Add Exception messages to exception logging 2022-06-24 21:00:54 +02:00
Peter Pfeufer
43cbfd1c47 Stop usage of deprecated logger functions 2022-06-24 20:57:58 +02:00
Peter Pfeufer
b9a8495a43 Add exception message to log output 2022-06-24 20:56:38 +02:00
Peter Pfeufer
e296477880 Use pwhash instead of passwd
This way we ensure that the initial password actually works and the user doesn't have to set a new one right away.
2022-06-24 20:55:04 +02:00
Peter Pfeufer
bd5c2d8cbc Removed non existent table columns from SQL query 2022-06-24 20:54:05 +02:00
Peter Pfeufer
ccd40d5c68 Remove unused templates 2022-06-23 21:26:13 +02:00
Ariel Rin
7f8ca4fad2 Version Bump 3.0.0b1 2022-06-18 14:48:40 +10:00
Ariel Rin
4bb9a7155d this should point at master, not an old branch 2022-06-18 14:48:14 +10:00
Ariel Rin
2ac79954f3 update from Transifex 2022-06-18 14:42:26 +10:00
Ariel Rin
585e1f47f3 cap docutils to enable recommonmark to still work. 2022-06-18 14:21:56 +10:00
Ariel Rin
a33c474b35 Use new Redis in tests 2022-06-18 14:05:39 +10:00
Ariel Rin
61c3d8964b use new django-redis package, reorganize to match setup.cfg order for better comparison 2022-06-18 13:48:46 +10:00
Ariel Rin
1c927c5820 move secret_detection gitlab job into gitlab stage 2022-06-18 13:41:39 +10:00
Ariel Rin
ff0fa0329d Merge branch 'master' of https://gitlab.com/allianceauth/allianceauth into v3.x 2022-06-18 13:28:15 +10:00
Ariel Rin
e51ea439ca update pre-commit in prep 2022-06-18 13:19:59 +10:00
Ariel Rin
113977b19f Version Bump 2.13.0 2022-06-18 13:07:36 +10:00
Ariel Rin
8f39b50b6d Merge branch 'Maestro-Zacht-fix-fat-attributeerror' into 'master'
fixed attribute error

See merge request allianceauth/allianceauth!1421
2022-06-18 02:53:11 +00:00
Maestro-Zacht
95b309c358 fixed attribute error 2022-06-18 02:53:11 +00:00
Ariel Rin
cf3df3b715 Merge branch 'fix_issue_1328' into 'master'
Fix: Changing group's state setting does not kick existing non-conforming group members

Closes #1328

See merge request allianceauth/allianceauth!1400
2022-06-18 02:47:14 +00:00
Erik Kalkoken
d815028c4d Fix: Changing group's state setting does not kick existing non-conforming group members 2022-06-18 02:47:14 +00:00
Ariel Rin
ac5570abe2 Merge branch 'fix_issue_1268' into 'master'
Fix: Service group updates broken when adding users to groups

Closes #1268

See merge request allianceauth/allianceauth!1403
2022-06-18 02:41:23 +00:00
Erik Kalkoken
84ad571aa4 Fix: Service group updates broken when adding users to groups 2022-06-18 02:41:23 +00:00
Ariel Rin
38e7705ae7 Merge branch 'docs-dark-mode' into 'master'
Add automatic dark mode to docs

See merge request allianceauth/allianceauth!1427
2022-06-18 02:39:59 +00:00
ErikKalkoken
0b6af014fa Add automatic dark mode to docs 2022-06-17 21:49:18 +02:00
Ariel Rin
2401f2299d Merge branch 'fix-doc-redis-issue' into 'master'
Fix: Broken docs generation on readthedocs.org (2nd attempt)

See merge request allianceauth/allianceauth!1425
2022-06-17 11:58:45 +00:00
Erik Kalkoken
919768c8bb Fix: Broken docs generation on readthedocs.org (2nd attempt) 2022-06-17 11:58:45 +00:00
Ariel Rin
24db21463b Merge branch 'docs-template-tags-example' into 'master'
Add example for template tags to docs

See merge request allianceauth/allianceauth!1426
2022-06-17 11:58:05 +00:00
Erik Kalkoken
1e029af83a Add example for template tags to docs 2022-06-17 11:58:05 +00:00
Ariel Rin
53dd8ce606 Merge branch 'buil-tests' into 'v3.x'
Add build test to downloadable artifacts

See merge request allianceauth/allianceauth!1424
2022-06-17 11:56:20 +00:00
Peter Pfeufer
0f4003366d Add build test to downloadable artifacts 2022-06-17 11:56:20 +00:00
Ariel Rin
2b31be789d Merge branch 'fix-issue-1336' into 'master'
Fix: Broken docs generation on readthedocs.org

Closes #1336

See merge request allianceauth/allianceauth!1423
2022-06-06 10:48:16 +00:00
Erik Kalkoken
bf1b4bb549 Fix: Broken docs generation on readthedocs.org 2022-06-06 10:48:16 +00:00
Ariel Rin
dd42b807f0 Version Bump 2.12.1 2022-05-13 00:19:45 +10:00
Ariel Rin
542fbafd98 Merge branch 'cherry-pick-4836559a' into 'v2.12.x'
Merge branch 'fix-decimal_widthratio-template-tag' into 'v2.12.x'

See merge request allianceauth/allianceauth!1420
2022-05-12 14:14:01 +00:00
Ariel Rin
37b9f5c882 Merge branch 'fix-decimal_widthratio-template-tag' into 'v3.x'
[FIX] Division by zero in decimal_widthratio template tag

See merge request allianceauth/allianceauth!1419

(cherry picked from commit 4836559abe)

8dd07b97 [FIX] Devision by zero in decimal_widthratio template tag
17b06c88 Make it a string in accordance to the return value type
2022-05-12 13:33:45 +00:00
Ariel Rin
5bde9a6952 Version Bump 2.12.0 2022-05-12 18:54:22 +10:00
133 changed files with 1016 additions and 458 deletions

View File

@@ -5,16 +5,16 @@
- merge_requests - merge_requests
stages: stages:
- pre-commit - pre-commit
- gitlab - gitlab
- test - test
- deploy - deploy
- docker - docker
include: include:
- template: Dependency-Scanning.gitlab-ci.yml - template: Dependency-Scanning.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml - template: Security/SAST.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml - template: Security/Secret-Detection.gitlab-ci.yml
before_script: before_script:
- apt-get update && apt-get install redis-server -y - apt-get update && apt-get install redis-server -y
@@ -42,16 +42,20 @@ 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 libmariadb-dev -y
- redis-server --daemonize yes - redis-server --daemonize yes
- python -V - python -V
- pip install wheel tox - pip install wheel tox
secret_detection:
stage: gitlab
before_script: []
test-3.8-core: test-3.8-core:
<<: *only-default <<: *only-default
image: python:3.8-bullseye image: python:3.8-bullseye
script: script:
- tox -e py38-core - tox -e py38-core
artifacts: artifacts:
when: always when: always
reports: reports:
@@ -63,7 +67,7 @@ test-3.9-core:
<<: *only-default <<: *only-default
image: python:3.9-bullseye image: python:3.9-bullseye
script: script:
- tox -e py39-core - tox -e py39-core
artifacts: artifacts:
when: always when: always
reports: reports:
@@ -75,7 +79,7 @@ test-3.10-core:
<<: *only-default <<: *only-default
image: python:3.10-bullseye image: python:3.10-bullseye
script: script:
- tox -e py310-core - tox -e py310-core
artifacts: artifacts:
when: always when: always
reports: reports:
@@ -87,7 +91,7 @@ test-3.11-core:
<<: *only-default <<: *only-default
image: python:3.11-rc-bullseye image: python:3.11-rc-bullseye
script: script:
- tox -e py311-core - tox -e py311-core
artifacts: artifacts:
when: always when: always
reports: reports:
@@ -100,7 +104,7 @@ test-3.8-all:
<<: *only-default <<: *only-default
image: python:3.8-bullseye image: python:3.8-bullseye
script: script:
- tox -e py38-all - tox -e py38-all
artifacts: artifacts:
when: always when: always
reports: reports:
@@ -112,7 +116,7 @@ test-3.9-all:
<<: *only-default <<: *only-default
image: python:3.9-bullseye image: python:3.9-bullseye
script: script:
- tox -e py39-all - tox -e py39-all
artifacts: artifacts:
when: always when: always
reports: reports:
@@ -124,7 +128,7 @@ test-3.10-all:
<<: *only-default <<: *only-default
image: python:3.10-bullseye image: python:3.10-bullseye
script: script:
- tox -e py310-all - tox -e py310-all
artifacts: artifacts:
when: always when: always
reports: reports:
@@ -136,7 +140,7 @@ test-3.11-all:
<<: *only-default <<: *only-default
image: python:3.11-rc-bullseye image: python:3.11-rc-bullseye
script: script:
- tox -e py311-all - tox -e py311-all
artifacts: artifacts:
when: always when: always
reports: reports:
@@ -145,6 +149,31 @@ test-3.11-all:
path: coverage.xml path: coverage.xml
allow_failure: true allow_failure: true
build-test:
stage: test
image: python:3.10-bullseye
before_script:
- python -m pip install --upgrade pip
- python -m pip install --upgrade build
- python -m pip install --upgrade setuptools wheel
script:
- python -m build
artifacts:
when: always
name: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
paths:
- dist/*
expire_in: 1 year
test-docs:
<<: *only-default
image: python:3.10-bullseye
script:
- tox -e docs
deploy_production: deploy_production:
stage: deploy stage: deploy
image: python:3.10-bullseye image: python:3.10-bullseye

View File

@@ -5,7 +5,7 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0 rev: v4.3.0
hooks: hooks:
- id: check-case-conflict - id: check-case-conflict
- id: check-json - id: check-json
@@ -25,15 +25,15 @@ repos:
rev: 2.4.0 rev: 2.4.0
hooks: hooks:
- id: editorconfig-checker - id: editorconfig-checker
exclude: ^(LICENSE|allianceauth\/static\/css\/themes\/bootstrap-locals.less|allianceauth\/eveonline\/swagger.json|(.*.po)|(.*.mo)) exclude: ^(LICENSE|allianceauth\/static\/allianceauth\/css\/themes\/bootstrap-locals.less|allianceauth\/eveonline\/swagger.json|(.*.po)|(.*.mo))
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.31.0 rev: v2.34.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [ --py38-plus ] args: [ --py38-plus ]
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.20.0 rev: v1.20.1
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt

View File

@@ -1,7 +0,0 @@
include LICENSE
include README.md
include MANIFEST.in
graft allianceauth
global-exclude __pycache__
global-exclude *.py[co]

View File

@@ -1,7 +1,7 @@
# This will make sure the app is always imported when # This will make sure the app is always imported when
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.
__version__ = '3.0.0a5' __version__ = '3.0.0b2'
__title__ = 'Alliance Auth' __title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth' __url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = f'{__title__} v{__version__}' NAME = f'{__title__} v{__version__}'

View File

@@ -322,7 +322,7 @@ class UserAdmin(BaseUserAdmin):
class Media: class Media:
css = { css = {
"all": ("authentication/css/admin.css",) "all": ("allianceauth/authentication/css/admin.css",)
} }
def get_queryset(self, request): def get_queryset(self, request):
@@ -542,7 +542,7 @@ class BaseOwnershipAdmin(admin.ModelAdmin):
class Media: class Media:
css = { css = {
"all": ("authentication/css/admin.css",) "all": ("allianceauth/authentication/css/admin.css",)
} }
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):

View File

@@ -1,27 +1,62 @@
import datetime as dt import datetime as dt
from typing import Optional, List import logging
from typing import List, Optional
from redis import Redis
from pytz import utc from pytz import utc
from redis import Redis, RedisError
from django_redis import get_redis_connection from allianceauth.utils.cache import get_redis_client
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: class EventSeries:
"""API for recording and analysing a series of events.""" """API for recording and analyzing a series of events."""
_ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES" _ROOT_KEY = "ALLIANCEAUTH_EVENT_SERIES"
def __init__(self, key_id: str, redis: Redis = None) -> None: def __init__(self, key_id: str, redis: Redis = None) -> None:
self._redis = get_redis_connection("default") if not redis else redis self._redis = get_redis_client() if not redis else redis
if not isinstance(self._redis, Redis): try:
raise TypeError( if not self._redis.ping():
"This class requires a Redis client, but none was provided " raise RuntimeError()
"and the default Django cache backend is not Redis either." 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._key_id = str(key_id)
self.clear() 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 @property
def _key_counter(self): def _key_counter(self):
return f"{self._ROOT_KEY}_{self._key_id}_COUNTER" return f"{self._ROOT_KEY}_{self._key_id}_COUNTER"

View File

@@ -1,13 +1,48 @@
import datetime as dt import datetime as dt
from unittest.mock import patch
from pytz import utc from pytz import utc
from redis import RedisError
from django.test import TestCase from django.test import TestCase
from django.utils.timezone import now from django.utils.timezone import now
from allianceauth.authentication.task_statistics.event_series import EventSeries from allianceauth.authentication.task_statistics.event_series import (
EventSeries,
_RedisStub,
)
MODULE_PATH = "allianceauth.authentication.task_statistics.event_series"
class TestEventSeries(TestCase): class TestEventSeries(TestCase):
def test_should_abort_without_redis_client(self):
# when
with patch(MODULE_PATH + ".get_redis_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 + ".get_redis_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 + ".get_redis_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): def test_should_add_event(self):
# given # given
events = EventSeries("dummy") events = EventSeries("dummy")

View File

@@ -1,5 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Dashboard" %}{% endblock %} {% block page_title %}{% translate "Dashboard" %}{% endblock %}
@@ -28,7 +27,7 @@
<table class="table"> <table class="table">
<tr> <tr>
<td class="text-center"> <td class="text-center">
<img class="ra-avatar"src="{{ main.portrait_url_128 }}"> <img class="ra-avatar" src="{{ main.portrait_url_128 }}">
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -40,7 +39,7 @@
<table class="table"> <table class="table">
<tr> <tr>
<td class="text-center"> <td class="text-center">
<img class="ra-avatar"src="{{ main.corporation_logo_url_128 }}"> <img class="ra-avatar" src="{{ main.corporation_logo_url_128 }}">
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -53,7 +52,7 @@
<table class="table"> <table class="table">
<tr> <tr>
<td class="text-center"> <td class="text-center">
<img class="ra-avatar"src="{{ main.alliance_logo_url_128 }}"> <img class="ra-avatar" src="{{ main.alliance_logo_url_128 }}">
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -64,7 +63,7 @@
<table class="table"> <table class="table">
<tr> <tr>
<td class="text-center"> <td class="text-center">
<img class="ra-avatar"src="{{ main.faction_logo_url_128 }}"> <img class="ra-avatar" src="{{ main.faction_logo_url_128 }}">
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@@ -7,7 +7,7 @@
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" content=""> <meta name="author" content="">
<meta property="og:title" content="{{ SITE_NAME }}"> <meta property="og:title" content="{{ SITE_NAME }}">
<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'icons/apple-touch-icon.png' %}"> <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{% static 'allianceauth/icons/apple-touch-icon.png' %}">
<meta property="og:description" content="Alliance Auth - An auth system for EVE Online to help in-game organizations manage online service access."> <meta property="og:description" content="Alliance Auth - An auth system for EVE Online to help in-game organizations manage online service access.">
{% include 'allianceauth/icons.html' %} {% include 'allianceauth/icons.html' %}
@@ -21,7 +21,7 @@
<style> <style>
body { body {
background: url('{% static 'authentication/img/background.jpg' %}') no-repeat center center fixed; background: url('{% static 'allianceauth/authentication/img/background.jpg' %}') no-repeat center center fixed;
-webkit-background-size: cover; -webkit-background-size: cover;
-moz-background-size: cover; -moz-background-size: cover;
-o-background-size: cover; -o-background-size: cover;

View File

@@ -7,6 +7,6 @@
{% 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 'allianceauth/authentication/img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}">
</a> </a>
{% endblock %} {% endblock %}

View File

@@ -1,5 +1,4 @@
{% extends 'public/base.html' %} {% extends 'public/base.html' %}
{% load static %}
{% 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 %}

View File

@@ -1,6 +1,5 @@
{% extends 'public/base.html' %} {% extends 'public/base.html' %}
{% load static %}
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}

View File

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

View File

@@ -1,14 +0,0 @@
{% extends 'public/middle_box.html' %}
{% load bootstrap %}
{% load i18n %}
{% load static %}
{% block page_title %}{% translate "Register" %}{% endblock %}
{% block middle_box_content %}
<form class="form-signin" role="form" action="" method="POST">
{% csrf_token %}
{{ form|bootstrap }}
<br>
<button class="btn btn-lg btn-primary btn-block" type="submit">{% translate "Submit" %}</button>
<br>
</form>
{% endblock %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Create Fatlink" %}{% endblock page_title %} {% block page_title %}{% translate "Create Fatlink" %}{% endblock page_title %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %} {% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %} {% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %} {% block page_title %}{% translate "Personal fatlink statistics" %}{% endblock page_title %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Fatlink Corp Statistics" %}{% endblock page_title %} {% block page_title %}{% translate "Fatlink Corp Statistics" %}{% endblock page_title %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Fatlink statistics" %}{% endblock page_title %} {% block page_title %}{% translate "Fatlink statistics" %}{% endblock page_title %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %} {% block page_title %}{% translate "Fatlink view" %}{% endblock page_title %}

View File

@@ -212,7 +212,14 @@ def fatlink_monthly_personal_statistics_view(request, year, month, char_id=None)
start_of_previous_month = first_day_of_previous_month(year, month) start_of_previous_month = first_day_of_previous_month(year, month)
if request.user.has_perm('auth.fleetactivitytracking_statistics') and char_id: if request.user.has_perm('auth.fleetactivitytracking_statistics') and char_id:
user = EveCharacter.objects.get(character_id=char_id).user try:
user = EveCharacter.objects.get(character_id=char_id).character_ownership.user
except EveCharacter.DoesNotExist:
messages.error(request, _('Character does not exist'))
return redirect('fatlink:view')
except AttributeError:
messages.error(request, _('User does not exist'))
return redirect('fatlink:view')
else: else:
user = request.user user = request.user
logger.debug(f"Personal monthly statistics view for user {user} called by {request.user}") logger.debug(f"Personal monthly statistics view for user {user} called by {request.user}")

View File

@@ -1,8 +1,8 @@
from django.apps import apps from django.apps import apps
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import Group as BaseGroup
from django.contrib.auth.models import Permission, User from django.contrib.auth.models import Group as BaseGroup, Permission, User
from django.db.models import Count from django.db.models import Count, Exists, OuterRef
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.db.models.signals import ( from django.db.models.signals import (
m2m_changed, m2m_changed,
@@ -15,6 +15,7 @@ from django.dispatch import receiver
from .forms import GroupAdminForm, ReservedGroupNameAdminForm from .forms import GroupAdminForm, ReservedGroupNameAdminForm
from .models import AuthGroup, GroupRequest, ReservedGroupName from .models import AuthGroup, GroupRequest, ReservedGroupName
from .tasks import remove_users_not_matching_states_from_group
if 'eve_autogroups' in apps.app_configs: if 'eve_autogroups' in apps.app_configs:
_has_auto_groups = True _has_auto_groups = True
@@ -106,14 +107,13 @@ class HasLeaderFilter(admin.SimpleListFilter):
class GroupAdmin(admin.ModelAdmin): class GroupAdmin(admin.ModelAdmin):
form = GroupAdminForm form = GroupAdminForm
list_select_related = ('authgroup',)
ordering = ('name',) ordering = ('name',)
list_display = ( list_display = (
'name', 'name',
'_description', '_description',
'_properties', '_properties',
'_member_count', '_member_count',
'has_leader' 'has_leader',
) )
list_filter = [ list_filter = [
'authgroup__internal', 'authgroup__internal',
@@ -129,31 +129,51 @@ class GroupAdmin(admin.ModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) qs = super().get_queryset(request)
if _has_auto_groups: has_leader_qs = (
qs = qs.prefetch_related('managedalliancegroup_set', 'managedcorpgroup_set') AuthGroup.objects.filter(group=OuterRef('pk'), group_leaders__isnull=False)
qs = qs.prefetch_related('authgroup__group_leaders').select_related('authgroup')
qs = qs.annotate(
member_count=Count('user', distinct=True),
) )
has_leader_groups_qs = (
AuthGroup.objects.filter(
group=OuterRef('pk'), group_leader_groups__isnull=False
)
)
qs = (
qs.select_related('authgroup')
.annotate(member_count=Count('user', distinct=True))
.annotate(has_leader=Exists(has_leader_qs))
.annotate(has_leader_groups=Exists(has_leader_groups_qs))
)
if _has_auto_groups:
is_autogroup_corp = (
Group.objects.filter(
pk=OuterRef('pk'), managedcorpgroup__isnull=False
)
)
is_autogroup_alliance = (
Group.objects.filter(
pk=OuterRef('pk'), managedalliancegroup__isnull=False
)
)
qs = (
qs.annotate(is_autogroup_corp=Exists(is_autogroup_corp))
.annotate(is_autogroup_alliance=Exists(is_autogroup_alliance))
)
return qs return qs
def _description(self, obj): def _description(self, obj):
return obj.authgroup.description return obj.authgroup.description
@admin.display(description="Members", ordering="member_count") @admin.display(description='Members', ordering='member_count')
def _member_count(self, obj): def _member_count(self, obj):
return obj.member_count return obj.member_count
@admin.display(boolean=True) @admin.display(boolean=True)
def has_leader(self, obj): def has_leader(self, obj):
return obj.authgroup.group_leaders.exists() or obj.authgroup.group_leader_groups.exists() return obj.has_leader or obj.has_leader_groups
def _properties(self, obj): def _properties(self, obj):
properties = list() properties = list()
if _has_auto_groups and ( if _has_auto_groups and (obj.is_autogroup_corp or obj.is_autogroup_alliance):
obj.managedalliancegroup_set.exists()
or obj.managedcorpgroup_set.exists()
):
properties.append('Auto Group') properties.append('Auto Group')
elif obj.authgroup.internal: elif obj.authgroup.internal:
properties.append('Internal') properties.append('Internal')
@@ -183,6 +203,8 @@ class GroupAdmin(admin.ModelAdmin):
ag_instance = inline_form.save(commit=False) ag_instance = inline_form.save(commit=False)
ag_instance.group = form.instance ag_instance.group = form.instance
ag_instance.save() ag_instance.save()
if ag_instance.states.exists():
remove_users_not_matching_states_from_group.delay(ag_instance.group.pk)
formset.save() formset.save()
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):

View File

@@ -189,6 +189,15 @@ class AuthGroup(models.Model):
| User.objects.filter(groups__in=list(self.group_leader_groups.all())) | User.objects.filter(groups__in=list(self.group_leader_groups.all()))
) )
def remove_users_not_matching_states(self):
"""Remove users not matching defined states from related group."""
states_qs = self.states.all()
if states_qs.exists():
states = list(states_qs)
non_compliant_users = self.group.user_set.exclude(profile__state__in=states)
for user in non_compliant_users:
self.group.user_set.remove(user)
class ReservedGroupName(models.Model): class ReservedGroupName(models.Model):
"""Name that can not be used for groups. """Name that can not be used for groups.

View File

@@ -0,0 +1,10 @@
from celery import shared_task
from django.contrib.auth.models import Group
@shared_task
def remove_users_not_matching_states_from_group(group_pk: int) -> None:
"""Remove users not matching defined states from related group."""
group = Group.objects.get(pk=group_pk)
group.authgroup.remove_users_not_matching_states()

View File

@@ -74,7 +74,7 @@
{% block extra_javascript %} {% block extra_javascript %}
{% include 'bundles/datatables-js.html' %} {% include 'bundles/datatables-js.html' %}
{% include 'bundles/moment-js.html' with locale=True %} {% include 'bundles/moment-js.html' with locale=True %}
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script> <script type="application/javascript" src="{% static 'allianceauth/js/filterDropDown/filterDropDown.min.js' %}"></script>
{% endblock %} {% endblock %}
{% block extra_css %} {% block extra_css %}

View File

@@ -1,5 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% load evelinks %} {% load evelinks %}

View File

@@ -1,5 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Groups Membership" %}{% endblock page_title %} {% block page_title %}{% translate "Groups Membership" %}{% endblock page_title %}

View File

@@ -1,5 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Available Groups" %}{% endblock page_title %} {% block page_title %}{% translate "Available Groups" %}{% endblock page_title %}

View File

@@ -1,5 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% load evelinks %} {% load evelinks %}

View File

@@ -1,4 +1,3 @@
{% load static %}
{% load i18n %} {% load i18n %}
{% load navactive %} {% load navactive %}

View File

@@ -6,7 +6,7 @@ from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.admin.sites import AdminSite from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase, RequestFactory, Client from django.test import TestCase, RequestFactory, Client, override_settings
from allianceauth.authentication.models import CharacterOwnership, State from allianceauth.authentication.models import CharacterOwnership, State
from allianceauth.eveonline.models import ( from allianceauth.eveonline.models import (
@@ -236,60 +236,104 @@ class TestGroupAdmin(TestCase):
self.assertEqual(result, expected) self.assertEqual(result, expected)
def test_member_count(self): def test_member_count(self):
expected = 1 # given
obj = self.modeladmin.get_queryset(MockRequest(user=self.user_1))\ request = MockRequest(user=self.user_1)
.get(pk=self.group_1.pk) obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
# when
result = self.modeladmin._member_count(obj) result = self.modeladmin._member_count(obj)
self.assertEqual(result, expected) # then
self.assertEqual(result, 1)
def test_has_leader_user(self): def test_has_leader_user(self):
result = self.modeladmin.has_leader(self.group_1) # given
request = MockRequest(user=self.user_1)
obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
# when
result = self.modeladmin.has_leader(obj)
# then
self.assertTrue(result) self.assertTrue(result)
def test_has_leader_group(self): def test_has_leader_group(self):
result = self.modeladmin.has_leader(self.group_2) # given
request = MockRequest(user=self.user_1)
obj = self.modeladmin.get_queryset(request).get(pk=self.group_2.pk)
# when
result = self.modeladmin.has_leader(obj)
# then
self.assertTrue(result) self.assertTrue(result)
def test_properties_1(self): def test_properties_1(self):
expected = ['Default'] # given
result = self.modeladmin._properties(self.group_1) request = MockRequest(user=self.user_1)
self.assertListEqual(result, expected) obj = self.modeladmin.get_queryset(request).get(pk=self.group_1.pk)
# when
result = self.modeladmin._properties(obj)
self.assertListEqual(result, ['Default'])
def test_properties_2(self): def test_properties_2(self):
expected = ['Internal'] # given
result = self.modeladmin._properties(self.group_2) request = MockRequest(user=self.user_1)
self.assertListEqual(result, expected) obj = self.modeladmin.get_queryset(request).get(pk=self.group_2.pk)
# when
result = self.modeladmin._properties(obj)
self.assertListEqual(result, ['Internal'])
def test_properties_3(self): def test_properties_3(self):
expected = ['Hidden'] # given
result = self.modeladmin._properties(self.group_3) request = MockRequest(user=self.user_1)
self.assertListEqual(result, expected) obj = self.modeladmin.get_queryset(request).get(pk=self.group_3.pk)
# when
result = self.modeladmin._properties(obj)
self.assertListEqual(result, ['Hidden'])
def test_properties_4(self): def test_properties_4(self):
expected = ['Open'] # given
result = self.modeladmin._properties(self.group_4) request = MockRequest(user=self.user_1)
self.assertListEqual(result, expected) obj = self.modeladmin.get_queryset(request).get(pk=self.group_4.pk)
# when
result = self.modeladmin._properties(obj)
self.assertListEqual(result, ['Open'])
def test_properties_5(self): def test_properties_5(self):
expected = ['Public'] # given
result = self.modeladmin._properties(self.group_5) request = MockRequest(user=self.user_1)
self.assertListEqual(result, expected) obj = self.modeladmin.get_queryset(request).get(pk=self.group_5.pk)
# when
result = self.modeladmin._properties(obj)
self.assertListEqual(result, ['Public'])
def test_properties_6(self): def test_properties_6(self):
expected = ['Hidden', 'Open', 'Public'] # given
result = self.modeladmin._properties(self.group_6) request = MockRequest(user=self.user_1)
self.assertListEqual(result, expected) obj = self.modeladmin.get_queryset(request).get(pk=self.group_6.pk)
# when
result = self.modeladmin._properties(obj)
self.assertListEqual(result, ['Hidden', 'Open', 'Public'])
if _has_auto_groups: if _has_auto_groups:
@patch(MODULE_PATH + '._has_auto_groups', True) @patch(MODULE_PATH + '._has_auto_groups', True)
def test_properties_7(self): def test_should_show_autogroup_for_corporation(self):
# given
self._create_autogroups() self._create_autogroups()
expected = ['Auto Group'] request = MockRequest(user=self.user_1)
my_group = Group.objects\ queryset = self.modeladmin.get_queryset(request)
.filter(managedcorpgroup__isnull=False)\ obj = queryset.filter(managedcorpgroup__isnull=False).first()
.first() # when
result = self.modeladmin._properties(my_group) result = self.modeladmin._properties(obj)
self.assertListEqual(result, expected) # then
self.assertListEqual(result, ['Auto Group'])
@patch(MODULE_PATH + '._has_auto_groups', True)
def test_should_show_autogroup_for_alliance(self):
# given
self._create_autogroups()
request = MockRequest(user=self.user_1)
queryset = self.modeladmin.get_queryset(request)
obj = queryset.filter(managedalliancegroup__isnull=False).first()
# when
result = self.modeladmin._properties(obj)
# then
self.assertListEqual(result, ['Auto Group'])
# actions # actions
@@ -539,6 +583,68 @@ class TestGroupAdminChangeFormSuperuserExclusiveEdits(WebTest):
self.assertNotIn(field, form.fields) self.assertNotIn(field, form.fields)
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class TestGroupAdmin2(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.superuser = User.objects.create_superuser("super")
def test_should_remove_users_from_state_groups(self):
# given
user_member = AuthUtils.create_user("Bruce Wayne")
character_member = AuthUtils.add_main_character_2(
user_member,
name="Bruce Wayne",
character_id=1001,
corp_id=2001,
corp_name="Wayne Technologies",
)
user_guest = AuthUtils.create_user("Lex Luthor")
AuthUtils.add_main_character_2(
user_guest,
name="Lex Luthor",
character_id=1011,
corp_id=2011,
corp_name="Luthor Corp",
)
member_state = AuthUtils.get_member_state()
member_state.member_characters.add(character_member)
user_member.refresh_from_db()
user_guest.refresh_from_db()
group = Group.objects.create(name="dummy")
user_member.groups.add(group)
user_guest.groups.add(group)
group.authgroup.states.add(member_state)
self.client.force_login(self.superuser)
# when
response = self.client.post(
f"/admin/groupmanagement/group/{group.pk}/change/",
data={
"name": f"{group.name}",
"authgroup-TOTAL_FORMS": "1",
"authgroup-INITIAL_FORMS": "1",
"authgroup-MIN_NUM_FORMS": "0",
"authgroup-MAX_NUM_FORMS": "1",
"authgroup-0-description": "",
"authgroup-0-states": f"{member_state.pk}",
"authgroup-0-internal": "on",
"authgroup-0-hidden": "on",
"authgroup-0-group": f"{group.pk}",
"authgroup-__prefix__-description": "",
"authgroup-__prefix__-internal": "on",
"authgroup-__prefix__-hidden": "on",
"authgroup-__prefix__-group": f"{group.pk}",
"_save": "Save"
}
)
# then
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/admin/groupmanagement/group/")
self.assertIn(group, user_member.groups.all())
self.assertNotIn(group, user_guest.groups.all())
class TestReservedGroupNameAdmin(TestCase): class TestReservedGroupNameAdmin(TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View File

@@ -232,6 +232,38 @@ class TestAuthGroup(TestCase):
expected = 'Superheros' expected = 'Superheros'
self.assertEqual(str(group.authgroup), expected) self.assertEqual(str(group.authgroup), expected)
def test_should_remove_guests_from_group_when_restricted_to_members_only(self):
# given
user_member = AuthUtils.create_user("Bruce Wayne")
character_member = AuthUtils.add_main_character_2(
user_member,
name="Bruce Wayne",
character_id=1001,
corp_id=2001,
corp_name="Wayne Technologies",
)
user_guest = AuthUtils.create_user("Lex Luthor")
AuthUtils.add_main_character_2(
user_guest,
name="Lex Luthor",
character_id=1011,
corp_id=2011,
corp_name="Luthor Corp",
)
member_state = AuthUtils.get_member_state()
member_state.member_characters.add(character_member)
user_member.refresh_from_db()
user_guest.refresh_from_db()
group = Group.objects.create(name="dummy")
user_member.groups.add(group)
user_guest.groups.add(group)
group.authgroup.states.add(member_state)
# when
group.authgroup.remove_users_not_matching_states()
# then
self.assertIn(group, user_member.groups.all())
self.assertNotIn(group, user_guest.groups.all())
class TestAuthGroupRequestApprovers(TestCase): class TestAuthGroupRequestApprovers(TestCase):
def setUp(self) -> None: def setUp(self) -> None:

View File

@@ -1,5 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Choose a Corp" %}{% endblock page_title %} {% block page_title %}{% translate "Choose a Corp" %}{% endblock page_title %}

View File

@@ -1,5 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Apply To" %} {{ corp.corporation_name }}{% endblock page_title %} {% block page_title %}{% translate "Apply To" %} {{ corp.corporation_name }}{% endblock page_title %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "HR Application Management" %}{% endblock page_title %} {% block page_title %}{% translate "HR Application Management" %}{% endblock page_title %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "HR Application Management" %}{% endblock page_title %} {% block page_title %}{% translate "HR Application Management" %}{% endblock page_title %}

View File

@@ -1,5 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load static %}
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}

View File

@@ -20,7 +20,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: es\n" "Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#: allianceauth/analytics/models.py:29 #: allianceauth/analytics/models.py:29
msgid "Google Analytics Universal" msgid "Google Analytics Universal"
@@ -450,6 +450,7 @@ msgid "%(user)s has collected one link this month."
msgid_plural "%(user)s has collected %(links)s links this month." msgid_plural "%(user)s has collected %(links)s links this month."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgstr[2] ""
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:28 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:28
msgid "Times used" msgid "Times used"
@@ -461,6 +462,7 @@ msgid "%(user)s has created one link this month."
msgid_plural "%(user)s has created %(links)s links this month." msgid_plural "%(user)s has created %(links)s links this month."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgstr[2] ""
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:48 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:48
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:27 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:27
@@ -2141,6 +2143,7 @@ msgid "%(tasks)s task"
msgid_plural "%(tasks)s tasks" msgid_plural "%(tasks)s tasks"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgstr[2] ""
#: allianceauth/templates/allianceauth/night-toggle.html:6 #: allianceauth/templates/allianceauth/night-toggle.html:6
msgid "Night Mode" msgid "Night Mode"

View File

@@ -5,11 +5,11 @@
# #
# Translators: # Translators:
# François LACROIX-DURANT <umbre@fallenstarscreations.com>, 2020 # François LACROIX-DURANT <umbre@fallenstarscreations.com>, 2020
# Philippe Querin-Laporte <philippe.querin@hotmail.com>, 2020
# Keven D. <theenarki@gmail.com>, 2020 # Keven D. <theenarki@gmail.com>, 2020
# Idea ., 2021 # Idea ., 2021
# Mickael PATTE, 2021 # Mickael PATTE, 2021
# Geoffrey Fabbro, 2021 # Geoffrey Fabbro, 2021
# Philippe Querin-Laporte <philippe.querin@hotmail.com>, 2022
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
@@ -18,13 +18,13 @@ msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-29 01:03+1000\n" "POT-Creation-Date: 2021-11-29 01:03+1000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n" "PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Geoffrey Fabbro, 2021\n" "Last-Translator: Philippe Querin-Laporte <philippe.querin@hotmail.com>, 2022\n"
"Language-Team: French (France) (https://www.transifex.com/alliance-auth/teams/107430/fr_FR/)\n" "Language-Team: French (France) (https://www.transifex.com/alliance-auth/teams/107430/fr_FR/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: fr_FR\n" "Language: fr_FR\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#: allianceauth/analytics/models.py:29 #: allianceauth/analytics/models.py:29
msgid "Google Analytics Universal" msgid "Google Analytics Universal"
@@ -460,6 +460,7 @@ msgid "%(user)s has collected one link this month."
msgid_plural "%(user)s has collected %(links)s links this month." msgid_plural "%(user)s has collected %(links)s links this month."
msgstr[0] "%(user)s a obtenu un lien ce mois." msgstr[0] "%(user)s a obtenu un lien ce mois."
msgstr[1] "%(user)s a obtenu %(links)s liens ce mois." msgstr[1] "%(user)s a obtenu %(links)s liens ce mois."
msgstr[2] "%(user)s a obtenu %(links)s liens ce mois."
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:28 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:28
msgid "Times used" msgid "Times used"
@@ -471,6 +472,7 @@ msgid "%(user)s has created one link this month."
msgid_plural "%(user)s has created %(links)s links this month." msgid_plural "%(user)s has created %(links)s links this month."
msgstr[0] "%(user)s a créé un lien ce mois." msgstr[0] "%(user)s a créé un lien ce mois."
msgstr[1] "%(user)s a créé %(links)s liens ce mois." msgstr[1] "%(user)s a créé %(links)s liens ce mois."
msgstr[2] "%(user)s a créé %(links)s liens ce mois."
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:48 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:48
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:27 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:27
@@ -2167,6 +2169,7 @@ msgid "%(tasks)s task"
msgid_plural "%(tasks)s tasks" msgid_plural "%(tasks)s tasks"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgstr[2] ""
#: allianceauth/templates/allianceauth/night-toggle.html:6 #: allianceauth/templates/allianceauth/night-toggle.html:6
msgid "Night Mode" msgid "Night Mode"

View File

@@ -5,7 +5,7 @@
# #
# Translators: # Translators:
# Alessandro Cresti, 2021 # Alessandro Cresti, 2021
# Linus Hope, 2021 # Linus Hope, 2022
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
@@ -14,13 +14,13 @@ msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-29 01:03+1000\n" "POT-Creation-Date: 2021-11-29 01:03+1000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n" "PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Linus Hope, 2021\n" "Last-Translator: Linus Hope, 2022\n"
"Language-Team: Italian (Italy) (https://www.transifex.com/alliance-auth/teams/107430/it_IT/)\n" "Language-Team: Italian (Italy) (https://www.transifex.com/alliance-auth/teams/107430/it_IT/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: it_IT\n" "Language: it_IT\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#: allianceauth/analytics/models.py:29 #: allianceauth/analytics/models.py:29
msgid "Google Analytics Universal" msgid "Google Analytics Universal"
@@ -460,6 +460,7 @@ msgid "%(user)s has collected one link this month."
msgid_plural "%(user)s has collected %(links)s links this month." msgid_plural "%(user)s has collected %(links)s links this month."
msgstr[0] "%(user)s ha ottenuto un link per questo mese." msgstr[0] "%(user)s ha ottenuto un link per questo mese."
msgstr[1] "%(user)s ha ottenuto %(links)s links questo mese." msgstr[1] "%(user)s ha ottenuto %(links)s links questo mese."
msgstr[2] "%(user)s ha ottenuto %(links)s links questo mese."
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:28 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:28
msgid "Times used" msgid "Times used"
@@ -471,6 +472,7 @@ msgid "%(user)s has created one link this month."
msgid_plural "%(user)s has created %(links)s links this month." msgid_plural "%(user)s has created %(links)s links this month."
msgstr[0] "%(user)s ha creato un link questo mese." msgstr[0] "%(user)s ha creato un link questo mese."
msgstr[1] "%(user)s ha creato %(links)s links questo mese." msgstr[1] "%(user)s ha creato %(links)s links questo mese."
msgstr[2] "%(user)s ha creato %(links)s links questo mese."
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:48 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:48
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:27 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:27
@@ -2155,6 +2157,7 @@ msgid "%(tasks)s task"
msgid_plural "%(tasks)s tasks" msgid_plural "%(tasks)s tasks"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgstr[2] ""
#: allianceauth/templates/allianceauth/night-toggle.html:6 #: allianceauth/templates/allianceauth/night-toggle.html:6
msgid "Night Mode" msgid "Night Mode"

View File

@@ -9,6 +9,7 @@
# Olgeda Choi <undead.choi@gmail.com>, 2020 # Olgeda Choi <undead.choi@gmail.com>, 2020
# Lahty <js03js70@gmail.com>, 2020 # Lahty <js03js70@gmail.com>, 2020
# Joel Falknau <ozirascal@gmail.com>, 2020 # Joel Falknau <ozirascal@gmail.com>, 2020
# ThatRagingKid, 2022
# #
#, fuzzy #, fuzzy
msgid "" msgid ""
@@ -17,7 +18,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-29 01:03+1000\n" "POT-Creation-Date: 2021-11-29 01:03+1000\n"
"PO-Revision-Date: 2020-02-18 03:14+0000\n" "PO-Revision-Date: 2020-02-18 03:14+0000\n"
"Last-Translator: Joel Falknau <ozirascal@gmail.com>, 2020\n" "Last-Translator: ThatRagingKid, 2022\n"
"Language-Team: Korean (Korea) (https://www.transifex.com/alliance-auth/teams/107430/ko_KR/)\n" "Language-Team: Korean (Korea) (https://www.transifex.com/alliance-auth/teams/107430/ko_KR/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -27,15 +28,15 @@ msgstr ""
#: allianceauth/analytics/models.py:29 #: allianceauth/analytics/models.py:29
msgid "Google Analytics Universal" msgid "Google Analytics Universal"
msgstr "" msgstr "구글 애널리틱스 유니버설"
#: allianceauth/analytics/models.py:30 #: allianceauth/analytics/models.py:30
msgid "Google Analytics V4" msgid "Google Analytics V4"
msgstr "" msgstr "구글 애널리틱스 V4"
#: allianceauth/authentication/decorators.py:37 #: allianceauth/authentication/decorators.py:37
msgid "A main character is required to perform that action. Add one below." msgid "A main character is required to perform that action. Add one below."
msgstr "해당 기능을 수행하려면 주 캐릭터가 요구됨. 아래에 하나를 추가하시오." msgstr "해당 기능을 수행하려면 주 캐릭터가 요구됨. 아래에 하나를 추가하시오."
#: allianceauth/authentication/forms.py:5 #: allianceauth/authentication/forms.py:5
msgid "Email" msgid "Email"
@@ -65,7 +66,7 @@ msgid ""
" " " "
msgstr "" msgstr ""
"\n" "\n"
" 메인 캐릭터 (상태: %(state)s)\n" " 캐릭터 (상태: %(state)s)\n"
" " " "
#: allianceauth/authentication/templates/authentication/dashboard.html:102 #: allianceauth/authentication/templates/authentication/dashboard.html:102
@@ -103,7 +104,7 @@ msgstr "이름"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:23
#: allianceauth/hrapplications/templates/hrapplications/view.html:46 #: allianceauth/hrapplications/templates/hrapplications/view.html:46
msgid "Corp" msgid "Corp"
msgstr "" msgstr "코퍼레이션"
#: allianceauth/authentication/templates/authentication/dashboard.html:152 #: allianceauth/authentication/templates/authentication/dashboard.html:152
#: allianceauth/corputils/templates/corputils/corpstats.html:76 #: allianceauth/corputils/templates/corputils/corpstats.html:76
@@ -118,7 +119,7 @@ msgstr "로그인"
#: allianceauth/authentication/templates/public/register.html:7 #: allianceauth/authentication/templates/public/register.html:7
msgid "Registration" msgid "Registration"
msgstr "" msgstr "가입"
#: allianceauth/authentication/templates/public/register.html:22 #: allianceauth/authentication/templates/public/register.html:22
#: allianceauth/authentication/templates/registration/registration_form.html:5 #: allianceauth/authentication/templates/registration/registration_form.html:5
@@ -137,7 +138,7 @@ msgstr "계정 패스워드 리셋을 요청하여 이 이메일을 보내드립
#: allianceauth/authentication/templates/registration/password_reset_email.html:5 #: allianceauth/authentication/templates/registration/password_reset_email.html:5
msgid "Please go to the following page and choose a new password:" msgid "Please go to the following page and choose a new password:"
msgstr "다음 페이지로 이동하여 새로운 패스워드를 입력하세요." msgstr "다음 페이지로 이동하여 새로운 패스워드를 입력하세요:"
#: allianceauth/authentication/templates/registration/password_reset_email.html:9 #: allianceauth/authentication/templates/registration/password_reset_email.html:9
msgid "Your username, in case you've forgotten:" msgid "Your username, in case you've forgotten:"
@@ -176,7 +177,7 @@ msgstr "계정에 %(name)s를 추가했습니다."
#: allianceauth/authentication/views.py:94 #: allianceauth/authentication/views.py:94
#, python-format #, python-format
msgid "Failed to add %(name)s to your account: they already have an account." msgid "Failed to add %(name)s to your account: they already have an account."
msgstr "계정에 %(name)s를 추가하지 못했습니다. 이미 추가된 계정입니다." msgstr "계정에 %(name)s를 추가하지 못했습니다. 이미 다른 계정에 추가되었습니다."
#: allianceauth/authentication/views.py:133 #: allianceauth/authentication/views.py:133
msgid "Unable to authenticate as the selected character." msgid "Unable to authenticate as the selected character."
@@ -184,7 +185,7 @@ msgstr "선택한 캐릭터로 인증을 수행할 수 없음"
#: allianceauth/authentication/views.py:197 #: allianceauth/authentication/views.py:197
msgid "Registration token has expired." msgid "Registration token has expired."
msgstr "등록토큰 만료" msgstr "가입 토큰 만료되었습니다."
#: allianceauth/authentication/views.py:252 #: allianceauth/authentication/views.py:252
msgid "" msgid ""
@@ -202,16 +203,16 @@ msgstr "현재 새로운 계정 등록은 받지않습니다."
#: allianceauth/corputils/auth_hooks.py:11 #: allianceauth/corputils/auth_hooks.py:11
msgid "Corporation Stats" msgid "Corporation Stats"
msgstr " 상태" msgstr "코퍼레이션 상태"
#: allianceauth/corputils/templates/corputils/base.html:3 #: allianceauth/corputils/templates/corputils/base.html:3
#: allianceauth/corputils/templates/corputils/base.html:6 #: allianceauth/corputils/templates/corputils/base.html:6
msgid "Corporation Member Data" msgid "Corporation Member Data"
msgstr " 멤버 데이터" msgstr "코퍼레이션 멤버 정보"
#: allianceauth/corputils/templates/corputils/base.html:12 #: allianceauth/corputils/templates/corputils/base.html:12
msgid "Corporations" msgid "Corporations"
msgstr "" msgstr "코퍼레이션"
#: allianceauth/corputils/templates/corputils/base.html:23 #: allianceauth/corputils/templates/corputils/base.html:23
msgid "Add" msgid "Add"
@@ -219,7 +220,7 @@ msgstr "추가"
#: allianceauth/corputils/templates/corputils/base.html:29 #: allianceauth/corputils/templates/corputils/base.html:29
msgid "Search all corporations..." msgid "Search all corporations..."
msgstr "모든 검색" msgstr "모든 코퍼레이션 검색"
#: allianceauth/corputils/templates/corputils/corpstats.html:33 #: allianceauth/corputils/templates/corputils/corpstats.html:33
msgid "Mains" msgid "Mains"
@@ -237,7 +238,7 @@ msgstr "미등록"
#: allianceauth/corputils/templates/corputils/corpstats.html:38 #: allianceauth/corputils/templates/corputils/corpstats.html:38
msgid "Last update:" msgid "Last update:"
msgstr "마지막 업데이트" msgstr "마지막 업데이트:"
#: allianceauth/corputils/templates/corputils/corpstats.html:74 #: allianceauth/corputils/templates/corputils/corpstats.html:74
#: allianceauth/corputils/templates/corputils/corpstats.html:112 #: allianceauth/corputils/templates/corputils/corpstats.html:112
@@ -260,7 +261,7 @@ msgstr "캐릭터"
#: allianceauth/hrapplications/templates/hrapplications/management.html:126 #: allianceauth/hrapplications/templates/hrapplications/management.html:126
#: allianceauth/hrapplications/templates/hrapplications/searchview.html:26 #: allianceauth/hrapplications/templates/hrapplications/searchview.html:26
msgid "Corporation" msgid "Corporation"
msgstr "" msgstr "코퍼레이션"
#: allianceauth/corputils/templates/corputils/corpstats.html:91 #: allianceauth/corputils/templates/corputils/corpstats.html:91
#: allianceauth/corputils/templates/corputils/corpstats.html:125 #: allianceauth/corputils/templates/corputils/corpstats.html:125
@@ -268,7 +269,7 @@ msgstr "콥"
#: allianceauth/corputils/templates/corputils/corpstats.html:167 #: allianceauth/corputils/templates/corputils/corpstats.html:167
#: allianceauth/corputils/templates/corputils/search.html:27 #: allianceauth/corputils/templates/corputils/search.html:27
msgid "Killboard" msgid "Killboard"
msgstr "킬보드" msgstr "사살권"
#: allianceauth/corputils/templates/corputils/corpstats.html:114 #: allianceauth/corputils/templates/corputils/corpstats.html:114
#: allianceauth/corputils/templates/corputils/search.html:16 #: allianceauth/corputils/templates/corputils/search.html:16
@@ -283,12 +284,12 @@ msgstr "주 캐릭터"
#: allianceauth/corputils/templates/corputils/corpstats.html:115 #: allianceauth/corputils/templates/corputils/corpstats.html:115
#: allianceauth/corputils/templates/corputils/search.html:17 #: allianceauth/corputils/templates/corputils/search.html:17
msgid "Main Corporation" msgid "Main Corporation"
msgstr "메인콥" msgstr "주 코퍼레이션"
#: allianceauth/corputils/templates/corputils/corpstats.html:116 #: allianceauth/corputils/templates/corputils/corpstats.html:116
#: allianceauth/corputils/templates/corputils/search.html:18 #: allianceauth/corputils/templates/corputils/search.html:18
msgid "Main Alliance" msgid "Main Alliance"
msgstr "메인 얼라이언스" msgstr " 얼라이언스"
#: allianceauth/corputils/templates/corputils/search.html:6 #: allianceauth/corputils/templates/corputils/search.html:6
msgid "Search Results" msgid "Search Results"
@@ -296,28 +297,28 @@ msgstr "검색결과"
#: allianceauth/corputils/templates/corputils/search.html:15 #: allianceauth/corputils/templates/corputils/search.html:15
msgid "zKillboard" msgid "zKillboard"
msgstr "킬보드" msgstr "zKillboard"
#: allianceauth/corputils/views.py:54 #: allianceauth/corputils/views.py:54
msgid "Selected corp already has a statistics module." msgid "Selected corp already has a statistics module."
msgstr "선택한 은 이미 통계 모듈을 갖고있습니다." msgstr "선택한 코퍼레이션은 이미 통계 모듈을 갖고 있습니다."
#: allianceauth/corputils/views.py:56 #: allianceauth/corputils/views.py:56
msgid "Failed to gather corporation statistics with selected token." msgid "Failed to gather corporation statistics with selected token."
msgstr "선택한 토큰으로 통계 수집 실패" msgstr "선택한 토큰으로 코퍼레이션 통계 수집 실패했습니다."
#: allianceauth/fleetactivitytracking/auth_hooks.py:9 #: allianceauth/fleetactivitytracking/auth_hooks.py:9
msgid "Fleet Activity Tracking" msgid "Fleet Activity Tracking"
msgstr "플릿활동 추적" msgstr "함대 활동"
#: allianceauth/fleetactivitytracking/forms.py:6 allianceauth/srp/form.py:8 #: allianceauth/fleetactivitytracking/forms.py:6 allianceauth/srp/form.py:8
#: allianceauth/srp/templates/srp/management.html:37 #: allianceauth/srp/templates/srp/management.html:37
msgid "Fleet Name" msgid "Fleet Name"
msgstr "플릿 이름" msgstr "함대 이름"
#: allianceauth/fleetactivitytracking/forms.py:7 #: allianceauth/fleetactivitytracking/forms.py:7
msgid "Duration of fat-link" msgid "Duration of fat-link"
msgstr "플릿활동추적 링크 주기" msgstr "함대 활동 링크 주기"
#: allianceauth/fleetactivitytracking/forms.py:7 #: allianceauth/fleetactivitytracking/forms.py:7
msgid "minutes" msgid "minutes"
@@ -325,7 +326,7 @@ msgstr "분"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:3 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:3
msgid "Fleet Participation" msgid "Fleet Participation"
msgstr "플릿 참여" msgstr "함대 참여"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:7 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:7
msgid "Character not found!" msgid "Character not found!"
@@ -337,25 +338,25 @@ msgstr "캐릭터가 등록되지 않음!"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:19 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:19
msgid "This character is not associated with an auth account." msgid "This character is not associated with an auth account."
msgstr "해당 캐릭터는 본 계정에 연결되어있지 않." msgstr "해당 캐릭터는 본 계정에 연결되어 있지 않습니다."
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:19 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:19
msgid "Add it here" msgid "Add it here"
msgstr "여기 추가" msgstr "여기 추가하시오"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:19 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/characternotexisting.html:19
msgid "before attempting to click fleet attendance links." msgid "before attempting to click fleet attendance links."
msgstr "플릿 참여 링크를 클릭하기 전" msgstr "함대 참여 링크를 클릭하기 전"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkformatter.html:6 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkformatter.html:6
msgid "Create Fatlink" msgid "Create Fatlink"
msgstr "플릿활동추적 생성" msgstr "함대 활동 링크 생성"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkformatter.html:10 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkformatter.html:10
#: allianceauth/optimer/templates/optimer/add.html:14 #: allianceauth/optimer/templates/optimer/add.html:14
#: allianceauth/optimer/templates/optimer/add.html:23 #: allianceauth/optimer/templates/optimer/add.html:23
msgid "Create Fleet Operation" msgid "Create Fleet Operation"
msgstr "플릿 옵 생성" msgstr "함대 오퍼레이션 생성"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkformatter.html:14 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkformatter.html:14
msgid "Bad request!" msgid "Bad request!"
@@ -364,20 +365,20 @@ msgstr "잘못된 요청!"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkformatter.html:25 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkformatter.html:25
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:65 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:65
msgid "Create fatlink" msgid "Create fatlink"
msgstr "플릿활동추적 링크 생성" msgstr "함대 활동 링크 생성"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:5 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:5
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:6 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:6
msgid "Fatlink view" msgid "Fatlink view"
msgstr "플릿활동추적 링크 보기" msgstr "함대 활동 링크 보기"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:9 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:9
msgid "Edit fatlink" msgid "Edit fatlink"
msgstr "플릿활동추적 수정" msgstr "함대 활동 링크 수정"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:13 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:13
msgid "Delete fat" msgid "Delete fat"
msgstr "플릿활동추적 수정" msgstr "함대 활동 링크 삭제"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:19 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:19
msgid "Registered characters" msgid "Registered characters"
@@ -401,7 +402,7 @@ msgstr "시스템"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:27 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:27
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:30 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:30
msgid "Ship" msgid "Ship"
msgstr "" msgstr "함선"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:27 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkmodify.html:27
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:50 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:50
@@ -422,7 +423,7 @@ msgstr "도킹"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:6 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:6
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalstatisticsview.html:6 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalstatisticsview.html:6
msgid "Personal fatlink statistics" msgid "Personal fatlink statistics"
msgstr "개인별 플릿활동추적 통계" msgstr "개인별 함대 활동 링크 통계"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:10 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalmonthlystatisticsview.html:10
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:10 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:10
@@ -492,11 +493,11 @@ msgstr "%(year)s년 동안의 참여 통계자료"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalstatisticsview.html:12 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalstatisticsview.html:12
msgid "Previous year" msgid "Previous year"
msgstr "지난 해" msgstr "작년"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalstatisticsview.html:14 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalstatisticsview.html:14
msgid "Next year" msgid "Next year"
msgstr "다음 해" msgstr "내년"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalstatisticsview.html:21 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkpersonalstatisticsview.html:21
msgid "Month" msgid "Month"
@@ -506,20 +507,20 @@ msgstr "달"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:24 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:24
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:25 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:25
msgid "Fats" msgid "Fats"
msgstr "플릿활동추적" msgstr "함대 활동"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:6 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:6
msgid "Fatlink Corp Statistics" msgid "Fatlink Corp Statistics"
msgstr "콥별 플릿활동추적 통계" msgstr "코퍼레이션별 함대 활동 통계"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:25 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticscorpview.html:25
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:26 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:26
msgid "Average fats" msgid "Average fats"
msgstr "평균 플릿활동추적" msgstr "평균 함대 활동"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:6 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:6
msgid "Fatlink statistics" msgid "Fatlink statistics"
msgstr "플릿활동추적 통계" msgstr "함대 활동 통계"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:22 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkstatisticsview.html:22
msgid "Ticker" msgid "Ticker"
@@ -531,7 +532,7 @@ msgstr "참여 자료"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:14 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:14
msgid "Most recent clicked fatlinks" msgid "Most recent clicked fatlinks"
msgstr "가장 최근에 클릭한 플릿활동추적 링크" msgstr "가장 최근에 클릭한 함대 활동 링크"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:19 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:19
msgid "Personal statistics" msgid "Personal statistics"
@@ -539,11 +540,11 @@ msgstr "개인별 통계"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:48 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:48
msgid "No fleet activity on record." msgid "No fleet activity on record."
msgstr "플릿 활동기록이 없음" msgstr "함대 활동 기록이 없음"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:55 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:55
msgid "Most recent fatlinks" msgid "Most recent fatlinks"
msgstr "가장 최근의 플릿활동추적 링크" msgstr "가장 최근의 함대 활동 링크"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:60 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:60
msgid "View statistics" msgid "View statistics"
@@ -551,27 +552,27 @@ msgstr "통계 보기"
#: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:97 #: allianceauth/fleetactivitytracking/templates/fleetactivitytracking/fatlinkview.html:97
msgid "No created fatlinks on record." msgid "No created fatlinks on record."
msgstr "생성된 플릿활동추적 링크 기록이 없음" msgstr "생성된 함대 활동 링크 기록이 없음"
#: allianceauth/fleetactivitytracking/views.py:280 #: allianceauth/fleetactivitytracking/views.py:280
msgid "Fleet participation registered." msgid "Fleet participation registered."
msgstr "플릿 참여 등록됨" msgstr "함대 참여 등록됨"
#: allianceauth/fleetactivitytracking/views.py:296 #: allianceauth/fleetactivitytracking/views.py:296
msgid "FAT link has expired." msgid "FAT link has expired."
msgstr "플릿활동추적 링크 기한만료" msgstr "함대 활동 링크 기한만료"
#: allianceauth/groupmanagement/admin.py:104 #: allianceauth/groupmanagement/admin.py:104
msgid "This name has been reserved and can not be used for groups." msgid "This name has been reserved and can not be used for groups."
msgstr "" msgstr "이 이름은 이미 할당되었고 그룹의 이름으로 사용될 수 없습니다."
#: allianceauth/groupmanagement/admin.py:230 #: allianceauth/groupmanagement/admin.py:230
msgid "(auto)" msgid "(auto)"
msgstr "" msgstr "(자동)"
#: allianceauth/groupmanagement/admin.py:239 #: allianceauth/groupmanagement/admin.py:239
msgid "There already exists a group with that name." msgid "There already exists a group with that name."
msgstr "" msgstr "이 이름을 가진 그룹이 이미 있습니다."
#: allianceauth/groupmanagement/auth_hooks.py:17 #: allianceauth/groupmanagement/auth_hooks.py:17
#: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14 #: allianceauth/groupmanagement/templates/groupmanagement/menu.html:14
@@ -584,10 +585,12 @@ msgid ""
"group.<br>Used for groups such as Members, Corp_*, Alliance_* " "group.<br>Used for groups such as Members, Corp_*, Alliance_* "
"etc.<br><b>Overrides Hidden and Open options when selected.</b>" "etc.<br><b>Overrides Hidden and Open options when selected.</b>"
msgstr "" msgstr ""
"시스템 그룹, 유저들은 이 그룹을 보거나, 참여하거나, 지원할 수 없습니다. <br>멤버, 코퍼레이션_*, 얼라이언스_* 등에 "
"사용됨.<br><b>선택된 경우 비공개와 공개 옵션을 무시함.</b>"
#: allianceauth/groupmanagement/models.py:110 #: allianceauth/groupmanagement/models.py:110
msgid "Group is hidden from users but can still join with the correct link." msgid "Group is hidden from users but can still join with the correct link."
msgstr "" msgstr "비공개 그룹이지만 링크를 통해 참여할 수 있음."
#: allianceauth/groupmanagement/models.py:116 #: allianceauth/groupmanagement/models.py:116
msgid "" msgid ""
@@ -670,7 +673,7 @@ msgstr "감사 기록"
#: allianceauth/permissions_tool/templates/permissions_tool/audit.html:13 #: allianceauth/permissions_tool/templates/permissions_tool/audit.html:13
#: allianceauth/timerboard/templates/timerboard/index_button.html:3 #: allianceauth/timerboard/templates/timerboard/index_button.html:3
msgid "Back" msgid "Back"
msgstr "돌아가기" msgstr "뒤로"
#: allianceauth/groupmanagement/templates/groupmanagement/audit.html:28 #: allianceauth/groupmanagement/templates/groupmanagement/audit.html:28
msgid "Date/Time" msgid "Date/Time"
@@ -984,15 +987,15 @@ msgstr "문자열 검색"
#: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:5 #: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:5
#: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:8 #: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:8
msgid "Choose a Corp" msgid "Choose a Corp"
msgstr " 선택" msgstr "코퍼레이션 선택"
#: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:11 #: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:11
msgid "Available Corps" msgid "Available Corps"
msgstr "사용 가능한 " msgstr "사용 가능한 코퍼레이션"
#: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:23 #: allianceauth/hrapplications/templates/hrapplications/corpchoice.html:23
msgid "No corps are accepting applications at this time." msgid "No corps are accepting applications at this time."
msgstr "현재 입사지원 가능한 이 없습니다." msgstr "현재 입사지원 가능한 코퍼레이션이 없습니다."
#: allianceauth/hrapplications/templates/hrapplications/create.html:5 #: allianceauth/hrapplications/templates/hrapplications/create.html:5
#: allianceauth/hrapplications/templates/hrapplications/create.html:8 #: allianceauth/hrapplications/templates/hrapplications/create.html:8
@@ -1222,7 +1225,7 @@ msgstr "모든 읽은 알림을 삭제했습니다."
#: allianceauth/optimer/auth_hooks.py:10 #: allianceauth/optimer/auth_hooks.py:10
msgid "Fleet Operations" msgid "Fleet Operations"
msgstr "플릿 옵" msgstr "함대 옵"
#: allianceauth/optimer/form.py:12 #: allianceauth/optimer/form.py:12
#: allianceauth/optimer/templates/optimer/fleetoptable.html:11 #: allianceauth/optimer/templates/optimer/fleetoptable.html:11
@@ -1246,7 +1249,7 @@ msgstr ""
#: allianceauth/optimer/form.py:17 #: allianceauth/optimer/form.py:17
#: allianceauth/srp/templates/srp/management.html:40 #: allianceauth/srp/templates/srp/management.html:40
msgid "Fleet Commander" msgid "Fleet Commander"
msgstr "플릿 커맨더" msgstr "함대 커맨더"
#: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14 #: allianceauth/optimer/form.py:22 allianceauth/srp/form.py:14
#: allianceauth/srp/templates/srp/data.html:93 #: allianceauth/srp/templates/srp/data.html:93
@@ -1279,11 +1282,11 @@ msgstr "FC"
#: allianceauth/optimer/templates/optimer/management.html:6 #: allianceauth/optimer/templates/optimer/management.html:6
msgid "Fleet Operation Management" msgid "Fleet Operation Management"
msgstr "플릿 옵 관리" msgstr "함대 옵 관리"
#: allianceauth/optimer/templates/optimer/management.html:11 #: allianceauth/optimer/templates/optimer/management.html:11
msgid "Fleet Operation Timers" msgid "Fleet Operation Timers"
msgstr "플릿 옵 타이머" msgstr "함대 옵 타이머"
#: allianceauth/optimer/templates/optimer/management.html:21 #: allianceauth/optimer/templates/optimer/management.html:21
#: allianceauth/timerboard/templates/timerboard/view.html:23 #: allianceauth/timerboard/templates/timerboard/view.html:23
@@ -1312,11 +1315,11 @@ msgstr "최근 지나간 옵 타이머가 없습니다."
#: allianceauth/optimer/templates/optimer/update.html:16 #: allianceauth/optimer/templates/optimer/update.html:16
#: allianceauth/optimer/templates/optimer/update.html:28 #: allianceauth/optimer/templates/optimer/update.html:28
msgid "Update Fleet Operation" msgid "Update Fleet Operation"
msgstr "플릿 옵 수정" msgstr "함대 옵 수정"
#: allianceauth/optimer/templates/optimer/update.html:22 #: allianceauth/optimer/templates/optimer/update.html:22
msgid "Fleet Operation Does Not Exist" msgid "Fleet Operation Does Not Exist"
msgstr "존재하지 않는 플릿 옵" msgstr "존재하지 않는 함대 옵"
#: allianceauth/optimer/views.py:69 #: allianceauth/optimer/views.py:69
#, python-format #, python-format
@@ -1434,23 +1437,23 @@ msgstr "서드파티"
#: allianceauth/services/forms.py:6 #: allianceauth/services/forms.py:6
msgid "Name of Fleet:" msgid "Name of Fleet:"
msgstr "플릿 이름:" msgstr "함대 이름:"
#: allianceauth/services/forms.py:7 #: allianceauth/services/forms.py:7
msgid "Fleet Commander:" msgid "Fleet Commander:"
msgstr "플릿 커맨더:" msgstr "함대 커맨더:"
#: allianceauth/services/forms.py:8 #: allianceauth/services/forms.py:8
msgid "Fleet Comms:" msgid "Fleet Comms:"
msgstr "플릿 음성 채널:" msgstr "함대 음성 채널:"
#: allianceauth/services/forms.py:9 #: allianceauth/services/forms.py:9
msgid "Fleet Type:" msgid "Fleet Type:"
msgstr "플릿 타입:" msgstr "함대 타입:"
#: allianceauth/services/forms.py:10 #: allianceauth/services/forms.py:10
msgid "Ship Priorities:" msgid "Ship Priorities:"
msgstr "플릿 우선도:" msgstr "함대 우선도:"
#: allianceauth/services/forms.py:11 #: allianceauth/services/forms.py:11
msgid "Formup Location:" msgid "Formup Location:"
@@ -1595,7 +1598,7 @@ msgstr "재버 방송"
#: allianceauth/services/modules/openfire/auth_hooks.py:94 #: allianceauth/services/modules/openfire/auth_hooks.py:94
msgid "Fleet Broadcast Formatter" msgid "Fleet Broadcast Formatter"
msgstr "플릿 신호 설정" msgstr "함대 신호 설정"
#: allianceauth/services/modules/openfire/forms.py:7 #: allianceauth/services/modules/openfire/forms.py:7
msgid "Message" msgid "Message"
@@ -1749,11 +1752,11 @@ msgstr "XenForo 비밀번호 변경 완료"
#: allianceauth/services/templates/services/fleetformattertool.html:6 #: allianceauth/services/templates/services/fleetformattertool.html:6
msgid "Fleet Formatter Tool" msgid "Fleet Formatter Tool"
msgstr "플릿 구성 도구" msgstr "함대 구성 도구"
#: allianceauth/services/templates/services/fleetformattertool.html:11 #: allianceauth/services/templates/services/fleetformattertool.html:11
msgid "Fleet Broadcast Formatter Tool" msgid "Fleet Broadcast Formatter Tool"
msgstr "플릿 브로드캐스트 설정 도구" msgstr "함대 브로드캐스트 설정 도구"
#: allianceauth/services/templates/services/fleetformattertool.html:24 #: allianceauth/services/templates/services/fleetformattertool.html:24
msgid "Format" msgid "Format"
@@ -1814,12 +1817,12 @@ msgstr "SRP"
#: allianceauth/srp/form.py:9 #: allianceauth/srp/form.py:9
#: allianceauth/srp/templates/srp/management.html:38 #: allianceauth/srp/templates/srp/management.html:38
msgid "Fleet Time" msgid "Fleet Time"
msgstr "플릿 시간" msgstr "함대 시간"
#: allianceauth/srp/form.py:10 #: allianceauth/srp/form.py:10
#: allianceauth/srp/templates/srp/management.html:39 #: allianceauth/srp/templates/srp/management.html:39
msgid "Fleet Doctrine" msgid "Fleet Doctrine"
msgstr "플릿 독트린" msgstr "함대 독트린"
#: allianceauth/srp/form.py:16 #: allianceauth/srp/form.py:16
msgid "Killboard Link (zkillboard.com or kb.evetools.org)" msgid "Killboard Link (zkillboard.com or kb.evetools.org)"
@@ -1839,12 +1842,12 @@ msgstr "사후조치 보고서 링크"
#: allianceauth/srp/templates/srp/add.html:6 #: allianceauth/srp/templates/srp/add.html:6
msgid "SRP Fleet Create" msgid "SRP Fleet Create"
msgstr "SRP 보상 플릿 생성" msgstr "SRP 보상 함대 생성"
#: allianceauth/srp/templates/srp/add.html:14 #: allianceauth/srp/templates/srp/add.html:14
#: allianceauth/srp/templates/srp/add.html:24 #: allianceauth/srp/templates/srp/add.html:24
msgid "Create SRP Fleet" msgid "Create SRP Fleet"
msgstr "SRP 보상 플릿 생성" msgstr "SRP 보상 함대 생성"
#: allianceauth/srp/templates/srp/add.html:27 #: allianceauth/srp/templates/srp/add.html:27
msgid "Give this link to the line members" msgid "Give this link to the line members"
@@ -1852,7 +1855,7 @@ msgstr "이 링크를 직계 멤버들에게 전달"
#: allianceauth/srp/templates/srp/data.html:52 #: allianceauth/srp/templates/srp/data.html:52
msgid "SRP Fleet Data" msgid "SRP Fleet Data"
msgstr "SRP 보상 플릿 데이터" msgstr "SRP 보상 함대 데이터"
#: allianceauth/srp/templates/srp/data.html:57 #: allianceauth/srp/templates/srp/data.html:57
msgid "Mark Incomplete" msgid "Mark Incomplete"
@@ -1908,7 +1911,7 @@ msgstr "작성 시간"
#: allianceauth/srp/templates/srp/data.html:178 #: allianceauth/srp/templates/srp/data.html:178
msgid "No SRP requests for this fleet." msgid "No SRP requests for this fleet."
msgstr "이 플릿에는 SRP 보상 요청이 없습니다." msgstr "이 함대에는 SRP 보상 요청이 없습니다."
#: allianceauth/srp/templates/srp/management.html:8 #: allianceauth/srp/templates/srp/management.html:8
msgid "Srp Management" msgid "Srp Management"
@@ -1924,19 +1927,19 @@ msgstr "모두 조회하기"
#: allianceauth/srp/templates/srp/management.html:23 #: allianceauth/srp/templates/srp/management.html:23
msgid "Add SRP Fleet" msgid "Add SRP Fleet"
msgstr "SRP 보상 플릿 추가" msgstr "SRP 보상 함대 추가"
#: allianceauth/srp/templates/srp/management.html:41 #: allianceauth/srp/templates/srp/management.html:41
msgid "Fleet AAR" msgid "Fleet AAR"
msgstr "플릿 사후처리 보고서" msgstr "함대 사후처리 보고서"
#: allianceauth/srp/templates/srp/management.html:42 #: allianceauth/srp/templates/srp/management.html:42
msgid "Fleet SRP Code" msgid "Fleet SRP Code"
msgstr "플릿 SRP 보상 코드" msgstr "함대 SRP 보상 코드"
#: allianceauth/srp/templates/srp/management.html:43 #: allianceauth/srp/templates/srp/management.html:43
msgid "Fleet ISK Cost" msgid "Fleet ISK Cost"
msgstr "플릿 ISK 비용" msgstr "함대 ISK 비용"
#: allianceauth/srp/templates/srp/management.html:44 #: allianceauth/srp/templates/srp/management.html:44
msgid "SRP Status" msgid "SRP Status"
@@ -1983,37 +1986,37 @@ msgstr "사후처리 보고서 링크 업데이트"
#: allianceauth/srp/templates/srp/update.html:17 #: allianceauth/srp/templates/srp/update.html:17
msgid "SRP Fleet Does Not Exist" msgid "SRP Fleet Does Not Exist"
msgstr "SRP 보상 플릿이 존재하지 않습니다." msgstr "SRP 보상 함대이 존재하지 않습니다."
#: allianceauth/srp/views.py:85 #: allianceauth/srp/views.py:85
#, python-format #, python-format
msgid "Created SRP fleet %(fleetname)s." msgid "Created SRP fleet %(fleetname)s."
msgstr "SRP 보상 플릿 %(fleetname)s 생성 완료" msgstr "SRP 보상 함대 %(fleetname)s 생성 완료"
#: allianceauth/srp/views.py:103 #: allianceauth/srp/views.py:103
#, python-format #, python-format
msgid "Removed SRP fleet %(fleetname)s." msgid "Removed SRP fleet %(fleetname)s."
msgstr "SRP 보상 플릿 %(fleetname)s삭제 완료" msgstr "SRP 보상 함대 %(fleetname)s삭제 완료"
#: allianceauth/srp/views.py:115 #: allianceauth/srp/views.py:115
#, python-format #, python-format
msgid "Disabled SRP fleet %(fleetname)s." msgid "Disabled SRP fleet %(fleetname)s."
msgstr "SRP 보상 플릿 %(fleetname)s비활성화 완료" msgstr "SRP 보상 함대 %(fleetname)s비활성화 완료"
#: allianceauth/srp/views.py:127 #: allianceauth/srp/views.py:127
#, python-format #, python-format
msgid "Enabled SRP fleet %(fleetname)s." msgid "Enabled SRP fleet %(fleetname)s."
msgstr "SRP 보상 플릿 %(fleetname)s 활성화 완료" msgstr "SRP 보상 함대 %(fleetname)s 활성화 완료"
#: allianceauth/srp/views.py:140 #: allianceauth/srp/views.py:140
#, python-format #, python-format
msgid "Marked SRP fleet %(fleetname)s as completed." msgid "Marked SRP fleet %(fleetname)s as completed."
msgstr "SRP 보상 플릿 %(fleetname)s 을 완료된 것으로 표시" msgstr "SRP 보상 함대 %(fleetname)s 을 완료된 것으로 표시"
#: allianceauth/srp/views.py:153 #: allianceauth/srp/views.py:153
#, python-format #, python-format
msgid "Marked SRP fleet %(fleetname)s as incomplete." msgid "Marked SRP fleet %(fleetname)s as incomplete."
msgstr "SRP 보상 플릿 %(fleetname)s 을 미완료된 것으로 표시" msgstr "SRP 보상 함대 %(fleetname)s 을 미완료된 것으로 표시"
#: allianceauth/srp/views.py:165 #: allianceauth/srp/views.py:165
#, python-format #, python-format
@@ -2079,7 +2082,7 @@ msgstr "SRP 보상 요청 %(requestid)s을 찾을 수 없습니다. "
#: allianceauth/srp/views.py:360 #: allianceauth/srp/views.py:360
#, python-format #, python-format
msgid "Saved changes to SRP fleet %(fleetname)s" msgid "Saved changes to SRP fleet %(fleetname)s"
msgstr "SRP 보상 요청 플릿 %(fleetname)s의 변경 사항이 저장되었습니다." msgstr "SRP 보상 요청 함대 %(fleetname)s의 변경 사항이 저장되었습니다."
#: allianceauth/templates/allianceauth/admin-status/overview.html:6 #: allianceauth/templates/allianceauth/admin-status/overview.html:6
msgid "Alliance Auth Notifications" msgid "Alliance Auth Notifications"
@@ -2229,7 +2232,7 @@ msgstr "중요"
#: allianceauth/timerboard/form.py:70 #: allianceauth/timerboard/form.py:70
msgid "Corp-Restricted" msgid "Corp-Restricted"
msgstr " 제한" msgstr "코퍼레이션 제한"
#: allianceauth/timerboard/models.py:14 #: allianceauth/timerboard/models.py:14
msgid "Not Specified" msgid "Not Specified"
@@ -2294,7 +2297,7 @@ msgstr "스트럭처 타이머"
#: allianceauth/timerboard/templates/timerboard/view.html:28 #: allianceauth/timerboard/templates/timerboard/view.html:28
msgid "Corp Timers" msgid "Corp Timers"
msgstr " 타이머" msgstr "코퍼레이션 타이머"
#: allianceauth/timerboard/templates/timerboard/view.html:35 #: allianceauth/timerboard/templates/timerboard/view.html:35
#: allianceauth/timerboard/templates/timerboard/view.html:202 #: allianceauth/timerboard/templates/timerboard/view.html:202

View File

@@ -1,5 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Notifications" %}{% endblock %} {% block page_title %}{% translate "Notifications" %}{% endblock %}

View File

@@ -1,5 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "View Notification" %}{% endblock page_title %} {% block page_title %}{% translate "View Notification" %}{% endblock page_title %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}

View File

@@ -40,7 +40,7 @@
</div> </div>
{% include 'bundles/moment-js.html' with locale=True %} {% include 'bundles/moment-js.html' with locale=True %}
<script src="{% static 'js/timers.js' %}"></script> <script src="{% static 'allianceauth/js/timers.js' %}"></script>
<script type="application/javascript"> <script type="application/javascript">
// Data // Data

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}

View File

@@ -47,7 +47,7 @@
{% block extra_javascript %} {% block extra_javascript %}
{% include 'bundles/datatables-js.html' %} {% include 'bundles/datatables-js.html' %}
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script> <script type="application/javascript" src="{% static 'allianceauth/js/filterDropDown/filterDropDown.min.js' %}"></script>
{% endblock %} {% endblock %}
{% block extra_css %} {% block extra_css %}

View File

@@ -80,7 +80,7 @@
{% block extra_javascript %} {% block extra_javascript %}
{% include 'bundles/datatables-js.html' %} {% include 'bundles/datatables-js.html' %}
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script> <script type="application/javascript" src="{% static 'allianceauth/js/filterDropDown/filterDropDown.min.js' %}"></script>
{% endblock %} {% endblock %}
{% block extra_css %} {% block extra_css %}

View File

@@ -17,7 +17,7 @@ class ServicesUserAdmin(admin.ModelAdmin):
"""Parent class for UserAdmin classes for all services""" """Parent class for UserAdmin classes for all services"""
class Media: class Media:
css = { css = {
"all": ("services/admin.css",) "all": ("allianceauth/services/admin.css",)
} }
search_fields = ('user__username',) search_fields = ('user__username',)

View File

@@ -8,7 +8,7 @@ from uuid import uuid1
from redis import Redis from redis import Redis
import requests import requests
from django_redis import get_redis_connection from allianceauth.utils.cache import get_redis_client
from allianceauth import __title__ as AUTH_TITLE, __url__, __version__ from allianceauth import __title__ as AUTH_TITLE, __url__, __version__
@@ -103,7 +103,7 @@ class DiscordClient:
self._access_token = str(access_token) self._access_token = str(access_token)
self._is_rate_limited = bool(is_rate_limited) self._is_rate_limited = bool(is_rate_limited)
if not redis: if not redis:
self._redis = get_redis_connection("default") self._redis = get_redis_client()
if not isinstance(self._redis, Redis): if not isinstance(self._redis, Redis):
raise RuntimeError( raise RuntimeError(
'This class requires a Redis client, but none was provided ' 'This class requires a Redis client, but none was provided '

View File

@@ -85,25 +85,18 @@ class TestBasicsAndHelpers(TestCase):
client = DiscordClient(TEST_BOT_TOKEN, mock_redis, is_rate_limited=True) client = DiscordClient(TEST_BOT_TOKEN, mock_redis, is_rate_limited=True)
self.assertTrue(client.is_rate_limited) self.assertTrue(client.is_rate_limited)
@patch(MODULE_PATH + '.get_redis_connection') def test_use_default_redis_if_none_provided(self):
def test_use_default_redis_if_none_provided(self, mock_caches):
my_redis = MagicMock(spec=Redis)
mock_caches.return_value = my_redis
client = DiscordClient(TEST_BOT_TOKEN) client = DiscordClient(TEST_BOT_TOKEN)
self.assertTrue(mock_caches.called) self.assertIsInstance(client._redis, Redis)
self.assertEqual(client._redis, my_redis)
@patch(MODULE_PATH + '.get_redis_connection')
def test_raise_exception_if_default_cache_is_not_redis(self, mock_caches):
my_redis = MagicMock()
mock_caches.return_value = my_redis
@patch(MODULE_PATH + '.get_redis_client')
def test_raise_exception_if_redis_client_not_found(self, mock_get_redis_client):
# given
mock_get_redis_client.return_value = None
# when
with self.assertRaises(RuntimeError): with self.assertRaises(RuntimeError):
DiscordClient(TEST_BOT_TOKEN) DiscordClient(TEST_BOT_TOKEN)
self.assertTrue(mock_caches.called)
@requests_mock.Mocker() @requests_mock.Mocker()
class TestOtherMethods(TestCase): class TestOtherMethods(TestCase):

View File

@@ -35,17 +35,17 @@ import logging
from uuid import uuid1 from uuid import uuid1
import random import random
from django.core.cache import caches
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from allianceauth.services.modules.discord.models import DiscordUser from allianceauth.services.modules.discord.models import DiscordUser
from allianceauth.utils.cache import get_redis_client
logger = logging.getLogger('allianceauth') logger = logging.getLogger('allianceauth')
MAX_RUNS = 3 MAX_RUNS = 3
def clear_cache(): def clear_cache():
default_cache = caches['default'] redis = get_redis_client()
redis = default_cache.get_master_client()
redis.flushall() redis.flushall()
logger.info('Cache flushed') logger.info('Cache flushed')

View File

@@ -14,7 +14,6 @@ from requests.exceptions import HTTPError
import requests_mock import requests_mock
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django_redis import get_redis_connection
from django.shortcuts import reverse from django.shortcuts import reverse
from django.test import TransactionTestCase, TestCase from django.test import TransactionTestCase, TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
@@ -23,6 +22,7 @@ from allianceauth.authentication.models import State
from allianceauth.eveonline.models import EveCharacter from allianceauth.eveonline.models import EveCharacter
from allianceauth.notifications.models import Notification from allianceauth.notifications.models import Notification
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.utils.cache import get_redis_client
from . import ( from . import (
TEST_GUILD_ID, TEST_GUILD_ID,
@@ -87,7 +87,7 @@ remove_guild_member_request = DiscordRequest(
def clear_cache(): def clear_cache():
redis = get_redis_connection('default') redis = get_redis_client()
redis.flushall() redis.flushall()
logger.info('Cache flushed') logger.info('Cache flushed')
@@ -108,7 +108,6 @@ def reset_testdata():
class TestServiceFeatures(TransactionTestCase): class TestServiceFeatures(TransactionTestCase):
fixtures = ['disable_analytics.json'] fixtures = ['disable_analytics.json']
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Jabber Broadcast" %}{% endblock page_title %} {% block page_title %}{% translate "Jabber Broadcast" %}{% endblock page_title %}

View File

@@ -6,6 +6,8 @@ import hashlib
import logging import logging
import re import re
from packaging import version
from django.db import connections from django.db import connections
from django.conf import settings from django.conf import settings
from allianceauth.eveonline.models import EveCharacter from allianceauth.eveonline.models import EveCharacter
@@ -20,9 +22,18 @@ class SmfManager:
def __init__(self): def __init__(self):
pass pass
SQL_ADD_USER = r"INSERT INTO %smembers (member_name, passwd, email_address, date_registered, real_name," \ # For SMF < 2.1
r" buddy_list, message_labels, openid_uri, signature, ignore_boards) " \ SQL_ADD_USER_SMF_20 = r"INSERT INTO %smembers (member_name, passwd, email_address, date_registered, real_name," \
r"VALUES (%%s, %%s, %%s, %%s, %%s, 0, 0, 0, 0, 0)" % TABLE_PREFIX r" buddy_list, message_labels, openid_uri, signature, ignore_boards) " \
r"VALUES (%%s, %%s, %%s, %%s, %%s, 0, 0, 0, 0, 0)" % TABLE_PREFIX
# For SMF >= 2.1
SQL_ADD_USER_SMF_21 = r"INSERT INTO %smembers (member_name, passwd, email_address, date_registered, real_name," \
r" buddy_list, signature, ignore_boards) " \
r"VALUES (%%s, %%s, %%s, %%s, %%s, 0, 0, 0)" % TABLE_PREFIX
# returns something like »window.smfVersion = "SMF 2.0.19";«
SQL_GET_CURRENT_SMF_VERSION = r"SELECT data FROM %sadmin_info_files WHERE filename = %%s" % TABLE_PREFIX
SQL_DEL_USER = r"DELETE FROM %smembers where member_name = %%s" % TABLE_PREFIX SQL_DEL_USER = r"DELETE FROM %smembers where member_name = %%s" % TABLE_PREFIX
@@ -46,6 +57,26 @@ class SmfManager:
SQL_ADD_USER_AVATAR = r"UPDATE %smembers SET avatar = %%s WHERE id_member = %%s" % TABLE_PREFIX SQL_ADD_USER_AVATAR = r"UPDATE %smembers SET avatar = %%s WHERE id_member = %%s" % TABLE_PREFIX
@classmethod
def _get_current_smf_version(cls) -> str:
"""
Get the current SMF version from the DB
:return:
"""
cursor = connections['smf'].cursor()
cursor.execute(cls.SQL_GET_CURRENT_SMF_VERSION, ['current-version.js'])
row = cursor.fetchone()
db_result = row[0]
pattern = re.compile(r"\d+(\.\d+)+")
result = pattern.search(db_result)
smf_version = result.group(0)
return smf_version
@staticmethod @staticmethod
def _sanitize_groupname(name): def _sanitize_groupname(name):
name = name.strip(' _') name = name.strip(' _')
@@ -73,15 +104,15 @@ class SmfManager:
@classmethod @classmethod
def create_group(cls, groupname): def create_group(cls, groupname):
logger.debug("Creating smf group %s" % groupname) logger.debug(f"Creating smf group {groupname}")
cursor = connections['smf'].cursor() cursor = connections['smf'].cursor()
cursor.execute(cls.SQL_ADD_GROUP, [groupname, groupname]) cursor.execute(cls.SQL_ADD_GROUP, [groupname, groupname])
logger.info("Created smf group %s" % groupname) logger.info(f"Created smf group {groupname}")
return cls.get_group_id(groupname) return cls.get_group_id(groupname)
@classmethod @classmethod
def get_group_id(cls, groupname): def get_group_id(cls, groupname):
logger.debug("Getting smf group id for groupname %s" % groupname) logger.debug(f"Getting smf group id for groupname {groupname}")
cursor = connections['smf'].cursor() cursor = connections['smf'].cursor()
cursor.execute(cls.SQL_GET_GROUP_ID, [groupname]) cursor.execute(cls.SQL_GET_GROUP_ID, [groupname])
row = cursor.fetchone() row = cursor.fetchone()
@@ -90,14 +121,14 @@ class SmfManager:
@classmethod @classmethod
def check_user(cls, username): def check_user(cls, username):
logger.debug("Checking smf username %s" % username) logger.debug(f"Checking smf username {username}")
cursor = connections['smf'].cursor() cursor = connections['smf'].cursor()
cursor.execute(cls.SQL_USER_ID_FROM_USERNAME, [cls.santatize_username(username)]) cursor.execute(cls.SQL_USER_ID_FROM_USERNAME, [cls.santatize_username(username)])
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
logger.debug("Found user %s on smf" % username) logger.debug(f"Found user {username} on smf")
return True return True
logger.debug("User %s not found on smf" % username) logger.debug(f"User {username} not found on smf")
return False return False
@classmethod @classmethod
@@ -110,7 +141,7 @@ class SmfManager:
@classmethod @classmethod
def get_user_id(cls, username): def get_user_id(cls, username):
logger.debug("Getting smf user id for username %s" % username) logger.debug(f"Getting smf user id for username {username}")
cursor = connections['smf'].cursor() cursor = connections['smf'].cursor()
cursor.execute(cls.SQL_USER_ID_FROM_USERNAME, [username]) cursor.execute(cls.SQL_USER_ID_FROM_USERNAME, [username])
row = cursor.fetchone() row = cursor.fetchone()
@@ -118,7 +149,7 @@ class SmfManager:
logger.debug(f"Got smf user id {row[0]} for username {username}") logger.debug(f"Got smf user id {row[0]} for username {username}")
return row[0] return row[0]
else: else:
logger.error("username %s not found on smf. Unable to determine user id ." % username) logger.error(f"username {username} not found on smf. Unable to determine user id .")
return None return None
@classmethod @classmethod
@@ -130,12 +161,12 @@ class SmfManager:
out = {} out = {}
for row in rows: for row in rows:
out[row[1]] = row[0] out[row[1]] = row[0]
logger.debug("Got smf groups %s" % out) logger.debug(f"Got smf groups {out}")
return out return out
@classmethod @classmethod
def get_user_groups(cls, userid): def get_user_groups(cls, userid):
logger.debug("Getting smf user id %s groups" % userid) logger.debug(f"Getting smf user id {userid} groups")
cursor = connections['smf'].cursor() cursor = connections['smf'].cursor()
cursor.execute(cls.SQL_GET_USER_GROUPS, [userid]) cursor.execute(cls.SQL_GET_USER_GROUPS, [userid])
out = [row[0] for row in cursor.fetchall()] out = [row[0] for row in cursor.fetchall()]
@@ -144,8 +175,11 @@ class SmfManager:
@classmethod @classmethod
def add_user(cls, username, email_address, groups, characterid): def add_user(cls, username, email_address, groups, characterid):
logger.debug("Adding smf user with member_name {}, email_address {}, characterid {}".format( logger.debug(
username, email_address, characterid)) f"Adding smf user with member_name {username}, "
f"email_address {email_address}, "
f"characterid {characterid}"
)
cursor = connections['smf'].cursor() cursor = connections['smf'].cursor()
username_clean = cls.santatize_username(username) username_clean = cls.santatize_username(username)
passwd = cls.generate_random_pass() passwd = cls.generate_random_pass()
@@ -154,42 +188,59 @@ class SmfManager:
register_date = cls.get_current_utc_date() register_date = cls.get_current_utc_date()
# check if the username was simply revoked # check if the username was simply revoked
if cls.check_user(username) is True: if cls.check_user(username) is True:
logger.warn("Unable to add smf user with username %s - already exists. Updating user instead." % username) logger.warning(f"Unable to add smf user with username {username} - already exists. Updating user instead.")
cls.__update_user_info(username_clean, email_address, pwhash) cls.__update_user_info(username_clean, email_address, pwhash)
else: else:
try: try:
cursor.execute(cls.SQL_ADD_USER, smf_version = cls._get_current_smf_version()
[username_clean, passwd, email_address, register_date, username_clean])
if version.parse(smf_version) < version.parse("2.1"):
logger.debug("SMF compatibility: < 2.1")
cursor.execute(
cls.SQL_ADD_USER_SMF_20,
[username_clean, pwhash, email_address, register_date, username_clean]
)
else:
logger.debug("SMF compatibility: >= 2.1")
cursor.execute(
cls.SQL_ADD_USER_SMF_21,
[username_clean, pwhash, email_address, register_date, username_clean]
)
cls.add_avatar(username_clean, characterid) cls.add_avatar(username_clean, characterid)
logger.info("Added smf member_name %s" % username_clean) logger.info(f"Added smf member_name {username_clean}")
cls.update_groups(username_clean, groups) cls.update_groups(username_clean, groups)
except: except Exception as e:
logger.warn("Unable to add smf user %s" % username_clean) logger.warning(f"Unable to add smf user {username_clean}: {e}")
pass pass
return username_clean, passwd return username_clean, passwd
@classmethod @classmethod
def __update_user_info(cls, username, email_address, passwd): def __update_user_info(cls, username, email_address, passwd):
logger.debug( logger.debug(
f"Updating smf user {username} info: username {email_address} password of length {len(passwd)}") f"Updating smf user {username} info: "
f"username {email_address} "
f"password of length {len(passwd)}"
)
cursor = connections['smf'].cursor() cursor = connections['smf'].cursor()
try: try:
cursor.execute(cls.SQL_DIS_USER, [email_address, passwd, username]) cursor.execute(cls.SQL_DIS_USER, [email_address, passwd, username])
logger.info("Updated smf user %s info" % username) logger.info(f"Updated smf user {username} info")
except: except Exception as e:
logger.exception("Unable to update smf user %s info." % username) logger.exception(f"Unable to update smf user {username} info. ({e})")
pass pass
@classmethod @classmethod
def delete_user(cls, username): def delete_user(cls, username):
logger.debug("Deleting smf user %s" % username) logger.debug(f"Deleting smf user {username}")
cursor = connections['smf'].cursor() cursor = connections['smf'].cursor()
if cls.check_user(username): if cls.check_user(username):
cursor.execute(cls.SQL_DEL_USER, [username]) cursor.execute(cls.SQL_DEL_USER, [username])
logger.info("Deleted smf user %s" % username) logger.info(f"Deleted smf user {username}")
return True return True
logger.error("Unable to delete smf user %s - user not found on smf." % username) logger.error(f"Unable to delete smf user {username} - user not found on smf.")
return False return False
@classmethod @classmethod
@@ -218,8 +269,8 @@ class SmfManager:
cursor = connections['smf'].cursor() cursor = connections['smf'].cursor()
cursor.execute(cls.SQL_ADD_USER_GROUP, [groupid, userid]) cursor.execute(cls.SQL_ADD_USER_GROUP, [groupid, userid])
logger.info(f"Added smf user id {userid} to group id {groupid}") logger.info(f"Added smf user id {userid} to group id {groupid}")
except: except Exception as e:
logger.exception(f"Unable to add smf user id {userid} to group id {groupid}") logger.exception(f"Unable to add smf user id {userid} to group id {groupid} ({e})")
pass pass
@classmethod @classmethod
@@ -229,13 +280,13 @@ class SmfManager:
cursor = connections['smf'].cursor() cursor = connections['smf'].cursor()
cursor.execute(cls.SQL_REMOVE_USER_GROUP, [groupid, userid]) cursor.execute(cls.SQL_REMOVE_USER_GROUP, [groupid, userid])
logger.info(f"Removed smf user id {userid} from group id {groupid}") logger.info(f"Removed smf user id {userid} from group id {groupid}")
except: except Exception as e:
logger.exception(f"Unable to remove smf user id {userid} from group id {groupid}") logger.exception(f"Unable to remove smf user id {userid} from group id {groupid} ({e})")
pass pass
@classmethod @classmethod
def disable_user(cls, username): def disable_user(cls, username):
logger.debug("Disabling smf user %s" % username) logger.debug(f"Disabling smf user {username}")
cursor = connections['smf'].cursor() cursor = connections['smf'].cursor()
password = cls.generate_random_pass() password = cls.generate_random_pass()
@@ -245,15 +296,15 @@ class SmfManager:
cursor.execute(cls.SQL_DIS_USER, [revoke_email, pwhash, username]) cursor.execute(cls.SQL_DIS_USER, [revoke_email, pwhash, username])
cls.get_user_id(username) cls.get_user_id(username)
cls.update_groups(username, []) cls.update_groups(username, [])
logger.info("Disabled smf user %s" % username) logger.info(f"Disabled smf user {username}")
return True return True
except TypeError: except TypeError:
logger.exception("TypeError occured while disabling user %s - failed to disable." % username) logger.exception(f"TypeError occured while disabling user {username} - failed to disable.")
return False return False
@classmethod @classmethod
def update_user_password(cls, username, characterid, password=None): def update_user_password(cls, username, characterid, password=None):
logger.debug("Updating smf user %s password" % username) logger.debug(f"Updating smf user {username} password")
cursor = connections['smf'].cursor() cursor = connections['smf'].cursor()
if not password: if not password:
password = cls.generate_random_pass() password = cls.generate_random_pass()
@@ -261,10 +312,12 @@ class SmfManager:
username_clean = cls.santatize_username(username) username_clean = cls.santatize_username(username)
pwhash = cls.gen_hash(username_clean, password) pwhash = cls.gen_hash(username_clean, password)
logger.debug( logger.debug(
f"Proceeding to update smf user {username} password with pwhash starting with {pwhash[0:5]}") f"Proceeding to update smf user {username} "
f"password with pwhash starting with {pwhash[0:5]}"
)
cursor.execute(cls.SQL_UPDATE_USER_PASSWORD, [pwhash, username]) cursor.execute(cls.SQL_UPDATE_USER_PASSWORD, [pwhash, username])
cls.add_avatar(username, characterid) cls.add_avatar(username, characterid)
logger.info("Updated smf user %s password." % username) logger.info(f"Updated smf user {username} password.")
return password return password
logger.error("Unable to update smf user %s password - user not found on smf." % username) logger.error(f"Unable to update smf user {username} password - user not found on smf.")
return "" return ""

View File

@@ -1,5 +1,4 @@
{% extends "admin/change_list.html" %} {% extends "admin/change_list.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block object-tools-items %} {% block object-tools-items %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Verify Teamspeak" %}{% endblock page_title %} {% block page_title %}{% translate "Verify Teamspeak" %}{% endblock page_title %}

View File

@@ -1,4 +1,5 @@
import logging import logging
from functools import partial
from django.contrib.auth.models import User, Group, Permission from django.contrib.auth.models import User, Group, Permission
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@@ -8,7 +9,7 @@ from django.db.models.signals import pre_delete
from django.db.models.signals import pre_save from django.db.models.signals import pre_save
from django.dispatch import receiver from django.dispatch import receiver
from .hooks import ServicesHook from .hooks import ServicesHook
from .tasks import disable_user from .tasks import disable_user, update_groups_for_user
from allianceauth.authentication.models import State, UserProfile from allianceauth.authentication.models import State, UserProfile
from allianceauth.authentication.signals import state_changed from allianceauth.authentication.signals import state_changed
@@ -19,21 +20,27 @@ logger = logging.getLogger(__name__)
@receiver(m2m_changed, sender=User.groups.through) @receiver(m2m_changed, sender=User.groups.through)
def m2m_changed_user_groups(sender, instance, action, *args, **kwargs): def m2m_changed_user_groups(sender, instance, action, *args, **kwargs):
logger.debug(f"Received m2m_changed from {instance} groups with action {action}") logger.debug(
"%s: Received m2m_changed from groups with action %s", instance, action
def trigger_service_group_update(): )
logger.debug("Triggering service group update for %s" % instance) if instance.pk and (
# Iterate through Service hooks action == "post_add" or action == "post_remove" or action == "post_clear"
for svc in ServicesHook.get_services(): ):
try: if isinstance(instance, User):
svc.validate_user(instance) logger.debug(
svc.update_groups(instance) "Waiting for commit to trigger service group update for %s", instance
except: )
logger.exception(f'Exception running update_groups for services module {svc} on user {instance}') transaction.on_commit(partial(update_groups_for_user.delay, instance.pk))
elif (
if instance.pk and (action == "post_add" or action == "post_remove" or action == "post_clear"): isinstance(instance, Group)
logger.debug("Waiting for commit to trigger service group update for %s" % instance) and kwargs.get("model") is User
transaction.on_commit(trigger_service_group_update) and "pk_set" in kwargs
):
for user_pk in kwargs["pk_set"]:
logger.debug(
"%s: Waiting for commit to trigger service group update for user", user_pk
)
transaction.on_commit(partial(update_groups_for_user.delay, user_pk))
@receiver(m2m_changed, sender=User.user_permissions.through) @receiver(m2m_changed, sender=User.user_permissions.through)

View File

@@ -47,3 +47,20 @@ def disable_user(user):
for svc in ServicesHook.get_services(): for svc in ServicesHook.get_services():
if svc.service_active_for_user(user): if svc.service_active_for_user(user):
svc.delete_user(user) svc.delete_user(user)
@shared_task
def update_groups_for_user(user_pk: int) -> None:
"""Update groups for all services registered to a user."""
user = User.objects.get(pk=user_pk)
logger.debug("%s: Triggering service group update for user", user)
for svc in ServicesHook.get_services():
try:
svc.validate_user(user)
svc.update_groups(user)
except Exception:
logger.exception(
'Exception running update_groups for services module %s on user %s',
svc,
user
)

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Fleet Formatter Tool" %}{% endblock page_title %} {% block page_title %}{% translate "Fleet Formatter Tool" %}{% endblock page_title %}

View File

@@ -1,5 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% blocktrans with service_name=view.service_name|title %}{{ service_name }} Credentials{% endblocktrans %}{% endblock page_title %} {% block page_title %}{% blocktrans with service_name=view.service_name|title %}{{ service_name }} Credentials{% endblocktrans %}{% endblock page_title %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% blocktrans with service_name=view.service_name|title %}{{ service_name }} Password Change{% endblocktrans %}{% endblock page_title %} {% block page_title %}{% blocktrans with service_name=view.service_name|title %}{{ service_name }} Password Change{% endblocktrans %}{% endblock page_title %}

View File

@@ -1,5 +1,4 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Services Management" %}{% endblock page_title %} {% block page_title %}{% translate "Services Management" %}{% endblock page_title %}

View File

@@ -1,7 +1,7 @@
from copy import deepcopy from copy import deepcopy
from unittest import mock from unittest import mock
from django.test import TestCase from django.test import override_settings, TestCase, TransactionTestCase
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from allianceauth.authentication.models import State from allianceauth.authentication.models import State
@@ -9,6 +9,9 @@ from allianceauth.eveonline.models import EveCharacter
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
MODULE_PATH = 'allianceauth.services.signals'
class ServicesSignalsTestCase(TestCase): class ServicesSignalsTestCase(TestCase):
def setUp(self): def setUp(self):
self.member = AuthUtils.create_user('auth_member', disconnect_signals=True) self.member = AuthUtils.create_user('auth_member', disconnect_signals=True)
@@ -17,17 +20,12 @@ class ServicesSignalsTestCase(TestCase):
) )
self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True) self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True)
@mock.patch('allianceauth.services.signals.transaction') @mock.patch(MODULE_PATH + '.transaction', spec=True)
@mock.patch('allianceauth.services.signals.ServicesHook') @mock.patch(MODULE_PATH + '.update_groups_for_user', spec=True)
def test_m2m_changed_user_groups(self, services_hook, transaction): def test_m2m_changed_user_groups(self, update_groups_for_user, transaction):
""" """
Test that update_groups hook function is called on user groups change Test that update_groups hook function is called on user groups change
""" """
svc = mock.Mock()
svc.update_groups.return_value = None
svc.validate_user.return_value = None
services_hook.get_services.return_value = [svc]
# Overload transaction.on_commit so everything happens synchronously # Overload transaction.on_commit so everything happens synchronously
transaction.on_commit = lambda fn: fn() transaction.on_commit = lambda fn: fn()
@@ -39,17 +37,11 @@ class ServicesSignalsTestCase(TestCase):
self.member.save() self.member.save()
# Assert # Assert
self.assertTrue(services_hook.get_services.called) self.assertTrue(update_groups_for_user.delay.called)
args, _ = update_groups_for_user.delay.call_args
self.assertEqual(self.member.pk, args[0])
self.assertTrue(svc.update_groups.called) @mock.patch(MODULE_PATH + '.disable_user')
args, kwargs = svc.update_groups.call_args
self.assertEqual(self.member, args[0])
self.assertTrue(svc.validate_user.called)
args, kwargs = svc.validate_user.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.disable_user')
def test_pre_delete_user(self, disable_user): def test_pre_delete_user(self, disable_user):
""" """
Test that disable_member is called when a user is deleted Test that disable_member is called when a user is deleted
@@ -60,7 +52,7 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = disable_user.call_args args, kwargs = disable_user.call_args
self.assertEqual(self.none_user, args[0]) self.assertEqual(self.none_user, args[0])
@mock.patch('allianceauth.services.signals.disable_user') @mock.patch(MODULE_PATH + '.disable_user')
def test_pre_save_user_inactivation(self, disable_user): def test_pre_save_user_inactivation(self, disable_user):
""" """
Test a user set inactive has disable_member called Test a user set inactive has disable_member called
@@ -72,7 +64,7 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = disable_user.call_args args, kwargs = disable_user.call_args
self.assertEqual(self.member, args[0]) self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.disable_user') @mock.patch(MODULE_PATH + '.disable_user')
def test_disable_services_on_loss_of_main_character(self, disable_user): def test_disable_services_on_loss_of_main_character(self, disable_user):
""" """
Test a user set inactive has disable_member called Test a user set inactive has disable_member called
@@ -84,8 +76,8 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = disable_user.call_args args, kwargs = disable_user.call_args
self.assertEqual(self.member, args[0]) self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.transaction') @mock.patch(MODULE_PATH + '.transaction')
@mock.patch('allianceauth.services.signals.ServicesHook') @mock.patch(MODULE_PATH + '.ServicesHook')
def test_m2m_changed_group_permissions(self, services_hook, transaction): def test_m2m_changed_group_permissions(self, services_hook, transaction):
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
svc = mock.Mock() svc = mock.Mock()
@@ -116,8 +108,8 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = svc.validate_user.call_args args, kwargs = svc.validate_user.call_args
self.assertEqual(self.member, args[0]) self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.transaction') @mock.patch(MODULE_PATH + '.transaction')
@mock.patch('allianceauth.services.signals.ServicesHook') @mock.patch(MODULE_PATH + '.ServicesHook')
def test_m2m_changed_user_permissions(self, services_hook, transaction): def test_m2m_changed_user_permissions(self, services_hook, transaction):
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
svc = mock.Mock() svc = mock.Mock()
@@ -145,8 +137,8 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = svc.validate_user.call_args args, kwargs = svc.validate_user.call_args
self.assertEqual(self.member, args[0]) self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.transaction') @mock.patch(MODULE_PATH + '.transaction')
@mock.patch('allianceauth.services.signals.ServicesHook') @mock.patch(MODULE_PATH + '.ServicesHook')
def test_m2m_changed_user_state_permissions(self, services_hook, transaction): def test_m2m_changed_user_state_permissions(self, services_hook, transaction):
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
svc = mock.Mock() svc = mock.Mock()
@@ -180,7 +172,7 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = svc.validate_user.call_args args, kwargs = svc.validate_user.call_args
self.assertEqual(self.member, args[0]) self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.ServicesHook') @mock.patch(MODULE_PATH + '.ServicesHook')
def test_state_changed_services_validation_and_groups_update(self, services_hook): def test_state_changed_services_validation_and_groups_update(self, services_hook):
"""Test a user changing state has service accounts validated and groups updated """Test a user changing state has service accounts validated and groups updated
""" """
@@ -206,8 +198,7 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = svc.update_groups.call_args args, kwargs = svc.update_groups.call_args
self.assertEqual(self.member, args[0]) self.assertEqual(self.member, args[0])
@mock.patch(MODULE_PATH + '.ServicesHook')
@mock.patch('allianceauth.services.signals.ServicesHook')
def test_state_changed_services_validation_and_groups_update_1(self, services_hook): def test_state_changed_services_validation_and_groups_update_1(self, services_hook):
"""Test a user changing main has service accounts validated and sync updated """Test a user changing main has service accounts validated and sync updated
""" """
@@ -238,7 +229,7 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = svc.sync_nickname.call_args args, kwargs = svc.sync_nickname.call_args
self.assertEqual(self.member, args[0]) self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.ServicesHook') @mock.patch(MODULE_PATH + '.ServicesHook')
def test_state_changed_services_validation_and_groups_update_2(self, services_hook): def test_state_changed_services_validation_and_groups_update_2(self, services_hook):
"""Test a user changing main has service does not have accounts validated """Test a user changing main has service does not have accounts validated
and sync updated if the new main is equal to the old main and sync updated if the new main is equal to the old main
@@ -260,3 +251,71 @@ class ServicesSignalsTestCase(TestCase):
self.assertFalse(services_hook.get_services.called) self.assertFalse(services_hook.get_services.called)
self.assertFalse(svc.validate_user.called) self.assertFalse(svc.validate_user.called)
self.assertFalse(svc.sync_nickname.called) self.assertFalse(svc.sync_nickname.called)
@mock.patch(
"allianceauth.services.modules.mumble.auth_hooks.MumbleService.update_groups"
)
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class TestUserGroupBulkUpdate(TransactionTestCase):
def test_should_run_user_service_check_when_group_added_to_user(
self, mock_update_groups
):
# given
user = AuthUtils.create_user("Bruce Wayne")
AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
group = Group.objects.create(name="Group")
mock_update_groups.reset_mock()
# when
user.groups.add(group)
# then
users_updated = {obj[0][0] for obj in mock_update_groups.call_args_list}
self.assertSetEqual(users_updated, {user})
def test_should_run_user_service_check_when_multiple_groups_are_added_to_user(
self, mock_update_groups
):
# given
user = AuthUtils.create_user("Bruce Wayne")
AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
group_1 = Group.objects.create(name="Group 1")
group_2 = Group.objects.create(name="Group 2")
mock_update_groups.reset_mock()
# when
user.groups.add(group_1, group_2)
# then
users_updated = {obj[0][0] for obj in mock_update_groups.call_args_list}
self.assertSetEqual(users_updated, {user})
def test_should_run_user_service_check_when_user_added_to_group(
self, mock_update_groups
):
# given
user = AuthUtils.create_user("Bruce Wayne")
AuthUtils.add_main_character_2(user, "Bruce Wayne", 1001)
group = Group.objects.create(name="Group")
mock_update_groups.reset_mock()
# when
group.user_set.add(user)
# then
users_updated = {obj[0][0] for obj in mock_update_groups.call_args_list}
self.assertSetEqual(users_updated, {user})
def test_should_run_user_service_check_when_multiple_users_are_added_to_group(
self, mock_update_groups
):
# given
user_1 = AuthUtils.create_user("Bruce Wayne")
AuthUtils.add_main_character_2(user_1, "Bruce Wayne", 1001)
user_2 = AuthUtils.create_user("Peter Parker")
AuthUtils.add_main_character_2(user_2, "Peter Parker", 1002)
user_3 = AuthUtils.create_user("Lex Luthor")
AuthUtils.add_main_character_2(user_3, "Lex Luthor", 1011)
group = Group.objects.create(name="Group")
user_1.groups.add(group)
mock_update_groups.reset_mock()
# when
group.user_set.add(user_2, user_3)
# then
users_updated = {obj[0][0] for obj in mock_update_groups.call_args_list}
self.assertSetEqual(users_updated, {user_2, user_3})

View File

@@ -3,32 +3,50 @@ from unittest import mock
from celery_once import AlreadyQueued from celery_once import AlreadyQueued
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase from django.test import override_settings, TestCase
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.services.tasks import validate_services from allianceauth.services.tasks import validate_services, update_groups_for_user
from ..tasks import DjangoBackend from ..tasks import DjangoBackend
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class ServicesTasksTestCase(TestCase): class ServicesTasksTestCase(TestCase):
def setUp(self): def setUp(self):
self.member = AuthUtils.create_user('auth_member') self.member = AuthUtils.create_user('auth_member')
@mock.patch('allianceauth.services.tasks.ServicesHook') @mock.patch('allianceauth.services.tasks.ServicesHook')
def test_validate_services(self, services_hook): def test_validate_services(self, services_hook):
# given
svc = mock.Mock() svc = mock.Mock()
svc.validate_user.return_value = None svc.validate_user.return_value = None
services_hook.get_services.return_value = [svc] services_hook.get_services.return_value = [svc]
# when
validate_services.delay(self.member.pk) validate_services.delay(self.member.pk)
# then
self.assertTrue(services_hook.get_services.called) self.assertTrue(services_hook.get_services.called)
self.assertTrue(svc.validate_user.called) self.assertTrue(svc.validate_user.called)
args, kwargs = svc.validate_user.call_args args, _ = svc.validate_user.call_args
self.assertEqual(self.member, args[0]) # Assert correct user is passed to service hook function self.assertEqual(self.member, args[0]) # Assert correct user is passed to service hook function
@mock.patch('allianceauth.services.tasks.ServicesHook')
def test_update_groups_for_user(self, services_hook):
# given
svc = mock.Mock()
svc.validate_user.return_value = None
services_hook.get_services.return_value = [svc]
# when
update_groups_for_user.delay(self.member.pk)
# then
self.assertTrue(services_hook.get_services.called)
self.assertTrue(svc.validate_user.called)
args, _ = svc.validate_user.call_args
self.assertEqual(self.member, args[0]) # Assert correct user
self.assertTrue(svc.update_groups.called)
args, _ = svc.update_groups.call_args
self.assertEqual(self.member, args[0]) # Assert correct user
class TestDjangoBackend(TestCase): class TestDjangoBackend(TestCase):

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "SRP Fleet Create" %}{% endblock page_title %} {% block page_title %}{% translate "SRP Fleet Create" %}{% endblock page_title %}

View File

@@ -8,7 +8,7 @@
{% block extra_css %} {% block extra_css %}
{% include 'bundles/datatables-css.html' %} {% include 'bundles/datatables-css.html' %}
{% include 'bundles/x-editable.css.html' %} {% include 'bundles/x-editable.css.html' %}
<link href="{% static 'css/checkbox.css' %}" rel="stylesheet" type="text/css"> <link href="{% static 'allianceauth/css/checkbox.css' %}" rel="stylesheet" type="text/css">
<style> <style>
.copy-text-fa-icon:hover { .copy-text-fa-icon:hover {
cursor: pointer; cursor: pointer;
@@ -30,8 +30,8 @@
width: 95%; width: 95%;
} }
.radio, .checkbox { .radio, .checkbox {
margin-top: 0px; margin-top: 0;
margin-bottom: 0px; margin-bottom: 0;
} }
.editable-error-block { .editable-error-block {
white-space: nowrap; white-space: nowrap;

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% load humanize %} {% load humanize %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "SRP Request" %}{% endblock page_title %} {% block page_title %}{% translate "SRP Request" %}{% endblock page_title %}

View File

@@ -1,6 +1,5 @@
{% extends "allianceauth/base.html" %} {% extends "allianceauth/base.html" %}
{% load bootstrap %} {% load bootstrap %}
{% load static %}
{% load i18n %} {% load i18n %}
{% block page_title %}{% translate "Update AAR Link" %}{% endblock page_title %} {% block page_title %}{% translate "Update AAR Link" %}{% endblock page_title %}

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 483 B

After

Width:  |  Height:  |  Size: 483 B

View File

Before

Width:  |  Height:  |  Size: 868 B

After

Width:  |  Height:  |  Size: 868 B

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

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