Joey Hines 2018-12-08 22:10:12 -06:00
commit 938ec65422
13 changed files with 275 additions and 184 deletions

View File

@ -5,6 +5,7 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from minecraft_manager.models import Application, Warning, Ticket, Player, IP, UserSettings, Alert, Note
from minecraft_manager.api.admin import register as api_register
class PlayerInline(admin.StackedInline):
@ -100,6 +101,7 @@ try:
admin.site.register(IP, IPAdmin)
admin.site.register(Alert)
admin.site.register(Note)
api_register()
except admin.sites.AlreadyRegistered:
pass

45
api/admin.py 100644
View File

@ -0,0 +1,45 @@
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from minecraft_manager.api.models import Token
class TokenActiveFilter(admin.SimpleListFilter):
title = _('Active')
parameter_name = 'active'
def lookups(self, request, model_admin):
return (
('0', _('Active')),
('1', _('Inactive')),
)
def queryset(self, request, queryset):
if self.value() == '0':
return queryset.filter(active=True)
if self.value() == '1':
return queryset.filter(active=False)
class TokenAdmin(admin.ModelAdmin):
list_filter = (TokenActiveFilter,)
list_display = ('display', 'active', 'web_get_permission', 'web_post_permission', 'plugin_get_permission',
'plugin_post_permission', 'form_get_permission', 'form_post_permission', 'model_get_permission',
'model_post_permission', 'stats_get_permission', 'stats_post_permission')
fieldsets = (
(None, {
'fields': ('key', 'active', 'description')
}),
('Permissions', {
'fields': ('web_get_permission', 'web_post_permission', 'plugin_get_permission', 'plugin_post_permission',
'form_get_permission', 'form_post_permission', 'model_get_permission', 'model_post_permission',
'stats_get_permission', 'stats_post_permission')
})
)
def register():
try:
admin.site.register(Token, TokenAdmin)
except admin.sites.AlreadyRegistered:
pass

View File

@ -1,4 +1,4 @@
import socket, requests, logging, os, datetime, pytz, mcstatus, discord
import socket, requests, logging, os, datetime, pytz, mcstatus, random, string
from minecraft_manager.models import Alert
from django.contrib.auth.models import User
from django.conf import settings
@ -18,6 +18,7 @@ PLUGIN_ACCEPT = 'accept'
PLUGIN_DENY = 'deny'
PLUGIN_GLOBAL_CHAT = 'global'
PLUGIN_STAFF_CHAT = 'staff'
PLUGIN_DEMOTE = 'demote'
def plugin(key, command):
@ -140,3 +141,7 @@ def get_query():
except:
return {'max': 0, 'online': 0,
'players': []}
def generate_password(size=20):
return "".join([random.choice(string.ascii_letters + string.digits) for idx in range(0, size)])

View File

@ -1,6 +1,7 @@
import discord, logging, re, sys, traceback, asyncio, datetime, os, time
from minecraft_manager.models import Application, Player
from minecraft_manager.api import api
from django.contrib.auth.models import User
from django.conf import settings
from django.db import close_old_connections
from threading import Thread
@ -67,7 +68,8 @@ class Discord(discord.Client):
embed.add_field(name="{}[app ]search <username>".format(self.prefix), value="Search for applications by partial or exact username.")
embed.add_field(name="{}[app ]info <app ID>".format(self.prefix), value="Get detailed information about a specific application.")
embed.add_field(name="{}[app ]accept|deny <app ID>".format(self.prefix), value="Take action on an application.")
embed.add_field(name="{}compare".format(self.prefix), value="Compare Discord users to the Whitelist")
embed.add_field(name="{}demote <username>".format(self.prefix), value="Demote a player to the role given to accepted applications.")
embed.add_field(name="{}compare".format(self.prefix), value="Compare Discord users to the Whitelist.")
yield from self.discord_message(message.channel, embed)
# APP COMMANDS WITH APP ID
match = re.match("[{0}](?:app )?(i|info|a|accept|d|deny) (\d+)$".format(self.prefix), message.content)
@ -103,7 +105,7 @@ class Discord(discord.Client):
match = re.match("[{0}](?:app )?(?:search|info) (\S+)?$".format(self.prefix), message.content)
if match:
search = match.group(1)
yield from self.discord_message(message.channel,"Searching for applications whose username contains '{0}'".format(search))
yield from self.discord_message(message.channel, "Searching for applications whose username contains '{0}'".format(search))
applications = Application.objects.filter(username__icontains=search)[:10]
count = Application.objects.filter(username__icontains=search).count()
if count > 0:
@ -118,6 +120,19 @@ class Discord(discord.Client):
else:
info = "No applications matched that search."
yield from self.discord_message(message.channel, info)
# DEMOTE A PLAYER TO MEMBER
match = re.match("[{0}]demote (\w+)$".format(self.prefix), message.content)
if match:
yield from self.delete_message(message)
username = match.group(1)
api.plugin(api.PLUGIN_DEMOTE, username)
deactivated = ""
if User.objects.filter(username__iexact=username).exists():
user = User.objects.get(username__iexact=username)
user.is_active = False
user.save()
deactivated = " and de-activated"
yield from self.discord_message(message.channel, "{} has been demoted{}.".format(username, deactivated))
# COMPARE DISCORD USERS TO WHITELIST
match = re.match("[{0}]compare".format(self.prefix), message.content)
if match:

