aa-fatanalysis 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aa_fatanalysis-0.1.0.dist-info/METADATA +82 -0
- aa_fatanalysis-0.1.0.dist-info/RECORD +24 -0
- aa_fatanalysis-0.1.0.dist-info/WHEEL +4 -0
- aa_fatanalysis-0.1.0.dist-info/licenses/LICENSE +21 -0
- fatanalysis/__init__.py +1 -0
- fatanalysis/apps.py +6 -0
- fatanalysis/auth_hooks.py +30 -0
- fatanalysis/forms.py +15 -0
- fatanalysis/migrations/0001_initial.py +36 -0
- fatanalysis/migrations/0002_alter_uploadperiod_upload_date.py +19 -0
- fatanalysis/migrations/__init__.py +1 -0
- fatanalysis/models.py +27 -0
- fatanalysis/templates/fatanalysis/base.html +21 -0
- fatanalysis/templates/fatanalysis/import_csv.html +20 -0
- fatanalysis/templates/fatanalysis/index.html +94 -0
- fatanalysis/templates/fatanalysis/overview.html +141 -0
- fatanalysis/templates/fatanalysis/period_delete.html +24 -0
- fatanalysis/templates/fatanalysis/period_detail.html +90 -0
- fatanalysis/templates/fatanalysis/period_list.html +66 -0
- fatanalysis/templates/fatanalysis/player_detail.html +139 -0
- fatanalysis/tests/__init__.py +0 -0
- fatanalysis/tests/test_settings.py +26 -0
- fatanalysis/urls.py +15 -0
- fatanalysis/views.py +374 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aa-fatanalysis
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FAT Analysis Plugin for Alliance Auth
|
|
5
|
+
Author-email: Maddog Broekman <maddogbroekman@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Maddog Broekman
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Keywords: allianceauth,fatanalysis
|
|
29
|
+
Classifier: Environment :: Web Environment
|
|
30
|
+
Classifier: Framework :: Django
|
|
31
|
+
Classifier: Framework :: Django :: 5.2
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Classifier: Programming Language :: Python
|
|
36
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
37
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
38
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
39
|
+
Requires-Python: <3.14,>=3.10
|
|
40
|
+
Requires-Dist: allianceauth>=5
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# FAT Analysis
|
|
44
|
+
|
|
45
|
+
Alliance Auth plugin for analyzing FATs (Fleet Activity Tracking). This plugin allows for the upload of 30-day and 90-day FAT CSV files, parses them dynamically, and provides overviews and interactive charts on both a per-character and per-corporation basis.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
1. **Activate your Alliance Auth virtual environment**
|
|
50
|
+
```bash
|
|
51
|
+
source /home/allianceserver/venv/auth/bin/activate
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
2. **Install the plugin**
|
|
55
|
+
Navigate to the directory containing this plugin and run:
|
|
56
|
+
```bash
|
|
57
|
+
pip install -e .
|
|
58
|
+
```
|
|
59
|
+
*(Or install via git / pip once published to a repository)*
|
|
60
|
+
|
|
61
|
+
3. **Update your Alliance Auth settings**
|
|
62
|
+
Add `'fatanalysis'` to your `INSTALLED_APPS` in `myauth/myauth/settings/local.py`:
|
|
63
|
+
```python
|
|
64
|
+
INSTALLED_APPS += [
|
|
65
|
+
'fatanalysis',
|
|
66
|
+
]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
4. **Run migrations**
|
|
70
|
+
```bash
|
|
71
|
+
python manage.py makemigrations
|
|
72
|
+
python manage.py migrate
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
5. **Restart Alliance Auth services**
|
|
76
|
+
```bash
|
|
77
|
+
sudo systemctl restart supervisor
|
|
78
|
+
```
|
|
79
|
+
*(Or restart gunicorn and celery according to your specific setup)*
|
|
80
|
+
|
|
81
|
+
## Permissions
|
|
82
|
+
Assign the `fatanalysis.basic_access` permission in the admin panel to any user or group that should have access to view and upload FAT analysis data.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
fatanalysis/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
fatanalysis/apps.py,sha256=KWWdeiQvY4fT68_uSH0nnms2yoq-tRxw5pe9a_yKZaY,156
|
|
3
|
+
fatanalysis/auth_hooks.py,sha256=LOzksVwH4AcsBjLNwhQU9HJzEfXCasPlLW5hXumu90s,912
|
|
4
|
+
fatanalysis/forms.py,sha256=dJHghi0VUaF8kK752poLOWAqzE3P4HiZM5lhttqXjdg,586
|
|
5
|
+
fatanalysis/models.py,sha256=wmbXla9zm32sH53Iv7gkSfBzfPg0iUCL5hK9w-rVCas,1114
|
|
6
|
+
fatanalysis/urls.py,sha256=LLBv3OhFYHpR2aSVHY5CDHao9mD_0qiXk4ctKOY8JyE,654
|
|
7
|
+
fatanalysis/views.py,sha256=hSQ8DgoIyxT0EMUfZ-BEFNhuTgJRbjEnZp55AgXYe-4,14433
|
|
8
|
+
fatanalysis/migrations/0001_initial.py,sha256=tg_VFhVcRwBenv77D0e4QWEBFNKKv9a0Hex0WEG70PY,1508
|
|
9
|
+
fatanalysis/migrations/0002_alter_uploadperiod_upload_date.py,sha256=NaNY4vtLKttvD0JKre0tLILd45dQgiW_GBWZNC7y3JY,444
|
|
10
|
+
fatanalysis/migrations/__init__.py,sha256=YqBpMx21tXZquHbEpYhi5ZhuIYcvG3SWJ6ZL9eVU6-c,20
|
|
11
|
+
fatanalysis/templates/fatanalysis/base.html,sha256=l_sAAOYg_c0nkPFm7S2av3k5pwKTiOU96ghEaHkI2nw,584
|
|
12
|
+
fatanalysis/templates/fatanalysis/import_csv.html,sha256=xGt8OQM86zPaDG3sv_iTrsu9L1F3CYdrvq2Zwpsltek,866
|
|
13
|
+
fatanalysis/templates/fatanalysis/index.html,sha256=4JziR2JMjmxKV4MOLm3qMaMdS9Z272LyPUnLcxxhc8U,3555
|
|
14
|
+
fatanalysis/templates/fatanalysis/overview.html,sha256=WJOTq2mvSfSzJ_5Ytnh3DVt5UyF6jnZpNxVCrwRr0yk,5440
|
|
15
|
+
fatanalysis/templates/fatanalysis/period_delete.html,sha256=oAeo5XPgqK2dwcILPcGEEIf6UWqgB-0XJc5mPUUoYF8,1203
|
|
16
|
+
fatanalysis/templates/fatanalysis/period_detail.html,sha256=nzW-lzLUUjb14HoYSeZeezGspZSGqR54xPP0gqW3DHs,3053
|
|
17
|
+
fatanalysis/templates/fatanalysis/period_list.html,sha256=pGedOZaBSIHX_EIdjjhrBA1_ToAHUy8zZovdz-FgYvs,2529
|
|
18
|
+
fatanalysis/templates/fatanalysis/player_detail.html,sha256=GKzuqx0iVM2oe-wS75rCj8KMyk9U74_OvAk4UDLa9ik,5491
|
|
19
|
+
fatanalysis/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
fatanalysis/tests/test_settings.py,sha256=gTRcJjt7e6rVas-JzCx4I1vm08ptLitH5NEdY7EkyC8,587
|
|
21
|
+
aa_fatanalysis-0.1.0.dist-info/METADATA,sha256=OmjqR8NqfGRcMlCg9UJjiphOX2KSXokbxumnHRwPICQ,3264
|
|
22
|
+
aa_fatanalysis-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
23
|
+
aa_fatanalysis-0.1.0.dist-info/licenses/LICENSE,sha256=VpXurLL80VG0dI9a9tuQl8t4iSOxm_6dbRscbI9LVdY,1072
|
|
24
|
+
aa_fatanalysis-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maddog Broekman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fatanalysis/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
fatanalysis/apps.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from allianceauth import hooks
|
|
2
|
+
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
|
3
|
+
from django.utils.translation import gettext_lazy as _
|
|
4
|
+
|
|
5
|
+
from . import urls
|
|
6
|
+
|
|
7
|
+
class FatAnalysisMenuItem(MenuItemHook):
|
|
8
|
+
"""This class ensures only authorized users will see the menu entry"""
|
|
9
|
+
def __init__(self):
|
|
10
|
+
# Setup menu entry for sidebar
|
|
11
|
+
MenuItemHook.__init__(
|
|
12
|
+
self,
|
|
13
|
+
_('FAT Analysis'),
|
|
14
|
+
'fas fa-chart-line fa-fw',
|
|
15
|
+
'fatanalysis:index',
|
|
16
|
+
navactive=['fatanalysis:']
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def render(self, request):
|
|
20
|
+
if request.user.has_perm('fatanalysis.basic_access'):
|
|
21
|
+
return MenuItemHook.render(self, request)
|
|
22
|
+
return ''
|
|
23
|
+
|
|
24
|
+
@hooks.register('menu_item_hook')
|
|
25
|
+
def register_menu():
|
|
26
|
+
return FatAnalysisMenuItem()
|
|
27
|
+
|
|
28
|
+
@hooks.register('url_hook')
|
|
29
|
+
def register_urls():
|
|
30
|
+
return UrlHook(urls, 'fatanalysis', r'^fatanalysis/')
|
fatanalysis/forms.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
|
|
3
|
+
class CsvImportForm(forms.Form):
|
|
4
|
+
PERIOD_CHOICES = (
|
|
5
|
+
(30, '30 Days'),
|
|
6
|
+
(90, '90 Days'),
|
|
7
|
+
)
|
|
8
|
+
csv_file = forms.FileField(label='FAT CSV File')
|
|
9
|
+
period_type = forms.ChoiceField(choices=PERIOD_CHOICES, label='Period Type')
|
|
10
|
+
custom_date = forms.DateTimeField(
|
|
11
|
+
required=False,
|
|
12
|
+
label='Custom Date (Optional)',
|
|
13
|
+
help_text='Leave empty to use current date and time. Use format YYYY-MM-DD HH:MM if your browser does not show a date picker.',
|
|
14
|
+
widget=forms.DateTimeInput(attrs={'type': 'datetime-local'})
|
|
15
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Generated by Django 5.2.16 on 2026-07-31 17:05
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
('eveonline', '0025_remove_evecharacter_last_updated_and_more'),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.CreateModel(
|
|
17
|
+
name='UploadPeriod',
|
|
18
|
+
fields=[
|
|
19
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
20
|
+
('upload_date', models.DateTimeField(auto_now_add=True)),
|
|
21
|
+
('uploader', models.CharField(max_length=255)),
|
|
22
|
+
('period_type', models.IntegerField(choices=[(30, '30 Days'), (90, '90 Days')])),
|
|
23
|
+
],
|
|
24
|
+
),
|
|
25
|
+
migrations.CreateModel(
|
|
26
|
+
name='FatEntry',
|
|
27
|
+
fields=[
|
|
28
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
29
|
+
('character_name', models.CharField(max_length=255)),
|
|
30
|
+
('total_fats', models.IntegerField(default=0)),
|
|
31
|
+
('fat_details', models.JSONField(blank=True, default=dict)),
|
|
32
|
+
('character', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='eveonline.evecharacter')),
|
|
33
|
+
('period', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fat_entries', to='fatanalysis.uploadperiod')),
|
|
34
|
+
],
|
|
35
|
+
),
|
|
36
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Generated by Django 5.2.15 on 2026-07-31 17:31
|
|
2
|
+
|
|
3
|
+
import django.utils.timezone
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('fatanalysis', '0001_initial'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AlterField(
|
|
15
|
+
model_name='uploadperiod',
|
|
16
|
+
name='upload_date',
|
|
17
|
+
field=models.DateTimeField(default=django.utils.timezone.now),
|
|
18
|
+
),
|
|
19
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Migrations module
|
fatanalysis/models.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from allianceauth.eveonline.models import EveCharacter
|
|
3
|
+
from django.utils import timezone
|
|
4
|
+
|
|
5
|
+
class UploadPeriod(models.Model):
|
|
6
|
+
PERIOD_CHOICES = (
|
|
7
|
+
(30, '30 Days'),
|
|
8
|
+
(90, '90 Days'),
|
|
9
|
+
)
|
|
10
|
+
upload_date = models.DateTimeField(default=timezone.now)
|
|
11
|
+
uploader = models.CharField(max_length=255)
|
|
12
|
+
period_type = models.IntegerField(choices=PERIOD_CHOICES)
|
|
13
|
+
|
|
14
|
+
def __str__(self):
|
|
15
|
+
return f"{self.get_period_type_display()} Upload ({self.upload_date.strftime('%Y-%m-%d %H:%M')})"
|
|
16
|
+
|
|
17
|
+
class FatEntry(models.Model):
|
|
18
|
+
period = models.ForeignKey(UploadPeriod, on_delete=models.CASCADE, related_name='fat_entries')
|
|
19
|
+
character = models.ForeignKey(EveCharacter, on_delete=models.SET_NULL, null=True, blank=True)
|
|
20
|
+
character_name = models.CharField(max_length=255)
|
|
21
|
+
total_fats = models.IntegerField(default=0)
|
|
22
|
+
|
|
23
|
+
# Stores dynamic fleet types (e.g. Strategic, QRF, Astartes) without hardcoding columns
|
|
24
|
+
fat_details = models.JSONField(default=dict, blank=True)
|
|
25
|
+
|
|
26
|
+
def __str__(self):
|
|
27
|
+
return f"{self.character_name} - {self.total_fats} FATs"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{% extends "allianceauth/base-bs5.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block page_title %}{% trans "FAT Analysis" %}{% endblock page_title %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
<div class="row">
|
|
8
|
+
<div class="col-md-12">
|
|
9
|
+
<h1 class="page-header text-center">{% trans "FAT Analysis Dashboard" %}</h1>
|
|
10
|
+
{% block fatanalysis_content %}{% endblock %}
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
{% endblock %}
|
|
14
|
+
|
|
15
|
+
{% block extra_css %}
|
|
16
|
+
{% include 'bundles/datatables-css-bs5.html' %}
|
|
17
|
+
{% endblock %}
|
|
18
|
+
|
|
19
|
+
{% block extra_javascript %}
|
|
20
|
+
{% include 'bundles/datatables-js-bs5.html' %}
|
|
21
|
+
{% endblock %}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{% extends "fatanalysis/base.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
{% load django_bootstrap5 %}
|
|
4
|
+
|
|
5
|
+
{% block fatanalysis_content %}
|
|
6
|
+
<div class="panel panel-default">
|
|
7
|
+
<div class="panel-heading">
|
|
8
|
+
<h3 class="panel-title">{% trans "Import FAT CSV" %}</h3>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="panel-body">
|
|
11
|
+
<p>{% trans "Upload your 30 or 90 days FAT CSV file here. The system will automatically parse all columns, including any dynamic fleet types." %}</p>
|
|
12
|
+
<form method="POST" enctype="multipart/form-data">
|
|
13
|
+
{% csrf_token %}
|
|
14
|
+
{% bootstrap_form form %}
|
|
15
|
+
<button type="submit" class="btn btn-primary">{% trans "Upload" %}</button>
|
|
16
|
+
<a href="{% url 'fatanalysis:index' %}" class="btn btn-default">{% trans "Cancel" %}</a>
|
|
17
|
+
</form>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
{% endblock %}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{% extends "fatanalysis/base.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block fatanalysis_content %}
|
|
5
|
+
<div class="row">
|
|
6
|
+
<div class="col-md-12">
|
|
7
|
+
<a href="{% url 'fatanalysis:period_list' %}" class="btn btn-default pull-right" style="margin-left: 10px;">
|
|
8
|
+
<i class="fas fa-list"></i> {% trans "Manage Periods" %}
|
|
9
|
+
</a>
|
|
10
|
+
<a href="{% url 'fatanalysis:import_csv' %}" class="btn btn-primary pull-right">
|
|
11
|
+
<i class="fas fa-upload"></i> {% trans "Import CSV" %}
|
|
12
|
+
</a>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<br>
|
|
16
|
+
|
|
17
|
+
<div class="panel panel-default">
|
|
18
|
+
<div class="panel-heading">
|
|
19
|
+
<h3 class="panel-title">{% trans "FAT Trend Over Time (Alliance Total)" %}</h3>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="panel-body">
|
|
22
|
+
<canvas id="trendChart" height="100"></canvas>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="row">
|
|
27
|
+
<div class="col-md-6">
|
|
28
|
+
<div class="panel panel-default text-center">
|
|
29
|
+
<div class="panel-body">
|
|
30
|
+
<h3>{% trans "30 Days Dashboard" %}</h3>
|
|
31
|
+
<p>{% trans "View the latest 30 days FATs per character." %}</p>
|
|
32
|
+
<a href="{% url 'fatanalysis:overview_30' %}" class="btn btn-primary btn-lg">
|
|
33
|
+
<i class="fas fa-calendar-alt"></i> {% trans "30 Days Overview" %}
|
|
34
|
+
</a>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="col-md-6">
|
|
39
|
+
<div class="panel panel-default text-center">
|
|
40
|
+
<div class="panel-body">
|
|
41
|
+
<h3>{% trans "90 Days Dashboard" %}</h3>
|
|
42
|
+
<p>{% trans "View the latest 90 days FATs per character." %}</p>
|
|
43
|
+
<a href="{% url 'fatanalysis:overview_90' %}" class="btn btn-primary btn-lg">
|
|
44
|
+
<i class="fas fa-calendar-alt"></i> {% trans "90 Days Overview" %}
|
|
45
|
+
</a>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
{% endblock %}
|
|
51
|
+
|
|
52
|
+
{% block extra_javascript %}
|
|
53
|
+
{{ block.super }}
|
|
54
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
55
|
+
<script>
|
|
56
|
+
$(document).ready(function() {
|
|
57
|
+
|
|
58
|
+
// Trend Chart
|
|
59
|
+
const trendCtx = document.getElementById('trendChart').getContext('2d');
|
|
60
|
+
new Chart(trendCtx, {
|
|
61
|
+
type: 'bar',
|
|
62
|
+
data: {
|
|
63
|
+
labels: {{ trend_labels|safe }},
|
|
64
|
+
datasets: [
|
|
65
|
+
{
|
|
66
|
+
label: '{% trans "30 Days FATs" %}',
|
|
67
|
+
data: {{ trend_30|safe }},
|
|
68
|
+
borderColor: '#36A2EB',
|
|
69
|
+
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
|
70
|
+
tension: 0.1,
|
|
71
|
+
fill: true
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
label: '{% trans "90 Days FATs" %}',
|
|
75
|
+
data: {{ trend_90|safe }},
|
|
76
|
+
borderColor: '#FF6384',
|
|
77
|
+
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
|
78
|
+
tension: 0.1,
|
|
79
|
+
fill: true
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
options: {
|
|
84
|
+
responsive: true,
|
|
85
|
+
scales: {
|
|
86
|
+
y: {
|
|
87
|
+
beginAtZero: true
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
</script>
|
|
94
|
+
{% endblock %}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
{% extends "fatanalysis/base.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block fatanalysis_content %}
|
|
5
|
+
<div class="row">
|
|
6
|
+
<div class="col-md-12">
|
|
7
|
+
<a href="{% url 'fatanalysis:index' %}" class="btn btn-default pull-right">
|
|
8
|
+
<i class="fas fa-arrow-left"></i> {% trans "Back to Dashboard" %}
|
|
9
|
+
</a>
|
|
10
|
+
<h2>{{ title }} <small>({{ period.upload_date|date:"Y-m-d"|default:"No uploads yet" }})</small></h2>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
{% if period %}
|
|
14
|
+
<div class="row">
|
|
15
|
+
<div class="col-md-12">
|
|
16
|
+
<div class="panel panel-default">
|
|
17
|
+
<div class="panel-heading">
|
|
18
|
+
<h3 class="panel-title">{% trans "Historical FAT Trend" %} ({{ period_type }} {% trans "Days" %})</h3>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="panel-body">
|
|
21
|
+
<canvas id="historicalTrendChart" height="100"></canvas>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="row">
|
|
28
|
+
<div class="col-md-4">
|
|
29
|
+
<div class="panel panel-default">
|
|
30
|
+
<div class="panel-heading">
|
|
31
|
+
<h3 class="panel-title">{% trans "Top 10 Players" %}</h3>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="panel-body">
|
|
34
|
+
<canvas id="topPlayersChart"></canvas>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="col-md-8">
|
|
39
|
+
<div class="panel panel-default">
|
|
40
|
+
<div class="panel-heading">
|
|
41
|
+
<h3 class="panel-title">{% trans "All Players" %}</h3>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="panel-body">
|
|
44
|
+
<table class="table table-striped table-hover" id="overview-table">
|
|
45
|
+
<thead>
|
|
46
|
+
<tr>
|
|
47
|
+
<th>{% trans "Character" %}</th>
|
|
48
|
+
<th>{% trans "Total FATs" %}</th>
|
|
49
|
+
{% for header in dynamic_headers %}
|
|
50
|
+
<th>{{ header }}</th>
|
|
51
|
+
{% endfor %}
|
|
52
|
+
</tr>
|
|
53
|
+
</thead>
|
|
54
|
+
<tbody>
|
|
55
|
+
{% for entry in entries %}
|
|
56
|
+
<tr>
|
|
57
|
+
<td>
|
|
58
|
+
<a href="{% url 'fatanalysis:player_detail' character_name=entry.character_name period_type=period_type %}">
|
|
59
|
+
{{ entry.character_name }}
|
|
60
|
+
</a>
|
|
61
|
+
</td>
|
|
62
|
+
<td><strong>{{ entry.total_fats }}</strong></td>
|
|
63
|
+
{% for value in entry.dynamic_values %}
|
|
64
|
+
<td>{{ value }}</td>
|
|
65
|
+
{% endfor %}
|
|
66
|
+
</tr>
|
|
67
|
+
{% empty %}
|
|
68
|
+
<tr>
|
|
69
|
+
<td colspan="{{ dynamic_headers|length|add:2 }}" class="text-center">{% trans "No FAT data available." %}</td>
|
|
70
|
+
</tr>
|
|
71
|
+
{% endfor %}
|
|
72
|
+
</tbody>
|
|
73
|
+
</table>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
{% else %}
|
|
79
|
+
<div class="alert alert-info">
|
|
80
|
+
{% trans "No uploads found for this period type yet." %}
|
|
81
|
+
</div>
|
|
82
|
+
{% endif %}
|
|
83
|
+
{% endblock %}
|
|
84
|
+
|
|
85
|
+
{% block extra_javascript %}
|
|
86
|
+
{{ block.super }}
|
|
87
|
+
{% if period %}
|
|
88
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
89
|
+
<script>
|
|
90
|
+
$(document).ready(function() {
|
|
91
|
+
$('#overview-table').DataTable({
|
|
92
|
+
"order": [[ 1, "desc" ]]
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Pie Chart for Top Players
|
|
96
|
+
const topCtx = document.getElementById('topPlayersChart').getContext('2d');
|
|
97
|
+
new Chart(topCtx, {
|
|
98
|
+
type: 'pie',
|
|
99
|
+
data: {
|
|
100
|
+
labels: {{ top_labels|safe }},
|
|
101
|
+
datasets: [{
|
|
102
|
+
data: {{ top_data|safe }},
|
|
103
|
+
backgroundColor: [
|
|
104
|
+
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
|
|
105
|
+
'#FF9F40', '#E7E9ED', '#8AC926', '#1982C4', '#6A4C93'
|
|
106
|
+
]
|
|
107
|
+
}]
|
|
108
|
+
},
|
|
109
|
+
options: {
|
|
110
|
+
responsive: true
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Trend Chart
|
|
115
|
+
const trendCtx = document.getElementById('historicalTrendChart').getContext('2d');
|
|
116
|
+
new Chart(trendCtx, {
|
|
117
|
+
type: 'line',
|
|
118
|
+
data: {
|
|
119
|
+
labels: {{ trend_labels|safe }},
|
|
120
|
+
datasets: [{
|
|
121
|
+
label: '{{ period_type }} {% trans "Days FATs" %}',
|
|
122
|
+
data: {{ trend_data|safe }},
|
|
123
|
+
borderColor: '#36A2EB',
|
|
124
|
+
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
|
125
|
+
tension: 0.1,
|
|
126
|
+
fill: true
|
|
127
|
+
}]
|
|
128
|
+
},
|
|
129
|
+
options: {
|
|
130
|
+
responsive: true,
|
|
131
|
+
scales: {
|
|
132
|
+
y: {
|
|
133
|
+
beginAtZero: true
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
</script>
|
|
140
|
+
{% endif %}
|
|
141
|
+
{% endblock %}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{% extends "fatanalysis/base.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block fatanalysis_content %}
|
|
5
|
+
<div class="row">
|
|
6
|
+
<div class="col-md-6 col-md-offset-3">
|
|
7
|
+
<div class="panel panel-danger">
|
|
8
|
+
<div class="panel-heading">
|
|
9
|
+
<h3 class="panel-title">{% trans "Delete Upload Period" %}</h3>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="panel-body">
|
|
12
|
+
<p>{% trans "Are you sure you want to delete the FAT upload for" %} <strong>{{ period.get_period_type_display }}</strong> {% trans "on" %} <strong>{{ period.upload_date|date:"Y-m-d H:i" }}</strong>?</p>
|
|
13
|
+
<p class="text-danger">{% trans "This will also delete all associated FAT entries and data. This action cannot be undone." %}</p>
|
|
14
|
+
|
|
15
|
+
<form method="POST">
|
|
16
|
+
{% csrf_token %}
|
|
17
|
+
<button type="submit" class="btn btn-danger">{% trans "Yes, Delete" %}</button>
|
|
18
|
+
<a href="{% url 'fatanalysis:period_list' %}" class="btn btn-default">{% trans "Cancel" %}</a>
|
|
19
|
+
</form>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
{% endblock %}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{% extends "fatanalysis/base.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block fatanalysis_content %}
|
|
5
|
+
<div class="row">
|
|
6
|
+
<div class="col-md-12">
|
|
7
|
+
<a href="{% url 'fatanalysis:index' %}" class="btn btn-default pull-right">
|
|
8
|
+
<i class="fas fa-arrow-left"></i> {% trans "Back to Overview" %}
|
|
9
|
+
</a>
|
|
10
|
+
<h2>{% trans "Period Details" %} - {{ period.get_period_type_display }} ({{ period.upload_date|date:"Y-m-d" }})</h2>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
<br>
|
|
14
|
+
|
|
15
|
+
<div class="row">
|
|
16
|
+
<div class="col-md-12">
|
|
17
|
+
<div class="panel panel-default">
|
|
18
|
+
<div class="panel-heading">
|
|
19
|
+
<h3 class="panel-title">{% trans "Top 20 Players" %}</h3>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="panel-body">
|
|
22
|
+
<canvas id="playerChart" width="400" height="100"></canvas>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="panel panel-default">
|
|
29
|
+
<div class="panel-heading">
|
|
30
|
+
<h3 class="panel-title">{% trans "All Entries" %}</h3>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="panel-body">
|
|
33
|
+
<table class="table table-striped table-hover" id="entries-table">
|
|
34
|
+
<thead>
|
|
35
|
+
<tr>
|
|
36
|
+
<th>{% trans "Character" %}</th>
|
|
37
|
+
<th>{% trans "Total FATs" %}</th>
|
|
38
|
+
{% for header in dynamic_headers %}
|
|
39
|
+
<th>{{ header }}</th>
|
|
40
|
+
{% endfor %}
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody>
|
|
44
|
+
{% for entry in entries %}
|
|
45
|
+
<tr>
|
|
46
|
+
<td>{{ entry.character_name }}</td>
|
|
47
|
+
<td>{{ entry.total_fats }}</td>
|
|
48
|
+
{% for val in entry.dynamic_values %}
|
|
49
|
+
<td>{{ val }}</td>
|
|
50
|
+
{% endfor %}
|
|
51
|
+
</tr>
|
|
52
|
+
{% endfor %}
|
|
53
|
+
</tbody>
|
|
54
|
+
</table>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
{% endblock %}
|
|
58
|
+
|
|
59
|
+
{% block extra_javascript %}
|
|
60
|
+
{{ block.super }}
|
|
61
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
62
|
+
<script>
|
|
63
|
+
$(document).ready(function() {
|
|
64
|
+
$('#entries-table').DataTable({
|
|
65
|
+
"order": [[ 1, "desc" ]]
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const playerCtx = document.getElementById('playerChart').getContext('2d');
|
|
69
|
+
|
|
70
|
+
new Chart(playerCtx, {
|
|
71
|
+
type: 'bar',
|
|
72
|
+
data: {
|
|
73
|
+
labels: {{ player_labels|safe }},
|
|
74
|
+
datasets: [{
|
|
75
|
+
label: '{% trans "FATs" %}',
|
|
76
|
+
data: {{ player_values|safe }},
|
|
77
|
+
backgroundColor: '#36A2EB'
|
|
78
|
+
}]
|
|
79
|
+
},
|
|
80
|
+
options: {
|
|
81
|
+
scales: {
|
|
82
|
+
y: {
|
|
83
|
+
beginAtZero: true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
</script>
|
|
90
|
+
{% endblock %}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{% extends "fatanalysis/base.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block fatanalysis_content %}
|
|
5
|
+
<div class="row">
|
|
6
|
+
<div class="col-md-12">
|
|
7
|
+
<a href="{% url 'fatanalysis:index' %}" class="btn btn-default pull-right">
|
|
8
|
+
<i class="fas fa-arrow-left"></i> {% trans "Back to Overview" %}
|
|
9
|
+
</a>
|
|
10
|
+
<h2>{% trans "Manage Uploaded Periods" %}</h2>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
<br>
|
|
14
|
+
|
|
15
|
+
<div class="panel panel-default">
|
|
16
|
+
<div class="panel-heading">
|
|
17
|
+
<h3 class="panel-title">{% trans "Uploaded Periods" %}</h3>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="panel-body">
|
|
20
|
+
<table class="table table-striped table-hover" id="periods-table">
|
|
21
|
+
<thead>
|
|
22
|
+
<tr>
|
|
23
|
+
<th>{% trans "Date" %}</th>
|
|
24
|
+
<th>{% trans "Type" %}</th>
|
|
25
|
+
<th>{% trans "Uploaded By" %}</th>
|
|
26
|
+
<th>{% trans "FAT Entries" %}</th>
|
|
27
|
+
<th>{% trans "Actions" %}</th>
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody>
|
|
31
|
+
{% for period in periods %}
|
|
32
|
+
<tr>
|
|
33
|
+
<td>{{ period.upload_date|date:"Y-m-d H:i" }}</td>
|
|
34
|
+
<td>{{ period.get_period_type_display }}</td>
|
|
35
|
+
<td>{{ period.uploader }}</td>
|
|
36
|
+
<td>{{ period.fat_entries.count }}</td>
|
|
37
|
+
<td>
|
|
38
|
+
<a href="{% url 'fatanalysis:period_detail' period.id %}" class="btn btn-info btn-xs">
|
|
39
|
+
<i class="fas fa-eye"></i> {% trans "View" %}
|
|
40
|
+
</a>
|
|
41
|
+
<a href="{% url 'fatanalysis:period_delete' period.id %}" class="btn btn-danger btn-xs">
|
|
42
|
+
<i class="fas fa-trash"></i> {% trans "Delete" %}
|
|
43
|
+
</a>
|
|
44
|
+
</td>
|
|
45
|
+
</tr>
|
|
46
|
+
{% empty %}
|
|
47
|
+
<tr>
|
|
48
|
+
<td colspan="5" class="text-center">{% trans "No periods uploaded yet." %}</td>
|
|
49
|
+
</tr>
|
|
50
|
+
{% endfor %}
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
{% endblock %}
|
|
56
|
+
|
|
57
|
+
{% block extra_javascript %}
|
|
58
|
+
{{ block.super }}
|
|
59
|
+
<script>
|
|
60
|
+
$(document).ready(function() {
|
|
61
|
+
$('#periods-table').DataTable({
|
|
62
|
+
"order": [[ 0, "desc" ]]
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
</script>
|
|
66
|
+
{% endblock %}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
{% extends "fatanalysis/base.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
|
|
4
|
+
{% block fatanalysis_content %}
|
|
5
|
+
<div class="row">
|
|
6
|
+
<div class="col-md-12">
|
|
7
|
+
<h2>{{ title }} <small>({{ period_type }} {% trans "Days Data" %})</small></h2>
|
|
8
|
+
<a href="javascript:history.back()" class="btn btn-default pull-right"><i class="fas fa-arrow-left"></i> {% trans "Back" %}</a>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
<br>
|
|
12
|
+
|
|
13
|
+
<div class="row">
|
|
14
|
+
<div class="col-md-12">
|
|
15
|
+
<div class="panel panel-default">
|
|
16
|
+
<div class="panel-heading">
|
|
17
|
+
<h3 class="panel-title">{% trans "Historical FAT Trend per Type" %}</h3>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="panel-body">
|
|
20
|
+
<canvas id="playerTrendChart" height="100"></canvas>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="row">
|
|
27
|
+
<div class="col-md-4">
|
|
28
|
+
<div class="panel panel-default">
|
|
29
|
+
<div class="panel-heading">
|
|
30
|
+
<h3 class="panel-title">{% trans "Current FAT Breakdown" %} <small>({{ latest_period.upload_date|date:"Y-m-d" }})</small></h3>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="panel-body">
|
|
33
|
+
{% if pie_data %}
|
|
34
|
+
<canvas id="playerPieChart"></canvas>
|
|
35
|
+
{% else %}
|
|
36
|
+
<p class="text-center text-muted">{% trans "No FATs in the latest period." %}</p>
|
|
37
|
+
{% endif %}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="col-md-8">
|
|
42
|
+
<div class="panel panel-default">
|
|
43
|
+
<div class="panel-heading">
|
|
44
|
+
<h3 class="panel-title">{% trans "Historical Raw Data" %}</h3>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="panel-body">
|
|
47
|
+
<table class="table table-striped table-hover" id="player-table">
|
|
48
|
+
<thead>
|
|
49
|
+
<tr>
|
|
50
|
+
<th>{% trans "Upload Date" %}</th>
|
|
51
|
+
<th>{% trans "Total FATs" %}</th>
|
|
52
|
+
{% for header in dynamic_headers %}
|
|
53
|
+
<th>{{ header }}</th>
|
|
54
|
+
{% endfor %}
|
|
55
|
+
</tr>
|
|
56
|
+
</thead>
|
|
57
|
+
<tbody>
|
|
58
|
+
{% for entry in entries reversed %}
|
|
59
|
+
<tr>
|
|
60
|
+
<td>{{ entry.period.upload_date|date:"Y-m-d H:i" }}</td>
|
|
61
|
+
<td><strong>{{ entry.total_fats }}</strong></td>
|
|
62
|
+
{% for header in dynamic_headers %}
|
|
63
|
+
{% for key, val in entry.fat_details.items %}
|
|
64
|
+
{% if key == header %}
|
|
65
|
+
<td>{{ val }}</td>
|
|
66
|
+
{% endif %}
|
|
67
|
+
{% endfor %}
|
|
68
|
+
{% if header not in entry.fat_details %}
|
|
69
|
+
<td>0</td>
|
|
70
|
+
{% endif %}
|
|
71
|
+
{% endfor %}
|
|
72
|
+
</tr>
|
|
73
|
+
{% empty %}
|
|
74
|
+
<tr>
|
|
75
|
+
<td colspan="{{ dynamic_headers|length|add:2 }}" class="text-center">{% trans "No FAT data available." %}</td>
|
|
76
|
+
</tr>
|
|
77
|
+
{% endfor %}
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
{% endblock %}
|
|
85
|
+
|
|
86
|
+
{% block extra_javascript %}
|
|
87
|
+
{{ block.super }}
|
|
88
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
89
|
+
<script>
|
|
90
|
+
$(document).ready(function() {
|
|
91
|
+
$('#player-table').DataTable({
|
|
92
|
+
"order": [[ 0, "desc" ]]
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Trend Chart
|
|
96
|
+
const trendCtx = document.getElementById('playerTrendChart').getContext('2d');
|
|
97
|
+
new Chart(trendCtx, {
|
|
98
|
+
type: 'line',
|
|
99
|
+
data: {
|
|
100
|
+
labels: {{ trend_labels|safe }},
|
|
101
|
+
datasets: {{ trend_datasets|safe }}
|
|
102
|
+
},
|
|
103
|
+
options: {
|
|
104
|
+
responsive: true,
|
|
105
|
+
interaction: {
|
|
106
|
+
mode: 'index',
|
|
107
|
+
intersect: false,
|
|
108
|
+
},
|
|
109
|
+
scales: {
|
|
110
|
+
y: {
|
|
111
|
+
beginAtZero: true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Pie Chart
|
|
118
|
+
{% if pie_data %}
|
|
119
|
+
const pieCtx = document.getElementById('playerPieChart').getContext('2d');
|
|
120
|
+
new Chart(pieCtx, {
|
|
121
|
+
type: 'pie',
|
|
122
|
+
data: {
|
|
123
|
+
labels: {{ pie_labels|safe }},
|
|
124
|
+
datasets: [{
|
|
125
|
+
data: {{ pie_data|safe }},
|
|
126
|
+
backgroundColor: [
|
|
127
|
+
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
|
|
128
|
+
'#FF9F40', '#E7E9ED', '#8AC926', '#1982C4', '#6A4C93'
|
|
129
|
+
]
|
|
130
|
+
}]
|
|
131
|
+
},
|
|
132
|
+
options: {
|
|
133
|
+
responsive: true
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
{% endif %}
|
|
137
|
+
});
|
|
138
|
+
</script>
|
|
139
|
+
{% endblock %}
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
INSTALLED_APPS = [
|
|
2
|
+
'django.contrib.auth',
|
|
3
|
+
'django.contrib.contenttypes',
|
|
4
|
+
'esi',
|
|
5
|
+
'eveuniverse',
|
|
6
|
+
'allianceauth',
|
|
7
|
+
'allianceauth.authentication',
|
|
8
|
+
'allianceauth.eveonline',
|
|
9
|
+
'fatanalysis',
|
|
10
|
+
]
|
|
11
|
+
DATABASES = {
|
|
12
|
+
'default': {
|
|
13
|
+
'ENGINE': 'django.db.backends.sqlite3',
|
|
14
|
+
'NAME': 'db.sqlite3',
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
SECRET_KEY = 'dummy'
|
|
18
|
+
CACHES = {
|
|
19
|
+
'default': {
|
|
20
|
+
'BACKEND': 'django_redis.cache.RedisCache',
|
|
21
|
+
'LOCATION': 'redis://127.0.0.1:6379/1',
|
|
22
|
+
'OPTIONS': {
|
|
23
|
+
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
fatanalysis/urls.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django.urls import path
|
|
2
|
+
from . import views
|
|
3
|
+
|
|
4
|
+
app_name = 'fatanalysis'
|
|
5
|
+
|
|
6
|
+
urlpatterns = [
|
|
7
|
+
path('', views.index, name='index'),
|
|
8
|
+
path('overview/30/', views.overview_30, name='overview_30'),
|
|
9
|
+
path('overview/90/', views.overview_90, name='overview_90'),
|
|
10
|
+
path('player/<str:character_name>/<int:period_type>/', views.player_detail, name='player_detail'),
|
|
11
|
+
path('import/', views.import_csv, name='import_csv'),
|
|
12
|
+
path('periods/', views.period_list, name='period_list'),
|
|
13
|
+
path('period/<int:period_id>/', views.period_detail, name='period_detail'),
|
|
14
|
+
path('period/<int:period_id>/delete/', views.period_delete, name='period_delete'),
|
|
15
|
+
]
|
fatanalysis/views.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
from django.shortcuts import render, redirect
|
|
3
|
+
from django.contrib.auth.decorators import login_required, permission_required
|
|
4
|
+
from django.contrib import messages
|
|
5
|
+
from django.db import transaction
|
|
6
|
+
from django.db.models import Sum
|
|
7
|
+
from django.utils.translation import gettext_lazy as _
|
|
8
|
+
|
|
9
|
+
from allianceauth.eveonline.models import EveCharacter
|
|
10
|
+
from .forms import CsvImportForm
|
|
11
|
+
from .models import UploadPeriod, FatEntry
|
|
12
|
+
|
|
13
|
+
@login_required
|
|
14
|
+
@permission_required('fatanalysis.basic_access')
|
|
15
|
+
def index(request):
|
|
16
|
+
from django.db.models import Sum
|
|
17
|
+
|
|
18
|
+
# Data for the Trend Chart
|
|
19
|
+
all_periods = UploadPeriod.objects.all().order_by('upload_date')
|
|
20
|
+
trend_data = {}
|
|
21
|
+
|
|
22
|
+
for period in all_periods:
|
|
23
|
+
# Use period ID in the key to ensure uniqueness if multiple uploads happen on the same day
|
|
24
|
+
date_str = f"{period.upload_date.strftime('%Y-%m-%d')} (ID:{period.id})"
|
|
25
|
+
if date_str not in trend_data:
|
|
26
|
+
trend_data[date_str] = {30: 0, 90: 0}
|
|
27
|
+
|
|
28
|
+
# Get total fats for this period
|
|
29
|
+
total_fats = FatEntry.objects.filter(period=period).aggregate(Sum('total_fats'))['total_fats__sum'] or 0
|
|
30
|
+
|
|
31
|
+
trend_data[date_str][period.period_type] += total_fats
|
|
32
|
+
|
|
33
|
+
trend_labels = list(trend_data.keys())
|
|
34
|
+
trend_30 = [trend_data[d][30] for d in trend_labels]
|
|
35
|
+
trend_90 = [trend_data[d][90] for d in trend_labels]
|
|
36
|
+
|
|
37
|
+
# Data for the 30-Day and 90-Day tables (Removed from index)
|
|
38
|
+
|
|
39
|
+
context = {
|
|
40
|
+
'trend_labels': trend_labels,
|
|
41
|
+
'trend_30': trend_30,
|
|
42
|
+
'trend_90': trend_90,
|
|
43
|
+
}
|
|
44
|
+
return render(request, 'fatanalysis/index.html', context)
|
|
45
|
+
|
|
46
|
+
@login_required
|
|
47
|
+
@permission_required('fatanalysis.basic_access')
|
|
48
|
+
def overview_30(request):
|
|
49
|
+
latest_30 = UploadPeriod.objects.filter(period_type=30).order_by('-upload_date').first()
|
|
50
|
+
entries = FatEntry.objects.filter(period=latest_30) if latest_30 else []
|
|
51
|
+
|
|
52
|
+
player_data = []
|
|
53
|
+
dynamic_headers_set = set()
|
|
54
|
+
if entries:
|
|
55
|
+
for entry in entries:
|
|
56
|
+
dynamic_headers_set.update(entry.fat_details.keys())
|
|
57
|
+
|
|
58
|
+
dynamic_headers = sorted(list(dynamic_headers_set))
|
|
59
|
+
|
|
60
|
+
enriched_entries = []
|
|
61
|
+
if entries:
|
|
62
|
+
for entry in entries:
|
|
63
|
+
player_data.append({'name': entry.character_name, 'fats': entry.total_fats})
|
|
64
|
+
|
|
65
|
+
row_details = []
|
|
66
|
+
for header in dynamic_headers:
|
|
67
|
+
row_details.append(entry.fat_details.get(header, 0))
|
|
68
|
+
|
|
69
|
+
entry.dynamic_values = row_details
|
|
70
|
+
enriched_entries.append(entry)
|
|
71
|
+
|
|
72
|
+
player_data = sorted(player_data, key=lambda x: x['fats'], reverse=True)[:10]
|
|
73
|
+
|
|
74
|
+
all_30 = UploadPeriod.objects.filter(period_type=30).order_by('upload_date')
|
|
75
|
+
trend_labels = []
|
|
76
|
+
trend_data = []
|
|
77
|
+
for p in all_30:
|
|
78
|
+
trend_labels.append(f"{p.upload_date.strftime('%Y-%m-%d')} (ID:{p.id})")
|
|
79
|
+
total = FatEntry.objects.filter(period=p).aggregate(Sum('total_fats'))['total_fats__sum'] or 0
|
|
80
|
+
trend_data.append(total)
|
|
81
|
+
|
|
82
|
+
context = {
|
|
83
|
+
'title': _('Latest 30 Days FATs Overview'),
|
|
84
|
+
'period': latest_30,
|
|
85
|
+
'entries': enriched_entries,
|
|
86
|
+
'dynamic_headers': dynamic_headers,
|
|
87
|
+
'top_labels': [p['name'] for p in player_data],
|
|
88
|
+
'top_data': [p['fats'] for p in player_data],
|
|
89
|
+
'trend_labels': trend_labels,
|
|
90
|
+
'trend_data': trend_data,
|
|
91
|
+
'period_type': 30,
|
|
92
|
+
}
|
|
93
|
+
return render(request, 'fatanalysis/overview.html', context)
|
|
94
|
+
|
|
95
|
+
@login_required
|
|
96
|
+
@permission_required('fatanalysis.basic_access')
|
|
97
|
+
def overview_90(request):
|
|
98
|
+
latest_90 = UploadPeriod.objects.filter(period_type=90).order_by('-upload_date').first()
|
|
99
|
+
entries = FatEntry.objects.filter(period=latest_90) if latest_90 else []
|
|
100
|
+
|
|
101
|
+
player_data = []
|
|
102
|
+
dynamic_headers_set = set()
|
|
103
|
+
if entries:
|
|
104
|
+
for entry in entries:
|
|
105
|
+
dynamic_headers_set.update(entry.fat_details.keys())
|
|
106
|
+
|
|
107
|
+
dynamic_headers = sorted(list(dynamic_headers_set))
|
|
108
|
+
|
|
109
|
+
enriched_entries = []
|
|
110
|
+
if entries:
|
|
111
|
+
for entry in entries:
|
|
112
|
+
player_data.append({'name': entry.character_name, 'fats': entry.total_fats})
|
|
113
|
+
|
|
114
|
+
row_details = []
|
|
115
|
+
for header in dynamic_headers:
|
|
116
|
+
row_details.append(entry.fat_details.get(header, 0))
|
|
117
|
+
|
|
118
|
+
entry.dynamic_values = row_details
|
|
119
|
+
enriched_entries.append(entry)
|
|
120
|
+
|
|
121
|
+
player_data = sorted(player_data, key=lambda x: x['fats'], reverse=True)[:10]
|
|
122
|
+
|
|
123
|
+
all_90 = UploadPeriod.objects.filter(period_type=90).order_by('upload_date')
|
|
124
|
+
trend_labels = []
|
|
125
|
+
trend_data = []
|
|
126
|
+
for p in all_90:
|
|
127
|
+
trend_labels.append(f"{p.upload_date.strftime('%Y-%m-%d')} (ID:{p.id})")
|
|
128
|
+
total = FatEntry.objects.filter(period=p).aggregate(Sum('total_fats'))['total_fats__sum'] or 0
|
|
129
|
+
trend_data.append(total)
|
|
130
|
+
|
|
131
|
+
context = {
|
|
132
|
+
'title': _('Latest 90 Days FATs Overview'),
|
|
133
|
+
'period': latest_90,
|
|
134
|
+
'entries': enriched_entries,
|
|
135
|
+
'dynamic_headers': dynamic_headers,
|
|
136
|
+
'top_labels': [p['name'] for p in player_data],
|
|
137
|
+
'top_data': [p['fats'] for p in player_data],
|
|
138
|
+
'trend_labels': trend_labels,
|
|
139
|
+
'trend_data': trend_data,
|
|
140
|
+
'period_type': 90,
|
|
141
|
+
}
|
|
142
|
+
return render(request, 'fatanalysis/overview.html', context)
|
|
143
|
+
|
|
144
|
+
@login_required
|
|
145
|
+
@permission_required('fatanalysis.basic_access')
|
|
146
|
+
def import_csv(request):
|
|
147
|
+
if request.method == 'POST':
|
|
148
|
+
form = CsvImportForm(request.POST, request.FILES)
|
|
149
|
+
if form.is_valid():
|
|
150
|
+
csv_file = request.FILES['csv_file']
|
|
151
|
+
period_type = int(form.cleaned_data['period_type'])
|
|
152
|
+
|
|
153
|
+
if not csv_file.name.endswith('.csv'):
|
|
154
|
+
messages.error(request, _("Please upload a valid CSV file."))
|
|
155
|
+
return redirect('fatanalysis:import_csv')
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
decoded_file = csv_file.read().decode('utf-8').splitlines()
|
|
159
|
+
reader = csv.DictReader(decoded_file)
|
|
160
|
+
|
|
161
|
+
with transaction.atomic():
|
|
162
|
+
# Create Upload Period
|
|
163
|
+
kwargs = {
|
|
164
|
+
'uploader': request.user.username,
|
|
165
|
+
'period_type': period_type
|
|
166
|
+
}
|
|
167
|
+
if form.cleaned_data.get('custom_date'):
|
|
168
|
+
kwargs['upload_date'] = form.cleaned_data['custom_date']
|
|
169
|
+
upload_period = UploadPeriod.objects.create(**kwargs)
|
|
170
|
+
|
|
171
|
+
entries_to_create = []
|
|
172
|
+
|
|
173
|
+
for row in reader:
|
|
174
|
+
char_name = row.get('Main Character', '').strip()
|
|
175
|
+
if not char_name:
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
# Try to find character in DB
|
|
179
|
+
eve_char = EveCharacter.objects.filter(character_name=char_name).first()
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
total_fats = int(row.get('Total FATs', 0))
|
|
183
|
+
except ValueError:
|
|
184
|
+
total_fats = 0
|
|
185
|
+
|
|
186
|
+
# Get all other columns as details
|
|
187
|
+
details = {}
|
|
188
|
+
standard_keys = ['Main Character', 'Total FATs']
|
|
189
|
+
for key, value in row.items():
|
|
190
|
+
if key not in standard_keys:
|
|
191
|
+
try:
|
|
192
|
+
details[key] = int(value) if value else 0
|
|
193
|
+
except ValueError:
|
|
194
|
+
details[key] = value
|
|
195
|
+
|
|
196
|
+
entries_to_create.append(
|
|
197
|
+
FatEntry(
|
|
198
|
+
period=upload_period,
|
|
199
|
+
character=eve_char,
|
|
200
|
+
character_name=char_name,
|
|
201
|
+
total_fats=total_fats,
|
|
202
|
+
fat_details=details
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
FatEntry.objects.bulk_create(entries_to_create)
|
|
207
|
+
messages.success(request, _(f"Successfully imported {len(entries_to_create)} FAT entries."))
|
|
208
|
+
return redirect('fatanalysis:period_detail', period_id=upload_period.id)
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
messages.error(request, _(f"Error parsing CSV: {e}"))
|
|
212
|
+
return redirect('fatanalysis:import_csv')
|
|
213
|
+
|
|
214
|
+
else:
|
|
215
|
+
form = CsvImportForm()
|
|
216
|
+
|
|
217
|
+
return render(request, 'fatanalysis/import_csv.html', {'form': form})
|
|
218
|
+
|
|
219
|
+
@login_required
|
|
220
|
+
@permission_required('fatanalysis.basic_access')
|
|
221
|
+
def period_detail(request, period_id):
|
|
222
|
+
from django.shortcuts import get_object_or_404
|
|
223
|
+
from django.db.models import Sum
|
|
224
|
+
|
|
225
|
+
period = get_object_or_404(UploadPeriod, id=period_id)
|
|
226
|
+
entries = FatEntry.objects.filter(period=period).select_related('character')
|
|
227
|
+
|
|
228
|
+
# Aggregations for charts
|
|
229
|
+
player_data = []
|
|
230
|
+
|
|
231
|
+
# Collect dynamic headers
|
|
232
|
+
dynamic_headers_set = set()
|
|
233
|
+
for entry in entries:
|
|
234
|
+
dynamic_headers_set.update(entry.fat_details.keys())
|
|
235
|
+
|
|
236
|
+
# Sort headers to ensure consistent column ordering
|
|
237
|
+
dynamic_headers = sorted(list(dynamic_headers_set))
|
|
238
|
+
|
|
239
|
+
# Create enriched entries for template rendering
|
|
240
|
+
enriched_entries = []
|
|
241
|
+
for entry in entries:
|
|
242
|
+
player_data.append({
|
|
243
|
+
'name': entry.character_name,
|
|
244
|
+
'fats': entry.total_fats
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
row_details = []
|
|
248
|
+
for header in dynamic_headers:
|
|
249
|
+
row_details.append(entry.fat_details.get(header, 0))
|
|
250
|
+
|
|
251
|
+
enriched_entries.append({
|
|
252
|
+
'character_name': entry.character_name,
|
|
253
|
+
'total_fats': entry.total_fats,
|
|
254
|
+
'dynamic_values': row_details
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
# Sort for charts
|
|
258
|
+
player_data = sorted(player_data, key=lambda x: x['fats'], reverse=True)[:20] # Top 20 players
|
|
259
|
+
|
|
260
|
+
player_labels = [p['name'] for p in player_data]
|
|
261
|
+
player_values = [p['fats'] for p in player_data]
|
|
262
|
+
|
|
263
|
+
context = {
|
|
264
|
+
'period': period,
|
|
265
|
+
'entries': enriched_entries,
|
|
266
|
+
'dynamic_headers': dynamic_headers,
|
|
267
|
+
'player_labels': player_labels,
|
|
268
|
+
'player_values': player_values,
|
|
269
|
+
}
|
|
270
|
+
return render(request, 'fatanalysis/period_detail.html', context)
|
|
271
|
+
|
|
272
|
+
@login_required
|
|
273
|
+
@permission_required('fatanalysis.basic_access')
|
|
274
|
+
def period_list(request):
|
|
275
|
+
periods = UploadPeriod.objects.all().order_by('-upload_date')
|
|
276
|
+
return render(request, 'fatanalysis/period_list.html', {'periods': periods})
|
|
277
|
+
|
|
278
|
+
@login_required
|
|
279
|
+
@permission_required('fatanalysis.basic_access')
|
|
280
|
+
def period_delete(request, period_id):
|
|
281
|
+
from django.shortcuts import get_object_or_404
|
|
282
|
+
period = get_object_or_404(UploadPeriod, id=period_id)
|
|
283
|
+
if request.method == 'POST':
|
|
284
|
+
period.delete()
|
|
285
|
+
messages.success(request, _("Upload period and all associated data deleted successfully."))
|
|
286
|
+
return redirect('fatanalysis:period_list')
|
|
287
|
+
return render(request, 'fatanalysis/period_delete.html', {'period': period})
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@login_required
|
|
291
|
+
@permission_required('fatanalysis.basic_access')
|
|
292
|
+
def player_detail(request, character_name, period_type):
|
|
293
|
+
from collections import defaultdict
|
|
294
|
+
# Get all entries for this character and period type, chronologically
|
|
295
|
+
periods = UploadPeriod.objects.filter(period_type=period_type).order_by('upload_date')
|
|
296
|
+
entries = FatEntry.objects.filter(character_name=character_name, period__period_type=period_type).select_related('period').order_by('period__upload_date')
|
|
297
|
+
|
|
298
|
+
if not entries:
|
|
299
|
+
messages.warning(request, _(f"No data found for {character_name}."))
|
|
300
|
+
return redirect('fatanalysis:index')
|
|
301
|
+
|
|
302
|
+
dynamic_headers_set = set()
|
|
303
|
+
for entry in entries:
|
|
304
|
+
dynamic_headers_set.update(entry.fat_details.keys())
|
|
305
|
+
dynamic_headers = sorted(list(dynamic_headers_set))
|
|
306
|
+
|
|
307
|
+
# Prepare Line Chart Data (Trend per FAT type)
|
|
308
|
+
trend_labels = []
|
|
309
|
+
trend_datasets_dict = defaultdict(list)
|
|
310
|
+
total_fats_trend = []
|
|
311
|
+
|
|
312
|
+
# We want a data point for ALL periods of this type, even if the player had 0
|
|
313
|
+
# Create a lookup for player entries
|
|
314
|
+
entry_lookup = {e.period.id: e for e in entries}
|
|
315
|
+
|
|
316
|
+
for p in periods:
|
|
317
|
+
trend_labels.append(f"{p.upload_date.strftime('%Y-%m-%d')} (ID:{p.id})")
|
|
318
|
+
|
|
319
|
+
entry = entry_lookup.get(p.id)
|
|
320
|
+
if entry:
|
|
321
|
+
total_fats_trend.append(entry.total_fats)
|
|
322
|
+
for header in dynamic_headers:
|
|
323
|
+
trend_datasets_dict[header].append(entry.fat_details.get(header, 0))
|
|
324
|
+
else:
|
|
325
|
+
total_fats_trend.append(0)
|
|
326
|
+
for header in dynamic_headers:
|
|
327
|
+
trend_datasets_dict[header].append(0)
|
|
328
|
+
|
|
329
|
+
# Convert datasets to Chart.js format
|
|
330
|
+
trend_datasets = []
|
|
331
|
+
colors = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#E7E9ED', '#8AC926', '#1982C4', '#6A4C93']
|
|
332
|
+
for i, header in enumerate(dynamic_headers):
|
|
333
|
+
trend_datasets.append({
|
|
334
|
+
'label': header,
|
|
335
|
+
'data': trend_datasets_dict[header],
|
|
336
|
+
'borderColor': colors[i % len(colors)],
|
|
337
|
+
'tension': 0.1,
|
|
338
|
+
'fill': False,
|
|
339
|
+
'hidden': False
|
|
340
|
+
})
|
|
341
|
+
# Add Total line
|
|
342
|
+
trend_datasets.append({
|
|
343
|
+
'label': 'Total FATs',
|
|
344
|
+
'data': total_fats_trend,
|
|
345
|
+
'borderColor': '#000000',
|
|
346
|
+
'borderDash': [5, 5],
|
|
347
|
+
'tension': 0.1,
|
|
348
|
+
'fill': False
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
# Prepare Pie Chart Data (Latest Period Distribution)
|
|
352
|
+
latest_entry = entries.last()
|
|
353
|
+
pie_labels = []
|
|
354
|
+
pie_data = []
|
|
355
|
+
if latest_entry:
|
|
356
|
+
for header in dynamic_headers:
|
|
357
|
+
val = latest_entry.fat_details.get(header, 0)
|
|
358
|
+
if val > 0:
|
|
359
|
+
pie_labels.append(header)
|
|
360
|
+
pie_data.append(val)
|
|
361
|
+
|
|
362
|
+
context = {
|
|
363
|
+
'title': _(f"Player Detail: {character_name}"),
|
|
364
|
+
'character_name': character_name,
|
|
365
|
+
'period_type': period_type,
|
|
366
|
+
'entries': entries,
|
|
367
|
+
'dynamic_headers': dynamic_headers,
|
|
368
|
+
'trend_labels': trend_labels,
|
|
369
|
+
'trend_datasets': trend_datasets,
|
|
370
|
+
'pie_labels': pie_labels,
|
|
371
|
+
'pie_data': pie_data,
|
|
372
|
+
'latest_period': latest_entry.period if latest_entry else None,
|
|
373
|
+
}
|
|
374
|
+
return render(request, 'fatanalysis/player_detail.html', context)
|