25
api/models.py 100644
View File

@ -0,0 +1,25 @@
from django.db import models
from minecraft_manager.api.api import generate_password
class Token(models.Model):
key = models.CharField("Key", default=generate_password, max_length=50, unique=True)
description = models.CharField("Description", max_length=200, blank=True)
active = models.BooleanField("Active", default=True)
web_get_permission = models.BooleanField("Web GET", default=False)
web_post_permission = models.BooleanField("Web POST", default=False)
plugin_get_permission = models.BooleanField("Plugin GET", default=False)
plugin_post_permission = models.BooleanField("Plugin POST", default=False)
form_get_permission = models.BooleanField("Form GET", default=False)
form_post_permission = models.BooleanField("Form POST", default=False)
model_get_permission = models.BooleanField("Model GET", default=False)
model_post_permission = models.BooleanField("Model POST", default=False)
stats_get_permission = models.BooleanField("Stats GET", default=False)
stats_post_permission = models.BooleanField("Stats POST", default=False)
@property
def display(self):
return self.description if self.description else self.key
def __str__(self):
return self.key

View File

@ -1,43 +1,43 @@
from __future__ import absolute_import
import logging, random, string, datetime
import logging, datetime
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash
from django.apps import apps
from django.conf import settings
from django.contrib.auth.models import User
from django.http import JsonResponse, HttpResponse
from django.urls import reverse
from django.utils import timezone
from django.views.generic import View
from django.forms import modelform_factory
import minecraft_manager.forms as MCMForms
import minecraft_manager.forms as mcm_forms
from minecraft_manager.models import Player, UserSettings, Application, IP, Ticket, Warning
import minecraft_manager.api.api as mcm_api
from minecraft_manager.api.models import Token
import minecraft_manager.utils as mcm_utils
import minecraft_manager.external.stats as mcm_stats
logger = logging.getLogger(__name__)
def request_allowed(request):
def request_allowed(request, permission):
is_authenticated = False
if hasattr(request, 'user'):
if hasattr(request.user, 'is_authenticated'):
is_authenticated = request.user.is_authenticated
password = getattr(settings, 'API_PASSWORD', None)
get = request.GET
post= request.POST
post = request.POST
request_password = None
if 'api' in get:
request_password = get['api']
elif 'api' in post:
request_password = post['api']
correct_password = False
if password and request_password:
correct_password = request_password == password
return is_authenticated or correct_password
token_permission = False
if Token.objects.filter(active=True, key=request_password).exists():
token = Token.objects.get(active=True, key=request_password)
token_permission = getattr(token, permission, False)
return is_authenticated or token_permission
def clean(model, data):
@ -51,16 +51,12 @@ def clean(model, data):
return cleaned
def generate_password():
return "".join([random.choice(string.ascii_letters + string.digits) for idx in range(0, 20)])
class WebAPI(View):
def get(self, request, keyword):
get = request.GET
data = {'success': False, 'message': 'API failed'}
if request_allowed(request):
if request_allowed(request, 'web_get_permission'):
keyword = keyword.lower()
if keyword == 'log':
html_global = ""
@ -102,7 +98,7 @@ class WebAPI(View):
def post(self, request, keyword):
post = request.POST
data = {}
if request_allowed(request):
if request_allowed(request, 'web_post_permission'):
keyword = keyword.lower()
if keyword == 'settings' and request.user.usersettings:
for s in [a for a in dir(UserSettings) if not a.startswith('__') and not callable(getattr(UserSettings,a))]:
@ -119,7 +115,7 @@ class WebAPI(View):
else:
return HttpResponse(form.as_p())
elif keyword == 'alert':
form = MCMForms.AlertForm(request.POST)
form = mcm_forms.AlertForm(request.POST)
if form.is_valid():
if mcm_api.create_alert(form.cleaned_data['message']):
data = {'success': True}
@ -155,7 +151,7 @@ class PluginAPI(View):
def get(self, request, keyword):
json = {'status': True, 'message': '', 'extra': ''}
if request_allowed(request):
if request_allowed(request, 'plugin_get_permission'):
get = request.GET
keyword = keyword.lower()
@ -163,7 +159,7 @@ class PluginAPI(View):
def post(self, request, keyword):
json = {'status': True, 'message': '', 'extra': ''}
if request_allowed(request):
if request_allowed(request, 'plugin_post_permission'):
post = request.POST
keyword = keyword.lower()
if "application" == keyword:
@ -272,10 +268,13 @@ class PluginAPI(View):
player.last_seen = timezone.now().strftime("%Y-%m-%d")
player.save()
if new_player and ip.associated:
associated = []
for assoc in ip.associated:
if assoc.uuid is not player.uuid and assoc.is_banned:
mcm_api.plugin("staff", "Server {0}'s IP matches the banned player {1}".format(player.username, assoc.username))
mcm_api.discord_notification("{0}'s IP matches the banned player {1}".format(player.username, assoc.username), ping=True)
associated.append(assoc)
if associated:
mcm_api.plugin("staff", "Server {0}'s IP matches the banned player(s) {1}".format(player.username, ", ".join([assoc.username for assoc in associated])))
mcm_api.discord_notification("{0}'s IP matches the banned player(s) {1}".format(player.username, ", ".join([assoc.username for assoc in associated])), ping=True)
json['status'] = True
json['message'] = "Updated {0}".format(post['username'])
elif "register" == keyword:
@ -284,7 +283,7 @@ class PluginAPI(View):
json['status'] = False
json['message'] = "You are already registered. To change your password, contact an Admin."
else:
password = generate_password()
password = mcm_api.generate_password()
user = User.objects.create_user(username=player.username.lower(), password=password)
user.save()
player.auth_user = user
@ -323,7 +322,7 @@ class FormAPI(View):
def get(self, request, request_model):
html = ""
if request_allowed(request):
if request_allowed(request, 'form_get_permission'):
get = request.GET
model = None
for m in apps.get_app_config('minecraft_manager').get_models():
@ -332,7 +331,7 @@ class FormAPI(View):
break
if model:
form = None
for modelform in MCMForms.__all__():
for modelform in mcm_forms.__all__():
if modelform.Meta.model == model:
form = modelform()
break
@ -346,7 +345,7 @@ class FormAPI(View):
def post(self, request, request_model):
html = ""
if request_allowed(request):
if request_allowed(request, 'form_post_permission'):
post = request.POST
model = None
for m in apps.get_app_config('minecraft_manager').get_models():
@ -355,7 +354,7 @@ class FormAPI(View):
break
if model:
form = None
for modelform in MCMForms.__all__():
for modelform in mcm_forms.__all__():
if modelform.Meta.model == model:
form = modelform(post)
break
@ -376,7 +375,7 @@ class ModelAPI(View):
def get(self, request, request_model):
json = []
if request_allowed(request):
if request_allowed(request, 'model_get_permission'):
get = request.GET
model = None
for m in apps.get_app_config('minecraft_manager').get_models():
@ -404,7 +403,7 @@ class StatsAPI(View):
def get(self, request):
json = []
if request_allowed(request):
if request_allowed(request, 'stats_get_permission'):
get = request.GET
if 'stat' in get:
if 'uuid' in get:

View File

@ -47,8 +47,6 @@ Optional
``SERVER_QUERY_IP`` - The full IP (and port) used to query your server. (This is used to get a player list)
``API_PASSWORD`` - The password used to validate API requests from unauthenticated sources.
``COREPROTECT_WEB_URL`` - The URL to your CoreProtect Web UI, if it exists.
``COREPROTECT_ACTIVITY_URL`` - The URL to your CoreProtect Activity Web UI, if it exists.

View File

@ -47,6 +47,7 @@ Add MCM urls to your ``urls.py``
Django doesn't provide login/logout templates by default, so MCM has some generic ones if needed.
::
from django.contrib.auth import views as auth_views
path('accounts/login/', auth_views.LoginView.as_view(template_name='minecraft_manager/login.html'), name='login'),
path('accounts/logout/', auth_views.LogoutView.as_view(template_name='minecraft_manager/logged_out.html'), name='logout'),

12
external/stats.py vendored
View File

@ -1,4 +1,4 @@
import os, json
import os, json, copy
from minecraft_manager.models import Player
from django.conf import settings
@ -16,9 +16,13 @@ def get_stats():
with open(stats_dir + "/" + filename) as json_file:
raw = json.load(json_file)['stats']
clean = {}
for r in raw:
if not any(sf.lower() in r.lower() for sf in stats_filter):
clean[r] = raw[r]
raw_copy = copy.deepcopy(raw)
for ra in raw_copy:
if not any(sf.lower() in ra.lower() for sf in stats_filter):
for r in raw_copy[ra]:
if any(sf.lower() in r.lower() for sf in stats_filter):
del raw[ra][r]
clean[ra] = raw[ra]
uuid = filename.replace(".json", "")
stats[uuid] = clean
return stats

75
overview.py 100644
View File

@ -0,0 +1,75 @@
import os
import json
from datetime import datetime, timedelta
from django.conf import settings
from minecraft_manager.models import Application, Player, Ticket, Warning, IP
from django.contrib.auth.models import User
def overview_data():
data = {}
# Setup
with open(os.path.join(settings.MINECRAFT_BASE_DIR, 'banned-players.json'), encoding='utf-8') as f:
bans = json.load(f)
# Totals
data['total'] = {
'application': {
'accepted': Application.objects.filter(accepted=True).count(),
'denied': Application.objects.filter(accepted=False).count(),
'all': Application.objects.count()
},
'player': {
'banned': len(bans),
'unbanned': Player.objects.count() - len(bans),
'all': Player.objects.count()
},
'ticket': {
'claimed': Ticket.objects.filter(staff__isnull=False).count(),
'unclaimed': Ticket.objects.filter(staff__isnull=True).count(),
'resolved': Ticket.objects.filter(resolved=True).count(),
'unresolved': Ticket.objects.filter(resolved=False).count(),
'all': Ticket.objects.count()
},
'warning': Warning.objects.count(),
'ip': IP.objects.count()
}
# Averages
data['average'] = {
'age': round(sum([application.age for application in Application.objects.filter(accepted=True)]) /
data['total']['application']['accepted'] if data['total']['application']['accepted'] != 0 else 1, 2)
}
# Percentage
data['percentage'] = {
'accepted': round((data['total']['application']['accepted'] /
data['total']['application']['all'] if data['total']['application']['all'] != 0 else 1) * 100, 2),
'banned': round((data['total']['player']['banned'] /
data['total']['player']['all'] if data['total']['player']['all'] != 0 else 1) * 100, 2),
'applied': round((data['total']['application']['all'] / data['total']['player']['all']) * 100, 2)
}
# Unique logins
now = datetime.now()
day = now - timedelta(days=1)
week = now - timedelta(weeks=1)
month = now - timedelta(weeks=4)
data['unique'] = {
'day': Player.objects.filter(last_seen__range=[day, now]).count(),
'week': Player.objects.filter(last_seen__range=[week, now]).count(),
'month': Player.objects.filter(last_seen__range=[month, now]).count()
}
# Admin
data['resolved'] = []
for user in User.objects.all().order_by('username'):
data['resolved'].append({
'active': user.is_active,
'username': user.username,
'tickets': Ticket.objects.filter(staff=user).count()
})
return data

View File

@ -6,141 +6,72 @@
{% endblock %}
{% block section %}
<div id="content">
{% if request.user.is_staff %}
<div class="panel panel-danger">
<div class="panel-body">
<h4><span class="label label-danger">Admin Area</span></h4>
<h3>Resolved Tickets</h3>
<div class="row">
{% for staff in data.resolved %}
<div class="col-xs-6 col-md-4">
<p><span class="label label-{% if staff.active %}success{% else %}danger{% endif %}">{% if staff.active %}Active{% else %}Inactive{% endif %}</span> {{ staff.username }}: {{ staff.tickets }}</p>
</div>
{% endfor %}
</div>
</div>
</div>
<hr/>
{% endif %}
<div class="row">
<div class="col-xs-9 col-md-6">
<h3 class="center">Applications{% if form.apps.unanswered > 0 %} <a href="{% url "application" %}?accepted=">({{ form.apps.unanswered }} Unanswered)</a>{% endif %}</h3>
<canvas id="appChart" style="width:30em;height:15em" ></canvas>
<h3>Applications: {{ data.total.application.all }}</h3>
<p>Accepted: {{ data.total.application.accepted }}</p>
<p>Denied: {{ data.total.application.denied }}</p>
<br/>
<h3>Tickets: {{ data.total.ticket.all }}</h3>
<p>Claimed: {{ data.total.ticket.claimed }}</p>
<p>Unclaimed: {{ data.total.ticket.unclaimed }}</p>
<p>Resolved: {{ data.total.ticket.resolved }}</p>
<p>Unresolved: {{ data.total.ticket.unresolved }}</p>
</div>
<div class="col-xs-9 col-md-6">
<h3 class="center">Tickets{% if form.tickets.unclaimed > 0 %} <a href="{% url "ticket" %}?claimed=false">({{ form.tickets.unclaimed }} Unclaimed)</a>{% endif %}</h3>
<h4></h4>
<canvas id="ticketChart" style="width:30em;height:15em" ></canvas>
<h3>Players: {{ data.total.player.all }}</h3>
<p>Not Banned: {{ data.total.player.unbanned }}</p>
<p>Banned: {{ data.total.player.banned }}</p>
<br/>
<h3>Warnings: {{ data.total.warning }}</h3>
<br/>
<h3>IPs: {{ data.total.ip }}</h3>
</div>
</div>
<hr/>
<div class="row">
<div class="col-xs-18 col-md-12">
<h3 class="center">Totals</h3>
<canvas id="totalChart" style="width:30em;height:15em" ></canvas>
<div class="col-xs-9 col-md-6">
<h3>Average Age: {{ data.average.age }}</h3>
</div>
</div>
</div>
<script>
$(document).ready(function() {
var app_data = {
labels: [
"Unanswered",
"Denied",
"Accepted"
],
datasets: [
{
data: [{{ form.apps.unanswered }}, {{ form.apps.denied }}, {{ form.apps.accepted }}],
backgroundColor: [
"#8080ff",
"#ff3333",
"#33cc33"
],
hoverBackgroundColor: [
"#8080ff",
"#ff3333",
"#33cc33"
]
}
]
};
var app_options = {
animation: {
animateRotate: true
}
}
var app_ctx = $("#appChart");
var app_chart = new Chart(app_ctx,
{
type: 'pie',
data: app_data,
options: app_options
}
);
<div class="col-xs-9 col-md-6">
var ticket_data = {
labels: [
"Unclaimed",
"Claimed",
"Resolved"
],
datasets: [
{
data: [{{ form.tickets.unclaimed }}, {{ form.tickets.claimed }}, {{ form.tickets.resolved }}],
backgroundColor: [
"#8080ff",
"#ff3333",
"#33cc33"
],
hoverBackgroundColor: [
"#8080ff",
"#ff3333",
"#33cc33"
]
}
]
};
var ticket_options = {
animation: {
animateRotate: true
}
}
var ticket_ctx = $("#ticketChart");
var ticket_chart = new Chart(ticket_ctx,
{
type: 'pie',
data: ticket_data,
options: ticket_options
}
);
var total_data = {
labels: [
"Applications",
"Players",
"IPs",
"Tickets",
"Warnings"
],
datasets: [
{
label: "Totals",
data: [
{{ form.counts.applications }},
{{ form.counts.players }},
{{ form.counts.ips }},
{{ form.counts.tickets }},
{{ form.counts.warnings }}
],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)'
],
borderColor: [
'rgba(255,99,132,1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)'
],
borderWidth: 1
}
]
}
var total_ctx = $("#totalChart");
var total_chart = new Chart(total_ctx,
{
type: 'bar',
data: total_data
}
);
});
</script>
</div>
</div>
<hr/>
<div class="row">
<div class="col-xs-9 col-md-6">
<h3>Acceptance Rate: {{ data.percentage.accepted }}%</h3>
<h3>Application/Player Rate: {{ data.percentage.applied }}%</h3>
</div>
<div class="col-xs-9 col-md-6">
<h3>Ban Rate: {{ data.percentage.banned }}%</h3>
</div>
</div>
<hr/>
<div class="row">
<div class="col-xs-9 col-md-6">
<h3>Logins Today: {{ data.unique.day }}</h3>
<h3>Logins Last Month: {{ data.unique.month }}</h3>
</div>
<div class="col-xs-9 col-md-6">
<h3>Logins Last Week: {{ data.unique.week }}</h3>
</div>
</div>
</div>
{% endblock section %}

View File

@ -17,7 +17,7 @@ def get_sidebar(current_app, request):
ret = '<li {}><a href="{}"><span class="glyphicon glyphicon-home"></span>&nbsp;&nbsp;Overview</a></li>'.format('class="active"' if current_app == 'overview' else "", reverse('overview'))
ret += '<li {}><a href="{}"><span class="glyphicon glyphicon-ban-circle"></span>&nbsp;&nbsp;Bans</a></li>'.format('class="active"' if current_app == 'ban' else '', reverse('ban'))
ret += '<li {}><a href="{}"><span class="glyphicon glyphicon-inbox"></span>&nbsp;&nbsp;Alerts{}</a></li>'.format('class="active"' if current_app == 'alert' else '', reverse('alert'), unseen_html)
ret += '<li {}><a href="{}"><span class="glyphicon glyphicon-bell"></span>&nbsp;&nbsp;Alerts{}</a></li>'.format('class="active"' if current_app == 'alert' else '', reverse('alert'), unseen_html)
# Models
ret += '<li {}><a href="{}"><span class="glyphicon glyphicon-file"></span>&nbsp;&nbsp;Applications</a></li>'.format('class="active"' if current_app == 'application' else '', reverse('application'))

View File

@ -14,6 +14,7 @@ from django.views.generic import View
from django.contrib.auth.models import User
from minecraft_manager.models import Application as AppModel, Player as PlayerModel, Ticket as TicketModel, Warning as WarningModel, IP as IPModel, Alert as AlertModel, Note as NoteModel, UserSettings as UserSettingsModel
from minecraft_manager.forms import WarningForm, NoteForm
from minecraft_manager.overview import overview_data
import minecraft_manager.api.api as API
import subprocess
@ -33,19 +34,9 @@ class Overview(View):
request.user.usersettings = UserSettingsModel(auth_user=request.user)
request.user.usersettings.last_ip = user_ip
request.user.usersettings.save()
unanswered_apps = AppModel.objects.filter(accepted=None).count()
accepted_apps = AppModel.objects.filter(accepted=True).count()
denied_apps = AppModel.objects.filter(accepted=False).count()
unclaimed_tickets = TicketModel.objects.filter(staff=None, resolved=False).count()
claimed_tickets = TicketModel.objects.filter(staff__isnull=False, resolved=False).count()
resolved_tickets = TicketModel.objects.filter(resolved=True).count()
counts = {'applications': AppModel.objects.count(), 'players': PlayerModel.objects.count(),
"ips": IPModel.objects.count(), "tickets": TicketModel.objects.count(),
"warnings": WarningModel.objects.count()}
form = {'apps': {'unanswered': unanswered_apps, 'accepted': accepted_apps, 'denied': denied_apps},
'tickets': {'unclaimed': unclaimed_tickets, 'claimed': claimed_tickets, 'resolved': resolved_tickets},
'counts': counts}
return render(request, 'minecraft_manager/overview.html', {'current_app': 'overview', 'form': form})
return render(request, 'minecraft_manager/overview.html', {'current_app': 'overview', 'data': overview_data()})
class CoreProtect(View):